Synchronize certified canvas scopes while preserving the original loading snapshot

This commit is contained in:
2026-11-09 04:02:22 +08:00
parent cabf2ec667
commit 6c0a421f59
19 changed files with 199 additions and 64 deletions
+11 -1
View File
@@ -22,6 +22,7 @@ if(performanceMode){
}
const boards = [root, other].map((id, i) => ({ id, workspace_id: workspace, kind: 'board', version: 1, title: i ? 'Other board' : 'Headless board' }));
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aB1sAAAAASUVORK5CYII=', 'base64');
let scopeStreamRoot=null,releaseScopeStream;const scopeStreamGate=new Promise(resolve=>releaseScopeStream=resolve);
let brokenStreamRoot=null,abandonedRoot=null,releaseAbandoned;const abandonedGate=new Promise(resolve=>releaseAbandoned=resolve);
let releaseFirstModel;const firstModelGate=new Promise(resolve=>{releaseFirstModel=resolve;});let gatedFirstModel=!performanceMode;
let releaseImport;const importGate=new Promise(resolve=>{releaseImport=resolve;});let importWriteSeen=false;const editReceipts=new Map();
@@ -48,6 +49,7 @@ const upstream = createServer(async (request, response) => {
line({kind:'header',documentFormatVersion:1,projectId:value.projectId,canvases:value.canvases,versions:value.versions,tombstones:value.tombstones,removedCanvasIds:value.removedCanvasIds,elementCount:value.elements.length,canvasElementCounts:Object.fromEntries(value.canvases.map(canvas=>[canvas.id,value.elements.filter(element=>element.ownerCanvasId===canvas.id).length]))});
if(read[1]===brokenStreamRoot){line({kind:'elements',elements:value.elements.slice(0,1)});response.end('{broken tail}\n');return;}
let start=0;
if(read[1]===scopeStreamRoot){line({kind:'elements',elements:value.elements.slice(0,3)});start=3;await scopeStreamGate;if(response.destroyed)return;}
if(read[1]===abandonedRoot){line({kind:'elements',elements:value.elements.slice(0,1)});start=1;await abandonedGate;if(response.destroyed)return;}
if(gatedFirstModel&&read[1]===root&&value.elements.length){gatedFirstModel=false;line({kind:'elements',elements:value.elements.slice(0,1)});start=1;await firstModelGate;}
for(let offset=start;offset<value.elements.length;offset+=64)line({kind:'elements',elements:value.elements.slice(offset,offset+64)});
@@ -364,6 +366,14 @@ try {
await until(`!!document.querySelector('[data-headless-canvas]:not([data-model-loading])')&&document.querySelectorAll('.react-flow__node').length===2&&!document.querySelector('[role="alert"]')`,'retry did not replace the failed prefix with a complete editor');
console.log('PASS malformed model tail retains its legal partial editor, reports a readable error and retries into the complete editor without model writes');
scopeStreamRoot=id(850);const scopedRoot=scopeStreamRoot,scopedChild=id(851),scopeFrame={...node('scope-frame',scopedRoot,450),componentId:'frame',geometry:{x:450,y:150,width:600,height:450,rotation:0}};
documents.set(scopedRoot,createDocument(scopedRoot,[{id:scopedRoot,parent:null},{id:scopedChild,parent:{canvasId:scopedRoot,entryElementId:scopeFrame.id}}],[node('scope-node',scopedRoot,80),scopeFrame,node('scope-child',scopedChild,20),node('scope-tail',scopedChild,320)]));versions.set(scopedRoot,1);versions.set(scopedChild,1);boards.push({id:scopedRoot,workspace_id:workspace,kind:'board',version:1,title:'Scope ACK board'});
await call('Page.navigate',{url:`http://127.0.0.1:${port}/dashboard/${workspace}/${scopedRoot}`});await until(`!!document.querySelector('[data-headless-canvas][data-model-loading] .react-flow__node[data-id="scope-node"]')`,'scope fixture did not show its certified owner');
const scopePoint=await evaluate(`(()=>{const r=document.querySelector('.react-flow__node[data-id="scope-node"] [aria-label^="单击选中"]').getBoundingClientRect();return{x:r.x+50,y:r.y+25};})()`);
await call('Input.dispatchMouseEvent',{type:'mousePressed',...scopePoint,button:'left',buttons:1,clickCount:2});await call('Input.dispatchMouseEvent',{type:'mouseReleased',...scopePoint,button:'left',buttons:0,clickCount:2});await until(`document.activeElement===document.querySelector('.react-flow__node[data-id="scope-node"] textarea')&&!document.activeElement.readOnly`,'certified scope text did not enter editing');await call('Input.insertText',{text:' ACK before tail'});const scopeText=await evaluate('document.activeElement.value');
for(let i=0;i<150&&documents.get(scopedRoot).elements.get('scope-node').data.text!==scopeText;i++)await pause(20);assert.equal(documents.get(scopedRoot).elements.get('scope-node').data.text,scopeText,'certified scope edit waited for another owner tail');assert.equal(versions.get(scopedRoot),2);assert.equal(versions.get(scopedChild),1);assert.equal(await evaluate(`!!document.querySelector('[data-headless-canvas][data-model-loading]')`),true);assert.equal(await evaluate(`!!document.querySelector('.react-flow__node[data-id="scope-tail"]')`),false);
await evaluate('window.__scopeTextarea=document.activeElement;true');releaseScopeStream();await until(`!!document.querySelector('[data-headless-canvas]:not([data-model-loading])')&&!!document.querySelector('.react-flow__node[data-id="scope-tail"]')`,'original stream did not complete after a scoped ACK');assert.equal(await evaluate('window.__scopeTextarea.isConnected&&document.activeElement===window.__scopeTextarea'),true);assert.equal(await evaluate('window.__scopeTextarea.value'),scopeText);assert.equal(versions.get(scopedRoot),2,'old tail caused a duplicate write');console.log('PASS certified owner edits ACK before another canvas tail, then original-source completion preserves content, focus, history and advanced versions');
abandonedRoot=id(840);const backgroundRoot=abandonedRoot;
documents.set(backgroundRoot,createDocument(backgroundRoot,[{id:backgroundRoot,parent:null}],[node('background-node',backgroundRoot,80),node('background-tail',backgroundRoot,450)]));versions.set(backgroundRoot,1);boards.push({id:backgroundRoot,workspace_id:workspace,kind:'board',version:1,title:'Background loading board'});
await call('Page.navigate',{url:`http://127.0.0.1:${port}/dashboard/${workspace}/${backgroundRoot}`});await until(`!!document.querySelector('[data-headless-canvas][data-model-loading] .react-flow__node[data-id="background-node"]')`,'background fixture did not open the prefix');
@@ -381,7 +391,7 @@ try {
console.log('PASS logout retires the runtime before dropping its view without a render error');
}
} finally {
releaseAbandoned();releaseFirstModel();releaseImport();releaseApply();releaseUpload(); socket?.close();
releaseScopeStream();releaseAbandoned();releaseFirstModel();releaseImport();releaseApply();releaseUpload(); socket?.close();
if (chrome) { const exit = new Promise(resolve => { if (chrome.exitCode !== null) resolve(); else chrome.once('exit', resolve); }); chrome.kill(); await exit; }
if (next?.pid) { const exit = new Promise(resolve => { if (next.exitCode !== null) resolve(); else next.once('exit', resolve); }); try { process.kill(-next.pid, 'SIGTERM'); } catch {} await Promise.race([exit, pause(3000)]); if (next.exitCode === null) try { process.kill(-next.pid, 'SIGKILL'); } catch {} }
upstream.closeAllConnections(); await new Promise(resolve => upstream.close(resolve));