/** * 内核共享的值操作:错误归一化、JSON 值复制/比较,以及运行时只读集合。 * 这些工具不读取网络或编辑器状态。注意类型上的 readonly 与运行时不可修改不是一回事; * readMap/readSet 通过隐藏可变容器实现后者,freeze 不会使 Map 的 set 方法失效。 */ import type { Id, Json, ReadonlyValueSet } from './types.ts'; /** 构造携带稳定错误码和相关对象 ID 的错误,供命令层返回可解释的拒绝结果。 * targets 沿用传入数组引用;这个辅助函数本身不捕获或冻结错误明细。 */ export function fault(code: string, targets: readonly Id[] = []): Error & { code: string; targets: readonly Id[] } { return Object.assign(new Error(code), { code, targets }); } /** 将捕获的异常归一化为浅冻结的 rejected 外壳;无领域错误码时使用 invalid_command。 * 已有 targets 数组仍沿用原引用,不应把外壳冻结解释为整个错误结果深度不可变。 */ export function reject(error: unknown) { const value = error as { code?: string; targets?: readonly string[] }; return Object.freeze({ status: 'rejected' as const, code: value?.code || 'invalid_command', targets: value?.targets || [] }); } /** 递归冻结普通对象/数组的可枚举值;已冻结对象直接返回,不继续检查其子树。 * 调用者负责提供适合此工具的数据,不能用它证明任意外部对象已经深度不可变。 */ export function freeze(value: T): T { if (value && typeof value === 'object' && !Object.isFrozen(value)) { for (const child of Object.values(value)) freeze(child); Object.freeze(value); } return value; } /** JSON 往返复制已验证的数据,切断共享引用;undefined 原样返回。 * 此处不是校验器:JSON 会执行 toJSON、读取可枚举字段并丢弃/改写不支持的值。 * 不保留原型、描述符或共享子对象身份;需要严格输入语义时须在调用边界捕获。 */ export function clone(value: T): T { if (value === undefined) return value; return JSON.parse(JSON.stringify(value)) as T; } /** 先以 Object.is 判断,再按可枚举自有字符串键递归比较 JSON 风格的值;数组与对象不能互相视为相等。 * 不检查 Symbol/不可枚举字段,数组长度本身也不在 Object.keys 中;稀疏数组不适合作为此比较器的输入。 * 不处理任意循环对象,也不提供 Map/Set 成员比较语义。 */ export function equal(a: unknown, b: unknown): boolean { if (Object.is(a, b)) return true; if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) return false; const left = Object.keys(a), right = Object.keys(b); return left.length === right.length && left.every(key => Object.prototype.hasOwnProperty.call(b, key) && equal((a as Record)[key], (b as Record)[key])); } // 只在本模块中标记只读视图的来源,WeakSet 不会阻止无引用的视图被回收。 const ownedMapViews=new WeakSet(); /** Internal provenance only; table ownership does not imply immutable values. */ /** 查询“是否由 readMap 创建”,不证明表内每个 value 已冻结或通过 schema。 */ export function isOwnedMapView(value:object){return ownedMapViews.has(value);} /** A capability-safe read view: unlike TS ReadonlyMap, no .set is exposed at runtime. */ /** 复制表成员并仅暴露读取能力,外部既拿不到原 Map,也拿不到 set/delete。 * value 保留原引用,深度不可变性由生产这些 value 的模块负责。 */ export function readMap(input: ReadonlyMap): ReadonlyMap { // 从此以后 input 的增删与本视图无关;但 K/V 对象仍共享引用,键/值本身不是在此深复制的。 const map = new Map(input); const view: ReadonlyMap = { get size() { return map.size; }, get: key => map.get(key), has: key => map.has(key), entries: () => map.entries(), keys: () => map.keys(), values: () => map.values(), [Symbol.iterator]: () => map[Symbol.iterator](), // 回调的第三个参数也必须是安全视图,不能把内部可变 Map 泄漏出去。 forEach: (callback, thisArg) => map.forEach((value, key) => callback.call(thisArg, value, key, view)), }; const result=Object.freeze(view);ownedMapViews.add(result);return result; } /** 捕获一份成员集合并隐藏 add/delete;回调同样只接触只读视图。 */ export function readSet(input: Iterable): ReadonlyValueSet { // 成员表独立于原 iterable,成员若为对象则仍保留原身份;隐藏可变方法并不冻结对象成员。 const set = new Set(input); const view: ReadonlyValueSet = { get size() { return set.size; }, has: key => set.has(key), entries: () => set.entries(), keys: () => set.keys(), values: () => set.values(), [Symbol.iterator]: () => set[Symbol.iterator](), forEach: callback => set.forEach(value => callback(value, value, view)), }; return Object.freeze(view); } /** 递归检查 JSON 值域:排除 undefined/函数/非有限数,最多允许 64 层。 * 对象要求 Object.prototype 原型,但只遍历 Object.values;Symbol/不可枚举属性不参与。 * 数组用 every 跳过空洞,也不检查额外属性;它不是稠密数组或所有属性描述符的严格捕获器。 * 校验读取可能触发 getter;这里不证明组件 schema、输入稳定性或数据所有权。 */ export function validJson(value: unknown, depth = 0): value is Json { if (depth > 64) return false; if (value === null || typeof value === 'string' || typeof value === 'boolean') return true; if (typeof value === 'number') return Number.isFinite(value); if (Array.isArray(value)) return value.every(item => validJson(item, depth + 1)); return !!value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(child => validJson(child, depth + 1)); }