111 lines
9.6 KiB
TypeScript
111 lines
9.6 KiB
TypeScript
/**
|
||
* 组件定义的装配与显式数据迁移。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<Record<Capability, CapabilityHandler>>): 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<string, ComponentDefinition>(), capabilities = new Map<string, ReadonlyValueSet<Capability>>(), handlers = new Map<string, ReadonlyMap<Capability, CapabilityHandler>>();
|
||
for (const input of definitions) {
|
||
const definition = defineComponent(input);
|
||
if (components.has(definition.id)) throw fault('duplicate_component', [definition.id]);
|
||
const found = new Map<Capability, CapabilityHandler>(), features = new Set<string>();
|
||
// 即使两个 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<value.length;index++){if(!Object.hasOwn(value,index))return invalid();result.push(capture(value[index],depth+1));}return freeze(result);
|
||
}
|
||
if(Object.getPrototypeOf(value)!==Object.prototype)return invalid();
|
||
// Object.keys 不遍历不可枚举字段;getter 在这里读取,结果复制为普通数据属性,不保留原描述符。
|
||
const source=value as Record<string,unknown>;
|
||
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 });
|
||
}
|