UNPKG

dpaw-brocket-relay

Version:

Interapp communication via websockets

522 lines (447 loc) 14.1 kB
/** * @fileOverview * @name relay.js * @author Gavin Coombes * @license MIT * @version 0.2.0 * @module relay * * A brocket relay using the EventEmitter interface * */ 'use strict'; 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;