import {dataRecord} from '../canvas-core/data-containers.ts'; import {createImportReadProof,IMPORT_PROOF_BLOCK_BYTES} from './import-read-proof.ts'; import {isImportByteFailure} from './import-byte-reader.ts'; import {assertImportEvidence,importSeals,importsMaySend} from './import-verification.ts'; import {createAssetDepot,sharedAssetRef,type AssetDepot,type SharedAsset} from './asset-depot.ts'; import {prepareDepotCut} from './depot-cut.ts'; import {loadingAssetURLs} from './loading-assets.ts'; import {loadingImportAdmission,validateLoadingCopySource} from './loading-import.ts'; import {loadingCanSend,mergeLoadingRead,loadingQueryCoverage,assertLoadingChanges,loadingCommand,validateLoadingRuntime,validatePrefixExtension,validateLoadingCompletion,type LoadingState} from './loading.ts'; import {validImageDimensions,type ImageDecoderPort} from './image-decoder.ts'; import {unitScale} from '../canvas-core/units.ts'; import { validateCreationSizeEffect } from '../canvas-core/creation-size-plan.ts'; import { captureDefaultImageSizing, prepareDefaultImageSizing } from './default-image-size.ts'; import { createDisplayCache, type DisplayCache, type DisplayLease } from './display-cache.ts'; import {orphanCoverage,prepareOrphanFiles,abortableFileRead,type OrphanFile} from './orphan-files.ts'; import {importFiles,terminalImport,markImportFiles,prepareImportFileRetention,type FileRetirement} from './import-file-retention.ts'; import {resourceRetentionPins,prepareResourceRetention} from './resource-retention.ts'; import { encodeIndexedCopySnapshot, openIndexedCopySnapshot, type IndexedCopySnapshot } from './copy-snapshot-index.ts'; import { decodeCopySnapshot, copyRegistrySignature } from './copy-snapshot.ts'; import { createDuplicatePlanSource } from './duplicate-source.ts'; import { planDuplicateAsync, planDuplicateResumable, prepareDuplicateStream } from './duplicate.ts'; import type { DigestPort } from './digest-pool.ts'; import { importSourceDigest } from './import-integrity.ts'; import { encodeDuplicateSource, prepareDuplicateImport, assertDuplicateExternal } from './import-duplicate.ts'; import {openDocumentImportSource} from './import-document.ts'; import {openStreamingDocumentImportSource} from './import-document-stream.ts'; import {createJsonArrayImportSource} from './import-json-array.ts'; import { createImportJob, importNeedsSeal, createImportRunner, createNdjsonImportSource, type ImportJob } from './imports.ts'; import { assertNativeImportDependencies, assertNativeImportTarget, prepareNativeImport } from './import-native.ts'; import { elisionBatchLimit, findElision } from './elision.ts'; import type { Document, Id, Registry, Result, RunInput, RunApplyInput, RunCancelInput, Target, Point, Json, OperationIntent, Data } from '../canvas-core/types.ts'; import { createEditor, type EditorOptions, type Transaction } from '../canvas-core/editor.ts'; import { createDocument } from '../canvas-core/document.ts'; import { createSessions } from '../canvas-core/gestures.ts'; import { clone, equal, fault, freeze, readMap, reject, validJson } from '../canvas-core/values.ts'; import type { CanonicalProposal, EngineSnapshot, PersistencePort, RuntimeState } from './contracts.ts'; import { createRunTracker, validateRunInput, runContainsTarget, type RunPort, type RunRecord, type RunPage } from './runs.ts'; import { createNetworkGate } from './network.ts'; import { createLedger, type FrozenRequest } from './ledger.ts'; import { operationCanvases, replayPending, validateCanonicalRead } from './reconcile.ts'; import { decodeRecovery, encodeRecoveryAsync } from './recovery.ts'; import { createScheduler, type Clock, type OperationStatus } from './scheduler.ts'; import { planConflictRecovery, type ConflictPlan, type RecoveryChoice } from './conflicts.ts'; import { stageAppliedRecovery } from './applied-recovery.ts'; import { conflictsWith, maxResourceBytes, operationFootprint, referencedResources, type AssetPort, type StoredAsset, type ResourceRecord, type ResourceHandle, type StagedFile } from './resources.ts'; import type { StoragePort, StorageScope } from './storage-port.ts'; const digestFailures=new Set(['digest_capacity','digest_worker_failed','digest_worker_unavailable','invalid_digest_response','digest_pool_closed']); export type EngineOptions = { loading?:LoadingState; scope: StorageScope; registry: Registry; document: Document; versions: ReadonlyMap; storage: StoragePort; assetDepot?:AssetDepot; displays?: DisplayCache; images?:Pick; digests?:Pick; persistence: PersistencePort; assets?: AssetPort; runs?: RunPort; createId?: () => Id; historyLimit?: number; queryBudget?:EditorOptions['queryBudget']; policy?: EditorOptions['policy']; traversalPolicy?: EditorOptions['traversalPolicy']; clock?: Clock; onError?: (error: unknown) => void; isCurrentIdentity?: () => boolean; }; export type RecoverySession = { readonly baseRootRevision: number; readonly canonical: CanonicalProposal; readonly pendingOperationIds: readonly Id[]; readonly acknowledgedRequestIds: readonly Id[]; /** A conflict is diagnostic only: the existing document/history/requests remain intact. */ readonly conflict?: { readonly code: string; readonly targets: readonly Id[] }; plan(choices:ReadonlyMap):ConflictPlan; resolve(plan:ConflictPlan):Promise; apply(): Promise; cancel(): Promise; }; /** One project owner. Detaching a view does not close this engine or abort persistent work. */ export async function openProjectEngine(options: EngineOptions) { if (options.scope.projectId !== options.document.projectId) throw fault('project_mismatch'); const openingLoading=options.loading?freeze(clone(options.loading)):undefined,openingVersions=readMap(options.versions); const leader = await options.storage.claim(options.scope); try { const image = await options.storage.recover(leader), recovered = image ? decodeRecovery(image, options.registry) : undefined; if (recovered && recovered.document.projectId !== options.scope.projectId) throw fault('project_mismatch'); for (const canvas of options.document.canvases.keys()) if (!Number.isSafeInteger(openingVersions.get(canvas)) || openingVersions.get(canvas)! < 1) throw fault('missing_server_version', [canvas]); let loadingRead:Promise|undefined,loadingReadController:AbortController|undefined; let acceptingLoading=false,loadingRelease:(()=>void)|undefined,loadingCompletionRevision:number|undefined; let closing = false, closed = false, durableRootRevision = image?.rootRevision ?? -1; const digestLifecycle=new AbortController(); const sourceDigest=(bytes:Blob,signal?:AbortSignal)=>{const combined=signal?AbortSignal.any([signal,digestLifecycle.signal]):digestLifecycle.signal;return options.digests?options.digests.digest(bytes,combined):importSourceDigest(bytes,combined);}; const report = (error: unknown) => { try { options.onError?.(error); } catch { /* Error reporting cannot alter state. */ } }; const durabilityListeners=new Set<()=>void>();let durabilityError:string|undefined,durabilityCache:Readonly<{rootRevision:number;durableRootRevision:number;closing:boolean;closed:boolean;error?:string}>|undefined; function getDurability(){const rootRevision=editor.getSnapshot().rootRevision;if(!durabilityCache||durabilityCache.rootRevision!==rootRevision||durabilityCache.durableRootRevision!==durableRootRevision||durabilityCache.closing!==closing||durabilityCache.closed!==closed||durabilityCache.error!==durabilityError)durabilityCache=Object.freeze({rootRevision,durableRootRevision,closing,closed,...(durabilityError?{error:durabilityError}:{})});return durabilityCache;} function notifyDurability(){for(const listener of [...durabilityListeners])try{listener();}catch(error){report(error);}} const createId = options.createId ?? (() => crypto.randomUUID()); const network = createNetworkGate(2,()=>{if(!leader.active)throw fault('storage_leader_lost');if(options.isCurrentIdentity&&!options.isCurrentIdentity())throw fault('identity_changed');}); const depot=options.assetDepot??(options.storage.assetDepot?createAssetDepot({scope:options.scope,port:options.storage.assetDepot,current:()=>!closing&&!closed&&(!options.isCurrentIdentity||options.isCurrentIdentity()),digest:sourceDigest}):undefined); if(depot&&(depot.scope.accountId!==options.scope.accountId||depot.scope.workspaceId!==options.scope.workspaceId))throw fault('asset_depot_scope_mismatch'); const adoptionTasks=new Map>(),adoptedFiles=new Map(); let adopting:SharedAsset|undefined;let importAdoptions:readonly SharedAsset[]=[]; const activeAdoptions=()=>[...(adopting?[adopting]:[]),...importAdoptions]; const runClock: Clock = options.clock ?? { now: () => Date.now(), setTimer: (fn, delay) => setTimeout(fn, delay), clearTimer: handle => clearTimeout(handle as ReturnType) }; const displays = new Set<() => void>(); const displayOwner = Object.freeze({}), displayCache = options.displays ?? createDisplayCache({ onError: options.onError }); const assertDisplayIdentity=()=>{if(closing||closed)throw fault('engine_closing');if(options.isCurrentIdentity&&!options.isCurrentIdentity())throw fault('identity_changed');}; const displayPins=new Map(),collectionReservations=new Set(),durablyPurged=new Set([...(recovered?.runtime.resources.values()??[])].filter(resource=>resource.retirement?.purgedAt!==undefined).map(resource=>resource.id)); type CollectionReport={readonly markedIds:readonly Id[];readonly purgedIds:readonly Id[];readonly complete:boolean;readonly checkedRootRevision:number;readonly rootRevision:number;readonly durableRootRevision:number}; let sourceCollection:Promise|undefined; let orphanCollection:Promise<{readonly markedIds:readonly Id[];readonly purgedIds:readonly Id[];readonly changedIds:readonly Id[];readonly complete:boolean;readonly blockers:readonly string[];readonly checkedRootRevision:number;readonly scanned:number;readonly nextAfter?:Id;readonly hashBudgetExhausted:boolean;readonly scanFinished:boolean;readonly durableRootRevision:number}>|undefined; let orphanController:AbortController|undefined; for(const [id,file]of recovered?.runtime.orphanFiles??[])if(file.retirement?.purgedAt!==undefined)durablyPurged.add(id); for(const [id,entry]of importFiles(recovered?.runtime.imports))if(entry.file.retirement?.purgedAt!==undefined)durablyPurged.add(id); let collection:Promise|undefined; const imageTasks=new Map>(); let sizingTask: Promise | undefined; const fileWrites = new Map(), digestTasks = new Map>(); let staged: readonly ResourceRecord[] = []; const stagedDefaultSizes = new Set(); let applying: RunApplyInput | undefined; let importing: { job: ImportJob; expectedGeneration: number } | undefined; let integrityOwner:Id|undefined; const proofReaders=new Map}>(); const verificationTasks=new Map}>(); const preparationControllers=new Map(); const copyPreparations=new Map>(); const importRunners=new Map>(),importLoads=new Map>(),importHolds=new Mapvoid>>(); let recovery: { controller: AbortController; release(): void; cancel():Promise } | undefined; const admissionPins=(snapshot:EngineSnapshot)=>{const pins=new Map();if(copyPreparations.size)pins.set('copy-admission',[...snapshot.runtime.resources.values()].filter(resource=>resource.retirement?.purgedAt===undefined).map(resource=>resource.id));if(imageTasks.size)pins.set('image-decode',[...imageTasks.keys()]);return pins;}; const assertWritable = (complete=true) => { if(!leader.active)throw fault('storage_leader_lost');if(options.isCurrentIdentity&&!options.isCurrentIdentity())throw fault('identity_changed');if(complete&&editor.getSnapshot().runtime.loading)throw fault('model_loading');if(closing||closed)throw fault('engine_closing');if(recovery)throw fault('recovery_in_progress'); }; function assetURLs(){const root=editor.getSnapshot(),urls=loadingAssetURLs(root.runtime);return staged.length||activeAdoptions().length?new Map([...urls,...staged.map(resource=>[resource.id,options.assets!.sourceURL(resource.id)] as const),...activeAdoptions().map(ref=>[ref.asset.id,ref.asset.url] as const)]):urls;} const initial: RuntimeState = recovered?.runtime ?? Object.freeze({ imports:readMap(new Map()), recoveries:readMap(new Map()), runs: readMap(new Map()), acknowledged: createDocument(options.document.projectId, [...options.document.canvases.values()], [...options.document.elements.values()], options.document.tombstones), versions: openingVersions, ...(openingLoading?{loading:openingLoading,...(openingLoading.version>=3?{loadingRead:Object.freeze({document:createDocument(options.document.projectId,[...options.document.canvases.values()],[...options.document.elements.values()],options.document.tombstones),versions:openingVersions})}:{})}:{}), ledger: createLedger().getSnapshot(), receipts: readMap(new Map()), conflicts: readMap(new Map()), resources: readMap(new Map()), resourcePins: readMap(new Map()) }); let durableImportSeals=importSeals(initial.imports); validateLoadingRuntime(initial,options.registry); for(const [id,binding]of initial.assetBindings??[])if(!depot||!options.assets?.authority||binding.authority!==options.assets.authority.scope||binding.asset.url!==options.assets.sourceURL(id))throw fault('asset_binding_interpreter_unavailable',[id]); if(initial.loading?.version===6&&options.assets)for(const resource of initial.resources.values())if(resource.loadingAdmission?.sourceURL!==options.assets.sourceURL(resource.id))throw fault('asset_source_changed',[resource.id]); const editor = createEditor({ registry: options.registry, document: options.document, recovery: recovered, runtime: initial, createId: options.createId, historyLimit: options.historyLimit,queryBudget:options.queryBudget,queryCoverage:loadingQueryCoverage, policy(command, document) { if(!leader.active)return{enabled:false,code:'storage_leader_lost',targets:[]};if(options.isCurrentIdentity&&!options.isCurrentIdentity())return{enabled:false,code:'identity_changed',targets:[]};if (closing || closed || recovery) return { enabled: false, code: closing||closed?'engine_closing':'recovery_in_progress', targets: [] }; if(editor.getSnapshot().runtime.loading){const allowed=loadingCommand(command,document,options.registry,editor.getSnapshot().runtime.loading,editor.getSnapshot().runtime.loadingRead?.document??editor.getSnapshot().runtime.acknowledged,editor.getSnapshot().runtime.loadingRead,assetURLs());if(!allowed.enabled)return allowed;}return options.policy?.(command, document) ?? { enabled: true }; }, traversalPolicy(entry, direction) { if(!leader.active)return{enabled:false,code:'storage_leader_lost',targets:[]};if(options.isCurrentIdentity&&!options.isCurrentIdentity())return{enabled:false,code:'identity_changed',targets:[]};if(closing||closed||recovery)return{enabled:false,code:closing||closed?'engine_closing':'recovery_in_progress',targets:[]};if(editor.getSnapshot().runtime.loading&&(editor.getSnapshot().runtime.loading?.version??0)<4){try{assertLoadingChanges(entry.changes,editor.getSnapshot().document,options.registry,editor.getSnapshot().runtime.loading,editor.getSnapshot().runtime.loadingRead);}catch(error){return{enabled:false,code:(error as Error).message,targets:[]};}}return options.traversalPolicy?.(entry,direction)??{enabled:true}; }, onListenerError: report, prepareRuntime:(previous,next)=>{ if(!leader.active)throw fault('storage_leader_lost'); assertImportEvidence(previous.runtime.imports,next.runtime.imports,integrityOwner??importing?.job.id); if(!equal([...(previous.runtime.assetBindings??[])],[...(next.runtime.assetBindings??[])])){ const before=new Map(previous.runtime.assetBindings??[]),after=new Map(next.runtime.assetBindings??[]); const permitted=new Map(activeAdoptions().map(ref=>[ref.asset.id,ref]));if(after.size!equal(after.get(id),binding))||[...after].some(([id,binding])=>!before.has(id)&&(!permitted.has(id)||!equal(sharedAssetRef(binding),sharedAssetRef(permitted.get(id)!)))))throw fault('asset_bindings_owned'); } if(!acceptingLoading&&!recovery&&(previous.runtime.loading||next.runtime.loading||previous.runtime.loadingRead||next.runtime.loadingRead)&&(!equal(previous.runtime.loading,next.runtime.loading)||previous.runtime.loadingRead!==next.runtime.loadingRead||previous.runtime.acknowledged!==next.runtime.acknowledged||!equal(Object.fromEntries(previous.runtime.versions),Object.fromEntries(next.runtime.versions))))throw fault('loading_state_owned'); for(const id of next.runtime.assetBindings?.keys()??[])if(next.runtime.resources.has(id)||next.runtime.orphanFiles?.has(id)||importFiles(next.runtime.imports).has(id))throw fault('resource_identity_conflict',[id]); validateLoadingRuntime(next.runtime,options.registry); const sizing=prepareDefaultImageSizing(previous,next); const runtime=prepareResourceRetention({...next,runtime:sizing},options.registry,displayPins,collectionReservations,admissionPins(next)); const changed=previous.document!==next.document||previous.history!==next.history||previous.runtime.ledger!==runtime.ledger||previous.runtime.resourcePins!==runtime.resourcePins; const imports=prepareImportFileRetention(previous.runtime,runtime,()=>resourceRetentionPins({...next,runtime},options.registry,displayPins,admissionPins({...next,runtime})),collectionReservations,changed); return prepareOrphanFiles(previous.runtime,imports,()=>orphanCoverage({...next,runtime:imports},options.registry,displayPins,admissionPins({...next,runtime:imports})),collectionReservations,changed); }, prepareIntent: intent => applying && intent.origin==='user' ? {...intent,operationType:'run.apply',action:applying} : intent, nextOperationSequence(runtime, fallback) { let next = fallback; for (const record of runtime.ledger.operations.values()) next = Math.max(next, record.intent.sequence + 1); return next + staged.length; }, reduceRuntime(previous, next, commit) { if(previous.loading)assertLoadingChanges(commit.changes,previous.loading.version>=4?editor.getSnapshot().document:previous.acknowledged,options.registry,previous.loading,previous.loadingRead,assetURLs()); if (!commit.operationIds.length) return previous; const ledger = createLedger(previous.ledger), resources = new Map(previous.resources); const firstSequence = next.operations.get(commit.operationIds[0])!.sequence - staged.length; staged.forEach((resource, index) => { if (resources.has(resource.id) || ledger.getSnapshot().operations.has(resource.operationId)) throw fault('resource_identity_conflict', [resource.id]); const canvasIds=[...new Set([...next.document.elements.values()].filter(element=>referencedResources([{kind:'element',id:element.id,before:null,after:element}],options.registry).has(resource.id)).map(element=>element.ownerCanvasId))].sort(); const admitted=previous.loading?.version===6?freeze({...resource,loadingAdmission:{version:1 as const,sourceToken:previous.loading.sourceToken,sourceURL:options.assets!.sourceURL(resource.id),canvasIds}}):resource; resources.set(resource.id, stagedDefaultSizes.has(resource.id) ? captureDefaultImageSizing(admitted, next, commit, options.registry) : admitted); ledger.add(freeze({ id: resource.operationId, operationType: 'asset.upload', operationSchemaVersion: 1, sequence: firstSequence + index, origin: 'user', canvasIds: [], changes: [], dependencies: [], resourceId: resource.id }), {}); ledger.block(resource.operationId, ['waiting_digest']); }); for (const id of commit.operationIds) { const intent = next.operations.get(id)!, footprint = operationFootprint(intent, next.document); if (intent.historyEffect) { const effect = intent.historyEffect; for (const [resourceId, resource] of resources) if (resource.sizing?.version === 1) { const grants = resource.sizing.grants.map(grant => grant.status === 'pending' && grant.entryId === effect.entryId && grant.target.id === effect.target.id && grant.target.incarnation === effect.target.incarnation ? freeze({ ...grant, status: 'applied' as const, code: undefined }) : grant); if (grants.some((grant, index) => grant !== resource.sizing!.grants[index])) resources.set(resourceId, freeze({ ...resource, sizing: { version: 1 as const, grants } })); } } const dependencies = new Set(intent.dependencies); for (const record of ledger.getSnapshot().operations.values()) if (record.phase !== 'resolved' && (record.intent.operationType === 'run.start' || ['document.change','run.apply'].includes(record.intent.operationType) && conflictsWith(operationFootprint(record.intent, next.document), footprint))) dependencies.add(record.intent.id); for (const id of referencedResources(intent.changes, options.registry)) { const resource = resources.get(id); if (resource && !resource.stored) dependencies.add(resource.operationId); } const basePresence: Record = Object.create(null); for (const change of intent.changes) if (change.kind === 'element') basePresence[change.id] ??= change.before ? 'exists' : 'absent'; else if (change.kind === 'field') basePresence[change.target.id] ??= 'exists'; ledger.add(freeze({ ...intent, dependencies: [...dependencies] }), basePresence); } const assetBindings=new Map(previous.assetBindings??[]); for(const adopting of activeAdoptions()){ const id=adopting.asset.id,first=commit.operationIds.map(operation=>next.operations.get(operation)!).find(intent=>referencedResources(intent.changes,options.registry).has(id)); if(!first)throw fault('adopted_resource_unused',[id]); const existing=assetBindings.get(id);if(existing&&!equal(sharedAssetRef(existing),sharedAssetRef(adopting)))throw fault('shared_asset_identity_conflict',[id]); if(!existing){ const canvasIds=[...new Set([...next.document.elements.values()].filter(element=>referencedResources([{kind:'element',id:element.id,before:null,after:element}],options.registry).has(id)).map(element=>element.ownerCanvasId))].sort(); assetBindings.set(id,freeze({...adopting,operationId:first.id,...(previous.loading?.version===6?{loadingAdmission:{version:1 as const,sourceToken:previous.loading.sourceToken,sourceURL:adopting.asset.url,canvasIds,operationId:first.id}}:{})})); } } const imports=new Map(previous.imports??[]); if(importing){if(imports.get(importing.job.id)?.generation!==importing.expectedGeneration)throw fault('stale_import_job');imports.set(importing.job.id,importing.job);} return Object.freeze({ ...previous, imports:readMap(imports), ...(assetBindings.size?{assetBindings:readMap(assetBindings)}:{}), resources: readMap(resources), ledger: ledger.getSnapshot() }); }, }); const pending = new Map(); if (!recovered) pending.set(editor.getSnapshot().rootRevision, editor.getSnapshot()); let writing: Promise | undefined, scheduled = false, persistenceFailed = false; // Receipt-derived publication obligation survives a failed metadata publication. let confirmedLedgerGeneration=editor.getSnapshot().runtime.ledger.durableGeneration; function writePrefix(throughRevision:number):Promise { if (writing) return writing; const task = Promise.resolve().then(async () => { const publishDurability=()=>{if(editor.getSnapshot().runtime.ledger.durableGenerationledger.markDurable(confirmedLedgerGeneration));}; try { while (pending.size) { const [revision, snapshot] = pending.entries().next().value!; if(revision>throughRevision)break; const sourceFiles=importFiles(snapshot.runtime.imports); const files = [...fileWrites].filter(([id]) => snapshot.runtime.resources.has(id)||sourceFiles.has(id)).map(([id, bytes]) => ({ id, bytes })); const deleteFiles=[...[...snapshot.runtime.resources.values()].filter(resource=>resource.retirement?.purgedAt!==undefined&&!durablyPurged.has(resource.id)).map(resource=>resource.id),...[...sourceFiles].filter(([id,entry])=>entry.file.retirement?.purgedAt!==undefined&&!durablyPurged.has(id)).map(([id])=>id),...[...(snapshot.runtime.orphanFiles??[])].filter(([id,file])=>file.retirement?.purgedAt!==undefined&&!durablyPurged.has(id)).map(([id])=>id)]; const depot=options.assets?.authority&&options.storage.assetDepot?await prepareDepotCut({snapshot,registry:options.registry,storage:options.storage,leader,authority:options.assets.authority.scope,files:fileWrites,adoptedFiles,displays:displayPins,temporary:admissionPins(snapshot)}):undefined; const receipt = await options.storage.commit(leader, { image: await encodeRecoveryAsync(snapshot), files,deleteFiles,...(depot?{depot}:{}) }); if (receipt.durableRootRevision !== revision) throw fault('invalid_durable_receipt'); confirmedLedgerGeneration=Math.max(confirmedLedgerGeneration,snapshot.runtime.ledger.generation); durableRootRevision = revision;durableImportSeals=importSeals(snapshot.runtime.imports); pending.delete(revision);notifyDurability(); for(const id of deleteFiles){durablyPurged.add(id);collectionReservations.delete(id);} for (const file of files) fileWrites.delete(file.id); for(const [id,entry]of adoptedFiles)if(entry.rootRevision<=snapshot.rootRevision&&depot?.writes.some(write=>write.ref.asset.id===id&&write.bytes===entry.bytes))adoptedFiles.delete(id); } } catch(error) { // Preserve the known durable prefix even if a later write fails. publishDurability();throw error; } // One derived publication per captured prefix, not one per receipt. publishDurability(); persistenceFailed = false;durabilityError=undefined;notifyDurability(); }); writing = task.catch(error => { persistenceFailed = true;const failure=error&&typeof error==='object'?error as {code?:unknown;message?:unknown}:{};durabilityError=typeof failure.code==='string'&&failure.code?failure.code:typeof failure.message==='string'&&failure.message?failure.message:'persistence_failed';notifyDurability(); throw error; }).finally(() => { writing = undefined;if(!persistenceFailed){if(loadingCompletionRevision!==undefined&&durableRootRevision>=loadingCompletionRevision&&!editor.getSnapshot().runtime.loading){const release=loadingRelease;loadingRelease=undefined;loadingCompletionRevision=undefined;release?.();}if(!closing&&!closed)scheduler.refreshAdmission();} }); return writing; } function persistThrough(requiredRevision=editor.getSnapshot().rootRevision):Promise { if(durableRootRevision>=requiredRevision)return Promise.resolve(); return writePrefix(requiredRevision).then(()=>durableRootRevision { do {await writePrefix(editor.getSnapshot().rootRevision);} while(pending.size||writing||confirmedLedgerGeneration>editor.getSnapshot().runtime.ledger.durableGeneration); } function collectResources(settings:{retentionMs:number;maxFiles?:number}) { assertWritable(); if(options.storage.fileDeletion!=='atomic-v1')throw fault('resource_collection_unavailable'); const retentionMs=settings.retentionMs,maxFiles=settings.maxFiles??64; if(!Number.isSafeInteger(retentionMs)||retentionMs<0||!Number.isSafeInteger(maxFiles)||maxFiles<1||maxFiles>128)throw fault('invalid_resource_collection'); if(collection)return collection; const task=(async()=>{ await persist();assertWritable(); const now=runClock.now();if(!Number.isSafeInteger(now)||now<0)throw fault('invalid_resource_collection_clock'); let root=editor.getSnapshot(),pins=resourceRetentionPins(root,options.registry,displayPins,admissionPins(root)); const markedIds:Id[]=[],purgedIds:Id[]=[]; const report=()=>Object.freeze({markedIds:Object.freeze(markedIds),purgedIds:Object.freeze(purgedIds),complete:pins.complete,checkedRootRevision:root.rootRevision,rootRevision:editor.getSnapshot().rootRevision,durableRootRevision}); if(!pins.complete)return report(); const fresh=[...root.runtime.resources.values()].filter(resource=>!resource.retirement&&!pins.pins.has(resource.id)&&!fileWrites.has(resource.id)&&!digestTasks.has(resource.id)).slice(0,maxFiles); if(fresh.length){ const result=editor.updateRuntime(previous=>{const resources=new Map(previous.resources);for(const resource of fresh){resources.set(resource.id,freeze({...resource,retirement:{version:1 as const,unreferencedAt:now}}));markedIds.push(resource.id);}return Object.freeze({...previous,resources:readMap(resources)});},root.rootRevision); if(result.status==='rejected')throw fault(result.code,result.targets); await persist();assertWritable(); } root=editor.getSnapshot();pins=resourceRetentionPins(root,options.registry,displayPins,admissionPins(root)); if(!pins.complete)return report(); const eligible=[...root.runtime.resources.values()].filter(resource=>resource.retirement&&resource.retirement.purgedAt===undefined&&now-resource.retirement.unreferencedAt>=retentionMs&&!pins.pins.has(resource.id)&&!fileWrites.has(resource.id)&&!digestTasks.has(resource.id)).slice(0,maxFiles); if(eligible.length){ for(const resource of eligible)collectionReservations.add(resource.id); const result=editor.updateRuntime(previous=>{const resources=new Map(previous.resources);for(const resource of eligible)resources.set(resource.id,freeze({...resource,retirement:{...resource.retirement!,purgedAt:now}}));return Object.freeze({...previous,resources:readMap(resources)});},root.rootRevision); if(result.status==='rejected'){for(const resource of eligible)collectionReservations.delete(resource.id);throw fault(result.code,result.targets);} // Persistence derives byte deletions from this immutable root, even if a listener already started it. await persist();for(const resource of eligible)purgedIds.push(resource.id); } return report(); })(); collection=task.finally(()=>{collection=undefined;});return collection; } function collectImportSources(settings:{retentionMs:number;maxFiles?:number}):Promise { assertWritable();if(options.storage.fileDeletion!=='atomic-v1')throw fault('resource_collection_unavailable'); const retentionMs=settings.retentionMs,maxFiles=settings.maxFiles??64; if(!Number.isSafeInteger(retentionMs)||retentionMs<0||!Number.isSafeInteger(maxFiles)||maxFiles<1||maxFiles>128)throw fault('invalid_resource_collection'); if(sourceCollection)return sourceCollection; const task=(async()=>{ await persist();assertWritable(); const now=runClock.now();if(!Number.isSafeInteger(now)||now<0)throw fault('invalid_resource_collection_clock'); let root=editor.getSnapshot(),pins=resourceRetentionPins(root,options.registry,displayPins,admissionPins(root)); const markedIds:Id[]=[],purgedIds:Id[]=[]; const report=()=>Object.freeze({markedIds:Object.freeze(markedIds),purgedIds:Object.freeze(purgedIds),complete:pins.complete,checkedRootRevision:root.rootRevision,rootRevision:editor.getSnapshot().rootRevision,durableRootRevision}); const candidates=()=>[...importFiles(root.runtime.imports)].filter(([id,entry])=>!pins.pins.has(id)&&!fileWrites.has(id)&&(!entry.current||terminalImport(root.runtime.imports!.get(entry.jobId)!.status)&&!importLoads.has(entry.jobId))); if(!pins.complete)return report(); const fresh=candidates().filter(([,entry])=>!entry.file.retirement).slice(0,maxFiles); if(fresh.length){ const markers=new Map(fresh.map(([id])=>[id,{version:1 as const,unreferencedAt:now}])); const result=editor.updateRuntime(previous=>markImportFiles(previous,markers),root.rootRevision);if(result.status==='rejected')throw fault(result.code,result.targets); markedIds.push(...markers.keys());await persist();assertWritable(); } root=editor.getSnapshot();pins=resourceRetentionPins(root,options.registry,displayPins,admissionPins(root));if(!pins.complete)return report(); const eligible=candidates().filter(([,entry])=>entry.file.retirement&&entry.file.retirement.purgedAt===undefined&&now-entry.file.retirement.unreferencedAt>=retentionMs).slice(0,maxFiles); if(eligible.length){ const markers=new Map(eligible.map(([id,entry])=>[id,{...entry.file.retirement!,purgedAt:now}])); for(const id of markers.keys())collectionReservations.add(id); const result=editor.updateRuntime(previous=>markImportFiles(previous,markers),root.rootRevision); if(result.status==='rejected'){for(const id of markers.keys())collectionReservations.delete(id);throw fault(result.code,result.targets);} await persist();purgedIds.push(...markers.keys()); } return report(); })(); sourceCollection=task.finally(()=>{sourceCollection=undefined;});return sourceCollection; } async function openImportSource(jobId:Id,fileId?:Id) { if(closing||closed)throw fault('engine_closing');const job=getImport(jobId),file=fileId!==undefined?importFiles(new Map([[jobId,job]])).get(fileId)?.file:job.source;if(!file)throw fault('import_source_unavailable'); if(collectionReservations.has(file.fileId))throw fault('resource_collection_in_progress',[file.fileId]); if(file.retirement?.purgedAt!==undefined)throw fault('import_source_retired',[file.fileId]); displayPins.set(file.fileId,(displayPins.get(file.fileId)??0)+1);let released=false; const release=()=>{if(released)return;released=true;displays.delete(release);const count=displayPins.get(file.fileId)??1;if(count<=1)displayPins.delete(file.fileId);else displayPins.set(file.fileId,count-1);};displays.add(release); try{ if(file.retirement){const result=editor.updateRuntime(previous=>Object.freeze({...previous}));if(result.status==='rejected')throw fault(result.code,result.targets);} const bytes=fileWrites.get(file.fileId)??await options.storage.readFile(leader,file.fileId); if(closing||closed)throw fault('engine_closing');if(!bytes||bytes.size!==file.byteLength)throw fault('import_source_unavailable',[file.fileId]); if(file.sha256&&await sourceDigest(bytes)!==file.sha256)throw fault('import_source_changed',[file.fileId]); if(closing||closed)throw fault('engine_closing');return Object.freeze({bytes,release}); }catch(error){release();if(closing||closed)throw fault('engine_closing');throw error;} } async function scanLocalFiles(root:EngineSnapshot,after:Id|undefined,limit:number){ if(!options.storage.scanFiles)throw fault('file_inventory_unavailable'); const page=await options.storage.scanFiles(leader,{expectedRootRevision:root.rootRevision,after,limit}); if(closing||closed)throw fault('engine_closing');if(editor.getSnapshot()!==root)throw fault('stale_file_inventory'); let previous=after; if(page.rootRevision!==root.rootRevision||!Array.isArray(page.files)||page.files.length>limit)throw fault('invalid_file_inventory'); for(const file of page.files){if(!file.id||typeof file.id!=='string'||previous!==undefined&&file.id<=previous||!Number.isSafeInteger(file.byteLength)||file.byteLength<0||typeof file.mimeType!=='string')throw fault('invalid_file_inventory');previous=file.id;} if(page.nextAfter!==undefined&&(!page.files.length||page.nextAfter!==previous))throw fault('invalid_file_inventory');return page; } async function inventoryLocalFiles(settings:{after?:Id;limit?:number}={}){ assertWritable();const limit=settings.limit??128;if(!Number.isSafeInteger(limit)||limit<1||limit>128||settings.after!==undefined&&(typeof settings.after!=='string'||!settings.after))throw fault('invalid_file_inventory'); await persist();assertWritable();const root=editor.getSnapshot(),page=await scanLocalFiles(root,settings.after,limit),sources=importFiles(root.runtime.imports),pins=orphanCoverage(root,options.registry,displayPins,admissionPins(root)); return Object.freeze({rootRevision:root.rootRevision,complete:pins.complete,blockers:pins.blockers,nextAfter:page.nextAfter,files:Object.freeze(page.files.map(file=>{const resource=root.runtime.resources.get(file.id),source=sources.get(file.id),orphan=root.runtime.orphanFiles?.get(file.id);return Object.freeze({...file,ownership:resource?'resource' as const:source?'import-source' as const:orphan?'registered-orphan' as const:'unregistered' as const,...(source?{jobId:source.jobId,current:source.current}:{}),retired:(resource?.retirement??source?.file.retirement??orphan?.retirement)?.purgedAt!==undefined,pins:Object.freeze([...(pins.pins.get(file.id)??[])])});}))}); } function collectOrphanFiles(settings:{retentionMs:number;maxFiles?:number;after?:Id;pageSize?:number;maxHashBytes?:number;signal?:AbortSignal}){ assertWritable();if(options.storage.fileDeletion!=='atomic-v1'||!options.storage.scanFiles)throw fault('file_inventory_unavailable'); const retentionMs=settings.retentionMs,maxFiles=settings.maxFiles??64,pageSize=settings.pageSize??128,maxHashBytes=settings.maxHashBytes??64*1024*1024; if(!Number.isSafeInteger(retentionMs)||retentionMs<0||![maxFiles,pageSize].every(value=>Number.isSafeInteger(value)&&value>=1&&value<=128)||!Number.isSafeInteger(maxHashBytes)||maxHashBytes<1||settings.after!==undefined&&(typeof settings.after!=='string'||!settings.after))throw fault('invalid_file_inventory'); if(orphanCollection)return orphanCollection; const controller=new AbortController(),signal=settings.signal?AbortSignal.any([controller.signal,settings.signal]):controller.signal;orphanController=controller; const check=()=>{assertWritable();if(signal.aborted)throw fault('file_inventory_aborted');}; const task=(async()=>{ await persist();check();const now=runClock.now();if(!Number.isSafeInteger(now)||now<0)throw fault('invalid_resource_collection_clock'); let root=editor.getSnapshot(),pins=orphanCoverage(root,options.registry,displayPins,admissionPins(root)),scanned=0,nextAfter=settings.after,hashBudgetExhausted=false,scanFinished=false; const markedIds:Id[]=[],purgedIds:Id[]=[],changedIds:Id[]=[],verified=new Map(); const result=()=>Object.freeze({markedIds:Object.freeze(markedIds),purgedIds:Object.freeze(purgedIds),changedIds:Object.freeze(changedIds),complete:pins.complete,blockers:pins.blockers,checkedRootRevision:root.rootRevision,scanned,nextAfter,hashBudgetExhausted,scanFinished,durableRootRevision}); if(!pins.complete)return result(); const page=await abortableFileRead(scanLocalFiles(root,settings.after,pageSize),signal),sources=importFiles(root.runtime.imports);let hashed=0,finished=true; for(const file of page.files){ check();const registered=root.runtime.orphanFiles?.get(file.id),candidate=!root.runtime.resources.has(file.id)&&!sources.has(file.id)&®istered?.retirement?.purgedAt===undefined&&!pins.pins.has(file.id)&&!fileWrites.has(file.id); if(candidate){ if(verified.size>=maxFiles){finished=false;break;}if(file.byteLength>maxHashBytes-hashed){hashBudgetExhausted=true;finished=false;break;} const bytes=await abortableFileRead(options.storage.readFile(leader,file.id),signal);check();if(editor.getSnapshot()!==root)throw fault('stale_file_inventory'); if(!(bytes instanceof Blob)||bytes.size!==file.byteLength||bytes.type!==file.mimeType)throw fault('file_inventory_changed',[file.id]); const digest=await abortableFileRead(sourceDigest(bytes,signal),signal);check();if(editor.getSnapshot()!==root)throw fault('stale_file_inventory');hashed+=bytes.size; verified.set(file.id,freeze({version:1 as const,id:file.id,byteLength:file.byteLength,mimeType:file.mimeType,digest})); } scanned++;nextAfter=file.id; } if(finished)nextAfter=page.nextAfter;scanFinished=finished&&page.nextAfter===undefined; pins=orphanCoverage(root,options.registry,displayPins,admissionPins(root));if(!pins.complete)return result(); const updates=new Map(); for(const [id,file]of verified){if(pins.pins.has(id))continue;const old=root.runtime.orphanFiles?.get(id),changed=!!old&&(old.digest!==file.digest||old.byteLength!==file.byteLength||old.mimeType!==file.mimeType); if(!old?.retirement||changed){updates.set(id,freeze({...file,retirement:{version:1 as const,unreferencedAt:now}}));markedIds.push(id);if(changed)changedIds.push(id);} } if(updates.size){const update=editor.updateRuntime(previous=>{const files=new Map(previous.orphanFiles);for(const [id,file]of updates)files.set(id,file);return Object.freeze({...previous,orphanFiles:readMap(files)});},root.rootRevision);if(update.status==='rejected')throw fault(update.code,update.targets);await persist();check();} root=editor.getSnapshot();pins=orphanCoverage(root,options.registry,displayPins,admissionPins(root));if(!pins.complete)return result(); const eligible=[...verified].filter(([id,proof])=>{const file=root.runtime.orphanFiles?.get(id);return file?.retirement&&file.retirement.purgedAt===undefined&&now-file.retirement.unreferencedAt>=retentionMs&&file.digest===proof.digest&&file.byteLength===proof.byteLength&&file.mimeType===proof.mimeType&&!pins.pins.has(id)&&!root.runtime.resources.has(id)&&!importFiles(root.runtime.imports).has(id)&&!fileWrites.has(id);}); if(eligible.length){ for(const [id]of eligible)collectionReservations.add(id); const update=editor.updateRuntime(previous=>{const files=new Map(previous.orphanFiles);for(const [id]of eligible){const file=files.get(id)!;files.set(id,freeze({...file,retirement:{...file.retirement!,purgedAt:now}}));}return Object.freeze({...previous,orphanFiles:readMap(files)});},root.rootRevision); if(update.status==='rejected'){for(const [id]of eligible)collectionReservations.delete(id);throw fault(update.code,update.targets);} await persist();purgedIds.push(...eligible.map(([id])=>id)); } return result(); })(); orphanCollection=task.finally(()=>{orphanCollection=undefined;if(orphanController===controller)orphanController=undefined;});return orphanCollection; } function mutate(reduce: (ledger: ReturnType) => void) { const result = editor.updateRuntime(previous => { const ledger = createLedger(previous.ledger); reduce(ledger); return Object.freeze({ ...previous, ledger: ledger.getSnapshot() }); }); if (result.status === 'rejected') throw fault(result.code, result.targets); } function complete(request: FrozenRequest, status: OperationStatus): { readonly blocked: true } | void { const root = editor.getSnapshot(), previous = root.runtime, ledger = createLedger(previous.ledger); if (request.phase === 'resolved') return; if (status.outcome !== 'applied') { const result=editor.updateRuntime(runtime=>{const ledger=createLedger(runtime.ledger),receipts=new Map(runtime.receipts);ledger.result(request.id,status.outcome,status.error);if(status.outcome==='not-applied'&&['run.start','run.cancel'].includes(request.kind??''))ledger.resolveNoEffect(request.id);if(status.canonical!==undefined)receipts.set(request.id,freeze(status.canonical));return Object.freeze({...runtime,ledger:ledger.getSnapshot(),receipts:readMap(receipts)});},root.rootRevision); if(result.status==='rejected')throw fault(result.code,result.targets);return; } if(request.kind==='run.cancel'){ const receipts=new Map(previous.receipts);if(status.canonical!==undefined)receipts.set(request.id,freeze(status.canonical)); try{ if(!options.runs)throw fault('run_interpreter_unavailable'); const accepted=options.runs.acceptAction(request,status,root),run=previous.runs.get(accepted.runId);if(!run||!accepted.runStatus)throw fault('run_identity_mismatch'); ledger.result(request.id,'applied');const runs=new Map(previous.runs);runs.set(run.id,freeze({...run,controlEpoch:(run.controlEpoch??0)+1,status:accepted.runStatus,hasMore:true,blocked:false,error:undefined,nextPollAt:runClock.now()})); const result=editor.updateRuntime(()=>Object.freeze({...previous,runs:readMap(runs),receipts:readMap(receipts),ledger:ledger.getSnapshot()}),root.rootRevision);if(result.status==='rejected')throw fault(result.code,result.targets); queueMicrotask(()=>runTracker.notify([run.id]));return; }catch(error){ledger.result(request.id,'applied',(error as Error).message,false);const result=editor.updateRuntime(()=>Object.freeze({...previous,receipts:readMap(receipts),ledger:ledger.getSnapshot()}),root.rootRevision);if(result.status==='rejected')throw fault(result.code,result.targets);report(error);return{blocked:true};} } if (request.kind === 'asset.upload') { const receipts = new Map(previous.receipts); if (status.canonical !== undefined) receipts.set(request.id, freeze(status.canonical)); try { const intent = previous.ledger.operations.get(request.operationIds[0])?.intent; if (intent?.operationType !== 'asset.upload' || !options.assets) throw fault('asset_interpreter_unavailable'); const resource = previous.resources.get(intent.resourceId); if (!resource) throw fault('resource_missing'); const stored = freeze(options.assets.accept(request, status, resource)); if (stored.id !== resource.id || stored.bytes !== resource.byteLength) throw fault('asset_receipt_mismatch'); ledger.result(request.id, 'applied'); const resources = new Map(previous.resources); resources.set(resource.id, freeze({ ...resource, stored })); const result = editor.updateRuntime(() => Object.freeze({ ...previous, receipts: readMap(receipts), resources: readMap(resources), ledger: ledger.getSnapshot() }), root.rootRevision); if (result.status === 'rejected') throw fault(result.code, result.targets); return; } catch (error) { ledger.result(request.id, 'applied', (error as Error).message, false); const result = editor.updateRuntime(() => Object.freeze({ ...previous, receipts: readMap(receipts), ledger: ledger.getSnapshot() }), root.rootRevision); if (result.status === 'rejected') throw fault(result.code, result.targets); report(error); return { blocked: true }; } } const receipts = new Map(previous.receipts), conflicts = new Map(previous.conflicts); if (status.canonical !== undefined) receipts.set(request.id, freeze(status.canonical)); try { const action = request.kind==='run.apply' ? options.runs?.acceptAction(request,status,root) : undefined; if(request.kind==='run.apply'&&!action?.proposal)throw fault('run_interpreter_unavailable'); const admission = request.kind === 'run.start' ? options.runs?.accept(request, status, root) : undefined; if (request.kind === 'run.start' && !admission) throw fault('run_interpreter_unavailable'); const proposal = action?.proposal ?? admission?.proposal ?? options.persistence.canonical(request, status, root); const writtenCanvases = admission ? [request.ownerCanvasId!] : request.canvasIds; if (proposal.document.projectId !== root.document.projectId) throw fault('project_mismatch'); for (const canvas of writtenCanvases) { const version = proposal.versions.get(canvas); if (!version || version <= request.expectedVersions[canvas] || version < (previous.versions.get(canvas) ?? 0)) throw fault('stale_canonical_version', [canvas]); } // A codec cannot roll back an unrelated canvas while reconciling this request. for (const [canvas, version] of previous.versions) if (!writtenCanvases.includes(canvas)) { if (proposal.versions.get(canvas) !== version || !equal(proposal.document.canvases.get(canvas), previous.acknowledged.canvases.get(canvas)) || !equal([...proposal.document.elements.values()].filter(element => element.ownerCanvasId === canvas), [...previous.acknowledged.elements.values()].filter(element => element.ownerCanvasId === canvas))) throw fault('canonical_scope_mismatch', [canvas]); } ledger.result(request.id, 'applied'); const acknowledged = createDocument(proposal.document.projectId, [...proposal.document.canvases.values()], [...proposal.document.elements.values()], proposal.document.tombstones); const document = replayPending(acknowledged, ledger.getSnapshot(), options.registry); conflicts.delete(request.id); const runs = new Map(previous.runs); if (admission) { const existing = runs.get(admission.run.id); if (existing && (existing.requestId !== request.id || existing.operationId !== admission.run.operationId)) throw fault('run_identity_mismatch'); if (!existing) runs.set(admission.run.id, admission.run); } if(action){const run=runs.get(action.runId);if(!run)throw fault('run_identity_mismatch');runs.set(run.id,freeze({...run,controlEpoch:(run.controlEpoch??0)+1,appliedOutputIds:[...new Set([...(run.appliedOutputIds??[]),action.outputId!])],hasMore:true,blocked:false,error:undefined,pendingPage:undefined,nextPollAt:runClock.now()}));} const appliedVersions=readMap(proposal.versions);let resolvedReceipts=receipts; if(request.kind==='document.change'&&status.canonical!==undefined&&options.persistence.compactResolvedReceipt){ try{ const compact=options.persistence.compactResolvedReceipt(ledger.getSnapshot().requests.get(request.id)!,status.canonical,Object.freeze({document:acknowledged,versions:appliedVersions})); if(!validJson(compact))throw fault('invalid_receipt_compaction'); if(compact!==status.canonical){const owned=freeze(clone(compact));if(!validJson(owned))throw fault('invalid_receipt_compaction');resolvedReceipts=new Map(receipts);resolvedReceipts.set(request.id,owned);} }catch(error){report(error);} } // Keep the original map intact for a rejected final publication below. const runtime: RuntimeState = Object.freeze({ ...previous, runs: readMap(runs), ledger: ledger.getSnapshot(), acknowledged, versions: appliedVersions, receipts: readMap(resolvedReceipts), conflicts: readMap(conflicts) }); const oldAccepting=acceptingLoading;acceptingLoading=oldAccepting||(previous.loading?.version??0)>=3;let result:Result;try{result=editor.reconcile(document, runtime, root.rootRevision);}finally{acceptingLoading=oldAccepting;} if (result.status === 'rejected') throw fault(result.code, result.targets); if(action)queueMicrotask(()=>runTracker.notify([action.runId])); } catch (error) { // Preserve both the known receipt and the user's working document. No rollback or lane release. const waiting = createLedger(previous.ledger); waiting.result(request.id, 'applied', (error as Error).message, false); conflicts.set(request.id, (error as Error).message || 'reconcile_conflict'); const result = editor.updateRuntime(() => Object.freeze({ ...previous, ledger: waiting.getSnapshot(), receipts: readMap(receipts), conflicts: readMap(conflicts) }), root.rootRevision); if (result.status === 'rejected') throw fault(result.code, result.targets); report(error); return { blocked: true }; } } const scheduler = createScheduler({ admit:intent=>{const root=editor.getSnapshot();if(!importsMaySend(root.runtime.imports,durableImportSeals))return false;if(root.runtime.loading&&root.runtime.loading.version<3)return true;if(closing||closed||loadingCompletionRevision!==undefined&&durableRootRevision=root.rootRevision);},getLedger: () => editor.getSnapshot().runtime.ledger, mutate, persist:persistThrough, createId: options.createId ?? (() => crypto.randomUUID()), participants: intent => { if(intent.historyEffect)validateCreationSizeEffect(intent.historyEffect);return operationCanvases(intent, editor.getSnapshot().document); }, compile(intent, id) { if (intent.operationType === 'document.change') return options.persistence.compile(intent, id, editor.getSnapshot()); if (['run.start','run.cancel','run.apply'].includes(intent.operationType)) { if (!options.runs) throw fault('run_interpreter_unavailable');return options.runs.compile(intent,id,editor.getSnapshot()); } if(intent.operationType!=='asset.upload')throw fault('operation_interpreter_unavailable'); const resource = editor.getSnapshot().runtime.resources.get(intent.resourceId); if (!options.assets || !resource?.digest) throw fault('asset_not_ready'); return { kind: 'asset.upload', protocolVersion: 2, canvasIds: [], expectedVersions: {}, payload: { protocol_version: 2, request_id: id, resource_id: resource.id, operation_id: resource.operationId, sha256: resource.digest, byte_length: resource.byteLength, name: resource.name, mime_type: resource.mimeType } }; }, batchLimit(intents) { const snapshot = editor.getSnapshot(); return elisionBatchLimit(intents, snapshot.runtime.acknowledged, snapshot.runtime.ledger); }, elide(intents) { const snapshot = editor.getSnapshot(), proof = findElision(intents, snapshot.runtime.acknowledged, snapshot.runtime.ledger); if (!proof) return []; mutate(ledger => ledger.elide(proof)); return proof.operationIds; }, compileBatch: options.persistence.compileBatch ? (intents, id) => options.persistence.compileBatch!(intents, id, editor.getSnapshot()) : undefined, async submit(request, signal) { if (['run.start','run.cancel','run.apply'].includes(request.kind??'')) return options.runs ? network.run(signal, () => options.runs!.submit(request,signal)) : { outcome: 'unresolved', blocked: true, error: 'run_interpreter_unavailable' }; if (request.kind !== 'asset.upload') return network.run(signal, () => options.persistence.submit(request, signal)); if (!options.assets) return { outcome: 'unresolved', blocked: true, error: 'asset_interpreter_unavailable' }; const payload = JSON.parse(request.body), bytes = await options.storage.readFile(leader, payload.resource_id); if (!bytes || bytes.size !== payload.byte_length || await digest(bytes,signal) !== payload.sha256) return { outcome: 'unresolved', blocked: true, error: 'resource_bytes_unavailable' }; return network.run(signal, () => options.assets!.upload(request, bytes, signal)); }, query: (request, signal) => network.run(signal, () => ['run.start','run.cancel','run.apply'].includes(request.kind??'') ? options.runs?.query(request,signal) ?? Promise.resolve({ outcome: 'unresolved', blocked: true, error: 'run_interpreter_unavailable' }) : request.kind === 'asset.upload' ? options.assets?.query(request, signal) ?? Promise.resolve({ outcome: 'unresolved', blocked: true, error: 'asset_interpreter_unavailable' }) : options.persistence.query(request, signal)), supportsRequest: request => ['run.start','run.cancel','run.apply'].includes(request.kind??'') ? !!options.runs && request.protocolVersion === 2 : request.kind === 'asset.upload' ? !!options.assets && request.protocolVersion === 2 : options.persistence.supportsRequest?.(request) ?? request.protocolVersion === 1, complete, onError: report, clock: options.clock, }); if(initial.loading&&initial.loading.version<3)loadingRelease=scheduler.hold(); function completeLoading(document:Document,versions:ReadonlyMap,settings:{fresh?:boolean;sourceToken?:string}={}):Result{ try{assertWritable(false);const root=editor.getSnapshot(),previous=root.runtime;if(!previous.loading)throw fault('loading_not_active'); if(settings.fresh&&previous.loading.version>=3&&[...previous.ledger.requests.values()].some(request=>request.phase!=='resolved'))throw fault('loading_waiting_for_receipts'); if(settings.fresh&&((previous.loading.version===4||previous.loading.version===5||previous.loading.version===6))){const complete=new Set(loadingQueryCoverage(root).completeCanvasIds),capture=previous.loadingRead!; for(const [id,owner]of Object.entries(previous.loading.elementOwners))if(versions.get(owner)===capture.versions.get(owner)&&document.elements.get(id)?.ownerCanvasId!==owner)throw fault('canonical_version_mismatch',[owner]); for(const element of document.elements.values()){if(versions.get(element.ownerCanvasId)===capture.versions.get(element.ownerCanvasId)&&previous.loading.elementOwners[element.id]!==element.ownerCanvasId)throw fault('canonical_version_mismatch',[element.ownerCanvasId]);if(complete.has(element.ownerCanvasId)&&versions.get(element.ownerCanvasId)===previous.versions.get(element.ownerCanvasId)&&!equal(element,previous.acknowledged.elements.get(element.id)))throw fault('canonical_version_mismatch',[element.ownerCanvasId]);}} const source=!settings.fresh&&previous.loading.version>=3?previous.loadingRead:undefined;validateLoadingCompletion(source?.document??previous.acknowledged,source?.versions??previous.versions,previous.loading,document,versions,options.registry,settings.fresh,settings.sourceToken); const proposal=!settings.fresh?mergeLoadingRead(previous,{document,versions},options.registry):{document,versions}; const acknowledged=createDocument(document.projectId,[...proposal.document.canvases.values()],[...proposal.document.elements.values()],proposal.document.tombstones),working=replayPending(acknowledged,previous.ledger,options.registry),conflicts=new Map(previous.conflicts);conflicts.delete('loading');conflicts.delete('refresh'); acceptingLoading=true;const result=editor.reconcile(working,Object.freeze({...previous,loading:undefined,loadingRead:undefined,acknowledged,versions:readMap(proposal.versions),conflicts:readMap(conflicts)}),root.rootRevision); if(result.status==='applied')loadingCompletionRevision=editor.getSnapshot().rootRevision;return result; }catch(error){const result=reject(error);if(editor.getSnapshot().runtime.loading&&!['engine_closing','identity_changed','recovery_in_progress','loading_not_active','loading_waiting_for_receipts'].includes(result.code))editor.updateRuntime(previous=>{const conflicts=new Map(previous.conflicts);conflicts.set('loading',result.code);return Object.freeze({...previous,conflicts:readMap(conflicts)});});return result;}finally{acceptingLoading=false;} } function updateRun(id: Id, reduce: (run: RunRecord) => RunRecord) { const result = editor.updateRuntime(previous => { const current = previous.runs.get(id);if(!current)throw fault('run_missing');const runs = new Map(previous.runs);runs.set(id,freeze(reduce(current)));return Object.freeze({...previous,runs:readMap(runs)}); }); if(result.status==='rejected')throw fault(result.code,result.targets); } function acceptRunPage(run: RunRecord, page: RunPage) { const root=editor.getSnapshot(),previous=root.runtime,current=previous.runs.get(run.id); if(!current || current.requestId!==run.requestId)throw fault('run_identity_mismatch'); if((current.controlEpoch??0)!==(run.controlEpoch??0))return; if(current.cursor!==run.cursor)throw fault('run_identity_mismatch'); const outputEvents=new Map(current.outputEvents.map(event=>[event.step_id,event])); for(const event of page.events)if(event.type==='step.output')outputEvents.set(event.step_id,event); let acknowledged=previous.acknowledged,versions=previous.versions; const conflicts=new Map(previous.conflicts); try { for(const event of page.events) { if(event.seq<=current.cursor || event.type!=='step.applied'&&!(event.type==='step.output'&&event.model&&typeof event.model==='object'&&!Array.isArray(event.model)&&event.model.complete===true))continue; const canvas=event.board_id as Id,version=event.canvas_version as number; if(!Number.isSafeInteger(version)||version<1)throw fault('invalid_run_response'); // A later document receipt already includes this event; never rewind its participant. if(version<=(versions.get(canvas)??0))continue; const model=event.model as {[key:string]:Json}|undefined,context=model?.complete===true,participants=context?Object.keys(model.versions as {[key:string]:Json}):[canvas]; if([...previous.ledger.requests.values()].some(request=>request.phase!=='resolved'&&request.canvasIds.some(id=>participants.includes(id))))throw fault('run_waiting_for_receipts'); if(!options.runs)throw fault('run_interpreter_unavailable'); const snapshot={...root,runtime:{...previous,acknowledged,versions}}; const proposal=options.runs.canonical(event,snapshot); for(const record of previous.ledger.operations.values())if(record.phase!=='resolved')for(const change of record.intent.changes)if(change.kind==='field'&&(change.field==='geometry'||change.field==='path')){const before=acknowledged.elements.get(change.target.id),after=proposal.document.elements.get(change.target.id);if(before&&after&&before.ownerCanvasId!==after.ownerCanvasId)throw fault('ownership_conflict',[change.target.id]);} acknowledged=proposal.document;versions=proposal.versions; } const document=replayPending(acknowledged,previous.ledger,options.registry),runs=new Map(previous.runs); const appliedOutputIds=[...new Set([...(current.appliedOutputIds??[]),...page.events.filter(event=>event.type==='step.applied'&&typeof event.output_id==='string').map(event=>event.output_id as Id)])]; runs.set(run.id,freeze({...current,appliedOutputIds,cursor:page.next,status:page.status,hasMore:page.hasMore,outputs:page.outputs,outputEvents:[...outputEvents.values()],steps:page.steps,error:page.error,blocked:false,pendingPage:undefined})); conflicts.delete(`run:${run.id}`); const result=editor.reconcile(document,Object.freeze({...previous,acknowledged,versions:readMap(versions),runs:readMap(runs),conflicts:readMap(conflicts)}),root.rootRevision); if(result.status==='rejected')throw fault(result.code,result.targets); } catch(error) { const code=(error as Error).message; // Save the page as evidence without overwriting local content or advancing the cursor. const result=editor.updateRuntime(runtime=>{const runs=new Map(runtime.runs);runs.set(run.id,freeze({...current,outputs:page.outputs,outputEvents:[...outputEvents.values()],steps:page.steps,pendingPage:page,error:code,blocked:code!=='run_waiting_for_receipts'}));if(code!=='run_waiting_for_receipts')conflicts.set(`run:${run.id}`,code);return Object.freeze({...runtime,runs:readMap(runs),conflicts:readMap(conflicts)});},root.rootRevision); if(result.status==='rejected')throw fault(result.code,result.targets); if(code!=='run_waiting_for_receipts')report(error); } } const runTracker=createRunTracker({clock:runClock,getRuns:()=>editor.getSnapshot().runtime.runs,persist:persistThrough,update:updateRun,accept:acceptRunPage, poll:(run,signal)=>options.runs ? network.run(signal,()=>options.runs!.poll(run,signal)) : Promise.reject(fault('run_interpreter_unavailable')),onError:report}); const trackedRuns=new Set(initial.runs.keys()); const sessions = createSessions(editor); const unsubscribeSizing = sessions.subscribe(() => { if (!sessions.getSnapshot().gesture) queueDefaultSizes(); }); const unsubscribe = editor.subscribe((commit, snapshot) => { pending.set(snapshot.rootRevision, snapshot);notifyDurability(); const addedRuns=[...snapshot.runtime.runs.keys()].filter(id=>!trackedRuns.has(id));for(const id of addedRuns)trackedRuns.add(id); if(addedRuns.length)queueMicrotask(()=>runTracker.notify(addedRuns)); if (!scheduled) { scheduled = true; queueMicrotask(() => { scheduled = false; void persist().catch(report); }); } // Intents already exist in this root; notification only schedules their IDs. const operationIds = [...commit.operationIds, ...staged.map(resource => resource.operationId)]; queueMicrotask(() => { if (!closing && !closed) { scheduler.notify(operationIds); if (operationIds.length) prepareDigests(); }prepareImages(); }); }); async function digest(bytes:Blob,signal?:AbortSignal){return sourceDigest(bytes,signal);} function prepareDigests() { if (closing || closed) return; for (const resource of editor.getSnapshot().runtime.resources.values()) { if (digestTasks.size >= 2) break; if (resource.digest || digestTasks.has(resource.id)) continue; const operation = editor.getSnapshot().runtime.ledger.operations.get(resource.operationId); if (operation?.phase !== 'blocked' || !operation.blockers.includes('waiting_digest')) continue; const task = Promise.resolve().then(async () => { const bytes = fileWrites.get(resource.id) ?? await options.storage.readFile(leader, resource.id); if (!bytes || bytes.size !== resource.byteLength) throw fault('resource_bytes_unavailable', [resource.id]); const hash = await digest(bytes); if (closing || closed) return; const result = editor.updateRuntime(previous => { const current=previous.resources.get(resource.id);if(!current||current.operationId!==resource.operationId||current.byteLength!==resource.byteLength||current.mimeType!==resource.mimeType||current.digest!==undefined&¤t.digest!==hash||current.retirement?.purgedAt!==undefined)throw fault('resource_identity_conflict',[resource.id]); const resources = new Map(previous.resources), ledger = createLedger(previous.ledger); resources.set(resource.id, freeze({ ...current, digest: hash })); ledger.block(resource.operationId, []); return Object.freeze({ ...previous, resources: readMap(resources), ledger: ledger.getSnapshot() }); }); if (result.status === 'rejected') throw fault(result.code, result.targets); scheduler.notify([resource.operationId]); }).catch(error => { if (!closing && !closed) { const code=(error as {code?:string}).code;mutate(ledger => ledger.block(resource.operationId, [code&&digestFailures.has(code)?code:'resource_bytes_unavailable'])); report(error); } }).finally(() => { digestTasks.delete(resource.id); prepareDigests(); }); digestTasks.set(resource.id, task); } } function prepareImages(){ queueDefaultSizes(); if(closing||closed||recovery||!options.images)return; for(const resource of editor.getSnapshot().runtime.resources.values()){ if(imageTasks.size>=2)break; if(imageTasks.has(resource.id)||resource.image&&(resource.image.version!==1||resource.image.status!=='pending')||resource.retirement?.purgedAt!==undefined)continue; if(!resource.image){editor.updateRuntime(previous=>{const current=previous.resources.get(resource.id);if(!current||current.image)return previous;const resources=new Map(previous.resources);resources.set(resource.id,freeze({...current,image:{version:1 as const,status:'pending' as const}}));return Object.freeze({...previous,resources:readMap(resources)});});return;} const task=(async()=>{ const bytes=fileWrites.get(resource.id)??await options.storage.readFile(leader,resource.id); if(bytes&&bytes.size!==resource.byteLength||!bytes&&!resource.stored)throw fault('resource_bytes_unavailable',[resource.id]); const size=bytes?await options.images!.decode(bytes,digestLifecycle.signal):resource.stored; if(!size||!validImageDimensions(size))throw fault('invalid_image_dimensions');if(closing||closed||recovery)return; const result=editor.updateRuntime(previous=>{const current=previous.resources.get(resource.id);if(!current||current.operationId!==resource.operationId||current.image!==resource.image||current.retirement?.purgedAt!==undefined)return previous;const resources=new Map(previous.resources);resources.set(resource.id,freeze({...current,image:{version:1 as const,status:'ready' as const,width:size.width,height:size.height,source:bytes?'decode' as const:'server' as const}}));return Object.freeze({...previous,resources:readMap(resources)});}); if(result.status==='rejected')throw fault(result.code,result.targets); })().catch(error=>{if(closing||closed||recovery)return;const code=(error as{code?:string}).code??'image_decode_failed';editor.updateRuntime(previous=>{const current=previous.resources.get(resource.id);if(!current||current.operationId!==resource.operationId||current.image!==resource.image)return previous;const resources=new Map(previous.resources);resources.set(resource.id,freeze({...current,image:{version:1 as const,status:'failed' as const,code}}));return Object.freeze({...previous,resources:readMap(resources)});});}).finally(()=>{imageTasks.delete(resource.id);prepareImages();});imageTasks.set(resource.id,task); } } function queueDefaultSizes() { if (sizingTask || closing || closed || recovery || ![...editor.getSnapshot().runtime.resources.values()].some(resource => resource.sizing?.version === 1 && resource.sizing.grants.some(grant => grant.status === 'pending'))) return; // A scheduling yield, not a logical retry deadline: avoid a self-extending // microtask chain when one resource supplies many newly created elements. sizingTask = new Promise(resolve => { setTimeout(() => { sizingTask = undefined; try { prepareDefaultSizes(); } catch (error) { report(error); } finally { resolve(); } }, 0); }); } function prepareDefaultSizes() { if (closing || closed || recovery || sessions.getSnapshot().gesture) return; const root = editor.getSnapshot(); for (const [id, resource] of root.runtime.resources) { if (resource.sizing?.version !== 1 || resource.image && resource.image.version !== 1) continue; const size = resource.image?.version === 1 && resource.image.status === 'ready' ? resource.image : !options.images ? resource.stored : undefined; if (!size || !validImageDimensions(size)) continue; for (const grant of resource.sizing.grants) { if (grant.status !== 'pending') continue; const element = root.document.elements.get(grant.target.id); if (element?.structure !== 'node') continue; const units = unitScale(element.data.presentation), min = units, max = 100000 * units; const scale = Math.min(element.geometry.width / size.width, max / size.width, max / size.height), grow = Math.max(scale, min / size.width, min / size.height); const width = size.width * grow, height = size.height * grow; const result = !Number.isFinite(width) || !Number.isFinite(height) || width > max || height > max ? reject(fault('image_fit_constraint')) : editor.resolveCreationSize({ version: 1, kind: 'creation-size', entryId: grant.entryId, target: grant.target, size: { width: width / units, height: height / units } }, root.rootRevision); if (result.status === 'rejected' || !result.operationIds.length) { editor.updateRuntime(previous => { const current = previous.resources.get(id); if (current?.sizing?.version !== 1) return previous; const grants = current.sizing.grants.map(value => value !== grant ? value : freeze({ ...value, status: result.status === 'rejected' ? 'blocked' as const : 'applied' as const, code: result.status === 'rejected' ? result.code : undefined })); const resources = new Map(previous.resources); resources.set(id, freeze({ ...current, sizing: { version: 1 as const, grants } })); return Object.freeze({ ...previous, resources: readMap(resources) }); }); if (result.status === 'rejected') report(fault(result.code, result.targets)); } return; // One publication per pump; its notification schedules further grants. } } } function retryDefaultSizes() { editor.updateRuntime(previous => { const resources = new Map(previous.resources); let changed = false; for (const [id, resource] of resources) if (resource.sizing?.version === 1 && resource.sizing.grants.some(grant => grant.status === 'blocked')) { resources.set(id, freeze({ ...resource, sizing: { version: 1 as const, grants: resource.sizing.grants.map(grant => grant.status === 'blocked' ? freeze({ ...grant, status: 'pending' as const, code: undefined }) : grant) } })); changed = true; } return changed ? Object.freeze({ ...previous, resources: readMap(resources) }) : previous; }); queueDefaultSizes(); } function queueRunAction(operationType:'run.cancel'|'run.apply',action:RunCancelInput|RunApplyInput):Result { const root=editor.getSnapshot();let sequence=1;for(const record of root.runtime.ledger.operations.values())sequence=Math.max(sequence,record.intent.sequence+1); const id=createId(),intent={id,operationType,action,operationSchemaVersion:1,sequence,origin:'user',canvasIds:operationType==='run.apply'?[(action as RunApplyInput).targetCanvasId]:[],changes:[],dependencies:operationType==='run.cancel'?[]:[...root.runtime.ledger.operations.values()].filter(record=>record.phase!=='resolved').map(record=>record.intent.id)} as OperationIntent; const result=editor.updateRuntime(previous=>{const ledger=createLedger(previous.ledger);ledger.add(freeze(intent),{});return Object.freeze({...previous,ledger:ledger.getSnapshot()});},root.rootRevision); if(result.status==='rejected')return result;queueMicrotask(()=>{if(!closing&&!closed)scheduler.notify([id]);});return{status:'applied',revision:root.revision,operationIds:[id]}; } function assertImportWritable(){assertWritable(false);const loading=editor.getSnapshot().runtime.loading;if(loading&&loading.version<5)throw fault('model_loading');} function getImport(id:Id){const job=editor.getSnapshot().runtime.imports?.get(id);if(!job)throw fault('import_missing',[id]);return job;} function publishImport(job:ImportJob,expectedGeneration:number){ assertImportWritable();const permission=options.policy?.({type:'changes.apply',changes:[]},editor.getSnapshot().document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets); integrityOwner=job.id;try{const result=editor.updateRuntime(previous=>{const imports=new Map(previous.imports??[]);if(imports.get(job.id)?.generation!==expectedGeneration)throw fault('stale_import_job');imports.set(job.id,job.version===3?job:freeze({...job,version:3 as const}));return Object.freeze({...previous,imports:readMap(imports)});});if(result.status==='rejected')throw fault(result.code,result.targets);}finally{integrityOwner=undefined;} } function checkpointImportSource(job:ImportJob):ImportJob{ if(job.source?.integrityVersion!==2)return job; const context=proofReaders.get(job.id),current=editor.getSnapshot().runtime.imports?.get(job.id)?.source; if(!context||context.fileId!==job.source.fileId)throw fault('import_source_unavailable'); const {sha256:_,integrityError:__,...source}=job.source; return dataRecord(freeze({...job,source:{...source,readProof:context.reader.getProof(),...(current?.sha256?{sha256:current.sha256}:{}),...(current?.integrityError?{integrityError:current.integrityError}:{})}})); } function failImportVerification(job:ImportJob,error:unknown){ const code=(error as {code?:string}).code??'import_source_verification_failed'; publishImport(freeze({...job,generation:job.generation+1,status:['cancelled','partial'].includes(job.status)?job.status:'paused',source:{...job.source!,integrityError:code},error:code}),job.generation); const release=importRunners.get(job.id)?.hold();release?.();report(error); } function verifyImportSource(id:Id,context:{fileId:Id;bytes:Blob;reader:ReturnType}):Promise{ const active=verificationTasks.get(id);if(active)return active.controller.signal.aborted?active.promise.then(()=>{if(!closing&&!closed&&!importHolds.size&&proofReaders.get(id)===context&&sourceNeedsSeal(getImport(id)))return verifyImportSource(id,context);}):active.promise; const admitted=getImport(id),controller=new AbortController(),signal=AbortSignal.any([controller.signal,digestLifecycle.signal]); const current=()=>{signal.throwIfAborted();if(closing||closed||importHolds.size)throw fault('import_aborted');const job=getImport(id);if(job.sourceFingerprint!==admitted.sourceFingerprint||job.source?.fileId!==context.fileId||proofReaders.get(id)!==context)throw fault('import_identity_changed');return job;}; const promise=(async()=>{ let sha256:string; try{await context.reader.verify(signal);sha256=await abortableFileRead(sourceDigest(context.bytes,signal),signal);const job=current();if(job.source?.sha256&&job.source.sha256!==sha256)throw fault('import_source_changed');} catch(error){if(signal.aborted||closing||closed||importHolds.size)return;failImportVerification(current(),error);try{await persist();}catch{}return;} const job=current();if(job.status==='cancelled'&&!job.committedSourceKeys.length)return;const{integrityError:oldError,...source}=job.source!,next={...job,source:{...source,sha256,readProof:context.reader.getProof()}};if(oldError&&next.error===oldError)delete next.error; publishImport(freeze(next),job.generation);await persist(); })().catch(error=>{if(!signal.aborted&&!closing&&!closed)report(error);}).finally(()=>{if(verificationTasks.get(id)?.controller===controller)verificationTasks.delete(id);}); verificationTasks.set(id,{controller,promise});return promise; } async function resetImportReader(id:Id){ const active=verificationTasks.get(id);active?.controller.abort();if(active)await active.promise; const runner=importRunners.get(id);if(runner){await runner.close();importRunners.delete(id);} proofReaders.get(id)?.reader.close();proofReaders.delete(id); } function sourceNeedsSeal(job:ImportJob){return importNeedsSeal(job);} const deferredImportLoads=new Set(); function holdImports(){for(const id of importLoads.keys())if(!importRunners.has(id))deferredImportLoads.add(id);const token=Symbol(),releases=new Mapvoid>();importHolds.set(token,releases);for(const controller of preparationControllers.values())controller.abort();for(const {controller}of verificationTasks.values())controller.abort();for(const[id,runner]of importRunners)releases.set(id,runner.hold());return()=>{if(!importHolds.delete(token))return;for(const release of releases.values())release();if(!closing&&!closed)for(const job of editor.getSnapshot().runtime.imports?.values()??[])if(sourceNeedsSeal(job)||['running','preparing'].includes(job.status)&&!importRunners.has(job.id))void loadImport(job.id);};} async function materializeCopy(id:Id,bytes:Blob,controller:AbortController){ const admitted=getImport(id),preparation=admitted.preparation; if(!preparation||admitted.source?.kind!=='duplicate-snapshot')throw fault('invalid_copy_preparation'); const check=()=>{const job=getImport(id);if(controller.signal.aborted||closing||closed||importHolds.size||job.status!=='preparing'||job.sourceFingerprint!==admitted.sourceFingerprint)throw fault('copy_aborted');return job;}; await persist();check();const indexed=admitted.source.snapshotVersion===2?await openIndexedCopySnapshot(bytes,options.scope.projectId,options.registry,controller.signal):undefined,document=indexed?.document??await decodeCopySnapshot(bytes,options.scope.projectId,options.registry,controller.signal); validateLoadingCopySource(editor.getSnapshot(),admitted,document); if(copyRegistrySignature(document,options.registry)!==preparation.registrySignature)throw fault('copy_interpreter_changed'); if(preparation.version===3||preparation.version===4){ const state=await prepareDuplicateStream(document,options.registry,preparation.selection,{worldOffset:preparation.worldOffset,createId},{sourceToken:admitted.source.sha256!,schemaToken:preparation.registrySignature,checkpoint:preparation.planner,topologyOnly:!!indexed},{signal:controller.signal,checkpointIntervalMs:50,maxStepsPerCheckpoint:8192,checkpoint:async state=>{ const job=check(),previous=job.preparation!;if(previous.planner&&state.steps<=previous.steps)return; publishImport(freeze({...job,generation:job.generation+1,preparation:{...previous,steps:state.steps,allocationIds:[...state.elementIds,...state.canvasIds].map(pair=>pair[1]),planner:state}}),job.generation);await persist();check(); }}),job=check();assertNativeImportTarget(job,editor.getSnapshot().document); const stableIds=Object.fromEntries([...state.elementIds.map(([old,id])=>[`element:${old}`,id]),...state.canvasIds.map(([old,id])=>[`canvas:${old}`,id])]),next={...job,generation:job.generation+1,parserVersion:2,status:'running' as const,stableIds,source:{...job.source!,kind:'duplicate-plan' as const,plan:state,copy:{sourceRevision:job.source!.copy!.sourceRevision,roots:state.roots.map(old=>stableIds[`element:${old}`])}}};delete next.preparation;delete next.error; publishImport(freeze(next),job.generation);return{job:getImport(id),bytes,document,indexed}; } const allocationIds=[...preparation.allocationIds];let cursor=0; const plan=preparation.version===2?await planDuplicateResumable(document,options.registry,preparation.selection,{worldOffset:preparation.worldOffset,createId},{sourceToken:admitted.source.sha256!,schemaToken:preparation.registrySignature,checkpoint:preparation.planner},{signal:controller.signal,checkpoint:async state=>{ const job=check(),previous=job.preparation!;if(previous.planner&&state.steps<=previous.steps)return; publishImport(freeze({...job,generation:job.generation+1,preparation:{...previous,steps:state.steps,allocationIds:[...state.elementIds,...state.canvasIds].map(pair=>pair[1]),planner:state}}),job.generation);await persist();check(); }}):await planDuplicateAsync(document,options.registry,preparation.selection,{worldOffset:preparation.worldOffset,createId:()=>{const index=cursor++;if(index{ const job=check(),previous=job.preparation!; if(steps<=previous.steps&&allocationIds.length===previous.allocationIds.length)return; publishImport(freeze({...job,generation:job.generation+1,preparation:{...previous,steps:Math.max(previous.steps,steps),allocationIds:[...allocationIds]}}),job.generation);await persist();check(); }}); check();if(preparation.version===1&&cursor!==allocationIds.length)throw fault('copy_interpreter_changed'); const encoded=await encodeDuplicateSource(document,options.registry,plan,controller.signal),sha256=await sourceDigest(encoded,controller.signal),job=check();assertImportWritable();assertNativeImportTarget(job,editor.getSnapshot().document); const permission=options.policy?.({type:'changes.apply',changes:plan.changes},editor.getSnapshot().document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets); const fileId=createId(),runtime=editor.getSnapshot().runtime;if(!fileId||fileWrites.has(fileId)||runtime.resources.has(fileId)||[...(runtime.imports?.values()??[])].some(job=>job.source?.fileId===fileId))throw fault('import_identity_conflict'); const stableIds=Object.fromEntries([...Object.entries(plan.elementIds).map(([old,id])=>[`element:${old}`,id]),...Object.entries(plan.canvasIds).map(([old,id])=>[`canvas:${old}`,id])]); const next={...job,generation:job.generation+1,status:'running' as const,stableIds,sourceFingerprint:`local-file:${fileId}:${encoded.size}`,source:{...job.source!,previousFiles:[...(job.source!.previousFiles??[]),{fileId:job.source!.fileId,byteLength:job.source!.byteLength,...(job.source!.sha256?{sha256:job.source!.sha256}:{})}],kind:'duplicate-ndjson' as const,fileId,byteLength:encoded.size,sha256,integrityVersion:1 as const,copy:{sourceRevision:job.source!.copy!.sourceRevision,roots:plan.roots}}};delete next.preparation;delete next.error; fileWrites.set(fileId,encoded);try{publishImport(freeze(next),job.generation);}catch(error){fileWrites.delete(fileId);throw error;} return{job:getImport(id),bytes:encoded,document:undefined,indexed:undefined}; } function loadImport(id:Id):Promise{ if(importLoads.has(id))return importLoads.get(id)!; if(importRunners.has(id)){const context=proofReaders.get(id);if(context&&sourceNeedsSeal(getImport(id)))void verifyImportSource(id,context);if(getImport(id).status==='running')void importRunners.get(id)!.start();return Promise.resolve();} deferredImportLoads.delete(id); let expectedFingerprint=getImport(id).sourceFingerprint; const preparationController=new AbortController();if(getImport(id).source?.integrityVersion===2||['native-document','duplicate-snapshot','duplicate-plan'].includes(getImport(id).source?.kind??''))preparationControllers.set(id,preparationController); const task=(async()=>{ let job=getImport(id),source=job.source;if(!source||!['native-document','native-json-array','native-ndjson','duplicate-ndjson','duplicate-snapshot','duplicate-plan'].includes(source.kind)||(source.kind==='native-document'?![4,5].includes(job.parserVersion):job.parserVersion!==(source.kind==='native-json-array'?3:source.kind==='duplicate-plan'?2:1)))throw fault('import_interpreter_unavailable'); let bytes=fileWrites.get(source.fileId)??await abortableFileRead(options.storage.readFile(leader,source.fileId),preparationController.signal); if(closing||closed)return; const latest=getImport(id);if(latest.sourceFingerprint!==job.sourceFingerprint||latest.source?.fileId!==source.fileId||['completed','partial','cancelled'].includes(latest.status)&&(source.integrityVersion!==2||!sourceNeedsSeal(latest)))return; if(!bytes)throw fault('import_source_unavailable');if(bytes.size!==source.byteLength)throw fault('import_source_changed'); let readBytes:ReturnType['port']|undefined; if(source.integrityVersion===2){ let context=proofReaders.get(id);if(!context||context.fileId!==source.fileId){context={fileId:source.fileId,bytes,reader:createImportReadProof({bytes,fileId:source.fileId,proof:source.readProof})};proofReaders.set(id,context);} await context.reader.verify(preparationController.signal);const checking=verifyImportSource(id,context);if(source.sha256)await checking; job=getImport(id);source=job.source!;if(preparationController.signal.aborted||['completed','partial','cancelled','paused'].includes(job.status)||importHolds.size)return;readBytes=context.reader.port; }else{ if(source.integrityVersion!==1)throw fault('import_source_integrity_unavailable'); const sha256=await sourceDigest(bytes,preparationController.signal); if(closing||closed)return; const verified=getImport(id);if(verified.generation!==latest.generation||verified.sourceFingerprint!==job.sourceFingerprint||verified.source?.fileId!==source.fileId||['completed','partial','cancelled','paused'].includes(verified.status))return; if(importHolds.size){deferredImportLoads.add(id);return;} if(source.sha256&&source.sha256!==sha256)throw fault('import_source_changed'); if(!source.sha256){publishImport(freeze({...verified,generation:verified.generation+1,source:{...source,sha256}}),verified.generation);job=getImport(id);source=job.source!;}else job=verified; } let copyDocument:Document|undefined,indexed:IndexedCopySnapshot|undefined; if(source.kind==='duplicate-snapshot'){if(preparationController.signal.aborted||importHolds.size||latest.status!=='preparing')return;const materialized=await materializeCopy(id,bytes,preparationController);job=materialized.job;source=job.source!;bytes=materialized.bytes;copyDocument=materialized.document;indexed=materialized.indexed;expectedFingerprint=job.sourceFingerprint;} if(source.kind==='duplicate-plan'&&source.snapshotVersion===2){indexed??=await openIndexedCopySnapshot(bytes,options.scope.projectId,options.registry,preparationController.signal);copyDocument=indexed.document;} if(source.kind==='duplicate-plan'&©Document)validateLoadingCopySource(editor.getSnapshot(),job,copyDocument); const importSource=source.kind==='duplicate-plan'?createDuplicatePlanSource(copyDocument??await decodeCopySnapshot(bytes,options.scope.projectId,options.registry,preparationController.signal),options.registry,source.plan!,job.sourceFingerprint,indexed?.readElement):source.kind==='native-document'?await (job.parserVersion===5?openStreamingDocumentImportSource:openDocumentImportSource)({bytes,readBytes,fingerprint:job.sourceFingerprint,signal:preparationController.signal}):source.kind==='native-json-array'?createJsonArrayImportSource({bytes,readBytes,fingerprint:job.sourceFingerprint}):createNdjsonImportSource({bytes,readBytes,fingerprint:job.sourceFingerprint}); if(closing||closed||preparationController.signal.aborted||getImport(id).generation!==job.generation)return; if(importHolds.size){deferredImportLoads.add(id);return;} const authorizedImportAssets=new Map(),assetPreparationSignals=new WeakSet(); // Ordinary-batch ceiling only; commit timing and indivisible SCC limits still apply. const runner=createImportRunner({maxBatch:128,optimisticLocal:true,checkpointSource:source.integrityVersion===2?checkpointImportSource:undefined,awaitDurabilityBudget:stage=>pending.size>=32||stage!=='parsed'&&persistenceFailed?persist():undefined,source:importSource,getJob:()=>getImport(id),createId, publish:publishImport,prepare:(line,job,reserve)=>['duplicate-ndjson','duplicate-plan'].includes(job.source?.kind??'')?prepareDuplicateImport(line,job,reserve,options.registry):prepareNativeImport(line,job,reserve,editor.getSnapshot().document,options.registry,depot&&options.assets?.authority?options.assets.sourceURL:undefined),persist,onError:report, async beforeCommit(units,job,signal){ authorizedImportAssets.clear();if(job.source?.kind!=='native-document')return{enabled:true}; const ids=referencedResources(units.flatMap(unit=>unit.changes),options.registry);if(!ids.size)return{enabled:true};if(editor.getSnapshot().runtime.loading&&editor.getSnapshot().runtime.loading?.version!==6)return{enabled:false,code:'model_loading',targets:[]}; if(!depot||!options.assets?.authority||!options.storage.assetDepot)return{enabled:false,code:'asset_depot_unavailable',targets:[]}; if(!assetPreparationSignals.has(signal)){assetPreparationSignals.add(signal);signal.addEventListener('abort',()=>authorizedImportAssets.clear(),{once:true});}let total=0; try{for(const id of ids){const authorized=await depot.authorize(leader,{scope:options.assets.authority.scope,resolve:(id,abort)=>network.run(abort,()=>options.assets!.authority!.resolve(id,abort)),read:(id,abort)=>network.run(abort,()=>options.assets!.authority!.read(id,abort))},id,signal);signal.throwIfAborted();assertImportWritable();if(authorized.ref.asset.url!==options.assets.sourceURL(id))throw fault('asset_authorization_mismatch');total+=authorized.bytes.size;if(total>32*1024*1024)throw fault('import_asset_capacity');authorizedImportAssets.set(id,authorized);}return{enabled:true};}catch(error){authorizedImportAssets.clear();return{enabled:false,code:(error as {code?:string}).code??'import_asset_authorization_failed',targets:[]};} }, commit(units,next,expected){ const preparedAssets=new Map(authorizedImportAssets);authorizedImportAssets.clear(); assertImportWritable();if(importing)throw fault('reentrant_dispatch');const current=getImport(id);if(current.generation!==expected)throw fault('stale_import_job'); try{assertNativeImportTarget(current,editor.getSnapshot().document);for(const unit of units){assertNativeImportDependencies(current,editor.getSnapshot().document,unit.dependencies);if(['duplicate-ndjson','duplicate-plan'].includes(current.source?.kind??''))assertDuplicateExternal({cursor:0,nextCursor:1,key:unit.key,value:unit.guard},current,editor.getSnapshot().document,options.registry);}}catch(error){return reject(error);} const previousFiles=new Map();let accepted=false; try{for(const [assetId,authorized]of preparedAssets){const runtime=editor.getSnapshot().runtime,own=runtime.resources.get(assetId);if(own){if(!own.stored||own.digest!==authorized.ref.digest||!equal(own.stored,authorized.ref.asset))throw fault('shared_asset_identity_conflict',[assetId]);continue;}if(runtime.orphanFiles?.has(assetId)||importFiles(runtime.imports).has(assetId)||fileWrites.has(assetId)||collectionReservations.has(assetId))throw fault('resource_identity_conflict',[assetId]);previousFiles.set(assetId,adoptedFiles.get(assetId));adoptedFiles.set(assetId,{bytes:authorized.bytes,rootRevision:Number.MAX_SAFE_INTEGER});}importAdoptions=[...preparedAssets].filter(([assetId])=>!editor.getSnapshot().runtime.resources.has(assetId)).map(([,value])=>value.ref);}catch(error){for(const [assetId,previous]of previousFiles)if(previous)adoptedFiles.set(assetId,previous);else adoptedFiles.delete(assetId);authorizedImportAssets.clear();return reject(error);} importing={job:next,expectedGeneration:expected}; try{const result=editor.transaction(tx=>{for(const unit of units)for(const change of unit.changes){if(change.kind==='element'&&change.before===null&&change.after)tx.dispatch({type:'element.create',element:change.after});else if(change.kind==='canvas'&&change.before===null&&change.after)tx.dispatch({type:'canvas.create',canvas:change.after});else throw fault('invalid_native_import_unit');}},{label:['duplicate-ndjson','duplicate-plan'].includes(current.source?.kind??'')?'Duplicate selection':'Import canvas content',groupId:`import:${id}`});accepted=result.status==='applied';if(accepted)for(const assetId of previousFiles.keys())adoptedFiles.set(assetId,{bytes:preparedAssets.get(assetId)!.bytes,rootRevision:editor.getSnapshot().rootRevision});return result;}finally{importing=undefined;importAdoptions=[];if(!accepted)for(const [assetId,previous]of previousFiles)if(previous)adoptedFiles.set(assetId,previous);else adoptedFiles.delete(assetId);authorizedImportAssets.clear();} }, }); importRunners.set(id,runner);for(const releases of importHolds.values())releases.set(id,runner.hold()); if(getImport(id).status==='running')void runner.start(); })().catch(error=>{if(preparationController.signal.aborted)return;if(!closing&&!closed){try{const job=getImport(id);if(job.source?.integrityVersion===2&&!importRunners.has(id)&&!verificationTasks.has(id)){proofReaders.get(id)?.reader.close();proofReaders.delete(id);}if(job.sourceFingerprint!==expectedFingerprint)return;if(job.source?.integrityVersion===2&&(isImportByteFailure(error)||(error as {code?:string}).code==='import_source_unavailable')){failImportVerification(job,error);void persist().catch(report);return;}if(['completed','partial','cancelled','paused'].includes(job.status))return;publishImport(freeze({...job,generation:job.generation+1,status:'paused',error:(error as {code?:string}).code??'import_failed'}),job.generation);}catch{/* Recovery/identity fences retain the original job. */}report(error);}}).finally(()=>{preparationControllers.delete(id);importLoads.delete(id);if(!['running','preparing'].includes(getImport(id).status))deferredImportLoads.delete(id);if(!closing&&!closed&&!importHolds.size&&deferredImportLoads.delete(id)&&!importRunners.has(id)&&['running','preparing'].includes(getImport(id).status))queueMicrotask(()=>{if(!closing&&!closed)void loadImport(id);});}); importLoads.set(id,task);return task; } function startNativeImport(bytes:Blob,targetCanvasId:Id,format:'ndjson'|'json-array'|'document'):Id{ if(!['ndjson','json-array','document'].includes(format))throw fault('import_interpreter_unavailable'); assertImportWritable();const loadingAdmission=loadingImportAdmission(editor.getSnapshot(),targetCanvasId);const permission=options.policy?.({type:'changes.apply',changes:[]},editor.getSnapshot().document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets);const canvas=editor.getSnapshot().document.canvases.get(targetCanvasId);if(!canvas)throw fault('canvas_not_loaded',[targetCanvasId]);if(!(bytes instanceof Blob)||bytes.size>64*1024*1024)throw fault('invalid_import_source'); const id=createId(),fileId=createId(),current=editor.getSnapshot().runtime;if(!id||!fileId||id===fileId||current.imports?.has(id)||current.resources.has(fileId)||current.orphanFiles?.has(fileId)||fileWrites.has(fileId)||importFiles(current.imports).has(fileId))throw fault('import_identity_conflict'); const entry=canvas.parent?editor.getSnapshot().document.elements.get(canvas.parent.entryElementId)!:undefined; const job=freeze({...createImportJob({id,projectId:options.scope.projectId,sourceFingerprint:`local-file:${fileId}:${bytes.size}`,parserVersion:format==='document'?5:format==='json-array'?3:1,source:{...(loadingAdmission?{loadingAdmission}:{}),integrityVersion:2,readProof:{version:1,fileId,byteLength:bytes.size,blockBytes:IMPORT_PROOF_BLOCK_BYTES,blocks:[]},kind:format==='document'?'native-document':format==='json-array'?'native-json-array':'native-ndjson',name:((bytes as Blob&{name?:string}).name||'画布数据').slice(0,255),fileId,byteLength:bytes.size,targetCanvasId,...(entry?{targetEntry:{id:entry.id,incarnation:entry.incarnation}}:{})}}),status:'running' as const}); fileWrites.set(fileId,bytes); integrityOwner=id;try{const result=editor.updateRuntime(previous=>{const jobs=new Map(previous.imports??[]);jobs.set(id,job);return Object.freeze({...previous,imports:readMap(jobs)});}); if(result.status==='rejected'){fileWrites.delete(fileId);throw fault(result.code,result.targets);}void loadImport(id);return id;}finally{integrityOwner=undefined;} } const imports=Object.freeze({ getJobs:()=>readMap(editor.getSnapshot().runtime.imports??new Map()), canStart(targetCanvasId:Id){try{assertImportWritable();const snapshot=editor.getSnapshot(),permission=options.policy?.({type:'changes.apply',changes:[]},snapshot.document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets);if(!snapshot.document.canvases.has(targetCanvasId))throw fault('canvas_not_loaded',[targetCanvasId]);loadingImportAdmission(snapshot,targetCanvasId);return{enabled:true as const};}catch(error){const result=reject(error);return{enabled:false as const,code:result.code,targets:result.targets};}}, canDuplicate(selection:readonly Target[],settings:{viewCanvasId?:Id}={}){try{assertImportWritable();if(collectionReservations.size)throw fault('resource_collection_in_progress',[...collectionReservations]);const snapshot=editor.getSnapshot(),permission=options.policy?.({type:'changes.apply',changes:[]},snapshot.document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets);if(!selection.length)throw fault('empty_copy_selection');for(const selected of selection){const element=snapshot.document.elements.get(selected.id);if(!element||element.incarnation!==selected.incarnation)throw fault('stale_incarnation',[selected.id]);}const target=settings.viewCanvasId??snapshot.document.elements.get(selection[0].id)!.ownerCanvasId;if(!snapshot.document.canvases.has(target))throw fault('canvas_not_loaded',[target]);loadingImportAdmission(snapshot,target,selection);return{enabled:true as const};}catch(error){const result=reject(error);return{enabled:false as const,code:result.code,targets:result.targets};}}, openSource:openImportSource, startNdjson:(bytes:Blob,targetCanvasId:Id)=>startNativeImport(bytes,targetCanvasId,'ndjson'), startDocument:(bytes:Blob,targetCanvasId:Id)=>startNativeImport(bytes,targetCanvasId,'document'), startJsonArray:(bytes:Blob,targetCanvasId:Id)=>startNativeImport(bytes,targetCanvasId,'json-array'), async duplicate(selection:readonly Target[],settings:{readonly worldOffset?:Point;readonly signal?:AbortSignal;readonly viewCanvasId?:Id}={}):Promise{ assertImportWritable();if(collectionReservations.size)throw fault('resource_collection_in_progress',[...collectionReservations]); const permission=options.policy?.({type:'changes.apply',changes:[]},editor.getSnapshot().document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets); if([...editor.getSnapshot().runtime.resources.values(),...(editor.getSnapshot().runtime.orphanFiles?.values()??[])].some(file=>file.retirement&&file.retirement.purgedAt===undefined)){ const reset=editor.updateRuntime(previous=>{const resources=new Map(previous.resources);for(const [id,resource]of resources)if(resource.retirement&&resource.retirement.purgedAt===undefined){const {retirement:_,...kept}=resource;resources.set(id,freeze(kept));}const files=new Map(previous.orphanFiles);for(const [id,file]of files)if(file.retirement&&file.retirement.purgedAt===undefined){const {retirement:_,...kept}=file;files.set(id,freeze(kept));}return Object.freeze({...previous,resources:readMap(resources),...(previous.orphanFiles?{orphanFiles:readMap(files)}:{})});});if(reset.status==='rejected')throw fault(reset.code,reset.targets); } const captured=editor.getSnapshot(),target=settings.viewCanvasId??(selection[0]?captured.document.elements.get(selection[0].id)?.ownerCanvasId:undefined),signal=settings.signal,controller=new AbortController(),abort=()=>controller.abort(); const loadingAdmission=target?loadingImportAdmission(captured,target,selection):undefined; const targets=freeze(clone(selection)),offset=freeze(clone(settings.worldOffset??{x:32,y:32})); for(const selected of targets){const element=captured.document.elements.get(selected.id);if(!element||element.incarnation!==selected.incarnation)throw fault('stale_incarnation',[selected.id]);}if(![offset.x,offset.y].every(Number.isFinite))throw fault('invalid_copy_offset'); signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)controller.abort(); const task=(async()=>{ if(!target||!targets.length)throw fault('empty_copy_selection'); const bytes=await encodeIndexedCopySnapshot(captured.document,controller.signal);assertImportWritable();if(controller.signal.aborted)throw fault('copy_aborted'); const canvas=captured.document.canvases.get(target);if(!canvas)throw fault('canvas_not_loaded',[target]); const id=createId(),fileId=createId(),current=editor.getSnapshot().runtime; if(!id||!fileId||id===fileId||current.imports?.has(id)||current.resources.has(fileId)||current.orphanFiles?.has(fileId)||fileWrites.has(fileId)||importFiles(current.imports).has(fileId))throw fault('import_identity_conflict'); const entry=canvas.parent?captured.document.elements.get(canvas.parent.entryElementId)!:undefined; const job=freeze({...createImportJob({id,projectId:options.scope.projectId,sourceFingerprint:`local-file:${fileId}:${bytes.size}`,parserVersion:1,source:{...(loadingAdmission?{loadingAdmission}:{}),kind:'duplicate-snapshot',snapshotVersion:2,name:'复制选中内容',fileId,byteLength:bytes.size,targetCanvasId:target,copy:{sourceRevision:captured.revision,roots:[]},...(entry?{targetEntry:{id:entry.id,incarnation:entry.incarnation}}:{})}}),status:'preparing' as const,preparation:{version:4 as const,registrySignature:copyRegistrySignature(captured.document,options.registry),selection:targets,worldOffset:offset,allocationIds:[],steps:0}}); fileWrites.set(fileId,bytes);const result=editor.updateRuntime(previous=>{const jobs=new Map(previous.imports??[]);jobs.set(id,job);return Object.freeze({...previous,imports:readMap(jobs)});}); if(result.status==='rejected'){fileWrites.delete(fileId);throw fault(result.code,result.targets);}void loadImport(id);return id; })(); copyPreparations.set(controller,task);void task.finally(()=>{copyPreparations.delete(controller);signal?.removeEventListener('abort',abort);}).catch(()=>{});return task; }, async pause(id:Id){assertImportWritable();const runner=importRunners.get(id);if(runner)await runner.pause();else{const job=getImport(id);if(['running','preparing'].includes(job.status)){if(job.source?.integrityVersion!==2)preparationControllers.get(id)?.abort();publishImport(freeze({...job,generation:job.generation+1,status:'paused'}),job.generation);await persist();}}}, async resume(id:Id){assertImportWritable();const job=getImport(id);if(['completed','partial','cancelled'].includes(job.status))return;if(['running','preparing'].includes(job.status)){await loadImport(id);return;}await importLoads.get(id);await importRunners.get(id)?.settle();if(getImport(id).source?.integrityVersion===2)await resetImportReader(id);await persist();await scheduler.resume();assertImportWritable();const current=getImport(id);if(current.status!=='paused')return;const next={...current,generation:current.generation+1,status:current.source?.kind==='duplicate-snapshot'?'preparing' as const:'running' as const};delete next.error;publishImport(freeze(next),current.generation);await loadImport(id);}, async cancel(id:Id){assertImportWritable();const runner=importRunners.get(id);if(runner)await runner.cancel();else{const job=getImport(id);if(['running','preparing','paused'].includes(job.status)){if(job.source?.integrityVersion!==2)preparationControllers.get(id)?.abort();const next={...job,generation:job.generation+1,status:'cancelled' as const};delete next.error;publishImport(freeze(next),job.generation);await persist();}}if(!sourceNeedsSeal(getImport(id)))verificationTasks.get(id)?.controller.abort();}, }); scheduler.start(); prepareDigests();prepareImages(); runTracker.notify([...initial.runs.keys()]); for(const job of initial.imports?.values()??[])if(sourceNeedsSeal(job)||['running','preparing'].includes(job.status))void loadImport(job.id); return Object.freeze({ editor, sessions, imports, loading:Object.freeze({getState:()=>editor.getSnapshot().runtime.loading, resume():Promise{ if(loadingRead)return loadingRead; const task=(async()=>{assertWritable(false);if(!editor.getSnapshot().runtime.loading)return{status:'applied' as const,revision:editor.getSnapshot().revision,operationIds:[]};if(!options.persistence.read)throw fault('canonical_reader_unavailable');const controller=new AbortController();loadingReadController=controller;let releaseRead:(()=>void)|undefined; try{if((editor.getSnapshot().runtime.loading?.version??0)>=3)await new Promise((resolve,reject)=>{ let unsubscribe=()=>{};const finish=(error?:unknown)=>{unsubscribe();controller.signal.removeEventListener('abort',abort);error===undefined?resolve():reject(error);},abort=()=>finish(controller.signal.reason),check=()=>{if(![...editor.getSnapshot().runtime.ledger.requests.values()].some(request=>request.phase!=='resolved')){releaseRead=scheduler.hold();finish();}}; unsubscribe=editor.subscribe(check);controller.signal.addEventListener('abort',abort,{once:true});if(controller.signal.aborted)abort();else check(); });const read=await network.run(controller.signal,()=>options.persistence.read!(controller.signal));assertWritable(false);controller.signal.throwIfAborted();if(!editor.getSnapshot().runtime.loading)return{status:'applied' as const,revision:editor.getSnapshot().revision,operationIds:[]};const result=completeLoading(read.document,read.versions,{fresh:true});if(result.status==='applied')await persist();return result;}finally{releaseRead?.();if(loadingReadController===controller)loadingReadController=undefined;} })();loadingRead=task.finally(()=>{if(loadingRead===wrapped)loadingRead=undefined;});const wrapped=loadingRead;return wrapped; }, accept(document:Document,versions:ReadonlyMap,state:LoadingState):Result{ try{assertWritable(false);const root=editor.getSnapshot(),previous=root.runtime;if(!previous.loading)throw fault('loading_not_active');const source=previous.loadingRead;validatePrefixExtension(source?.document??previous.acknowledged,source?.versions??previous.versions,previous.loading,document,versions,state,options.registry);const nextRead=Object.freeze({document:createDocument(document.projectId,[...document.canvases.values()],[...document.elements.values()],document.tombstones),versions:readMap(versions)}),proposal=mergeLoadingRead(previous,nextRead,options.registry),acknowledged=proposal.document,working=replayPending(acknowledged,previous.ledger,options.registry);acceptingLoading=true;return editor.reconcile(working,Object.freeze({...previous,loading:freeze(clone(state)),...(state.version>=3?{loadingRead:nextRead}:{}),versions:readMap(proposal.versions),acknowledged}),root.rootRevision);}catch(error){return reject(error);}finally{acceptingLoading=false;} }, async complete(document:Document,versions:ReadonlyMap,settings:{fresh?:boolean;sourceToken?:string}={}):Promise{const result=completeLoading(document,versions,settings);if(result.status==='applied')await persist();return result;}, }), collectResources,collectImportSources,inventoryLocalFiles,collectOrphanFiles, getResourcePins:()=>resourceRetentionPins(editor.getSnapshot(),options.registry,displayPins,admissionPins(editor.getSnapshot())), /** Read/preview under a scheduling hold. No request is rewritten or declared resolved here. */ async prepareRecovery(): Promise { assertWritable(false); if(!options.persistence.read)throw fault('canonical_reader_unavailable'); const releaseScheduler=scheduler.hold(),releaseRuns=runTracker.hold(),releaseImports=holdImports(),controller=new AbortController(); const active={controller,release(){if(recovery!==active)return;recovery=undefined;releaseRuns();releaseScheduler();releaseImports();queueMicrotask(prepareImages);},async cancel(){controller.abort();active.release();}}; recovery=active; sessions.cancel('recovery'); const assertCurrent=()=>{if(closing||closed)throw fault('engine_closing');if(recovery!==active||controller.signal.aborted)throw fault('recovery_session_closed');}; try { // Both holds are acquired before awaiting either lane; already admitted callbacks finish normally. await Promise.all([...importLoads.values()]);assertCurrent();await Promise.all([scheduler.settle(),runTracker.settle(),...[...importRunners.values()].map(runner=>runner.settle())]);assertCurrent(); while(digestTasks.size){await Promise.all([...digestTasks.values()]);assertCurrent();} await persist();assertCurrent(); const staged=stageAppliedRecovery(editor.getSnapshot(),{persistence:options.persistence,runs:options.runs,assets:options.assets,now:runClock.now()}); const root=staged.snapshot,previous=root.runtime; const loaded=await network.run(controller.signal,()=>options.persistence.read!(controller.signal));assertCurrent(); if(editor.getSnapshot().rootRevision!==root.rootRevision)throw fault('stale_plan'); if(previous.loading)validateLoadingCompletion(previous.acknowledged,previous.versions,previous.loading,loaded.document,loaded.versions,options.registry,true);else validateCanonicalRead(previous.acknowledged,previous.versions,loaded.document,loaded.versions,options.registry); const acknowledged=createDocument(loaded.document.projectId,[...loaded.document.canvases.values()],[...loaded.document.elements.values()],loaded.document.tombstones); const canonical=Object.freeze({document:acknowledged,versions:readMap(loaded.versions)});staged.assertCanonical(canonical); let working:Document|undefined,conflict:RecoverySession['conflict']; try{working=replayPending(acknowledged,previous.ledger,options.registry);}catch(error){const rejected=reject(error);conflict=freeze({code:rejected.code,targets:rejected.targets});} let committed:ConflictPlan|'refresh'|undefined,committedIds:readonly Id[]=[]; const plans=new WeakSet(); const finish=async()=>{if(previous.loading&&!editor.getSnapshot().runtime.loading)loadingCompletionRevision=editor.getSnapshot().rootRevision;await persist();assertCurrent();await scheduler.resume();assertCurrent();scheduler.notify(committedIds);runTracker.notify(staged.runIds);active.release();return{status:'applied' as const,revision:editor.getSnapshot().revision,operationIds:committedIds};}; active.cancel=async()=>{if(recovery!==active)return;if(committed){await finish();return;}active.release();}; return Object.freeze({baseRootRevision:root.rootRevision,canonical,acknowledgedRequestIds:staged.requestIds, pendingOperationIds:Object.freeze([...previous.ledger.operations.values()].filter(record=>record.phase!=='resolved').map(record=>record.intent.id)), ...(conflict?{conflict}:{}), plan(choices:ReadonlyMap){ assertCurrent();if(committed)throw fault('recovery_already_committed');if(editor.getSnapshot().rootRevision!==root.rootRevision)throw fault('stale_plan'); const plan=planConflictRecovery(root,canonical,choices,options.registry,createId,options.historyLimit??100);plans.add(plan);return plan; }, async resolve(plan:ConflictPlan):Promise{ assertCurrent();if(!plans.has(plan))throw fault('invalid_recovery_plan');if(committed&&committed!==plan)throw fault('recovery_already_committed'); if(!committed){ const permission=options.policy?.({type:'changes.apply',changes:[]},plan.document);if(permission&&!permission.enabled)return{status:'rejected',code:permission.code,targets:permission.targets}; for(const id of plan.operationIds){const intent=plan.operations.get(id)!;const allowed=options.policy?.({type:'changes.apply',changes:intent.changes},plan.document);if(allowed&&!allowed.enabled)return{status:'rejected',code:allowed.code,targets:allowed.targets}; if(intent.operationType==='run.apply'){const run=previous.runs.get(intent.action.runId);if(!run||run.blocked||run.status!=='conflict'||!runContainsTarget(run,intent.action.target))return{status:'rejected',code:'output_not_applicable',targets:[intent.action.target.id]};}} const recoveries=new Map(previous.recoveries),resourcePins=new Map(previous.resourcePins),conflicts=new Map(previous.conflicts);recoveries.set(plan.id,plan.audit);resourcePins.set(`recovery:${plan.id}`,plan.resourceIds);conflicts.delete('refresh'); for(const decision of plan.audit.decisions){const requestId=previous.ledger.operations.get(decision.operationId)?.requestId;if(requestId)conflicts.delete(requestId);} const result=editor.reconcile(plan.document,Object.freeze({...previous,ledger:plan.ledger,loading:undefined,loadingRead:undefined,acknowledged,versions:canonical.versions,recoveries:readMap(recoveries),resourcePins:readMap(resourcePins),conflicts:readMap(conflicts)}),root.rootRevision,plan); if(result.status==='rejected')return result;committed=plan;committedIds=plan.operationIds; } return finish(); }, async apply():Promise{ assertCurrent(); if(committed&&committed!=='refresh')throw fault('recovery_already_committed'); if(conflict)return{status:'rejected',...conflict}; if(!committed){ const conflicts=new Map(previous.conflicts);conflicts.delete('refresh'); const result=editor.reconcile(working!,Object.freeze({...previous,loading:undefined,loadingRead:undefined,acknowledged,versions:canonical.versions,conflicts:readMap(conflicts)}),root.rootRevision); if(result.status==='rejected')return result; committed='refresh'; } // Persistence failure retains the hold; apply/cancel can retry this barrier without republishing. return finish(); }, cancel:()=>active.cancel(), }); }catch(error){active.release();throw error;} }, models(boardId: Id, signal: AbortSignal) { if(closing||closed||!options.runs)return Promise.reject(fault('run_interpreter_unavailable')); if(!editor.getSnapshot().document.canvases.has(boardId))return Promise.reject(fault('canvas_missing')); return network.run(signal,()=>options.runs!.catalog(boardId,signal)); }, cancelRun(runId:Id):Result { try{ assertWritable();if(!options.runs)throw fault('run_interpreter_unavailable'); const root=editor.getSnapshot(),run=root.runtime.runs.get(runId);if(!run)throw fault('run_missing'); const allowed=options.policy?.({type:'element.data',target:run.target,patch:{}},root.document);if(allowed&&!allowed.enabled)throw fault(allowed.code,allowed.targets); if([...root.runtime.ledger.operations.values()].some(record=>record.phase!=='resolved'&&record.intent.operationType==='run.cancel'&&record.intent.action.runId===runId))throw fault('run_action_pending'); return queueRunAction('run.cancel',{runId,boardId:run.boardId}); }catch(error){return reject(error);} }, applyRunOutput(runId:Id,target:RunApplyInput['target'],outputId:Id):Result { try{ assertWritable();if(!options.runs)throw fault('run_interpreter_unavailable');if(applying)throw fault('reentrant_dispatch'); const root=editor.getSnapshot(),run=root.runtime.runs.get(runId),element=root.document.elements.get(target.id); if(!run||run.blocked||!runContainsTarget(run,target)||!element||element.incarnation!==target.incarnation)throw fault('target_changed'); if((run.appliedOutputIds??[]).includes(outputId))throw fault('output_already_applied'); if([...root.runtime.ledger.operations.values()].some(record=>record.phase!=='resolved'&&(record.intent.operationType==='run.apply'||record.intent.operationType==='run.cancel')&&record.intent.action.runId===runId))throw fault('run_action_pending'); const event=run.outputEvents.find(event=>event.node_id===target.id&&event.output&&typeof event.output==='object'&&!Array.isArray(event.output)&&event.output.output_id===outputId),output=event?.output as {[key:string]:Json}|undefined; if(!output||run.status!=='conflict')throw fault('output_not_applicable'); const patch:Data|undefined=output.kind==='text'&&typeof output.text==='string'?{text:output.text}:output.kind==='image'&&typeof output.src==='string'&&typeof output.assetId==='string'?{src:output.src,assetId:output.assetId}:undefined;if(!patch)throw fault('invalid_run_output'); const action:RunApplyInput={runId,boardId:run.boardId,targetCanvasId:element.ownerCanvasId,target,outputId};applying=action; const result=editor.dispatch({type:'element.data',target,patch},{label:'Apply generated result'}); if(result.status==='rejected'||result.operationIds.length)return result; return queueRunAction('run.apply',action); }catch(error){return reject(error);}finally{applying=undefined;} }, startRun(input: RunInput): Result { try { assertWritable(); if(!options.runs)throw fault('run_interpreter_unavailable'); validateRunInput(input); const root=editor.getSnapshot(),element=root.document.elements.get(input.target.id); if(!element||element.incarnation!==input.target.incarnation||element.ownerCanvasId!==input.boardId||element.structure!=='node'||options.registry.resolve(element)!=='known'||!['text','plain-text','script','image','plain-image'].includes(element.componentId))throw fault('target_changed'); const allowed=editor.can({type:'element.data',target:input.target,patch:{ai:{enabled:true,modelId:input.modelId,prompt:input.prompt,parameters:input.parameters,propagate:input.propagate}}}); if(!allowed.enabled)throw fault(allowed.code,allowed.targets); if([...root.runtime.ledger.operations.values()].some(record=>record.intent.operationType==='run.start'&&record.phase!=='resolved'&&record.intent.run.target.id===input.target.id))throw fault('node_busy'); if([...root.runtime.runs.values()].some(run=>runContainsTarget(run,input.target)&&(run.hasMore||run.pendingPage||['queued','running'].includes(run.status))))throw fault('node_busy'); const id=createId(),dependencies=[...root.runtime.ledger.operations.values()].filter(record=>record.phase!=='resolved').map(record=>record.intent.id); let sequence=1;for(const record of root.runtime.ledger.operations.values())sequence=Math.max(sequence,record.intent.sequence+1); const result=editor.updateRuntime(previous=>{const ledger=createLedger(previous.ledger);ledger.add(freeze({id,operationType:'run.start',operationSchemaVersion:1,sequence,origin:'user',canvasIds:[...root.document.canvases.keys()].sort(),changes:[],dependencies,run:JSON.parse(JSON.stringify(input))}),{});return Object.freeze({...previous,ledger:ledger.getSnapshot()});},root.rootRevision); if(result.status==='rejected')return result; queueMicrotask(()=>{if(!closing&&!closed)scheduler.notify([id]);}); return{status:'applied',revision:root.revision,operationIds:[id]}; }catch(error){return reject(error);} }, refresh(document: Document, versions: ReadonlyMap): Result { if(editor.getSnapshot().runtime.loading)return completeLoading(document,versions,{fresh:true}); const root = editor.getSnapshot(), previous = root.runtime; try { assertWritable(); if ([...previous.ledger.requests.values()].some(request => request.phase !== 'resolved')) throw fault('refresh_waiting_for_receipts'); validateCanonicalRead(previous.acknowledged,previous.versions,document,versions,options.registry); if (equal([...previous.versions], [...versions]) && equal([...previous.acknowledged.canvases], [...document.canvases]) && equal([...previous.acknowledged.elements], [...document.elements]) && equal([...previous.acknowledged.tombstones], [...document.tombstones])) return { status: 'applied', revision: root.revision, operationIds: [] }; const acknowledged = createDocument(document.projectId, [...document.canvases.values()], [...document.elements.values()], document.tombstones); const working = replayPending(acknowledged, previous.ledger, options.registry); const conflicts = new Map(previous.conflicts); conflicts.delete('refresh'); return editor.reconcile(working, Object.freeze({ ...previous, acknowledged, versions: readMap(versions), conflicts: readMap(conflicts) }), root.rootRevision); } catch (error) { const result = reject(error); if (!['refresh_waiting_for_receipts','engine_closing','recovery_in_progress','identity_changed'].includes(result.code)) editor.updateRuntime(previous => { const conflicts = new Map(previous.conflicts); conflicts.set('refresh', result.code); return Object.freeze({ ...previous, conflicts: readMap(conflicts) }); }); return result; } }, canStageFiles(canvasId:Id){try{assertWritable(false);if(!options.assets)throw fault('asset_interpreter_unavailable');const snapshot=editor.getSnapshot();if(!snapshot.document.canvases.has(canvasId))throw fault('canvas_not_loaded',[canvasId]);if(snapshot.runtime.loading&&(snapshot.runtime.loading.version!==6||!loadingQueryCoverage(snapshot).completeCanvasIds.includes(canvasId)))throw fault('loading_scope_incomplete',[canvasId]);const permission=options.policy?.({type:'changes.apply',changes:[]},snapshot.document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets);return{enabled:true as const};}catch(error){const result=reject(error);return{enabled:false as const,code:result.code,targets:result.targets};}}, stageFiles(files: readonly StagedFile[], callback: (resources: readonly ResourceHandle[], transaction: Transaction) => unknown) { assertWritable(false);if(editor.getSnapshot().runtime.loading&&editor.getSnapshot().runtime.loading?.version!==6)throw fault('model_loading'); if (staged.length||adopting) throw fault('reentrant_dispatch'); if (!options.assets) throw fault('asset_interpreter_unavailable'); const resources = files.map(file => { if (!(file.bytes instanceof Blob) || !file.bytes.size || file.bytes.size > maxResourceBytes || !file.bytes.type.startsWith('image/') || typeof file.name !== 'string' || !file.name || file.defaultSize !== undefined && typeof file.defaultSize !== 'boolean') throw fault('invalid_staged_file'); return freeze({ id: createId(), operationId: createId(), name: file.name, mimeType: file.bytes.type, byteLength: file.bytes.size,...(options.images?{image:{version:1 as const,status:'pending' as const}}:{}) }); }); if (new Set(resources.flatMap(resource => [resource.id, resource.operationId])).size !== resources.length * 2 || resources.some(resource => editor.getSnapshot().runtime.resources.has(resource.id) || editor.getSnapshot().runtime.assetBindings?.has(resource.id) || editor.getSnapshot().runtime.orphanFiles?.has(resource.id) || fileWrites.has(resource.id) || importFiles(editor.getSnapshot().runtime.imports).has(resource.id))) throw fault('resource_identity_conflict'); staged = resources; resources.forEach((resource, index) => { fileWrites.set(resource.id, files[index].bytes);if(files[index].defaultSize)stagedDefaultSizes.add(resource.id); }); try { const handles = freeze(resources.map(resource => ({ id: resource.id, src: options.assets!.sourceURL(resource.id) }))); const result = editor.transaction(tx => callback(handles, tx), { label: 'Add images' }); if (result.status === 'rejected' || !result.operationIds.length) for (const resource of resources) fileWrites.delete(resource.id); return result; } finally { staged = []; stagedDefaultSizes.clear();for (const resource of resources) if (!editor.getSnapshot().runtime.resources.has(resource.id)) fileWrites.delete(resource.id); } }, async adoptResource(id:Id,callback:(resource:ResourceHandle&{readonly asset:StoredAsset},transaction:Transaction)=>unknown,settings:{signal?:AbortSignal}={}):Promise{ assertWritable(false);if(!depot||!options.assets?.authority||!options.storage.assetDepot)throw fault('asset_depot_unavailable');if(editor.getSnapshot().runtime.loading&&editor.getSnapshot().runtime.loading?.version!==6)throw fault('model_loading'); if(staged.length||adopting)throw fault('reentrant_dispatch'); const controller=new AbortController(),signal=AbortSignal.any([controller.signal,...(settings.signal?[settings.signal]:[])]); const task=(async()=>{ const authorized=await depot.authorize(leader,{scope:options.assets!.authority!.scope,resolve:(id,abort)=>network.run(abort,()=>options.assets!.authority!.resolve(id,abort)),read:(id,abort)=>network.run(abort,()=>options.assets!.authority!.read(id,abort))},id,signal); assertWritable(false);signal.throwIfAborted();if(authorized.ref.asset.url!==options.assets!.sourceURL(id))throw fault('asset_authorization_mismatch'); if(staged.length||adopting)throw fault('reentrant_dispatch');const runtime=editor.getSnapshot().runtime; if(runtime.resources.has(id)||runtime.orphanFiles?.has(id)||importFiles(runtime.imports).has(id)||fileWrites.has(id)||collectionReservations.has(id))throw fault('resource_identity_conflict',[id]); const previous=adoptedFiles.get(id);adopting=authorized.ref;adoptedFiles.set(id,{bytes:authorized.bytes,rootRevision:Number.MAX_SAFE_INTEGER}); let committed=false; try{const result=editor.transaction(tx=>callback(freeze({id,src:authorized.ref.asset.url,asset:authorized.ref.asset}),tx),{label:'Add existing image'});committed=result.status==='applied'&&result.operationIds.length>0;if(committed)adoptedFiles.set(id,{bytes:authorized.bytes,rootRevision:editor.getSnapshot().rootRevision});return result;} finally{adopting=undefined;if(!committed){if(previous)adoptedFiles.set(id,previous);else adoptedFiles.delete(id);}} })(); adoptionTasks.set(controller,task);void task.finally(()=>adoptionTasks.delete(controller)).catch(()=>{});return task; }, async displayResource(id: Id, settings: { signal?: AbortSignal } = {}) { assertDisplayIdentity(); if(collectionReservations.has(id))throw fault('resource_collection_in_progress',[id]); displayPins.set(id,(displayPins.get(id)??0)+1); let released=false, lease: DisplayLease | undefined; const release=()=>{if(released)return;released=true;settings.signal?.removeEventListener('abort',release);displays.delete(release);const count=displayPins.get(id)??1;if(count<=1)displayPins.delete(id);else displayPins.set(id,count-1);lease?.release();}; displays.add(release); settings.signal?.addEventListener('abort',release,{once:true}); try{ if(settings.signal?.aborted)throw fault('display_aborted'); const retained=editor.getSnapshot().runtime.resources.get(id); const orphan=editor.getSnapshot().runtime.orphanFiles?.get(id);if(orphan?.retirement?.purgedAt!==undefined)throw fault('local_file_retired',[id]); if(orphan?.retirement){const result=editor.updateRuntime(previous=>Object.freeze({...previous}));if(result.status==='rejected')throw fault(result.code,result.targets);} if(retained?.retirement&&retained.retirement.purgedAt===undefined){ const result=editor.updateRuntime(previous=>{const resources=new Map(previous.resources),{retirement:_,...kept}=retained;resources.set(id,freeze(kept));return Object.freeze({...previous,resources:readMap(resources)});}); if(result.status==='rejected')throw fault(result.code,result.targets); } const source=!retained?importFiles(editor.getSnapshot().runtime.imports).get(id)?.file:undefined; if(!retained){ if(source?.retirement?.purgedAt!==undefined)throw fault('import_source_retired',[id]); if(source?.retirement){const result=editor.updateRuntime(previous=>Object.freeze({...previous}));if(result.status==='rejected')throw fault(result.code,result.targets);} } if(released)throw fault('display_aborted'); const binding=editor.getSnapshot().runtime.assetBindings?.get(id); const key=JSON.stringify(binding?['shared-asset',id,binding.authority,binding.digest]:retained?['asset',id,retained.operationId,retained.retirement?.purgedAt??null]:orphan?['orphan',id,orphan.digest]:source?['source',id,source.sha256??null]:['unmanaged',id]); lease=displayCache.acquire(displayOwner,key,async signal=>{ let bytes=adoptedFiles.get(id)?.bytes??fileWrites.get(id)??await options.storage.readFile(leader,id); if(!bytes&&binding&&depot)bytes=await depot.read(leader,binding,signal); assertDisplayIdentity();if(signal.aborted)throw fault('display_aborted'); if(orphan&&(!bytes||bytes.size!==orphan.byteLength||bytes.type!==orphan.mimeType||await sourceDigest(bytes,signal)!==orphan.digest))throw fault('file_inventory_changed',[id]); assertDisplayIdentity();if(signal.aborted)throw fault('display_aborted'); const resource=editor.getSnapshot().runtime.resources.get(id); if(!bytes&&resource?.retirement?.purgedAt!==undefined&&!resource.stored)throw fault('resource_bytes_unavailable',[id]); if(!bytes&&!options.assets)throw fault('resource_missing',[id]); const url=bytes?URL.createObjectURL(bytes):options.assets!.displayURL(id); return Object.freeze({url,...(bytes?{release:()=>URL.revokeObjectURL(url)}:{})}); }); const url=await lease.ready; assertDisplayIdentity();if(released||settings.signal?.aborted)throw fault('display_aborted'); return Object.freeze({url,release}); }catch(error){release();assertDisplayIdentity();if(settings.signal?.aborted)throw fault('display_aborted');throw error;} }, fitImage(target:Target,resourceId:Id):Result{ try{assertWritable();if(sessions.getSnapshot().gesture)throw fault('gesture_in_progress');const root=editor.getSnapshot(),element=root.document.elements.get(target.id),resource=root.runtime.resources.get(resourceId),size=resource?.image?.version===1&&resource.image.status==='ready'?resource.image:resource?.stored; if(!element||element.incarnation!==target.incarnation||element.structure!=='node'||options.registry.resolve(element)!=='known'||!options.registry.components.get(element.componentId)?.resourceFields?.includes('assetId')||element.data.assetId!==resourceId)throw fault('target_changed');if(!size||!validImageDimensions(size))throw fault('image_dimensions_unavailable'); const units=unitScale(element.data.presentation),min=units,max=100000*units,scale=Math.min(element.geometry.width/size.width,max/size.width,max/size.height),grow=Math.max(scale,min/size.width,min/size.height),width=size.width*grow,height=size.height*grow;if(!Number.isFinite(width)||!Number.isFinite(height)||width>max||height>max)throw fault('image_fit_constraint'); return editor.transaction(tx=>{tx.dispatch({type:'element.geometry',target,patch:{width,height}});const after=tx.getDocument().elements.get(target.id);if(after?.structure!=='node'||Math.abs(after.geometry.width-width)>1e-8||Math.abs(after.geometry.height-height)>1e-8)throw fault('image_fit_constraint');},{label:'Fit image ratio'}); }catch(error){return reject(error);} }, getSnapshot: editor.getSnapshot, persist, cancelRecovery:async()=>{await recovery?.cancel();}, getDurability, subscribeDurability(listener:()=>void){if(closed)return()=>{};durabilityListeners.add(listener);return()=>{durabilityListeners.delete(listener);};}, retry: async () => { if(recovery)await recovery.cancel();assertWritable(false);await persist();retryDefaultSizes();const runtime=editor.getSnapshot().runtime,ids=[...runtime.resources.values()].filter(resource=>!resource.digest&&runtime.ledger.operations.get(resource.operationId)?.blockers.some(code=>digestFailures.has(code))).map(resource=>resource.operationId);if(ids.length){mutate(ledger=>{for(const id of ids)ledger.block(id,['waiting_digest']);});prepareDigests();}if(options.images){editor.updateRuntime(previous=>{const resources=new Map(previous.resources);let changed=false;for(const [id,resource]of resources)if(resource.image?.version===1&&resource.image.status==='failed'){resources.set(id,freeze({...resource,image:{version:1 as const,status:'pending' as const}}));changed=true;}return changed?Object.freeze({...previous,resources:readMap(resources)}):previous;});prepareImages();}for(const job of editor.getSnapshot().runtime.imports?.values()??[])if(sourceNeedsSeal(job)){await importLoads.get(job.id);await resetImportReader(job.id);await loadImport(job.id);}await scheduler.resume();await runTracker.retry(); }, async settle() { let revision:number; do { revision=editor.getSnapshot().rootRevision; await Promise.resolve();await Promise.allSettled([...adoptionTasks.values()]);await Promise.allSettled([...copyPreparations.values()]);while(importLoads.size)await Promise.all([...importLoads.values()]);await Promise.all([...importRunners.values()].map(runner=>runner.settle()));while(verificationTasks.size)await Promise.allSettled([...verificationTasks.values()].map(task=>task.promise));while(digestTasks.size)await Promise.all([...digestTasks.values()]);while(imageTasks.size)await Promise.all([...imageTasks.values()]);while(sizingTask)await sizingTask; await scheduler.settle();await runTracker.settle();await persist();await scheduler.settle();await persist(); } while(revision!==editor.getSnapshot().rootRevision||pending.size||writing||adoptionTasks.size||copyPreparations.size||importLoads.size||verificationTasks.size||digestTasks.size||imageTasks.size||sizingTask); }, async close() { if (closed) return; closing = true;notifyDurability();loadingReadController?.abort();await loadingRead?.catch(()=>{});unsubscribeSizing();orphanController?.abort();await Promise.allSettled([collection,sourceCollection,orphanCollection].filter(Boolean));digestLifecycle.abort();for(const {controller}of verificationTasks.values())controller.abort();for(const controller of preparationControllers.values())controller.abort();for(const controller of copyPreparations.keys())controller.abort();await Promise.allSettled([...copyPreparations.values()]);for(const controller of adoptionTasks.keys())controller.abort();await Promise.allSettled([...adoptionTasks.values()]); sessions.dispose(); scheduler.stop(); runTracker.stop(); await Promise.all([...importRunners.values()].map(runner=>runner.close()));while(importLoads.size)await Promise.all([...importLoads.values()]);while(verificationTasks.size)await Promise.allSettled([...verificationTasks.values()].map(task=>task.promise));for(const {reader}of proofReaders.values())reader.close();proofReaders.clear();while(digestTasks.size)await Promise.allSettled([...digestTasks.values()]);while(imageTasks.size)await Promise.allSettled([...imageTasks.values()]);while(sizingTask)await sizingTask; recovery?.controller.abort();recovery?.release(); for (const release of [...displays]) release();displayCache.releaseOwner(displayOwner);if(!options.displays)displayCache.close(); // Failure keeps the leader/recovery material available for an explicit retry of close. await persist(); unsubscribe(); editor.dispose();if(!options.assetDepot)depot?.close();adoptedFiles.clear(); await leader.release(); closed = true;notifyDurability();durabilityListeners.clear(); }, }); } catch (error) { await leader.release(); throw error; } }