@akala/core
Version:
1,053 lines • 42.9 kB
JavaScript
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