docs(core): clarify capture validation and replay guarantees
This commit is contained in:
@@ -16,7 +16,8 @@ function documentEnvelope(document:Document):Document{const value=Object.freeze(
|
||||
export const isFactoryDocument=(document:Document)=>factoryDocuments.has(document);
|
||||
|
||||
/** 捕获输入记录、拒绝同一表内重复 ID,并脱离调用者的可变数据。
|
||||
* 不在此处验证整棵树或所有跨记录引用;完整发布前仍需 validateDocument。 */
|
||||
* 不在此处验证整棵树、组件 schema 或所有跨记录引用;完整发布前仍需 validateDocument。
|
||||
* canvas 总是复制冻结,element 可复用元素捕获模块已持有的记录;墓碑复制表成员,元素固有证明只省去重复形状遍历。 */
|
||||
export function createDocument(projectId: Id, canvases: readonly CanvasRecord[], elements: readonly ElementRecord[] = [], tombstones: ReadonlyMap<Id, number> = new Map()): Document {
|
||||
for (const element of elements) { const id = element.id; if (!staticElementProofs.has(element)&&!validJson(element)) throw fault('invalid_element', [id]); }
|
||||
for (const canvas of canvases) if (!validJson(canvas)) throw fault('invalid_canvas');
|
||||
@@ -61,14 +62,19 @@ const staticElementProofs=new WeakSet<object>();
|
||||
/** 校验单条记录的基础形状、墓碑代次和已知组件数据。
|
||||
* staticElementProofs 仅缓存已捕获记录的固有形状;墓碑和 registry 相关检查每次仍执行。 */
|
||||
export function validateElementRecord(element: ElementRecord, registry: Registry, tombstones: ReadonlyMap<Id,number> = new Map(), componentData = true): void {
|
||||
// 静态证明仅随不可变记录身份复用,不能因浅冻结或一个文档 envelope 标记就跳过校验。
|
||||
const proven=!!element&&staticElementProofs.has(element);
|
||||
if (!proven&&(!element || !validJson(element) || typeof element.id !== 'string' || !element.id || typeof element.ownerCanvasId !== 'string' || !element.ownerCanvasId || typeof element.componentId !== 'string' || !element.componentId || !['node','connection'].includes(element.structure) || !Number.isSafeInteger(element.incarnation) || element.incarnation < 1 || !Number.isSafeInteger(element.dataVersion) || element.dataVersion < 1 || !element.data || typeof element.data !== 'object' || Array.isArray(element.data) || !validJson(element.data))) throw fault('invalid_element', [element?.id]);
|
||||
// 墓碑属于当前文档上下文,即使同一记录以前通过过形状校验,也要重新核对已死亡代次。
|
||||
const dead=tombstones.get(element.id);if(dead!==undefined&&element.incarnation<=dead)throw fault('stale_incarnation',[element.id]);
|
||||
// 只有精确支持的组件版本进入 schema 检查;未知组件/不支持版本仍须通过通用形状约束,不在这里解释其 payload。
|
||||
const definition=registry.components.get(element.componentId);
|
||||
if(componentData&®istry.resolve(element)==='known'&&(!definition||definition.structure!==element.structure||!componentDataValid(element,definition)))throw fault('invalid_component_data',[element.id]);
|
||||
// 命中固有证明只能略过下方几何/端点形状遍历,不能略过上方墓碑与本次启用的 schema 检查。
|
||||
if(proven)return;
|
||||
if(element.structure==='node'){if(!geometryValid(element.geometry))throw fault('invalid_geometry',[element.id]);}
|
||||
else if(!element.source||!element.target||typeof element.source.elementId!=='string'||!element.source.elementId||typeof element.target.elementId!=='string'||!element.target.elementId||[element.source,element.target].some(endpoint=>endpoint.portId!==undefined&&(typeof endpoint.portId!=='string'||!endpoint.portId))||!validJson(element.path))throw fault('invalid_endpoint',[element.id]);
|
||||
// 拓扑规划可 componentData=false;此时仍可记固有证明,但以后启用 schema 的调用依然会重查 data。
|
||||
if(hasOwnedElementData(element))staticElementProofs.add(element);
|
||||
}
|
||||
|
||||
@@ -101,6 +107,7 @@ function validateElementInDocument(document:Document,element:ElementRecord,regis
|
||||
function* validateDocumentChecks(document:Document,registry:Registry,componentData:boolean):Generator<void,void>{
|
||||
if(!document.canvases.size)throw fault('missing_root');
|
||||
for(const[id,incarnation]of document.tombstones){yield;validateTombstone(id,incarnation);}
|
||||
// 一次遍历累计入口占用和根数;必须继续到最后,才能得到根数及所有元素的完整结论。
|
||||
const entries=new Set<Id>();let roots=0;
|
||||
for(const canvas of document.canvases.values()){yield;roots+=validateCanvasInDocument(document,canvas,entries);}
|
||||
if(roots!==1)throw fault('invalid_roots');
|
||||
@@ -111,7 +118,8 @@ function* validateDocumentChecks(document:Document,registry:Registry,componentDa
|
||||
export function* validateDocumentTopologySteps(document:Document,registry:Registry):Generator<void,void>{yield* validateDocumentChecks(document,registry,false);}
|
||||
/** 含组件 payload 的完整分片校验;必须迭代到结束才证明整份文档通过。 */
|
||||
export function* validateDocumentSteps(document:Document,registry:Registry):Generator<void,void>{yield* validateDocumentChecks(document,registry,true);}
|
||||
/** 同步完整校验,要求恰好一个根,所有拥有关系、端点和组件数据均合法。 */
|
||||
/** 同步走完整文档检查:要求一个根,检查拥有关系、入口、节点端点及连线共同 owner。
|
||||
* 已支持的精确组件版本检查 data schema;未知/不支持版本按通用记录规则保留,不冒称已理解其 payload。 */
|
||||
export function validateDocument(document:Document,registry:Registry):void{
|
||||
if(!document.canvases.size)throw fault('missing_root');
|
||||
for(const[id,incarnation]of document.tombstones)validateTombstone(id,incarnation);
|
||||
@@ -120,21 +128,24 @@ export function validateDocument(document:Document,registry:Registry):void{
|
||||
if(roots!==1)throw fault('invalid_roots');
|
||||
for(const element of document.elements.values())validateElementInDocument(document,element,registry,true);
|
||||
}
|
||||
/** 一批有序修改及其允许的 canvas 上下文;回放不能跨越已冻结的操作范围。 */
|
||||
/** 一批有序修改及可选 canvas 上下文;当前执行器用它检查 field 目标的 owner。
|
||||
* 这不是覆盖所有 change 种类的授权器;整元素/树修改还需调用方规划、权限及发布前校验。 */
|
||||
export type ReplayChangeBatch = {readonly changes:readonly Change[];readonly canvasIds?:readonly Id[]};
|
||||
/** 在私有表副本中应用一批变化;默认校验 before 条件,失败不会修改输入文档。 */
|
||||
/** 在已捕获的不可变文档上,以私有表副本应用变化;默认校验 before 条件。
|
||||
* 未改记录沿用原引用,不是任意外部 Document 的深隔离入口;修改记录的 freeze 也可能触及共享嵌套值。 */
|
||||
export function applyChanges(document: Document, changes: readonly Change[], conditional = true): Document {
|
||||
return applyChangeBatches(document,[{changes}],conditional,false);
|
||||
}
|
||||
/** Ordered conditional replay against one private draft; no intermediate document escapes. */
|
||||
/** 顺序回放多批变化,已经满足的 after 可跳过,但仍检查目标的 scope。 */
|
||||
/** 顺序回放多批变化,已经满足的 after 可跳过;field 变化在跳过前仍检查当前 owner 范围。
|
||||
* 批次未提供 canvasIds 或提供空数组时,这一局部 owner 范围检查不启用。 */
|
||||
export function replayChanges(document:Document,batches:Iterable<ReplayChangeBatch>):Document {
|
||||
return applyChangeBatches(document,batches,true,true);
|
||||
}
|
||||
/** 三类变化共用的执行器:field 改字段、element 增删元素、canvas 改树记录。
|
||||
* 仅在全部循环成功后返回冻结 envelope,不泄漏半成品;此处不做整图合法性校验。 */
|
||||
function applyChangeBatches(document:Document,batches:Iterable<ReplayChangeBatch>,conditional:boolean,replay:boolean):Document {
|
||||
// 复制表成员而复用未改记录;出错时丢弃这些私有 Map,输入快照保持不变。
|
||||
// 复制表成员而复用未改记录;失败不返回私有 Map。对有效不可变输入,不会发布或改写原快照。
|
||||
const elements = new Map(document.elements), canvases = new Map(document.canvases), tombstones = new Map(document.tombstones);
|
||||
for (const batch of batches) for (const change of batch.changes) {
|
||||
if(replay){
|
||||
@@ -158,7 +169,8 @@ function applyChangeBatches(document:Document,batches:Iterable<ReplayChangeBatch
|
||||
if (change.afterPresent === false) delete data[dataKey]; else data[dataKey] = clone(change.after);
|
||||
elements.set(element.id, freeze({ ...element, data }));
|
||||
} else elements.set(element.id, freeze({ ...element, [change.field]: clone(change.after) }) as ElementRecord);
|
||||
// 整元素修改必须匹配 before;删除同时保留墓碑,使旧 incarnation 永远不能复活。
|
||||
// 整元素修改在 conditional=true 时匹配 before;删除只记录已死亡的最大代次。
|
||||
// 此执行器不直接以墓碑拒绝后续插入,发布前 validateElementRecord/validateDocument 才拦截旧代次。
|
||||
} else if (change.kind === 'element') {
|
||||
if (conditional && !equal(elements.get(change.id) || null, change.before)) throw fault('element_conflict', [change.id]);
|
||||
if (change.after) {
|
||||
@@ -184,6 +196,7 @@ function applyChangeBatches(document:Document,batches:Iterable<ReplayChangeBatch
|
||||
}
|
||||
/** 检测回放目标效果是否已经存在;字段目标必须仍是同一个 incarnation 才可视为满足。 */
|
||||
function changeSatisfied(elements:ReadonlyMap<Id,ElementRecord>,canvases:ReadonlyMap<Id,CanvasRecord>,change:Change):boolean {
|
||||
// canvas/element 使用整记录 after 比较;删除后的缺失等同于 after:null,重复删除可跳过。
|
||||
if(change.kind==='canvas')return equal(canvases.get(change.id)??null,change.after);
|
||||
if(change.kind==='element')return equal(elements.get(change.id)??null,change.after);
|
||||
const element=elements.get(change.target.id);
|
||||
|
||||
Reference in New Issue
Block a user