Certify loaded canvas scopes and publish completeness with runtime queries
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import {validateCanvasElementCounts,type CanvasElementCounts} from './scope-counts.ts';
|
||||
import type { CanvasRecord, Document, ElementRecord, Id, Registry } from '../canvas-core/types.ts';
|
||||
import { ancestors, createDocument, validateDocument, validateElementRecord } from '../canvas-core/document.ts';
|
||||
import { copyReferenceIds } from '../canvas-core/copy-paths.ts';
|
||||
@@ -5,10 +6,11 @@ import { clone, equal, fault, freeze, readMap } from '../canvas-core/values.ts';
|
||||
import type { CanonicalReadUpdate } from './contracts.ts';
|
||||
|
||||
export type LiveModelPreview=ModelPreview&{readonly loading:import('./loading.ts').LoadingState;readonly versions:ReadonlyMap<Id,number>};
|
||||
export type ModelPreview = { readonly document:Document;readonly receivedElements:number;readonly totalElements:number;readonly waitingElements:number;readonly complete:false };
|
||||
export type ModelPreview = { readonly canvasElementCounts?:CanvasElementCounts; readonly document:Document;readonly receivedElements:number;readonly totalElements:number;readonly waitingElements:number;readonly complete:false };
|
||||
/** An ephemeral read projection. It owns neither a working document nor ACKs,
|
||||
* history, operations, file bytes or a durable recovery cut. */
|
||||
export function createModelPreview(registry:Registry){
|
||||
let canvasElementCounts:CanvasElementCounts|undefined;const ownerCounts=new Map<Id,number>();
|
||||
let header=false,finished=false,projectId='',expected=0,received=0;
|
||||
let tombstones:ReadonlyMap<Id,number>=new Map(),published:Document|undefined,snapshot:ModelPreview|undefined;
|
||||
const canvases=new Map<Id,CanvasRecord>(),records=new Map<Id,ElementRecord>();
|
||||
@@ -32,12 +34,13 @@ export function createModelPreview(registry:Registry){
|
||||
else if(!canvas.parent||typeof canvas.parent.entryElementId!=='string'||!canvas.parent.entryElementId||entries.has(canvas.parent.entryElementId))throw fault('invalid_canvas_entry');
|
||||
else entries.add(canvas.parent.entryElementId);
|
||||
}
|
||||
if(roots!==1)throw fault('invalid_roots');header=true;
|
||||
}else if(value.projectId!==projectId||value.totalElements!==expected||value.canvases.length||value.tombstones.size)throw fault('model_preview_changed');
|
||||
if(roots!==1)throw fault('invalid_roots');if(value.canvasElementCounts!==undefined){validateCanvasElementCounts(value.canvasElementCounts,[...canvases.keys()],expected);canvasElementCounts=freeze(clone(value.canvasElementCounts));}header=true;
|
||||
}else if(value.projectId!==projectId||value.totalElements!==expected||value.canvases.length||value.tombstones.size||value.canvasElementCounts!==undefined)throw fault('model_preview_changed');
|
||||
if(value.receivedElements!==received+value.elements.length||value.receivedElements>expected)throw fault('invalid_model_preview');
|
||||
// Validate the entire incoming batch before publishing any prefix from it.
|
||||
const incoming=new Set<Id>();
|
||||
for(const record of value.elements){if(records.has(record.id)||known.has(key('element',record.id))||incoming.has(record.id)||!canvases.has(record.ownerCanvasId))throw fault('invalid_model_preview');validateElementRecord(record,registry,tombstones);incoming.add(record.id);}
|
||||
for(const record of value.elements){const count=(ownerCounts.get(record.ownerCanvasId)??0)+1;if(canvasElementCounts&&count>canvasElementCounts[record.ownerCanvasId])throw fault('invalid_model_coverage');ownerCounts.set(record.ownerCanvasId,count);}
|
||||
for(const record of value.elements)records.set(record.id,freeze(clone(record)));received=value.receivedElements;
|
||||
// Greatest closed subset: removing an unavailable dependency also removes its
|
||||
// dependants. Fully received cycles survive and publish in one legal prefix.
|
||||
@@ -56,7 +59,7 @@ export function createModelPreview(registry:Registry){
|
||||
const nextCanvases=[...(published?.canvases.values()??[])],nextElements=[...(published?.elements.values()??[])],accepted:string[]=[];
|
||||
for(const [id,item]of candidates)if(!blocked.has(id)){if(item.canvas)nextCanvases.push(item.canvas);if(item.element)nextElements.push(item.element);accepted.push(id);}
|
||||
if(!published||accepted.length){const next=createDocument(projectId,nextCanvases,nextElements,tombstones);validateDocument(next,registry);published=next;for(const id of accepted){known.add(id);const item=candidates.get(id);if(item?.element)records.delete(item.element.id);}}
|
||||
snapshot=Object.freeze({document:published,receivedElements:received,totalElements:expected,waitingElements:received-published.elements.size,complete:false});return snapshot;
|
||||
snapshot=Object.freeze({...(canvasElementCounts?{canvasElementCounts}:{}),document:published,receivedElements:received,totalElements:expected,waitingElements:received-published.elements.size,complete:false});return snapshot;
|
||||
}
|
||||
return Object.freeze({update(value:CanonicalReadUpdate){try{return update(value);}catch(error){finished=true;throw error;}},getSnapshot:()=>snapshot,close(){finished=true;records.clear();canvases.clear();known.clear();published=undefined;snapshot=undefined;}});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user