Files
Endoleap/src/lib/canvas-runtime/reconcile.ts
T

53 lines
2.7 KiB
TypeScript

import type { Change, Document, Id, OperationIntent, Registry } from '../canvas-core/types.ts';
import { applyChanges, validateDocument } from '../canvas-core/document.ts';
import { equal, fault } from '../canvas-core/values.ts';
import type { LedgerSnapshot } from './ledger.ts';
/** The footprint follows ownership; geometry does not decide which canvas owns an object. */
export function operationCanvases(intent: OperationIntent, document: Document): readonly Id[] {
if(intent.operationType==='asset.upload'||intent.operationType==='run.cancel')return[];
if (intent.canvasIds?.length) return intent.canvasIds;
const ids = new Set<Id>();
for (const change of intent.changes) {
if (change.kind === 'canvas') {
ids.add(change.id);
if (change.before?.parent) ids.add(change.before.parent.canvasId);
if (change.after?.parent) ids.add(change.after.parent.canvasId);
} else if (change.kind === 'element') {
if (change.before) ids.add(change.before.ownerCanvasId);
if (change.after) ids.add(change.after.ownerCanvasId);
} else {
const owner = document.elements.get(change.target.id)?.ownerCanvasId;
if (owner) ids.add(owner);
if (change.field === 'ownerCanvasId') { ids.add(change.before as Id); ids.add(change.after as Id); }
}
}
if (!ids.size) throw fault('missing_participants', [intent.id]);
return [...ids].sort();
}
/** Replay once, by logical sequence. A receipt can already contain a desired field value. */
export function replayPending(base: Document, ledger: LedgerSnapshot, registry: Registry): Document {
let document = base;
for (const record of [...ledger.operations.values()].sort((a, b) => a.intent.sequence - b.intent.sequence)) {
if (record.phase === 'resolved' || !['document.change','run.apply'].includes(record.intent.operationType)) continue;
for (const change of record.intent.changes) {
if (satisfied(document, change)) continue;
document = applyChanges(document, [change]);
}
}
validateDocument(document, registry);
return document;
}
function satisfied(document: Document, change: Change) {
if (change.kind === 'canvas') return equal(document.canvases.get(change.id) ?? null, change.after);
if (change.kind === 'element') return equal(document.elements.get(change.id) ?? null, change.after);
const element = document.elements.get(change.target.id);
if (!element || element.incarnation !== change.target.incarnation) return false;
if (change.field.startsWith('data.')) {
const key = change.field.slice(5);
return Object.hasOwn(element.data, key) === (change.afterPresent ?? true) && equal(element.data[key] ?? null, change.after);
}
return equal((element as unknown as Record<string, unknown>)[change.field], change.after);
}