T4rk1n/tarkjs

View on GitHub
src/event-bus/event-bus.js

Summary

Maintainability
B
4 hrs
Test Coverage
/**
* Created by T4rk on 6/30/2017.
*/
 
import { promiseWrap } from '../extensions/prom-extensions'
import {objCopy, objExtend } from '../extensions/obj-extensions'
import {arrIncludes, arrMerge} from '../extensions/arr-extensions'
import { TObject } from '../containers/tobject'
 
/**
* Simple object with event type and optional payload.
* @typedef {Object} TEvent
* @property {string} event
* @property {*} [payload]
*/
 
/**
* Custom event object dispatched by {@link EventBus#dispatch}.
* @typedef {TEvent} TEventHandlerParams
* @property {function} cancel abort the next handlers from dispatching.
* @property {Array} acc Accumulator of the return values of the previous handlers.
* @property {number} i iterator of the handler.
* @property {number} end length of the handlers.
*/
 
/**
* {@link EventBus#addEventHandler}
*
* A handler can return a value to be accumulated in dispatch.
* It may also return a Promise to be resolved.
* @typedef {function} TEventHandler
* @param {!TEventHandlerParams} params
*/
 
/**
* EventBus is a custom event dispatcher that wraps handlers in promises.
*
* @example
* const bus = new EventBus()
* const handle = ({payload}) => {
* console.log(`Hello ${payload}`)
* }
* bus.addEventHandler('hello', handle)
* bus.dispatch({event: 'hello', payload: 'bob'}) // prints Hello bob
*/
export class EventBus {
constructor() {
this._handlers = new TObject()
}
 
/**
* Add an handler function to an event handler list.
* @param {string|RegExp} event If regex it will match events.
* @param {TEventHandler} handler
* @param {boolean} [once=false] For once to work properly, wait for a dispatch to finish before sending a new one.
*/
addEventHandler(event, handler, once=false) {
const isRegex = event instanceof RegExp
const h = this._handlers.opt(event, { isRegex, handlers: [], original: event })
if (!arrIncludes(h.handlers, handler)) {
if (once) {
const _wrap = (e) => {
const ret = handler(e)
this.removeEventHandler(event, _wrap)
return ret
}
h.handlers.push(_wrap)
} else {
h.handlers.push(handler)
}
this._handlers[event] = h
}
}
 
//noinspection JSCommentMatchesSignature,JSValidateJSDoc
/**
* Dispatch a {@link TEventHandlerParams} to all the concerned handlers
* @param {TEvent} param
* @return {CancelablePromise} canceling a dispatch will prevent next handler from executing.
*/
Function `dispatch` has a Cognitive Complexity of 13 (exceeds 5 allowed). Consider refactoring.
Function `dispatch` has 38 lines of code (exceeds 25 allowed). Consider refactoring.
dispatch({event, payload}) {
let canceled = false, curProm
const cancel = () => {
canceled = true
}
Function `promise` has 28 lines of code (exceeds 25 allowed). Consider refactoring.
const promise = new Promise((resolve, reject) => {
const handlers = this.findHandlers(event)
const end = handlers.length
if (!handlers || handlers.length < 1) return reject({
error: 'missing_handler',
message:`No handler to dispatch ${event}`})
let i = 0
const acc = []
const handle = (value) => {
if (canceled)
return reject({
error: 'canceled',
message: `Dispatch ${event} was canceled after ${i} handlers.`})
 
if (value) {
acc.push(value)
}
 
if (i < end) {
curProm = promiseWrap(() => {
const r = handlers[i]({event, payload, cancel, acc, i, end})
i++
return r
})
curProm.promise.then(handle, reject)
}
else {
resolve({event, acc, dispatched: i})
}
}
handle()
})
return {
promise,
cancel
}
}
 
/**
* Remove the handler.
* @param {string} event
* @param {function} handler
*/
removeEventHandler(event, handler) {
const h = this._handlers[event]
if (!h) return
this._handlers[event].handlers = h.handlers.filter(h => h !== handler)
}
 
/**
* Merge the handlers for a event.
* @param {string} event Event to handlers for.
*/
findHandlers(event) {
return arrMerge([], ...this._handlers.items()
.filter(([k,{ isRegex, original }]) => isRegex ? original.test(event): k === event)
// eslint-disable-next-line
.map(([_, {handlers}]) => handlers))
}
}
 
/**
* @typedef {TEvent} ValueChangedEvent
* @property {{oldValue: *, newValue: *}} payload
*/
 
/**
* @param {string} key
* @param {?string} prefix
* @return {string}
*/
export const valueChanged = (key, prefix=null) => `${prefix ? `${prefix}_`: ''}${key}_value_changed`
 
 
// eslint-disable-next-line no-console
const valueChangedOptions = { onDispatchError: (e) => console.log(e) }
 
/**
* Wraps an object properties to dispatch a value_changed event on the setter.
* The dispatched event key is `${key}_value_changed`.
*
* Watch out to not set the value again in the event callbacks otherwise there could be
* circular madness.
* @param {Object} obj The obj to modify. Will use _data property to hold the values.
* @param {EventBus} eventBus The event bus to dispatch events.
* @param {Object} [options={}]
* @return {Object}
* @throws {TypeError} if fail to wraps a property, should not happen if using plain objects.
* @example
* const e = changeNotifier({hello: 'hello'})
* e.hello = 'hi'
* // dispatched event {event: 'hello_value_changed', payload: {newValue: 'hi', oldValue: 'hello'}}
*/
export const changeNotifier = (obj, eventBus, options=valueChangedOptions) => {
const { prefix, onDispatchError } = objExtend({}, valueChangedOptions, options)
const notifier = {}
notifier._data = objCopy(obj)
Object.keys(obj).filter(f => obj.hasOwnProperty(f)).reduce((a, k) => {
Object.defineProperty(notifier, k, {
set: (value) => {
const oldValue = notifier._data[k]
notifier._data[k] = value
eventBus.dispatch({
event: valueChanged(k, prefix),
payload: {newValue: value, oldValue}
}).promise.catch(onDispatchError)
},
get: () => notifier._data[k]
})
}, notifier)
return notifier
}