nats
Version:
Node.js client for NATS, a lightweight, high-performance cloud native messaging system
527 lines • 17.8 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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServiceImpl = exports.ServiceGroupImpl = exports.ServiceMsgImpl = exports.ServiceApiPrefix = void 0;
/*
* Copyright 2022-2023 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 util_1 = require("./util");
const headers_1 = require("./headers");
const codec_1 = require("./codec");
const nuid_1 = require("./nuid");
const queued_iterator_1 = require("./queued_iterator");
const jsutil_1 = require("../jetstream/jsutil");
const semver_1 = require("./semver");
const encoders_1 = require("./encoders");
const core_1 = require("./core");
/**
* Services have common backplane subject pattern:
*
* `$SRV.PING|STATS|INFO` - pings or retrieves status for all services
* `$SRV.PING|STATS|INFO.<name>` - pings or retrieves status for all services having the specified name
* `$SRV.PING|STATS|INFO.<name>.<id>` - pings or retrieves status of a particular service
*
* Note that <name> and <id> are upper-cased.
*/
exports.ServiceApiPrefix = "$SRV";
class ServiceMsgImpl {
constructor(msg) {
this.msg = msg;
}
get data() {
return this.msg.data;
}
get sid() {
return this.msg.sid;
}
get subject() {
return this.msg.subject;
}
get reply() {
return this.msg.reply || "";
}
get headers() {
return this.msg.headers;
}
respond(data, opts) {
return this.msg.respond(data, opts);
}
respondError(code, description, data, opts) {
var _a, _b;
opts = opts || {};
opts.headers = opts.headers || (0, headers_1.headers)();
(_a = opts.headers) === null || _a === void 0 ? void 0 : _a.set(core_1.ServiceErrorCodeHeader, `${code}`);
(_b = opts.headers) === null || _b === void 0 ? void 0 : _b.set(core_1.ServiceErrorHeader, description);
return this.msg.respond(data, opts);
}
json(reviver) {
return this.msg.json(reviver);
}
string() {
return this.msg.string();
}
}
exports.ServiceMsgImpl = ServiceMsgImpl;
class ServiceGroupImpl {
constructor(parent, name = "", queue = "") {
if (name !== "") {
validInternalToken("service group", name);
}
let root = "";
if (parent instanceof ServiceImpl) {
this.srv = parent;
root = "";
}
else if (parent instanceof ServiceGroupImpl) {
const sg = parent;
this.srv = sg.srv;
if (queue === "" && sg.queue !== "") {
queue = sg.queue;
}
root = sg.subject;
}
else {
throw new Error("unknown ServiceGroup type");
}
this.subject = this.calcSubject(root, name);
this.queue = queue;
}
calcSubject(root, name = "") {
if (name === "") {
return root;
}
return root !== "" ? `${root}.${name}` : name;
}
addEndpoint(name = "", opts) {
opts = opts || { subject: name };
const args = typeof opts === "function"
? { handler: opts, subject: name }
: opts;
(0, jsutil_1.validateName)("endpoint", name);
let { subject, handler, metadata, queue } = args;
subject = subject || name;
queue = queue || this.queue;
validSubjectName("endpoint subject", subject);
subject = this.calcSubject(this.subject, subject);
const ne = { name, subject, queue, handler, metadata };
return this.srv._addEndpoint(ne);
}
addGroup(name = "", queue = "") {
return new ServiceGroupImpl(this, name, queue);
}
}
exports.ServiceGroupImpl = ServiceGroupImpl;
function validSubjectName(context, subj) {
if (subj === "") {
throw new Error(`${context} cannot be empty`);
}
if (subj.indexOf(" ") !== -1) {
throw new Error(`${context} cannot contain spaces: '${subj}'`);
}
const tokens = subj.split(".");
tokens.forEach((v, idx) => {
if (v === ">" && idx !== tokens.length - 1) {
throw new Error(`${context} cannot have internal '>': '${subj}'`);
}
});
}
function validInternalToken(context, subj) {
if (subj.indexOf(" ") !== -1) {
throw new Error(`${context} cannot contain spaces: '${subj}'`);
}
const tokens = subj.split(".");
tokens.forEach((v) => {
if (v === ">") {
throw new Error(`${context} name cannot contain internal '>': '${subj}'`);
}
});
}
class ServiceImpl {
/**
* @param verb
* @param name
* @param id
* @param prefix - this is only supplied by tooling when building control subject that crosses an account
*/
static controlSubject(verb, name = "", id = "", prefix) {
// the prefix is used as is, because it is an
// account boundary permission
const pre = prefix !== null && prefix !== void 0 ? prefix : exports.ServiceApiPrefix;
if (name === "" && id === "") {
return `${pre}.${verb}`;
}
(0, jsutil_1.validateName)("control subject name", name);
if (id !== "") {
(0, jsutil_1.validateName)("control subject id", id);
return `${pre}.${verb}.${name}.${id}`;
}
return `${pre}.${verb}.${name}`;
}
constructor(nc, config = { name: "", version: "" }) {
this.nc = nc;
this.config = Object.assign({}, config);
if (!this.config.queue) {
this.config.queue = "q";
}
// this will throw if no name
(0, jsutil_1.validateName)("name", this.config.name);
(0, jsutil_1.validateName)("queue", this.config.queue);
// this will throw if not semver
(0, semver_1.parseSemVer)(this.config.version);
this._id = nuid_1.nuid.next();
this.internal = [];
this._done = (0, util_1.deferred)();
this._stopped = false;
this.handlers = [];
this.started = new Date().toISOString();
// initialize the stats
this.reset();
// close if the connection closes
this.nc.closed()
.then(() => {
this.close().catch();
})
.catch((err) => {
this.close(err).catch();
});
}
get subjects() {
return this.handlers.filter((s) => {
return s.internal === false;
}).map((s) => {
return s.subject;
});
}
get id() {
return this._id;
}
get name() {
return this.config.name;
}
get description() {
var _a;
return (_a = this.config.description) !== null && _a !== void 0 ? _a : "";
}
get version() {
return this.config.version;
}
get metadata() {
return this.config.metadata;
}
errorToHeader(err) {
const h = (0, headers_1.headers)();
if (err instanceof core_1.ServiceError) {
const se = err;
h.set(core_1.ServiceErrorHeader, se.message);
h.set(core_1.ServiceErrorCodeHeader, `${se.code}`);
}
else {
h.set(core_1.ServiceErrorHeader, err.message);
h.set(core_1.ServiceErrorCodeHeader, "500");
}
return h;
}
setupHandler(h, internal = false) {
// internals don't use a queue
const queue = internal ? "" : (h.queue ? h.queue : this.config.queue);
const { name, subject, handler } = h;
const sv = h;
sv.internal = internal;
if (internal) {
this.internal.push(sv);
}
sv.stats = new NamedEndpointStatsImpl(name, subject, queue);
sv.queue = queue;
const callback = handler
? (err, msg) => {
if (err) {
this.close(err);
return;
}
const start = Date.now();
try {
handler(err, new ServiceMsgImpl(msg));
}
catch (err) {
sv.stats.countError(err);
msg === null || msg === void 0 ? void 0 : msg.respond(encoders_1.Empty, { headers: this.errorToHeader(err) });
}
finally {
sv.stats.countLatency(start);
}
}
: undefined;
sv.sub = this.nc.subscribe(subject, {
callback,
queue,
});
sv.sub.closed
.then(() => {
if (!this._stopped) {
this.close(new Error(`required subscription ${h.subject} stopped`))
.catch();
}
})
.catch((err) => {
if (!this._stopped) {
const ne = new Error(`required subscription ${h.subject} errored: ${err.message}`);
ne.stack = err.stack;
this.close(ne).catch();
}
});
return sv;
}
info() {
return {
type: core_1.ServiceResponseType.INFO,
name: this.name,
id: this.id,
version: this.version,
description: this.description,
metadata: this.metadata,
endpoints: this.endpoints(),
};
}
endpoints() {
return this.handlers.map((v) => {
const { subject, metadata, name, queue } = v;
return { subject, metadata, name, queue_group: queue };
});
}
stats() {
return __awaiter(this, void 0, void 0, function* () {
const endpoints = [];
for (const h of this.handlers) {
if (typeof this.config.statsHandler === "function") {
try {
h.stats.data = yield this.config.statsHandler(h);
}
catch (err) {
h.stats.countError(err);
}
}
endpoints.push(h.stats.stats(h.qi));
}
return {
type: core_1.ServiceResponseType.STATS,
name: this.name,
id: this.id,
version: this.version,
started: this.started,
metadata: this.metadata,
endpoints,
};
});
}
addInternalHandler(verb, handler) {
const v = `${verb}`.toUpperCase();
this._doAddInternalHandler(`${v}-all`, verb, handler);
this._doAddInternalHandler(`${v}-kind`, verb, handler, this.name);
this._doAddInternalHandler(`${v}`, verb, handler, this.name, this.id);
}
_doAddInternalHandler(name, verb, handler, kind = "", id = "") {
const endpoint = {};
endpoint.name = name;
endpoint.subject = ServiceImpl.controlSubject(verb, kind, id);
endpoint.handler = handler;
this.setupHandler(endpoint, true);
}
start() {
const jc = (0, codec_1.JSONCodec)();
const statsHandler = (err, msg) => {
if (err) {
this.close(err);
return Promise.reject(err);
}
return this.stats().then((s) => {
msg === null || msg === void 0 ? void 0 : msg.respond(jc.encode(s));
return Promise.resolve();
});
};
const infoHandler = (err, msg) => {
if (err) {
this.close(err);
return Promise.reject(err);
}
msg === null || msg === void 0 ? void 0 : msg.respond(jc.encode(this.info()));
return Promise.resolve();
};
const ping = jc.encode(this.ping());
const pingHandler = (err, msg) => {
if (err) {
this.close(err).then().catch();
return Promise.reject(err);
}
msg.respond(ping);
return Promise.resolve();
};
this.addInternalHandler(core_1.ServiceVerb.PING, pingHandler);
this.addInternalHandler(core_1.ServiceVerb.STATS, statsHandler);
this.addInternalHandler(core_1.ServiceVerb.INFO, infoHandler);
// now the actual service
this.handlers.forEach((h) => {
const { subject } = h;
if (typeof subject !== "string") {
return;
}
// this is expected in cases where main subject is just
// a root subject for multiple endpoints - user can disable
// listening to the root endpoint, by specifying null
if (h.handler === null) {
return;
}
this.setupHandler(h);
});
return Promise.resolve(this);
}
close(err) {
if (this._stopped) {
return this._done;
}
this._stopped = true;
let buf = [];
if (!this.nc.isClosed()) {
buf = this.handlers.concat(this.internal).map((h) => {
return h.sub.drain();
});
}
Promise.allSettled(buf)
.then(() => {
this._done.resolve(err ? err : null);
});
return this._done;
}
get stopped() {
return this._done;
}
get isStopped() {
return this._stopped;
}
stop(err) {
return this.close(err);
}
ping() {
return {
type: core_1.ServiceResponseType.PING,
name: this.name,
id: this.id,
version: this.version,
metadata: this.metadata,
};
}
reset() {
// pretend we restarted
this.started = new Date().toISOString();
if (this.handlers) {
for (const h of this.handlers) {
h.stats.reset(h.qi);
}
}
}
addGroup(name, queue) {
return new ServiceGroupImpl(this, name, queue);
}
addEndpoint(name, handler) {
const sg = new ServiceGroupImpl(this);
return sg.addEndpoint(name, handler);
}
_addEndpoint(e) {
const qi = new queued_iterator_1.QueuedIteratorImpl();
qi.noIterator = typeof e.handler === "function";
if (!qi.noIterator) {
e.handler = (err, msg) => {
err ? this.stop(err).catch() : qi.push(new ServiceMsgImpl(msg));
};
// close the service if the iterator closes
qi.iterClosed.then(() => {
this.close().catch();
});
}
// track the iterator for stats
const ss = this.setupHandler(e, false);
ss.qi = qi;
this.handlers.push(ss);
return qi;
}
}
exports.ServiceImpl = ServiceImpl;
class NamedEndpointStatsImpl {
constructor(name, subject, queue = "") {
this.name = name;
this.subject = subject;
this.average_processing_time = 0;
this.num_errors = 0;
this.num_requests = 0;
this.processing_time = 0;
this.queue = queue;
}
reset(qi) {
this.num_requests = 0;
this.processing_time = 0;
this.average_processing_time = 0;
this.num_errors = 0;
this.last_error = undefined;
this.data = undefined;
const qii = qi;
if (qii) {
qii.time = 0;
qii.processed = 0;
}
}
countLatency(start) {
this.num_requests++;
this.processing_time += (0, util_1.nanos)(Date.now() - start);
this.average_processing_time = Math.round(this.processing_time / this.num_requests);
}
countError(err) {
this.num_errors++;
this.last_error = err.message;
}
_stats() {
const { name, subject, average_processing_time, num_errors, num_requests, processing_time, last_error, data, queue, } = this;
return {
name,
subject,
average_processing_time,
num_errors,
num_requests,
processing_time,
last_error,
data,
queue_group: queue,
};
}
stats(qi) {
const qii = qi;
if ((qii === null || qii === void 0 ? void 0 : qii.noIterator) === false) {
// grab stats in the iterator
this.processing_time = (0, util_1.nanos)(qii.time);
this.num_requests = qii.processed;
this.average_processing_time =
this.processing_time > 0 && this.num_requests > 0
? this.processing_time / this.num_requests
: 0;
}
return this._stats();
}
}
//# sourceMappingURL=service.js.map