import {Accumulator, BaseSignal, Cache, Listener, ReadableSignal} from './interfaces'; import { TagMap } from './tag-map'; export class ExtendedSignal implements ReadableSignal { public static merge(...signals: BaseSignal[]): ReadableSignal { const listeners = new Map(); return new ExtendedSignal({ add(listener) { const newListener = (payload: U) => listener(payload); listeners.set(listener, newListener); signals.forEach(signal => signal.add(newListener)); }, remove(listener) { const newListener = listeners.get(listener); listeners.delete(listener); signals.forEach(signal => signal.remove(newListener)); }, }); } public static promisify( resolveSignal: BaseSignal, rejectSignal?: BaseSignal, ): Promise { return new Promise((resolve, reject) => { function clearListeners() { resolveSignal.remove(completeResolution); if (rejectSignal) { rejectSignal.remove(completeRejection); } } function completeRejection(payload: any) { clearListeners(); reject(payload); } function completeResolution(payload: U) { clearListeners(); resolve(payload); } resolveSignal.add(completeResolution); if (rejectSignal) { rejectSignal.add(completeRejection); } }); } private _tagMap = new TagMap(); constructor(private _baseSignal: BaseSignal) {} public add(listener: Listener, ...tags: any[]): void { this._tagMap.setListeners(listener, ...tags); this._baseSignal.add(listener); } public remove(listenerOrTag: any): void { this._tagMap.getListeners(listenerOrTag) .forEach(taggedListener => { this._baseSignal.remove(taggedListener); this._tagMap.clearListener(taggedListener); }); this._baseSignal.remove(listenerOrTag); this._tagMap.clearListener(listenerOrTag); } public addOnce(listener: Listener, ...tags: any[]): void { // to match the set behavior of add, only add the listener if the listener is not already // registered, don't add the same listener twice if (this._tagMap.getListeners(listener).size > 0) { return; } const oneTimeListener = (payload: T) => { this._baseSignal.remove(oneTimeListener); listener(payload); }; this._tagMap.setListeners(oneTimeListener, listener, ...tags); this._baseSignal.add(oneTimeListener); } public filter(filter: (payload: T) => payload is U): ReadableSignal; public filter(filter: (payload: T) => boolean): ReadableSignal; public filter(filter: (payload: T) => boolean): ReadableSignal { return convertedListenerSignal( this._baseSignal, listener => payload => { if (filter(payload)) { listener(payload); } }, ); } public map(transform: (payload: T) => U): ReadableSignal { return convertedListenerSignal( this._baseSignal, listener => payload => listener(transform(payload)), ); } public merge(...signals: BaseSignal[]): ReadableSignal { return ExtendedSignal.merge(this._baseSignal, ...signals); } public promisify(rejectSignal?: ReadableSignal): Promise { return ExtendedSignal.promisify(this._baseSignal, rejectSignal); } public readOnly(): ReadableSignal { return convertedListenerSignal( this._baseSignal, listener => payload => listener(payload), ); } public reduce(accumulator: Accumulator, initialValue: U): ReadableSignal { return convertedListenerSignal( this._baseSignal, listener => (() => { let accum = initialValue; return (payload: T) => { accum = accumulator(accum, payload); listener(accum); }; })(), ); } public peek(peekaboo: (payload: T) => void): ReadableSignal { return convertedListenerSignal( this._baseSignal, listener => payload => { peekaboo(payload); listener(payload); }, ); } public cache(cache: Cache): ReadableSignal { this._baseSignal.add(payload => cache.add(payload)); return convertedListenerSignal( this._baseSignal, listener => payload => listener(payload), (listener, listenerActive) => { cache.forEach(payload => { if (listenerActive()) { listener(payload); } }); }, ); } } /** * Provides a new signal, with its own set of listeners, and the ability to transform listeners that * are added to the new signal. */ function convertedListenerSignal( baseSignal: BaseSignal, convertListener: (listener: Listener) => Listener, postAddHook?: (listener: Listener, listenerActive: () => boolean) => void, ): ExtendedSignal { const listenerMap = new Map, Listener>(); return new ExtendedSignal({ add: listener => { const newListener = convertListener(listener); listenerMap.set(listener, newListener); baseSignal.add(newListener); if (postAddHook) { postAddHook(listener, () => listenerMap.has(listener)); } }, remove: listener => { const newListener = listenerMap.get(listener); listenerMap.delete(listener); // TODO undefined ok in other case if (newListener !== undefined) { baseSignal.remove(newListener); } }, }); }