@horizon-republic/nestjs-jetstream 2.9.0 → 2.10.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.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { ModuleMetadata, FactoryProvider, Type, Logger, OnApplicationShutdown, DynamicModule } from '@nestjs/common';
2
- import { MsgHdrs, ConnectionOptions, NatsConnection, Status, Msg } from '@nats-io/transport-node';
3
- import { StreamConfig, ConsumerConfig, ConsumeOptions, DeliverPolicy, ReplayPolicy, JetStreamManager, JetStreamClient, ConsumerInfo, JsMsg } from '@nats-io/jetstream';
4
- import { MessageHandler, Server, CustomTransportStrategy, ClientProxy, ReadPacket, WritePacket, BaseRpcContext } from '@nestjs/microservices';
1
+ import { Logger, ModuleMetadata, FactoryProvider, Type, OnApplicationShutdown, DynamicModule } from '@nestjs/common';
2
+ import { MsgHdrs, NatsConnection, Status, ConnectionOptions, Msg } from '@nats-io/transport-node';
3
+ import { JetStreamManager, JetStreamClient, StreamConfig, ConsumerConfig, ConsumeOptions, DeliverPolicy, ReplayPolicy, ConsumerInfo, JsMsg } from '@nats-io/jetstream';
4
+ import { Span } from '@opentelemetry/api';
5
+ import { ClientProxy, ReadPacket, WritePacket, MessageHandler, Server, CustomTransportStrategy, BaseRpcContext } from '@nestjs/microservices';
5
6
  import { Observable } from 'rxjs';
6
7
 
