Yield recovery container encoding without changing durable cuts
This commit is contained in:
@@ -348,3 +348,27 @@ test('a failed final publication keeps the raw receipt and waiting claim instead
|
||||
test('receipt compaction is optional and invalid codecs cannot block a valid ACK or discard its raw payload',async()=>{
|
||||
for(const compactResolvedReceipt of [undefined,()=>{throw Error('compact failed');},()=>undefined]){const storage=storageFixture(),port=persistenceFixture(compactResolvedReceipt?{compactResolvedReceipt}:{}),{engine,errors}=await open(storage,port);try{engine.editor.dispatch({type:'element.data',target,patch:{title:'keep raw'}});await engine.settle();const request=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert.equal(request.phase,'resolved');assert.deepEqual(engine.getSnapshot().runtime.receipts.get(request.id),{receipt:request.id});assert.equal(engine.getSnapshot().runtime.conflicts.size,0);assert.equal(errors.length,compactResolvedReceipt?1:0);}finally{await engine.close();}}
|
||||
});
|
||||
|
||||
test('encoding yields preserve the captured Root, ordered durable prefix and send barrier', {timeout:15000},async()=>{
|
||||
const previous=Object.getOwnPropertyDescriptor(globalThis,'scheduler'),entered=deferred(),resume=deferred();let hold=false,paused=false,yields=0;
|
||||
Object.defineProperty(globalThis,'scheduler',{configurable:true,value:{async yield(){yields++;if(hold&&!paused){paused=true;entered.resolve();await resume.promise;}}}});
|
||||
let engine;
|
||||
try{
|
||||
const rows=Array.from({length:17000},(_,i)=>({...original.elements.get('a'),id:i===0?'a':`encoding-${i}`}));
|
||||
const document=createDocument('project',[{id:'canvas',parent:null}],rows),storage=storageFixture(),port=persistenceFixture({async submit(request){port.calls.push(['submit',request]);const stored=storage.image();assert.ok(stored.payload.runtime.ledger.requests.some(([id,row])=>id===request.id&&row.body===request.body&&row.phase==='dispatch-marked'));return{outcome:'unresolved'};}});
|
||||
({engine}=await open(storage,port,{document,clock:idleClock()}));await engine.persist();
|
||||
const start=engine.getSnapshot().rootRevision,saved=storage.saved.length;hold=true;
|
||||
assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{title:'captured-first'}}).status,'applied');
|
||||
const captured=engine.getSnapshot(),first=engine.persist();await entered.promise;
|
||||
assert.equal(storage.saved.length,saved);assert.equal(port.calls.length,0);
|
||||
assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{title:'edited-during-yield'}}).status,'applied');
|
||||
const second=engine.persist();assert.equal(engine.getSnapshot().document.elements.get('a').data.title,'edited-during-yield');
|
||||
resume.resolve();await Promise.all([first,second]);
|
||||
const cuts=storage.saved.slice(saved);assert.ok(yields>0);
|
||||
assert.deepEqual(cuts.map(cut=>cut.rootRevision),Array.from({length:cuts.length},(_,i)=>start+i+1));
|
||||
assert.deepEqual(cuts[0],encodeRecovery(captured));
|
||||
assert.equal(cuts[0].payload.document.elements.find(row=>row.id==='a').data.title,'captured-first');
|
||||
assert.equal(storage.image().payload.document.elements.find(row=>row.id==='a').data.title,'edited-during-yield');
|
||||
assert.equal(engine.getDurability().durableRootRevision,engine.getSnapshot().rootRevision);
|
||||
}finally{resume.resolve();if(engine)await engine.close();if(previous)Object.defineProperty(globalThis,'scheduler',previous);else delete globalThis.scheduler;}
|
||||
});
|
||||
|
||||
@@ -31,3 +31,44 @@ test('sealed image byte sizes match UTF-8 JSON for reused data and all codec nor
|
||||
const {sealedRecoveryByteLength}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts'),shared=freeze({text:'中文😀\ud800\n"\\',items:[null,1,true]}),payloads=[null,'😀',-0,{shared,omitted:undefined,array:[,undefined,NaN]},new Date('2026-01-01Z'),{['__proto__']:'保留'},Object.freeze({get text(){return '动态😀';}})];
|
||||
for(const payload of payloads)for(let revision=0;revision<3;revision++){const image=sealRecoveryImage(17,revision,revision,payload);assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength);assert.equal(sealedRecoveryByteLength(structuredClone(image)),undefined);}
|
||||
});
|
||||
|
||||
|
||||
test('cooperative sealing captures unknown values before yielding and retains exact bytes',async()=>{
|
||||
const {dataArray,dataRecord}=await import('../../src/lib/canvas-core/data-containers.ts');
|
||||
const {sealRecoveryImageAsync,sealedRecoveryByteLength}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts');
|
||||
let current='before',calls=[];
|
||||
const mutable={title:'before'},shallow=Object.freeze({mutable}),date=Object.freeze(new Date('2026-01-01Z'));
|
||||
const custom=Object.freeze({toJSON(key){calls.push(key);return {key,current};}});
|
||||
const getter=Object.freeze({get title(){calls.push('getter');return current;}});
|
||||
const rows=dataArray(Array.from({length:1000},(_,i)=>dataRecord({id:i,text:'中文😀\ud800'})));
|
||||
const payload=dataRecord({rows,shallow,date,getter,left:custom,right:custom,['__proto__']:{safe:true}});
|
||||
const expected=sealRecoveryImage(17,2,1,payload);calls=[];let yields=0;
|
||||
const pending=sealRecoveryImageAsync(17,2,1,dataRecord({...payload,rows:dataArray(rows)}),{now:(()=>{let t=0;return()=>t++;})(),frameBudgetMs:1,yieldTask:async()=>{yields++;current='after';mutable.title='after';date.setUTCFullYear(2027);}});
|
||||
assert.deepEqual(calls,['getter','left','right']);
|
||||
mutable.title='outside';current='outside';
|
||||
const image=await pending;
|
||||
// A second uncached table below exercises the work ceiling with a coarse clock.
|
||||
assert.ok(yields>0);assert.deepEqual(image,expected);assert.equal(isSealedRecoveryImage(image),true);
|
||||
assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength);
|
||||
const beforeCoarse=yields;
|
||||
const large=dataArray(Array.from({length:20000},(_,i)=>i));
|
||||
const more=await sealRecoveryImageAsync(17,3,2,large,{now:()=>100,frameBudgetMs:1,yieldTask:async()=>{yields++;}});
|
||||
assert.ok(yields>beforeCoarse);assert.deepEqual(more.payload,large);
|
||||
});
|
||||
|
||||
test('cooperative preparation evaluates every unsafe occurrence and rejects errors without an image',async()=>{
|
||||
const {dataArray,dataRecord}=await import('../../src/lib/canvas-core/data-containers.ts');
|
||||
const {sealRecoveryImageAsync}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts');
|
||||
let value=0;
|
||||
const dynamic=Object.freeze({get title(){return ++value;}}),shared=dataRecord({dynamic});
|
||||
const image=await sealRecoveryImageAsync(17,1,1,dataRecord({left:shared,right:shared}));
|
||||
assert.deepEqual(image.payload,{left:{dynamic:{title:1}},right:{dynamic:{title:2}}});
|
||||
let ticks=0;
|
||||
await assert.rejects(sealRecoveryImageAsync(17,1,1,dataArray(Array.from({length:1000},(_,i)=>i)),{now:()=>ticks++,frameBudgetMs:1,yieldTask:async()=>{throw Error('yield_failed');}}),/yield_failed/);
|
||||
const nested=(n,leaf)=>{for(let i=0;i<n;i++)leaf=dataRecord({child:leaf});return leaf;};
|
||||
const leaf=freeze({value:1});encodeRecoveryValue(leaf);
|
||||
await assert.rejects(sealRecoveryImageAsync(17,1,1,nested(64,leaf)),/invalid_recovery_payload/);
|
||||
await assert.rejects(sealRecoveryImageAsync(17,1,1,nested(64,dataArray([undefined]))),/invalid_recovery_payload/);
|
||||
assert.ok(await sealRecoveryImageAsync(17,1,1,nested(64,dataRecord({omitted:undefined}))));
|
||||
await assert.rejects(sealRecoveryImageAsync(17,0,1,{}),/corrupt_recovery_image/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user