Files
Endoleap/src/lib/canvas-runtime/scope-counts.ts
T

34 lines
3.0 KiB
TypeScript

import type {Id} from '../canvas-core/types.ts';
import {fault} from '../canvas-core/values.ts';
/** Snapshot-bound direct membership counts, including nodes and connections. */
export type CanvasElementCounts=Readonly<Record<Id,number>>;
export function validateCanvasElementCounts(value:unknown,canvasIds:readonly Id[],total:number):asserts value is CanvasElementCounts {
if(!value||typeof value!=='object'||Array.isArray(value))throw fault('invalid_model_coverage');
const counts=value as Record<Id,unknown>,keys=Object.keys(counts);let sum=0;
if(keys.length!==canvasIds.length||canvasIds.some(id=>!Object.hasOwn(counts,id)))throw fault('invalid_model_coverage');
for(const count of Object.values(counts)){if(typeof count!=='number'||!Number.isSafeInteger(count)||count<0)throw fault('invalid_model_coverage');sum+=count;if(!Number.isSafeInteger(sum))throw fault('invalid_model_coverage');}
if(sum!==total)throw fault('invalid_model_coverage');
}
/** All active IDs in the capture, including records not yet received. */
export type ElementOwners=Readonly<Record<Id,Id>>;
export function validateElementOwners(value:unknown,counts:CanvasElementCounts,total:number):asserts value is ElementOwners {
if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).length!==total)throw fault('invalid_model_identity');
const actual:Record<Id,number>=Object.create(null);
for(const [id,owner]of Object.entries(value)){if(!id||typeof owner!=='string'||!Object.hasOwn(counts,owner))throw fault('invalid_model_identity');actual[owner]=(actual[owner]??0)+1;}
if(Object.keys(counts).some(id=>(actual[id]??0)!==counts[id]))throw fault('invalid_model_identity');
}
/** Full captured topology, independent of which entry payloads have arrived. */
export function validateCanvasTopology(canvases:readonly import('../canvas-core/types.ts').CanvasRecord[],owners:ElementOwners,projectId:Id):void {
if(!Array.isArray(canvases)||!canvases.length)throw fault('invalid_model_topology');
const table=new Map(canvases.map(canvas=>[canvas?.id,canvas]));if(table.size!==canvases.length||!table.has(projectId))throw fault('invalid_model_topology');
let roots=0;const entries=new Set<Id>();
for(const canvas of canvases){if(!canvas||typeof canvas.id!=='string'||!canvas.id||!Object.hasOwn(canvas,'parent'))throw fault('invalid_model_topology');if(canvas.parent===null){roots++;if(canvas.id!==projectId)throw fault('invalid_model_topology');continue;}
const parent=canvas.parent;if(!parent||!table.has(parent.canvasId)||typeof parent.entryElementId!=='string'||!parent.entryElementId||entries.has(parent.entryElementId)||owners[parent.entryElementId]!==parent.canvasId)throw fault('invalid_model_topology');entries.add(parent.entryElementId);
const seen=new Set<Id>();let current=canvas;while(current.parent){if(seen.has(current.id))throw fault('invalid_model_topology');seen.add(current.id);const next=table.get(current.parent.canvasId);if(!next)throw fault('invalid_model_topology');current=next;}
}
if(roots!==1)throw fault('invalid_model_topology');
}