@horizon-republic/nestjs-jetstream 2.6.1 → 2.7.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 +34 -5
- package/dist/index.cjs +221 -73
- package/dist/index.d.cts +122 -14
- package/dist/index.d.ts +122 -14
- package/dist/index.js +219 -82
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @horizon-republic/nestjs-jetstream
|
|
2
4
|
|
|
3
|
-
Ship reliable microservices with NATS JetStream and NestJS
|
|
5
|
+
**Ship reliable microservices with NATS JetStream and NestJS.**
|
|
6
|
+
Events, broadcast, ordered delivery, and RPC — with two lines of config.
|
|
4
7
|
|
|
5
8
|
[](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
|
|
6
9
|
[](https://codecov.io/github/HorizonRepublic/nestjs-jetstream)
|
|
10
|
+
[](https://github.com/HorizonRepublic/nestjs-jetstream/actions)
|
|
11
|
+
[](https://horizonrepublic.github.io/nestjs-jetstream/)
|
|
12
|
+
|
|
13
|
+
[](https://nodejs.org)
|
|
14
|
+
[](https://www.typescriptlang.org)
|
|
7
15
|
[](https://opensource.org/licenses/MIT)
|
|
8
|
-
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why this library?
|
|
22
|
+
|
|
23
|
+
NestJS ships with a NATS transport, but it's fire-and-forget. Messages vanish if no one's listening. This library adds JetStream — so your messages **survive restarts**, **retry on failure**, and **replay for new consumers**.
|
|
24
|
+
|
|
25
|
+
You keep writing `@EventPattern()` and `@MessagePattern()`. The library handles streams, consumers, and subjects automatically.
|
|
26
|
+
|
|
27
|
+
## What's inside
|
|
28
|
+
|
|
29
|
+
**Delivery modes** — workqueue (one consumer), broadcast (all consumers), ordered (sequential), and dual-mode RPC (Core or JetStream-backed).
|
|
30
|
+
|
|
31
|
+
**Production-ready** — dead letter queue, health checks, graceful shutdown with drain, lifecycle hooks for observability.
|
|
32
|
+
|
|
33
|
+
**Flexible** — pluggable codecs (JSON/MsgPack/Protobuf), per-stream configuration, publisher-only mode for API gateways.
|
|
9
34
|
|
|
10
35
|
## Quick Start
|
|
11
36
|
|
|
@@ -44,9 +69,13 @@ export class OrdersController {
|
|
|
44
69
|
|
|
45
70
|
**[Read the full documentation →](https://horizonrepublic.github.io/nestjs-jetstream/)**
|
|
46
71
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
| Section | What you'll learn |
|
|
73
|
+
|---------|-------------------|
|
|
74
|
+
| [Getting Started](https://horizonrepublic.github.io/nestjs-jetstream/docs/getting-started/installation) | Installation, module setup, first handler |
|
|
75
|
+
| [Messaging Patterns](https://horizonrepublic.github.io/nestjs-jetstream/docs/patterns/rpc) | RPC, Events, Broadcast, Ordered Events |
|
|
76
|
+
| [Guides](https://horizonrepublic.github.io/nestjs-jetstream/docs/guides/record-builder) | Handler context, DLQ, health checks, performance tuning |
|
|
77
|
+
| [Migration](https://horizonrepublic.github.io/nestjs-jetstream/docs/guides/migration) | From built-in NATS transport or between versions |
|
|
78
|
+
| [API Reference](https://horizonrepublic.github.io/nestjs-jetstream/docs/reference/api/) | Full TypeDoc-generated API |
|
|
50
79
|
|
|
51
80
|
## Links
|
|
52
81
|
|
package/dist/index.cjs
CHANGED
|
@@ -47,9 +47,13 @@ __export(index_exports, {
|
|
|
47
47
|
RpcContext: () => RpcContext,
|
|
48
48
|
StreamKind: () => StreamKind,
|
|
49
49
|
TransportEvent: () => TransportEvent,
|
|
50
|
+
buildSubject: () => buildSubject,
|
|
51
|
+
consumerName: () => consumerName,
|
|
50
52
|
getClientToken: () => getClientToken,
|
|
53
|
+
internalName: () => internalName,
|
|
51
54
|
isCoreRpcMode: () => isCoreRpcMode,
|
|
52
55
|
isJetStreamRpcMode: () => isJetStreamRpcMode,
|
|
56
|
+
streamName: () => streamName,
|
|
53
57
|
toNanos: () => toNanos
|
|
54
58
|
});
|
|
55
59
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -830,6 +834,23 @@ var EventBus = class {
|
|
|
830
834
|
emit(event, ...args) {
|
|
831
835
|
const hook = this.hooks[event];
|
|
832
836
|
if (!hook) return;
|
|
837
|
+
this.callHook(event, hook, ...args);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
841
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
842
|
+
*/
|
|
843
|
+
emitMessageRouted(subject, kind) {
|
|
844
|
+
const hook = this.hooks["messageRouted" /* MessageRouted */];
|
|
845
|
+
if (!hook) return;
|
|
846
|
+
this.callHook(
|
|
847
|
+
"messageRouted" /* MessageRouted */,
|
|
848
|
+
hook,
|
|
849
|
+
subject,
|
|
850
|
+
kind
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
callHook(event, hook, ...args) {
|
|
833
854
|
try {
|
|
834
855
|
const result = hook(...args);
|
|
835
856
|
if (result && typeof result.catch === "function") {
|
|
@@ -959,7 +980,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
959
980
|
this.eventRouter.start();
|
|
960
981
|
}
|
|
961
982
|
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
962
|
-
this.rpcRouter.start();
|
|
983
|
+
await this.rpcRouter.start();
|
|
963
984
|
}
|
|
964
985
|
}
|
|
965
986
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
@@ -1053,6 +1074,13 @@ var import_nats5 = require("nats");
|
|
|
1053
1074
|
// src/context/rpc.context.ts
|
|
1054
1075
|
var import_microservices3 = require("@nestjs/microservices");
|
|
1055
1076
|
var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
1077
|
+
_shouldRetry = false;
|
|
1078
|
+
_retryDelay;
|
|
1079
|
+
_shouldTerminate = false;
|
|
1080
|
+
_terminateReason;
|
|
1081
|
+
// ---------------------------------------------------------------------------
|
|
1082
|
+
// Message accessors
|
|
1083
|
+
// ---------------------------------------------------------------------------
|
|
1056
1084
|
/**
|
|
1057
1085
|
* Get the underlying NATS message.
|
|
1058
1086
|
*
|
|
@@ -1087,6 +1115,96 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
1087
1115
|
isJetStream() {
|
|
1088
1116
|
return "ack" in this.args[0];
|
|
1089
1117
|
}
|
|
1118
|
+
// ---------------------------------------------------------------------------
|
|
1119
|
+
// JetStream metadata (return undefined for Core NATS messages)
|
|
1120
|
+
// ---------------------------------------------------------------------------
|
|
1121
|
+
/** How many times this message has been delivered. */
|
|
1122
|
+
getDeliveryCount() {
|
|
1123
|
+
return this.asJetStream()?.info.deliveryCount;
|
|
1124
|
+
}
|
|
1125
|
+
/** The JetStream stream this message belongs to. */
|
|
1126
|
+
getStream() {
|
|
1127
|
+
return this.asJetStream()?.info.stream;
|
|
1128
|
+
}
|
|
1129
|
+
/** The stream sequence number. */
|
|
1130
|
+
getSequence() {
|
|
1131
|
+
return this.asJetStream()?.seq;
|
|
1132
|
+
}
|
|
1133
|
+
/** The message timestamp as a `Date` (derived from `info.timestampNanos`). */
|
|
1134
|
+
getTimestamp() {
|
|
1135
|
+
const nanos = this.asJetStream()?.info.timestampNanos;
|
|
1136
|
+
return typeof nanos === "number" ? new Date(nanos / 1e6) : void 0;
|
|
1137
|
+
}
|
|
1138
|
+
/** The name of the service that published this message (from `x-caller-name` header). */
|
|
1139
|
+
getCallerName() {
|
|
1140
|
+
return this.getHeader("x-caller-name" /* CallerName */);
|
|
1141
|
+
}
|
|
1142
|
+
// ---------------------------------------------------------------------------
|
|
1143
|
+
// Handler-controlled settlement
|
|
1144
|
+
// ---------------------------------------------------------------------------
|
|
1145
|
+
/**
|
|
1146
|
+
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1147
|
+
*
|
|
1148
|
+
* Use for business-level retries without throwing errors.
|
|
1149
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1150
|
+
*
|
|
1151
|
+
* @param opts - Optional delay in ms before redelivery.
|
|
1152
|
+
* @throws Error if {@link terminate} was already called.
|
|
1153
|
+
*/
|
|
1154
|
+
retry(opts) {
|
|
1155
|
+
this.assertJetStream("retry");
|
|
1156
|
+
if (this._shouldTerminate) {
|
|
1157
|
+
throw new Error("Cannot retry \u2014 terminate() was already called");
|
|
1158
|
+
}
|
|
1159
|
+
this._shouldRetry = true;
|
|
1160
|
+
this._retryDelay = opts?.delayMs;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Signal the transport to permanently reject (term) this message.
|
|
1164
|
+
*
|
|
1165
|
+
* Use when a message is no longer relevant and should not be retried or sent to DLQ.
|
|
1166
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1167
|
+
*
|
|
1168
|
+
* @param reason - Optional reason for termination (logged by NATS).
|
|
1169
|
+
* @throws Error if {@link retry} was already called.
|
|
1170
|
+
*/
|
|
1171
|
+
terminate(reason) {
|
|
1172
|
+
this.assertJetStream("terminate");
|
|
1173
|
+
if (this._shouldRetry) {
|
|
1174
|
+
throw new Error("Cannot terminate \u2014 retry() was already called");
|
|
1175
|
+
}
|
|
1176
|
+
this._shouldTerminate = true;
|
|
1177
|
+
this._terminateReason = reason;
|
|
1178
|
+
}
|
|
1179
|
+
/** Narrow to JsMsg or return null for Core messages. Used by metadata getters. */
|
|
1180
|
+
asJetStream() {
|
|
1181
|
+
return this.isJetStream() ? this.args[0] : null;
|
|
1182
|
+
}
|
|
1183
|
+
/** Ensure the message is JetStream — settlement actions are not available for Core NATS. */
|
|
1184
|
+
assertJetStream(method) {
|
|
1185
|
+
if (!this.isJetStream()) {
|
|
1186
|
+
throw new Error(`${method}() is only available for JetStream messages`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// ---------------------------------------------------------------------------
|
|
1190
|
+
// Transport-facing state (read by EventRouter)
|
|
1191
|
+
// ---------------------------------------------------------------------------
|
|
1192
|
+
/** @internal */
|
|
1193
|
+
get shouldRetry() {
|
|
1194
|
+
return this._shouldRetry;
|
|
1195
|
+
}
|
|
1196
|
+
/** @internal */
|
|
1197
|
+
get retryDelay() {
|
|
1198
|
+
return this._retryDelay;
|
|
1199
|
+
}
|
|
1200
|
+
/** @internal */
|
|
1201
|
+
get shouldTerminate() {
|
|
1202
|
+
return this._shouldTerminate;
|
|
1203
|
+
}
|
|
1204
|
+
/** @internal */
|
|
1205
|
+
get terminateReason() {
|
|
1206
|
+
return this._terminateReason;
|
|
1207
|
+
}
|
|
1090
1208
|
};
|
|
1091
1209
|
|
|
1092
1210
|
// src/utils/ack-extension.ts
|
|
@@ -1125,15 +1243,20 @@ var serializeError = (err) => {
|
|
|
1125
1243
|
|
|
1126
1244
|
// src/utils/unwrap-result.ts
|
|
1127
1245
|
var import_rxjs2 = require("rxjs");
|
|
1128
|
-
var
|
|
1246
|
+
var RESOLVED_VOID = Promise.resolve(void 0);
|
|
1247
|
+
var RESOLVED_NULL = Promise.resolve(null);
|
|
1248
|
+
var unwrapResult = (result) => {
|
|
1249
|
+
if (result === void 0) return RESOLVED_VOID;
|
|
1250
|
+
if (result === null) return RESOLVED_NULL;
|
|
1129
1251
|
if ((0, import_rxjs2.isObservable)(result)) {
|
|
1130
1252
|
return subscribeToFirst(result);
|
|
1131
1253
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1254
|
+
if (typeof result.then === "function") {
|
|
1255
|
+
return result.then(
|
|
1256
|
+
(resolved) => (0, import_rxjs2.isObservable)(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1257
|
+
);
|
|
1135
1258
|
}
|
|
1136
|
-
return
|
|
1259
|
+
return Promise.resolve(result);
|
|
1137
1260
|
};
|
|
1138
1261
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1139
1262
|
let done = false;
|
|
@@ -1211,7 +1334,7 @@ var CoreRpcServer = class {
|
|
|
1211
1334
|
this.respondWithError(msg, new Error(`No handler for subject: ${msg.subject}`));
|
|
1212
1335
|
return;
|
|
1213
1336
|
}
|
|
1214
|
-
this.eventBus.
|
|
1337
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
1215
1338
|
let data;
|
|
1216
1339
|
try {
|
|
1217
1340
|
data = this.codec.decode(msg.data);
|
|
@@ -1696,6 +1819,10 @@ var PatternRegistry = class {
|
|
|
1696
1819
|
registry = /* @__PURE__ */ new Map();
|
|
1697
1820
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1698
1821
|
cachedPatterns = null;
|
|
1822
|
+
_hasEvents = false;
|
|
1823
|
+
_hasCommands = false;
|
|
1824
|
+
_hasBroadcasts = false;
|
|
1825
|
+
_hasOrdered = false;
|
|
1699
1826
|
/**
|
|
1700
1827
|
* Register all handlers from the NestJS strategy.
|
|
1701
1828
|
*
|
|
@@ -1729,6 +1856,10 @@ var PatternRegistry = class {
|
|
|
1729
1856
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1730
1857
|
}
|
|
1731
1858
|
this.cachedPatterns = this.buildPatternsByKind();
|
|
1859
|
+
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
1860
|
+
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1861
|
+
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1862
|
+
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
1732
1863
|
this.logSummary();
|
|
1733
1864
|
}
|
|
1734
1865
|
/** Find handler for a full NATS subject. */
|
|
@@ -1740,16 +1871,16 @@ var PatternRegistry = class {
|
|
|
1740
1871
|
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
1741
1872
|
}
|
|
1742
1873
|
hasBroadcastHandlers() {
|
|
1743
|
-
return this.
|
|
1874
|
+
return this._hasBroadcasts;
|
|
1744
1875
|
}
|
|
1745
1876
|
hasRpcHandlers() {
|
|
1746
|
-
return this.
|
|
1877
|
+
return this._hasCommands;
|
|
1747
1878
|
}
|
|
1748
1879
|
hasEventHandlers() {
|
|
1749
|
-
return this.
|
|
1880
|
+
return this._hasEvents;
|
|
1750
1881
|
}
|
|
1751
1882
|
hasOrderedHandlers() {
|
|
1752
|
-
return this.
|
|
1883
|
+
return this._hasOrdered;
|
|
1753
1884
|
}
|
|
1754
1885
|
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1755
1886
|
getOrderedSubjects() {
|
|
@@ -1853,13 +1984,8 @@ var EventRouter = class {
|
|
|
1853
1984
|
const isOrdered = kind === "ordered" /* Ordered */;
|
|
1854
1985
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
1855
1986
|
const concurrency = this.getConcurrency(kind);
|
|
1856
|
-
const route = (msg) => (0, import_rxjs4.
|
|
1857
|
-
|
|
1858
|
-
).pipe(
|
|
1859
|
-
(0, import_rxjs4.catchError)((err) => {
|
|
1860
|
-
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
1861
|
-
return import_rxjs4.EMPTY;
|
|
1862
|
-
})
|
|
1987
|
+
const route = (msg) => (0, import_rxjs4.from)(
|
|
1988
|
+
isOrdered ? this.handleOrderedSafe(msg) : this.handleSafe(msg, ackExtensionInterval, kind)
|
|
1863
1989
|
);
|
|
1864
1990
|
const subscription = stream$.pipe(isOrdered ? (0, import_rxjs4.concatMap)(route) : (0, import_rxjs4.mergeMap)(route, concurrency)).subscribe();
|
|
1865
1991
|
this.subscriptions.push(subscription);
|
|
@@ -1874,23 +2000,36 @@ var EventRouter = class {
|
|
|
1874
2000
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
1875
2001
|
return void 0;
|
|
1876
2002
|
}
|
|
1877
|
-
/** Handle a single event message
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
this.executeHandler(
|
|
1883
|
-
|
|
2003
|
+
/** Handle a single event message with error isolation. */
|
|
2004
|
+
async handleSafe(msg, ackExtensionInterval, kind) {
|
|
2005
|
+
try {
|
|
2006
|
+
const resolved = this.decodeMessage(msg);
|
|
2007
|
+
if (!resolved) return;
|
|
2008
|
+
await this.executeHandler(
|
|
2009
|
+
resolved.handler,
|
|
2010
|
+
resolved.data,
|
|
2011
|
+
resolved.ctx,
|
|
2012
|
+
msg,
|
|
2013
|
+
ackExtensionInterval
|
|
2014
|
+
);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2017
|
+
}
|
|
1884
2018
|
}
|
|
1885
|
-
/** Handle an ordered message
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
unwrapResult(resolved.handler(resolved.data, resolved.ctx))
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2019
|
+
/** Handle an ordered message with error isolation. */
|
|
2020
|
+
async handleOrderedSafe(msg) {
|
|
2021
|
+
try {
|
|
2022
|
+
const resolved = this.decodeMessage(msg, true);
|
|
2023
|
+
if (!resolved) return;
|
|
2024
|
+
await unwrapResult(resolved.handler(resolved.data, resolved.ctx));
|
|
2025
|
+
if (resolved.ctx.shouldRetry || resolved.ctx.shouldTerminate) {
|
|
2026
|
+
this.logger.warn(
|
|
2027
|
+
`retry()/terminate() ignored for ordered message ${msg.subject} \u2014 ordered consumers auto-acknowledge`
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
2032
|
+
}
|
|
1894
2033
|
}
|
|
1895
2034
|
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
1896
2035
|
decodeMessage(msg, isOrdered = false) {
|
|
@@ -1908,7 +2047,7 @@ var EventRouter = class {
|
|
|
1908
2047
|
this.logger.error(`Decode error for ${msg.subject}:`, err);
|
|
1909
2048
|
return null;
|
|
1910
2049
|
}
|
|
1911
|
-
this.eventBus.
|
|
2050
|
+
this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
|
|
1912
2051
|
return { handler, data, ctx: new RpcContext([msg]) };
|
|
1913
2052
|
}
|
|
1914
2053
|
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
@@ -1916,7 +2055,13 @@ var EventRouter = class {
|
|
|
1916
2055
|
const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
|
|
1917
2056
|
try {
|
|
1918
2057
|
await unwrapResult(handler(data, ctx));
|
|
1919
|
-
|
|
2058
|
+
if (ctx.shouldTerminate) {
|
|
2059
|
+
msg.term(ctx.terminateReason);
|
|
2060
|
+
} else if (ctx.shouldRetry) {
|
|
2061
|
+
msg.nak(ctx.retryDelay);
|
|
2062
|
+
} else {
|
|
2063
|
+
msg.ack();
|
|
2064
|
+
}
|
|
1920
2065
|
} catch (err) {
|
|
1921
2066
|
this.logger.error(`Event handler error (${msg.subject}):`, err);
|
|
1922
2067
|
if (this.isDeadLetter(msg)) {
|
|
@@ -1983,6 +2128,7 @@ var RpcRouter = class {
|
|
|
1983
2128
|
concurrency;
|
|
1984
2129
|
resolvedAckExtensionInterval;
|
|
1985
2130
|
subscription = null;
|
|
2131
|
+
cachedNc = null;
|
|
1986
2132
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
1987
2133
|
get ackExtensionInterval() {
|
|
1988
2134
|
if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
|
|
@@ -1993,56 +2139,50 @@ var RpcRouter = class {
|
|
|
1993
2139
|
return this.resolvedAckExtensionInterval;
|
|
1994
2140
|
}
|
|
1995
2141
|
/** Start routing command messages to handlers. */
|
|
1996
|
-
start() {
|
|
1997
|
-
this.
|
|
1998
|
-
|
|
1999
|
-
(msg) => (0, import_rxjs5.defer)(() => this.handle(msg)).pipe(
|
|
2000
|
-
(0, import_rxjs5.catchError)((err) => {
|
|
2001
|
-
this.logger.error("Unexpected error in RPC router", err);
|
|
2002
|
-
return import_rxjs5.EMPTY;
|
|
2003
|
-
})
|
|
2004
|
-
),
|
|
2005
|
-
this.concurrency
|
|
2006
|
-
)
|
|
2007
|
-
).subscribe();
|
|
2142
|
+
async start() {
|
|
2143
|
+
this.cachedNc = await this.connection.getConnection();
|
|
2144
|
+
this.subscription = this.messageProvider.commands$.pipe((0, import_rxjs5.mergeMap)((msg) => (0, import_rxjs5.from)(this.handleSafe(msg)), this.concurrency)).subscribe();
|
|
2008
2145
|
}
|
|
2009
2146
|
/** Stop routing and unsubscribe. */
|
|
2010
2147
|
destroy() {
|
|
2011
2148
|
this.subscription?.unsubscribe();
|
|
2012
2149
|
this.subscription = null;
|
|
2013
2150
|
}
|
|
2014
|
-
/** Handle a single RPC command message. */
|
|
2015
|
-
|
|
2016
|
-
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
2017
|
-
if (!handler) {
|
|
2018
|
-
msg.term(`No handler for RPC: ${msg.subject}`);
|
|
2019
|
-
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2020
|
-
return import_rxjs5.EMPTY;
|
|
2021
|
-
}
|
|
2022
|
-
const replyTo = msg.headers?.get("x-reply-to" /* ReplyTo */);
|
|
2023
|
-
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
2024
|
-
if (!replyTo || !correlationId) {
|
|
2025
|
-
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2026
|
-
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2027
|
-
return import_rxjs5.EMPTY;
|
|
2028
|
-
}
|
|
2029
|
-
let data;
|
|
2151
|
+
/** Handle a single RPC command message with error isolation. */
|
|
2152
|
+
async handleSafe(msg) {
|
|
2030
2153
|
try {
|
|
2031
|
-
|
|
2154
|
+
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
2155
|
+
if (!handler) {
|
|
2156
|
+
msg.term(`No handler for RPC: ${msg.subject}`);
|
|
2157
|
+
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
const { headers: msgHeaders } = msg;
|
|
2161
|
+
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
2162
|
+
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
2163
|
+
if (!replyTo || !correlationId) {
|
|
2164
|
+
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2165
|
+
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
let data;
|
|
2169
|
+
try {
|
|
2170
|
+
data = this.codec.decode(msg.data);
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
msg.term("Decode error");
|
|
2173
|
+
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
2177
|
+
await this.executeHandler(handler, data, msg, replyTo, correlationId);
|
|
2032
2178
|
} catch (err) {
|
|
2033
|
-
|
|
2034
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2035
|
-
return import_rxjs5.EMPTY;
|
|
2179
|
+
this.logger.error("Unexpected error in RPC router", err);
|
|
2036
2180
|
}
|
|
2037
|
-
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "rpc" /* Rpc */);
|
|
2038
|
-
return (0, import_rxjs5.from)(this.executeHandler(handler, data, msg, replyTo, correlationId));
|
|
2039
2181
|
}
|
|
2040
2182
|
/** Execute handler, publish response, settle message. */
|
|
2041
2183
|
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
2042
|
-
const nc = await this.connection.getConnection();
|
|
2184
|
+
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
2043
2185
|
const ctx = new RpcContext([msg]);
|
|
2044
|
-
const hdrs = (0, import_nats9.headers)();
|
|
2045
|
-
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2046
2186
|
let settled = false;
|
|
2047
2187
|
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
2048
2188
|
const timeoutId = setTimeout(() => {
|
|
@@ -2061,6 +2201,8 @@ var RpcRouter = class {
|
|
|
2061
2201
|
stopAckExtension?.();
|
|
2062
2202
|
msg.ack();
|
|
2063
2203
|
try {
|
|
2204
|
+
const hdrs = (0, import_nats9.headers)();
|
|
2205
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2064
2206
|
nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
|
|
2065
2207
|
} catch (publishErr) {
|
|
2066
2208
|
this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
|
|
@@ -2071,6 +2213,8 @@ var RpcRouter = class {
|
|
|
2071
2213
|
clearTimeout(timeoutId);
|
|
2072
2214
|
stopAckExtension?.();
|
|
2073
2215
|
try {
|
|
2216
|
+
const hdrs = (0, import_nats9.headers)();
|
|
2217
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2074
2218
|
hdrs.set("x-error" /* Error */, "true");
|
|
2075
2219
|
nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
|
|
2076
2220
|
} catch (encodeErr) {
|
|
@@ -2519,8 +2663,12 @@ JetstreamModule = __decorateClass([
|
|
|
2519
2663
|
RpcContext,
|
|
2520
2664
|
StreamKind,
|
|
2521
2665
|
TransportEvent,
|
|
2666
|
+
buildSubject,
|
|
2667
|
+
consumerName,
|
|
2522
2668
|
getClientToken,
|
|
2669
|
+
internalName,
|
|
2523
2670
|
isCoreRpcMode,
|
|
2524
2671
|
isJetStreamRpcMode,
|
|
2672
|
+
streamName,
|
|
2525
2673
|
toNanos
|
|
2526
2674
|
});
|