@horizon-republic/nestjs-jetstream 2.9.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,20 +13,22 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
13
13
  // src/jetstream.module.ts
14
14
  import {
15
15
  Global,
16
- Inject,
17
- Logger as Logger14,
18
- Module,
19
- Optional
16
+ Inject as Inject2,
17
+ Logger as Logger20,
18
+ Module as Module2,
19
+ Optional as Optional2
20
20
  } from "@nestjs/common";
21
21
 
22
22
  // src/client/jetstream.client.ts
23
- import { Logger } from "@nestjs/common";
23
+ import { Logger as Logger5 } from "@nestjs/common";
24
24
  import { ClientProxy } from "@nestjs/microservices";
25
+ import { context as context6 } from "@opentelemetry/api";
26
+ import { nuid } from "@nats-io/nuid";
25
27
  import {
26
28
  createInbox,
27
- headers as natsHeaders
29
+ headers as natsHeaders,
30
+ TimeoutError
28
31
  } from "@nats-io/transport-node";
29
- import { nuid } from "@nats-io/nuid";
30
32
 
31
33
  // src/interfaces/hooks.interface.ts
32
34
  var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
@@ -44,6 +46,10 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
44
46
  TransportEvent2["ShutdownStart"] = "shutdownStart";
45
47
  TransportEvent2["ShutdownComplete"] = "shutdownComplete";
46
48
  TransportEvent2["DeadLetter"] = "deadLetter";
49
+ TransportEvent2["ConsumerRecovered"] = "consumerRecovered";
50
+ TransportEvent2["HandlerCompleted"] = "handlerCompleted";
51
+ TransportEvent2["Published"] = "published";
52
+ TransportEvent2["RpcCompleted"] = "rpcCompleted";
47
53
  return TransportEvent2;
48
54
  })(TransportEvent || {});
49
55
 
@@ -224,6 +230,920 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
224
230
  var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
225
231
  var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
226
232
 
