@horizon-republic/nestjs-jetstream 2.3.0 → 2.3.2

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
@@ -16,24 +16,25 @@ A production-grade NestJS transport for NATS JetStream with built-in support for
16
16
  - [forRoot / forRootAsync](#forroot--forrootasync)
17
17
  - [forFeature](#forfeature)
18
18
  - [Full Options Reference](#full-options-reference)
19
- - [RPC (Request/Reply)](#rpc-requestreply)
20
- - [Core Mode (Default)](#core-mode-default)
21
- - [JetStream Mode](#jetstream-mode)
22
- - [Events](#events)
23
- - [Workqueue Events](#workqueue-events)
24
- - [Broadcast Events](#broadcast-events)
25
- - [JetstreamRecord Builder](#jetstreamrecord-builder)
26
- - [Custom Codec](#custom-codec)
27
- - [RpcContext](#rpccontext)
28
- - [Lifecycle Hooks](#lifecycle-hooks)
29
- - [Health Checks](#health-checks)
30
- - [Graceful Shutdown](#graceful-shutdown)
31
- - [Edge Cases & Important Notes](#edge-cases--important-notes)
32
- - [NATS Naming Conventions](#nats-naming-conventions)
33
- - [Default Stream & Consumer Configs](#default-stream--consumer-configs)
34
- - [API Reference](#api-reference)
35
- - [Testing](#testing)
36
- - [Contributing](#contributing)
19
+ - [Messaging Patterns](#messaging-patterns)
20
+ - [RPC (Request/Reply)](#rpc-requestreply)
21
+ - [Events](#events)
22
+ - [JetstreamRecord Builder](#jetstreamrecord-builder)
23
+ - [Handler Context & Serialization](#handler-context--serialization)
24
+ - [RpcContext](#rpccontext)
25
+ - [Custom Codec](#custom-codec)
26
+ - [Operations](#operations)
27
+ - [Lifecycle Hooks](#lifecycle-hooks)
28
+ - [Health Checks](#health-checks)
29
+ - [Graceful Shutdown](#graceful-shutdown)
30
+ - [Reference](#reference)
31
+ - [Edge Cases & Important Notes](#edge-cases--important-notes)
32
+ - [NATS Naming Conventions](#nats-naming-conventions)
33
+ - [Default Stream & Consumer Configs](#default-stream--consumer-configs)
34
+ - [API Reference](#api-reference)
35
+ - [Development](#development)
36
+ - [Testing](#testing)
37
+ - [Contributing](#contributing)
37
38
  - [License](#license)
38
39
  - [Links](#links)
39
40
 
@@ -317,9 +318,11 @@ rpc: {
317
318
  }
318
319
  ```
319
320
 
320
- ## RPC (Request/Reply)
321
+ ## Messaging Patterns
321
322
 
322
- ### Core Mode (Default)
323
+ ### RPC (Request/Reply)
324
+
325
+ #### Core Mode (Default)
323
326
 
324
327
  Uses NATS native `request/reply` for the lowest possible latency.
325
328
 
@@ -348,7 +351,7 @@ JetstreamModule.forRoot({
348
351
  | No handler running | Client times out |
349
352
  | Decode error | Error response returned to caller |
350
353
 
351
- ### JetStream Mode
354
+ #### JetStream Mode
352
355
 
353
356
  Commands are persisted in a JetStream stream. Responses flow back via NATS Core inbox.
354
357
 
@@ -379,9 +382,9 @@ JetstreamModule.forRoot({
379
382
 
380
383
  > **Why `term` instead of `nak` for RPC errors?** Redelivering a failed command could cause duplicate side effects. The caller is responsible for retrying.
381
384
 
382
- ## Events
385
+ ### Events
383
386
 
384
- ### Workqueue Events
387
+ #### Workqueue Events
385
388
 
386
389
  Each event is delivered to **one** handler instance (load-balanced). Messages are acked **after** the handler completes successfully.
387
390
 
@@ -429,7 +432,7 @@ JetstreamModule.forRoot({
429
432
  })
430
433
  ```
431
434
 
432
- ### Broadcast Events
435
+ #### Broadcast Events
433
436
 
434
437
  Broadcast events are delivered to **all** subscribing services. Each service gets its own durable consumer on a shared `broadcast-stream`.
435
438
 
@@ -469,7 +472,7 @@ JetstreamModule.forRoot({
469
472
 
470
473
  > **Note:** The broadcast stream is shared across all services — stream-level settings (e.g., `max_age`, `max_bytes`) affect everyone. Consumer-level settings are per-service.
471
474
 
472
- ## JetstreamRecord Builder
475
+ ### JetstreamRecord Builder
473
476
 
474
477
  Attach custom headers and per-request timeouts using the builder pattern:
475
478
 
@@ -507,7 +510,41 @@ Attempting to set a reserved header throws an error at build time.
507
510
  | `x-trace-id` | Available for distributed tracing |
508
511
  | `x-span-id` | Available for distributed tracing |
509
512
 
510
- ## Custom Codec
513
+ ## Handler Context & Serialization
514
+
515
+ ### RpcContext
516
+
517
+ Execution context available in all handlers via `@Ctx()`:
518
+
519
+ ```typescript
520
+ import { Ctx, Payload, MessagePattern } from '@nestjs/microservices';
521
+ import { RpcContext } from '@horizon-republic/nestjs-jetstream';
522
+
523
+ @MessagePattern('user.get')
524
+ getUser(@Payload() data: GetUserDto, @Ctx() ctx: RpcContext) {
525
+ const subject = ctx.getSubject(); // Full NATS subject
526
+ const traceId = ctx.getHeader('x-trace-id'); // Single header value
527
+ const headers = ctx.getHeaders(); // All headers (MsgHdrs)
528
+ const isJs = ctx.isJetStream(); // true for JetStream messages
529
+ const msg = ctx.getMessage(); // Raw JsMsg | Msg (escape hatch)
530
+
531
+ return this.userService.findOne(data.id);
532
+ }
533
+ ```
534
+
535
+ **Available methods:**
536
+
537
+ | Method | Returns | Description |
538
+ |------------------|------------------------|-------------------------------------------|
539
+ | `getSubject()` | `string` | NATS subject the message was published to |
540
+ | `getHeader(key)` | `string \| undefined` | Single header value by key |
541
+ | `getHeaders()` | `MsgHdrs \| undefined` | All NATS message headers |
542
+ | `isJetStream()` | `boolean` | Whether the message supports ack/nak/term |
543
+ | `getMessage()` | `JsMsg \| Msg` | Raw NATS message (escape hatch) |
544
+
545
+ Available on both `@EventPattern` and `@MessagePattern` handlers.
546
+
547
+ ### Custom Codec
511
548
 
512
549
  The library uses JSON by default. Implement the `Codec` interface for any serialization format:
513
550
 
@@ -543,39 +580,9 @@ JetstreamModule.forFeature({
543
580
 
544
581
  > All services communicating with each other **must use the same codec**. A codec mismatch results in decode errors (`term`, no redelivery).
545
582
 
546
- ## RpcContext
583
+ ## Operations
547
584
 
548
- Execution context available in all handlers via `@Ctx()`:
549
-
550
- ```typescript
551
- import { Ctx, Payload, MessagePattern } from '@nestjs/microservices';
552
- import { RpcContext } from '@horizon-republic/nestjs-jetstream';
553
-
554
- @MessagePattern('user.get')
555
- getUser(@Payload() data: GetUserDto, @Ctx() ctx: RpcContext) {
556
- const subject = ctx.getSubject(); // Full NATS subject
557
- const traceId = ctx.getHeader('x-trace-id'); // Single header value
558
- const headers = ctx.getHeaders(); // All headers (MsgHdrs)
559
- const isJs = ctx.isJetStream(); // true for JetStream messages
560
- const msg = ctx.getMessage(); // Raw JsMsg | Msg (escape hatch)
561
-
562
- return this.userService.findOne(data.id);
563
- }
564
- ```
565
-
566
- **Available methods:**
567
-
568
- | Method | Returns | Description |
569
- |------------------|------------------------|-------------------------------------------|
570
- | `getSubject()` | `string` | NATS subject the message was published to |
571
- | `getHeader(key)` | `string \| undefined` | Single header value by key |
572
- | `getHeaders()` | `MsgHdrs \| undefined` | All NATS message headers |
573
- | `isJetStream()` | `boolean` | Whether the message supports ack/nak/term |
574
- | `getMessage()` | `JsMsg \| Msg` | Raw NATS message (escape hatch) |
575
-
576
- Available on both `@EventPattern` and `@MessagePattern` handlers.
577
-
578
- ## Lifecycle Hooks
585
+ ### Lifecycle Hooks
579
586
 
580
587
  Subscribe to transport events for monitoring, alerting, or custom logic:
581
588
 
@@ -616,7 +623,7 @@ JetstreamModule.forRoot({
616
623
  | `shutdownComplete` | `()` | `Logger.log` |
617
624
  | `deadLetter` | `(info: DeadLetterInfo)` | `Logger.warn` |
618
625
 
619
- ### Dead Letter Queue (DLQ)
626
+ #### Dead Letter Queue (DLQ)
620
627
 
621
628
  When an event handler fails on every delivery attempt (`max_deliver`), the message becomes a "dead letter." By default, NATS terminates it silently. Configure `onDeadLetter` to intercept these messages:
622
629
 
@@ -662,7 +669,7 @@ JetstreamModule.forRootAsync({
662
669
  });
663
670
  ```
664
671
 
665
- ## Health Checks
672
+ ### Health Checks
666
673
 
667
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.
668
675
 
@@ -702,7 +709,7 @@ const status = await this.jetstream.check();
702
709
  | `check()` | `JetstreamHealthStatus` | Never |
703
710
  | `isHealthy(key?)` | `{ [key]: { status: 'up', ... } }` | On unhealthy (Terminus convention) |
704
711
 
705
- ## Graceful Shutdown
712
+ ### Graceful Shutdown
706
713
 
707
714
  The transport shuts down automatically via NestJS `onApplicationShutdown()`:
708
715
 
@@ -720,13 +727,15 @@ JetstreamModule.forRoot({
720
727
 
721
728
  No manual shutdown code needed.
722
729
 
723
- ## Edge Cases & Important Notes
730
+ ## Reference
724
731
 
725
- ### Event handlers must be idempotent
732
+ ### Edge Cases & Important Notes
733
+
734
+ #### Event handlers must be idempotent
726
735
 
727
736
  Events use at-least-once delivery. If your handler throws, the message is `nak`'d and NATS redelivers it (up to `max_deliver` times, default 3). Design handlers to be safe for repeated execution.
728
737
 
729
- ### RPC error handling
738
+ #### RPC error handling
730
739
 
731
740
  The transport fully supports NestJS `RpcException` and custom exception filters. Throw `RpcException` with any payload — it will be delivered to the caller as-is:
732
741
 
@@ -751,11 +760,11 @@ this.client.send('user.update', data).subscribe({
751
760
 
752
761
  In JetStream mode, failed RPC messages are `term`'d (not `nak`'d) to prevent duplicate side effects. The caller is responsible for implementing retry logic.
753
762
 
754
- ### Fire-and-forget events
763
+ #### Fire-and-forget events
755
764
 
756
765
  This library focuses on **reliable, persistent** event delivery via JetStream. If you need fire-and-forget (no persistence, no ack) for high-throughput scenarios, use the standard [NestJS NATS transport](https://docs.nestjs.com/microservices/nats) — it works perfectly alongside this library on the same NATS server.
757
766
 
758
- ### Publisher-only mode
767
+ #### Publisher-only mode
759
768
 
760
769
  For services that only send messages (e.g., API gateways), disable consumer infrastructure:
761
770
 
@@ -767,17 +776,17 @@ JetstreamModule.forRoot({
767
776
  })
768
777
  ```
769
778
 
770
- ### Broadcast stream is shared
779
+ #### Broadcast stream is shared
771
780
 
772
781
  All services share a single `broadcast-stream`. Each service creates its own durable consumer with `filter_subjects` matching only its registered broadcast patterns. Stream-level configuration (`broadcast.stream`) affects all services.
773
782
 
774
783
  Broadcast consumers use the same ack/nak semantics as workqueue consumers. Because each service has an **isolated durable consumer**, a `nak` (retry) from one service only causes redelivery to that specific service — other consumers are unaffected. This gives broadcast **at-least-once delivery per consumer** with independent retry.
775
784
 
776
- ### Connection failure behavior
785
+ #### Connection failure behavior
777
786
 
778
- If the initial NATS connection is refused, the module throws a `RuntimeException` immediately (fail fast). For transient disconnects after startup, NATS handles reconnection automatically and the `reconnect` hook fires.
787
+ If the initial NATS connection is refused, the module throws an `Error` immediately (fail fast). For transient disconnects after startup, NATS handles reconnection automatically and the `reconnect` hook fires.
779
788
 
780
- ### Observable return values
789
+ #### Observable return values
781
790
 
782
791
  Handlers can return Observables. The transport takes the **first emitted value** for RPC responses and awaits completion for events:
783
792
 
@@ -793,15 +802,15 @@ handleOrder(@Payload() data: OrderDto): Observable<void> {
793
802
  }
794
803
  ```
795
804
 
796
- ### Consumer self-healing
805
+ #### Consumer self-healing
797
806
 
798
- If a JetStream consumer's message iterator ends unexpectedly (e.g., NATS restart), the transport automatically re-establishes consumption after a 100ms delay. This is logged as a warning.
807
+ If a JetStream consumer's message iterator ends unexpectedly (e.g., NATS restart), the transport automatically re-establishes consumption with exponential backoff (100ms up to 30s). This is logged as a warning.
799
808
 
800
- ### NATS header size
809
+ #### NATS header size
801
810
 
802
811
  Custom headers are transmitted as NATS message headers. NATS has a default header size limit. If you're attaching large metadata, consider putting it in the message body instead.
803
812
 
804
- ## NATS Naming Conventions
813
+ ### NATS Naming Conventions
805
814
 
806
815
  The transport generates NATS subjects, streams, and consumers based on the service `name`:
807
816
 
@@ -818,7 +827,7 @@ The transport generates NATS subjects, streams, and consumers based on the servi
818
827
  | Command consumer | `{internal}_cmd-consumer` | `orders__microservice_cmd-consumer` |
819
828
  | Broadcast consumer | `{internal}_broadcast-consumer` | `orders__microservice_broadcast-consumer` |
820
829
 
821
- ## Default Stream & Consumer Configs
830
+ ### Default Stream & Consumer Configs
822
831
 
823
832
  All defaults can be overridden via `events`, `broadcast`, or `rpc` options.
824
833
 
@@ -905,9 +914,9 @@ All defaults can be overridden via `events`, `broadcast`, or `rpc` options.
905
914
 
906
915
  </details>
907
916
 
908
- ## API Reference
917
+ ### API Reference
909
918
 
910
- ### Exports
919
+ #### Exports
911
920
 
912
921
  ```typescript
913
922
  // Module
@@ -951,7 +960,7 @@ StreamConsumerOverrides
951
960
  TransportHooks
952
961
  ```
953
962
 
954
- ### Helper: `nanos(ms)`
963
+ #### Helper: `nanos(ms)`
955
964
 
956
965
  Convert milliseconds to nanoseconds (required by NATS JetStream config):
957
966
 
@@ -965,9 +974,11 @@ events: {
965
974
  }
966
975
  ```
967
976
 
968
- ## Testing
977
+ ## Development
978
+
979
+ ### Testing
969
980
 
970
- The project uses [Jest](https://jestjs.io/) with two test suites configured as [projects](https://jestjs.io/docs/configuration#projects-arraystring--projectconfig):
981
+ The project uses [Vitest](https://vitest.dev/) with two test suites configured as [projects](https://vitest.dev/guide/workspace):
971
982
 
972
983
  ```bash
973
984
  # Unit tests (no external dependencies)
@@ -986,7 +997,7 @@ pnpm test:watch
986
997
  pnpm test:cov
987
998
  ```
988
999
 
989
- ### Running NATS locally
1000
+ #### Running NATS locally
990
1001
 
991
1002
  Integration tests require a NATS server with JetStream enabled:
992
1003
 
@@ -994,15 +1005,15 @@ Integration tests require a NATS server with JetStream enabled:
994
1005
  docker run -d --name nats -p 4222:4222 nats:latest -js
995
1006
  ```
996
1007
 
997
- ### Writing tests
1008
+ #### Writing tests
998
1009
 
999
1010
  - Use `sut` (system under test) for the main instance
1000
- - Use `createMock<T>()` from `@golevelup/ts-jest` for mocking
1011
+ - Use `createMock<T>()` from `@golevelup/ts-vitest` for mocking
1001
1012
  - Follow Given-When-Then structure with comments
1002
1013
  - Order: happy path → edge cases → error cases
1003
- - Always include `afterEach(jest.resetAllMocks)`
1014
+ - Always include `afterEach(vi.resetAllMocks)`
1004
1015
 
1005
- ## Contributing
1016
+ ### Contributing
1006
1017
 
1007
1018
  Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
1008
1019
 
@@ -1017,4 +1028,4 @@ Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for
1017
1028
  - [GitHub Repository](https://github.com/HorizonRepublic/nestjs-jetstream)
1018
1029
  - [npm Package](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
1019
1030
  - [Report bugs](https://github.com/HorizonRepublic/nestjs-jetstream/issues)
1020
- - [Discussions](https://github.com/HorizonRepublic/nestjs-jetstream/discussions)
1031
+ - [Discussions](https://github.com/HorizonRepublic/nestjs-jetstream/discussions)
package/dist/index.cjs CHANGED
@@ -291,7 +291,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
291
291
  }
292
292
  /** Direct access to the raw NATS connection. */
293
293
  unwrap() {
294
- return this.connection.unwrap;
294
+ const nc = this.connection.unwrap;
295
+ if (!nc) {
296
+ throw new Error("Not connected \u2014 call connect() before unwrap()");
297
+ }
298
+ return nc;
295
299
  }
296
300
  /**
297
301
  * Publish a fire-and-forget event to JetStream.
@@ -304,10 +308,13 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
304
308
  const { data, hdrs } = this.extractRecordData(packet.data);
305
309
  const subject = this.buildEventSubject(packet.pattern);
306
310
  const msgHeaders = this.buildHeaders(hdrs, { subject });
307
- await nc.jetstream().publish(subject, this.codec.encode(data), {
311
+ const ack = await nc.jetstream().publish(subject, this.codec.encode(data), {
308
312
  headers: msgHeaders,
309
313
  msgID: crypto.randomUUID()
310
314
  });
315
+ if (ack.duplicate) {
316
+ this.logger.warn(`Duplicate event publish detected: ${subject} (seq: ${ack.seq})`);
317
+ }
311
318
  return void 0;
312
319
  }
313
320
  /**
@@ -571,7 +578,10 @@ var ConnectionProvider = class {
571
578
  if (this.connectionPromise) {
572
579
  return this.connectionPromise;
573
580
  }
574
- this.connectionPromise = this.establish();
581
+ this.connectionPromise = this.establish().catch((err) => {
582
+ this.connectionPromise = null;
583
+ throw err;
584
+ });
575
585
  return this.connectionPromise;
576
586
  }
577
587
  /**
@@ -624,8 +634,7 @@ var ConnectionProvider = class {
624
634
  this.monitorStatus(nc);
625
635
  return nc;
626
636
  } catch (err) {
627
- const natsErr = err;
628
- if (natsErr.code === "CONNECTION_REFUSED") {
637
+ if (err instanceof import_nats4.NatsError && err.code === "CONNECTION_REFUSED") {
629
638
  throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
630
639
  }
631
640
  throw err;
@@ -793,12 +802,18 @@ var JetstreamStrategy = class extends import_microservices2.Server {
793
802
  transportId = /* @__PURE__ */ Symbol("jetstream-transport");
794
803
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
795
804
  listeners = /* @__PURE__ */ new Map();
805
+ started = false;
796
806
  /**
797
807
  * Start the transport: register handlers, create infrastructure, begin consumption.
798
808
  *
799
809
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
800
810
  */
801
811
  async listen(callback) {
812
+ if (this.started) {
813
+ this.logger.warn("listen() called more than once \u2014 ignoring");
814
+ return;
815
+ }
816
+ this.started = true;
802
817
  this.patternRegistry.registerHandlers(this.getHandlers());
803
818
  const streamKinds = this.resolveStreamKinds();
804
819
  if (streamKinds.length > 0) {
@@ -823,11 +838,12 @@ var JetstreamStrategy = class extends import_microservices2.Server {
823
838
  this.rpcRouter.destroy();
824
839
  this.coreRpcServer.stop();
825
840
  this.messageProvider.destroy();
841
+ this.started = false;
826
842
  }
827
843
  /**
828
844
  * Register event listener (required by Server base class).
829
845
  *
830
- * Stores callbacks for potential use. Primary lifecycle events
846
+ * Stores callbacks for client use. Primary lifecycle events
831
847
  * are routed through EventBus.
832
848
  */
833
849
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -838,7 +854,11 @@ var JetstreamStrategy = class extends import_microservices2.Server {
838
854
  }
839
855
  /** Unwrap the underlying NATS connection. */
840
856
  unwrap() {
841
- return this.connection.unwrap;
857
+ const nc = this.connection.unwrap;
858
+ if (!nc) {
859
+ throw new Error("Not connected \u2014 transport has not started");
860
+ }
861
+ return nc;
842
862
  }
843
863
  /** Access the pattern registry (for module-level introspection). */
844
864
  getPatternRegistry() {
@@ -1008,6 +1028,7 @@ var CoreRpcServer = class {
1008
1028
 
1009
1029
  // src/server/infrastructure/stream.provider.ts
1010
1030
  var import_common5 = require("@nestjs/common");
1031
+ var import_nats6 = require("nats");
1011
1032
  var STREAM_NOT_FOUND = 10059;
1012
1033
  var StreamProvider = class {
1013
1034
  constructor(options, connection) {
@@ -1050,8 +1071,7 @@ var StreamProvider = class {
1050
1071
  this.logger.debug(`Stream exists, updating: ${config.name}`);
1051
1072
  return await jsm.streams.update(config.name, config);
1052
1073
  } catch (err) {
1053
- const natsErr = err;
1054
- if (natsErr.api_error?.err_code === STREAM_NOT_FOUND) {
1074
+ if (err instanceof import_nats6.NatsError && err.api_error?.err_code === STREAM_NOT_FOUND) {
1055
1075
  this.logger.log(`Creating stream: ${config.name}`);
1056
1076
  return await jsm.streams.add(config);
1057
1077
  }
@@ -1099,6 +1119,7 @@ var StreamProvider = class {
1099
1119
 
1100
1120
  // src/server/infrastructure/consumer.provider.ts
1101
1121
  var import_common6 = require("@nestjs/common");
1122
+ var import_nats7 = require("nats");
1102
1123
  var CONSUMER_NOT_FOUND = 10014;
1103
1124
  var ConsumerProvider = class {
1104
1125
  constructor(options, connection, streamProvider, patternRegistry) {
@@ -1139,8 +1160,7 @@ var ConsumerProvider = class {
1139
1160
  this.logger.debug(`Consumer exists: ${name}`);
1140
1161
  return info;
1141
1162
  } catch (err) {
1142
- const natsErr = err;
1143
- if (natsErr.api_error?.err_code === CONSUMER_NOT_FOUND) {
1163
+ if (err instanceof import_nats7.NatsError && err.api_error?.err_code === CONSUMER_NOT_FOUND) {
1144
1164
  this.logger.log(`Creating consumer: ${name}`);
1145
1165
  return await jsm.consumers.add(stream, config);
1146
1166
  }
@@ -1216,6 +1236,7 @@ var MessageProvider = class {
1216
1236
  }
1217
1237
  logger = new import_common7.Logger("Jetstream:Message");
1218
1238
  destroy$ = new import_rxjs3.Subject();
1239
+ activeIterators = /* @__PURE__ */ new Set();
1219
1240
  eventMessages$ = new import_rxjs3.Subject();
1220
1241
  commandMessages$ = new import_rxjs3.Subject();
1221
1242
  broadcastMessages$ = new import_rxjs3.Subject();
@@ -1250,6 +1271,10 @@ var MessageProvider = class {
1250
1271
  destroy() {
1251
1272
  this.destroy$.next();
1252
1273
  this.destroy$.complete();
1274
+ for (const messages of this.activeIterators) {
1275
+ messages.stop();
1276
+ }
1277
+ this.activeIterators.clear();
1253
1278
  this.eventMessages$.complete();
1254
1279
  this.commandMessages$.complete();
1255
1280
  this.broadcastMessages$.complete();
@@ -1257,8 +1282,13 @@ var MessageProvider = class {
1257
1282
  /** Create a self-healing consumer flow for a specific kind. */
1258
1283
  createFlow(kind, info) {
1259
1284
  const target$ = this.getTargetSubject(kind);
1285
+ let consecutiveFailures = 0;
1260
1286
  return (0, import_rxjs3.defer)(() => this.consumeOnce(info, target$)).pipe(
1287
+ (0, import_rxjs3.tap)(() => {
1288
+ consecutiveFailures = 0;
1289
+ }),
1261
1290
  (0, import_rxjs3.catchError)((err) => {
1291
+ consecutiveFailures++;
1262
1292
  this.logger.error(`Consumer ${info.name} error, will restart:`, err);
1263
1293
  this.eventBus.emit(
1264
1294
  "error" /* Error */,
@@ -1269,13 +1299,14 @@ var MessageProvider = class {
1269
1299
  }),
1270
1300
  (0, import_rxjs3.repeat)({
1271
1301
  delay: () => {
1272
- this.logger.warn(`Consumer ${info.name} stream ended, restarting...`);
1302
+ const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
1303
+ this.logger.warn(`Consumer ${info.name} stream ended, restarting in ${delay}ms...`);
1273
1304
  this.eventBus.emit(
1274
1305
  "error" /* Error */,
1275
1306
  new Error(`Consumer ${info.name} stream ended`),
1276
1307
  "message-provider"
1277
1308
  );
1278
- return (0, import_rxjs3.timer)(100);
1309
+ return (0, import_rxjs3.timer)(delay);
1279
1310
  }
1280
1311
  }),
1281
1312
  (0, import_rxjs3.takeUntil)(this.destroy$)
@@ -1286,8 +1317,13 @@ var MessageProvider = class {
1286
1317
  const js = (await this.connection.getConnection()).jetstream();
1287
1318
  const consumer = await js.consumers.get(info.stream_name, info.name);
1288
1319
  const messages = await consumer.consume();
1289
- for await (const msg of messages) {
1290
- target$.next(msg);
1320
+ this.activeIterators.add(messages);
1321
+ try {
1322
+ for await (const msg of messages) {
1323
+ target$.next(msg);
1324
+ }
1325
+ } finally {
1326
+ this.activeIterators.delete(messages);
1291
1327
  }
1292
1328
  }
1293
1329
  /** Get the target subject for a consumer kind. */
@@ -1427,11 +1463,14 @@ var EventRouter = class {
1427
1463
  /** Subscribe to a message stream and route each message. */
1428
1464
  subscribeToStream(stream$, label) {
1429
1465
  const subscription = stream$.pipe(
1430
- (0, import_rxjs4.mergeMap)((msg) => this.handle(msg)),
1431
- (0, import_rxjs4.catchError)((err, caught) => {
1432
- this.logger.error(`Unexpected error in ${label} event router`, err);
1433
- return caught;
1434
- })
1466
+ (0, import_rxjs4.mergeMap)(
1467
+ (msg) => (0, import_rxjs4.defer)(() => this.handle(msg)).pipe(
1468
+ (0, import_rxjs4.catchError)((err) => {
1469
+ this.logger.error(`Unexpected error in ${label} event router`, err);
1470
+ return import_rxjs4.EMPTY;
1471
+ })
1472
+ )
1473
+ )
1435
1474
  ).subscribe();
1436
1475
  this.subscriptions.push(subscription);
1437
1476
  }
@@ -1505,7 +1544,7 @@ var EventRouter = class {
1505
1544
 
1506
1545
  // src/server/routing/rpc.router.ts
1507
1546
  var import_common10 = require("@nestjs/common");
1508
- var import_nats6 = require("nats");
1547
+ var import_nats8 = require("nats");
1509
1548
  var import_rxjs5 = require("rxjs");
1510
1549
  var RpcRouter = class {
1511
1550
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, timeout) {
@@ -1522,11 +1561,14 @@ var RpcRouter = class {
1522
1561
  /** Start routing command messages to handlers. */
1523
1562
  start() {
1524
1563
  this.subscription = this.messageProvider.commands$.pipe(
1525
- (0, import_rxjs5.mergeMap)((msg) => this.handle(msg)),
1526
- (0, import_rxjs5.catchError)((err, caught) => {
1527
- this.logger.error("Unexpected error in RPC router", err);
1528
- return caught;
1529
- })
1564
+ (0, import_rxjs5.mergeMap)(
1565
+ (msg) => (0, import_rxjs5.defer)(() => this.handle(msg)).pipe(
1566
+ (0, import_rxjs5.catchError)((err) => {
1567
+ this.logger.error("Unexpected error in RPC router", err);
1568
+ return import_rxjs5.EMPTY;
1569
+ })
1570
+ )
1571
+ )
1530
1572
  ).subscribe();
1531
1573
  }
1532
1574
  /** Stop routing and unsubscribe. */
@@ -1564,7 +1606,7 @@ var RpcRouter = class {
1564
1606
  async executeHandler(handler, data, msg, replyTo, correlationId) {
1565
1607
  const nc = await this.connection.getConnection();
1566
1608
  const ctx = new RpcContext([msg]);
1567
- const hdrs = (0, import_nats6.headers)();
1609
+ const hdrs = (0, import_nats8.headers)();
1568
1610
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
1569
1611
  let settled = false;
1570
1612
  const timeoutId = setTimeout(() => {
@@ -1579,8 +1621,12 @@ var RpcRouter = class {
1579
1621
  if (settled) return;
1580
1622
  settled = true;
1581
1623
  clearTimeout(timeoutId);
1582
- nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
1583
1624
  msg.ack();
1625
+ try {
1626
+ nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
1627
+ } catch (publishErr) {
1628
+ this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
1629
+ }
1584
1630
  } catch (err) {
1585
1631
  if (settled) return;
1586
1632
  settled = true;
@@ -1614,10 +1660,17 @@ var ShutdownManager = class {
1614
1660
  this.eventBus.emit("shutdownStart" /* ShutdownStart */);
1615
1661
  this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
1616
1662
  strategy?.close();
1617
- await Promise.race([
1618
- this.connection.shutdown(),
1619
- new Promise((resolve) => setTimeout(resolve, this.timeout))
1620
- ]);
1663
+ let timeoutId;
1664
+ try {
1665
+ await Promise.race([
1666
+ this.connection.shutdown(),
1667
+ new Promise((resolve) => {
1668
+ timeoutId = setTimeout(resolve, this.timeout);
1669
+ })
1670
+ ]);
1671
+ } finally {
1672
+ clearTimeout(timeoutId);
1673
+ }
1621
1674
  this.eventBus.emit("shutdownComplete" /* ShutdownComplete */);
1622
1675
  this.logger.log("Graceful shutdown complete");
1623
1676
  }