@horizon-republic/nestjs-jetstream 2.8.0 → 2.9.1

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
@@ -117,6 +117,14 @@ interface JetstreamHealthStatus {
117
117
  latency: number | null;
118
118
  }
119
119
 
120
+ /**
121
+ * Stream config overrides exposed to users.
122
+ *
123
+ * `retention` is excluded because it is controlled by the transport layer
124
+ * (Workqueue for events/commands, Limits for broadcast/ordered).
125
+ * Any `retention` value provided at runtime is silently stripped.
126
+ */
127
+ type StreamConfigOverrides = Partial<Omit<StreamConfig, 'retention'>>;
120
128
  /**
121
129
  * RPC transport configuration.
122
130
  *
@@ -136,7 +144,7 @@ type RpcConfig = {
136
144
  /** Handler timeout in ms. Default: 180_000 (3 min). */
137
145
  timeout?: number;
138
146
  /** Raw NATS StreamConfig overrides for the command stream. */
139
- stream?: Partial<StreamConfig>;
147
+ stream?: StreamConfigOverrides;
140
148
  /** Raw NATS ConsumerConfig overrides for the command consumer. */
141
149
  consumer?: Partial<ConsumerConfig>;
142
150
  /** Options passed to the nats.js `consumer.consume()` call for the command consumer. */
@@ -151,7 +159,7 @@ type RpcConfig = {
151
159
  };
152
160
  /** Overrides for JetStream stream and consumer configuration. */
153
161
  interface StreamConsumerOverrides {
154
- stream?: Partial<StreamConfig>;
162
+ stream?: StreamConfigOverrides;
155
163
  consumer?: Partial<ConsumerConfig>;
156
164
  /**
157
165
  * Options passed to the nats.js `consumer.consume()` call.
@@ -195,7 +203,7 @@ interface StreamConsumerOverrides {
195
203
  */
196
204
  interface OrderedEventOverrides {
197
205
  /** Stream overrides (e.g. `max_age`, `max_bytes`). */
198
- stream?: Partial<StreamConfig>;
206
+ stream?: StreamConfigOverrides;
199
207
  /**
200
208
  * Where to start reading when the consumer is (re)created.
201
209
  * @default DeliverPolicy.All
@@ -215,6 +223,38 @@ interface OrderedEventOverrides {
215
223
  */
216
224
  replayPolicy?: ReplayPolicy;
217
225
  }
226
+ /**
227
+ * Configuration for the handler metadata KV registry.
228
+ *
229
+ * When any handler has `meta` in its extras, the transport writes metadata
230
+ * entries to a NATS KV bucket at startup. External services (API gateways,
231
+ * dashboards) can watch the bucket for service discovery.
232
+ *
233
+ * All fields are optional — sensible defaults are applied.
234
+ */
235
+ interface MetadataRegistryOptions {
236
+ /**
237
+ * KV bucket name.
238
+ * @default 'handler_registry'
239
+ */
240
+ bucket?: string;
241
+ /**
242
+ * Number of KV bucket replicas. Must be an odd number (1, 3, 5, 7, ...).
243
+ * Requires a NATS cluster with at least this many nodes.
244
+ * @default 1
245
+ */
246
+ replicas?: number;
247
+ /**
248
+ * KV bucket TTL in milliseconds.
249
+ *
250
+ * Entries expire automatically unless refreshed by a heartbeat.
251
+ * The transport refreshes entries every `ttl / 2` while the pod is alive.
252
+ * When the pod stops (graceful or crash), entries expire after this duration.
253
+ *
254
+ * @default 30_000 (30 seconds)
255
+ */
256
+ ttl?: number;
257
+ }
218
258
  /**
219
259
  * Root module configuration for `JetstreamModule.forRoot()`.
220
260
  *
@@ -283,12 +323,62 @@ interface JetstreamModuleOptions {
283
323
  * ```
284
324
  */
285
325
  onDeadLetter?(info: DeadLetterInfo): Promise<void>;
326
+ /**
327
+ * Dead-letter queue (DLQ) configuration.
328
+ * DLQ is a separate stream used to store messages that have exhausted all delivery attempts.
329
+ * @example
330
+ * ```typescript
331
+ * JetstreamModule.forRootAsync({
332
+ * name: 'my-service',
333
+ * servers: ['nats://localhost:4222'],
334
+ * dlq: {
335
+ * stream: {
336
+ * max_age: toNanos(30, 'days'),
337
+ * },
338
+ * },
339
+ * })
340
+ * ```
341
+ */
342
+ dlq?: {
343
+ stream?: StreamConfigOverrides;
344
+ };
286
345
  /**
287
346
  * Graceful shutdown timeout in ms.
288
347
  * Handlers exceeding this are abandoned.
289
348
  * @default 10_000
290
349
  */
291
350
  shutdownTimeout?: number;
351
+ /**
352
+ * Allow destructive stream migration when immutable config changes are detected.
353
+ *
354
+ * When `true`, the transport will recreate streams (via blue-green sourcing)
355
+ * if immutable properties like `storage` differ from the running stream.
356
+ * Messages are preserved during migration.
357
+ *
358
+ * `retention` is NOT migratable — it is controlled by the transport
359
+ * (Workqueue for events, Limits for broadcast/ordered) and a mismatch
360
+ * is always treated as an error regardless of this flag.
361
+ *
362
+ * When `false` (default), immutable conflicts are logged as warnings and
363
+ * the stream continues with its existing configuration.
364
+ *
365
+ * @default false
366
+ */
367
+ allowDestructiveMigration?: boolean;
368
+ /**
369
+ * Handler metadata KV registry configuration.
370
+ *
371
+ * When any handler has `meta` in its `@EventPattern` / `@MessagePattern` extras,
372
+ * the transport writes metadata to a NATS KV bucket at startup.
373
+ * External services (API gateways, dashboards, CLI tools) can read or watch
374
+ * the bucket for dynamic service discovery.
375
+ *
376
+ * Auto-enabled when any handler has `meta`. Set to customize bucket name,
377
+ * replicas, or TTL.
378
+ *
379
+ * @see MetadataRegistryOptions
380
+ */
381
+ metadata?: MetadataRegistryOptions;
292
382
  /**
293
383
  * Raw NATS ConnectionOptions pass-through for advanced connection config.
294
384
  * Allows setting tls, auth, reconnect behavior, maxReconnectAttempts, etc.
@@ -435,6 +525,13 @@ declare class EventBus {
435
525
  * Avoids rest/spread overhead of the generic `emit()`.
436
526
  */
437
527
  emitMessageRouted(subject: string, kind: MessageKind): void;
528
+ /**
529
+ * Check whether a hook is registered for the given event.
530
+ *
531
+ * Used by the routing hot path to elide the emit call entirely when the
532
+ * transport owner did not register a listener.
533
+ */
534
+ hasHook(event: keyof TransportHooks): boolean;
438
535
  private callHook;
439
536
  }
