UNPKG

ws-wrapper

Version:

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

321 lines (301 loc) 8.47 kB
import WebSocketChannel from "./channel.mjs" class WebSocketWrapper extends WebSocketChannel { constructor(socket, options) { // Make `this` a WebSocketChannel super() this._wrapper = this options = options || {} if (typeof options.debug === "function") { this._debug = options.debug } else if (options.debug === true) { this._debug = console.log.bind(console) } else { this._debug = () => {} // no-op } if (typeof options.errorToJSON !== "function") { // Provide default error serialization this._errorToJSON = (err) => { if (typeof window === "undefined") { // Node.js environment only serializes `message` return JSON.stringify({ message: err.message }) } else { // Browser serializes all error properties return JSON.stringify(err, Object.getOwnPropertyNames(err)) } } } else { this._errorToJSON = options.errorToJSON } if (options.requestTimeout > 0) { this._requestTimeout = options.requestTimeout | 0 } // Flag set once the socket is opened this._opened = false // Array of data to be sent once the connection is opened this._pendingSend = [] // Incrementing request ID counter for this WebSocket this._lastRequestId = 0 /* Object of pending requests; keys are the request ID, values are Objects containing `resolve` and `reject` functions used to resolve the request's Promise. */ this._pendingRequests = {} /* Object of WebSocketChannels (except `this` associated with this WebSocket); keys are the channel name. */ this._channels = {} // Object containing user-assigned socket data this._data = {} // Bind this wrapper to the `socket` passed to the constructor this._socket = null if (socket && socket.constructor) { this.bind(socket) } } bind(socket) { // Clean up any event handlers on `this._socket` if (this._socket) { const s = this._socket s.onopen = s.onmessage = s.onerror = s.onclose = null } // Save the `socket` and add event listeners this._socket = socket socket.onopen = (event) => { this._opened = true this._debug("socket: onopen") // Send all pending messages let i for (i = 0; i < this._pendingSend.length; i++) { if (this.isConnected) { this._debug("wrapper: Sending pending message:", this._pendingSend[i]) try { this._socket.send(this._pendingSend[i]) } catch (e) { this._pendingSend = this._pendingSend.slice(i - 1) throw e } } else { break } } this._pendingSend = this._pendingSend.slice(i) this.emit("open", event) this.emit("connect", event) } socket.onmessage = (event) => { this._debug("socket: onmessage", event.data) this.emit("message", event, event.data) this._onMessage(event.data) } socket.onerror = (event) => { this._debug("socket: onerror", event) this.emit("error", event) } socket.onclose = (event) => { const opened = this._opened this._opened = false this._debug("socket: onclose", event) this.emit("close", event, opened) this.emit("disconnect", event, opened) } // If the socket is already open, send all pending messages now if (this.isConnected) { socket.onopen() } return this } get socket() { return this._socket } set socket(socket) { this.bind(socket) } // Rejects all pending requests and then clears the send queue abort() { for (const id in this._pendingRequests) { this._pendingRequests[id].reject(new Error("Request was aborted")) } this._pendingRequests = {} this._pendingSend = [] return this } // Returns a channel with the specified `namespace` of(namespace) { if (namespace == null) { return this } if (!this._channels[namespace]) { this._channels[namespace] = new WebSocketChannel(namespace, this) } return this._channels[namespace] } get isConnecting() { return ( this._socket && this._socket.readyState === this._socket.constructor.CONNECTING ) } get isConnected() { return ( this._socket && this._socket.readyState === this._socket.constructor.OPEN ) } send(data, ignoreMaxQueueSize) { if (this.isConnected) { this._debug("wrapper: Sending message:", data) this._socket.send(data) } else if ( ignoreMaxQueueSize || this._pendingSend.length < WebSocketWrapper.MAX_SEND_QUEUE_SIZE ) { this._debug("wrapper: Queuing message:", data) this._pendingSend.push(data) } else { throw new Error("WebSocket is not connected and send queue is full") } return this } disconnect() { if (this._socket) { this._socket.close.apply(this._socket, arguments) } return this } // Called whenever the bound Socket receives a message _onMessage(msg) { try { msg = JSON.parse(msg) // If `msg` contains special ignore property, we'll ignore it if (msg["ws-wrapper"] === false) { return } if (msg.a) { const argsArray = [] for (const i in msg.a) { argsArray[i] = msg.a[i] } msg.a = argsArray } /* If `msg` does not have an `a` Array with at least 1 element, ignore the message because it is not a valid event/request */ if ( msg.a instanceof Array && msg.a.length >= 1 && (msg.c || WebSocketChannel.NO_WRAP_EVENTS.indexOf(msg.a[0]) < 0) ) { // Process inbound event/request const event = { name: msg.a.shift(), args: msg.a, requestId: msg.i, } const channel = msg.c == null ? this : this._channels[msg.c] if (!channel) { if (msg.i >= 0) { this._sendReject( msg.i, new Error(`Channel '${msg.c}' does not exist`) ) } this._debug( `wrapper: Event '${event.name}' ignored ` + `because channel '${msg.c}' does not exist.` ) } else { channel._runMiddleware(event) } } else if (this._pendingRequests[msg.i]) { this._debug("wrapper: Processing response for request", msg.i) // Process response to prior request if (msg.e !== undefined) { let err = msg.e // `msg._` indicates that `msg.e` is an Error if (msg._ && err) { err = new Error(err.message) // Copy other properties to Error for (const key in msg.e) { err[key] = msg.e[key] } } else if (typeof err === "string") { err = new Error(err) } this._pendingRequests[msg.i].reject(err) } else { this._pendingRequests[msg.i].resolve(msg.d) } clearTimeout(this._pendingRequests[msg.i].timer) delete this._pendingRequests[msg.i] } // else ignore the message because it's not valid } catch (e) { // Non-JSON messages are ignored /* Note: It's also possible for uncaught exceptions from event handlers to end up here. */ } } /* The following methods are called by a WebSocketChannel to send data to the Socket. */ _sendEvent(channel, eventName, args, isRequest) { // Serialize data for sending over the socket const data = { a: Array.prototype.slice.call(args) } if (channel != null) { data.c = channel } let request if (isRequest) { /* Unless we send petabytes of data using the same socket, we won't worry about `_lastRequestId` getting too big. */ data.i = ++this._lastRequestId // Return a Promise to the caller to be resolved later request = new Promise((resolve, reject) => { const pendReq = (this._pendingRequests[data.i] = { resolve, reject, }) if (this._requestTimeout > 0) { pendReq.timer = setTimeout(() => { reject(new Error("Request timed out")) delete this._pendingRequests[data.i] }, this._requestTimeout) } }) } // Send the message this.send(JSON.stringify(data)) // Return the request, if needed return request } _sendResolve(id, data) { this.send( JSON.stringify({ i: id, d: data, }), true /* ignore max queue length */ ) } _sendReject(id, err) { const isError = err instanceof Error if (isError) { err = JSON.parse(this._errorToJSON(err)) } this.send( JSON.stringify({ i: id, e: err, _: isError ? 1 : undefined, }), true /* ignore max queue length */ ) } get(key) { return this._data[key] } set(key, value) { this._data[key] = value return this } } /* Maximum number of items in the send queue. If a user tries to send more messages than this number while a WebSocket is not connected, errors will be thrown. */ WebSocketWrapper.MAX_SEND_QUEUE_SIZE = 10 export default WebSocketWrapper