UNPKG

signal-utils

Version:

Utils for use with the Signals Proposal: https://github.com/proposal-signals/proposal-signals

275 lines (265 loc) 8.21 kB
import { createStorage, fnCacheFor } from './-private/util.ts.js'; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ // TODO: see if we can utilize these existing implementations // would these require yet another proxy? // Array clones the whole array and deep object does not // what are tradeoffs? does it matter much? // import { SignalObject } from "./object.ts"; // import { SignalArray } from "./array.ts"; const COLLECTION = Symbol("__ COLLECTION __"); const STORAGES_CACHE = new WeakMap(); function ensureStorages(context) { let existing = STORAGES_CACHE.get(context); if (!existing) { existing = new Map(); STORAGES_CACHE.set(context, existing); } return existing; } function storageFor(context, key) { let storages = ensureStorages(context); return storages.get(key); } function initStorage(context, key, initialValue = null) { let storages = ensureStorages(context); let initialStorage = createStorage(initialValue); storages.set(key, initialStorage); return initialStorage.get(); } function hasStorage(context, key) { return Boolean(storageFor(context, key)); } function readStorage(context, key) { let storage = storageFor(context, key); if (storage === undefined) { return initStorage(context, key, null); } return storage.get(); } function updateStorage(context, key, value = null) { let storage = storageFor(context, key); if (!storage) { initStorage(context, key, value); return; } storage.set(value); } function readCollection(context) { if (!hasStorage(context, COLLECTION)) { initStorage(context, COLLECTION, context); } return readStorage(context, COLLECTION); } function dirtyCollection(context) { if (!hasStorage(context, COLLECTION)) { initStorage(context, COLLECTION, context); } return updateStorage(context, COLLECTION, context); } /** * Deeply track an Array, and all nested objects/arrays within. * * If an element / value is ever a non-object or non-array, deep-tracking will exit * */ /** * Deeply track an Object, and all nested objects/arrays within. * * If an element / value is ever a non-object or non-array, deep-tracking will exit * */ /** * Deeply track an Object or Array, and all nested objects/arrays within. * * If an element / value is ever a non-object or non-array, deep-tracking will exit * */ function deepSignal(...[target, context]) { if ("kind" in context) { if (context.kind === "accessor") { return deepTrackedForDescriptor(target, context); } throw new Error(`Decorators of kind ${context.kind} are not supported.`); } return deep(target); } function deepTrackedForDescriptor(target, context) { const { name: key } = context; const { get } = target; return { get() { if (hasStorage(this, key)) { return readStorage(this, key); } let value = get.call(this); // already deep, due to init return initStorage(this, key, value); }, set(value) { let deepValue = deep(value); updateStorage(this, key, deepValue); // set.call(this, deepValue); //updateStorage(this, key, deepTracked(value)); // SAFETY: does TS not allow us to have a different type internally? // maybe I did something goofy. //(get.call(this) as Signal.State<Value>).set(value); }, init(value) { return deep(value); } }; } const TARGET = Symbol("TARGET"); const IS_PROXIED = Symbol("IS_PROXIED"); const SECRET_PROPERTIES = [TARGET, IS_PROXIED]; const ARRAY_COLLECTION_PROPERTIES = ["length"]; const ARRAY_CONSUME_METHODS = [Symbol.iterator, "at", "concat", "entries", "every", "filter", "find", "findIndex", "findLast", "findLastIndex", "flat", "flatMap", "forEach", "group", "groupToMap", "includes", "indexOf", "join", "keys", "lastIndexOf", "map", "reduce", "reduceRight", "slice", "some", "toString", "values", "length"]; const ARRAY_DIRTY_METHODS = ["sort", "fill", "pop", "push", "shift", "splice", "unshift", "reverse"]; const ARRAY_QUERY_METHODS = ["indexOf", "contains", "lastIndexOf", "includes"]; function deep(obj) { if (obj === null || obj === undefined) { return obj; } if (obj[IS_PROXIED]) { return obj; } if (Array.isArray(obj)) { return deepProxy(obj, arrayProxyHandler); } if (typeof obj === "object") { return deepProxy(obj, objProxyHandler); } return obj; } const arrayProxyHandler = { get(target, property, receiver) { let value = Reflect.get(target, property, receiver); if (property === TARGET) { return value; } if (property === IS_PROXIED) { return true; } if (typeof property === "string") { let parsed = parseInt(property, 10); if (!isNaN(parsed)) { // Why consume the collection? // because indices can change if the collection changes readCollection(target); readStorage(target, parsed); return deep(value); } if (ARRAY_COLLECTION_PROPERTIES.includes(property)) { readCollection(target); return value; } } if (typeof value === "function") { let fnCache = fnCacheFor(target); let existing = fnCache.get(property); if (!existing) { let fn = (...args) => { if (typeof property === "string") { if (ARRAY_QUERY_METHODS.includes(property)) { readCollection(target); let fn = target[property]; if (typeof fn === "function") { return fn.call(target, ...args.map(unwrap)); } } else if (ARRAY_CONSUME_METHODS.includes(property)) { readCollection(target); } else if (ARRAY_DIRTY_METHODS.includes(property)) { dirtyCollection(target); } } return Reflect.apply(value, receiver, args); }; fnCache.set(property, fn); return fn; } return existing; } return value; }, set(target, property, value, receiver) { if (typeof property === "string") { let parsed = parseInt(property, 10); if (!isNaN(parsed)) { updateStorage(target, property, value); // when setting, the collection must be dirtied.. :( // this is to support updating {{#each}}, // which uses object identity by default dirtyCollection(target); return Reflect.set(target, property, value, receiver); } else if (property === "length") { dirtyCollection(target); return Reflect.set(target, property, value, receiver); } } dirtyCollection(target); return Reflect.set(target, property, value, receiver); }, has(target, property) { if (SECRET_PROPERTIES.includes(property)) { return true; } readStorage(target, property); return property in target; }, getPrototypeOf() { return Array.prototype; } }; const objProxyHandler = { get(target, prop, receiver) { if (prop === TARGET) { return target; } if (prop === IS_PROXIED) { return true; } readStorage(target, prop); return deep(Reflect.get(target, prop, receiver)); }, has(target, prop) { if (SECRET_PROPERTIES.includes(prop)) { return true; } readStorage(target, prop); return prop in target; }, ownKeys(target) { readCollection(target); return Reflect.ownKeys(target); }, set(target, prop, value, receiver) { updateStorage(target, prop); dirtyCollection(target); return Reflect.set(target, prop, value, receiver); }, getPrototypeOf() { return Object.prototype; } }; const PROXY_CACHE = new WeakMap(); function unwrap(obj) { if (typeof obj === "object" && obj && TARGET in obj) { return obj[TARGET]; } return obj; } function deepProxy(obj, handler) { let existing = PROXY_CACHE.get(obj); if (existing) { return existing; } let proxied = new Proxy(obj, handler); PROXY_CACHE.set(obj, proxied); return proxied; } export { deep, deepSignal, dirtyCollection, hasStorage, initStorage, readCollection, readStorage, updateStorage }; //# sourceMappingURL=deep.ts.js.map