Skip to content

Commit 7ac9c50

Browse files
elirangoshenclaude
andcommitted
fix: handle probe promise rejection to avoid unhandled rejection
The visibilitychange probe adds a separate .then branch off the cached open promise. If the tab becomes visible while dbp is still a pending indexedDB.open() that later rejects, that branch rejected with no handler, surfacing an unhandled rejection on foregrounding. Add a .catch on the probe chain (cacheOpenPromise already clears dbp on its own branch). Addresses PR Expensify#788 review (chatgpt-codex-connector). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e3a8cc5 commit 7ac9c50

2 files changed

Lines changed: 68 additions & 18 deletions

File tree

lib/storage/providers/IDBKeyValProvider/createStore.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -157,24 +157,29 @@ function createStore(dbName: string, storeName: string): UseStore {
157157
dbp = undefined;
158158
};
159159

160-
probePromise.then((db) => {
161-
if (dbp !== probePromise) {
162-
return;
163-
}
164-
try {
165-
const tx = db.transaction(storeName, 'readonly');
166-
const probeStore = tx.objectStore(storeName);
167-
const req = probeStore.count();
168-
req.onsuccess = () => {
169-
Logger.logInfo('IDB visibilitychange probe: connection is healthy', {dbName, storeName});
170-
};
171-
req.onerror = () => {
172-
dropCacheIfStale(req.error);
173-
};
174-
} catch (error) {
175-
dropCacheIfStale(error);
176-
}
177-
});
160+
probePromise
161+
.then((db) => {
162+
if (dbp !== probePromise) {
163+
return;
164+
}
165+
try {
166+
const tx = db.transaction(storeName, 'readonly');
167+
const probeStore = tx.objectStore(storeName);
168+
const req = probeStore.count();
169+
req.onsuccess = () => {
170+
Logger.logInfo('IDB visibilitychange probe: connection is healthy', {dbName, storeName});
171+
};
172+
req.onerror = () => {
173+
dropCacheIfStale(req.error);
174+
};
175+
} catch (error) {
176+
dropCacheIfStale(error);
177+
}
178+
})
179+
.catch(() => {
180+
// The cached open promise rejected; cacheOpenPromise already cleared dbp on its own
181+
// branch. Swallow here so the probe's separate branch doesn't surface an unhandled rejection.
182+
});
178183
});
179184

180185
// Handles three recoverable error classes:

tests/unit/storage/providers/createStoreTest.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,5 +666,50 @@ describe('createStore', () => {
666666
expect(result).toBe('value');
667667
expect(logAlertSpy).toHaveBeenCalledWith(expect.stringContaining('IDB visibilitychange probe: stale connection detected'), expect.objectContaining({dbName: expect.any(String)}));
668668
});
669+
670+
it('should not surface an unhandled rejection when the probe runs while the open promise rejects', async () => {
671+
const store = createStore(uniqueDBName(), STORE_NAME);
672+
673+
// Keep the initial open pending until we reject it manually, so the probe attaches to a pending dbp.
674+
let rejectOpen: () => void = () => {
675+
/* assigned inside the indexedDB.open mock below */
676+
};
677+
jest.spyOn(indexedDB, 'open').mockImplementation(() => {
678+
const req = {} as IDBOpenDBRequest;
679+
rejectOpen = () => {
680+
Object.defineProperty(req, 'error', {value: new DOMException('probe open failed', 'AbortError'), configurable: true});
681+
req.onerror?.(new Event('error') as Event & {target: IDBOpenDBRequest});
682+
};
683+
return req;
684+
});
685+
686+
const unhandled = jest.fn();
687+
process.on('unhandledRejection', unhandled);
688+
689+
try {
690+
// Start an op so dbp becomes a pending open promise; keep its rejection handled.
691+
const opAssertion = expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow('probe open failed');
692+
693+
// Tab becomes visible while the open is still pending — probe attaches to the pending dbp.
694+
simulateVisibilityChange('hidden');
695+
simulateVisibilityChange('visible');
696+
697+
// Now the open rejects — both the op chain and the probe chain see it.
698+
rejectOpen();
699+
700+
await opAssertion;
701+
// Give the probe's separate branch a couple of ticks to (not) surface an unhandled rejection.
702+
await new Promise((resolve) => {
703+
setTimeout(resolve, 0);
704+
});
705+
await new Promise((resolve) => {
706+
setTimeout(resolve, 0);
707+
});
708+
709+
expect(unhandled).not.toHaveBeenCalled();
710+
} finally {
711+
process.off('unhandledRejection', unhandled);
712+
}
713+
});
669714
});
670715
});

0 commit comments

Comments
 (0)