440
537
 
@@ -516,6 +613,7 @@ declare class PatternRegistry {
516
613
  private _hasCommands;
517
614
  private _hasBroadcasts;
518
615
  private _hasOrdered;
616
+ private _hasMetadata;
519
617
  constructor(options: JetstreamModuleOptions);
520
618
  /**
521
619
  * Register all handlers from the NestJS strategy.
@@ -533,11 +631,21 @@ declare class PatternRegistry {
533
631
  hasOrderedHandlers(): boolean;
534
632
  /** Get fully-qualified NATS subjects for ordered handlers. */
535
633
  getOrderedSubjects(): string[];
634
+ /** Check if any registered handler has metadata. */
635
+ hasMetadata(): boolean;
636
+ /**
637
+ * Get handler metadata entries for KV publishing.
638
+ *
639
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
640
+ * Key format: `{serviceName}.{kind}.{pattern}`.
641
+ */
642
+ getMetadataEntries(): Map<string, Record<string, unknown>>;
536
643
  /** Get patterns grouped by kind (cached after registration). */
537
644
  getPatternsByKind(): PatternsByKind;
538
645
  /** Normalize a full NATS subject back to the user-facing pattern. */
539
646
  normalizeSubject(subject: string): string;
540
647
  private buildPatternsByKind;
648
+ private resolveStreamKind;
541
649
  private logSummary;
542
650
  }
543
651
 
