Adopt authorized shared assets with durable project bindings and recovery

This commit is contained in:
2026-11-09 16:11:25 +08:00
parent a294c6ca41
commit a626e6c300
17 changed files with 214 additions and 37 deletions
+13
View File
@@ -306,3 +306,16 @@ test('v6 replaces an image in a complete original owner and preserves both resou
test('v6 failed Blob/root persistence keeps the local picture visible and sends neither upload nor document until retry',async()=>{
const f=fixture(),port=assetPort(f),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:assetLoading,assets:port.assets});try{await engine.persist();f.storage.broken=true;const {result,ref}=stageLoadingImage(engine);assert.equal(result.status,'applied');const display=await engine.displayResource(ref.id);assert.equal(await(await fetch(display.url)).text(),'image bytes');display.release();await assert.rejects(engine.persist(),/quota/);await new Promise(resolve=>setTimeout(resolve,10));assert.equal(port.uploads.length,0);assert.equal(f.calls.length,0);assert.equal(engine.getSnapshot().document.elements.get('partial-image').data.assetId,ref.id);f.storage.broken=false;await engine.retry();await engine.settle();assert.equal(port.uploads.length,1);assert.equal(f.server().elements.get('partial-image').data.assetId,ref.id);assert.equal(engine.getSnapshot().history.undo.length,1);}finally{f.storage.broken=false;await engine.close();}
});
test('v6 adopts an authorized shared image into a complete owner without an upload and restores its binding before the unread tail',async()=>{
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),bytes=new Blob(['shared bytes'],{type:'image/png'}),digest=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await bytes.arrayBuffer()))].map(n=>n.toString(16).padStart(2,'0')).join(''),id='shared-adopted',authority='https://assets.example';
const cache=new Map(),commit=f.storage.commit;f.storage.assetDepot={async read(_leader,_authority,id){return cache.get(id);},async collect(){throw Error('unexpected GC');}};f.storage.commit=async(leader,batch)=>{const result=await commit(leader,batch);for(const write of batch.depot?.writes??[])cache.set(write.ref.asset.id,{ref:write.ref,bytes:write.bytes??cache.get(write.ref.asset.id)?.bytes});return result;};
const assets={authority:{scope:authority,async resolve(id){return{version:1,authority,digest,asset:{id,url:`${authority}/${id}`,width:2,height:1,bytes:bytes.size,mime_type:'image/png'}};},async read(){return bytes;}},sourceURL:id=>`${authority}/${id}`,displayURL:id=>`/assets/${id}`,async upload(){throw Error('adoption must not upload');},async query(){throw Error('adoption must not query upload');},accept(){throw Error('adoption must not accept upload');}};
const create=(handle,tx,owner=child)=>tx.dispatch({type:'element.create',element:{...node('adopted-image',owner),componentId:'image',data:{title:'adopted',text:'',assetId:handle.id,src:handle.src}}});let engine=await f.open({document,loading:assetLoading,assets});
try{
assert.equal((await engine.adoptResource(id,(handle,tx)=>create(handle,tx,root))).status,'rejected');assert.equal(engine.getSnapshot().runtime.assetBindings,undefined);
assert.equal((await engine.adoptResource(id,create)).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('adopted-image').data.assetId,id);await engine.settle();assert.equal(f.server().elements.get('adopted-image').data.assetId,id);assert.equal(engine.getSnapshot().runtime.resources.size,0);assert.equal(f.storage.image().formatVersion,13);assert.equal(f.storage.saved.at(-1).payload.runtime.loading.version,6);decodeRecovery(f.storage.image(),registry);
const saved=f.storage.image();for(const change of [image=>image.formatVersion=12,image=>image.payload.runtime.assetBindings[0][1].loadingAdmission.sourceToken='other',image=>image.payload.runtime.assetBindings[0][1].loadingAdmission.operationId='other',image=>image.payload.runtime.assetBindings[0][1].loadingAdmission.canvasIds=[root]]){const broken=structuredClone(saved);change(broken);assert.throws(()=>decodeRecovery(broken,registry));}
await engine.close();engine=await f.open({document,loading:assetLoading,assets});const display=await engine.displayResource(id);assert.equal(await(await fetch(display.url)).text(),'shared bytes');display.release();assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('adopted-image').incarnation,2);assert.equal((await engine.loading.complete(full,versions,{sourceToken:assetLoading.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().runtime.assetBindings.get(id).asset.id,id);assert.equal(f.errors.length,0);
}finally{await engine.close();}
});