/*! Implementation of signal/slot pattern. cons: - type safety - faster than events in some engines (\see https://github.com/evg656e/vbase-benchmarks/blob/master/benchmarks/observer/results.md) pros: - non-standard approach - less memory efficient \todo Add 'prepend' options to 'connect' method. \see https://github.com/millermedeiros/js-signals/wiki/Comparison-between-different-Observer-Pattern-implementations \see http://robdodson.me/javascript-design-patterns-observer/ \see https://stackoverflow.com/questions/340383/can-a-javascript-object-have-a-prototype-chain-but-also-be-a-function \see https://stackoverflow.com/questions/36871299/how-to-extend-function-with-es6-classes \see https://github.com/Gozala/events#readme \see https://github.com/nodejs/node/blob/master/lib/events.js */ export type Signal any> = Slot & { connect(slot: Slot): void; disconnect(slot: Slot): void; disconnectAll(): void; once(slot: Slot): void; slotCount(): number; }; interface SignalConstructor { new any>(): Signal; any>(): Signal; } export const Signal = function Signal() { function emit(...args: any[]) { const slots = (<{ (...args: any[]): boolean; _slots: undefined | Function | Function[] }>emit)._slots; if (slots === undefined) return false; if (typeof slots === 'function') Reflect.apply(slots, emit, args); else { const len = slots.length; const _slots = clone(slots, len); for (let i = 0; i < len; i++) Reflect.apply(_slots[i], emit, args); } return true; } return Object.setPrototypeOf(emit, Signal.prototype); }; type SignalThis any> = Signal & { _slots: undefined | Slot | Slot[] }; type Wrapped any> = Slot & { wrapped: Slot }; Signal.prototype.connect = function any>(this: SignalThis, slot: Slot) { if (typeof slot !== 'function') throw new TypeError('Slot must be a function'); const slots = this._slots; if (slots === undefined) this._slots = slot; else if (typeof slots === 'function') this._slots = [slots, slot]; else slots.push(slot); }; Signal.prototype.disconnect = function any>(this: SignalThis, slot: Slot) { if (typeof slot !== 'function') throw new TypeError('Slot must be a function'); const slots = this._slots; if (slots === undefined) return; if (typeof slots === 'function') { if (slot === slots || slot === (>slots).wrapped) delete this._slots; } else { for (let i = slots.length; i-- > 0;) { const _slot = slots[i]; if (slot === _slot || slot === (>_slot).wrapped) slots.splice(i, 1); } if (slots.length === 1) this._slots = slots[0]; else if (slots.length === 0) delete this._slots; } }; Signal.prototype.disconnectAll = function any>(this: SignalThis) { delete this._slots; }; Signal.prototype.once = function any>(this: SignalThis, slot: Slot) { if (typeof slot !== 'function') throw new TypeError('Slot must be a function'); function wrapper(this: SignalThis, ...args: any[]) { this.disconnect(wrapper); Reflect.apply(slot, this, args); } (>wrapper).wrapped = slot; this.connect(wrapper); }; Signal.prototype.slotCount = function any>(this: SignalThis) { const slots = this._slots; if (slots === undefined) return 0; if (typeof slots === 'function') return 1; return slots.length; }; function clone(arr: any[], n: number) { const ret = new Array(n); for (let i = 0; i < n; ++i) ret[i] = arr[i]; return ret; }