@bayonai/offline 0.5.1 → 0.8.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/CHANGELOG.md +34 -0
- package/README.md +69 -3
- package/dist/browserFallbackRecovery.js +5 -2
- package/dist/durableCommandStore.d.ts +49 -0
- package/dist/durableCommandStore.js +223 -0
- package/dist/idbDriver.d.ts +3 -0
- package/dist/idbDriver.js +80 -31
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pendingWrites.d.ts +13 -4
- package/dist/pendingWrites.js +40 -12
- package/dist/readCache.d.ts +1 -1
- package/dist/readCache.js +22 -5
- package/dist-cjs/browserFallbackRecovery.js +5 -2
- package/dist-cjs/durableCommandStore.js +226 -0
- package/dist-cjs/idbDriver.js +82 -31
- package/dist-cjs/index.js +4 -0
- package/dist-cjs/pendingWrites.js +39 -11
- package/dist-cjs/readCache.js +22 -5
- package/package.json +3 -3
package/dist-cjs/idbDriver.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OfflineStorageError = void 0;
|
|
3
4
|
exports.createIdbKeyValueDriver = createIdbKeyValueDriver;
|
|
4
5
|
const idb_1 = require("idb");
|
|
6
|
+
class OfflineStorageError extends Error {
|
|
7
|
+
name = "OfflineStorageError";
|
|
8
|
+
}
|
|
9
|
+
exports.OfflineStorageError = OfflineStorageError;
|
|
5
10
|
function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB = globalThis.indexedDB, operationTimeoutMs, storeName, }) {
|
|
6
11
|
if (!indexedDB) {
|
|
7
12
|
return null;
|
|
@@ -12,22 +17,33 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
12
17
|
indexedDB,
|
|
13
18
|
storeName,
|
|
14
19
|
});
|
|
20
|
+
function runMutation(operationName, mutate) {
|
|
21
|
+
return runTransactionalStorageOperation({
|
|
22
|
+
failureMode: "throw",
|
|
23
|
+
fallback: undefined,
|
|
24
|
+
operationName,
|
|
25
|
+
storeName,
|
|
26
|
+
operation: async (setTransaction, isCancelled) => {
|
|
27
|
+
const database = await databasePromise;
|
|
28
|
+
if (isCancelled())
|
|
29
|
+
return;
|
|
30
|
+
const transaction = database.transaction(storeName, "readwrite");
|
|
31
|
+
setTransaction(transaction);
|
|
32
|
+
await mutate(transaction.objectStore(storeName));
|
|
33
|
+
await transaction.done;
|
|
34
|
+
},
|
|
35
|
+
timeoutMs: operationTimeoutMs,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
15
38
|
return {
|
|
16
39
|
async clear() {
|
|
17
|
-
await
|
|
18
|
-
fallback: undefined,
|
|
19
|
-
operationName: "clear object store",
|
|
20
|
-
operation: async () => {
|
|
21
|
-
const database = await databasePromise;
|
|
22
|
-
await database.clear(storeName);
|
|
23
|
-
},
|
|
24
|
-
timeoutMs: operationTimeoutMs,
|
|
25
|
-
});
|
|
40
|
+
await runMutation("clear object store", (objectStore) => objectStore.clear());
|
|
26
41
|
},
|
|
27
42
|
async close() {
|
|
28
43
|
await runBrowserStorageOperation({
|
|
29
44
|
fallback: undefined,
|
|
30
45
|
operationName: "close database",
|
|
46
|
+
storeName,
|
|
31
47
|
operation: async () => {
|
|
32
48
|
const database = await databasePromise;
|
|
33
49
|
database.close();
|
|
@@ -36,15 +52,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
36
52
|
});
|
|
37
53
|
},
|
|
38
54
|
async delete(key) {
|
|
39
|
-
await
|
|
40
|
-
fallback: undefined,
|
|
41
|
-
operationName: "delete key",
|
|
42
|
-
operation: async () => {
|
|
43
|
-
const database = await databasePromise;
|
|
44
|
-
await database.delete(storeName, key);
|
|
45
|
-
},
|
|
46
|
-
timeoutMs: operationTimeoutMs,
|
|
47
|
-
});
|
|
55
|
+
await runMutation("delete key", (objectStore) => objectStore.delete(key));
|
|
48
56
|
},
|
|
49
57
|
async deleteIf(key, predicate) {
|
|
50
58
|
return runConditionalStorageOperation({
|
|
@@ -65,6 +73,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
65
73
|
return runBrowserStorageOperation({
|
|
66
74
|
fallback: null,
|
|
67
75
|
operationName: "read key",
|
|
76
|
+
storeName,
|
|
68
77
|
operation: async () => {
|
|
69
78
|
const database = await databasePromise;
|
|
70
79
|
return (await database.get(storeName, key)) ?? null;
|
|
@@ -76,6 +85,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
76
85
|
return runBrowserStorageOperation({
|
|
77
86
|
fallback: [],
|
|
78
87
|
operationName: "list object store",
|
|
88
|
+
storeName,
|
|
79
89
|
operation: async () => {
|
|
80
90
|
const database = await databasePromise;
|
|
81
91
|
return database.getAll(storeName);
|
|
@@ -87,6 +97,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
87
97
|
return runBrowserStorageOperation({
|
|
88
98
|
fallback: [],
|
|
89
99
|
operationName: "list object-store entries",
|
|
100
|
+
storeName,
|
|
90
101
|
operation: async () => {
|
|
91
102
|
const database = await databasePromise;
|
|
92
103
|
// Keys and values must come from one transaction. Separate database
|
|
@@ -108,15 +119,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
108
119
|
});
|
|
109
120
|
},
|
|
110
121
|
async set(key, value) {
|
|
111
|
-
await
|
|
112
|
-
fallback: undefined,
|
|
113
|
-
operationName: "write key",
|
|
114
|
-
operation: async () => {
|
|
115
|
-
const database = await databasePromise;
|
|
116
|
-
await database.put(storeName, value, key);
|
|
117
|
-
},
|
|
118
|
-
timeoutMs: operationTimeoutMs,
|
|
119
|
-
});
|
|
122
|
+
await runMutation("write key", (objectStore) => objectStore.put(value, key));
|
|
120
123
|
},
|
|
121
124
|
async setIf(key, value, predicate) {
|
|
122
125
|
return runConditionalStorageOperation({
|
|
@@ -149,18 +152,27 @@ async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB,
|
|
|
149
152
|
}
|
|
150
153
|
return database;
|
|
151
154
|
}
|
|
152
|
-
async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation, operationName, timeoutMs, }) {
|
|
155
|
+
async function runBrowserStorageOperation({ cancelOnTimeout, failureMode = "fallback", fallback, operation, operationName, storeName, timeoutMs, }) {
|
|
153
156
|
let timeoutId = null;
|
|
157
|
+
const startedAt = Date.now();
|
|
154
158
|
try {
|
|
155
159
|
return await Promise.race([
|
|
156
160
|
operation(),
|
|
157
|
-
new Promise((resolve) => {
|
|
161
|
+
new Promise((resolve, reject) => {
|
|
158
162
|
timeoutId = setTimeout(() => {
|
|
159
|
-
console.
|
|
163
|
+
console.warn("[Bayon offline] IndexedDB operation timed out.", {
|
|
164
|
+
elapsedMs: Date.now() - startedAt,
|
|
165
|
+
operationName,
|
|
166
|
+
storeName,
|
|
160
167
|
timeoutMs,
|
|
161
168
|
});
|
|
162
169
|
if (!cancelOnTimeout || cancelOnTimeout()) {
|
|
163
|
-
|
|
170
|
+
if (failureMode === "throw") {
|
|
171
|
+
reject(new OfflineStorageError(`IndexedDB timed out while attempting to ${operationName} in ${storeName}.`));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
resolve(fallback);
|
|
175
|
+
}
|
|
164
176
|
}
|
|
165
177
|
}, timeoutMs);
|
|
166
178
|
}),
|
|
@@ -168,6 +180,11 @@ async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation
|
|
|
168
180
|
}
|
|
169
181
|
catch (error) {
|
|
170
182
|
console.error(`IndexedDB failed to ${operationName}.`, { error });
|
|
183
|
+
if (failureMode === "throw") {
|
|
184
|
+
if (error instanceof OfflineStorageError)
|
|
185
|
+
throw error;
|
|
186
|
+
throw new OfflineStorageError(`IndexedDB failed to ${operationName} in ${storeName}.`, { cause: error });
|
|
187
|
+
}
|
|
171
188
|
return fallback;
|
|
172
189
|
}
|
|
173
190
|
finally {
|
|
@@ -176,12 +193,46 @@ async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation
|
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
}
|
|
196
|
+
async function runTransactionalStorageOperation({ failureMode, fallback, operation, operationName, storeName, timeoutMs, }) {
|
|
197
|
+
let transaction;
|
|
198
|
+
let cancelled = false;
|
|
199
|
+
return runBrowserStorageOperation({
|
|
200
|
+
failureMode,
|
|
201
|
+
fallback,
|
|
202
|
+
operationName,
|
|
203
|
+
storeName,
|
|
204
|
+
timeoutMs,
|
|
205
|
+
cancelOnTimeout() {
|
|
206
|
+
cancelled = true;
|
|
207
|
+
if (!transaction)
|
|
208
|
+
return true;
|
|
209
|
+
try {
|
|
210
|
+
transaction.abort();
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.warn("[Bayon offline] IndexedDB transaction already settling at timeout.", { error, operationName });
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
operation: async () => {
|
|
219
|
+
return operation((nextTransaction) => {
|
|
220
|
+
transaction = nextTransaction;
|
|
221
|
+
// Aborting can reject transaction.done. The operation also awaits it,
|
|
222
|
+
// but observing it here prevents a race from becoming unhandled.
|
|
223
|
+
void transaction.done.catch(() => undefined);
|
|
224
|
+
}, () => cancelled);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
179
228
|
async function runConditionalStorageOperation({ apply, databasePromise, operationName, storeName, timeoutMs, }) {
|
|
180
229
|
let transaction;
|
|
181
230
|
let cancelled = false;
|
|
182
231
|
return runBrowserStorageOperation({
|
|
232
|
+
failureMode: "throw",
|
|
183
233
|
fallback: false,
|
|
184
234
|
operationName,
|
|
235
|
+
storeName,
|
|
185
236
|
timeoutMs,
|
|
186
237
|
cancelOnTimeout() {
|
|
187
238
|
cancelled = true;
|
package/dist-cjs/index.js
CHANGED
|
@@ -14,8 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.OfflineStorageError = void 0;
|
|
17
18
|
__exportStar(require("./readCache"), exports);
|
|
18
19
|
__exportStar(require("./pendingWrites"), exports);
|
|
19
20
|
__exportStar(require("./browserRecovery"), exports);
|
|
20
21
|
__exportStar(require("./browserCoordination"), exports);
|
|
21
22
|
__exportStar(require("./browserFallbackRecovery"), exports);
|
|
23
|
+
__exportStar(require("./durableCommandStore"), exports);
|
|
24
|
+
var idbDriver_1 = require("./idbDriver");
|
|
25
|
+
Object.defineProperty(exports, "OfflineStorageError", { enumerable: true, get: function () { return idbDriver_1.OfflineStorageError; } });
|
|
@@ -162,19 +162,34 @@ function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexed
|
|
|
162
162
|
operationTimeoutMs,
|
|
163
163
|
storeName,
|
|
164
164
|
});
|
|
165
|
-
return createPendingWriteStore(driver ??
|
|
165
|
+
return createPendingWriteStore(driver ?? createUnavailablePendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
|
|
166
166
|
}
|
|
167
167
|
function createPendingWriteKey({ id, prefix }) {
|
|
168
168
|
return `${prefix}:${encodeURIComponent(id)}`;
|
|
169
169
|
}
|
|
170
170
|
function createPendingWriteSnapshotId() {
|
|
171
|
-
|
|
171
|
+
const randomId = globalThis.crypto?.randomUUID?.();
|
|
172
|
+
if (randomId)
|
|
173
|
+
return randomId;
|
|
174
|
+
pendingWriteSnapshotSequence += 1;
|
|
175
|
+
return [
|
|
176
|
+
"snapshot",
|
|
177
|
+
Date.now().toString(36),
|
|
178
|
+
pendingWriteSnapshotSequence.toString(36),
|
|
179
|
+
Math.random().toString(36).slice(2),
|
|
180
|
+
].join("-");
|
|
172
181
|
}
|
|
182
|
+
let pendingWriteSnapshotSequence = 0;
|
|
173
183
|
async function syncPendingWrite({ id, now = () => new Date().toISOString(), store, sync, }) {
|
|
174
184
|
const pending = await store.get(id);
|
|
175
185
|
if (!pending) {
|
|
176
186
|
return { status: "idle" };
|
|
177
187
|
}
|
|
188
|
+
// A delayed command remains in its ordered lane until its grace period has
|
|
189
|
+
// elapsed. This makes cancellation survive UI unmounts and app restarts.
|
|
190
|
+
if (pending.notBefore && Date.parse(pending.notBefore) > Date.parse(now())) {
|
|
191
|
+
return { status: "idle" };
|
|
192
|
+
}
|
|
178
193
|
const claimedSnapshotId = createPendingWriteSnapshotId();
|
|
179
194
|
const claimed = await store.claimIfSnapshotId(id, pending.snapshotId, {
|
|
180
195
|
...pending,
|
|
@@ -187,7 +202,7 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
187
202
|
return { status: "superseded" };
|
|
188
203
|
}
|
|
189
204
|
try {
|
|
190
|
-
await sync(pending.payload);
|
|
205
|
+
await sync(pending.payload, { recordId: pending.id });
|
|
191
206
|
}
|
|
192
207
|
catch (error) {
|
|
193
208
|
const message = error instanceof Error ? error.message : "Unable to sync pending write.";
|
|
@@ -204,7 +219,7 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
204
219
|
store,
|
|
205
220
|
});
|
|
206
221
|
return markedFailed
|
|
207
|
-
? { error: message, status: "failed" }
|
|
222
|
+
? { error: message, failure: error, status: "failed" }
|
|
208
223
|
: { status: "superseded" };
|
|
209
224
|
}
|
|
210
225
|
return (await store.clearIfSnapshotId(id, claimedSnapshotId))
|
|
@@ -216,7 +231,7 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
216
231
|
* same store/scope. Callers own the selection predicate and idempotent remote
|
|
217
232
|
* mutation semantics.
|
|
218
233
|
*/
|
|
219
|
-
function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
|
|
234
|
+
function syncPendingWrites({ now, scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
|
|
220
235
|
let inFlightByScope = pendingWriteSyncs.get(store);
|
|
221
236
|
if (!inFlightByScope) {
|
|
222
237
|
inFlightByScope = new Map();
|
|
@@ -227,6 +242,7 @@ function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailur
|
|
|
227
242
|
return existing;
|
|
228
243
|
}
|
|
229
244
|
const syncRun = performPendingWriteSync({
|
|
245
|
+
now,
|
|
230
246
|
select,
|
|
231
247
|
stopOnFailure,
|
|
232
248
|
store,
|
|
@@ -273,7 +289,7 @@ function summarizePendingWrites(records) {
|
|
|
273
289
|
}, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
|
|
274
290
|
}
|
|
275
291
|
const pendingWriteSyncs = new WeakMap();
|
|
276
|
-
async function performPendingWriteSync({ select, stopOnFailure, store, sync, }) {
|
|
292
|
+
async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
|
|
277
293
|
const summary = {
|
|
278
294
|
attempted: 0,
|
|
279
295
|
failed: 0,
|
|
@@ -286,6 +302,7 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
|
|
|
286
302
|
for (const record of records) {
|
|
287
303
|
const result = await syncPendingWrite({
|
|
288
304
|
id: record.id,
|
|
305
|
+
now,
|
|
289
306
|
store,
|
|
290
307
|
sync,
|
|
291
308
|
});
|
|
@@ -294,7 +311,9 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
|
|
|
294
311
|
// Product decision: a superseded result can represent an older live claim.
|
|
295
312
|
// Preserve strict ordering by leaving every later record for a future run.
|
|
296
313
|
if (stopOnFailure &&
|
|
297
|
-
(result.status === "failed" ||
|
|
314
|
+
(result.status === "failed" ||
|
|
315
|
+
result.status === "idle" ||
|
|
316
|
+
result.status === "superseded")) {
|
|
298
317
|
break;
|
|
299
318
|
}
|
|
300
319
|
}
|
|
@@ -319,7 +338,7 @@ function isPendingWriteSyncClaimAvailable(record, now) {
|
|
|
319
338
|
* workspace. Use its `recover` method for background replay and `run` for new
|
|
320
339
|
* writes so a newer remote mutation cannot overtake an older queued mutation.
|
|
321
340
|
*/
|
|
322
|
-
function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => true, store, sync, }) {
|
|
341
|
+
function createOrderedPendingWriteExecutor({ now, scopeKey = "all", select = () => true, store, sync, }) {
|
|
323
342
|
let execution = Promise.resolve();
|
|
324
343
|
function inOrder(operation) {
|
|
325
344
|
const result = execution.then(operation, operation);
|
|
@@ -328,6 +347,7 @@ function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => tr
|
|
|
328
347
|
}
|
|
329
348
|
function recover() {
|
|
330
349
|
return inOrder(() => syncPendingWrites({
|
|
350
|
+
now,
|
|
331
351
|
scopeKey,
|
|
332
352
|
select,
|
|
333
353
|
stopOnFailure: true,
|
|
@@ -343,6 +363,7 @@ function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => tr
|
|
|
343
363
|
return runPendingWrite(options);
|
|
344
364
|
}
|
|
345
365
|
const replay = await syncPendingWrites({
|
|
366
|
+
now,
|
|
346
367
|
scopeKey,
|
|
347
368
|
select,
|
|
348
369
|
stopOnFailure: true,
|
|
@@ -360,7 +381,8 @@ function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => tr
|
|
|
360
381
|
},
|
|
361
382
|
};
|
|
362
383
|
}
|
|
363
|
-
function
|
|
384
|
+
function createUnavailablePendingWriteDriver() {
|
|
385
|
+
const unavailable = () => new idbDriver_1.OfflineStorageError("IndexedDB is unavailable; the pending write was not stored.");
|
|
364
386
|
return {
|
|
365
387
|
async clear() { },
|
|
366
388
|
async clearIf() {
|
|
@@ -374,9 +396,11 @@ function createNoopPendingWriteDriver() {
|
|
|
374
396
|
async list() {
|
|
375
397
|
return [];
|
|
376
398
|
},
|
|
377
|
-
async set() {
|
|
399
|
+
async set() {
|
|
400
|
+
throw unavailable();
|
|
401
|
+
},
|
|
378
402
|
async setIf() {
|
|
379
|
-
|
|
403
|
+
throw unavailable();
|
|
380
404
|
},
|
|
381
405
|
};
|
|
382
406
|
}
|
|
@@ -416,6 +440,9 @@ function normalizePendingWriteRecord(record, normalizePayload) {
|
|
|
416
440
|
return {
|
|
417
441
|
...(typeof record.error === "string" ? { error: record.error } : {}),
|
|
418
442
|
id: record.id,
|
|
443
|
+
...(typeof record.notBefore === "string"
|
|
444
|
+
? { notBefore: record.notBefore }
|
|
445
|
+
: {}),
|
|
419
446
|
payload,
|
|
420
447
|
savedAt: record.savedAt,
|
|
421
448
|
snapshotId: record.snapshotId ?? createLegacyPendingWriteSnapshotId(record),
|
|
@@ -444,6 +471,7 @@ function isPendingWriteRecord(value) {
|
|
|
444
471
|
typeof value === "object" &&
|
|
445
472
|
"id" in value &&
|
|
446
473
|
typeof value.id === "string" &&
|
|
474
|
+
(!("notBefore" in value) || typeof value.notBefore === "string") &&
|
|
447
475
|
"savedAt" in value &&
|
|
448
476
|
typeof value.savedAt === "string" &&
|
|
449
477
|
(!("snapshotId" in value) || typeof value.snapshotId === "string") &&
|
package/dist-cjs/readCache.js
CHANGED
|
@@ -13,7 +13,9 @@ exports.readCacheSchemaVersion = 1;
|
|
|
13
13
|
exports.readCacheFreshnessMaxAgeMs = 30_000;
|
|
14
14
|
exports.durableReadCacheFreshnessMaxAgeMs = 60 * 60_000;
|
|
15
15
|
exports.identityReadCacheFreshnessMaxAgeMs = 24 * 60 * 60_000;
|
|
16
|
-
|
|
16
|
+
// Cache reads are optional and should yield to the fresh loader quickly when
|
|
17
|
+
// IndexedDB is blocked by a browser/WebView lifecycle or another tab.
|
|
18
|
+
exports.browserReadCacheOperationTimeoutMs = 1_000;
|
|
17
19
|
const pendingFreshLoads = new WeakMap();
|
|
18
20
|
const cachePruners = new WeakMap();
|
|
19
21
|
function createReadCache(driver) {
|
|
@@ -143,8 +145,7 @@ function createBrowserReadCache({ databaseName, databaseVersion, indexedDB = glo
|
|
|
143
145
|
*/
|
|
144
146
|
function pruneReadCache({ cache, maxAgeMs, now = () => Date.now(), }) {
|
|
145
147
|
const resourceCache = cache.capture?.() ?? cache;
|
|
146
|
-
return cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ??
|
|
147
|
-
Promise.resolve(0);
|
|
148
|
+
return (cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ?? Promise.resolve(0));
|
|
148
149
|
}
|
|
149
150
|
function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
|
|
150
151
|
const serializedParams = Object.entries(params)
|
|
@@ -166,7 +167,15 @@ async function readThroughCache({ cache, forceFresh = false, key, loadFresh, max
|
|
|
166
167
|
if (forceFresh) {
|
|
167
168
|
// Deletion advances the cache version, so a request started before this
|
|
168
169
|
// post-write refresh cannot restore stale data after it finishes.
|
|
169
|
-
|
|
170
|
+
try {
|
|
171
|
+
await resourceCache.delete(key);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error("[Bayon offline] Unable to invalidate the read cache.", {
|
|
175
|
+
error: normalizeError(error),
|
|
176
|
+
key,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
170
179
|
}
|
|
171
180
|
const version = resourceCache.getVersion(key);
|
|
172
181
|
const cached = await resourceCache.get(key);
|
|
@@ -180,7 +189,15 @@ async function readThroughCache({ cache, forceFresh = false, key, loadFresh, max
|
|
|
180
189
|
}
|
|
181
190
|
try {
|
|
182
191
|
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
183
|
-
|
|
192
|
+
try {
|
|
193
|
+
await resourceCache.setIfVersion(key, fresh, version);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error("[Bayon offline] Unable to refresh the read cache.", {
|
|
197
|
+
error: normalizeError(error),
|
|
198
|
+
key,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
184
201
|
return {
|
|
185
202
|
data: fresh,
|
|
186
203
|
error: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bayonai/offline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Reusable offline read-cache, pending-write, and silent recovery primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bayon AI",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"fake-indexeddb": "^6.2.5",
|
|
59
|
-
"typescript": "^
|
|
60
|
-
"vitest": "^
|
|
59
|
+
"typescript": "^6.0.3",
|
|
60
|
+
"vitest": "^5.0.1"
|
|
61
61
|
}
|
|
62
62
|
}
|