Skip to content

Commit 8bc7cd4

Browse files
leshniakclaude
andcommitted
fix: improve diagnostic logging for all heal and error paths
- Heal attempts: logAlert with error type, action taken, remaining budget - Heal success: logInfo confirming recovery after each error type - Budget exhaustion: explicit logAlert when heal budget drains - Non-recoverable errors: logAlert with error details - Updated test assertions to match new log messages and levels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1f14374 commit 8bc7cd4

2 files changed

Lines changed: 46 additions & 20 deletions

File tree

lib/storage/providers/IDBKeyValProvider/createStore.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ function createStore(dbName: string, storeName: string): UseStore {
140140
.then(resetHealBudget)
141141
.catch((error) => {
142142
if (isInvalidStateError(error)) {
143-
Logger.logAlert('IDB InvalidStateError, retrying with fresh connection', {
143+
Logger.logAlert('IDB InvalidStateError — dropping cached connection and retrying', {
144144
dbName,
145145
storeName,
146146
txMode,
@@ -152,15 +152,30 @@ function createStore(dbName: string, storeName: string): UseStore {
152152

153153
if (isBudgetedHealError(error) && healAttemptsRemaining > 0) {
154154
healAttemptsRemaining--;
155-
Logger.logInfo(`IDB heal: ${getBudgetedHealErrorLabel(error)} error, attempting drop cached connection and reopen`, {
155+
const label = getBudgetedHealErrorLabel(error);
156+
Logger.logAlert(`IDB heal: ${label} error detected — dropping cached connection and reopening (${healAttemptsRemaining} attempts left)`, {
156157
dbName,
157158
storeName,
158-
healAttemptsRemaining,
159159
});
160160
dbp = undefined;
161-
return executeTransaction(txMode, callback).then(resetHealBudget);
161+
return executeTransaction(txMode, callback).then((result) => {
162+
Logger.logInfo(`IDB heal: successfully recovered after ${label} error`, {dbName, storeName});
163+
return resetHealBudget(result);
164+
});
162165
}
163166

167+
if (isBudgetedHealError(error)) {
168+
Logger.logAlert(`IDB heal: ${getBudgetedHealErrorLabel(error)} error — heal budget exhausted, giving up`, {
169+
dbName,
170+
storeName,
171+
});
172+
} else {
173+
Logger.logAlert('IDB error is not recoverable, giving up', {
174+
dbName,
175+
storeName,
176+
errorMessage: error instanceof Error ? error.message : String(error),
177+
});
178+
}
164179
throw error;
165180
});
166181
}

tests/unit/storage/providers/createStoreTest.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ describe('createStore', () => {
9696

9797
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow(DOMException);
9898
expect(callCount).toBe(1);
99-
expect(logAlertSpy).not.toHaveBeenCalled();
99+
expect(logAlertSpy).toHaveBeenCalledWith('IDB error is not recoverable, giving up', expect.objectContaining({errorMessage: 'Not found'}));
100+
expect(logAlertSpy).not.toHaveBeenCalledWith(expect.stringContaining('dropping cached connection'), expect.anything());
100101
});
101102

102103
it('should not retry on non-DOMException errors', async () => {
@@ -115,7 +116,8 @@ describe('createStore', () => {
115116

116117
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow(TypeError);
117118
expect(callCount).toBe(1);
118-
expect(logAlertSpy).not.toHaveBeenCalled();
119+
expect(logAlertSpy).toHaveBeenCalledWith('IDB error is not recoverable, giving up', expect.objectContaining({errorMessage: 'Something went wrong'}));
120+
expect(logAlertSpy).not.toHaveBeenCalledWith(expect.stringContaining('dropping cached connection'), expect.anything());
119121
});
120122

121123
it('should preserve data integrity after a successful retry', async () => {
@@ -174,7 +176,7 @@ describe('createStore', () => {
174176
return IDB.promisifyRequest(s.transaction);
175177
});
176178

177-
expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', {
179+
expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError — dropping cached connection and retrying', {
178180
dbName,
179181
storeName: STORE_NAME,
180182
txMode: 'readwrite',
@@ -275,7 +277,8 @@ describe('createStore', () => {
275277
const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1')));
276278
expect(result).toBe('value');
277279
expect(callCount).toBe(2);
278-
expect(logInfoSpy).toHaveBeenCalledWith('IDB heal: backing store error, attempting drop cached connection and reopen', expect.objectContaining({healAttemptsRemaining: 2}));
280+
expect(logAlertSpy).toHaveBeenCalledWith('IDB heal: backing store error detected — dropping cached connection and reopening (2 attempts left)', expect.objectContaining({dbName: expect.any(String)}));
281+
expect(logInfoSpy).toHaveBeenCalledWith('IDB heal: successfully recovered after backing store error', expect.objectContaining({dbName: expect.any(String)}));
279282
});
280283

281284
it('should heal on init when indexedDB.open() rejects with UnknownError', async () => {
@@ -337,10 +340,11 @@ describe('createStore', () => {
337340
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow('Internal error opening backing store');
338341
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow('Internal error opening backing store');
339342

340-
// Budget exhausted — 4th call should NOT attempt healing
341-
logInfoSpy.mockClear();
343+
// Budget exhausted — 4th call should NOT attempt healing, but should log budget exhausted
344+
logAlertSpy.mockClear();
342345
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow('Internal error opening backing store');
343-
expect(logInfoSpy).not.toHaveBeenCalledWith(expect.stringContaining('IDB heal'), expect.anything());
346+
expect(logAlertSpy).toHaveBeenCalledWith(expect.stringContaining('heal budget exhausted'), expect.anything());
347+
expect(logAlertSpy).not.toHaveBeenCalledWith(expect.stringContaining('dropping cached connection and reopening'), expect.anything());
344348
});
345349

346350
it('should reset heal budget after a successful operation', async () => {
@@ -404,15 +408,16 @@ describe('createStore', () => {
404408
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow('Some other unknown error');
405409

406410
jest.restoreAllMocks();
407-
logInfoSpy = jest.spyOn(Logger, 'logInfo');
411+
logAlertSpy = jest.spyOn(Logger, 'logAlert');
408412

409413
// QuotaExceededError
410414
jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(() => {
411415
throw new DOMException('Quota exceeded', 'QuotaExceededError');
412416
});
413417
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow('Quota exceeded');
414418

415-
expect(logInfoSpy).not.toHaveBeenCalledWith(expect.stringContaining('IDB heal'), expect.anything());
419+
expect(logAlertSpy).not.toHaveBeenCalledWith(expect.stringContaining('dropping cached connection and reopening'), expect.anything());
420+
expect(logAlertSpy).toHaveBeenCalledWith('IDB error is not recoverable, giving up', expect.objectContaining({errorMessage: 'Quota exceeded'}));
416421
});
417422
});
418423

@@ -442,9 +447,13 @@ describe('createStore', () => {
442447
const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1')));
443448
expect(result).toBe('value');
444449
expect(callCount).toBe(2);
450+
expect(logAlertSpy).toHaveBeenCalledWith(
451+
expect.stringContaining('connection lost error detected — dropping cached connection and reopening'),
452+
expect.objectContaining({dbName: expect.any(String)}),
453+
);
445454
expect(logInfoSpy).toHaveBeenCalledWith(
446-
'IDB heal: connection lost error, attempting drop cached connection and reopen',
447-
expect.objectContaining({healAttemptsRemaining: expect.any(Number)}),
455+
'IDB heal: successfully recovered after connection lost error',
456+
expect.objectContaining({dbName: expect.any(String)}),
448457
);
449458
});
450459

@@ -472,10 +481,11 @@ describe('createStore', () => {
472481
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow('Connection to Indexed Database server lost');
473482
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow('Connection to Indexed Database server lost');
474483

475-
// Budget exhausted — 4th call should NOT attempt healing
476-
logInfoSpy.mockClear();
484+
// Budget exhausted — 4th call should NOT attempt healing, but should log budget exhausted
485+
logAlertSpy.mockClear();
477486
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow('Connection to Indexed Database server lost');
478-
expect(logInfoSpy).not.toHaveBeenCalledWith(expect.stringContaining('IDB heal'), expect.anything());
487+
expect(logAlertSpy).toHaveBeenCalledWith(expect.stringContaining('heal budget exhausted'), expect.anything());
488+
expect(logAlertSpy).not.toHaveBeenCalledWith(expect.stringContaining('dropping cached connection and reopening'), expect.anything());
479489
});
480490

481491
it('should also heal "connection is closing" variant', async () => {
@@ -537,9 +547,10 @@ describe('createStore', () => {
537547
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow();
538548

539549
// Budget exhausted — no more healing for either error type
540-
logInfoSpy.mockClear();
550+
logAlertSpy.mockClear();
541551
await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('k')))).rejects.toThrow();
542-
expect(logInfoSpy).not.toHaveBeenCalledWith(expect.stringContaining('IDB heal'), expect.anything());
552+
expect(logAlertSpy).toHaveBeenCalledWith(expect.stringContaining('heal budget exhausted'), expect.anything());
553+
expect(logAlertSpy).not.toHaveBeenCalledWith(expect.stringContaining('dropping cached connection and reopening'), expect.anything());
543554
});
544555
});
545556
});

0 commit comments

Comments
 (0)