@horizon-republic/nestjs-jetstream 2.3.2 → 2.3.5

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/README.md CHANGED
@@ -5,7 +5,7 @@ A production-grade NestJS transport for NATS JetStream with built-in support for
5
5
  [![npm version](https://img.shields.io/npm/v/@horizon-republic/nestjs-jetstream.svg)](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
6
6
  [![codecov](https://codecov.io/github/HorizonRepublic/nestjs-jetstream/graph/badge.svg?token=40IPSWFMT4)](https://codecov.io/github/HorizonRepublic/nestjs-jetstream)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
- [![Socket Badge](https://badge.socket.dev/npm/package/@horizon-republic/nestjs-jetstream)](https://badge.socket.dev/npm/package/@horizon-republic/nestjs-jetstream)
8
+ [![Socket Badge](https://badge.socket.dev/npm/package/@horizon-republic/nestjs-jetstream)](https://socket.dev/npm/package/@horizon-republic/nestjs-jetstream)
9
9
 
10
10
  ## Table of Contents
11
11
 
@@ -63,9 +63,9 @@ yarn add @horizon-republic/nestjs-jetstream
63
63
  **Peer dependencies:**
64
64
 
65
65
  ```
66
- @nestjs/common ^11.0.0
67
- @nestjs/core ^11.0.0
68
- @nestjs/microservices ^11.0.0
66
+ @nestjs/common ^10.2.0 || ^11.0.0
67
+ @nestjs/core ^10.2.0 || ^11.0.0
68
+ @nestjs/microservices ^10.2.0 || ^11.0.0
69
69
  nats ^2.0.0
70
70
  reflect-metadata ^0.2.0
71
71
  rxjs ^7.8.0
@@ -252,7 +252,7 @@ interface JetstreamModuleOptions {
252
252
  /** Broadcast event stream/consumer overrides. */
253
253
  broadcast?: { stream?: Partial<StreamConfig>; consumer?: Partial<ConsumerConfig> };
254
254
 
255
- /** Transport lifecycle hook handlers. Unset hooks fall back to NestJS Logger. */
255
+ /** Transport lifecycle hook handlers. Unset hooks are silently ignored. */
256
256
  hooks?: Partial<TransportHooks>;
257
257
 
258
258
  /** Async callback for dead letter handling. See Dead Letter Queue section below. */
@@ -305,6 +305,8 @@ Discriminated union on `mode`:
305
305
  | `'core'` | 30s | None | Low-latency, simple RPC |
306
306
  | `'jetstream'` | 3 min | JetStream stream | Commands must survive handler downtime |
307
307
 
308
+ > **Note:** `timeout` controls both the **client-side wait** (how long the caller waits for a response) and the **server-side handler limit** (how long the handler is allowed to run before being terminated). Both sides use the same value from their own `forRoot()` config.
309
+
308
310
  ```typescript
309
311
  // Core mode (default)
310
312
  rpc: { mode: 'core', timeout: 10_000 }
@@ -348,7 +350,8 @@ JetstreamModule.forRoot({
348
350
  |----------|--------|
349
351
  | Handler success | Response returned to caller |
350
352
  | Handler throws | Error response returned to caller |
351
- | No handler running | Client times out |
353
+ | No handler registered | Error response returned to caller |
354
+ | Server not running | Client times out |
352
355
  | Decode error | Error response returned to caller |
353
356
 
354
357
  #### JetStream Mode
@@ -584,7 +587,7 @@ JetstreamModule.forFeature({
584
587
 
585
588
  ### Lifecycle Hooks
586
589
 
587
- Subscribe to transport events for monitoring, alerting, or custom logic:
590
+ Subscribe to transport events for monitoring, alerting, or custom logic. Events without a registered hook are silently ignored — no default logging:
588
591
 
589
592
  ```typescript
590
593
  import { JetstreamModule, TransportEvent } from '@horizon-republic/nestjs-jetstream';
@@ -611,17 +614,17 @@ JetstreamModule.forRoot({
611
614
 
612
615
  **Available events:**
613
616
 
614
- | Event | Arguments | Default (no hook) |
615
- |--------------------|---------------------------------------------|-------------------|
616
- | `connect` | `(server: string)` | `Logger.log` |
617
- | `disconnect` | `()` | `Logger.warn` |
618
- | `reconnect` | `(server: string)` | `Logger.log` |
619
- | `error` | `(error: Error, context?: string)` | `Logger.error` |
620
- | `rpcTimeout` | `(subject: string, correlationId: string)` | `Logger.warn` |
621
- | `messageRouted` | `(subject: string, kind: 'rpc' \| 'event')` | `Logger.debug` |
622
- | `shutdownStart` | `()` | `Logger.log` |
623
- | `shutdownComplete` | `()` | `Logger.log` |
624
- | `deadLetter` | `(info: DeadLetterInfo)` | `Logger.warn` |
617
+ | Event | Arguments |
618
+ |--------------------|---------------------------------------------|
619
+ | `connect` | `(server: string)` |
620
+ | `disconnect` | `()` |
621
+ | `reconnect` | `(server: string)` |
622
+ | `error` | `(error: Error, context?: string)` |
623
+ | `rpcTimeout` | `(subject: string, correlationId: string)` |
624
+ | `messageRouted` | `(subject: string, kind: 'rpc' \| 'event')` |
625
+ | `shutdownStart` | `()` |
626
+ | `shutdownComplete` | `()` |
627
+ | `deadLetter` | `(info: DeadLetterInfo)` |
625
628
 
626
629
  #### Dead Letter Queue (DLQ)
627
630
 
@@ -671,7 +674,9 @@ JetstreamModule.forRootAsync({
671
674
 
672
675
  ### Health Checks
673
676
 
674
- `JetstreamHealthIndicator` is automatically registered and exported by `forRoot()`. It checks NATS connection status and measures round-trip latency. `@nestjs/terminus` is **not required** — the indicator follows the Terminus API convention so it works seamlessly when Terminus is present, but can also be used standalone.
677
+ `JetstreamHealthIndicator` is automatically registered and exported by `forRoot()`. It checks NATS connection status and measures round-trip latency. `@nestjs/terminus` is **not required** — the indicator follows the Terminus API convention and can be used standalone.
678
+
679
+ > **Note:** `isHealthy()` throws a plain `Error` with attached status details rather than Terminus's `HealthCheckError`. Terminus will report the service as unhealthy, but the structured `{ status: 'down', server, latency }` details may not appear in the response body. For full Terminus formatting, use the `check()` method in a custom wrapper.
675
680
 
676
681
  **With [@nestjs/terminus](https://docs.nestjs.com/recipes/terminus) (zero boilerplate):**
677
682
 
@@ -788,7 +793,7 @@ If the initial NATS connection is refused, the module throws an `Error` immediat
788
793
 
789
794
  #### Observable return values
790
795
 
791
- Handlers can return Observables. The transport takes the **first emitted value** for RPC responses and awaits completion for events:
796
+ Handlers can return Observables. The transport resolves on the **first emitted value** (or on completion if the Observable emits nothing):
792
797
 
793
798
  ```typescript
794
799
  @MessagePattern('user.get')
@@ -798,7 +803,7 @@ getUser(@Payload() data: { id: number }): Observable<UserDto> {
798
803
 
799
804
  @EventPattern('order.created')
800
805
  handleOrder(@Payload() data: OrderDto): Observable<void> {
801
- return this.pipeline.process(data); // awaits completion before ack
806
+ return this.pipeline.process(data); // ack after first emission or completion
802
807
  }
803
808
  ```
804
809
 
package/dist/index.cjs CHANGED
@@ -203,7 +203,11 @@ var JetstreamRecordBuilder = class {
203
203
  constructor(data) {
204
204
  this.data = data;
205
205
  }
206
- /** Set the message payload. */
206
+ /**
207
+ * Set the message payload.
208
+ *
209
+ * @param data - Payload to serialize via the configured {@link Codec}.
210
+ */
207
211
  setData(data) {
208
212
  this.data = data;
209
213
  return this;
@@ -211,6 +215,8 @@ var JetstreamRecordBuilder = class {
211
215
  /**
212
216
  * Set a single custom header.
213
217
  *
218
+ * @param key - Header name (e.g. `'x-tenant'`).
219
+ * @param value - Header value.
214
220
  * @throws Error if the header name is reserved by the transport.
215
221
  */
216
222
  setHeader(key, value) {
@@ -221,6 +227,7 @@ var JetstreamRecordBuilder = class {
221
227
  /**
222
228
  * Set multiple custom headers at once.
223
229
  *
230
+ * @param headers - Key-value pairs to set as headers.
224
231
  * @throws Error if any header name is reserved by the transport.
225
232
  */
226
233
  setHeaders(headers2) {
@@ -229,12 +236,20 @@ var JetstreamRecordBuilder = class {
229
236
  }
230
237
  return this;
231
238
  }
232
- /** Set RPC request timeout in milliseconds. */
239
+ /**
240
+ * Set per-request RPC timeout.
241
+ *
242
+ * @param ms - Timeout in milliseconds. Overrides the global RPC timeout for this request only.
243
+ */
233
244
  setTimeout(ms) {
234
245
  this.timeout = ms;
235
246
  return this;
236
247
  }
237
- /** Build the immutable JetstreamRecord. */
248
+ /**
249
+ * Build the immutable {@link JetstreamRecord}.
250
+ *
251
+ * @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
252
+ */
238
253
  build() {
239
254
  return new JetstreamRecord(this.data, new Map(this.headers), this.timeout);
240
255
  }
@@ -270,7 +285,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
270
285
  pendingTimeouts = /* @__PURE__ */ new Map();
271
286
  /** Subscription to connection status events for disconnect handling. */
272
287
  statusSubscription = null;
273
- /** Establish connection. Called automatically by NestJS on first use. */
288
+ /**
289
+ * Establish connection. Called automatically by NestJS on first use.
290
+ *
291
+ * Sets up the JetStream RPC inbox (if in JetStream mode) and subscribes
292
+ * to connection status events for fail-fast disconnect handling.
293
+ *
294
+ * @returns The underlying NATS connection.
295
+ */
274
296
  async connect() {
275
297
  const nc = await this.connection.getConnection();
276
298
  if (this.isJetStreamRpcMode() && !this.inboxSubscription) {
@@ -283,13 +305,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
283
305
  });
284
306
  return nc;
285
307
  }
286
- /** Clean up resources. */
308
+ /** Clean up resources: reject pending RPCs, unsubscribe from status events. */
287
309
  async close() {
288
310
  this.statusSubscription?.unsubscribe();
289
311
  this.statusSubscription = null;
290
312
  this.rejectPendingRpcs(new Error("Client closed"));
291
313
  }
292
- /** Direct access to the raw NATS connection. */
314
+ /**
315
+ * Direct access to the raw NATS connection.
316
+ *
317
+ * @throws Error if not connected.
318
+ */
293
319
  unwrap() {
294
320
  const nc = this.connection.unwrap;
295
321
  if (!nc) {
@@ -566,6 +592,7 @@ var ConnectionProvider = class {
566
592
  connection = null;
567
593
  connectionPromise = null;
568
594
  jsmInstance = null;
595
+ jsmPromise = null;
569
596
  /**
570
597
  * Establish NATS connection. Idempotent — returns cached connection on subsequent calls.
571
598
  *
@@ -585,16 +612,24 @@ var ConnectionProvider = class {
585
612
  return this.connectionPromise;
586
613
  }
587
614
  /**
588
- * Get JetStream manager. Cached after first call.
615
+ * Get the JetStream manager. Cached after first call.
616
+ *
617
+ * @returns The JetStreamManager for stream/consumer administration.
589
618
  */
590
619
  async getJetStreamManager() {
591
620
  if (this.jsmInstance) return this.jsmInstance;
592
- const nc = await this.getConnection();
593
- this.jsmInstance = await nc.jetstreamManager();
594
- this.logger.log("JetStream manager initialized");
595
- return this.jsmInstance;
621
+ if (this.jsmPromise) return this.jsmPromise;
622
+ this.jsmPromise = (async () => {
623
+ const nc = await this.getConnection();
624
+ this.jsmInstance = await nc.jetstreamManager();
625
+ this.logger.log("JetStream manager initialized");
626
+ return this.jsmInstance;
627
+ })().finally(() => {
628
+ this.jsmPromise = null;
629
+ });
630
+ return this.jsmPromise;
596
631
  }
597
- /** Direct access to the raw NATS connection (assumes already connected). */
632
+ /** Direct access to the raw NATS connection, or `null` if not yet connected. */
598
633
  get unwrap() {
599
634
  return this.connection;
600
635
  }
@@ -604,6 +639,12 @@ var ConnectionProvider = class {
604
639
  * Sequence: drain → wait for close. Falls back to force-close on error.
605
640
  */
606
641
  async shutdown() {
642
+ if (this.connectionPromise) {
643
+ try {
644
+ await this.connectionPromise;
645
+ } catch {
646
+ }
647
+ }
607
648
  if (!this.connection || this.connection.isClosed()) return;
608
649
  try {
609
650
  await this.connection.drain();
@@ -617,6 +658,7 @@ var ConnectionProvider = class {
617
658
  this.connection = null;
618
659
  this.connectionPromise = null;
619
660
  this.jsmInstance = null;
661
+ this.jsmPromise = null;
620
662
  }
621
663
  }
622
664
  /** Internal: establish the physical connection with reconnect monitoring. */
@@ -650,6 +692,7 @@ var ConnectionProvider = class {
650
692
  break;
651
693
  case import_nats4.Events.Reconnect:
652
694
  this.jsmInstance = null;
695
+ this.jsmPromise = null;
653
696
  this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
654
697
  break;
655
698
  case import_nats4.Events.Error:
@@ -678,53 +721,28 @@ var EventBus = class {
678
721
  this.logger = logger;
679
722
  this.hooks = hooks ?? {};
680
723
  }
681
- /** Emit a lifecycle event. Dispatches to custom hook or Logger fallback. */
724
+ /**
725
+ * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
726
+ *
727
+ * @param event - The {@link TransportEvent} to emit.
728
+ * @param args - Arguments matching the hook signature for this event.
729
+ */
682
730
  emit(event, ...args) {
683
731
  const hook = this.hooks[event];
684
- if (hook) {
685
- try {
686
- hook(...args);
687
- } catch (err) {
688
- this.logger.error(
689
- `Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
690
- );
691
- }
692
- return;
693
- }
694
- this.defaultHandler(event, args);
695
- }
696
- /** Default Logger-based handlers for each event type. */
697
- defaultHandler(event, args) {
698
- switch (event) {
699
- case "connect" /* Connect */:
700
- this.logger.log(`Connected to NATS: ${args[0]}`);
701
- break;
702
- case "disconnect" /* Disconnect */:
703
- this.logger.warn("NATS connection lost");
704
- break;
705
- case "reconnect" /* Reconnect */:
706
- this.logger.log(`Reconnected to NATS: ${args[0]}`);
707
- break;
708
- case "error" /* Error */:
709
- this.logger.error(`Transport error: ${args[0]}`, args[1] ?? "");
710
- break;
711
- case "rpcTimeout" /* RpcTimeout */:
712
- this.logger.warn(`RPC timeout: ${args[0]} (cid: ${args[1]})`);
713
- break;
714
- case "messageRouted" /* MessageRouted */:
715
- this.logger.debug(`Message routed: ${args[0]} [${args[1]}]`);
716
- break;
717
- case "shutdownStart" /* ShutdownStart */:
718
- this.logger.log("Graceful shutdown initiated");
719
- break;
720
- case "shutdownComplete" /* ShutdownComplete */:
721
- this.logger.log("Graceful shutdown complete");
722
- break;
723
- case "deadLetter" /* DeadLetter */: {
724
- const info = args[0];
725
- this.logger.warn(`Dead letter: ${info?.subject ?? "unknown"}`);
726
- break;
732
+ if (!hook) return;
733
+ try {
734
+ const result = hook(...args);
735
+ if (result && typeof result.catch === "function") {
736
+ result.catch((err) => {
737
+ this.logger.error(
738
+ `Async hook "${event}" rejected: ${err instanceof Error ? err.message : err}`
739
+ );
740
+ });
727
741
  }
742
+ } catch (err) {
743
+ this.logger.error(
744
+ `Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
745
+ );
728
746
  }
729
747
  }
730
748
  };
@@ -741,6 +759,8 @@ var JetstreamHealthIndicator = class {
741
759
  *
742
760
  * Returns the current connection status without throwing.
743
761
  * Use this for custom health endpoints or monitoring integrations.
762
+ *
763
+ * @returns Connection status with server URL and RTT latency.
744
764
  */
745
765
  async check() {
746
766
  const nc = this.connection.unwrap;
@@ -763,7 +783,9 @@ var JetstreamHealthIndicator = class {
763
783
  * Returns `{ [key]: { status: 'up', ... } }` on success.
764
784
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
765
785
  *
766
- * @param key Health indicator key (default: 'jetstream')
786
+ * @param key - Health indicator key (default: `'jetstream'`).
787
+ * @returns Object with status, server, and latency under the given key.
788
+ * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
767
789
  */
768
790
  async isHealthy(key = "jetstream") {
769
791
  const status = await this.check();
@@ -819,6 +841,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
819
841
  if (streamKinds.length > 0) {
820
842
  await this.streamProvider.ensureStreams(streamKinds);
821
843
  const consumers = await this.consumerProvider.ensureConsumers(streamKinds);
844
+ this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
822
845
  this.messageProvider.start(consumers);
823
846
  if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers()) {
824
847
  this.eventRouter.start();
@@ -832,7 +855,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
832
855
  }
833
856
  callback();
834
857
  }
835
- /** Gracefully stop the transport. */
858
+ /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
836
859
  close() {
837
860
  this.eventRouter.destroy();
838
861
  this.rpcRouter.destroy();
@@ -852,7 +875,11 @@ var JetstreamStrategy = class extends import_microservices2.Server {
852
875
  existing.push(callback);
853
876
  this.listeners.set(event, existing);
854
877
  }
855
- /** Unwrap the underlying NATS connection. */
878
+ /**
879
+ * Unwrap the underlying NATS connection.
880
+ *
881
+ * @throws Error if the transport has not started.
882
+ */
856
883
  unwrap() {
857
884
  const nc = this.connection.unwrap;
858
885
  if (!nc) {
@@ -878,6 +905,18 @@ var JetstreamStrategy = class extends import_microservices2.Server {
878
905
  }
879
906
  return kinds;
880
907
  }
908
+ /** Build max_deliver map from actual NATS consumer configs (not options). */
909
+ buildMaxDeliverMap(consumers) {
910
+ const map = /* @__PURE__ */ new Map();
911
+ for (const [, info] of consumers) {
912
+ const stream = info.stream_name;
913
+ const maxDeliver = info.config.max_deliver;
914
+ if (stream && maxDeliver !== void 0 && maxDeliver > 0) {
915
+ map.set(stream, maxDeliver);
916
+ }
917
+ }
918
+ return map;
919
+ }
881
920
  isCoreRpcMode() {
882
921
  return !this.options.rpc || this.options.rpc.mode === "core";
883
922
  }
@@ -893,23 +932,37 @@ var import_nats5 = require("nats");
893
932
  // src/context/rpc.context.ts
894
933
  var import_microservices3 = require("@nestjs/microservices");
895
934
  var RpcContext = class extends import_microservices3.BaseRpcContext {
896
- /** Get the underlying NATS message (JsMsg for JetStream, Msg for Core). */
935
+ /**
936
+ * Get the underlying NATS message.
937
+ *
938
+ * @returns `JsMsg` for JetStream handlers, `Msg` for Core RPC handlers.
939
+ */
897
940
  getMessage() {
898
941
  return this.args[0];
899
942
  }
900
- /** Get the NATS subject this message was published to. */
943
+ /** @returns The NATS subject this message was published to. */
901
944
  getSubject() {
902
945
  return this.args[0].subject;
903
946
  }
904
- /** Get all NATS message headers, or undefined if none are present. */
947
+ /** @returns All NATS message headers, or `undefined` if none are present. */
905
948
  getHeaders() {
906
949
  return this.args[0].headers;
907
950
  }
908
- /** Get a single header value by key. Returns undefined if the header or headers object is missing. */
951
+ /**
952
+ * Get a single header value by key.
953
+ *
954
+ * @param key - Header name (e.g. `'x-trace-id'`).
955
+ * @returns Header value, or `undefined` if the header is missing.
956
+ */
909
957
  getHeader(key) {
910
958
  return this.args[0].headers?.get(key);
911
959
  }
912
- /** Type guard: narrows getMessage() return type to JsMsg when true. */
960
+ /**
961
+ * Type guard: returns `true` when the message is a JetStream message.
962
+ *
963
+ * Narrows `getMessage()` return type to `JsMsg`, giving access to
964
+ * `ack()`, `nak()`, `term()`, and delivery metadata.
965
+ */
913
966
  isJetStream() {
914
967
  return "ack" in this.args[0];
915
968
  }
@@ -937,11 +990,13 @@ var unwrapResult = async (result) => {
937
990
  };
938
991
  var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
939
992
  let done = false;
940
- obs.subscribe({
993
+ let subscription = null;
994
+ subscription = obs.subscribe({
941
995
  next: (val) => {
942
996
  if (!done) {
943
997
  done = true;
944
998
  resolve(val);
999
+ subscription?.unsubscribe();
945
1000
  }
946
1001
  },
947
1002
  error: reject,
@@ -949,6 +1004,9 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
949
1004
  if (!done) resolve(void 0);
950
1005
  }
951
1006
  });
1007
+ if (done) {
1008
+ subscription.unsubscribe();
1009
+ }
952
1010
  });
953
1011
 
954
1012
  // src/server/core-rpc.server.ts
@@ -994,6 +1052,7 @@ var CoreRpcServer = class {
994
1052
  const handler = this.patternRegistry.getHandler(msg.subject);
995
1053
  if (!handler) {
996
1054
  this.logger.warn(`No handler for Core RPC: ${msg.subject}`);
1055
+ this.respondWithError(msg, new Error(`No handler for subject: ${msg.subject}`));
997
1056
  return;
998
1057
  }
999
1058
  this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "rpc");
@@ -1156,9 +1215,9 @@ var ConsumerProvider = class {
1156
1215
  const name = config.durable_name;
1157
1216
  this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
1158
1217
  try {
1159
- const info = await jsm.consumers.info(stream, name);
1160
- this.logger.debug(`Consumer exists: ${name}`);
1161
- return info;
1218
+ await jsm.consumers.info(stream, name);
1219
+ this.logger.debug(`Consumer exists, updating: ${name}`);
1220
+ return await jsm.consumers.update(stream, name, config);
1162
1221
  } catch (err) {
1163
1222
  if (err instanceof import_nats7.NatsError && err.api_error?.err_code === CONSUMER_NOT_FOUND) {
1164
1223
  this.logger.log(`Creating consumer: ${name}`);
@@ -1176,6 +1235,9 @@ var ConsumerProvider = class {
1176
1235
  const overrides = this.getOverrides(kind);
1177
1236
  if (kind === "broadcast") {
1178
1237
  const broadcastPatterns = this.patternRegistry.getBroadcastPatterns();
1238
+ if (broadcastPatterns.length === 0) {
1239
+ throw new Error("Broadcast consumer requested but no broadcast patterns are registered");
1240
+ }
1179
1241
  if (broadcastPatterns.length === 1) {
1180
1242
  return {
1181
1243
  ...defaults,
@@ -1235,8 +1297,8 @@ var MessageProvider = class {
1235
1297
  this.eventBus = eventBus;
1236
1298
  }
1237
1299
  logger = new import_common7.Logger("Jetstream:Message");
1238
- destroy$ = new import_rxjs3.Subject();
1239
1300
  activeIterators = /* @__PURE__ */ new Set();
1301
+ destroy$ = new import_rxjs3.Subject();
1240
1302
  eventMessages$ = new import_rxjs3.Subject();
1241
1303
  commandMessages$ = new import_rxjs3.Subject();
1242
1304
  broadcastMessages$ = new import_rxjs3.Subject();
@@ -1267,7 +1329,7 @@ var MessageProvider = class {
1267
1329
  (0, import_rxjs3.merge)(...flows).pipe((0, import_rxjs3.takeUntil)(this.destroy$)).subscribe();
1268
1330
  }
1269
1331
  }
1270
- /** Stop all consumer flows and complete all subjects. */
1332
+ /** Stop all consumer flows and reinitialize subjects for potential restart. */
1271
1333
  destroy() {
1272
1334
  this.destroy$.next();
1273
1335
  this.destroy$.complete();
@@ -1278,17 +1340,23 @@ var MessageProvider = class {
1278
1340
  this.eventMessages$.complete();
1279
1341
  this.commandMessages$.complete();
1280
1342
  this.broadcastMessages$.complete();
1343
+ this.destroy$ = new import_rxjs3.Subject();
1344
+ this.eventMessages$ = new import_rxjs3.Subject();
1345
+ this.commandMessages$ = new import_rxjs3.Subject();
1346
+ this.broadcastMessages$ = new import_rxjs3.Subject();
1281
1347
  }
1282
1348
  /** Create a self-healing consumer flow for a specific kind. */
1283
1349
  createFlow(kind, info) {
1284
1350
  const target$ = this.getTargetSubject(kind);
1285
1351
  let consecutiveFailures = 0;
1352
+ let lastRunFailed = false;
1286
1353
  return (0, import_rxjs3.defer)(() => this.consumeOnce(info, target$)).pipe(
1287
1354
  (0, import_rxjs3.tap)(() => {
1288
- consecutiveFailures = 0;
1355
+ lastRunFailed = false;
1289
1356
  }),
1290
1357
  (0, import_rxjs3.catchError)((err) => {
1291
1358
  consecutiveFailures++;
1359
+ lastRunFailed = true;
1292
1360
  this.logger.error(`Consumer ${info.name} error, will restart:`, err);
1293
1361
  this.eventBus.emit(
1294
1362
  "error" /* Error */,
@@ -1299,13 +1367,11 @@ var MessageProvider = class {
1299
1367
  }),
1300
1368
  (0, import_rxjs3.repeat)({
1301
1369
  delay: () => {
1370
+ if (!lastRunFailed) {
1371
+ consecutiveFailures = 0;
1372
+ }
1302
1373
  const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
1303
1374
  this.logger.warn(`Consumer ${info.name} stream ended, restarting in ${delay}ms...`);
1304
- this.eventBus.emit(
1305
- "error" /* Error */,
1306
- new Error(`Consumer ${info.name} stream ended`),
1307
- "message-provider"
1308
- );
1309
1375
  return (0, import_rxjs3.timer)(delay);
1310
1376
  }
1311
1377
  }),
@@ -1448,6 +1514,14 @@ var EventRouter = class {
1448
1514
  }
1449
1515
  logger = new import_common9.Logger("Jetstream:EventRouter");
1450
1516
  subscriptions = [];
1517
+ /**
1518
+ * Update the max_deliver thresholds from actual NATS consumer configs.
1519
+ * Called after consumers are ensured so the DLQ map reflects reality.
1520
+ */
1521
+ updateMaxDeliverMap(consumerMaxDelivers) {
1522
+ if (!this.deadLetterConfig) return;
1523
+ this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
1524
+ }
1451
1525
  /** Start routing event and broadcast messages to handlers. */
1452
1526
  start() {
1453
1527
  this.subscribeToStream(this.messageProvider.events$, "workqueue");
@@ -1497,10 +1571,7 @@ var EventRouter = class {
1497
1571
  /** Execute handler, then ack on success or nak/dead-letter on failure. */
1498
1572
  async executeHandler(handler, data, ctx, msg) {
1499
1573
  try {
1500
- const result = await handler(data, ctx);
1501
- if ((0, import_rxjs4.isObservable)(result)) {
1502
- await (0, import_rxjs4.lastValueFrom)(result, { defaultValue: void 0 });
1503
- }
1574
+ await unwrapResult(handler(data, ctx));
1504
1575
  msg.ack();
1505
1576
  } catch (err) {
1506
1577
  this.logger.error(`Event handler error (${msg.subject}):`, err);
@@ -1515,7 +1586,7 @@ var EventRouter = class {
1515
1586
  isDeadLetter(msg) {
1516
1587
  if (!this.deadLetterConfig) return false;
1517
1588
  const maxDeliver = this.deadLetterConfig.maxDeliverByStream.get(msg.info.stream);
1518
- if (maxDeliver === void 0) return false;
1589
+ if (maxDeliver === void 0 || maxDeliver <= 0) return false;
1519
1590
  return msg.info.deliveryCount >= maxDeliver;
1520
1591
  }
1521
1592
  /** Handle a dead letter: invoke callback, then term or nak based on result. */
@@ -1773,26 +1844,6 @@ var JetstreamModule = class {
1773
1844
  // -------------------------------------------------------------------
1774
1845
  // Provider factories
1775
1846
  // -------------------------------------------------------------------
1776
- /** Create all providers for synchronous forRoot(). */
1777
- /**
1778
- * Build a map of stream name -> max_deliver for dead letter detection.
1779
- * Each stream kind (ev, broadcast) has its own consumer config with potentially
1780
- * different max_deliver values.
1781
- */
1782
- static buildMaxDeliverMap(options) {
1783
- const map = /* @__PURE__ */ new Map();
1784
- const defaultEventMax = DEFAULT_EVENT_CONSUMER_CONFIG.max_deliver ?? 3;
1785
- const defaultBroadcastMax = DEFAULT_BROADCAST_CONSUMER_CONFIG.max_deliver ?? 3;
1786
- map.set(
1787
- streamName(options.name, "ev"),
1788
- options.events?.consumer?.max_deliver ?? defaultEventMax
1789
- );
1790
- map.set(
1791
- streamName(options.name, "broadcast"),
1792
- options.broadcast?.consumer?.max_deliver ?? defaultBroadcastMax
1793
- );
1794
- return map;
1795
- }
1796
1847
  static createCoreProviders(options) {
1797
1848
  return [
1798
1849
  {
@@ -1903,7 +1954,7 @@ var JetstreamModule = class {
1903
1954
  useFactory: (options, messageProvider, patternRegistry, codec, eventBus) => {
1904
1955
  if (options.consumer === false) return null;
1905
1956
  const deadLetterConfig = options.onDeadLetter ? {
1906
- maxDeliverByStream: JetstreamModule.buildMaxDeliverMap(options),
1957
+ maxDeliverByStream: /* @__PURE__ */ new Map(),
1907
1958
  onDeadLetter: options.onDeadLetter
1908
1959
  } : void 0;
1909
1960
  return new EventRouter(