@@ -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' ) ;
0 commit comments