@bayonai/offline 0.1.0 → 0.3.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/readCache.js CHANGED
@@ -1,16 +1,38 @@
1
1
  import { createIdbKeyValueDriver, } from "./idbDriver.js";
2
2
  export const readCacheSchemaVersion = 1;
3
3
  export const readCacheFreshnessMaxAgeMs = 30_000;
4
+ export const durableReadCacheFreshnessMaxAgeMs = 60 * 60_000;
5
+ export const identityReadCacheFreshnessMaxAgeMs = 24 * 60 * 60_000;
4
6
  export const browserReadCacheOperationTimeoutMs = 2_000;
5
- const pendingFreshLoads = new Map();
7
+ const pendingFreshLoads = new WeakMap();
8
+ const cachePruners = new WeakMap();
6
9
  export function createReadCache(driver) {
7
- return {
10
+ let globalVersion = 0;
11
+ const keyVersions = new Map();
12
+ let operationQueue = Promise.resolve();
13
+ const pendingLoads = new Map();
14
+ function getVersion(key) {
15
+ return `${globalVersion}:${keyVersions.get(key) ?? 0}`;
16
+ }
17
+ function enqueue(operation) {
18
+ const next = operationQueue.then(operation, operation);
19
+ operationQueue = next.then(() => undefined, () => undefined);
20
+ return next;
21
+ }
22
+ const cache = {
8
23
  async clear() {
9
- pendingFreshLoads.clear();
10
- await driver.clear();
24
+ pendingLoads.clear();
25
+ globalVersion += 1;
26
+ keyVersions.clear();
27
+ await enqueue(() => driver.clear());
11
28
  },
12
29
  async close() {
13
- await driver.close?.();
30
+ await enqueue(async () => driver.close?.());
31
+ },
32
+ async delete(key) {
33
+ pendingLoads.delete(key);
34
+ keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
35
+ await enqueue(() => driver.delete(key));
14
36
  },
15
37
  async get(key) {
16
38
  const envelope = toReadCacheEnvelope(await driver.get(key));
@@ -22,14 +44,48 @@ export function createReadCache(driver) {
22
44
  }
23
45
  : null;
24
46
  },
47
+ getVersion,
25
48
  async set(key, value) {
26
- await driver.set(key, {
49
+ await enqueue(() => driver.set(key, {
27
50
  savedAt: new Date().toISOString(),
28
51
  schemaVersion: readCacheSchemaVersion,
29
52
  value,
53
+ }));
54
+ },
55
+ async setIfVersion(key, value, version) {
56
+ if (getVersion(key) !== version) {
57
+ return false;
58
+ }
59
+ return enqueue(async () => {
60
+ if (getVersion(key) !== version) {
61
+ return false;
62
+ }
63
+ await driver.set(key, {
64
+ savedAt: new Date().toISOString(),
65
+ schemaVersion: readCacheSchemaVersion,
66
+ value,
67
+ });
68
+ return true;
30
69
  });
31
70
  },
32
71
  };
72
+ pendingFreshLoads.set(cache, pendingLoads);
73
+ cachePruners.set(cache, async ({ maxAgeMs, now }) => {
74
+ if (!driver.listEntries || maxAgeMs < 0) {
75
+ return 0;
76
+ }
77
+ const cutoff = now() - maxAgeMs;
78
+ const expiredKeys = (await driver.listEntries())
79
+ .filter(({ value }) => {
80
+ const envelope = toReadCacheEnvelope(value);
81
+ const savedAt = envelope ? Date.parse(envelope.savedAt) : Number.NaN;
82
+ return Number.isFinite(savedAt) && savedAt < cutoff;
83
+ })
84
+ .map(({ key }) => key);
85
+ await Promise.all(expiredKeys.map((key) => cache.delete(key)));
86
+ return expiredKeys.length;
87
+ });
88
+ return cache;
33
89
  }
34
90
  export function createMemoryReadCacheDriver() {
35
91
  const records = new Map();
@@ -38,9 +94,15 @@ export function createMemoryReadCacheDriver() {
38
94
  records.clear();
39
95
  },
40
96
  async close() { },
97
+ async delete(key) {
98
+ records.delete(key);
99
+ },
41
100
  async get(key) {
42
101
  return records.get(key) ?? null;
43
102
  },
103
+ async listEntries() {
104
+ return [...records.entries()].map(([key, value]) => ({ key, value }));
105
+ },
44
106
  async set(key, value) {
45
107
  records.set(key, value);
46
108
  },
@@ -54,7 +116,25 @@ export function createBrowserReadCache({ databaseName, databaseVersion, indexedD
54
116
  operationTimeoutMs,
55
117
  storeName,
56
118
  });
57
- return createReadCache(driver ?? createNoopReadCacheDriver());
119
+ return createReadCache(driver
120
+ ? {
121
+ clear: driver.clear,
122
+ close: driver.close,
123
+ delete: driver.delete,
124
+ get: driver.get,
125
+ listEntries: driver.listEntries,
126
+ set: driver.set,
127
+ }
128
+ : createNoopReadCacheDriver());
129
+ }
130
+ /**
131
+ * Deletes only valid read-cache envelopes older than the supplied age. It is
132
+ * opt-in and returns zero for custom cache adapters that cannot enumerate keys.
133
+ */
134
+ export function pruneReadCache({ cache, maxAgeMs, now = () => Date.now(), }) {
135
+ const resourceCache = cache.capture?.() ?? cache;
136
+ return cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ??
137
+ Promise.resolve(0);
58
138
  }
59
139
  export function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
60
140
  const serializedParams = Object.entries(params)
@@ -71,11 +151,26 @@ export function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
71
151
  .filter(Boolean)
72
152
  .join(":");
73
153
  }
74
- export async function readThroughCache({ cache, key, loadFresh, }) {
75
- const cached = await cache.get(key);
154
+ export async function readThroughCache({ cache, forceFresh = false, key, loadFresh, maxAgeMs, }) {
155
+ const resourceCache = cache.capture?.() ?? cache;
156
+ if (forceFresh) {
157
+ // Deletion advances the cache version, so a request started before this
158
+ // post-write refresh cannot restore stale data after it finishes.
159
+ await resourceCache.delete(key);
160
+ }
161
+ const version = resourceCache.getVersion(key);
162
+ const cached = await resourceCache.get(key);
163
+ if (cached && isCacheFresh(cached.savedAt, maxAgeMs)) {
164
+ return {
165
+ data: cached.value,
166
+ error: null,
167
+ fromCache: true,
168
+ stale: false,
169
+ };
170
+ }
76
171
  try {
77
- const fresh = await loadFresh();
78
- await cache.set(key, fresh);
172
+ const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
173
+ await resourceCache.setIfVersion(key, fresh, version);
79
174
  return {
80
175
  data: fresh,
81
176
  error: null,
@@ -95,8 +190,10 @@ export async function readThroughCache({ cache, key, loadFresh, }) {
95
190
  throw error;
96
191
  }
97
192
  }
98
- export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onUpdate, }) {
99
- const cached = await cache.get(key);
193
+ export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache = false, }) {
194
+ const resourceCache = cache.capture?.() ?? cache;
195
+ const version = resourceCache.getVersion(key);
196
+ const cached = skipCache ? null : await resourceCache.get(key);
100
197
  if (cached) {
101
198
  onUpdate({ data: cached.value, fromCache: true });
102
199
  if (isCacheFresh(cached.savedAt, maxAgeMs)) {
@@ -104,9 +201,16 @@ export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, o
104
201
  }
105
202
  }
106
203
  try {
107
- const fresh = await loadSharedFreshResource(key, loadFresh);
204
+ const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
108
205
  onUpdate({ data: fresh, fromCache: false });
109
- void cache.set(key, fresh).catch(() => undefined);
206
+ void resourceCache.setIfVersion(key, fresh, version).catch((error) => {
207
+ const normalizedError = normalizeError(error);
208
+ console.error("[Bayon offline] Unable to refresh the read cache.", {
209
+ error: normalizedError,
210
+ key,
211
+ });
212
+ onCacheWriteError?.(normalizedError);
213
+ });
110
214
  return fresh;
111
215
  }
112
216
  catch (error) {
@@ -125,9 +229,13 @@ function createNoopReadCacheDriver() {
125
229
  return {
126
230
  async clear() { },
127
231
  async close() { },
232
+ async delete() { },
128
233
  async get() {
129
234
  return null;
130
235
  },
236
+ async listEntries() {
237
+ return [];
238
+ },
131
239
  async set() { },
132
240
  };
133
241
  }
@@ -138,22 +246,32 @@ function isCacheFresh(savedAt, maxAgeMs) {
138
246
  const savedTime = Date.parse(savedAt);
139
247
  return Number.isFinite(savedTime) && Date.now() - savedTime <= maxAgeMs;
140
248
  }
141
- async function loadSharedFreshResource(key, loadFresh) {
142
- const pendingLoad = pendingFreshLoads.get(key);
249
+ async function loadSharedFreshResource(cache, key, loadFresh) {
250
+ const pendingLoads = getPendingFreshLoads(cache);
251
+ const pendingLoad = pendingLoads.get(key);
143
252
  if (pendingLoad) {
144
253
  return (await pendingLoad);
145
254
  }
146
255
  const nextLoad = loadFresh();
147
- pendingFreshLoads.set(key, nextLoad);
256
+ pendingLoads.set(key, nextLoad);
148
257
  try {
149
258
  return await nextLoad;
150
259
  }
151
260
  finally {
152
- if (pendingFreshLoads.get(key) === nextLoad) {
153
- pendingFreshLoads.delete(key);
261
+ if (pendingLoads.get(key) === nextLoad) {
262
+ pendingLoads.delete(key);
154
263
  }
155
264
  }
156
265
  }
266
+ function getPendingFreshLoads(cache) {
267
+ const existing = pendingFreshLoads.get(cache);
268
+ if (existing) {
269
+ return existing;
270
+ }
271
+ const next = new Map();
272
+ pendingFreshLoads.set(cache, next);
273
+ return next;
274
+ }
157
275
  function toReadCacheEnvelope(value) {
158
276
  if (!value || typeof value !== "object") {
159
277
  return null;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBrowserPendingWriteRecoveryCoordination = createBrowserPendingWriteRecoveryCoordination;
4
+ function createBrowserPendingWriteRecoveryCoordination({ channelName, leaseName, channel = createDefaultChannel(channelName), locks = readDefaultLockManager(), }) {
5
+ const listeners = new Set();
6
+ const handleMessage = () => {
7
+ for (const listener of listeners) {
8
+ try {
9
+ listener();
10
+ }
11
+ catch (error) {
12
+ console.error("[Bayon offline] Cross-tab recovery listener failed.", {
13
+ channelName,
14
+ error,
15
+ });
16
+ }
17
+ }
18
+ };
19
+ channel?.addEventListener("message", handleMessage);
20
+ return {
21
+ close() {
22
+ channel?.removeEventListener("message", handleMessage);
23
+ channel?.close();
24
+ listeners.clear();
25
+ },
26
+ notify() {
27
+ try {
28
+ channel?.postMessage("pending-write-recovery");
29
+ }
30
+ catch (error) {
31
+ console.error("[Bayon offline] Unable to wake cross-tab recovery.", {
32
+ channelName,
33
+ error,
34
+ });
35
+ }
36
+ },
37
+ async runExclusive(attempt) {
38
+ if (!locks) {
39
+ return attempt();
40
+ }
41
+ return locks.request(leaseName, { ifAvailable: true }, (lock) => lock ? attempt() : Promise.resolve(null));
42
+ },
43
+ subscribe(listener) {
44
+ listeners.add(listener);
45
+ return () => listeners.delete(listener);
46
+ },
47
+ };
48
+ }
49
+ function createDefaultChannel(name) {
50
+ if (typeof BroadcastChannel === "undefined") {
51
+ return null;
52
+ }
53
+ return new BroadcastChannel(name);
54
+ }
55
+ function readDefaultLockManager() {
56
+ if (typeof navigator === "undefined") {
57
+ return null;
58
+ }
59
+ return navigator.locks ?? null;
60
+ }
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.maxPendingWriteRetryDelayMs = exports.initialPendingWriteRetryDelayMs = void 0;
4
+ exports.createBrowserPendingWriteRecovery = createBrowserPendingWriteRecovery;
5
+ const pendingWrites_1 = require("./pendingWrites");
6
+ exports.initialPendingWriteRetryDelayMs = 2_000;
7
+ exports.maxPendingWriteRetryDelayMs = 60_000;
8
+ function createBrowserPendingWriteRecovery({ environment = createDefaultBrowserRecoveryEnvironment(), coordination, initialRetryDelayMs = exports.initialPendingWriteRetryDelayMs, maxRetryDelayMs = exports.maxPendingWriteRetryDelayMs, onError, onResult, scopeKey = "all", select = () => true, store, sync, }) {
9
+ let active = false;
10
+ let retryDelayMs = Math.max(0, initialRetryDelayMs);
11
+ let retryTimer;
12
+ let recoveryPromise;
13
+ let immediateRecoveryRequested = false;
14
+ let unsubscribePeerWakeups;
15
+ let unsubscribe;
16
+ function clearRetryTimer() {
17
+ if (retryTimer) {
18
+ environment.clearTimeout(retryTimer);
19
+ }
20
+ retryTimer = undefined;
21
+ }
22
+ function emitResult(result) {
23
+ try {
24
+ onResult?.(result);
25
+ }
26
+ catch (error) {
27
+ console.error("[Bayon offline] Pending-write recovery result listener failed.", {
28
+ error,
29
+ });
30
+ }
31
+ }
32
+ function emitError(error) {
33
+ console.error("[Bayon offline] Pending-write recovery failed.", {
34
+ error,
35
+ scopeKey,
36
+ });
37
+ try {
38
+ onError?.(error);
39
+ }
40
+ catch (callbackError) {
41
+ console.error("[Bayon offline] Pending-write recovery error listener failed.", {
42
+ callbackError,
43
+ scopeKey,
44
+ });
45
+ }
46
+ }
47
+ function scheduleRetry() {
48
+ if (!active || !environment.isOnline() || retryTimer) {
49
+ return;
50
+ }
51
+ retryTimer = environment.setTimeout(() => {
52
+ retryTimer = undefined;
53
+ void recoverNow();
54
+ }, retryDelayMs);
55
+ retryDelayMs = Math.min(Math.max(initialRetryDelayMs, retryDelayMs * 2), Math.max(initialRetryDelayMs, maxRetryDelayMs));
56
+ }
57
+ function recoverNow() {
58
+ if (!environment.isOnline()) {
59
+ clearRetryTimer();
60
+ return Promise.resolve(null);
61
+ }
62
+ if (recoveryPromise) {
63
+ return recoveryPromise;
64
+ }
65
+ const attempt = (async () => {
66
+ try {
67
+ const run = () => (0, pendingWrites_1.syncPendingWrites)({
68
+ scopeKey,
69
+ select,
70
+ store,
71
+ sync,
72
+ });
73
+ const result = coordination
74
+ ? await coordination.runExclusive(run)
75
+ : await run();
76
+ if (!result) {
77
+ return null;
78
+ }
79
+ emitResult(result);
80
+ if (result.remaining > 0) {
81
+ scheduleRetry();
82
+ }
83
+ else {
84
+ retryDelayMs = Math.max(0, initialRetryDelayMs);
85
+ }
86
+ return result;
87
+ }
88
+ catch (error) {
89
+ emitError(error);
90
+ scheduleRetry();
91
+ return null;
92
+ }
93
+ })();
94
+ recoveryPromise = attempt.finally(() => {
95
+ recoveryPromise = undefined;
96
+ if (active && immediateRecoveryRequested) {
97
+ immediateRecoveryRequested = false;
98
+ triggerImmediateRecovery();
99
+ }
100
+ });
101
+ return recoveryPromise;
102
+ }
103
+ function triggerImmediateRecovery() {
104
+ if (!active) {
105
+ return;
106
+ }
107
+ immediateRecoveryRequested ||= Boolean(recoveryPromise);
108
+ clearRetryTimer();
109
+ retryDelayMs = Math.max(0, initialRetryDelayMs);
110
+ void recoverNow();
111
+ }
112
+ function handleOffline() {
113
+ clearRetryTimer();
114
+ }
115
+ function handleStoreChange(change) {
116
+ if (change.type === "set") {
117
+ coordination?.notify();
118
+ triggerImmediateRecovery();
119
+ }
120
+ }
121
+ return {
122
+ notifyQueued() {
123
+ coordination?.notify();
124
+ triggerImmediateRecovery();
125
+ },
126
+ recoverNow,
127
+ start() {
128
+ if (active) {
129
+ return;
130
+ }
131
+ active = true;
132
+ unsubscribe = store.subscribe(handleStoreChange);
133
+ unsubscribePeerWakeups = coordination?.subscribe(triggerImmediateRecovery);
134
+ environment.addEventListener("focus", triggerImmediateRecovery);
135
+ environment.addEventListener("online", triggerImmediateRecovery);
136
+ environment.addEventListener("offline", handleOffline);
137
+ if (environment.isOnline()) {
138
+ triggerImmediateRecovery();
139
+ }
140
+ },
141
+ stop() {
142
+ if (!active) {
143
+ return;
144
+ }
145
+ active = false;
146
+ clearRetryTimer();
147
+ unsubscribe?.();
148
+ unsubscribe = undefined;
149
+ unsubscribePeerWakeups?.();
150
+ unsubscribePeerWakeups = undefined;
151
+ environment.removeEventListener("focus", triggerImmediateRecovery);
152
+ environment.removeEventListener("online", triggerImmediateRecovery);
153
+ environment.removeEventListener("offline", handleOffline);
154
+ },
155
+ };
156
+ }
157
+ function createDefaultBrowserRecoveryEnvironment() {
158
+ const target = typeof window === "undefined" ? null : window;
159
+ return {
160
+ addEventListener(type, listener) {
161
+ target?.addEventListener(type, listener);
162
+ },
163
+ clearTimeout(timer) {
164
+ globalThis.clearTimeout(timer);
165
+ },
166
+ isOnline() {
167
+ return typeof navigator !== "undefined" && navigator.onLine;
168
+ },
169
+ removeEventListener(type, listener) {
170
+ target?.removeEventListener(type, listener);
171
+ },
172
+ setTimeout(callback, delayMs) {
173
+ return globalThis.setTimeout(callback, delayMs);
174
+ },
175
+ };
176
+ }
@@ -16,6 +16,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
16
16
  async clear() {
17
17
  await runBrowserStorageOperation({
18
18
  fallback: undefined,
19
+ operationName: "clear object store",
19
20
  operation: async () => {
20
21
  const database = await databasePromise;
21
22
  await database.clear(storeName);
@@ -26,6 +27,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
26
27
  async close() {
27
28
  await runBrowserStorageOperation({
28
29
  fallback: undefined,
30
+ operationName: "close database",
29
31
  operation: async () => {
30
32
  const database = await databasePromise;
31
33
  database.close();
@@ -36,6 +38,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
36
38
  async delete(key) {
37
39
  await runBrowserStorageOperation({
38
40
  fallback: undefined,
41
+ operationName: "delete key",
39
42
  operation: async () => {
40
43
  const database = await databasePromise;
41
44
  await database.delete(storeName, key);
@@ -43,9 +46,32 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
43
46
  timeoutMs: operationTimeoutMs,
44
47
  });
45
48
  },
49
+ async deleteIf(key, predicate) {
50
+ return runBrowserStorageOperation({
51
+ fallback: false,
52
+ operationName: "conditionally delete key",
53
+ operation: async () => {
54
+ const database = await databasePromise;
55
+ // The ownership check and deletion must share a transaction; splitting
56
+ // them can delete a newer offline snapshot written by another trigger.
57
+ const transaction = database.transaction(storeName, "readwrite");
58
+ const objectStore = transaction.objectStore(storeName);
59
+ const current = (await objectStore.get(key)) ?? null;
60
+ if (!predicate(current)) {
61
+ await transaction.done;
62
+ return false;
63
+ }
64
+ await objectStore.delete(key);
65
+ await transaction.done;
66
+ return true;
67
+ },
68
+ timeoutMs: operationTimeoutMs,
69
+ });
70
+ },
46
71
  async get(key) {
47
72
  return runBrowserStorageOperation({
48
73
  fallback: null,
74
+ operationName: "read key",
49
75
  operation: async () => {
50
76
  const database = await databasePromise;
51
77
  return (await database.get(storeName, key)) ?? null;
@@ -56,6 +82,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
56
82
  async list() {
57
83
  return runBrowserStorageOperation({
58
84
  fallback: [],
85
+ operationName: "list object store",
59
86
  operation: async () => {
60
87
  const database = await databasePromise;
61
88
  return database.getAll(storeName);
@@ -63,9 +90,34 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
63
90
  timeoutMs: operationTimeoutMs,
64
91
  });
65
92
  },
93
+ async listEntries() {
94
+ return runBrowserStorageOperation({
95
+ fallback: [],
96
+ operationName: "list object-store entries",
97
+ operation: async () => {
98
+ const database = await databasePromise;
99
+ // Keys and values must come from one transaction. Separate database
100
+ // reads could pair a key from one cross-tab storage state with a
101
+ // value from another, causing retention code to delete the wrong key.
102
+ const transaction = database.transaction(storeName, "readonly");
103
+ const objectStore = transaction.objectStore(storeName);
104
+ const [keys, values] = await Promise.all([
105
+ objectStore.getAllKeys(),
106
+ objectStore.getAll(),
107
+ ]);
108
+ await transaction.done;
109
+ return keys.map((key, index) => ({
110
+ key: String(key),
111
+ value: values[index],
112
+ }));
113
+ },
114
+ timeoutMs: operationTimeoutMs,
115
+ });
116
+ },
66
117
  async set(key, value) {
67
118
  await runBrowserStorageOperation({
68
119
  fallback: undefined,
120
+ operationName: "write key",
69
121
  operation: async () => {
70
122
  const database = await databasePromise;
71
123
  await database.put(storeName, value, key);
@@ -73,6 +125,26 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
73
125
  timeoutMs: operationTimeoutMs,
74
126
  });
75
127
  },
128
+ async setIf(key, value, predicate) {
129
+ return runBrowserStorageOperation({
130
+ fallback: false,
131
+ operationName: "conditionally replace key",
132
+ operation: async () => {
133
+ const database = await databasePromise;
134
+ const transaction = database.transaction(storeName, "readwrite");
135
+ const objectStore = transaction.objectStore(storeName);
136
+ const current = (await objectStore.get(key)) ?? null;
137
+ if (!predicate(current)) {
138
+ await transaction.done;
139
+ return false;
140
+ }
141
+ await objectStore.put(value, key);
142
+ await transaction.done;
143
+ return true;
144
+ },
145
+ timeoutMs: operationTimeoutMs,
146
+ });
147
+ },
76
148
  };
77
149
  }
78
150
  async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB, storeName, }) {
@@ -89,17 +161,23 @@ async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB,
89
161
  }
90
162
  return database;
91
163
  }
92
- async function runBrowserStorageOperation({ fallback, operation, timeoutMs, }) {
164
+ async function runBrowserStorageOperation({ fallback, operation, operationName, timeoutMs, }) {
93
165
  let timeoutId = null;
94
166
  try {
95
167
  return await Promise.race([
96
168
  operation(),
97
169
  new Promise((resolve) => {
98
- timeoutId = setTimeout(() => resolve(fallback), timeoutMs);
170
+ timeoutId = setTimeout(() => {
171
+ console.error(`IndexedDB timed out while trying to ${operationName}.`, {
172
+ timeoutMs,
173
+ });
174
+ resolve(fallback);
175
+ }, timeoutMs);
99
176
  }),
100
177
  ]);
101
178
  }
102
- catch {
179
+ catch (error) {
180
+ console.error(`IndexedDB failed to ${operationName}.`, { error });
103
181
  return fallback;
104
182
  }
105
183
  finally {
package/dist-cjs/index.js CHANGED
@@ -16,3 +16,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./readCache"), exports);
18
18
  __exportStar(require("./pendingWrites"), exports);
19
+ __exportStar(require("./browserRecovery"), exports);
20
+ __exportStar(require("./browserCoordination"), exports);