Resolve default image sizes atomically with creation history and recovery
This commit is contained in:
@@ -8,6 +8,8 @@ import { unitScale, withUnitScale } from './units.ts';
|
||||
import { removalChanges, repairConnectionOwners } from './structure.ts';
|
||||
import { planHistoryChanges } from './history.ts';
|
||||
import { describeHistoryTransition } from './history-transition.ts';
|
||||
import { planCreationSize, validateCreationSizeEffect } from './creation-size-plan.ts';
|
||||
import type { CreationSizeEffect } from './types.ts';
|
||||
|
||||
export type EditorOptions<Runtime = undefined> = {
|
||||
registry: Registry; document: Document; queryBudget?:QueryBudget; createId?: () => Id; historyLimit?: number;
|
||||
@@ -135,7 +137,7 @@ export function createEditor<Runtime = undefined>(options: EditorOptions<Runtime
|
||||
if (!id || used.has(id) || state.operations.has(id) || state.history.entries.has(id)) throw fault('duplicate_operation_id');
|
||||
used.add(id); return id;
|
||||
}
|
||||
function publish(document: Document, changes: readonly Change[], origin: Origin, history: HistoryState, historyEntryId?: Id): Applied {
|
||||
function publish(document: Document, changes: readonly Change[], origin: Origin, history: HistoryState, historyEntryId?: Id, historyEffect?: CreationSizeEffect): Applied {
|
||||
const used = new Set<Id>();
|
||||
for (const id of history.entries.keys()) used.add(id);
|
||||
// Canonical remote/ACK/replay updates must never feed back into the outbox.
|
||||
@@ -180,9 +182,9 @@ export function createEditor<Runtime = undefined>(options: EditorOptions<Runtime
|
||||
}
|
||||
}
|
||||
const historyTransition=describeHistoryTransition(state.history,history,state.rootRevision+1,historyEntryId,true);
|
||||
const intent: OperationIntent = freeze({ id: operationIds[0], operationType: 'document.change', operationSchemaVersion: 1, sequence: nextSequence, origin, canvasIds: [...canvasIds].sort(), changes, dependencies: [],...(historyTransition?{historyTransition}:{}) });
|
||||
const intent: OperationIntent = freeze({ id: operationIds[0], operationType: 'document.change', operationSchemaVersion: 1, sequence: nextSequence, origin, canvasIds: [...canvasIds].sort(), changes, dependencies: [],...(historyTransition?{historyTransition}:{}),...(historyEffect?{historyEffect}:{}) });
|
||||
const prepared=options.prepareIntent?.(intent,state.runtime) ?? intent;
|
||||
if(prepared.operationType==='run.apply'&&!validJson(prepared.action)||!['document.change','run.apply'].includes(prepared.operationType)||['id','operationSchemaVersion','sequence','origin','canvasIds','changes','dependencies','historyTransition'].some(key=>!equal((prepared as unknown as Record<string,unknown>)[key],(intent as unknown as Record<string,unknown>)[key])))throw fault('invalid_prepared_intent');
|
||||
if(prepared.operationType==='run.apply'&&!validJson(prepared.action)||!['document.change','run.apply'].includes(prepared.operationType)||['id','operationSchemaVersion','sequence','origin','canvasIds','changes','dependencies','historyTransition','historyEffect'].some(key=>!equal((prepared as unknown as Record<string,unknown>)[key],(intent as unknown as Record<string,unknown>)[key])))throw fault('invalid_prepared_intent');
|
||||
operations.set(intent.id, freeze(clone(prepared)));
|
||||
}
|
||||
const next: CoreSnapshot = Object.freeze({ revision: state.revision + 1, document, history, operations: readMap(operations) });
|
||||
@@ -268,6 +270,23 @@ export function createEditor<Runtime = undefined>(options: EditorOptions<Runtime
|
||||
finally { entered = false; }
|
||||
}
|
||||
return Object.freeze({ getSnapshot: () => state,getQueries:(document=state.document)=>{const value=queriesByDocument.get(document);if(!value)throw fault('query_snapshot_unavailable');return value;}, registry: options.registry, dispatch, transaction, can,
|
||||
/** Coordinator entry: document, amended history and outbound fact share one Root. */
|
||||
resolveCreationSize(effect: CreationSizeEffect, expectedRootRevision: number): Result {
|
||||
try { assertOpen(); } catch (error) { return reject(error); }
|
||||
entered = true;
|
||||
try {
|
||||
if (expectedRootRevision !== state.rootRevision) throw fault('stale_plan');
|
||||
validateCreationSizeEffect(effect);
|
||||
const plan = planCreationSize(state.document, state.history, options.registry, effect.entryId, effect.target, effect.size);
|
||||
const after = plan.document.elements.get(effect.target.id)!;
|
||||
if (after.structure !== 'node') throw fault('creation_size_target_changed');
|
||||
const allowed = options.policy?.({ type: 'element.geometry', target: effect.target, patch: { width: after.geometry.width, height: after.geometry.height } }, state.document);
|
||||
if (allowed && !allowed.enabled) throw fault(allowed.code, allowed.targets);
|
||||
if (!plan.changes.length) return Object.freeze({ status: 'applied', revision: state.revision, operationIds: Object.freeze([]) });
|
||||
return publish(plan.document, plan.changes, 'asset-resolution', plan.history, effect.entryId, freeze(clone(effect)));
|
||||
} catch (error) { return reject(error); }
|
||||
finally { entered = false; }
|
||||
},
|
||||
subscribe(listener: (commit: Commit, snapshot: Snapshot<Runtime>) => void) { if (disposed) throw fault('editor_disposed'); listeners.add(listener); return () => { listeners.delete(listener); }; },
|
||||
/** Engine-only metadata commit, sharing the document/history publication boundary. */
|
||||
updateRuntime(reduce: (runtime: Runtime) => Runtime, expectedRootRevision = state.rootRevision): Result {
|
||||
|
||||
Reference in New Issue
Block a user