ws-wrapper
Version:
Lightweight WebSocket wrapper lib with socket.io-like event handling, requests, and channels
201 lines (179 loc) • 6.38 kB
JavaScript
"use strict";
// TODO: Use native "events" module if in Node.js environment?
const EventEmitter = require("eventemitter3").EventEmitter;
/* A WebSocketChannel exposes an EventEmitter-like API for sending and handling
events or requests over the channel through the attached WebSocketWrapper.
`var channel = new WebSocketChannel(name, socketWrapper);`
- `name` - the namespace for the channel
- `socketWrapper` - the WebSocketWrapper instance to which data should
be sent
*/
class WebSocketChannel {
constructor(name, socketWrapper) {
// Channel name; `null` only for the WebSocketWrapper instance
this._name = name;
// Reference to WebSocketWrapper instance
this._wrapper = socketWrapper;
// This channel's EventEmitter
this._emitter = new EventEmitter();
// WeakMap of wrapped event listeners
this._wrappedListeners = new WeakMap();
}
// Retrieve channel name
get name() {
return this._name;
}
// Changing the channel name after it's been created is a bad idea.
set name(name) {
throw new Error("Setting the channel name is not allowed");
}
/* Expose EventEmitter-like API
When `eventName` is one of the `NO_WRAP_EVENTS`, the event handlers
are left untouched, and the emitted events are just sent to the
EventEmitter; otherwise, event listeners are wrapped to process the
incoming request and the emitted events are sent to the WebSocketWrapper
to be serialized and sent over the WebSocket. */
on(eventName, listener) {
if(this._name == null && WebSocketChannel.NO_WRAP_EVENTS.indexOf(eventName) >= 0)
/* Note: The following is equivalent to:
`this._emitter.on(eventName, listener.bind(this));`
But thanks to eventemitter3, the following is a touch faster. */
this._emitter.on(eventName, listener, this);
else
this._emitter.on(eventName, this._wrapListener(listener) );
return this;
}
once(eventName, listener) {
if(this._name == null && WebSocketChannel.NO_WRAP_EVENTS.indexOf(eventName) >= 0)
this._emitter.once(eventName, listener, this);
else
this._emitter.once(eventName, this._wrapListener(listener) );
return this;
}
removeListener(eventName, listener) {
if(this._name == null && WebSocketChannel.NO_WRAP_EVENTS.indexOf(eventName) >= 0)
this._emitter.removeListener(eventName, listener);
else
this._emitter.removeListener(eventName,
this._wrappedListeners.get(listener) );
return this;
}
removeAllListeners(eventName) {
this._emitter.removeAllListeners(eventName);
return this;
}
eventNames() {
return this._emitter.eventNames();
}
listeners(eventName) {
if(this._name == null && WebSocketChannel.NO_WRAP_EVENTS.indexOf(eventName) >= 0)
return this._emitter.listeners(eventName);
else {
return this._emitter.listeners(eventName).map((wrapper) => {
return wrapper._original;
});
}
}
/* The following `emit` and `request` methods will serialize and send the
event over the WebSocket using the WebSocketWrapper. */
emit(eventName) {
if(this._name == null && WebSocketChannel.NO_WRAP_EVENTS.indexOf(eventName) >= 0)
return this._emitter.emit.apply(this._emitter, arguments);
else
return this._wrapper._sendEvent(this._name, eventName, arguments);
}
/* Temporarily set the request timeout for the next request. */
timeout(tempTimeout) {
this._tempTimeout = tempTimeout;
return this;
}
request(eventName) {
var oldTimeout = this._wrapper._requestTimeout;
if(this._tempTimeout !== undefined) {
this._wrapper._requestTimeout = this._tempTimeout;
delete this._tempTimeout;
}
var ret = this._wrapper._sendEvent(this._name, eventName, arguments, true);
this._wrapper._requestTimeout = oldTimeout;
return ret;
}
_wrapListener(listener) {
if(typeof listener !== "function") {
throw new TypeError("\"listener\" argument must be a function");
}
var wrapped = this._wrappedListeners.get(listener);
if(!wrapped) {
wrapped = function channelListenerWrapper(event)
{
/* This function is called when an event is emitted on this
WebSocketChannel's `_emitter` when the WebSocketWrapper
receives an incoming message for this channel. If this
event is a request, special processing is needed to
send the response back over the socket. Below we use
the return value from the original `listener` to
determine what response should be sent back.
`this` refers to the WebSocketChannel instance
`event` has the following properties:
- `name`
- `args`
- `requestId`
*/
try {
var returnVal = listener.apply(this, event.args);
} catch(err) {
if(event.requestId >= 0) {
/* If event listener throws, pass that Error back
as a response to the request */
this._wrapper._sendReject(
event.requestId, err);
}
// Re-throw
throw err;
}
if(returnVal instanceof Promise) {
/* If event listener returns a Promise, respond once
the Promise resolves */
returnVal
.then((data) => {
if(event.requestId >= 0) {
this._wrapper._sendResolve(
event.requestId, data);
}
})
.catch((err) => {
if(event.requestId >= 0) {
this._wrapper._sendReject(
event.requestId, err);
}
// else silently ignore error
});
} else if(event.requestId >= 0) {
/* Otherwise, assume that the `returnVal` is what
should be passed back as the response */
this._wrapper._sendResolve(
event.requestId, returnVal);
}
// else return value is ignored for simple events
}.bind(this); // Bind the channel to the `channelListenerWrapper`
// Add a reference back to the original listener
wrapped._original = listener;
this._wrappedListeners.set(listener, wrapped);
}
// Finally, return the wrapped listener
return wrapped;
}
get(key) {
return this._wrapper.get(key);
}
set(key, value) {
this._wrapper.set(key, value);
return this;
}
}
// Add aliases to existing methods
WebSocketChannel.prototype.addListener = WebSocketChannel.prototype.on;
WebSocketChannel.prototype.off = WebSocketChannel.prototype.removeListener;
// List of "special" reserved events whose listeners don't need to be wrapped
WebSocketChannel.NO_WRAP_EVENTS = ["open", "message", "error", "close", "disconnect"];
// Expose the class
module.exports = WebSocketChannel;