@fairfox/polly 0.60.0 → 0.62.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/src/mesh.js CHANGED
@@ -69,6 +69,90 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
69
69
  throw Error('Dynamic require of "' + x + '" is not supported');
70
70
  });
71
71
 
72
+ // src/shared/lib/idb-helpers.ts
73
+ function openIDB(options) {
74
+ return new Promise((resolve, reject) => {
75
+ const start = Date.now();
76
+ const request = indexedDB.open(options.name, options.version);
77
+ let settled = false;
78
+ const timer = setTimeout(() => {
79
+ if (settled)
80
+ return;
81
+ settled = true;
82
+ const elapsedMs = Date.now() - start;
83
+ reject(new Error(`Polly IndexedDB open of '${options.name}' timed out after ${elapsedMs}ms (cross-tab lock or zombie connection?)`));
84
+ }, IDB_OPEN_TIMEOUT_MS);
85
+ request.onerror = () => {
86
+ if (settled)
87
+ return;
88
+ settled = true;
89
+ clearTimeout(timer);
90
+ reject(request.error);
91
+ };
92
+ request.onsuccess = () => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ clearTimeout(timer);
97
+ resolve(request.result);
98
+ };
99
+ request.onupgradeneeded = (event) => {
100
+ const db = event.target.result;
101
+ options.upgrade(db, event);
102
+ };
103
+ });
104
+ }
105
+ function cachedOpen(ref, options) {
106
+ if (ref.promise)
107
+ return ref.promise;
108
+ const pending = openIDB(options);
109
+ pending.catch(() => {
110
+ if (ref.promise === pending)
111
+ ref.promise = null;
112
+ });
113
+ ref.promise = pending;
114
+ return pending;
115
+ }
116
+ function runRequest(request) {
117
+ return new Promise((resolve, reject) => {
118
+ request.onsuccess = () => resolve(request.result);
119
+ request.onerror = () => reject(request.error);
120
+ });
121
+ }
122
+ function runTx(db, storeName, mode, fn) {
123
+ return new Promise((resolve, reject) => {
124
+ const tx = db.transaction(storeName, mode);
125
+ const store = tx.objectStore(storeName);
126
+ tx.oncomplete = () => resolve();
127
+ tx.onerror = () => reject(tx.error);
128
+ tx.onabort = () => reject(tx.error);
129
+ try {
130
+ fn(store);
131
+ } catch (err) {
132
+ try {
133
+ tx.abort();
134
+ } catch {}
135
+ reject(err);
136
+ }
137
+ });
138
+ }
139
+ function iterateCursor(db, storeName, visit) {
140
+ return new Promise((resolve, reject) => {
141
+ const tx = db.transaction(storeName, "readonly");
142
+ const store = tx.objectStore(storeName);
143
+ const request = store.openCursor();
144
+ request.onsuccess = () => {
145
+ const cursor = request.result;
146
+ if (!cursor)
147
+ return resolve();
148
+ visit(cursor.key, cursor.value);
149
+ cursor.continue();
150
+ };
151
+ request.onerror = () => reject(request.error);
152
+ });
153
+ }
154
+ var IDB_OPEN_TIMEOUT_MS = 5000;
155
+
72
156
  // src/shared/lib/encryption.ts
73
157
  var exports_encryption = {};
