@horizon-republic/nestjs-jetstream 2.7.1 → 2.9.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,5 +1,6 @@
1
1
  import { ModuleMetadata, FactoryProvider, Type, Logger, OnApplicationShutdown, DynamicModule } from '@nestjs/common';
2
- import { MsgHdrs, StreamConfig, ConsumerConfig, ConsumeOptions, DeliverPolicy, ReplayPolicy, ConnectionOptions, NatsConnection, Status, JetStreamManager, JetStreamClient, ConsumerInfo, JsMsg, Msg } from 'nats';
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';
3
4
  import { MessageHandler, Server, CustomTransportStrategy, ClientProxy, ReadPacket, WritePacket, BaseRpcContext } from '@nestjs/microservices';
4
5
  import { Observable } from 'rxjs';
5
6
 
@@ -116,6 +117,14 @@ interface JetstreamHealthStatus {
116
117
  latency: number | null;
117
118
  }
118
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'>>;
119
128
  /**
120
129
  * RPC transport configuration.
121
130
  *
@@ -135,7 +144,7 @@ type RpcConfig = {
135
144
  /** Handler timeout in ms. Default: 180_000 (3 min). */
136
145
  timeout?: number;
137
146
  /** Raw NATS StreamConfig overrides for the command stream. */
138
- stream?: Partial<StreamConfig>;
147
+ stream?: StreamConfigOverrides;
139
148
  /** Raw NATS ConsumerConfig overrides for the command consumer. */
140
149
  consumer?: Partial<ConsumerConfig>;
141
150
  /** Options passed to the nats.js `consumer.consume()` call for the command consumer. */
@@ -150,7 +159,7 @@ type RpcConfig = {
150
159
  };
151
160
  /** Overrides for JetStream stream and consumer configuration. */
152
161
  interface StreamConsumerOverrides {
153
- stream?: Partial<StreamConfig>;
162
+ stream?: StreamConfigOverrides;
154
163
  consumer?: Partial<ConsumerConfig>;
155
164
  /**
156
165
  * Options passed to the nats.js `consumer.consume()` call.
@@ -194,7 +203,7 @@ interface StreamConsumerOverrides {
194
203
  */
195
204
  interface OrderedEventOverrides {
196
205
  /** Stream overrides (e.g. `max_age`, `max_bytes`). */
197
- stream?: Partial<StreamConfig>;
206
+ stream?: StreamConfigOverrides;
198
207
  /**
199
208
  * Where to start reading when the consumer is (re)created.
200
209
  * @default DeliverPolicy.All
@@ -214,6 +223,38 @@ interface OrderedEventOverrides {
214
223
  */
215
224
  replayPolicy?: ReplayPolicy;
216
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
+ }
217
258
  /**
218
259
  * Root module configuration for `JetstreamModule.forRoot()`.
219
260
  *
@@ -282,12 +323,62 @@ interface JetstreamModuleOptions {
282
323
  * ```
283
324
  */
284
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
+ };
285
345
  /**
286
346
  * Graceful shutdown timeout in ms.
287
347
  * Handlers exceeding this are abandoned.
288
348
  * @default 10_000
289
349
  */
290
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;
291
382
  /**
292
383
  * Raw NATS ConnectionOptions pass-through for advanced connection config.
293
384
  * Allows setting tls, auth, reconnect behavior, maxReconnectAttempts, etc.
@@ -354,6 +445,12 @@ declare enum StreamKind {
354
445
  */
355
446
  type SubjectKind = Exclude<StreamKind, StreamKind.Broadcast>;
356
447
 
448
+ /** Options for one-shot delayed delivery via NATS 2.12 message scheduling. */
449
+ interface ScheduleRecordOptions {
450
+ /** When to deliver the message. Must be in the future. */
451
+ at: Date;
452
+ }
453
+
357
454
  /** @internal Grouped pattern lists by stream kind, used for stream/consumer setup. */
