/** A second host: no DOM, React, Next, React Flow or network service required. * Storage and protocol-2 transport below are in-memory examples, not a disk or * production server implementation. Replace them with your host's real ports. */ import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { createDocument, createRegistry, builtinComponents, defineComponent, editable, movable, resizable, rectangleGeometry, applyChanges, validateDocument } from '@endoleap/headless-canvas/core'; import { openProjectEngine } from '@endoleap/headless-canvas/runtime'; import { createFoundryPersistence, loadFoundryModel } from '@endoleap/headless-canvas/service'; const scope={accountId:'10000000-0000-4000-8000-000000000001',workspaceId:'10000000-0000-4000-8000-000000000002',projectId:'10000000-0000-4000-8000-000000000003'}; const registry=createRegistry([...builtinComponents(),defineComponent({id:'example.counter',dataVersion:1,structure:'node',validate:data=>typeof data.title==='string'&&Number.isSafeInteger(data.count)&&data.count>=0,features:[editable(),movable(),resizable()],geometry:rectangleGeometry,resourceFields:[],copy:{references:[],omit:[]}})]); const original=createDocument(scope.projectId,[{id:scope.projectId,parent:null}],[]); let image=null,leader,fence=0,server=original,version=1; const files=new Map(),requests=new Map(); const check=handle=>{assert.strictEqual(handle,leader);assert.equal(handle.active,true);}; const storage={ async claim(requested){assert.equal(leader,undefined);assert.deepEqual(requested,scope);let active=true;const handle={scope:requested,fence:++fence,get active(){return active;},async release(){active=false;if(leader===handle)leader=undefined;}};leader=handle;return handle;}, async recover(handle){check(handle);return structuredClone(image);}, async commit(handle,batch){ check(handle);const cut=structuredClone(batch.image),nextFiles=new Map(files); for(const file of batch.files??[]){assert.equal(nextFiles.has(file.id),false);nextFiles.set(file.id,file.bytes);} for(const id of batch.deleteFiles??[])nextFiles.delete(id); // Prepare before publishing. This is atomic in this process, not crash-durable storage. image=cut;files.clear();for(const entry of nextFiles)files.set(...entry);return{durableRootRevision:cut.rootRevision}; }, async readFile(handle,id){check(handle);return files.get(id);},close(){assert.equal(leader,undefined);}, }; const model=()=>({documentFormatVersion:1,projectId:scope.projectId,complete:true,canvases:[...server.canvases.values()],elements:[...server.elements.values()],tombstones:Object.fromEntries(server.tombstones),versions:{[scope.projectId]:version},removedCanvasIds:[]}); const transport=async input=>{ input.signal?.throwIfAborted(); if(input.method==='GET'){ if(input.path.endsWith('/canvas/model'))return{status:200,body:{model:model()}}; const id=input.path.split('/').at(-1),known=requests.get(id); return known?{status:200,body:known.receipt}:{status:404,body:{protocol_version:2,request_id:id,status:'unknown'}}; } const body=JSON.parse(input.body),known=requests.get(input.requestId); assert.equal(body.protocol_version,2);assert.equal(body.request_id,input.requestId); if(known){assert.equal(known.body,input.body);return{status:200,body:known.receipt};} assert.deepEqual(body.expected_versions,{[scope.projectId]:version}); server=applyChanges(server,body.operations.flatMap(operation=>operation.changes));validateDocument(server,registry);version++; const receipt={protocol_version:2,request_id:input.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:{...model(),complete:false}}; requests.set(input.requestId,{body:input.body,receipt});return{status:200,body:receipt}; }; const persistence=createFoundryPersistence({workspaceId:scope.workspaceId,rootId:scope.projectId,registry,transport}); const open=()=>openProjectEngine({scope,registry,document:original,versions:new Map([[scope.projectId,1]]),storage,persistence,createId:randomUUID}); let engine=await open(); try{ const target={id:randomUUID(),incarnation:1}; const element={...target,ownerCanvasId:scope.projectId,componentId:'example.counter',dataVersion:1,structure:'node',data:{title:'Portable counter',count:1},geometry:{x:0,y:0,width:160,height:80,rotation:0}}; assert.equal(engine.editor.dispatch({type:'element.create',element}).status,'applied');await engine.settle(); const before=engine.getSnapshot(); assert.equal(engine.editor.transaction(tx=>{tx.dispatch({type:'element.geometry',target,patch:{x:32}});tx.dispatch({type:'element.data',target,patch:{count:-1}});}).status,'rejected');assert.strictEqual(engine.getSnapshot(),before); assert.equal(engine.editor.transaction(tx=>{tx.dispatch({type:'element.geometry',target,patch:{x:32}});tx.dispatch({type:'element.data',target,patch:{count:2}});},{label:'Move and increment'}).status,'applied');await engine.settle(); assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(server.elements.get(target.id).data.count,1); assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(server.elements.get(target.id).data.count,2); engine.sessions.attach('headless-view');const writes=requests.size; assert.equal(engine.sessions.begin('headless-view',[target]).enabled,true);engine.sessions.update('headless-view',target,{x:90});assert.equal(engine.sessions.effectiveGeometry(target.id).x,90);engine.sessions.cancel('example-cancel');await engine.settle();assert.equal(requests.size,writes);assert.equal(engine.getSnapshot().document.elements.get(target.id).geometry.x,32); const copyId=await engine.imports.duplicate([target],{worldOffset:{x:24,y:24}});await engine.settle();assert.equal(engine.imports.getJobs().get(copyId).status,'completed',JSON.stringify(engine.imports.getJobs().get(copyId)));assert.equal(engine.getSnapshot().document.elements.size,2);assert.equal(server.elements.size,2); const loaded=await loadFoundryModel(transport,scope.workspaceId,scope.projectId,registry);assert.equal(loaded.status,'ready');assert.equal(loaded.document.elements.size,2); const snapshot=engine.getSnapshot(),undoDepth=snapshot.history.undo.length;await engine.close();engine=await open();await engine.settle(); assert.equal(engine.getSnapshot().document.elements.size,2);assert.equal(engine.getSnapshot().document.elements.get(target.id).data.count,2);assert.equal(engine.getSnapshot().history.undo.length,undoDepth);assert.equal(engine.sessions.getSnapshot().views.size,0);assert.ok([...engine.getSnapshot().runtime.ledger.operations.values()].every(record=>record.phase==='resolved')); console.log(JSON.stringify({status:'ok',elements:2,count:2,undoDepth,reopened:true,transientViews:0,protocol:2,requests:requests.size,sourceFiles:files.size})); }finally{await engine.close();storage.close();}