UNPKG

@phnq/message

Version:

Asynchronous, incremental messaging client and server

220 lines (219 loc) 10.1 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NATSTransport = void 0; const log_1 = require("@phnq/log"); const nats_1 = require("nats"); const object_hash_1 = __importDefault(require("object-hash")); const uuid_1 = require("uuid"); const MessageTransport_1 = require("../MessageTransport"); const serialize_1 = require("../serialize"); const log = (0, log_1.createLogger)('NATSTransport'); const logTraffic = process.env.PHNQ_MESSAGE_LOG_NATS === '1'; const CHUNK_HEADER_PREFIX = Buffer.from('@phnq/message/chunk', 'utf-8'); // Keep track of clients by config hash so they can be shared const clients = new Map(); const JSON_CODEC = (0, nats_1.JSONCodec)(); const connectToNats = (config) => __awaiter(void 0, void 0, void 0, function* () { const maxConnectAttempts = config.maxConnectAttempts || 1; const connectTimeWait = config.connectTimeWait || 2000; let connectAttempts = 0; while (maxConnectAttempts === -1 || connectAttempts < maxConnectAttempts) { try { return yield (0, nats_1.connect)(config); } catch (err) { if (maxConnectAttempts === 1) { throw err; } log.error('NATS connection failed: ', err); log('Retrying in %d ms', connectTimeWait); yield sleep(connectTimeWait); } connectAttempts += 1; } throw new Error(`NATS connection failed after ${connectAttempts} attempts`); }); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); class NATSTransport { static create(config, options) { return __awaiter(this, void 0, void 0, function* () { const [nc, refCount] = clients.get((0, object_hash_1.default)(config)) || [yield connectToNats(config), 0]; clients.set((0, object_hash_1.default)(config), [nc, refCount + 1]); const natsTransport = new NATSTransport(config, nc, options); natsTransport.initialize(); return natsTransport; }); } constructor(config, nc, options) { var _a; this.subjectById = new Map(); this.chunkedMessages = new Map(); this.config = config; this.nc = nc; this.options = options; this.maxPayload = ((_a = this.nc.info) === null || _a === void 0 ? void 0 : _a.max_payload) || 0; if (this.maxPayload === 0) { throw new Error('NATS max_payload not set'); } } close() { return __awaiter(this, void 0, void 0, function* () { const clientPoolKey = (0, object_hash_1.default)(this.config); const clientInfo = clients.get(clientPoolKey); if (clientInfo) { const [nc, refCount] = clientInfo; if (refCount > 1) { clients.set(clientPoolKey, [nc, refCount - 1]); } else { log('Closing NATS connection: ', this.config); this.nc.close(); clients.delete(clientPoolKey); } } }); } getConnections() { return __awaiter(this, void 0, void 0, function* () { if (!this.config.monitorUrl) { throw new Error('monitorUrl not set'); } const connz = (yield (yield fetch([this.config.monitorUrl, 'connz'].join('/'), { headers: { Connection: 'close' } })).json()); return connz.connections; }); } send(message) { return __awaiter(this, void 0, void 0, function* () { const publishSubject = this.options.publishSubject; let subject; if (message.t === MessageTransport_1.MessageType.End) { subject = this.subjectById.get(message.c); } else { subject = typeof publishSubject === 'string' ? publishSubject : publishSubject(message); } if (subject === undefined) { throw new Error('Could not get subject'); } if (message.t === MessageTransport_1.MessageType.End) { this.subjectById.delete(message.c); } else { this.subjectById.set(message.c, subject); } if (logTraffic) { log('PUBLISH [%s] %O', subject, message); } const marshalledMessage = this.marshall(message); if (marshalledMessage.length > this.maxPayload) { this.sendMessageInChunks(subject, marshalledMessage); } else { this.nc.publish(subject, marshalledMessage); } }); } onReceive(receiveHandler) { this.receiveHandler = receiveHandler; } marshall(message) { return new Uint8Array(JSON_CODEC.encode((0, serialize_1.annotate)(message))); } unmarshall(data) { return (0, serialize_1.deannotate)(JSON_CODEC.decode(data)); } initialize() { this.options.subscriptions.forEach((subscription) => __awaiter(this, void 0, void 0, function* () { var _a, e_1, _b, _c; const subject = typeof subscription === 'string' ? subscription : subscription.subject; const options = typeof subscription === 'string' ? undefined : subscription.options; const sub = this.nc.subscribe(subject, options); try { for (var _d = true, sub_1 = __asyncValues(sub), sub_1_1; sub_1_1 = yield sub_1.next(), _a = sub_1_1.done, !_a;) { _c = sub_1_1.value; _d = false; try { const msg = _c; if (this.receiveHandler) { const msgData = msg.data; if (CHUNK_HEADER_PREFIX.some((b, i) => msgData[i] !== b)) { const message = this.unmarshall(msgData); if (logTraffic) { log('RECEIVE [%s] %O', subject, message); } this.receiveHandler(message); } else { this.receiveMessageChunk(msgData); } } } finally { _d = true; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = sub_1.return)) yield _b.call(sub_1); } finally { if (e_1) throw e_1.error; } } })); } /** * |----- ? bytes -----|| 16 || 1 || 1 | * [CHUNK_HEADER_PREFIX][uuid][index][total] */ sendMessageInChunks(subject, marshalledMessage) { const chunkHeaderLen = CHUNK_HEADER_PREFIX.length + 18; const chunkBodyLen = this.maxPayload - chunkHeaderLen; const numChunks = Math.ceil(marshalledMessage.length / chunkBodyLen); const uuidBuf = []; (0, uuid_1.v4)(undefined, uuidBuf); for (let i = 0; i < numChunks; i++) { const chunkHeader = Buffer.concat([CHUNK_HEADER_PREFIX, Buffer.from(uuidBuf), Buffer.from([i, numChunks])], chunkHeaderLen); const chunkBody = marshalledMessage.slice(i * chunkBodyLen, Math.min((i + 1) * chunkBodyLen, marshalledMessage.length)); this.nc.publish(subject, new Uint8Array(Buffer.concat([chunkHeader, chunkBody]))); } } receiveMessageChunk(chunkBuf) { if (!this.receiveHandler) { return; } const prefixLen = CHUNK_HEADER_PREFIX.length; const uuidStr = (0, object_hash_1.default)(chunkBuf.slice(prefixLen, prefixLen + 16)); const [chunkIndex, numChunks] = chunkBuf.slice(prefixLen + 16, prefixLen + 18); let chunkedMessage = this.chunkedMessages.get(uuidStr); if (!chunkedMessage) { chunkedMessage = new Array(numChunks); this.chunkedMessages.set(uuidStr, chunkedMessage); } chunkedMessage[chunkIndex] = chunkBuf.slice(prefixLen + 18); if (chunkedMessage.length === chunkedMessage.filter(Boolean).length) { this.receiveHandler(this.unmarshall(Buffer.concat(chunkedMessage))); this.chunkedMessages.delete(uuidStr); } } } exports.NATSTransport = NATSTransport;