Skip to content

Commit a2c8ea7

Browse files
leshniakclaude
andcommitted
fix: guard stale dbp reject handler, document concurrent budget drain
- Capture dbp reference before attaching reject handler; only clear if dbp hasn't been replaced by a concurrent heal/retry (prevents stale rejection handler from clearing a newer promise) - Add comment documenting concurrent store() budget drain behavior - Fix test formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6569dc7 commit a2c8ea7

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

lib/storage/providers/IDBKeyValProvider/createStore.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ function createStore(dbName: string, storeName: string): UseStore {
4949
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
5050
dbp = IDB.promisifyRequest(request);
5151

52+
const currentPromise = dbp;
5253
dbp.then(attachHandlers, () => {
5354
// Clear the cached rejected promise so the next operation retries
5455
// with a fresh indexedDB.open() instead of returning the same rejection.
56+
// Guard: only clear if dbp hasn't been replaced by a concurrent heal/retry.
57+
if (dbp !== currentPromise) {
58+
return;
59+
}
5560
dbp = undefined;
5661
});
5762
return dbp;
@@ -80,7 +85,11 @@ function createStore(dbName: string, storeName: string): UseStore {
8085
};
8186

8287
dbp = IDB.promisifyRequest(request);
88+
const currentPromise = dbp;
8389
dbp.then(attachHandlers, () => {
90+
if (dbp !== currentPromise) {
91+
return;
92+
}
8493
dbp = undefined;
8594
});
8695
return dbp;
@@ -103,6 +112,9 @@ function createStore(dbName: string, storeName: string): UseStore {
103112
// 2. Backing store corruption (Chromium UnknownError) — close + reopen the IDB connection.
104113
// Bounded by a shared heal budget (3 attempts, reset on success).
105114
// Mirrors Dexie's PR1398_maxLoop pattern: https://github.com/dexie/Dexie.js/blob/master/src/functions/temp-transaction.ts
115+
// Note: concurrent store() calls share the budget. Under overlapping failures each caller
116+
// decrements independently, so the budget may drain faster than one-per-incident. This is
117+
// acceptable — same as Dexie's approach — and the budget resets on any success.
106118
return (txMode, callback) =>
107119
executeTransaction(txMode, callback)
108120
.then(resetHealBudget)

tests/unit/storage/providers/createStoreTest.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ describe('createStore', () => {
365365
spy.mockRestore();
366366
return original.apply(this, args);
367367
});
368-
await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); }
368+
await store('readonly', (s) => IDB.promisifyRequest(s.get('key1')));
369+
}
369370

370371
// Clean success — resets budget to 3
371372
jest.restoreAllMocks();
@@ -383,7 +384,8 @@ describe('createStore', () => {
383384
spy.mockRestore();
384385
return original.apply(this, args);
385386
});
386-
const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); expect(result).toBe('value');
387+
const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1')));
388+
expect(result).toBe('value');
387389
}
388390
});
389391

0 commit comments

Comments
 (0)