@horizon-republic/nestjs-jetstream 2.8.0 → 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
@@ -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.
@@ -516,6 +606,7 @@ declare class PatternRegistry {
516
606
  private _hasCommands;
517
607
  private _hasBroadcasts;
518
608
  private _hasOrdered;
609
+ private _hasMetadata;
519
610
  constructor(options: JetstreamModuleOptions);
520
611
  /**
521
612
  * Register all handlers from the NestJS strategy.
@@ -533,11 +624,21 @@ declare class PatternRegistry {
533
624
  hasOrderedHandlers(): boolean;
534
625
  /** Get fully-qualified NATS subjects for ordered handlers. */
535
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>>;
536
636
  /** Get patterns grouped by kind (cached after registration). */
537
637
  getPatternsByKind(): PatternsByKind;
538
638
  /** Normalize a full NATS subject back to the user-facing pattern. */
539
639
  normalizeSubject(subject: string): string;
540
640
  private buildPatternsByKind;
641
+ private resolveStreamKind;
541
642
  private logSummary;
542
643
  }
543
644
 
@@ -582,12 +683,14 @@ declare class StreamProvider {
582
683
  private readonly options;
583
684
  private readonly connection;
584
685
  private readonly logger;
686
+ private readonly migration;
585
687
  constructor(options: JetstreamModuleOptions, connection: ConnectionProvider);
586
688
  /**
587
689
  * Ensure all required streams exist with correct configuration.
588
690
  *
589
691
  * @param kinds Which stream kinds to create. Determined by the module based
590
692
  * on RPC mode and registered handler patterns.
693
+ * If the dlq option is enabled, also ensures the DLQ stream exists.
591
694
  */
592
695
  ensureStreams(kinds: StreamKind[]): Promise<void>;
593
696
  /** Get the stream name for a given kind. */
@@ -596,14 +699,32 @@ declare class StreamProvider {
596
699
  getSubjects(kind: StreamKind): string[];
597
700
  /** Ensure a single stream exists, creating or updating as needed. */
598
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;
599
707
  /** Build the full stream config by merging defaults with user overrides. */
600
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;
601
716
  /** Get default config for a stream kind. */
602
717
  private getDefaults;
603
718
  /** Check if scheduling is enabled for a stream kind via `allow_msg_schedules` override. */
604
719
  private isSchedulingEnabled;
605
- /** Get user-provided overrides for a stream kind. */
720
+ /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
606
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;
607
728
  }
608
729
 
609
730
  /**
@@ -615,6 +736,11 @@ declare class StreamProvider {
615
736
  * **Ordered** — strict sequential delivery:
616
737
  * - No ack/nak/DLQ — nats.js auto-acknowledges ordered consumer messages.
617
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.
618
744
  */
619
745
  declare class EventRouter {
620
746
  private readonly messageProvider;
@@ -624,9 +750,11 @@ declare class EventRouter {
624
750
  private readonly deadLetterConfig?;
625
751
  private readonly processingConfig?;
626
752
  private readonly ackWaitMap?;
753
+ private readonly connection?;
754
+ private readonly options?;
627
755
  private readonly logger;
628
756
  private readonly subscriptions;
629
- 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);
630
758
  /**
631
759
  * Update the max_deliver thresholds from actual NATS consumer configs.
632
760
  * Called after consumers are ensured so the DLQ map reflects reality.
@@ -651,6 +779,30 @@ declare class EventRouter {
651
779
  /** Check if the message has exhausted all delivery attempts. */
652
780
  private isDeadLetter;
653
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
+ */
654
806
  private handleDeadLetter;
655
807
  }
656
808
 
@@ -714,8 +866,36 @@ declare class ConsumerProvider {
714
866
  ensureConsumers(kinds: StreamKind[]): Promise<Map<StreamKind, ConsumerInfo>>;
715
867
  /** Get the consumer name for a given kind. */
716
868
  getConsumerName(kind: StreamKind): string;
717
- /** Ensure a single consumer exists, creating if needed. */
718
- 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;
719
899
  /** Build consumer config by merging defaults with user overrides. */
720
900
  private buildConfig;
721
901
  /** Get default config for a consumer kind. */
@@ -724,6 +904,8 @@ declare class ConsumerProvider {
724
904
  private getOverrides;
725
905
  }
726
906
 
907
+ /** Callback to recreate a consumer when it disappears. */
908
+ type ConsumerRecoveryFn = (kind: StreamKind) => Promise<ConsumerInfo>;
727
909
  /**
728
910
  * Manages pull-based message consumption from JetStream consumers.
729
911
  *
@@ -737,6 +919,7 @@ declare class MessageProvider {
737
919
  private readonly connection;
738
920
  private readonly eventBus;
739
921
  private readonly consumeOptionsMap;
922
+ private readonly consumerRecoveryFn?;
740
923
  private readonly logger;
741
924
  private readonly activeIterators;
742
925
  private orderedReadyResolve;
@@ -746,7 +929,7 @@ declare class MessageProvider {
746
929
  private commandMessages$;
747
930
  private broadcastMessages$;
748
931
  private orderedMessages$;
749
- constructor(connection: ConnectionProvider, eventBus: EventBus, consumeOptionsMap?: Map<StreamKind, Partial<ConsumeOptions>>);
932
+ constructor(connection: ConnectionProvider, eventBus: EventBus, consumeOptionsMap?: Map<StreamKind, Partial<ConsumeOptions>>, consumerRecoveryFn?: ConsumerRecoveryFn | undefined);
750
933
  /** Observable stream of workqueue event messages. */
751
934
  get events$(): Observable<JsMsg>;
752
935
  /** Observable stream of RPC command messages (jetstream mode). */
@@ -779,6 +962,14 @@ declare class MessageProvider {
779
962
  private createFlow;
780
963
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
781
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;
782
973
  /** Get the target subject for a consumer kind. */
783
974
  private getTargetSubject;
784
975
  /** Monitor heartbeats and restart the consumer iterator on prolonged silence. */
@@ -791,6 +982,57 @@ declare class MessageProvider {
791
982
  private consumeOrderedOnce;
792
983
  }
793
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
+
794
1036
  /**
795
1037
  * NestJS custom transport strategy for NATS JetStream.
796
1038
  *
@@ -813,17 +1055,18 @@ declare class JetstreamStrategy extends Server implements CustomTransportStrateg
813
1055
  private readonly rpcRouter;
814
1056
  private readonly coreRpcServer;
815
1057
  private readonly ackWaitMap;
1058
+ private readonly metadataProvider?;
816
1059
  readonly transportId: symbol;
817
1060
  private readonly listeners;
818
1061
  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>);
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);
820
1063
  /**
821
1064
  * Start the transport: register handlers, create infrastructure, begin consumption.
822
1065
  *
823
1066
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
824
1067
  */
825
1068
  listen(callback: () => void): Promise<void>;
826
- /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
1069
+ /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
827
1070
  close(): void;
828
1071
  /**
829
1072
  * Register event listener (required by Server base class).
@@ -858,22 +1101,30 @@ interface Stoppable {
858
1101
  * Shutdown sequence:
859
1102
  * 1. Emit onShutdownStart hook
860
1103
  * 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
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.
864
1111
  */
865
1112
  declare class ShutdownManager {
866
1113
  private readonly connection;
867
1114
  private readonly eventBus;
868
1115
  private readonly timeout;
869
1116
  private readonly logger;
1117
+ private shutdownPromise?;
870
1118
  constructor(connection: ConnectionProvider, eventBus: EventBus, timeout: number);
871
1119
  /**
872
1120
  * Execute the full shutdown sequence.
873
1121
  *
1122
+ * Idempotent — concurrent or repeated calls return the same promise.
1123
+ *
874
1124
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
875
1125
  */
876
1126
  shutdown(strategy?: Stoppable): Promise<void>;
1127
+ private doShutdown;
877
1128
  }
878
1129
 
879
1130
  /**
@@ -1079,6 +1330,8 @@ declare class JetstreamRecord<TData = unknown> {
1079
1330
  readonly messageId?: string | undefined;
1080
1331
  /** Schedule options for delayed delivery. */
1081
1332
  readonly schedule?: ScheduleRecordOptions | undefined;
1333
+ /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
1334
+ readonly ttl?: string | undefined;
1082
1335
  constructor(
1083
1336
  /** Message payload. */
1084
1337
  data: TData,
@@ -1089,7 +1342,9 @@ declare class JetstreamRecord<TData = unknown> {
1089
1342
  /** Custom message ID for JetStream deduplication. */
1090
1343
  messageId?: string | undefined,
1091
1344
  /** Schedule options for delayed delivery. */
1092
- schedule?: ScheduleRecordOptions | undefined);
1345
+ schedule?: ScheduleRecordOptions | undefined,
1346
+ /** Per-message TTL as Go duration string (e.g. "30s", "5m"). */
1347
+ ttl?: string | undefined);
1093
1348
  }
1094
1349
  /**
1095
1350
  * Fluent builder for constructing JetstreamRecord instances.
@@ -1103,6 +1358,7 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1103
1358
  private timeout;
1104
1359
  private messageId;
1105
1360
  private scheduleOptions;
1361
+ private ttlDuration;
1106
1362
  constructor(data?: TData);
1107
1363
  /**
1108
1364
  * Set the message payload.
@@ -1164,6 +1420,27 @@ declare class JetstreamRecordBuilder<TData = unknown> {
1164
1420
  * @throws Error if the date is not in the future.
1165
1421
  */
1166
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;
1167
1444
  /**
1168
1445
  * Build the immutable {@link JetstreamRecord}.
1169
1446
  *
@@ -1212,7 +1489,7 @@ type NatsMessage = JsMsg | Msg;
1212
1489
  * return;
1213
1490
  * }
1214
1491
  * if (!this.isReady()) {
1215
- * ctx.retry({ delay: 5000 });
1492
+ * ctx.retry({ delayMs: 5000 });
1216
1493
  * return;
1217
1494
  * }
1218
1495
  * await this.process(data);
@@ -1330,9 +1607,14 @@ declare class JetstreamHealthIndicator {
1330
1607
  * Returns `{ [key]: { status: 'up', ... } }` on success.
1331
1608
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
1332
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
+ *
1333
1615
  * @param key - Health indicator key (default: `'jetstream'`).
1334
1616
  * @returns Object with status, server, and latency under the given key.
1335
- * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
1617
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
1336
1618
  */
1337
1619
  isHealthy(key?: string): Promise<Record<string, Record<string, unknown>>>;
1338
1620
  }
@@ -1377,6 +1659,25 @@ type TimeUnit = 'ms' | 'seconds' | 'minutes' | 'hours' | 'days';
1377
1659
  * ```
1378
1660
  */
1379
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;
1380
1681
  /**
1381
1682
  * NATS headers managed by the transport.
1382
1683
  *
@@ -1396,6 +1697,18 @@ declare enum JetstreamHeader {
1396
1697
  /** Set to `'true'` on error responses so the client can distinguish success from failure. */
1397
1698
  Error = "x-error"
1398
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
+ }
1399
1712
  /**
1400
1713
  * Build the internal service name with microservice suffix.
1401
1714
  *
@@ -1412,6 +1725,13 @@ declare const internalName: (name: string) => string;
1412
1725
  * @returns `{serviceName}__microservice.{kind}.{pattern}`
1413
1726
  */
1414
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;
1415
1735
  /**
1416
1736
  * Build the JetStream stream name for a given service and kind.
1417
1737
  *
@@ -1420,6 +1740,13 @@ declare const buildSubject: (serviceName: string, kind: SubjectKind, pattern: st
1420
1740
  * @returns Stream name (e.g. `orders__microservice_ev-stream` or `broadcast-stream`).
1421
1741
  */
1422
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;
1423
1750
  /**
1424
1751
  * Build the JetStream consumer name for a given service and kind.
1425
1752
  *
@@ -1443,4 +1770,4 @@ declare const isJetStreamRpcMode: (rpc: RpcConfig | undefined) => boolean;
1443
1770
  /** Check if the RPC config specifies Core mode (default). */
1444
1771
  declare const isCoreRpcMode: (rpc: RpcConfig | undefined) => boolean;
1445
1772
 
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 };
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 };