275 lines
8.0 KiB
JavaScript
275 lines
8.0 KiB
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
|