@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/data",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "type": "module",
5
5
  "description": "Storage drivers and shared helpers",
6
6
  "license": "LGPL-3.0-only",
@@ -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
- tableReady ??= dbSql`
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 tableReady;
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 file = fileFor(key);
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
- const value = await loader();
57
- if (value === undefined) return entry?.value as T | undefined;
58
- const next: Entry = { value, expires: Date.now() + ttlMs };
59
- if (db) await d1Write(key, next);
60
- else saveJsonFile(file, next);
61
- return value;
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
  }