/** * 组件定义的装配与显式数据迁移。Registry 保存可复用的定义/handler,不保存元素实例或 view 状态。 * 装配时把 feature 合成为 componentId→capability→handler 查找表,冲突即拒绝,避免依赖安装顺序。 * 迁移只生成候选记录,不写 editor;调用方负责把结果作为一次合法事务发布。 * 冻结的是定义配置容器,handler 函数及其闭包仍沿用;容器只读不证明外部上下文不会变化。 */ import type { Capability, CapabilityHandler, ComponentDefinition, Data, ElementRecord, Feature, Json, Registry, ReadonlyValueSet } from './types.ts'; import { clone, fault, freeze, readMap, readSet } from './values.ts'; /** 捕获一份行为表并冻结:ID 非空、至少一个 handler,且每个已提供 handler 都是函数。 */ export function defineFeature(id: string, handlers: Partial>): Feature { if (!id || !Object.values(handlers).length || Object.values(handlers).some(value => typeof value !== 'function')) throw fault('invalid_feature'); return freeze({ id, handlers: { ...handlers } }); } /** 校验组件定义与复制/几何协议,并复制声明过的配置容器后冻结。 * validate、几何方法、feature handler 和 migration 函数本身保持原引用,不复制函数闭包。 */ export function defineComponent(definition: ComponentDefinition): ComponentDefinition { if (!definition.id || !Number.isSafeInteger(definition.dataVersion) || definition.dataVersion < 1 || typeof definition.validate !== 'function') throw fault('invalid_component'); // 复制路径访问自有字段,禁止原型污染键;数组遍历由 * 路径段表达。 const pathValid = (path: readonly string[]) => Array.isArray(path) && path.length > 0 && path.every(key => typeof key === 'string' && !!key && !['__proto__','prototype','constructor'].includes(key)); if (definition.copy) { const references = definition.copy.references, omitted = definition.copy.omit; if (!Array.isArray(references) || !Array.isArray(omitted) || omitted.some(path => !pathValid(path)) || references.some(ref => !pathValid(ref.path) || !['element','canvas'].includes(ref.target) || !['preserve','reject'].includes(ref.external) || ref.many !== undefined && typeof ref.many !== 'boolean')) throw fault('invalid_copy_schema'); // reference 和 omit 不可指向重叠路径;通配符也参与前缀冲突判断,避免复制顺序改变结果。 const paths: readonly (readonly string[])[] = [...references.map(ref => ref.path), ...omitted]; for (let i = 0; i < paths.length; i++) for (let j = 0; j < i; j++) if (paths[i].slice(0, Math.min(paths[i].length, paths[j].length)).every((key, index) => (key === paths[j][index] || key === '*' || paths[j][index] === '*'))) throw fault('conflicting_copy_paths'); } // 节点提供 bounds/hit,边提供 segments/strokeWidth;协议种类必须与组件结构一致。 const geometry=definition.geometry;if(geometry&&(geometry.version!==1||geometry.kind!==definition.structure||geometry.kind==='node'&&(typeof geometry.bounds!=='function'||typeof geometry.hit!=='function'||geometry.dataDependencies!==undefined&&(!Array.isArray(geometry.dataDependencies)||geometry.dataDependencies.some(key=>typeof key!=='string'||!key)))||geometry.kind==='connection'&&(typeof geometry.segments!=='function'||typeof geometry.strokeWidth!=='function')))throw fault('invalid_geometry_protocol'); return freeze({ ...definition, geometry:geometry?{...geometry,...(geometry.kind==='node'&&geometry.dataDependencies?{dataDependencies:[...geometry.dataDependencies]}:{})}:undefined, copy: definition.copy ? clone(definition.copy) : undefined, features: definition.features.map(feature => defineFeature(feature.id, feature.handlers)), migrations: definition.migrations ? { ...definition.migrations } : undefined, resourceFields: definition.resourceFields ? [...definition.resourceFields] : undefined }); } /** 一次性建立只读 registry;组件 ID、同组件 feature ID 和 capability 都要求唯一。 */ export function createRegistry(definitions: readonly ComponentDefinition[]): Registry { const components = new Map(), capabilities = new Map>(), handlers = new Map>(); for (const input of definitions) { const definition = defineComponent(input); if (components.has(definition.id)) throw fault('duplicate_component', [definition.id]); const found = new Map(), features = new Set(); // 即使两个 feature 的 ID 不同,也不能偷偷覆盖同一个 capability 的 handler。 for (const feature of definition.features) { if (features.has(feature.id)) throw fault('duplicate_feature', [feature.id]); features.add(feature.id); for (const [key, handler] of Object.entries(feature.handlers)) { const capability = key as Capability; if (!handler) throw fault('invalid_feature'); if (found.has(capability)) throw fault('duplicate_capability', [definition.id, capability]); found.set(capability, handler); } } components.set(definition.id, definition); capabilities.set(definition.id, readSet(found.keys())); handlers.set(definition.id, readMap(found)); } // 这里只按 componentId+dataVersion 分派,不执行 data schema 或结构校验;known 不是整条记录合法的证明。 // 可保留未知记录,但不能假设它可被当前 handler 编辑。 const resolve: Registry['resolve'] = element => { const definition = components.get(element.componentId); if (!definition) return 'unknown-component'; if (definition.dataVersion !== element.dataVersion) return 'unsupported-version'; return 'known'; }; return Object.freeze({ components: readMap(components), capabilities: readMap(capabilities), handlers: readMap(handlers), resolve, // supports 同时要求版本受支持及行为已安装,而不是仅看 componentId 是否存在。 supports: (element: ElementRecord, capability: Capability) => resolve(element) === 'known' && !!capabilities.get(element.componentId)?.has(capability) }); } /** Capture each field once; JSON.stringify would silently erase invalid values. */ /** 迁移边界的 JSON 捕获:遍历的自有可枚举字符串字段取值一次,捕获后递归冻结。 * 不先 JSON.stringify,因为 undefined、函数、非有限数会被丢弃/改值,掩盖非法迁移输出。 */ function captureMigrationData(input:unknown,id:string):Data { // 本函数发现形状非法时带原元素 ID 抛 invalid_migration;getter/转换器自身抛出的异常不在这里包装。 const invalid=():never=>{throw fault('invalid_migration',[id]);}; // 递归接收 JSON 原子、稠密数组和普通对象;深度上限防止循环/异常深输入无限递归。 const capture=(value:unknown,depth=0):Json=>{ if(depth>64)return invalid(); if(value===null||typeof value==='string'||typeof value==='boolean')return value; if(typeof value==='number')return Number.isFinite(value)?value:invalid(); if(!value||typeof value!=='object')return invalid(); // JSON 无法表达可枚举 Symbol 键;明确拒绝,不能静默漏掉迁移数据。 if(Object.getOwnPropertySymbols(value).some(key=>Object.prototype.propertyIsEnumerable.call(value,key)))return invalid(); // 同时要求自有连续下标与键数一致,排除空洞及附加属性。 if(Array.isArray(value)){ if(Object.keys(value).length!==value.length)return invalid(); const result:Json[]=[];for(let index=0;index; return freeze(Object.fromEntries(Object.keys(source).map(key=>[key,capture(source[key],depth+1)]))); }; const result=capture(input); if(!result||typeof result!=='object'||Array.isArray(result))return invalid(); return result; } /** 按 migrations[旧版本] 逐级转换到当前版本,每步重新捕获,最后通过目标 schema。 * 未知组件/未来版本原样保留;缺 converter 或任一步失败抛错,不返回半迁移记录。 * ID、incarnation、owner 与其它信封字段值不变,转换器只负责 data。 * 其余信封对象按引用沿用,返回时 freeze 可能冻结这些共享嵌套值;调用方应传入已捕获的记录。 */ export function migrateElement(element: ElementRecord, registry: Registry): ElementRecord { const definition = registry.components.get(element.componentId); // 未安装/未来版本保持原引用且不捕获;即使版本已是当前版本,下方仍捕获 data 并校验 schema。 if (!definition || element.dataVersion > definition.dataVersion) return element; let data = captureMigrationData(element.data,element.id), version = element.dataVersion; // 一个 converter 仅前进一个版本,不把“有版本号”当成自动跨版本兼容承诺。 while (version < definition.dataVersion) { const migrate = definition.migrations?.[version]; if (!migrate) throw fault('missing_migration', [element.id]); data = captureMigrationData(migrate(data),element.id); version++; } if (!definition.validate(data)) throw fault('invalid_component_data', [element.id]); return freeze({ ...element, dataVersion: version, data }); }