dpaw-brocket-relay
Version:
Interapp communication via websockets
522 lines (447 loc) • 14.1 kB
JavaScript
/**
* @fileOverview
* @name relay.js
* @author Gavin Coombes
* @license MIT
* @version 0.2.0
* @module relay
*
* A brocket relay using the EventEmitter interface
*
*/
;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
var events = require('events');
var WebSocket = require('reconnectingwebsocket');
var cuid = require('cuid');
var utility = require('./utility');
// Aliases
var EventEmitter = events.EventEmitter;
var u = utility;
var log = utility.log;
var MODE = {
RECV: 0,
DUPLEX: 1,
SEND: 2
};
/**
* Separator for network level tags
* @const {string}
*/
var TAG_SEP = '|';
/**
* Default websocket scheme
* @const {string}
*/
var WEBSOCKET_PROTOCOLS = 'wss';
/**
* Default websocket options - mirror ReconnectingSocket defaults
* @const {object}
*/
var WEBSOCKET_OPTIONS = {
/* Unless noted, these are the ReconnectingSocket defaults */
debug: false, // default: false
automaticOpen: false, // default: true
reconnectInterval: 1000,
maxReconnectInterval: 30 * 1000,
reconnectDecay: 1.5,
timeoutInterval: 2000,
maxReconnectAttempts: null,
binaryType: 'blob'
};
/**
* Default relay options
* @const {object}
*/
var DEFAULT_OPTIONS = {
baseAddr: undefined,
channel: '',
appID: undefined,
userID: undefined, // Must be provided to init
connID: undefined, // Generate on init
pulse: 30 * 1000, /* ms */
recvMode: 1,
sendMode: 1,
conchMode: 1,
ignoreEcho: false,
wrapping: true, // Wrap app messages with network tags
webSocketOptions: WEBSOCKET_OPTIONS
};
/**
* Prototype object of the Relay class
* @implements EventEmitter
* @class
*/
var RelayModule = (function () {
var obj = Object.create(new events.EventEmitter());
obj.$type = "RelayModule";
return obj;
})();
/**
* The central Relay class
* @class
*
*/
var RelayObject = Object.create(RelayModule);
/**
* Initialise a relay object with options
* @function
*
* @param {object} [opts] - Sensible defaults if omitted.
*/
RelayObject.init = function init(opts) {
var self = this;
self.opts = u.defaults({}, opts, DEFAULT_OPTIONS);
// get/set properties
self.recvMode = u.prop(self.opts.recvMode);
self.sendMode = u.prop(self.opts.sendMode);
self.conchMode = u.prop(self.opts.conchMode);
self._channel = u.prop(self.opts.channel);
var _ws_addresses = ws_addresses(self.opts.baseAddr, self._channel());
var _ws_addresses2 = _slicedToArray(_ws_addresses, 2);
self.pubAddr = _ws_addresses2[0];
self.subAddr = _ws_addresses2[1];
self.message$ = null;
self.opts.connID = u._s6();
self.tag = [self.opts.appID, self.opts.userID, self.opts.connID].join(TAG_SEP);
self.subscriptions = Object.create({}); // Empty and no prototype
self.ackRegistry = new Map();
if (self.opts.automaticOpen) self.connect();
};
/** Relay a message from the Brocket network to subscribed listeners
* The message will go to every global listener,
* but only by tag to tagged listeners
*
* @param {object} - With tag, payload properties
*/
RelayObject.broadcast = function broadcast(msg) {
var self = this;
var subs = self.subscriptions;
var tag = msg.tag;
var payload = msg.payload;
sendBroadcast(subs.global, msg);
sendBroadcast(subs[tag], payload);
/**
* @event RelayObject#broadcast
* @type {object}
* @property (object} msg - The broadcast message)
*/
self.emit('broadcast', msg);
};
/**
* Ensures that the websocket reconnect with new address on channel change
*
* @property {string} - The name of the brocket channel.
* @param {String} value
* @returns {String | undefined}
*/
RelayObject.channel = function (value) {
var self = this;
if (arguments.length) {
self._channel(value);
var _ws_addresses3 = ws_addresses(self.opts.baseAddr, self._channel());
var _ws_addresses4 = _slicedToArray(_ws_addresses3, 2);
self.pubAddr = _ws_addresses4[0];
self.subAddr = _ws_addresses4[1];
self.reconnect();
}
return self._channel();
};
/**
* Open the publish and subscribe websockets
* @function
*/
RelayObject.connect = function connect() {
var self = this;
self._connect(WebSocket, self.opts.webSocketOptions);
};
RelayObject._connect = function _connect(WebSocket, opts) {
var self = this;
/* WebSocket(url, protocals, options) */
var pws = new WebSocket(self.pubAddr, null, opts);
pws.addEventListener('open', function (v) {
return self.emit('pub/open');
});
// pws.addEventListener('open', v => log('Opening ' + self.pubAddr));
pws.addEventListener('close', function () {
return self.emit('pub/close');
});
// pws.addEventListener('close', v => log('Closing ' + self.pubAddr));
pws.addEventListener('error', function (evt) {
return self.emit('pub/error', evt);
});
pws.addEventListener('error', function (evt) {
return self.emit('error', evt);
});
pws.open();
self.pubWS = pws;
self.subWS = new WebSocket(self.subAddr, null, opts);
self.subWS.addEventListener('open', function (evt) {
return self.emit('sub/open');
});
self.subWS.addEventListener('open', function (v) {
return log('Opening ' + self.subAddr);
});
self.subWS.addEventListener('close', function (evt) {
return self.emit('sub/close', evt);
});
self.subWS.addEventListener('error', function (evt) {
return self.emit('sub/error', evt);
});
self.subWS.addEventListener('error', function (evt) {
return self.emit('error', evt);
});
self.subWS.addEventListener('message', function (evt) {
return self.receive(evt);
});
// self.addEventListener('sub/close', v => log('Closing ' + self.subAddr));
self.subWS.open();
self.emit('beat', 'Connecting');
};
/**
* Send a message into the Brocket network
*
* @function
* @param {object} msg - Must have tag and payload properties
*/
RelayObject.dispatch = function dispatch(msg) {
// log('Relay.dispatch: before wrap, msg is ', msg);
var self = this;
var outmsg = undefined;
if (self.sendMode < 1) return;
if (self.opts.wrapping) {
var wrapped_msg = self.wrap(msg);
outmsg = self.serialise(wrapped_msg);
} else {
outmsg = self.serialise(msg);
}
if (self.pubWS.readyState === 1) {
self.emit('dispatch', msg);
self.pubWS.send(outmsg);
} else {
console.warn('Websocket is not open! Message dropped is ', msg);
}
};
RelayObject.boundDispatch = function () {
return dispatch.bind(null, this);
};
RelayObject.dispatchStream = function dispatchStream(outgoing$) {
// Use relay.dispatchStream(outgoing$)
var dispatch = self.dispatch.bind(self);
outgoing$.subscribe(dispatch);
};
RelayObject.inject_kefir = function inject_kefir(Kefir) {
// To avoid the rather heavy dependency of Rx, use inject a local dependency.
var self = this;
var message$ = Kefir.fromEvents(self, 'receive');
self.message$ = message$;
};
RelayObject.inject_rx = function inject_rx(Rx) {
// To avoid the rather heavy dependency of Rx, use inject a local dependency.
var self = this;
var message$ = Rx.Observable.fromEvent(self, 'receive');
// See TODO comment below
var messageSubject = new Rx.BehaviorSubject();
message$.subscribe(messageSubject);
self.message$ = messageSubject;
// TODO: Swap and test share method for subject
// self.message$ = message$.share();
};
/**
* Relay each network message
*
* @function
*
*/
RelayObject.receive = function receive(evt) {
var self = this;
// Ignore if in broadcast mode
if (self.recvMode < 1) return;
var msg = self.deserialise(evt.data);
acknowledge(self.ackRegistry, msg);
var _maybe_unwrap = maybe_unwrap(self.opts.wrapping, msg);
var tag = _maybe_unwrap.tag;
var payload = _maybe_unwrap.payload;
var rest = _objectWithoutProperties(_maybe_unwrap, ['tag', 'payload']);
var _tag$split = tag.split(TAG_SEP);
var _tag$split2 = _slicedToArray(_tag$split, 3);
var appID = _tag$split2[0];
var userID = _tag$split2[1];
var connID = _tag$split2[2];
/* Bail out on echos */
if (self.opts.ignoreEcho && self.opts.connID === connID) {
log('Echo ignored. connID: ' + self.opts.connID);
return;
}
var inmsg = _extends({ tag: tag, payload: payload }, rest);
self.broadcast(inmsg);
self.emit('receive', inmsg);
},
/**
* Close and reopen the sockets, no timeout
*/
RelayObject.reconnect = function reconnect() {
var self = this;
if (self.pubWS) self.pubWS.close();
if (self.subWS) self.subWS.close();
self.connect();
},
// I really want to bind disaptch to itself
/**
*
* Register a callback for every message.
* var f = msg => log('Received msg ', msg);
* relay.subscribe(f)
*
* Register a callback for messages with an appTag of 'bounds'
* var g = msg => log('Received bounds message');
* relay.subscribe('bounds', g)
*
* @function
* @param {...args}
* args can be single callback for all messages
* or tag, cb to set a listener for a specific tag.
*/
RelayObject.subscribe = function subscribe() {
var self = this;
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var tag = args[0];
var cb = args[1];
if (!cb) {
;
var _ref = ['global', tag];
tag = _ref[0];
cb = _ref[1];
}self.subscriptions = addSubscription(self.subscriptions, tag, cb);
},
/**
* Destroy all the thingz, with a timeout for slow sockets
* @function
*/
RelayObject.destroy = function destroy(timeout) {
var self = this;
timeout = timeout || 100;
closeSocket(self.pubWS, timeout);
closeSocket(self.subWS, timeout);
};
RelayObject.wrap = function (_msg) {
var self = this;
return wrap(self.tag, _msg);
};
RelayObject.unwrap = unwrap;
RelayObject.serialise = serialise;
RelayObject.deserialise = deserialise;
///////// Module functions ////////////////////
function acknowledge(reg, msg) {
var deleteKey = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
if (msg.ack && reg.has(msg.ack)) {
sendBroadcast(reg.get(msg.ack), msg);
if (deleteKey) reg.delete(msg.ack);
}
}
function addSubscription(subs, key, val) {
if (!u.has(subs, key)) subs[key] = [];
subs[key] = [].concat(_toConsumableArray(subs[key]), [val]);
return subs;
}
function registerSyn(reg, msg) {
var callback = arguments.length <= 2 || arguments[2] === undefined ? logAck : arguments[2];
if (msg.syn) {
var key = msg.cuid;
var xs = reg.get(key) || [];
var new_callbacks = [].concat(_toConsumableArray(xs), [callback]);
reg.set(key, new_callbacks);
}
return reg;
}
function closeSocket(s, interval) {
if (s && s.readyState === 0) {
// Connecting
window.setTimeout(closeSocket.bind(null, s, interval), interval);
} else if (s && s.readyState === 1) {
// Open
s.close();
} else {
log('Socket does not exist or is closed.'); // Socket does not exist or is closing/closed
}
}
function dispatch(self, msg, doSyn) {
// log('Relay.dispatch: before wrap, msg is ', msg);
if (self.sendMode < 1) return;
var w_msg = maybe_wrap(self.opts.wrapping, self.tag, msg);
var s_msg = maybe_syn(doSyn, msg);
registerSyn(self.ackRegistry, s_msg);
var outmsg = serialise(s_msg);
self._send(outmsg);
self.emit('dispatch', s_msg);
};
function logAck(msg) {
console.log('Ack: ', msg);
}
function maybe_syn(doSyn, msg) {
return doSyn ? _extends({ syn: true }, msg) : msg;
}
function sendBroadcast(listeners, msg) {
if (listeners && listeners.length) {
listeners.forEach(function (f) {
return f(msg);
});
}
}
function serialise(msg) {
return JSON.stringify(msg);
};
function deserialise(msg) {
/// Check if the msg is already an object
// The issue here seems to be some 'helpful' libraries will automatically deserialise json
var out = undefined;
if (u.isString(msg)) {
out = JSON.parse(msg);
} else if (u.isObject(msg)) {
out = msg;
} else {
throw new Error("I don't even know what he is!? " + msg);
}
return out;
};
function ws_addresses(base, chan) {
var pub = base + '/pubsub/' + chan;
var sub = base + '/pubsub/' + chan;
/** Network payload -> application message */
return [pub, sub];
}
function maybe_wrap(doWrap, tag, msg) {
return doWrap ? wrap(tag, msg) : msg;
}
function wrap(tag, _msg) {
/** Application message -> Network message */
var msg = {
tag: tag,
payload: _msg,
time: u.now(),
cuid: cuid()
};
return msg;
};
function maybe_unwrap(doUnwrap, msg) {
if (doUnwrap) msg = unwrap(msg);
return msg;
}
function unwrap(msg) {
// Unwraps a payload into tag and payload
var payload = msg.payload;
var rest = _objectWithoutProperties(msg, ['payload']);
return payload;
};
RelayObject.module = {
acknowledge: acknowledge,
registerSyn: registerSyn,
maybe_syn: maybe_syn,
wrap: wrap
};
module.exports = RelayObject;