@phnq/message
Version:
Asynchronous, incremental messaging client and server
220 lines (219 loc) • 10.1 kB
JavaScript
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;
;