UNPKG

mancha

Version:

Javscript HTML rendering engine

278 lines 9.57 kB
import * as jexpr from "jexpr"; class IDebouncer { timeouts = new Map(); debounce(millis, callback) { return new Promise((resolve, reject) => { const timeout = this.timeouts.get(callback); if (timeout) clearTimeout(timeout); this.timeouts.set(callback, setTimeout(() => { try { resolve(callback()); this.timeouts.delete(callback); } catch (exc) { reject(exc); } }, millis)); }); } } /** Default debouncer time in millis. */ export const REACTIVE_DEBOUNCE_MILLIS = 10; /** Shared AST factory. */ const AST_FACTORY = new jexpr.EvalAstFactory(); function isProxified(object) { return object instanceof SignalStore || object["__is_proxy__"]; } export function getAncestorValue(store, key) { const map = store?._store; if (map?.has(key)) { return map.get(key); } else if (map?.has("$parent")) { return getAncestorValue(map.get("$parent"), key); } else { return null; } } export function getAncestorKeyStore(store, key) { const map = store?._store; if (map?.has(key)) { return store; } else if (map?.has("$parent")) { return getAncestorKeyStore(map.get("$parent"), key); } else { return null; } } export function setAncestorValue(store, key, value) { const ancestor = getAncestorKeyStore(store, key); if (ancestor) { ancestor._store.set(key, value); } else { store._store.set(key, value); } } export function setNestedProperty(obj, path, value) { const keys = path.split("."); for (let i = 0; i < keys.length - 1; i++) { if (!(keys[i] in obj)) obj[keys[i]] = {}; obj = obj[keys[i]]; } obj[keys[keys.length - 1]] = value; } export class SignalStore extends IDebouncer { evalkeys = ["$elem", "$event"]; expressionCache = new Map(); observers = new Map(); keyHandlers = new Map(); _observer = null; _store = new Map(); _lock = Promise.resolve(); constructor(data) { super(); for (let [key, value] of Object.entries(data || {})) { // Use our set method to ensure that callbacks and wrappers are appropriately set, but ignore // the return value since we know that no observers will be triggered. this.set(key, value); } } wrapFunction(fn) { return (...args) => fn.call(this.$, ...args); } wrapObject(obj, callback) { // If this object is already a proxy or not a plain object (or array), return it as-is. if (obj == null || isProxified(obj) || (obj.constructor !== Object && !Array.isArray(obj))) { return obj; } return new Proxy(obj, { deleteProperty: (target, property) => { if (property in target) { delete target[property]; callback(); return true; } else { return false; } }, set: (target, prop, value, receiver) => { if (typeof value === "object" && obj != null) value = this.wrapObject(value, callback); const ret = Reflect.set(target, prop, value, receiver); callback(); return ret; }, get: (target, prop, receiver) => { if (prop === "__is_proxy__") return true; return Reflect.get(target, prop, receiver); }, }); } watch(key, observer) { const owner = getAncestorKeyStore(this, key); if (!owner) { throw new Error(`Cannot watch key "${key}" as it does not exist in the store.`); } if (!owner.observers.has(key)) { owner.observers.set(key, new Set()); } if (!owner.observers.get(key)?.has(observer)) { owner.observers.get(key)?.add(observer); } } addKeyHandler(pattern, handler) { if (!this.keyHandlers.has(pattern)) { this.keyHandlers.set(pattern, new Set()); } this.keyHandlers.get(pattern).add(handler); } async notify(key, debounceMillis = REACTIVE_DEBOUNCE_MILLIS) { const owner = getAncestorKeyStore(this, key); const observers = Array.from(owner?.observers.get(key) || []); await this.debounce(debounceMillis, () => Promise.all(observers.map((observer) => observer.call(this.proxify(observer))))); } get(key, observer) { if (observer) this.watch(key, observer); return getAncestorValue(this, key); } async set(key, value) { if (value === this._store.get(key)) return; const callback = () => this.notify(key); if (value && typeof value === "function") { value = this.wrapFunction(value); } if (value && typeof value === "object") { value = this.wrapObject(value, callback); } setAncestorValue(this, key, value); // Invoke any key handlers. for (const [pattern, handlers] of this.keyHandlers.entries()) { if (pattern.test(key)) { for (const handler of handlers) { await Promise.resolve(handler.call(this.$, key, value)); } } } // Invoke the callback to notify observers. await callback(); } async del(key) { // By setting to null, we trigger observers before deletion. await this.set(key, null); this._store.delete(key); this.observers.delete(key); } keys() { return Array.from(this._store.keys()); } has(key) { return this._store.has(key); } effect(observer) { return observer.call(this.proxify(observer)); } proxify(observer) { const keys = Array.from(this._store.entries()).map(([key]) => key); const keyval = Object.fromEntries(keys.map((key) => [key, null])); return new Proxy(keyval, { get: (_, prop, receiver) => { if (typeof prop === "string" && getAncestorKeyStore(this, prop)) { return this.get(prop, observer); } else if (prop === "$") { return this.proxify(observer); } else { return Reflect.get(this, prop, receiver); } }, set: (_, prop, value, receiver) => { if (typeof prop !== "string" || prop in this) { Reflect.set(this, prop, value, receiver); } else { this.set(prop, value); } return true; }, }); } get $() { return this.proxify(); } /** * Creates an evaluation function for the provided expression. * @param expr The expression to be evaluated. * @returns The evaluation function. */ makeEvalFunction(expr) { // Throw an error if the expression is not a simple one-liner. if (expr.includes(";")) { throw new Error("Complex expressions are not supported."); } // If the expression includes assignment, save the left-hand side for later. let assignResult = null; if (expr.includes(" = ")) { const [lhs, rhs] = expr.split(" = "); assignResult = lhs.trim(); expr = rhs.trim(); } // Otherwise, just return the simple expression function. return (thisArg, args) => { const ast = jexpr.parse(expr, AST_FACTORY); const ctx = ast ?.getIds([]) ?.map((id) => [id, args[id] ?? thisArg[id] ?? globalThis[id]]); const res = ast?.evaluate(Object.fromEntries(ctx || [])); if (assignResult) { setNestedProperty(thisArg, assignResult, res); } else { return res; } }; } /** * Retrieves or creates a cached expression function for the provided expression. * @param expr - The expression to retrieve or create a cached function for. * @returns The cached expression function. */ cachedExpressionFunction(expr) { expr = expr.trim(); if (!this.expressionCache.has(expr)) { this.expressionCache.set(expr, this.makeEvalFunction(expr)); } return this.expressionCache.get(expr); } eval(expr, args = {}) { // Determine whether we have already been proxified to avoid doing it again. const thisArg = this._observer ? this : this.$; if (this._store.has(expr)) { // Shortcut: if the expression is just an item from the value store, use that directly. return thisArg[expr]; } else { // Otherwise, perform the expression evaluation. const fn = this.cachedExpressionFunction(expr); try { return fn(thisArg, args); } catch (exc) { console.error(`Failed to evaluate expression: ${expr}`); console.error(exc); return null; } } } } //# sourceMappingURL=store.js.map