@terrafirmagreg-team/data 1.2.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/data",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "type": "module",
5
5
  "description": "Storage drivers and shared helpers",
6
6
  "license": "LGPL-3.0-only",
@@ -0,0 +1,124 @@
1
+ import 'server-only';
2
+ import { createHash } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { dataDb, dbSql } from './db';
5
+ import { loadJsonFile, saveJsonFile } from './json-file';
6
+
7
+ type Entry = { value: unknown; expires: number };
8
+
9
+ let tableReady: Promise<unknown> | undefined;
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
+
16
+ function fileFor(key: string): string {
17
+ const name = key.replace(/[^a-z0-9._-]+/gi, '_').slice(0, 60);
18
+ const hash = createHash('sha1').update(key).digest('hex').slice(0, 8);
19
+ return join(process.cwd(), 'data', 'cache', `${name}-${hash}.json`);
20
+ }
21
+
22
+ async function d1Read(key: string): Promise<Entry | undefined> {
23
+ try {
24
+ await ready();
25
+ const rows = (await dbSql`
26
+ SELECT value, expires FROM tfg_cache WHERE key = ${key}
27
+ `) as { rows?: { value: string; expires: number }[] } | undefined;
28
+ const row = rows?.rows?.[0];
29
+ return row
30
+ ? { value: JSON.parse(row.value), expires: row.expires }
31
+ : undefined;
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ async function d1Write(key: string, entry: Entry): Promise<void> {
38
+ try {
39
+ await ready();
40
+ await dbSql`
41
+ INSERT OR REPLACE INTO tfg_cache (key, value, expires)
42
+ VALUES (${key}, ${JSON.stringify(entry.value)}, ${entry.expires})
43
+ `;
44
+ } catch {}
45
+ }
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
+
103
+ export async function persistentCache<T>(
104
+ key: string,
105
+ ttlMs: number,
106
+ loader: () => Promise<T | undefined>,
107
+ opts?: { staleWhileRevalidate?: boolean },
108
+ ): Promise<T | undefined> {
109
+ const db = await dataDb().catch(() => undefined);
110
+ const entry = await readEntry(db, key);
111
+ if (entry && entry.expires > Date.now()) return entry.value as T;
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);
124
+ }