butlerd
Version:
Node.js library for butlerd, the butler daemon
226 lines (225 loc) • 8.24 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const support_1 = require("./support");
const net = require("net");
const split2 = require("split2");
exports.CONNECTION_TIMEOUT = 2000; // 2s timeouts, out to be enough for loopback connections..
const genericResult = support_1.createResult();
const MetaAuthenticate = support_1.createRequest("Meta.Authenticate");
const ProxyConnect = support_1.createRequest("Proxy.Connect");
class Conversation {
constructor(client) {
this.cancelled = false;
this.closed = false;
this.notificationHandlers = {};
this.missingNotificationHandlersWarned = {};
this.requestHandlers = {};
this.inboundRequests = {};
this.outboundRequests = {};
this.client = client;
this.socket = new net.Socket();
}
async connect() {
let { endpoint } = this.client;
const p = new Promise((resolve, reject) => {
let sock = this.socket;
let onConnect = () => {
resolve();
};
setTimeout(() => {
reject(support_1.RequestError.fromInternalCode(support_1.InternalCode.ConnectionTimedOut));
}, exports.CONNECTION_TIMEOUT);
sock.on("error", e => {
reject(e);
});
sock.on("close", () => {
this.close();
reject(support_1.RequestError.fromInternalCode(support_1.InternalCode.SocketClosed));
});
let { host, port } = this.client.proxy || this.client;
sock.connect({ host, port }, onConnect);
sock.pipe(split2(JSON.parse)).on("data", (message) => {
this.handleMessage(message).catch(e => {
this.client.warn(`While processing message: ${e.stack}`);
});
});
});
p.catch(e => { }); // avoid unhandled rejections
if (this.cancelled) {
throw support_1.RequestError.fromInternalCode(support_1.InternalCode.ConversationCancelled);
}
await p;
if (this.client.proxy) {
await this.internalCall(ProxyConnect, {
address: `${endpoint.tcp.address}`,
});
}
await this.internalCall(MetaAuthenticate, { secret: endpoint.secret });
}
onRequest(rc, handler) {
if (this.requestHandlers[rc.__method]) {
throw new Error(`cannot register a second request handler for ${rc.__method}`);
}
this.requestHandlers[rc.__method] = handler;
}
onNotification(nc, handler) {
if (this.notificationHandlers[nc.__method]) {
throw new Error(`cannot register a second notification handler for ${nc.__method}`);
}
this.notificationHandlers[nc.__method] = handler;
}
async handleMessage(msg) {
if (this.cancelled) {
return;
}
if (typeof msg !== "object") {
return;
}
if (msg.jsonrpc != "2.0") {
return;
}
if (typeof msg.id === "undefined") {
// we got a notification!
const handler = this.notificationHandlers[msg.method];
if (!handler) {
if (!this.missingNotificationHandlersWarned[msg.method]) {
this.missingNotificationHandlersWarned[msg.method] = true;
this.client.warn(`no handler for notification ${msg.method} (in ${this.firstMethod} convo)`);
}
return;
}
try {
await Promise.resolve(handler(msg.params));
}
catch (e) {
this.client.warn(`notification handler error: ${e.stack}`);
if (this.client.errorHandler) {
this.client.errorHandler(e);
}
}
return;
}
if (msg.method) {
try {
this.inboundRequests[msg.id] = true;
let receivedAt = Date.now();
const handler = this.requestHandlers[msg.method];
if (!handler) {
if (this.cancelled) {
return;
}
this.sendResult(genericResult, msg.id, null, {
code: support_1.StandardErrorCode.MethodNotFound,
message: `no handler is registered for method ${msg.method}`,
});
return;
}
try {
const result = await handler(msg.params);
if (this.cancelled) {
return;
}
this.sendResult(genericResult, msg.id, result, undefined);
}
catch (e) {
if (this.cancelled) {
return;
}
this.sendResult(genericResult, msg.id, null, {
code: support_1.StandardErrorCode.InternalError,
message: `async error: ${e.message}`,
data: {
stack: e.stack,
},
});
}
}
finally {
delete this.inboundRequests[msg.id];
}
return;
}
if (msg.result || msg.error) {
let req = this.outboundRequests[msg.id];
delete this.outboundRequests[msg.id];
if (msg.error) {
req.reject(new support_1.RequestError(msg.error));
}
else {
req.resolve(msg.result);
}
return;
}
if (this.cancelled) {
return;
}
this.sendResult(genericResult, msg.id, null, {
code: support_1.StandardErrorCode.InvalidRequest,
message: "has id but doesn't have method, result, or error",
});
}
sendResult(rc, id, result, error) {
const obj = rc(id, result, error);
if (typeof obj.id !== "number") {
throw new Error(`missing id in result ${JSON.stringify(obj)}`);
}
this.write(obj);
}
async call(rc, params) {
if (!this.firstMethod) {
this.firstMethod = rc({})(this.client).method;
}
return await this.internalCall(rc, params);
}
async internalCall(rc, params) {
const obj = rc(params || {})(this.client);
if (typeof obj.id !== "number") {
throw new Error(`missing id in request ${JSON.stringify(obj)}`);
}
let method = obj.method;
{
let sentAt = Date.now();
try {
const res = await new Promise((resolve, reject) => {
this.outboundRequests[obj.id] = { resolve, reject };
this.write(obj);
});
return res;
}
catch (err) {
throw err;
}
finally {
delete this.outboundRequests[obj.id];
}
}
}
write(obj) {
if (this.cancelled) {
return;
}
let payload = JSON.stringify(obj);
this.socket.write(payload);
this.socket.write("\n");
}
cancel() {
if (this.cancelled) {
return;
}
this.cancelled = true;
for (const id of Object.keys(this.outboundRequests)) {
let req = this.outboundRequests[parseInt(id, 10)];
req.reject(support_1.RequestError.fromInternalCode(support_1.InternalCode.ConversationCancelled));
}
this.outboundRequests = {};
this.socket.end();
}
close() {
if (this.closed) {
return;
}
this.cancel();
this.closed = true;
}
}
exports.Conversation = Conversation;