@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
JavaScript
'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;