@@ -582,12 +690,14 @@ declare class StreamProvider {
582
690
  private readonly options;
583
691
  private readonly connection;
584
692
  private readonly logger;
693
+ private readonly migration;
585
694
  constructor(options: JetstreamModuleOptions, connection: ConnectionProvider);
586
695
  /**
587
696
  * Ensure all required streams exist with correct configuration.
588
697
  *
589
698
  * @param kinds Which stream kinds to create. Determined by the module based
590
699
  * on RPC mode and registered handler patterns.
700
+ * If the dlq option is enabled, also ensures the DLQ stream exists.
591
701
  */
592
702
  ensureStreams(kinds: StreamKind[]): Promise<void>;
593
703
  /** Get the stream name for a given kind. */
@@ -596,14 +706,32 @@ declare class StreamProvider {
596
706
  getSubjects(kind: StreamKind): string[];
597
707
  /** Ensure a single stream exists, creating or updating as needed. */
598
708
  private ensureStream;
709
+ /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
710
+ private ensureDlqStream;
711
+ private handleExistingStream;
712
+ private buildMutableOnlyConfig;
713
+ private logChanges;
599
714
  /** Build the full stream config by merging defaults with user overrides. */
600
715
  private buildConfig;
716
+ /**
717
+ * Build the stream configuration for the Dead-Letter Queue (DLQ).
718
+ *
719
+ * Merges the library default DLQ config with user-provided overrides.
720
+ * Ensures transport-controlled settings like retention are safely decoupled.
721
+ */
722
+ private buildDlqConfig;
601
723
  /** Get default config for a stream kind. */
602
724
  private getDefaults;
603
725
  /** Check if scheduling is enabled for a stream kind via `allow_msg_schedules` override. */
604
726
  private isSchedulingEnabled;
605
- /** Get user-provided overrides for a stream kind. */
727
+ /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
606
728
  private getOverrides;
729
+ /**
730
+ * Remove transport-controlled properties from user overrides.
731
+ * `retention` is managed by the transport (Workqueue/Limits per stream kind)
732
+ * and silently stripped to protect users from misconfiguration.
733
+ */
734
+ private stripTransportControlled;
607
735
  }
608
736
 
609
737
  /**
@@ -615,6 +743,11 @@ declare class StreamProvider {
615
743
  * **Ordered** — strict sequential delivery:
616
744
  * - No ack/nak/DLQ — nats.js auto-acknowledges ordered consumer messages.
617
745
  * - Handler errors are logged but do not affect delivery.
746
+ *
747
+ * **Dead-Letter Queue (DLQ) - for handling failed message deliveries**
748
+ * - If `options.dlq` is configured, messages that exhaust their max delivery attempts are published to a DLQ stream.
749
+ * - The DLQ stream name is derived from the service name (e.g., `orders__microservice_dlq-stream`).
750
+ * - Original message data and metadata are preserved in the DLQ message, with additional headers indicating the reason for failure.
618
751
  */
619
752
  declare class EventRouter {
620
753
  private readonly messageProvider;
@@ -624,9 +757,11 @@ declare class EventRouter {
624
757
  private readonly deadLetterConfig?;
625
758
  private readonly processingConfig?;
626
759
  private readonly ackWaitMap?;
760
+ private readonly connection?;
761
+ private readonly options?;
627
762
  private readonly logger;
628
763
  private readonly subscriptions;
629
- constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, codec: Codec, eventBus: EventBus, deadLetterConfig?: DeadLetterConfig | undefined, processingConfig?: EventProcessingConfig | undefined, ackWaitMap?: Map<StreamKind, number> | undefined);
764
+ 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);
630
765
  /**
631
766
  * Update the max_deliver thresholds from actual NATS consumer configs.
632
767
  * Called after consumers are ensured so the DLQ map reflects reality.
@@ -636,21 +771,35 @@ declare class EventRouter {
636
771
  start(): void;
637
772
  /** Stop routing and unsubscribe from all streams. */
638
773
  destroy(): void;
639
- /** Subscribe to a message stream and route each message. */
774
+ /** Subscribe to a message stream and route each message to its handler. */
640
775
  private subscribeToStream;
641
776
  private getConcurrency;
642
777
  private getAckExtensionConfig;
643
- /** Handle a single event message with error isolation. */
644
- private handleSafe;
645
- /** Handle an ordered message with error isolation. */
646
- private handleOrderedSafe;
647
- /** Resolve handler, decode payload, and build context. Returns null on failure. */
648
- private decodeMessage;
649
- /** Execute handler, then ack on success or nak/dead-letter on failure. */
650
- private executeHandler;
651
- /** Check if the message has exhausted all delivery attempts. */
652
- private isDeadLetter;
653
778
  /** Handle a dead letter: invoke callback, then term or nak based on result. */
779
+ /**
780
+ * Fallback execution for a dead letter when DLQ is disabled, or when
781
+ * publishing to the DLQ stream fails (due to network or NATS errors).
782
+ *
783
+ * Triggers the user-provided `onDeadLetter` hook for logging/alerting.
784
+ * On success, terminates the message. On error, leaves it unacknowledged (nak)
785
+ * so NATS can retry the delivery on the next cycle.
786
+ */
787
+ private fallbackToOnDeadLetterCallback;
788
+ /**
789
+ * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
790
+ *
791
+ * Appends diagnostic metadata headers to the original message and preserves
792
+ * the primary payload. If publishing succeeds, it notifies the standard
793
+ * `onDeadLetter` callback and terminates the message. If it fails, it falls
794
+ * back to the callback entirely to prevent silent data loss.
795
+ */
796
+ private publishToDlq;
797
+ /**
798
+ * Orchestrates the handling of a message that has exhausted delivery limits.
799
+ *
800
+ * Emits a system event and delegates either to the robust DLQ stream publisher
801
+ * or directly to the fallback callback based on the active module configuration.
802
+ */
654
803
  private handleDeadLetter;
655
804
  }
656
805
 
@@ -687,10 +836,6 @@ declare class RpcRouter {
687
836
  start(): Promise<void>;
688
837
  /** Stop routing and unsubscribe. */
689
838
  destroy(): void;
690
- /** Handle a single RPC command message with error isolation. */
691
- private handleSafe;
692
- /** Execute handler, publish response, settle message. */
693
- private executeHandler;
694
839
  }
695
840
 
696
841
  /**
@@ -714,8 +859,36 @@ declare class ConsumerProvider {
714
859
  ensureConsumers(kinds: StreamKind[]): Promise<Map<StreamKind, ConsumerInfo>>;
715
860
  /** Get the consumer name for a given kind. */
716
861
  getConsumerName(kind: StreamKind): string;
717
- /** Ensure a single consumer exists, creating if needed. */
718
- private ensureConsumer;
862
+ /**
863
+ * Ensure a single consumer exists with the desired config.
864
+ * Used at **startup** — creates or updates the consumer to match
865
+ * the current pod's configuration.
866
+ */
867
+ ensureConsumer(jsm: Awaited<ReturnType<ConnectionProvider['getJetStreamManager']>>, kind: StreamKind): Promise<ConsumerInfo>;
868
+ /**
869
+ * Recover a consumer that disappeared during runtime.
870
+ * Used by **self-healing** — creates if missing, but NEVER updates config.
871
+ *
872
+ * If a migration backup stream exists, another pod is mid-migration — we
873
+ * throw so the self-healing retry loop waits with backoff until migration
874
+ * completes and the backup is cleaned up.
875
+ *
876
+ * This prevents old pods from:
877
+ * - Overwriting a newer pod's consumer config during rolling updates
878
+ * - Creating consumers during migration (which would consume and delete
879
+ * workqueue messages while they're being restored)
880
+ */
881
+ recoverConsumer(jsm: Awaited<ReturnType<ConnectionProvider['getJetStreamManager']>>, kind: StreamKind): Promise<ConsumerInfo>;
882
+ /**
883
+ * Throw if a migration backup stream exists for this stream.
884
+ * The self-healing retry loop catches the error and retries with backoff,
885
+ * naturally waiting until the migrating pod finishes and cleans up the backup.
886
+ */
887
+ private assertNoMigrationInProgress;
888
+ /**
889
+ * Create a consumer, handling the race where another pod creates it first.
890
+ */
891
+ private createConsumer;
719
892
  /** Build consumer config by merging defaults with user overrides. */
