Skip to content

Commit a9013d0

Browse files
committed
fix(sync): rebuild the sync snapshot when its row deserialises to null
1 parent 07a1cd1 commit a9013d0

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

‎src/client/initMatrix.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,37 @@ describe('startupSyncStore', () => {
9898
expect(deleteDatabase).not.toHaveBeenCalled();
9999
});
100100

101+
it('recovers when the sync row comes back as a null value', async () => {
102+
const original = IDBObjectStore.prototype.openCursor;
103+
let reads = 0;
104+
vi.spyOn(IDBObjectStore.prototype, 'openCursor').mockImplementation(function (
105+
this: IDBObjectStore,
106+
...args
107+
) {
108+
if (this.name !== 'sync' || reads > 0) return original.apply(this, args);
109+
reads += 1;
110+
const request: IDBRequest<IDBCursorWithValue | null> = new IDBRequest();
111+
let cursor: unknown = {
112+
value: null,
113+
continue: () => {
114+
cursor = null;
115+
queueMicrotask(() => request.onsuccess?.(new Event('success')));
116+
},
117+
};
118+
Object.defineProperty(request, 'result', { get: () => cursor });
119+
queueMicrotask(() => request.onsuccess?.(new Event('success')));
120+
return request;
121+
});
122+
const deleteDatabase = vi.spyOn(indexedDB, 'deleteDatabase');
123+
124+
await expect(startupSyncStore(mx, dbName)).resolves.toBeUndefined();
125+
126+
expect(await mx.store.getSavedSync()).toBeNull();
127+
expect(await mx.store.getOldestToDeviceBatch()).toMatchObject({ txnId: 'pending-message' });
128+
expect(mx.getAccessToken()).toBe('session-token');
129+
expect(deleteDatabase).not.toHaveBeenCalled();
130+
});
131+
101132
it('keeps a readable sync snapshot', async () => {
102133
await startupSyncStore(mx, dbName);
103134
expect(await mx.store.getSavedSyncToken()).toBe('cached-token');

‎src/client/initMatrix.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,18 @@ type ClientInitializationResult =
456456
| { ok: true; mx: MatrixClient }
457457
| { ok: false; error: unknown; phase: 'sync_store' | 'rust_crypto' };
458458

459+
const isUnreadableSyncSnapshot = (error: unknown): boolean => {
460+
if (error instanceof TypeError) return error.message.includes('nextBatch');
461+
return error instanceof Error && error.message === 'selectQuery failed for sync';
462+
};
463+
459464
export const startupSyncStore = async (mx: MatrixClient, dbName: string): Promise<void> => {
460465
try {
461466
await mx.store.startup();
462467
return;
463468
} catch (error) {
464469
await mx.store.destroy();
465-
if (!(error instanceof Error) || error.message !== 'selectQuery failed for sync') throw error;
470+
if (!isUnreadableSyncSnapshot(error)) throw error;
466471
debugLog.warn('sync', 'Rebuilding unreadable sync snapshot', { error });
467472
}
468473

0 commit comments

Comments
 (0)