diff --git a/src/beth/cache.ts b/src/beth/cache.ts deleted file mode 100644 index 4478519..0000000 --- a/src/beth/cache.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { cache } from "beth-jsx"; -import { Database } from "bun:sqlite"; - -class BethPersistCache { - private callBackMap: Map< - string, - { - callBack: () => Promise; - tags: string[]; - cache: "memory" | "json"; - } - >; - private pendingMap: Map>; - private inMemoryDataCache: Map; - private jsonDataCache: Database; - private intervals: Set; - private keys: Set; - - constructor() { - this.callBackMap = new Map(); - this.inMemoryDataCache = new Map(); - this.jsonDataCache = new Database("beth-cache.sqlite"); - this.intervals = new Set(); - this.pendingMap = new Map(); - this.keys = new Set(); - - this.jsonDataCache.run(` - DROP TABLE IF EXISTS cache; - `); - - this.jsonDataCache.run(` - CREATE TABLE cache ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - `); - } - - private setJsonCache(key: string, value: any) { - this.jsonDataCache.run( - ` - INSERT INTO cache (key, value) - VALUES (?, ?) - ON CONFLICT (key) DO UPDATE SET value = excluded.value; - `, - [key, JSON.stringify(value)] - ); - } - - private getJsonCache(key: string) { - console.log("JSON Cache HIT:", key); - const result = this.jsonDataCache - .query("SELECT value FROM cache WHERE key = ?") - .get(key) as { value: string } | undefined; - if (!result) throw new Error("JSON Cache Miss"); - return JSON.parse(result.value); - } - - public seed({ - key, - tags, - revalidate, - callBack, - cache, - }: { - cache: "memory" | "json"; - callBack: () => Promise; - key: string; - tags: string[]; - revalidate: number; - }) { - if (this.keys.has(key)) { - throw new Error( - `Persistant Cache Key already exists: ${key} - these much be unqiue across your entire app` - ); - } else { - this.keys.add(key); - } - - const promise = callBack(); - this.pendingMap.set(key, promise); - - promise.then((value) => { - this.pendingMap.delete(key); - console.log(`Seeding ${cache} Cache:`, key); - if (cache === "memory") { - this.inMemoryDataCache.set(key, value); - } else if (cache === "json") { - this.setJsonCache(key, value); - } - this.callBackMap.set(key, { - callBack, - tags, - cache, - }); - if (revalidate > 0) { - this.setInterval(key, revalidate); - } - }); - } - - private rerunCallBack(key: string) { - const pending = this.pendingMap.get(key); - if (pending) { - console.log("PENDING CACHE HIT:", key); - return pending; - } - - console.log("rerunning callback:", key); - const result = this.callBackMap.get(key); - if (!result) { - throw new Error("No callback found for key: " + key); - } - const { callBack, tags, cache } = result; - const callBackPromise = callBack(); - this.pendingMap.set(key, callBackPromise); - callBackPromise.then((value) => { - if (cache === "memory") { - this.inMemoryDataCache.set(key, value); - } else if (cache === "json") { - this.setJsonCache(key, value); - } - this.callBackMap.set(key, { - callBack, - tags, - cache, - }); - this.pendingMap.delete(key); - }); - return callBackPromise; - } - - private setInterval(key: string, revalidate: number) { - if (revalidate === Infinity) { - console.log("No revalidate interval for:", key); - return; - } - const interval = setInterval(() => { - console.log(`Cache Revalidating (on ${revalidate}s interval):`, key); - this.rerunCallBack(key); - }, revalidate * 1000); - console.log("Setting Revalidate Interval:", key, revalidate); - this.intervals.add(interval); - } - - private getMemoryCache(key: string) { - const cacheResult = this.inMemoryDataCache.get(key); - if (cacheResult) { - console.log("Memory Cache HIT:", key); - return cacheResult; - } else { - throw new Error("Memory Cache Miss"); - } - } - - public revalidateTag(tag: string) { - console.log("Revalidating tag:", tag); - this.callBackMap.forEach((value, key) => { - if (value.tags.includes(tag)) { - this.rerunCallBack(key); - } - }); - } - - public getCachedValue(key: string, cache: "memory" | "json") { - try { - const pending = this.pendingMap.get(key); - if (pending) { - console.log("PENDING CACHE HIT:", key); - return pending; - } - - if (cache === "memory") { - return this.getMemoryCache(key); - } else if (cache === "json") { - return this.getJsonCache(key); - } - } catch (e) { - if (e instanceof Error) { - if (e.message === "Memory Cache Miss") { - return this.rerunCallBack(key); - } else if (e.message === "JSON Cache Miss") { - return this.rerunCallBack(key); - } else { - throw e; - } - } else { - throw e; - } - } - } -} - -const GLOBAL_CACHE = new BethPersistCache(); - -type CacheOptions = { - persist?: "memory" | "json"; - revalidate?: number; - tags?: string[]; -}; - -export function persistedCache Promise>( - callBack: T, - key: string, - options?: CacheOptions -): T { - const persist = options?.persist ?? "json"; - const revalidate = options?.revalidate ?? Infinity; - const tags = options?.tags ?? []; - - console.log("Cache MISS: ", key); - GLOBAL_CACHE.seed({ - callBack, - key, - tags, - revalidate, - cache: persist, - }); - return cache(() => GLOBAL_CACHE.getCachedValue(key, persist)) as T; -} - -export function revalidateTag(tag: string) { - GLOBAL_CACHE.revalidateTag(tag); -} diff --git a/src/beth/test/cache.test.tsx b/src/beth/test/cache.test.tsx deleted file mode 100644 index c838ef9..0000000 --- a/src/beth/test/cache.test.tsx +++ /dev/null @@ -1,552 +0,0 @@ -import { test, expect } from "bun:test"; -import { persistedCache, revalidateTag } from "../cache"; -import "beth-jsx/register"; -import { renderToString } from "beth-jsx"; - -test("static json cache", async () => { - let count = 0; - const getCount = async () => ++count; - const cachedGetCount = persistedCache(getCount, "getCount1"); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - // This should result in no 'cache hit' log, because the render cache is never reset from the previous render - const html2 = await ( - <> - - - - ); - - expect(html2).toBe(`

number: 1

number: 1

`); - - // even in a new render we get the same results - const Test = () => ; - - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 1

number: 1

`); -}); - -test("static memory cache", async () => { - let count = 0; - const getCount = async () => ++count; - const cachedGetCount = persistedCache(getCount, "getCount2", { - persist: "memory", - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - // even in a new render we get the same results - const Test = () => ; - - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 1

number: 1

`); -}); - -test("json cache revalidate interval", async () => { - let count = 0; - const getCount = async () => ++count; - const cachedGetCount = persistedCache(getCount, "getCount3", { - revalidate: 1, - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - count++; - - // should the be same right away - - const html2 = await renderToString(() => ( - <> - - - - )); - - expect(html2).toBe(`

number: 1

number: 1

`); - - // and the same until a second has passed - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 1

number: 1

`); - - resolve(void 0); - }, 500) - ); - - // but after a second it should be different - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); - - resolve(void 0); - }, 1100) - ); -}); - -test("memory cache revalidate interval", async () => { - let count = 0; - const getCount = async () => ++count; - const cachedGetCount = persistedCache(getCount, "getCount4", { - persist: "memory", - revalidate: 1, - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - count++; - - // should the be same right away - - const html2 = await renderToString(() => ( - <> - - - - )); - - expect(html2).toBe(`