720
893
  private buildConfig;
721
894
  /** Get default config for a consumer kind. */
@@ -724,6 +897,8 @@ declare class ConsumerProvider {
724
897
  private getOverrides;
725
898
  }
726
899
 
900
+ /** Callback to recreate a consumer when it disappears. */
901
+ type ConsumerRecoveryFn = (kind: StreamKind) => Promise<ConsumerInfo>;
727
902
  /**
728
903
  * Manages pull-based message consumption from JetStream consumers.
729
904
  *
@@ -737,6 +912,7 @@ declare class MessageProvider {
737
912
  private readonly connection;
738
913
  private readonly eventBus;
739
914
  private readonly consumeOptionsMap;
915
+ private readonly consumerRecoveryFn?;
740
916
  private readonly logger;
741
917
  private readonly activeIterators;
742
918
  private orderedReadyResolve;
@@ -746,7 +922,7 @@ declare class MessageProvider {
746
922
  private commandMessages$;
747
923
  private broadcastMessages$;
748
924
  private orderedMessages$;
749
- constructor(connection: ConnectionProvider, eventBus: EventBus, consumeOptionsMap?: Map<StreamKind, Partial<ConsumeOptions>>);
925
+ constructor(connection: ConnectionProvider, eventBus: EventBus, consumeOptionsMap?: Map<StreamKind, Partial<ConsumeOptions>>, consumerRecoveryFn?: ConsumerRecoveryFn | undefined);
750
926
  /** Observable stream of workqueue event messages. */
751
927
  get events$(): Observable<JsMsg>;
752
928
  /** Observable stream of RPC command messages (jetstream mode). */
