node-jet
Version:
Jet Realtime Message Bus for the Web. Daemon and Peer implementation.
266 lines (265 loc) • 8.25 kB
JavaScript
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;