UNPKG

@nestjs/microservices

Version:

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

263 lines (262 loc) • 9.7 kB
import { connectable, EMPTY, from as fromPromise, isObservable, of, ReplaySubject, Subject, } from 'rxjs'; import { catchError, distinctUntilChanged, finalize, mergeMap, } from 'rxjs/operators'; import { NO_EVENT_HANDLER, UNSERIALIZABLE_PATTERN } from '../constants.js'; import { IncomingRequestDeserializer } from '../deserializers/incoming-request.deserializer.js'; import { IdentitySerializer } from '../serializers/identity.serializer.js'; import { transformPatternToRoute } from '../utils/index.js'; import { Logger } from '@nestjs/common'; import { isString, loadPackage, loadPackageSync, } from '@nestjs/common/internal'; /** * @publicApi */ export class Server { /** * Unique transport identifier. */ transportId; messageHandlers = new Map(); logger = new Logger(Server.name); /** * Whether this transport hands an event handler failure to its client * library, which then reports it. The base `handleEvent` connects the * stream without subscribing, so nothing observes the failure and Nest logs * it instead. `ServerKafka` awaits the stream and sets this to `true`. */ propagatesEventHandlerErrors = false; serializer; deserializer; onProcessingStartHook = (transportId, context, done) => done(); onProcessingEndHook; _status$ = new ReplaySubject(1); /** * Returns an observable that emits status changes. */ get status() { return this._status$.asObservable().pipe(distinctUntilChanged()); } /** * Sets the transport identifier. * @param transportId Unique transport identifier. */ setTransportId(transportId) { this.transportId = transportId; } /** * Sets a hook that will be called when processing starts. */ setOnProcessingStartHook(hook) { this.onProcessingStartHook = hook; } /** * Sets a hook that will be called when processing ends. */ setOnProcessingEndHook(hook) { this.onProcessingEndHook = hook; } addHandler(pattern, callback, isEventHandler = false, extras = {}) { const normalizedPattern = this.normalizePattern(pattern); callback.isEventHandler = isEventHandler; callback.extras = extras; if (this.messageHandlers.has(normalizedPattern) && isEventHandler) { const headRef = this.messageHandlers.get(normalizedPattern); const getTail = (handler) => handler?.next ? getTail(handler.next) : handler; const tailRef = getTail(headRef); tailRef.next = callback; } else { this.messageHandlers.set(normalizedPattern, callback); } } getHandlers() { return this.messageHandlers; } getHandlerByPattern(pattern) { const route = this.getRouteFromPattern(pattern); return this.messageHandlers.has(route) ? this.messageHandlers.get(route) : null; } send(stream$, respond) { const dataQueue = []; let isProcessing = false; const scheduleOnNextTick = (data) => { if (data.isDisposed && dataQueue.length > 0) { dataQueue[dataQueue.length - 1].isDisposed = true; } else { dataQueue.push(data); } if (!isProcessing) { isProcessing = true; process.nextTick(async () => { while (dataQueue.length > 0) { const packet = dataQueue.shift(); if (!packet) { continue; } try { await respond(packet); } catch (err) { // A reply that cannot be published must neither surface as an // unhandled rejection nor stop the replies queued behind it. this.logger.error(err); } } isProcessing = false; }); } }; return stream$ .pipe(catchError((err) => { scheduleOnNextTick({ err }); return EMPTY; }), finalize(() => scheduleOnNextTick({ isDisposed: true }))) .subscribe((response) => scheduleOnNextTick({ response })); } async handleEvent(pattern, packet, context) { const handler = this.getHandlerByPattern(pattern); if (!handler) { return this.logger.error(NO_EVENT_HANDLER `${pattern}`); } return this.runWithProcessingHooks(context, async (runEndHook) => { const resultOrStream = await handler(packet.data, context); if (isObservable(resultOrStream)) { const connectableSource = connectable(resultOrStream.pipe(finalize(runEndHook)), { connector: () => new Subject(), resetOnDisconnect: false, }); connectableSource.connect(); } else { runEndHook(); } }); } /** * Handles a request-response message. `produce` returns the response * stream, whose values are replied through `respond`, and the processing * end hook runs once that stream settles (or once `produce` rejects). */ handleRequest(context, produce, respond) { return this.runWithProcessingHooks(context, async (runEndHook) => { const response$ = await produce(); this.send(response$.pipe(finalize(runEndHook)), respond); }); } /** * Runs `fn` between the processing start and end hooks. * * `fn` receives a runner that closes the span exactly once and attaches it * to the teardown of whatever stream it produces. If `fn` rejects before * that teardown can run, the span is closed here and the rejection travels * on to the transport's client library. * * `endHookContext` is for transports that report a different context to * the end hook than to the start hook (gRPC passes the request). */ runWithProcessingHooks(context, fn, endHookContext = context) { return this.onProcessingStartHook(this.transportId, context, async () => { const runEndHook = this.createProcessingEndHookRunner(endHookContext); try { await fn(runEndHook); } catch (err) { runEndHook(); throw err; } }); } /** * Returns a function that runs the processing end hook exactly once. * * An event handler can fail either by rejecting or by returning a stream * that errors, and `ServerKafka#handleEvent` awaits that stream, so both the * `finalize` teardown and the `catch` block can be reached for a single * event. The hook closes a span, so it must not run twice. */ createProcessingEndHookRunner(context) { let isEndHookCalled = false; return () => { if (isEndHookCalled) { return; } isEndHookCalled = true; this.onProcessingEndHook?.(this.transportId, context); }; } transformToObservable(resultOrDeferred) { if (resultOrDeferred instanceof Promise) { return fromPromise(resultOrDeferred).pipe(mergeMap(val => (isObservable(val) ? val : of(val)))); } if (isObservable(resultOrDeferred)) { return resultOrDeferred; } return of(resultOrDeferred); } getOptionsProp(obj, prop, defaultValue = undefined) { return obj && prop in obj ? obj[prop] : defaultValue; } handleError(error) { this.logger.error(error); } loadPackage(name, ctx, loader) { return loadPackage(name, ctx, loader); } loadPackageSynchronously(name, ctx, loader) { return loadPackageSync(name, ctx, loader); } initializeSerializer(options) { this.serializer = (options && options.serializer) || new IdentitySerializer(); } initializeDeserializer(options) { this.deserializer = (options && options.deserializer) || new IncomingRequestDeserializer(); } /** * Transforms the server Pattern to valid type and returns a route for him. * * @param {string} pattern - server pattern * @returns string */ getRouteFromPattern(pattern) { let validPattern; try { validPattern = JSON.parse(pattern); } catch (error) { // Uses a fundamental object (`pattern` variable without any conversion) validPattern = pattern; } return this.normalizePattern(validPattern); } normalizePattern(pattern) { return transformPatternToRoute(pattern); } /** * Returns the string representation of an incoming message pattern. * * Patterns are client-controlled: serializing a deeply nested one makes * `JSON.stringify` throw a `RangeError`, which must not escape the message * handler (as an unhandled promise rejection that terminates the process). * * @param {unknown} pattern - client pattern * @returns string */ getPatternAsString(pattern) { if (isString(pattern)) { return pattern; } try { return JSON.stringify(pattern); } catch { return UNSERIALIZABLE_PATTERN; } } }