233
+ // src/otel/constants.ts
234
+ var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
235
+
236
+ // src/otel/attribute-keys.ts
237
+ var ATTR_MESSAGING_SYSTEM = "messaging.system";
238
+ var ATTR_MESSAGING_DESTINATION_NAME = "messaging.destination.name";
239
+ var ATTR_MESSAGING_DESTINATION_TEMPLATE = "messaging.destination.template";
240
+ var ATTR_MESSAGING_CLIENT_ID = "messaging.client.id";
241
+ var ATTR_MESSAGING_OPERATION_NAME = "messaging.operation.name";
242
+ var ATTR_MESSAGING_OPERATION_TYPE = "messaging.operation.type";
243
+ var ATTR_MESSAGING_MESSAGE_BODY_SIZE = "messaging.message.body.size";
244
+ var ATTR_MESSAGING_MESSAGE_ID = "messaging.message.id";
245
+ var ATTR_MESSAGING_MESSAGE_CONVERSATION_ID = "messaging.message.conversation_id";
246
+ var ATTR_MESSAGING_CONSUMER_GROUP_NAME = "messaging.consumer.group.name";
247
+ var ATTR_MESSAGING_HEADER_PREFIX = "messaging.header.";
248
+ var ATTR_MESSAGING_NATS_STREAM_NAME = "messaging.nats.stream.name";
249
+ var ATTR_MESSAGING_NATS_STREAM_SEQUENCE = "messaging.nats.message.stream_sequence";
250
+ var ATTR_MESSAGING_NATS_CONSUMER_SEQUENCE = "messaging.nats.message.consumer_sequence";
251
+ var ATTR_MESSAGING_NATS_DELIVERY_COUNT = "messaging.nats.message.delivery_count";
252
+ var ATTR_MESSAGING_NATS_BODY = "messaging.nats.message.body";
253
+ var ATTR_MESSAGING_NATS_BODY_TRUNCATED = "messaging.nats.message.body.truncated";
254
+ var ATTR_SERVER_ADDRESS = "server.address";
255
+ var ATTR_SERVER_PORT = "server.port";
256
+ var ATTR_JETSTREAM_SERVICE_NAME = "jetstream.service.name";
257
+ var ATTR_JETSTREAM_KIND = "jetstream.kind";
258
+ var ATTR_JETSTREAM_RPC_REPLY_HAS_ERROR = "jetstream.rpc.reply.has_error";
259
+ var ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE = "jetstream.rpc.reply.error.code";
260
+ var ATTR_JETSTREAM_PROVISIONING_ENTITY = "jetstream.provisioning.entity";
261
+ var ATTR_JETSTREAM_PROVISIONING_ACTION = "jetstream.provisioning.action";
262
+ var ATTR_JETSTREAM_PROVISIONING_NAME = "jetstream.provisioning.name";
263
+ var ATTR_JETSTREAM_SELF_HEALING_REASON = "jetstream.self_healing.reason";
264
+ var ATTR_JETSTREAM_MIGRATION_REASON = "jetstream.migration.reason";
265
+ var ATTR_JETSTREAM_DEAD_LETTER_REASON = "jetstream.dead_letter.reason";
266
+ var ATTR_JETSTREAM_SCHEDULE_TARGET = "jetstream.schedule.target";
267
+ var ATTR_NATS_CONNECTION_SERVER = "nats.connection.server";
268
+ var NATS_MSG_ID_HEADER = "Nats-Msg-Id";
269
+ var HOOK_PUBLISH = "publishHook";
270
+ var HOOK_CONSUME = "consumeHook";
271
+ var HOOK_RESPONSE = "responseHook";
272
+ var SPAN_NAME_PUBLISH = "publish";
273
+ var SPAN_NAME_PROCESS = "process";
274
+ var SPAN_NAME_SEND = "send";
275
+ var SPAN_NAME_DEAD_LETTER = "dead_letter";
276
+ var SPAN_NAME_NATS_CONNECTION = "nats.connection";
277
+ var SPAN_NAME_JETSTREAM_SHUTDOWN = "jetstream.shutdown";
278
+ var SPAN_NAME_JETSTREAM_SELF_HEALING = "jetstream.self_healing";
279
+ var SPAN_NAME_JETSTREAM_MIGRATION = "jetstream.migration";
280
+ var SPAN_NAME_JETSTREAM_PROVISIONING_PREFIX = "jetstream.provisioning.";
281
+ var EVENT_CONNECTION_DISCONNECTED = "connection.disconnected";
282
+ var EVENT_CONNECTION_RECONNECTED = "connection.reconnected";
283
+ var messagingHeaderAttr = (headerName) => `${ATTR_MESSAGING_HEADER_PREFIX}${headerName.toLowerCase()}`;
284
+
285
+ // src/otel/trace-kinds.ts
286
+ var JetstreamTrace = /* @__PURE__ */ ((JetstreamTrace2) => {
287
+ JetstreamTrace2["Publish"] = "publish";
288
+ JetstreamTrace2["Consume"] = "consume";
289
+ JetstreamTrace2["RpcClientSend"] = "rpc.client.send";
290
+ JetstreamTrace2["DeadLetter"] = "dead_letter";
291
+ JetstreamTrace2["ConnectionLifecycle"] = "connection.lifecycle";
292
+ JetstreamTrace2["SelfHealing"] = "self_healing";
293
+ JetstreamTrace2["Provisioning"] = "provisioning";
294
+ JetstreamTrace2["Migration"] = "migration";
295
+ JetstreamTrace2["Shutdown"] = "shutdown";
296
+ return JetstreamTrace2;
297
+ })(JetstreamTrace || {});
298
+ var DEFAULT_TRACES = [
299
+ "publish" /* Publish */,
300
+ "consume" /* Consume */,
301
+ "rpc.client.send" /* RpcClientSend */,
302
+ "dead_letter" /* DeadLetter */
303
+ ];
304
+
305
+ // src/otel/capture.ts
306
+ var NEGATION_PREFIX = "!";
307
+ var REGEX_SPECIAL_RE = /[.+?^${}()|[\]\\*]/gu;
308
+ var escapeForRegex = (input) => input.replace(REGEX_SPECIAL_RE, "\\$&");
309
+ var globToRegex = (glob) => {
310
+ const escaped = escapeForRegex(glob.toLowerCase()).replace(/\\\*/gu, ".*");
311
+ return new RegExp(`^${escaped}$`, "u");
312
+ };
313
+ var compileHeaderAllowlist = (allowlist) => {
314
+ if (allowlist === true) return () => true;
315
+ if (allowlist === false) return () => false;
316
+ if (allowlist.length === 0) return () => false;
317
+ const includes = [];
318
+ const excludes = [];
319
+ for (const pattern of allowlist) {
320
+ const isExclude = pattern.startsWith(NEGATION_PREFIX);
321
+ const body = isExclude ? pattern.slice(NEGATION_PREFIX.length) : pattern;
322
+ const regex = globToRegex(body);
323
+ if (isExclude) excludes.push(regex);
324
+ else includes.push(regex);
325
+ }
326
+ return (name) => {
327
+ const lower = name.toLowerCase();
328
+ if (!includes.some((re) => re.test(lower))) return false;
329
+ if (excludes.some((re) => re.test(lower))) return false;
330
+ return true;
331
+ };
332
+ };
333
+ var subjectMatcherCache = /* @__PURE__ */ new WeakMap();
334
+ var compileSubjectAllowlist = (allowlist) => {
335
+ if (!allowlist || allowlist.length === 0) return () => true;
336
+ const cached = subjectMatcherCache.get(allowlist);
337
+ if (cached) return cached;
338
+ const regexes = allowlist.map(globToRegex);
339
+ const matcher = (subject) => {
340
+ const lower = subject.toLowerCase();
341
+ return regexes.some((re) => re.test(lower));
342
+ };
343
+ subjectMatcherCache.set(allowlist, matcher);
344
+ return matcher;
345
+ };
346
+ var subjectMatchesAllowlist = (subject, allowlist) => compileSubjectAllowlist(allowlist)(subject);
347
+ var HEADER_DENYLIST = /* @__PURE__ */ new Set([
348
+ "traceparent",
349
+ "tracestate",
350
+ "baggage",
351
+ "sentry-trace",
352
+ "b3",
353
+ "x-b3-traceid",
354
+ "x-b3-spanid",
355
+ "x-b3-parentspanid",
356
+ "x-b3-sampled",
357
+ "x-b3-flags",
358
+ "uber-trace-id",
359
+ "x-correlation-id",
360
+ "x-reply-to",
361
+ "x-error",
362
+ "x-subject",
363
+ "x-caller-name"
364
+ ]);
365
+ var isNatsServerHeader = (lower) => lower.startsWith("nats-");
366
+ var captureMatchingHeaders = (headers2, matcher) => {
367
+ if (!headers2) return {};
368
+ const out = {};
369
+ for (const key of headers2.keys()) {
370
+ const lower = key.toLowerCase();
371
+ if (HEADER_DENYLIST.has(lower)) continue;
372
+ if (isNatsServerHeader(lower)) continue;
373
+ if (!matcher(key)) continue;
374
+ const value = headers2.get(key);
375
+ if (value === "") continue;
376
+ out[messagingHeaderAttr(lower)] = value;
377
+ }
378
+ return out;
379
+ };
380
+ var tryUtf8Decode = (bytes) => {
381
+ try {
382
+ return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
383
+ } catch {
384
+ return Buffer.from(bytes).toString("base64");
385
+ }
386
+ };
387
+ var captureBodyAttribute = (subject, payload, capture) => {
388
+ if (capture === false) return {};
389
+ if (payload.byteLength === 0) return {};
390
+ if (!subjectMatchesAllowlist(subject, capture.subjectAllowlist)) return {};
391
+ const { maxBytes } = capture;
392
+ const truncated = payload.byteLength > maxBytes;
393
+ const slice = truncated ? payload.subarray(0, maxBytes) : payload;
394
+ const decoded = tryUtf8Decode(slice);
395
+ const out = { [ATTR_MESSAGING_NATS_BODY]: decoded };
396
+ if (truncated) out[ATTR_MESSAGING_NATS_BODY_TRUNCATED] = true;
397
+ return out;
398
+ };
399
+
400
+ // src/otel/config.ts
401
+ var PublishKind = /* @__PURE__ */ ((PublishKind2) => {
402
+ PublishKind2["Event"] = "event";
403
+ PublishKind2["RpcRequest"] = "rpc.request";
404
+ PublishKind2["Broadcast"] = "broadcast";
405
+ PublishKind2["Ordered"] = "ordered";
406
+ return PublishKind2;
407
+ })(PublishKind || {});
408
+ var ConsumeKind = /* @__PURE__ */ ((ConsumeKind2) => {
409
+ ConsumeKind2["Event"] = "event";
410
+ ConsumeKind2["Rpc"] = "rpc";
411
+ ConsumeKind2["Broadcast"] = "broadcast";
412
+ ConsumeKind2["Ordered"] = "ordered";
413
+ return ConsumeKind2;
414
+ })(ConsumeKind || {});
415
+ var DEFAULT_CAPTURE_HEADERS = ["x-request-id"];
416
+ var DEFAULT_CAPTURE_BODY_MAX_BYTES = 4096;
417
+ var NESTJS_BARE_ERROR_MESSAGE = "Internal server error";
418
+ var isNestjsBareErrorSentinel = (obj) => obj.status === "error" && obj.message === NESTJS_BARE_ERROR_MESSAGE;
419
+ var defaultErrorClassifier = (err) => {
420
+ if (err === null || typeof err !== "object") return "unexpected";
421
+ if (!(err instanceof Error)) {
422
+ if (isNestjsBareErrorSentinel(err)) return "unexpected";
423
+ return "expected";
424
+ }
425
+ let proto = err;
426
+ while (proto) {
427
+ const name = proto.constructor?.name;
428
+ if (name === "RpcException" || name === "HttpException") return "expected";
429
+ proto = Object.getPrototypeOf(proto);
430
+ }
431
+ return "unexpected";
432
+ };
433
+ var expandTracesOption = (option) => {
434
+ if (option === void 0 || option === "default") return new Set(DEFAULT_TRACES);
435
+ if (option === "all") return new Set(Object.values(JetstreamTrace));
436
+ if (option === "none") return /* @__PURE__ */ new Set();
437
+ return new Set(option);
438
+ };
439
+ var compileHeaderMatcher = (option) => compileHeaderAllowlist(option ?? DEFAULT_CAPTURE_HEADERS);
440
+ var resolveCaptureBody = (option) => {
441
+ if (option === void 0 || option === false) return false;
442
+ if (option === true) return { maxBytes: DEFAULT_CAPTURE_BODY_MAX_BYTES };
443
+ return {
444
+ maxBytes: option.maxBytes ?? DEFAULT_CAPTURE_BODY_MAX_BYTES,
445
+ subjectAllowlist: option.subjectAllowlist
446
+ };
447
+ };
448
+ var resolveOtelOptions = (options = {}) => {
449
+ return {
450
+ enabled: options.enabled ?? true,
451
+ traces: expandTracesOption(options.traces),
452
+ captureHeaders: compileHeaderMatcher(options.captureHeaders),
453
+ captureBody: resolveCaptureBody(options.captureBody),
454
+ publishHook: options.publishHook,
455
+ consumeHook: options.consumeHook,
456
+ responseHook: options.responseHook,
457
+ shouldTracePublish: options.shouldTracePublish,
458
+ shouldTraceConsume: options.shouldTraceConsume,
459
+ errorClassifier: options.errorClassifier ?? defaultErrorClassifier
460
+ };
461
+ };
462
+
463
+ // src/otel/internal-utils.ts
464
+ import { Logger } from "@nestjs/common";
465
+ var logger = new Logger("Jetstream:Otel");
466
+ var safelyInvokeHook = (hookName, hook, ...args) => {
467
+ if (!hook) return;
468
+ const logHookFailure = (err) => {
469
+ const message = err instanceof Error ? err.message : String(err);
470
+ logger.debug(`OTel ${hookName} threw: ${message}`);
471
+ };
472
+ try {
473
+ const result = hook(...args);
474
+ if (result !== null && typeof result === "object" && "then" in result && typeof result.then === "function") {
475
+ result.then(void 0, logHookFailure);
476
+ }
477
+ } catch (err) {
478
+ logHookFailure(err);
479
+ }
480
+ };
481
+ var stripIpv6Brackets = (host) => host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
482
+ var parsePort = (portRaw) => {
483
+ if (!portRaw) return void 0;
484
+ const port = Number.parseInt(portRaw, 10);
485
+ return Number.isInteger(port) ? port : void 0;
486
+ };
487
+ var parseServerAddress = (servers) => {
488
+ const raw = servers[0];
489
+ if (!raw) return null;
490
+ if (raw.includes("://")) {
491
+ try {
492
+ const url = new URL(raw);
493
+ if (url.hostname.length === 0) return null;
494
+ const host2 = stripIpv6Brackets(url.hostname);
495
+ const port2 = parsePort(url.port || void 0);
496
+ return port2 === void 0 ? { host: host2 } : { host: host2, port: port2 };
497
+ } catch {
498
+ return null;
499
+ }
500
+ }
501
+ if (raw.startsWith("[")) {
502
+ const closeIdx = raw.indexOf("]");
503
+ if (closeIdx <= 0) return null;
504
+ const host2 = raw.slice(1, closeIdx);
505
+ const port2 = parsePort(raw.slice(closeIdx + 1).replace(/^:/u, ""));
506
+ return port2 === void 0 ? { host: host2 } : { host: host2, port: port2 };
507
+ }
508
+ const [host, portRaw] = raw.split(":");
509
+ if (!host) return null;
510
+ const port = parsePort(portRaw);
511
+ return port === void 0 ? { host } : { host, port };
512
+ };
513
+ var deriveOtelAttrs = (options) => ({
514
+ otel: resolveOtelOptions(options.otel),
515
+ serviceName: internalName(options.name),
516
+ serverEndpoint: parseServerAddress(options.servers)
517
+ });
518
+
519
+ // src/otel/propagator.ts
520
+ import {
521
+ propagation
522
+ } from "@opentelemetry/api";
523
+ var injectContext = (ctx, carrier, setter) => {
524
+ propagation.inject(ctx, carrier, setter);
525
+ };
526
+ var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier, getter);
527
+
528
+ // src/otel/tracer.ts
529
+ import { trace } from "@opentelemetry/api";
530
+ var PACKAGE_VERSION = true ? "2.11.0" : "0.0.0";
531
+ var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
532
+
533
+ // src/otel/carrier.ts
534
+ var hdrsSetter = {
535
+ set: (headers2, key, value) => {
536
+ headers2.set(key, value);
537
+ }
538
+ };
539
+ var hdrsGetter = {
540
+ keys: (headers2) => headers2 ? headers2.keys() : [],
541
+ get: (headers2, key) => {
542
+ if (!headers2) return void 0;
543
+ const all = typeof headers2.values === "function" ? headers2.values(key) : void 0;
544
+ if (Array.isArray(all)) {
545
+ const nonEmpty = all.filter((value) => value !== "");
546
+ if (nonEmpty.length === 0) return void 0;
547
+ return nonEmpty.join(",");
548
+ }
549
+ const single = headers2.get(key);
550
+ return single === "" ? void 0 : single;
551
+ }
552
+ };
553
+
554
+ // src/otel/attributes.ts
555
+ var MESSAGING_SYSTEM = "nats";
556
+ var baseMessagingAttributes = (ctx) => {
557
+ const attrs = {
558
+ [ATTR_MESSAGING_SYSTEM]: MESSAGING_SYSTEM,
559
+ [ATTR_MESSAGING_DESTINATION_NAME]: ctx.subject,
560
+ [ATTR_MESSAGING_CLIENT_ID]: ctx.serviceName
561
+ };
562
+ if (ctx.serverAddress) attrs[ATTR_SERVER_ADDRESS] = ctx.serverAddress;
563
+ if (ctx.serverPort !== void 0) attrs[ATTR_SERVER_PORT] = ctx.serverPort;
564
+ if (ctx.pattern && ctx.pattern !== ctx.subject) {
565
+ attrs[ATTR_MESSAGING_DESTINATION_TEMPLATE] = ctx.pattern;
566
+ }
567
+ return attrs;
568
+ };
569
+ var jetstreamKindForPublish = (kind) => kind === "rpc.request" /* RpcRequest */ ? "rpc" /* Rpc */ : kind;
570
+ var buildPublishAttributes = (ctx) => {
571
+ const attrs = {
572
+ ...baseMessagingAttributes(ctx),
573
+ [ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_PUBLISH,
574
+ [ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_SEND,
575
+ [ATTR_MESSAGING_MESSAGE_BODY_SIZE]: ctx.payloadBytes,
576
+ [ATTR_JETSTREAM_KIND]: jetstreamKindForPublish(ctx.kind)
577
+ };
578
+ if (ctx.messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = ctx.messageId;
579
+ if (ctx.correlationId) attrs[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID] = ctx.correlationId;
580
+ return attrs;
581
+ };
582
+ var buildConsumeAttributes = (ctx) => {
583
+ const { msg, info, kind, payloadBytes } = ctx;
584
+ const attrs = {
585
+ ...baseMessagingAttributes(ctx),
586
+ [ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_PROCESS,
587
+ [ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_PROCESS,
588
+ [ATTR_MESSAGING_MESSAGE_BODY_SIZE]: payloadBytes,
589
+ [ATTR_JETSTREAM_KIND]: kind
590
+ };
591
+ if (info) {
592
+ attrs[ATTR_MESSAGING_NATS_STREAM_NAME] = info.stream;
593
+ attrs[ATTR_MESSAGING_CONSUMER_GROUP_NAME] = info.consumer;
594
+ attrs[ATTR_MESSAGING_NATS_STREAM_SEQUENCE] = info.streamSequence;
595
+ attrs[ATTR_MESSAGING_NATS_CONSUMER_SEQUENCE] = info.deliverySequence;
596
+ attrs[ATTR_MESSAGING_NATS_DELIVERY_COUNT] = info.deliveryCount;
597
+ }
598
+ const messageId = msg.headers?.get(NATS_MSG_ID_HEADER);
599
+ if (messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = messageId;
600
+ return attrs;
601
+ };
602
+ var buildRpcClientAttributes = (ctx) => {
603
+ const attrs = {
604
+ ...baseMessagingAttributes(ctx),
605
+ [ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_SEND,
606
+ [ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_SEND,
607
+ [ATTR_MESSAGING_MESSAGE_BODY_SIZE]: ctx.payloadBytes,
608
+ [ATTR_JETSTREAM_KIND]: "rpc" /* Rpc */
609
+ };
610
+ if (ctx.correlationId) attrs[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID] = ctx.correlationId;
611
+ if (ctx.messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = ctx.messageId;
612
+ return attrs;
613
+ };
614
+ var buildDeadLetterAttributes = (ctx) => {
615
+ const attrs = {
616
+ ...baseMessagingAttributes(ctx),
617
+ [ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_DEAD_LETTER,
618
+ [ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_PROCESS,
619
+ [ATTR_MESSAGING_NATS_DELIVERY_COUNT]: ctx.finalDeliveryCount
620
+ };
621
+ if (ctx.reason) attrs[ATTR_JETSTREAM_DEAD_LETTER_REASON] = ctx.reason;
622
+ return attrs;
623
+ };
624
+ var extractFromRpcException = (record) => {
625
+ const getError = record.getError;
626
+ if (typeof getError !== "function") return void 0;
627
+ return codeFromPayload(getError.call(record));
628
+ };
629
+ var extractFromHttpException = (record) => {
630
+ const getStatus = record.getStatus;
631
+ if (typeof getStatus !== "function") return void 0;
632
+ const status = getStatus.call(record);
633
+ return typeof status === "number" && Number.isFinite(status) ? `HTTP_${status}` : void 0;
634
+ };
635
+ var extractFromOwnCode = (record) => {
636
+ const code = record.code ?? record.errorCode;
637
+ return typeof code === "string" && isStableErrorCode(code) ? code : void 0;
638
+ };
639
+ var extractExpectedErrorCode = (err) => {
640
+ if (err === null || typeof err !== "object") return void 0;
641
+ const record = err;
642
+ if (hasAncestorNamed(err, "RpcException")) {
643
+ const fromPayload = extractFromRpcException(record);
644
+ if (fromPayload !== void 0) return fromPayload;
645
+ }
646
+ if (hasAncestorNamed(err, "HttpException")) {
647
+ const fromStatus = extractFromHttpException(record);
648
+ if (fromStatus !== void 0) return fromStatus;
649
+ }
650
+ return extractFromOwnCode(record);
651
+ };
652
+ var hasAncestorNamed = (err, name) => {
653
+ let proto = Object.getPrototypeOf(err);
654
+ while (proto) {
655
+ const ctorName = proto.constructor?.name;
656
+ if (ctorName === name) return true;
657
+ proto = Object.getPrototypeOf(proto);
658
+ }
659
+ return false;
660
+ };
661
+ var STABLE_ERROR_CODE_RE = /^[A-Z][A-Z0-9_]*$/u;
662
+ var isStableErrorCode = (value) => STABLE_ERROR_CODE_RE.test(value);
663
+ var codeFromPayload = (payload) => {
664
+ if (payload === null || payload === void 0) return void 0;
665
+ if (typeof payload === "string") return isStableErrorCode(payload) ? payload : void 0;
666
+ if (typeof payload === "object") {
667
+ const code = payload.code;
668
+ if (typeof code === "string" && isStableErrorCode(code)) return code;
669
+ }
670
+ return void 0;
671
+ };
672
+ var buildExpectedErrorAttributes = (err) => {
673
+ const attrs = {
674
+ [ATTR_JETSTREAM_RPC_REPLY_HAS_ERROR]: true
675
+ };
676
+ const code = extractExpectedErrorCode(err);
677
+ if (code !== void 0) attrs[ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE] = code;
678
+ return attrs;
679
+ };
680
+ var applyExpectedErrorAttributes = (span, err) => {
681
+ span.setAttributes(buildExpectedErrorAttributes(err));
682
+ };
683
+
684
+ // src/otel/spans/publish.ts
685
+ import { Logger as Logger2 } from "@nestjs/common";
686
+ import { SpanKind, SpanStatusCode, context, trace as trace2 } from "@opentelemetry/api";
687
+ var logger2 = new Logger2("Jetstream:Otel");
688
+ var shouldTracePublishSafe = (predicate, subject, record) => {
689
+ if (!predicate) return true;
690
+ try {
691
+ return predicate(subject, record);
692
+ } catch (err) {
693
+ const message = err instanceof Error ? err.message : String(err);
694
+ logger2.debug(`OTel shouldTracePublish threw: ${message}`);
695
+ return true;
696
+ }
697
+ };
698
+ var withPublishSpan = async (ctx, config, fn) => {
699
+ if (!config.enabled) return fn();
700
+ const shouldCreateSpan = config.traces.has("publish" /* Publish */) && shouldTracePublishSafe(config.shouldTracePublish, ctx.subject, ctx.record);
701
+ if (!shouldCreateSpan) {
702
+ injectContext(context.active(), ctx.headers, hdrsSetter);
703
+ return fn();
704
+ }
705
+ const tracer = getTracer();
706
+ const span = tracer.startSpan(`${SPAN_NAME_PUBLISH} ${ctx.subject}`, {
707
+ kind: SpanKind.PRODUCER,
708
+ attributes: {
709
+ ...buildPublishAttributes({
710
+ subject: ctx.subject,
711
+ pattern: ctx.pattern,
712
+ serviceName: ctx.serviceName,
713
+ serverAddress: ctx.endpoint?.host,
714
+ serverPort: ctx.endpoint?.port,
715
+ kind: ctx.kind,
716
+ payloadBytes: ctx.payloadBytes,
717
+ messageId: ctx.messageId,
718
+ correlationId: ctx.correlationId
719
+ }),
720
+ ...ctx.scheduleTarget ? { [ATTR_JETSTREAM_SCHEDULE_TARGET]: ctx.scheduleTarget } : {},
721
+ ...captureMatchingHeaders(ctx.headers, config.captureHeaders),
722
+ ...captureBodyAttribute(ctx.subject, ctx.payload, config.captureBody)
723
+ }
724
+ });
725
+ const ctxWithSpan = trace2.setSpan(context.active(), span);
726
+ injectContext(ctxWithSpan, ctx.headers, hdrsSetter);
727
+ const start = Date.now();
728
+ const invokeResponseHook = (error) => {
729
+ context.with(ctxWithSpan, () => {
730
+ safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
731
+ subject: ctx.subject,
732
+ durationMs: Date.now() - start,
733
+ error
734
+ });
735
+ });
736
+ };
737
+ try {
738
+ const result = await context.with(ctxWithSpan, async () => {
739
+ safelyInvokeHook(HOOK_PUBLISH, config.publishHook, span, {
740
+ subject: ctx.subject,
741
+ record: ctx.record,
742
+ kind: ctx.kind
743
+ });
744
+ return fn();
745
+ });
746
+ span.setStatus({ code: SpanStatusCode.OK });
747
+ invokeResponseHook();
748
+ return result;
749
+ } catch (err) {
750
+ const error = err instanceof Error ? err : new Error(String(err));
751
+ span.recordException(error);
752
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
753
+ invokeResponseHook(error);
754
+ throw err;
755
+ } finally {
756
+ span.end();
757
+ }
758
+ };
759
+
760
+ // src/otel/spans/consume.ts
761
+ import {
762
+ ROOT_CONTEXT,
763
+ SpanKind as SpanKind2,
764
+ SpanStatusCode as SpanStatusCode2,
765
+ context as context2,
766
+ trace as trace3
767
+ } from "@opentelemetry/api";
768
+ var isPromiseLike = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
769
+ var applyExpectedError = (span, err) => {
770
+ span.setStatus({ code: SpanStatusCode2.OK });
771
+ applyExpectedErrorAttributes(span, err);
772
+ };
773
+ var applyUnexpectedError = (span, err) => {
774
+ const error = err instanceof Error ? err : new Error(String(err));
775
+ span.recordException(error);
776
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: error.message });
777
+ };
778
+ var withConsumeSpan = (ctx, config, fn, options = {}) => {
779
+ if (!config.enabled) return fn();
780
+ const parentCtx = extractContext(ROOT_CONTEXT, ctx.msg.headers, hdrsGetter);
781
+ const shouldCreateSpan = config.traces.has("consume" /* Consume */) && (config.shouldTraceConsume?.(ctx.subject, ctx.msg) ?? true);
782
+ if (!shouldCreateSpan) {
783
+ return context2.with(parentCtx, fn);
784
+ }
785
+ const tracer = getTracer();
786
+ const span = tracer.startSpan(
787
+ `${SPAN_NAME_PROCESS} ${ctx.subject}`,
788
+ {
789
+ kind: SpanKind2.CONSUMER,
790
+ attributes: {
791
+ ...buildConsumeAttributes({
792
+ subject: ctx.subject,
793
+ pattern: ctx.pattern,
794
+ msg: ctx.msg,
795
+ info: ctx.info,
796
+ kind: ctx.kind,
797
+ payloadBytes: ctx.payloadBytes,
798
+ serviceName: ctx.serviceName,
799
+ serverAddress: ctx.endpoint?.host,
800
+ serverPort: ctx.endpoint?.port
801
+ }),
802
+ ...captureMatchingHeaders(ctx.msg.headers, config.captureHeaders),
803
+ ...captureBodyAttribute(ctx.subject, ctx.msg.data, config.captureBody)
804
+ }
805
+ },
806
+ parentCtx
807
+ );
808
+ const ctxWithSpan = trace3.setSpan(parentCtx, span);
809
+ const start = Date.now();
810
+ let finalized = false;
811
+ const { signal, timeoutLabel = "handler.timeout" } = options;
812
+ let detachAbort = null;
813
+ const invokeResponseHook = (durationMs, error) => {
814
+ context2.with(ctxWithSpan, () => {
815
+ safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
816
+ subject: ctx.subject,
817
+ durationMs,
818
+ error
819
+ });
820
+ });
821
+ };
822
+ const finishOk2 = () => {
823
+ if (finalized) return;
824
+ finalized = true;
825
+ detachAbort?.();
826
+ span.setStatus({ code: SpanStatusCode2.OK });
827
+ invokeResponseHook(Date.now() - start);
828
+ span.end();
829
+ };
830
+ const finishError2 = (err) => {
831
+ if (finalized) return;
832
+ finalized = true;
833
+ detachAbort?.();
834
+ let classification = "unexpected";
835
+ try {
836
+ classification = config.errorClassifier(err);
837
+ } catch {
838
+ }
839
+ if (classification === "expected") {
840
+ applyExpectedError(span, err);
841
+ } else {
842
+ applyUnexpectedError(span, err);
843
+ }
844
+ invokeResponseHook(Date.now() - start, err instanceof Error ? err : new Error(String(err)));
845
+ span.end();
846
+ };
847
+ const onAbort = () => {
848
+ if (finalized) return;
849
+ finalized = true;
850
+ const error = new Error(timeoutLabel);
851
+ span.addEvent(timeoutLabel);
852
+ span.recordException(error);
853
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: timeoutLabel });
854
+ invokeResponseHook(Date.now() - start, error);
855
+ span.end();
856
+ };
857
+ if (signal) {
858
+ if (signal.aborted) {
859
+ onAbort();
860
+ } else {
861
+ signal.addEventListener("abort", onAbort, { once: true });
862
+ detachAbort = () => {
863
+ signal.removeEventListener("abort", onAbort);
864
+ };
865
+ }
866
+ }
867
+ let result;
868
+ try {
869
+ result = context2.with(ctxWithSpan, () => {
870
+ safelyInvokeHook(HOOK_CONSUME, config.consumeHook, span, {
871
+ subject: ctx.subject,
872
+ msg: ctx.msg,
873
+ handlerMetadata: ctx.handlerMetadata,
874
+ kind: ctx.kind
875
+ });
876
+ return fn();
877
+ });
878
+ } catch (err) {
879
+ finishError2(err);
880
+ throw err;
881
+ }
882
+ if (isPromiseLike(result)) {
883
+ return Promise.resolve(result).then(
884
+ (value) => {
885
+ finishOk2();
886
+ return value;
887
+ },
888
+ (err) => {
889
+ finishError2(err);
890
+ throw err;
891
+ }
892
+ );
893
+ }
894
+ finishOk2();
895
+ return result;
896
+ };
897
+
898
+ // src/otel/spans/rpc-client.ts
899
+ import { Logger as Logger3 } from "@nestjs/common";
900
+ import { SpanKind as SpanKind3, SpanStatusCode as SpanStatusCode3, context as context3, trace as trace4 } from "@opentelemetry/api";
901
+ var logger3 = new Logger3("Jetstream:Otel");
902
+ var RPC_TIMEOUT_MESSAGE = "rpc.timeout";
903
+ var beginRpcClientSpan = (ctx, config) => {
904
+ if (!config.enabled) {
905
+ return {
906
+ activeContext: context3.active(),
907
+ finish: () => void 0
908
+ };
909
+ }
910
+ if (!config.traces.has("rpc.client.send" /* RpcClientSend */)) {
911
+ injectContext(context3.active(), ctx.headers, hdrsSetter);
912
+ return {
913
+ activeContext: context3.active(),
914
+ finish: () => void 0
915
+ };
916
+ }
917
+ const tracer = getTracer();
918
+ const span = tracer.startSpan(`${SPAN_NAME_SEND} ${ctx.subject}`, {
919
+ kind: SpanKind3.CLIENT,
920
+ attributes: {
921
+ ...buildRpcClientAttributes({
922
+ subject: ctx.subject,
923
+ pattern: ctx.pattern,
924
+ correlationId: ctx.correlationId,
925
+ payloadBytes: ctx.payloadBytes,
926
+ messageId: ctx.messageId,
927
+ serviceName: ctx.serviceName,
928
+ serverAddress: ctx.endpoint?.host,
929
+ serverPort: ctx.endpoint?.port
930
+ }),
931
+ ...captureMatchingHeaders(ctx.headers, config.captureHeaders),
932
+ ...captureBodyAttribute(ctx.subject, ctx.payload, config.captureBody)
933
+ }
934
+ });
935
+ const ctxWithSpan = trace4.setSpan(context3.active(), span);
936
+ injectContext(ctxWithSpan, ctx.headers, hdrsSetter);
937
+ const start = Date.now();
938
+ let finalized = false;
939
+ const finish = (outcome) => {
940
+ if (finalized) return;
941
+ finalized = true;
942
+ let reply;
943
+ let error;
944
+ switch (outcome.kind) {
945
+ case "ok" /* Ok */:
946
+ reply = outcome.reply;
947
+ span.setStatus({ code: SpanStatusCode3.OK });
948
+ break;
949
+ case "reply-error" /* ReplyError */:
950
+ reply = outcome.replyPayload;
951
+ applyExpectedErrorAttributes(span, outcome.replyPayload);
952
+ span.setStatus({ code: SpanStatusCode3.OK });
953
+ break;
954
+ case "timeout" /* Timeout */:
955
+ error = new Error(RPC_TIMEOUT_MESSAGE);
956
+ span.addEvent(RPC_TIMEOUT_MESSAGE);
957
+ span.setStatus({ code: SpanStatusCode3.ERROR, message: RPC_TIMEOUT_MESSAGE });
958
+ break;
959
+ case "error" /* Error */:
960
+ error = outcome.error;
961
+ span.recordException(outcome.error);
962
+ span.setStatus({ code: SpanStatusCode3.ERROR, message: outcome.error.message });
963
+ break;
964
+ default: {
965
+ const unknownOutcome = outcome;
966
+ logger3.error(`Unhandled RPC outcome: ${String(unknownOutcome.kind)}`);
967
+ span.setStatus({ code: SpanStatusCode3.ERROR, message: "unknown outcome" });
968
+ }
969
+ }
970
+ context3.with(ctxWithSpan, () => {
971
+ safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
972
+ subject: ctx.subject,
973
+ durationMs: Date.now() - start,
974
+ reply,
975
+ error
976
+ });
977
+ });
978
+ span.end();
979
+ };
980
+ return { activeContext: ctxWithSpan, finish };
981
+ };
982
+
983
+ // src/otel/spans/dead-letter.ts
984
+ import { ROOT_CONTEXT as ROOT_CONTEXT2, SpanKind as SpanKind4, SpanStatusCode as SpanStatusCode4, context as context4, trace as trace5 } from "@opentelemetry/api";
985
+ var withDeadLetterSpan = async (ctx, config, fn) => {
986
+ if (!config.enabled || !config.traces.has("dead_letter" /* DeadLetter */)) {
987
+ return fn();
988
+ }
989
+ const parentCtx = extractContext(ROOT_CONTEXT2, ctx.msg.headers, hdrsGetter);
990
+ const tracer = getTracer();
991
+ const span = tracer.startSpan(
992
+ `${SPAN_NAME_DEAD_LETTER} ${ctx.msg.subject}`,
993
+ {
994
+ kind: SpanKind4.INTERNAL,
995
+ attributes: buildDeadLetterAttributes({
996
+ subject: ctx.msg.subject,
997
+ pattern: ctx.pattern,
998
+ serviceName: ctx.serviceName,
999
+ serverAddress: ctx.endpoint?.host,
1000
+ serverPort: ctx.endpoint?.port,
1001
+ finalDeliveryCount: ctx.finalDeliveryCount,
1002
+ reason: ctx.reason
1003
+ })
1004
+ },
1005
+ parentCtx
1006
+ );
1007
+ const ctxWithSpan = trace5.setSpan(parentCtx, span);
1008
+ const start = Date.now();
1009
+ const invokeResponseHook = (error) => {
1010
+ context4.with(ctxWithSpan, () => {
1011
+ safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
1012
+ subject: ctx.msg.subject,
1013
+ durationMs: Date.now() - start,
1014
+ error
1015
+ });
1016
+ });
1017
+ };
1018
+ try {
1019
+ const result = await context4.with(ctxWithSpan, fn);
1020
+ span.setStatus({ code: SpanStatusCode4.OK });
1021
+ invokeResponseHook();
1022
+ return result;
1023
+ } catch (err) {
1024
+ const error = err instanceof Error ? err : new Error(String(err));
1025
+ span.recordException(error);
1026
+ span.setStatus({ code: SpanStatusCode4.ERROR, message: error.message });
1027
+ invokeResponseHook(error);
1028
+ throw err;
1029
+ } finally {
1030
+ span.end();
1031
+ }
1032
+ };
1033
+
1034
+ // src/otel/spans/infrastructure.ts
1035
+ import { Logger as Logger4 } from "@nestjs/common";
1036
+ import {
1037
+ SpanKind as SpanKind5,
1038
+ SpanStatusCode as SpanStatusCode5,
1039
+ context as context5,
1040
+ trace as trace6
1041
+ } from "@opentelemetry/api";
1042
+ var logger4 = new Logger4("Jetstream:Otel");
1043
+ var startInfraSpan = (config, traceKind, name, ctx, extraAttributes = {}) => {
1044
+ if (!config.enabled || !config.traces.has(traceKind)) return null;
1045
+ const tracer = getTracer();
1046
+ const attributes = {
1047
+ [ATTR_JETSTREAM_SERVICE_NAME]: ctx.serviceName
1048
+ };
1049
+ if (ctx.endpoint?.host) attributes[ATTR_SERVER_ADDRESS] = ctx.endpoint.host;
1050
+ if (ctx.endpoint?.port !== void 0) attributes[ATTR_SERVER_PORT] = ctx.endpoint.port;
1051
+ for (const [key, value] of Object.entries(extraAttributes)) {
1052
+ if (value !== void 0) attributes[key] = value;
1053
+ }
1054
+ return tracer.startSpan(name, { kind: SpanKind5.INTERNAL, attributes });
1055
+ };
1056
+ var finishOk = (span) => {
1057
+ span.setStatus({ code: SpanStatusCode5.OK });
1058
+ span.end();
1059
+ };
1060
+ var finishError = (span, err) => {
1061
+ const error = err instanceof Error ? err : new Error(String(err));
1062
+ span.recordException(error);
1063
+ span.setStatus({ code: SpanStatusCode5.ERROR, message: error.message });
1064
+ span.end();
1065
+ };
1066
+ var wrapInfra = async (config, traceKind, name, ctx, attributes, op) => {
1067
+ const span = startInfraSpan(config, traceKind, name, ctx, attributes);
1068
+ if (!span) return op();
1069
+ const ctxWithSpan = trace6.setSpan(context5.active(), span);
1070
+ try {
1071
+ const result = await context5.with(ctxWithSpan, op);
1072
+ finishOk(span);
1073
+ return result;
1074
+ } catch (err) {
1075
+ finishError(span, err);
1076
+ throw err;
1077
+ }
1078
+ };
1079
+ var beginConnectionLifecycleSpan = (config, ctx) => {
1080
+ const span = startInfraSpan(
1081
+ config,
1082
+ "connection.lifecycle" /* ConnectionLifecycle */,
1083
+ SPAN_NAME_NATS_CONNECTION,
1084
+ ctx,
1085
+ { [ATTR_NATS_CONNECTION_SERVER]: ctx.server }
1086
+ );
1087
+ if (!span) {
1088
+ return {
1089
+ recordEvent: () => void 0,
1090
+ finish: () => void 0
1091
+ };
1092
+ }
1093
+ let finalized = false;
1094
+ return {
1095
+ recordEvent: (name, attributes) => {
1096
+ if (finalized) {
1097
+ logger4.debug(`recordEvent('${name}') called after connection span finished`);
1098
+ return;
1099
+ }
1100
+ span.addEvent(name, attributes);
1101
+ },
1102
+ finish: (err) => {
1103
+ if (finalized) return;
1104
+ finalized = true;
1105
+ if (err === void 0) finishOk(span);
1106
+ else finishError(span, err);
1107
+ }
1108
+ };
1109
+ };
1110
+ var withSelfHealingSpan = (config, ctx, op) => wrapInfra(
1111
+ config,
1112
+ "self_healing" /* SelfHealing */,
1113
+ SPAN_NAME_JETSTREAM_SELF_HEALING,
1114
+ ctx,
1115
+ {
1116
+ [ATTR_MESSAGING_NATS_STREAM_NAME]: ctx.stream,
1117
+ [ATTR_MESSAGING_CONSUMER_GROUP_NAME]: ctx.consumer,
1118
+ [ATTR_JETSTREAM_SELF_HEALING_REASON]: ctx.reason
1119
+ },
1120
+ op
1121
+ );
1122
+ var withProvisioningSpan = (config, ctx, op) => wrapInfra(
1123
+ config,
1124
+ "provisioning" /* Provisioning */,
1125
+ `${SPAN_NAME_JETSTREAM_PROVISIONING_PREFIX}${ctx.entity}`,
1126
+ ctx,
1127
+ {
1128
+ [ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
1129
+ [ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
1130
+ [ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name
1131
+ },
1132
+ op
1133
+ );
1134
+ var withMigrationSpan = (config, ctx, op) => wrapInfra(
1135
+ config,
1136
+ "migration" /* Migration */,
1137
+ SPAN_NAME_JETSTREAM_MIGRATION,
1138
+ ctx,
1139
+ {
1140
+ [ATTR_MESSAGING_NATS_STREAM_NAME]: ctx.stream,
1141
+ [ATTR_JETSTREAM_MIGRATION_REASON]: ctx.reason
1142
+ },
1143
+ op
1144
+ );
1145
+ var withShutdownSpan = (config, ctx, op) => wrapInfra(config, "shutdown" /* Shutdown */, SPAN_NAME_JETSTREAM_SHUTDOWN, ctx, {}, op);
1146
+
227
1147
  // src/client/jetstream.record.ts
228
1148
  var JetstreamRecord = class {
229
1149
  constructor(data, headers2, timeout, messageId, schedule, ttl) {
@@ -376,9 +1296,14 @@ var JetstreamRecordBuilder = class {
376
1296
  this.ttlDuration
377
1297
  );
378
1298
  }
379
- /** Validate that a header key is not reserved. */
1299
+ /**
1300
+ * Validate that a header key is not reserved. NATS treats header names
1301
+ * case-insensitively, so the check is against the lowercase form to keep
1302
+ * `'X-Correlation-ID'`, `'x-correlation-id'`, and any other casing in
1303
+ * lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
1304
+ */
380
1305
  validateHeaderKey(key) {
381
- if (RESERVED_HEADERS.has(key)) {
1306
+ if (RESERVED_HEADERS.has(key.toLowerCase())) {
382
1307
  throw new Error(
383
1308
  `Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
384
1309
  );
@@ -399,6 +1324,22 @@ var nanosToGoDuration = (nanos) => {
399
1324
 
400
1325
  // src/client/jetstream.client.ts
401
1326
  var BROADCAST_SUBJECT_PREFIX = "broadcast.";
1327
+ var detectEventKind = (pattern) => {
1328
+ if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
1329
+ if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
1330
+ return "event" /* Event */;
1331
+ };
1332
+ var declaredEventPattern = (pattern) => {
1333
+ if (pattern.startsWith("broadcast:" /* Broadcast */))
1334
+ return pattern.slice("broadcast:" /* Broadcast */.length);
1335
+ if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
1336
+ return pattern;
1337
+ };
1338
+ var eventStreamKind = (kind) => {
1339
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
1340
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
1341
+ return "ev" /* Event */;
1342
+ };
402
1343
  var JetstreamClient = class extends ClientProxy {
403
1344
  constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
404
1345
  super();
@@ -414,8 +1355,11 @@ var JetstreamClient = class extends ClientProxy {
414
1355
  this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
415
1356
  this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
416
1357
  this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
1358
+ const derived = deriveOtelAttrs(this.rootOptions);
1359
+ this.otel = derived.otel;
1360
+ this.serverEndpoint = derived.serverEndpoint;
417
1361
  }
418
- logger = new Logger("Jetstream:Client");
1362
+ logger = new Logger5("Jetstream:Client");
419
1363
  /** Target service name this client sends messages to. */
420
1364
  targetName;
421
1365
  /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
@@ -435,6 +1379,10 @@ var JetstreamClient = class extends ClientProxy {
435
1379
  */
436
1380
  isCoreMode;
437
1381
  defaultRpcTimeout;
1382
+ /** Resolved OpenTelemetry configuration, computed once in the constructor. */
1383
+ otel;
1384
+ /** Server endpoint parts used for `server.address` / `server.port` span attributes. */
1385
+ serverEndpoint;
438
1386
  /** Shared inbox for JetStream-mode RPC responses. */
439
1387
  inbox = null;
440
1388
  inboxSubscription = null;
@@ -502,32 +1450,64 @@ var JetstreamClient = class extends ClientProxy {
502
1450
  if (!this.readyForPublish) await this.connect();
503
1451
  const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
504
1452
  const eventSubject = this.buildEventSubject(packet.pattern);
1453
+ const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
505
1454
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
506
- if (schedule) {
507
- const scheduleSubject = this.buildScheduleSubject(eventSubject);
508
- const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
509
- headers: msgHeaders,
510
- msgID: messageId ?? nuid.next(),
511
- ttl,
512
- schedule: {
513
- specification: schedule.at,
514
- target: eventSubject
1455
+ const encoded = this.codec.encode(data);
1456
+ const effectiveMsgId = messageId ?? nuid.next();
1457
+ const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1458
+ const publishKind = detectEventKind(packet.pattern);
1459
+ const declaredPattern = declaredEventPattern(packet.pattern);
1460
+ const streamKind = eventStreamKind(publishKind);
1461
+ const startedAt = performance.now();
1462
+ try {
1463
+ await withPublishSpan(
1464
+ {
1465
+ subject: publishSubject,
1466
+ pattern: packet.pattern,
1467
+ record,
1468
+ kind: publishKind,
1469
+ payloadBytes: encoded.length,
1470
+ payload: encoded,
1471
+ messageId: effectiveMsgId,
1472
+ headers: msgHeaders,
1473
+ serviceName: this.callerName,
1474
+ endpoint: this.serverEndpoint,
1475
+ scheduleTarget: schedule ? eventSubject : void 0
1476
+ },
1477
+ this.otel,
1478
+ async () => {
1479
+ const warnIfDuplicate = (kindLabel, ack2) => {
1480
+ if (ack2.duplicate) {
1481
+ this.logger.warn(
1482
+ `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1483
+ );
1484
+ }
1485
+ };
1486
+ if (schedule) {
1487
+ const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1488
+ headers: msgHeaders,
1489
+ msgID: effectiveMsgId,
1490
+ ttl,
1491
+ schedule: {
1492
+ specification: schedule.at,
1493
+ target: eventSubject
1494
+ }
1495
+ });
1496
+ warnIfDuplicate("scheduled", ack2);
1497
+ return;
1498
+ }
1499
+ const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1500
+ headers: msgHeaders,
1501
+ msgID: effectiveMsgId,
1502
+ ttl
1503
+ });
1504
+ warnIfDuplicate("event", ack);
515
1505
  }
