UNPKG

@nestjs/microservices

Version:

Nest - modern, fast, powerful node.js web framework (@microservices)

287 lines (286 loc) • 11.9 kB
import { Logger } from '@nestjs/common'; import { isObject, loadPackageSync } from '@nestjs/common/internal'; import { EventEmitter } from 'events'; import { createRequire } from 'module'; import { NATS_DEFAULT_URL } from '../constants.js'; import { NatsResponseJSONDeserializer } from '../deserializers/nats-response-json.deserializer.js'; import { EmptyResponseException } from '../errors/empty-response.exception.js'; import { NatsRecordSerializer } from '../serializers/nats-record.serializer.js'; import { ClientProxy } from './client-proxy.js'; let natsPackage = {}; /** * @publicApi */ export class ClientNats extends ClientProxy { options; logger = new Logger(ClientNats.name); natsClient = null; connectionPromise = null; statusEventEmitter = new EventEmitter(); constructor(options) { super(); this.options = options; natsPackage = loadPackageSync('@nats-io/transport-node', ClientNats.name, () => createRequire(import.meta.url)('@nats-io/transport-node')); this.initializeSerializer(options); this.initializeDeserializer(options); } async close() { this.handleClose(); try { await this.natsClient?.close(); } finally { this.statusEventEmitter.removeAllListeners(); this.natsClient = null; this.connectionPromise = null; } } handleClose() { if (this.routingMap.size > 0) { const err = new Error('Connection closed'); const callbacks = [...this.routingMap.values()]; this.routingMap.clear(); for (const callback of callbacks) { try { callback({ err }); } catch (callbackErr) { // A failing callback must not keep the remaining requests pending // nor prevent the connection from being closed. this.logger.error(callbackErr); } } } } async connect() { if (this.connectionPromise) { return this.connectionPromise; } const connectionPromise = this.createClient(); this.connectionPromise = connectionPromise; const natsClient = await connectionPromise.catch(err => { // A rejected attempt must not be cached, but a newer attempt may have // replaced it in the meantime (close() followed by connect() while this // attempt was still pending): only reset the shared state when the // failing attempt is still the current one, as ClientRedis, ClientRMQ // and ClientKafka already do. if (this.connectionPromise === connectionPromise) { this.connectionPromise = null; } throw err; }); if (this.connectionPromise !== connectionPromise) { // The client was closed (and possibly reconnected) while this attempt // was still pending: the connection it opened is no longer tracked by // the client, so it must not replace the current one nor stay open. await natsClient.close().catch(() => { }); if (this.connectionPromise) { return this.connectionPromise; } throw new Error('Connection closed'); } this.natsClient = natsClient; this._status$.next("connected" /* NatsStatus.CONNECTED */); this.handleStatusUpdates(this.natsClient).catch(err => this.logger.error(err)); return this.natsClient; } async createClient() { // Eagerly initialize serializer/deserializer so they can be used synchronously if (this.serializer && typeof this.serializer.init === 'function') { await this.serializer.init(); } if (this.deserializer && typeof this.deserializer.init === 'function') { await this.deserializer.init(); } const options = this.options || {}; return natsPackage.connect({ servers: NATS_DEFAULT_URL, ...options, }); } async handleStatusUpdates(client) { for await (const status of client.status()) { if (this.natsClient !== client) { return; } try { switch (status.type) { case 'error': this.logger.error(`NatsError: type: "${status.type}", error: "${status.error}".`); break; case 'disconnect': this.connectionPromise = Promise.reject('Error: Connection lost. Trying to reconnect...'); // Prevent unhandled promise rejection this.connectionPromise.catch(() => { }); this.logger.error(`NatsError: type: "${status.type}".`); this._status$.next("disconnected" /* NatsStatus.DISCONNECTED */); this.statusEventEmitter.emit("disconnect" /* NatsEventsMap.DISCONNECT */, status.server); break; case 'reconnecting': this._status$.next("reconnecting" /* NatsStatus.RECONNECTING */); break; case 'reconnect': this.connectionPromise = Promise.resolve(client); this.logger.log(`NatsStatus: type: "${status.type}".`); this._status$.next("connected" /* NatsStatus.CONNECTED */); this.statusEventEmitter.emit("reconnect" /* NatsEventsMap.RECONNECT */, status.server); break; case 'ping': if (this.options.debug) { this.logger.debug(`NatsStatus: type: "${status.type}", pending pings: "${status.pendingPings}".`); } break; case 'update': this.logger.log(`NatsStatus: type: "${status.type}", added: "${status.added}", deleted: "${status.deleted}".`); this.statusEventEmitter.emit("update" /* NatsEventsMap.UPDATE */, { added: status.added, deleted: status.deleted, }); break; default: { const data = 'data' in status && isObject(status.data) ? JSON.stringify(status.data) : 'data' in status ? status.data : ''; this.logger.log(`NatsStatus: type: "${status.type}", data: "${data}".`); break; } } } catch (err) { // A throwing listener must neither end the status updates nor // escape as an unhandled rejection this.logger.error(err); } } // The status iterator only completes once the client has given up: it was // closed, or it exhausted `maxReconnectAttempts` and is not coming back. // The promise the "disconnect" case cached is never reset in that case, so // every later `connect()` call would replay the same rejection. Drop the // client and the cached promise so the next call starts over. if (this.natsClient === client) { this.natsClient = null; this.connectionPromise = null; // nats never calls back the reply subscriptions it closes. Fail them // after the reset, so that a request retried from its callback reconnects. this.handleClose(); } } on(event, callback) { this.statusEventEmitter.on(event, callback); } unwrap() { if (!this.natsClient) { throw new Error('Not initialized. Please call the "connect" method first.'); } return this.natsClient; } createSubscriptionHandler(packet, callback) { return async (error, natsMsg) => { if (error) { return callback({ err: error, }); } const rawPacket = natsMsg.data; if (rawPacket?.length === 0) { return callback({ err: new EmptyResponseException(this.normalizePattern(packet.pattern)), isDisposed: true, }); } // nats-core does not await this callback: a rejection here would crash the process. let message; try { message = await this.deserializer.deserialize(natsMsg); } catch (err) { return callback({ err, isDisposed: true }); } if (message.id && message.id !== packet.id) { return undefined; } const { err, response, isDisposed } = message; if (isDisposed || err) { return callback({ err, response, isDisposed: true, }); } callback({ err, response, }); }; } publish(partialPacket, callback) { const packet = this.assignPacketId(partialPacket); this.routingMap.set(packet.id, callback); let cleanup = () => { this.routingMap.delete(packet.id); }; try { const channel = this.normalizePattern(partialPacket.pattern); const serializedPacket = this.serializer.serialize(packet); const inbox = natsPackage.createInbox(this.options.inboxPrefix); const subscriptionHandler = this.createSubscriptionHandler(packet, callback); const subscription = this.natsClient.subscribe(inbox, { callback: subscriptionHandler, }); cleanup = () => { this.routingMap.delete(packet.id); subscription.unsubscribe(); }; const headers = this.mergeHeaders(serializedPacket.headers); this.natsClient.publish(channel, serializedPacket.data, { reply: inbox, headers, }); return cleanup; } catch (err) { cleanup(); callback({ err }); return () => { }; } } async dispatchEvent(packet) { const pattern = this.normalizePattern(packet.pattern); const serializedPacket = await this.serializer.serialize(packet); const headers = this.mergeHeaders(serializedPacket.headers); return new Promise((resolve, reject) => { try { this.natsClient.publish(pattern, serializedPacket.data, { headers, }); resolve(); } catch (err) { reject(err); } }); } initializeSerializer(options) { this.serializer = options?.serializer ?? new NatsRecordSerializer(); } initializeDeserializer(options) { this.deserializer = options?.deserializer ?? new NatsResponseJSONDeserializer(); } mergeHeaders(requestHeaders) { if (!requestHeaders && !this.options?.headers) { return undefined; } const headers = requestHeaders ?? natsPackage.headers(); for (const [key, value] of Object.entries(this.options?.headers || {})) { if (!headers.has(key)) { headers.set(key, value); } } return headers; } }