59 lines
7.0 KiB
TypeScript
59 lines
7.0 KiB
TypeScript
/**
|
||
* 为历史恢复选择可证明的事件区间:普通原生 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}};
|
||
}
|