516
- });
517
- if (ack.duplicate) {
518
- this.logger.warn(
519
- `Duplicate scheduled publish detected: ${scheduleSubject} (seq: ${ack.seq})`
520
- );
521
- }
522
- } else {
523
- const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
524
- headers: msgHeaders,
525
- msgID: messageId ?? nuid.next(),
526
- ttl
527
- });
528
- if (ack.duplicate) {
529
- this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
530
- }
1506
+ );
1507
+ this.reportPublished(declaredPattern, streamKind, startedAt, "success");
1508
+ } catch (err) {
1509
+ this.reportPublished(declaredPattern, streamKind, startedAt, "error");
1510
+ throw err;
531
1511
  }
532
1512
  return void 0;
533
1513
  }
@@ -556,14 +1536,17 @@ var JetstreamClient = class extends ClientProxy {
556
1536
  };
557
1537
  let jetStreamCorrelationId = null;
558
1538
  if (this.isCoreMode) {
559
- this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
1539
+ this.publishCoreRpc(subject, data, hdrs, timeout, callback, packet.pattern).catch(
1540
+ onUnhandled
1541
+ );
560
1542
  } else {
561
1543
  jetStreamCorrelationId = nuid.next();
562
1544
  this.publishJetStreamRpc(subject, data, callback, {
563
1545
  headers: hdrs,
564
1546
  timeout,
565
1547
  correlationId: jetStreamCorrelationId,
566
- messageId
1548
+ messageId,
1549
+ declaredPattern: packet.pattern
567
1550
  }).catch(onUnhandled);
568
1551
  }
569
1552
  return () => {
@@ -578,38 +1561,118 @@ var JetstreamClient = class extends ClientProxy {
578
1561
  };
579
1562
  }
580
1563
  /** Core mode: nc.request() with timeout. */
581
- async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
1564
+ async publishCoreRpc(subject, data, customHeaders, timeout, callback, declaredPattern) {
1565
+ const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
1566
+ const hdrs = this.buildHeaders(customHeaders, { subject });
1567
+ const encoded = this.codec.encode(data);
1568
+ const spanHandle = beginRpcClientSpan(
1569
+ {
1570
+ subject,
1571
+ payloadBytes: encoded.length,
1572
+ payload: encoded,
1573
+ headers: hdrs,
1574
+ serviceName: this.callerName,
1575
+ endpoint: this.serverEndpoint
1576
+ },
1577
+ this.otel
1578
+ );
1579
+ const startedAt = performance.now();
582
1580
  try {
583
1581
  const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
584
- const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
585
- const hdrs = this.buildHeaders(customHeaders, { subject });
586
- const response = await nc.request(subject, this.codec.encode(data), {
587
- timeout: effectiveTimeout,
588
- headers: hdrs
589
- });
1582
+ const response = await context6.with(
1583
+ spanHandle.activeContext,
1584
+ () => nc.request(subject, encoded, {
1585
+ timeout: effectiveTimeout,
1586
+ headers: hdrs
1587
+ })
1588
+ );
590
1589
  const decoded = this.codec.decode(response.data);
591
1590
  if (response.headers?.get("x-error" /* Error */)) {
1591
+ spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: decoded });
1592
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1593
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
592
1594
  callback({ err: decoded, response: null, isDisposed: true });
593
1595
  } else {
1596
+ spanHandle.finish({ kind: "ok" /* Ok */, reply: decoded });
1597
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1598
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
594
1599
  callback({ err: null, response: decoded, isDisposed: true });
595
1600
  }
596
1601
  } catch (err) {
597
1602
  const error = err instanceof Error ? err : new Error("Unknown error");
598
- this.logger.error(`Core RPC error (${subject}):`, err);
599
- this.eventBus.emit("error" /* Error */, error, "client-rpc");
1603
+ if (error instanceof TimeoutError) {
1604
+ spanHandle.finish({ kind: "timeout" /* Timeout */ });
1605
+ this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
1606
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1607
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1608
+ } else {
1609
+ spanHandle.finish({ kind: "error" /* Error */, error });
1610
+ this.eventBus.emit("error" /* Error */, error, "client-rpc");
1611
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1612
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1613
+ }
600
1614
  callback({ err: error, response: null, isDisposed: true });
601
1615
  }
602
1616
  }
603
1617
  /** JetStream mode: publish to stream + wait for inbox response. */
