UNPKG

@akala/core

Version:
1,053 lines 42.9 kB
import { map } from "../each.js"; import { EventEmitter } from "../events/event-emitter.js"; import { Event } from "../events/shared.js"; import { formatters, isReversible } from "../formatters/index.js"; // import { type AllEventKeys, ErrorWithStatus, FormatExpression, HttpStatusCode, isPromiseLike, ObservableArray, Parser } from "../index.js"; import { EvaluatorAsFunction } from "../parser/evaluator-as-function.js"; import { BinaryOperator } from "../parser/expressions/binary-operator.js"; import { ConstantExpression } from "../parser/expressions/constant-expression.js"; import { ExpressionVisitor } from "../parser/expressions/visitors/expression-visitor.js"; import { MemberExpression } from "../parser/expressions/member-expression.js"; import { ParameterExpression } from "../parser/expressions/parameter-expression.js"; import { TernaryOperator } from "../parser/expressions/ternary-operator.js"; import { ExpressionSimplifyer } from "../parser/expressions/visitors/expression-simplifyer.js"; import { combineSubscriptions, isPromiseLike } from "../teardown-manager.js"; import { watcher, WatcherFormatter } from './shared.js'; import { AssignmentOperator } from "../parser/expressions/assignment-operator.js"; import { ExpressionType } from "../parser/expressions/expression-type.js"; import { ObservableArray } from "./array.js"; import ErrorWithStatus, { HttpStatusCode } from "../errorWithStatus.js"; import { Parser } from "../parser/parser.js"; export class AsyncFormatter extends WatcherFormatter { promise; value; /** * Formats the value. * @param {unknown} value - The value to format. * @returns {unknown} The formatted value. */ format(value) { if (!isPromiseLike(value)) this.value = value; else { if (this.promise !== value) { this.promise = value; this.value = null; value.then(v => { this.value = v; this.watcher?.emit('change'); }, err => console.debug('a watched promise failed with err %O', err)); } } return this.value; } /** * Creates an instance of AsyncFormatter. * @param {Watcher} [watcher] - The watcher instance. */ constructor(watcher) { super(watcher); } } formatters.register('async', AsyncFormatter); export class EventFormatter extends WatcherFormatter { event; value; sub; /** * Formats the value. * @param {unknown} value - The value to format. * @returns {T} The formatted value. */ format(value) { if (!(value instanceof Event)) throw new Error("Cannot watch a non-event"); if (this.event !== value) { this.event = value; this.sub?.(); this.value = null; } this.watcher.on(Symbol.dispose, this.sub = value.addListener((...v) => { this.value = v; this.watcher?.emit('change'); })); return this.value; } /** * Creates an instance of EventFormatter. * @param {Watcher} watcher - The watcher instance. */ constructor(watcher) { super(watcher); } } formatters.register('event', EventFormatter); export default class Watch extends WatcherFormatter { value; /** * Formats the value. * @param {T} value - The value to format. * @returns {T} The formatted value. */ format(value) { if (value != this.value) { this.value = value; ObservableObject.watchAll(value, this.watcher); this.watcher.emit('change'); } return this.value; } } ; formatters.register('watch', Watch); export class BindingFormatter extends WatcherFormatter { binding = new EmptyBinding(); sub; value; /** * Formats the value. * @param {unknown} value - The value to format. * @returns {unknown} The formatted value. */ format(value) { if (this.value != value) { if (value) { this.sub?.(); if (value instanceof Binding) this.sub = value.onChanged(ev => this.binding.setValue(ev.value), true); else this.binding.setValue(value); } else this.binding.setValue(value); this.value = value; this.watcher.on(Symbol.dispose, this.binding.onChanged((ev) => { this.watcher?.emit('change'); })); this.watcher.on(Symbol.dispose, () => this.sub?.()); } return this.binding.getValue(); } /** * Creates an instance of BindingFormatter. * @param {Watcher} watcher - The watcher instance. */ constructor(watcher) { super(watcher); } } formatters.register('unbind', BindingFormatter); export class BuildWatcherAndSetter extends ExpressionVisitor { target; value; static memberWatcher(getter, member) { let sub; let change = new Event(); // let myWatcher: Watcher = new EventEmitter({ change }); let result; return (target, watcher) => { if (!sub && watcher) change.pipe(watcher.getOrCreate('change')); let x = getter(target, watcher); if (x instanceof Binding) { if (watcher) x.onChanged(ev => watcher.emit('change', ev.value)); x = x.getValue(); } if (!x || typeof x != 'object') { // if (sub) // { // change.emit(); // sub(); // } return x; } const prop = member(target); if (x instanceof ObservableArray) { if (result && result === x) return result.array[prop]; else result = x; if (watcher) watcher.on(Symbol.dispose, sub = result.addListener(() => watcher.emit('change', x))); // result.watch(watcher, prop); return result.array[prop]; } else { const newResult = new ObservableObject(x); if (result && result === newResult) return result.getValue(prop); else result = newResult; if (watcher) watcher.on(Symbol.dispose, sub = result.on(prop, () => watcher.emit('change', x))); // result.watch(watcher, prop); return result.getValue(prop); } }; } static formatWatcher(source, instance, expression) { const result = { getter: source, formatterInstance: null }; if (expression.formatter) { if (expression.settings) { instance.visit(expression.settings); result.settings = instance.getter; } const formatter = expression.formatter; if (typeof formatter === 'function' && formatter.prototype instanceof WatcherFormatter) if (result.settings) result.getter = (target, watcher) => { const value = source(target, watcher); return (result.formatterInstance || (result.formatterInstance = new formatter(result.settings(target, watcher), watcher))).format(value instanceof ObservableObject ? value.target : value); }; else result.getter = (target, watcher) => { const value = source(target, watcher); return (result.formatterInstance || (result.formatterInstance = new formatter(watcher))).format(value instanceof ObservableObject ? value.target : value); }; else result.getter = (target, watcher) => { const value = source(target, watcher); return (result.formatterInstance || (result.formatterInstance = new formatter(result.settings?.(target, watcher)))).format(value instanceof ObservableObject ? value.target : value); }; } return result; } /** * Evaluates the expression. * @param {Expressions} expression - The expression to evaluate. * @returns {{ watcher: WatchGetter<T, TValue>, setter?: Setter<T, TValue> }} The watcher and setter. */ eval(expression) { this.target = new ParameterExpression('target'); this.value = new ParameterExpression('value'); this.getter = (target, watcher) => { if (target instanceof Binding) { if (watcher) if (!this.boundObservables.includes(target)) { watcher.on(Symbol.dispose, target.onChanged(ev => watcher.emit('change', ev.value))); this.boundObservables.push(target); } const subTarget = target.getValue(); if (subTarget) return new ObservableObject(subTarget).target; return null; } if (typeof target == 'object') return new ObservableObject(target).target; return target; }; const getter = this.getter; let setter; switch (expression.type) { case 'assign': if (expression.left.type == ExpressionType.MemberExpression) { this.getter = getter; setter = null; if (expression.left.source) this.visit(expression.left.source); const upToBeforeLastGetter = this.getter; this.getter = getter; this.visit(expression.right); const rhs = this.getter; const internalSetter = function (target, watcher) { switch (expression.operator) { case AssignmentOperator.Equal: ObservableObject.setValue(upToBeforeLastGetter(target, null), expression.left.member, rhs(target, watcher)); break; case AssignmentOperator.NullCoaleasce: case AssignmentOperator.Unknown: throw new ErrorWithStatus(HttpStatusCode.NotImplemented, 'Not implemented/supported ' + expression.operator); } }; this.getter = (target, watcher) => { return internalSetter(target, watcher); }; } else if (expression.left.type == ExpressionType.ConstantExpression) { if (expression.left.value instanceof Binding) { setter = null; this.visit(expression.right); const rhs = this.getter; this.getter = function (target, watcher) { const value = rhs(target, watcher); if (value instanceof Binding) return function () { expression.left.value.setValue(value.getValue()); }; return function () { expression.left.value.setValue(value); }; }; } else throw new ErrorWithStatus(HttpStatusCode.BadRequest, 'Cannot set a constant value ' + expression.left.value); } break; case 'member': this.visit(expression.member); const member = this.getter; this.getter = getter; if (expression.source) this.visit(expression.source); const upToBeforeLastGetter = this.getter; this.getter = BuildWatcherAndSetter.memberWatcher(upToBeforeLastGetter, (target) => member(target, null)); if (setter !== null) setter = (target, value) => { let x = upToBeforeLastGetter(target, null); if (x) { if (x instanceof Binding) x = x.getValue(); if (typeof x == 'object') new ObservableObject(x).setValue(member(target, null), value); else x[member(target, null)] = value; } }; break; case 'format': const formatter = expression.formatter; if (isReversible(formatter) && setter !== null) { const previousSetter = this.eval(expression.lhs); const formatterGetter = BuildWatcherAndSetter.formatWatcher(previousSetter.watcher, this, expression); this.getter = formatterGetter.getter; // let settingsGetter: WatchGetter<T, any>; // if (expression.settings) // settingsGetter = new BuildWatcherAndSetter().eval(expression.settings).watcher // let formatterInstance: ReversibleFormatter<unknown, unknown>; if (previousSetter) setter = (target, value) => previousSetter.setter(target, (formatterGetter.formatterInstance || (formatterGetter.formatterInstance = new formatter(formatterGetter.settings?.(target, null)))).unformat(value)); } else { this.visit(expression); setter = null; } break; default: this.visit(expression); } return { watcher: this.getter, setter }; } boundObservables = []; getter; /** * Visits a constant expression. * @param {ConstantExpression<unknown>} arg0 - The constant expression. * @returns {StrictExpressions} The visited expression. */ visitConstant(arg0) { let sub; this.getter = (target, watcher) => { if (arg0.value instanceof Binding) { if (!sub) { sub = arg0.value.onChanged(ev => watcher.emit('change', arg0.value)); watcher?.on(Symbol.dispose, sub); } return arg0.value.getValue(); } return arg0.value; }; return arg0; } /** * Visits a format expression. * @param {FormatExpression<TOutput>} expression - The format expression. * @returns {FormatExpression<TOutput>} The visited expression. */ visitFormat(expression) { const getter = this.getter; this.visit(expression.lhs); const source = this.getter; this.getter = getter; this.getter = BuildWatcherAndSetter.formatWatcher(source, this, expression).getter; return expression; } /** * Visits a member expression. * @param {MemberExpression<T1, TMember, T1[TMember]>} arg0 - The member expression. * @returns {StrictExpressions} The visited expression. */ visitMember(arg0) { if (arg0.source) this.visit(arg0.source); const getter = this.getter; if (typeof arg0.member == 'undefined' || arg0.member === null) return arg0; const member = new EvaluatorAsFunction().eval(this.visit(arg0.member)); this.getter = BuildWatcherAndSetter.memberWatcher(getter, member); return arg0; } /** * Visits a ternary expression. * @param {TernaryExpression<T>} expression - The ternary expression. * @returns {TernaryExpression<Expressions>} The visited expression. */ visitTernary(expression) { const source = this.getter; this.visit(expression.first); switch (expression.operator) { case TernaryOperator.Question: const condition = this.getter; this.getter = source; this.visit(expression.second); const second = this.getter; this.getter = source; this.visit(expression.third); const third = this.getter; this.getter = source; this.getter = (target, watcher) => condition(target, watcher) ? second(target, watcher) : third(target, watcher); break; } return expression; } /** * Visits a call expression. * @param {CallExpression<T, TMethod>} arg0 - The call expression. * @returns {StrictExpressions} The visited expression. */ visitCall(arg0) { const getter = this.getter; if (arg0.source) this.visit(arg0.source); const sourceGetter = this.getter; const argGetters = arg0.arguments.map(a => { this.getter = getter; this.visit(a); return this.getter; }); if (arg0.method) { this.getter = getter; const member = new EvaluatorAsFunction().eval(this.visit(arg0.method)); this.getter = (target, watcher) => { const f = sourceGetter(target, watcher); if (arg0.optional) return f?.[member(target)]?.apply(f, argGetters.map(g => g(target, watcher))); return f?.[member(target)].apply(f, argGetters.map(g => g(target, watcher))); }; } else this.getter = (target, watcher) => { const f = sourceGetter(target, watcher); return f && f(...argGetters.map(g => g(target, watcher))); }; return arg0; } /** * Visits a new expression. * @param {NewExpression<T>} expression - The new expression. * @returns {StrictExpressions} The visited expression. */ visitNew(expression) { const source = this.getter; const result = []; const evaluator = new EvaluatorAsFunction(); this.visitEnumerable(expression.init, () => { }, (arg0) => { this.visit(arg0.source); const getter = this.getter; switch (expression.newType) { case '{': const member = evaluator.eval(this.visit(arg0.member)); this.getter = source; result.push((target, watcher) => [member(target), getter(target, watcher)]); break; case '[': result.push((target, watcher) => getter(target, watcher)); break; } this.getter = source; return arg0; }); switch (expression.newType) { case "{": this.getter = (target, watcher) => { return Object.fromEntries(result.map(r => r(target, watcher))); }; break; case "[": this.getter = (target, watcher) => { return result.map(r => r(target, watcher)); }; break; default: throw new Error('Invalid new type'); } return expression; } /** * Visits a binary expression. * @param {BinaryExpression<T>} expression - The binary expression. * @returns {BinaryExpression<Expressions>} The visited expression. */ visitAssign(expression) { const source = this.getter; this.visit(expression.left); const left = this.getter; this.getter = source; this.visit(expression.right); const right = this.getter; switch (expression.operator) { case AssignmentOperator.Equal: this.getter = (target, watcher) => left(target, watcher) == right(target, watcher); break; case AssignmentOperator.NullCoaleasce: this.getter = (target, watcher) => left(target, watcher) == right(target, watcher); break; case AssignmentOperator.Unknown: default: throw new ErrorWithStatus(HttpStatusCode.NotImplemented, 'Not implemented/supported ' + expression.operator); } return expression; } /** * Visits a binary expression. * @param {BinaryExpression<T>} expression - The binary expression. * @returns {BinaryExpression<Expressions>} The visited expression. */ visitBinary(expression) { const source = this.getter; this.visit(expression.left); const left = this.getter; this.getter = source; this.visit(expression.right); const right = this.getter; switch (expression.operator) { case BinaryOperator.Equal: this.getter = (target, watcher) => left(target, watcher) == right(target, watcher); break; case BinaryOperator.StrictEqual: this.getter = (target, watcher) => left(target, watcher) === right(target, watcher); break; case BinaryOperator.NotEqual: this.getter = (target, watcher) => left(target, watcher) != right(target, watcher); break; case BinaryOperator.StrictNotEqual: this.getter = (target, watcher) => left(target, watcher) !== right(target, watcher); break; case BinaryOperator.LessThan: this.getter = (target, watcher) => left(target, watcher) < right(target, watcher); break; case BinaryOperator.LessThanOrEqual: this.getter = (target, watcher) => left(target, watcher) <= right(target, watcher); break; case BinaryOperator.GreaterThan: this.getter = (target, watcher) => left(target, watcher) > right(target, watcher); break; case BinaryOperator.GreaterThanOrEqual: this.getter = (target, watcher) => left(target, watcher) >= right(target, watcher); break; case BinaryOperator.And: this.getter = (target, watcher) => left(target, watcher) && right(target, watcher); break; case BinaryOperator.Or: this.getter = (target, watcher) => left(target, watcher) || right(target, watcher); break; case BinaryOperator.Minus: this.getter = (target, watcher) => left(target, watcher) - right(target, watcher); break; case BinaryOperator.Plus: this.getter = (target, watcher) => left(target, watcher) + right(target, watcher); break; case BinaryOperator.Modulo: this.getter = (target, watcher) => left(target, watcher) % right(target, watcher); break; case BinaryOperator.Div: this.getter = (target, watcher) => left(target, watcher) / right(target, watcher); break; case BinaryOperator.Times: this.getter = (target, watcher) => left(target, watcher) * right(target, watcher); break; case BinaryOperator.Pow: this.getter = (target, watcher) => Math.pow(left(target, watcher), right(target, watcher)); break; case BinaryOperator.Dot: this.getter = (target, watcher) => left(target, watcher)[right(target, watcher)]; break; case BinaryOperator.QuestionDot: this.getter = (target, watcher) => left(target, watcher)?.[right(target, watcher)]; break; case BinaryOperator.Format: case BinaryOperator.Unknown: throw new ErrorWithStatus(HttpStatusCode.NotImplemented, 'Not implemented/supported ' + expression.operator); } return expression; } } export const BindingsProperty = Symbol('BindingsProperty'); export class Binding extends EventEmitter { target; expression; /** * Combines named bindings. * @param {T} obj - The object with named bindings. * @returns {Binding<UnboundObject<T>>} The combined binding. */ static combineNamed(obj) { const entries = Object.entries(obj); return Binding.combine(...entries.map(e => e[1])).pipe(ev => { return Object.fromEntries(entries.map((e, i) => [e[0], ev.value[i]])); }); } /** * Combines multiple bindings. * @param {...(T[K] | Binding<T[K]>)[]} bindings - The bindings to combine. * @returns {Binding<T>} The combined binding. */ static combine(...bindings) { const combinedBinding = new EmptyBinding(); let values; bindings = bindings.map(b => b instanceof Binding ? b : new EmptyBinding(b)); const subs = []; bindings.forEach((binding, index) => { subs.push(binding?.onChanged(ev => { if (!values) values = []; values[index] = ev.value; combinedBinding.emit('change', { value: values, oldValue: null }); })); }); combinedBinding.getValue = () => (values = bindings.map(b => b.getValue())); combinedBinding.setValue = (newValues) => { newValues.forEach((value, index) => { values[index] = value; bindings[index]?.setValue(value); }); }; combinedBinding.onChanged = (handler, triggerOnRegister) => { if (triggerOnRegister) { if (!values) values = bindings.map(b => b.getValue()); } return Binding.prototype.onChanged.call(combinedBinding, handler, triggerOnRegister); }; combinedBinding.on(Symbol.dispose, combineSubscriptions(...subs)); return combinedBinding; } /** * Checks if a target has a bound property. * @param {T} target - The target object. * @param {PropertyKey} property - The property key. * @returns {boolean} True if the target has a bound property, false otherwise. */ static hasBoundProperty(target, property) { if (typeof target !== 'object') return false; if (!(BindingsProperty in target)) return false; return !!target[BindingsProperty][property]; } /** * Defines a property on the target object. * @param {object} target - The target object. * @param {PropertyKey} property - The property key. * @param {T} [value] - The initial value. * @returns {Binding<T>} The defined binding. */ static defineProperty(target, property, value) { if (!(BindingsProperty in target)) target[BindingsProperty] = {}; if (target[BindingsProperty][property]) return target[BindingsProperty][property]; // const binding = new Binding<T>(target, typeof property == 'symbol' ? new MemberExpression(null, new ConstantExpression(property), false) : new Parser().parse(property)); const binding = value instanceof Binding ? value : new EmptyBinding(); target[BindingsProperty][property] = binding; if (value === binding) { let settingValue = false; Object.defineProperty(binding, 'canSet', { value: true }); binding.setValue = function (newValue) { if (settingValue) return; const oldValue = value; value = newValue; settingValue = true; // binding.setValue(newValue)//, binding); binding.emit('change', { value: newValue, oldValue }); settingValue = false; }; } let bindingValueSubscription; Object.defineProperty(target, property, { get() { return value; }, set(newValue) { if (newValue instanceof Binding) { bindingValueSubscription?.(); bindingValueSubscription = newValue.onChanged(ev => { binding.setValue(ev.value); }, true); binding.teardown(bindingValueSubscription); } else binding.setValue(newValue); } }); return binding; } pipe(expression) { if (typeof expression == 'function') { const formatter = expression; const binding = new EmptyBinding(undefined); let initialized = false; binding.getValue = () => { if (!initialized) { initialized = true; binding.setValue(formatter({ value: this.getValue(), oldValue: undefined })); } return EmptyBinding.prototype.getValue.call(binding); }; binding.onChanged = (handler, triggerOnRegister) => { const sub = Binding.prototype.onChanged.call(binding, handler, false); if (triggerOnRegister) handler({ value: binding.getValue(), oldValue: undefined }); return sub; }; const sub = this.onChanged(ev => binding.setValue(formatter({ value: ev.value, oldValue: undefined }))); binding.on(Symbol.dispose, () => sub()); return binding; } if (typeof expression == 'string') expression = Parser.parameterLess.parse(expression); else if (typeof expression != 'object') expression = new MemberExpression(null, new ConstantExpression(expression), true); const binding = new Binding(this, expression); const sub = this.onChanged(ev => { if (ev.value !== ev.oldValue) binding.attachWatcher(ev.value, binding.watcher); }); binding.on(Symbol.dispose, () => sub()); // this.watcher.on('change', () => // { // sub.watcher.emit('change'); // }) return binding; } watcher = new EventEmitter(Number.POSITIVE_INFINITY); [Symbol.dispose]() { super[Symbol.dispose](); this.watcher[Symbol.dispose](); } /** * Creates an instance of Binding. * @param {unknown} target - The target object. * @param {Expressions} expression - The expression. */ constructor(target, expression) { super(); this.target = target; this.expression = expression; if (target instanceof Binding) if (!expression) return target; else if (target instanceof EmptyBinding) { } else return Binding.simplify(target, expression); this.set('change', new Event(Number.POSITIVE_INFINITY)); if (expression) { let value; this.watcher.on('change', (x) => { const oldValue = value; if (x) { // this.watcher[Symbol.dispose](); // this.watcher = new EventEmitter(); value = watcherAndSetter.watcher(target, this.watcher); } else value = watcherAndSetter.watcher(this.target, null); // value = this.getValue(); if (isPromiseLike(value)) value.then(v => { if (oldValue !== v) this.emit('change', { value: v, oldValue }); }); else if (value !== oldValue) this.emit('change', { value, oldValue }); }); const watcherAndSetter = new BuildWatcherAndSetter().eval(expression); this.attachWatcher = watcherAndSetter.watcher; value = watcherAndSetter.watcher(target, this.watcher); if (watcherAndSetter.setter) this._setter = watcherAndSetter.setter; this.getValue = () => value; } else { this.getValue = () => this.target; this._setter = () => { throw new ErrorWithStatus(HttpStatusCode.MethodNotAllowed, 'There is no expression, thus you cannot set the value'); }; } } /** * Simplifies the binding. * @param {Binding<any>} target - The target binding. * @param {Expressions} expression - The expression. * @returns {Binding<T> | null} The simplified binding. */ static simplify(target, expression) { return new Binding(target.target, target.expression === null ? expression : new ExpressionSimplifyer(target.expression).visit(expression)); } attachWatcher; /** * Unwraps the element. * @param {T} element - The element to unwrap. * @returns {Partial<T>} The unwrapped element. */ static unwrap(element) { if (element instanceof Binding) return element.getValue(); return map(element, function (value) { if (typeof (value) == 'object') { if (value instanceof Binding) return value.getValue(); else return Binding.unwrap(value); } else return value; }); } _setter; /** * Registers a handler for the change event. * @param {(ev: { value: T, oldValue: T }) => void} handler - The event handler. * @param {boolean} [triggerOnRegister] - Whether to trigger the handler on registration. * @returns {Subscription} The subscription. */ onChanged(handler, triggerOnRegister) { const sub = this.on('change', handler); if (triggerOnRegister) handler({ value: this.getValue(), oldValue: null }); return sub; } /** * Sets the value. * @param {T} value - The value to set. */ setValue(value) { this._setter(this.target, value); // this.emit('change', { value, oldValue: this.getValue() }); } get canSet() { return !!this._setter; } /** * Gets the value. * @returns {T} The value. */ getValue() { if (!this.expression) return this.target; return this.attachWatcher(this.target, null); } } export class EmptyBinding extends Binding { /** * Creates an instance of EmptyBinding. * @param {T} [initialValue] - The initial value. */ constructor(initialValue) { super(initialValue, null); this.getValue = EmptyBinding.prototype.getValue; this.setValue = EmptyBinding.prototype.setValue; } get canSet() { return true; } /** * Gets the value. * @returns {T} The value. */ getValue() { return this.target; } /** * Sets the value. * @param {T} newValue - The value to set. */ setValue(newValue) { const oldValue = this.target; this.target = newValue; // if (value !== oldValue) this.emit('change', { oldValue, value: newValue }); } } export const allProperties = Symbol('*'); /** * Observable object implementation. * @param {Object} initialObject - The initial object. */ export class ObservableObject extends EventEmitter { /** * Unwraps the target object. * @param {T} arg0 - The target object. * @returns {T extends ObservableObject<infer X> ? X : T} The unwrapped object. */ static unwrap(arg0) { if (arg0 instanceof ObservableObject) return arg0.target; return arg0; } /** * Generates a dynamic proxy that gets and sets values from target, but triggers notifications on set. */ static wrap(target) { return new Proxy(new ObservableObject(target), { get(observableTarget, property) { if (property === watcher) return observableTarget; return observableTarget.getValue(property); }, set(observableTarget, property, value) { return observableTarget.setValue(property, value); } }); } target; /** * Creates an instance of ObservableObject. * @param {T & { [watcher]?: ObservableObject<T> } | ObservableObject<T>} target - The target object. */ constructor(target) { super(Number.POSITIVE_INFINITY); if (target instanceof ObservableObject) return target; if (ObservableObject.isWatched(target)) return target[watcher]; this.target = target; Object.defineProperty(target, watcher, { value: this, enumerable: false, configurable: false }); } /** * Watches all properties of the object. * @param {T} obj - The object to watch. * @param {Watcher} watcher - The watcher instance. * @returns {Subscription} The subscription. */ static watchAll(obj, watcher) { if (Array.isArray(obj)) { const oa = new ObservableArray(obj); const sub = ObservableObject.watchAll(oa, watcher); return sub; } let sub; if (obj instanceof ObservableArray) { const subs = []; watcher.on(Symbol.dispose, sub = obj.addListener(ev => { watcher.emit('change', obj); switch (ev.action) { case "pop": ev.oldItems.forEach(x => { subs.pop()(); }); break; case "init": case "push": subs.push(...ev.newItems.map(x => ObservableObject.watchAll(x, watcher))); break; case "shift": ev.oldItems.forEach(x => { subs.shift()(); }); break; case "unshift": subs.unshift(...ev.newItems.map(x => ObservableObject.watchAll(x, watcher))); break; case "replace": ev.replacedItems.forEach(x => { subs.splice(x.index, 1, ObservableObject.watchAll(x.newItem, watcher))[0]?.(); }); break; } }, { triggerAtRegistration: true })); return () => { const result = sub(); subs.forEach(s => s()); return result; }; } if (obj instanceof Binding) { watcher.on(Symbol.dispose, sub = obj.onChanged(ev => watcher.emit('change', obj))); return sub; } const oo = new ObservableObject(obj); watcher.on(Symbol.dispose, sub = oo.on(allProperties, (() => { watcher.emit('change', obj); }))); Object.entries(obj).forEach(e => { if (typeof e[1] == 'object') ObservableObject.watchAll(e[1], watcher); }); } /** * Watches a property of the object. * @param {Watcher} watcher - The watcher instance. * @param {TKey} property - The property to watch. * @returns {Subscription} The subscription. */ watch(watcher, property) { const sub = this.on(property, (ev => { watcher.emit('change'); })); watcher.once(Symbol.dispose, sub); return sub; } // private setters: { [key in keyof T]?: (target: T, value: T[key]) => void } = {}; /** * Checks if an object is watched. * @param {T} x - The object to check. * @returns {boolean} True if the object is watched, false otherwise. */ static isWatched(x) { return typeof x == 'object' && x && (watcher in x); } static setValue(target, expression, value) { if (typeof expression != 'object') if (typeof expression == 'string') expression = Parser.parameterLess.parse(expression, true); else expression = new MemberExpression(null, new ConstantExpression(expression), true); const evaluator = new BuildWatcherAndSetter().eval(expression); if (evaluator.setter) evaluator.setter(target, value); else throw new ErrorWithStatus(HttpStatusCode.MethodNotAllowed, 'This expression is not supported to apply reverse binding'); } /** * Sets the value of a property. * @param {TKey} property - The property key. * @param {T[TKey]} value - The value to set. * @returns {boolean} True if the value was set, false otherwise. */ setValue(property, value) { const oldValue = this.target[property]; this.target[property] = value; // This one is specific to the property this.emit(property, ...[{ property, value, oldValue }]); // or a tighter type if desired // This one is for the `*` symbol (allProperties) this.emit(allProperties, ...[{ property, value, oldValue }]); // it's the same shape return true; } /** * Gets the value of a property. * @param {TKey} property - The property key. * @returns {T[TKey]} The value of the property. */ getValue(property) { return ObservableObject.getValue(this.target, property); } static getValue(target, property) { let result; if (target instanceof Binding) result = target.getValue()?.[property]; else if (Binding.hasBoundProperty(target, property)) result = target[BindingsProperty][property].getValue(); else result = target[property]; if (typeof result == 'object') if (Array.isArray(result) || result instanceof ObservableArray) return new ObservableArray(result); else if (result !== null) return new ObservableObject(result).target; return result; } /** * Gets the observable object for a property. * @param {TKey} property - The property key. * @returns {ObservableObject<T[TKey]> | null} The observable object or null. */ getObservable(property) { if (typeof this.target[property] == 'object') return new ObservableObject(this.target[property]); return null; } /** * Gets the value of a property. * @param {T} target - The target object. * @param {keyof T} property - The property key. * @returns {T[keyof T]} The value of the property. */ static get(target, property) { return target[property]; } } //# sourceMappingURL=object.js.map