@terrafirmagreg-team/data 1.2.7 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/lib/persistent-cache.ts +77 -15
package/package.json
CHANGED
|
@@ -8,6 +8,11 @@ type Entry = { value: unknown; expires: number };
|
|
|
8
8
|
|
|
9
9
|
let tableReady: Promise<unknown> | undefined;
|
|
10
10
|
|
|
11
|
+
const ready = () =>
|
|
12
|
+
(tableReady ??= dbSql`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS tfg_cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires INTEGER NOT NULL)
|
|
14
|
+
`);
|
|
15
|
+
|
|
11
16
|
function fileFor(key: string): string {
|
|
12
17
|
const name = key.replace(/[^a-z0-9._-]+/gi, '_').slice(0, 60);
|
|
13
18
|
const hash = createHash('sha1').update(key).digest('hex').slice(0, 8);
|
|
@@ -16,10 +21,7 @@ function fileFor(key: string): string {
|
|
|
16
21
|
|
|
17
22
|
async function d1Read(key: string): Promise<Entry | undefined> {
|
|
18
23
|
try {
|
|
19
|
-
|
|
20
|
-
CREATE TABLE IF NOT EXISTS tfg_cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires INTEGER NOT NULL)
|
|
21
|
-
`;
|
|
22
|
-
await tableReady;
|
|
24
|
+
await ready();
|
|
23
25
|
const rows = (await dbSql`
|
|
24
26
|
SELECT value, expires FROM tfg_cache WHERE key = ${key}
|
|
25
27
|
`) as { rows?: { value: string; expires: number }[] } | undefined;
|
|
@@ -34,7 +36,7 @@ async function d1Read(key: string): Promise<Entry | undefined> {
|
|
|
34
36
|
|
|
35
37
|
async function d1Write(key: string, entry: Entry): Promise<void> {
|
|
36
38
|
try {
|
|
37
|
-
await
|
|
39
|
+
await ready();
|
|
38
40
|
await dbSql`
|
|
39
41
|
INSERT OR REPLACE INTO tfg_cache (key, value, expires)
|
|
40
42
|
VALUES (${key}, ${JSON.stringify(entry.value)}, ${entry.expires})
|
|
@@ -42,21 +44,81 @@ async function d1Write(key: string, entry: Entry): Promise<void> {
|
|
|
42
44
|
} catch {}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
type Db = NonNullable<Awaited<ReturnType<typeof dataDb>>>;
|
|
48
|
+
|
|
49
|
+
async function readEntry(
|
|
50
|
+
db: Db | undefined,
|
|
51
|
+
key: string,
|
|
52
|
+
): Promise<Entry | undefined> {
|
|
53
|
+
return db
|
|
54
|
+
? await d1Read(key)
|
|
55
|
+
: loadJsonFile<Entry | undefined>(fileFor(key), () => undefined);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeEntry(
|
|
59
|
+
db: Db | undefined,
|
|
60
|
+
key: string,
|
|
61
|
+
entry: Entry,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
if (db) await d1Write(key, entry);
|
|
64
|
+
else {
|
|
65
|
+
try {
|
|
66
|
+
saveJsonFile(fileFor(key), entry);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function persistentGet<T>(
|
|
72
|
+
key: string,
|
|
73
|
+
): Promise<{ value: T; expires: number } | undefined> {
|
|
74
|
+
const db = await dataDb().catch(() => undefined);
|
|
75
|
+
return (await readEntry(db, key)) as
|
|
76
|
+
| { value: T; expires: number }
|
|
77
|
+
| undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function persistentSet(
|
|
81
|
+
key: string,
|
|
82
|
+
value: unknown,
|
|
83
|
+
ttlMs: number,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const db = await dataDb().catch(() => undefined);
|
|
86
|
+
await writeEntry(db, key, { value, expires: Date.now() + ttlMs });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const inflight = new Map<string, Promise<unknown>>();
|
|
90
|
+
|
|
91
|
+
async function loadAndStore<T>(
|
|
92
|
+
key: string,
|
|
93
|
+
ttlMs: number,
|
|
94
|
+
loader: () => Promise<T | undefined>,
|
|
95
|
+
db: Db | undefined,
|
|
96
|
+
): Promise<T | undefined> {
|
|
97
|
+
const value = await loader();
|
|
98
|
+
if (value === undefined) return undefined;
|
|
99
|
+
await writeEntry(db, key, { value, expires: Date.now() + ttlMs });
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
45
103
|
export async function persistentCache<T>(
|
|
46
104
|
key: string,
|
|
47
105
|
ttlMs: number,
|
|
48
106
|
loader: () => Promise<T | undefined>,
|
|
107
|
+
opts?: { staleWhileRevalidate?: boolean },
|
|
49
108
|
): Promise<T | undefined> {
|
|
50
109
|
const db = await dataDb().catch(() => undefined);
|
|
51
|
-
const
|
|
52
|
-
const entry = db
|
|
53
|
-
? await d1Read(key)
|
|
54
|
-
: loadJsonFile<Entry | undefined>(file, () => undefined);
|
|
110
|
+
const entry = await readEntry(db, key);
|
|
55
111
|
if (entry && entry.expires > Date.now()) return entry.value as T;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
112
|
+
if (entry && opts?.staleWhileRevalidate) {
|
|
113
|
+
const pending =
|
|
114
|
+
inflight.get(key) ??
|
|
115
|
+
loadAndStore(key, ttlMs, loader, db).finally(() =>
|
|
116
|
+
inflight.delete(key),
|
|
117
|
+
);
|
|
118
|
+
inflight.set(key, pending);
|
|
119
|
+
void pending.catch(() => {});
|
|
120
|
+
return entry.value as T;
|
|
121
|
+
}
|
|
122
|
+
const value = await loadAndStore(key, ttlMs, loader, db);
|
|
123
|
+
return value ?? (entry?.value as T | undefined);
|
|
62
124
|
}
|