@@ -779,6 +955,14 @@ declare class MessageProvider {
779
955
  private createFlow;
780
956
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
781
957
  private consumeOnce;
958
+ /**
959
+ * Detect "consumer not found" errors from `js.consumers.get()`.
960
+ *
961
+ * Unlike JetStream Manager calls (which throw `JetStreamApiError`),
962
+ * the JetStream client's `consumers.get()` throws a plain `Error`
963
+ * with the error code embedded in the message text.
964
+ */
965
+ private isConsumerNotFound;
782
966
  /** Get the target subject for a consumer kind. */
783
967
  private getTargetSubject;
784
968
  /** Monitor heartbeats and restart the consumer iterator on prolonged silence. */
@@ -787,10 +971,76 @@ declare class MessageProvider {
787
971
  private createOrderedFlow;
788
972
  /** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
789
973
  private createSelfHealingFlow;
790
- /** Single iteration: create ordered consumer -> iterate messages. */
974
+ /** Single iteration: create ordered consumer -> push messages into the subject. */
791
975
  private consumeOrderedOnce;
792
976
  }
793
977
 
978
+ /**
979
+ * Publishes handler metadata to a NATS KV bucket for external service discovery.
980
+ *
981
+ * Uses TTL + heartbeat to manage entry lifecycle:
982
+ * - Entries are written on startup and refreshed every `ttl / 2`
983
+ * - When the pod stops (graceful or crash), heartbeat stops → entries expire via TTL
984
+ * - No explicit delete needed — NATS handles expiry automatically
985
+ *
986
+ * This provider is fully decoupled from stream/consumer infrastructure —
987
+ * it only depends on the NATS connection and module options.
988
+ */
989
+ declare class MetadataProvider {
990
+ private readonly connection;
991
+ private readonly logger;
992
+ private readonly bucketName;
993
+ private readonly replicas;
994
+ private readonly ttl;
995
+ private currentEntries?;
996
+ private heartbeatTimer?;
997
+ private cachedKv?;
998
+ constructor(options: JetstreamModuleOptions, connection: ConnectionProvider);
999
+ /**
1000
+ * Write handler metadata entries to the KV bucket and start heartbeat.
1001
+ *
1002
+ * Creates the bucket if it doesn't exist (idempotent).
1003
+ * Skips silently when entries map is empty.
1004
+ * Starts a heartbeat interval that refreshes entries every `ttl / 2`
1005
+ * to prevent TTL expiry while the pod is alive.
1006
+ *
1007
+ * Non-critical — errors are logged but do not prevent transport startup.
1008
+ *
1009
+ * @param entries Map of KV key → metadata object.
1010
+ */
1011
+ publish(entries: Map<string, Record<string, unknown>>): Promise<void>;
1012
+ /**
1013
+ * Stop the heartbeat timer.
1014
+ *
1015
+ * After this call, entries will expire via TTL once the heartbeat window passes.
1016
+ * Called during transport shutdown (strategy.close()).
1017
+ */
1018
+ destroy(): void;
1019
+ /** Write entries to KV with per-entry error handling. */
1020
+ private writeEntries;
1021
+ /** Start heartbeat interval that refreshes entries every ttl/2. */
1022
+ private startHeartbeat;
1023
+ /** Refresh all current entries in KV (heartbeat tick). */
1024
+ private refreshEntries;
1025
+ /** Create or open the KV bucket (cached after first call). */
1026
+ private openBucket;
1027
+ }
1028
+
1029
+ /**
1030
+ * NATS JetStream API error codes used by the transport.
1031
+ *
1032
+ * Ref: https://github.com/nats-io/nats-server (server error definitions)
1033
+ * Verified on NATS 2.12.6 via integration tests (2026-04-02).
1034
+ */
1035
+ declare enum NatsErrorCode {
1036
+ /** Consumer does not exist on the specified stream. */
1037
+ ConsumerNotFound = 10014,
1038
+ /** Consumer name already in use with different configuration (race condition on create). */
1039
+ ConsumerAlreadyExists = 10148,
1040
+ /** Stream does not exist. */
1041
+ StreamNotFound = 10059
1042
+ }
1043
+
794
1044
  /**
795
1045
  * NestJS custom transport strategy for NATS JetStream.
796
1046
  *
@@ -813,17 +1063,18 @@ declare class JetstreamStrategy extends Server implements CustomTransportStrateg
813
1063
  private readonly rpcRouter;
814
1064
  private readonly coreRpcServer;
815
1065
  private readonly ackWaitMap;
1066
+ private readonly metadataProvider?;
816
1067
  readonly transportId: symbol;
817
1068
  private readonly listeners;
818
1069
  private started;
819
- constructor(options: JetstreamModuleOptions, connection: ConnectionProvider, patternRegistry: PatternRegistry, streamProvider: StreamProvider, consumerProvider: ConsumerProvider, messageProvider: MessageProvider, eventRouter: EventRouter, rpcRouter: RpcRouter, coreRpcServer: CoreRpcServer, ackWaitMap?: Map<StreamKind, number>);
1070
+ constructor(options: JetstreamModuleOptions, connection: ConnectionProvider, patternRegistry: PatternRegistry, streamProvider: StreamProvider, consumerProvider: ConsumerProvider, messageProvider: MessageProvider, eventRouter: EventRouter, rpcRouter: RpcRouter, coreRpcServer: CoreRpcServer, ackWaitMap?: Map<StreamKind, number>, metadataProvider?: MetadataProvider | undefined);
820
1071
  /**
821
1072
  * Start the transport: register handlers, create infrastructure, begin consumption.
822
1073
  *
823
1074
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
824
1075
  */
825
1076
  listen(callback: () => void): Promise<void>;
826
- /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
1077
+ /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
827
1078
  close(): void;
828
1079
  /**
829
1080
  * Register event listener (required by Server base class).
@@ -858,22 +1109,30 @@ interface Stoppable {
858
1109
  * Shutdown sequence:
859
1110
  * 1. Emit onShutdownStart hook
860
1111
  * 2. Stop accepting new messages (close subscriptions, stop consumers)
861
- * 3. Wait for in-flight handlers to complete (up to timeout)
862
- * 4. Drain and close NATS connection
863
- * 5. Emit onShutdownComplete hook
1112
+ * 3. Drain and close NATS connection (with timeout safety net)
1113
+ * 4. Emit onShutdownComplete hook
1114
+ *
1115
+ * Idempotent — concurrent or repeated calls return the same promise.
1116
+ * This is critical because NestJS may call `onApplicationShutdown` on
1117
+ * multiple module instances (forRoot + forFeature) that share this
1118
+ * singleton, and the call order is not guaranteed.
864
1119
  */
865
1120
  declare class ShutdownManager {
866
1121
  private readonly connection;
867
1122
  private readonly eventBus;
868
1123
  private readonly timeout;
869
1124
  private readonly logger;
1125
+ private shutdownPromise?;
870
1126
  constructor(connection: ConnectionProvider, eventBus: EventBus, timeout: number);
871
1127
  /**
872
1128
  * Execute the full shutdown sequence.
873
1129
  *
1130
+ * Idempotent — concurrent or repeated calls return the same promise.
1131
+ *
874
1132
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
875
1133
  */
876
1134
  shutdown(strategy?: Stoppable): Promise<void>;
1135
+ private doShutdown;
877
1136
  }
878
1137
 
879
1138
  /**
@@ -976,6 +1235,21 @@ declare class JetstreamClient extends ClientProxy {
976
1235
  private readonly targetName;
977
1236
  /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
978
1237
  private readonly callerName;
1238
+ /**
1239
+ * Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
1240
+ * per stream kind this client may publish to. Built once in the constructor
1241
+ * so producing a full subject is a single string concat with the user pattern.
1242
+ */
1243
+ private readonly eventSubjectPrefix;
1244
+ private readonly commandSubjectPrefix;
1245
+ private readonly orderedSubjectPrefix;
1246
+ /**
1247
+ * RPC configuration snapshots. The values are derived from rootOptions at
1248
+ * construction time so the publish hot path never has to re-run
1249
+ * isCoreRpcMode / getRpcTimeout on every call.
1250
+ */
1251
+ private readonly isCoreMode;
1252
+ private readonly defaultRpcTimeout;
979
1253
  /** Shared inbox for JetStream-mode RPC responses. */
980
1254
  private inbox;
981
1255
  private inboxSubscription;
@@ -985,6 +1259,12 @@ declare class JetstreamClient extends ClientProxy {
985
1259
  private readonly pendingTimeouts;
986
1260
  /** Subscription to connection status events for disconnect handling. */
987
1261
  private statusSubscription;
1262
+ /**
1263
+ * Cached readiness flag. Once `connect()` has wired the inbox and status
1264
+ * subscription, subsequent publishes skip the `await connect()` microtask
1265
+ * and reach for the underlying connection synchronously instead.
1266
+ */
1267
+ private readyForPublish;
988
1268
  constructor(rootOptions: JetstreamModuleOptions, targetServiceName: string, connection: ConnectionProvider, codec: Codec, eventBus: EventBus);
989
1269
  /**
990
1270
  * Establish connection. Called automatically by NestJS on first use.
@@ -1031,7 +1311,14 @@ declare class JetstreamClient extends ClientProxy {
1031
1311
  private setupInbox;
1032
1312
  /** Route an inbox reply to the matching pending callback. */
1033
1313
  private routeInboxReply;
1034
- /** Build event subject — workqueue, broadcast, or ordered. */
1314
+ /**
1315
+ * Resolve a user pattern to a fully-qualified NATS subject, dispatching
1316
+ * between the event, broadcast, and ordered prefixes.
1317
+ *
1318
+ * The leading-char check short-circuits the `startsWith` comparisons for
1319
+ * patterns that cannot possibly carry a broadcast/ordered marker, which is
1320
+ * the overwhelmingly common case.
1321
+ */
1035
1322
  private buildEventSubject;
1036
1323
  /** Build NATS headers merging custom headers with transport headers. */
1037
1324
  private buildHeaders;
@@ -1049,7 +1336,6 @@ declare class JetstreamClient extends ClientProxy {
1049
1336
  * - `broadcast.config.updated` → `broadcast._sch.config.updated`
1050
1337
  */
1051
1338
  private buildScheduleSubject;
1052
- private getRpcTimeout;
1053
1339
  }
1054
1340
 
1055
1341
  /**
@@ -1079,6 +1365,8 @@ declare class JetstreamRecord<TData = unknown> {
1079
1365
  readonly messageId?: string | undefined;
1080
1366
  /** Schedule options for delayed delivery. */
1081
1367
  readonly schedule?: ScheduleRecordOptions | undefined;
1368
+ /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
1369
+ readonly ttl?: string | undefined;
1082
1370
  constructor(
1083
1371
  /** Message payload. */
1084
1372
  data: TData,
@@ -1089,7 +1377,9 @@ declare class JetstreamRecord<TData = unknown> {
1089
1377
  /** Custom message ID for JetStream deduplication. */
1090
1378
  messageId?: string | undefined,
1091
1379
  /** Schedule options for delayed delivery. */
1092
- schedule?: ScheduleRecordOptions | undefined);
1380
+ schedule?: ScheduleRecordOptions | undefined,
1381
+ /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
1382
+ ttl?: string | undefined);
1093
1383
  }
1094
1384
  /**
1095
1385
  * Fluent builder for constructing JetstreamRecord instances.
@@ -1103,6 +1393,7 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1103
1393
  private timeout;
1104
1394
  private messageId;
1105
1395
  private scheduleOptions;
1396
+ private ttlDuration;
1106
1397
  constructor(data?: TData);
1107
1398
  /**
1108
1399
  * Set the message payload.
@@ -1164,6 +1455,27 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1164
1455
  * @throws Error if the date is not in the future.
1165
1456
  */
1166
1457
  scheduleAt(date: Date): this;
1458
+ /**
1459
+ * Set per-message TTL (time-to-live).
1460
+ *
1461
+ * The message expires individually after the specified duration,
1462
+ * independent of the stream's `max_age`. Requires NATS >= 2.11 and
1463
+ * `allow_msg_ttl: true` on the stream.
1464
+ *
1465
+ * Only meaningful for events (`client.emit()`). If used with RPC
1466
+ * (`client.send()`), a warning is logged and the TTL is ignored.
1467
+ *
1468
+ * @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
1469
+ *
1470
+ * @example
1471
+ * ```typescript
1472
+ * import { toNanos } from '@horizon-republic/nestjs-jetstream';
1473
+ *
1474
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
1475
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
1476
+ * ```
1477
+ */
1478
+ ttl(nanos: number): this;
1167
1479
  /**
1168
1480
  * Build the immutable {@link JetstreamRecord}.
1169
1481
  *
@@ -1192,6 +1504,52 @@ declare class JsonCodec implements Codec {
1192
1504
  decode(data: Uint8Array): unknown;
1193
1505
  }
1194
1506
 
1507
+ /**
1508
+ * Minimal shape of a `msgpackr` `Packr` instance used by {@link MsgpackCodec}.
1509
+ *
1510
+ * Typed locally so consumers who do not use MessagePack encoding never pay
1511
+ * for a `msgpackr` import.
1512
+ */
1513
+ interface PackrLike {
1514
+ pack(data: unknown): Uint8Array;
1515
+ unpack(data: Uint8Array): unknown;
1516
+ }
1517
+ /**
1518
+ * MessagePack codec backed by a caller-provided `msgpackr` `Packr` instance.
1519
+ *
1520
+ * Use this codec when publishing structured payloads larger than roughly
1521
+ * 1–2 KB — below that size the default {@link JsonCodec} wins on per-call
1522
+ * constant overhead. Above it, MessagePack encodes and decodes several times
1523
+ * faster and produces smaller wire frames. The format is cross-language, so
1524
+ * Node producers and non-Node consumers (Python, Go, Java, Rust, ...) stay
1525
+ * interoperable.
1526
+ *
1527
+ * Requires installing the optional `msgpackr` peer dependency:
1528
+ *
1529
+ * ```bash
1530
+ * npm install msgpackr
1531
+ * # or: pnpm add msgpackr
1532
+ * ```
1533
+ *
1534
+ * @example
1535
+ * ```typescript
1536
+ * import { JetstreamModule, MsgpackCodec } from '@horizon-republic/nestjs-jetstream';
1537
+ * import { Packr } from 'msgpackr';
1538
+ *
1539
+ * JetstreamModule.forRoot({
1540
+ * name: 'orders',
1541
+ * servers: ['nats://localhost:4222'],
1542
+ * codec: new MsgpackCodec(new Packr()),
1543
+ * });
1544
+ * ```
1545
+ */
1546
+ declare class MsgpackCodec implements Codec {
1547
+ private readonly packr;
1548
+ constructor(packr: PackrLike);
1549
+ encode(data: unknown): Uint8Array;
1550
+ decode(data: Uint8Array): unknown;
1551
+ }
1552
+
1195
1553
  type NatsMessage = JsMsg | Msg;
1196
1554
  /**
1197
1555
  * Execution context for RPC and event handlers.
@@ -1212,7 +1570,7 @@ type NatsMessage = JsMsg | Msg;
1212
1570
  * return;
1213
1571
  * }
1214
1572
  * if (!this.isReady()) {
1215
- * ctx.retry({ delay: 5000 });
1573
+ * ctx.retry({ delayMs: 5000 });
1216
1574
  * return;
1217
1575
  * }
1218
1576
  * await this.process(data);
@@ -1330,9 +1688,14 @@ declare class JetstreamHealthIndicator {
1330
1688
  * Returns `{ [key]: { status: 'up', ... } }` on success.
1331
1689
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
1332
1690
  *
1691
+ * The thrown error sets `isHealthCheckError: true` and `causes` — the
1692
+ * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
1693
+ * health failures from unexpected exceptions. Works with both Terminus v10
1694
+ * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
1695
+ *
1333
1696
  * @param key - Health indicator key (default: `'jetstream'`).
1334
1697
  * @returns Object with status, server, and latency under the given key.
1335
- * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
1698
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
1336
1699
  */
1337
1700
  isHealthy(key?: string): Promise<Record<string, Record<string, unknown>>>;
1338
1701
  }
