|
|
|
@@ -1,9 +1,9 @@
|
|
|
|
|
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 type { Change, CoreSnapshot, Document, ElementRecord, Id, HistoryState, 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 { FrozenRequest, LedgerSnapshot, OperationRecord } from './ledger.ts';
|
|
|
|
|
import type { OperationStatus } from './scheduler.ts';
|
|
|
|
|
import { readMap, readSet } from '../canvas-core/values.ts';
|
|
|
|
|
|
|
|
|
@@ -72,47 +72,61 @@ export function conflictsWith(prior: ReturnType<typeof operationFootprint>, next
|
|
|
|
|
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<Id, readonly Id[]>) {
|
|
|
|
|
const pins = new Map<Id, Set<string>>();
|
|
|
|
|
let complete = true;
|
|
|
|
|
const pin = (id: Id, owner: string) => { const set = pins.get(id) ?? new Set<string>(); 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; }
|
|
|
|
|
/** One synchronous retention pass shares history interpretation across working/ACK.
|
|
|
|
|
* No snapshot or mutable reference-count state is retained after the caller returns. */
|
|
|
|
|
export function createResourcePinScan(history:HistoryState,ledger:LedgerSnapshot,registry:Registry){
|
|
|
|
|
const transitions=new Map<OperationRecord,readonly Change[]>(),fieldTargets=new Set<Id>();
|
|
|
|
|
const rememberTargets=(values:readonly Change[])=>{for(const change of values)if(change.kind==='field'&&change.field.startsWith('data.'))fieldTargets.add(change.target.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 entry of history.entries.values())rememberTargets(entry.changes);
|
|
|
|
|
let interpreted=true;
|
|
|
|
|
for(const record of ledger.operations.values()){
|
|
|
|
|
// Validate every understood transition, including resolved evidence, as before.
|
|
|
|
|
if(record.intent.historyTransition?.version===1)transitions.set(record,historyTransitionChanges(record.intent.historyTransition));
|
|
|
|
|
if(record.phase!=='resolved')rememberTargets(record.intent.changes);
|
|
|
|
|
if(record.intent.historyEffect&&record.intent.historyEffect.version!==1)interpreted=false;
|
|
|
|
|
if(record.intent.sequence>=earliest&&record.intent.historyTransition){if(record.intent.historyTransition.version!==1)interpreted=false;else rememberTargets(transitions.get(record)!);}
|
|
|
|
|
}
|
|
|
|
|
const schemas=new Map<Id,ElementRecord[]>();
|
|
|
|
|
const remember=(record:ElementRecord|null)=>{if(record&&fieldTargets.has(record.id)){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);}};
|
|
|
|
|
const rememberChanges=(values:readonly Change[])=>{for(const change of values)if(change.kind==='element'){remember(change.before);remember(change.after);}};
|
|
|
|
|
if(fieldTargets.size){
|
|
|
|
|
for(const entry of history.entries.values())rememberChanges(entry.changes);
|
|
|
|
|
for(const record of ledger.operations.values()){rememberChanges(record.intent.changes);const changes=transitions.get(record);if(changes)rememberChanges(changes);}
|
|
|
|
|
}
|
|
|
|
|
const fields=new Set([...registry.components.values()].flatMap(component=>component.resourceFields??[]));
|
|
|
|
|
return(document:Document,explicit:ReadonlyMap<Id,readonly Id[]>=new Map())=>{
|
|
|
|
|
const pins=new Map<Id,Set<string>>();let complete=interpreted;
|
|
|
|
|
const pin=(id:Id,owner:string)=>{const set=pins.get(id)??new Set<string>();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<Id,ElementRecord[]>();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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:ElementRecord[]=[],current=document.elements.get(change.target.id);if(current)candidates.push(current);
|
|
|
|
|
for(const record of schemas.get(change.target.id)??[])if(!candidates.some(item=>item.componentId===record.componentId&&item.dataVersion===record.dataVersion))candidates.push(record);
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
// Keep the conservative cross-schema fallback and its original insertion order.
|
|
|
|
|
const refs=new Set<Id>(),collect=(record:ElementRecord|null)=>{if(record)for(const field of registry.components.get(record.componentId)?.resourceFields??[])if(typeof record.data[field]==='string')refs.add(record.data[field] as string);};
|
|
|
|
|
for(const change of values)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')refs.add(change.before);if(typeof change.after==='string')refs.add(change.after);}
|
|
|
|
|
for(const id of refs)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)]))) });
|
|
|
|
|
for(const record of document.elements.values())element(record,`element:${record.id}:${record.incarnation}`);
|
|
|
|
|
for(const entry of 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}`);}
|
|
|
|
|
for(const record of ledger.operations.values())if(record.intent.sequence>=earliest&&record.intent.historyTransition?.version===1)changes(transitions.get(record)!,`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)])))});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
/** Pins are derived from owners; callers receive no second mutable reference count. */
|
|
|
|
|
export function resourcePins(snapshot:CoreSnapshot,ledger:LedgerSnapshot,registry:Registry,explicit:ReadonlyMap<Id,readonly Id[]>){return createResourcePinScan(snapshot.history,ledger,registry)(snapshot.document,explicit);}
|
|
|
|
|