85 lines
5.9 KiB
TypeScript
85 lines
5.9 KiB
TypeScript
/**
|
||
* 内核共享的值操作:错误归一化、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<T>(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<T>(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<string, unknown>)[key], (b as Record<string, unknown>)[key]));
|
||
}
|
||
// 只在本模块中标记只读视图的来源,WeakSet 不会阻止无引用的视图被回收。
|
||
const ownedMapViews=new WeakSet<object>();
|
||
/** 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<K, V>(input: ReadonlyMap<K, V>): ReadonlyMap<K, V> {
|
||
// 从此以后 input 的增删与本视图无关;但 K/V 对象仍共享引用,键/值本身不是在此深复制的。
|
||
const map = new Map(input);
|
||
const view: ReadonlyMap<K, V> = {
|
||
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<T>(input: Iterable<T>): ReadonlyValueSet<T> {
|
||
// 成员表独立于原 iterable,成员若为对象则仍保留原身份;隐藏可变方法并不冻结对象成员。
|
||
const set = new Set(input);
|
||
const view: ReadonlyValueSet<T> = {
|
||
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));
|
||
}
|