@@ -1343,8 +1706,6 @@ declare const JETSTREAM_OPTIONS: unique symbol;
1343
1706
  declare const JETSTREAM_CONNECTION: unique symbol;
1344
1707
  /** Token for the global Codec instance. */
1345
1708
  declare const JETSTREAM_CODEC: unique symbol;
1346
- /** Token for the EventBus instance. */
1347
- declare const JETSTREAM_EVENT_BUS: unique symbol;
1348
1709
  /**
1349
1710
  * Generate the injection token for a `forFeature()` client.
1350
1711
  *
@@ -1377,6 +1738,47 @@ type TimeUnit = 'ms' | 'seconds' | 'minutes' | 'hours' | 'days';
1377
1738
  * ```
1378
1739
  */
1379
1740
  declare const toNanos: (value: number, unit: TimeUnit) => number;
1741
+ /** Default config for workqueue event streams. */
1742
+ declare const DEFAULT_EVENT_STREAM_CONFIG: Partial<StreamConfig>;
1743
+ /** Default config for RPC command streams (jetstream mode only). */
1744
+ declare const DEFAULT_COMMAND_STREAM_CONFIG: Partial<StreamConfig>;
1745
+ /** Default config for broadcast event streams. */
1746
+ declare const DEFAULT_BROADCAST_STREAM_CONFIG: Partial<StreamConfig>;
1747
+ /** Default config for ordered event streams (Limits retention). */
1748
+ declare const DEFAULT_ORDERED_STREAM_CONFIG: Partial<StreamConfig>;
1749
+ /** Default config for dead-letter queue (DLQ) streams. */
1750
+ declare const DEFAULT_DLQ_STREAM_CONFIG: Partial<StreamConfig>;
1751
+ /** Default config for workqueue event consumers. */
1752
+ declare const DEFAULT_EVENT_CONSUMER_CONFIG: Partial<ConsumerConfig>;
1753
+ /** Default config for RPC command consumers (jetstream mode only). */
1754
+ declare const DEFAULT_COMMAND_CONSUMER_CONFIG: Partial<ConsumerConfig>;
1755
+ /** Default config for broadcast event consumers. */
1756
+ declare const DEFAULT_BROADCAST_CONSUMER_CONFIG: Partial<ConsumerConfig>;
1757
+ /** Default RPC timeout for Core mode (30 seconds). */
1758
+ declare const DEFAULT_RPC_TIMEOUT = 30000;
1759
+ /** Default RPC timeout for JetStream mode (3 minutes). */
1760
+ declare const DEFAULT_JETSTREAM_RPC_TIMEOUT = 180000;
1761
+ /** Default graceful shutdown timeout (10 seconds). */
1762
+ declare const DEFAULT_SHUTDOWN_TIMEOUT = 10000;
1763
+ /** Default KV bucket name for handler metadata. */
1764
+ declare const DEFAULT_METADATA_BUCKET = "handler_registry";
1765
+ /** Default number of KV bucket replicas. */
1766
+ declare const DEFAULT_METADATA_REPLICAS = 1;
1767
+ /** Default KV bucket history depth (latest value only). */
1768
+ declare const DEFAULT_METADATA_HISTORY = 1;
1769
+ /** Default KV bucket TTL in milliseconds (entries expire unless refreshed). */
1770
+ declare const DEFAULT_METADATA_TTL = 30000;
1771
+ /** Minimum allowed metadata TTL in milliseconds. Prevents tight heartbeat loops. */
1772
+ declare const MIN_METADATA_TTL = 5000;
1773
+ /**
1774
+ * Build a KV key for a handler's metadata entry.
1775
+ *
1776
+ * @param serviceName - Service name from `forRoot({ name })`.
1777
+ * @param kind - Handler's stream kind ({@link StreamKind}).
1778
+ * @param pattern - The message pattern (e.g. `'order.created'`).
1779
+ * @returns KV key (e.g. `orders.ev.order.created`).
1780
+ */
1781
+ declare const metadataKey: (serviceName: string, kind: StreamKind, pattern: string) => string;
1380
1782
  /**
1381
1783
  * NATS headers managed by the transport.
1382
1784
  *
@@ -1396,6 +1798,20 @@ declare enum JetstreamHeader {
1396
1798
  /** Set to `'true'` on error responses so the client can distinguish success from failure. */
