const events = require('events'); class EventManager { private emitter_:any; private appStatePrevious_:any; private appStateWatchedProps_:string[]; private appStateListeners_:any; constructor() { this.reset(); } reset() { this.emitter_ = new events.EventEmitter(); this.appStatePrevious_ = {}; this.appStateWatchedProps_ = []; this.appStateListeners_ = {}; } on(eventName:string, callback:Function) { return this.emitter_.on(eventName, callback); } emit(eventName:string, object:any = null) { return this.emitter_.emit(eventName, object); } removeListener(eventName:string, callback:Function) { return this.emitter_.removeListener(eventName, callback); } off(eventName:string, callback:Function) { return this.removeListener(eventName, callback); } filterOn(filterName:string, callback:Function) { return this.emitter_.on(`filter:${filterName}`, callback); } filterOff(filterName:string, callback:Function) { return this.removeListener(`filter:${filterName}`, callback); } filterEmit(filterName:string, object:any) { // We freeze the object we pass to the listeners so that they // don't modify it directly. Instead they must return a // modified copy (or the input itself). let output = Object.freeze(object); const listeners = this.emitter_.listeners(`filter:${filterName}`); for (const listener of listeners) { const newOutput = listener(output); if (newOutput === undefined) { throw new Error(`Filter "${filterName}": Filter must return a value or the unmodified input. Returning nothing or "undefined" is not supported.`); } if (newOutput !== output) { output = Object.freeze(newOutput); } } return output; } appStateOn(propName:string, callback:Function) { if (!this.appStateListeners_[propName]) { this.appStateListeners_[propName] = []; this.appStateWatchedProps_.push(propName); } this.appStateListeners_[propName].push(callback); } appStateOff(propName:string, callback:Function) { if (!this.appStateListeners_[propName]) { throw new Error('EventManager: Trying to unregister a state prop watch for a non-watched prop (1)'); } const idx = this.appStateListeners_[propName].indexOf(callback); if (idx < 0) throw new Error('EventManager: Trying to unregister a state prop watch for a non-watched prop (2)'); this.appStateListeners_[propName].splice(idx, 1); } stateValue_(state:any, propName:string) { const parts = propName.split('.'); let s = state; for (const p of parts) { if (!(p in s)) throw new Error(`Invalid state property path: ${propName}`); s = s[p]; } return s; } // This function works by keeping a copy of the watched props and, whenever this function // is called, comparing the previous and new values and emitting events if they have changed. // The appStateEmit function should be called from a middleware. appStateEmit(state:any) { if (!this.appStateWatchedProps_.length) return; for (const propName of this.appStateWatchedProps_) { let emit = false; const stateValue = this.stateValue_(state, propName); if (!(propName in this.appStatePrevious_) || this.appStatePrevious_[propName] !== stateValue) { this.appStatePrevious_[propName] = stateValue; emit = true; } if (emit) { const listeners = this.appStateListeners_[propName]; if (!listeners || !listeners.length) continue; const eventValue = Object.freeze(stateValue); for (const listener of listeners) { listener({ value: eventValue }); } } } } } const eventManager = new EventManager(); export default eventManager;