39 lines
3.2 KiB
TypeScript
39 lines
3.2 KiB
TypeScript
/**
|
|
* 历史执行规划:把 undo/redo 给出的 changes 应用到私有文档副本。
|
|
* 本模块不移动 undo/redo 栈、不发请求;editor/coordinator 在规划成功后统一发布。
|
|
* 删除后恢复的元素沿用 ID,但必须获得大于墓碑的新 incarnation,使旧异步回调失效。
|
|
*/
|
|
import type { Change, Document, ElementRecord, Id, Registry } from './types.ts';
|
|
import { applyChanges, validateDocument } from './document.ts';
|
|
import { freeze } from './values.ts';
|
|
|
|
/** Pure history execution, also used to validate recovery proposals before publication. */
|
|
/** 顺序回放一组历史修改,完成整份文档校验后返回候选文档和实际 changes。
|
|
* remap 供调用者把其它历史目标同步映射到恢复后的 incarnation;函数不发布任何候选。
|
|
* initial/planned 应来自已捕获的不可变状态,底层 applyChanges/freeze 不是任意外部对象的深隔离入口。 */
|
|
export function planHistoryChanges(initial:Document,planned:readonly Change[],registry:Registry){
|
|
// ID →(旧 incarnation → 新 incarnation);同一批恢复可能形成多级映射链。
|
|
const generations=new Map<Id,Map<number,number>>();
|
|
// 追踪到本批最终代次;每次恢复分配更大代次,因此按这些内部生成的映射链前进。
|
|
const incarnation=(id:Id,value:number)=>{const mapping=generations.get(id);while(mapping?.has(value))value=mapping.get(value)!;return value;};
|
|
// 只改生命周期身份:字段目标与整元素 before/after 都要一致,canvas 变更原样通过。
|
|
const remap=(change:Change):Change=>{
|
|
if(change.kind==='field'){const next=incarnation(change.target.id,change.target.incarnation);return next===change.target.incarnation?change:freeze({...change,target:{...change.target,incarnation:next}});}
|
|
// 整元素 before/after 只替换顶层 incarnation,记录内其它字段与引用不在此重新解释。
|
|
if(change.kind==='element'){const record=(value:ElementRecord|null)=>value?{...value,incarnation:incarnation(value.id,value.incarnation)}:null;return freeze({...change,before:record(change.before),after:record(change.after)});}
|
|
return change;
|
|
};
|
|
let document=initial;const changes:Change[]=[];
|
|
// 私有 draft 逐项推进,因此同一批后面的恢复可以看到前面已写入的墓碑。
|
|
for(const original of planned){
|
|
let change=remap(original);
|
|
// 从不存在恢复为存在时,若原代次已死,分配 dead+1 并重写本条及后续相关变化。
|
|
if(change.kind==='element'&&!change.before&&change.after){const dead=document.tombstones.get(change.id);if(dead!==undefined&&change.after.incarnation<=dead){const mapping=generations.get(change.id)??new Map<number,number>();mapping.set(change.after.incarnation,dead+1);generations.set(change.id,mapping);change=remap(change);}}
|
|
document=applyChanges(document,[change]);changes.push(change);
|
|
}
|
|
// 暂态允许先恢复端点再恢复边;只有整批完成后才检查最终树、引用和 owner 一致性。
|
|
validateDocument(document,registry);
|
|
// restored 只表示是否建立过恢复代次映射;即使 false,文档也可能因普通字段/删除变化而改变。
|
|
return{document,changes:freeze(changes),remap,restored:generations.size>0};
|
|
}
|