nats
Version:
Node.js client for NATS, a lightweight, high-performance cloud native messaging system
703 lines • 25.4 kB
JavaScript
"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); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProtocolHandler = exports.Connect = exports.createInbox = exports.INFO = void 0;
/*
* Copyright 2018-2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const types_1 = require("./types");
const transport_1 = require("./transport");
const error_1 = require("./error");
const util_1 = require("./util");
const nuid_1 = require("./nuid");
const databuffer_1 = require("./databuffer");
const servers_1 = require("./servers");
const queued_iterator_1 = require("./queued_iterator");
const subscription_1 = require("./subscription");
const subscriptions_1 = require("./subscriptions");
const muxsubscription_1 = require("./muxsubscription");
const heartbeats_1 = require("./heartbeats");
const parser_1 = require("./parser");
const msg_1 = require("./msg");
const encoders_1 = require("./encoders");
const semver_1 = require("./semver");
const FLUSH_THRESHOLD = 1024 * 32;
exports.INFO = /^INFO\s+([^\r\n]+)\r\n/i;
function createInbox(prefix = "") {
prefix = prefix || "_INBOX";
if (typeof prefix !== "string") {
throw (new Error("prefix must be a string"));
}
return `${prefix}.${nuid_1.nuid.next()}`;
}
exports.createInbox = createInbox;
const PONG_CMD = (0, encoders_1.encode)("PONG\r\n");
const PING_CMD = (0, encoders_1.encode)("PING\r\n");
class Connect {
constructor(transport, opts, nonce) {
this.protocol = 1;
this.version = transport.version;
this.lang = transport.lang;
this.echo = opts.noEcho ? false : undefined;
this.verbose = opts.verbose;
this.pedantic = opts.pedantic;
this.tls_required = opts.tls ? true : undefined;
this.name = opts.name;
const creds = (opts && opts.authenticator ? opts.authenticator(nonce) : {}) || {};
(0, util_1.extend)(this, creds);
}
}
exports.Connect = Connect;
class ProtocolHandler {
constructor(options, publisher) {
this._closed = false;
this.connected = false;
this.connectedOnce = false;
this.infoReceived = false;
this.noMorePublishing = false;
this.abortReconnect = false;
this.listeners = [];
this.pendingLimit = FLUSH_THRESHOLD;
this.outMsgs = 0;
this.inMsgs = 0;
this.outBytes = 0;
this.inBytes = 0;
this.options = options;
this.publisher = publisher;
this.subscriptions = new subscriptions_1.Subscriptions();
this.muxSubscriptions = new muxsubscription_1.MuxSubscription();
this.outbound = new databuffer_1.DataBuffer();
this.pongs = [];
//@ts-ignore: options.pendingLimit is hidden
this.pendingLimit = options.pendingLimit || this.pendingLimit;
this.features = new semver_1.Features({ major: 0, minor: 0, micro: 0 });
const servers = typeof options.servers === "string"
? [options.servers]
: options.servers;
this.servers = new servers_1.Servers(servers, {
randomize: !options.noRandomize,
});
this.closed = (0, util_1.deferred)();
this.parser = new parser_1.Parser(this);
this.heartbeats = new heartbeats_1.Heartbeat(this, this.options.pingInterval || types_1.DEFAULT_PING_INTERVAL, this.options.maxPingOut || types_1.DEFAULT_MAX_PING_OUT);
}
resetOutbound() {
this.outbound.reset();
const pongs = this.pongs;
this.pongs = [];
// reject the pongs - the disconnect from here shouldn't have a trace
// because that confuses API consumers
const err = error_1.NatsError.errorForCode(error_1.ErrorCode.Disconnect);
err.stack = "";
pongs.forEach((p) => {
p.reject(err);
});
this.parser = new parser_1.Parser(this);
this.infoReceived = false;
}
dispatchStatus(status) {
this.listeners.forEach((q) => {
q.push(status);
});
}
status() {
const iter = new queued_iterator_1.QueuedIteratorImpl();
this.listeners.push(iter);
return iter;
}
prepare() {
this.info = undefined;
this.resetOutbound();
const pong = (0, util_1.deferred)();
pong.catch(() => {
// provide at least one catch - as pong rejection can happen before it is expected
});
this.pongs.unshift(pong);
this.connectError = (err) => {
pong.reject(err);
};
this.transport = (0, transport_1.newTransport)();
this.transport.closed()
.then((_err) => __awaiter(this, void 0, void 0, function* () {
this.connected = false;
if (!this.isClosed()) {
// if the transport gave an error use that, otherwise
// we may have received a protocol error
yield this.disconnected(this.transport.closeError || this.lastError);
return;
}
}));
return pong;
}
disconnect() {
this.dispatchStatus({ type: types_1.DebugEvents.StaleConnection, data: "" });
this.transport.disconnect();
}
disconnected(err) {
return __awaiter(this, void 0, void 0, function* () {
this.dispatchStatus({
type: types_1.Events.Disconnect,
data: this.servers.getCurrentServer().toString(),
});
if (this.options.reconnect) {
yield this.dialLoop()
.then(() => {
this.dispatchStatus({
type: types_1.Events.Reconnect,
data: this.servers.getCurrentServer().toString(),
});
})
.catch((err) => {
this._close(err);
});
}
else {
yield this._close(err);
}
});
}
dial(srv) {
return __awaiter(this, void 0, void 0, function* () {
const pong = this.prepare();
let timer;
try {
timer = (0, util_1.timeout)(this.options.timeout || 20000);
const cp = this.transport.connect(srv, this.options);
yield Promise.race([cp, timer]);
(() => __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
try {
try {
for (var _d = true, _e = __asyncValues(this.transport), _f; _f = yield _e.next(), _a = _f.done, !_a;) {
_c = _f.value;
_d = false;
try {
const b = _c;
this.parser.parse(b);
}
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; }
}
}
catch (err) {
console.log("reader closed", err);
}
}))().then();
}
catch (err) {
pong.reject(err);
}
try {
yield Promise.race([timer, pong]);
if (timer) {
timer.cancel();
}
this.connected = true;
this.connectError = undefined;
this.sendSubscriptions();
this.connectedOnce = true;
this.server.didConnect = true;
this.server.reconnects = 0;
this.flushPending();
this.heartbeats.start();
}
catch (err) {
if (timer) {
timer.cancel();
}
yield this.transport.close(err);
throw err;
}
});
}
_doDial(srv) {
return __awaiter(this, void 0, void 0, function* () {
const alts = yield srv.resolve({
fn: (0, transport_1.getResolveFn)(),
debug: this.options.debug,
randomize: !this.options.noRandomize,
});
let lastErr = null;
for (const a of alts) {
try {
lastErr = null;
this.dispatchStatus({ type: types_1.DebugEvents.Reconnecting, data: a.toString() });
yield this.dial(a);
// if here we connected
return;
}
catch (err) {
lastErr = err;
}
}
// if we are here, we failed, and we have no additional
// alternatives for this server
throw lastErr;
});
}
dialLoop() {
return __awaiter(this, void 0, void 0, function* () {
let lastError;
while (true) {
if (this._closed) {
// if we are disconnected, and close is called, the client
// still tries to reconnect - to match the reconnect policy
// in the case of close, want to stop.
this.servers.clear();
}
const wait = this.options.reconnectDelayHandler
? this.options.reconnectDelayHandler()
: types_1.DEFAULT_RECONNECT_TIME_WAIT;
let maxWait = wait;
const srv = this.selectServer();
if (!srv || this.abortReconnect) {
if (lastError) {
throw lastError;
}
else if (this.lastError) {
throw this.lastError;
}
else {
throw error_1.NatsError.errorForCode(error_1.ErrorCode.ConnectionRefused);
}
}
const now = Date.now();
if (srv.lastConnect === 0 || srv.lastConnect + wait <= now) {
srv.lastConnect = Date.now();
try {
yield this._doDial(srv);
break;
}
catch (err) {
lastError = err;
if (!this.connectedOnce) {
if (this.options.waitOnFirstConnect) {
continue;
}
this.servers.removeCurrentServer();
}
srv.reconnects++;
const mra = this.options.maxReconnectAttempts || 0;
if (mra !== -1 && srv.reconnects >= mra) {
this.servers.removeCurrentServer();
}
}
}
else {
maxWait = Math.min(maxWait, srv.lastConnect + wait - now);
yield (0, util_1.delay)(maxWait);
}
}
});
}
static connect(options, publisher) {
return __awaiter(this, void 0, void 0, function* () {
const h = new ProtocolHandler(options, publisher);
yield h.dialLoop();
return h;
});
}
static toError(s) {
const t = s ? s.toLowerCase() : "";
if (t.indexOf("permissions violation") !== -1) {
const err = new error_1.NatsError(s, error_1.ErrorCode.PermissionsViolation);
const m = s.match(/(Publish|Subscription) to "(\S+)"/);
if (m) {
err.permissionContext = {
operation: m[1].toLowerCase(),
subject: m[2],
};
}
return err;
}
else if (t.indexOf("authorization violation") !== -1) {
return new error_1.NatsError(s, error_1.ErrorCode.AuthorizationViolation);
}
else if (t.indexOf("user authentication expired") !== -1) {
return new error_1.NatsError(s, error_1.ErrorCode.AuthenticationExpired);
}
else {
return new error_1.NatsError(s, error_1.ErrorCode.ProtocolError);
}
}
processMsg(msg, data) {
this.inMsgs++;
this.inBytes += data.length;
if (!this.subscriptions.sidCounter) {
return;
}
const sub = this.subscriptions.get(msg.sid);
if (!sub) {
return;
}
sub.received += 1;
if (sub.callback) {
sub.callback(null, new msg_1.MsgImpl(msg, data, this));
}
if (sub.max !== undefined && sub.received >= sub.max) {
sub.unsubscribe();
}
}
processError(m) {
const s = (0, encoders_1.decode)(m);
const err = ProtocolHandler.toError(s);
const status = { type: types_1.Events.Error, data: err.code };
if (err.isPermissionError()) {
let isMuxPermissionError = false;
if (err.permissionContext) {
status.permissionContext = err.permissionContext;
const mux = this.subscriptions.getMux();
isMuxPermissionError = (mux === null || mux === void 0 ? void 0 : mux.subject) === err.permissionContext.subject;
}
this.subscriptions.handleError(err);
this.muxSubscriptions.handleError(isMuxPermissionError, err);
if (isMuxPermissionError) {
// remove the permission - enable it to be recreated
this.subscriptions.setMux(null);
}
}
this.dispatchStatus(status);
this.handleError(err);
}
handleError(err) {
if (err.isAuthError()) {
this.handleAuthError(err);
}
if (err.isProtocolError()) {
this.lastError = err;
}
if (!err.isPermissionError()) {
this.lastError = err;
}
}
handleAuthError(err) {
if ((this.lastError && err.code === this.lastError.code) &&
this.options.ignoreAuthErrorAbort === false) {
this.abortReconnect = true;
}
if (this.connectError) {
this.connectError(err);
}
else {
this.disconnect();
}
}
processPing() {
this.transport.send(PONG_CMD);
}
processPong() {
const cb = this.pongs.shift();
if (cb) {
cb.resolve();
}
}
processInfo(m) {
const info = JSON.parse((0, encoders_1.decode)(m));
this.info = info;
const updates = this.options && this.options.ignoreClusterUpdates
? undefined
: this.servers.update(info);
if (!this.infoReceived) {
this.features.update((0, semver_1.parseSemVer)(info.version));
this.infoReceived = true;
if (this.transport.isEncrypted()) {
this.servers.updateTLSName();
}
// send connect
const { version, lang } = this.transport;
try {
const c = new Connect({ version, lang }, this.options, info.nonce);
if (info.headers) {
c.headers = true;
c.no_responders = true;
}
const cs = JSON.stringify(c);
this.transport.send((0, encoders_1.encode)(`CONNECT ${cs}${util_1.CR_LF}`));
this.transport.send(PING_CMD);
}
catch (err) {
// if we are dying here, this is likely some an authenticator blowing up
this._close(err);
}
}
if (updates) {
this.dispatchStatus({ type: types_1.Events.Update, data: updates });
}
const ldm = info.ldm !== undefined ? info.ldm : false;
if (ldm) {
this.dispatchStatus({
type: types_1.Events.LDM,
data: this.servers.getCurrentServer().toString(),
});
}
}
push(e) {
switch (e.kind) {
case parser_1.Kind.MSG: {
const { msg, data } = e;
this.processMsg(msg, data);
break;
}
case parser_1.Kind.OK:
break;
case parser_1.Kind.ERR:
this.processError(e.data);
break;
case parser_1.Kind.PING:
this.processPing();
break;
case parser_1.Kind.PONG:
this.processPong();
break;
case parser_1.Kind.INFO:
this.processInfo(e.data);
break;
}
}
sendCommand(cmd, ...payloads) {
const len = this.outbound.length();
let buf;
if (typeof cmd === "string") {
buf = (0, encoders_1.encode)(cmd);
}
else {
buf = cmd;
}
this.outbound.fill(buf, ...payloads);
if (len === 0) {
setTimeout(() => {
this.flushPending();
});
}
else if (this.outbound.size() >= this.pendingLimit) {
this.flushPending();
}
}
publish(subject, data, options) {
let len = data.length;
options = options || {};
options.reply = options.reply || "";
let headers = types_1.Empty;
let hlen = 0;
if (options.headers) {
if (this.info && !this.info.headers) {
throw new error_1.NatsError("headers", error_1.ErrorCode.ServerOptionNotAvailable);
}
const hdrs = options.headers;
headers = hdrs.encode();
hlen = headers.length;
len = data.length + hlen;
}
if (this.info && len > this.info.max_payload) {
throw error_1.NatsError.errorForCode(error_1.ErrorCode.MaxPayloadExceeded);
}
this.outBytes += len;
this.outMsgs++;
let proto;
if (options.headers) {
if (options.reply) {
proto = `HPUB ${subject} ${options.reply} ${hlen} ${len}${util_1.CR_LF}`;
}
else {
proto = `HPUB ${subject} ${hlen} ${len}\r\n`;
}
this.sendCommand(proto, headers, data, util_1.CRLF);
}
else {
if (options.reply) {
proto = `PUB ${subject} ${options.reply} ${len}\r\n`;
}
else {
proto = `PUB ${subject} ${len}\r\n`;
}
this.sendCommand(proto, data, util_1.CRLF);
}
}
request(r) {
this.initMux();
this.muxSubscriptions.add(r);
return r;
}
subscribe(s) {
this.subscriptions.add(s);
this._subunsub(s);
return s;
}
_sub(s) {
if (s.queue) {
this.sendCommand(`SUB ${s.subject} ${s.queue} ${s.sid}\r\n`);
}
else {
this.sendCommand(`SUB ${s.subject} ${s.sid}\r\n`);
}
}
_subunsub(s) {
this._sub(s);
if (s.max) {
this.unsubscribe(s, s.max);
}
return s;
}
unsubscribe(s, max) {
this.unsub(s, max);
if (s.max === undefined || s.received >= s.max) {
this.subscriptions.cancel(s);
}
}
unsub(s, max) {
if (!s || this.isClosed()) {
return;
}
if (max) {
this.sendCommand(`UNSUB ${s.sid} ${max}${util_1.CR_LF}`);
}
else {
this.sendCommand(`UNSUB ${s.sid}${util_1.CR_LF}`);
}
s.max = max;
}
resub(s, subject) {
if (!s || this.isClosed()) {
return;
}
s.subject = subject;
this.subscriptions.resub(s);
// we don't auto-unsub here because we don't
// really know "processed"
this._sub(s);
}
flush(p) {
if (!p) {
p = (0, util_1.deferred)();
}
this.pongs.push(p);
this.sendCommand(PING_CMD);
return p;
}
sendSubscriptions() {
const cmds = [];
this.subscriptions.all().forEach((s) => {
const sub = s;
if (sub.queue) {
cmds.push(`SUB ${sub.subject} ${sub.queue} ${sub.sid}${util_1.CR_LF}`);
}
else {
cmds.push(`SUB ${sub.subject} ${sub.sid}${util_1.CR_LF}`);
}
});
if (cmds.length) {
this.transport.send((0, encoders_1.encode)(cmds.join("")));
}
}
_close(err) {
return __awaiter(this, void 0, void 0, function* () {
if (this._closed) {
return;
}
this.heartbeats.cancel();
if (this.connectError) {
this.connectError(err);
this.connectError = undefined;
}
this.muxSubscriptions.close();
this.subscriptions.close();
this.listeners.forEach((l) => {
l.stop();
});
this._closed = true;
yield this.transport.close(err);
yield this.closed.resolve(err);
});
}
close() {
return this._close();
}
isClosed() {
return this._closed;
}
drain() {
const subs = this.subscriptions.all();
const promises = [];
subs.forEach((sub) => {
promises.push(sub.drain());
});
return Promise.all(promises)
.then(() => __awaiter(this, void 0, void 0, function* () {
this.noMorePublishing = true;
yield this.flush();
return this.close();
}))
.catch(() => {
// cannot happen
});
}
flushPending() {
if (!this.infoReceived || !this.connected) {
return;
}
if (this.outbound.size()) {
const d = this.outbound.drain();
this.transport.send(d);
}
}
initMux() {
const mux = this.subscriptions.getMux();
if (!mux) {
const inbox = this.muxSubscriptions.init(this.options.inboxPrefix);
// dot is already part of mux
const sub = new subscription_1.SubscriptionImpl(this, `${inbox}*`);
sub.callback = this.muxSubscriptions.dispatcher();
this.subscriptions.setMux(sub);
this.subscribe(sub);
}
}
selectServer() {
const server = this.servers.selectServer();
if (server === undefined) {
return undefined;
}
// Place in client context.
this.server = server;
return this.server;
}
getServer() {
return this.server;
}
}
exports.ProtocolHandler = ProtocolHandler;
//# sourceMappingURL=protocol.js.map