UNPKG

@simple-ui/cable

Version:

Cable is a messaging utility with tree and graph message broadcasting combining the centricity of mediators with the semantic protections of signal-slot.

471 lines (387 loc) 11.1 kB
import _ from 'lodash'; /** * Cable is a messaging utility with tree and graph message broadcasting combining the centricity of mediators with the semantic protections of signal-slot. * * @module simpleui-cable * @license * simpleui-cable 1.1.1 <https://simpleui.io/> * Copyright 2017 Simple UI <https://simpleui.io/> * Available under MIT license <https://simpleui.io/license> */ export default class Cable { /** * @param {String} channelName the name of the cable object to be created * @param {Object} options define if the cable should execute async or sync */ constructor(channelName, options) { channelName = (_.isString(channelName)) ? channelName : (_.isString(options)) ? options : 'root'; options = _.defaults(options, { asynchronous: true }); options.invocation = ((options.asynchronous) ? 'defer' : 'attempt'); this._ = { id: _.uniqueId(), channelName, options, root: undefined, parentsChannels: [], parentsPath: '', channels: [], bridges: [], slots: [] }; } /** * default method called for every slot * @note do not call this, it is used internally */ static receiptFn() { // override method } /** * @returns {String} unique name of cable */ get id() { return (_.isUndefined(this._.id)) ? '' : this._.id; } /** * @returns {String} contextual name of cable */ get channelName() { return (_.isUndefined(this._.channelName)) ? '' : this._.channelName; } /** * @returns {String} unique string of parent ids concatenated with '-' */ get parentsPath() { return (_.isUndefined(this._.parentsPath)) ? '' : this._.parentsPath; } /** * @returns {String} the full cable id path which is completely unique among all cable objects created */ get uniquePath() { return (this.parentsPath.length > 0) ? `${this.parentsPath}-${this.channelName}-${this.id}` : `${this.channelName}-${this.id}`; } /** * @returns {Cable} the parent cable object */ set parent(cableParent) { this.root = cableParent; } /** * @returns {Cable} the parent cable object */ get parent() { return this.root; } /** * @param {Cable} cableParent the parent (or root) object of the cable */ set root(cableParent) { const parentsChannels = []; this._.root = cableParent; while (cableParent) { parentsChannels.push(cableParent.channelName); cableParent = cableParent.root; } this._.parentsChannels = (parentsChannels.length <= 0) ? parentsChannels : parentsChannels.reverse(); this._.parentsPath = this._.parentsChannels.join(`-`); } /** * @returns {Cable} the parent cable object */ get root() { return this._.root; } /** * add new cable(s) as children to broadcast to * @param {String} channelName a new cable object or a string of cable ids to create * @returns {Cable} parent cable */ channel(channelName) { let cable = this; if (channelName instanceof Cable) { this._channel(channelName); } else if (_.isString(channelName)) { _.each(this._deconstructChannelPath(channelName), (name) => { const child = (_.isUndefined(cable[name])) ? new Cable(name, cable._.options) : cable[name]; cable._channel(child); cable = child; }); } else { throw new Error('cannot add channel without a non-string or Cable object'); } return this; } /** * @param {Cable} cable set as child of parent cable object * @returns {Cable} parent cable * @private */ _channel(cable) { cable.root = this; if (_.isUndefined(this[cable.channelName])) { this._.channels.push(cable.channelName); } this[cable.channelName] = cable; return this; } /** * connect two cable objects laterally, to form graph connections for communication * @param {(Cable|String)} bridgeFromCable a string pointing to a child cable or a cable * @param {(Cable|String)} [bridgeToCable=this] */ bridge(bridgeFromCable, bridgeToCable = this) { // if bridgeFromCable is a string bridgeFromCable = (_.isString(bridgeFromCable)) ? this.lookup(bridgeFromCable) : bridgeFromCable; // if bridgeToCable is a string bridgeToCable = (_.isString(bridgeToCable)) ? this.lookup(bridgeToCable) : bridgeToCable; bridgeToCable._.bridges.push(bridgeFromCable); return this; } /** * @param {String} channelString a string similar to #channel to search for existing children cables * @returns {Cable} the child at the end of the search path */ lookup(channelString) { return _.reduce(this._deconstructChannelPath(channelString), (cable, channelName) => { return cable[channelName]; }, this); } /** * send a message to all cables connected to this one (through child, parent, or bridge connections) * @param {*} any number of parameters to send * @returns {Cable} */ flood(...params) { return this._message('_flood', params); } /** * send a message to all parent cables * @param {*} any number of parameters to send * @returns {Cable} */ emit(...params) { return this._message('_emit', params); } /** * send a message to all slots on current cable * @param {*} any number of parameters to send * @returns {Cable} */ publish(...params) { return this._message('_publish', params); } /** * send a message to all child cables * @param {*} any number of parameters to send * @returns {Cable} */ broadcast(...params) { return this._message('_broadcast', params); } /** * setup the function receipt and invoke messaging async or async * @note do not call directly * @param {String} direction function name to invoke * @param {Array} params the parameters to send to all slots * @returns {Cable} * @private */ _message(direction, params) { const receiptFn = (_.isFunction(this._.receiptFn)) ? this._.receiptFn : Cable.receiptFn; this._.receiptFn = undefined; _[this._.options.invocation](() => { this[direction]({}, receiptFn, params); }); return this; } /** * implementation of flooding * @private */ _flood(callGraph, receiptFn, params) { if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; this._invokeSlots(receiptFn, params); if (this._.root) { this._.root._flood(callGraph, receiptFn, params); } _.each(this._.channels, (channelName) => { this[channelName]._flood(callGraph, receiptFn, params); }); _.each(this._.bridges, (bridge) => { bridge._flood(callGraph, receiptFn, params); }); return this; } /** * implementation of emitting * @private */ _emit(callGraph, receiptFn, params) { let cable = this._.root; if (!cable) { return this; } if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; cable._invokeSlots(receiptFn, params); cable._emit(callGraph, receiptFn, params); _.each(this._.bridges, (bridge) => { bridge._emit(callGraph, receiptFn, params); }); return this; } /** * implementation of publishing * @private */ _publish(callGraph, receiptFn, params) { if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; this._invokeSlots(receiptFn, params); _.each(this._.bridges, (bridge) => { bridge._publish(callGraph, receiptFn, params); }); return this; } /** * implementation of broadcasting * @private */ _broadcast(callGraph, receiptFn, params) { if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; _.each(this._.channels, (channelName) => { this[channelName]._invokeSlots(receiptFn, params); this[channelName]._broadcast(callGraph, receiptFn, params); }); _.each(this._.bridges, (bridge) => { bridge._broadcast(callGraph, receiptFn, params); }); return this; } /** * invoke all slots on a cable (the heart of messaging) * @param {Function} receiptFn the callback method to be invoked for every message * @param {Array} params parameters to deliver to slots * @private */ _invokeSlots(receiptFn, params) { _.each(this._.slots, (subscriber) => { receiptFn(subscriber.method.apply(subscriber.host, params)); }); _.each(Cable.taps, (tapFn) => { tapFn(this, params); }); } /** * register a receipt function, which allows a slot to respond back to the cable * @note current only one per call * @param {Function} fn the function to call on each invocation of a slot * @returns {Cable} */ receipt(fn) { // TODO allow multiple and then expire all after the call this._.receiptFn = fn; return this; } /** * listen to all messages sent, one cable * @param {Function} method */ static tap(method) { Cable.taps = Cable.taps || []; Cable.taps.push(method); } /** * add a slot (listener) to a cable * @param {(Object|Function)} host either a method to invoke or an object to invoke a method on * @param {(String|Function)} [method=undefined] if an object is provided for host, then provide a string (name of function on host) or a function to be invoked on the host object * @returns {*} */ subscribe(host, method) { const cable = this; const isSlot = !_.isUndefined(method); method = (isSlot) ? method : host; host = (isSlot) ? host : cable; if (isSlot && _.isString(method)) { method = host[method]; } this._.slots.push({ host, method }); return this; } /** * split a channel name into name parts using one of three characters /[./:]/ * @param channelName a set of channel names to create cable objects as children * @returns {Array} an array of channel names ('A.B' => ['A', 'B']) * @private */ _deconstructChannelPath(channelName) { const splitCharacter = this._splitChannelNameByGroupingCharacter(channelName); return (splitCharacter === '') ? [channelName] : channelName.split(splitCharacter); } /** * split a channel name into name parts using one of three characters /[./:]/ * @param {String} channelName a set of channel names * @returns {Array} an array of channel names ('A.B' => ['A', 'B']) * @private */ _splitChannelNameByGroupingCharacter(channelName) { return _.reduce(['.', '/', ':'], function getGroupingCharacter(splitCharacter, character) { return splitCharacter + ((channelName.indexOf(character) >= 0) ? character : ''); }, ''); } /* * @return {String} unique name for cable */ toString() { return this.uniquePath; } }