UNPKG

ws-wrapper

Version:

Lightweight WebSocket wrapper lib with socket.io-like event handling, requests, and channels

285 lines (259 loc) 8 kB
// TODO: Use native "events" module if in Node.js environment? import { EventEmitter } from "eventemitter3" /* 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() // Channel middleware added using `use()` method this._middleware = [] } // 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) => 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) { const oldTimeout = this._wrapper._requestTimeout if (this._tempTimeout !== undefined) { this._wrapper._requestTimeout = this._tempTimeout delete this._tempTimeout } const ret = this._wrapper._sendEvent(this._name, eventName, arguments, true) this._wrapper._requestTimeout = oldTimeout return ret } // Add middleware for this channel use(fn) { if (typeof fn !== "function") { throw new Error("Middleware must be a function") } this._middleware.push(fn) } // Receives an inbound message directed to this channel. Returns true if // and only if an event handler processed the inbound message. Events first // pass through all middleware and then to the event handler for `eventName` _runMiddleware(event) { const channel = this ;(function run(middleware) { let nextCalled = false const next = function (err) { // Ensure `next` is called exactly once if (nextCalled) { return } nextCalled = true if (err) { // Send request rejection if needed if (event.requestId >= 0) { channel._wrapper._sendReject(event.requestId, err) } channel._wrapper._debug(`channel: Event '${event.name}' dropped`) } else { // Run next middleware function run(middleware.slice(1)) } } const [fn] = middleware if (fn) { try { fn(event.name, event.args, next) } catch (err) { next(err) } } else { // No middleware remaining, so pass along to event handler if (channel._emitter.emit(event.name, event)) { channel._wrapper._debug( `channel: Event '${event.name}' sent to event listener` ) } else { next( new Error( `No event listener for '${event.name}'` + (channel._name ? " on channel '" + channel._name + "'" : "") ) ) } } })(this._middleware) } _wrapListener(listener) { if (typeof listener !== "function") { throw new TypeError('"listener" argument must be a function') } let 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` */ let returnVal try { 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", "connect", "message", "error", "close", "disconnect", ] // Expose the class export default WebSocketChannel