nats
Version:
Node.js client for NATS, a lightweight, high-performance cloud native messaging system
411 lines • 14.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.ServiceError = exports.ServiceMsgImpl = exports.ServiceVerb = exports.ServiceErrorCodeHeader = exports.ServiceErrorHeader = exports.ServiceApiPrefix = void 0;
/*
* Copyright 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 util_1 = require("./util");
const types_1 = require("./types");
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("./jsutil");
const semver_1 = require("./semver");
/**
* Services have common backplane subject pattern:
*
* `$SRV.PING|STATS|INFO|SCHEMA` - pings or retrieves status for all services
* `$SRV.PING|STATS|INFO|SCHEMA.<name>` - pings or retrieves status for all services having the specified name
* `$SRV.PING|STATS|INFO|SCHEMA.<name>.<id>` - pings or retrieves status of a particular service
*
* Note that <name> and <id> are upper-cased.
*/
exports.ServiceApiPrefix = "$SRV";
exports.ServiceErrorHeader = "Nats-Service-Error";
exports.ServiceErrorCodeHeader = "Nats-Service-Error-Code";
var ServiceVerb;
(function (ServiceVerb) {
ServiceVerb["PING"] = "PING";
ServiceVerb["STATS"] = "STATS";
ServiceVerb["INFO"] = "INFO";
ServiceVerb["SCHEMA"] = "SCHEMA";
})(ServiceVerb = exports.ServiceVerb || (exports.ServiceVerb = {}));
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;
}
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(exports.ServiceErrorCodeHeader, `${code}`);
(_b = opts.headers) === null || _b === void 0 ? void 0 : _b.set(exports.ServiceErrorHeader, description);
return this.msg.respond(data, opts);
}
}
exports.ServiceMsgImpl = ServiceMsgImpl;
// FIXME: perhaps the client is presented with a Request = Msg, but adds a respondError(code, description)
class ServiceError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
static isServiceError(msg) {
return ServiceError.toServiceError(msg) !== null;
}
static toServiceError(msg) {
var _a, _b;
const scode = ((_a = msg === null || msg === void 0 ? void 0 : msg.headers) === null || _a === void 0 ? void 0 : _a.get(exports.ServiceErrorCodeHeader)) || "";
if (scode !== "") {
const code = parseInt(scode) || 400;
const description = ((_b = msg === null || msg === void 0 ? void 0 : msg.headers) === null || _b === void 0 ? void 0 : _b.get(exports.ServiceErrorHeader)) || "";
return new ServiceError(code, description.length ? description : scode);
}
return null;
}
}
exports.ServiceError = ServiceError;
class ServiceImpl extends queued_iterator_1.QueuedIteratorImpl {
/**
* @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}`;
}
name = name.toUpperCase();
id = id.toUpperCase();
return id !== ""
? `${pre}.${verb}.${name}.${id}`
: `${pre}.${verb}.${name}`;
}
constructor(nc, config) {
var _a, _b;
super();
this.nc = nc;
config.name = ((_a = config === null || config === void 0 ? void 0 : config.name) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || "";
this.config = config;
const n = (0, jsutil_1.validName)(this.name);
if (n !== "") {
throw new Error(`name ${n}`);
}
// this will throw if not semver
(0, semver_1.parseSemVer)(this.config.version);
this.noIterator = typeof ((_b = config === null || config === void 0 ? void 0 : config.endpoint) === null || _b === void 0 ? void 0 : _b.handler) === "function";
if (!this.noIterator) {
this.config.endpoint.handler = (err, msg) => {
err ? this.stop(err).catch() : this.push(new ServiceMsgImpl(msg));
};
}
// initialize the stats
this.reset();
this._id = nuid_1.nuid.next();
this.handler = config.endpoint;
this.internal = [];
this._done = (0, util_1.deferred)();
this._stopped = false;
// close if the connection closes
this.nc.closed()
.then(() => {
this.close().catch();
})
.catch((err) => {
this.close(err).catch();
});
// close the service if the iterator closes
if (!this.noIterator) {
this.iterClosed.then(() => {
this.close().catch();
});
}
}
get subject() {
const { subject } = this.config.endpoint;
if (subject !== "") {
return subject;
}
return "";
}
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;
}
errorToHeader(err) {
const h = (0, headers_1.headers)();
if (err instanceof ServiceError) {
const se = err;
h.set(exports.ServiceErrorHeader, se.message);
h.set(exports.ServiceErrorCodeHeader, `${se.code}`);
}
else {
h.set(exports.ServiceErrorHeader, err.message);
h.set(exports.ServiceErrorCodeHeader, "500");
}
return h;
}
countError(err) {
this._lastException = err;
this._stats.num_errors++;
this._stats.last_error = err.message;
}
setupNATS(h, internal = false) {
// internals don't use a queue
const queue = internal ? "" : "q";
const { subject, handler } = h;
const sv = h;
sv.internal = internal;
if (internal) {
this.internal.push(sv);
}
const countLatency = (start) => {
if (internal)
return;
this._stats.num_requests++;
this._stats.processing_time = (0, jsutil_1.nanos)(Date.now() - start);
this._stats.average_processing_time = Math.round(this._stats.processing_time / this._stats.num_requests);
};
const callback = handler
? (err, msg) => {
if (err) {
this.close(err);
return;
}
const start = Date.now();
try {
handler(err, new ServiceMsgImpl(msg));
}
catch (err) {
this.countError(err);
msg === null || msg === void 0 ? void 0 : msg.respond(types_1.Empty, { headers: this.errorToHeader(err) });
}
finally {
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();
}
});
}
info() {
return {
name: this.name,
id: this.id,
version: this.version,
description: this.description,
subject: this.config.endpoint.subject,
};
}
stats() {
return __awaiter(this, void 0, void 0, function* () {
if (typeof this.config.statsHandler === "function") {
try {
this._stats.data = yield this.config.statsHandler(this.handler);
}
catch (err) {
this.countError(err);
}
}
if (!this.noIterator) {
this._stats.processing_time = this.time;
this._stats.num_requests = this.processed;
this._stats.average_processing_time = this.time > 0 && this.processed > 0
? this.time / this.processed
: 0;
}
return Object.assign({
name: this.name,
id: this.id,
version: this.version,
}, this._stats);
});
}
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.setupNATS(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({
name: this.name,
id: this.id,
version: this.version,
});
const pingHandler = (err, msg) => {
if (err) {
this.close(err).then().catch();
return Promise.reject(err);
}
msg.respond(ping);
return Promise.resolve();
};
const schemaHandler = (err, msg) => {
if (err) {
this.close(err);
return Promise.reject(err);
}
msg === null || msg === void 0 ? void 0 : msg.respond(this.schema);
return Promise.resolve();
};
this.addInternalHandler(ServiceVerb.PING, pingHandler);
this.addInternalHandler(ServiceVerb.STATS, statsHandler);
this.addInternalHandler(ServiceVerb.INFO, infoHandler);
this.addInternalHandler(ServiceVerb.SCHEMA, schemaHandler);
// now the actual service
const handlers = [this.handler];
handlers.forEach((h) => {
const { subject } = h;
if (typeof subject !== "string") {
return;
}
this.setupNATS(h);
});
return Promise.resolve(this);
}
close(err) {
if (this._stopped) {
return this._done;
}
this._stopped = true;
const buf = [];
if (!this.nc.isClosed()) {
buf.push(this.handler.sub.drain());
this.internal.forEach((serviceSub) => {
buf.push(serviceSub.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);
}
get schema() {
const jc = (0, codec_1.JSONCodec)();
if (!this._schema) {
this._schema = jc.encode({
name: this.name,
id: this.id,
version: this.version,
schema: this.config.schema,
});
}
return this._schema;
}
reset() {
this._lastException = undefined;
this._stats = {
name: this.name,
num_requests: 0,
num_errors: 0,
processing_time: 0,
average_processing_time: 0,
started: new Date().toISOString(),
};
if (!this.noIterator) {
this.processed = 0;
this.time = 0;
}
}
}
exports.ServiceImpl = ServiceImpl;
//# sourceMappingURL=service.js.map