@bayonai/offline 0.7.0 → 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 +22 -1
- package/README.md +36 -4
- package/dist/browserFallbackRecovery.js +5 -2
- package/dist/durableCommandStore.d.ts +8 -2
- package/dist/durableCommandStore.js +115 -50
- package/dist/idbDriver.d.ts +3 -0
- package/dist/idbDriver.js +52 -44
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pendingWrites.d.ts +10 -4
- package/dist/pendingWrites.js +28 -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 +115 -50
- package/dist-cjs/idbDriver.js +54 -44
- package/dist-cjs/index.js +3 -0
- package/dist-cjs/pendingWrites.js +27 -11
- package/dist-cjs/readCache.js +22 -5
- package/package.json +3 -3
package/dist-cjs/index.js
CHANGED
|
@@ -14,9 +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);
|
|
22
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,14 +162,24 @@ 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) {
|
|
@@ -177,8 +187,7 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
177
187
|
}
|
|
178
188
|
// A delayed command remains in its ordered lane until its grace period has
|
|
179
189
|
// elapsed. This makes cancellation survive UI unmounts and app restarts.
|
|
180
|
-
if (pending.notBefore &&
|
|
181
|
-
Date.parse(pending.notBefore) > Date.parse(now())) {
|
|
190
|
+
if (pending.notBefore && Date.parse(pending.notBefore) > Date.parse(now())) {
|
|
182
191
|
return { status: "idle" };
|
|
183
192
|
}
|
|
184
193
|
const claimedSnapshotId = createPendingWriteSnapshotId();
|
|
@@ -193,7 +202,7 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
193
202
|
return { status: "superseded" };
|
|
194
203
|
}
|
|
195
204
|
try {
|
|
196
|
-
await sync(pending.payload);
|
|
205
|
+
await sync(pending.payload, { recordId: pending.id });
|
|
197
206
|
}
|
|
198
207
|
catch (error) {
|
|
199
208
|
const message = error instanceof Error ? error.message : "Unable to sync pending write.";
|
|
@@ -222,7 +231,7 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
222
231
|
* same store/scope. Callers own the selection predicate and idempotent remote
|
|
223
232
|
* mutation semantics.
|
|
224
233
|
*/
|
|
225
|
-
function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
|
|
234
|
+
function syncPendingWrites({ now, scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
|
|
226
235
|
let inFlightByScope = pendingWriteSyncs.get(store);
|
|
227
236
|
if (!inFlightByScope) {
|
|
228
237
|
inFlightByScope = new Map();
|
|
@@ -233,6 +242,7 @@ function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailur
|
|
|
233
242
|
return existing;
|
|
234
243
|
}
|
|
235
244
|
const syncRun = performPendingWriteSync({
|
|
245
|
+
now,
|
|
236
246
|
select,
|
|
237
247
|
stopOnFailure,
|
|
238
248
|
store,
|
|
@@ -279,7 +289,7 @@ function summarizePendingWrites(records) {
|
|
|
279
289
|
}, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
|
|
280
290
|
}
|
|
281
291
|
const pendingWriteSyncs = new WeakMap();
|
|
282
|
-
async function performPendingWriteSync({ select, stopOnFailure, store, sync, }) {
|
|
292
|
+
async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
|
|
283
293
|
const summary = {
|
|
284
294
|
attempted: 0,
|
|
285
295
|
failed: 0,
|
|
@@ -292,6 +302,7 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
|
|
|
292
302
|
for (const record of records) {
|
|
293
303
|
const result = await syncPendingWrite({
|
|
294
304
|
id: record.id,
|
|
305
|
+
now,
|
|
295
306
|
store,
|
|
296
307
|
sync,
|
|
297
308
|
});
|
|
@@ -327,7 +338,7 @@ function isPendingWriteSyncClaimAvailable(record, now) {
|
|
|
327
338
|
* workspace. Use its `recover` method for background replay and `run` for new
|
|
328
339
|
* writes so a newer remote mutation cannot overtake an older queued mutation.
|
|
329
340
|
*/
|
|
330
|
-
function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => true, store, sync, }) {
|
|
341
|
+
function createOrderedPendingWriteExecutor({ now, scopeKey = "all", select = () => true, store, sync, }) {
|
|
331
342
|
let execution = Promise.resolve();
|
|
332
343
|
function inOrder(operation) {
|
|
333
344
|
const result = execution.then(operation, operation);
|
|
@@ -336,6 +347,7 @@ function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => tr
|
|
|
336
347
|
}
|
|
337
348
|
function recover() {
|
|
338
349
|
return inOrder(() => syncPendingWrites({
|
|
350
|
+
now,
|
|
339
351
|
scopeKey,
|
|
340
352
|
select,
|
|
341
353
|
stopOnFailure: true,
|
|
@@ -351,6 +363,7 @@ function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => tr
|
|
|
351
363
|
return runPendingWrite(options);
|
|
352
364
|
}
|
|
353
365
|
const replay = await syncPendingWrites({
|
|
366
|
+
now,
|
|
354
367
|
scopeKey,
|
|
355
368
|
select,
|
|
356
369
|
stopOnFailure: true,
|
|
@@ -368,7 +381,8 @@ function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => tr
|
|
|
368
381
|
},
|
|
369
382
|
};
|
|
370
383
|
}
|
|
371
|
-
function
|
|
384
|
+
function createUnavailablePendingWriteDriver() {
|
|
385
|
+
const unavailable = () => new idbDriver_1.OfflineStorageError("IndexedDB is unavailable; the pending write was not stored.");
|
|
372
386
|
return {
|
|
373
387
|
async clear() { },
|
|
374
388
|
async clearIf() {
|
|
@@ -382,9 +396,11 @@ function createNoopPendingWriteDriver() {
|
|
|
382
396
|
async list() {
|
|
383
397
|
return [];
|
|
384
398
|
},
|
|
385
|
-
async set() {
|
|
399
|
+
async set() {
|
|
400
|
+
throw unavailable();
|
|
401
|
+
},
|
|
386
402
|
async setIf() {
|
|
387
|
-
|
|
403
|
+
throw unavailable();
|
|
388
404
|
},
|
|
389
405
|
};
|
|
390
406
|
}
|
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
|
}
|