import type {ImageDecodeState} from './image-decoder.ts'; import type { DefaultImageSizing } from './default-image-size.ts'; import type { Change, CoreSnapshot, Document, ElementRecord, Id, OperationIntent, Registry } from '../canvas-core/types.ts'; import { historyTransitionChanges } from '../canvas-core/history-transition.ts'; import { ancestors } from '../canvas-core/document.ts'; import type { FrozenRequest, LedgerSnapshot } from './ledger.ts'; import type { OperationStatus } from './scheduler.ts'; import { readMap, readSet } from '../canvas-core/values.ts'; export type StoredAsset = { readonly id: Id; readonly url: string; readonly width: number; readonly height: number; readonly bytes: number; readonly mime_type: string }; export type LoadingAssetAdmission = {readonly version:1;readonly sourceToken:string;readonly sourceURL:string;readonly canvasIds:readonly Id[]}; export type ResourceRecord = { readonly loadingAdmission?:LoadingAssetAdmission; readonly id: Id; readonly operationId: Id; readonly name: string; readonly mimeType: string; readonly byteLength: number; readonly sizing?: DefaultImageSizing; readonly image?:ImageDecodeState; readonly digest?: string; readonly retirement?: {readonly version:1;readonly unreferencedAt:number;readonly purgedAt?:number}; readonly stored?: StoredAsset }; export type DisplayHandle = { readonly url: string; release(): void }; export type AssetPort = { readonly authority?:import('./asset-depot.ts').AssetAuthority; sourceURL(resourceId: Id): string; displayURL(resourceId: Id): string; upload(request: FrozenRequest, bytes: Blob, signal: AbortSignal): Promise; query(request: FrozenRequest, signal: AbortSignal): Promise; accept(request: FrozenRequest, status: OperationStatus, resource: ResourceRecord): StoredAsset; }; export type StagedFile = { readonly bytes: Blob; readonly name: string; readonly defaultSize?: boolean }; export type ResourceHandle = { readonly id: Id; readonly src: string }; export const maxResourceBytes = 20 * 1024 * 1024; export function referencedResources(changes: readonly Change[], registry: Registry): Set { const result = new Set(), fields = new Set([...registry.components.values()].flatMap(component => component.resourceFields ?? [])); const collect = (element: ElementRecord | null) => { if (!element) return; for (const field of registry.components.get(element.componentId)?.resourceFields ?? []) if (typeof element.data[field] === 'string') result.add(element.data[field] as string); }; for (const change of changes) { if (change.kind === 'element') { collect(change.before); collect(change.after); } else if (change.kind === 'field' && change.field.startsWith('data.') && fields.has(change.field.slice(5))) { if (typeof change.before === 'string') result.add(change.before); if (typeof change.after === 'string') result.add(change.after); } } return result; } /** Read/write dependencies use object identities, rather than blocking every edit behind a pending image. */ export function operationFootprint(intent: OperationIntent, document: Document) { const reads = new Set(), writes = new Set(); if (intent.operationType === 'asset.upload') { writes.add(`resource:${intent.resourceId}`); return { reads, writes }; } const readCanvas = (id: Id) => { if (document.canvases.has(id)) for (const ancestor of ancestors(document, id)) reads.add(`canvas:${ancestor}`); else reads.add(`canvas:${id}`); }; const record = (element: ElementRecord | null) => { if (!element) return; readCanvas(element.ownerCanvasId); if (element.structure === 'connection') { reads.add(`element:${element.source.elementId}`); reads.add(`element:${element.target.elementId}`); } }; for (const change of intent.changes) { if (change.kind === 'canvas') { writes.add(`canvas:${change.id}`); for (const canvas of [change.before, change.after]) if (canvas?.parent) { readCanvas(canvas.parent.canvasId); reads.add(`element:${canvas.parent.entryElementId}`); } } else if (change.kind === 'element') { writes.add(`element:${change.id}`); record(change.before); record(change.after); } else { writes.add(`element:${change.target.id}`); record(document.elements.get(change.target.id) ?? null); if (change.field === 'ownerCanvasId') { readCanvas(change.before as string); readCanvas(change.after as string); } if (change.field === 'source' || change.field === 'target') for (const endpoint of [change.before, change.after]) { if (endpoint && typeof endpoint === 'object' && !Array.isArray(endpoint) && typeof endpoint.elementId === 'string') reads.add(`element:${endpoint.elementId}`); } } } return { reads, writes }; } export function conflictsWith(prior: ReturnType, next: ReturnType) { return [...prior.writes].some(id => next.writes.has(id) || next.reads.has(id)) || [...prior.reads].some(id => next.writes.has(id)); } /** Pins are derived from their owners. There is no second mutable reference count. */ export function resourcePins(snapshot: CoreSnapshot, ledger: LedgerSnapshot, registry: Registry, explicit: ReadonlyMap) { const pins = new Map>(); let complete = true; const pin = (id: Id, owner: string) => { const set = pins.get(id) ?? new Set(); set.add(owner); pins.set(id, set); }; const element = (record: ElementRecord | null, owner: string) => { if (!record) return; const component = registry.components.get(record.componentId); if (registry.resolve(record) !== 'known' || component?.resourceFields === undefined) { complete = false; return; } for (const field of component.resourceFields) { const ref = record.data[field]; if (typeof ref === 'string') pin(ref, owner);else if(ref!==undefined&&ref!==null)complete=false; } }; const schemas=new Map(); const remember=(record:ElementRecord|null)=>{if(record){const list=schemas.get(record.id)??[];if(!list.some(item=>item.componentId===record.componentId&&item.dataVersion===record.dataVersion))list.push(record);schemas.set(record.id,list);}}; for(const record of snapshot.document.elements.values())remember(record); const rememberChanges=(changes:readonly Change[])=>{for(const change of changes)if(change.kind==='element'){remember(change.before);remember(change.after);}}; for(const entry of snapshot.history.entries.values())rememberChanges(entry.changes); for(const record of ledger.operations.values()){rememberChanges(record.intent.changes);if(record.intent.historyTransition?.version===1)rememberChanges(historyTransitionChanges(record.intent.historyTransition));} const changes = (values: readonly Change[], owner: string) => { for(const change of values){ if(change.kind==='element'){element(change.before,owner);element(change.after,owner);} else if(change.kind==='field'&&change.field.startsWith('data.')){ const candidates=schemas.get(change.target.id);if(!candidates?.length)complete=false; for(const record of candidates??[]){const component=registry.components.get(record.componentId);if(registry.resolve(record)!=='known'||component?.resourceFields===undefined){complete=false;continue;} if(component.resourceFields.includes(change.field.slice(5)))for(const value of [change.before,change.after])if(typeof value==='string')pin(value,owner);else if(value!==undefined&&value!==null)complete=false; } } } for (const id of referencedResources(values, registry)) pin(id, owner); }; for (const record of snapshot.document.elements.values()) element(record, `element:${record.id}:${record.incarnation}`); for (const entry of snapshot.history.entries.values()) changes(entry.changes, `history:${entry.id}`); for (const record of ledger.operations.values()) if (record.phase !== 'resolved') { if (record.intent.operationType === 'asset.upload') pin(record.intent.resourceId, `operation:${record.intent.id}`); else changes(record.intent.changes, `operation:${record.intent.id}`); } const pending=[...ledger.operations.values()].filter(record=>record.phase!=='resolved'),earliest=pending.reduce((value,record)=>Math.min(value,record.intent.sequence),Infinity); for (const record of ledger.operations.values()) if (record.intent.historyEffect && record.intent.historyEffect.version !== 1) complete = false; for(const record of ledger.operations.values())if(record.intent.sequence>=earliest&&record.intent.historyTransition){ if(record.intent.historyTransition.version!==1){complete=false;continue;} changes(historyTransitionChanges(record.intent.historyTransition),`history-transition:${record.intent.id}`); } for (const [owner, ids] of explicit) for (const id of ids) pin(id, owner); return Object.freeze({ complete, pins: readMap(new Map([...pins].map(([id, owners]) => [id, readSet(owners)]))) }); }