@bayonai/offline 0.7.0 → 0.9.0

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/dist/idbDriver.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { wrap, } from "idb";
2
+ export class OfflineStorageError extends Error {
3
+ name = "OfflineStorageError";
4
+ }
2
5
  export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB = globalThis.indexedDB, operationTimeoutMs, storeName, }) {
3
6
  if (!indexedDB) {
4
7
  return null;
@@ -9,25 +12,33 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
9
12
  indexedDB,
10
13
  storeName,
11
14
  });
15
+ function runMutation(operationName, mutate) {
16
+ return runTransactionalStorageOperation({
17
+ failureMode: "throw",
18
+ fallback: undefined,
19
+ operationName,
20
+ storeName,
21
+ operation: async (setTransaction, isCancelled) => {
22
+ const database = await databasePromise;
23
+ if (isCancelled())
24
+ return;
25
+ const transaction = database.transaction(storeName, "readwrite");
26
+ setTransaction(transaction);
27
+ await mutate(transaction.objectStore(storeName));
28
+ await transaction.done;
29
+ },
30
+ timeoutMs: operationTimeoutMs,
31
+ });
32
+ }
12
33
  return {
13
34
  async clear() {
14
- await runTransactionalStorageOperation({
15
- fallback: undefined,
16
- operationName: "clear object store",
17
- operation: async (setTransaction) => {
18
- const database = await databasePromise;
19
- const transaction = database.transaction(storeName, "readwrite");
20
- setTransaction(transaction);
21
- await transaction.objectStore(storeName).clear();
22
- await transaction.done;
23
- },
24
- timeoutMs: operationTimeoutMs,
25
- });
35
+ await runMutation("clear object store", (objectStore) => objectStore.clear());
26
36
  },
27
37
  async close() {
28
38
  await runBrowserStorageOperation({
29
39
  fallback: undefined,
30
40
  operationName: "close database",
41
+ storeName,
31
42
  operation: async () => {
32
43
  const database = await databasePromise;
33
44
  database.close();
@@ -36,18 +47,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
36
47
  });
37
48
  },
38
49
  async delete(key) {
39
- await runTransactionalStorageOperation({
40
- fallback: undefined,
41
- operationName: "delete key",
42
- operation: async (setTransaction) => {
43
- const database = await databasePromise;
44
- const transaction = database.transaction(storeName, "readwrite");
45
- setTransaction(transaction);
46
- await transaction.objectStore(storeName).delete(key);
47
- await transaction.done;
48
- },
49
- timeoutMs: operationTimeoutMs,
50
- });
50
+ await runMutation("delete key", (objectStore) => objectStore.delete(key));
51
51
  },
52
52
  async deleteIf(key, predicate) {
53
53
  return runConditionalStorageOperation({
@@ -68,6 +68,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
68
68
  return runBrowserStorageOperation({
69
69
  fallback: null,
70
70
  operationName: "read key",
71
+ storeName,
71
72
  operation: async () => {
72
73
  const database = await databasePromise;
73
74
  return (await database.get(storeName, key)) ?? null;
@@ -79,6 +80,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
79
80
  return runBrowserStorageOperation({
80
81
  fallback: [],
81
82
  operationName: "list object store",
83
+ storeName,
82
84
  operation: async () => {
83
85
  const database = await databasePromise;
84
86
  return database.getAll(storeName);
@@ -90,6 +92,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
90
92
  return runBrowserStorageOperation({
91
93
  fallback: [],
92
94
  operationName: "list object-store entries",
95
+ storeName,
93
96
  operation: async () => {
94
97
  const database = await databasePromise;
95
98
  // Keys and values must come from one transaction. Separate database
@@ -111,18 +114,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
111
114
  });
112
115
  },
113
116
  async set(key, value) {
114
- await runTransactionalStorageOperation({
115
- fallback: undefined,
116
- operationName: "write key",
117
- operation: async (setTransaction) => {
118
- const database = await databasePromise;
119
- const transaction = database.transaction(storeName, "readwrite");
120
- setTransaction(transaction);
121
- await transaction.objectStore(storeName).put(value, key);
122
- await transaction.done;
123
- },
124
- timeoutMs: operationTimeoutMs,
125
- });
117
+ await runMutation("write key", (objectStore) => objectStore.put(value, key));
126
118
  },
127
119
  async setIf(key, value, predicate) {
128
120
  return runConditionalStorageOperation({
@@ -155,18 +147,27 @@ async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB,
155
147
  }
156
148
  return database;
157
149
  }
158
- async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation, operationName, timeoutMs, }) {
150
+ async function runBrowserStorageOperation({ cancelOnTimeout, failureMode = "fallback", fallback, operation, operationName, storeName, timeoutMs, }) {
159
151
  let timeoutId = null;
152
+ const startedAt = Date.now();
160
153
  try {
161
154
  return await Promise.race([
162
155
  operation(),
163
- new Promise((resolve) => {
156
+ new Promise((resolve, reject) => {
164
157
  timeoutId = setTimeout(() => {
165
- console.error(`IndexedDB timed out while trying to ${operationName}.`, {
158
+ console.warn("[Bayon offline] IndexedDB operation timed out.", {
159
+ elapsedMs: Date.now() - startedAt,
160
+ operationName,
161
+ storeName,
166
162
  timeoutMs,
167
163
  });
168
164
  if (!cancelOnTimeout || cancelOnTimeout()) {
169
- resolve(fallback);
165
+ if (failureMode === "throw") {
166
+ reject(new OfflineStorageError(`IndexedDB timed out while attempting to ${operationName} in ${storeName}.`));
167
+ }
168
+ else {
169
+ resolve(fallback);
170
+ }
170
171
  }
171
172
  }, timeoutMs);
172
173
  }),
@@ -174,6 +175,11 @@ async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation
174
175
  }
175
176
  catch (error) {
176
177
  console.error(`IndexedDB failed to ${operationName}.`, { error });
178
+ if (failureMode === "throw") {
179
+ if (error instanceof OfflineStorageError)
180
+ throw error;
181
+ throw new OfflineStorageError(`IndexedDB failed to ${operationName} in ${storeName}.`, { cause: error });
182
+ }
177
183
  return fallback;
178
184
  }
179
185
  finally {
@@ -182,12 +188,14 @@ async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation
182
188
  }
183
189
  }
184
190
  }
185
- async function runTransactionalStorageOperation({ fallback, operation, operationName, timeoutMs, }) {
191
+ async function runTransactionalStorageOperation({ failureMode, fallback, operation, operationName, storeName, timeoutMs, }) {
186
192
  let transaction;
187
193
  let cancelled = false;
188
194
  return runBrowserStorageOperation({
195
+ failureMode,
189
196
  fallback,
190
197
  operationName,
198
+ storeName,
191
199
  timeoutMs,
192
200
  cancelOnTimeout() {
193
201
  cancelled = true;
@@ -203,14 +211,12 @@ async function runTransactionalStorageOperation({ fallback, operation, operation
203
211
  }
204
212
  },
205
213
  operation: async () => {
206
- if (cancelled)
207
- return fallback;
208
214
  return operation((nextTransaction) => {
209
215
  transaction = nextTransaction;
210
216
  // Aborting can reject transaction.done. The operation also awaits it,
211
217
  // but observing it here prevents a race from becoming unhandled.
212
218
  void transaction.done.catch(() => undefined);
213
- });
219
+ }, () => cancelled);
214
220
  },
215
221
  });
216
222
  }
@@ -218,8 +224,10 @@ async function runConditionalStorageOperation({ apply, databasePromise, operatio
218
224
  let transaction;
219
225
  let cancelled = false;
220
226
  return runBrowserStorageOperation({
227
+ failureMode: "throw",
221
228
  fallback: false,
222
229
  operationName,
230
+ storeName,
223
231
  timeoutMs,
224
232
  cancelOnTimeout() {
225
233
  cancelled = true;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export * from "./readCache";
2
- export * from "./pendingWrites";
3
- export * from "./browserRecovery";
4
- export * from "./browserCoordination";
5
- export * from "./browserFallbackRecovery";
6
- export * from "./durableCommandStore";
1
+ export * from "./readCache.js";
2
+ export * from "./pendingWrites.js";
3
+ export * from "./browserRecovery.js";
4
+ export * from "./browserCoordination.js";
5
+ export * from "./browserFallbackRecovery.js";
6
+ export * from "./durableCommandStore.js";
7
+ export { OfflineStorageError } from "./idbDriver.js";
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export * from "./browserRecovery.js";
4
4
  export * from "./browserCoordination.js";
5
5
  export * from "./browserFallbackRecovery.js";
6
6
  export * from "./durableCommandStore.js";
7
+ export { OfflineStorageError } from "./idbDriver.js";
@@ -1,4 +1,4 @@
1
- import { type BrowserKeyValueDriverOptions } from "./idbDriver";
1
+ import { type BrowserKeyValueDriverOptions } from "./idbDriver.js";
2
2
  export declare const pendingWriteSchemaVersion = 1;
3
3
  export declare const pendingWriteOperationTimeoutMs = 2000;
4
4
  export declare const pendingWriteSyncLeaseTimeoutMs = 60000;
@@ -22,10 +22,14 @@ export type PendingWritePayloadNormalizer<Payload = unknown> = (payload: unknown
22
22
  export type PendingWriteDriver = {
23
23
  clear: (key: string) => Promise<void>;
24
24
  clearIf: (key: string, predicate: PendingWritePredicate) => Promise<boolean>;
25
- clearAll: () => Promise<void>;
25
+ clearAll?: () => Promise<void>;
26
26
  close?: () => Promise<void>;
27
27
  get: (key: string) => Promise<unknown | null>;
28
28
  list: () => Promise<unknown[]>;
29
+ listEntries?: () => Promise<Array<{
30
+ key: string;
31
+ value: unknown;
32
+ }>>;
29
33
  set: (key: string, value: unknown) => Promise<void>;
30
34
  setIf: (key: string, value: unknown, predicate: PendingWritePredicate) => Promise<boolean>;
31
35
  };
@@ -94,7 +98,11 @@ export type SyncPendingWritesResult = {
94
98
  superseded: number;
95
99
  synced: number;
96
100
  };
97
- export type PendingWriteSync<Payload> = (payload: Payload) => Promise<unknown>;
101
+ export type PendingWriteSyncContext = {
102
+ /** Stable durable-record identity; safe to use as a remote idempotency key. */
103
+ recordId: string;
104
+ };
105
+ export type PendingWriteSync<Payload, Result = unknown> = (payload: Payload, context: PendingWriteSyncContext) => Promise<Result>;
98
106
  export type RunPendingWriteResult<TResult, TQueued> = {
99
107
  result: TResult;
100
108
  status: "synced";
@@ -130,7 +138,7 @@ export declare function createMemoryPendingWriteDriver(): PendingWriteDriver;
130
138
  export declare function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: Omit<BrowserPendingWriteStoreOptions, "keyPrefix" | "normalizePayload" | "schemaVersion">): PendingWriteDriver | null;
131
139
  export declare function createBrowserPendingWriteStore<Payload = unknown>({ databaseName, databaseVersion, indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs, schemaVersion, storeName, }: BrowserPendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
132
140
  export declare function createPendingWriteKey({ id, prefix }: PendingWriteKeyOptions): string;
133
- export declare function createPendingWriteSnapshotId(): `${string}-${string}-${string}-${string}-${string}`;
141
+ export declare function createPendingWriteSnapshotId(): string;
134
142
  export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
135
143
  id: string;
136
144
  now?: () => string;
@@ -142,7 +150,8 @@ export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
142
150
  * same store/scope. Callers own the selection predicate and idempotent remote
143
151
  * mutation semantics.
144
152
  */
145
- export declare function syncPendingWrites<Payload>({ scopeKey, select, stopOnFailure, store, sync, }: {
153
+ export declare function syncPendingWrites<Payload>({ now, scopeKey, select, stopOnFailure, store, sync, }: {
154
+ now?: () => string;
146
155
  scopeKey?: string;
147
156
  select?: (record: PendingWriteRecord<Payload>) => boolean;
148
157
  /** Preserve caller ordering by leaving later records for a future retry. */
@@ -161,7 +170,8 @@ export declare function summarizePendingWrites<Payload>(records: Array<PendingWr
161
170
  * workspace. Use its `recover` method for background replay and `run` for new
162
171
  * writes so a newer remote mutation cannot overtake an older queued mutation.
163
172
  */
164
- export declare function createOrderedPendingWriteExecutor<Payload>({ scopeKey, select, store, sync, }: {
173
+ export declare function createOrderedPendingWriteExecutor<Payload>({ now, scopeKey, select, store, sync, }: {
174
+ now?: () => string;
165
175
  scopeKey?: string;
166
176
  select?: (record: PendingWriteRecord<Payload>) => boolean;
167
177
  store: PendingWriteStore<Payload>;
@@ -1,4 +1,4 @@
1
- import { createIdbKeyValueDriver, } from "./idbDriver.js";
1
+ import { createIdbKeyValueDriver, OfflineStorageError, } from "./idbDriver.js";
2
2
  export const pendingWriteSchemaVersion = 1;
3
3
  export const pendingWriteOperationTimeoutMs = 2_000;
4
4
  export const pendingWriteSyncLeaseTimeoutMs = 60_000;
@@ -17,6 +17,29 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyReco
17
17
  }
18
18
  }
19
19
  };
20
+ async function listScopedRecords() {
21
+ const readOptions = { normalizeLegacyRecord, normalizePayload };
22
+ const entries = await driver.listEntries?.();
23
+ if (entries) {
24
+ const keyPrefixBoundary = `${keyPrefix}:`;
25
+ return entries
26
+ .filter(({ key }) => key.startsWith(keyPrefixBoundary))
27
+ .map(({ value }) => toPendingWriteEnvelope(value, schemaVersion, readOptions)
28
+ ?.record)
29
+ .filter((record) => Boolean(record));
30
+ }
31
+ // Compatibility path for custom drivers created before listEntries was
32
+ // available. Point reads prove that each candidate belongs to this prefix.
33
+ const candidateIds = new Set((await driver.list())
34
+ .map((value) => toPendingWriteEnvelope(value, schemaVersion, readOptions)
35
+ ?.record.id)
36
+ .filter((id) => Boolean(id)));
37
+ const records = await Promise.all([...candidateIds].map(async (id) => {
38
+ const value = await driver.get(createPendingWriteKey({ id, prefix: keyPrefix }));
39
+ return toPendingWriteEnvelope(value, schemaVersion, readOptions)?.record;
40
+ }));
41
+ return records.filter((record) => Boolean(record));
42
+ }
20
43
  return {
21
44
  async claimIfSnapshotId(id, snapshotId, record, now) {
22
45
  const claimed = await driver.setIf(createPendingWriteKey({ id, prefix: keyPrefix }), { record, schemaVersion }, (value) => {
@@ -37,7 +60,20 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyReco
37
60
  notify({ id, type: "clear" });
38
61
  },
39
62
  async clearAll() {
40
- await driver.clearAll();
63
+ const entries = await driver.listEntries?.();
64
+ if (entries) {
65
+ const keyPrefixBoundary = `${keyPrefix}:`;
66
+ await Promise.all(entries
67
+ .filter(({ key }) => key.startsWith(keyPrefixBoundary))
68
+ .map(({ key }) => driver.clear(key)));
69
+ }
70
+ else {
71
+ const records = await listScopedRecords();
72
+ await Promise.all(records.map((record) => driver.clearIf(createPendingWriteKey({ id: record.id, prefix: keyPrefix }), (value) => toPendingWriteEnvelope(value, schemaVersion, {
73
+ normalizeLegacyRecord,
74
+ normalizePayload,
75
+ })?.record.snapshotId === record.snapshotId)));
76
+ }
41
77
  notify({ type: "clearAll" });
42
78
  },
43
79
  async clearIfSnapshotId(id, snapshotId) {
@@ -57,13 +93,7 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyReco
57
93
  return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizeLegacyRecord, normalizePayload })?.record ?? null);
58
94
  },
59
95
  async list() {
60
- return (await driver.list())
61
- .map((value) => toPendingWriteEnvelope(value, schemaVersion, {
62
- normalizeLegacyRecord,
63
- normalizePayload,
64
- })?.record)
65
- .filter((record) => Boolean(record))
66
- .sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
96
+ return (await listScopedRecords()).sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
67
97
  left.id.localeCompare(right.id));
68
98
  },
69
99
  async replaceIfSnapshotId(id, snapshotId, record) {
@@ -118,6 +148,9 @@ export function createMemoryPendingWriteDriver() {
118
148
  async list() {
119
149
  return [...records.values()];
120
150
  },
151
+ async listEntries() {
152
+ return [...records].map(([key, value]) => ({ key, value }));
153
+ },
121
154
  async set(key, value) {
122
155
  records.set(key, value);
123
156
  },
@@ -148,14 +181,24 @@ export function createBrowserPendingWriteStore({ databaseName, databaseVersion,
148
181
  operationTimeoutMs,
149
182
  storeName,
150
183
  });
151
- return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
184
+ return createPendingWriteStore(driver ?? createUnavailablePendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
152
185
  }
153
186
  export function createPendingWriteKey({ id, prefix }) {
154
187
  return `${prefix}:${encodeURIComponent(id)}`;
155
188
  }
156
189
  export function createPendingWriteSnapshotId() {
157
- return globalThis.crypto.randomUUID();
190
+ const randomId = globalThis.crypto?.randomUUID?.();
191
+ if (randomId)
192
+ return randomId;
193
+ pendingWriteSnapshotSequence += 1;
194
+ return [
195
+ "snapshot",
196
+ Date.now().toString(36),
197
+ pendingWriteSnapshotSequence.toString(36),
198
+ Math.random().toString(36).slice(2),
199
+ ].join("-");
158
200
  }
201
+ let pendingWriteSnapshotSequence = 0;
159
202
  export async function syncPendingWrite({ id, now = () => new Date().toISOString(), store, sync, }) {
160
203
  const pending = await store.get(id);
161
204
  if (!pending) {
@@ -163,8 +206,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
163
206
  }
164
207
  // A delayed command remains in its ordered lane until its grace period has
165
208
  // elapsed. This makes cancellation survive UI unmounts and app restarts.
166
- if (pending.notBefore &&
167
- Date.parse(pending.notBefore) > Date.parse(now())) {
209
+ if (pending.notBefore && Date.parse(pending.notBefore) > Date.parse(now())) {
168
210
  return { status: "idle" };
169
211
  }
170
212
  const claimedSnapshotId = createPendingWriteSnapshotId();
@@ -179,7 +221,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
179
221
  return { status: "superseded" };
180
222
  }
181
223
  try {
182
- await sync(pending.payload);
224
+ await sync(pending.payload, { recordId: pending.id });
183
225
  }
184
226
  catch (error) {
185
227
  const message = error instanceof Error ? error.message : "Unable to sync pending write.";
@@ -208,7 +250,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
208
250
  * same store/scope. Callers own the selection predicate and idempotent remote
209
251
  * mutation semantics.
210
252
  */
211
- export function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
253
+ export function syncPendingWrites({ now, scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
212
254
  let inFlightByScope = pendingWriteSyncs.get(store);
213
255
  if (!inFlightByScope) {
214
256
  inFlightByScope = new Map();
@@ -219,6 +261,7 @@ export function syncPendingWrites({ scopeKey = "all", select = () => true, stopO
219
261
  return existing;
220
262
  }
221
263
  const syncRun = performPendingWriteSync({
264
+ now,
222
265
  select,
223
266
  stopOnFailure,
224
267
  store,
@@ -265,7 +308,7 @@ export function summarizePendingWrites(records) {
265
308
  }, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
266
309
  }
267
310
  const pendingWriteSyncs = new WeakMap();
268
- async function performPendingWriteSync({ select, stopOnFailure, store, sync, }) {
311
+ async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
269
312
  const summary = {
270
313
  attempted: 0,
271
314
  failed: 0,
@@ -278,6 +321,7 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
278
321
  for (const record of records) {
279
322
  const result = await syncPendingWrite({
280
323
  id: record.id,
324
+ now,
281
325
  store,
282
326
  sync,
283
327
  });
@@ -313,7 +357,7 @@ function isPendingWriteSyncClaimAvailable(record, now) {
313
357
  * workspace. Use its `recover` method for background replay and `run` for new
314
358
  * writes so a newer remote mutation cannot overtake an older queued mutation.
315
359
  */
316
- export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => true, store, sync, }) {
360
+ export function createOrderedPendingWriteExecutor({ now, scopeKey = "all", select = () => true, store, sync, }) {
317
361
  let execution = Promise.resolve();
318
362
  function inOrder(operation) {
319
363
  const result = execution.then(operation, operation);
@@ -322,6 +366,7 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
322
366
  }
323
367
  function recover() {
324
368
  return inOrder(() => syncPendingWrites({
369
+ now,
325
370
  scopeKey,
326
371
  select,
327
372
  stopOnFailure: true,
@@ -337,6 +382,7 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
337
382
  return runPendingWrite(options);
338
383
  }
339
384
  const replay = await syncPendingWrites({
385
+ now,
340
386
  scopeKey,
341
387
  select,
342
388
  stopOnFailure: true,
@@ -354,13 +400,13 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
354
400
  },
355
401
  };
356
402
  }
357
- function createNoopPendingWriteDriver() {
403
+ function createUnavailablePendingWriteDriver() {
404
+ const unavailable = () => new OfflineStorageError("IndexedDB is unavailable; the pending write was not stored.");
358
405
  return {
359
406
  async clear() { },
360
407
  async clearIf() {
361
408
  return false;
362
409
  },
363
- async clearAll() { },
364
410
  async close() { },
365
411
  async get() {
366
412
  return null;
@@ -368,9 +414,11 @@ function createNoopPendingWriteDriver() {
368
414
  async list() {
369
415
  return [];
370
416
  },
371
- async set() { },
417
+ async set() {
418
+ throw unavailable();
419
+ },
372
420
  async setIf() {
373
- return false;
421
+ throw unavailable();
374
422
  },
375
423
  };
376
424
  }
@@ -382,6 +430,7 @@ function toPendingWriteDriver(driver) {
382
430
  close: driver.close,
383
431
  get: driver.get,
384
432
  list: driver.list,
433
+ listEntries: driver.listEntries,
385
434
  set: driver.set,
386
435
  setIf: driver.setIf,
387
436
  };
@@ -1,9 +1,9 @@
1
- import { type BrowserKeyValueDriverOptions } from "./idbDriver";
1
+ import { type BrowserKeyValueDriverOptions } from "./idbDriver.js";
2
2
  export declare const readCacheSchemaVersion = 1;
3
3
  export declare const readCacheFreshnessMaxAgeMs = 30000;
4
4
  export declare const durableReadCacheFreshnessMaxAgeMs: number;
5
5
  export declare const identityReadCacheFreshnessMaxAgeMs: number;
6
- export declare const browserReadCacheOperationTimeoutMs = 2000;
6
+ export declare const browserReadCacheOperationTimeoutMs = 1000;
7
7
  export type ReadCacheHit<T> = {
8
8
  fromCache: true;
9
9
  savedAt: string;
@@ -29,21 +29,36 @@ export type ReadCache = {
29
29
  clear: () => Promise<void>;
30
30
  close: () => Promise<void>;
31
31
  delete: (key: string) => Promise<void>;
32
+ /** Advances a key's generation without deleting its retained value. */
33
+ invalidate?: (key: string) => void;
32
34
  get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
33
35
  getVersion: (key: string) => string;
34
36
  set: <T>(key: string, value: T) => Promise<void>;
35
37
  setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
36
38
  };
39
+ export type ReadCachePolicy = {
40
+ /** Time that a cached value can suppress a remote read. */
41
+ freshForMs: number;
42
+ /** Maximum age at which cached data remains eligible as an offline fallback. */
43
+ retainForMs: number;
44
+ /** Behavior when revalidation fails while retained data is available. */
45
+ refreshFailure: "use-retained" | "throw";
46
+ };
47
+ export type ReadCacheStatus = "fresh" | "stale" | "miss";
48
+ /** Payload-free cache lifecycle events suitable for host-owned telemetry. */
49
+ export type ReadCacheOutcome = "fresh-hit" | "stale-hit" | "cache-miss" | "forced-refresh" | "loader-success" | "loader-failure";
37
50
  export type ReadThroughCacheResult<T> = {
38
51
  data: T;
39
52
  error: Error | null;
40
53
  fromCache: boolean;
41
54
  stale: boolean;
55
+ status: ReadCacheStatus;
42
56
  };
43
57
  export type ReadCacheResourceUpdate<T> = {
44
58
  data: T;
45
59
  error?: Error;
46
60
  fromCache: boolean;
61
+ status: ReadCacheStatus;
47
62
  };
48
63
  export type BrowserReadCacheOptions = Partial<Pick<BrowserKeyValueDriverOptions, "databaseVersion" | "indexedDB">> & {
49
64
  databaseName: string;
@@ -69,22 +84,28 @@ export declare function pruneReadCache({ cache, maxAgeMs, now, }: {
69
84
  now?: () => number;
70
85
  }): Promise<number>;
71
86
  export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
72
- export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, }: {
87
+ export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, }: {
73
88
  cache: ReadCache;
74
89
  /**
75
- * Invalidates the current value before loading. Use after a successful write
76
- * when a cached or earlier in-flight response must not become authoritative.
90
+ * Revalidates after advancing the key generation, retaining the prior value
91
+ * as a possible fallback if refresh fails.
77
92
  */
78
93
  forceFresh?: boolean;
79
94
  key: string;
80
95
  loadFresh: () => Promise<T>;
81
96
  maxAgeMs?: number;
97
+ onOutcome?: (outcome: ReadCacheOutcome) => void;
98
+ policy?: ReadCachePolicy;
82
99
  }): Promise<ReadThroughCacheResult<T>>;
83
- export declare function loadReadCacheResource<T>({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache, }: {
100
+ export declare function loadReadCacheResource<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, }: {
84
101
  cache: ReadCache;
102
+ /** Revalidate while retaining eligible data as fallback on loader failure. */
103
+ forceFresh?: boolean;
85
104
  key: string;
86
105
  loadFresh: () => Promise<T>;
87
106
  maxAgeMs?: number;
107
+ onOutcome?: (outcome: ReadCacheOutcome) => void;
108
+ policy?: ReadCachePolicy;
88
109
  onCacheWriteError?: (error: Error) => void;
89
110
  onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
90
111
  skipCache?: boolean;