chia-agent
Version:
chia rpc/websocket client library
336 lines (335 loc) • 12 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDaemon = getDaemon;
const crypto_1 = require("crypto");
const logger_1 = require("../logger");
const connection_1 = require("./connection");
const index_1 = require("../config/index");
const DEFAULT_SERVICE_NAME = "wallet_ui";
let daemon = null;
function getDaemon(serviceName) {
if (daemon) {
return daemon;
}
return (daemon = new Daemon(serviceName));
}
// Gracefully disconnect from remote daemon server on Ctrl+C.
const onProcessExit = () => {
if (!daemon) {
process.removeListener("SIGINT", onProcessExit);
process.kill(process.pid, "SIGINT");
return;
}
setTimeout(async () => {
try {
if (daemon && daemon.connected && !daemon.closing) {
(0, logger_1.getLogger)().debug("Detected Ctrl+C. Initiating shutdown...");
await daemon.close();
process.removeListener("SIGINT", onProcessExit);
process.kill(process.pid, "SIGINT");
}
}
catch (e) {
process.stderr.write(JSON.stringify(e));
process.exit(1);
}
}, 67);
};
process.addListener("SIGINT", onProcessExit);
class Daemon {
get connected() {
return Boolean(this._connectedUrl);
}
get closing() {
return this._closing;
}
constructor(serviceName) {
this._socket = null;
this._connectedUrl = "";
this._responseQueue = {};
this._openEventListeners = [];
this._messageEventListeners = [];
this._errorEventListeners = [];
this._closeEventListeners = [];
this._messageListeners = {};
this._closing = false;
this._subscriptions = [];
this._serviceName = DEFAULT_SERVICE_NAME;
this.onOpen = this.onOpen.bind(this);
this.onError = this.onError.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onClose = this.onClose.bind(this);
this.onPing = this.onPing.bind(this);
this.onPong = this.onPong.bind(this);
this.onRejection = this.onRejection.bind(this);
if (serviceName) {
this._serviceName = serviceName;
}
}
onRejection(e) {
if (typeof e === "string") {
(0, logger_1.getLogger)().error(`Error: ${e}`);
}
else if (e instanceof Error) {
(0, logger_1.getLogger)().error(`Error ${e.name}: ${e.message}`);
if (e.stack) {
(0, logger_1.getLogger)().error(e.stack);
}
}
else {
try {
(0, logger_1.getLogger)().error(`Error: ${JSON.stringify(e)}`);
}
catch (_e) {
(0, logger_1.getLogger)().error("Unknown error");
}
}
return null;
}
/**
* Connect to local daemon via websocket.
* @param daemonServerURL
* @param timeoutMs
*/
async connect(daemonServerURL, timeoutMs) {
if (!daemonServerURL) {
const config = (0, index_1.getConfig)();
const daemonHost = config["/ui/daemon_host"];
const daemonPort = config["/ui/daemon_port"];
daemonServerURL = `wss://${daemonHost}:${daemonPort}`;
}
if (this._connectedUrl === daemonServerURL) {
return true;
}
else if (this._connectedUrl) {
(0, logger_1.getLogger)().error("Connection is still active. Please close living connection first");
return false;
}
(0, logger_1.getLogger)().debug(`Opening websocket connection to ${daemonServerURL}`);
const result = await (0, connection_1.open)(daemonServerURL, timeoutMs).catch(this.onRejection);
if (!result) {
return false;
}
this._socket = result.ws;
this._socket.on("error", this.onError);
this._socket.addEventListener("message", this.onMessage);
this._socket.on("close", this.onClose);
this._socket.on("ping", this.onPing);
this._socket.on("pong", this.onPong);
await this.onOpen(result.openEvent, daemonServerURL).catch(this.onRejection);
return true;
}
async close() {
return new Promise((resolve) => {
if (this._closing || !this._socket) {
return;
}
(0, logger_1.getLogger)().debug("Closing web socket connection");
this._socket.close();
this._closing = true;
this._connectedUrl = "";
this._onClosePromise = resolve; // Resolved in onClose function.
});
}
async sendMessage(destination, command, data) {
return new Promise((resolve, reject) => {
if (!this.connected || !this._socket) {
(0, logger_1.getLogger)().error("Tried to send message without active connection");
reject("Not connected");
return;
}
const message = this.createMessageTemplate(command, destination, data || {});
const reqId = message.request_id;
this._responseQueue[reqId] = resolve;
(0, logger_1.getLogger)().debug(`Sending message. dest=${destination} command=${command} reqId=${reqId}`);
const messageStr = JSON.stringify(message);
this._socket.send(messageStr, (err) => {
if (err) {
(0, logger_1.getLogger)().error(`Error while sending message: ${messageStr}`);
(0, logger_1.getLogger)().error(JSON.stringify(err));
}
});
});
}
createMessageTemplate(command, destination, data) {
return {
command,
data,
ack: false,
origin: this._serviceName,
destination,
request_id: (0, crypto_1.randomBytes)(32).toString("hex"),
};
}
async subscribe(service) {
if (!this.connected || !this._socket) {
(0, logger_1.getLogger)().error(`Tried to subscribe '${service}' without active connection`);
throw new Error("Not connected");
}
if (this._subscriptions.findIndex((s) => s === service) > -1) {
return {
command: "register_service",
data: { success: true },
ack: true,
origin: "daemon",
destination: service,
request_id: "",
};
}
let error;
const result = await this.sendMessage("daemon", "register_service", {
service,
}).catch((e) => {
error = e;
return null;
});
if (error || !result) {
(0, logger_1.getLogger)().error("Failed to register agent service to daemon");
(0, logger_1.getLogger)().error(error instanceof Error
? `${error.name}: ${error.message}`
: JSON.stringify(error));
throw new Error("Subscribe failed");
}
this._subscriptions.push(service);
return result;
}
addEventListener(type, listener) {
if (type === "open") {
this._openEventListeners.push(listener);
}
else if (type === "message") {
this._messageEventListeners.push(listener);
}
else if (type === "error") {
this._errorEventListeners.push(listener);
}
else if (type === "close") {
this._closeEventListeners.push(listener);
}
}
removeEventListener(type, listener) {
let listeners;
if (type === "open") {
listeners = this._openEventListeners;
}
else if (type === "message") {
listeners = this._messageEventListeners;
}
else if (type === "error") {
listeners = this._errorEventListeners;
}
else if (type === "close") {
listeners = this._closeEventListeners;
}
else {
return;
}
const index = listeners.findIndex((l) => l === listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
clearAllEventListeners() {
this._openEventListeners = [];
this._messageEventListeners = [];
this._errorEventListeners = [];
this._closeEventListeners = [];
this._messageListeners = {};
}
/**
* Add listener for message
* @param {string} origin - Can be chia_farmer, chia_full_node, chia_wallet, etc.
* @param listener - Triggered when a message arrives.
*/
addMessageListener(origin, listener) {
const o = origin || "all";
if (!this._messageListeners[o]) {
this._messageListeners[o] = [];
}
this._messageListeners[o].push(listener);
// Returns removeMessageListener function.
return () => {
this.removeMessageListener(o, listener);
};
}
removeMessageListener(origin, listener) {
const listeners = this._messageListeners[origin];
if (!listeners) {
return;
}
const index = listeners.findIndex((l) => l === listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
clearAllMessageListeners() {
this._messageListeners = {};
}
async onOpen(event, url) {
(0, logger_1.getLogger)().info("ws connection opened");
this._connectedUrl = url;
this._openEventListeners.forEach((l) => l(event));
return this.subscribe(this._serviceName);
}
onError(error) {
(0, logger_1.getLogger)().error(`ws connection error: ${error.message}`);
this._errorEventListeners.forEach((l) => l(error));
}
onMessage(event) {
let payload;
let request_id;
let origin;
let command;
try {
payload = JSON.parse(event.data);
({ request_id, origin, command } = payload);
}
catch (err) {
(0, logger_1.getLogger)().error(`Failed to parse message data: ${JSON.stringify(err)}`);
(0, logger_1.getLogger)().error(`payload: ${event.data}`);
return;
}
(0, logger_1.getLogger)().debug(`Arrived message. origin=${origin} command=${command} reqId=${request_id}`);
const resolver = this._responseQueue[request_id];
if (resolver) {
delete this._responseQueue[request_id];
resolver(payload);
}
this._messageEventListeners.forEach((l) => l(event));
for (const o in this._messageListeners) {
if (!Object.prototype.hasOwnProperty.call(this._messageListeners, o)) {
continue;
}
const listeners = this._messageListeners[o];
if (origin === o || o === "all") {
listeners.forEach((l) => l(payload));
}
}
}
onClose(event) {
if (this._socket) {
this._socket.off("error", this.onError);
this._socket.removeEventListener("message", this.onMessage);
this._socket.off("close", this.onClose);
this._socket.off("ping", this.onPing);
this._socket.off("pong", this.onPong);
this._socket = null;
}
this._closing = false;
this._connectedUrl = "";
this._subscriptions = [];
this._closeEventListeners.forEach((l) => l(event));
this.clearAllEventListeners();
(0, logger_1.getLogger)().info(`Closed ws connection. code:${event.code} wasClean:${event.wasClean} reason:${event.reason}`);
if (this._onClosePromise) {
this._onClosePromise();
this._onClosePromise = undefined;
}
}
onPing() {
(0, logger_1.getLogger)().debug("Received ping");
}
onPong() {
(0, logger_1.getLogger)().debug("Received pong");
}
}