import { DisposableWithEvents } from 'app/common/DisposableWithEvents'; import { Disposable, IDisposable, IDisposableOwner, Observable } from 'grainjs'; /** * A simple abstraction for events composition. It is an object that can emit a single value of type T, * and holds the last value emitted. It can be used to compose events from other events. * * Simple observables can't be used for this purpose because they are not reentrant. We can't update * an observable from within a listener, because it won't trigger a new event. * * This class is basically a wrapper around Observable, that emits events when the value changes after it is * set. * * Example: * const signal = Signal.create(null, 0); * signal.listen(value => console.log(value)); * const onlyEven = signal.filter(value => value % 2 === 0); * onlyEven.listen(value => console.log('even', value)); * * const flag1 = Signal.create(null, false); * const flag2 = Signal.create(null, false); * const flagAnd = Signal.compute(null, on => on(flag1) && on(flag2)); * // This will still emit multiple times with the same value repeated. * flagAnd.listen(value => console.log('Both are true', value)); * * // This will emit only when both are true, and will ignore further changes while both are true. * const toggle = flagAnd.distinct(); * * // Current value can be accessed via signal.state.get() * const emitter = Signal.from(null, 0); * // Emit values only when the toggle is true. * const emitterWhileAnd = emitter.filter(() => toggle.state.get()); * // Equivalent to: * const emitterWhileAnd = Signal.compute(null, on => on(toggle) ? on(emitter) : null).distinct(); */ export class Signal implements IDisposable, IDisposableOwner { /** * Creates a new event with a default value. A convenience method for creating an event that supports * generic attribute. */ public static create(owner: IDisposableOwner | null, value: T) { return new Signal(owner, value); } /** * Creates an event from a set of events. Holds last value emitted by any of the events. */ public static fromEvents( owner: Disposable | null, emitter: any, first: string, ...rest: string[] ) { const signal = Signal.create(owner, null); for(const event of [first, ...rest]) { signal._emitter.listenTo(emitter, event, (value: any) => signal.emit(value)); } return signal as Signal; } /** * Helper methods that creates a signal that emits the result of a function that takes a function */ public static compute(owner: Disposable | null, compute: ComputeFunction) { const signal = Signal.create(owner, null as any); const on: any = (s: Signal) => { if (!signal._listeners.has(s)) { signal._listeners.add(s); signal._emitter.listenTo(s._emitter, 'signal', () => signal.emit(compute(on))); } return s.state.get(); }; signal.state.set(compute(on)); return signal as Signal; } /** * Last value emitted if any. */ public state: Observable; /** * List of signals that we are listening to. Stored in a WeakSet to avoid memory leaks. */ private _listeners: WeakSet = new WeakSet(); /** * Flag that can be changed by stateless() function. It won't hold last value (but can't be used in compute function). */ private _emitter: DisposableWithEvents; private _beforeHandler: CustomEmitter; constructor(owner: IDisposableOwner|null, initialValue: T) { this._emitter = DisposableWithEvents.create(owner); this.state = Observable.create(this, initialValue); } public dispose() { this._emitter.dispose(); } public autoDispose(disposable: IDisposable) { this._emitter.autoDispose(disposable); } /** * Push all events from this signal to another signal. */ public pipe(signal: Signal) { this.autoDispose(this.listen(value => signal.emit(value))); return this; } /** * Modify all values emitted by this signal. */ public map(selector: (value: T) => Z): Signal { const signal = Signal.create(this, selector(this.state.get())); this.listen(value => { signal.emit(selector(value)); }); return signal; } /** * Creates a new signal with the same state, but it will only * emit those values that pass the test implemented by the provided function. */ public filter(selector: (value: T) => boolean): Signal { const signal = Signal.create(this, this.state.get()); this.listen(value => { if (selector(value)) { signal.emit(value); } }); return signal; } /** * Emit only the value that is different from the previous one. */ public distinct(): Signal { let last = this.state.get(); const signal = this.filter((value: any) => { if (value !== last) { last = value; return true; } return false; }); signal.state.set(last); return signal; } /** * Emits true or false only when the value is changed from truthy to falsy or vice versa. */ public flag() { return this.map(Boolean).distinct(); } /** * Listen to changes of the signal. */ public listen(handler: (value: T) => any) { const stateHandler = () => { handler(this.state.get()); }; this._emitter.on('signal', stateHandler); return { dispose: () => this._emitter.off('signal', stateHandler), }; } public emit(value: T) { if (this._beforeHandler) { this._beforeHandler(value, (emitted: T) => { this.state.set(emitted); this._emitter.trigger('signal', emitted); }); } else { this.state.set(value); this._emitter.trigger('signal', value); } } public before(handler: CustomEmitter) { this._beforeHandler = handler; } } type ComputeFunction = (on: (s: Signal) => TS) => T; type CustomEmitter = (value: T, emit: (value: T) => void) => any;