main
Ethan Niser 2023-09-16 23:45:16 -05:00
parent a42dc504fb
commit ca4b2509c2
7 changed files with 371 additions and 43 deletions

4
.gitignore vendored

@ -171,4 +171,6 @@ dist
tsconfig.tsbuildinfo
local.sqlite
local.sqlite-shm
local.sqlite-wal
local.sqlite-wal
.beth/

Binary file not shown.

@ -26,7 +26,6 @@
"@elysiajs/cron": "^0.6.0",
"@elysiajs/static": "^0.6.0",
"@elysiajs/swagger": "^0.6.2",
"@kitajs/html": "^2.1.2",
"@libsql/client": "0.3.5-pre.4",
"@lucia-auth/adapter-sqlite": "^2.0.0",
"@t3-oss/env-core": "^0.6.1",

@ -14,28 +14,29 @@ class BethPersistCache {
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.jsonDataCache = new Database("./.beth/beth-cache.sqlite");
this.intervals = new Set();
this.pendingMap = new Map();
this.keys = new Set();
this.jsonDataCache.exec(`
this.jsonDataCache.run(`
DROP TABLE IF EXISTS cache;
`);
this.jsonDataCache.exec(`
this.jsonDataCache.run(`
CREATE TABLE cache (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
value TEXT NOT NULL
);
`);
}
private setJsonCache(key: string, value: any) {
console.log("Seeding JSON Cache:", key, value);
this.jsonDataCache.run(
`
INSERT INTO cache (key, value)
@ -46,18 +47,17 @@ class BethPersistCache {
);
}
private async getJsonCache(key: string) {
private getJsonCache(key: string) {
console.log("JSON Cache HIT:", key);
const result = (await this.jsonDataCache
const result = this.jsonDataCache
.query("SELECT value FROM cache WHERE key = ?")
.get(key)) as { value: string } | undefined;
.get(key) as { value: string } | undefined;
if (!result) throw new Error("JSON Cache Miss");
return JSON.parse(result.value);
}
public seed({
key,
value,
tags,
revalidate,
callBack,
@ -66,31 +66,51 @@ class BethPersistCache {
cache: "memory" | "json";
callBack: () => Promise<any>;
key: string;
value: any;
tags: string[];
revalidate: number;
}) {
console.log(`Seeding ${cache} Cache:`, key, value);
if (cache === "memory") {
this.inMemoryDataCache.set(key, value);
} else if (cache === "json") {
this.setJsonCache(key, value);
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);
}
this.callBackMap.set(key, {
callBack,
tags,
cache,
const promise = callBack();
this.pendingMap.set(key, promise);
promise.then((value) => {
this.pendingMap.delete(key);
console.log(`Seeding ${cache} Cache:`, key, value);
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);
}
});
if (revalidate > 0) {
console.log("Setting Revalidate Interval:", key, revalidate);
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) return;
if (!result) {
throw new Error("No callback found for key: " + key);
}
const { callBack, tags, cache } = result;
const callBackPromise = callBack();
this.pendingMap.set(key, callBackPromise);
@ -111,15 +131,19 @@ class BethPersistCache {
}
private setInterval(key: string, revalidate: number) {
if (revalidate === Infinity) return;
const interval = setInterval(async () => {
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);
}, revalidate * 1000);
console.log("Setting Revalidate Interval:", key, revalidate);
this.intervals.add(interval);
}
private async getMemoryCache(key: string) {
private getMemoryCache(key: string) {
const cacheResult = this.inMemoryDataCache.get(key);
if (cacheResult) {
console.log("Memory Cache HIT:", key);
@ -142,7 +166,7 @@ class BethPersistCache {
try {
const pending = this.pendingMap.get(key);
if (pending) {
console.log("STALE HIT, returning pending promise:", key);
console.log("PENDING CACHE HIT:", key);
return pending;
}
@ -185,15 +209,12 @@ export function persistedCache<T extends () => Promise<any>>(
const tags = options?.tags ?? [];
console.log("Cache MISS: ", key);
callBack().then((result) => {
GLOBAL_CACHE.seed({
callBack,
key,
value: result,
tags,
revalidate,
cache: persist,
});
GLOBAL_CACHE.seed({
callBack,
key,
tags,
revalidate,
cache: persist,
});
return cache(() => GLOBAL_CACHE.getCachedValue(key, persist)) as T;
}

@ -0,0 +1,306 @@
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>`);
// 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>`);
});

@ -1,4 +1,4 @@
import { type PropsWithChildren } from "@kitajs/html";
import { type PropsWithChildren } from "beth-jsx";
import { config } from "../config";
export const BaseHtml = ({ children }: PropsWithChildren) => (

@ -3,8 +3,8 @@ import { Elysia } from "elysia";
// import pretty from "pino-pretty";
import { config } from "../config";
import { client, db } from "../db";
import "@kitajs/html/register";
import "@kitajs/html/htmx";
import "beth-jsx/register";
import "beth-jsx/htmx";
import { auth } from "../auth";
// import { cron } from "@elysiajs/cron";