mancha
Version:
Javscript HTML rendering engine
278 lines • 9.57 kB
JavaScript
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