move to package

main
Ethan Niser 2023-09-17 01:27:07 -05:00
parent bbf3c6f57d
commit a81e5427a7
3 changed files with 4 additions and 781 deletions

@ -1,224 +0,0 @@
import { cache } from "beth-jsx";
import { Database } from "bun:sqlite";
class BethPersistCache {
private callBackMap: Map<
string,
{
callBack: () => Promise<any>;
tags: string[];
cache: "memory" | "json";
}
>;
private pendingMap: Map<string, Promise<any>>;
private inMemoryDataCache: Map<string, any>;
private jsonDataCache: Database;
private intervals: Set<NodeJS.Timeout>;
private keys: Set<string>;
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<any>;
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<T extends () => Promise<any>>(
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);
}

@ -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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
// This should result in no 'cache hit' log, because the render cache is never reset from the previous render
const html2 = await (
<>
<Component />
<Component />
</>
);
expect(html2).toBe(`<p>number: 1</p><p>number: 1</p>`);
// even in a new render we get the same results
const Test = () => <Component />;
const html3 = await renderToString(() => (
<>
<Test />
<Component />
</>
));
expect(html3).toBe(`<p>number: 1</p><p>number: 1</p>`);
});
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
// even in a new render we get the same results
const Test = () => <Component />;
const html3 = await renderToString(() => (
<>
<Test />
<Component />
</>
));
expect(html3).toBe(`<p>number: 1</p><p>number: 1</p>`);
});
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
count++;
// should the be same right away
const html2 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html2).toBe(`<p>number: 1</p><p>number: 1</p>`);
// and the same until a second has passed
await new Promise((resolve) =>
setTimeout(async () => {
const html3 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 1</p><p>number: 1</p>`);
resolve(void 0);
}, 500)
);
// but after a second it should be different
await new Promise((resolve) =>
setTimeout(async () => {
const html3 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
count++;
// should the be same right away
const html2 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html2).toBe(`<p>number: 1</p><p>number: 1</p>`);
// and the same until a second has passed
await new Promise((resolve) =>
setTimeout(async () => {
const html3 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 1</p><p>number: 1</p>`);
resolve(void 0);
}, 500)
);
// but after a second it should be different
await new Promise((resolve) =>
setTimeout(async () => {
const html3 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
count++;
// should the be same right away
const html2 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html2).toBe(`<p>number: 1</p><p>number: 1</p>`);
revalidateTag("tag1");
// now should be different
const html3 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
});
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
count++;
// should the be same right away
const html2 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html2).toBe(`<p>number: 1</p><p>number: 1</p>`);
revalidateTag("tag1");
// now should be different
const html3 = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
});
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
// 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(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 2</p><p>number: 2</p>`);
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
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(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
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(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
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 <p>number: {data}</p>;
};
const html = await renderToString(() => (
<>
<Component />
<Component />
</>
));
expect(html).toBe(`<p>number: 1</p><p>number: 1</p>`);
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(() => (
<>
<Component />
<Component />
</>
));
expect(html3).toBe(`<p>number: 3</p><p>number: 3</p>`);
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],
},
});
});

@ -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(() => <p safe>{time}</p>);
return renderToString(() => <p>{time}</p>);
})
.get("/", async ({ set }) => {
set.headers["content-type"] = "text/html";
return renderToString(() => (
<BaseHtml>
<h1>cache revalidates on two second interval</h1>
<button hx-get="/test" hx-target="#foo" hx-swap="beforeend">
click me to get time (cached)
click me to get time since start (cached)
</button>
<br />
<br />
@ -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)
</button>
<div id="foo"></div>
</BaseHtml>