@horizon-republic/nestjs-jetstream 2.9.0 → 2.9.1
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 +15 -2
- package/dist/index.cjs +544 -212
- package/dist/index.d.cts +125 -22
- package/dist/index.d.ts +125 -22
- package/dist/index.js +526 -206
- package/package.json +22 -7
package/dist/index.js
CHANGED
|
@@ -398,6 +398,7 @@ var nanosToGoDuration = (nanos) => {
|
|
|
398
398
|
};
|
|
399
399
|
|
|
400
400
|
// src/client/jetstream.client.ts
|
|
401
|
+
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
401
402
|
var JetstreamClient = class extends ClientProxy {
|
|
402
403
|
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
403
404
|
super();
|
|
@@ -407,12 +408,33 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
407
408
|
this.eventBus = eventBus;
|
|
408
409
|
this.targetName = targetServiceName;
|
|
409
410
|
this.callerName = internalName(this.rootOptions.name);
|
|
411
|
+
const targetInternal = internalName(targetServiceName);
|
|
412
|
+
this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
|
|
413
|
+
this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
|
|
414
|
+
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
415
|
+
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
416
|
+
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
410
417
|
}
|
|
411
418
|
logger = new Logger("Jetstream:Client");
|
|
412
419
|
/** Target service name this client sends messages to. */
|
|
413
420
|
targetName;
|
|
414
421
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
415
422
|
callerName;
|
|
423
|
+
/**
|
|
424
|
+
* Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
|
|
425
|
+
* per stream kind this client may publish to. Built once in the constructor
|
|
426
|
+
* so producing a full subject is a single string concat with the user pattern.
|
|
427
|
+
*/
|
|
428
|
+
eventSubjectPrefix;
|
|
429
|
+
commandSubjectPrefix;
|
|
430
|
+
orderedSubjectPrefix;
|
|
431
|
+
/**
|
|
432
|
+
* RPC configuration snapshots. The values are derived from rootOptions at
|
|
433
|
+
* construction time so the publish hot path never has to re-run
|
|
434
|
+
* isCoreRpcMode / getRpcTimeout on every call.
|
|
435
|
+
*/
|
|
436
|
+
isCoreMode;
|
|
437
|
+
defaultRpcTimeout;
|
|
416
438
|
/** Shared inbox for JetStream-mode RPC responses. */
|
|
417
439
|
inbox = null;
|
|
418
440
|
inboxSubscription = null;
|
|
@@ -422,6 +444,12 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
422
444
|
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
423
445
|
/** Subscription to connection status events for disconnect handling. */
|
|
424
446
|
statusSubscription = null;
|
|
447
|
+
/**
|
|
448
|
+
* Cached readiness flag. Once `connect()` has wired the inbox and status
|
|
449
|
+
* subscription, subsequent publishes skip the `await connect()` microtask
|
|
450
|
+
* and reach for the underlying connection synchronously instead.
|
|
451
|
+
*/
|
|
452
|
+
readyForPublish = false;
|
|
425
453
|
/**
|
|
426
454
|
* Establish connection. Called automatically by NestJS on first use.
|
|
427
455
|
*
|
|
@@ -432,7 +460,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
432
460
|
*/
|
|
433
461
|
async connect() {
|
|
434
462
|
const nc = await this.connection.getConnection();
|
|
435
|
-
if (
|
|
463
|
+
if (!this.isCoreMode && !this.inboxSubscription) {
|
|
436
464
|
this.setupInbox(nc);
|
|
437
465
|
}
|
|
438
466
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
@@ -440,12 +468,14 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
440
468
|
this.handleDisconnect();
|
|
441
469
|
}
|
|
442
470
|
});
|
|
471
|
+
this.readyForPublish = true;
|
|
443
472
|
return nc;
|
|
444
473
|
}
|
|
445
474
|
/** Clean up resources: reject pending RPCs, unsubscribe from status events. */
|
|
446
475
|
async close() {
|
|
447
476
|
this.statusSubscription?.unsubscribe();
|
|
448
477
|
this.statusSubscription = null;
|
|
478
|
+
this.readyForPublish = false;
|
|
449
479
|
this.rejectPendingRpcs(new Error("Client closed"));
|
|
450
480
|
}
|
|
451
481
|
/**
|
|
@@ -469,7 +499,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
469
499
|
* set to the original event subject.
|
|
470
500
|
*/
|
|
471
501
|
async dispatchEvent(packet) {
|
|
472
|
-
await this.connect();
|
|
502
|
+
if (!this.readyForPublish) await this.connect();
|
|
473
503
|
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
474
504
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
475
505
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
@@ -508,7 +538,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
508
538
|
* JetStream mode: publishes to stream + waits for inbox response.
|
|
509
539
|
*/
|
|
510
540
|
publish(packet, callback) {
|
|
511
|
-
const subject =
|
|
541
|
+
const subject = this.commandSubjectPrefix + packet.pattern;
|
|
512
542
|
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
513
543
|
if (schedule) {
|
|
514
544
|
this.logger.warn(
|
|
@@ -525,7 +555,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
525
555
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
526
556
|
};
|
|
527
557
|
let jetStreamCorrelationId = null;
|
|
528
|
-
if (
|
|
558
|
+
if (this.isCoreMode) {
|
|
529
559
|
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
|
|
530
560
|
} else {
|
|
531
561
|
jetStreamCorrelationId = nuid.next();
|
|
@@ -550,8 +580,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
550
580
|
/** Core mode: nc.request() with timeout. */
|
|
551
581
|
async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
|
|
552
582
|
try {
|
|
553
|
-
const nc = await this.connect();
|
|
554
|
-
const effectiveTimeout = timeout ?? this.
|
|
583
|
+
const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
|
|
584
|
+
const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
|
|
555
585
|
const hdrs = this.buildHeaders(customHeaders, { subject });
|
|
556
586
|
const response = await nc.request(subject, this.codec.encode(data), {
|
|
557
587
|
timeout: effectiveTimeout,
|
|
@@ -573,10 +603,10 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
573
603
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
574
604
|
async publishJetStreamRpc(subject, data, callback, options) {
|
|
575
605
|
const { headers: customHeaders, correlationId, messageId } = options;
|
|
576
|
-
const effectiveTimeout = options.timeout ?? this.
|
|
606
|
+
const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
|
|
577
607
|
this.pendingMessages.set(correlationId, callback);
|
|
578
608
|
try {
|
|
579
|
-
await this.connect();
|
|
609
|
+
if (!this.readyForPublish) await this.connect();
|
|
580
610
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
581
611
|
if (!this.inbox) {
|
|
582
612
|
this.pendingMessages.delete(correlationId);
|
|
@@ -622,6 +652,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
622
652
|
handleDisconnect() {
|
|
623
653
|
this.rejectPendingRpcs(new Error("Connection lost"));
|
|
624
654
|
this.inbox = null;
|
|
655
|
+
this.readyForPublish = false;
|
|
625
656
|
}
|
|
626
657
|
/** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
|
|
627
658
|
rejectPendingRpcs(error) {
|
|
@@ -685,19 +716,22 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
685
716
|
this.pendingMessages.delete(correlationId);
|
|
686
717
|
}
|
|
687
718
|
}
|
|
688
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* Resolve a user pattern to a fully-qualified NATS subject, dispatching
|
|
721
|
+
* between the event, broadcast, and ordered prefixes.
|
|
722
|
+
*
|
|
723
|
+
* The leading-char check short-circuits the `startsWith` comparisons for
|
|
724
|
+
* patterns that cannot possibly carry a broadcast/ordered marker, which is
|
|
725
|
+
* the overwhelmingly common case.
|
|
726
|
+
*/
|
|
689
727
|
buildEventSubject(pattern) {
|
|
690
|
-
if (pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
691
|
-
return
|
|
692
|
-
}
|
|
693
|
-
if (pattern.startsWith("ordered:" /* Ordered */)) {
|
|
694
|
-
return buildSubject(
|
|
695
|
-
this.targetName,
|
|
696
|
-
"ordered" /* Ordered */,
|
|
697
|
-
pattern.slice("ordered:" /* Ordered */.length)
|
|
698
|
-
);
|
|
728
|
+
if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
729
|
+
return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
|
|
699
730
|
}
|
|
700
|
-
|
|
731
|
+
if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
|
|
732
|
+
return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
|
|
733
|
+
}
|
|
734
|
+
return this.eventSubjectPrefix + pattern;
|
|
701
735
|
}
|
|
702
736
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
703
737
|
buildHeaders(customHeaders, transport) {
|
|
@@ -722,7 +756,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
722
756
|
if (rawData instanceof JetstreamRecord) {
|
|
723
757
|
return {
|
|
724
758
|
data: rawData.data,
|
|
725
|
-
hdrs: rawData.headers.size > 0 ?
|
|
759
|
+
hdrs: rawData.headers.size > 0 ? rawData.headers : null,
|
|
726
760
|
timeout: rawData.timeout,
|
|
727
761
|
messageId: rawData.messageId,
|
|
728
762
|
schedule: rawData.schedule,
|
|
@@ -765,11 +799,6 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
765
799
|
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
766
800
|
return `${targetPrefix}_sch.${pattern}`;
|
|
767
801
|
}
|
|
768
|
-
getRpcTimeout() {
|
|
769
|
-
if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
|
|
770
|
-
const defaultTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? DEFAULT_JETSTREAM_RPC_TIMEOUT : DEFAULT_RPC_TIMEOUT;
|
|
771
|
-
return this.rootOptions.rpc.timeout ?? defaultTimeout;
|
|
772
|
-
}
|
|
773
802
|
};
|
|
774
803
|
|
|
775
804
|
// src/codec/json.codec.ts
|
|
@@ -784,6 +813,19 @@ var JsonCodec = class {
|
|
|
784
813
|
}
|
|
785
814
|
};
|
|
786
815
|
|
|
816
|
+
// src/codec/msgpack.codec.ts
|
|
817
|
+
var MsgpackCodec = class {
|
|
818
|
+
constructor(packr) {
|
|
819
|
+
this.packr = packr;
|
|
820
|
+
}
|
|
821
|
+
encode(data) {
|
|
822
|
+
return this.packr.pack(data);
|
|
823
|
+
}
|
|
824
|
+
decode(data) {
|
|
825
|
+
return this.packr.unpack(data);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
787
829
|
// src/connection/connection.provider.ts
|
|
788
830
|
import { Logger as Logger2 } from "@nestjs/common";
|
|
789
831
|
import {
|
|
@@ -1001,6 +1043,15 @@ var EventBus = class {
|
|
|
1001
1043
|
kind
|
|
1002
1044
|
);
|
|
1003
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Check whether a hook is registered for the given event.
|
|
1048
|
+
*
|
|
1049
|
+
* Used by the routing hot path to elide the emit call entirely when the
|
|
1050
|
+
* transport owner did not register a listener.
|
|
1051
|
+
*/
|
|
1052
|
+
hasHook(event) {
|
|
1053
|
+
return this.hooks[event] !== void 0;
|
|
1054
|
+
}
|
|
1004
1055
|
callHook(event, hook, ...args) {
|
|
1005
1056
|
try {
|
|
1006
1057
|
const result = hook(...args);
|
|
@@ -1383,16 +1434,71 @@ var resolveAckExtensionInterval = (config, ackWaitNanos) => {
|
|
|
1383
1434
|
const interval = Math.floor(ackWaitNanos / 1e6 / 2);
|
|
1384
1435
|
return Math.max(interval, MIN_ACK_EXTENSION_INTERVAL);
|
|
1385
1436
|
};
|
|
1437
|
+
var AckExtensionPool = class {
|
|
1438
|
+
entries = /* @__PURE__ */ new Set();
|
|
1439
|
+
handle = null;
|
|
1440
|
+
handleFireAt = 0;
|
|
1441
|
+
schedule(msg, interval) {
|
|
1442
|
+
const entry = {
|
|
1443
|
+
msg,
|
|
1444
|
+
interval,
|
|
1445
|
+
nextFireAt: Date.now() + interval,
|
|
1446
|
+
active: true
|
|
1447
|
+
};
|
|
1448
|
+
this.entries.add(entry);
|
|
1449
|
+
this.ensureWake(entry.nextFireAt);
|
|
1450
|
+
return entry;
|
|
1451
|
+
}
|
|
1452
|
+
cancel(entry) {
|
|
1453
|
+
if (!entry.active) return;
|
|
1454
|
+
entry.active = false;
|
|
1455
|
+
this.entries.delete(entry);
|
|
1456
|
+
if (this.entries.size === 0 && this.handle !== null) {
|
|
1457
|
+
clearTimeout(this.handle);
|
|
1458
|
+
this.handle = null;
|
|
1459
|
+
this.handleFireAt = 0;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Ensure the shared timer will fire no later than `dueAt`. If an earlier
|
|
1464
|
+
* wake is already scheduled, leave it; otherwise replace it with a tighter one.
|
|
1465
|
+
*/
|
|
1466
|
+
ensureWake(dueAt) {
|
|
1467
|
+
if (this.handle !== null && this.handleFireAt <= dueAt) return;
|
|
1468
|
+
if (this.handle !== null) clearTimeout(this.handle);
|
|
1469
|
+
const delay = Math.max(0, dueAt - Date.now());
|
|
1470
|
+
const handle = setTimeout(() => {
|
|
1471
|
+
this.tick();
|
|
1472
|
+
}, delay);
|
|
1473
|
+
if (typeof handle.unref === "function") handle.unref();
|
|
1474
|
+
this.handle = handle;
|
|
1475
|
+
this.handleFireAt = dueAt;
|
|
1476
|
+
}
|
|
1477
|
+
tick() {
|
|
1478
|
+
this.handle = null;
|
|
1479
|
+
this.handleFireAt = 0;
|
|
1480
|
+
const now = Date.now();
|
|
1481
|
+
let earliest = Infinity;
|
|
1482
|
+
for (const entry of this.entries) {
|
|
1483
|
+
if (!entry.active) continue;
|
|
1484
|
+
if (entry.nextFireAt <= now) {
|
|
1485
|
+
try {
|
|
1486
|
+
entry.msg.working();
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
entry.nextFireAt = now + entry.interval;
|
|
1490
|
+
}
|
|
1491
|
+
if (entry.nextFireAt < earliest) earliest = entry.nextFireAt;
|
|
1492
|
+
}
|
|
1493
|
+
if (earliest !== Infinity) this.ensureWake(earliest);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
var pool = new AckExtensionPool();
|
|
1386
1497
|
var startAckExtensionTimer = (msg, interval) => {
|
|
1387
1498
|
if (interval === null || interval <= 0) return null;
|
|
1388
|
-
const
|
|
1389
|
-
try {
|
|
1390
|
-
msg.working();
|
|
1391
|
-
} catch {
|
|
1392
|
-
}
|
|
1393
|
-
}, interval);
|
|
1499
|
+
const entry = pool.schedule(msg, interval);
|
|
1394
1500
|
return () => {
|
|
1395
|
-
|
|
1501
|
+
pool.cancel(entry);
|
|
1396
1502
|
};
|
|
1397
1503
|
};
|
|
1398
1504
|
|
|
@@ -1406,11 +1512,8 @@ var serializeError = (err) => {
|
|
|
1406
1512
|
|
|
1407
1513
|
// src/utils/unwrap-result.ts
|
|
1408
1514
|
import { isObservable } from "rxjs";
|
|
1409
|
-
var RESOLVED_VOID = Promise.resolve(void 0);
|
|
1410
|
-
var RESOLVED_NULL = Promise.resolve(null);
|
|
1411
1515
|
var unwrapResult = (result) => {
|
|
1412
|
-
if (result === void 0) return
|
|
1413
|
-
if (result === null) return RESOLVED_NULL;
|
|
1516
|
+
if (result === void 0 || result === null) return result;
|
|
1414
1517
|
if (isObservable(result)) {
|
|
1415
1518
|
return subscribeToFirst(result);
|
|
1416
1519
|
}
|
|
@@ -1419,8 +1522,9 @@ var unwrapResult = (result) => {
|
|
|
1419
1522
|
(resolved) => isObservable(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1420
1523
|
);
|
|
1421
1524
|
}
|
|
1422
|
-
return
|
|
1525
|
+
return result;
|
|
1423
1526
|
};
|
|
1527
|
+
var isPromiseLike = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
1424
1528
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1425
1529
|
let done = false;
|
|
1426
1530
|
let subscription = null;
|
|
@@ -1508,7 +1612,8 @@ var CoreRpcServer = class {
|
|
|
1508
1612
|
}
|
|
1509
1613
|
const ctx = new RpcContext([msg]);
|
|
1510
1614
|
try {
|
|
1511
|
-
const
|
|
1615
|
+
const raw = unwrapResult(handler(data, ctx));
|
|
1616
|
+
const result = isPromiseLike(raw) ? await raw : raw;
|
|
1512
1617
|
msg.respond(this.codec.encode(result));
|
|
1513
1618
|
} catch (err) {
|
|
1514
1619
|
this.logger.error(`Handler error for Core RPC ${msg.subject}:`, err);
|
|
@@ -1531,6 +1636,14 @@ var CoreRpcServer = class {
|
|
|
1531
1636
|
import { Logger as Logger6 } from "@nestjs/common";
|
|
1532
1637
|
import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
|
|
1533
1638
|
|
|
1639
|
+
// src/server/infrastructure/nats-error-codes.ts
|
|
1640
|
+
var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
1641
|
+
NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
|
|
1642
|
+
NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
|
|
1643
|
+
NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
|
|
1644
|
+
return NatsErrorCode2;
|
|
1645
|
+
})(NatsErrorCode || {});
|
|
1646
|
+
|
|
1534
1647
|
// src/server/infrastructure/stream-config-diff.ts
|
|
1535
1648
|
var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1536
1649
|
"retention"
|
|
@@ -2229,13 +2342,17 @@ var MessageProvider = class {
|
|
|
2229
2342
|
}
|
|
2230
2343
|
const defaults = { idle_heartbeat: 5e3 };
|
|
2231
2344
|
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
2232
|
-
const messages = await consumer.consume({
|
|
2345
|
+
const messages = await consumer.consume({
|
|
2346
|
+
...defaults,
|
|
2347
|
+
...userOptions,
|
|
2348
|
+
callback: (msg) => {
|
|
2349
|
+
target$.next(msg);
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2233
2352
|
this.activeIterators.add(messages);
|
|
2234
2353
|
this.monitorConsumerHealth(messages, consumerName2);
|
|
2235
2354
|
try {
|
|
2236
|
-
|
|
2237
|
-
target$.next(msg);
|
|
2238
|
-
}
|
|
2355
|
+
await messages.closed();
|
|
2239
2356
|
} finally {
|
|
2240
2357
|
this.activeIterators.delete(messages);
|
|
2241
2358
|
}
|
|
@@ -2327,11 +2444,16 @@ var MessageProvider = class {
|
|
|
2327
2444
|
})
|
|
2328
2445
|
);
|
|
2329
2446
|
}
|
|
2330
|
-
/** Single iteration: create ordered consumer ->
|
|
2447
|
+
/** Single iteration: create ordered consumer -> push messages into the subject. */
|
|
2331
2448
|
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
2332
2449
|
const js = this.connection.getJetStreamClient();
|
|
2333
2450
|
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
2334
|
-
const
|
|
2451
|
+
const orderedMessages$ = this.orderedMessages$;
|
|
2452
|
+
const messages = await consumer.consume({
|
|
2453
|
+
callback: (msg) => {
|
|
2454
|
+
orderedMessages$.next(msg);
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2335
2457
|
if (this.orderedReadyResolve) {
|
|
2336
2458
|
this.orderedReadyResolve();
|
|
2337
2459
|
this.orderedReadyResolve = null;
|
|
@@ -2339,9 +2461,7 @@ var MessageProvider = class {
|
|
|
2339
2461
|
}
|
|
2340
2462
|
this.activeIterators.add(messages);
|
|
2341
2463
|
try {
|
|
2342
|
-
|
|
2343
|
-
this.orderedMessages$.next(msg);
|
|
2344
|
-
}
|
|
2464
|
+
await messages.closed();
|
|
2345
2465
|
} finally {
|
|
2346
2466
|
this.activeIterators.delete(messages);
|
|
2347
2467
|
}
|
|
@@ -2620,7 +2740,6 @@ var PatternRegistry = class {
|
|
|
2620
2740
|
|
|
2621
2741
|
// src/server/routing/event.router.ts
|
|
2622
2742
|
import { Logger as Logger11 } from "@nestjs/common";
|
|
2623
|
-
import { concatMap, from as from2, mergeMap } from "rxjs";
|
|
2624
2743
|
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
2625
2744
|
var EventRouter = class {
|
|
2626
2745
|
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
@@ -2659,15 +2778,194 @@ var EventRouter = class {
|
|
|
2659
2778
|
}
|
|
2660
2779
|
this.subscriptions.length = 0;
|
|
2661
2780
|
}
|
|
2662
|
-
/** Subscribe to a message stream and route each message. */
|
|
2781
|
+
/** Subscribe to a message stream and route each message to its handler. */
|
|
2663
2782
|
subscribeToStream(stream$, kind) {
|
|
2664
2783
|
const isOrdered = kind === "ordered" /* Ordered */;
|
|
2784
|
+
const patternRegistry = this.patternRegistry;
|
|
2785
|
+
const codec = this.codec;
|
|
2786
|
+
const eventBus = this.eventBus;
|
|
2787
|
+
const logger = this.logger;
|
|
2788
|
+
const deadLetterConfig = this.deadLetterConfig;
|
|
2665
2789
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
2790
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
2666
2791
|
const concurrency = this.getConcurrency(kind);
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
)
|
|
2670
|
-
|
|
2792
|
+
const hasDlqCheck = deadLetterConfig !== void 0;
|
|
2793
|
+
const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
|
|
2794
|
+
const isDeadLetter = (msg) => {
|
|
2795
|
+
if (!hasDlqCheck) return false;
|
|
2796
|
+
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
2797
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
2798
|
+
return msg.info.deliveryCount >= maxDeliver;
|
|
2799
|
+
};
|
|
2800
|
+
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
2801
|
+
const settleSuccess = (msg, ctx) => {
|
|
2802
|
+
if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
|
|
2803
|
+
else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
|
|
2804
|
+
else msg.ack();
|
|
2805
|
+
};
|
|
2806
|
+
const settleFailure = async (msg, data, err) => {
|
|
2807
|
+
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
2808
|
+
await handleDeadLetter(msg, data, err);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
msg.nak();
|
|
2812
|
+
};
|
|
2813
|
+
const resolveEvent = (msg) => {
|
|
2814
|
+
const subject = msg.subject;
|
|
2815
|
+
try {
|
|
2816
|
+
const handler = patternRegistry.getHandler(subject);
|
|
2817
|
+
if (!handler) {
|
|
2818
|
+
msg.term(`No handler for event: ${subject}`);
|
|
2819
|
+
logger.error(`No handler for subject: ${subject}`);
|
|
2820
|
+
return null;
|
|
2821
|
+
}
|
|
2822
|
+
let data;
|
|
2823
|
+
try {
|
|
2824
|
+
data = codec.decode(msg.data);
|
|
2825
|
+
} catch (err) {
|
|
2826
|
+
msg.term("Decode error");
|
|
2827
|
+
logger.error(`Decode error for ${subject}:`, err);
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2831
|
+
return { handler, data };
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2834
|
+
try {
|
|
2835
|
+
msg.term("Unexpected router error");
|
|
2836
|
+
} catch (termErr) {
|
|
2837
|
+
logger.error(`Failed to terminate message ${subject}:`, termErr);
|
|
2838
|
+
}
|
|
2839
|
+
return null;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
const handleSafe = (msg) => {
|
|
2843
|
+
const resolved = resolveEvent(msg);
|
|
2844
|
+
if (resolved === null) return void 0;
|
|
2845
|
+
const { handler, data } = resolved;
|
|
2846
|
+
const ctx = new RpcContext([msg]);
|
|
2847
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
2848
|
+
let pending;
|
|
2849
|
+
try {
|
|
2850
|
+
pending = unwrapResult(handler(data, ctx));
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
|
|
2853
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2854
|
+
return settleFailure(msg, data, err);
|
|
2855
|
+
}
|
|
2856
|
+
if (!isPromiseLike(pending)) {
|
|
2857
|
+
settleSuccess(msg, ctx);
|
|
2858
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2859
|
+
return void 0;
|
|
2860
|
+
}
|
|
2861
|
+
return pending.then(
|
|
2862
|
+
() => {
|
|
2863
|
+
settleSuccess(msg, ctx);
|
|
2864
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2865
|
+
},
|
|
2866
|
+
async (err) => {
|
|
2867
|
+
logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
|
|
2868
|
+
try {
|
|
2869
|
+
await settleFailure(msg, data, err);
|
|
2870
|
+
} finally {
|
|
2871
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
);
|
|
2875
|
+
};
|
|
2876
|
+
const handleOrderedSafe = (msg) => {
|
|
2877
|
+
const subject = msg.subject;
|
|
2878
|
+
let handler;
|
|
2879
|
+
let data;
|
|
2880
|
+
try {
|
|
2881
|
+
handler = patternRegistry.getHandler(subject);
|
|
2882
|
+
if (!handler) {
|
|
2883
|
+
logger.error(`No handler for subject: ${subject}`);
|
|
2884
|
+
return void 0;
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
data = codec.decode(msg.data);
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
logger.error(`Decode error for ${subject}:`, err);
|
|
2890
|
+
return void 0;
|
|
2891
|
+
}
|
|
2892
|
+
if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2895
|
+
return void 0;
|
|
2896
|
+
}
|
|
2897
|
+
const ctx = new RpcContext([msg]);
|
|
2898
|
+
const warnIfSettlementAttempted = () => {
|
|
2899
|
+
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
2900
|
+
logger.warn(
|
|
2901
|
+
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
let pending;
|
|
2906
|
+
try {
|
|
2907
|
+
pending = unwrapResult(handler(data, ctx));
|
|
2908
|
+
} catch (err) {
|
|
2909
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2910
|
+
return void 0;
|
|
2911
|
+
}
|
|
2912
|
+
if (!isPromiseLike(pending)) {
|
|
2913
|
+
warnIfSettlementAttempted();
|
|
2914
|
+
return void 0;
|
|
2915
|
+
}
|
|
2916
|
+
return pending.then(warnIfSettlementAttempted, (err) => {
|
|
2917
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2918
|
+
});
|
|
2919
|
+
};
|
|
2920
|
+
const route = isOrdered ? handleOrderedSafe : handleSafe;
|
|
2921
|
+
const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
|
|
2922
|
+
const backlogWarnThreshold = 1e3;
|
|
2923
|
+
let active = 0;
|
|
2924
|
+
let backlogWarned = false;
|
|
2925
|
+
const backlog = [];
|
|
2926
|
+
const onAsyncDone = () => {
|
|
2927
|
+
active--;
|
|
2928
|
+
drainBacklog();
|
|
2929
|
+
};
|
|
2930
|
+
const drainBacklog = () => {
|
|
2931
|
+
while (active < maxActive) {
|
|
2932
|
+
const next = backlog.shift();
|
|
2933
|
+
if (next === void 0) return;
|
|
2934
|
+
active++;
|
|
2935
|
+
const result = route(next);
|
|
2936
|
+
if (result !== void 0) {
|
|
2937
|
+
void result.finally(onAsyncDone);
|
|
2938
|
+
} else {
|
|
2939
|
+
active--;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
2943
|
+
};
|
|
2944
|
+
const subscription = stream$.subscribe({
|
|
2945
|
+
next: (msg) => {
|
|
2946
|
+
if (active >= maxActive) {
|
|
2947
|
+
backlog.push(msg);
|
|
2948
|
+
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
2949
|
+
backlogWarned = true;
|
|
2950
|
+
logger.warn(
|
|
2951
|
+
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
active++;
|
|
2957
|
+
const result = route(msg);
|
|
2958
|
+
if (result !== void 0) {
|
|
2959
|
+
void result.finally(onAsyncDone);
|
|
2960
|
+
} else {
|
|
2961
|
+
active--;
|
|
2962
|
+
if (backlog.length > 0) drainBacklog();
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
error: (err) => {
|
|
2966
|
+
logger.error(`Stream error in ${kind} router`, err);
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2671
2969
|
this.subscriptions.push(subscription);
|
|
2672
2970
|
}
|
|
2673
2971
|
getConcurrency(kind) {
|
|
@@ -2680,86 +2978,6 @@ var EventRouter = class {
|
|
|
2680
2978
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
2681
2979
|
return void 0;
|
|
2682
2980
|
}
|
|
2683
|
-
/** Handle a single event message with error isolation. */
|
|
2684
|
-
async handleSafe(msg, ackExtensionInterval, kind) {
|
|
2685
|
-
try {
|
|
2686
|
-
const resolved = this.decodeMessage(msg);
|
|
2687
|
-
if (!resolved) return;
|
|
2688
|
-
await this.executeHandler(
|
|
2689
|
-
resolved.handler,
|
|
2690
|
-
resolved.data,
|
|
2691
|
-
resolved.ctx,
|
|
2692
|
-
msg,
|
|
2693
|
-
ackExtensionInterval
|
|
2694
|
-
);
|
|
2695
|
-
} catch (err) {
|
|
2696
|
-
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
/** Handle an ordered message with error isolation. */
|
|
2700
|
-
async handleOrderedSafe(msg) {
|
|
2701
|
-
try {
|
|
2702
|
-
const resolved = this.decodeMessage(msg, true);
|
|
2703
|
-
if (!resolved) return;
|
|
2704
|
-
await unwrapResult(resolved.handler(resolved.data, resolved.ctx));
|
|
2705
|
-
if (resolved.ctx.shouldRetry || resolved.ctx.shouldTerminate) {
|
|
2706
|
-
this.logger.warn(
|
|
2707
|
-
`retry()/terminate() ignored for ordered message ${msg.subject} \u2014 ordered consumers auto-acknowledge`
|
|
2708
|
-
);
|
|
2709
|
-
}
|
|
2710
|
-
} catch (err) {
|
|
2711
|
-
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
2715
|
-
decodeMessage(msg, isOrdered = false) {
|
|
2716
|
-
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
2717
|
-
if (!handler) {
|
|
2718
|
-
if (!isOrdered) msg.term(`No handler for event: ${msg.subject}`);
|
|
2719
|
-
this.logger.error(`No handler for subject: ${msg.subject}`);
|
|
2720
|
-
return null;
|
|
2721
|
-
}
|
|
2722
|
-
let data;
|
|
2723
|
-
try {
|
|
2724
|
-
data = this.codec.decode(msg.data);
|
|
2725
|
-
} catch (err) {
|
|
2726
|
-
if (!isOrdered) msg.term("Decode error");
|
|
2727
|
-
this.logger.error(`Decode error for ${msg.subject}:`, err);
|
|
2728
|
-
return null;
|
|
2729
|
-
}
|
|
2730
|
-
this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
|
|
2731
|
-
return { handler, data, ctx: new RpcContext([msg]) };
|
|
2732
|
-
}
|
|
2733
|
-
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
2734
|
-
async executeHandler(handler, data, ctx, msg, ackExtensionInterval) {
|
|
2735
|
-
const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
|
|
2736
|
-
try {
|
|
2737
|
-
await unwrapResult(handler(data, ctx));
|
|
2738
|
-
if (ctx.shouldTerminate) {
|
|
2739
|
-
msg.term(ctx.terminateReason);
|
|
2740
|
-
} else if (ctx.shouldRetry) {
|
|
2741
|
-
msg.nak(ctx.retryDelay);
|
|
2742
|
-
} else {
|
|
2743
|
-
msg.ack();
|
|
2744
|
-
}
|
|
2745
|
-
} catch (err) {
|
|
2746
|
-
this.logger.error(`Event handler error (${msg.subject}):`, err);
|
|
2747
|
-
if (this.isDeadLetter(msg)) {
|
|
2748
|
-
await this.handleDeadLetter(msg, data, err);
|
|
2749
|
-
} else {
|
|
2750
|
-
msg.nak();
|
|
2751
|
-
}
|
|
2752
|
-
} finally {
|
|
2753
|
-
stopAckExtension?.();
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
/** Check if the message has exhausted all delivery attempts. */
|
|
2757
|
-
isDeadLetter(msg) {
|
|
2758
|
-
if (!this.deadLetterConfig) return false;
|
|
2759
|
-
const maxDeliver = this.deadLetterConfig.maxDeliverByStream.get(msg.info.stream);
|
|
2760
|
-
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
2761
|
-
return msg.info.deliveryCount >= maxDeliver;
|
|
2762
|
-
}
|
|
2763
2981
|
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
2764
2982
|
/**
|
|
2765
2983
|
* Fallback execution for a dead letter when DLQ is disabled, or when
|
|
@@ -2871,7 +3089,6 @@ var EventRouter = class {
|
|
|
2871
3089
|
// src/server/routing/rpc.router.ts
|
|
2872
3090
|
import { Logger as Logger12 } from "@nestjs/common";
|
|
2873
3091
|
import { headers } from "@nats-io/transport-node";
|
|
2874
|
-
import { from as from3, mergeMap as mergeMap2 } from "rxjs";
|
|
2875
3092
|
var RpcRouter = class {
|
|
2876
3093
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
2877
3094
|
this.messageProvider = messageProvider;
|
|
@@ -2902,87 +3119,178 @@ var RpcRouter = class {
|
|
|
2902
3119
|
/** Start routing command messages to handlers. */
|
|
2903
3120
|
async start() {
|
|
2904
3121
|
this.cachedNc = await this.connection.getConnection();
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
this.
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2919
|
-
return;
|
|
2920
|
-
}
|
|
2921
|
-
const { headers: msgHeaders } = msg;
|
|
2922
|
-
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
2923
|
-
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
2924
|
-
if (!replyTo || !correlationId) {
|
|
2925
|
-
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2926
|
-
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
let data;
|
|
2930
|
-
try {
|
|
2931
|
-
data = this.codec.decode(msg.data);
|
|
2932
|
-
} catch (err) {
|
|
2933
|
-
msg.term("Decode error");
|
|
2934
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2935
|
-
return;
|
|
2936
|
-
}
|
|
2937
|
-
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
2938
|
-
await this.executeHandler(handler, data, msg, replyTo, correlationId);
|
|
2939
|
-
} catch (err) {
|
|
2940
|
-
this.logger.error("Unexpected error in RPC router", err);
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
/** Execute handler, publish response, settle message. */
|
|
2944
|
-
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
2945
|
-
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
2946
|
-
const ctx = new RpcContext([msg]);
|
|
2947
|
-
let settled = false;
|
|
2948
|
-
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
2949
|
-
const timeoutId = setTimeout(() => {
|
|
2950
|
-
if (settled) return;
|
|
2951
|
-
settled = true;
|
|
2952
|
-
stopAckExtension?.();
|
|
2953
|
-
this.logger.error(`RPC timeout (${this.timeout}ms): ${msg.subject}`);
|
|
2954
|
-
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, msg.subject, correlationId);
|
|
2955
|
-
msg.term("Handler timeout");
|
|
2956
|
-
}, this.timeout);
|
|
2957
|
-
try {
|
|
2958
|
-
const result = await unwrapResult(handler(data, ctx));
|
|
2959
|
-
if (settled) return;
|
|
2960
|
-
settled = true;
|
|
2961
|
-
clearTimeout(timeoutId);
|
|
2962
|
-
stopAckExtension?.();
|
|
2963
|
-
msg.ack();
|
|
3122
|
+
const nc = this.cachedNc;
|
|
3123
|
+
const patternRegistry = this.patternRegistry;
|
|
3124
|
+
const codec = this.codec;
|
|
3125
|
+
const eventBus = this.eventBus;
|
|
3126
|
+
const logger = this.logger;
|
|
3127
|
+
const timeout = this.timeout;
|
|
3128
|
+
const ackExtensionInterval = this.ackExtensionInterval;
|
|
3129
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
3130
|
+
const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
|
|
3131
|
+
const emitRpcTimeout = (subject, correlationId) => {
|
|
3132
|
+
eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
3133
|
+
};
|
|
3134
|
+
const publishReply = (replyTo, correlationId, payload) => {
|
|
2964
3135
|
try {
|
|
2965
3136
|
const hdrs = headers();
|
|
2966
3137
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2967
|
-
nc.publish(replyTo,
|
|
3138
|
+
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
2968
3139
|
} catch (publishErr) {
|
|
2969
|
-
|
|
3140
|
+
logger.error(`Failed to publish RPC response`, publishErr);
|
|
2970
3141
|
}
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
settled = true;
|
|
2974
|
-
clearTimeout(timeoutId);
|
|
2975
|
-
stopAckExtension?.();
|
|
3142
|
+
};
|
|
3143
|
+
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
2976
3144
|
try {
|
|
2977
3145
|
const hdrs = headers();
|
|
2978
3146
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2979
3147
|
hdrs.set("x-error" /* Error */, "true");
|
|
2980
|
-
nc.publish(replyTo,
|
|
3148
|
+
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
2981
3149
|
} catch (encodeErr) {
|
|
2982
|
-
|
|
3150
|
+
logger.error(`Failed to encode RPC error for ${subject}`, encodeErr);
|
|
2983
3151
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
3152
|
+
};
|
|
3153
|
+
const resolveCommand = (msg) => {
|
|
3154
|
+
const subject = msg.subject;
|
|
3155
|
+
try {
|
|
3156
|
+
const handler = patternRegistry.getHandler(subject);
|
|
3157
|
+
if (!handler) {
|
|
3158
|
+
msg.term(`No handler for RPC: ${subject}`);
|
|
3159
|
+
logger.error(`No handler for RPC subject: ${subject}`);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
const msgHeaders = msg.headers;
|
|
3163
|
+
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
3164
|
+
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
3165
|
+
if (!replyTo || !correlationId) {
|
|
3166
|
+
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
3167
|
+
logger.error(`Missing headers for RPC: ${subject}`);
|
|
3168
|
+
return null;
|
|
3169
|
+
}
|
|
3170
|
+
let data;
|
|
3171
|
+
try {
|
|
3172
|
+
data = codec.decode(msg.data);
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
msg.term("Decode error");
|
|
3175
|
+
logger.error(`Decode error for RPC ${subject}:`, err);
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
|
|
3179
|
+
return { handler, data, replyTo, correlationId };
|
|
3180
|
+
} catch (err) {
|
|
3181
|
+
logger.error("Unexpected error in RPC router", err);
|
|
3182
|
+
try {
|
|
3183
|
+
msg.term("Unexpected router error");
|
|
3184
|
+
} catch (termErr) {
|
|
3185
|
+
logger.error(`Failed to terminate RPC message ${subject}:`, termErr);
|
|
3186
|
+
}
|
|
3187
|
+
return null;
|
|
3188
|
+
}
|
|
3189
|
+
};
|
|
3190
|
+
const handleSafe = (msg) => {
|
|
3191
|
+
const resolved = resolveCommand(msg);
|
|
3192
|
+
if (resolved === null) return void 0;
|
|
3193
|
+
const { handler, data, replyTo, correlationId } = resolved;
|
|
3194
|
+
const subject = msg.subject;
|
|
3195
|
+
const ctx = new RpcContext([msg]);
|
|
3196
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
3197
|
+
let pending;
|
|
3198
|
+
try {
|
|
3199
|
+
pending = unwrapResult(handler(data, ctx));
|
|
3200
|
+
} catch (err) {
|
|
3201
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3202
|
+
logger.error(`RPC handler error (${subject}):`, err);
|
|
3203
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
3204
|
+
msg.term(`Handler error: ${subject}`);
|
|
3205
|
+
return void 0;
|
|
3206
|
+
}
|
|
3207
|
+
if (!isPromiseLike(pending)) {
|
|
3208
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3209
|
+
msg.ack();
|
|
3210
|
+
publishReply(replyTo, correlationId, pending);
|
|
3211
|
+
return void 0;
|
|
3212
|
+
}
|
|
3213
|
+
let settled = false;
|
|
3214
|
+
const timeoutId = setTimeout(() => {
|
|
3215
|
+
if (settled) return;
|
|
3216
|
+
settled = true;
|
|
3217
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3218
|
+
logger.error(`RPC timeout (${timeout}ms): ${subject}`);
|
|
3219
|
+
emitRpcTimeout(subject, correlationId);
|
|
3220
|
+
msg.term("Handler timeout");
|
|
3221
|
+
}, timeout);
|
|
3222
|
+
return pending.then(
|
|
3223
|
+
(result) => {
|
|
3224
|
+
if (settled) return;
|
|
3225
|
+
settled = true;
|
|
3226
|
+
clearTimeout(timeoutId);
|
|
3227
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3228
|
+
msg.ack();
|
|
3229
|
+
publishReply(replyTo, correlationId, result);
|
|
3230
|
+
},
|
|
3231
|
+
(err) => {
|
|
3232
|
+
if (settled) return;
|
|
3233
|
+
settled = true;
|
|
3234
|
+
clearTimeout(timeoutId);
|
|
3235
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3236
|
+
logger.error(`RPC handler error (${subject}):`, err);
|
|
3237
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
3238
|
+
msg.term(`Handler error: ${subject}`);
|
|
3239
|
+
}
|
|
3240
|
+
);
|
|
3241
|
+
};
|
|
3242
|
+
const backlogWarnThreshold = 1e3;
|
|
3243
|
+
let active = 0;
|
|
3244
|
+
let backlogWarned = false;
|
|
3245
|
+
const backlog = [];
|
|
3246
|
+
const onAsyncDone = () => {
|
|
3247
|
+
active--;
|
|
3248
|
+
drainBacklog();
|
|
3249
|
+
};
|
|
3250
|
+
const drainBacklog = () => {
|
|
3251
|
+
while (active < maxActive) {
|
|
3252
|
+
const next = backlog.shift();
|
|
3253
|
+
if (next === void 0) return;
|
|
3254
|
+
active++;
|
|
3255
|
+
const result = handleSafe(next);
|
|
3256
|
+
if (result !== void 0) {
|
|
3257
|
+
void result.finally(onAsyncDone);
|
|
3258
|
+
} else {
|
|
3259
|
+
active--;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
3263
|
+
};
|
|
3264
|
+
this.subscription = this.messageProvider.commands$.subscribe({
|
|
3265
|
+
next: (msg) => {
|
|
3266
|
+
if (active >= maxActive) {
|
|
3267
|
+
backlog.push(msg);
|
|
3268
|
+
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
3269
|
+
backlogWarned = true;
|
|
3270
|
+
logger.warn(
|
|
3271
|
+
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
active++;
|
|
3277
|
+
const result = handleSafe(msg);
|
|
3278
|
+
if (result !== void 0) {
|
|
3279
|
+
void result.finally(onAsyncDone);
|
|
3280
|
+
} else {
|
|
3281
|
+
active--;
|
|
3282
|
+
if (backlog.length > 0) drainBacklog();
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
error: (err) => {
|
|
3286
|
+
logger.error("Stream error in RPC router", err);
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
/** Stop routing and unsubscribe. */
|
|
3291
|
+
destroy() {
|
|
3292
|
+
this.subscription?.unsubscribe();
|
|
3293
|
+
this.subscription = null;
|
|
2986
3294
|
}
|
|
2987
3295
|
};
|
|
2988
3296
|
|
|
@@ -3430,14 +3738,23 @@ JetstreamModule = __decorateClass([
|
|
|
3430
3738
|
__decorateParam(1, Inject(JetstreamStrategy))
|
|
3431
3739
|
], JetstreamModule);
|
|
3432
3740
|
export {
|
|
3741
|
+
DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
3742
|
+
DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
3743
|
+
DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
3744
|
+
DEFAULT_COMMAND_STREAM_CONFIG,
|
|
3745
|
+
DEFAULT_DLQ_STREAM_CONFIG,
|
|
3746
|
+
DEFAULT_EVENT_CONSUMER_CONFIG,
|
|
3747
|
+
DEFAULT_EVENT_STREAM_CONFIG,
|
|
3748
|
+
DEFAULT_JETSTREAM_RPC_TIMEOUT,
|
|
3433
3749
|
DEFAULT_METADATA_BUCKET,
|
|
3434
3750
|
DEFAULT_METADATA_HISTORY,
|
|
3435
3751
|
DEFAULT_METADATA_REPLICAS,
|
|
3436
3752
|
DEFAULT_METADATA_TTL,
|
|
3437
|
-
|
|
3753
|
+
DEFAULT_ORDERED_STREAM_CONFIG,
|
|
3754
|
+
DEFAULT_RPC_TIMEOUT,
|
|
3755
|
+
DEFAULT_SHUTDOWN_TIMEOUT,
|
|
3438
3756
|
JETSTREAM_CODEC,
|
|
3439
3757
|
JETSTREAM_CONNECTION,
|
|
3440
|
-
JETSTREAM_EVENT_BUS,
|
|
3441
3758
|
JETSTREAM_OPTIONS,
|
|
3442
3759
|
JetstreamClient,
|
|
3443
3760
|
JetstreamDlqHeader,
|
|
@@ -3450,7 +3767,10 @@ export {
|
|
|
3450
3767
|
JsonCodec,
|
|
3451
3768
|
MIN_METADATA_TTL,
|
|
3452
3769
|
MessageKind,
|
|
3770
|
+
MsgpackCodec,
|
|
3771
|
+
NatsErrorCode,
|
|
3453
3772
|
PatternPrefix,
|
|
3773
|
+
RESERVED_HEADERS,
|
|
3454
3774
|
RpcContext,
|
|
3455
3775
|
StreamKind,
|
|
3456
3776
|
TransportEvent,
|