@akala/core
Version:
1,310 lines (1,174 loc) • 51.3 kB
text/typescript
import { map } from "../each.js";
import { EventEmitter } from "../events/event-emitter.js";
import { Event, type EventListener, type IEvent } from "../events/shared.js";
import { type Formatter, formatters, isReversible, type ReversibleFormatter } from "../formatters/index.js";
// import { type AllEventKeys, ErrorWithStatus, FormatExpression, HttpStatusCode, isPromiseLike, ObservableArray, Parser } from "../index.js";
import { EvaluatorAsFunction, type ParsedFunction } from "../parser/evaluator-as-function.js";
import { BinaryExpression } from "../parser/expressions/binary-expression.js";
import { BinaryOperator } from "../parser/expressions/binary-operator.js";
import { CallExpression } from "../parser/expressions/call-expression.js";
import { ConstantExpression } from "../parser/expressions/constant-expression.js";
import { ExpressionVisitor } from "../parser/expressions/visitors/expression-visitor.js";
import type { Expressions, StrictExpressions } from "../parser/expressions/expression.js";
import { MemberExpression } from "../parser/expressions/member-expression.js";
import { NewExpression } from "../parser/expressions/new-expression.js";
import { ParameterExpression } from "../parser/expressions/parameter-expression.js";
import { TernaryExpression } from "../parser/expressions/ternary-expression.js";
import { TernaryOperator } from "../parser/expressions/ternary-operator.js";
import { ExpressionSimplifyer } from "../parser/expressions/visitors/expression-simplifyer.js";
import { combineSubscriptions, isPromiseLike, type Subscription } from "../teardown-manager.js";
import { watcher, type Watcher, WatcherFormatter } from './shared.js'
import { AssignmentExpression } from "../parser/expressions/assignment-expression.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 { FormatExpression, Parser } from "../parser/parser.js";
import { AllEventKeys } from "../events/index.js";
export interface ObjectEvent<T, TKey extends keyof T>
{
readonly property: TKey;
readonly value: T[TKey];
readonly oldValue: T[TKey];
}
export class AsyncFormatter extends WatcherFormatter
{
private promise: PromiseLike<unknown>;
private value: unknown;
/**
* Formats the value.
* @param {unknown} value - The value to format.
* @returns {unknown} The formatted value.
*/
format(value: unknown)
{
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?: Watcher)
{
super(watcher);
}
}
formatters.register('async', AsyncFormatter);
export class EventFormatter<T extends unknown[]> extends WatcherFormatter
{
private event: Event<T>;
private value: T;
private sub?: Subscription;
/**
* Formats the value.
* @param {unknown} value - The value to format.
* @returns {T} The formatted value.
*/
format(value: unknown)
{
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 as T;
this.watcher?.emit('change');
}));
return this.value;
}
/**
* Creates an instance of EventFormatter.
* @param {Watcher} watcher - The watcher instance.
*/
constructor(watcher: Watcher)
{
super(watcher);
}
}
formatters.register('event', EventFormatter);
export default class Watch<T extends object> extends WatcherFormatter
{
private value: T;
/**
* Formats the value.
* @param {T} value - The value to format.
* @returns {T} The formatted value.
*/
format(value: T)
{
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
{
private binding: Binding<unknown> = new EmptyBinding();
private sub: Subscription;
private value: unknown;
/**
* Formats the value.
* @param {unknown} value - The value to format.
* @returns {unknown} The formatted value.
*/
format(value: unknown)
{
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: Watcher)
{
super(watcher);
}
}
formatters.register('unbind', BindingFormatter);
// export interface IWatched<T>
// {
// $$watchers?: { [key in keyof T]?: T[key] & ObservableObject<T[key]> };
// }
export type IWatched<T extends object> = T &
{
[watcher]: ObservableObject<T> | ObservableArray<T>;
};
export type Getter<TSource, TResult> = (target: TSource) => TResult;
export type WatchGetter<TSource, TResult> = (target: TSource, watcher: Watcher) => TResult;
export type Setter<TSource, TValue> = (target: TSource, value: TValue) => void;
export type IWatchable<T extends object> = {
[watcher]?: ObservableObject<T>;
};
export class BuildWatcherAndSetter<T> extends ExpressionVisitor
{
target: ParameterExpression<T>;
value: ParameterExpression<unknown>;
private static memberWatcher<T>(getter: WatchGetter<T, unknown>, member: ParsedFunction<PropertyKey>): WatchGetter<T, unknown>
{
let sub: Subscription;
let change = new Event<[]>();
// let myWatcher: Watcher = new EventEmitter({ change });
let result: ObservableObject<any> | ObservableArray<any>
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 as object)));
// result.watch(watcher, prop);
return result.array[prop];
}
else
{
const newResult = new ObservableObject<any>(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 as object)));
// result.watch(watcher, prop);
return result.getValue(prop);
}
}
}
private static formatWatcher<T>(source: WatchGetter<T, unknown>, instance: BuildWatcherAndSetter<T>, expression: FormatExpression<unknown>)
{
const result: { getter: WatchGetter<T, unknown>, formatterInstance: Formatter<unknown>, settings?: WatchGetter<T, unknown> } = { 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.
*/
public eval<TValue>(expression: Expressions): { watcher: WatchGetter<T, TValue>, setter?: Setter<T, TValue> }
{
this.target = new ParameterExpression<T>('target');
this.value = new ParameterExpression<TValue>('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: Setter<T, TValue>;
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: WatchGetter<unknown, void> = function (target, watcher)
{
switch (expression.operator)
{
case AssignmentOperator.Equal:
ObservableObject.setValue(upToBeforeLastGetter(target, null) as object, 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 as Binding<unknown>).setValue(value.getValue()) };
return function () { (expression.left.value as Binding<unknown>).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 as WatchGetter<unknown, PropertyKey>;
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: T, value: TValue) =>
{
let x = upToBeforeLastGetter(target, null);
if (x)
{
if (x instanceof Binding)
x = x.getValue();
if (typeof x == 'object')
new ObservableObject<any>(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: T, value: TValue) => previousSetter.setter(target, ((formatterGetter.formatterInstance || (formatterGetter.formatterInstance = new formatter(formatterGetter.settings?.(target, null)))) as ReversibleFormatter<unknown, unknown>).unformat(value));
}
else
{
this.visit(expression);
setter = null;
}
break;
default:
this.visit(expression);
}
return { watcher: this.getter as WatchGetter<T, TValue>, setter };
}
private boundObservables: Binding<unknown>[] = [];
private getter: WatchGetter<unknown, ObservableObject<any> | boolean | string | number | symbol | bigint | Function | undefined | unknown>;
/**
* Visits a constant expression.
* @param {ConstantExpression<unknown>} arg0 - The constant expression.
* @returns {StrictExpressions} The visited expression.
*/
visitConstant(arg0: ConstantExpression<unknown>): StrictExpressions
{
let sub;
this.getter = (target, watcher) =>
{
if (arg0.value instanceof Binding)
{
if (!sub)
{
sub = arg0.value.onChanged(ev => watcher.emit('change', arg0.value as object));
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<TOutput>(expression: FormatExpression<TOutput>): FormatExpression<TOutput>
{
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.
*/
public visitMember<T1, TMember extends keyof T1>(arg0: MemberExpression<T1, TMember, T1[TMember]>): StrictExpressions
{
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<PropertyKey>(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<T extends Expressions = StrictExpressions>(expression: TernaryExpression<T>): TernaryExpression<Expressions>
{
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<T, TMethod extends keyof T>(arg0: CallExpression<T, TMethod>): StrictExpressions
{
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<PropertyKey>(this.visit(arg0.method));
this.getter = (target, watcher) =>
{
const f = sourceGetter(target, watcher) as Function;
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) as Function;
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.
*/
public visitNew<T>(expression: NewExpression<T>): StrictExpressions
{
const source = this.getter;
const result: (WatchGetter<object, [PropertyKey, any]>)[] | (WatchGetter<object, any>)[] = [];
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<PropertyKey>(this.visit(arg0.member));
this.getter = source;
result.push((target, watcher) => [member(target), getter(target, watcher)] as const)
break;
case '[':
result.push((target, watcher) => getter(target, watcher) as any)
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<T extends Expressions = StrictExpressions>(expression: AssignmentExpression<T>): AssignmentExpression<Expressions>
{
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<T extends Expressions = StrictExpressions>(expression: BinaryExpression<T>): BinaryExpression<Expressions>
{
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) as number - (right(target, watcher) as number); break;
case BinaryOperator.Plus: this.getter = (target, watcher) => left(target, watcher) as number + (right(target, watcher) as number); break;
case BinaryOperator.Modulo: this.getter = (target, watcher) => left(target, watcher) as number % (right(target, watcher) as number); break;
case BinaryOperator.Div: this.getter = (target, watcher) => left(target, watcher) as number / (right(target, watcher) as number); break;
case BinaryOperator.Times: this.getter = (target, watcher) => left(target, watcher) as number * (right(target, watcher) as number); break;
case BinaryOperator.Pow: this.getter = (target, watcher) => Math.pow(left(target, watcher) as number, right(target, watcher) as number); break;
case BinaryOperator.Dot: this.getter = (target, watcher) => left(target, watcher)[right(target, watcher) as PropertyKey]; break;
case BinaryOperator.QuestionDot: this.getter = (target, watcher) => left(target, watcher)?.[right(target, watcher) as PropertyKey]; break;
case BinaryOperator.Format:
case BinaryOperator.Unknown:
throw new ErrorWithStatus(HttpStatusCode.NotImplemented, 'Not implemented/supported ' + expression.operator);
}
return expression;
}
}
type ObservableType<T extends object> =
{ [key in Exclude<keyof T, typeof allProperties>]: IEvent<[ObjectEvent<T, key>], void> } &
{ [allProperties]: IEvent<[ObjectEvent<T, keyof T>], void> };
export type BindingChangedEvent<T> = { value: T, oldValue: T };
export const BindingsProperty = Symbol('BindingsProperty');
type Bound<T> = T extends Binding<infer X> ? T : Binding<T>;
type Unbound<T> = T extends Binding<infer X> ? X : T;
type UnboundObject<T extends object> = { [K in keyof T]?: Unbound<T[K]> }
export class Binding<T> extends EventEmitter<{
change: Event<[BindingChangedEvent<T>]>
}>
{
/**
* Combines named bindings.
* @param {T} obj - The object with named bindings.
* @returns {Binding<UnboundObject<T>>} The combined binding.
*/
public static combineNamed<T extends { [K in keyof T]?: T[K] | Binding<T[K]> }>(obj: T): Binding<UnboundObject<T>>
{
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]])) as UnboundObject<T>;
})
}
/**
* Combines multiple bindings.
* @param {...(T[K] | Binding<T[K]>)[]} bindings - The bindings to combine.
* @returns {Binding<T>} The combined binding.
*/
public static combine<T extends unknown[]>(...bindings: { [K in keyof T]?: T[K] | Binding<T[K]> }): Binding<T>
{
const combinedBinding = new EmptyBinding<T>();
let values: T;
bindings = bindings.map(b => b instanceof Binding ? b : new EmptyBinding(b));
const subs: Subscription[] = []
bindings.forEach((binding, index) =>
{
subs.push((binding as Bound<T[typeof index]>)?.onChanged(ev =>
{
if (!values)
values = [] as T;
values[index] = ev.value;
combinedBinding.emit('change', { value: values, oldValue: null });
}));
});
combinedBinding.getValue = () => (values = bindings.map(b => (b as Binding<unknown>).getValue()) as T);
combinedBinding.setValue = (newValues: T) =>
{
newValues.forEach((value, index) =>
{
values[index] = value;
(bindings[index] as Bound<T[typeof index]>)?.setValue(value);
});
};
combinedBinding.onChanged = (handler, triggerOnRegister) =>
{
if (triggerOnRegister)
{
if (!values)
values = bindings.map(b => (b as Binding<unknown>).getValue()) as T;
}
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<T extends {}>(target: T, property: PropertyKey)
{
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.
*/
public static defineProperty<T = unknown>(target: object, property: PropertyKey, value?: T): Binding<T>
{
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<T>();
target[BindingsProperty][property] = binding;
if (value === binding)
{
let settingValue = false;
Object.defineProperty(binding, 'canSet', { value: true });
binding.setValue = function (newValue: T)
{
if (settingValue)
return;
const oldValue = value;
value = newValue;
settingValue = true;
// binding.setValue(newValue)//, binding);
binding.emit('change', { value: newValue, oldValue });
settingValue = false;
}
}
let bindingValueSubscription: Subscription;
Object.defineProperty(target, property, {
get()
{
return value;
}, set(newValue: T)
{
if (newValue instanceof Binding)
{
bindingValueSubscription?.();
bindingValueSubscription = newValue.onChanged(ev =>
{
binding.setValue(ev.value);
}, true);
binding.teardown(bindingValueSubscription);
}
else
binding.setValue(newValue);
}
});
return binding;
}
/**
* Pipes the binding to another expression.
* @param {TKey | string | Expressions | ((ev: BindingChangedEvent<T>) => U)} expression - The expression to pipe to.
* @returns {Binding<U>} The piped binding.
*/
public pipe<const TKey extends keyof T>(expression: TKey): Binding<T[TKey]>
public pipe<U>(expression: string | keyof T | Expressions | ((ev: BindingChangedEvent<T>) => U)): Binding<U>
public pipe<U>(expression: string | keyof T | Expressions | ((ev: BindingChangedEvent<T>) => U)): Binding<U>
{
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<U>(this, expression as Expressions);
const sub = this.onChanged(ev =>
{
if (ev.value !== ev.oldValue)
binding.attachWatcher(ev.value as object, binding.watcher);
})
binding.on(Symbol.dispose, () => sub());
// this.watcher.on('change', () =>
// {
// sub.watcher.emit('change');
// })
return binding;
}
watcher: Watcher = new EventEmitter<{ change: Event<[source?: object]> }>(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(public target: unknown, public readonly expression: Expressions)
{
super();
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: T;
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<T>(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 as T;
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<T>(target: Binding<any>, expression: Expressions): Binding<T> | null
{
return new Binding(target.target, target.expression === null ? expression : new ExpressionSimplifyer(target.expression).visit(expression))
}
private readonly attachWatcher: WatchGetter<unknown, T>;
/**
* Unwraps the element.
* @param {T} element - The element to unwrap.
* @returns {Partial<T>} The unwrapped element.
*/
public static unwrap<T>(element: T): Partial<T>
{
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;
})
}
private _setter?: (target: unknown, value: T) => void
/**
* 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.
*/
public onChanged(handler: (ev: { value: T, oldValue: T }) => void, triggerOnRegister?: boolean)
{
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.
*/
public setValue(value: T)
{
this._setter(this.target, value);
// this.emit('change', { value, oldValue: this.getValue() });
}
public get canSet() { return !!this._setter; }
/**
* Gets the value.
* @returns {T} The value.
*/
public getValue(): T
{
if (!this.expression)
return this.target as T;
return this.attachWatcher(this.target, null);
}
}
export class EmptyBinding<T> extends Binding<T>
{
/**
* Creates an instance of EmptyBinding.
* @param {T} [initialValue] - The initial value.
*/
constructor(initialValue?: T)
{
super(initialValue, null);
this.getValue = EmptyBinding.prototype.getValue;
this.setValue = EmptyBinding.prototype.setValue;
}
override get canSet(): boolean
{
return true;
}
/**
* Gets the value.
* @returns {T} The value.
*/
public getValue(): T
{
return this.target as T;
}
/**
* Sets the value.
* @param {T} newValue - The value to set.
*/
public setValue(newValue: T): void
{
const oldValue = this.target as T;
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<T extends object> extends EventEmitter<ObservableType<T>>
{
/**
* Unwraps the target object.
* @param {T} arg0 - The target object.
* @returns {T extends ObservableObject<infer X> ? X : T} The unwrapped object.
*/
static unwrap<T>(arg0: T): T extends ObservableObject<infer X> ? X : T
{
if (arg0 instanceof ObservableObject)
return arg0.target;
return arg0 as any;
}
/**
* Generates a dynamic proxy that gets and sets values from target, but triggers notifications on set.
*/
public static wrap<T extends object>(target: T): IWatched<T>
{
return new Proxy(new ObservableObject(target), {
get(observableTarget, property)
{
if (property === watcher)
return observableTarget;
return observableTarget.getValue(property as keyof T);
},
set(observableTarget, property, value)
{
return observableTarget.setValue(property as keyof T, value);
}
}) as any;
}
public readonly target: T & IWatchable<T>;
/**
* Creates an instance of ObservableObject.
* @param {T & { [watcher]?: ObservableObject<T> } | ObservableObject<T>} target - The target object.
*/
constructor(target: (T & IWatchable<T>) | ObservableObject<T>)
{
super(Number.POSITIVE_INFINITY);
if (target instanceof ObservableObject)
return target;
if (ObservableObject.isWatched<T>(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.
*/
public static watchAll<T extends object>(obj: T, watcher: Watcher): Subscription
{
if (Array.isArray(obj))
{
const oa = new ObservableArray(obj)
const sub = ObservableObject.watchAll(oa, watcher);
return sub;
}
let sub: Subscription;
if (obj instanceof ObservableArray)
{
const subs: Subscription[] = []
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 as AllEventKeys<ObservableType<T>>, (() =>
{
watcher.emit('change', obj);
}) as any));
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.
*/
public watch<const TKey extends AllEventKeys<ObservableType<T>>>(watcher: Watcher, property: TKey)
{
const sub = this.on(property, (ev =>
{
watcher.emit('change');
}) as EventListener<ObservableObject<T>['events'][TKey]>);
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.
*/
public static isWatched<T>(x: T): x is IWatched<T & object>
{
return typeof x == 'object' && x && (watcher in x);
}
/**
* Sets the value of a property.
* @param {Binding<T> | T} target - The target object or binding.
* @param {Expressions | PropertyKey} expression - The expression or property key.
* @param {any} value - The value to set.
*/
public static setValue<T extends object, const TKey extends keyof T>(target: Binding<T>, expression: TKey, value: T[TKey])
public static setValue<T extends object>(target: Binding<T>, expression: string, value: any)
public static setValue<T extends object>(target: Binding<T>, expression: Expressions, value: any)
public static setValue<T extends object, const TKey extends keyof T>(target: T, expression: TKey, value: T[TKey])
public static setValue<T extends object>(target: T, expression: string, value: any)
public static setValue<T extends object>(target: T, expression: Expressions, value: any)
public static setValue<T extends object>(target: T, expression: Expressions | PropertyKey, value: any)
{
if (typeof expression != 'object')
if (typeof expression == 'string')
expression = Parser.parameterLess.parse(expression, true);
else
expression = new MemberExpression<T, keyof T, T[keyof T]>(null, new ConstantExpression(expression as keyof T), 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.
*/
public setValue<const TKey extends keyof T>(property: TKey, value: T[TKey])
{
const oldValue = this.target[property];
this.target[property] = value;
// This one is specific to the property
this.emit(property as AllEventKeys<ObservableType<T>>, ...[{
property,
value,
oldValue
}] as any); // or a tighter type if desired
// This one is for the `*` symbol (allProperties)
this.emit(allProperties as AllEventKeys<ObservableType<T>>, ...[{
property,
value,
oldValue
} as ObjectEvent<T, TKey>] as any); // 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.
*/
public getValue<const TKey extends keyof T>(property: TKey): T[TKey]
{
return ObservableObject.getValue(this.target, property);
}
/**
* Gets the value of a property.
* @param {Binding<T> | T} target - The target object or binding.
* @param {TKey} property - The property key.
* @returns {T[TKey]} The value of the property.
*/
public static getValue<T, const TKey extends keyof T>(target: Binding<T>, property: TKey): T[TKey]
public static getValue<T, const TKey extends keyof T>(target: T, property: TKey): T[TKey]
public static getValue<T, const TKey extends keyof T>(target: T, property: TKey): T[TKey]
{
let result: T[TKey];
if (target instanceof Binding)
result = target.getValue()?.[property];
else if (Binding.hasBoundProperty(target,