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.

565 lines (437 loc) 15 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * 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> */ var Cable = function () { /** * @param {String} channelName the name of the cable object to be created * @param {Object} options define if the cable should execute async or sync */ function Cable(channelName, options) { _classCallCheck(this, Cable); channelName = _lodash2.default.isString(channelName) ? channelName : _lodash2.default.isString(options) ? options : 'root'; options = _lodash2.default.defaults(options, { asynchronous: true }); options.invocation = options.asynchronous ? 'defer' : 'attempt'; this._ = { id: _lodash2.default.uniqueId(), channelName: channelName, options: options, root: undefined, parentsChannels: [], parentsPath: '', channels: [], bridges: [], slots: [] }; } /** * default method called for every slot * @note do not call this, it is used internally */ _createClass(Cable, [{ key: 'channel', /** * 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 */ value: function channel(channelName) { var cable = this; if (channelName instanceof Cable) { this._channel(channelName); } else if (_lodash2.default.isString(channelName)) { _lodash2.default.each(this._deconstructChannelPath(channelName), function (name) { var child = _lodash2.default.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 */ }, { key: '_channel', value: function _channel(cable) { cable.root = this; if (_lodash2.default.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] */ }, { key: 'bridge', value: function bridge(bridgeFromCable) { var bridgeToCable = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this; // if bridgeFromCable is a string bridgeFromCable = _lodash2.default.isString(bridgeFromCable) ? this.lookup(bridgeFromCable) : bridgeFromCable; // if bridgeToCable is a string bridgeToCable = _lodash2.default.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 */ }, { key: 'lookup', value: function lookup(channelString) { return _lodash2.default.reduce(this._deconstructChannelPath(channelString), function (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} */ }, { key: 'flood', value: function flood() { for (var _len = arguments.length, params = Array(_len), _key = 0; _key < _len; _key++) { params[_key] = arguments[_key]; } return this._message('_flood', params); } /** * send a message to all parent cables * @param {*} any number of parameters to send * @returns {Cable} */ }, { key: 'emit', value: function emit() { for (var _len2 = arguments.length, params = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { params[_key2] = arguments[_key2]; } return this._message('_emit', params); } /** * send a message to all slots on current cable * @param {*} any number of parameters to send * @returns {Cable} */ }, { key: 'publish', value: function publish() { for (var _len3 = arguments.length, params = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { params[_key3] = arguments[_key3]; } return this._message('_publish', params); } /** * send a message to all child cables * @param {*} any number of parameters to send * @returns {Cable} */ }, { key: 'broadcast', value: function broadcast() { for (var _len4 = arguments.length, params = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { params[_key4] = arguments[_key4]; } 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 */ }, { key: '_message', value: function _message(direction, params) { var _this = this; var receiptFn = _lodash2.default.isFunction(this._.receiptFn) ? this._.receiptFn : Cable.receiptFn; this._.receiptFn = undefined; _lodash2.default[this._.options.invocation](function () { _this[direction]({}, receiptFn, params); }); return this; } /** * implementation of flooding * @private */ }, { key: '_flood', value: function _flood(callGraph, receiptFn, params) { var _this2 = this; if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; this._invokeSlots(receiptFn, params); if (this._.root) { this._.root._flood(callGraph, receiptFn, params); } _lodash2.default.each(this._.channels, function (channelName) { _this2[channelName]._flood(callGraph, receiptFn, params); }); _lodash2.default.each(this._.bridges, function (bridge) { bridge._flood(callGraph, receiptFn, params); }); return this; } /** * implementation of emitting * @private */ }, { key: '_emit', value: function _emit(callGraph, receiptFn, params) { var 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); _lodash2.default.each(this._.bridges, function (bridge) { bridge._emit(callGraph, receiptFn, params); }); return this; } /** * implementation of publishing * @private */ }, { key: '_publish', value: function _publish(callGraph, receiptFn, params) { if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; this._invokeSlots(receiptFn, params); _lodash2.default.each(this._.bridges, function (bridge) { bridge._publish(callGraph, receiptFn, params); }); return this; } /** * implementation of broadcasting * @private */ }, { key: '_broadcast', value: function _broadcast(callGraph, receiptFn, params) { var _this3 = this; if (callGraph[this.uniquePath]) { return; } callGraph[this.uniquePath] = true; _lodash2.default.each(this._.channels, function (channelName) { _this3[channelName]._invokeSlots(receiptFn, params); _this3[channelName]._broadcast(callGraph, receiptFn, params); }); _lodash2.default.each(this._.bridges, function (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 */ }, { key: '_invokeSlots', value: function _invokeSlots(receiptFn, params) { var _this4 = this; _lodash2.default.each(this._.slots, function (subscriber) { receiptFn(subscriber.method.apply(subscriber.host, params)); }); _lodash2.default.each(Cable.taps, function (tapFn) { tapFn(_this4, 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} */ }, { key: 'receipt', value: function 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 */ }, { key: 'subscribe', /** * 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 {*} */ value: function subscribe(host, method) { var cable = this; var isSlot = !_lodash2.default.isUndefined(method); method = isSlot ? method : host; host = isSlot ? host : cable; if (isSlot && _lodash2.default.isString(method)) { method = host[method]; } this._.slots.push({ host: host, method: 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 */ }, { key: '_deconstructChannelPath', value: function _deconstructChannelPath(channelName) { var 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 */ }, { key: '_splitChannelNameByGroupingCharacter', value: function _splitChannelNameByGroupingCharacter(channelName) { return _lodash2.default.reduce(['.', '/', ':'], function getGroupingCharacter(splitCharacter, character) { return splitCharacter + (channelName.indexOf(character) >= 0 ? character : ''); }, ''); } /* * @return {String} unique name for cable */ }, { key: 'toString', value: function toString() { return this.uniquePath; } }, { key: 'id', /** * @returns {String} unique name of cable */ get: function get() { return _lodash2.default.isUndefined(this._.id) ? '' : this._.id; } /** * @returns {String} contextual name of cable */ }, { key: 'channelName', get: function get() { return _lodash2.default.isUndefined(this._.channelName) ? '' : this._.channelName; } /** * @returns {String} unique string of parent ids concatenated with '-' */ }, { key: 'parentsPath', get: function get() { return _lodash2.default.isUndefined(this._.parentsPath) ? '' : this._.parentsPath; } /** * @returns {String} the full cable id path which is completely unique among all cable objects created */ }, { key: 'uniquePath', get: function get() { return this.parentsPath.length > 0 ? this.parentsPath + '-' + this.channelName + '-' + this.id : this.channelName + '-' + this.id; } /** * @returns {Cable} the parent cable object */ }, { key: 'parent', set: function set(cableParent) { this.root = cableParent; } /** * @returns {Cable} the parent cable object */ , get: function get() { return this.root; } /** * @param {Cable} cableParent the parent (or root) object of the cable */ }, { key: 'root', set: function set(cableParent) { var 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: function get() { return this._.root; } }], [{ key: 'receiptFn', value: function receiptFn() {} // override method }, { key: 'tap', value: function tap(method) { Cable.taps = Cable.taps || []; Cable.taps.push(method); } }]); return Cable; }(); exports.default = Cable;