36 lines
4.6 KiB
TypeScript
36 lines
4.6 KiB
TypeScript
import type { OperationIntent } from '../canvas-core/types.ts';
|
|
import { validateCreationSizeEffect } from '../canvas-core/creation-size-plan.ts';
|
|
import { validateHistoryTransition, type HistoryTransition } from '../canvas-core/history-transition.ts';
|
|
import { fault } from '../canvas-core/values.ts';
|
|
import type { EngineSnapshot } from './contracts.ts';
|
|
import type { OperationRecord } from './ledger.ts';
|
|
import { validateHistoryReplay, type HistoryReplay } from './history-replay.ts';
|
|
|
|
export type HistorySourceEvent={readonly record:OperationRecord|undefined;readonly transition:HistoryTransition};
|
|
export type HistoryEventSource={readonly events:readonly HistorySourceEvent[];readonly adoption?:{readonly count:number;readonly replay:HistoryReplay;readonly transition:HistoryTransition}};
|
|
/** Resolve either a native interval or the owning adoption's proved virtual events, followed by later native events. */
|
|
export function historyEventSource(snapshot:EngineSnapshot,pending:readonly OperationIntent[]):HistoryEventSource{
|
|
const content=pending.filter(intent=>intent.changes.length).sort((a,b)=>a.sequence-b.sequence),first=content[0];if(!first)return{events:[]};
|
|
const records=snapshot.runtime.ledger.operations,audits=[...snapshot.runtime.recoveries.values()],owners=audits.filter(audit=>audit.decisions.some(decision=>decision.replacementId===first.id));
|
|
if(owners.length>1)throw fault('recovery_history_adoption_ambiguous',[first.id]);
|
|
const interpreter=(record:OperationRecord)=>{if(record.intent.operationSchemaVersion!==1)throw fault('operation_interpreter_unavailable',[record.intent.id]);if(record.intent.historyEffect)validateCreationSizeEffect(record.intent.historyEffect);};
|
|
const pendingIds=new Set(content.map(intent=>intent.id));
|
|
const order=(events:readonly HistorySourceEvent[])=>{const seen=new Set<string>(),active:string[]=[];for(const event of events)if(event.record){const id=event.record.intent.id;if(seen.has(id))throw fault('recovery_history_event_order',[id]);seen.add(id);if(pendingIds.has(id))active.push(id);}if(active.length!==content.length||active.some((id,index)=>id!==content[index].id))throw fault('recovery_history_event_order');};
|
|
const native=(record:OperationRecord):HistorySourceEvent=>{interpreter(record);const transition=record.intent.historyTransition;if(!transition)throw fault('recovery_history_transition_missing',[record.intent.id]);validateHistoryTransition(transition);return{record,transition};};
|
|
if(!owners.length){
|
|
const events=[...records.values()].filter(record=>record.intent.sequence>=first.sequence&&record.intent.changes.length).sort((a,b)=>a.intent.sequence-b.intent.sequence).map(native);
|
|
const start=events[0]?.transition.rootRevision??Infinity;if(audits.some(audit=>(audit.historyTransition?.rootRevision??audit.baseRootRevision+1)>=start))throw fault('recovery_history_adoption_replay_required');
|
|
for(let i=1;i<events.length;i++)if(events[i].transition.rootRevision<=events[i-1].transition.rootRevision)throw fault('recovery_history_event_order');order(events);return{events};
|
|
}
|
|
const audit=owners[0],revision=audit.baseRootRevision+1,replay=audit.historyReplay;
|
|
if(!replay||!audit.historyTransition)throw fault('recovery_history_adoption_evidence_missing',[audit.id]);validateHistoryReplay(replay,revision);validateHistoryTransition(audit.historyTransition);
|
|
if(audits.some(other=>other.baseRootRevision+1>revision))throw fault('recovery_history_adoption_stale',[audit.id]);
|
|
const witnessed=new Set<string>(),events:HistorySourceEvent[]=replay.events.map(event=>{
|
|
if(event.kind==='adjustment')return{record:undefined,transition:event.transition};const record=records.get(event.operationId);if(!record?.intent.changes.length)throw fault('history_replay_operation_missing',[event.operationId]);interpreter(record);witnessed.add(event.operationId);return{record,transition:event.transition};
|
|
});
|
|
for(const intent of content)if(!witnessed.has(intent.id)&&!(intent.historyTransition&&intent.historyTransition.rootRevision>revision))throw fault('recovery_history_transition_missing',[intent.id]);
|
|
const later=[...records.values()].filter(record=>record.intent.changes.length&&record.intent.historyTransition&&record.intent.historyTransition.rootRevision>revision).sort((a,b)=>a.intent.sequence-b.intent.sequence).map(native);
|
|
let previous=revision;for(const event of later){if(event.transition.rootRevision<=previous)throw fault('recovery_history_event_order');previous=event.transition.rootRevision;}
|
|
order([...events,...later]);return{events:[...events,...later],adoption:{count:events.length,replay,transition:audit.historyTransition}};
|
|
}
|