@horizon-republic/nestjs-jetstream 2.5.1 → 2.7.0
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 +34 -5
- package/dist/index.cjs +620 -307
- package/dist/index.d.cts +239 -62
- package/dist/index.d.ts +239 -62
- package/dist/index.js +621 -317
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
} from "nats";
|
|
30
30
|
|
|
31
31
|
// src/interfaces/hooks.interface.ts
|
|
32
|
+
var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
|
|
33
|
+
MessageKind2["Event"] = "event";
|
|
34
|
+
MessageKind2["Rpc"] = "rpc";
|
|
35
|
+
return MessageKind2;
|
|
36
|
+
})(MessageKind || {});
|
|
32
37
|
var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
|
|
33
38
|
TransportEvent2["Connect"] = "connect";
|
|
34
39
|
TransportEvent2["Disconnect"] = "disconnect";
|
|
@@ -42,6 +47,15 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
|
|
|
42
47
|
return TransportEvent2;
|
|
43
48
|
})(TransportEvent || {});
|
|
44
49
|
|
|
50
|
+
// src/interfaces/stream.interface.ts
|
|
51
|
+
var StreamKind = /* @__PURE__ */ ((StreamKind2) => {
|
|
52
|
+
StreamKind2["Event"] = "ev";
|
|
53
|
+
StreamKind2["Command"] = "cmd";
|
|
54
|
+
StreamKind2["Broadcast"] = "broadcast";
|
|
55
|
+
StreamKind2["Ordered"] = "ordered";
|
|
56
|
+
return StreamKind2;
|
|
57
|
+
})(StreamKind || {});
|
|
58
|
+
|
|
45
59
|
// src/jetstream.constants.ts
|
|
46
60
|
import {
|
|
47
61
|
AckPolicy,
|
|
@@ -74,7 +88,7 @@ var baseStreamConfig = {
|
|
|
74
88
|
num_replicas: 1,
|
|
75
89
|
discard: DiscardPolicy.Old,
|
|
76
90
|
allow_direct: true,
|
|
77
|
-
compression: StoreCompression.
|
|
91
|
+
compression: StoreCompression.S2
|
|
78
92
|
};
|
|
79
93
|
var DEFAULT_EVENT_STREAM_CONFIG = {
|
|
80
94
|
...baseStreamConfig,
|
|
@@ -166,13 +180,20 @@ var internalName = (name) => `${name}__microservice`;
|
|
|
166
180
|
var buildSubject = (serviceName, kind, pattern) => `${internalName(serviceName)}.${kind}.${pattern}`;
|
|
167
181
|
var buildBroadcastSubject = (pattern) => `broadcast.${pattern}`;
|
|
168
182
|
var streamName = (serviceName, kind) => {
|
|
169
|
-
if (kind === "broadcast") return "broadcast-stream";
|
|
183
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
170
184
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
171
185
|
};
|
|
172
186
|
var consumerName = (serviceName, kind) => {
|
|
173
|
-
if (kind === "broadcast") return `${internalName(serviceName)}_broadcast-consumer`;
|
|
187
|
+
if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
|
|
174
188
|
return `${internalName(serviceName)}_${kind}-consumer`;
|
|
175
189
|
};
|
|
190
|
+
var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
|
|
191
|
+
PatternPrefix2["Broadcast"] = "broadcast:";
|
|
192
|
+
PatternPrefix2["Ordered"] = "ordered:";
|
|
193
|
+
return PatternPrefix2;
|
|
194
|
+
})(PatternPrefix || {});
|
|
195
|
+
var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
|
|
196
|
+
var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
176
197
|
|
|
177
198
|
// src/client/jetstream.record.ts
|
|
178
199
|
var JetstreamRecord = class {
|
|
@@ -287,10 +308,13 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
287
308
|
this.codec = codec;
|
|
288
309
|
this.eventBus = eventBus;
|
|
289
310
|
this.targetName = targetServiceName;
|
|
311
|
+
this.callerName = internalName(this.rootOptions.name);
|
|
290
312
|
}
|
|
291
313
|
logger = new Logger("Jetstream:Client");
|
|
292
314
|
/** Target service name this client sends messages to. */
|
|
293
315
|
targetName;
|
|
316
|
+
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
317
|
+
callerName;
|
|
294
318
|
/** Shared inbox for JetStream-mode RPC responses. */
|
|
295
319
|
inbox = null;
|
|
296
320
|
inboxSubscription = null;
|
|
@@ -310,7 +334,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
310
334
|
*/
|
|
311
335
|
async connect() {
|
|
312
336
|
const nc = await this.connection.getConnection();
|
|
313
|
-
if (this.
|
|
337
|
+
if (isJetStreamRpcMode(this.rootOptions.rpc) && !this.inboxSubscription) {
|
|
314
338
|
this.setupInbox(nc);
|
|
315
339
|
}
|
|
316
340
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
@@ -345,11 +369,11 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
345
369
|
* depending on the subject prefix.
|
|
346
370
|
*/
|
|
347
371
|
async dispatchEvent(packet) {
|
|
348
|
-
|
|
372
|
+
await this.connect();
|
|
349
373
|
const { data, hdrs, messageId } = this.extractRecordData(packet.data);
|
|
350
374
|
const subject = this.buildEventSubject(packet.pattern);
|
|
351
375
|
const msgHeaders = this.buildHeaders(hdrs, { subject });
|
|
352
|
-
const ack = await
|
|
376
|
+
const ack = await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
|
|
353
377
|
headers: msgHeaders,
|
|
354
378
|
msgID: messageId ?? crypto.randomUUID()
|
|
355
379
|
});
|
|
@@ -365,26 +389,23 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
365
389
|
* JetStream mode: publishes to stream + waits for inbox response.
|
|
366
390
|
*/
|
|
367
391
|
publish(packet, callback) {
|
|
368
|
-
const subject = buildSubject(this.targetName, "cmd"
|
|
392
|
+
const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
|
|
369
393
|
const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
|
|
370
394
|
const onUnhandled = (err) => {
|
|
371
395
|
this.logger.error("Unhandled publish error:", err);
|
|
372
396
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
373
397
|
};
|
|
374
398
|
let jetStreamCorrelationId = null;
|
|
375
|
-
if (this.
|
|
399
|
+
if (isCoreRpcMode(this.rootOptions.rpc)) {
|
|
376
400
|
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
|
|
377
401
|
} else {
|
|
378
402
|
jetStreamCorrelationId = crypto.randomUUID();
|
|
379
|
-
this.publishJetStreamRpc(
|
|
380
|
-
|
|
381
|
-
data,
|
|
382
|
-
hdrs,
|
|
403
|
+
this.publishJetStreamRpc(subject, data, callback, {
|
|
404
|
+
headers: hdrs,
|
|
383
405
|
timeout,
|
|
384
|
-
|
|
385
|
-
jetStreamCorrelationId,
|
|
406
|
+
correlationId: jetStreamCorrelationId,
|
|
386
407
|
messageId
|
|
387
|
-
).catch(onUnhandled);
|
|
408
|
+
}).catch(onUnhandled);
|
|
388
409
|
}
|
|
389
410
|
return () => {
|
|
390
411
|
if (jetStreamCorrelationId) {
|
|
@@ -421,35 +442,46 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
421
442
|
}
|
|
422
443
|
}
|
|
423
444
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
424
|
-
async publishJetStreamRpc(subject, data,
|
|
425
|
-
const
|
|
445
|
+
async publishJetStreamRpc(subject, data, callback, options) {
|
|
446
|
+
const { headers: customHeaders, correlationId, messageId } = options;
|
|
447
|
+
const effectiveTimeout = options.timeout ?? this.getRpcTimeout();
|
|
426
448
|
this.pendingMessages.set(correlationId, callback);
|
|
427
|
-
const timeoutId = setTimeout(() => {
|
|
428
|
-
if (!this.pendingMessages.has(correlationId)) return;
|
|
429
|
-
this.pendingTimeouts.delete(correlationId);
|
|
430
|
-
this.pendingMessages.delete(correlationId);
|
|
431
|
-
this.logger.error(`JetStream RPC timeout (${effectiveTimeout}ms): ${subject}`);
|
|
432
|
-
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
433
|
-
callback({ err: new Error("RPC timeout"), response: null, isDisposed: true });
|
|
434
|
-
}, effectiveTimeout);
|
|
435
|
-
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
436
449
|
try {
|
|
437
|
-
|
|
450
|
+
await this.connect();
|
|
451
|
+
if (!this.pendingMessages.has(correlationId)) return;
|
|
438
452
|
if (!this.inbox) {
|
|
439
|
-
|
|
453
|
+
this.pendingMessages.delete(correlationId);
|
|
454
|
+
callback({
|
|
455
|
+
err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
|
|
456
|
+
response: null,
|
|
457
|
+
isDisposed: true
|
|
458
|
+
});
|
|
459
|
+
return;
|
|
440
460
|
}
|
|
461
|
+
const timeoutId = setTimeout(() => {
|
|
462
|
+
if (!this.pendingMessages.has(correlationId)) return;
|
|
463
|
+
this.pendingTimeouts.delete(correlationId);
|
|
464
|
+
this.pendingMessages.delete(correlationId);
|
|
465
|
+
this.logger.error(`JetStream RPC timeout (${effectiveTimeout}ms): ${subject}`);
|
|
466
|
+
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
467
|
+
callback({ err: new Error("RPC timeout"), response: null, isDisposed: true });
|
|
468
|
+
}, effectiveTimeout);
|
|
469
|
+
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
441
470
|
const hdrs = this.buildHeaders(customHeaders, {
|
|
442
471
|
subject,
|
|
443
472
|
correlationId,
|
|
444
473
|
replyTo: this.inbox
|
|
445
474
|
});
|
|
446
|
-
await
|
|
475
|
+
await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
|
|
447
476
|
headers: hdrs,
|
|
448
477
|
msgID: messageId ?? crypto.randomUUID()
|
|
449
478
|
});
|
|
450
479
|
} catch (err) {
|
|
451
|
-
|
|
452
|
-
|
|
480
|
+
const existingTimeout = this.pendingTimeouts.get(correlationId);
|
|
481
|
+
if (existingTimeout) {
|
|
482
|
+
clearTimeout(existingTimeout);
|
|
483
|
+
this.pendingTimeouts.delete(correlationId);
|
|
484
|
+
}
|
|
453
485
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
454
486
|
this.pendingMessages.delete(correlationId);
|
|
455
487
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
@@ -525,19 +557,23 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
525
557
|
}
|
|
526
558
|
/** Build event subject — workqueue, broadcast, or ordered. */
|
|
527
559
|
buildEventSubject(pattern) {
|
|
528
|
-
if (pattern.startsWith("broadcast:")) {
|
|
529
|
-
return buildBroadcastSubject(pattern.slice("broadcast:"
|
|
530
|
-
}
|
|
531
|
-
if (pattern.startsWith("ordered:")) {
|
|
532
|
-
return buildSubject(
|
|
560
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
561
|
+
return buildBroadcastSubject(pattern.slice("broadcast:" /* Broadcast */.length));
|
|
562
|
+
}
|
|
563
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) {
|
|
564
|
+
return buildSubject(
|
|
565
|
+
this.targetName,
|
|
566
|
+
"ordered" /* Ordered */,
|
|
567
|
+
pattern.slice("ordered:" /* Ordered */.length)
|
|
568
|
+
);
|
|
533
569
|
}
|
|
534
|
-
return buildSubject(this.targetName, "ev"
|
|
570
|
+
return buildSubject(this.targetName, "ev" /* Event */, pattern);
|
|
535
571
|
}
|
|
536
572
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
537
573
|
buildHeaders(customHeaders, transport) {
|
|
538
574
|
const hdrs = natsHeaders();
|
|
539
575
|
hdrs.set("x-subject" /* Subject */, transport.subject);
|
|
540
|
-
hdrs.set("x-caller-name" /* CallerName */,
|
|
576
|
+
hdrs.set("x-caller-name" /* CallerName */, this.callerName);
|
|
541
577
|
if (transport.correlationId) {
|
|
542
578
|
hdrs.set("x-correlation-id" /* CorrelationId */, transport.correlationId);
|
|
543
579
|
}
|
|
@@ -563,15 +599,9 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
563
599
|
}
|
|
564
600
|
return { data: rawData, hdrs: null, timeout: void 0, messageId: void 0 };
|
|
565
601
|
}
|
|
566
|
-
isCoreRpcMode() {
|
|
567
|
-
return !this.rootOptions.rpc || this.rootOptions.rpc.mode === "core";
|
|
568
|
-
}
|
|
569
|
-
isJetStreamRpcMode() {
|
|
570
|
-
return this.rootOptions.rpc?.mode === "jetstream";
|
|
571
|
-
}
|
|
572
602
|
getRpcTimeout() {
|
|
573
603
|
if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
|
|
574
|
-
const defaultTimeout = this.
|
|
604
|
+
const defaultTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? DEFAULT_JETSTREAM_RPC_TIMEOUT : DEFAULT_RPC_TIMEOUT;
|
|
575
605
|
return this.rootOptions.rpc.timeout ?? defaultTimeout;
|
|
576
606
|
}
|
|
577
607
|
};
|
|
@@ -597,6 +627,10 @@ import {
|
|
|
597
627
|
NatsError
|
|
598
628
|
} from "nats";
|
|
599
629
|
import { defer, from, share, shareReplay, switchMap } from "rxjs";
|
|
630
|
+
var DEFAULT_OPTIONS = {
|
|
631
|
+
maxReconnectAttempts: -1,
|
|
632
|
+
reconnectTimeWait: 1e3
|
|
633
|
+
};
|
|
600
634
|
var ConnectionProvider = class {
|
|
601
635
|
constructor(options, eventBus) {
|
|
602
636
|
this.options = options;
|
|
@@ -616,6 +650,7 @@ var ConnectionProvider = class {
|
|
|
616
650
|
logger = new Logger2("Jetstream:Connection");
|
|
617
651
|
connection = null;
|
|
618
652
|
connectionPromise = null;
|
|
653
|
+
jsClient = null;
|
|
619
654
|
jsmInstance = null;
|
|
620
655
|
jsmPromise = null;
|
|
621
656
|
/**
|
|
@@ -654,6 +689,22 @@ var ConnectionProvider = class {
|
|
|
654
689
|
});
|
|
655
690
|
return this.jsmPromise;
|
|
656
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Get a cached JetStream client.
|
|
694
|
+
*
|
|
695
|
+
* Invalidated automatically on reconnect and shutdown so consumers always
|
|
696
|
+
* operate against the live connection.
|
|
697
|
+
*
|
|
698
|
+
* @returns The cached JetStreamClient.
|
|
699
|
+
* @throws Error if the connection has not been established yet.
|
|
700
|
+
*/
|
|
701
|
+
getJetStreamClient() {
|
|
702
|
+
if (!this.connection || this.connection.isClosed()) {
|
|
703
|
+
throw new Error("Not connected \u2014 call getConnection() before getJetStreamClient()");
|
|
704
|
+
}
|
|
705
|
+
this.jsClient ??= this.connection.jetstream();
|
|
706
|
+
return this.jsClient;
|
|
707
|
+
}
|
|
657
708
|
/** Direct access to the raw NATS connection, or `null` if not yet connected. */
|
|
658
709
|
get unwrap() {
|
|
659
710
|
return this.connection;
|
|
@@ -682,6 +733,7 @@ var ConnectionProvider = class {
|
|
|
682
733
|
} finally {
|
|
683
734
|
this.connection = null;
|
|
684
735
|
this.connectionPromise = null;
|
|
736
|
+
this.jsClient = null;
|
|
685
737
|
this.jsmInstance = null;
|
|
686
738
|
this.jsmPromise = null;
|
|
687
739
|
}
|
|
@@ -691,6 +743,7 @@ var ConnectionProvider = class {
|
|
|
691
743
|
const name = internalName(this.options.name);
|
|
692
744
|
try {
|
|
693
745
|
const nc = await connect({
|
|
746
|
+
...DEFAULT_OPTIONS,
|
|
694
747
|
...this.options.connectionOptions,
|
|
695
748
|
servers: this.options.servers,
|
|
696
749
|
name
|
|
@@ -716,6 +769,7 @@ var ConnectionProvider = class {
|
|
|
716
769
|
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
717
770
|
break;
|
|
718
771
|
case Events2.Reconnect:
|
|
772
|
+
this.jsClient = null;
|
|
719
773
|
this.jsmInstance = null;
|
|
720
774
|
this.jsmPromise = null;
|
|
721
775
|
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
@@ -755,6 +809,23 @@ var EventBus = class {
|
|
|
755
809
|
emit(event, ...args) {
|
|
756
810
|
const hook = this.hooks[event];
|
|
757
811
|
if (!hook) return;
|
|
812
|
+
this.callHook(event, hook, ...args);
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
816
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
817
|
+
*/
|
|
818
|
+
emitMessageRouted(subject, kind) {
|
|
819
|
+
const hook = this.hooks["messageRouted" /* MessageRouted */];
|
|
820
|
+
if (!hook) return;
|
|
821
|
+
this.callHook(
|
|
822
|
+
"messageRouted" /* MessageRouted */,
|
|
823
|
+
hook,
|
|
824
|
+
subject,
|
|
825
|
+
kind
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
callHook(event, hook, ...args) {
|
|
758
829
|
try {
|
|
759
830
|
const result = hook(...args);
|
|
760
831
|
if (result && typeof result.catch === "function") {
|
|
@@ -834,7 +905,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
834
905
|
// src/server/strategy.ts
|
|
835
906
|
import { Server } from "@nestjs/microservices";
|
|
836
907
|
var JetstreamStrategy = class extends Server {
|
|
837
|
-
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer) {
|
|
908
|
+
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
|
|
838
909
|
super();
|
|
839
910
|
this.options = options;
|
|
840
911
|
this.connection = connection;
|
|
@@ -845,6 +916,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
845
916
|
this.eventRouter = eventRouter;
|
|
846
917
|
this.rpcRouter = rpcRouter;
|
|
847
918
|
this.coreRpcServer = coreRpcServer;
|
|
919
|
+
this.ackWaitMap = ackWaitMap;
|
|
848
920
|
}
|
|
849
921
|
transportId = /* @__PURE__ */ Symbol("jetstream-transport");
|
|
850
922
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -862,17 +934,17 @@ var JetstreamStrategy = class extends Server {
|
|
|
862
934
|
}
|
|
863
935
|
this.started = true;
|
|
864
936
|
this.patternRegistry.registerHandlers(this.getHandlers());
|
|
865
|
-
const streamKinds = this.
|
|
866
|
-
const durableKinds = this.resolveDurableConsumerKinds();
|
|
937
|
+
const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
|
|
867
938
|
if (streamKinds.length > 0) {
|
|
868
939
|
await this.streamProvider.ensureStreams(streamKinds);
|
|
869
940
|
if (durableKinds.length > 0) {
|
|
870
941
|
const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
|
|
942
|
+
this.populateAckWaitMap(consumers);
|
|
871
943
|
this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
|
|
872
944
|
this.messageProvider.start(consumers);
|
|
873
945
|
}
|
|
874
946
|
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
875
|
-
const orderedStreamName = this.streamProvider.getStreamName("ordered");
|
|
947
|
+
const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
|
|
876
948
|
await this.messageProvider.startOrdered(
|
|
877
949
|
orderedStreamName,
|
|
878
950
|
this.patternRegistry.getOrderedSubjects(),
|
|
@@ -882,11 +954,11 @@ var JetstreamStrategy = class extends Server {
|
|
|
882
954
|
if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
|
|
883
955
|
this.eventRouter.start();
|
|
884
956
|
}
|
|
885
|
-
if (this.
|
|
886
|
-
this.rpcRouter.start();
|
|
957
|
+
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
958
|
+
await this.rpcRouter.start();
|
|
887
959
|
}
|
|
888
960
|
}
|
|
889
|
-
if (this.
|
|
961
|
+
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
890
962
|
await this.coreRpcServer.start();
|
|
891
963
|
}
|
|
892
964
|
callback();
|
|
@@ -927,36 +999,34 @@ var JetstreamStrategy = class extends Server {
|
|
|
927
999
|
getPatternRegistry() {
|
|
928
1000
|
return this.patternRegistry;
|
|
929
1001
|
}
|
|
930
|
-
/** Determine which
|
|
931
|
-
|
|
932
|
-
const
|
|
1002
|
+
/** Determine which streams and durable consumers are needed. */
|
|
1003
|
+
resolveRequiredKinds() {
|
|
1004
|
+
const streams = [];
|
|
1005
|
+
const durableConsumers = [];
|
|
933
1006
|
if (this.patternRegistry.hasEventHandlers()) {
|
|
934
|
-
|
|
1007
|
+
streams.push("ev" /* Event */);
|
|
1008
|
+
durableConsumers.push("ev" /* Event */);
|
|
935
1009
|
}
|
|
936
|
-
if (this.patternRegistry.
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
940
|
-
kinds.push("cmd");
|
|
1010
|
+
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
1011
|
+
streams.push("cmd" /* Command */);
|
|
1012
|
+
durableConsumers.push("cmd" /* Command */);
|
|
941
1013
|
}
|
|
942
1014
|
if (this.patternRegistry.hasBroadcastHandlers()) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
return kinds;
|
|
946
|
-
}
|
|
947
|
-
/** Determine which stream kinds need durable consumers (ordered consumers are ephemeral). */
|
|
948
|
-
resolveDurableConsumerKinds() {
|
|
949
|
-
const kinds = [];
|
|
950
|
-
if (this.patternRegistry.hasEventHandlers()) {
|
|
951
|
-
kinds.push("ev");
|
|
1015
|
+
streams.push("broadcast" /* Broadcast */);
|
|
1016
|
+
durableConsumers.push("broadcast" /* Broadcast */);
|
|
952
1017
|
}
|
|
953
|
-
if (this.
|
|
954
|
-
|
|
1018
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
1019
|
+
streams.push("ordered" /* Ordered */);
|
|
955
1020
|
}
|
|
956
|
-
|
|
957
|
-
|
|
1021
|
+
return { streams, durableConsumers };
|
|
1022
|
+
}
|
|
1023
|
+
/** Populate the shared ack_wait map from actual NATS consumer configs. */
|
|
1024
|
+
populateAckWaitMap(consumers) {
|
|
1025
|
+
for (const [kind, info] of consumers) {
|
|
1026
|
+
if (info.config.ack_wait) {
|
|
1027
|
+
this.ackWaitMap.set(kind, info.config.ack_wait);
|
|
1028
|
+
}
|
|
958
1029
|
}
|
|
959
|
-
return kinds;
|
|
960
1030
|
}
|
|
961
1031
|
/** Build max_deliver map from actual NATS consumer configs (not options). */
|
|
962
1032
|
buildMaxDeliverMap(consumers) {
|
|
@@ -970,12 +1040,6 @@ var JetstreamStrategy = class extends Server {
|
|
|
970
1040
|
}
|
|
971
1041
|
return map;
|
|
972
1042
|
}
|
|
973
|
-
isCoreRpcMode() {
|
|
974
|
-
return !this.options.rpc || this.options.rpc.mode === "core";
|
|
975
|
-
}
|
|
976
|
-
isJetStreamRpcMode() {
|
|
977
|
-
return this.options.rpc?.mode === "jetstream";
|
|
978
|
-
}
|
|
979
1043
|
};
|
|
980
1044
|
|
|
981
1045
|
// src/server/core-rpc.server.ts
|
|
@@ -985,6 +1049,13 @@ import { headers as natsHeaders2 } from "nats";
|
|
|
985
1049
|
// src/context/rpc.context.ts
|
|
986
1050
|
import { BaseRpcContext } from "@nestjs/microservices";
|
|
987
1051
|
var RpcContext = class extends BaseRpcContext {
|
|
1052
|
+
_shouldRetry = false;
|
|
1053
|
+
_retryDelay;
|
|
1054
|
+
_shouldTerminate = false;
|
|
1055
|
+
_terminateReason;
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// Message accessors
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
988
1059
|
/**
|
|
989
1060
|
* Get the underlying NATS message.
|
|
990
1061
|
*
|
|
@@ -1019,6 +1090,122 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
1019
1090
|
isJetStream() {
|
|
1020
1091
|
return "ack" in this.args[0];
|
|
1021
1092
|
}
|
|
1093
|
+
// ---------------------------------------------------------------------------
|
|
1094
|
+
// JetStream metadata (return undefined for Core NATS messages)
|
|
1095
|
+
// ---------------------------------------------------------------------------
|
|
1096
|
+
/** How many times this message has been delivered. */
|
|
1097
|
+
getDeliveryCount() {
|
|
1098
|
+
return this.asJetStream()?.info.deliveryCount;
|
|
1099
|
+
}
|
|
1100
|
+
/** The JetStream stream this message belongs to. */
|
|
1101
|
+
getStream() {
|
|
1102
|
+
return this.asJetStream()?.info.stream;
|
|
1103
|
+
}
|
|
1104
|
+
/** The stream sequence number. */
|
|
1105
|
+
getSequence() {
|
|
1106
|
+
return this.asJetStream()?.seq;
|
|
1107
|
+
}
|
|
1108
|
+
/** The message timestamp as a `Date` (derived from `info.timestampNanos`). */
|
|
1109
|
+
getTimestamp() {
|
|
1110
|
+
const nanos = this.asJetStream()?.info.timestampNanos;
|
|
1111
|
+
return typeof nanos === "number" ? new Date(nanos / 1e6) : void 0;
|
|
1112
|
+
}
|
|
1113
|
+
/** The name of the service that published this message (from `x-caller-name` header). */
|
|
1114
|
+
getCallerName() {
|
|
1115
|
+
return this.getHeader("x-caller-name" /* CallerName */);
|
|
1116
|
+
}
|
|
1117
|
+
// ---------------------------------------------------------------------------
|
|
1118
|
+
// Handler-controlled settlement
|
|
1119
|
+
// ---------------------------------------------------------------------------
|
|
1120
|
+
/**
|
|
1121
|
+
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1122
|
+
*
|
|
1123
|
+
* Use for business-level retries without throwing errors.
|
|
1124
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1125
|
+
*
|
|
1126
|
+
* @param opts - Optional delay in ms before redelivery.
|
|
1127
|
+
* @throws Error if {@link terminate} was already called.
|
|
1128
|
+
*/
|
|
1129
|
+
retry(opts) {
|
|
1130
|
+
this.assertJetStream("retry");
|
|
1131
|
+
if (this._shouldTerminate) {
|
|
1132
|
+
throw new Error("Cannot retry \u2014 terminate() was already called");
|
|
1133
|
+
}
|
|
1134
|
+
this._shouldRetry = true;
|
|
1135
|
+
this._retryDelay = opts?.delayMs;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Signal the transport to permanently reject (term) this message.
|
|
1139
|
+
*
|
|
1140
|
+
* Use when a message is no longer relevant and should not be retried or sent to DLQ.
|
|
1141
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1142
|
+
*
|
|
1143
|
+
* @param reason - Optional reason for termination (logged by NATS).
|
|
1144
|
+
* @throws Error if {@link retry} was already called.
|
|
1145
|
+
*/
|
|
1146
|
+
terminate(reason) {
|
|
1147
|
+
this.assertJetStream("terminate");
|
|
1148
|
+
if (this._shouldRetry) {
|
|
1149
|
+
throw new Error("Cannot terminate \u2014 retry() was already called");
|
|
1150
|
+
}
|
|
1151
|
+
this._shouldTerminate = true;
|
|
1152
|
+
this._terminateReason = reason;
|
|
1153
|
+
}
|
|
1154
|
+
/** Narrow to JsMsg or return null for Core messages. Used by metadata getters. */
|
|
1155
|
+
asJetStream() {
|
|
1156
|
+
return this.isJetStream() ? this.args[0] : null;
|
|
1157
|
+
}
|
|
1158
|
+
/** Ensure the message is JetStream — settlement actions are not available for Core NATS. */
|
|
1159
|
+
assertJetStream(method) {
|
|
1160
|
+
if (!this.isJetStream()) {
|
|
1161
|
+
throw new Error(`${method}() is only available for JetStream messages`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// ---------------------------------------------------------------------------
|
|
1165
|
+
// Transport-facing state (read by EventRouter)
|
|
1166
|
+
// ---------------------------------------------------------------------------
|
|
1167
|
+
/** @internal */
|
|
1168
|
+
get shouldRetry() {
|
|
1169
|
+
return this._shouldRetry;
|
|
1170
|
+
}
|
|
1171
|
+
/** @internal */
|
|
1172
|
+
get retryDelay() {
|
|
1173
|
+
return this._retryDelay;
|
|
1174
|
+
}
|
|
1175
|
+
/** @internal */
|
|
1176
|
+
get shouldTerminate() {
|
|
1177
|
+
return this._shouldTerminate;
|
|
1178
|
+
}
|
|
1179
|
+
/** @internal */
|
|
1180
|
+
get terminateReason() {
|
|
1181
|
+
return this._terminateReason;
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// src/utils/ack-extension.ts
|
|
1186
|
+
var DEFAULT_ACK_EXTENSION_INTERVAL = 5e3;
|
|
1187
|
+
var MIN_ACK_EXTENSION_INTERVAL = 500;
|
|
1188
|
+
var resolveAckExtensionInterval = (config, ackWaitNanos) => {
|
|
1189
|
+
if (config === false || config === void 0) return null;
|
|
1190
|
+
if (typeof config === "number") {
|
|
1191
|
+
if (!Number.isFinite(config) || config <= 0) return null;
|
|
1192
|
+
return Math.floor(config);
|
|
1193
|
+
}
|
|
1194
|
+
if (!ackWaitNanos) return DEFAULT_ACK_EXTENSION_INTERVAL;
|
|
1195
|
+
const interval = Math.floor(ackWaitNanos / 1e6 / 2);
|
|
1196
|
+
return Math.max(interval, MIN_ACK_EXTENSION_INTERVAL);
|
|
1197
|
+
};
|
|
1198
|
+
var startAckExtensionTimer = (msg, interval) => {
|
|
1199
|
+
if (interval === null || interval <= 0) return null;
|
|
1200
|
+
const timer2 = setInterval(() => {
|
|
1201
|
+
try {
|
|
1202
|
+
msg.working();
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
}, interval);
|
|
1206
|
+
return () => {
|
|
1207
|
+
clearInterval(timer2);
|
|
1208
|
+
};
|
|
1022
1209
|
};
|
|
1023
1210
|
|
|
1024
1211
|
// src/utils/serialize-error.ts
|
|
@@ -1031,15 +1218,20 @@ var serializeError = (err) => {
|
|
|
1031
1218
|
|
|
1032
1219
|
// src/utils/unwrap-result.ts
|
|
1033
1220
|
import { isObservable } from "rxjs";
|
|
1034
|
-
var
|
|
1221
|
+
var RESOLVED_VOID = Promise.resolve(void 0);
|
|
1222
|
+
var RESOLVED_NULL = Promise.resolve(null);
|
|
1223
|
+
var unwrapResult = (result) => {
|
|
1224
|
+
if (result === void 0) return RESOLVED_VOID;
|
|
1225
|
+
if (result === null) return RESOLVED_NULL;
|
|
1035
1226
|
if (isObservable(result)) {
|
|
1036
1227
|
return subscribeToFirst(result);
|
|
1037
1228
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1229
|
+
if (typeof result.then === "function") {
|
|
1230
|
+
return result.then(
|
|
1231
|
+
(resolved) => isObservable(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1232
|
+
);
|
|
1041
1233
|
}
|
|
1042
|
-
return
|
|
1234
|
+
return Promise.resolve(result);
|
|
1043
1235
|
};
|
|
1044
1236
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1045
1237
|
let done = false;
|
|
@@ -1107,13 +1299,17 @@ var CoreRpcServer = class {
|
|
|
1107
1299
|
}
|
|
1108
1300
|
/** Handle an incoming Core NATS request. */
|
|
1109
1301
|
async handleRequest(msg) {
|
|
1302
|
+
if (!msg.reply) {
|
|
1303
|
+
this.logger.warn(`Ignoring fire-and-forget message on RPC subject: ${msg.subject}`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1110
1306
|
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1111
1307
|
if (!handler) {
|
|
1112
1308
|
this.logger.warn(`No handler for Core RPC: ${msg.subject}`);
|
|
1113
1309
|
this.respondWithError(msg, new Error(`No handler for subject: ${msg.subject}`));
|
|
1114
1310
|
return;
|
|
1115
1311
|
}
|
|
1116
|
-
this.eventBus.
|
|
1312
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
1117
1313
|
let data;
|
|
1118
1314
|
try {
|
|
1119
1315
|
data = this.codec.decode(msg.data);
|
|
@@ -1171,14 +1367,14 @@ var StreamProvider = class {
|
|
|
1171
1367
|
getSubjects(kind) {
|
|
1172
1368
|
const name = internalName(this.options.name);
|
|
1173
1369
|
switch (kind) {
|
|
1174
|
-
case "ev"
|
|
1175
|
-
return [`${name}
|
|
1176
|
-
case "cmd"
|
|
1177
|
-
return [`${name}
|
|
1178
|
-
case "broadcast"
|
|
1370
|
+
case "ev" /* Event */:
|
|
1371
|
+
return [`${name}.${"ev" /* Event */}.>`];
|
|
1372
|
+
case "cmd" /* Command */:
|
|
1373
|
+
return [`${name}.${"cmd" /* Command */}.>`];
|
|
1374
|
+
case "broadcast" /* Broadcast */:
|
|
1179
1375
|
return ["broadcast.>"];
|
|
1180
|
-
case "ordered"
|
|
1181
|
-
return [`${name}
|
|
1376
|
+
case "ordered" /* Ordered */:
|
|
1377
|
+
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
1182
1378
|
}
|
|
1183
1379
|
}
|
|
1184
1380
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
@@ -1215,26 +1411,26 @@ var StreamProvider = class {
|
|
|
1215
1411
|
/** Get default config for a stream kind. */
|
|
1216
1412
|
getDefaults(kind) {
|
|
1217
1413
|
switch (kind) {
|
|
1218
|
-
case "ev"
|
|
1414
|
+
case "ev" /* Event */:
|
|
1219
1415
|
return DEFAULT_EVENT_STREAM_CONFIG;
|
|
1220
|
-
case "cmd"
|
|
1416
|
+
case "cmd" /* Command */:
|
|
1221
1417
|
return DEFAULT_COMMAND_STREAM_CONFIG;
|
|
1222
|
-
case "broadcast"
|
|
1418
|
+
case "broadcast" /* Broadcast */:
|
|
1223
1419
|
return DEFAULT_BROADCAST_STREAM_CONFIG;
|
|
1224
|
-
case "ordered"
|
|
1420
|
+
case "ordered" /* Ordered */:
|
|
1225
1421
|
return DEFAULT_ORDERED_STREAM_CONFIG;
|
|
1226
1422
|
}
|
|
1227
1423
|
}
|
|
1228
1424
|
/** Get user-provided overrides for a stream kind. */
|
|
1229
1425
|
getOverrides(kind) {
|
|
1230
1426
|
switch (kind) {
|
|
1231
|
-
case "ev"
|
|
1427
|
+
case "ev" /* Event */:
|
|
1232
1428
|
return this.options.events?.stream ?? {};
|
|
1233
|
-
case "cmd"
|
|
1429
|
+
case "cmd" /* Command */:
|
|
1234
1430
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1235
|
-
case "broadcast"
|
|
1431
|
+
case "broadcast" /* Broadcast */:
|
|
1236
1432
|
return this.options.broadcast?.stream ?? {};
|
|
1237
|
-
case "ordered"
|
|
1433
|
+
case "ordered" /* Ordered */:
|
|
1238
1434
|
return this.options.ordered?.stream ?? {};
|
|
1239
1435
|
}
|
|
1240
1436
|
}
|
|
@@ -1297,7 +1493,7 @@ var ConsumerProvider = class {
|
|
|
1297
1493
|
const serviceName = internalName(this.options.name);
|
|
1298
1494
|
const defaults = this.getDefaults(kind);
|
|
1299
1495
|
const overrides = this.getOverrides(kind);
|
|
1300
|
-
if (kind === "broadcast") {
|
|
1496
|
+
if (kind === "broadcast" /* Broadcast */) {
|
|
1301
1497
|
const broadcastPatterns = this.patternRegistry.getBroadcastPatterns();
|
|
1302
1498
|
if (broadcastPatterns.length === 0) {
|
|
1303
1499
|
throw new Error("Broadcast consumer requested but no broadcast patterns are registered");
|
|
@@ -1319,7 +1515,10 @@ var ConsumerProvider = class {
|
|
|
1319
1515
|
filter_subjects: broadcastPatterns
|
|
1320
1516
|
};
|
|
1321
1517
|
}
|
|
1322
|
-
|
|
1518
|
+
if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
|
|
1519
|
+
throw new Error(`Unexpected durable consumer kind: ${kind}`);
|
|
1520
|
+
}
|
|
1521
|
+
const filter_subject = `${serviceName}.${kind}.>`;
|
|
1323
1522
|
return {
|
|
1324
1523
|
...defaults,
|
|
1325
1524
|
...overrides,
|
|
@@ -1331,26 +1530,26 @@ var ConsumerProvider = class {
|
|
|
1331
1530
|
/** Get default config for a consumer kind. */
|
|
1332
1531
|
getDefaults(kind) {
|
|
1333
1532
|
switch (kind) {
|
|
1334
|
-
case "ev"
|
|
1533
|
+
case "ev" /* Event */:
|
|
1335
1534
|
return DEFAULT_EVENT_CONSUMER_CONFIG;
|
|
1336
|
-
case "cmd"
|
|
1535
|
+
case "cmd" /* Command */:
|
|
1337
1536
|
return DEFAULT_COMMAND_CONSUMER_CONFIG;
|
|
1338
|
-
case "broadcast"
|
|
1537
|
+
case "broadcast" /* Broadcast */:
|
|
1339
1538
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1340
|
-
case "ordered"
|
|
1539
|
+
case "ordered" /* Ordered */:
|
|
1341
1540
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1342
1541
|
}
|
|
1343
1542
|
}
|
|
1344
1543
|
/** Get user-provided overrides for a consumer kind. */
|
|
1345
1544
|
getOverrides(kind) {
|
|
1346
1545
|
switch (kind) {
|
|
1347
|
-
case "ev"
|
|
1546
|
+
case "ev" /* Event */:
|
|
1348
1547
|
return this.options.events?.consumer ?? {};
|
|
1349
|
-
case "cmd"
|
|
1548
|
+
case "cmd" /* Command */:
|
|
1350
1549
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
1351
|
-
case "broadcast"
|
|
1550
|
+
case "broadcast" /* Broadcast */:
|
|
1352
1551
|
return this.options.broadcast?.consumer ?? {};
|
|
1353
|
-
case "ordered"
|
|
1552
|
+
case "ordered" /* Ordered */:
|
|
1354
1553
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1355
1554
|
}
|
|
1356
1555
|
}
|
|
@@ -1358,7 +1557,10 @@ var ConsumerProvider = class {
|
|
|
1358
1557
|
|
|
1359
1558
|
// src/server/infrastructure/message.provider.ts
|
|
1360
1559
|
import { Logger as Logger7 } from "@nestjs/common";
|
|
1361
|
-
import {
|
|
1560
|
+
import {
|
|
1561
|
+
ConsumerEvents,
|
|
1562
|
+
DeliverPolicy as DeliverPolicy2
|
|
1563
|
+
} from "nats";
|
|
1362
1564
|
import {
|
|
1363
1565
|
catchError,
|
|
1364
1566
|
defer as defer2,
|
|
@@ -1371,9 +1573,10 @@ import {
|
|
|
1371
1573
|
timer
|
|
1372
1574
|
} from "rxjs";
|
|
1373
1575
|
var MessageProvider = class {
|
|
1374
|
-
constructor(connection, eventBus) {
|
|
1576
|
+
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
|
|
1375
1577
|
this.connection = connection;
|
|
1376
1578
|
this.eventBus = eventBus;
|
|
1579
|
+
this.consumeOptionsMap = consumeOptionsMap;
|
|
1377
1580
|
}
|
|
1378
1581
|
logger = new Logger7("Jetstream:Message");
|
|
1379
1582
|
activeIterators = /* @__PURE__ */ new Set();
|
|
@@ -1423,6 +1626,7 @@ var MessageProvider = class {
|
|
|
1423
1626
|
*
|
|
1424
1627
|
* @param streamName - JetStream stream to consume from.
|
|
1425
1628
|
* @param filterSubjects - NATS subjects to filter on.
|
|
1629
|
+
* @param orderedConfig - Optional overrides for ordered consumer options.
|
|
1426
1630
|
*/
|
|
1427
1631
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1428
1632
|
const consumerOpts = { filterSubjects };
|
|
@@ -1448,6 +1652,11 @@ var MessageProvider = class {
|
|
|
1448
1652
|
}
|
|
1449
1653
|
/** Stop all consumer flows and reinitialize subjects for potential restart. */
|
|
1450
1654
|
destroy() {
|
|
1655
|
+
if (this.orderedReadyReject) {
|
|
1656
|
+
this.orderedReadyReject(new Error("Destroyed before ordered consumer connected"));
|
|
1657
|
+
this.orderedReadyResolve = null;
|
|
1658
|
+
this.orderedReadyReject = null;
|
|
1659
|
+
}
|
|
1451
1660
|
this.destroy$.next();
|
|
1452
1661
|
this.destroy$.complete();
|
|
1453
1662
|
for (const messages of this.activeIterators) {
|
|
@@ -1467,42 +1676,17 @@ var MessageProvider = class {
|
|
|
1467
1676
|
/** Create a self-healing consumer flow for a specific kind. */
|
|
1468
1677
|
createFlow(kind, info) {
|
|
1469
1678
|
const target$ = this.getTargetSubject(kind);
|
|
1470
|
-
|
|
1471
|
-
let lastRunFailed = false;
|
|
1472
|
-
return defer2(() => this.consumeOnce(info, target$)).pipe(
|
|
1473
|
-
tap(() => {
|
|
1474
|
-
lastRunFailed = false;
|
|
1475
|
-
}),
|
|
1476
|
-
catchError((err) => {
|
|
1477
|
-
consecutiveFailures++;
|
|
1478
|
-
lastRunFailed = true;
|
|
1479
|
-
this.logger.error(`Consumer ${info.name} error, will restart:`, err);
|
|
1480
|
-
this.eventBus.emit(
|
|
1481
|
-
"error" /* Error */,
|
|
1482
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
1483
|
-
"message-provider"
|
|
1484
|
-
);
|
|
1485
|
-
return EMPTY;
|
|
1486
|
-
}),
|
|
1487
|
-
repeat({
|
|
1488
|
-
delay: () => {
|
|
1489
|
-
if (!lastRunFailed) {
|
|
1490
|
-
consecutiveFailures = 0;
|
|
1491
|
-
}
|
|
1492
|
-
const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
|
|
1493
|
-
this.logger.warn(`Consumer ${info.name} stream ended, restarting in ${delay}ms...`);
|
|
1494
|
-
return timer(delay);
|
|
1495
|
-
}
|
|
1496
|
-
}),
|
|
1497
|
-
takeUntil(this.destroy$)
|
|
1498
|
-
);
|
|
1679
|
+
return this.createSelfHealingFlow(() => this.consumeOnce(kind, info, target$), info.name);
|
|
1499
1680
|
}
|
|
1500
1681
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
1501
|
-
async consumeOnce(info, target$) {
|
|
1502
|
-
const js =
|
|
1682
|
+
async consumeOnce(kind, info, target$) {
|
|
1683
|
+
const js = this.connection.getJetStreamClient();
|
|
1503
1684
|
const consumer = await js.consumers.get(info.stream_name, info.name);
|
|
1504
|
-
const
|
|
1685
|
+
const defaults = { idle_heartbeat: 5e3 };
|
|
1686
|
+
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
1687
|
+
const messages = await consumer.consume({ ...defaults, ...userOptions });
|
|
1505
1688
|
this.activeIterators.add(messages);
|
|
1689
|
+
this.monitorConsumerHealth(messages, info.name);
|
|
1506
1690
|
try {
|
|
1507
1691
|
for await (const msg of messages) {
|
|
1508
1692
|
target$.next(msg);
|
|
@@ -1514,47 +1698,73 @@ var MessageProvider = class {
|
|
|
1514
1698
|
/** Get the target subject for a consumer kind. */
|
|
1515
1699
|
getTargetSubject(kind) {
|
|
1516
1700
|
switch (kind) {
|
|
1517
|
-
case "ev"
|
|
1701
|
+
case "ev" /* Event */:
|
|
1518
1702
|
return this.eventMessages$;
|
|
1519
|
-
case "cmd"
|
|
1703
|
+
case "cmd" /* Command */:
|
|
1520
1704
|
return this.commandMessages$;
|
|
1521
|
-
case "broadcast"
|
|
1705
|
+
case "broadcast" /* Broadcast */:
|
|
1522
1706
|
return this.broadcastMessages$;
|
|
1523
|
-
case "ordered"
|
|
1707
|
+
case "ordered" /* Ordered */:
|
|
1524
1708
|
return this.orderedMessages$;
|
|
1709
|
+
default: {
|
|
1710
|
+
const _exhaustive = kind;
|
|
1711
|
+
throw new Error(`Unknown stream kind: ${_exhaustive}`);
|
|
1712
|
+
}
|
|
1525
1713
|
}
|
|
1526
1714
|
}
|
|
1715
|
+
/** Monitor heartbeats and restart the consumer iterator on prolonged silence. */
|
|
1716
|
+
monitorConsumerHealth(messages, name) {
|
|
1717
|
+
(async () => {
|
|
1718
|
+
for await (const status of await messages.status()) {
|
|
1719
|
+
if (status.type === ConsumerEvents.HeartbeatsMissed && status.data >= 2) {
|
|
1720
|
+
this.logger.warn(`Consumer ${name}: ${status.data} heartbeats missed, restarting`);
|
|
1721
|
+
messages.stop();
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
})().catch((err) => {
|
|
1726
|
+
if (err) {
|
|
1727
|
+
this.logger.debug(`Consumer ${name} health monitor ended:`, err);
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1527
1731
|
/** Create a self-healing ordered consumer flow. */
|
|
1528
1732
|
createOrderedFlow(streamName2, consumerOpts) {
|
|
1733
|
+
return this.createSelfHealingFlow(
|
|
1734
|
+
() => this.consumeOrderedOnce(streamName2, consumerOpts),
|
|
1735
|
+
"ordered" /* Ordered */,
|
|
1736
|
+
(err) => {
|
|
1737
|
+
if (this.orderedReadyReject) {
|
|
1738
|
+
this.orderedReadyReject(err);
|
|
1739
|
+
this.orderedReadyReject = null;
|
|
1740
|
+
this.orderedReadyResolve = null;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
/** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
|
|
1746
|
+
createSelfHealingFlow(source, label, onFirstError) {
|
|
1529
1747
|
let consecutiveFailures = 0;
|
|
1530
|
-
|
|
1531
|
-
return defer2(() => this.consumeOrderedOnce(streamName2, consumerOpts)).pipe(
|
|
1748
|
+
return defer2(source).pipe(
|
|
1532
1749
|
tap(() => {
|
|
1533
|
-
|
|
1750
|
+
consecutiveFailures = 0;
|
|
1534
1751
|
}),
|
|
1535
1752
|
catchError((err) => {
|
|
1536
1753
|
consecutiveFailures++;
|
|
1537
|
-
|
|
1538
|
-
this.logger.error("Ordered consumer error, will restart:", err);
|
|
1754
|
+
this.logger.error(`Consumer ${label} error, will restart:`, err);
|
|
1539
1755
|
this.eventBus.emit(
|
|
1540
1756
|
"error" /* Error */,
|
|
1541
1757
|
err instanceof Error ? err : new Error(String(err)),
|
|
1542
1758
|
"message-provider"
|
|
1543
1759
|
);
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
this.orderedReadyReject = null;
|
|
1547
|
-
this.orderedReadyResolve = null;
|
|
1548
|
-
}
|
|
1760
|
+
onFirstError?.(err);
|
|
1761
|
+
onFirstError = void 0;
|
|
1549
1762
|
return EMPTY;
|
|
1550
1763
|
}),
|
|
1551
1764
|
repeat({
|
|
1552
1765
|
delay: () => {
|
|
1553
|
-
if (!lastRunFailed) {
|
|
1554
|
-
consecutiveFailures = 0;
|
|
1555
|
-
}
|
|
1556
1766
|
const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
|
|
1557
|
-
this.logger.warn(`
|
|
1767
|
+
this.logger.warn(`Consumer ${label} stream ended, restarting in ${delay}ms...`);
|
|
1558
1768
|
return timer(delay);
|
|
1559
1769
|
}
|
|
1560
1770
|
})
|
|
@@ -1562,7 +1772,7 @@ var MessageProvider = class {
|
|
|
1562
1772
|
}
|
|
1563
1773
|
/** Single iteration: create ordered consumer -> iterate messages. */
|
|
1564
1774
|
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
1565
|
-
const js =
|
|
1775
|
+
const js = this.connection.getJetStreamClient();
|
|
1566
1776
|
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
1567
1777
|
const messages = await consumer.consume();
|
|
1568
1778
|
if (this.orderedReadyResolve) {
|
|
@@ -1583,12 +1793,24 @@ var MessageProvider = class {
|
|
|
1583
1793
|
|
|
1584
1794
|
// src/server/routing/pattern-registry.ts
|
|
1585
1795
|
import { Logger as Logger8 } from "@nestjs/common";
|
|
1796
|
+
var HANDLER_LABELS = {
|
|
1797
|
+
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
1798
|
+
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
1799
|
+
["ev" /* Event */]: "event" /* Event */,
|
|
1800
|
+
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
1801
|
+
};
|
|
1586
1802
|
var PatternRegistry = class {
|
|
1587
1803
|
constructor(options) {
|
|
1588
1804
|
this.options = options;
|
|
1589
1805
|
}
|
|
1590
1806
|
logger = new Logger8("Jetstream:PatternRegistry");
|
|
1591
1807
|
registry = /* @__PURE__ */ new Map();
|
|
1808
|
+
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1809
|
+
cachedPatterns = null;
|
|
1810
|
+
_hasEvents = false;
|
|
1811
|
+
_hasCommands = false;
|
|
1812
|
+
_hasBroadcasts = false;
|
|
1813
|
+
_hasOrdered = false;
|
|
1592
1814
|
/**
|
|
1593
1815
|
* Register all handlers from the NestJS strategy.
|
|
1594
1816
|
*
|
|
@@ -1606,16 +1828,12 @@ var PatternRegistry = class {
|
|
|
1606
1828
|
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
1607
1829
|
);
|
|
1608
1830
|
}
|
|
1609
|
-
let
|
|
1610
|
-
if (isBroadcast)
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
fullSubject = buildSubject(serviceName, "ev", pattern);
|
|
1616
|
-
} else {
|
|
1617
|
-
fullSubject = buildSubject(serviceName, "cmd", pattern);
|
|
1618
|
-
}
|
|
1831
|
+
let kind;
|
|
1832
|
+
if (isBroadcast) kind = "broadcast" /* Broadcast */;
|
|
1833
|
+
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
1834
|
+
else if (isEvent) kind = "ev" /* Event */;
|
|
1835
|
+
else kind = "cmd" /* Command */;
|
|
1836
|
+
const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
|
|
1619
1837
|
this.registry.set(fullSubject, {
|
|
1620
1838
|
handler,
|
|
1621
1839
|
pattern,
|
|
@@ -1623,18 +1841,13 @@ var PatternRegistry = class {
|
|
|
1623
1841
|
isBroadcast,
|
|
1624
1842
|
isOrdered
|
|
1625
1843
|
});
|
|
1626
|
-
|
|
1627
|
-
if (isBroadcast) {
|
|
1628
|
-
kind = "broadcast";
|
|
1629
|
-
} else if (isOrdered) {
|
|
1630
|
-
kind = "ordered";
|
|
1631
|
-
} else if (isEvent) {
|
|
1632
|
-
kind = "event";
|
|
1633
|
-
} else {
|
|
1634
|
-
kind = "rpc";
|
|
1635
|
-
}
|
|
1636
|
-
this.logger.debug(`Registered ${kind}: ${pattern} -> ${fullSubject}`);
|
|
1844
|
+
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1637
1845
|
}
|
|
1846
|
+
this.cachedPatterns = this.buildPatternsByKind();
|
|
1847
|
+
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
1848
|
+
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1849
|
+
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1850
|
+
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
1638
1851
|
this.logSummary();
|
|
1639
1852
|
}
|
|
1640
1853
|
/** Find handler for a full NATS subject. */
|
|
@@ -1643,33 +1856,53 @@ var PatternRegistry = class {
|
|
|
1643
1856
|
}
|
|
1644
1857
|
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
1645
1858
|
getBroadcastPatterns() {
|
|
1646
|
-
return
|
|
1859
|
+
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
1647
1860
|
}
|
|
1648
|
-
/** Check if any broadcast handlers are registered. */
|
|
1649
1861
|
hasBroadcastHandlers() {
|
|
1650
|
-
return
|
|
1862
|
+
return this._hasBroadcasts;
|
|
1651
1863
|
}
|
|
1652
|
-
/** Check if any RPC (command) handlers are registered. */
|
|
1653
1864
|
hasRpcHandlers() {
|
|
1654
|
-
return
|
|
1655
|
-
(r) => !r.isEvent && !r.isBroadcast && !r.isOrdered
|
|
1656
|
-
);
|
|
1865
|
+
return this._hasCommands;
|
|
1657
1866
|
}
|
|
1658
|
-
/** Check if any workqueue event handlers are registered. */
|
|
1659
1867
|
hasEventHandlers() {
|
|
1660
|
-
return
|
|
1868
|
+
return this._hasEvents;
|
|
1661
1869
|
}
|
|
1662
|
-
/** Check if any ordered event handlers are registered. */
|
|
1663
1870
|
hasOrderedHandlers() {
|
|
1664
|
-
return
|
|
1871
|
+
return this._hasOrdered;
|
|
1665
1872
|
}
|
|
1666
1873
|
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1667
1874
|
getOrderedSubjects() {
|
|
1668
|
-
|
|
1669
|
-
|
|
1875
|
+
return this.getPatternsByKind().ordered.map(
|
|
1876
|
+
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
1877
|
+
);
|
|
1670
1878
|
}
|
|
1671
|
-
/** Get patterns grouped by kind. */
|
|
1879
|
+
/** Get patterns grouped by kind (cached after registration). */
|
|
1672
1880
|
getPatternsByKind() {
|
|
1881
|
+
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
1882
|
+
return {
|
|
1883
|
+
events: [...patterns.events],
|
|
1884
|
+
commands: [...patterns.commands],
|
|
1885
|
+
broadcasts: [...patterns.broadcasts],
|
|
1886
|
+
ordered: [...patterns.ordered]
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
1890
|
+
normalizeSubject(subject) {
|
|
1891
|
+
const name = internalName(this.options.name);
|
|
1892
|
+
const prefixes = [
|
|
1893
|
+
`${name}.${"cmd" /* Command */}.`,
|
|
1894
|
+
`${name}.${"ev" /* Event */}.`,
|
|
1895
|
+
`${name}.${"ordered" /* Ordered */}.`,
|
|
1896
|
+
`${"broadcast" /* Broadcast */}.`
|
|
1897
|
+
];
|
|
1898
|
+
for (const prefix of prefixes) {
|
|
1899
|
+
if (subject.startsWith(prefix)) {
|
|
1900
|
+
return subject.slice(prefix.length);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return subject;
|
|
1904
|
+
}
|
|
1905
|
+
buildPatternsByKind() {
|
|
1673
1906
|
const events = [];
|
|
1674
1907
|
const commands = [];
|
|
1675
1908
|
const broadcasts = [];
|
|
@@ -1682,18 +1915,6 @@ var PatternRegistry = class {
|
|
|
1682
1915
|
}
|
|
1683
1916
|
return { events, commands, broadcasts, ordered };
|
|
1684
1917
|
}
|
|
1685
|
-
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
1686
|
-
normalizeSubject(subject) {
|
|
1687
|
-
const name = internalName(this.options.name);
|
|
1688
|
-
const prefixes = [`${name}.cmd.`, `${name}.ev.`, `${name}.ordered.`, "broadcast."];
|
|
1689
|
-
for (const prefix of prefixes) {
|
|
1690
|
-
if (subject.startsWith(prefix)) {
|
|
1691
|
-
return subject.slice(prefix.length);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
return subject;
|
|
1695
|
-
}
|
|
1696
|
-
/** Log a summary of all registered handlers. */
|
|
1697
1918
|
logSummary() {
|
|
1698
1919
|
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
1699
1920
|
const parts = [
|
|
@@ -1710,21 +1931,16 @@ var PatternRegistry = class {
|
|
|
1710
1931
|
|
|
1711
1932
|
// src/server/routing/event.router.ts
|
|
1712
1933
|
import { Logger as Logger9 } from "@nestjs/common";
|
|
1713
|
-
import {
|
|
1714
|
-
catchError as catchError2,
|
|
1715
|
-
concatMap,
|
|
1716
|
-
defer as defer3,
|
|
1717
|
-
EMPTY as EMPTY2,
|
|
1718
|
-
from as from2,
|
|
1719
|
-
mergeMap
|
|
1720
|
-
} from "rxjs";
|
|
1934
|
+
import { concatMap, from as from2, mergeMap } from "rxjs";
|
|
1721
1935
|
var EventRouter = class {
|
|
1722
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig) {
|
|
1936
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
1723
1937
|
this.messageProvider = messageProvider;
|
|
1724
1938
|
this.patternRegistry = patternRegistry;
|
|
1725
1939
|
this.codec = codec;
|
|
1726
1940
|
this.eventBus = eventBus;
|
|
1727
1941
|
this.deadLetterConfig = deadLetterConfig;
|
|
1942
|
+
this.processingConfig = processingConfig;
|
|
1943
|
+
this.ackWaitMap = ackWaitMap;
|
|
1728
1944
|
}
|
|
1729
1945
|
logger = new Logger9("Jetstream:EventRouter");
|
|
1730
1946
|
subscriptions = [];
|
|
@@ -1738,10 +1954,10 @@ var EventRouter = class {
|
|
|
1738
1954
|
}
|
|
1739
1955
|
/** Start routing event, broadcast, and ordered messages to handlers. */
|
|
1740
1956
|
start() {
|
|
1741
|
-
this.subscribeToStream(this.messageProvider.events$, "
|
|
1742
|
-
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast");
|
|
1957
|
+
this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
|
|
1958
|
+
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
|
|
1743
1959
|
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
1744
|
-
this.subscribeToStream(this.messageProvider.ordered$, "ordered"
|
|
1960
|
+
this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
|
|
1745
1961
|
}
|
|
1746
1962
|
}
|
|
1747
1963
|
/** Stop routing and unsubscribe from all streams. */
|
|
@@ -1752,41 +1968,88 @@ var EventRouter = class {
|
|
|
1752
1968
|
this.subscriptions.length = 0;
|
|
1753
1969
|
}
|
|
1754
1970
|
/** Subscribe to a message stream and route each message. */
|
|
1755
|
-
subscribeToStream(stream$,
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1971
|
+
subscribeToStream(stream$, kind) {
|
|
1972
|
+
const isOrdered = kind === "ordered" /* Ordered */;
|
|
1973
|
+
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
1974
|
+
const concurrency = this.getConcurrency(kind);
|
|
1975
|
+
const route = (msg) => from2(
|
|
1976
|
+
isOrdered ? this.handleOrderedSafe(msg) : this.handleSafe(msg, ackExtensionInterval, kind)
|
|
1761
1977
|
);
|
|
1762
|
-
const subscription = stream$.pipe(isOrdered ? concatMap(route) : mergeMap(route)).subscribe();
|
|
1978
|
+
const subscription = stream$.pipe(isOrdered ? concatMap(route) : mergeMap(route, concurrency)).subscribe();
|
|
1763
1979
|
this.subscriptions.push(subscription);
|
|
1764
1980
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1981
|
+
getConcurrency(kind) {
|
|
1982
|
+
if (kind === "ev" /* Event */) return this.processingConfig?.events?.concurrency;
|
|
1983
|
+
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.concurrency;
|
|
1984
|
+
return void 0;
|
|
1985
|
+
}
|
|
1986
|
+
getAckExtensionConfig(kind) {
|
|
1987
|
+
if (kind === "ev" /* Event */) return this.processingConfig?.events?.ackExtension;
|
|
1988
|
+
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
1989
|
+
return void 0;
|
|
1990
|
+
}
|
|
1991
|
+
/** Handle a single event message with error isolation. */
|
|
1992
|
+
async handleSafe(msg, ackExtensionInterval, kind) {
|
|
1993
|
+
try {
|
|
1994
|
+
const resolved = this.decodeMessage(msg);
|
|
1995
|
+
if (!resolved) return;
|
|
1996
|
+
await this.executeHandler(
|
|
1997
|
+
resolved.handler,
|
|
1998
|
+
resolved.data,
|
|
1999
|
+
resolved.ctx,
|
|
2000
|
+
msg,
|
|
2001
|
+
ackExtensionInterval
|
|
2002
|
+
);
|
|
2003
|
+
} catch (err) {
|
|
2004
|
+
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
/** Handle an ordered message with error isolation. */
|
|
2008
|
+
async handleOrderedSafe(msg) {
|
|
2009
|
+
try {
|
|
2010
|
+
const resolved = this.decodeMessage(msg, true);
|
|
2011
|
+
if (!resolved) return;
|
|
2012
|
+
await unwrapResult(resolved.handler(resolved.data, resolved.ctx));
|
|
2013
|
+
if (resolved.ctx.shouldRetry || resolved.ctx.shouldTerminate) {
|
|
2014
|
+
this.logger.warn(
|
|
2015
|
+
`retry()/terminate() ignored for ordered message ${msg.subject} \u2014 ordered consumers auto-acknowledge`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
2023
|
+
decodeMessage(msg, isOrdered = false) {
|
|
1767
2024
|
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1768
2025
|
if (!handler) {
|
|
1769
|
-
msg.term(`No handler for event: ${msg.subject}`);
|
|
1770
|
-
this.logger.error(`No handler for
|
|
1771
|
-
return
|
|
2026
|
+
if (!isOrdered) msg.term(`No handler for event: ${msg.subject}`);
|
|
2027
|
+
this.logger.error(`No handler for subject: ${msg.subject}`);
|
|
2028
|
+
return null;
|
|
1772
2029
|
}
|
|
1773
2030
|
let data;
|
|
1774
2031
|
try {
|
|
1775
2032
|
data = this.codec.decode(msg.data);
|
|
1776
2033
|
} catch (err) {
|
|
1777
|
-
msg.term("Decode error");
|
|
2034
|
+
if (!isOrdered) msg.term("Decode error");
|
|
1778
2035
|
this.logger.error(`Decode error for ${msg.subject}:`, err);
|
|
1779
|
-
return
|
|
2036
|
+
return null;
|
|
1780
2037
|
}
|
|
1781
|
-
this.eventBus.
|
|
1782
|
-
|
|
1783
|
-
return from2(this.executeHandler(handler, data, ctx, msg));
|
|
2038
|
+
this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
|
|
2039
|
+
return { handler, data, ctx: new RpcContext([msg]) };
|
|
1784
2040
|
}
|
|
1785
2041
|
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
1786
|
-
async executeHandler(handler, data, ctx, msg) {
|
|
2042
|
+
async executeHandler(handler, data, ctx, msg, ackExtensionInterval) {
|
|
2043
|
+
const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
|
|
1787
2044
|
try {
|
|
1788
2045
|
await unwrapResult(handler(data, ctx));
|
|
1789
|
-
|
|
2046
|
+
if (ctx.shouldTerminate) {
|
|
2047
|
+
msg.term(ctx.terminateReason);
|
|
2048
|
+
} else if (ctx.shouldRetry) {
|
|
2049
|
+
msg.nak(ctx.retryDelay);
|
|
2050
|
+
} else {
|
|
2051
|
+
msg.ack();
|
|
2052
|
+
}
|
|
1790
2053
|
} catch (err) {
|
|
1791
2054
|
this.logger.error(`Event handler error (${msg.subject}):`, err);
|
|
1792
2055
|
if (this.isDeadLetter(msg)) {
|
|
@@ -1794,30 +2057,10 @@ var EventRouter = class {
|
|
|
1794
2057
|
} else {
|
|
1795
2058
|
msg.nak();
|
|
1796
2059
|
}
|
|
2060
|
+
} finally {
|
|
2061
|
+
stopAckExtension?.();
|
|
1797
2062
|
}
|
|
1798
2063
|
}
|
|
1799
|
-
/** Handle an ordered message: decode -> execute handler -> no ack/nak. */
|
|
1800
|
-
handleOrdered(msg) {
|
|
1801
|
-
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1802
|
-
if (!handler) {
|
|
1803
|
-
this.logger.error(`No handler for ordered subject: ${msg.subject}`);
|
|
1804
|
-
return EMPTY2;
|
|
1805
|
-
}
|
|
1806
|
-
let data;
|
|
1807
|
-
try {
|
|
1808
|
-
data = this.codec.decode(msg.data);
|
|
1809
|
-
} catch (err) {
|
|
1810
|
-
this.logger.error(`Decode error for ordered ${msg.subject}:`, err);
|
|
1811
|
-
return EMPTY2;
|
|
1812
|
-
}
|
|
1813
|
-
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "event");
|
|
1814
|
-
const ctx = new RpcContext([msg]);
|
|
1815
|
-
return from2(
|
|
1816
|
-
unwrapResult(handler(data, ctx)).catch((err) => {
|
|
1817
|
-
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
1818
|
-
})
|
|
1819
|
-
);
|
|
1820
|
-
}
|
|
1821
2064
|
/** Check if the message has exhausted all delivery attempts. */
|
|
1822
2065
|
isDeadLetter(msg) {
|
|
1823
2066
|
if (!this.deadLetterConfig) return false;
|
|
@@ -1838,7 +2081,10 @@ var EventRouter = class {
|
|
|
1838
2081
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
1839
2082
|
};
|
|
1840
2083
|
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
1841
|
-
if (!this.deadLetterConfig)
|
|
2084
|
+
if (!this.deadLetterConfig) {
|
|
2085
|
+
msg.term("Dead letter config unavailable");
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
1842
2088
|
try {
|
|
1843
2089
|
await this.deadLetterConfig.onDeadLetter(info);
|
|
1844
2090
|
msg.term("Dead letter processed");
|
|
@@ -1852,73 +2098,85 @@ var EventRouter = class {
|
|
|
1852
2098
|
// src/server/routing/rpc.router.ts
|
|
1853
2099
|
import { Logger as Logger10 } from "@nestjs/common";
|
|
1854
2100
|
import { headers } from "nats";
|
|
1855
|
-
import {
|
|
2101
|
+
import { from as from3, mergeMap as mergeMap2 } from "rxjs";
|
|
1856
2102
|
var RpcRouter = class {
|
|
1857
|
-
constructor(messageProvider, patternRegistry, connection, codec, eventBus,
|
|
2103
|
+
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
1858
2104
|
this.messageProvider = messageProvider;
|
|
1859
2105
|
this.patternRegistry = patternRegistry;
|
|
1860
2106
|
this.connection = connection;
|
|
1861
2107
|
this.codec = codec;
|
|
1862
2108
|
this.eventBus = eventBus;
|
|
1863
|
-
this.
|
|
2109
|
+
this.rpcOptions = rpcOptions;
|
|
2110
|
+
this.ackWaitMap = ackWaitMap;
|
|
2111
|
+
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
2112
|
+
this.concurrency = rpcOptions?.concurrency;
|
|
1864
2113
|
}
|
|
1865
2114
|
logger = new Logger10("Jetstream:RpcRouter");
|
|
1866
2115
|
timeout;
|
|
2116
|
+
concurrency;
|
|
2117
|
+
resolvedAckExtensionInterval;
|
|
1867
2118
|
subscription = null;
|
|
2119
|
+
cachedNc = null;
|
|
2120
|
+
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
2121
|
+
get ackExtensionInterval() {
|
|
2122
|
+
if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
|
|
2123
|
+
this.resolvedAckExtensionInterval = resolveAckExtensionInterval(
|
|
2124
|
+
this.rpcOptions?.ackExtension,
|
|
2125
|
+
this.ackWaitMap?.get("cmd" /* Command */)
|
|
2126
|
+
);
|
|
2127
|
+
return this.resolvedAckExtensionInterval;
|
|
2128
|
+
}
|
|
1868
2129
|
/** Start routing command messages to handlers. */
|
|
1869
|
-
start() {
|
|
1870
|
-
this.
|
|
1871
|
-
|
|
1872
|
-
(msg) => defer4(() => this.handle(msg)).pipe(
|
|
1873
|
-
catchError3((err) => {
|
|
1874
|
-
this.logger.error("Unexpected error in RPC router", err);
|
|
1875
|
-
return EMPTY3;
|
|
1876
|
-
})
|
|
1877
|
-
)
|
|
1878
|
-
)
|
|
1879
|
-
).subscribe();
|
|
2130
|
+
async start() {
|
|
2131
|
+
this.cachedNc = await this.connection.getConnection();
|
|
2132
|
+
this.subscription = this.messageProvider.commands$.pipe(mergeMap2((msg) => from3(this.handleSafe(msg)), this.concurrency)).subscribe();
|
|
1880
2133
|
}
|
|
1881
2134
|
/** Stop routing and unsubscribe. */
|
|
1882
2135
|
destroy() {
|
|
1883
2136
|
this.subscription?.unsubscribe();
|
|
1884
2137
|
this.subscription = null;
|
|
1885
2138
|
}
|
|
1886
|
-
/** Handle a single RPC command message. */
|
|
1887
|
-
|
|
1888
|
-
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1889
|
-
if (!handler) {
|
|
1890
|
-
msg.term(`No handler for RPC: ${msg.subject}`);
|
|
1891
|
-
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
1892
|
-
return EMPTY3;
|
|
1893
|
-
}
|
|
1894
|
-
const replyTo = msg.headers?.get("x-reply-to" /* ReplyTo */);
|
|
1895
|
-
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
1896
|
-
if (!replyTo || !correlationId) {
|
|
1897
|
-
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
1898
|
-
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
1899
|
-
return EMPTY3;
|
|
1900
|
-
}
|
|
1901
|
-
let data;
|
|
2139
|
+
/** Handle a single RPC command message with error isolation. */
|
|
2140
|
+
async handleSafe(msg) {
|
|
1902
2141
|
try {
|
|
1903
|
-
|
|
2142
|
+
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
2143
|
+
if (!handler) {
|
|
2144
|
+
msg.term(`No handler for RPC: ${msg.subject}`);
|
|
2145
|
+
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const { headers: msgHeaders } = msg;
|
|
2149
|
+
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
2150
|
+
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
2151
|
+
if (!replyTo || !correlationId) {
|
|
2152
|
+
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2153
|
+
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
let data;
|
|
2157
|
+
try {
|
|
2158
|
+
data = this.codec.decode(msg.data);
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
msg.term("Decode error");
|
|
2161
|
+
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
2165
|
+
await this.executeHandler(handler, data, msg, replyTo, correlationId);
|
|
1904
2166
|
} catch (err) {
|
|
1905
|
-
|
|
1906
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
1907
|
-
return EMPTY3;
|
|
2167
|
+
this.logger.error("Unexpected error in RPC router", err);
|
|
1908
2168
|
}
|
|
1909
|
-
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "rpc");
|
|
1910
|
-
return from3(this.executeHandler(handler, data, msg, replyTo, correlationId));
|
|
1911
2169
|
}
|
|
1912
2170
|
/** Execute handler, publish response, settle message. */
|
|
1913
2171
|
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
1914
|
-
const nc = await this.connection.getConnection();
|
|
2172
|
+
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
1915
2173
|
const ctx = new RpcContext([msg]);
|
|
1916
|
-
const hdrs = headers();
|
|
1917
|
-
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
1918
2174
|
let settled = false;
|
|
2175
|
+
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
1919
2176
|
const timeoutId = setTimeout(() => {
|
|
1920
2177
|
if (settled) return;
|
|
1921
2178
|
settled = true;
|
|
2179
|
+
stopAckExtension?.();
|
|
1922
2180
|
this.logger.error(`RPC timeout (${this.timeout}ms): ${msg.subject}`);
|
|
1923
2181
|
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, msg.subject, correlationId);
|
|
1924
2182
|
msg.term("Handler timeout");
|
|
@@ -1928,8 +2186,11 @@ var RpcRouter = class {
|
|
|
1928
2186
|
if (settled) return;
|
|
1929
2187
|
settled = true;
|
|
1930
2188
|
clearTimeout(timeoutId);
|
|
2189
|
+
stopAckExtension?.();
|
|
1931
2190
|
msg.ack();
|
|
1932
2191
|
try {
|
|
2192
|
+
const hdrs = headers();
|
|
2193
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
1933
2194
|
nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
|
|
1934
2195
|
} catch (publishErr) {
|
|
1935
2196
|
this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
|
|
@@ -1938,7 +2199,10 @@ var RpcRouter = class {
|
|
|
1938
2199
|
if (settled) return;
|
|
1939
2200
|
settled = true;
|
|
1940
2201
|
clearTimeout(timeoutId);
|
|
2202
|
+
stopAckExtension?.();
|
|
1941
2203
|
try {
|
|
2204
|
+
const hdrs = headers();
|
|
2205
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
1942
2206
|
hdrs.set("x-error" /* Error */, "true");
|
|
1943
2207
|
nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
|
|
1944
2208
|
} catch (encodeErr) {
|
|
@@ -1961,7 +2225,7 @@ var ShutdownManager = class {
|
|
|
1961
2225
|
/**
|
|
1962
2226
|
* Execute the full shutdown sequence.
|
|
1963
2227
|
*
|
|
1964
|
-
* @param strategy Optional
|
|
2228
|
+
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
1965
2229
|
*/
|
|
1966
2230
|
async shutdown(strategy) {
|
|
1967
2231
|
this.eventBus.emit("shutdownStart" /* ShutdownStart */);
|
|
@@ -1984,6 +2248,7 @@ var ShutdownManager = class {
|
|
|
1984
2248
|
};
|
|
1985
2249
|
|
|
1986
2250
|
// src/jetstream.module.ts
|
|
2251
|
+
var JETSTREAM_ACK_WAIT_MAP = /* @__PURE__ */ Symbol("JETSTREAM_ACK_WAIT_MAP");
|
|
1987
2252
|
var JetstreamModule = class {
|
|
1988
2253
|
constructor(shutdownManager, strategy) {
|
|
1989
2254
|
this.shutdownManager = shutdownManager;
|
|
@@ -2168,13 +2433,26 @@ var JetstreamModule = class {
|
|
|
2168
2433
|
return new ConsumerProvider(options, connection, streamProvider, patternRegistry);
|
|
2169
2434
|
}
|
|
2170
2435
|
},
|
|
2436
|
+
// Shared ack_wait map — populated by strategy after ensureConsumers()
|
|
2437
|
+
{
|
|
2438
|
+
provide: JETSTREAM_ACK_WAIT_MAP,
|
|
2439
|
+
useFactory: () => /* @__PURE__ */ new Map()
|
|
2440
|
+
},
|
|
2171
2441
|
// MessageProvider — pull-based message consumption
|
|
2172
2442
|
{
|
|
2173
2443
|
provide: MessageProvider,
|
|
2174
2444
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
|
|
2175
2445
|
useFactory: (options, connection, eventBus) => {
|
|
2176
2446
|
if (options.consumer === false) return null;
|
|
2177
|
-
|
|
2447
|
+
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
2448
|
+
if (options.events?.consume)
|
|
2449
|
+
consumeOptionsMap.set("ev" /* Event */, options.events.consume);
|
|
2450
|
+
if (options.broadcast?.consume)
|
|
2451
|
+
consumeOptionsMap.set("broadcast" /* Broadcast */, options.broadcast.consume);
|
|
2452
|
+
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
2453
|
+
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
2454
|
+
}
|
|
2455
|
+
return new MessageProvider(connection, eventBus, consumeOptionsMap);
|
|
2178
2456
|
}
|
|
2179
2457
|
},
|
|
2180
2458
|
// EventRouter — routes event and broadcast messages to handlers
|
|
@@ -2185,20 +2463,33 @@ var JetstreamModule = class {
|
|
|
2185
2463
|
MessageProvider,
|
|
2186
2464
|
PatternRegistry,
|
|
2187
2465
|
JETSTREAM_CODEC,
|
|
2188
|
-
JETSTREAM_EVENT_BUS
|
|
2466
|
+
JETSTREAM_EVENT_BUS,
|
|
2467
|
+
JETSTREAM_ACK_WAIT_MAP
|
|
2189
2468
|
],
|
|
2190
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus) => {
|
|
2469
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
|
|
2191
2470
|
if (options.consumer === false) return null;
|
|
2192
2471
|
const deadLetterConfig = options.onDeadLetter ? {
|
|
2193
2472
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
2194
2473
|
onDeadLetter: options.onDeadLetter
|
|
2195
2474
|
} : void 0;
|
|
2475
|
+
const processingConfig = {
|
|
2476
|
+
events: {
|
|
2477
|
+
concurrency: options.events?.concurrency,
|
|
2478
|
+
ackExtension: options.events?.ackExtension
|
|
2479
|
+
},
|
|
2480
|
+
broadcast: {
|
|
2481
|
+
concurrency: options.broadcast?.concurrency,
|
|
2482
|
+
ackExtension: options.broadcast?.ackExtension
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2196
2485
|
return new EventRouter(
|
|
2197
2486
|
messageProvider,
|
|
2198
2487
|
patternRegistry,
|
|
2199
2488
|
codec,
|
|
2200
2489
|
eventBus,
|
|
2201
|
-
deadLetterConfig
|
|
2490
|
+
deadLetterConfig,
|
|
2491
|
+
processingConfig,
|
|
2492
|
+
ackWaitMap
|
|
2202
2493
|
);
|
|
2203
2494
|
}
|
|
2204
2495
|
},
|
|
@@ -2211,18 +2502,24 @@ var JetstreamModule = class {
|
|
|
2211
2502
|
PatternRegistry,
|
|
2212
2503
|
JETSTREAM_CONNECTION,
|
|
2213
2504
|
JETSTREAM_CODEC,
|
|
2214
|
-
JETSTREAM_EVENT_BUS
|
|
2505
|
+
JETSTREAM_EVENT_BUS,
|
|
2506
|
+
JETSTREAM_ACK_WAIT_MAP
|
|
2215
2507
|
],
|
|
2216
|
-
useFactory: (options, messageProvider, patternRegistry, connection, codec, eventBus) => {
|
|
2508
|
+
useFactory: (options, messageProvider, patternRegistry, connection, codec, eventBus, ackWaitMap) => {
|
|
2217
2509
|
if (options.consumer === false) return null;
|
|
2218
|
-
const
|
|
2510
|
+
const rpcOptions = options.rpc?.mode === "jetstream" ? {
|
|
2511
|
+
timeout: options.rpc.timeout,
|
|
2512
|
+
concurrency: options.rpc.concurrency,
|
|
2513
|
+
ackExtension: options.rpc.ackExtension
|
|
2514
|
+
} : void 0;
|
|
2219
2515
|
return new RpcRouter(
|
|
2220
2516
|
messageProvider,
|
|
2221
2517
|
patternRegistry,
|
|
2222
2518
|
connection,
|
|
2223
2519
|
codec,
|
|
2224
2520
|
eventBus,
|
|
2225
|
-
|
|
2521
|
+
rpcOptions,
|
|
2522
|
+
ackWaitMap
|
|
2226
2523
|
);
|
|
2227
2524
|
}
|
|
2228
2525
|
},
|
|
@@ -2253,9 +2550,10 @@ var JetstreamModule = class {
|
|
|
2253
2550
|
MessageProvider,
|
|
2254
2551
|
EventRouter,
|
|
2255
2552
|
RpcRouter,
|
|
2256
|
-
CoreRpcServer
|
|
2553
|
+
CoreRpcServer,
|
|
2554
|
+
JETSTREAM_ACK_WAIT_MAP
|
|
2257
2555
|
],
|
|
2258
|
-
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer) => {
|
|
2556
|
+
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
|
|
2259
2557
|
if (options.consumer === false) return null;
|
|
2260
2558
|
return new JetstreamStrategy(
|
|
2261
2559
|
options,
|
|
@@ -2266,7 +2564,8 @@ var JetstreamModule = class {
|
|
|
2266
2564
|
messageProvider,
|
|
2267
2565
|
eventRouter,
|
|
2268
2566
|
rpcRouter,
|
|
2269
|
-
coreRpcServer
|
|
2567
|
+
coreRpcServer,
|
|
2568
|
+
ackWaitMap
|
|
2270
2569
|
);
|
|
2271
2570
|
}
|
|
2272
2571
|
}
|
|
@@ -2346,8 +2645,13 @@ export {
|
|
|
2346
2645
|
JetstreamRecordBuilder,
|
|
2347
2646
|
JetstreamStrategy,
|
|
2348
2647
|
JsonCodec,
|
|
2648
|
+
MessageKind,
|
|
2649
|
+
PatternPrefix,
|
|
2349
2650
|
RpcContext,
|
|
2651
|
+
StreamKind,
|
|
2350
2652
|
TransportEvent,
|
|
2351
2653
|
getClientToken,
|
|
2654
|
+
isCoreRpcMode,
|
|
2655
|
+
isJetStreamRpcMode,
|
|
2352
2656
|
toNanos
|
|
2353
2657
|
};
|