@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/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 ?? createNoopPendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
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
- return globalThis.crypto.randomUUID();
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 createNoopPendingWriteDriver() {
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
- return false;
403
+ throw unavailable();
388
404
  },
389
405
  };
390
406
  }
@@ -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
- exports.browserReadCacheOperationTimeoutMs = 2_000;
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
- await resourceCache.delete(key);
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
- await resourceCache.setIfVersion(key, fresh, version);
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.7.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": "^5",
60
- "vitest": "^4.1.6"
59
+ "typescript": "^6.0.3",
60
+ "vitest": "^5.0.1"
61
61
  }
62
62
  }