'use strict'; const {vm,assert,functionSource}=require('./helpers/app-source'); const initSource=functionSource('init'); const initialSource=functionSource('readInitialDataAsync'); assert(initialSource.includes("Object.prototype.hasOwnProperty.call(bundle,'snapshot')?bundle.snapshot:bundle"), 'A fresh database bundle with snapshot:null can be mistaken for game data'); assert(!initSource.includes('fullResync')&&!initSource.includes("cloudOutboxDeleteKeys.add('full-resync')"),'Retired full-resync migration remains in startup'); assert(initSource.includes('recoveredDeletionTombstones')&&initSource.includes('deletedBoardRevisions.set(id,tombstone.rev')&&initSource.includes('deletedBoardAuthors.set(id,tombstone.revAuthor'),'Recovered deletion tombstones are not restored into the durable deletion queue'); async function runUpgradeCase(oldVersion){ const worldWrites=[],deletedStores=[];let openedVersion=0; const genericStore=()=>({indexNames:{contains:()=>true},createIndex:()=>{},put:()=>{},clear:()=>{},get:()=>{const request={result:null};queueMicrotask(()=>request.onsuccess?.());return request}}), worldsStore={...genericStore(),put:row=>worldWrites.push(row)}, stores=new Proxy({worlds:worldsStore},{get:(target,name)=>target[name]||(target[name]=genericStore())}),db={objectStoreNames:{contains:()=>true},deleteObjectStore:name=>deletedStores.push(name),close(){}},request={result:db,transaction:{objectStore:name=>stores[name]}}; const context={ Promise,console:{warn:()=>{}},idbAvailable:true,idbHealth:'unverified',worldDbAbandoned:false,worldDbPromise:null,worldDbName:'test-world',IDB_LAYOUT_VERSION:8,FIELD_STORAGE_FORMAT:2,SAVE_SCHEMA:31,WORLD_GENERATION:'current-only-test',IDB_STARTUP_TIMEOUT:8000,cloudApiEnabled:false,cloudOutboxReady:true, indexedDB:{open(_name,version){openedVersion=version;queueMicrotask(()=>{request.onupgradeneeded?.({oldVersion});queueMicrotask(()=>request.onsuccess?.())});return request}}, validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),createWorldEpoch:()=> 'world:upgrade-test',rememberWorldEpoch:()=>true, defaultData:()=>({metas:{},states:{},worldEpoch:null}),globalForStorage:value=>({...value,metas:undefined,states:undefined}), requestValue:request=>new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)}), queueMicrotask,setTimeout,clearTimeout }; vm.createContext(context);vm.runInContext(`${functionSource('openWorldDb')}\nthis.openWorldDb=openWorldDb;`,context);await context.openWorldDb(); return{openedVersion,worldWrites,deletedStores}; } async function runCase({mirror,stored,loadError=null,journals=[],coverage=new Map()}){ const removedKeys=[]; const context={ console:{warn:()=>{}},idbAvailable:true,idbHealth:'ready',indexedDbExpectedAtStartup:true,worldDbAbandoned:false,worldDbPromise:null,cloudApiEnabled:false,cloudOutboxReady:true,cloudAvailable:false,activeStorageFormat:2,FIELD_STORAGE_FORMAT:2,IDB_STARTUP_TIMEOUT:8000, storageKey:'save',loadNotices:[],readInitialData:()=>mirror,safeLocalGet:()=>mirror?'{mirror}':null, readRecoveryJournals:()=>journals,safeLocalRemove:key=>{removedKeys.push(key);return true}, recoveryJournalsToCover:[],recoveredDeletionTombstones:new Map(), startupRecoveredMetaIds:new Set(),startupRecoveredStateIds:new Set(),startupRecoveredDeletedIds:new Set(), loadSnapshotFromDb:async()=>{if(loadError)throw loadError;return{snapshot:stored,coverage,databaseJournals:[],worldEpoch:'world:test-epoch'}}, withStartupTimeout:promise=>promise, validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),storedWorldEpoch:()=>null,createWorldEpoch:()=> 'world:test-epoch',rememberWorldEpoch:()=>true, defaultData:()=>({metas:{},states:{}}), attachWorldEpoch:(snapshot,epoch)=>Object.assign(snapshot||{metas:{},states:{}},{worldEpoch:epoch}),abandonWorldDatabase:()=>{context.worldDbAbandoned=true;context.idbAvailable=false;context.idbHealth='uncertain'}, isPlainObject:value=>!!value&&typeof value==='object'&&!Array.isArray(value), deepClone:value=>JSON.parse(JSON.stringify(value)), mergeBoardStates:(current,incoming)=>!current?JSON.parse(JSON.stringify(incoming)):!incoming?current:((incoming.rev||0)>(current.rev||0)?JSON.parse(JSON.stringify(incoming)):current), normalizeCloudPending:value=>({metaIds:[...new Set(value?.metaIds||[])],stateIds:[...new Set(value?.stateIds||[])],deleted:[...new Set(value?.deleted||[])],globalChanged:value?.globalChanged===true}), normalizeMeta:(_id,value)=>value,normalizeState:value=>value, normalizeSnapshot:value=>value }; vm.createContext(context); vm.runInContext(`${functionSource('revisionVersion')}\n${functionSource('compareRevisionVersions')}\n${functionSource('setRecoveryJournalsToCover')}\n${functionSource('mergeRecoveryJournal')}\n${functionSource('mergeRecoveryJournals')}\n${functionSource('combineRecoveryJournals')}\n${functionSource('mergeV2RecoveryJournals')}\n${functionSource('readInitialDataAsync')}\nthis.logic={readInitialDataAsync,mergeRecoveryJournal};`,context); return{result:await context.logic.readInitialDataAsync(),context,removedKeys}; } (async()=>{ let test=await runCase({mirror:{metas:{},states:{}},stored:null}); assert(test.result.metas&&test.result.states,'A fresh database bundle did not produce a valid default snapshot'); const mirror={metas:{B0:{id:'B0'}},updatedAt:20},emptyDb={metas:{},updatedAt:30}; test=await runCase({mirror,stored:emptyDb}); assert(Object.keys(test.result.metas).length===0,'The retired compact mirror overrode the current database'); const db={metas:{B0:{id:'B0'},B1:{id:'B1'}},updatedAt:40}; test=await runCase({mirror,stored:db}); assert(Object.keys(test.result.metas).length===2&&test.result.metas.B1,'Populated database was not loaded'); const richerMirror={metas:{B0:{id:'B0'},B1:{id:'B1'},B2:{id:'B2'},B3:{id:'B3'}},updatedAt:35}; const staleDb={metas:{B0:{id:'B0'}},updatedAt:50}; test=await runCase({mirror:richerMirror,stored:staleDb}); assert(Object.keys(test.result.metas).length===1&&test.result.metas.B0&&!test.result.metas.B3,'A stale richer mirror resurrected a board deleted from the newer database'); const newerMirror={metas:{B0:{id:'B0'},B1:{id:'B1'}},updatedAt:60}; const olderSameSizeDb={metas:{B0:{id:'B0'},B1:{id:'B1'}},updatedAt:55}; test=await runCase({mirror:newerMirror,stored:olderSameSizeDb}); assert(Object.keys(test.result.metas).length===2&&test.result.updatedAt===55,'The current database was not authoritative over a retired mirror'); test=await runCase({mirror,stored:null,loadError:new Error('timeout')}); assert(Object.keys(test.result.metas).length===1&&test.result.metas.B0,'Database failure did not fall back to the compact mirror'); assert(test.context.idbAvailable===false&&test.context.idbHealth==='uncertain','Unverified database fallback remained writable'); const journalBase={metas:{B0:{id:'B0',rev:100}},states:{B0:{rev:100}},updatedAt:100,nextId:2}; const staleDelete={schema:30,worldGeneration:'test',updatedAt:101,sessionId:'delete-session',seq:1,deleted:[{id:'B0',rev:99}]}; const freshDelete={...staleDelete,seq:2,deleted:[{id:'B0',rev:101}]}; let merged=test.context.logic.mergeRecoveryJournal(journalBase,staleDelete); assert(merged.metas.B0&&merged.states.B0,'Older deletion tombstone removed a newer board revision'); merged=test.context.logic.mergeRecoveryJournal(journalBase,freshDelete); assert(!merged.metas.B0&&!merged.states.B0,'Newer deletion tombstone did not remove the covered board and state'); const currentDb={metas:{B0:{id:'B0',rev:10}},states:{B0:{rev:10}},updatedAt:100,nextId:2}; const coveredJournal={schema:30,worldGeneration:'test',updatedAt:105,sessionId:'covered',seq:3,_storageKey:'journal:covered',metas:[{id:'B2',rev:1}],states:[],deleted:[]}; const uncoveredJournal={schema:30,worldGeneration:'test',updatedAt:110,sessionId:'pending',seq:4,_storageKey:'journal:pending',metas:[{id:'B1',rev:11}],states:[],deleted:[]}; test=await runCase({mirror:currentDb,stored:currentDb,journals:[coveredJournal,uncoveredJournal],coverage:new Map([['covered',3],['pending',2]])}); assert(test.result.metas.B1&&!test.result.metas.B2,'Recovery coverage did not merge only the uncovered session journal'); assert(test.removedKeys.includes('journal:covered')&&!test.removedKeys.includes('journal:pending'),'Covered journal cleanup removed the wrong per-session journal'); assert(test.context.recoveryJournalsToCover.length===1&&test.context.recoveryJournalsToCover[0].sessionId==='pending','Uncovered journal was not retained for the next coverage commit'); let upgrade=await runUpgradeCase(2);assert(upgrade.openedVersion===8&&['metas','states','global','recovery','tombstones','outbox'].every(name=>upgrade.deletedStores.includes(name))&&upgrade.worldWrites.some(row=>row.status==='active'),'Existing database did not delete retired stores and initialize a current-only world'); upgrade=await runUpgradeCase(0);assert(upgrade.openedVersion===8&&upgrade.worldWrites.some(row=>row.status==='active'),'Fresh current-only world was not initialized'); const outboxContext={normalizeCloudPending:value=>({metaIds:[...new Set(value?.metaIds||[])],stateIds:[...new Set(value?.stateIds||[])],deleted:[...new Set(value?.deleted||[])],globalChanged:value?.globalChanged===true})}; vm.createContext(outboxContext);vm.runInContext(`${functionSource('cloudPendingFromOutboxRows')}\nthis.readOutbox=cloudPendingFromOutboxRows;`,outboxContext); const pending=outboxContext.readOutbox([{type:'meta',id:'B1'},{type:'deleted',id:'B2'},{type:'global'}]); assert(pending.metaIds[0]==='B1'&&pending.deleted[0]==='B2'&&pending.globalChanged,'Startup outbox rows were not integrated into pending cloud work'); const journalPrefix='journal:',journalValues=new Map([ ['journal',JSON.stringify({schema:30,worldGeneration:'world',updatedAt:3,sessionId:'retired-single-key',seq:1})], [`${journalPrefix}one`,JSON.stringify({schema:30,worldGeneration:'world',updatedAt:1,sessionId:'one',seq:2})], [`${journalPrefix}two`,JSON.stringify({schema:30,worldGeneration:'world',updatedAt:2,sessionId:'two',seq:3})] ]),journalContext={ recoveryJournalKey:'journal',recoveryJournalPrefix:journalPrefix,LOCAL_MIRROR_MAX_BYTES:10000,SAVE_SCHEMA:30,WORLD_GENERATION:'world', safeLocalKeys:prefix=>[...journalValues.keys()].filter(key=>key.startsWith(prefix)),safeLocalGet:key=>journalValues.get(key)||null }; vm.createContext(journalContext);vm.runInContext(`${functionSource('readRecoveryJournals')}\nthis.readRecoveryJournals=readRecoveryJournals;`,journalContext); const discovered=journalContext.readRecoveryJournals(); assert(discovered.map(journal=>journal.sessionId).join(',')==='one,two'&&discovered.every(journal=>journal._storageKey),'Current per-session recovery journals were not discovered and ordered'); console.log('Storage recovery integration passed'); })().catch(error=>{console.error(error);process.exitCode=1});