Files
Endoleap/src/lib/canvas-core/values.ts
T

85 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 内核共享的值操作:错误归一化、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));
}