number: 1

number: 1

`); - - // and the same until a second has passed - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 1

number: 1

`); - - resolve(void 0); - }, 500) - ); - - // but after a second it should be different - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); - - resolve(void 0); - }, 1100) - ); -}); - -test("json cache revalidate tag", async () => { - let count = 0; - const getCount = async () => ++count; - const cachedGetCount = persistedCache(getCount, "getCount5", { - tags: ["tag1"], - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - count++; - - // should the be same right away - - const html2 = await renderToString(() => ( - <> - - - - )); - - expect(html2).toBe(`

number: 1

number: 1

`); - - revalidateTag("tag1"); - - // now should be different - - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); -}); - -test("memory cache revalidate tag", async () => { - let count = 0; - const getCount = async () => ++count; - const cachedGetCount = persistedCache(getCount, "getCount6", { - tags: ["tag1"], - persist: "memory", - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - count++; - - // should the be same right away - - const html2 = await renderToString(() => ( - <> - - - - )); - - expect(html2).toBe(`

number: 1

number: 1

`); - - revalidateTag("tag1"); - - // now should be different - - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); -}); - -test("request during interval revalidation", async () => { - let count = 0; - const getCount = async () => - new Promise((resolve) => setTimeout(() => resolve(++count), 100)); - const cachedGetCount = persistedCache(getCount, "getCount7", { - revalidate: 1, - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - // cache request goes off during revalidation - // should result in 'pending cache hit' log + updated data - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 2

number: 2

`); - - resolve(void 0); - }, 1010) - ); -}); - -test("request during tag revalidation", async () => { - let count = 0; - const getCount = async () => - new Promise((resolve) => setTimeout(() => resolve(++count), 100)); - const cachedGetCount = persistedCache(getCount, "getCount8", { - tags: ["tag1"], - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - setTimeout(() => { - count++; - revalidateTag("tag1"); - }, 1000); - - // cache request goes off during revalidation - // should result in 'pending cache hit' log + updated data - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); - - resolve(void 0); - }, 1010) - ); -}); - -test("interval during tag revalidation", async () => { - let count = 0; - const getCount = async () => - new Promise((resolve) => setTimeout(() => resolve(++count), 300)); - const cachedGetCount = persistedCache(getCount, "getCount9", { - tags: ["tag1"], - revalidate: 1, - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - setTimeout(() => { - count++; - revalidateTag("tag1"); - }, 900); - - // should see pending cache hit for interval revalidation - - // cache request goes off during revalidation - // should result in 2nd 'pending cache hit' log + updated data - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); - - resolve(void 0); - }, 1100) - ); -}); - -test("interval during tag revalidation", async () => { - let count = 0; - const getCount = async () => - new Promise((resolve) => setTimeout(() => resolve(++count), 300)); - const cachedGetCount = persistedCache(getCount, "getCount10", { - tags: ["tag1"], - revalidate: 1, - }); - - const Component = async () => { - const data = await cachedGetCount(); - return

number: {data}

; - }; - - const html = await renderToString(() => ( - <> - - - - )); - - expect(html).toBe(`

number: 1

number: 1

`); - - setTimeout(() => { - count++; - revalidateTag("tag1"); - }, 1100); - - // should see pending cache hit for tag revalidation - - // cache request goes off during revalidation - // should result in 2nd 'pending cache hit' log + updated data - - await new Promise((resolve) => - setTimeout(async () => { - const html3 = await renderToString(() => ( - <> - - - - )); - - expect(html3).toBe(`

number: 3

number: 3

`); - - resolve(void 0); - }, 1150) - ); -}); - -test("complex object storage to memory", async () => { - const getData = async () => ({ - a: 1, - b: 2, - c: { - d: 3, - e: [4, 5, 6], - }, - }); - - const cachedGetData = persistedCache(getData, "getData1", { - persist: "memory", - }); - - const data = await cachedGetData(); - - expect(data).toStrictEqual({ - a: 1, - b: 2, - c: { - d: 3, - e: [4, 5, 6], - }, - }); -}); -test("complex object storage to json", async () => { - const getData = async () => ({ - a: 1, - b: 2, - c: { - d: 3, - e: [4, 5, 6], - }, - }); - - const cachedGetData = persistedCache(getData, "getData2", { - persist: "json", - }); - - const data = await cachedGetData(); - - expect(data).toStrictEqual({ - a: 1, - b: 2, - c: { - d: 3, - e: [4, 5, 6], - }, - }); -}); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f619e7b..595f0d9 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,7 +2,7 @@ import Elysia from "elysia"; import { BaseHtml } from "../components/base"; import { ctx } from "../context"; import { renderToString } from "beth-jsx"; -import { persistedCache, revalidateTag } from "../beth/cache"; +import { persistedCache, revalidateTag } from "beth-jsx"; const start = Date.now(); @@ -26,15 +26,14 @@ export const index = new Elysia() }) .get("/test", async () => { const time = await cachedGetTime(); - return renderToString(() =>

{time}

); + return renderToString(() =>

{time}

); }) .get("/", async ({ set }) => { - set.headers["content-type"] = "text/html"; return renderToString(() => (

cache revalidates on two second interval



@@ -44,7 +43,7 @@ export const index = new Elysia() hx-swap="beforeend" hx-revalidate="time" > - click me (revalidate now) + click me to get time since start (revalidate now)