UNPKG

nats

Version:

Node.js client for NATS, a lightweight, high-performance cloud native messaging system

411 lines 14.8 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()); }); }; 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