UNPKG

node-jet

Version:

Jet Realtime Message Bus for the Web. Daemon and Peer implementation.

266 lines (265 loc) 8.25 kB
import { ConnectionClosed, methodNotFoundError, ParseError } from '../3_jet/errors.js'; import { castMessage } from '../3_jet/messages.js'; import { Socket } from '../1_socket/socket.js'; import { EventEmitter } from '../1_socket/index.js'; /** * Helper shorthands. */ const encode = JSON.stringify; const decode = JSON.parse; const isResultMessage = (msg) => 'result' in msg; const isErrorMessage = (msg) => 'error' in msg; /** * JsonRPC Instance * class used to interpret jsonrpc messages. This class can parse incoming socket messages to jsonrpc messages and fires events */ export class JsonRPC extends EventEmitter { sock; config; messages = []; messageId = 1; user = ''; _isOpen = false; openRequests = {}; requestId = ''; resolveDisconnect; rejectDisconnect; disconnectPromise; resolveConnect; rejectConnect; connectPromise; logger; abortController; constructor(logger, config, sock) { super(); this.config = config || {}; this.createDisconnectPromise(); this.createConnectPromise(); this.logger = logger; if (sock) { this.sock = sock; this._isOpen = true; this.subscribeToSocketEvents(); } } /** * Method called before disconnecting from the device to initialize Promise, that is only resolved when disconnected */ createDisconnectPromise = () => { this.disconnectPromise = new Promise((resolve, reject) => { this.resolveDisconnect = resolve; this.rejectDisconnect = reject; }); }; /** * Method called before connecting to the device to initialize Promise, that is only resolved when a connection is established */ createConnectPromise = () => { this.connectPromise = new Promise((resolve, reject) => { this.resolveConnect = resolve; this.rejectConnect = reject; }); }; /** * Method called to subscribe to all relevant socket events */ subscribeToSocketEvents = () => { this.sock.addEventListener('error', this._handleError); this.sock.addEventListener('message', this._handleMessage); this.sock.addEventListener('open', () => { this._isOpen = true; this.createDisconnectPromise(); if (this.abortController.signal.aborted) { this.logger.warn('user requested abort'); this.close(); this.rejectConnect(); } else { this.resolveConnect(); } }); this.sock.addEventListener('close', () => { this._isOpen = false; this.resolveDisconnect(); this.createConnectPromise(); }); }; /** * Method to connect to a Server instance. Either TCP Server or Webserver * @params controller: an AbortController that can be used to abort the connection */ connect = async (controller = new AbortController()) => { if (this._isOpen) { await Promise.resolve(); return; } this.abortController = controller; const { config } = this; this.sock = new Socket(); this.sock.connect(config.url, config.ip, config.port || 11122); this.subscribeToSocketEvents(); await this.connectPromise; }; /** * Close. */ close = async () => { if (!this._isOpen) { await Promise.resolve(); return; } this.send(); this.sock.close(); await this.disconnectPromise; }; _handleError = (err) => { this.logger.error(`Error in socket connection: ${err}`); if (!this._isOpen) { this.rejectConnect(err); } }; _convertMessage = async (message) => { if (message instanceof Blob) { return await message .arrayBuffer() .then((buf) => new TextDecoder().decode(buf)); } return await Promise.resolve(message); }; /** * _dispatchMessage * * @api private */ _handleMessage = (event) => { this._convertMessage(event.data).then((message) => { this.logger.sock(`Received message: ${message}`); let decoded; try { decoded = decode(message); if (Array.isArray(decoded)) { for (let i = 0; i < decoded.length; i++) { this._dispatchSingleMessage(decoded[i]); } } else { this._dispatchSingleMessage(decoded); } this.send(); } catch (err) { const decodedId = decoded?.id || ''; this.respond(decodedId, new ParseError(message), false); this.logger.error(err); } }); }; /** * _dispatchSingleMessage * * @api private */ _dispatchSingleMessage = (message) => { if (isResultMessage(message) || isErrorMessage(message)) { this._dispatchResponse(message); } else this._dispatchRequest(castMessage(message)); }; /** * _dispatchResponse * * @api private */ _dispatchResponse = (message) => { const mid = message.id; if (isResultMessage(message)) this.successCb(mid, message.result); if (isErrorMessage(message)) this.errorCb(mid, message.error); }; /** * _dispatchRequest. * Handles both method calls and fetchers (notifications) * * @api private */ _dispatchRequest = (message) => { if (this.listenerCount(message.method) === 0) { this.logger.error(`Method ${message.method} is unknown`); this.respond(message.id, new methodNotFoundError(message.method), false); } else this.emit(message.method, this, message.id, message.params); }; /** * Queue. */ queue = async (message, id = '') => { if (!this._isOpen) return await Promise.reject(new ConnectionClosed()); if (id) this.messages.push({ method: id, params: message }); else this.messages.push(message); if (!this.config.batches) this.send(); }; /** * Send. */ send = () => { if (this.messages.length > 0) { const encoded = encode(this.messages.length === 1 ? this.messages[0] : this.messages); this.logger.sock(`Sending message: ${encoded}`); this.sock.send(encoded); this.messages = []; } }; /** * Responding a request * @param id the request id to respond to * @param params the result of the request * @param success if the request was fulfilled */ respond = (id, params, success) => { this.queue({ id, [success ? 'result' : 'error']: params }); }; successCb = (id, result) => { if (id in this.openRequests) { this.openRequests[id].resolve(result); delete this.openRequests[id]; } }; errorCb = (id, error) => { if (id in this.openRequests) { this.openRequests[id].reject(error); delete this.openRequests[id]; } }; /** * Method to send a request to a JSONRPC Server. */ sendRequest = async (method, params, // Jet Peer uses send immediate to call all functions without delay sendImmediate = false) => await new Promise((resolve, reject) => { if (!this._isOpen) reject(new ConnectionClosed()); else { const rpcId = this.messageId.toString(); this.messageId++; this.openRequests[rpcId] = { resolve: resolve, reject }; this.queue({ id: rpcId.toString(), method, params }); if (sendImmediate) this.send(); } }); } export default JsonRPC;