49 lines
4.4 KiB
TypeScript
49 lines
4.4 KiB
TypeScript
/**
|
||
* 同步事务内的大批创建优化。追加表用固定成员的 base+added 视图避免每新增一项都复制整表,
|
||
* 已有追加层达到 64 项时,下一次追加先折叠为新 base。只用于未发布 draft,提交前统一物化文档表。
|
||
* 输入表须已具固定成员;视图借用 base 和未变记录,不是任意可变 Map 的隔离器。
|
||
*/
|
||
import {detachElementData} from './element-data.ts';
|
||
import type {CanvasRecord,Change,Document,ElementRecord,Id} from './types.ts';
|
||
import {applyChanges} from './document.ts';
|
||
import {clone,fault,freeze,readMap,validJson} from './values.ts';
|
||
|
||
/** Transaction-local append views. Each retained view has fixed membership;
|
||
* at most 64 additions are copied before folding into a new read-only base. */
|
||
const overlays=new WeakMap<object,{base:ReadonlyMap<Id,unknown>;added:ReadonlyMap<Id,unknown>}>(),drafts=new WeakSet<Document>();
|
||
/** 在不可变输入表上创建新的追加视图;复制 added 而不改旧视图,保留之前 draft 的快照语义。 */
|
||
function append<T>(input:ReadonlyMap<Id,T>,id:Id,value:T):ReadonlyMap<Id,T>{
|
||
if(input.has(id))throw fault('duplicate_identity',[id]);
|
||
let previous=overlays.get(input) as {base:ReadonlyMap<Id,T>;added:ReadonlyMap<Id,T>}|undefined;
|
||
// 当前 input 若含 64 个 added,本次先通过 readMap(input) 折叠,再以新 added 放入第 65 个;不是整个事务最多 64 项。
|
||
if(previous&&previous.added.size>=64)previous=undefined;
|
||
// overlays 记录的是框架创建的视图身份;普通输入直接借用,只有已知旧 overlay 才需要折叠。
|
||
const base=previous?.base??(overlays.has(input)?readMap(input):input),added=new Map(previous?.added);
|
||
added.set(id,value);
|
||
/** 迭代先 base 后 added,保持插入顺序;视图的 get/has 优先查 appended 成员。 */
|
||
const entries=function*():Generator<[Id,T],undefined,unknown>{yield* base;yield* added;return undefined;};
|
||
// get 用 has 区分“added 中显式存 undefined”与缺失;迭代顺序始终 base 在前、追加成员在后。
|
||
// keys/values/iterator 复用 entries;forEach 第三个参数仍是安全视图,不泄漏 added Map。
|
||
const view:ReadonlyMap<Id,T>=Object.freeze({size:base.size+added.size,get:(key:Id)=>added.has(key)?added.get(key):base.get(key),has:(key:Id)=>added.has(key)||base.has(key),entries,
|
||
*keys():Generator<Id,undefined,unknown>{for(const [key]of entries())yield key;return undefined;},*values():Generator<T,undefined,unknown>{for(const [,item]of entries())yield item;return undefined;},[Symbol.iterator]:entries,
|
||
forEach(callback:(value:T,key:Id,map:ReadonlyMap<Id,T>)=>void,thisArg?:unknown){for(const[key,item]of entries())callback.call(thisArg,item,key,view);},
|
||
});
|
||
overlays.set(view,{base,added});return view;
|
||
}
|
||
/** Internal editor path: input tables already have fixed membership. */
|
||
/** 仅接收 before=null 且 after 存在的创建变化,校验 JSON/ID 后捕获记录并追加相应表。
|
||
* 整图依赖可在后续命令补齐,因此此处不提前做完整树/端点校验。
|
||
* 本层只检查创建形状、JSON、ID 一致及活表重复;registry 支持与墓碑禁复用由 editor/最终校验负责。 */
|
||
export function stageCreation(document:Document,change:Extract<Change,{kind:'canvas'|'element'}>):Document{
|
||
if(change.before!==null||!change.after)throw fault('invalid_creation_draft');
|
||
if(!validJson(change.after))throw fault(change.kind==='canvas'?'invalid_canvas':'invalid_element',[change.id]);
|
||
if(change.after.id!==change.id)throw fault('identity_mismatch',[change.id]);
|
||
const value=change.kind==='element'?detachElementData(change.after as ElementRecord):freeze(clone(change.after));
|
||
// 只替换被追加的表,另一表/墓碑沿用;新 Document 标记为 draft,旧视图不随本次 added 改变。
|
||
const next=Object.freeze({...document,...(change.kind==='canvas'?{canvases:append(document.canvases,change.id,value as CanvasRecord)}:{elements:append(document.elements,change.id,value as ElementRecord)})});drafts.add(next);return next;
|
||
}
|
||
/** Materialize transaction-local append tables before publication. */
|
||
/** 只有已标记 draft 通过空 changes 物化成工厂 envelope/readMap;其它文档原样返回。
|
||
* 不在这里校验 schema/树,也不把任何外来 Document 自动视为已校验或深度不可变。 */
|
||
export const materializeCreations=(document:Document)=>drafts.has(document)?applyChanges(document,[]):document;
|