paul-revere
Version:
Lightweight WebSocket messaging between browsers and Node
339 lines (269 loc) • 9.13 kB
JavaScript
;
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; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var urlUtil = require('url'),
_require = require('schemapack'),
build = _require.build,
toBuffer = require('blob-to-buffer'),
uuid = require('uuid'),
WebSocket = require('./websocket'),
isNode = require('detect-node');
// Symbols to keep the class properties somewhat private
var schemaMap = Symbol('schemaMap'),
ws = Symbol('websocket'),
wsc = Symbol('wsClient'),
id = Symbol('id'),
ci = Symbol('ci'),
ps = Symbol('pubSub'),
bs = Symbol('builtSchema'),
si = Symbol('schemaIndex'),
rb = Symbol('receiveBuffer'),
om = Symbol('onMessage');
/**
* Universal message parser
* @param {(Blob|Buffer|ArrayBuffer)} b - Schemapack message
* @return {Promise} Resolves to Buffer and matching Schema
*/
function parseMessage(b, sMap) {
return new Promise(function (res, rej) {
// Handle blobs
if (typeof Blob !== 'undefined' && b instanceof Blob) {
toBuffer(b, function (err, buff) {
if (err) rej(err);
var schema = sMap.get(buff[0]);
res({ schema: schema, buff: buff });
});
}
// Handle Buffer/ArrayBuffer
else {
var buff = b;
// Convert ArrayBuffer so we can parse the first byte for the right schema
if (typeof ArrayBuffer !== 'undefined' && b instanceof ArrayBuffer) buff = Buffer.from(b);
try {
var schema = sMap.get(buff[0]);
res({ schema: schema, buff: buff });
} catch (e) {
rej(e);
}
}
});
}
/**
* Stub pub/sub
*/
var stubPubSub = {
publish: function publish(subject, msg, exclude) {
if (!this.listeners['paulrevere.' + subject]) return;
this.listeners['paulrevere.' + subject].forEach(function (cb) {
return cb(msg, exclude);
});
},
subscribe: function subscribe(subject, cb) {
if (!this.listeners['paulrevere.' + subject]) this.listeners['paulrevere.' + subject] = [];
this.listeners['paulrevere.' + subject].push(cb);
},
listeners: {}
};
/**
* Schema class handles its own tranmission and reception of data
*/
var Schema = function () {
function Schema(builtSchema, schemaIndex, websocket, clientId, pubSub) {
var _this = this;
_classCallCheck(this, Schema);
this[si] = schemaIndex;
this[bs] = builtSchema;
this[ws] = websocket;
this[ci] = clientId;
this[ps] = pubSub;
this[rb] = function (buff) {
var msg = _this[bs].decode(buff);
_this[om](msg);
};
// Default to noop
this[om] = function () {};
// Subscribe to broadcasts
if (isNode && this[ps]) {
this[ps].subscribe(String(this[si]), function (msg, exclude) {
_this[ws].clients.forEach(function (c) {
// Exclude a specific client
// Check for argument existence to avoid accidental `undefined === undefined === true`
if (exclude && exclude === c.__uuid) return;
// Set the schema index
msg.__schema = _this[si];
// Set the client id
msg.__uuid = _this[ci];
c.send(_this[bs].encode(msg));
});
});
}
}
_createClass(Schema, [{
key: 'onMessage',
value: function onMessage(cb) {
this[om] = cb;
}
}, {
key: 'send',
value: function send() {
var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
// Set the schema index
msg.__schema = this[si];
// Set the client id
msg.__uuid = this[ci];
this[ws].send(this[bs].encode(msg));
}
/**
* Broadcast a message to all clients, and optionally exclude one
* @param {Object} msg - Message to broadcast. Must follow the rules set in the schema
* @param {Client} [exclude] - Client to exclude from the broadcast
*/
}, {
key: 'broadcast',
value: function broadcast() {
var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var exclude = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!isNode || !this[ps]) throw Error('Cannot broadcast from single client');
this[ps].publish(String(this[si]), msg, exclude.__uuid);
}
}]);
return Schema;
}();
var Client = function () {
function Client(uwsClient, builtSchemas) {
var _this2 = this;
_classCallCheck(this, Client);
this.__uuid = uwsClient.__uuid;
this.upgradeReq = uwsClient.upgradeReq;
this[wsc] = uwsClient;
this[schemaMap] = new Map();
var i = 0;
builtSchemas.forEach(function (builtSchema, key) {
var schema = new Schema(builtSchema, i, _this2[wsc], _this2.__uuid);
_this2[schemaMap].set(i, schema);
// Set a public property for the consumer to use
_this2[key] = schema;
i++;
});
this[wsc].on('message', function (m) {
parseMessage(m, _this2[schemaMap]).then(function (_ref) {
var schema = _ref.schema,
buff = _ref.buff;
return schema[rb](buff);
}).catch(function (e) {
return console.error(e);
});
});
}
_createClass(Client, [{
key: 'close',
value: function close() {
this[wsc].close();
}
}, {
key: 'onClose',
value: function onClose(cb) {
this[wsc].on('close', cb);
}
}]);
return Client;
}();
var PaulRevere = function () {
function PaulRevere() {
var _this3 = this;
var schemas = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { message: { payload: 'string', meta: { timestamp: 'varuint' } } };
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
url = _ref2.url,
_ref2$queryParams = _ref2.queryParams,
queryParams = _ref2$queryParams === undefined ? {} : _ref2$queryParams,
server = _ref2.server,
_ref2$pubSub = _ref2.pubSub,
pubSub = _ref2$pubSub === undefined ? stubPubSub : _ref2$pubSub;
_classCallCheck(this, PaulRevere);
if (url && typeof url !== 'string') throw new TypeError('Remote url must be a string');
if (url && server) throw new Error('Remote and Server cannot both be defined');
this[schemaMap] = new Map();
this[id] = uuid.v4();
// Switch between client and server websocket
var query = void 0;
switch (true) {
case !!url:
query = Object.assign({}, queryParams, { clientId: this[id] });
this[ws] = new WebSocket(url + urlUtil.format({ query: query }));
break;
case !!server:
if (!isNode) throw new Error('Cannot create WebSocket server in browser environment');
this[ws] = new WebSocket.Server({ server: server });
break;
default:
throw new Error('Remote or Server must be defined');
}
// Local reference of the schemapack schemas for Clients to use
var builtSchemas = new Map();
// Map all of the Schemas to an index
Object.keys(schemas).forEach(function (key, i) {
var builtSchema = build(Object.assign({
// Adding this parameter to a schema allows us to parse it later as a buffer
// so the receipient does not need to know what type of information was sent
__schema: 'uint8',
__uuid: 'string'
}, schemas[key]));
var schema = new Schema(builtSchema, i, _this3[ws], _this3[id], pubSub);
_this3[schemaMap].set(i, schema);
builtSchemas.set(key, builtSchema);
// Set a public property for the consumer to use
_this3[key] = schema;
});
// Set up listeners
if (isNode) {
this[ws].on('message', function (m) {
parseMessage(m, _this3[schemaMap]).then(function (_ref3) {
var schema = _ref3.schema,
buff = _ref3.buff;
return schema[rb](buff);
}).catch(function (e) {
return console.error(e);
});
});
} else {
this[ws].onmessage = function (m) {
parseMessage(m.data, _this3[schemaMap]).then(function (_ref4) {
var schema = _ref4.schema,
buff = _ref4.buff;
return schema[rb](buff);
}).catch(function (e) {
return console.error(e);
});
};
this.onClose = function (cb) {
_this3[ws].onclose = cb;
};
}
// For servers, bind a connection listener and return a Client
if (typeof remote !== 'string') {
this.onConnection = function (cb) {
_this3[ws].on('connection', function (c, req) {
// Add the connection request to the client
c.upgradeReq = req;
c.__uuid = urlUtil.parse(c.upgradeReq.url, true).query.clientId || uuid.v4();
var client = new Client(c, builtSchemas);
cb(client);
});
return _this3;
};
}
}
_createClass(PaulRevere, [{
key: 'close',
value: function close() {
this[ws].close();
}
}, {
key: 'rawClients',
get: function get() {
return this[ws].clients;
}
}]);
return PaulRevere;
}();
module.exports = PaulRevere;