604
1618
  async publishJetStreamRpc(subject, data, callback, options) {
605
- const { headers: customHeaders, correlationId, messageId } = options;
1619
+ const { headers: customHeaders, correlationId, messageId, declaredPattern } = options;
606
1620
  const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
607
- this.pendingMessages.set(correlationId, callback);
1621
+ const hdrs = this.buildHeaders(customHeaders, {
1622
+ subject,
1623
+ correlationId,
1624
+ replyTo: this.inbox ?? ""
1625
+ });
1626
+ const encoded = this.codec.encode(data);
1627
+ const spanHandle = beginRpcClientSpan(
1628
+ {
1629
+ subject,
1630
+ correlationId,
1631
+ payloadBytes: encoded.length,
1632
+ payload: encoded,
1633
+ messageId,
1634
+ headers: hdrs,
1635
+ serviceName: this.callerName,
1636
+ endpoint: this.serverEndpoint
1637
+ },
1638
+ this.otel
1639
+ );
1640
+ const startedAt = performance.now();
1641
+ this.pendingMessages.set(correlationId, (packet) => {
1642
+ if (packet.err) {
1643
+ if (packet.err instanceof Error) {
1644
+ spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
1645
+ } else {
1646
+ spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: packet.err });
1647
+ }
1648
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1649
+ } else {
1650
+ spanHandle.finish({ kind: "ok" /* Ok */, reply: packet.response });
1651
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1652
+ }
1653
+ callback(packet);
1654
+ });
1655
+ const timeoutId = setTimeout(() => {
1656
+ if (!this.pendingMessages.has(correlationId)) return;
1657
+ this.pendingTimeouts.delete(correlationId);
1658
+ this.pendingMessages.delete(correlationId);
1659
+ spanHandle.finish({ kind: "timeout" /* Timeout */ });
1660
+ this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
1661
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1662
+ callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
1663
+ }, effectiveTimeout);
1664
+ this.pendingTimeouts.set(correlationId, timeoutId);
608
1665
  try {
609
1666
  if (!this.readyForPublish) await this.connect();
610
1667
  if (!this.pendingMessages.has(correlationId)) return;
611
1668
  if (!this.inbox) {
1669
+ clearTimeout(timeoutId);
1670
+ this.pendingTimeouts.delete(correlationId);
612
1671
  this.pendingMessages.delete(correlationId);
1672
+ const inboxError = new Error("Inbox not initialized");
1673
+ spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
1674
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1675
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
613
1676
  callback({
614
1677
  err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
615
1678
  response: null,
@@ -617,24 +1680,14 @@ var JetstreamClient = class extends ClientProxy {
617
1680
  });
618
1681
  return;
619
1682
  }
620
- const timeoutId = setTimeout(() => {
621
- if (!this.pendingMessages.has(correlationId)) return;
622
- this.pendingTimeouts.delete(correlationId);
623
- this.pendingMessages.delete(correlationId);
624
- this.logger.error(`JetStream RPC timeout (${effectiveTimeout}ms): ${subject}`);
625
- this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
626
- callback({ err: new Error("RPC timeout"), response: null, isDisposed: true });
627
- }, effectiveTimeout);
628
- this.pendingTimeouts.set(correlationId, timeoutId);
629
- const hdrs = this.buildHeaders(customHeaders, {
630
- subject,
631
- correlationId,
632
- replyTo: this.inbox
633
- });
634
- await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
635
- headers: hdrs,
636
- msgID: messageId ?? nuid.next()
637
- });
1683
+ await context6.with(
1684
+ spanHandle.activeContext,
1685
+ () => this.connection.getJetStreamClient().publish(subject, encoded, {
1686
+ headers: hdrs,
1687
+ msgID: messageId ?? nuid.next()
1688
+ })
1689
+ );
1690
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
638
1691
  } catch (err) {
639
1692
  const existingTimeout = this.pendingTimeouts.get(correlationId);
640
1693
  if (existingTimeout) {
@@ -644,10 +1697,34 @@ var JetstreamClient = class extends ClientProxy {
644
1697
  if (!this.pendingMessages.has(correlationId)) return;
645
1698
  this.pendingMessages.delete(correlationId);
646
1699
  const error = err instanceof Error ? err : new Error("Unknown error");
647
- this.logger.error(`JetStream RPC publish error (${subject}):`, err);
1700
+ spanHandle.finish({ kind: "error" /* Error */, error });
1701
+ this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
1702
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1703
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
648
1704
  callback({ err: error, response: null, isDisposed: true });
649
1705
  }
650
1706
  }
1707
+ // hasHook is per-emit so late subscribers (JetstreamMetricsService during
1708
+ // OnApplicationBootstrap) still receive events.
1709
+ reportPublished(declaredPattern, kind, startedAt, status) {
1710
+ if (!this.eventBus.hasHook("published" /* Published */)) return;
1711
+ this.eventBus.emit(
1712
+ "published" /* Published */,
1713
+ declaredPattern,
1714
+ kind,
1715
+ performance.now() - startedAt,
1716
+ status
1717
+ );
1718
+ }
1719
+ reportRpcCompleted(declaredPattern, startedAt, status) {
1720
+ if (!this.eventBus.hasHook("rpcCompleted" /* RpcCompleted */)) return;
1721
+ this.eventBus.emit(
1722
+ "rpcCompleted" /* RpcCompleted */,
1723
+ declaredPattern,
1724
+ performance.now() - startedAt,
1725
+ status
1726
+ );
1727
+ }
651
1728
  /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
652
1729
  handleDisconnect() {
653
1730
  this.rejectPendingRpcs(new Error("Connection lost"));
@@ -827,7 +1904,7 @@ var MsgpackCodec = class {
827
1904
  };
828
1905
 
829
1906
  // src/connection/connection.provider.ts
830
- import { Logger as Logger2 } from "@nestjs/common";
1907
+ import { Logger as Logger6 } from "@nestjs/common";
831
1908
  import {
832
1909
  connect
833
1910
  } from "@nats-io/transport-node";
@@ -844,6 +1921,10 @@ var ConnectionProvider = class {
844
1921
  constructor(options, eventBus) {
845
1922
  this.options = options;
846
1923
  this.eventBus = eventBus;
1924
+ const derived = deriveOtelAttrs(options);
1925
+ this.otel = derived.otel;
1926
+ this.otelServiceName = derived.serviceName;
1927
+ this.otelEndpoint = derived.serverEndpoint;
847
1928
  this.nc$ = defer(() => this.getConnection()).pipe(
848
1929
  shareReplay({ bufferSize: 1, refCount: false })
849
1930
  );
@@ -856,12 +1937,16 @@ var ConnectionProvider = class {
856
1937
  nc$;
857
1938
  /** Live stream of connection status events (no replay). */
858
1939
  status$;
859
- logger = new Logger2("Jetstream:Connection");
1940
+ logger = new Logger6("Jetstream:Connection");
860
1941
  connection = null;
861
1942
  connectionPromise = null;
862
1943
  jsClient = null;
863
1944
  jsmInstance = null;
864
1945
  jsmPromise = null;
1946
+ otel;
1947
+ otelServiceName;
1948
+ otelEndpoint;
1949
+ lifecycleSpan = null;
865
1950
  /**
866
1951
  * Establish NATS connection. Idempotent — returns cached connection on subsequent calls.
867
1952
  *
@@ -911,230 +1996,959 @@ var ConnectionProvider = class {
911
1996
  get unwrap() {
912
1997
  return this.connection;
913
1998
  }
914
- /**
915
- * Gracefully shut down the connection.
916
- *
917
- * Sequence: drain → wait for close. Falls back to force-close on error.
918
- */
919
- async shutdown() {
920
- if (this.connectionPromise) {
921
- try {
922
- await this.connectionPromise;
923
- } catch {
1999
+ /**
2000
+ * Gracefully shut down the connection.
2001
+ *
2002
+ * Sequence: drain → wait for close. Falls back to force-close on error.
2003
+ */
2004
+ async shutdown() {
2005
+ if (this.connectionPromise) {
2006
+ try {
2007
+ await this.connectionPromise;
2008
+ } catch {
2009
+ }
2010
+ }
2011
+ if (!this.connection || this.connection.isClosed()) return;
2012
+ try {
2013
+ await withShutdownSpan(
2014
+ this.otel,
2015
+ { serviceName: this.otelServiceName, endpoint: this.otelEndpoint },
2016
+ async () => {
2017
+ try {
2018
+ await this.connection?.drain();
2019
+ await this.connection?.closed();
2020
+ } catch {
2021
+ try {
2022
+ await this.connection?.close();
2023
+ } catch {
2024
+ }
2025
+ }
2026
+ }
2027
+ );
2028
+ } finally {
2029
+ this.lifecycleSpan?.finish();
2030
+ this.lifecycleSpan = null;
2031
+ this.connection = null;
2032
+ this.connectionPromise = null;
2033
+ this.jsClient = null;
2034
+ this.jsmInstance = null;
2035
+ this.jsmPromise = null;
2036
+ }
2037
+ }
2038
+ async initJetStreamManager() {
2039
+ try {
2040
+ const nc = await this.getConnection();
2041
+ this.jsmInstance = await jetstreamManager(nc);
2042
+ this.logger.log("JetStream manager initialized");
2043
+ return this.jsmInstance;
2044
+ } finally {
2045
+ this.jsmPromise = null;
2046
+ }
2047
+ }
2048
+ /** Internal: establish the physical connection with reconnect monitoring. */
2049
+ async establish() {
2050
+ try {
2051
+ const nc = await connect({
2052
+ ...DEFAULT_OPTIONS,
2053
+ // Default the NATS connection name to the OTel-derived service name so
2054
+ // `nats server info` lines up with span attributes, but let user-supplied
2055
+ // `connectionOptions.name` win when set.
2056
+ name: this.otelServiceName,
2057
+ ...this.options.connectionOptions,
2058
+ servers: this.options.servers
2059
+ });
2060
+ this.connection = nc;
2061
+ this.logger.log(`NATS connection established: ${nc.getServer()}`);
2062
+ this.eventBus.emit("connect" /* Connect */, nc.getServer());
2063
+ this.lifecycleSpan?.finish();
2064
+ this.lifecycleSpan = beginConnectionLifecycleSpan(this.otel, {
2065
+ serviceName: this.otelServiceName,
2066
+ endpoint: this.otelEndpoint,
2067
+ server: nc.getServer()
2068
+ });
2069
+ this.monitorStatus(nc);
2070
+ return nc;
2071
+ } catch (err) {
2072
+ if (err instanceof Error && err.message.includes("REFUSED")) {
2073
+ throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
2074
+ }
2075
+ throw err;
2076
+ }
2077
+ }
2078
+ /** Handle a single `nc.status()` event, emitting hooks and span events. */
2079
+ handleStatusEvent(status, nc) {
2080
+ switch (status.type) {
2081
+ case "disconnect":
2082
+ this.eventBus.emit("disconnect" /* Disconnect */);
2083
+ this.lifecycleSpan?.recordEvent(EVENT_CONNECTION_DISCONNECTED);
2084
+ break;
2085
+ case "reconnect":
2086
+ this.jsClient = null;
2087
+ this.jsmInstance = null;
2088
+ this.jsmPromise = null;
2089
+ this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
2090
+ this.lifecycleSpan?.recordEvent(EVENT_CONNECTION_RECONNECTED, {
2091
+ [ATTR_NATS_CONNECTION_SERVER]: nc.getServer()
2092
+ });
2093
+ break;
2094
+ case "error":
2095
+ this.eventBus.emit(
2096
+ "error" /* Error */,
2097
+ status.error,
2098
+ "connection"
2099
+ );
2100
+ break;
2101
+ case "update":
2102
+ case "ldm":
2103
+ case "reconnecting":
2104
+ case "ping":
2105
+ case "staleConnection":
2106
+ case "forceReconnect":
2107
+ case "slowConsumer":
2108
+ case "close":
2109
+ break;
2110
+ default: {
2111
+ const _exhaustive = status;
2112
+ const unknown = _exhaustive.type ?? "unknown";
2113
+ this.logger.warn(`Unhandled NATS status event: ${unknown}`);
2114
+ }
2115
+ }
2116
+ }
2117
+ /** Subscribe to connection status events and emit hooks. */
2118
+ monitorStatus(nc) {
2119
+ void (async () => {
2120
+ try {
2121
+ for await (const status of nc.status()) {
2122
+ this.handleStatusEvent(status, nc);
2123
+ }
2124
+ } finally {
2125
+ this.lifecycleSpan?.finish();
2126
+ this.lifecycleSpan = null;
2127
+ }
2128
+ })().catch((err) => {
2129
+ this.logger.error("Status monitor error", err);
2130
+ });
2131
+ }
2132
+ };
2133
+
2134
+ // src/hooks/event-bus.ts
2135
+ var EventBus = class {
2136
+ hooks;
2137
+ logger;
2138
+ subscribers = /* @__PURE__ */ new Map();
2139
+ constructor(logger5, hooks) {
2140
+ this.logger = logger5;
2141
+ this.hooks = hooks ?? {};
2142
+ }
2143
+ /**
2144
+ * Subscribe to a transport event. Used by built-in observers (e.g. metrics).
2145
+ * Multiple subscribers per event are supported; each is called independently.
2146
+ */
2147
+ subscribe(event, handler) {
2148
+ const list = this.subscribers.get(event) ?? [];
2149
+ list.push(handler);
2150
+ this.subscribers.set(event, list);
2151
+ }
2152
+ /**
2153
+ * Emit a lifecycle event. Dispatches to all internal subscribers and the
2154
+ * registered user hook (if any).
2155
+ */
2156
+ emit(event, ...args) {
2157
+ this.dispatch(event, args);
2158
+ }
2159
+ /**
2160
+ * Hot-path optimized emit for MessageRouted events.
2161
+ * Avoids rest/spread overhead of the generic `emit()`.
2162
+ */
2163
+ emitMessageRouted(subject, kind) {
2164
+ this.dispatch("messageRouted" /* MessageRouted */, [subject, kind]);
2165
+ }
2166
+ /**
2167
+ * Check whether any listener (user hook or internal subscriber) is registered
2168
+ * for the given event. Used by routing hot path to elide the emit call when
2169
+ * no one is listening.
2170
+ */
2171
+ hasHook(event) {
2172
+ return this.hooks[event] !== void 0 || (this.subscribers.get(event)?.length ?? 0) > 0;
2173
+ }
2174
+ dispatch(event, args) {
2175
+ const subs = this.subscribers.get(event);
2176
+ if (subs?.length) {
2177
+ for (const sub of [...subs]) {
2178
+ this.callHook(event, sub, ...args);
2179
+ }
2180
+ }
2181
+ const hook = this.hooks[event];
2182
+ if (hook) {
2183
+ this.callHook(event, hook, ...args);
2184
+ }
2185
+ }
2186
+ callHook(event, hook, ...args) {
2187
+ try {
2188
+ const result = hook(...args);
2189
+ if (result && typeof result.catch === "function") {
2190
+ result.catch((err) => {
2191
+ this.logger.error(
2192
+ `Async hook "${event}" rejected: ${err instanceof Error ? err.message : err}`
2193
+ );
2194
+ });
2195
+ }
2196
+ } catch (err) {
2197
+ this.logger.error(
2198
+ `Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
2199
+ );
2200
+ }
2201
+ }
2202
+ };
2203
+
2204
+ // src/health/jetstream.health-indicator.ts
2205
+ import { Injectable, Logger as Logger7 } from "@nestjs/common";
2206
+ var JetstreamHealthIndicator = class {
2207
+ constructor(connection) {
2208
+ this.connection = connection;
2209
+ }
2210
+ logger = new Logger7("Jetstream:Health");
2211
+ /**
2212
+ * Plain health status check.
2213
+ *
2214
+ * Returns the current connection status without throwing.
2215
+ * Use this for custom health endpoints or monitoring integrations.
2216
+ *
2217
+ * @returns Connection status with server URL and RTT latency.
2218
+ */
2219
+ async check() {
2220
+ const nc = this.connection.unwrap;
2221
+ if (!nc || nc.isClosed()) {
2222
+ return { connected: false, server: null, latency: null };
2223
+ }
2224
+ try {
2225
+ const start = performance.now();
2226
+ await nc.rtt();
2227
+ const latency = Math.round(performance.now() - start);
2228
+ return { connected: true, server: nc.getServer(), latency };
2229
+ } catch (err) {
2230
+ this.logger.warn(`Health check failed: ${err instanceof Error ? err.message : err}`);
2231
+ return { connected: false, server: nc.getServer(), latency: null };
2232
+ }
2233
+ }
2234
+ /**
2235
+ * Terminus-compatible health check.
2236
+ *
2237
+ * Returns `{ [key]: { status: 'up', ... } }` on success.
2238
+ * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
2239
+ *
2240
+ * The thrown error sets `isHealthCheckError: true` and `causes` — the
2241
+ * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
2242
+ * health failures from unexpected exceptions. Works with both Terminus v10
2243
+ * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
2244
+ *
2245
+ * @param key - Health indicator key (default: `'jetstream'`).
2246
+ * @returns Object with status, server, and latency under the given key.
2247
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
2248
+ */
2249
+ async isHealthy(key = "jetstream") {
2250
+ const status = await this.check();
2251
+ const details = {
2252
+ status: status.connected ? "up" : "down",
2253
+ server: status.server,
2254
+ latency: status.latency
2255
+ };
2256
+ if (!status.connected) {
2257
+ const causes = { [key]: details };
2258
+ throw Object.assign(new Error("Jetstream health check failed"), {
2259
+ causes,
2260
+ isHealthCheckError: true
2261
+ });
2262
+ }
2263
+ return { [key]: details };
2264
+ }
2265
+ };
2266
+ JetstreamHealthIndicator = __decorateClass([
2267
+ Injectable()
2268
+ ], JetstreamHealthIndicator);
2269
+
2270
+ // src/metrics/metrics.module.ts
2271
+ import { Module } from "@nestjs/common";
2272
+
2273
+ // src/server/routing/pattern-registry.ts
2274
+ import { Logger as Logger8 } from "@nestjs/common";
2275
+ var HANDLER_LABELS = {
2276
+ ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2277
+ ["ordered" /* Ordered */]: "ordered" /* Ordered */,
2278
+ ["ev" /* Event */]: "event" /* Event */,
2279
+ ["cmd" /* Command */]: "rpc" /* Rpc */
2280
+ };
2281
+ var PatternRegistry = class {
2282
+ constructor(options) {
2283
+ this.options = options;
2284
+ }
2285
+ logger = new Logger8("Jetstream:PatternRegistry");
2286
+ registry = /* @__PURE__ */ new Map();
2287
+ // Cached after registerHandlers() — the registry is immutable from that point
2288
+ cachedPatterns = null;
2289
+ _hasEvents = false;
2290
+ _hasCommands = false;
2291
+ _hasBroadcasts = false;
2292
+ _hasOrdered = false;
2293
+ _hasMetadata = false;
2294
+ /**
2295
+ * Register all handlers from the NestJS strategy.
2296
+ *
2297
+ * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2298
+ */
2299
+ registerHandlers(handlers) {
2300
+ const serviceName = this.options.name;
2301
+ for (const [pattern, handler] of handlers) {
2302
+ const extras = handler.extras;
2303
+ const isEvent = handler.isEventHandler ?? false;
2304
+ const isBroadcast = !!extras?.broadcast;
2305
+ const isOrdered = !!extras?.ordered;
2306
+ const meta = extras?.meta;
2307
+ if (isBroadcast && isOrdered) {
2308
+ throw new Error(
2309
+ `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
2310
+ );
2311
+ }
2312
+ let kind;
2313
+ if (isBroadcast) kind = "broadcast" /* Broadcast */;
2314
+ else if (isOrdered) kind = "ordered" /* Ordered */;
2315
+ else if (isEvent) kind = "ev" /* Event */;
2316
+ else kind = "cmd" /* Command */;
2317
+ const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2318
+ this.registry.set(fullSubject, {
2319
+ handler,
2320
+ pattern,
2321
+ isEvent: isEvent && !isOrdered,
2322
+ isBroadcast,
2323
+ isOrdered,
2324
+ meta
2325
+ });
2326
+ this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
2327
+ }
2328
+ this.cachedPatterns = this.buildPatternsByKind();
2329
+ this._hasEvents = this.cachedPatterns.events.length > 0;
2330
+ this._hasCommands = this.cachedPatterns.commands.length > 0;
2331
+ this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
2332
+ this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2333
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
2334
+ this.logSummary();
2335
+ }
2336
+ /** Find handler for a full NATS subject. */
2337
+ getHandler(subject) {
2338
+ return this.registry.get(subject)?.handler ?? null;
2339
+ }
2340
+ /**
2341
+ * Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
2342
+ *
2343
+ * Returns `null` when the subject is not registered. The declared pattern is
2344
+ * the value the user passed to `@EventPattern`/`@MessagePattern` — stable and
2345
+ * bounded, suitable for use as a Prometheus label without cardinality risk.
2346
+ */
2347
+ resolveDeclared(subject) {
2348
+ const entry = this.registry.get(subject);
2349
+ if (!entry) return null;
2350
+ return { pattern: entry.pattern, kind: this.resolveStreamKind(entry) };
2351
+ }
2352
+ /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2353
+ getBroadcastPatterns() {
2354
+ return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2355
+ }
2356
+ hasBroadcastHandlers() {
2357
+ return this._hasBroadcasts;
2358
+ }
2359
+ hasRpcHandlers() {
2360
+ return this._hasCommands;
2361
+ }
2362
+ hasEventHandlers() {
2363
+ return this._hasEvents;
2364
+ }
2365
+ hasOrderedHandlers() {
2366
+ return this._hasOrdered;
2367
+ }
2368
+ /** Get fully-qualified NATS subjects for ordered handlers. */
2369
+ getOrderedSubjects() {
2370
+ return this.getPatternsByKind().ordered.map(
2371
+ (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2372
+ );
2373
+ }
2374
+ /** Check if any registered handler has metadata. */
2375
+ hasMetadata() {
2376
+ return this._hasMetadata;
2377
+ }
2378
+ /**
2379
+ * Get handler metadata entries for KV publishing.
2380
+ *
2381
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2382
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2383
+ */
2384
+ getMetadataEntries() {
2385
+ const entries = /* @__PURE__ */ new Map();
2386
+ for (const entry of this.registry.values()) {
2387
+ if (!entry.meta) continue;
2388
+ const kind = this.resolveStreamKind(entry);
2389
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2390
+ entries.set(key, entry.meta);
2391
+ }
2392
+ return entries;
2393
+ }
2394
+ /** Get patterns grouped by kind (cached after registration). */
2395
+ getPatternsByKind() {
2396
+ const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
2397
+ return {
2398
+ events: [...patterns.events],
2399
+ commands: [...patterns.commands],
2400
+ broadcasts: [...patterns.broadcasts],
2401
+ ordered: [...patterns.ordered]
2402
+ };
2403
+ }
2404
+ /** Normalize a full NATS subject back to the user-facing pattern. */
2405
+ normalizeSubject(subject) {
2406
+ const name = internalName(this.options.name);
2407
+ const prefixes = [
2408
+ `${name}.${"cmd" /* Command */}.`,
2409
+ `${name}.${"ev" /* Event */}.`,
2410
+ `${name}.${"ordered" /* Ordered */}.`,
2411
+ `${"broadcast" /* Broadcast */}.`
2412
+ ];
2413
+ for (const prefix of prefixes) {
2414
+ if (subject.startsWith(prefix)) {
2415
+ return subject.slice(prefix.length);
2416
+ }
2417
+ }
2418
+ return subject;
2419
+ }
2420
+ buildPatternsByKind() {
2421
+ const events = [];
2422
+ const commands = [];
2423
+ const broadcasts = [];
2424
+ const ordered = [];
2425
+ for (const entry of this.registry.values()) {
2426
+ if (entry.isBroadcast) broadcasts.push(entry.pattern);
2427
+ else if (entry.isOrdered) ordered.push(entry.pattern);
2428
+ else if (entry.isEvent) events.push(entry.pattern);
2429
+ else commands.push(entry.pattern);
2430
+ }
2431
+ return { events, commands, broadcasts, ordered };
2432
+ }
2433
+ resolveStreamKind(entry) {
2434
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2435
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2436
+ if (entry.isEvent) return "ev" /* Event */;
2437
+ return "cmd" /* Command */;
2438
+ }
2439
+ logSummary() {
2440
+ const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2441
+ const parts = [
2442
+ `${commands.length} RPC`,
2443
+ `${events.length} events`,
2444
+ `${broadcasts.length} broadcasts`
2445
+ ];
2446
+ if (ordered.length > 0) {
2447
+ parts.push(`${ordered.length} ordered`);
2448
+ }
2449
+ this.logger.log(`Registered handlers: ${parts.join(", ")}`);
2450
+ }
2451
+ };
2452
+
2453
+ // src/metrics/metrics.constants.ts
2454
+ var JETSTREAM_METRICS_CONFIG = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_CONFIG");
2455
+ var JETSTREAM_METRICS_REGISTRY = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_REGISTRY");
2456
+ var JETSTREAM_METRICS_PROM_CLIENT = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_PROM_CLIENT");
2457
+ var DEFAULT_METRICS_PREFIX = "jetstream_";
2458
+ var DEFAULT_POLL_INTERVAL_MS = 15e3;
2459
+ var DEFAULT_HISTOGRAM_BUCKETS = {
2460
+ handlerDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
2461
+ publishDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
2462
+ rpcDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
2463
+ };
2464
+ var ERROR_CONTEXT_PREFIXES = [
2465
+ ["connection", "connection"],
2466
+ ["codec", "codec"],
2467
+ ["client-rpc", "publish"],
2468
+ ["jetstream-rpc-publish", "publish"],
2469
+ ["publish", "publish"],
2470
+ ["message-provider", "consume"],
2471
+ ["consume", "consume"],
2472
+ ["core-rpc-handler", "handler"],
2473
+ ["rpc-handler", "handler"],
2474
+ // EventRouter formats contexts as `${StreamKind.*}-handler:...` — the enum
2475
+ // uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
2476
+ ["ev-handler", "handler"],
2477
+ ["event-handler", "handler"],
2478
+ ["broadcast-handler", "handler"],
2479
+ ["ordered-handler", "handler"],
2480
+ ["handler", "handler"],
2481
+ ["shutdown", "shutdown"]
2482
+ ];
2483
+ var UNMATCHED_SUBJECT_LABEL = "<unmatched>";
2484
+ var STREAM_KIND_LABEL = {
2485
+ ["ev" /* Event */]: "event",
2486
+ ["cmd" /* Command */]: "command",
2487
+ ["broadcast" /* Broadcast */]: "broadcast",
2488
+ ["ordered" /* Ordered */]: "ordered"
2489
+ };
2490
+
2491
+ // src/metrics/metrics.service.ts
2492
+ import {
2493
+ Inject,
2494
+ Injectable as Injectable2,
2495
+ Logger as Logger10,
2496
+ Optional
2497
+ } from "@nestjs/common";
2498
+
2499
+ // src/metrics/error-context-mapper.ts
2500
+ var mapErrorContext = (context7) => {
2501
+ if (!context7) return "other";
2502
+ for (const [prefix, mapped] of ERROR_CONTEXT_PREFIXES) {
2503
+ if (context7 === prefix || context7.startsWith(`${prefix}:`)) {
2504
+ return mapped;
2505
+ }
2506
+ }
2507
+ return "other";
2508
+ };
2509
+
2510
+ // src/metrics/metrics.factory.ts
2511
+ var createMetrics = (opts) => {
2512
+ const { register, promClient } = opts;
2513
+ const prefix = opts.prefix ?? DEFAULT_METRICS_PREFIX;
2514
+ const buckets = {
2515
+ handlerDuration: opts.buckets?.handlerDuration ?? DEFAULT_HISTOGRAM_BUCKETS.handlerDuration,
2516
+ publishDuration: opts.buckets?.publishDuration ?? DEFAULT_HISTOGRAM_BUCKETS.publishDuration,
2517
+ rpcDuration: opts.buckets?.rpcDuration ?? DEFAULT_HISTOGRAM_BUCKETS.rpcDuration
2518
+ };
2519
+ if (opts.defaultLabels && Object.keys(opts.defaultLabels).length > 0) {
2520
+ register.setDefaultLabels(opts.defaultLabels);
2521
+ }
2522
+ const counter = (name, help, labelNames) => new promClient.Counter({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2523
+ const histogram = (name, help, labelNames, bucketArr) => new promClient.Histogram({
2524
+ name: `${prefix}${name}`,
2525
+ help,
2526
+ labelNames,
2527
+ buckets: bucketArr,
2528
+ registers: [register]
2529
+ });
2530
+ const gauge = (name, help, labelNames) => new promClient.Gauge({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2531
+ return {
2532
+ messagesReceivedTotal: counter(
2533
+ "messages_received_total",
2534
+ "Total messages routed to a handler.",
2535
+ ["stream", "subject", "kind"]
2536
+ ),
2537
+ messagesProcessedTotal: counter(
2538
+ "messages_processed_total",
2539
+ "Total messages whose handler completed.",
2540
+ ["stream", "subject", "kind", "status"]
2541
+ ),
2542
+ messagesUnhandledTotal: counter(
2543
+ "messages_unhandled_total",
2544
+ "Messages received but not matching any registered handler.",
2545
+ ["subject"]
2546
+ ),
2547
+ messagesDeadLetterTotal: counter(
2548
+ "messages_dead_letter_total",
2549
+ "Messages routed to dead-letter after exhausting redelivery attempts.",
2550
+ ["stream", "subject"]
2551
+ ),
2552
+ publishTotal: counter(
2553
+ "publish_total",
2554
+ "Total publish/send operations performed by the client.",
2555
+ ["subject", "kind", "status"]
2556
+ ),
2557
+ rpcTimeoutTotal: counter("rpc_timeout_total", "RPC calls that exceeded the timeout deadline.", [
2558
+ "subject"
2559
+ ]),
2560
+ consumerRecoveredTotal: counter(
2561
+ "consumer_recovered_total",
2562
+ "Self-healing recoveries after consume-loop failures.",
2563
+ ["kind"]
2564
+ ),
2565
+ errorsTotal: counter("errors_total", "Transport-level errors emitted on the EventBus.", [
2566
+ "context"
2567
+ ]),
2568
+ handlerDurationSeconds: histogram(
2569
+ "handler_duration_seconds",
2570
+ "Wall-clock duration of handler execution.",
2571
+ ["stream", "subject", "kind", "status"],
2572
+ buckets.handlerDuration
2573
+ ),
2574
+ publishDurationSeconds: histogram(
2575
+ "publish_duration_seconds",
2576
+ "Wall-clock duration of client publish/send operations.",
2577
+ ["subject", "kind", "status"],
2578
+ buckets.publishDuration
2579
+ ),
2580
+ rpcDurationSeconds: histogram(
2581
+ "rpc_duration_seconds",
2582
+ "Wall-clock duration of RPC round-trips from client perspective.",
2583
+ ["subject", "status"],
2584
+ buckets.rpcDuration
2585
+ ),
2586
+ consumerNumPending: gauge(
2587
+ "consumer_num_pending",
2588
+ "Messages not yet delivered to this consumer.",
2589
+ ["stream", "consumer", "kind"]
2590
+ ),
2591
+ consumerNumAckPending: gauge(
2592
+ "consumer_num_ack_pending",
2593
+ "Messages delivered but not yet acked.",
2594
+ ["stream", "consumer", "kind"]
2595
+ ),
2596
+ consumerNumRedelivered: gauge(
2597
+ "consumer_num_redelivered",
2598
+ "Messages currently in redelivery state.",
2599
+ ["stream", "consumer", "kind"]
2600
+ ),
2601
+ consumerNumWaiting: gauge(
2602
+ "consumer_num_waiting",
2603
+ "Pull-request waiting count for this consumer.",
2604
+ ["stream", "consumer", "kind"]
2605
+ ),
2606
+ streamMessages: gauge("stream_messages", "Total messages stored in this stream.", ["stream"]),
2607
+ streamBytes: gauge("stream_bytes", "Total bytes stored in this stream.", ["stream"]),
2608
+ connectionUp: gauge("connection_up", "NATS connection state (1 connected, 0 disconnected).", [
2609
+ "server"
2610
+ ]),
2611
+ metricsPollErrorsTotal: counter(
2612
+ "metrics_poll_errors_total",
2613
+ "Errors encountered while polling JetStreamManager for gauge data.",
2614
+ ["target"]
2615
+ )
2616
+ };
2617
+ };
2618
+
2619
+ // src/metrics/poll-runner.ts
2620
+ import { Logger as Logger9 } from "@nestjs/common";
2621
+ var PollRunner = class {
2622
+ constructor(opts) {
2623
+ this.opts = opts;
2624
+ }
2625
+ logger = new Logger9("Jetstream:Metrics:Poll");
2626
+ timer = null;
2627
+ inFlight = null;
2628
+ start() {
2629
+ if (this.timer !== null) return;
2630
+ if (this.opts.intervalMs <= 0) return;
2631
+ if (this.opts.targets.length === 0) return;
2632
+ this.timer = setInterval(() => {
2633
+ if (this.inFlight !== null) {
2634
+ this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
2635
+ return;
924
2636
  }
2637
+ this.inFlight = this.tick().finally(() => {
2638
+ this.inFlight = null;
2639
+ });
2640
+ }, this.opts.intervalMs);
2641
+ }
2642
+ async stop() {
2643
+ if (this.timer !== null) {
2644
+ clearInterval(this.timer);
2645
+ this.timer = null;
925
2646
  }
926
- if (!this.connection || this.connection.isClosed()) return;
2647
+ if (this.inFlight !== null) await this.inFlight;
2648
+ }
2649
+ /** @internal Visible for tests. Runs one poll cycle. */
2650
+ async tick() {
2651
+ let jsm;
927
2652
  try {
928
- await this.connection.drain();
929
- await this.connection.closed();
2653
+ jsm = await this.opts.jsmFactory();
930
2654
  } catch {
2655
+ this.recordPollError("jsm.connect");
2656
+ return;
2657
+ }
2658
+ await Promise.all([this.pollConsumers(jsm), this.pollStreams(jsm)]);
2659
+ }
2660
+ async pollConsumers(jsm) {
2661
+ for (const target of this.opts.targets) {
931
2662
  try {
932
- await this.connection.close();
2663
+ const info = await jsm.consumers.info(target.stream, target.consumer);
2664
+ const labels = {
2665
+ stream: target.stream,
2666
+ consumer: target.consumer,
2667
+ kind: STREAM_KIND_LABEL[target.kind]
2668
+ };
2669
+ this.opts.metrics.consumerNumPending.labels(labels).set(info.num_pending);
2670
+ this.opts.metrics.consumerNumAckPending.labels(labels).set(info.num_ack_pending);
2671
+ this.opts.metrics.consumerNumRedelivered.labels(labels).set(info.num_redelivered);
2672
+ this.opts.metrics.consumerNumWaiting.labels(labels).set(info.num_waiting);
933
2673
  } catch {
2674
+ this.recordPollError("consumer.info");
934
2675
  }
935
- } finally {
936
- this.connection = null;
937
- this.connectionPromise = null;
938
- this.jsClient = null;
939
- this.jsmInstance = null;
940
- this.jsmPromise = null;
941
- }
942
- }
943
- async initJetStreamManager() {
944
- try {
945
- const nc = await this.getConnection();
946
- this.jsmInstance = await jetstreamManager(nc);
947
- this.logger.log("JetStream manager initialized");
948
- return this.jsmInstance;
949
- } finally {
950
- this.jsmPromise = null;
951
2676
  }
952
2677
  }
953
- /** Internal: establish the physical connection with reconnect monitoring. */
954
- async establish() {
955
- const name = internalName(this.options.name);
956
- try {
957
- const nc = await connect({
958
- ...DEFAULT_OPTIONS,
959
- ...this.options.connectionOptions,
960
- servers: this.options.servers,
961
- name
962
- });
963
- this.connection = nc;
964
- this.logger.log(`NATS connection established: ${nc.getServer()}`);
965
- this.eventBus.emit("connect" /* Connect */, nc.getServer());
966
- this.monitorStatus(nc);
967
- return nc;
968
- } catch (err) {
969
- if (err instanceof Error && err.message.includes("REFUSED")) {
970
- throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
2678
+ async pollStreams(jsm) {
2679
+ const uniqueStreams = new Set(this.opts.targets.map((t) => t.stream));
2680
+ for (const stream of uniqueStreams) {
2681
+ try {
2682
+ const info = await jsm.streams.info(stream);
2683
+ this.opts.metrics.streamMessages.labels({ stream }).set(info.state.messages);
2684
+ this.opts.metrics.streamBytes.labels({ stream }).set(info.state.bytes);
2685
+ } catch {
2686
+ this.recordPollError("stream.info");
971
2687
  }
972
- throw err;
973
2688
  }
974
2689
  }
975
- /** Subscribe to connection status events and emit hooks. */
976
- monitorStatus(nc) {
977
- void (async () => {
978
- for await (const status of nc.status()) {
979
- switch (status.type) {
980
- case "disconnect":
981
- this.eventBus.emit("disconnect" /* Disconnect */);
982
- break;
983
- case "reconnect":
984
- this.jsClient = null;
985
- this.jsmInstance = null;
986
- this.jsmPromise = null;
987
- this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
988
- break;
989
- case "error":
990
- this.eventBus.emit(
991
- "error" /* Error */,
992
- status.error,
993
- "connection"
994
- );
995
- break;
996
- case "update":
997
- case "ldm":
998
- case "reconnecting":
999
- case "ping":
1000
- case "staleConnection":
1001
- case "forceReconnect":
1002
- case "slowConsumer":
1003
- case "close":
1004
- break;
1005
- }
1006
- }
1007
- })().catch((err) => {
1008
- this.logger.error("Status monitor error", err);
1009
- });
2690
+ recordPollError(target) {
2691
+ this.opts.metrics.metricsPollErrorsTotal.labels({ target }).inc();
1010
2692
  }
1011
2693
  };
1012
2694
 
1013
- // src/hooks/event-bus.ts
1014
- var EventBus = class {
1015
- hooks;
1016
- logger;
1017
- constructor(logger, hooks) {
1018
- this.logger = logger;
1019
- this.hooks = hooks ?? {};
1020
- }
1021
- /**
1022
- * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
1023
- *
1024
- * @param event - The {@link TransportEvent} to emit.
1025
- * @param args - Arguments matching the hook signature for this event.
1026
- */
1027
- emit(event, ...args) {
1028
- const hook = this.hooks[event];
1029
- if (!hook) return;
1030
- this.callHook(event, hook, ...args);
2695
+ // src/metrics/metrics.service.ts
2696
+ var JetstreamMetricsService = class {
2697
+ constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
2698
+ this.eventBus = eventBus;
2699
+ this.config = config;
2700
+ this.promClient = promClient;
2701
+ this.options = options;
2702
+ this.patternRegistry = patternRegistry;
2703
+ this.connection = connection;
1031
2704
  }
1032
- /**
1033
- * Hot-path optimized emit for MessageRouted events.
1034
- * Avoids rest/spread overhead of the generic `emit()`.
1035
- */
1036
- emitMessageRouted(subject, kind) {
1037
- const hook = this.hooks["messageRouted" /* MessageRouted */];
1038
- if (!hook) return;
1039
- this.callHook(
1040
- "messageRouted" /* MessageRouted */,
1041
- hook,
1042
- subject,
1043
- kind
2705
+ logger = new Logger10("Jetstream:Metrics");
2706
+ metrics = null;
2707
+ pollRunner = null;
2708
+ activeServers = /* @__PURE__ */ new Set();
2709
+ async onApplicationBootstrap() {
2710
+ if (this.metrics !== null) return;
2711
+ if (!this.config.register) {
2712
+ throw new Error(
2713
+ "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
2714
+ );
2715
+ }
2716
+ this.metrics = createMetrics({
2717
+ register: this.config.register,
2718
+ promClient: this.promClient,
2719
+ prefix: this.config.prefix,
2720
+ defaultLabels: this.config.defaultLabels,
2721
+ buckets: this.config.buckets
2722
+ });
2723
+ this.subscribeToEvents();
2724
+ this.syncInitialConnectionState();
2725
+ this.startPolling();
2726
+ this.logger.log(
2727
+ `Metrics enabled (prefix=${this.config.prefix ?? DEFAULT_METRICS_PREFIX}, poll=${this.getEffectivePollInterval()}ms)`
1044
2728
  );
1045
2729
  }
2730
+ async onModuleDestroy() {
2731
+ await this.pollRunner?.stop();
2732
+ this.pollRunner = null;
2733
+ }
2734
+ /** @internal Visible for tests. `0` disables polling. */
2735
+ getEffectivePollInterval() {
2736
+ return this.config.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
2737
+ }
1046
2738
  /**
1047
- * Check whether a hook is registered for the given event.
1048
- *
1049
- * Used by the routing hot path to elide the emit call entirely when the
1050
- * transport owner did not register a listener.
2739
+ * NATS connects during early bootstrap, before this service subscribes to
2740
+ * the EventBus — the initial `Connect` emission misses us. Mirror the
2741
+ * current state here so `connection_up` reflects reality the moment metrics
2742
+ * come online; later disconnects/reconnects update it normally.
1051
2743
  */
1052
- hasHook(event) {
1053
- return this.hooks[event] !== void 0;
1054
- }
1055
- callHook(event, hook, ...args) {
1056
- try {
1057
- const result = hook(...args);
1058
- if (result && typeof result.catch === "function") {
1059
- result.catch((err) => {
1060
- this.logger.error(
1061
- `Async hook "${event}" rejected: ${err instanceof Error ? err.message : err}`
1062
- );
1063
- });
1064
- }
1065
- } catch (err) {
1066
- this.logger.error(
1067
- `Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
1068
- );
2744
+ syncInitialConnectionState() {
2745
+ const nc = this.connection?.unwrap;
2746
+ if (!nc) return;
2747
+ const server = nc.getServer();
2748
+ this.activeServers.add(server);
2749
+ this.metrics?.connectionUp.labels({ server }).set(1);
2750
+ }
2751
+ /** Skips polling for publisher-only deployments and when no kinds are active. */
2752
+ startPolling() {
2753
+ const interval = this.getEffectivePollInterval();
2754
+ const connection = this.connection;
2755
+ if (interval <= 0 || !this.patternRegistry || !connection || !this.metrics) return;
2756
+ const targets = this.buildPollTargets();
2757
+ if (targets.length === 0) return;
2758
+ this.pollRunner = new PollRunner({
2759
+ intervalMs: interval,
2760
+ jsmFactory: async () => connection.getJetStreamManager(),
2761
+ metrics: this.metrics,
2762
+ targets
2763
+ });
2764
+ this.pollRunner.start();
2765
+ }
2766
+ buildPollTargets() {
2767
+ const registry = this.patternRegistry;
2768
+ if (!registry) return [];
2769
+ const targets = [];
2770
+ if (registry.hasEventHandlers()) {
2771
+ targets.push({
2772
+ kind: "ev" /* Event */,
2773
+ stream: streamName(this.options.name, "ev" /* Event */),
2774
+ consumer: consumerName(this.options.name, "ev" /* Event */)
2775
+ });
2776
+ }
2777
+ if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
2778
+ targets.push({
2779
+ kind: "cmd" /* Command */,
2780
+ stream: streamName(this.options.name, "cmd" /* Command */),
2781
+ consumer: consumerName(this.options.name, "cmd" /* Command */)
2782
+ });
2783
+ }
2784
+ if (registry.hasBroadcastHandlers()) {
2785
+ targets.push({
2786
+ kind: "broadcast" /* Broadcast */,
2787
+ stream: streamName(this.options.name, "broadcast" /* Broadcast */),
2788
+ consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
2789
+ });
2790
+ }
2791
+ return targets;
2792
+ }
2793
+ subscribeToEvents() {
2794
+ this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
2795
+ this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
2796
+ this.eventBus.subscribe("reconnect" /* Reconnect */, this.onReconnect);
2797
+ this.eventBus.subscribe("error" /* Error */, this.onError);
2798
+ this.eventBus.subscribe("rpcTimeout" /* RpcTimeout */, this.onRpcTimeout);
2799
+ this.eventBus.subscribe("messageRouted" /* MessageRouted */, this.onMessageRouted);
2800
+ this.eventBus.subscribe("deadLetter" /* DeadLetter */, this.onDeadLetter);
2801
+ this.eventBus.subscribe("consumerRecovered" /* ConsumerRecovered */, this.onConsumerRecovered);
2802
+ this.eventBus.subscribe("handlerCompleted" /* HandlerCompleted */, this.onHandlerCompleted);
2803
+ this.eventBus.subscribe("published" /* Published */, this.onPublished);
2804
+ this.eventBus.subscribe("rpcCompleted" /* RpcCompleted */, this.onRpcCompleted);
2805
+ }
2806
+ onConnect = (server) => {
2807
+ this.activeServers.add(server);
2808
+ this.metrics?.connectionUp.labels({ server }).set(1);
2809
+ };
2810
+ onReconnect = (server) => {
2811
+ this.activeServers.add(server);
2812
+ this.metrics?.connectionUp.labels({ server }).set(1);
2813
+ };
2814
+ onDisconnect = () => {
2815
+ for (const server of this.activeServers) {
2816
+ this.metrics?.connectionUp.labels({ server }).set(0);
2817
+ }
2818
+ };
2819
+ onError = (_err, context7) => {
2820
+ this.metrics?.errorsTotal.labels({ context: mapErrorContext(context7) }).inc();
2821
+ };
2822
+ onRpcTimeout = (subject, _correlationId) => {
2823
+ const declared = this.resolveDeclared(subject);
2824
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2825
+ this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
2826
+ };
2827
+ // `_kind` collapses broadcast/ordered into MessageKind.Event — we use
2828
+ // declared.kind from PatternRegistry for the precise label instead.
2829
+ onMessageRouted = (subject, _kind) => {
2830
+ if (!this.metrics) return;
2831
+ const declared = this.resolveDeclared(subject);
2832
+ if (!declared) {
2833
+ this.metrics.messagesUnhandledTotal.labels({ subject: UNMATCHED_SUBJECT_LABEL }).inc();
2834
+ return;
1069
2835
  }
2836
+ this.metrics.messagesReceivedTotal.labels({
2837
+ stream: streamName(this.options.name, declared.kind),
2838
+ subject: declared.pattern,
2839
+ kind: STREAM_KIND_LABEL[declared.kind]
2840
+ }).inc();
2841
+ };
2842
+ onDeadLetter = (info) => {
2843
+ const declared = this.resolveDeclared(info.subject);
2844
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2845
+ this.metrics?.messagesDeadLetterTotal.labels({ stream: info.stream, subject: subjectLabel }).inc();
2846
+ };
2847
+ onConsumerRecovered = (label, _attempts) => {
2848
+ const kindLabel = STREAM_KIND_LABEL[label] ?? String(label);
2849
+ this.metrics?.consumerRecoveredTotal.labels({ kind: kindLabel }).inc();
2850
+ };
2851
+ onHandlerCompleted = (pattern, kind, durationMs, status) => {
2852
+ if (!this.metrics) return;
2853
+ const stream = streamName(this.options.name, kind);
2854
+ const kindLabel = STREAM_KIND_LABEL[kind];
2855
+ const labels = { stream, subject: pattern, kind: kindLabel, status };
2856
+ this.metrics.messagesProcessedTotal.labels(labels).inc();
2857
+ this.metrics.handlerDurationSeconds.labels(labels).observe(durationMs / 1e3);
2858
+ };
2859
+ onPublished = (pattern, kind, durationMs, status) => {
2860
+ if (!this.metrics) return;
2861
+ const labels = { subject: pattern, kind: STREAM_KIND_LABEL[kind], status };
2862
+ this.metrics.publishTotal.labels(labels).inc();
2863
+ this.metrics.publishDurationSeconds.labels(labels).observe(durationMs / 1e3);
2864
+ };
2865
+ onRpcCompleted = (pattern, durationMs, status) => {
2866
+ this.metrics?.rpcDurationSeconds.labels({ subject: pattern, status }).observe(durationMs / 1e3);
2867
+ };
2868
+ resolveDeclared(subject) {
2869
+ return this.patternRegistry?.resolveDeclared(subject) ?? null;
1070
2870
  }
1071
2871
  };
2872
+ JetstreamMetricsService = __decorateClass([
2873
+ Injectable2(),
2874
+ __decorateParam(1, Inject(JETSTREAM_METRICS_CONFIG)),
2875
+ __decorateParam(2, Inject(JETSTREAM_METRICS_PROM_CLIENT)),
2876
+ __decorateParam(3, Inject(JETSTREAM_OPTIONS)),
2877
+ __decorateParam(4, Optional()),
2878
+ __decorateParam(5, Optional()),
2879
+ __decorateParam(5, Inject(JETSTREAM_CONNECTION))
2880
+ ], JetstreamMetricsService);
1072
2881
 
1073
- // src/health/jetstream.health-indicator.ts
1074
- import { Injectable, Logger as Logger3 } from "@nestjs/common";
1075
- var JetstreamHealthIndicator = class {
1076
- constructor(connection) {
1077
- this.connection = connection;
2882
+ // src/metrics/metrics.module.ts
2883
+ var PROM_CLIENT_INSTALL_MESSAGE = "prom-client is required when JetstreamModule.forRoot({ metrics: ... }) is enabled. Install it with: pnpm add prom-client";
2884
+ var resolvePromClient = async () => {
2885
+ try {
2886
+ return await import("prom-client");
2887
+ } catch {
2888
+ throw new Error(PROM_CLIENT_INSTALL_MESSAGE);
1078
2889
  }
1079
- logger = new Logger3("Jetstream:Health");
1080
- /**
1081
- * Plain health status check.
1082
- *
1083
- * Returns the current connection status without throwing.
1084
- * Use this for custom health endpoints or monitoring integrations.
1085
- *
1086
- * @returns Connection status with server URL and RTT latency.
1087
- */
1088
- async check() {
1089
- const nc = this.connection.unwrap;
1090
- if (!nc || nc.isClosed()) {
1091
- return { connected: false, server: null, latency: null };
1092
- }
1093
- try {
1094
- const start = performance.now();
1095
- await nc.rtt();
1096
- const latency = Math.round(performance.now() - start);
1097
- return { connected: true, server: nc.getServer(), latency };
1098
- } catch (err) {
1099
- this.logger.warn(`Health check failed: ${err instanceof Error ? err.message : err}`);
1100
- return { connected: false, server: nc.getServer(), latency: null };
2890
+ };
2891
+ var normalizeMetricsConfig = (option, promClient) => {
2892
+ const user = option && option !== true ? option : {};
2893
+ return {
2894
+ register: user.register ?? promClient.register,
2895
+ prefix: user.prefix ?? DEFAULT_METRICS_PREFIX,
2896
+ defaultLabels: user.defaultLabels,
2897
+ pollInterval: user.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
2898
+ buckets: user.buckets
2899
+ };
2900
+ };
2901
+ var JetstreamMetricsModule = class {
2902
+ static forFeature(metricsOption) {
2903
+ if (!metricsOption) {
2904
+ return { module: JetstreamMetricsModule, providers: [], exports: [] };
1101
2905
  }
1102
- }
1103
- /**
1104
- * Terminus-compatible health check.
1105
- *
1106
- * Returns `{ [key]: { status: 'up', ... } }` on success.
1107
- * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
1108
- *
1109
- * The thrown error sets `isHealthCheckError: true` and `causes` — the
1110
- * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
1111
- * health failures from unexpected exceptions. Works with both Terminus v10
1112
- * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
1113
- *
1114
- * @param key - Health indicator key (default: `'jetstream'`).
1115
- * @returns Object with status, server, and latency under the given key.
1116
- * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
1117
- */
1118
- async isHealthy(key = "jetstream") {
1119
- const status = await this.check();
1120
- const details = {
1121
- status: status.connected ? "up" : "down",
1122
- server: status.server,
1123
- latency: status.latency
2906
+ const promClientProvider = {
2907
+ provide: JETSTREAM_METRICS_PROM_CLIENT,
2908
+ useFactory: async () => {
2909
+ const mod = await resolvePromClient();
2910
+ return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
2911
+ }
2912
+ };
2913
+ const configProvider = {
2914
+ provide: JETSTREAM_METRICS_CONFIG,
2915
+ useFactory: async () => {
2916
+ const mod = await resolvePromClient();
2917
+ return normalizeMetricsConfig(metricsOption, mod);
2918
+ }
2919
+ };
2920
+ const registryProvider = {
2921
+ provide: JETSTREAM_METRICS_REGISTRY,
2922
+ inject: [JETSTREAM_METRICS_CONFIG],
2923
+ useFactory: (cfg) => cfg.register
2924
+ };
2925
+ const serviceProvider = {
2926
+ provide: JetstreamMetricsService,
2927
+ inject: [
2928
+ JETSTREAM_EVENT_BUS,
2929
+ JETSTREAM_METRICS_CONFIG,
2930
+ JETSTREAM_METRICS_PROM_CLIENT,
2931
+ JETSTREAM_OPTIONS,
2932
+ { token: PatternRegistry, optional: true },
2933
+ { token: JETSTREAM_CONNECTION, optional: true }
2934
+ ],
2935
+ useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
2936
+ };
2937
+ return {
2938
+ module: JetstreamMetricsModule,
2939
+ providers: [promClientProvider, configProvider, registryProvider, serviceProvider],
2940
+ exports: [
2941
+ JetstreamMetricsService,
2942
+ JETSTREAM_METRICS_CONFIG,
2943
+ JETSTREAM_METRICS_REGISTRY,
2944
+ JETSTREAM_METRICS_PROM_CLIENT
2945
+ ]
1124
2946
  };
1125
- if (!status.connected) {
1126
- const causes = { [key]: details };
1127
- throw Object.assign(new Error("Jetstream health check failed"), {
1128
- causes,
1129
- isHealthCheckError: true
1130
- });
1131
- }
1132
- return { [key]: details };
1133
2947
  }
1134
2948
  };
1135
- JetstreamHealthIndicator = __decorateClass([
1136
- Injectable()
1137
- ], JetstreamHealthIndicator);
2949
+ JetstreamMetricsModule = __decorateClass([
2950
+ Module({})
2951
+ ], JetstreamMetricsModule);
1138
2952
 
1139
2953
  // src/server/strategy.ts
1140
2954
  import { Server } from "@nestjs/microservices";
@@ -1210,6 +3024,26 @@ var JetstreamStrategy = class extends Server {
1210
3024
  this.messageProvider.destroy();
1211
3025
  this.started = false;
1212
3026
  }
3027
+ /**
3028
+ * Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
3029
+ *
3030
+ * The base class silently overwrites duplicate RPC handlers (last wins) and appends
3031
+ * duplicate event handlers to a linked list. Both behaviors are hazardous in a
3032
+ * JetStream context: silent overwrite drops a handler the user wrote, and double
3033
+ * event dispatch double-acks/double-processes the same JetStream message.
3034
+ *
3035
+ * We treat any pattern collision as a fatal misconfiguration so it surfaces at
3036
+ * bootstrap instead of in production traffic.
3037
+ */
3038
+ addHandler(pattern, callback, isEventHandler = false, extras = {}) {
3039
+ const normalizedPattern = this.normalizePattern(pattern);
3040
+ if (this.messageHandlers.has(normalizedPattern)) {
3041
+ throw new Error(
3042
+ `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
3043
+ );
3044
+ }
3045
+ super.addHandler(pattern, callback, isEventHandler, extras);
3046
+ }
1213
3047
  /**
1214
3048
  * Register event listener (required by Server base class).
1215
3049
  *
@@ -1282,7 +3116,7 @@ var JetstreamStrategy = class extends Server {
1282
3116
  };
1283
3117
 
1284
3118
  // src/server/core-rpc.server.ts
1285
- import { Logger as Logger4 } from "@nestjs/common";
3119
+ import { Logger as Logger11 } from "@nestjs/common";
1286
3120
  import { headers as natsHeaders2 } from "@nats-io/transport-node";
1287
3121
 
1288
3122
  // src/context/rpc.context.ts
@@ -1292,9 +3126,6 @@ var RpcContext = class extends BaseRpcContext {
1292
3126
  _retryDelay;
1293
3127
  _shouldTerminate = false;
1294
3128
  _terminateReason;
1295
- // ---------------------------------------------------------------------------
1296
- // Message accessors
1297
- // ---------------------------------------------------------------------------
1298
3129
  /**
1299
3130
  * Get the underlying NATS message.
1300
3131
  *
@@ -1329,9 +3160,6 @@ var RpcContext = class extends BaseRpcContext {
1329
3160
  isJetStream() {
1330
3161
  return "ack" in this.args[0];
1331
3162
  }
1332
- // ---------------------------------------------------------------------------
1333
- // JetStream metadata (return undefined for Core NATS messages)
1334
- // ---------------------------------------------------------------------------
1335
3163
  /** How many times this message has been delivered. */
1336
3164
  getDeliveryCount() {
1337
3165
  return this.asJetStream()?.info.deliveryCount;
@@ -1353,9 +3181,6 @@ var RpcContext = class extends BaseRpcContext {
1353
3181
  getCallerName() {
1354
3182
  return this.getHeader("x-caller-name" /* CallerName */);
1355
3183
  }
1356
- // ---------------------------------------------------------------------------
1357
- // Handler-controlled settlement
1358
- // ---------------------------------------------------------------------------
1359
3184
  /**
1360
3185
  * Signal the transport to retry (nak) this message instead of acknowledging it.
1361
3186
  *
@@ -1400,9 +3225,6 @@ var RpcContext = class extends BaseRpcContext {
1400
3225
  throw new Error(`${method}() is only available for JetStream messages`);
1401
3226
  }
1402
3227
  }
1403
- // ---------------------------------------------------------------------------
1404
- // Transport-facing state (read by EventRouter)
1405
- // ---------------------------------------------------------------------------
1406
3228
  /** @internal */
1407
3229
  get shouldRetry() {
1408
3230
  return this._shouldRetry;
@@ -1524,7 +3346,7 @@ var unwrapResult = (result) => {
1524
3346
  }
1525
3347
  return result;
1526
3348
  };
1527
- var isPromiseLike = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
3349
+ var isPromiseLike2 = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
1528
3350
  var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
1529
3351
  let done = false;
1530
3352
  let subscription = null;
@@ -1554,20 +3376,25 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
1554
3376
  // src/server/core-rpc.server.ts
1555
3377
  var CoreRpcServer = class {
1556
3378
  constructor(options, connection, patternRegistry, codec, eventBus) {
1557
- this.options = options;
1558
3379
  this.connection = connection;
1559
3380
  this.patternRegistry = patternRegistry;
1560
3381
  this.codec = codec;
1561
3382
  this.eventBus = eventBus;
3383
+ const derived = deriveOtelAttrs(options);
3384
+ this.otel = derived.otel;
3385
+ this.serviceName = derived.serviceName;
3386
+ this.serverEndpoint = derived.serverEndpoint;
1562
3387
  }
1563
- logger = new Logger4("Jetstream:CoreRpc");
3388
+ logger = new Logger11("Jetstream:CoreRpc");
1564
3389
  subscription = null;
3390
+ otel;
3391
+ serviceName;
3392
+ serverEndpoint;
1565
3393
  /** Start listening for RPC requests on the command subject. */
1566
3394
  async start() {
1567
3395
  const nc = await this.connection.getConnection();
1568
- const serviceName = internalName(this.options.name);
1569
- const subject = `${serviceName}.cmd.>`;
1570
- const queue = `${serviceName}_cmd_queue`;
3396
+ const subject = `${this.serviceName}.cmd.>`;
3397
+ const queue = `${this.serviceName}_cmd_queue`;
1571
3398
  this.subscription = nc.subscribe(subject, {
1572
3399
  queue,
1573
3400
  callback: (err, msg) => {
@@ -1611,15 +3438,51 @@ var CoreRpcServer = class {
1611
3438
  return;
1612
3439
  }
1613
3440
  const ctx = new RpcContext([msg]);
3441
+ const startedAt = performance.now();
1614
3442
  try {
1615
- const raw = unwrapResult(handler(data, ctx));
1616
- const result = isPromiseLike(raw) ? await raw : raw;
1617
- msg.respond(this.codec.encode(result));
3443
+ const raw = await withConsumeSpan(
3444
+ {
3445
+ subject: msg.subject,
3446
+ msg,
3447
+ kind: "rpc" /* Rpc */,
3448
+ payloadBytes: msg.data.length,
3449
+ handlerMetadata: { pattern: msg.subject },
3450
+ serviceName: this.serviceName,
3451
+ endpoint: this.serverEndpoint
3452
+ },
3453
+ this.otel,
3454
+ () => {
3455
+ const out = unwrapResult(handler(data, ctx));
3456
+ return isPromiseLike2(out) ? out : out;
3457
+ }
3458
+ );
3459
+ msg.respond(this.codec.encode(raw));
3460
+ this.reportHandlerCompleted(msg, startedAt, "success");
1618
3461
  } catch (err) {
1619
- this.logger.error(`Handler error for Core RPC ${msg.subject}:`, err);
3462
+ this.eventBus.emit(
3463
+ "error" /* Error */,
3464
+ err instanceof Error ? err : new Error(String(err)),
3465
+ `core-rpc-handler:${msg.subject}`
3466
+ );
1620
3467
  this.respondWithError(msg, err);
3468
+ this.reportHandlerCompleted(msg, startedAt, "error");
1621
3469
  }
1622
3470
  }
3471
+ // See EventRouter.reportHandlerCompleted for the rationale on declared
3472
+ // pattern + per-emit hasHook check.
3473
+ reportHandlerCompleted(msg, startedAt, status) {
3474
+ if (!this.eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
3475
+ const declared = this.patternRegistry.resolveDeclared(msg.subject);
3476
+ const pattern = declared?.pattern ?? msg.subject;
3477
+ const kind = declared?.kind ?? "cmd" /* Command */;
3478
+ this.eventBus.emit(
3479
+ "handlerCompleted" /* HandlerCompleted */,
3480
+ pattern,
3481
+ kind,
3482
+ performance.now() - startedAt,
3483
+ status
3484
+ );
3485
+ }
1623
3486
  /** Send an error response back to the caller with x-error header. */
1624
3487
  respondWithError(msg, error) {
1625
3488
  try {
@@ -1633,7 +3496,7 @@ var CoreRpcServer = class {
1633
3496
  };
1634
3497
 
1635
3498
  // src/server/infrastructure/stream.provider.ts
1636
- import { Logger as Logger6 } from "@nestjs/common";
3499
+ import { Logger as Logger13 } from "@nestjs/common";
1637
3500
  import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
1638
3501
 
1639
3502
  // src/server/infrastructure/nats-error-codes.ts
@@ -1700,7 +3563,7 @@ var isEqual = (a, b) => {
1700
3563
  };
1701
3564
 
1702
3565
  // src/server/infrastructure/stream-migration.ts
1703
- import { Logger as Logger5 } from "@nestjs/common";
3566
+ import { Logger as Logger12 } from "@nestjs/common";
1704
3567
  import { JetStreamApiError } from "@nats-io/jetstream";
1705
3568
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
1706
3569
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
@@ -1709,7 +3572,7 @@ var StreamMigration = class {
1709
3572
  constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
1710
3573
  this.sourcingTimeoutMs = sourcingTimeoutMs;
1711
3574
  }
1712
- logger = new Logger5("Jetstream:Stream");
3575
+ logger = new Logger12("Jetstream:Stream");
1713
3576
  async migrate(jsm, streamName2, newConfig) {
1714
3577
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
1715
3578
  const startTime = Date.now();
@@ -1791,9 +3654,16 @@ var StreamProvider = class {
1791
3654
  constructor(options, connection) {
1792
3655
  this.options = options;
1793
3656
  this.connection = connection;
3657
+ const derived = deriveOtelAttrs(options);
3658
+ this.otel = derived.otel;
3659
+ this.otelServiceName = derived.serviceName;
3660
+ this.otelEndpoint = derived.serverEndpoint;
1794
3661
  }
1795
- logger = new Logger6("Jetstream:Stream");
3662
+ logger = new Logger13("Jetstream:Stream");
1796
3663
  migration = new StreamMigration();
3664
+ otel;
3665
+ otelServiceName;
3666
+ otelEndpoint;
1797
3667
  /**
1798
3668
  * Ensure all required streams exist with correct configuration.
1799
3669
  *
@@ -1839,32 +3709,56 @@ var StreamProvider = class {
1839
3709
  /** Ensure a single stream exists, creating or updating as needed. */
1840
3710
  async ensureStream(jsm, kind) {
1841
3711
  const config = this.buildConfig(kind);
1842
- this.logger.log(`Ensuring stream: ${config.name}`);
1843
- try {
1844
- const currentInfo = await jsm.streams.info(config.name);
1845
- return await this.handleExistingStream(jsm, currentInfo, config);
1846
- } catch (err) {
1847
- if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
1848
- this.logger.log(`Creating stream: ${config.name}`);
1849
- return await jsm.streams.add(config);
3712
+ return withProvisioningSpan(
3713
+ this.otel,
3714
+ {
3715
+ serviceName: this.otelServiceName,
3716
+ endpoint: this.otelEndpoint,
3717
+ entity: "stream",
3718
+ name: config.name,
3719
+ action: "ensure"
3720
+ },
3721
+ async () => {
3722
+ this.logger.log(`Ensuring stream: ${config.name}`);
3723
+ try {
3724
+ const currentInfo = await jsm.streams.info(config.name);
3725
+ return await this.handleExistingStream(jsm, currentInfo, config);
3726
+ } catch (err) {
3727
+ if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
3728
+ this.logger.log(`Creating stream: ${config.name}`);
3729
+ return await jsm.streams.add(config);
3730
+ }
3731
+ throw err;
3732
+ }
1850
3733
  }
1851
- throw err;
1852
- }
3734
+ );
1853
3735
  }
1854
3736
  /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
1855
3737
  async ensureDlqStream(jsm) {
1856
3738
  const config = this.buildDlqConfig();
1857
- this.logger.log(`Ensuring DLQ stream: ${config.name}`);
1858
- try {
1859
- const currentInfo = await jsm.streams.info(config.name);
1860
- return await this.handleExistingStream(jsm, currentInfo, config);
1861
- } catch (err) {
1862
- if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
1863
- this.logger.log(`Creating DLQ stream: ${config.name}`);
1864
- return await jsm.streams.add(config);
3739
+ return withProvisioningSpan(
3740
+ this.otel,
3741
+ {
3742
+ serviceName: this.otelServiceName,
3743
+ endpoint: this.otelEndpoint,
3744
+ entity: "stream",
3745
+ name: config.name,
3746
+ action: "ensure"
3747
+ },
3748
+ async () => {
3749
+ this.logger.log(`Ensuring DLQ stream: ${config.name}`);
3750
+ try {
3751
+ const currentInfo = await jsm.streams.info(config.name);
3752
+ return await this.handleExistingStream(jsm, currentInfo, config);
3753
+ } catch (err) {
3754
+ if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
3755
+ this.logger.log(`Creating DLQ stream: ${config.name}`);
3756
+ return await jsm.streams.add(config);
3757
+ }
3758
+ throw err;
3759
+ }
1865
3760
  }
1866
- throw err;
1867
- }
3761
+ );
1868
3762
  }
1869
3763
  async handleExistingStream(jsm, currentInfo, config) {
1870
3764
  const diff = compareStreamConfig(currentInfo.config, config);
@@ -1893,7 +3787,18 @@ var StreamProvider = class {
1893
3787
  }
1894
3788
  return currentInfo;
1895
3789
  }
1896
- await this.migration.migrate(jsm, config.name, config);
3790
+ await withMigrationSpan(
3791
+ this.otel,
3792
+ {
3793
+ serviceName: this.otelServiceName,
3794
+ endpoint: this.otelEndpoint,
3795
+ stream: config.name,
3796
+ reason: diff.changes.filter((c) => c.mutability === "immutable").map((c) => c.property).join(", ")
3797
+ },
3798
+ async () => {
3799
+ await this.migration.migrate(jsm, config.name, config);
3800
+ }
3801
+ );
1897
3802
  return await jsm.streams.info(config.name);
1898
3803
  }
1899
3804
  buildMutableOnlyConfig(config, currentConfig, diff) {
@@ -2009,7 +3914,7 @@ var StreamProvider = class {
2009
3914
  };
2010
3915
 
2011
3916
  // src/server/infrastructure/consumer.provider.ts
2012
- import { Logger as Logger7 } from "@nestjs/common";
3917
+ import { Logger as Logger14 } from "@nestjs/common";
2013
3918
  import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
2014
3919
  var ConsumerProvider = class {
2015
3920
  constructor(options, connection, streamProvider, patternRegistry) {
@@ -2017,8 +3922,15 @@ var ConsumerProvider = class {
2017
3922
  this.connection = connection;
2018
3923
  this.streamProvider = streamProvider;
2019
3924
  this.patternRegistry = patternRegistry;
2020
- }
2021
- logger = new Logger7("Jetstream:Consumer");
3925
+ const derived = deriveOtelAttrs(options);
3926
+ this.otel = derived.otel;
3927
+ this.otelServiceName = derived.serviceName;
3928
+ this.otelEndpoint = derived.serverEndpoint;
3929
+ }
3930
+ logger = new Logger14("Jetstream:Consumer");
3931
+ otel;
3932
+ otelServiceName;
3933
+ otelEndpoint;
2022
3934
  /**
2023
3935
  * Ensure consumers exist for the specified kinds.
2024
3936
  *
@@ -2048,17 +3960,29 @@ var ConsumerProvider = class {
2048
3960
  const stream = this.streamProvider.getStreamName(kind);
2049
3961
  const config = this.buildConfig(kind);
2050
3962
  const name = config.durable_name;
2051
- this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
2052
- try {
2053
- await jsm.consumers.info(stream, name);
2054
- this.logger.debug(`Consumer exists, updating: ${name}`);
2055
- return await jsm.consumers.update(stream, name, config);
2056
- } catch (err) {
2057
- if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2058
- throw err;
3963
+ return withProvisioningSpan(
3964
+ this.otel,
3965
+ {
3966
+ serviceName: this.otelServiceName,
3967
+ endpoint: this.otelEndpoint,
3968
+ entity: "consumer",
3969
+ name,
3970
+ action: "ensure"
3971
+ },
3972
+ async () => {
3973
+ this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
3974
+ try {
3975
+ await jsm.consumers.info(stream, name);
3976
+ this.logger.debug(`Consumer exists, updating: ${name}`);
3977
+ return await jsm.consumers.update(stream, name, config);
3978
+ } catch (err) {
3979
+ if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
3980
+ throw err;
3981
+ }
3982
+ return await this.createConsumer(jsm, stream, name, config);
3983
+ }
2059
3984
  }
2060
- return await this.createConsumer(jsm, stream, name, config);
2061
- }
3985
+ );
2062
3986
  }
2063
3987
  /**
2064
3988
  * Recover a consumer that disappeared during runtime.
@@ -2077,16 +4001,28 @@ var ConsumerProvider = class {
2077
4001
  const stream = this.streamProvider.getStreamName(kind);
2078
4002
  const config = this.buildConfig(kind);
2079
4003
  const name = config.durable_name;
2080
- this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
2081
- await this.assertNoMigrationInProgress(jsm, stream);
2082
- try {
2083
- return await jsm.consumers.info(stream, name);
2084
- } catch (err) {
2085
- if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2086
- throw err;
4004
+ return withProvisioningSpan(
4005
+ this.otel,
4006
+ {
4007
+ serviceName: this.otelServiceName,
4008
+ endpoint: this.otelEndpoint,
4009
+ entity: "consumer",
4010
+ name,
4011
+ action: "recover"
4012
+ },
4013
+ async () => {
4014
+ this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
4015
+ await this.assertNoMigrationInProgress(jsm, stream);
4016
+ try {
4017
+ return await jsm.consumers.info(stream, name);
4018
+ } catch (err) {
4019
+ if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4020
+ throw err;
4021
+ }
4022
+ return await this.createConsumer(jsm, stream, name, config);
4023
+ }
2087
4024
  }
2088
- return await this.createConsumer(jsm, stream, name, config);
2089
- }
4025
+ );
2090
4026
  }
2091
4027
  /**
2092
4028
  * Throw if a migration backup stream exists for this stream.
@@ -2202,7 +4138,7 @@ var ConsumerProvider = class {
2202
4138
  };
2203
4139
 
2204
4140
  // src/server/infrastructure/message.provider.ts
2205
- import { Logger as Logger8 } from "@nestjs/common";
4141
+ import { Logger as Logger15 } from "@nestjs/common";
2206
4142
  import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
2207
4143
  import {
2208
4144
  catchError,
@@ -2212,7 +4148,6 @@ import {
2212
4148
  repeat,
2213
4149
  Subject,
2214
4150
  takeUntil,
2215
- tap,
2216
4151
  timer
2217
4152
  } from "rxjs";
2218
4153
  var MessageProvider = class {
@@ -2222,7 +4157,7 @@ var MessageProvider = class {
2222
4157
  this.consumeOptionsMap = consumeOptionsMap;
2223
4158
  this.consumerRecoveryFn = consumerRecoveryFn;
2224
4159
  }
2225
- logger = new Logger8("Jetstream:Message");
4160
+ logger = new Logger15("Jetstream:Message");
2226
4161
  activeIterators = /* @__PURE__ */ new Set();
2227
4162
  orderedReadyResolve = null;
2228
4163
  orderedReadyReject = null;
@@ -2320,10 +4255,13 @@ var MessageProvider = class {
2320
4255
  /** Create a self-healing consumer flow for a specific kind. */
2321
4256
  createFlow(kind, info) {
2322
4257
  const target$ = this.getTargetSubject(kind);
2323
- return this.createSelfHealingFlow(() => this.consumeOnce(kind, info, target$), info.name);
4258
+ return this.createSelfHealingFlow(
4259
+ (onConnected) => this.consumeOnce(kind, info, target$, onConnected),
4260
+ info.name
4261
+ );
2324
4262
  }
2325
4263
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
2326
- async consumeOnce(kind, info, target$) {
4264
+ async consumeOnce(kind, info, target$, onConnected) {
2327
4265
  const js = this.connection.getJetStreamClient();
2328
4266
  let consumer;
2329
4267
  let consumerName2 = info.name;
@@ -2351,6 +4289,7 @@ var MessageProvider = class {
2351
4289
  });
2352
4290
  this.activeIterators.add(messages);
2353
4291
  this.monitorConsumerHealth(messages, consumerName2);
4292
+ onConnected();
2354
4293
  try {
2355
4294
  await messages.closed();
2356
4295
  } finally {
@@ -2405,7 +4344,7 @@ var MessageProvider = class {
2405
4344
  /** Create a self-healing ordered consumer flow. */
2406
4345
  createOrderedFlow(streamName2, consumerOpts) {
2407
4346
  return this.createSelfHealingFlow(
2408
- () => this.consumeOrderedOnce(streamName2, consumerOpts),
4347
+ (onConnected) => this.consumeOrderedOnce(streamName2, consumerOpts, onConnected),
2409
4348
  "ordered" /* Ordered */,
2410
4349
  (err) => {
2411
4350
  if (this.orderedReadyReject) {
@@ -2419,10 +4358,15 @@ var MessageProvider = class {
2419
4358
  /** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
2420
4359
  createSelfHealingFlow(source, label, onFirstError) {
2421
4360
  let consecutiveFailures = 0;
2422
- return defer2(source).pipe(
2423
- tap(() => {
2424
- consecutiveFailures = 0;
2425
- }),
4361
+ const onConnected = () => {
4362
+ if (consecutiveFailures > 0) {
4363
+ const attempts = consecutiveFailures;
4364
+ this.logger.log(`Consumer ${label} recovered after ${attempts} failed attempt(s)`);
4365
+ this.eventBus.emit("consumerRecovered" /* ConsumerRecovered */, label, attempts);
4366
+ }
4367
+ consecutiveFailures = 0;
4368
+ };
4369
+ return defer2(() => source(onConnected)).pipe(
2426
4370
  catchError((err) => {
2427
4371
  consecutiveFailures++;
2428
4372
  this.logger.error(`Consumer ${label} error, will restart:`, err);
@@ -2445,7 +4389,7 @@ var MessageProvider = class {
2445
4389
  );
2446
4390
  }
2447
4391
  /** Single iteration: create ordered consumer -> push messages into the subject. */
2448
- async consumeOrderedOnce(streamName2, consumerOpts) {
4392
+ async consumeOrderedOnce(streamName2, consumerOpts, onConnected) {
2449
4393
  const js = this.connection.getJetStreamClient();
2450
4394
  const consumer = await js.consumers.get(streamName2, consumerOpts);
2451
4395
  const orderedMessages$ = this.orderedMessages$;
@@ -2460,6 +4404,7 @@ var MessageProvider = class {
2460
4404
  this.orderedReadyReject = null;
2461
4405
  }
2462
4406
  this.activeIterators.add(messages);
4407
+ onConnected();
2463
4408
  try {
2464
4409
  await messages.closed();
2465
4410
  } finally {
@@ -2469,7 +4414,7 @@ var MessageProvider = class {
2469
4414
  };
2470
4415
 
2471
4416
  // src/server/infrastructure/metadata.provider.ts
2472
- import { Logger as Logger9 } from "@nestjs/common";
4417
+ import { Logger as Logger16 } from "@nestjs/common";
2473
4418
  import { Kvm } from "@nats-io/kv";
2474
4419
  var MetadataProvider = class {
2475
4420
  constructor(options, connection) {
@@ -2478,7 +4423,7 @@ var MetadataProvider = class {
2478
4423
  this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
2479
4424
  this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
2480
4425
  }
2481
- logger = new Logger9("Jetstream:Metadata");
4426
+ logger = new Logger16("Jetstream:Metadata");
2482
4427
  bucketName;
2483
4428
  replicas;
2484
4429
  ttl;
@@ -2570,177 +4515,14 @@ var MetadataProvider = class {
2570
4515
  }
2571
4516
  };
2572
4517
 
2573
- // src/server/routing/pattern-registry.ts
2574
- import { Logger as Logger10 } from "@nestjs/common";
2575
- var HANDLER_LABELS = {
2576
- ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2577
- ["ordered" /* Ordered */]: "ordered" /* Ordered */,
2578
- ["ev" /* Event */]: "event" /* Event */,
2579
- ["cmd" /* Command */]: "rpc" /* Rpc */
2580
- };
2581
- var PatternRegistry = class {
2582
- constructor(options) {
2583
- this.options = options;
2584
- }
2585
- logger = new Logger10("Jetstream:PatternRegistry");
2586
- registry = /* @__PURE__ */ new Map();
2587
- // Cached after registerHandlers() — the registry is immutable from that point
2588
- cachedPatterns = null;
2589
- _hasEvents = false;
2590
- _hasCommands = false;
2591
- _hasBroadcasts = false;
2592
- _hasOrdered = false;
2593
- _hasMetadata = false;
2594
- /**
2595
- * Register all handlers from the NestJS strategy.
2596
- *
2597
- * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2598
- */
2599
- registerHandlers(handlers) {
2600
- const serviceName = this.options.name;
2601
- for (const [pattern, handler] of handlers) {
2602
- const extras = handler.extras;
2603
- const isEvent = handler.isEventHandler ?? false;
2604
- const isBroadcast = !!extras?.broadcast;
2605
- const isOrdered = !!extras?.ordered;
2606
- const meta = extras?.meta;
2607
- if (isBroadcast && isOrdered) {
2608
- throw new Error(
2609
- `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
2610
- );
2611
- }
2612
- let kind;
2613
- if (isBroadcast) kind = "broadcast" /* Broadcast */;
2614
- else if (isOrdered) kind = "ordered" /* Ordered */;
2615
- else if (isEvent) kind = "ev" /* Event */;
2616
- else kind = "cmd" /* Command */;
2617
- const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2618
- this.registry.set(fullSubject, {
2619
- handler,
2620
- pattern,
2621
- isEvent: isEvent && !isOrdered,
2622
- isBroadcast,
2623
- isOrdered,
2624
- meta
2625
- });
2626
- this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
2627
- }
2628
- this.cachedPatterns = this.buildPatternsByKind();
2629
- this._hasEvents = this.cachedPatterns.events.length > 0;
2630
- this._hasCommands = this.cachedPatterns.commands.length > 0;
2631
- this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
2632
- this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2633
- this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
2634
- this.logSummary();
2635
- }
2636
- /** Find handler for a full NATS subject. */
2637
- getHandler(subject) {
2638
- return this.registry.get(subject)?.handler ?? null;
2639
- }
2640
- /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2641
- getBroadcastPatterns() {
2642
- return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2643
- }
2644
- hasBroadcastHandlers() {
2645
- return this._hasBroadcasts;
2646
- }
2647
- hasRpcHandlers() {
2648
- return this._hasCommands;
2649
- }
2650
- hasEventHandlers() {
2651
- return this._hasEvents;
2652
- }
2653
- hasOrderedHandlers() {
2654
- return this._hasOrdered;
2655
- }
2656
- /** Get fully-qualified NATS subjects for ordered handlers. */
2657
- getOrderedSubjects() {
2658
- return this.getPatternsByKind().ordered.map(
2659
- (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2660
- );
2661
- }
2662
- /** Check if any registered handler has metadata. */
2663
- hasMetadata() {
2664
- return this._hasMetadata;
2665
- }
2666
- /**
2667
- * Get handler metadata entries for KV publishing.
2668
- *
2669
- * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2670
- * Key format: `{serviceName}.{kind}.{pattern}`.
2671
- */
2672
- getMetadataEntries() {
2673
- const entries = /* @__PURE__ */ new Map();
2674
- for (const entry of this.registry.values()) {
2675
- if (!entry.meta) continue;
2676
- const kind = this.resolveStreamKind(entry);
2677
- const key = metadataKey(this.options.name, kind, entry.pattern);
2678
- entries.set(key, entry.meta);
2679
- }
2680
- return entries;
2681
- }
2682
- /** Get patterns grouped by kind (cached after registration). */
2683
- getPatternsByKind() {
2684
- const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
2685
- return {
2686
- events: [...patterns.events],
2687
- commands: [...patterns.commands],
2688
- broadcasts: [...patterns.broadcasts],
2689
- ordered: [...patterns.ordered]
2690
- };
2691
- }
2692
- /** Normalize a full NATS subject back to the user-facing pattern. */
2693
- normalizeSubject(subject) {
2694
- const name = internalName(this.options.name);
2695
- const prefixes = [
2696
- `${name}.${"cmd" /* Command */}.`,
2697
- `${name}.${"ev" /* Event */}.`,
2698
- `${name}.${"ordered" /* Ordered */}.`,
2699
- `${"broadcast" /* Broadcast */}.`
2700
- ];
2701
- for (const prefix of prefixes) {
2702
- if (subject.startsWith(prefix)) {
2703
- return subject.slice(prefix.length);
2704
- }
2705
- }
2706
- return subject;
2707
- }
2708
- buildPatternsByKind() {
2709
- const events = [];
2710
- const commands = [];
2711
- const broadcasts = [];
2712
- const ordered = [];
2713
- for (const entry of this.registry.values()) {
2714
- if (entry.isBroadcast) broadcasts.push(entry.pattern);
2715
- else if (entry.isOrdered) ordered.push(entry.pattern);
2716
- else if (entry.isEvent) events.push(entry.pattern);
2717
- else commands.push(entry.pattern);
2718
- }
2719
- return { events, commands, broadcasts, ordered };
2720
- }
2721
- resolveStreamKind(entry) {
2722
- if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2723
- if (entry.isOrdered) return "ordered" /* Ordered */;
2724
- if (entry.isEvent) return "ev" /* Event */;
2725
- return "cmd" /* Command */;
2726
- }
2727
- logSummary() {
2728
- const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2729
- const parts = [
2730
- `${commands.length} RPC`,
2731
- `${events.length} events`,
2732
- `${broadcasts.length} broadcasts`
2733
- ];
2734
- if (ordered.length > 0) {
2735
- parts.push(`${ordered.length} ordered`);
2736
- }
2737
- this.logger.log(`Registered handlers: ${parts.join(", ")}`);
2738
- }
2739
- };
2740
-
2741
4518
  // src/server/routing/event.router.ts
2742
- import { Logger as Logger11 } from "@nestjs/common";
4519
+ import { Logger as Logger17 } from "@nestjs/common";
2743
4520
  import { headers as natsHeaders3 } from "@nats-io/transport-node";
4521
+ var eventConsumeKindFor = (kind) => {
4522
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4523
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
4524
+ return "event" /* Event */;
4525
+ };
2744
4526
  var EventRouter = class {
2745
4527
  constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
2746
4528
  this.messageProvider = messageProvider;
@@ -2752,9 +4534,22 @@ var EventRouter = class {
2752
4534
  this.ackWaitMap = ackWaitMap;
2753
4535
  this.connection = connection;
2754
4536
  this.options = options;
4537
+ if (options) {
4538
+ const derived = deriveOtelAttrs(options);
4539
+ this.otel = derived.otel;
4540
+ this.serviceName = derived.serviceName;
4541
+ this.serverEndpoint = derived.serverEndpoint;
4542
+ } else {
4543
+ this.otel = resolveOtelOptions({ enabled: false });
4544
+ this.serviceName = "";
4545
+ this.serverEndpoint = null;
4546
+ }
2755
4547
  }
2756
- logger = new Logger11("Jetstream:EventRouter");
4548
+ logger = new Logger17("Jetstream:EventRouter");
2757
4549
  subscriptions = [];
4550
+ otel;
4551
+ serviceName;
4552
+ serverEndpoint;
2758
4553
  /**
2759
4554
  * Update the max_deliver thresholds from actual NATS consumer configs.
2760
4555
  * Called after consumers are ensured so the DLQ map reflects reality.
@@ -2784,13 +4579,24 @@ var EventRouter = class {
2784
4579
  const patternRegistry = this.patternRegistry;
2785
4580
  const codec = this.codec;
2786
4581
  const eventBus = this.eventBus;
2787
- const logger = this.logger;
4582
+ const logger5 = this.logger;
2788
4583
  const deadLetterConfig = this.deadLetterConfig;
4584
+ const otel = this.otel;
4585
+ const serviceName = this.serviceName;
4586
+ const serverEndpoint = this.serverEndpoint;
4587
+ const spanKind = eventConsumeKindFor(kind);
2789
4588
  const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
2790
4589
  const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
2791
4590
  const concurrency = this.getConcurrency(kind);
2792
4591
  const hasDlqCheck = deadLetterConfig !== void 0;
2793
- const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
4592
+ const reportHandlerCompleted = (msg, startedAt, status) => {
4593
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
4594
+ const declared = patternRegistry.resolveDeclared(msg.subject);
4595
+ const pattern = declared?.pattern ?? msg.subject;
4596
+ const declaredKind = declared?.kind ?? kind;
4597
+ const durationMs = performance.now() - startedAt;
4598
+ eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
4599
+ };
2794
4600
  const isDeadLetter = (msg) => {
2795
4601
  if (!hasDlqCheck) return false;
2796
4602
  const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
@@ -2816,7 +4622,7 @@ var EventRouter = class {
2816
4622
  const handler = patternRegistry.getHandler(subject);
2817
4623
  if (!handler) {
2818
4624
  msg.term(`No handler for event: ${subject}`);
2819
- logger.error(`No handler for subject: ${subject}`);
4625
+ logger5.error(`No handler for subject: ${subject}`);
2820
4626
  return null;
2821
4627
  }
2822
4628
  let data;
@@ -2824,47 +4630,79 @@ var EventRouter = class {
2824
4630
  data = codec.decode(msg.data);
2825
4631
  } catch (err) {
2826
4632
  msg.term("Decode error");
2827
- logger.error(`Decode error for ${subject}:`, err);
4633
+ logger5.error(`Decode error for ${subject}:`, err);
2828
4634
  return null;
2829
4635
  }
2830
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4636
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
2831
4637
  return { handler, data };
2832
4638
  } catch (err) {
2833
- logger.error(`Unexpected error in ${kind} event router`, err);
4639
+ logger5.error(`Unexpected error in ${kind} event router`, err);
2834
4640
  try {
2835
4641
  msg.term("Unexpected router error");
2836
4642
  } catch (termErr) {
2837
- logger.error(`Failed to terminate message ${subject}:`, termErr);
4643
+ logger5.error(`Failed to terminate message ${subject}:`, termErr);
2838
4644
  }
2839
4645
  return null;
2840
4646
  }
2841
4647
  };
4648
+ const statusForContext = (ctx) => {
4649
+ if (ctx.shouldTerminate) return "terminated";
4650
+ if (ctx.shouldRetry) return "retried";
4651
+ return "success";
4652
+ };
2842
4653
  const handleSafe = (msg) => {
2843
4654
  const resolved = resolveEvent(msg);
2844
4655
  if (resolved === null) return void 0;
2845
4656
  const { handler, data } = resolved;
2846
4657
  const ctx = new RpcContext([msg]);
2847
4658
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
4659
+ const startedAt = performance.now();
2848
4660
  let pending;
2849
4661
  try {
2850
- pending = unwrapResult(handler(data, ctx));
4662
+ pending = withConsumeSpan(
4663
+ {
4664
+ subject: msg.subject,
4665
+ msg,
4666
+ info: msg.info,
4667
+ kind: spanKind,
4668
+ payloadBytes: msg.data.length,
4669
+ handlerMetadata: { pattern: msg.subject },
4670
+ serviceName,
4671
+ endpoint: serverEndpoint
4672
+ },
4673
+ otel,
4674
+ () => unwrapResult(handler(data, ctx))
4675
+ );
2851
4676
  } catch (err) {
2852
- logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
2853
- if (stopAckExtension !== null) stopAckExtension();
2854
- return settleFailure(msg, data, err);
4677
+ eventBus.emit(
4678
+ "error" /* Error */,
4679
+ err instanceof Error ? err : new Error(String(err)),
4680
+ `${kind}-handler:${msg.subject}`
4681
+ );
4682
+ reportHandlerCompleted(msg, startedAt, "error");
4683
+ return settleFailure(msg, data, err).finally(() => {
4684
+ if (stopAckExtension !== null) stopAckExtension();
4685
+ });
2855
4686
  }
2856
- if (!isPromiseLike(pending)) {
4687
+ if (!isPromiseLike2(pending)) {
2857
4688
  settleSuccess(msg, ctx);
4689
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
2858
4690
  if (stopAckExtension !== null) stopAckExtension();
2859
4691
  return void 0;
2860
4692
  }
2861
4693
  return pending.then(
2862
4694
  () => {
2863
4695
  settleSuccess(msg, ctx);
4696
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
2864
4697
  if (stopAckExtension !== null) stopAckExtension();
2865
4698
  },
2866
4699
  async (err) => {
2867
- logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
4700
+ eventBus.emit(
4701
+ "error" /* Error */,
4702
+ err instanceof Error ? err : new Error(String(err)),
4703
+ `${kind}-handler:${msg.subject}`
4704
+ );
4705
+ reportHandlerCompleted(msg, startedAt, "error");
2868
4706
  try {
2869
4707
  await settleFailure(msg, data, err);
2870
4708
  } finally {
@@ -2880,42 +4718,65 @@ var EventRouter = class {
2880
4718
  try {
2881
4719
  handler = patternRegistry.getHandler(subject);
2882
4720
  if (!handler) {
2883
- logger.error(`No handler for subject: ${subject}`);
4721
+ logger5.error(`No handler for subject: ${subject}`);
2884
4722
  return void 0;
2885
4723
  }
2886
4724
  try {
2887
4725
  data = codec.decode(msg.data);
2888
4726
  } catch (err) {
2889
- logger.error(`Decode error for ${subject}:`, err);
4727
+ logger5.error(`Decode error for ${subject}:`, err);
2890
4728
  return void 0;
2891
4729
  }
2892
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4730
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
2893
4731
  } catch (err) {
2894
- logger.error(`Ordered handler error (${subject}):`, err);
4732
+ logger5.error(`Ordered handler error (${subject}):`, err);
2895
4733
  return void 0;
2896
4734
  }
2897
4735
  const ctx = new RpcContext([msg]);
2898
4736
  const warnIfSettlementAttempted = () => {
2899
4737
  if (ctx.shouldRetry || ctx.shouldTerminate) {
2900
- logger.warn(
4738
+ logger5.warn(
2901
4739
  `retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
2902
4740
  );
2903
4741
  }
2904
4742
  };
4743
+ const startedAt = performance.now();
2905
4744
  let pending;
2906
4745
  try {
2907
- pending = unwrapResult(handler(data, ctx));
4746
+ pending = withConsumeSpan(
4747
+ {
4748
+ subject: msg.subject,
4749
+ msg,
4750
+ info: msg.info,
4751
+ kind: spanKind,
4752
+ payloadBytes: msg.data.length,
4753
+ handlerMetadata: { pattern: msg.subject },
4754
+ serviceName,
4755
+ endpoint: serverEndpoint
4756
+ },
4757
+ otel,
4758
+ () => unwrapResult(handler(data, ctx))
4759
+ );
2908
4760
  } catch (err) {
2909
- logger.error(`Ordered handler error (${subject}):`, err);
4761
+ logger5.error(`Ordered handler error (${subject}):`, err);
4762
+ reportHandlerCompleted(msg, startedAt, "error");
2910
4763
  return void 0;
2911
4764
  }
2912
- if (!isPromiseLike(pending)) {
4765
+ if (!isPromiseLike2(pending)) {
2913
4766
  warnIfSettlementAttempted();
4767
+ reportHandlerCompleted(msg, startedAt, "success");
2914
4768
  return void 0;
2915
4769
  }
2916
- return pending.then(warnIfSettlementAttempted, (err) => {
2917
- logger.error(`Ordered handler error (${subject}):`, err);
2918
- });
4770
+ return pending.then(
4771
+ () => {
4772
+ warnIfSettlementAttempted();
4773
+ reportHandlerCompleted(msg, startedAt, "success");
4774
+ },
4775
+ (err) => {
4776
+ logger5.error(`Ordered handler error (${subject}):`, err);
4777
+ reportHandlerCompleted(msg, startedAt, "error");
4778
+ }
4779
+ );
2919
4780
  };
2920
4781
  const route = isOrdered ? handleOrderedSafe : handleSafe;
2921
4782
  const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
@@ -2947,7 +4808,7 @@ var EventRouter = class {
2947
4808
  backlog.push(msg);
2948
4809
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
2949
4810
  backlogWarned = true;
2950
- logger.warn(
4811
+ logger5.warn(
2951
4812
  `${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
2952
4813
  );
2953
4814
  }
@@ -2963,7 +4824,7 @@ var EventRouter = class {
2963
4824
  }
2964
4825
  },
2965
4826
  error: (err) => {
2966
- logger.error(`Stream error in ${kind} router`, err);
4827
+ logger5.error(`Stream error in ${kind} router`, err);
2967
4828
  }
2968
4829
  });
2969
4830
  this.subscriptions.push(subscription);
@@ -2978,14 +4839,11 @@ var EventRouter = class {
2978
4839
  if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
2979
4840
  return void 0;
2980
4841
  }
2981
- /** Handle a dead letter: invoke callback, then term or nak based on result. */
2982
4842
  /**
2983
- * Fallback execution for a dead letter when DLQ is disabled, or when
2984
- * publishing to the DLQ stream fails (due to network or NATS errors).
2985
- *
2986
- * Triggers the user-provided `onDeadLetter` hook for logging/alerting.
2987
- * On success, terminates the message. On error, leaves it unacknowledged (nak)
2988
- * so NATS can retry the delivery on the next cycle.
4843
+ * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
4844
+ * success or `nak` on hook failure so NATS retries on the next delivery
4845
+ * cycle. Used when DLQ stream isn't configured, or when publishing to it
4846
+ * failed and we still have to surface the message somewhere observable.
2989
4847
  */
2990
4848
  async fallbackToOnDeadLetterCallback(info, msg) {
2991
4849
  if (!this.deadLetterConfig) {
@@ -3077,20 +4935,37 @@ var EventRouter = class {
3077
4935
  streamSequence: msg.info.streamSequence,
3078
4936
  timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
3079
4937
  };
3080
- this.eventBus.emit("deadLetter" /* DeadLetter */, info);
3081
- if (!this.options?.dlq) {
3082
- await this.fallbackToOnDeadLetterCallback(info, msg);
3083
- } else {
3084
- await this.publishToDlq(msg, info, error);
3085
- }
4938
+ await withDeadLetterSpan(
4939
+ {
4940
+ msg,
4941
+ // Pattern resolution mirrors event-routing: when a registered
4942
+ // pattern matches, surface it on the DLQ span so APM can filter
4943
+ // dead letters by handler without parsing the subject. Falls back
4944
+ // to the subject itself when no glob handler is in play.
4945
+ pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
4946
+ finalDeliveryCount: msg.info.deliveryCount,
4947
+ reason: error instanceof Error ? error.message : String(error),
4948
+ serviceName: this.serviceName,
4949
+ endpoint: this.serverEndpoint
4950
+ },
4951
+ this.otel,
4952
+ async () => {
4953
+ this.eventBus.emit("deadLetter" /* DeadLetter */, info);
4954
+ if (!this.options?.dlq) {
4955
+ await this.fallbackToOnDeadLetterCallback(info, msg);
4956
+ } else {
4957
+ await this.publishToDlq(msg, info, error);
4958
+ }
4959
+ }
4960
+ );
3086
4961
  }
3087
4962
  };
3088
4963
 
3089
4964
  // src/server/routing/rpc.router.ts
3090
- import { Logger as Logger12 } from "@nestjs/common";
4965
+ import { Logger as Logger18 } from "@nestjs/common";
3091
4966
  import { headers } from "@nats-io/transport-node";
3092
4967
  var RpcRouter = class {
3093
- constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
4968
+ constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
3094
4969
  this.messageProvider = messageProvider;
3095
4970
  this.patternRegistry = patternRegistry;
3096
4971
  this.connection = connection;
@@ -3100,13 +4975,26 @@ var RpcRouter = class {
3100
4975
  this.ackWaitMap = ackWaitMap;
3101
4976
  this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
3102
4977
  this.concurrency = rpcOptions?.concurrency;
4978
+ if (options) {
4979
+ const derived = deriveOtelAttrs(options);
4980
+ this.otel = derived.otel;
4981
+ this.serviceName = derived.serviceName;
4982
+ this.serverEndpoint = derived.serverEndpoint;
4983
+ } else {
4984
+ this.otel = resolveOtelOptions({ enabled: false });
4985
+ this.serviceName = "";
4986
+ this.serverEndpoint = null;
4987
+ }
3103
4988
  }
3104
- logger = new Logger12("Jetstream:RpcRouter");
4989
+ logger = new Logger18("Jetstream:RpcRouter");
3105
4990
  timeout;
3106
4991
  concurrency;
3107
4992
  resolvedAckExtensionInterval;
3108
4993
  subscription = null;
3109
4994
  cachedNc = null;
4995
+ otel;
4996
+ serviceName;
4997
+ serverEndpoint;
3110
4998
  /** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
3111
4999
  get ackExtensionInterval() {
3112
5000
  if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
@@ -3123,21 +5011,37 @@ var RpcRouter = class {
3123
5011
  const patternRegistry = this.patternRegistry;
3124
5012
  const codec = this.codec;
3125
5013
  const eventBus = this.eventBus;
3126
- const logger = this.logger;
5014
+ const logger5 = this.logger;
3127
5015
  const timeout = this.timeout;
3128
5016
  const ackExtensionInterval = this.ackExtensionInterval;
3129
5017
  const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
3130
5018
  const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
5019
+ const otel = this.otel;
5020
+ const serviceName = this.serviceName;
5021
+ const serverEndpoint = this.serverEndpoint;
3131
5022
  const emitRpcTimeout = (subject, correlationId) => {
3132
5023
  eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
3133
5024
  };
5025
+ const reportHandlerCompleted = (msg, startedAt, status) => {
5026
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5027
+ const declared = patternRegistry.resolveDeclared(msg.subject);
5028
+ const pattern = declared?.pattern ?? msg.subject;
5029
+ const declaredKind = declared?.kind ?? "cmd" /* Command */;
5030
+ eventBus.emit(
5031
+ "handlerCompleted" /* HandlerCompleted */,
5032
+ pattern,
5033
+ declaredKind,
5034
+ performance.now() - startedAt,
5035
+ status
5036
+ );
5037
+ };
3134
5038
  const publishReply = (replyTo, correlationId, payload) => {
3135
5039
  try {
3136
5040
  const hdrs = headers();
3137
5041
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
3138
5042
  nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
3139
5043
  } catch (publishErr) {
3140
- logger.error(`Failed to publish RPC response`, publishErr);
5044
+ logger5.error(`Failed to publish RPC response`, publishErr);
3141
5045
  }
3142
5046
  };
3143
5047
  const publishErrorReply = (replyTo, correlationId, subject, err) => {
@@ -3147,7 +5051,7 @@ var RpcRouter = class {
3147
5051
  hdrs.set("x-error" /* Error */, "true");
3148
5052
  nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
3149
5053
  } catch (encodeErr) {
3150
- logger.error(`Failed to encode RPC error for ${subject}`, encodeErr);
5054
+ logger5.error(`Failed to encode RPC error for ${subject}`, encodeErr);
3151
5055
  }
3152
5056
  };
3153
5057
  const resolveCommand = (msg) => {
@@ -3156,7 +5060,7 @@ var RpcRouter = class {
3156
5060
  const handler = patternRegistry.getHandler(subject);
3157
5061
  if (!handler) {
3158
5062
  msg.term(`No handler for RPC: ${subject}`);
3159
- logger.error(`No handler for RPC subject: ${subject}`);
5063
+ logger5.error(`No handler for RPC subject: ${subject}`);
3160
5064
  return null;
3161
5065
  }
3162
5066
  const msgHeaders = msg.headers;
@@ -3164,7 +5068,7 @@ var RpcRouter = class {
3164
5068
  const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
3165
5069
  if (!replyTo || !correlationId) {
3166
5070
  msg.term("Missing required headers (reply-to or correlation-id)");
3167
- logger.error(`Missing headers for RPC: ${subject}`);
5071
+ logger5.error(`Missing headers for RPC: ${subject}`);
3168
5072
  return null;
3169
5073
  }
3170
5074
  let data;
@@ -3172,17 +5076,17 @@ var RpcRouter = class {
3172
5076
  data = codec.decode(msg.data);
3173
5077
  } catch (err) {
3174
5078
  msg.term("Decode error");
3175
- logger.error(`Decode error for RPC ${subject}:`, err);
5079
+ logger5.error(`Decode error for RPC ${subject}:`, err);
3176
5080
  return null;
3177
5081
  }
3178
5082
  eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
3179
5083
  return { handler, data, replyTo, correlationId };
3180
5084
  } catch (err) {
3181
- logger.error("Unexpected error in RPC router", err);
5085
+ logger5.error("Unexpected error in RPC router", err);
3182
5086
  try {
3183
5087
  msg.term("Unexpected router error");
3184
5088
  } catch (termErr) {
3185
- logger.error(`Failed to terminate RPC message ${subject}:`, termErr);
5089
+ logger5.error(`Failed to terminate RPC message ${subject}:`, termErr);
3186
5090
  }
3187
5091
  return null;
3188
5092
  }
@@ -3194,20 +5098,45 @@ var RpcRouter = class {
3194
5098
  const subject = msg.subject;
3195
5099
  const ctx = new RpcContext([msg]);
3196
5100
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5101
+ const startedAt = performance.now();
5102
+ const reportHandlerError = (err) => {
5103
+ eventBus.emit(
5104
+ "error" /* Error */,
5105
+ err instanceof Error ? err : new Error(String(err)),
5106
+ `rpc-handler:${subject}`
5107
+ );
5108
+ publishErrorReply(replyTo, correlationId, subject, err);
5109
+ msg.term(`Handler error: ${subject}`);
5110
+ };
5111
+ const abortController = new AbortController();
3197
5112
  let pending;
3198
5113
  try {
3199
- pending = unwrapResult(handler(data, ctx));
5114
+ pending = withConsumeSpan(
5115
+ {
5116
+ subject,
5117
+ msg,
5118
+ info: msg.info,
5119
+ kind: "rpc" /* Rpc */,
5120
+ payloadBytes: msg.data.length,
5121
+ handlerMetadata: { pattern: subject },
5122
+ serviceName,
5123
+ endpoint: serverEndpoint
5124
+ },
5125
+ otel,
5126
+ () => unwrapResult(handler(data, ctx)),
5127
+ { signal: abortController.signal, timeoutLabel: "rpc.handler.timeout" }
5128
+ );
3200
5129
  } catch (err) {
3201
5130
  if (stopAckExtension !== null) stopAckExtension();
3202
- logger.error(`RPC handler error (${subject}):`, err);
3203
- publishErrorReply(replyTo, correlationId, subject, err);
3204
- msg.term(`Handler error: ${subject}`);
5131
+ reportHandlerError(err);
5132
+ reportHandlerCompleted(msg, startedAt, "error");
3205
5133
  return void 0;
3206
5134
  }
3207
- if (!isPromiseLike(pending)) {
5135
+ if (!isPromiseLike2(pending)) {
3208
5136
  if (stopAckExtension !== null) stopAckExtension();
3209
5137
  msg.ack();
3210
5138
  publishReply(replyTo, correlationId, pending);
5139
+ reportHandlerCompleted(msg, startedAt, "success");
3211
5140
  return void 0;
3212
5141
  }
3213
5142
  let settled = false;
@@ -3215,9 +5144,10 @@ var RpcRouter = class {
3215
5144
  if (settled) return;
3216
5145
  settled = true;
3217
5146
  if (stopAckExtension !== null) stopAckExtension();
3218
- logger.error(`RPC timeout (${timeout}ms): ${subject}`);
5147
+ abortController.abort();
3219
5148
  emitRpcTimeout(subject, correlationId);
3220
5149
  msg.term("Handler timeout");
5150
+ reportHandlerCompleted(msg, startedAt, "terminated");
3221
5151
  }, timeout);
3222
5152
  return pending.then(
3223
5153
  (result) => {
@@ -3227,15 +5157,15 @@ var RpcRouter = class {
3227
5157
  if (stopAckExtension !== null) stopAckExtension();
3228
5158
  msg.ack();
3229
5159
  publishReply(replyTo, correlationId, result);
5160
+ reportHandlerCompleted(msg, startedAt, "success");
3230
5161
  },
3231
5162
  (err) => {
3232
5163
  if (settled) return;
3233
5164
  settled = true;
3234
5165
  clearTimeout(timeoutId);
3235
5166
  if (stopAckExtension !== null) stopAckExtension();
3236
- logger.error(`RPC handler error (${subject}):`, err);
3237
- publishErrorReply(replyTo, correlationId, subject, err);
3238
- msg.term(`Handler error: ${subject}`);
5167
+ reportHandlerError(err);
5168
+ reportHandlerCompleted(msg, startedAt, "error");
3239
5169
  }
3240
5170
  );
3241
5171
  };
@@ -3267,7 +5197,7 @@ var RpcRouter = class {
3267
5197
  backlog.push(msg);
3268
5198
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
3269
5199
  backlogWarned = true;
3270
- logger.warn(
5200
+ logger5.warn(
3271
5201
  `RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
3272
5202
  );
3273
5203
  }
@@ -3283,7 +5213,7 @@ var RpcRouter = class {
3283
5213
  }
3284
5214
  },
3285
5215
  error: (err) => {
3286
- logger.error("Stream error in RPC router", err);
5216
+ logger5.error("Stream error in RPC router", err);
3287
5217
  }
3288
5218
  });
3289
5219
  }
@@ -3295,14 +5225,14 @@ var RpcRouter = class {
3295
5225
  };
3296
5226
 
3297
5227
  // src/shutdown/shutdown.manager.ts
3298
- import { Logger as Logger13 } from "@nestjs/common";
5228
+ import { Logger as Logger19 } from "@nestjs/common";
3299
5229
  var ShutdownManager = class {
3300
5230
  constructor(connection, eventBus, timeout) {
3301
5231
  this.connection = connection;
3302
5232
  this.eventBus = eventBus;
3303
5233
  this.timeout = timeout;
3304
5234
  }
3305
- logger = new Logger13("Jetstream:Shutdown");
5235
+ logger = new Logger19("Jetstream:Shutdown");
3306
5236
  shutdownPromise;
3307
5237
  /**
3308
5238
  * Execute the full shutdown sequence.
@@ -3342,9 +5272,6 @@ var JetstreamModule = class {
3342
5272
  this.shutdownManager = shutdownManager;
3343
5273
  this.strategy = strategy;
3344
5274
  }
3345
- // -------------------------------------------------------------------
3346
- // forRoot — global module registration
3347
- // -------------------------------------------------------------------
3348
5275
  /**
3349
5276
  * Register the JetStream transport globally.
3350
5277
  *
@@ -3359,21 +5286,20 @@ var JetstreamModule = class {
3359
5286
  return {
3360
5287
  module: JetstreamModule,
3361
5288
  global: true,
5289
+ imports: options.metrics ? [JetstreamMetricsModule.forFeature(options.metrics)] : [],
3362
5290
  providers,
3363
5291
  exports: [
3364
5292
  JETSTREAM_CONNECTION,
3365
5293
  JETSTREAM_CODEC,
3366
5294
  JETSTREAM_EVENT_BUS,
3367
5295
  JETSTREAM_OPTIONS,
5296
+ PatternRegistry,
3368
5297
  ShutdownManager,
3369
5298
  JetstreamStrategy,
3370
5299
  JetstreamHealthIndicator
3371
5300
  ]
3372
5301
  };
3373
5302
  }
3374
- // -------------------------------------------------------------------
3375
- // forRootAsync — async global module registration
3376
- // -------------------------------------------------------------------
3377
5303
  /**
3378
5304
  * Register the JetStream transport globally with async configuration.
3379
5305
  *
@@ -3386,25 +5312,24 @@ var JetstreamModule = class {
3386
5312
  static forRootAsync(asyncOptions) {
3387
5313
  const asyncProviders = this.createAsyncOptionsProvider(asyncOptions);
3388
5314
  const coreProviders = this.createCoreDependentProviders();
5315
+ const metricsImports = asyncOptions.metrics ? [JetstreamMetricsModule.forFeature(asyncOptions.metrics)] : [];
3389
5316
  return {
3390
5317
  module: JetstreamModule,
3391
5318
  global: true,
3392
- imports: asyncOptions.imports ?? [],
5319
+ imports: [...asyncOptions.imports ?? [], ...metricsImports],
3393
5320
  providers: [...asyncProviders, ...coreProviders],
3394
5321
  exports: [
3395
5322
  JETSTREAM_CONNECTION,
3396
5323
  JETSTREAM_CODEC,
3397
5324
  JETSTREAM_EVENT_BUS,
3398
5325
  JETSTREAM_OPTIONS,
5326
+ PatternRegistry,
3399
5327
  ShutdownManager,
3400
5328
  JetstreamStrategy,
3401
5329
  JetstreamHealthIndicator
3402
5330
  ]
3403
5331
  };
3404
5332
  }
3405
- // -------------------------------------------------------------------
3406
- // forFeature — per-module client registration
3407
- // -------------------------------------------------------------------
3408
5333
  /**
3409
5334
  * Register a lightweight client proxy for a target service.
3410
5335
  *
@@ -3430,9 +5355,6 @@ var JetstreamModule = class {
3430
5355
  exports: [clientToken]
3431
5356
  };
3432
5357
  }
3433
- // -------------------------------------------------------------------
3434
- // Provider factories
3435
- // -------------------------------------------------------------------
3436
5358
  static createCoreProviders(options) {
3437
5359
  return [
3438
5360
  {
@@ -3450,8 +5372,8 @@ var JetstreamModule = class {
3450
5372
  provide: JETSTREAM_EVENT_BUS,
3451
5373
  inject: [JETSTREAM_OPTIONS],
3452
5374
  useFactory: (options) => {
3453
- const logger = new Logger14("Jetstream:Module");
3454
- return new EventBus(logger, options.hooks);
5375
+ const logger5 = new Logger20("Jetstream:Module");
5376
+ return new EventBus(logger5, options.hooks);
3455
5377
  }
3456
5378
  },
3457
5379
  // Codec — global encode/decode
@@ -3490,10 +5412,8 @@ var JetstreamModule = class {
3490
5412
  );
3491
5413
  }
3492
5414
  },
3493
- // ---------------------------------------------------------------
3494
5415
  // Consumer infrastructure — only created when consumer !== false.
3495
5416
  // Providers return null when consumer is disabled (publisher-only mode).
3496
- // ---------------------------------------------------------------
3497
5417
  // PatternRegistry — subject-to-handler mapping
3498
5418
  {
3499
5419
  provide: PatternRegistry,
@@ -3529,8 +5449,14 @@ var JetstreamModule = class {
3529
5449
  // MessageProvider — pull-based message consumption
3530
5450
  {
3531
5451
  provide: MessageProvider,
3532
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
3533
- useFactory: (options, connection, eventBus, consumerProvider) => {
5452
+ inject: [
5453
+ JETSTREAM_OPTIONS,
5454
+ JETSTREAM_CONNECTION,
5455
+ JETSTREAM_EVENT_BUS,
5456
+ ConsumerProvider,
5457
+ StreamProvider
5458
+ ],
5459
+ useFactory: (options, connection, eventBus, consumerProvider, streamProvider) => {
3534
5460
  if (options.consumer === false) return null;
3535
5461
  const consumeOptionsMap = /* @__PURE__ */ new Map();
3536
5462
  if (options.events?.consume)
@@ -3540,10 +5466,22 @@ var JetstreamModule = class {
3540
5466
  if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
3541
5467
  consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
3542
5468
  }
3543
- const consumerRecoveryFn = consumerProvider ? async (kind) => {
3544
- const jsm = await connection.getJetStreamManager();
3545
- return consumerProvider.recoverConsumer(jsm, kind);
3546
- } : void 0;
5469
+ const derived = deriveOtelAttrs(options);
5470
+ const { otel, serverEndpoint: otelEndpoint, serviceName: otelServiceName } = derived;
5471
+ const consumerRecoveryFn = consumerProvider && streamProvider ? async (kind) => withSelfHealingSpan(
5472
+ otel,
5473
+ {
5474
+ serviceName: otelServiceName,
5475
+ endpoint: otelEndpoint,
5476
+ consumer: consumerProvider.getConsumerName(kind),
5477
+ stream: streamProvider.getStreamName(kind),
5478
+ reason: "consumer not found"
5479
+ },
5480
+ async () => {
5481
+ const jsm = await connection.getJetStreamManager();
5482
+ return consumerProvider.recoverConsumer(jsm, kind);
5483
+ }
5484
+ ) : void 0;
3547
5485
  return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
3548
5486
  }
3549
5487
  },
@@ -3614,7 +5552,8 @@ var JetstreamModule = class {
3614
5552
  codec,
3615
5553
  eventBus,
3616
5554
  rpcOptions,
3617
- ackWaitMap
5555
+ ackWaitMap,
5556
+ options
3618
5557
  );
3619
5558
  }
3620
5559
  },
@@ -3717,9 +5656,6 @@ var JetstreamModule = class {
3717
5656
  }
3718
5657
  ];
3719
5658
  }
3720
- // -------------------------------------------------------------------
3721
- // Lifecycle hooks
3722
- // -------------------------------------------------------------------
3723
5659
  /**
3724
5660
  * Gracefully shut down the transport on application termination.
3725
5661
  */
@@ -3731,13 +5667,14 @@ var JetstreamModule = class {
3731
5667
  };
3732
5668
  JetstreamModule = __decorateClass([
3733
5669
  Global(),
3734
- Module({}),
3735
- __decorateParam(0, Optional()),
3736
- __decorateParam(0, Inject(ShutdownManager)),
3737
- __decorateParam(1, Optional()),
3738
- __decorateParam(1, Inject(JetstreamStrategy))
5670
+ Module2({}),
5671
+ __decorateParam(0, Optional2()),
5672
+ __decorateParam(0, Inject2(ShutdownManager)),
5673
+ __decorateParam(1, Optional2()),
5674
+ __decorateParam(1, Inject2(JetstreamStrategy))
3739
5675
  ], JetstreamModule);
3740
5676
  export {
5677
+ ConsumeKind,
3741
5678
  DEFAULT_BROADCAST_CONSUMER_CONFIG,
3742
5679
  DEFAULT_BROADCAST_STREAM_CONFIG,
3743
5680
  DEFAULT_COMMAND_CONSUMER_CONFIG,
@@ -3753,6 +5690,7 @@ export {
3753
5690
  DEFAULT_ORDERED_STREAM_CONFIG,
3754
5691
  DEFAULT_RPC_TIMEOUT,
3755
5692
  DEFAULT_SHUTDOWN_TIMEOUT,
5693
+ DEFAULT_TRACES,
3756
5694
  JETSTREAM_CODEC,
3757
5695
  JETSTREAM_CONNECTION,
3758
5696
  JETSTREAM_OPTIONS,
@@ -3764,15 +5702,18 @@ export {
3764
5702
  JetstreamRecord,
3765
5703
  JetstreamRecordBuilder,
3766
5704
  JetstreamStrategy,
5705
+ JetstreamTrace,
3767
5706
  JsonCodec,
3768
5707
  MIN_METADATA_TTL,
3769
5708
  MessageKind,
3770
5709
  MsgpackCodec,
3771
5710
  NatsErrorCode,
3772
5711
  PatternPrefix,
5712
+ PublishKind,
3773
5713
  RESERVED_HEADERS,
3774
5714
  RpcContext,
3775
5715
  StreamKind,
5716
+ TRACER_NAME,
3776
5717
  TransportEvent,
3777
5718
  buildBroadcastSubject,
3778
5719
  buildSubject,