7
8
  /**
@@ -117,6 +118,689 @@ interface JetstreamHealthStatus {
117
118
  latency: number | null;
118
119
  }
119
120
 
121
+ /**
122
+ * Instrumentation scope name reported on every span the library emits.
123
+ * Matches the npm package name — the convention used by
124
+ * `@opentelemetry/instrumentation-*` and most third-party instrumentations.
125
+ */
126
+ declare const TRACER_NAME = "@horizon-republic/nestjs-jetstream";
127
+
128
+ /**
129
+ * Enumeration of OpenTelemetry trace kinds this library can emit.
130
+ *
131
+ * Each identifier is toggleable via `otel.traces`. Functional kinds (publish,
132
+ * consume, RPC client round-trip, dead letter) are enabled by default;
133
+ * infrastructure kinds (connection lifecycle, self-healing, provisioning,
134
+ * migration, shutdown) are opt-in.
135
+ *
136
+ * @example
137
+ * JetstreamModule.forRoot({
138
+ * otel: {
139
+ * traces: [JetstreamTrace.Publish, JetstreamTrace.Consume, JetstreamTrace.ConnectionLifecycle],
140
+ * },
141
+ * })
142
+ */
143
+ declare enum JetstreamTrace {
144
+ /** `PRODUCER` span around each `emit()` / `send()` publish operation. Default: ON. */
145
+ Publish = "publish",
146
+ /**
147
+ * `CONSUMER` span around each message delivery to a handler. Retries
148
+ * produce separate spans with `messaging.nats.message.delivery_count > 1`.
149
+ * Default: ON.
150
+ */
151
+ Consume = "consume",
152
+ /**
153
+ * `CLIENT` span covering a full RPC round-trip on the caller side
154
+ * (`client.send()` → reply received). Wraps the inner publish.
155
+ * Default: ON.
156
+ */
157
+ RpcClientSend = "rpc.client.send",
158
+ /**
159
+ * `INTERNAL` span emitted when a message exhausts `maxDeliver` and is
160
+ * dead-lettered. Captures the duration of the `onDeadLetter` callback.
161
+ * Default: ON.
162
+ */
163
+ DeadLetter = "dead_letter",
164
+ /**
165
+ * `INTERNAL` span spanning one NATS connection session. Emits events on
166
+ * connect, reconnect, disconnect. Default: OFF.
167
+ */
168
+ ConnectionLifecycle = "connection.lifecycle",
169
+ /**
170
+ * `INTERNAL` span per consumer recovery attempt following a transient
171
+ * failure (deleted consumer, missed heartbeat). Default: OFF.
172
+ */
173
+ SelfHealing = "self_healing",
174
+ /** `INTERNAL` span per stream or consumer provisioning / update at startup. Default: OFF. */
175
+ Provisioning = "provisioning",
176
+ /** `INTERNAL` span covering a destructive migration flow. Default: OFF. */
177
+ Migration = "migration",
178
+ /** `INTERNAL` span covering the graceful shutdown sequence. Default: OFF. */
179
+ Shutdown = "shutdown"
180
+ }
181
+ /**
182
+ * The set of trace kinds enabled when `otel.traces` is not configured
183
+ * (or set to `'default'`). Covers message-flow operations; infrastructure
184
+ * spans are opt-in.
185
+ */
186
+ declare const DEFAULT_TRACES: readonly JetstreamTrace[];
187
+
188
+ /**
189
+ * Central event bus for transport lifecycle notifications.
190
+ *
191
+ * Dispatches events to user-provided hooks. Events without a
192
+ * registered hook are silently ignored — no default logging.
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const bus = new EventBus(logger, {
197
+ * [TransportEvent.Error]: (err) => sentry.captureException(err),
198
+ * });
199
+ *
200
+ * bus.emit(TransportEvent.Error, new Error('timeout'), 'rpc-router');
201
+ * // → calls sentry
202
+ *
203
+ * bus.emit(TransportEvent.Connect, 'nats://localhost:4222');
204
+ * // → no-op (no hook registered)
205
+ * ```
206
+ */
207
+ declare class EventBus {
208
+ private readonly hooks;
209
+ private readonly logger;
210
+ constructor(logger: Logger, hooks?: Partial<TransportHooks>);
211
+ /**
212
+ * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
213
+ *
214
+ * @param event - The {@link TransportEvent} to emit.
215
+ * @param args - Arguments matching the hook signature for this event.
216
+ */
217
+ emit<K extends keyof TransportHooks>(event: K, ...args: Parameters<TransportHooks[K]>): void;
218
+ /**
219
+ * Hot-path optimized emit for MessageRouted events.
220
+ * Avoids rest/spread overhead of the generic `emit()`.
221
+ */
222
+ emitMessageRouted(subject: string, kind: MessageKind): void;
223
+ /**
224
+ * Check whether a hook is registered for the given event.
225
+ *
226
+ * Used by the routing hot path to elide the emit call entirely when the
227
+ * transport owner did not register a listener.
228
+ */
229
+ hasHook(event: keyof TransportHooks): boolean;
230
+ private callHook;
231
+ }
232
+
233
+ /**
234
+ * Manages the lifecycle of a single NATS connection shared across the application.
235
+ *
236
+ * Provides both Promise-based and Observable-based access to the connection:
237
+ * - `connect()` / `getConnection()` — async/await for one-time setup
238
+ * - `nc$` — cached observable (shareReplay) for reactive consumers
239
+ * - `status$` — live connection status event stream
240
+ *
241
+ * One instance per application, created by `JetstreamModule.forRoot()`.
242
+ */
243
+ declare class ConnectionProvider {
244
+ private readonly options;
245
+ private readonly eventBus;
246
+ /** Cached observable that replays the established connection to new subscribers. */
247
+ readonly nc$: Observable<NatsConnection>;
248
+ /** Live stream of connection status events (no replay). */
249
+ readonly status$: Observable<Status>;
250
+ private readonly logger;
251
+ private connection;
252
+ private connectionPromise;
253
+ private jsClient;
254
+ private jsmInstance;
255
+ private jsmPromise;
256
+ private readonly otel;
257
+ private readonly otelServiceName;
258
+ private readonly otelEndpoint;
259
+ private lifecycleSpan;
260
+ constructor(options: JetstreamModuleOptions, eventBus: EventBus);
261
+ /**
262
+ * Establish NATS connection. Idempotent — returns cached connection on subsequent calls.
263
+ *
264
+ * @throws Error if connection is refused (fail fast).
265
+ */
266
+ getConnection(): Promise<NatsConnection>;
267
+ /**
268
+ * Get the JetStream manager. Cached after first call.
269
+ *
270
+ * @returns The JetStreamManager for stream/consumer administration.
271
+ */
272
+ getJetStreamManager(): Promise<JetStreamManager>;
273
+ /**
274
+ * Get a cached JetStream client.
275
+ *
276
+ * Invalidated automatically on reconnect and shutdown so consumers always
277
+ * operate against the live connection.
278
+ *
279
+ * @returns The cached JetStreamClient.
280
+ * @throws Error if the connection has not been established yet.
281
+ */
282
+ getJetStreamClient(): JetStreamClient;
283
+ /** Direct access to the raw NATS connection, or `null` if not yet connected. */
284
+ get unwrap(): NatsConnection | null;
285
+ /**
286
+ * Gracefully shut down the connection.
287
+ *
288
+ * Sequence: drain → wait for close. Falls back to force-close on error.
289
+ */
290
+ shutdown(): Promise<void>;
291
+ private initJetStreamManager;
292
+ /** Internal: establish the physical connection with reconnect monitoring. */
293
+ private establish;
294
+ /** Handle a single `nc.status()` event, emitting hooks and span events. */
295
+ private handleStatusEvent;
296
+ /** Subscribe to connection status events and emit hooks. */
297
+ private monitorStatus;
298
+ }
299
+
300
+ /**
301
+ * NestJS ClientProxy implementation for the JetStream transport.
302
+ *
303
+ * Supports two operational modes:
304
+ * - **Core mode** (default): Uses `nc.request()` for RPC, `nc.publish()` for events.
305
+ * - **JetStream mode**: Uses `js.publish()` for RPC commands + inbox for responses.
306
+ *
307
+ * Events always go through JetStream publish for guaranteed delivery.
308
+ * The mode only affects RPC (request/reply) behavior.
309
+ *
310
+ * Clients are lightweight — they share the NATS connection from `forRoot()`.
311
+ */
312
+ declare class JetstreamClient extends ClientProxy {
313
+ private readonly rootOptions;
314
+ private readonly connection;
315
+ private readonly codec;
316
+ private readonly eventBus;
317
+ private readonly logger;
318
+ /** Target service name this client sends messages to. */
319
+ private readonly targetName;
320
+ /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
321
+ private readonly callerName;
322
+ /**
323
+ * Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
324
+ * per stream kind this client may publish to. Built once in the constructor
325
+ * so producing a full subject is a single string concat with the user pattern.
326
+ */
327
+ private readonly eventSubjectPrefix;
328
+ private readonly commandSubjectPrefix;
329
+ private readonly orderedSubjectPrefix;
330
+ /**
331
+ * RPC configuration snapshots. The values are derived from rootOptions at
332
+ * construction time so the publish hot path never has to re-run
333
+ * isCoreRpcMode / getRpcTimeout on every call.
334
+ */
335
+ private readonly isCoreMode;
336
+ private readonly defaultRpcTimeout;
337
+ /** Resolved OpenTelemetry configuration, computed once in the constructor. */
338
+ private readonly otel;
339
+ /** Server endpoint parts used for `server.address` / `server.port` span attributes. */
340
+ private readonly serverEndpoint;
341
+ /** Shared inbox for JetStream-mode RPC responses. */
342
+ private inbox;
343
+ private inboxSubscription;
344
+ /** Pending JetStream-mode RPC callbacks, keyed by correlation ID. */
345
+ private readonly pendingMessages;
346
+ /** Pending JetStream-mode RPC timeouts, keyed by correlation ID. */
347
+ private readonly pendingTimeouts;
348
+ /** Subscription to connection status events for disconnect handling. */
349
+ private statusSubscription;
350
+ /**
351
+ * Cached readiness flag. Once `connect()` has wired the inbox and status
352
+ * subscription, subsequent publishes skip the `await connect()` microtask
353
+ * and reach for the underlying connection synchronously instead.
354
+ */
355
+ private readyForPublish;
356
+ constructor(rootOptions: JetstreamModuleOptions, targetServiceName: string, connection: ConnectionProvider, codec: Codec, eventBus: EventBus);
357
+ /**
358
+ * Establish connection. Called automatically by NestJS on first use.
359
+ *
360
+ * Sets up the JetStream RPC inbox (if in JetStream mode) and subscribes
361
+ * to connection status events for fail-fast disconnect handling.
362
+ *
363
+ * @returns The underlying NATS connection.
364
+ */
365
+ connect(): Promise<NatsConnection>;
366
+ /** Clean up resources: reject pending RPCs, unsubscribe from status events. */
367
+ close(): Promise<void>;
368
+ /**
369
+ * Direct access to the raw NATS connection.
370
+ *
371
+ * @throws Error if not connected.
372
+ */
373
+ unwrap<T = NatsConnection>(): T;
374
+ /**
375
+ * Publish a fire-and-forget event to JetStream.
376
+ *
377
+ * Events are published to either the workqueue stream or broadcast stream
378
+ * depending on the subject prefix. When a schedule is present the message
379
+ * is published to a `_sch` subject within the same stream, with the target
380
+ * set to the original event subject.
381
+ */
382
+ protected dispatchEvent<T = unknown>(packet: ReadPacket): Promise<T>;
383
+ /**
384
+ * Publish an RPC command and register callback for response.
385
+ *
386
+ * Core mode: uses nc.request() with timeout.
387
+ * JetStream mode: publishes to stream + waits for inbox response.
388
+ */
389
+ protected publish(packet: ReadPacket, callback: (p: WritePacket) => void): () => void;
390
+ /** Core mode: nc.request() with timeout. */
391
+ private publishCoreRpc;
392
+ /** JetStream mode: publish to stream + wait for inbox response. */
393
+ private publishJetStreamRpc;
394
+ /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
395
+ private handleDisconnect;
396
+ /** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
397
+ private rejectPendingRpcs;
398
+ /** Setup shared inbox subscription for JetStream RPC responses. */
399
+ private setupInbox;
400
+ /** Route an inbox reply to the matching pending callback. */
401
+ private routeInboxReply;
402
+ /**
403
+ * Resolve a user pattern to a fully-qualified NATS subject, dispatching
404
+ * between the event, broadcast, and ordered prefixes.
405
+ *
406
+ * The leading-char check short-circuits the `startsWith` comparisons for
407
+ * patterns that cannot possibly carry a broadcast/ordered marker, which is
408
+ * the overwhelmingly common case.
409
+ */
410
+ private buildEventSubject;
411
+ /** Build NATS headers merging custom headers with transport headers. */
412
+ private buildHeaders;
413
+ /** Extract data, headers, timeout, and schedule from raw packet data or JetstreamRecord. */
414
+ private extractRecordData;
415
+ /**
416
+ * Build a schedule-holder subject for NATS message scheduling.
417
+ *
418
+ * The schedule-holder subject resides in the same stream as the target but
419
+ * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
420
+ * NATS holds the message and publishes it to the target subject after the delay.
421
+ *
422
+ * Examples:
423
+ * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
424
+ * - `broadcast.config.updated` → `broadcast._sch.config.updated`
425
+ */
426
+ private buildScheduleSubject;
427
+ }
428
+
429
+ /**
430
+ * Immutable message record for JetStream transport.
431
+ *
432
+ * Compatible with NestJS record builder pattern (like RmqRecord, NatsRecord).
433
+ * Pass as the second argument to `client.send()` or `client.emit()`.
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * const record = new JetstreamRecordBuilder({ id: 1 })
438
+ * .setHeader('x-tenant', 'acme')
439
+ * .setTimeout(5000)
440
+ * .build();
441
+ *
442
+ * client.send('get.user', record);
443
+ * ```
444
+ */
445
+ declare class JetstreamRecord<TData = unknown> {
446
+ /** Message payload. */
447
+ readonly data: TData;
448
+ /** Custom headers set via {@link JetstreamRecordBuilder.setHeader}. */
449
+ readonly headers: ReadonlyMap<string, string>;
450
+ /** Per-request RPC timeout override in ms. */
451
+ readonly timeout?: number | undefined;
452
+ /** Custom message ID for JetStream deduplication. */
453
+ readonly messageId?: string | undefined;
454
+ /** Schedule options for delayed delivery. */
455
+ readonly schedule?: ScheduleRecordOptions | undefined;
456
+ /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
457
+ readonly ttl?: string | undefined;
458
+ constructor(
459
+ /** Message payload. */
460
+ data: TData,
461
+ /** Custom headers set via {@link JetstreamRecordBuilder.setHeader}. */
462
+ headers: ReadonlyMap<string, string>,
463
+ /** Per-request RPC timeout override in ms. */
464
+ timeout?: number | undefined,
465
+ /** Custom message ID for JetStream deduplication. */
466
+ messageId?: string | undefined,
467
+ /** Schedule options for delayed delivery. */
468
+ schedule?: ScheduleRecordOptions | undefined,
469
+ /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
470
+ ttl?: string | undefined);
471
+ }
472
+ /**
473
+ * Fluent builder for constructing JetstreamRecord instances.
474
+ *
475
+ * Protected headers (`correlation-id`, `reply-to`, `error`) cannot be
476
+ * set by the user — attempting to do so throws an error at build time.
477
+ */
478
+ declare class JetstreamRecordBuilder<TData = unknown> {
479
+ private data;
480
+ private readonly headers;
481
+ private timeout;
482
+ private messageId;
483
+ private scheduleOptions;
484
+ private ttlDuration;
485
+ constructor(data?: TData);
486
+ /**
487
+ * Set the message payload.
488
+ *
489
+ * @param data - Payload to serialize via the configured {@link Codec}.
490
+ */
491
+ setData(data: TData): this;
492
+ /**
493
+ * Set a single custom header.
494
+ *
495
+ * @param key - Header name (e.g. `'x-tenant'`).
496
+ * @param value - Header value.
497
+ * @throws Error if the header name is reserved by the transport.
498
+ */
499
+ setHeader(key: string, value: string): this;
500
+ /**
501
+ * Set multiple custom headers at once.
502
+ *
503
+ * @param headers - Key-value pairs to set as headers.
504
+ * @throws Error if any header name is reserved by the transport.
505
+ */
506
+ setHeaders(headers: Record<string, string>): this;
507
+ /**
508
+ * Set a custom message ID for JetStream deduplication.
509
+ *
510
+ * NATS JetStream uses this ID to detect duplicate publishes within the
511
+ * stream's `duplicate_window`. If two messages with the same ID arrive
512
+ * within the window, the second is silently dropped.
513
+ *
514
+ * When not set, a random UUID is generated automatically.
515
+ *
516
+ * @param id - Unique message identifier (e.g. order ID, idempotency key).
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * new JetstreamRecordBuilder(data)
521
+ * .setMessageId(`order-${order.id}`)
522
+ * .build();
523
+ * ```
524
+ */
525
+ setMessageId(id: string): this;
526
+ /**
527
+ * Set per-request RPC timeout.
528
+ *
529
+ * @param ms - Timeout in milliseconds. Overrides the global RPC timeout for this request only.
530
+ */
531
+ setTimeout(ms: number): this;
532
+ /**
533
+ * Schedule one-shot delayed delivery.
534
+ *
535
+ * The message is held by NATS and delivered to the event consumer
536
+ * at the specified time. Requires NATS >= 2.12 and `allow_msg_schedules: true`
537
+ * on the event stream (via `events: { stream: { allow_msg_schedules: true } }`).
538
+ *
539
+ * Only meaningful for events (`client.emit()`). If used with RPC
540
+ * (`client.send()`), a warning is logged and the schedule is ignored.
541
+ *
542
+ * @param date - Delivery time. Must be in the future.
543
+ * @throws Error if the date is not in the future.
544
+ */
545
+ scheduleAt(date: Date): this;
546
+ /**
547
+ * Set per-message TTL (time-to-live).
548
+ *
549
+ * The message expires individually after the specified duration,
550
+ * independent of the stream's `max_age`. Requires NATS >= 2.11 and
551
+ * `allow_msg_ttl: true` on the stream.
552
+ *
553
+ * Only meaningful for events (`client.emit()`). If used with RPC
554
+ * (`client.send()`), a warning is logged and the TTL is ignored.
555
+ *
556
+ * @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * import { toNanos } from '@horizon-republic/nestjs-jetstream';
561
+ *
562
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
563
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
564
+ * ```
565
+ */
566
+ ttl(nanos: number): this;
567
+ /**
568
+ * Build the immutable {@link JetstreamRecord}.
569
+ *
570
+ * @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
571
+ */
572
+ build(): JetstreamRecord<TData>;
573
+ /**
574
+ * Validate that a header key is not reserved. NATS treats header names
575
+ * case-insensitively, so the check is against the lowercase form to keep
576
+ * `'X-Correlation-ID'`, `'x-correlation-id'`, and any other casing in
577
+ * lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
578
+ */
579
+ private validateHeaderKey;
580
+ }
581
+
582
+ /** Tag attached to every outgoing publish span so consumers can distinguish event / RPC / broadcast / ordered flows. */
583
+ declare enum PublishKind {
584
+ Event = "event",
585
+ RpcRequest = "rpc.request",
586
+ Broadcast = "broadcast",
587
+ Ordered = "ordered"
588
+ }
589
+ /** Tag attached to every incoming consume span. `Rpc` covers both Core and JetStream RPC handlers. */
590
+ declare enum ConsumeKind {
591
+ Event = "event",
592
+ Rpc = "rpc",
593
+ Broadcast = "broadcast",
594
+ Ordered = "ordered"
595
+ }
596
+ /**
597
+ * Handler metadata for the dispatched message. `pattern` is always set;
598
+ * `className` / `methodName` come from NestJS reflection when available.
599
+ */
600
+ interface HandlerMetadata {
601
+ readonly pattern: string;
602
+ readonly className?: string;
603
+ readonly methodName?: string;
604
+ }
605
+ /**
606
+ * Host / port pair surfaced as `server.address` / `server.port` span
607
+ * attributes. `port` is optional — OTel semconv makes it conditional on
608
+ * being different from the protocol default, and we'd rather emit nothing
609
+ * than invent a number the user never configured.
610
+ */
611
+ interface ServerEndpoint {
612
+ readonly host: string;
613
+ readonly port?: number;
614
+ }
615
+ /**
616
+ * Minimum message shape the consume span helper requires. Both `JsMsg`
617
+ * (JetStream) and Core NATS `Msg` satisfy it structurally; JetStream-only
618
+ * delivery metadata (`info`) is read separately and omitted on the Core
619
+ * RPC path.
620
+ */
621
+ interface ConsumeSourceMsg {
622
+ readonly subject: string;
623
+ readonly data: Uint8Array;
624
+ readonly headers?: MsgHdrs;
625
+ }
626
+ /** Context passed to {@link OtelOptions.publishHook} for an outgoing publish. */
627
+ interface JetstreamPublishContext {
628
+ /** Fully-resolved subject the message is being published to. */
629
+ readonly subject: string;
630
+ /** The record being published (payload, headers, builder-supplied options). */
631
+ readonly record: JetstreamRecord;
632
+ /** Classification of the outgoing operation. */
633
+ readonly kind: PublishKind;
634
+ }
635
+ /** Context passed to {@link OtelOptions.consumeHook} for an incoming dispatch. */
636
+ interface JetstreamConsumeContext {
637
+ /** Fully-resolved subject of the incoming message. */
638
+ readonly subject: string;
639
+ /**
640
+ * The incoming NATS message. JetStream messages (events, broadcast,
641
+ * ordered, JetStream RPC) satisfy `JsMsg`; Core RPC messages satisfy
642
+ * the looser {@link ConsumeSourceMsg} shape only. Narrow on `kind` to
643
+ * decide which fields are safe to access.
644
+ */
645
+ readonly msg: ConsumeSourceMsg;
646
+ /** Handler metadata; see {@link HandlerMetadata}. */
647
+ readonly handlerMetadata: HandlerMetadata;
648
+ /** Classification of the incoming operation. */
649
+ readonly kind: ConsumeKind;
650
+ }
651
+ /** Context passed to {@link OtelOptions.responseHook} once an outcome is known, just before the span ends. */
652
+ interface JetstreamResponseContext {
653
+ /** Subject the operation targeted. */
654
+ readonly subject: string;
655
+ /** Wall-clock duration of the operation in milliseconds. */
656
+ readonly durationMs: number;
657
+ /** Decoded reply payload for RPC client spans when available. */
658
+ readonly reply?: unknown;
659
+ /** Error instance if the operation failed. */
660
+ readonly error?: Error;
661
+ }
662
+ /**
663
+ * Classification outcome for a thrown error. Affects span status and
664
+ * attributes only — reply envelopes and internal logging are unchanged.
665
+ */
666
+ type ErrorClassification = 'expected' | 'unexpected';
667
+ /** Object form of {@link OtelOptions.captureBody}. */
668
+ interface CaptureBodyOptions {
669
+ /**
670
+ * Maximum number of bytes to capture. Payloads longer than this are
671
+ * truncated; a `messaging.nats.message.body.truncated` attribute is
672
+ * added to indicate truncation occurred.
673
+ *
674
+ * @default 4096
675
+ */
676
+ readonly maxBytes?: number;
677
+ /**
678
+ * Only capture body for subjects matching these glob patterns. Each
679
+ * pattern supports `*` wildcards and an optional leading `!` for
680
+ * exclusion. When omitted, body capture applies to all subjects.
681
+ */
682
+ readonly subjectAllowlist?: readonly string[];
683
+ }
684
+ /**
685
+ * OpenTelemetry configuration for `JetstreamModule.forRoot({ otel: … })`.
686
+ * All fields are optional; when the host app has not registered an OTel
687
+ * SDK, every call made by the library is a no-op regardless of config.
688
+ */
689
+ interface OtelOptions {
690
+ /**
691
+ * Global kill switch. When `false`, no spans are emitted, no propagation
692
+ * is attempted, and no hooks fire.
693
+ *
694
+ * @default true
695
+ */
696
+ readonly enabled?: boolean;
697
+ /**
698
+ * Which trace kinds to emit.
699
+ *
700
+ * - `'default'` — publish, consume, RPC client round-trip, dead letter
701
+ * - `'all'` — every trace kind defined in {@link JetstreamTrace}
702
+ * - `'none'` — emit no spans at all (useful with `enabled: true` for
703
+ * pure trace-context propagation without the span overhead)
704
+ * - `JetstreamTrace[]` — explicit selection
705
+ *
706
+ * @default 'default'
707
+ */
708
+ readonly traces?: readonly JetstreamTrace[] | 'default' | 'all' | 'none';
709
+ /**
710
+ * Header names to capture as span attributes (`messaging.header.<name>`).
711
+ * Match is case-insensitive. Glob wildcards (`*`) and negation (`!prefix-*`)
712
+ * are supported in the per-pattern form.
713
+ *
714
+ * SECURITY / GDPR: headers may contain authentication tokens, session
715
+ * identifiers, or other sensitive data. Captured values are exported to
716
+ * the configured OTel backend. Use an explicit allowlist in production.
717
+ * Setting this to `true` captures every header and is intended for
718
+ * development environments only.
719
+ *
720
+ * Transport-internal headers (`x-correlation-id`, `x-reply-to`, `x-error`,
721
+ * `x-subject`, `x-caller-name`) and propagator-owned headers
722
+ * (`traceparent`, `tracestate`, `baggage`, `sentry-trace`, `b3`, …) are
723
+ * always suppressed regardless of the allowlist.
724
+ *
725
+ * @default ['x-request-id']
726
+ */
727
+ readonly captureHeaders?: readonly string[] | boolean;
728
+ /**
729
+ * Capture the message payload as the `messaging.nats.message.body` span
730
+ * attribute. Defaults to `false` for privacy and cost reasons.
731
+ *
732
+ * Passing `true` captures up to 4 KiB per message. Use the object form
733
+ * for finer control over size and subject scope.
734
+ *
735
+ * SECURITY / GDPR: payloads commonly contain PII, credentials, financial
736
+ * data, or content regulated by HIPAA / PCI-DSS. Enabling body capture
737
+ * in production is almost always a policy violation. Prefer enabling
738
+ * only in development, or pair with a custom `SpanProcessor` that
739
+ * scrubs or drops the attribute before export.
740
+ *
741
+ * @default false
742
+ */
743
+ readonly captureBody?: boolean | CaptureBodyOptions;
744
+ /**
745
+ * Invoked after a publish span has been started and before the actual
746
+ * publish call executes. Use to enrich the span with custom attributes.
747
+ * Must be synchronous — thrown errors are caught and logged at debug
748
+ * level without affecting the publish path.
749
+ */
750
+ publishHook?(span: Span, ctx: JetstreamPublishContext): void;
751
+ /**
752
+ * Invoked after a consume span has been started and before the handler
753
+ * is dispatched. Use to enrich the span with custom attributes derived
754
+ * from the incoming message or handler metadata.
755
+ */
756
+ consumeHook?(span: Span, ctx: JetstreamConsumeContext): void;
757
+ /**
758
+ * Invoked once an operation's outcome is known (success, error, or
759
+ * timeout) and the span status has been set, just before the span ends.
760
+ * Fires for publish, consume, and RPC client spans.
761
+ */
762
+ responseHook?(span: Span, ctx: JetstreamResponseContext): void;
763
+ /**
764
+ * Predicate evaluated at the top of every outgoing publish. Returning
765
+ * `false` skips span creation for that publish; propagation of trace
766
+ * context through headers still occurs, so downstream consumers are
767
+ * unaffected. Useful for suppressing noise from health-check or
768
+ * internal-only subjects.
769
+ */
770
+ shouldTracePublish?(subject: string, record: JetstreamRecord): boolean;
771
+ /**
772
+ * Predicate evaluated at the top of every incoming message delivery.
773
+ * Returning `false` skips span creation for that delivery; the handler
774
+ * still executes normally and trace context is still extracted from
775
+ * headers for downstream calls.
776
+ */
777
+ shouldTraceConsume?(subject: string, msg: ConsumeSourceMsg): boolean;
778
+ /**
779
+ * Classify a thrown error as `'expected'` (business error, part of the
780
+ * RPC contract) or `'unexpected'` (infrastructure failure or bug). Drives
781
+ * OpenTelemetry span status and attributes only.
782
+ *
783
+ * - `'expected'` → span status `OK` with `jetstream.rpc.reply.has_error`
784
+ * and `jetstream.rpc.reply.error.code` attributes
785
+ * - `'unexpected'` → span status `ERROR` with `span.recordException(err)`
786
+ *
787
+ * Reply envelopes delivered to RPC clients are identical in both cases.
788
+ * This classification affects only the observability artifact.
789
+ *
790
+ * The default recognizes NestJS-idiomatic primitives for business errors:
791
+ * `RpcException` and `HttpException`. Teams with custom error hierarchies
792
+ * override to recognize their own types.
793
+ *
794
+ * @example
795
+ * errorClassifier: (err) => {
796
+ * if (err instanceof MyDomainError) return 'expected';
797
+ * if (err?.code?.startsWith('BIZ_')) return 'expected';
798
+ * return 'unexpected';
799
+ * }
800
+ */
801
+ errorClassifier?(err: unknown): ErrorClassification;
802
+ }
803
+
120
804
  /**
121
805
  * Stream config overrides exposed to users.
122
806
  *
@@ -385,6 +1069,16 @@ interface JetstreamModuleOptions {
385
1069
  * Merged with `name` and `servers` — those take precedence.
386
1070
  */