1397
1799
  Error = "x-error"
1398
1800
  }
1801
+ declare enum JetstreamDlqHeader {
1802
+ /** Reason for the message being sent to the DLQ — the last handler error message. */
1803
+ DeadLetterReason = "x-dead-letter-reason",
1804
+ /** Original NATS subject the message was originally published to */
1805
+ OriginalSubject = "x-original-subject",
1806
+ /** Source stream name */
1807
+ OriginalStream = "x-original-stream",
1808
+ /** ISO timestamp of when the message was moved to DLQ */
1809
+ FailedAt = "x-failed-at",
1810
+ /** Number of times the message has been delivered */
1811
+ DeliveryCount = "x-delivery-count"
1812
+ }
1813
+ /** Set of header names that are reserved and cannot be set by users. */
1814
+ declare const RESERVED_HEADERS: Set<string>;
1399
1815
  /**
1400
1816
  * Build the internal service name with microservice suffix.
1401
1817
  *
@@ -1412,6 +1828,13 @@ declare const internalName: (name: string) => string;
1412
1828
  * @returns `{serviceName}__microservice.{kind}.{pattern}`
1413
1829
  */
1414
1830
  declare const buildSubject: (serviceName: string, kind: SubjectKind, pattern: string) => string;
1831
+ /**
1832
+ * Build a broadcast subject.
1833
+ *
1834
+ * @param pattern - The message pattern (e.g. `'config.updated'`).
1835
+ * @returns `broadcast.{pattern}`
1836
+ */
1837
+ declare const buildBroadcastSubject: (pattern: string) => string;
1415
1838
  /**
1416
1839
  * Build the JetStream stream name for a given service and kind.
1417
1840
  *
@@ -1420,6 +1843,13 @@ declare const buildSubject: (serviceName: string, kind: SubjectKind, pattern: st
1420
1843
  * @returns Stream name (e.g. `orders__microservice_ev-stream` or `broadcast-stream`).
1421
1844
  */
