@horizon-republic/nestjs-jetstream 2.3.0 → 2.3.4

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
 
@@ -251,7 +252,7 @@ interface JetstreamModuleOptions {
251
252
  /** Broadcast event stream/consumer overrides. */
252
253
  broadcast?: { stream?: Partial<StreamConfig>; consumer?: Partial<ConsumerConfig> };
253
254
 
254
- /** Transport lifecycle hook handlers. Unset hooks fall back to NestJS Logger. */
255
+ /** Transport lifecycle hook handlers. Unset hooks are silently ignored. */
255
256
  hooks?: Partial<TransportHooks>;
256
257
 
257
258
  /** Async callback for dead letter handling. See Dead Letter Queue section below. */
@@ -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,41 +580,11 @@ 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
- Subscribe to transport events for monitoring, alerting, or custom logic:
587
+ Subscribe to transport events for monitoring, alerting, or custom logic. Events without a registered hook are silently ignored — no default logging:
581
588
 
582
589
  ```typescript
583
590
  import { JetstreamModule, TransportEvent } from '@horizon-republic/nestjs-jetstream';
@@ -604,19 +611,19 @@ JetstreamModule.forRoot({
604
611
 
605
612
  **Available events:**
606
613
 
607
- | Event | Arguments | Default (no hook) |
608
- |--------------------|---------------------------------------------|-------------------|
609
- | `connect` | `(server: string)` | `Logger.log` |
610
- | `disconnect` | `()` | `Logger.warn` |
611
- | `reconnect` | `(server: string)` | `Logger.log` |
612
- | `error` | `(error: Error, context?: string)` | `Logger.error` |
613
- | `rpcTimeout` | `(subject: string, correlationId: string)` | `Logger.warn` |
614
- | `messageRouted` | `(subject: string, kind: 'rpc' \| 'event')` | `Logger.debug` |
615
- | `shutdownStart` | `()` | `Logger.log` |
616
- | `shutdownComplete` | `()` | `Logger.log` |
617
- | `deadLetter` | `(info: DeadLetterInfo)` | `Logger.warn` |
614
+ | Event | Arguments |
615
+ |--------------------|---------------------------------------------|
616
+ | `connect` | `(server: string)` |
617
+ | `disconnect` | `()` |
618
+ | `reconnect` | `(server: string)` |
619
+ | `error` | `(error: Error, context?: string)` |
620
+ | `rpcTimeout` | `(subject: string, correlationId: string)` |
621
+ | `messageRouted` | `(subject: string, kind: 'rpc' \| 'event')` |
622
+ | `shutdownStart` | `()` |
623
+ | `shutdownComplete` | `()` |
624
+ | `deadLetter` | `(info: DeadLetterInfo)` |
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)