/** * history 的可逆增量记录:保存两栈 ID 顺序,以及历史项发生变化的数组区间。 * 与 history.ts 的区别:这里恢复“历史元数据”,不执行文档修改或发送请求。 * journal/恢复与资源 pin 扫描使用这些记录,既能回放,也保留被清 redo/淘汰的旧内容。 */ import type { Change, HistoryEntry, HistoryState, Id } from './types.ts'; import { equal, fault, freeze, readMap, validJson } from './values.ts'; /** 数组区间补丁:共同前缀后的 offset、两边总长度与替换前后区间;共同后缀不重复保存。 */ type Splice={readonly offset:number;readonly beforeLength:number;readonly afterLength:number;readonly before:readonly T[];readonly after:readonly T[]}; /** 历史项的展示/分组元数据;变化数组和 operation IDs 分开描述。 */ type Meta={readonly label:string;readonly groupId?:Id}; /** 同一 entry 的元数据与两个数组补丁;null 表示该侧不存在这个历史项。 */ type EntryPatch={readonly id:Id;readonly before:Meta|null;readonly after:Meta|null;readonly changes:Splice;readonly operationIds:Splice}; /** version 控制解释器兼容性,rootRevision 关联发布版本;不等同于服务端版本号。 */ export type HistoryTransition={readonly version:1;readonly rootRevision:number;readonly entryId?:Id;readonly before:{readonly undo:readonly Id[];readonly redo:readonly Id[]};readonly after:{readonly undo:readonly Id[];readonly redo:readonly Id[]};readonly entries:readonly EntryPatch[]}; // Resource scans revisit the same transition across many durability cuts. Cache // only its flattened changes, never registry-dependent references or callbacks. const transitionChanges=new WeakMap(); const immutableData=new WeakSet(); /** 检查缓存候选的冻结数据描述符与普通原型,最多递归 64 层;不是单独的 JSON/schema 校验器。 * 冻结的 getter 仍可能返回不同值,稀疏数组也不是完整区间,因此都不允许缓存。 */ function immutablePlainData(value:unknown,depth=0):boolean{ if(depth>64)return false; if(value===null||typeof value!=='object')return typeof value!=='function'; // 只记住已遍历成功的对象身份;祖先链若太深仍会先被 depth 条件拒绝。 if(immutableData.has(value))return true; if(!Object.isFrozen(value)||Object.getPrototypeOf(value)!==(Array.isArray(value)?Array.prototype:Object.prototype))return false; if(Array.isArray(value))for(let i=0;i{if(!entry)return null;const{id:_id,changes:_changes,operationIds:_operations,...remaining}=entry;return remaining;}; /** 去掉相等的共同前缀、后缀,只保存真正变化的中间区间;后缀不能与前缀重叠。 */ function splice(before:readonly T[],after:readonly T[]):Splice{ let offset=0,suffix=0;while(offsetunknown):asserts value is HistoryTransition{ // 四个局部校验器统一抛领域错误,分别约束非数组记录形状、非负整数、非空 ID 和 ID 数组。 const object=(v:unknown):Record=>{if(!v||typeof v!=='object'||Array.isArray(v))throw fault('invalid_history_transition');return v as Record;}; const integer=(v:unknown)=>{if(typeof v!=='number'||!Number.isSafeInteger(v)||v<0)throw fault('invalid_history_transition');}; const id=(v:unknown)=>{if(typeof v!=='string'||!v)throw fault('invalid_history_transition');}; const ids=(v:unknown):string[]=>{if(!Array.isArray(v))throw fault('invalid_history_transition');v.forEach(id);return v;}; const raw=object(value);if(raw.version!==1)throw fault('history_transition_interpreter_unavailable');if(!validJson(value))throw fault('invalid_history_transition');integer(raw.rootRevision);if(raw.entryId!==undefined)id(raw.entryId); // 同一侧 undo/redo 中不允许重复 ID,避免同一项同时处于两个栈或重复出现。 for(const side of [raw.before,raw.after]){const s=object(side),list=[...ids(s.undo),...ids(s.redo)];if(new Set(list).size!==list.length)throw fault('invalid_history_transition');} if(!Array.isArray(raw.entries))throw fault('invalid_history_transition');const seen=new Set(); for(const item of raw.entries){const p=object(item);id(p.id);if(seen.has(p.id as string))throw fault('invalid_history_transition');seen.add(p.id as string); for(const side of [p.before,p.after])if(side!==null){const m=object(side);if(typeof m.label!=='string'||['id','changes','operationIds'].some(key=>Object.hasOwn(m,key)))throw fault('invalid_history_transition');if(m.groupId!==undefined)id(m.groupId);} // 两边保留的前后缀长度必须一致;不存在的历史项不允许带非零原数组长度。 for(const key of ['changes','operationIds']){const patch=object(p[key]);for(const field of ['offset','beforeLength','afterLength'])integer(patch[field]);if(!Array.isArray(patch.before)||!Array.isArray(patch.after))throw fault('invalid_history_transition');const{offset,beforeLength,afterLength}=patch as unknown as Splice;if(offset+patch.before.length>beforeLength||offset+patch.after.length>afterLength||beforeLength-patch.before.length!==afterLength-patch.after.length)throw fault('invalid_history_transition');if(key==='operationIds'){ids(patch.before);ids(patch.after);}else{checkChanges?.(patch.before);checkChanges?.(patch.after);} if(p.before===null&&beforeLength!==0||p.after===null&&afterLength!==0)throw fault('invalid_history_transition'); } } } /** Conditional metadata-only replay. A mismatch leaves the supplied history untouched. */ /** 先检查两栈,再逐 entry 检查元数据/数组区间并在私有副本上正向或反向回放。 * 不改写输入两栈/entries 表;不能拿旧 journal 覆盖新的历史顺序。 * history/transition 应已捕获:补丁内 Change 会沿用并冻结,不应把此接口当任意外部值的深隔离入口。 */ export function applyHistoryTransition(history:HistoryState,transition:HistoryTransition,direction:'forward'|'reverse'):HistoryState{ validateHistoryTransition(transition);const reverse=direction==='reverse',source=reverse?transition.after:transition.before,destination=reverse?transition.before:transition.after; if(!equal({undo:history.undo,redo:history.redo},source))throw fault('history_transition_conflict'); /** 按方向交换 expected/insert;同时比较总长度和旧区间,防止错位拼接。 */ function apply(current:readonly T[],patch:Splice):readonly T[]{const expected=reverse?patch.after:patch.before,insert=reverse?patch.before:patch.after,length=reverse?patch.afterLength:patch.beforeLength;if(current.length!==length||!equal(current.slice(patch.offset,patch.offset+expected.length),expected))throw fault('history_transition_conflict');return[...current.slice(0,patch.offset),...insert,...current.slice(patch.offset+expected.length)];} // 各 entry 使用独立数组补丁,新增/删除也校验旧元数据,不直接拿整份快照覆盖。 const entries=new Map(history.entries); for(const patch of transition.entries){const entry=entries.get(patch.id),expected=reverse?patch.after:patch.before,next=reverse?patch.before:patch.after;if(!equal(meta(entry),expected))throw fault('history_transition_conflict');const changes=apply(entry?.changes??[],patch.changes),operationIds=apply(entry?.operationIds??[],patch.operationIds);if(next)entries.set(patch.id,freeze({id:patch.id,...next,changes,operationIds}));else entries.delete(patch.id);} // 最终两栈必须精确覆盖 entries 表,不能留下孤立项或指向缺失 entry 的栈 ID。 const ids=[...destination.undo,...destination.redo];if(ids.length!==entries.size||ids.some(id=>!entries.has(id)))throw fault('history_transition_conflict'); return Object.freeze({undo:freeze([...destination.undo]),redo:freeze([...destination.redo]),entries:readMap(entries)}); } /** 汇总增量两侧出现的 Change,供资源保留扫描使用;不执行这些修改。 * 只有自有区间且整条 transition 深不可变时才缓存,否则每次重新计算。 */ export function historyTransitionChanges(transition:HistoryTransition):readonly Change[]{ const cached=transitionChanges.get(transition);if(cached)return cached; validateHistoryTransition(transition); // 仅收集被记录的变化区间两侧,未重复保存的共同前后缀不在本结果中;它不是完整 history 的展开。 const changes=transition.entries.flatMap(patch=>[...patch.changes.before,...patch.changes.after]); // 属性来自原型或 getter 时不可把一次结果当永久事实,避免错误释放资源 pin。 const ownRanges=Object.hasOwn(transition,'entries')&&transition.entries.every(patch=>Object.hasOwn(patch,'changes')&&Object.hasOwn(patch.changes,'before')&&Object.hasOwn(patch.changes,'after')); if(!ownRanges||!immutablePlainData(transition))return changes; const result=Object.freeze(changes);transitionChanges.set(transition,result);return result; }