UNPKG

@vistadataproject/vista-client

Version:

Updated VISTA RPC Client NodeJS module with Promise-based API

286 lines (255 loc) 10.6 kB
'use strict'; // # ClientSocket // A NodeJS Net.Socket client wrapper implementation with a Promise-based API // // There is a huge impedance mismatch between the event-based stream mechanisms used by the // Net.Socket objects and any sort of Promise-based interfaces. This class is meant to bridge // the gap between the two technologies using managed queueing techniques. const net = require('net'); const util = require('util'); const debug = require('debug')('ClientSocket'); class ClientSocket { // ## API Methods // // ### ClientSocket.create // Static class factory method used to create an new instance of the `ClientSocket` class // ##### Parameters // - **host** [`String`]: Host name or IP address of the entity to connect to // - **port** [`String`|`Number`]: TCP port to connect to // // ##### Returns // `ClientSocket` instance static create(host, port) { debug('Creating new instance via "create"'); return new ClientSocket(new net.Socket(), { host, port }); } // ### ClientSocket.attach // Static class factory method used to attach and wrap a `Net.Socket` instance with a `ClientSocket` instance. // Use this if a `Net.Socket` is provided to you, like via a server socket `connect` event. Note that this // assumes that the socket instance is already connected. Otherwise, use the `ClientSocket.create` method. // ##### Parameters // - **socket** [`Net.Socket`]: Previously created `Net.Socket` instance // // ##### Returns // `ClientSocket` instance static attach(socket) { debug('Creating new instance via "attach"'); return new ClientSocket(socket, { host: socket.remoteAddress, port: socket.remotePort, isConnected: true, }); } // ### connect // Connect to the configured host and port. // // ##### Returns // `Promise` which is fulfilled when the socket connection is established. async connect() { if (this.isConnected) { return; } await this.connectAsync(this.port, this.host); } // ### read // Read a specified number of bytes from the socket. // ##### Parameters // - **readLength** [`Number`] (default: 0): The number of bytes to read from the socket. If the specified // value is equal to or less than zero, the `Promise` returned by the `read` call will be resolved with // either whatever data is stored in the interal buffer, at least 1 byte in length. // // ##### Returns // `Promise` which will be resolved with the read data, or rejected on read error. read(readLength = 0) { return new Promise((resolve, reject) => { if (!this.isConnected) { reject(new Error('read attempted on disconnected socket')); return; } this.requests.push({ process: () => { const bufferLength = this.buffer.length; debug('Read length = %d, buffer length = %d', readLength, bufferLength); if (readLength <= 0 && bufferLength === 0) { return false; } if (readLength > bufferLength) { return false; } const sliceLength = (readLength > 0) ? readLength : bufferLength; const results = this.buffer.slice(0, sliceLength); this.buffer = this.buffer.slice(sliceLength); debug('Results: %s, Buffer: %s', results.toString(), this.buffer.toString()); resolve(results.toString()); return true; }, fail: reject, }); this.processRequestQueue(); }); } // ### readUntil // Read data from the socket until a specified pattern has been found. // ##### Parameters // - **pattern** [`String`] (default: "\0"): The byte stream delimiter pattern to look for in the // data stream. The resolved `Promise` will include the pattern delimiter as well. // // ##### Returns // `Promise` which will be resolved with the read data, or rejected on read error. readUntil(pattern = '\0') { return new Promise((resolve, reject) => { if (!this.isConnected) { reject(new Error('readUntil attempted on disconnected socket')); return; } this.requests.push({ process: () => { const index = this.buffer.indexOf(pattern); if (index < 0) { return false; } debug('Read until "%s" pattern index %d', pattern, index); const sliceLength = index + pattern.length; const results = this.buffer.slice(0, sliceLength); this.buffer = this.buffer.slice(sliceLength); debug('Results: %s, Buffer: %s', results.toString(), this.buffer.toString()); resolve(results.toString()); return true; }, fail: reject, }); this.processRequestQueue(); }); } // ### flush // Flush out the receive buffer. This method is similar to the `read()` method. The difference here is that // if the receive buffer is empty, the method will resolve immediately and not wait for data. // ##### Returns // `Promise` which will be resolved with the read data, or an empty string if there is no data in the receive // queye, or rejected on read error. flush() { return new Promise((resolve, reject) => { if (!this.isConnected) { reject(new Error('read attempted on disconnected socket')); return; } this.requests.push({ process: () => { const bufferLength = this.buffer.length; if (bufferLength === 0) { resolve(''); return true; } const results = this.buffer.slice(0, bufferLength); this.buffer = this.buffer.slice(bufferLength); debug('Results: %s, Buffer: %s', results.toString(), this.buffer.toString()); resolve(results.toString()); return true; }, fail: reject, }); this.processRequestQueue(); }); } // ### write // Write the specified data to the socket. // ##### Parameters // - **data** [`String`|`Buffer`]: The data to write to the socket // // ##### Returns // `Promise` which will be resolved when the data has been written, or rejected on write error. async write(data) { if (!this.isConnected) { throw new Error('write attempted on disconnected socket'); } await this.writeAsync(data); } // ### close // Close the socket implementation. After this is called, the `ClientSocket` can no longer be used. // // ##### Returns // `Promise` which will be resolved when the socket has been closed. async close() { if (!this.isConnected) { return; } this.onClose(); } // ## Internal Methods // These are not meant to be called by the user // #### Constructor // You can technically use the `ClientSocket` constructor, but the preferred way to instantiate the // class instances is via the factory methods: [ClientSocket.create](#section-3) and [ClientSocket.attach](#section-4) constructor(netSocket, { host, port, isConnected = false, }) { Object.assign(this, { isConnected, host, port, socket: netSocket, buffer: Buffer.from(''), requests: [], }); Object.assign(this, { onClose: this.onClose.bind(this), onData: this.onData.bind(this), connectAsync: util.promisify(this.socket.connect).bind(this.socket), writeAsync: util.promisify(this.socket.write).bind(this.socket), }); this.socket .on('data', this.onData) .on('error', this.onClose) .on('close', this.onClose) .on('end', this.onClose) .once('connect', () => { debug(`Connected (${this.host}:${this.port})`); this.isConnected = true; }); } // #### onClose // This is the `close` event handler, which performs housekeeping on the wrapped `Net.Socket` object. onClose(err) { this.socket.destroy(); this.isConnected = false; this.socket.removeAllListeners(); this.drainRequests('Socket has been disconnected'); } // #### onData // This is the `data` event handler, which queues the received data then performs read request resolution. onData(data) { this.buffer = Buffer.concat([this.buffer, data]); this.processRequestQueue(); } // #### processRequestQueue // This method is called whenever a `read` request is made or data has been received via a `data` event. // It attempts to match the queued `read` requests with the data contained within the managed data buffer. processRequestQueue() { let isStillProcessing = this.isConnected; let [currentRequest] = this.requests; if (!isStillProcessing) { this.drainRequests('Socket has been disconnected'); } while (isStillProcessing && currentRequest) { isStillProcessing = currentRequest.process(); debug('Pending requests: %d, isStillProcessing: %s', this.requests.length, isStillProcessing); if (isStillProcessing) { this.requests.shift(); [currentRequest] = this.requests; } } } // #### drainRequests // Clear out the request queue by sequentially failing (rejecting) each one. This will typically be // called via `processRequestQueue` if the socket has been disconnected and there are requests still // lingering in the requests queue. drainRequests(message) { this.requests.forEach(request => request.fail(new Error(message))); this.requests.splice(0); } } module.exports = { ClientSocket, };