Files
Endoleap/src/lib/canvas-runtime/history-event-source.ts
T

59 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 为历史恢复选择可证明的事件区间:普通原生 transition,或最近一次 adoption 的虚拟事件再接后续原生事件。
* 不把已采纳后压缩过的历史误当作原始 sequence 直接重放;所有解释器/先后关系都需显式检查。
*/
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';
/** 有 record 表示对应逻辑操作;没有 record 则是纯历史 adjustment。 */
export type HistorySourceEvent={readonly record:OperationRecord|undefined;readonly transition:HistoryTransition};
/** 事件序列与可选 owning adoption 证据;count 标识虚拟事件前缀长度。 */
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. */
/** 从 pending 中有 changes 的最早 sequence 选择事件源;全为空则返回空列表,不读取其它证据。
* 无 owning adoption 时取原生区间;有时先取其虚拟 replay.events,再接 revision 之后的原生事件。
* 这里只解析形状/解释器/先后关系,不反向执行历史或核验事件文档效果;resolution 由消费规划器继续检查。
* pending 应来自同一已验证账本;ID 顺序检查不等于逐字段比对传入 intent 与 ledger 对象。
* 返回数组/envelope 未冻结,record/transition 沿用快照引用,调用方按不可变证据使用。 */
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]);
/** 要求 operationSchemaVersion=1,存在尺寸效果时验其解释器形状;不在此执行尺寸计划或校验操作类别。
* 未来数据可被上层保留,不表示此路径能据它恢复历史。 */
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));
/** 带 record 的所有事件 ID 不重复,抽出的 pending 子序列须与按 sequence 排序的 content 完全对应。
* adjustment 无 operation ID 不参与此计数;不要求所有账本 sequence 数值连续,也不检查服务执行顺序。 */
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');};
/** 提取原生 transition 并验结构;未传领域 checkChanges,也不在这里重放其 before/after 元数据。
* 缺失证据直接拒绝,完整可回放性由 createHistoryJournalRecovery 的反向条件执行验证。 */
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};};
// 无 owning adoption 时用原生区间;区间内还有 adoption 就不能越过其边界盲目回放。
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};
}
// owning audit 必须有 replay/transition,replay 绑定 baseRootRevision+1,且不允许其它更晚审计遮盖。
// 此处只组织虚拟事件,anchor 与整体 historyTransition 是否真的一致,留给日志规划器反向验证。
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]);
// adjustment 不需要服务操作;operation 虚拟事件必须能定位真实账本记录并具有可用解释器。
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]);
// 后续原生事件按逻辑 sequence 排序,再要求 rootRevision 严格递增,不以网络回执顺序拼接。
// 本层不按 phase/resolution 过滤;消费端仍须拒绝非 pending 但结果未明确的历史贡献。
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}};
}