358
455
  interface PatternsByKind {
359
456
  /** Workqueue event patterns. */
@@ -485,6 +582,7 @@ declare class ConnectionProvider {
485
582
  * Sequence: drain → wait for close. Falls back to force-close on error.
486
583
  */
487
584
  shutdown(): Promise<void>;
585
+ private initJetStreamManager;
488
586
  /** Internal: establish the physical connection with reconnect monitoring. */
489
587
  private establish;
490
588
  /** Subscribe to connection status events and emit hooks. */
@@ -508,6 +606,7 @@ declare class PatternRegistry {
508
606
  private _hasCommands;
509
607
  private _hasBroadcasts;
510
608
  private _hasOrdered;
609
+ private _hasMetadata;
511
610
  constructor(options: JetstreamModuleOptions);
512
611
  /**
513
612
  * Register all handlers from the NestJS strategy.
@@ -525,11 +624,21 @@ declare class PatternRegistry {
525
624
  hasOrderedHandlers(): boolean;
526
625
  /** Get fully-qualified NATS subjects for ordered handlers. */
527
626
  getOrderedSubjects(): string[];
627
+ /** Check if any registered handler has metadata. */
628
+ hasMetadata(): boolean;
629
+ /**
630
+ * Get handler metadata entries for KV publishing.
631
+ *
632
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
633
+ * Key format: `{serviceName}.{kind}.{pattern}`.
634
+ */
635
+ getMetadataEntries(): Map<string, Record<string, unknown>>;
528
636
  /** Get patterns grouped by kind (cached after registration). */
529
637
  getPatternsByKind(): PatternsByKind;
530
638
  /** Normalize a full NATS subject back to the user-facing pattern. */
531
639
  normalizeSubject(subject: string): string;
532
640
  private buildPatternsByKind;
641
+ private resolveStreamKind;
533
642
  private logSummary;
534
643
  }
535
644
 
@@ -574,12 +683,14 @@ declare class StreamProvider {
574
683
  private readonly options;
575
684
  private readonly connection;
576
685
  private readonly logger;
686
+ private readonly migration;
577
687
  constructor(options: JetstreamModuleOptions, connection: ConnectionProvider);
578
688
  /**
579
689
  * Ensure all required streams exist with correct configuration.
580
690
  *
581
691
  * @param kinds Which stream kinds to create. Determined by the module based
582
692
  * on RPC mode and registered handler patterns.
693
+ * If the dlq option is enabled, also ensures the DLQ stream exists.
583
694
  */
584
695
  ensureStreams(kinds: StreamKind[]): Promise<void>;
585
696
  /** Get the stream name for a given kind. */
@@ -588,12 +699,32 @@ declare class StreamProvider {
588
699
  getSubjects(kind: StreamKind): string[];
589
700
  /** Ensure a single stream exists, creating or updating as needed. */
590
701
  private ensureStream;
702
+ /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
703
+ private ensureDlqStream;
704
+ private handleExistingStream;
705
+ private buildMutableOnlyConfig;
706
+ private logChanges;
591
707
  /** Build the full stream config by merging defaults with user overrides. */
592
708
  private buildConfig;
709
+ /**
710
+ * Build the stream configuration for the Dead-Letter Queue (DLQ).
711
+ *
712
+ * Merges the library default DLQ config with user-provided overrides.
713
+ * Ensures transport-controlled settings like retention are safely decoupled.
714
+ */
715
+ private buildDlqConfig;
593
716
  /** Get default config for a stream kind. */
594
717
  private getDefaults;
595
- /** Get user-provided overrides for a stream kind. */
718
+ /** Check if scheduling is enabled for a stream kind via `allow_msg_schedules` override. */
719
+ private isSchedulingEnabled;
720
+ /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
596
721
  private getOverrides;
722
+ /**
723
+ * Remove transport-controlled properties from user overrides.
724
+ * `retention` is managed by the transport (Workqueue/Limits per stream kind)
725
+ * and silently stripped to protect users from misconfiguration.
726
+ */
727
+ private stripTransportControlled;
597
728
  }
598
729
 
599
730
  /**
@@ -605,6 +736,11 @@ declare class StreamProvider {
605
736
  * **Ordered** — strict sequential delivery:
606
737
  * - No ack/nak/DLQ — nats.js auto-acknowledges ordered consumer messages.
607
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.
608
744
  */
609
745
  declare class EventRouter {
610
746
  private readonly messageProvider;
@@ -614,9 +750,11 @@ declare class EventRouter {
614
750
  private readonly deadLetterConfig?;
615
751
  private readonly processingConfig?;
616
752
  private readonly ackWaitMap?;
753
+ private readonly connection?;
754
+ private readonly options?;
617
755
  private readonly logger;
618
756
  private readonly subscriptions;
619
- constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, codec: Codec, eventBus: EventBus, deadLetterConfig?: DeadLetterConfig | undefined, processingConfig?: EventProcessingConfig | undefined, ackWaitMap?: Map<StreamKind, number> | undefined);
757
+ 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);
620
758
  /**
621
759
  * Update the max_deliver thresholds from actual NATS consumer configs.
622
760
  * Called after consumers are ensured so the DLQ map reflects reality.
@@ -641,6 +779,30 @@ declare class EventRouter {
641
779
  /** Check if the message has exhausted all delivery attempts. */
642
780
  private isDeadLetter;
643
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.
789
+ */
790
+ private fallbackToOnDeadLetterCallback;
791
+ /**
792
+ * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
793
+ *
794
+ * Appends diagnostic metadata headers to the original message and preserves
795
+ * the primary payload. If publishing succeeds, it notifies the standard
796
+ * `onDeadLetter` callback and terminates the message. If it fails, it falls
797
+ * back to the callback entirely to prevent silent data loss.
798
+ */
799
+ private publishToDlq;
800
+ /**
801
+ * Orchestrates the handling of a message that has exhausted delivery limits.
802
+ *
803
+ * Emits a system event and delegates either to the robust DLQ stream publisher
804
+ * or directly to the fallback callback based on the active module configuration.
805
+ */
644
806
  private handleDeadLetter;
645
807
  }
646
808
 
@@ -704,8 +866,36 @@ declare class ConsumerProvider {
704
866
  ensureConsumers(kinds: StreamKind[]): Promise<Map<StreamKind, ConsumerInfo>>;
705
867
  /** Get the consumer name for a given kind. */
706
868
  getConsumerName(kind: StreamKind): string;
707
- /** Ensure a single consumer exists, creating if needed. */
708
- private ensureConsumer;
869
+ /**
870
+ * Ensure a single consumer exists with the desired config.
871
+ * Used at **startup** — creates or updates the consumer to match
872
+ * the current pod's configuration.
873
+ */
874
+ ensureConsumer(jsm: Awaited<ReturnType<ConnectionProvider['getJetStreamManager']>>, kind: StreamKind): Promise<ConsumerInfo>;
875
+ /**
876
+ * Recover a consumer that disappeared during runtime.
877
+ * Used by **self-healing** — creates if missing, but NEVER updates config.
878
+ *
879
+ * If a migration backup stream exists, another pod is mid-migration — we
880
+ * throw so the self-healing retry loop waits with backoff until migration
881
+ * completes and the backup is cleaned up.
882
+ *
883
+ * This prevents old pods from:
884
+ * - Overwriting a newer pod's consumer config during rolling updates
885
+ * - Creating consumers during migration (which would consume and delete
886
+ * workqueue messages while they're being restored)
887
+ */
888
+ recoverConsumer(jsm: Awaited<ReturnType<ConnectionProvider['getJetStreamManager']>>, kind: StreamKind): Promise<ConsumerInfo>;
889
+ /**
890
+ * Throw if a migration backup stream exists for this stream.
891
+ * The self-healing retry loop catches the error and retries with backoff,
892
+ * naturally waiting until the migrating pod finishes and cleans up the backup.
893
+ */
894
+ private assertNoMigrationInProgress;
895
+ /**
896
+ * Create a consumer, handling the race where another pod creates it first.
897
+ */
898
+ private createConsumer;
709
899
  /** Build consumer config by merging defaults with user overrides. */
710
900
  private buildConfig;
711
901
  /** Get default config for a consumer kind. */
@@ -714,6 +904,8 @@ declare class ConsumerProvider {
714
904
  private getOverrides;
715
905
  }
716
906
 
907
+ /** Callback to recreate a consumer when it disappears. */
908
+ type ConsumerRecoveryFn = (kind: StreamKind) => Promise<ConsumerInfo>;
717
909
  /**
718
910
  * Manages pull-based message consumption from JetStream consumers.
719
911
  *
@@ -727,6 +919,7 @@ declare class MessageProvider {
727
919
  private readonly connection;
728
920
  private readonly eventBus;
729
921
  private readonly consumeOptionsMap;
922
+ private readonly consumerRecoveryFn?;
730
923
  private readonly logger;
731
924
  private readonly activeIterators;
732
925
  private orderedReadyResolve;
@@ -736,7 +929,7 @@ declare class MessageProvider {
736
929
  private commandMessages$;
737
930
  private broadcastMessages$;
738
931
  private orderedMessages$;
739
- constructor(connection: ConnectionProvider, eventBus: EventBus, consumeOptionsMap?: Map<StreamKind, Partial<ConsumeOptions>>);
932
+ constructor(connection: ConnectionProvider, eventBus: EventBus, consumeOptionsMap?: Map<StreamKind, Partial<ConsumeOptions>>, consumerRecoveryFn?: ConsumerRecoveryFn | undefined);
740
933
  /** Observable stream of workqueue event messages. */
741
934
  get events$(): Observable<JsMsg>;
742
935
  /** Observable stream of RPC command messages (jetstream mode). */
@@ -769,6 +962,14 @@ declare class MessageProvider {
769
962
  private createFlow;
770
963
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
771
964
  private consumeOnce;
965
+ /**
966
+ * Detect "consumer not found" errors from `js.consumers.get()`.
967
+ *
968
+ * Unlike JetStream Manager calls (which throw `JetStreamApiError`),
969
+ * the JetStream client's `consumers.get()` throws a plain `Error`
970
+ * with the error code embedded in the message text.
971
+ */
972
+ private isConsumerNotFound;
772
973
  /** Get the target subject for a consumer kind. */
773
974
  private getTargetSubject;
774
975
  /** Monitor heartbeats and restart the consumer iterator on prolonged silence. */
@@ -781,6 +982,57 @@ declare class MessageProvider {
781
982
  private consumeOrderedOnce;
782
983
  }
783
984
 
985
+ /**
986
+ * Publishes handler metadata to a NATS KV bucket for external service discovery.
987
+ *
988
+ * Uses TTL + heartbeat to manage entry lifecycle:
989
+ * - Entries are written on startup and refreshed every `ttl / 2`
990
+ * - When the pod stops (graceful or crash), heartbeat stops → entries expire via TTL
991
+ * - No explicit delete needed — NATS handles expiry automatically
992
+ *
993
+ * This provider is fully decoupled from stream/consumer infrastructure —
994
+ * it only depends on the NATS connection and module options.
995
+ */
996
+ declare class MetadataProvider {
997
+ private readonly connection;
998
+ private readonly logger;
999
+ private readonly bucketName;
1000
+ private readonly replicas;
1001
+ private readonly ttl;
1002
+ private currentEntries?;
1003
+ private heartbeatTimer?;
1004
+ private cachedKv?;
1005
+ constructor(options: JetstreamModuleOptions, connection: ConnectionProvider);
1006
+ /**
1007
+ * Write handler metadata entries to the KV bucket and start heartbeat.
1008
+ *
1009
+ * Creates the bucket if it doesn't exist (idempotent).
1010
+ * Skips silently when entries map is empty.
1011
+ * Starts a heartbeat interval that refreshes entries every `ttl / 2`
1012
+ * to prevent TTL expiry while the pod is alive.
1013
+ *
1014
+ * Non-critical — errors are logged but do not prevent transport startup.
1015
+ *
1016
+ * @param entries Map of KV key → metadata object.
1017
+ */
1018
+ publish(entries: Map<string, Record<string, unknown>>): Promise<void>;
1019
+ /**
1020
+ * Stop the heartbeat timer.
1021
+ *
1022
+ * After this call, entries will expire via TTL once the heartbeat window passes.
1023
+ * Called during transport shutdown (strategy.close()).
1024
+ */
1025
+ destroy(): void;
1026
+ /** Write entries to KV with per-entry error handling. */
1027
+ private writeEntries;
1028
+ /** Start heartbeat interval that refreshes entries every ttl/2. */
1029
+ private startHeartbeat;
1030
+ /** Refresh all current entries in KV (heartbeat tick). */
1031
+ private refreshEntries;
1032
+ /** Create or open the KV bucket (cached after first call). */
1033
+ private openBucket;
1034
+ }
1035
+
784
1036
  /**
785
1037
  * NestJS custom transport strategy for NATS JetStream.
786
1038
  *
@@ -803,17 +1055,18 @@ declare class JetstreamStrategy extends Server implements CustomTransportStrateg
803
1055
  private readonly rpcRouter;
804
1056
  private readonly coreRpcServer;
805
1057
  private readonly ackWaitMap;
1058
+ private readonly metadataProvider?;
806
1059
  readonly transportId: symbol;
807
1060
  private readonly listeners;
808
1061
  private started;
809
- constructor(options: JetstreamModuleOptions, connection: ConnectionProvider, patternRegistry: PatternRegistry, streamProvider: StreamProvider, consumerProvider: ConsumerProvider, messageProvider: MessageProvider, eventRouter: EventRouter, rpcRouter: RpcRouter, coreRpcServer: CoreRpcServer, ackWaitMap?: Map<StreamKind, number>);
1062
+ 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);
810
1063
  /**
811
1064
  * Start the transport: register handlers, create infrastructure, begin consumption.
812
1065
  *
813
1066
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
814
1067
  */
815
1068
  listen(callback: () => void): Promise<void>;
816
- /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
1069
+ /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
817
1070
  close(): void;
818
1071
  /**
819
1072
  * Register event listener (required by Server base class).
@@ -848,22 +1101,30 @@ interface Stoppable {
848
1101
  * Shutdown sequence:
849
1102
  * 1. Emit onShutdownStart hook
850
1103
  * 2. Stop accepting new messages (close subscriptions, stop consumers)
851
- * 3. Wait for in-flight handlers to complete (up to timeout)
852
- * 4. Drain and close NATS connection
853
- * 5. Emit onShutdownComplete hook
1104
+ * 3. Drain and close NATS connection (with timeout safety net)
1105
+ * 4. Emit onShutdownComplete hook
1106
+ *
1107
+ * Idempotent — concurrent or repeated calls return the same promise.
1108
+ * This is critical because NestJS may call `onApplicationShutdown` on
1109
+ * multiple module instances (forRoot + forFeature) that share this
1110
+ * singleton, and the call order is not guaranteed.
854
1111
  */
855
1112
  declare class ShutdownManager {
856
1113
  private readonly connection;
857
1114
  private readonly eventBus;
858
1115
  private readonly timeout;
859
1116
  private readonly logger;
1117
+ private shutdownPromise?;
860
1118
  constructor(connection: ConnectionProvider, eventBus: EventBus, timeout: number);
861
1119
  /**
862
1120
  * Execute the full shutdown sequence.
863
1121
  *
1122
+ * Idempotent — concurrent or repeated calls return the same promise.
1123
+ *
864
1124
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
865
1125
  */
866
1126
  shutdown(strategy?: Stoppable): Promise<void>;
1127
+ private doShutdown;
867
1128
  }
868
1129
 
869
1130
  /**
@@ -997,7 +1258,9 @@ declare class JetstreamClient extends ClientProxy {
997
1258
  * Publish a fire-and-forget event to JetStream.
998
1259
  *
999
1260
  * Events are published to either the workqueue stream or broadcast stream
1000
- * depending on the subject prefix.
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.
1001
1264
  */
1002
1265
  protected dispatchEvent<T = unknown>(packet: ReadPacket): Promise<T>;
1003
1266
  /**
@@ -1023,8 +1286,20 @@ declare class JetstreamClient extends ClientProxy {
1023
1286
  private buildEventSubject;
1024
1287
  /** Build NATS headers merging custom headers with transport headers. */
1025
1288
  private buildHeaders;
1026
- /** Extract data, headers, and timeout from raw packet data or JetstreamRecord. */
1289
+ /** Extract data, headers, timeout, and schedule from raw packet data or JetstreamRecord. */
1027
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;
1028
1303
  private getRpcTimeout;
1029
1304
  }
1030
1305
 
@@ -1053,6 +1328,10 @@ declare class JetstreamRecord<TData = unknown> {
1053
1328
  readonly timeout?: number | undefined;
1054
1329
  /** Custom message ID for JetStream deduplication. */
1055
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;
1056
1335
  constructor(
1057
1336
  /** Message payload. */
1058
1337
  data: TData,
@@ -1061,7 +1340,11 @@ declare class JetstreamRecord<TData = unknown> {
1061
1340
  /** Per-request RPC timeout override in ms. */
1062
1341
  timeout?: number | undefined,
1063
1342
  /** Custom message ID for JetStream deduplication. */
1064
- messageId?: string | undefined);
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);
1065
1348
  }
1066
1349
  /**
1067
1350
  * Fluent builder for constructing JetstreamRecord instances.
@@ -1074,6 +1357,8 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1074
1357
  private readonly headers;
1075
1358
  private timeout;
1076
1359
  private messageId;
1360
+ private scheduleOptions;
1361
+ private ttlDuration;
1077
1362
  constructor(data?: TData);
1078
1363
  /**
1079
1364
  * Set the message payload.
@@ -1121,6 +1406,41 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1121
1406
  * @param ms - Timeout in milliseconds. Overrides the global RPC timeout for this request only.
1122
1407
  */
1123
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;
1124
1444
  /**
1125
1445
  * Build the immutable {@link JetstreamRecord}.
1126
1446
  *
@@ -1132,10 +1452,10 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1132
1452
  }
1133
1453
 
1134
1454
  /**
1135
- * Default JSON codec wrapping the nats.js JSONCodec.
1455
+ * Default JSON codec using native `TextEncoder`/`TextDecoder`.
1136
1456
  *
1137
- * Serializes to/from JSON using the native NATS implementation
1138
- * which handles `TextEncoder`/`TextDecoder` internally.
1457
+ * Serializes values to JSON via `JSON.stringify` and encodes the
1458
+ * resulting string into a `Uint8Array`. Decoding reverses the process.
1139
1459
  *
1140
1460
  * @example
1141
1461
  * ```typescript
@@ -1145,7 +1465,6 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1145
1465
  * ```
1146
1466
  */
1147
1467
  declare class JsonCodec implements Codec {
1148
- private readonly inner;
1149
1468
  encode(data: unknown): Uint8Array;
1150
1469
  decode(data: Uint8Array): unknown;
1151
1470
  }
@@ -1170,7 +1489,7 @@ type NatsMessage = JsMsg | Msg;
1170
1489
  * return;
1171
1490
  * }
1172
1491
  * if (!this.isReady()) {
1173
- * ctx.retry({ delay: 5000 });
1492
+ * ctx.retry({ delayMs: 5000 });
1174
1493
  * return;
1175
1494
  * }
1176
1495
  * await this.process(data);
@@ -1288,9 +1607,14 @@ declare class JetstreamHealthIndicator {
1288
1607
  * Returns `{ [key]: { status: 'up', ... } }` on success.
1289
1608
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
1290
1609
  *
1610
+ * The thrown error sets `isHealthCheckError: true` and `causes` — the
1611
+ * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
1612
+ * health failures from unexpected exceptions. Works with both Terminus v10
1613
+ * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
1614
+ *
1291
1615
  * @param key - Health indicator key (default: `'jetstream'`).
1292
1616
  * @returns Object with status, server, and latency under the given key.
1293
- * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
1617
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
1294
1618
  */
1295
1619
  isHealthy(key?: string): Promise<Record<string, Record<string, unknown>>>;
1296
1620
  }
@@ -1335,6 +1659,25 @@ type TimeUnit = 'ms' | 'seconds' | 'minutes' | 'hours' | 'days';
1335
1659
  * ```
1336
1660
  */
1337
1661
  declare const toNanos: (value: number, unit: TimeUnit) => number;
1662
+ /** Default KV bucket name for handler metadata. */
1663
+ declare const DEFAULT_METADATA_BUCKET = "handler_registry";
1664
+ /** Default number of KV bucket replicas. */
1665
+ declare const DEFAULT_METADATA_REPLICAS = 1;
1666
+ /** Default KV bucket history depth (latest value only). */
1667
+ declare const DEFAULT_METADATA_HISTORY = 1;
1668
+ /** Default KV bucket TTL in milliseconds (entries expire unless refreshed). */
1669
+ declare const DEFAULT_METADATA_TTL = 30000;
1670
+ /** Minimum allowed metadata TTL in milliseconds. Prevents tight heartbeat loops. */
1671
+ declare const MIN_METADATA_TTL = 5000;
1672
+ /**
1673
+ * Build a KV key for a handler's metadata entry.
1674
+ *
1675
+ * @param serviceName - Service name from `forRoot({ name })`.
1676
+ * @param kind - Handler's stream kind ({@link StreamKind}).
1677
+ * @param pattern - The message pattern (e.g. `'order.created'`).
1678
+ * @returns KV key (e.g. `orders.ev.order.created`).
1679
+ */
1680
+ declare const metadataKey: (serviceName: string, kind: StreamKind, pattern: string) => string;
1338
1681
  /**
1339
1682
  * NATS headers managed by the transport.
1340
1683
  *
@@ -1354,6 +1697,18 @@ declare enum JetstreamHeader {
1354
1697
  /** Set to `'true'` on error responses so the client can distinguish success from failure. */
1355
1698
  Error = "x-error"
1356
1699
  }
1700
+ declare enum JetstreamDlqHeader {
1701
+ /** Reason for the message being sent to the DLQ (error message or 'max_deliver_exceeded') */
1702
+ DeadLetterReason = "x-dead-letter-reason",
1703
+ /** Original NATS subject the message was originally published to */
1704
+ OriginalSubject = "x-original-subject",
1705
+ /** Source stream name */
1706
+ OriginalStream = "x-original-stream",
1707
+ /** ISO timestamp of when the message was moved to DLQ */
1708
+ FailedAt = "x-failed-at",
1709
+ /** Number of times the message has been delivered */
1710
+ DeliveryCount = "x-delivery-count"
1711
+ }
1357
1712
  /**
1358
1713
  * Build the internal service name with microservice suffix.
1359
1714
  *
@@ -1365,24 +1720,38 @@ declare const internalName: (name: string) => string;
1365
1720
  * Build a fully-qualified NATS subject for workqueue events, RPC commands, or ordered events.
1366
1721
  *
1367
1722
  * @param serviceName - Target service name.
1368
- * @param kind - Subject kind (`'ev'`, `'cmd'`, or `'ordered'`).
1723
+ * @param kind - Subject kind ({@link StreamKind.Event}, {@link StreamKind.Command}, or {@link StreamKind.Ordered}).
1369
1724
  * @param pattern - The message pattern (e.g. `'user.created'`).
1370
1725
  * @returns `{serviceName}__microservice.{kind}.{pattern}`
1371
1726
  */
1372
1727
  declare const buildSubject: (serviceName: string, kind: SubjectKind, pattern: string) => string;
1728
+ /**
1729
+ * Build a broadcast subject.
1730
+ *
1731
+ * @param pattern - The message pattern (e.g. `'config.updated'`).
1732
+ * @returns `broadcast.{pattern}`
1733
+ */
1734
+ declare const buildBroadcastSubject: (pattern: string) => string;
1373
1735
  /**
1374
1736
  * Build the JetStream stream name for a given service and kind.
1375
1737
  *
1376
1738
  * @param serviceName - Service name from `forRoot({ name })`.
1377
- * @param kind - Stream kind (`'ev'`, `'cmd'`, or `'broadcast'`).
1739
+ * @param kind - Stream kind ({@link StreamKind}).
1378
1740
  * @returns Stream name (e.g. `orders__microservice_ev-stream` or `broadcast-stream`).
1379
1741
  */
1380
1742
  declare const streamName: (serviceName: string, kind: StreamKind) => string;
1743
+ /**
1744
+ * Build the JetStream dead-letter queue stream name for a given service.
1745
+ *
1746
+ * @param serviceName - Service name from `forRoot({ name })`.
1747
+ * @returns DLQ Stream name (e.g. `orders__microservice_dlq-stream`).
1748
+ */
1749
+ declare const dlqStreamName: (serviceName: string) => string;
1381
1750
  /**
1382
1751
  * Build the JetStream consumer name for a given service and kind.
1383
1752
  *
1384
1753
  * @param serviceName - Service name from `forRoot({ name })`.
1385
- * @param kind - Stream kind (`'ev'`, `'cmd'`, or `'broadcast'`).
1754
+ * @param kind - Stream kind ({@link StreamKind}).
1386
1755
  * @returns Consumer name (e.g. `orders__microservice_ev-consumer`).
1387
1756
  */
1388
1757
  declare const consumerName: (serviceName: string, kind: StreamKind) => string;
@@ -1401,4 +1770,4 @@ declare const isJetStreamRpcMode: (rpc: RpcConfig | undefined) => boolean;
1401
1770
  /** Check if the RPC config specifies Core mode (default). */
1402
1771
  declare const isCoreRpcMode: (rpc: RpcConfig | undefined) => boolean;
1403
1772
 
1404
- 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 StreamConsumerOverrides, StreamKind, TransportEvent, type TransportHooks, buildSubject, consumerName, getClientToken, internalName, isCoreRpcMode, isJetStreamRpcMode, streamName, toNanos };
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 };