1422
1845
  declare const streamName: (serviceName: string, kind: StreamKind) => string;
1846
+ /**
1847
+ * Build the JetStream dead-letter queue stream name for a given service.
1848
+ *
1849
+ * @param serviceName - Service name from `forRoot({ name })`.
1850
+ * @returns DLQ Stream name (e.g. `orders__microservice_dlq-stream`).
1851
+ */
1852
+ declare const dlqStreamName: (serviceName: string) => string;
1423
1853
  /**
1424
1854
  * Build the JetStream consumer name for a given service and kind.
1425
1855
  *
@@ -1443,4 +1873,4 @@ declare const isJetStreamRpcMode: (rpc: RpcConfig | undefined) => boolean;
1443
1873
  /** Check if the RPC config specifies Core mode (default). */
1444
1874
  declare const isCoreRpcMode: (rpc: RpcConfig | undefined) => boolean;
1445
1875
 
1446
- export { type Codec, type DeadLetterInfo, EventBus, JETSTREAM_CODEC, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS, JetstreamClient, type JetstreamFeatureOptions, JetstreamHeader, JetstreamHealthIndicator, type JetstreamHealthStatus, JetstreamModule, type JetstreamModuleAsyncOptions, type JetstreamModuleOptions, JetstreamRecord, JetstreamRecordBuilder, JetstreamStrategy, JsonCodec, MessageKind, type OrderedEventOverrides, PatternPrefix, type RpcConfig, RpcContext, type ScheduleRecordOptions, type StreamConsumerOverrides, StreamKind, TransportEvent, type TransportHooks, buildSubject, consumerName, getClientToken, internalName, isCoreRpcMode, isJetStreamRpcMode, streamName, toNanos };
1876
+ export { type Codec, 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, type DeadLetterInfo, JETSTREAM_CODEC, JETSTREAM_CONNECTION, 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, MsgpackCodec, NatsErrorCode, type OrderedEventOverrides, PatternPrefix, RESERVED_HEADERS, type RpcConfig, RpcContext, type ScheduleRecordOptions, type StreamConfigOverrides, type StreamConsumerOverrides, StreamKind, TransportEvent, type TransportHooks, buildBroadcastSubject, buildSubject, consumerName, dlqStreamName, getClientToken, internalName, isCoreRpcMode, isJetStreamRpcMode, metadataKey, streamName, toNanos };