@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 +26 -21
- package/dist/index.cjs +153 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -49
- package/dist/index.d.ts +157 -49
- package/dist/index.js +154 -111
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A production-grade NestJS transport for NATS JetStream with built-in support for
|
|
|
5
5
|
[](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
|
|
6
6
|
[](https://codecov.io/github/HorizonRepublic/nestjs-jetstream)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
|
-
[](https://
|
|
8
|
+
[](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
|
|
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
|
|
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 |
|
|
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)` |
|
|
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
|
|
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
|
|
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); //
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
593
|
-
this.
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
943
|
+
/** @returns The NATS subject this message was published to. */
|
|
901
944
|
getSubject() {
|
|
902
945
|
return this.args[0].subject;
|
|
903
946
|
}
|
|
904
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
1160
|
-
this.logger.debug(`Consumer exists: ${name}`);
|
|
1161
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1957
|
+
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
1907
1958
|
onDeadLetter: options.onDeadLetter
|
|
1908
1959
|
} : void 0;
|
|
1909
1960
|
return new EventRouter(
|