UNPKG

@phnq/message

Version:

Asynchronous, incremental messaging client and server

420 lines (419 loc) 19.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MessageConnection = exports.ConversationPerspective = void 0; const log_1 = require("@phnq/log"); const streams_1 = require("@phnq/streams"); const browser_process_hrtime_1 = __importDefault(require("browser-process-hrtime")); const uuid_1 = require("uuid"); const errors_1 = require("./errors"); const MessageTransport_1 = require("./MessageTransport"); const sign_1 = require("./sign"); /** * MessageConnection * ================= * A conversation between agents consists of a single request by one agent, followed by zero or more responses from another agent. * Accordingly, there are two possible perspectives in any conversation: * * 1) Client -- one who sends a message and gets responses, * 2) Server -- one who waits for messages and sends responses. * * The complication in this familiar client/server relationship is that a single MessageConnection instance * may be both client and server. * * As Client * --------- * This simply involves calling methods `send()` or `request*()`. Send is one-way so since there will be no response, * there is no return value. The `request*()` methods return either an async value (i.e. promise) or iterator (multiple * values). * * As Server * --------- * The `onReceive` field must be set to act as a server. Implementing `onReceive` involves dealing with an incoming * message and returning either an async value or an async iterator. */ const log = (0, log_1.createLogger)('MessageConnection'); const idIterator = (function* () { let i = 0; while (true) { yield ++i; } })(); const possiblyThrow = (message) => { switch (message.t) { case MessageTransport_1.MessageType.Anomaly: { const anomalyMessage = message; throw new errors_1.Anomaly(anomalyMessage.p.message, anomalyMessage.p.info); } case MessageTransport_1.MessageType.Error: throw new Error(message.p.message); } }; var ConversationPerspective; (function (ConversationPerspective) { ConversationPerspective["Requester"] = "requester"; ConversationPerspective["Responder"] = "responder"; })(ConversationPerspective = exports.ConversationPerspective || (exports.ConversationPerspective = {})); const DEFAULT_RESPONSE_TIMEOUT = 30000; class MessageConnection { constructor(transport, { signSalt, marshalPayload, unmarshalPayload } = {}) { this.responseTimeout = DEFAULT_RESPONSE_TIMEOUT; this.connId = (0, uuid_1.v4)(); this.responseQueues = new Map(); this.attributes = new Map(); this.transport = transport; this.signSalt = signSalt; this.marshalPayload = marshalPayload || (p => p); this.unmarshalPayload = unmarshalPayload || (p => p); transport.onReceive(message => { var _a; let errorMessage; if (this.signSalt) { try { (0, sign_1.verifyMessage)(message, this.signSalt); } catch (err) { errorMessage = { c: message.c, s: message.s, t: MessageTransport_1.MessageType.Error, p: { message: (_a = err.message) !== null && _a !== void 0 ? _a : 'Failed to verify message', requestPayload: message.p }, }; } } const unmarshaledMessage = this.unmarshalMessage(errorMessage || message); if (unmarshaledMessage.t === MessageTransport_1.MessageType.Request) { this.handleRequest(unmarshaledMessage); return; } /** * It is, in fact, possible to receive messages that are not intended for * this MessageConnection instance. This is because multiple connections * may share a single MessageTransport; in this case, they will all receive * every incoming message. Since request ids are assigned by the global * idIterator, there is a zero collision guarantee. */ const responseQueue = this.responseQueues.get(unmarshaledMessage.c); if (responseQueue) { switch (unmarshaledMessage.t) { case MessageTransport_1.MessageType.Response: case MessageTransport_1.MessageType.Anomaly: case MessageTransport_1.MessageType.Error: case MessageTransport_1.MessageType.End: responseQueue.enqueue(unmarshaledMessage); responseQueue.flush(); break; case MessageTransport_1.MessageType.Multi: responseQueue.enqueue(unmarshaledMessage); break; } } }); } get onReceive() { return this.receiveHandler; } set onReceive(receiveHandler) { this.receiveHandler = receiveHandler; } get id() { return this.connId; } getAttribute(key) { return this.attributes.get(key); } /** * Set a keyed value on the connection. This key/value pair are cached on the * connection instance. * @param key the key * @param value the value */ setAttribute(key, value) { this.attributes.set(key, value); } deleteAttribute(key) { this.attributes.delete(key); } send(data) { return __awaiter(this, void 0, void 0, function* () { yield this.requestOne(data, false); }); } requestOne(data, expectResponse = true) { var _a, e_1, _b, _c; return __awaiter(this, void 0, void 0, function* () { const resp = yield this.request(data, expectResponse); if (typeof resp === 'object' && resp[Symbol.asyncIterator]) { const resps = []; try { for (var _d = true, _e = __asyncValues(resp), _f; _f = yield _e.next(), _a = _f.done, !_a;) { _c = _f.value; _d = false; try { const r = _c; resps.push(r); } finally { _d = true; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) yield _b.call(_e); } finally { if (e_1) throw e_1.error; } } if (resps.length > 1) { log.warn('requestOne: multiple responses were returned -- all but the first were discarded'); } return resps[0]; } else { return resp; } }); } requestMulti(data) { return __awaiter(this, void 0, void 0, function* () { const resp = yield this.request(data); if (typeof resp === 'object' && resp[Symbol.asyncIterator]) { return resp; } else { return (function () { return __asyncGenerator(this, arguments, function* () { yield yield __await(resp); }); })(); } }); } request(data, expectResponse = true) { return __awaiter(this, void 0, void 0, function* () { return this.doRequest(data, expectResponse); }); } marshalMessage(message) { switch (message.t) { case MessageTransport_1.MessageType.Request: return Object.assign(Object.assign({}, message), { p: this.marshalPayload(message.p) }); case MessageTransport_1.MessageType.Response: case MessageTransport_1.MessageType.Multi: return Object.assign(Object.assign({}, message), { p: this.marshalPayload(message.p) }); } return message; } unmarshalMessage(message) { switch (message.t) { case MessageTransport_1.MessageType.Request: return Object.assign(Object.assign({}, message), { p: this.unmarshalPayload(message.p) }); case MessageTransport_1.MessageType.Response: case MessageTransport_1.MessageType.Multi: return Object.assign(Object.assign({}, message), { p: this.unmarshalPayload(message.p) }); } return message; } signMessage(message) { if (this.signSalt) { return (0, sign_1.signMessage)(message, this.signSalt); } return message; } doRequest(payload, expectResponse) { return __awaiter(this, void 0, void 0, function* () { const reqId = idIterator.next().value; const responseQueues = this.responseQueues; const source = this.id; const requestMessage = this.signMessage({ t: MessageTransport_1.MessageType.Request, c: reqId, p: payload, s: source, }); const conversation = { perspective: ConversationPerspective.Requester, request: requestMessage, responses: [], }; const start = (0, browser_process_hrtime_1.default)(); const responseQueue = new streams_1.AsyncQueue(); if (expectResponse) { responseQueue.maxWaitTime = this.responseTimeout; responseQueues.set(reqId, responseQueue); } yield this.transport.send(requestMessage); if (expectResponse) { const iter = responseQueue.iterator(); const firstMsg = (yield iter.next()).value; conversation.responses.push({ message: firstMsg, time: (0, browser_process_hrtime_1.default)(start) }); const onConversation = this.onConversation; if (firstMsg.t === MessageTransport_1.MessageType.Multi) { return (function () { return __asyncGenerator(this, arguments, function* () { var _a, e_2, _b, _c; yield yield __await(firstMsg.p); try { try { for (var _d = true, _e = __asyncValues(responseQueue.iterator()), _f; _f = yield __await(_e.next()), _a = _f.done, !_a;) { _c = _f.value; _d = false; try { const message = _c; if (message.s === firstMsg.s) { conversation.responses.push({ message, time: (0, browser_process_hrtime_1.default)(start) }); possiblyThrow(message); if (message.t === MessageTransport_1.MessageType.Multi) { yield yield __await(message.p); } } else { log.warn('Received responses from multiple sources for request -- keeping the first, ignoring the rest: %s', JSON.stringify(payload)); } } finally { _d = true; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) yield __await(_b.call(_e)); } finally { if (e_2) throw e_2.error; } } if (onConversation) { onConversation(conversation); } } finally { responseQueues.delete(reqId); } }); })(); } else { responseQueues.delete(reqId); if (onConversation) { onConversation(conversation); } possiblyThrow(firstMsg); return firstMsg.p; } } }); } handleRequest(message) { var _a, e_3, _b, _c; return __awaiter(this, void 0, void 0, function* () { const source = this.id; const conversation = { perspective: ConversationPerspective.Responder, request: message, responses: [], }; const start = (0, browser_process_hrtime_1.default)(); const requestPayload = message.p; const respond = (m) => { const signedMessage = this.signMessage(this.marshalMessage(m)); this.transport.send(signedMessage); conversation.responses.push({ message: signedMessage, time: (0, browser_process_hrtime_1.default)(start) }); }; if (!this.receiveHandler) { throw new Error('No receive handler set.'); } try { const result = yield this.receiveHandler(requestPayload); if (typeof result === 'object' && result[Symbol.asyncIterator]) { try { for (var _d = true, _e = __asyncValues(result), _f; _f = yield _e.next(), _a = _f.done, !_a;) { _c = _f.value; _d = false; try { const responsePayload = _c; respond({ p: responsePayload, c: message.c, s: source, t: MessageTransport_1.MessageType.Multi }); } finally { _d = true; } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) yield _b.call(_e); } finally { if (e_3) throw e_3.error; } } respond({ c: message.c, s: source, t: MessageTransport_1.MessageType.End, p: 'END' }); } else if (result) { const responsePayload = result; respond({ p: responsePayload, c: message.c, s: source, t: MessageTransport_1.MessageType.Response }); } else { // kill the async queue } } catch (err) { if (err instanceof errors_1.Anomaly) { const anomalyMessage = { p: { message: err.message, info: err.info, requestPayload }, c: message.c, s: source, t: MessageTransport_1.MessageType.Anomaly, }; respond(anomalyMessage); } else { const errMessage = err.message || String(err); const errorMessage = { p: { message: errMessage, requestPayload }, c: message.c, s: source, t: MessageTransport_1.MessageType.Error, }; respond(errorMessage); } } finally { if (this.onConversation) { this.onConversation(conversation); } } }); } } exports.MessageConnection = MessageConnection;