signal-utils
Version:
Utils for use with the Signals Proposal: https://github.com/proposal-signals/proposal-signals
275 lines (265 loc) • 8.21 kB
JavaScript
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