34 lines
4.2 KiB
TypeScript
34 lines
4.2 KiB
TypeScript
/**
|
||
* 一次本地恢复采纳的历史证据:anchor 把旧 history 移到回放起点,events 解释之后的操作/调整。
|
||
* 这些 transition 是同一原子发布的元数据,不是新服务写入;重复重开必须能验证相同历史结果。
|
||
*/
|
||
import type { HistoryState, Id } from '../canvas-core/types.ts';
|
||
import { applyHistoryTransition, validateHistoryTransition, type HistoryTransition } from '../canvas-core/history-transition.ts';
|
||
import { fault, equal, validJson } from '../canvas-core/values.ts';
|
||
|
||
/** operation 关联稳定逻辑 ID;adjustment 只修历史元数据,不伪造一条服务操作。 */
|
||
export type HistoryReplayEvent={readonly kind:'operation';readonly operationId:Id;readonly transition:HistoryTransition}|{readonly kind:'adjustment';readonly transition:HistoryTransition};
|
||
/** version=1 的 anchor+有序事件;所有 transition 绑定同一次 adoption Root revision。 */
|
||
export type HistoryReplay={readonly version:1;readonly anchor:HistoryTransition;readonly events:readonly HistoryReplayEvent[]};
|
||
/** Local evidence only. The anchor and event deltas explain one atomic adoption, not new service writes. */
|
||
/** 检查证据版本/JSON、anchor 无 entry、事件 revision 一致及 operation ID 唯一;未知版本拒绝解释。
|
||
* revision 由调用方提供;不查询真实发布日志,checkChanges 缺省时不完整解释各条领域 Change。 */
|
||
export function validateHistoryReplay(value:unknown,revision:number,checkChanges?:(value:unknown)=>unknown):asserts value is HistoryReplay{
|
||
if(!value||typeof value!=='object'||Array.isArray(value))throw fault('invalid_history_replay');const raw=value as Record<string,unknown>;
|
||
if(raw.version!==1)throw fault('history_replay_interpreter_unavailable');if(!validJson(value))throw fault('invalid_history_replay');validateHistoryTransition(raw.anchor,checkChanges);if(raw.anchor.rootRevision!==revision||raw.anchor.entryId!==undefined||!Array.isArray(raw.events))throw fault('invalid_history_replay');const ids=new Set<Id>();
|
||
for(const value of raw.events){if(!value||typeof value!=='object'||Array.isArray(value))throw fault('invalid_history_replay');const event=value as Record<string,unknown>;validateHistoryTransition(event.transition,checkChanges);if(event.transition.rootRevision!==revision)throw fault('invalid_history_replay');if(event.kind==='operation'){if(typeof event.operationId!=='string'||!event.operationId||ids.has(event.operationId))throw fault('invalid_history_replay');ids.add(event.operationId);}else if(event.kind!=='adjustment'||event.operationId!==undefined)throw fault('invalid_history_replay');}
|
||
}
|
||
/** 条件回放历史元数据:先正向 anchor,再按 events 顺序更新两栈/entries,不执行文档 changes 或网络。
|
||
* 用 anchor 自身 revision 调结构校验,不独自核对外部 adoption 的 Root;调用方另需绑定那次发布。
|
||
* hasOperation 可选检查 ID 存在,带 entry 的操作事件需关联同 ID;不在此比较逻辑意图内容与 transition 是否语义等价。 */
|
||
export function replayHistoryEvidence(before:HistoryState,replay:HistoryReplay,hasOperation?:(id:Id)=>boolean):HistoryState{
|
||
validateHistoryReplay(replay,replay.anchor.rootRevision);let history=applyHistoryTransition(before,replay.anchor,'forward');
|
||
for(const event of replay.events){if(event.kind==='operation'&&hasOperation&&!hasOperation(event.operationId))throw fault('history_replay_operation_missing',[event.operationId]);history=applyHistoryTransition(history,event.transition,'forward');if(event.kind==='operation'&&event.transition.entryId&&!history.entries.get(event.transition.entryId)?.operationIds.includes(event.operationId))throw fault('history_replay_operation_mismatch',[event.operationId]);}
|
||
return history;
|
||
}
|
||
/** 比较两栈和全部 entry;entry 表按 ID 排序,避免 Map 插入顺序差异被误认作历史不一致。 */
|
||
export function sameHistory(left:HistoryState,right:HistoryState):boolean{
|
||
// 仅规范化 entries 表的顺序;undo/redo 数组顺序仍按栈语义精确比较,不能排序后当集合。
|
||
const value=(history:HistoryState)=>({undo:history.undo,redo:history.redo,entries:[...history.entries].sort(([a],[b])=>a.localeCompare(b))});return equal(value(left),value(right));
|
||
}
|