@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 +97 -86
- package/dist/index.cjs +85 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +88 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
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
|
-
- [
|
|
36
|
-
- [
|
|
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
|
-
##
|
|
321
|
+
## Messaging Patterns
|
|
321
322
|
|
|
322
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
385
|
+
### Events
|
|
383
386
|
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
583
|
+
## Operations
|
|
547
584
|
|
|
548
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
730
|
+
## Reference
|
|
724
731
|
|
|
725
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
785
|
+
#### Connection failure behavior
|
|
777
786
|
|
|
778
|
-
If the initial NATS connection is refused, the module throws
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
917
|
+
### API Reference
|
|
909
918
|
|
|
910
|
-
|
|
919
|
+
#### Exports
|
|
911
920
|
|
|
912
921
|
```typescript
|
|
913
922
|
// Module
|
|
@@ -951,7 +960,7 @@ StreamConsumerOverrides
|
|
|
951
960
|
TransportHooks
|
|
952
961
|
```
|
|
953
962
|
|
|
954
|
-
|
|
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
|
-
##
|
|
977
|
+
## Development
|
|
978
|
+
|
|
979
|
+
### Testing
|
|
969
980
|
|
|
970
|
-
The project uses [
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
+
#### Writing tests
|
|
998
1009
|
|
|
999
1010
|
- Use `sut` (system under test) for the main instance
|
|
1000
|
-
- Use `createMock<T>()` from `@golevelup/ts-
|
|
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(
|
|
1014
|
+
- Always include `afterEach(vi.resetAllMocks)`
|
|
1004
1015
|
|
|
1005
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)(
|
|
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
|
-
|
|
1290
|
-
|
|
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)(
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
|
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)(
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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,
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
}
|