Certify loaded canvas scopes and publish completeness with runtime queries
This commit is contained in:
@@ -679,22 +679,23 @@ async function engineRecoveryCase(name, assert) {
|
||||
} finally { await engine?.close(); storage.close(); indexedDB.deleteDatabase(`${name}-engine`); }
|
||||
}
|
||||
|
||||
async function loadingRecoveryCase(name,assert){
|
||||
const root=crypto.randomUUID(),scope={accountId:crypto.randomUUID(),workspaceId:crypto.randomUUID(),projectId:root},registry=createRegistry(builtinComponents()),node=id=>({id,incarnation:1,ownerCanvasId:root,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:'original'},geometry:{x:0,y:0,width:120,height:80,rotation:0}}),a=node('A'),b=node('B'),canvases=[{id:root,parent:null}],prefix=createDocument(root,canvases,[a]),full=createDocument(root,canvases,[a,b]),versions=new Map([[root,1]]),loading={version:1,sourceToken:'browser-captured-read',declaredCanvasIds:[root],receivedElements:1,totalElements:2};
|
||||
async function loadingRecoveryCase(name,assert,coverage=false){
|
||||
const format=coverage?8:7;name+=coverage?"-coverage":"-legacy";
|
||||
const root=crypto.randomUUID(),scope={accountId:crypto.randomUUID(),workspaceId:crypto.randomUUID(),projectId:root},registry=createRegistry(builtinComponents()),node=id=>({id,incarnation:1,ownerCanvasId:root,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:'original'},geometry:{x:0,y:0,width:120,height:80,rotation:0}}),a=node('A'),b=node('B'),canvases=[{id:root,parent:null}],prefix=createDocument(root,canvases,[a]),full=createDocument(root,canvases,[a,b]),versions=new Map([[root,1]]),loading={...(coverage?{version:2,canvasElementCounts:{[root]:2}}:{version:1}),sourceToken:'browser-captured-read',declaredCanvasIds:[root],receivedElements:1,totalElements:2};
|
||||
const adapter=createBrowserStorage({name:`${name}-loading`,checkpointEvery:3});let ownerForTest;const storage={...adapter,async claim(scope){ownerForTest=await adapter.claim(scope);return ownerForTest;}};const calls=[],receipts=new Map();let server=full,version=1,engine;
|
||||
const model=(complete=true)=>({documentFormatVersion:1,projectId:root,complete,canvases,elements:[...server.elements.values()],tombstones:{},versions:{[root]:version},removedCanvasIds:[]});
|
||||
const transport=async input=>{if(input.method==='GET')return{status:200,body:{model:model()}};calls.push(input);const body=JSON.parse(input.body);assert(body.expected_versions[root]===version,'loading writer used a stale version');server=applyChanges(server,body.operations.flatMap(operation=>operation.changes));version++;const receipt={protocol_version:2,request_id:input.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false)};receipts.set(input.requestId,receipt);return{status:200,body:receipt};};
|
||||
const persistence=createFoundryPersistence({registry,workspaceId:scope.workspaceId,rootId:root,transport}),open=()=>openProjectEngine({scope,registry,document:prefix,versions,loading,storage,persistence,createId:()=>crypto.randomUUID()});
|
||||
try{engine=await open();const target={id:'A',incarnation:1};assert(engine.editor.dispatch({type:'element.data',target,patch:{text:'local before tail'}}).status==='applied','loading field rejected');await engine.settle();assert(calls.length===0&&engine.getSnapshot().runtime.ledger.requests.size===0,'loading prefix froze or sent a request');await engine.close();
|
||||
const owner=await storage.claim(scope),image=await storage.recover(owner);assert(image.formatVersion===7,'loading state did not require recovery v7');decodeRecovery(image,registry);await owner.release();engine=await open();assert(engine.getSnapshot().document.elements.get('A').data.text==='local before tail','loading edit lost after IndexedDB reopen');assert(engine.editor.history.undo().status==='applied'&&engine.editor.history.redo().status==='applied','loading history did not reopen');await engine.settle();assert(calls.length===0,'history traversal escaped the loading hold');
|
||||
const owner=await storage.claim(scope),image=await storage.recover(owner);assert(image.formatVersion===format,'loading state did not require its versioned recovery envelope');decodeRecovery(image,registry);await owner.release();engine=await open();assert(engine.getSnapshot().document.elements.get('A').data.text==='local before tail','loading edit lost after IndexedDB reopen');assert(engine.editor.history.undo().status==='applied'&&engine.editor.history.redo().status==='applied','loading history did not reopen');await engine.settle();assert(calls.length===0,'history traversal escaped the loading hold');
|
||||
const before=engine.getDurability().durableRootRevision,nativePut=IDBObjectStore.prototype.put;let injected=false,failure;
|
||||
IDBObjectStore.prototype.put=function(value,key){const request=nativePut.call(this,value,key);if(!injected&&this.name==='checkpoints'&&value?.image?.formatVersion!==7){injected=true;throw Error('loading_full_cut_abort');}return request;};
|
||||
IDBObjectStore.prototype.put=function(value,key){const request=nativePut.call(this,value,key);if(!injected&&this.name==='checkpoints'&&value?.image?.formatVersion!==format){injected=true;throw Error('loading_full_cut_abort');}return request;};
|
||||
try{await engine.loading.complete(full,versions,{sourceToken:loading.sourceToken});}catch(error){failure=error.message;}finally{IDBObjectStore.prototype.put=nativePut;}
|
||||
assert(injected&&failure==='loading_full_cut_abort','completion did not reach forced checkpoint rollback');const partial=await adapter.recover(ownerForTest);assert(partial.rootRevision===before&&partial.formatVersion===7&&calls.length===0,'aborted full checkpoint released loading writes or lost the partial cut');
|
||||
assert(injected&&failure==='loading_full_cut_abort','completion did not reach forced checkpoint rollback');const partial=await adapter.recover(ownerForTest);assert(partial.rootRevision===before&&partial.formatVersion===format&&calls.length===0,'aborted full checkpoint released loading writes or lost the partial cut');
|
||||
await engine.persist();await engine.settle();
|
||||
assert(!engine.loading.getState()&&server.elements.get('A').data.text==='local before tail'&&engine.getSnapshot().document.elements.size===2,'full tail overwrote the local prefix');await engine.close();engine=await open();await engine.settle();assert(!engine.loading.getState()&&engine.getSnapshot().document.elements.size===2,'completed recovery reopened as partial');
|
||||
const database=await new Promise((resolve,reject)=>{const request=indexedDB.open(`${name}-loading`,2);request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error);});
|
||||
try{const transaction=database.transaction(['checkpoints','journal'],'readonly'),read=store=>new Promise((resolve,reject)=>{const request=transaction.objectStore(store).getAll();request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error);});const cuts=(await Promise.all([read('checkpoints'),read('journal')])).flat();assert(cuts.every(cut=>(cut.image?.formatVersion??cut.header?.formatVersion)<7),'completed storage still requires a v7 journal base');}finally{database.close();}
|
||||
try{const transaction=database.transaction(['checkpoints','journal'],'readonly'),read=store=>new Promise((resolve,reject)=>{const request=transaction.objectStore(store).getAll();request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error);});const cuts=(await Promise.all([read('checkpoints'),read('journal')])).flat();assert(cuts.every(cut=>(cut.image?.formatVersion??cut.header?.formatVersion)<7),'completed storage still requires a partial journal base');}finally{database.close();}
|
||||
}finally{await engine?.close();storage.close();indexedDB.deleteDatabase(`${name}-loading`);}
|
||||
}
|
||||
|
||||
@@ -719,7 +720,7 @@ export async function run() {
|
||||
await storage.commit(leader, { image: image(1) });
|
||||
await rejects(() => storage.commit(leader, { image: { ...image(1), payload: null } }), 'storage_revision_conflict');
|
||||
await rejects(() => storage.commit(leader, { image: image(3) }), 'storage_revision_gap');
|
||||
await rejects(() => storage.commit(leader, { image: { ...image(2), formatVersion: 8 } }), 'unsupported_recovery_version');
|
||||
await rejects(() => storage.commit(leader, { image: { ...image(2), formatVersion: 9 } }), 'unsupported_recovery_version');
|
||||
results.push('same-cut retry is idempotent; conflicting, skipped and future-format cuts reject');
|
||||
|
||||
// The first put succeeds before a later duplicate resource aborts the whole transaction.
|
||||
@@ -770,6 +771,8 @@ export async function run() {
|
||||
results.push('staged file and image recover from real IndexedDB; lost upload ACK queries stable resource identity');
|
||||
await loadingRecoveryCase(name,assert);
|
||||
results.push('recovery v7 preserves loaded-field edits/history in IndexedDB; an injected full-checkpoint failure retains the partial cut and zero writes, then retry removes v7 journal dependencies before releasing requests');
|
||||
await loadingRecoveryCase(name,assert,true);
|
||||
results.push('recovery v8 preserves certified scope coverage and loading edits; full-checkpoint fault rolls back atomically, retry retires all partial journal dependencies before sending');
|
||||
await scopedNetworkLoaderCase(assert);
|
||||
results.push('scoped loader shares one real streamed fetch, exposes the first chunk before tail release, keeps the other subscriber alive after cancellation and preserves partial data after a malformed tail');
|
||||
await defaultImageSizeCase(name, assert);
|
||||
|
||||
Reference in New Issue
Block a user