387
1071
  connectionOptions?: Partial<ConnectionOptions>;
1072
+ /**
1073
+ * OpenTelemetry integration. When omitted, sensible defaults are applied:
1074
+ * tracing is enabled, default trace kinds are emitted, only standard
1075
+ * correlation headers are captured. If no OTel SDK is registered in the
1076
+ * consuming application, all tracer calls are no-ops — there is no
1077
+ * runtime cost.
1078
+ *
1079
+ * @see OtelOptions
1080
+ */
1081
+ otel?: OtelOptions;
388
1082
  }
389
1083
  /** Options for `JetstreamModule.forFeature()`. */
390
1084
  interface JetstreamFeatureOptions {
@@ -490,105 +1184,6 @@ interface RpcRouterOptions {
490
1184
  ackExtension?: boolean | number;
491
1185
  }
492
1186
 
493
- /**
494
- * Central event bus for transport lifecycle notifications.
495
- *
496
- * Dispatches events to user-provided hooks. Events without a
497
- * registered hook are silently ignored — no default logging.
498
- *
499
- * @example
500
- * ```typescript
501
- * const bus = new EventBus(logger, {
502
- * [TransportEvent.Error]: (err) => sentry.captureException(err),
503
- * });
504
- *
505
- * bus.emit(TransportEvent.Error, new Error('timeout'), 'rpc-router');
506
- * // → calls sentry
507
- *
508
- * bus.emit(TransportEvent.Connect, 'nats://localhost:4222');
509
- * // → no-op (no hook registered)
510
- * ```
511
- */
512
- declare class EventBus {
513
- private readonly hooks;
514
- private readonly logger;
515
- constructor(logger: Logger, hooks?: Partial<TransportHooks>);
516
- /**
517
- * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
518
- *
519
- * @param event - The {@link TransportEvent} to emit.
520
- * @param args - Arguments matching the hook signature for this event.
521
- */
522
- emit<K extends keyof TransportHooks>(event: K, ...args: Parameters<TransportHooks[K]>): void;
523
- /**
524
- * Hot-path optimized emit for MessageRouted events.
525
- * Avoids rest/spread overhead of the generic `emit()`.
526
- */
527
- emitMessageRouted(subject: string, kind: MessageKind): void;
528
- private callHook;
529
- }
530
-
531
- /**
532
- * Manages the lifecycle of a single NATS connection shared across the application.
533
- *
534
- * Provides both Promise-based and Observable-based access to the connection:
535
- * - `connect()` / `getConnection()` — async/await for one-time setup
536
- * - `nc$` — cached observable (shareReplay) for reactive consumers
537
- * - `status$` — live connection status event stream
538
- *
539
- * One instance per application, created by `JetstreamModule.forRoot()`.
540
- */
541
- declare class ConnectionProvider {
542
- private readonly options;
543
- private readonly eventBus;
544
- /** Cached observable that replays the established connection to new subscribers. */
545
- readonly nc$: Observable<NatsConnection>;
546
- /** Live stream of connection status events (no replay). */
547
- readonly status$: Observable<Status>;
548
- private readonly logger;
549
- private connection;
550
- private connectionPromise;
551
- private jsClient;
552
- private jsmInstance;
553
- private jsmPromise;
554
- constructor(options: JetstreamModuleOptions, eventBus: EventBus);
555
- /**
556
- * Establish NATS connection. Idempotent — returns cached connection on subsequent calls.
557
- *
558
- * @throws Error if connection is refused (fail fast).
559
- */
560
- getConnection(): Promise<NatsConnection>;
561
- /**
562
- * Get the JetStream manager. Cached after first call.
563
- *
564
- * @returns The JetStreamManager for stream/consumer administration.
565
- */
566
- getJetStreamManager(): Promise<JetStreamManager>;
567
- /**
568
- * Get a cached JetStream client.
569
- *
570
- * Invalidated automatically on reconnect and shutdown so consumers always
571
- * operate against the live connection.
572
- *
573
- * @returns The cached JetStreamClient.
574
- * @throws Error if the connection has not been established yet.
575
- */
576
- getJetStreamClient(): JetStreamClient;
577
- /** Direct access to the raw NATS connection, or `null` if not yet connected. */
578
- get unwrap(): NatsConnection | null;
579
- /**
580
- * Gracefully shut down the connection.
581
- *
582
- * Sequence: drain → wait for close. Falls back to force-close on error.
583
- */
584
- shutdown(): Promise<void>;
585
- private initJetStreamManager;
586
- /** Internal: establish the physical connection with reconnect monitoring. */
587
- private establish;
588
- /** Subscribe to connection status events and emit hooks. */
589
- private monitorStatus;
590
- }
591
-
592
1187
  /**
593
1188
  * Registry mapping NATS subjects to NestJS message handlers.
594
1189
  *
@@ -651,13 +1246,15 @@ declare class PatternRegistry {
651
1246
  * This is the default RPC mode — lowest latency, no persistence overhead.
652
1247
  */
653
1248
  declare class CoreRpcServer {
654
- private readonly options;
655
1249
  private readonly connection;
656
1250
  private readonly patternRegistry;
657
1251
  private readonly codec;
658
1252
  private readonly eventBus;
659
1253
  private readonly logger;
660
1254
  private subscription;
1255
+ private readonly otel;
1256
+ private readonly serviceName;
1257
+ private readonly serverEndpoint;
661
1258
  constructor(options: JetstreamModuleOptions, connection: ConnectionProvider, patternRegistry: PatternRegistry, codec: Codec, eventBus: EventBus);
662
1259
  /** Start listening for RPC requests on the command subject. */
663
1260
  start(): Promise<void>;
@@ -684,6 +1281,9 @@ declare class StreamProvider {
684
1281
  private readonly connection;
685
1282
  private readonly logger;
686
1283
  private readonly migration;
1284
+ private readonly otel;
1285
+ private readonly otelServiceName;
1286
+ private readonly otelEndpoint;
687
1287
  constructor(options: JetstreamModuleOptions, connection: ConnectionProvider);
688
1288
  /**
689
1289
  * Ensure all required streams exist with correct configuration.
@@ -727,21 +1327,6 @@ declare class StreamProvider {
727
1327
  private stripTransportControlled;
728
1328
  }
729
1329
 
730
- /**
731
- * Routes incoming event messages (workqueue, broadcast, and ordered) to NestJS handlers.
732
- *
733
- * **Workqueue & Broadcast** — at-least-once delivery:
734
- * - Success -> ack | Error -> nak (retry) | Dead letter -> term
735
- *
736
- * **Ordered** — strict sequential delivery:
737
- * - No ack/nak/DLQ — nats.js auto-acknowledges ordered consumer messages.
738
- * - Handler errors are logged but do not affect delivery.
739
- *
740
- * **Dead-Letter Queue (DLQ) - for handling failed message deliveries**
741
- * - If `options.dlq` is configured, messages that exhaust their max delivery attempts are published to a DLQ stream.
742
- * - The DLQ stream name is derived from the service name (e.g., `orders__microservice_dlq-stream`).
743
- * - Original message data and metadata are preserved in the DLQ message, with additional headers indicating the reason for failure.
744
- */
745
1330
  declare class EventRouter {
746
1331
  private readonly messageProvider;
747
1332
  private readonly patternRegistry;
@@ -754,6 +1339,9 @@ declare class EventRouter {
754
1339
  private readonly options?;
755
1340
  private readonly logger;
756
1341
  private readonly subscriptions;
1342
+ private readonly otel;
1343
+ private readonly serviceName;
1344
+ private readonly serverEndpoint;
757
1345
  constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, codec: Codec, eventBus: EventBus, deadLetterConfig?: DeadLetterConfig | undefined, processingConfig?: EventProcessingConfig | undefined, ackWaitMap?: Map<StreamKind, number> | undefined, connection?: ConnectionProvider | undefined, options?: JetstreamModuleOptions | undefined);
758
1346
  /**
759
1347
  * Update the max_deliver thresholds from actual NATS consumer configs.
@@ -764,28 +1352,15 @@ declare class EventRouter {
764
1352
  start(): void;
765
1353
  /** Stop routing and unsubscribe from all streams. */
766
1354
  destroy(): void;
767
- /** Subscribe to a message stream and route each message. */
1355
+ /** Subscribe to a message stream and route each message to its handler. */
768
1356
  private subscribeToStream;
769
1357
  private getConcurrency;
770
1358
  private getAckExtensionConfig;
771
- /** Handle a single event message with error isolation. */
772
- private handleSafe;
773
- /** Handle an ordered message with error isolation. */
774
- private handleOrderedSafe;
775
- /** Resolve handler, decode payload, and build context. Returns null on failure. */
776
- private decodeMessage;
777
- /** Execute handler, then ack on success or nak/dead-letter on failure. */
778
- private executeHandler;
779
- /** Check if the message has exhausted all delivery attempts. */
780
- private isDeadLetter;
781
- /** Handle a dead letter: invoke callback, then term or nak based on result. */
782
- /**
783
- * Fallback execution for a dead letter when DLQ is disabled, or when
784
- * publishing to the DLQ stream fails (due to network or NATS errors).
785
- *
786
- * Triggers the user-provided `onDeadLetter` hook for logging/alerting.
787
- * On success, terminates the message. On error, leaves it unacknowledged (nak)
788
- * so NATS can retry the delivery on the next cycle.
1359
+ /**
1360
+ * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
1361
+ * success or `nak` on hook failure so NATS retries on the next delivery
1362
+ * cycle. Used when DLQ stream isn't configured, or when publishing to it
1363
+ * failed and we still have to surface the message somewhere observable.
789
1364
  */
790
1365
  private fallbackToOnDeadLetterCallback;
791
1366
  /**
@@ -832,17 +1407,16 @@ declare class RpcRouter {
832
1407
  private resolvedAckExtensionInterval;
833
1408
  private subscription;
834
1409
  private cachedNc;
835
- constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, connection: ConnectionProvider, codec: Codec, eventBus: EventBus, rpcOptions?: RpcRouterOptions | undefined, ackWaitMap?: Map<StreamKind, number> | undefined);
1410
+ private readonly otel;
1411
+ private readonly serviceName;
1412
+ private readonly serverEndpoint;
1413
+ constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, connection: ConnectionProvider, codec: Codec, eventBus: EventBus, rpcOptions?: RpcRouterOptions | undefined, ackWaitMap?: Map<StreamKind, number> | undefined, options?: JetstreamModuleOptions);
836
1414
  /** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
837
1415
  private get ackExtensionInterval();
838
1416
  /** Start routing command messages to handlers. */
839
1417
  start(): Promise<void>;
840
1418
  /** Stop routing and unsubscribe. */
841
1419
  destroy(): void;
842
- /** Handle a single RPC command message with error isolation. */
843
- private handleSafe;
844
- /** Execute handler, publish response, settle message. */
845
- private executeHandler;
846
1420
  }
847
1421
 
848
1422
  /**
@@ -857,6 +1431,9 @@ declare class ConsumerProvider {
857
1431
  private readonly streamProvider;
858
1432
  private readonly patternRegistry;
859
1433
  private readonly logger;
1434
+ private readonly otel;
1435
+ private readonly otelServiceName;
1436
+ private readonly otelEndpoint;
860
1437
  constructor(options: JetstreamModuleOptions, connection: ConnectionProvider, streamProvider: StreamProvider, patternRegistry: PatternRegistry);
861
1438
  /**
862
1439
  * Ensure consumers exist for the specified kinds.
@@ -978,7 +1555,7 @@ declare class MessageProvider {
978
1555
  private createOrderedFlow;
979
1556
  /** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
980
1557
  private createSelfHealingFlow;
981
- /** Single iteration: create ordered consumer -> iterate messages. */
1558
+ /** Single iteration: create ordered consumer -> push messages into the subject. */
982
1559
  private consumeOrderedOnce;
983
1560
  }
984
1561
 
@@ -1033,6 +1610,21 @@ declare class MetadataProvider {
1033
1610
  private openBucket;
1034
1611
  }
1035
1612
 
1613
+ /**
1614
+ * NATS JetStream API error codes used by the transport.
1615
+ *
1616
+ * Ref: https://github.com/nats-io/nats-server (server error definitions)
1617
+ * Verified on NATS 2.12.6 via integration tests (2026-04-02).
1618
+ */
1619
+ declare enum NatsErrorCode {
1620
+ /** Consumer does not exist on the specified stream. */
1621
+ ConsumerNotFound = 10014,
1622
+ /** Consumer name already in use with different configuration (race condition on create). */
1623
+ ConsumerAlreadyExists = 10148,
1624
+ /** Stream does not exist. */
1625
+ StreamNotFound = 10059
1626
+ }
1627
+
1036
1628
  /**
1037
1629
  * NestJS custom transport strategy for NATS JetStream.
1038
1630
  *
@@ -1206,265 +1798,65 @@ declare class JetstreamModule implements OnApplicationShutdown {
1206
1798
  }
1207
1799
 
1208
1800
  /**
1209
- * NestJS ClientProxy implementation for the JetStream transport.
1210
- *
1211
- * Supports two operational modes:
1212
- * - **Core mode** (default): Uses `nc.request()` for RPC, `nc.publish()` for events.
1213
- * - **JetStream mode**: Uses `js.publish()` for RPC commands + inbox for responses.
1214
- *
1215
- * Events always go through JetStream publish for guaranteed delivery.
1216
- * The mode only affects RPC (request/reply) behavior.
1217
- *
1218
- * Clients are lightweight — they share the NATS connection from `forRoot()`.
1219
- */
1220
- declare class JetstreamClient extends ClientProxy {
1221
- private readonly rootOptions;
1222
- private readonly connection;
1223
- private readonly codec;
1224
- private readonly eventBus;
1225
- private readonly logger;
1226
- /** Target service name this client sends messages to. */
1227
- private readonly targetName;
1228
- /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
1229
- private readonly callerName;
1230
- /** Shared inbox for JetStream-mode RPC responses. */
1231
- private inbox;
1232
- private inboxSubscription;
1233
- /** Pending JetStream-mode RPC callbacks, keyed by correlation ID. */
1234
- private readonly pendingMessages;
1235
- /** Pending JetStream-mode RPC timeouts, keyed by correlation ID. */
1236
- private readonly pendingTimeouts;
1237
- /** Subscription to connection status events for disconnect handling. */
1238
- private statusSubscription;
1239
- constructor(rootOptions: JetstreamModuleOptions, targetServiceName: string, connection: ConnectionProvider, codec: Codec, eventBus: EventBus);
1240
- /**
1241
- * Establish connection. Called automatically by NestJS on first use.
1242
- *
1243
- * Sets up the JetStream RPC inbox (if in JetStream mode) and subscribes
1244
- * to connection status events for fail-fast disconnect handling.
1245
- *
1246
- * @returns The underlying NATS connection.
1247
- */
1248
- connect(): Promise<NatsConnection>;
1249
- /** Clean up resources: reject pending RPCs, unsubscribe from status events. */
1250
- close(): Promise<void>;
1251
- /**
1252
- * Direct access to the raw NATS connection.
1253
- *
1254
- * @throws Error if not connected.
1255
- */
1256
- unwrap<T = NatsConnection>(): T;
1257
- /**
1258
- * Publish a fire-and-forget event to JetStream.
1259
- *
1260
- * Events are published to either the workqueue stream or broadcast stream
1261
- * depending on the subject prefix. When a schedule is present the message
1262
- * is published to a `_sch` subject within the same stream, with the target
1263
- * set to the original event subject.
1264
- */
1265
- protected dispatchEvent<T = unknown>(packet: ReadPacket): Promise<T>;
1266
- /**
1267
- * Publish an RPC command and register callback for response.
1268
- *
1269
- * Core mode: uses nc.request() with timeout.
1270
- * JetStream mode: publishes to stream + waits for inbox response.
1271
- */
1272
- protected publish(packet: ReadPacket, callback: (p: WritePacket) => void): () => void;
1273
- /** Core mode: nc.request() with timeout. */
1274
- private publishCoreRpc;
1275
- /** JetStream mode: publish to stream + wait for inbox response. */
1276
- private publishJetStreamRpc;
1277
- /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
1278
- private handleDisconnect;
1279
- /** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
1280
- private rejectPendingRpcs;
1281
- /** Setup shared inbox subscription for JetStream RPC responses. */
1282
- private setupInbox;
1283
- /** Route an inbox reply to the matching pending callback. */
1284
- private routeInboxReply;
1285
- /** Build event subject — workqueue, broadcast, or ordered. */
1286
- private buildEventSubject;
1287
- /** Build NATS headers merging custom headers with transport headers. */
1288
- private buildHeaders;
1289
- /** Extract data, headers, timeout, and schedule from raw packet data or JetstreamRecord. */
1290
- private extractRecordData;
1291
- /**
1292
- * Build a schedule-holder subject for NATS message scheduling.
1293
- *
1294
- * The schedule-holder subject resides in the same stream as the target but
1295
- * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
1296
- * NATS holds the message and publishes it to the target subject after the delay.
1297
- *
1298
- * Examples:
1299
- * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
1300
- * - `broadcast.config.updated` → `broadcast._sch.config.updated`
1301
- */
1302
- private buildScheduleSubject;
1303
- private getRpcTimeout;
1304
- }
1305
-
1306
- /**
1307
- * Immutable message record for JetStream transport.
1801
+ * Default JSON codec using native `TextEncoder`/`TextDecoder`.
1308
1802
  *
1309
- * Compatible with NestJS record builder pattern (like RmqRecord, NatsRecord).
1310
- * Pass as the second argument to `client.send()` or `client.emit()`.
1803
+ * Serializes values to JSON via `JSON.stringify` and encodes the
1804
+ * resulting string into a `Uint8Array`. Decoding reverses the process.
1311
1805
  *
1312
1806
  * @example
1313
1807
  * ```typescript
1314
- * const record = new JetstreamRecordBuilder({ id: 1 })
1315
- * .setHeader('x-tenant', 'acme')
1316
- * .setTimeout(5000)
1317
- * .build();
1318
- *
1319
- * client.send('get.user', record);
1808
+ * const codec = new JsonCodec();
1809
+ * const bytes = codec.encode({ hello: 'world' });
1810
+ * const data = codec.decode(bytes); // { hello: 'world' }
1320
1811
  * ```
1321
1812
  */
1322
- declare class JetstreamRecord<TData = unknown> {
1323
- /** Message payload. */
1324
- readonly data: TData;
1325
- /** Custom headers set via {@link JetstreamRecordBuilder.setHeader}. */
1326
- readonly headers: ReadonlyMap<string, string>;
1327
- /** Per-request RPC timeout override in ms. */
1328
- readonly timeout?: number | undefined;
1329
- /** Custom message ID for JetStream deduplication. */
1330
- readonly messageId?: string | undefined;
1331
- /** Schedule options for delayed delivery. */
1332
- readonly schedule?: ScheduleRecordOptions | undefined;
1333
- /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
1334
- readonly ttl?: string | undefined;
1335
- constructor(
1336
- /** Message payload. */
1337
- data: TData,
1338
- /** Custom headers set via {@link JetstreamRecordBuilder.setHeader}. */
1339
- headers: ReadonlyMap<string, string>,
1340
- /** Per-request RPC timeout override in ms. */
1341
- timeout?: number | undefined,
1342
- /** Custom message ID for JetStream deduplication. */
1343
- messageId?: string | undefined,
1344
- /** Schedule options for delayed delivery. */
1345
- schedule?: ScheduleRecordOptions | undefined,
1346
- /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
1347
- ttl?: string | undefined);
1813
+ declare class JsonCodec implements Codec {
1814
+ encode(data: unknown): Uint8Array;
1815
+ decode(data: Uint8Array): unknown;
1348
1816
  }
1817
+
1349
1818
  /**
1350
- * Fluent builder for constructing JetstreamRecord instances.
1819
+ * Minimal shape of a `msgpackr` `Packr` instance used by {@link MsgpackCodec}.
1351
1820
  *
1352
- * Protected headers (`correlation-id`, `reply-to`, `error`) cannot be
1353
- * set by the user — attempting to do so throws an error at build time.
1821
+ * Typed locally so consumers who do not use MessagePack encoding never pay
1822
+ * for a `msgpackr` import.
1354
1823
  */
1355
- declare class JetstreamRecordBuilder<TData = unknown> {
1356
- private data;
1357
- private readonly headers;
1358
- private timeout;
1359
- private messageId;
1360
- private scheduleOptions;
1361
- private ttlDuration;
1362
- constructor(data?: TData);
1363
- /**
1364
- * Set the message payload.
1365
- *
1366
- * @param data - Payload to serialize via the configured {@link Codec}.
1367
- */
1368
- setData(data: TData): this;
1369
- /**
1370
- * Set a single custom header.
1371
- *
1372
- * @param key - Header name (e.g. `'x-tenant'`).
1373
- * @param value - Header value.
1374
- * @throws Error if the header name is reserved by the transport.
1375
- */
1376
- setHeader(key: string, value: string): this;
1377
- /**
1378
- * Set multiple custom headers at once.
1379
- *
1380
- * @param headers - Key-value pairs to set as headers.
1381
- * @throws Error if any header name is reserved by the transport.
1382
- */
1383
- setHeaders(headers: Record<string, string>): this;
1384
- /**
1385
- * Set a custom message ID for JetStream deduplication.
1386
- *
1387
- * NATS JetStream uses this ID to detect duplicate publishes within the
1388
- * stream's `duplicate_window`. If two messages with the same ID arrive
1389
- * within the window, the second is silently dropped.
1390
- *
1391
- * When not set, a random UUID is generated automatically.
1392
- *
1393
- * @param id - Unique message identifier (e.g. order ID, idempotency key).
1394
- *
1395
- * @example
1396
- * ```typescript
1397
- * new JetstreamRecordBuilder(data)
1398
- * .setMessageId(`order-${order.id}`)
1399
- * .build();
1400
- * ```
1401
- */
1402
- setMessageId(id: string): this;
1403
- /**
1404
- * Set per-request RPC timeout.
1405
- *
1406
- * @param ms - Timeout in milliseconds. Overrides the global RPC timeout for this request only.
1407
- */
1408
- setTimeout(ms: number): this;
1409
- /**
1410
- * Schedule one-shot delayed delivery.
1411
- *
1412
- * The message is held by NATS and delivered to the event consumer
1413
- * at the specified time. Requires NATS >= 2.12 and `allow_msg_schedules: true`
1414
- * on the event stream (via `events: { stream: { allow_msg_schedules: true } }`).
1415
- *
1416
- * Only meaningful for events (`client.emit()`). If used with RPC
1417
- * (`client.send()`), a warning is logged and the schedule is ignored.
1418
- *
1419
- * @param date - Delivery time. Must be in the future.
1420
- * @throws Error if the date is not in the future.
1421
- */
1422
- scheduleAt(date: Date): this;
1423
- /**
1424
- * Set per-message TTL (time-to-live).
1425
- *
1426
- * The message expires individually after the specified duration,
1427
- * independent of the stream's `max_age`. Requires NATS >= 2.11 and
1428
- * `allow_msg_ttl: true` on the stream.
1429
- *
1430
- * Only meaningful for events (`client.emit()`). If used with RPC
1431
- * (`client.send()`), a warning is logged and the TTL is ignored.
1432
- *
1433
- * @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
1434
- *
1435
- * @example
1436
- * ```typescript
1437
- * import { toNanos } from '@horizon-republic/nestjs-jetstream';
1438
- *
1439
- * new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
1440
- * new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
1441
- * ```
1442
- */
1443
- ttl(nanos: number): this;
1444
- /**
1445
- * Build the immutable {@link JetstreamRecord}.
1446
- *
1447
- * @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
1448
- */
1449
- build(): JetstreamRecord<TData>;
1450
- /** Validate that a header key is not reserved. */
1451
- private validateHeaderKey;
1824
+ interface PackrLike {
1825
+ pack(data: unknown): Uint8Array;
1826
+ unpack(data: Uint8Array): unknown;
1452
1827
  }
1453
-
1454
1828
  /**
1455
- * Default JSON codec using native `TextEncoder`/`TextDecoder`.
1829
+ * MessagePack codec backed by a caller-provided `msgpackr` `Packr` instance.
1456
1830
  *
1457
- * Serializes values to JSON via `JSON.stringify` and encodes the
1458
- * resulting string into a `Uint8Array`. Decoding reverses the process.
1831
+ * Use this codec when publishing structured payloads larger than roughly
1832
+ * 1–2 KB below that size the default {@link JsonCodec} wins on per-call
1833
+ * constant overhead. Above it, MessagePack encodes and decodes several times
1834
+ * faster and produces smaller wire frames. The format is cross-language, so
1835
+ * Node producers and non-Node consumers (Python, Go, Java, Rust, ...) stay
1836
+ * interoperable.
1837
+ *
1838
+ * Requires installing the optional `msgpackr` peer dependency:
1839
+ *
1840
+ * ```bash
1841
+ * npm install msgpackr
1842
+ * # or: pnpm add msgpackr
1843
+ * ```
1459
1844
  *
1460
1845
  * @example
1461
1846
  * ```typescript
1462
- * const codec = new JsonCodec();
1463
- * const bytes = codec.encode({ hello: 'world' });
1464
- * const data = codec.decode(bytes); // { hello: 'world' }
1847
+ * import { JetstreamModule, MsgpackCodec } from '@horizon-republic/nestjs-jetstream';
1848
+ * import { Packr } from 'msgpackr';
1849
+ *
1850
+ * JetstreamModule.forRoot({
1851
+ * name: 'orders',
1852
+ * servers: ['nats://localhost:4222'],
1853
+ * codec: new MsgpackCodec(new Packr()),
1854
+ * });
1465
1855
  * ```
1466
1856
  */
1467
- declare class JsonCodec implements Codec {
1857
+ declare class MsgpackCodec implements Codec {
1858
+ private readonly packr;
1859
+ constructor(packr: PackrLike);
1468
1860
  encode(data: unknown): Uint8Array;
1469
1861
  decode(data: Uint8Array): unknown;
1470
1862
  }
@@ -1625,8 +2017,6 @@ declare const JETSTREAM_OPTIONS: unique symbol;
1625
2017
  declare const JETSTREAM_CONNECTION: unique symbol;
1626
2018
  /** Token for the global Codec instance. */
1627
2019
  declare const JETSTREAM_CODEC: unique symbol;
1628
- /** Token for the EventBus instance. */
1629
- declare const JETSTREAM_EVENT_BUS: unique symbol;
1630
2020
  /**
1631
2021
  * Generate the injection token for a `forFeature()` client.
1632
2022
  *
@@ -1659,6 +2049,28 @@ type TimeUnit = 'ms' | 'seconds' | 'minutes' | 'hours' | 'days';
1659
2049
  * ```
1660
2050
  */
1661
2051
  declare const toNanos: (value: number, unit: TimeUnit) => number;
2052
+ /** Default config for workqueue event streams. */
2053
+ declare const DEFAULT_EVENT_STREAM_CONFIG: Partial<StreamConfig>;
2054
+ /** Default config for RPC command streams (jetstream mode only). */
2055
+ declare const DEFAULT_COMMAND_STREAM_CONFIG: Partial<StreamConfig>;
2056
+ /** Default config for broadcast event streams. */
2057
+ declare const DEFAULT_BROADCAST_STREAM_CONFIG: Partial<StreamConfig>;
2058
+ /** Default config for ordered event streams (Limits retention). */
2059
+ declare const DEFAULT_ORDERED_STREAM_CONFIG: Partial<StreamConfig>;
2060
+ /** Default config for dead-letter queue (DLQ) streams. */
2061
+ declare const DEFAULT_DLQ_STREAM_CONFIG: Partial<StreamConfig>;
2062
+ /** Default config for workqueue event consumers. */
2063
+ declare const DEFAULT_EVENT_CONSUMER_CONFIG: Partial<ConsumerConfig>;
2064
+ /** Default config for RPC command consumers (jetstream mode only). */
2065
+ declare const DEFAULT_COMMAND_CONSUMER_CONFIG: Partial<ConsumerConfig>;
2066
+ /** Default config for broadcast event consumers. */
2067
+ declare const DEFAULT_BROADCAST_CONSUMER_CONFIG: Partial<ConsumerConfig>;
2068
+ /** Default RPC timeout for Core mode (30 seconds). */
2069
+ declare const DEFAULT_RPC_TIMEOUT = 30000;
2070
+ /** Default RPC timeout for JetStream mode (3 minutes). */
2071
+ declare const DEFAULT_JETSTREAM_RPC_TIMEOUT = 180000;
2072
+ /** Default graceful shutdown timeout (10 seconds). */
2073
+ declare const DEFAULT_SHUTDOWN_TIMEOUT = 10000;
1662
2074
  /** Default KV bucket name for handler metadata. */
1663
2075
  declare const DEFAULT_METADATA_BUCKET = "handler_registry";
1664
2076
  /** Default number of KV bucket replicas. */
@@ -1698,7 +2110,7 @@ declare enum JetstreamHeader {
1698
2110
  Error = "x-error"
1699
2111
  }
1700
2112
  declare enum JetstreamDlqHeader {
1701
- /** Reason for the message being sent to the DLQ (error message or 'max_deliver_exceeded') */
2113
+ /** Reason for the message being sent to the DLQ — the last handler error message. */
1702
2114
  DeadLetterReason = "x-dead-letter-reason",
1703
2115
  /** Original NATS subject the message was originally published to */
1704
2116
  OriginalSubject = "x-original-subject",
@@ -1709,6 +2121,8 @@ declare enum JetstreamDlqHeader {
1709
2121
  /** Number of times the message has been delivered */
1710
2122
  DeliveryCount = "x-delivery-count"
1711
2123
  }
2124
+ /** Set of header names that are reserved and cannot be set by users. */
2125
+ declare const RESERVED_HEADERS: Set<string>;
1712
2126
  /**
1713
2127
  * Build the internal service name with microservice suffix.
1714
2128
  *
@@ -1770,4 +2184,4 @@ declare const isJetStreamRpcMode: (rpc: RpcConfig | undefined) => boolean;
1770
2184
  /** Check if the RPC config specifies Core mode (default). */
1771
2185
  declare const isCoreRpcMode: (rpc: RpcConfig | undefined) => boolean;
1772
2186
 
1773
- export { type Codec, DEFAULT_METADATA_BUCKET, DEFAULT_METADATA_HISTORY, DEFAULT_METADATA_REPLICAS, DEFAULT_METADATA_TTL, type DeadLetterInfo, EventBus, JETSTREAM_CODEC, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS, JetstreamClient, JetstreamDlqHeader, type JetstreamFeatureOptions, JetstreamHeader, JetstreamHealthIndicator, type JetstreamHealthStatus, JetstreamModule, type JetstreamModuleAsyncOptions, type JetstreamModuleOptions, JetstreamRecord, JetstreamRecordBuilder, JetstreamStrategy, JsonCodec, MIN_METADATA_TTL, MessageKind, type MetadataRegistryOptions, type OrderedEventOverrides, PatternPrefix, type RpcConfig, RpcContext, type ScheduleRecordOptions, type StreamConfigOverrides, type StreamConsumerOverrides, StreamKind, TransportEvent, type TransportHooks, buildBroadcastSubject, buildSubject, consumerName, dlqStreamName, getClientToken, internalName, isCoreRpcMode, isJetStreamRpcMode, metadataKey, streamName, toNanos };
2187
+ export { type CaptureBodyOptions, type Codec, ConsumeKind, type ConsumeSourceMsg, DEFAULT_BROADCAST_CONSUMER_CONFIG, DEFAULT_BROADCAST_STREAM_CONFIG, DEFAULT_COMMAND_CONSUMER_CONFIG, DEFAULT_COMMAND_STREAM_CONFIG, DEFAULT_DLQ_STREAM_CONFIG, DEFAULT_EVENT_CONSUMER_CONFIG, DEFAULT_EVENT_STREAM_CONFIG, DEFAULT_JETSTREAM_RPC_TIMEOUT, DEFAULT_METADATA_BUCKET, DEFAULT_METADATA_HISTORY, DEFAULT_METADATA_REPLICAS, DEFAULT_METADATA_TTL, DEFAULT_ORDERED_STREAM_CONFIG, DEFAULT_RPC_TIMEOUT, DEFAULT_SHUTDOWN_TIMEOUT, DEFAULT_TRACES, type DeadLetterInfo, type ErrorClassification, type HandlerMetadata, JETSTREAM_CODEC, JETSTREAM_CONNECTION, JETSTREAM_OPTIONS, JetstreamClient, type JetstreamConsumeContext, JetstreamDlqHeader, type JetstreamFeatureOptions, JetstreamHeader, JetstreamHealthIndicator, type JetstreamHealthStatus, JetstreamModule, type JetstreamModuleAsyncOptions, type JetstreamModuleOptions, type JetstreamPublishContext, JetstreamRecord, JetstreamRecordBuilder, type JetstreamResponseContext, JetstreamStrategy, JetstreamTrace, JsonCodec, MIN_METADATA_TTL, MessageKind, type MetadataRegistryOptions, MsgpackCodec, NatsErrorCode, type OrderedEventOverrides, type OtelOptions, PatternPrefix, PublishKind, RESERVED_HEADERS, type RpcConfig, RpcContext, type ScheduleRecordOptions, type ServerEndpoint, type StreamConfigOverrides, type StreamConsumerOverrides, StreamKind, TRACER_NAME, TransportEvent, type TransportHooks, buildBroadcastSubject, buildSubject, consumerName, dlqStreamName, getClientToken, internalName, isCoreRpcMode, isJetStreamRpcMode, metadataKey, streamName, toNanos };