74
158
  __export(exports_encryption, {
@@ -255,42 +339,28 @@ class IndexedDBBlobCache {
255
339
  static DB_NAME = "polly-blobs";
256
340
  static DB_VERSION = 1;
257
341
  static STORE_NAME = "blobs";
258
- dbPromise = null;
342
+ dbRef = { promise: null };
259
343
  urls = new Map;
260
344
  openDB() {
261
- if (this.dbPromise)
262
- return this.dbPromise;
263
- this.dbPromise = new Promise((resolve, reject) => {
264
- const request = indexedDB.open(IndexedDBBlobCache.DB_NAME, IndexedDBBlobCache.DB_VERSION);
265
- request.onerror = () => reject(request.error);
266
- request.onsuccess = () => resolve(request.result);
267
- request.onupgradeneeded = (event) => {
268
- const db = event.target.result;
345
+ return cachedOpen(this.dbRef, {
346
+ name: IndexedDBBlobCache.DB_NAME,
347
+ version: IndexedDBBlobCache.DB_VERSION,
348
+ upgrade: (db) => {
269
349
  if (!db.objectStoreNames.contains(IndexedDBBlobCache.STORE_NAME)) {
270
350
  db.createObjectStore(IndexedDBBlobCache.STORE_NAME);
271
351
  }
272
- };
352
+ }
273
353
  });
274
- return this.dbPromise;
275
354
  }
276
355
  async getRecord(hash) {
277
356
  const db = await this.openDB();
278
- return new Promise((resolve, reject) => {
279
- const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
280
- const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
281
- const request = store.get(hash);
282
- request.onsuccess = () => resolve(request.result);
283
- request.onerror = () => reject(request.error);
284
- });
357
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
358
+ return runRequest(tx.objectStore(IndexedDBBlobCache.STORE_NAME).get(hash));
285
359
  }
286
360
  async putRecord(hash, record) {
287
361
  const db = await this.openDB();
288
- return new Promise((resolve, reject) => {
289
- const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readwrite");
290
- const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
362
+ await runTx(db, IndexedDBBlobCache.STORE_NAME, "readwrite", (store) => {
291
363
  store.put(record, hash);
292
- tx.oncomplete = () => resolve();
293
- tx.onerror = () => reject(tx.error);
294
364
  });
295
365
  }
296
366
  async get(hash) {
@@ -311,13 +381,9 @@ class IndexedDBBlobCache {
311
381
  }
312
382
  async has(hash) {
313
383
  const db = await this.openDB();
314
- return new Promise((resolve, reject) => {
315
- const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
316
- const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
317
- const request = store.count(hash);
318
- request.onsuccess = () => resolve(request.result > 0);
319
- request.onerror = () => reject(request.error);
320
- });
384
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
385
+ const count = await runRequest(tx.objectStore(IndexedDBBlobCache.STORE_NAME).count(hash));
386
+ return count > 0;
321
387
  }
322
388
  async delete(hash) {
323
389
  const url = this.urls.get(hash);
@@ -326,12 +392,8 @@ class IndexedDBBlobCache {
326
392
  this.urls.delete(hash);
327
393
  }
328
394
  const db = await this.openDB();
329
- return new Promise((resolve, reject) => {
330
- const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readwrite");
331
- const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
395
+ await runTx(db, IndexedDBBlobCache.STORE_NAME, "readwrite", (store) => {
332
396
  store.delete(hash);
333
- tx.oncomplete = () => resolve();
334
- tx.onerror = () => reject(tx.error);
335
397
  });
336
398
  }
337
399
  async pin(hash) {
@@ -348,50 +410,25 @@ class IndexedDBBlobCache {
348
410
  }
349
411
  async size() {
350
412
  const db = await this.openDB();
351
- return new Promise((resolve, reject) => {
352
- const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
353
- const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
354
- const request = store.openCursor();
355
- let total = 0;
356
- request.onsuccess = () => {
357
- const cursor = request.result;
358
- if (cursor) {
359
- const value = cursor.value;
360
- total += value.size;
361
- cursor.continue();
362
- } else {
363
- resolve(total);
364
- }
365
- };
366
- request.onerror = () => reject(request.error);
413
+ let total = 0;
414
+ await iterateCursor(db, IndexedDBBlobCache.STORE_NAME, (_key, value) => {
415
+ total += value.size;
367
416
  });
417
+ return total;
368
418
  }
369
419
  async evict(maxBytes) {
370
420
  const db = await this.openDB();
371
421
  const candidates = [];
372
422
  let totalSize = 0;
373
- await new Promise((resolve, reject) => {
374
- const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
375
- const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
376
- const request = store.openCursor();
377
- request.onsuccess = () => {
378
- const cursor = request.result;
379
- if (cursor) {
380
- const value = cursor.value;
381
- totalSize += value.size;
382
- if (!value.pinned) {
383
- candidates.push({
384
- hash: cursor.key,
385
- accessedAt: value.accessedAt,
386
- size: value.size
387
- });
388
- }
389
- cursor.continue();
390
- } else {
391
- resolve();
392
- }
393
- };
394
- request.onerror = () => reject(request.error);
423
+ await iterateCursor(db, IndexedDBBlobCache.STORE_NAME, (key, value) => {
424
+ totalSize += value.size;
425
+ if (!value.pinned) {
426
+ candidates.push({
427
+ hash: key,
428
+ accessedAt: value.accessedAt,
429
+ size: value.size
430
+ });
431
+ }
395
432
  });
396
433
  if (totalSize <= maxBytes)
397
434
  return 0;
@@ -1783,6 +1820,7 @@ function resetMeshState() {
1783
1820
  lazyInvocations = 0;
1784
1821
  lazyReachedRepo = 0;
1785
1822
  lastLoadedRejection = undefined;
1823
+ lastStorageOpenError = undefined;
1786
1824
  lazyWrappers = [];
1787
1825
  }
1788
1826
  function isMeshStateConfigured() {
@@ -1813,6 +1851,47 @@ function recordLoadedRejection(thrown) {
1813
1851
  at: Date.now()
1814
1852
  };
1815
1853
  }
1854
+ var STORAGE_OP_TIMEOUT_MS = 5000;
1855
+ var lastStorageOpenError;
1856
+ function getStorageOpenError() {
1857
+ return lastStorageOpenError;
1858
+ }
1859
+ function recordStorageOpenError(error) {
1860
+ lastStorageOpenError = error;
1861
+ }
1862
+ async function withStorageTimeout(operation, documentId, promise, timeoutMs = STORAGE_OP_TIMEOUT_MS) {
1863
+ const start = Date.now();
1864
+ let timer;
1865
+ let timedOut = false;
1866
+ try {
1867
+ return await new Promise((resolve, reject) => {
1868
+ timer = setTimeout(() => {
1869
+ timedOut = true;
1870
+ const elapsedMs = Date.now() - start;
1871
+ const message = `Polly $meshState: storage operation '${operation}' on document '${documentId}' timed out after ${timeoutMs}ms`;
1872
+ recordStorageOpenError({
1873
+ operation,
1874
+ documentId,
1875
+ timeoutMs,
1876
+ elapsedMs,
1877
+ message,
1878
+ at: Date.now()
1879
+ });
1880
+ reject(new Error(message));
1881
+ }, timeoutMs);
1882
+ promise.then((value) => {
1883
+ if (!timedOut)
1884
+ resolve(value);
1885
+ }, (err) => {
1886
+ if (!timedOut)
1887
+ reject(err);
1888
+ });
1889
+ });
1890
+ } finally {
1891
+ if (timer !== undefined)
1892
+ clearTimeout(timer);
1893
+ }
1894
+ }
1816
1895
  var LAZY_WRAPPER_LOG_CAP = 64;
1817
1896
  var lazyWrappers = [];
1818
1897
  function getLazyWrappers() {
@@ -1857,14 +1936,16 @@ function buildHandleFactory(repo, key, initialDoc) {
1857
1936
  try {
1858
1937
  const cached = repo.handles[documentId];
1859
1938
  lazyReachedRepo++;
1939
+ const docIdString = documentId;
1860
1940
  if (cached) {
1861
- await cached.whenReady(["ready", "unavailable"]);
1941
+ await withStorageTimeout("whenReady", docIdString, cached.whenReady(["ready", "unavailable"]));
1862
1942
  if (cached.state === "ready") {
1863
1943
  exitReason = "returned-cached";
1864
1944
  return cached;
1865
1945
  }
1866
1946
  }
1867
- const stored = await repo.storageSubsystem?.loadDoc(documentId);
1947
+ const loadPromise = repo.storageSubsystem?.loadDoc(documentId);
1948
+ const stored = loadPromise ? await withStorageTimeout("loadDoc", docIdString, loadPromise) : undefined;
1868
1949
  if (stored) {
1869
1950
  exitReason = "loaded-from-storage";
1870
1951
  return repo.find(documentId, { allowableStates: ["ready"] });
@@ -2987,6 +3068,7 @@ function getReevaluateDocumentShare(repo) {
2987
3068
  return () => fn.call(sync);
2988
3069
  }
2989
3070
  function buildMeshStateModuleDiagnostics() {
3071
+ const lazyWrappers2 = getLazyWrappers();
2990
3072
  return {
2991
3073
  moduleId: getMeshStateModuleId(),
2992
3074
  configured: isMeshStateConfigured(),
@@ -2995,9 +3077,30 @@ function buildMeshStateModuleDiagnostics() {
2995
3077
  lazyInvocations: getLazyInvocations(),
2996
3078
  lazyReachedRepo: getLazyReachedRepo(),
2997
3079
  lastLoadedRejection: getLastLoadedRejection(),
2998
- lazyWrappers: getLazyWrappers()
3080
+ storageOpenError: getStorageOpenError(),
3081
+ lazyWrappers: lazyWrappers2,
3082
+ lazyWrapperDuplicateDocIds: findLazyWrapperDocIdDuplicates(lazyWrappers2)
2999
3083
  };
3000
3084
  }
3085
+ function findLazyWrapperDocIdDuplicates(records) {
3086
+ const byDocId = new Map;
3087
+ for (const record of records) {
3088
+ let entry = byDocId.get(record.docId);
3089
+ if (!entry) {
3090
+ entry = { keys: new Set, count: 0 };
3091
+ byDocId.set(record.docId, entry);
3092
+ }
3093
+ entry.keys.add(record.key);
3094
+ entry.count++;
3095
+ }
3096
+ const duplicates = [];
3097
+ for (const [docId, entry] of byDocId) {
3098
+ if (entry.count > 1) {
3099
+ duplicates.push({ docId, keys: [...entry.keys], recordCount: entry.count });
3100
+ }
3101
+ }
3102
+ return duplicates;
3103
+ }
3001
3104
  function installPolly107SyncReevaluation(networkAdapter, repo) {
3002
3105
  const disable = typeof process !== "undefined" && process.env?.["POLLY_107_DISABLE_FIX"] === "1";
3003
3106
  if (disable)
@@ -3507,6 +3610,7 @@ export {
3507
3610
  isPairingTokenExpired,
3508
3611
  isMeshStateConfigured,
3509
3612
  isBlobRef,
3613
+ getStorageOpenError,
3510
3614
  getMeshStateModuleId,
3511
3615
  getLazyWrappers,
3512
3616
  getLazyReachedRepo,
@@ -3562,4 +3666,4 @@ export {
3562
3666
  $meshCounter
3563
3667
  };
3564
3668
 
3565
- //# debugId=CC67EB2297E1036764756E2164756E21
3669
+ //# debugId=EFE12C3DC2653D4B64756E2164756E21