@horizon-republic/nestjs-jetstream 2.6.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 +213 -73
- package/dist/index.d.cts +82 -13
- package/dist/index.d.ts +82 -13
- package/dist/index.js +215 -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
|
@@ -830,6 +830,23 @@ var EventBus = class {
|
|
|
830
830
|
emit(event, ...args) {
|
|
831
831
|
const hook = this.hooks[event];
|
|
832
832
|
if (!hook) return;
|
|
833
|
+
this.callHook(event, hook, ...args);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
837
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
838
|
+
*/
|
|
839
|
+
emitMessageRouted(subject, kind) {
|
|
840
|
+
const hook = this.hooks["messageRouted" /* MessageRouted */];
|
|
841
|
+
if (!hook) return;
|
|
842
|
+
this.callHook(
|
|
843
|
+
"messageRouted" /* MessageRouted */,
|
|
844
|
+
hook,
|
|
845
|
+
subject,
|
|
846
|
+
kind
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
callHook(event, hook, ...args) {
|
|
833
850
|
try {
|
|
834
851
|
const result = hook(...args);
|
|
835
852
|
if (result && typeof result.catch === "function") {
|
|
@@ -959,7 +976,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
959
976
|
this.eventRouter.start();
|
|
960
977
|
}
|
|
961
978
|
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
962
|
-
this.rpcRouter.start();
|
|
979
|
+
await this.rpcRouter.start();
|
|
963
980
|
}
|
|
964
981
|
}
|
|
965
982
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
@@ -1053,6 +1070,13 @@ var import_nats5 = require("nats");
|
|
|
1053
1070
|
// src/context/rpc.context.ts
|
|
1054
1071
|
var import_microservices3 = require("@nestjs/microservices");
|
|
1055
1072
|
var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
1073
|
+
_shouldRetry = false;
|
|
1074
|
+
_retryDelay;
|
|
1075
|
+
_shouldTerminate = false;
|
|
1076
|
+
_terminateReason;
|
|
1077
|
+
// ---------------------------------------------------------------------------
|
|
1078
|
+
// Message accessors
|
|
1079
|
+
// ---------------------------------------------------------------------------
|
|
1056
1080
|
/**
|
|
1057
1081
|
* Get the underlying NATS message.
|
|
1058
1082
|
*
|
|
@@ -1087,6 +1111,96 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
1087
1111
|
isJetStream() {
|
|
1088
1112
|
return "ack" in this.args[0];
|
|
1089
1113
|
}
|
|
1114
|
+
// ---------------------------------------------------------------------------
|
|
1115
|
+
// JetStream metadata (return undefined for Core NATS messages)
|
|
1116
|
+
// ---------------------------------------------------------------------------
|
|
1117
|
+
/** How many times this message has been delivered. */
|
|
1118
|
+
getDeliveryCount() {
|
|
1119
|
+
return this.asJetStream()?.info.deliveryCount;
|
|
1120
|
+
}
|
|
1121
|
+
/** The JetStream stream this message belongs to. */
|
|
1122
|
+
getStream() {
|
|
1123
|
+
return this.asJetStream()?.info.stream;
|
|
1124
|
+
}
|
|
1125
|
+
/** The stream sequence number. */
|
|
1126
|
+
getSequence() {
|
|
1127
|
+
return this.asJetStream()?.seq;
|
|
1128
|
+
}
|
|
1129
|
+
/** The message timestamp as a `Date` (derived from `info.timestampNanos`). */
|
|
1130
|
+
getTimestamp() {
|
|
1131
|
+
const nanos = this.asJetStream()?.info.timestampNanos;
|
|
1132
|
+
return typeof nanos === "number" ? new Date(nanos / 1e6) : void 0;
|
|
1133
|
+
}
|
|
1134
|
+
/** The name of the service that published this message (from `x-caller-name` header). */
|
|
1135
|
+
getCallerName() {
|
|
1136
|
+
return this.getHeader("x-caller-name" /* CallerName */);
|
|
1137
|
+
}
|
|
1138
|
+
// ---------------------------------------------------------------------------
|
|
1139
|
+
// Handler-controlled settlement
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
/**
|
|
1142
|
+
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1143
|
+
*
|
|
1144
|
+
* Use for business-level retries without throwing errors.
|
|
1145
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1146
|
+
*
|
|
1147
|
+
* @param opts - Optional delay in ms before redelivery.
|
|
1148
|
+
* @throws Error if {@link terminate} was already called.
|
|
1149
|
+
*/
|
|
1150
|
+
retry(opts) {
|
|
1151
|
+
this.assertJetStream("retry");
|
|
1152
|
+
if (this._shouldTerminate) {
|
|
1153
|
+
throw new Error("Cannot retry \u2014 terminate() was already called");
|
|
1154
|
+
}
|
|
1155
|
+
this._shouldRetry = true;
|
|
1156
|
+
this._retryDelay = opts?.delayMs;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Signal the transport to permanently reject (term) this message.
|
|
1160
|
+
*
|
|
1161
|
+
* Use when a message is no longer relevant and should not be retried or sent to DLQ.
|
|
1162
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1163
|
+
*
|
|
1164
|
+
* @param reason - Optional reason for termination (logged by NATS).
|
|
1165
|
+
* @throws Error if {@link retry} was already called.
|
|
1166
|
+
*/
|
|
1167
|
+
terminate(reason) {
|
|
1168
|
+
this.assertJetStream("terminate");
|
|
1169
|
+
if (this._shouldRetry) {
|
|
1170
|
+
throw new Error("Cannot terminate \u2014 retry() was already called");
|
|
1171
|
+
}
|
|
1172
|
+
this._shouldTerminate = true;
|
|
1173
|
+
this._terminateReason = reason;
|
|
1174
|
+
}
|
|
1175
|
+
/** Narrow to JsMsg or return null for Core messages. Used by metadata getters. */
|
|
1176
|
+
asJetStream() {
|
|
1177
|
+
return this.isJetStream() ? this.args[0] : null;
|
|
1178
|
+
}
|
|
1179
|
+
/** Ensure the message is JetStream — settlement actions are not available for Core NATS. */
|
|
1180
|
+
assertJetStream(method) {
|
|
1181
|
+
if (!this.isJetStream()) {
|
|
1182
|
+
throw new Error(`${method}() is only available for JetStream messages`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
// Transport-facing state (read by EventRouter)
|
|
1187
|
+
// ---------------------------------------------------------------------------
|
|
1188
|
+
/** @internal */
|
|
1189
|
+
get shouldRetry() {
|
|
1190
|
+
return this._shouldRetry;
|
|
1191
|
+
}
|
|
1192
|
+
/** @internal */
|
|
1193
|
+
get retryDelay() {
|
|
1194
|
+
return this._retryDelay;
|
|
1195
|
+
}
|
|
1196
|
+
/** @internal */
|
|
1197
|
+
get shouldTerminate() {
|
|
1198
|
+
return this._shouldTerminate;
|
|
1199
|
+
}
|
|
1200
|
+
/** @internal */
|
|
1201
|
+
get terminateReason() {
|
|
1202
|
+
return this._terminateReason;
|
|
1203
|
+
}
|
|
1090
1204
|
};
|
|
1091
1205
|
|
|
1092
1206
|
// src/utils/ack-extension.ts
|
|
@@ -1125,15 +1239,20 @@ var serializeError = (err) => {
|
|
|
1125
1239
|
|
|
1126
1240
|
// src/utils/unwrap-result.ts
|
|
1127
1241
|
var import_rxjs2 = require("rxjs");
|
|
1128
|
-
var
|
|
1242
|
+
var RESOLVED_VOID = Promise.resolve(void 0);
|
|
1243
|
+
var RESOLVED_NULL = Promise.resolve(null);
|
|
1244
|
+
var unwrapResult = (result) => {
|
|
1245
|
+
if (result === void 0) return RESOLVED_VOID;
|
|
1246
|
+
if (result === null) return RESOLVED_NULL;
|
|
1129
1247
|
if ((0, import_rxjs2.isObservable)(result)) {
|
|
1130
1248
|
return subscribeToFirst(result);
|
|
1131
1249
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1250
|
+
if (typeof result.then === "function") {
|
|
1251
|
+
return result.then(
|
|
1252
|
+
(resolved) => (0, import_rxjs2.isObservable)(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1253
|
+
);
|
|
1135
1254
|
}
|
|
1136
|
-
return
|
|
1255
|
+
return Promise.resolve(result);
|
|
1137
1256
|
};
|
|
1138
1257
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1139
1258
|
let done = false;
|
|
@@ -1211,7 +1330,7 @@ var CoreRpcServer = class {
|
|
|
1211
1330
|
this.respondWithError(msg, new Error(`No handler for subject: ${msg.subject}`));
|
|
1212
1331
|
return;
|
|
1213
1332
|
}
|
|
1214
|
-
this.eventBus.
|
|
1333
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
1215
1334
|
let data;
|
|
1216
1335
|
try {
|
|
1217
1336
|
data = this.codec.decode(msg.data);
|
|
@@ -1696,6 +1815,10 @@ var PatternRegistry = class {
|
|
|
1696
1815
|
registry = /* @__PURE__ */ new Map();
|
|
1697
1816
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1698
1817
|
cachedPatterns = null;
|
|
1818
|
+
_hasEvents = false;
|
|
1819
|
+
_hasCommands = false;
|
|
1820
|
+
_hasBroadcasts = false;
|
|
1821
|
+
_hasOrdered = false;
|
|
1699
1822
|
/**
|
|
1700
1823
|
* Register all handlers from the NestJS strategy.
|
|
1701
1824
|
*
|
|
@@ -1729,6 +1852,10 @@ var PatternRegistry = class {
|
|
|
1729
1852
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1730
1853
|
}
|
|
1731
1854
|
this.cachedPatterns = this.buildPatternsByKind();
|
|
1855
|
+
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
1856
|
+
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1857
|
+
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1858
|
+
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
1732
1859
|
this.logSummary();
|
|
1733
1860
|
}
|
|
1734
1861
|
/** Find handler for a full NATS subject. */
|
|
@@ -1740,16 +1867,16 @@ var PatternRegistry = class {
|
|
|
1740
1867
|
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
1741
1868
|
}
|
|
1742
1869
|
hasBroadcastHandlers() {
|
|
1743
|
-
return this.
|
|
1870
|
+
return this._hasBroadcasts;
|
|
1744
1871
|
}
|
|
1745
1872
|
hasRpcHandlers() {
|
|
1746
|
-
return this.
|
|
1873
|
+
return this._hasCommands;
|
|
1747
1874
|
}
|
|
1748
1875
|
hasEventHandlers() {
|
|
1749
|
-
return this.
|
|
1876
|
+
return this._hasEvents;
|
|
1750
1877
|
}
|
|
1751
1878
|
hasOrderedHandlers() {
|
|
1752
|
-
return this.
|
|
1879
|
+
return this._hasOrdered;
|
|
1753
1880
|
}
|
|
1754
1881
|
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1755
1882
|
getOrderedSubjects() {
|
|
@@ -1853,13 +1980,8 @@ var EventRouter = class {
|
|
|
1853
1980
|
const isOrdered = kind === "ordered" /* Ordered */;
|
|
1854
1981
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
1855
1982
|
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
|
-
})
|
|
1983
|
+
const route = (msg) => (0, import_rxjs4.from)(
|
|
1984
|
+
isOrdered ? this.handleOrderedSafe(msg) : this.handleSafe(msg, ackExtensionInterval, kind)
|
|
1863
1985
|
);
|
|
1864
1986
|
const subscription = stream$.pipe(isOrdered ? (0, import_rxjs4.concatMap)(route) : (0, import_rxjs4.mergeMap)(route, concurrency)).subscribe();
|
|
1865
1987
|
this.subscriptions.push(subscription);
|
|
@@ -1874,23 +1996,36 @@ var EventRouter = class {
|
|
|
1874
1996
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
1875
1997
|
return void 0;
|
|
1876
1998
|
}
|
|
1877
|
-
/** Handle a single event message
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
this.executeHandler(
|
|
1883
|
-
|
|
1999
|
+
/** Handle a single event message with error isolation. */
|
|
2000
|
+
async handleSafe(msg, ackExtensionInterval, kind) {
|
|
2001
|
+
try {
|
|
2002
|
+
const resolved = this.decodeMessage(msg);
|
|
2003
|
+
if (!resolved) return;
|
|
2004
|
+
await this.executeHandler(
|
|
2005
|
+
resolved.handler,
|
|
2006
|
+
resolved.data,
|
|
2007
|
+
resolved.ctx,
|
|
2008
|
+
msg,
|
|
2009
|
+
ackExtensionInterval
|
|
2010
|
+
);
|
|
2011
|
+
} catch (err) {
|
|
2012
|
+
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2013
|
+
}
|
|
1884
2014
|
}
|
|
1885
|
-
/** Handle an ordered message
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
unwrapResult(resolved.handler(resolved.data, resolved.ctx))
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2015
|
+
/** Handle an ordered message with error isolation. */
|
|
2016
|
+
async handleOrderedSafe(msg) {
|
|
2017
|
+
try {
|
|
2018
|
+
const resolved = this.decodeMessage(msg, true);
|
|
2019
|
+
if (!resolved) return;
|
|
2020
|
+
await unwrapResult(resolved.handler(resolved.data, resolved.ctx));
|
|
2021
|
+
if (resolved.ctx.shouldRetry || resolved.ctx.shouldTerminate) {
|
|
2022
|
+
this.logger.warn(
|
|
2023
|
+
`retry()/terminate() ignored for ordered message ${msg.subject} \u2014 ordered consumers auto-acknowledge`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
2028
|
+
}
|
|
1894
2029
|
}
|
|
1895
2030
|
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
1896
2031
|
decodeMessage(msg, isOrdered = false) {
|
|
@@ -1908,7 +2043,7 @@ var EventRouter = class {
|
|
|
1908
2043
|
this.logger.error(`Decode error for ${msg.subject}:`, err);
|
|
1909
2044
|
return null;
|
|
1910
2045
|
}
|
|
1911
|
-
this.eventBus.
|
|
2046
|
+
this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
|
|
1912
2047
|
return { handler, data, ctx: new RpcContext([msg]) };
|
|
1913
2048
|
}
|
|
1914
2049
|
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
@@ -1916,7 +2051,13 @@ var EventRouter = class {
|
|
|
1916
2051
|
const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
|
|
1917
2052
|
try {
|
|
1918
2053
|
await unwrapResult(handler(data, ctx));
|
|
1919
|
-
|
|
2054
|
+
if (ctx.shouldTerminate) {
|
|
2055
|
+
msg.term(ctx.terminateReason);
|
|
2056
|
+
} else if (ctx.shouldRetry) {
|
|
2057
|
+
msg.nak(ctx.retryDelay);
|
|
2058
|
+
} else {
|
|
2059
|
+
msg.ack();
|
|
2060
|
+
}
|
|
1920
2061
|
} catch (err) {
|
|
1921
2062
|
this.logger.error(`Event handler error (${msg.subject}):`, err);
|
|
1922
2063
|
if (this.isDeadLetter(msg)) {
|
|
@@ -1983,6 +2124,7 @@ var RpcRouter = class {
|
|
|
1983
2124
|
concurrency;
|
|
1984
2125
|
resolvedAckExtensionInterval;
|
|
1985
2126
|
subscription = null;
|
|
2127
|
+
cachedNc = null;
|
|
1986
2128
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
1987
2129
|
get ackExtensionInterval() {
|
|
1988
2130
|
if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
|
|
@@ -1993,56 +2135,50 @@ var RpcRouter = class {
|
|
|
1993
2135
|
return this.resolvedAckExtensionInterval;
|
|
1994
2136
|
}
|
|
1995
2137
|
/** 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();
|
|
2138
|
+
async start() {
|
|
2139
|
+
this.cachedNc = await this.connection.getConnection();
|
|
2140
|
+
this.subscription = this.messageProvider.commands$.pipe((0, import_rxjs5.mergeMap)((msg) => (0, import_rxjs5.from)(this.handleSafe(msg)), this.concurrency)).subscribe();
|
|
2008
2141
|
}
|
|
2009
2142
|
/** Stop routing and unsubscribe. */
|
|
2010
2143
|
destroy() {
|
|
2011
2144
|
this.subscription?.unsubscribe();
|
|
2012
2145
|
this.subscription = null;
|
|
2013
2146
|
}
|
|
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;
|
|
2147
|
+
/** Handle a single RPC command message with error isolation. */
|
|
2148
|
+
async handleSafe(msg) {
|
|
2030
2149
|
try {
|
|
2031
|
-
|
|
2150
|
+
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
2151
|
+
if (!handler) {
|
|
2152
|
+
msg.term(`No handler for RPC: ${msg.subject}`);
|
|
2153
|
+
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
const { headers: msgHeaders } = msg;
|
|
2157
|
+
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
2158
|
+
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
2159
|
+
if (!replyTo || !correlationId) {
|
|
2160
|
+
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2161
|
+
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
let data;
|
|
2165
|
+
try {
|
|
2166
|
+
data = this.codec.decode(msg.data);
|
|
2167
|
+
} catch (err) {
|
|
2168
|
+
msg.term("Decode error");
|
|
2169
|
+
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
2173
|
+
await this.executeHandler(handler, data, msg, replyTo, correlationId);
|
|
2032
2174
|
} catch (err) {
|
|
2033
|
-
|
|
2034
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2035
|
-
return import_rxjs5.EMPTY;
|
|
2175
|
+
this.logger.error("Unexpected error in RPC router", err);
|
|
2036
2176
|
}
|
|
2037
|
-
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "rpc" /* Rpc */);
|
|
2038
|
-
return (0, import_rxjs5.from)(this.executeHandler(handler, data, msg, replyTo, correlationId));
|
|
2039
2177
|
}
|
|
2040
2178
|
/** Execute handler, publish response, settle message. */
|
|
2041
2179
|
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
2042
|
-
const nc = await this.connection.getConnection();
|
|
2180
|
+
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
2043
2181
|
const ctx = new RpcContext([msg]);
|
|
2044
|
-
const hdrs = (0, import_nats9.headers)();
|
|
2045
|
-
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2046
2182
|
let settled = false;
|
|
2047
2183
|
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
2048
2184
|
const timeoutId = setTimeout(() => {
|
|
@@ -2061,6 +2197,8 @@ var RpcRouter = class {
|
|
|
2061
2197
|
stopAckExtension?.();
|
|
2062
2198
|
msg.ack();
|
|
2063
2199
|
try {
|
|
2200
|
+
const hdrs = (0, import_nats9.headers)();
|
|
2201
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2064
2202
|
nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
|
|
2065
2203
|
} catch (publishErr) {
|
|
2066
2204
|
this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
|
|
@@ -2071,6 +2209,8 @@ var RpcRouter = class {
|
|
|
2071
2209
|
clearTimeout(timeoutId);
|
|
2072
2210
|
stopAckExtension?.();
|
|
2073
2211
|
try {
|
|
2212
|
+
const hdrs = (0, import_nats9.headers)();
|
|
2213
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2074
2214
|
hdrs.set("x-error" /* Error */, "true");
|
|
2075
2215
|
nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
|
|
2076
2216
|
} catch (encodeErr) {
|
package/dist/index.d.cts
CHANGED
|
@@ -416,6 +416,12 @@ declare class EventBus {
|
|
|
416
416
|
* @param args - Arguments matching the hook signature for this event.
|
|
417
417
|
*/
|
|
418
418
|
emit<K extends keyof TransportHooks>(event: K, ...args: Parameters<TransportHooks[K]>): void;
|
|
419
|
+
/**
|
|
420
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
421
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
422
|
+
*/
|
|
423
|
+
emitMessageRouted(subject: string, kind: MessageKind): void;
|
|
424
|
+
private callHook;
|
|
419
425
|
}
|
|
420
426
|
|
|
421
427
|
/**
|
|
@@ -491,6 +497,10 @@ declare class PatternRegistry {
|
|
|
491
497
|
private readonly logger;
|
|
492
498
|
private readonly registry;
|
|
493
499
|
private cachedPatterns;
|
|
500
|
+
private _hasEvents;
|
|
501
|
+
private _hasCommands;
|
|
502
|
+
private _hasBroadcasts;
|
|
503
|
+
private _hasOrdered;
|
|
494
504
|
constructor(options: JetstreamModuleOptions);
|
|
495
505
|
/**
|
|
496
506
|
* Register all handlers from the NestJS strategy.
|
|
@@ -613,10 +623,10 @@ declare class EventRouter {
|
|
|
613
623
|
private subscribeToStream;
|
|
614
624
|
private getConcurrency;
|
|
615
625
|
private getAckExtensionConfig;
|
|
616
|
-
/** Handle a single event message
|
|
617
|
-
private
|
|
618
|
-
/** Handle an ordered message
|
|
619
|
-
private
|
|
626
|
+
/** Handle a single event message with error isolation. */
|
|
627
|
+
private handleSafe;
|
|
628
|
+
/** Handle an ordered message with error isolation. */
|
|
629
|
+
private handleOrderedSafe;
|
|
620
630
|
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
621
631
|
private decodeMessage;
|
|
622
632
|
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
@@ -652,15 +662,16 @@ declare class RpcRouter {
|
|
|
652
662
|
private readonly concurrency;
|
|
653
663
|
private resolvedAckExtensionInterval;
|
|
654
664
|
private subscription;
|
|
665
|
+
private cachedNc;
|
|
655
666
|
constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, connection: ConnectionProvider, codec: Codec, eventBus: EventBus, rpcOptions?: RpcRouterOptions | undefined, ackWaitMap?: Map<StreamKind, number> | undefined);
|
|
656
667
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
657
668
|
private get ackExtensionInterval();
|
|
658
669
|
/** Start routing command messages to handlers. */
|
|
659
|
-
start(): void
|
|
670
|
+
start(): Promise<void>;
|
|
660
671
|
/** Stop routing and unsubscribe. */
|
|
661
672
|
destroy(): void;
|
|
662
|
-
/** Handle a single RPC command message. */
|
|
663
|
-
private
|
|
673
|
+
/** Handle a single RPC command message with error isolation. */
|
|
674
|
+
private handleSafe;
|
|
664
675
|
/** Execute handler, publish response, settle message. */
|
|
665
676
|
private executeHandler;
|
|
666
677
|
}
|
|
@@ -1137,19 +1148,33 @@ type NatsMessage = JsMsg | Msg;
|
|
|
1137
1148
|
* Execution context for RPC and event handlers.
|
|
1138
1149
|
*
|
|
1139
1150
|
* Provides convenient accessors for the NATS message, subject,
|
|
1140
|
-
* and
|
|
1151
|
+
* headers, and JetStream metadata without needing to interact
|
|
1152
|
+
* with the raw message directly.
|
|
1153
|
+
*
|
|
1154
|
+
* Handlers can also control message settlement via {@link retry}
|
|
1155
|
+
* and {@link terminate} instead of throwing errors.
|
|
1141
1156
|
*
|
|
1142
1157
|
* @example
|
|
1143
1158
|
* ```typescript
|
|
1144
|
-
* @
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1159
|
+
* @EventPattern('order.process')
|
|
1160
|
+
* async handle(@Payload() data: OrderDto, @Ctx() ctx: RpcContext) {
|
|
1161
|
+
* if (ctx.getDeliveryCount()! >= 3) {
|
|
1162
|
+
* ctx.terminate('Max business retries exceeded');
|
|
1163
|
+
* return;
|
|
1164
|
+
* }
|
|
1165
|
+
* if (!this.isReady()) {
|
|
1166
|
+
* ctx.retry({ delay: 5000 });
|
|
1167
|
+
* return;
|
|
1168
|
+
* }
|
|
1169
|
+
* await this.process(data);
|
|
1149
1170
|
* }
|
|
1150
1171
|
* ```
|
|
1151
1172
|
*/
|
|
1152
1173
|
declare class RpcContext extends BaseRpcContext<[NatsMessage]> {
|
|
1174
|
+
private _shouldRetry;
|
|
1175
|
+
private _retryDelay;
|
|
1176
|
+
private _shouldTerminate;
|
|
1177
|
+
private _terminateReason;
|
|
1153
1178
|
/**
|
|
1154
1179
|
* Get the underlying NATS message.
|
|
1155
1180
|
*
|
|
@@ -1176,6 +1201,50 @@ declare class RpcContext extends BaseRpcContext<[NatsMessage]> {
|
|
|
1176
1201
|
isJetStream(): this is RpcContext & {
|
|
1177
1202
|
getMessage(): JsMsg;
|
|
1178
1203
|
};
|
|
1204
|
+
/** How many times this message has been delivered. */
|
|
1205
|
+
getDeliveryCount(): number | undefined;
|
|
1206
|
+
/** The JetStream stream this message belongs to. */
|
|
1207
|
+
getStream(): string | undefined;
|
|
1208
|
+
/** The stream sequence number. */
|
|
1209
|
+
getSequence(): number | undefined;
|
|
1210
|
+
/** The message timestamp as a `Date` (derived from `info.timestampNanos`). */
|
|
1211
|
+
getTimestamp(): Date | undefined;
|
|
1212
|
+
/** The name of the service that published this message (from `x-caller-name` header). */
|
|
1213
|
+
getCallerName(): string | undefined;
|
|
1214
|
+
/**
|
|
1215
|
+
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1216
|
+
*
|
|
1217
|
+
* Use for business-level retries without throwing errors.
|
|
1218
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1219
|
+
*
|
|
1220
|
+
* @param opts - Optional delay in ms before redelivery.
|
|
1221
|
+
* @throws Error if {@link terminate} was already called.
|
|
1222
|
+
*/
|
|
1223
|
+
retry(opts?: {
|
|
1224
|
+
delayMs?: number;
|
|
1225
|
+
}): void;
|
|
1226
|
+
/**
|
|
1227
|
+
* Signal the transport to permanently reject (term) this message.
|
|
1228
|
+
*
|
|
1229
|
+
* Use when a message is no longer relevant and should not be retried or sent to DLQ.
|
|
1230
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1231
|
+
*
|
|
1232
|
+
* @param reason - Optional reason for termination (logged by NATS).
|
|
1233
|
+
* @throws Error if {@link retry} was already called.
|
|
1234
|
+
*/
|
|
1235
|
+
terminate(reason?: string): void;
|
|
1236
|
+
/** Narrow to JsMsg or return null for Core messages. Used by metadata getters. */
|
|
1237
|
+
private asJetStream;
|
|
1238
|
+
/** Ensure the message is JetStream — settlement actions are not available for Core NATS. */
|
|
1239
|
+
private assertJetStream;
|
|
1240
|
+
/** @internal */
|
|
1241
|
+
get shouldRetry(): boolean;
|
|
1242
|
+
/** @internal */
|
|
1243
|
+
get retryDelay(): number | undefined;
|
|
1244
|
+
/** @internal */
|
|
1245
|
+
get shouldTerminate(): boolean;
|
|
1246
|
+
/** @internal */
|
|
1247
|
+
get terminateReason(): string | undefined;
|
|
1179
1248
|
}
|
|
1180
1249
|
|
|
1181
1250
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -416,6 +416,12 @@ declare class EventBus {
|
|
|
416
416
|
* @param args - Arguments matching the hook signature for this event.
|
|
417
417
|
*/
|
|
418
418
|
emit<K extends keyof TransportHooks>(event: K, ...args: Parameters<TransportHooks[K]>): void;
|
|
419
|
+
/**
|
|
420
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
421
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
422
|
+
*/
|
|
423
|
+
emitMessageRouted(subject: string, kind: MessageKind): void;
|
|
424
|
+
private callHook;
|
|
419
425
|
}
|
|
420
426
|
|
|
421
427
|
/**
|
|
@@ -491,6 +497,10 @@ declare class PatternRegistry {
|
|
|
491
497
|
private readonly logger;
|
|
492
498
|
private readonly registry;
|
|
493
499
|
private cachedPatterns;
|
|
500
|
+
private _hasEvents;
|
|
501
|
+
private _hasCommands;
|
|
502
|
+
private _hasBroadcasts;
|
|
503
|
+
private _hasOrdered;
|
|
494
504
|
constructor(options: JetstreamModuleOptions);
|
|
495
505
|
/**
|
|
496
506
|
* Register all handlers from the NestJS strategy.
|
|
@@ -613,10 +623,10 @@ declare class EventRouter {
|
|
|
613
623
|
private subscribeToStream;
|
|
614
624
|
private getConcurrency;
|
|
615
625
|
private getAckExtensionConfig;
|
|
616
|
-
/** Handle a single event message
|
|
617
|
-
private
|
|
618
|
-
/** Handle an ordered message
|
|
619
|
-
private
|
|
626
|
+
/** Handle a single event message with error isolation. */
|
|
627
|
+
private handleSafe;
|
|
628
|
+
/** Handle an ordered message with error isolation. */
|
|
629
|
+
private handleOrderedSafe;
|
|
620
630
|
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
621
631
|
private decodeMessage;
|
|
622
632
|
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
@@ -652,15 +662,16 @@ declare class RpcRouter {
|
|
|
652
662
|
private readonly concurrency;
|
|
653
663
|
private resolvedAckExtensionInterval;
|
|
654
664
|
private subscription;
|
|
665
|
+
private cachedNc;
|
|
655
666
|
constructor(messageProvider: MessageProvider, patternRegistry: PatternRegistry, connection: ConnectionProvider, codec: Codec, eventBus: EventBus, rpcOptions?: RpcRouterOptions | undefined, ackWaitMap?: Map<StreamKind, number> | undefined);
|
|
656
667
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
657
668
|
private get ackExtensionInterval();
|
|
658
669
|
/** Start routing command messages to handlers. */
|
|
659
|
-
start(): void
|
|
670
|
+
start(): Promise<void>;
|
|
660
671
|
/** Stop routing and unsubscribe. */
|
|
661
672
|
destroy(): void;
|
|
662
|
-
/** Handle a single RPC command message. */
|
|
663
|
-
private
|
|
673
|
+
/** Handle a single RPC command message with error isolation. */
|
|
674
|
+
private handleSafe;
|
|
664
675
|
/** Execute handler, publish response, settle message. */
|
|
665
676
|
private executeHandler;
|
|
666
677
|
}
|
|
@@ -1137,19 +1148,33 @@ type NatsMessage = JsMsg | Msg;
|
|
|
1137
1148
|
* Execution context for RPC and event handlers.
|
|
1138
1149
|
*
|
|
1139
1150
|
* Provides convenient accessors for the NATS message, subject,
|
|
1140
|
-
* and
|
|
1151
|
+
* headers, and JetStream metadata without needing to interact
|
|
1152
|
+
* with the raw message directly.
|
|
1153
|
+
*
|
|
1154
|
+
* Handlers can also control message settlement via {@link retry}
|
|
1155
|
+
* and {@link terminate} instead of throwing errors.
|
|
1141
1156
|
*
|
|
1142
1157
|
* @example
|
|
1143
1158
|
* ```typescript
|
|
1144
|
-
* @
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1159
|
+
* @EventPattern('order.process')
|
|
1160
|
+
* async handle(@Payload() data: OrderDto, @Ctx() ctx: RpcContext) {
|
|
1161
|
+
* if (ctx.getDeliveryCount()! >= 3) {
|
|
1162
|
+
* ctx.terminate('Max business retries exceeded');
|
|
1163
|
+
* return;
|
|
1164
|
+
* }
|
|
1165
|
+
* if (!this.isReady()) {
|
|
1166
|
+
* ctx.retry({ delay: 5000 });
|
|
1167
|
+
* return;
|
|
1168
|
+
* }
|
|
1169
|
+
* await this.process(data);
|
|
1149
1170
|
* }
|
|
1150
1171
|
* ```
|
|
1151
1172
|
*/
|
|
1152
1173
|
declare class RpcContext extends BaseRpcContext<[NatsMessage]> {
|
|
1174
|
+
private _shouldRetry;
|
|
1175
|
+
private _retryDelay;
|
|
1176
|
+
private _shouldTerminate;
|
|
1177
|
+
private _terminateReason;
|
|
1153
1178
|
/**
|
|
1154
1179
|
* Get the underlying NATS message.
|
|
1155
1180
|
*
|
|
@@ -1176,6 +1201,50 @@ declare class RpcContext extends BaseRpcContext<[NatsMessage]> {
|
|
|
1176
1201
|
isJetStream(): this is RpcContext & {
|
|
1177
1202
|
getMessage(): JsMsg;
|
|
1178
1203
|
};
|
|
1204
|
+
/** How many times this message has been delivered. */
|
|
1205
|
+
getDeliveryCount(): number | undefined;
|
|
1206
|
+
/** The JetStream stream this message belongs to. */
|
|
1207
|
+
getStream(): string | undefined;
|
|
1208
|
+
/** The stream sequence number. */
|
|
1209
|
+
getSequence(): number | undefined;
|
|
1210
|
+
/** The message timestamp as a `Date` (derived from `info.timestampNanos`). */
|
|
1211
|
+
getTimestamp(): Date | undefined;
|
|
1212
|
+
/** The name of the service that published this message (from `x-caller-name` header). */
|
|
1213
|
+
getCallerName(): string | undefined;
|
|
1214
|
+
/**
|
|
1215
|
+
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1216
|
+
*
|
|
1217
|
+
* Use for business-level retries without throwing errors.
|
|
1218
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1219
|
+
*
|
|
1220
|
+
* @param opts - Optional delay in ms before redelivery.
|
|
1221
|
+
* @throws Error if {@link terminate} was already called.
|
|
1222
|
+
*/
|
|
1223
|
+
retry(opts?: {
|
|
1224
|
+
delayMs?: number;
|
|
1225
|
+
}): void;
|
|
1226
|
+
/**
|
|
1227
|
+
* Signal the transport to permanently reject (term) this message.
|
|
1228
|
+
*
|
|
1229
|
+
* Use when a message is no longer relevant and should not be retried or sent to DLQ.
|
|
1230
|
+
* Only affects JetStream event handlers (workqueue/broadcast).
|
|
1231
|
+
*
|
|
1232
|
+
* @param reason - Optional reason for termination (logged by NATS).
|
|
1233
|
+
* @throws Error if {@link retry} was already called.
|
|
1234
|
+
*/
|
|
1235
|
+
terminate(reason?: string): void;
|
|
1236
|
+
/** Narrow to JsMsg or return null for Core messages. Used by metadata getters. */
|
|
1237
|
+
private asJetStream;
|
|
1238
|
+
/** Ensure the message is JetStream — settlement actions are not available for Core NATS. */
|
|
1239
|
+
private assertJetStream;
|
|
1240
|
+
/** @internal */
|
|
1241
|
+
get shouldRetry(): boolean;
|
|
1242
|
+
/** @internal */
|
|
1243
|
+
get retryDelay(): number | undefined;
|
|
1244
|
+
/** @internal */
|
|
1245
|
+
get shouldTerminate(): boolean;
|
|
1246
|
+
/** @internal */
|
|
1247
|
+
get terminateReason(): string | undefined;
|
|
1179
1248
|
}
|
|
1180
1249
|
|
|
1181
1250
|
/**
|
package/dist/index.js
CHANGED
|
@@ -809,6 +809,23 @@ var EventBus = class {
|
|
|
809
809
|
emit(event, ...args) {
|
|
810
810
|
const hook = this.hooks[event];
|
|
811
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) {
|
|
812
829
|
try {
|
|
813
830
|
const result = hook(...args);
|
|
814
831
|
if (result && typeof result.catch === "function") {
|
|
@@ -938,7 +955,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
938
955
|
this.eventRouter.start();
|
|
939
956
|
}
|
|
940
957
|
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
941
|
-
this.rpcRouter.start();
|
|
958
|
+
await this.rpcRouter.start();
|
|
942
959
|
}
|
|
943
960
|
}
|
|
944
961
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
@@ -1032,6 +1049,13 @@ import { headers as natsHeaders2 } from "nats";
|
|
|
1032
1049
|
// src/context/rpc.context.ts
|
|
1033
1050
|
import { BaseRpcContext } from "@nestjs/microservices";
|
|
1034
1051
|
var RpcContext = class extends BaseRpcContext {
|
|
1052
|
+
_shouldRetry = false;
|
|
1053
|
+
_retryDelay;
|
|
1054
|
+
_shouldTerminate = false;
|
|
1055
|
+
_terminateReason;
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// Message accessors
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1035
1059
|
/**
|
|
1036
1060
|
* Get the underlying NATS message.
|
|
1037
1061
|
*
|
|
@@ -1066,6 +1090,96 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
1066
1090
|
isJetStream() {
|
|
1067
1091
|
return "ack" in this.args[0];
|
|
1068
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
|
+
}
|
|
1069
1183
|
};
|
|
1070
1184
|
|
|
1071
1185
|
// src/utils/ack-extension.ts
|
|
@@ -1104,15 +1218,20 @@ var serializeError = (err) => {
|
|
|
1104
1218
|
|
|
1105
1219
|
// src/utils/unwrap-result.ts
|
|
1106
1220
|
import { isObservable } from "rxjs";
|
|
1107
|
-
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;
|
|
1108
1226
|
if (isObservable(result)) {
|
|
1109
1227
|
return subscribeToFirst(result);
|
|
1110
1228
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1229
|
+
if (typeof result.then === "function") {
|
|
1230
|
+
return result.then(
|
|
1231
|
+
(resolved) => isObservable(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1232
|
+
);
|
|
1114
1233
|
}
|
|
1115
|
-
return
|
|
1234
|
+
return Promise.resolve(result);
|
|
1116
1235
|
};
|
|
1117
1236
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1118
1237
|
let done = false;
|
|
@@ -1190,7 +1309,7 @@ var CoreRpcServer = class {
|
|
|
1190
1309
|
this.respondWithError(msg, new Error(`No handler for subject: ${msg.subject}`));
|
|
1191
1310
|
return;
|
|
1192
1311
|
}
|
|
1193
|
-
this.eventBus.
|
|
1312
|
+
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
1194
1313
|
let data;
|
|
1195
1314
|
try {
|
|
1196
1315
|
data = this.codec.decode(msg.data);
|
|
@@ -1688,6 +1807,10 @@ var PatternRegistry = class {
|
|
|
1688
1807
|
registry = /* @__PURE__ */ new Map();
|
|
1689
1808
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1690
1809
|
cachedPatterns = null;
|
|
1810
|
+
_hasEvents = false;
|
|
1811
|
+
_hasCommands = false;
|
|
1812
|
+
_hasBroadcasts = false;
|
|
1813
|
+
_hasOrdered = false;
|
|
1691
1814
|
/**
|
|
1692
1815
|
* Register all handlers from the NestJS strategy.
|
|
1693
1816
|
*
|
|
@@ -1721,6 +1844,10 @@ var PatternRegistry = class {
|
|
|
1721
1844
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1722
1845
|
}
|
|
1723
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;
|
|
1724
1851
|
this.logSummary();
|
|
1725
1852
|
}
|
|
1726
1853
|
/** Find handler for a full NATS subject. */
|
|
@@ -1732,16 +1859,16 @@ var PatternRegistry = class {
|
|
|
1732
1859
|
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
1733
1860
|
}
|
|
1734
1861
|
hasBroadcastHandlers() {
|
|
1735
|
-
return this.
|
|
1862
|
+
return this._hasBroadcasts;
|
|
1736
1863
|
}
|
|
1737
1864
|
hasRpcHandlers() {
|
|
1738
|
-
return this.
|
|
1865
|
+
return this._hasCommands;
|
|
1739
1866
|
}
|
|
1740
1867
|
hasEventHandlers() {
|
|
1741
|
-
return this.
|
|
1868
|
+
return this._hasEvents;
|
|
1742
1869
|
}
|
|
1743
1870
|
hasOrderedHandlers() {
|
|
1744
|
-
return this.
|
|
1871
|
+
return this._hasOrdered;
|
|
1745
1872
|
}
|
|
1746
1873
|
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1747
1874
|
getOrderedSubjects() {
|
|
@@ -1804,14 +1931,7 @@ var PatternRegistry = class {
|
|
|
1804
1931
|
|
|
1805
1932
|
// src/server/routing/event.router.ts
|
|
1806
1933
|
import { Logger as Logger9 } from "@nestjs/common";
|
|
1807
|
-
import {
|
|
1808
|
-
catchError as catchError2,
|
|
1809
|
-
concatMap,
|
|
1810
|
-
defer as defer3,
|
|
1811
|
-
EMPTY as EMPTY2,
|
|
1812
|
-
from as from2,
|
|
1813
|
-
mergeMap
|
|
1814
|
-
} from "rxjs";
|
|
1934
|
+
import { concatMap, from as from2, mergeMap } from "rxjs";
|
|
1815
1935
|
var EventRouter = class {
|
|
1816
1936
|
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
1817
1937
|
this.messageProvider = messageProvider;
|
|
@@ -1852,13 +1972,8 @@ var EventRouter = class {
|
|
|
1852
1972
|
const isOrdered = kind === "ordered" /* Ordered */;
|
|
1853
1973
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
1854
1974
|
const concurrency = this.getConcurrency(kind);
|
|
1855
|
-
const route = (msg) =>
|
|
1856
|
-
|
|
1857
|
-
).pipe(
|
|
1858
|
-
catchError2((err) => {
|
|
1859
|
-
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
1860
|
-
return EMPTY2;
|
|
1861
|
-
})
|
|
1975
|
+
const route = (msg) => from2(
|
|
1976
|
+
isOrdered ? this.handleOrderedSafe(msg) : this.handleSafe(msg, ackExtensionInterval, kind)
|
|
1862
1977
|
);
|
|
1863
1978
|
const subscription = stream$.pipe(isOrdered ? concatMap(route) : mergeMap(route, concurrency)).subscribe();
|
|
1864
1979
|
this.subscriptions.push(subscription);
|
|
@@ -1873,23 +1988,36 @@ var EventRouter = class {
|
|
|
1873
1988
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
1874
1989
|
return void 0;
|
|
1875
1990
|
}
|
|
1876
|
-
/** Handle a single event message
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
this.executeHandler(
|
|
1882
|
-
|
|
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
|
+
}
|
|
1883
2006
|
}
|
|
1884
|
-
/** Handle an ordered message
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
unwrapResult(resolved.handler(resolved.data, resolved.ctx))
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
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
|
+
}
|
|
1893
2021
|
}
|
|
1894
2022
|
/** Resolve handler, decode payload, and build context. Returns null on failure. */
|
|
1895
2023
|
decodeMessage(msg, isOrdered = false) {
|
|
@@ -1907,7 +2035,7 @@ var EventRouter = class {
|
|
|
1907
2035
|
this.logger.error(`Decode error for ${msg.subject}:`, err);
|
|
1908
2036
|
return null;
|
|
1909
2037
|
}
|
|
1910
|
-
this.eventBus.
|
|
2038
|
+
this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
|
|
1911
2039
|
return { handler, data, ctx: new RpcContext([msg]) };
|
|
1912
2040
|
}
|
|
1913
2041
|
/** Execute handler, then ack on success or nak/dead-letter on failure. */
|
|
@@ -1915,7 +2043,13 @@ var EventRouter = class {
|
|
|
1915
2043
|
const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
|
|
1916
2044
|
try {
|
|
1917
2045
|
await unwrapResult(handler(data, ctx));
|
|
1918
|
-
|
|
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
|
+
}
|
|
1919
2053
|
} catch (err) {
|
|
1920
2054
|
this.logger.error(`Event handler error (${msg.subject}):`, err);
|
|
1921
2055
|
if (this.isDeadLetter(msg)) {
|
|
@@ -1964,7 +2098,7 @@ var EventRouter = class {
|
|
|
1964
2098
|
// src/server/routing/rpc.router.ts
|
|
1965
2099
|
import { Logger as Logger10 } from "@nestjs/common";
|
|
1966
2100
|
import { headers } from "nats";
|
|
1967
|
-
import {
|
|
2101
|
+
import { from as from3, mergeMap as mergeMap2 } from "rxjs";
|
|
1968
2102
|
var RpcRouter = class {
|
|
1969
2103
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
1970
2104
|
this.messageProvider = messageProvider;
|
|
@@ -1982,6 +2116,7 @@ var RpcRouter = class {
|
|
|
1982
2116
|
concurrency;
|
|
1983
2117
|
resolvedAckExtensionInterval;
|
|
1984
2118
|
subscription = null;
|
|
2119
|
+
cachedNc = null;
|
|
1985
2120
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
1986
2121
|
get ackExtensionInterval() {
|
|
1987
2122
|
if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
|
|
@@ -1992,56 +2127,50 @@ var RpcRouter = class {
|
|
|
1992
2127
|
return this.resolvedAckExtensionInterval;
|
|
1993
2128
|
}
|
|
1994
2129
|
/** Start routing command messages to handlers. */
|
|
1995
|
-
start() {
|
|
1996
|
-
this.
|
|
1997
|
-
|
|
1998
|
-
(msg) => defer4(() => this.handle(msg)).pipe(
|
|
1999
|
-
catchError3((err) => {
|
|
2000
|
-
this.logger.error("Unexpected error in RPC router", err);
|
|
2001
|
-
return EMPTY3;
|
|
2002
|
-
})
|
|
2003
|
-
),
|
|
2004
|
-
this.concurrency
|
|
2005
|
-
)
|
|
2006
|
-
).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();
|
|
2007
2133
|
}
|
|
2008
2134
|
/** Stop routing and unsubscribe. */
|
|
2009
2135
|
destroy() {
|
|
2010
2136
|
this.subscription?.unsubscribe();
|
|
2011
2137
|
this.subscription = null;
|
|
2012
2138
|
}
|
|
2013
|
-
/** Handle a single RPC command message. */
|
|
2014
|
-
|
|
2015
|
-
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
2016
|
-
if (!handler) {
|
|
2017
|
-
msg.term(`No handler for RPC: ${msg.subject}`);
|
|
2018
|
-
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2019
|
-
return EMPTY3;
|
|
2020
|
-
}
|
|
2021
|
-
const replyTo = msg.headers?.get("x-reply-to" /* ReplyTo */);
|
|
2022
|
-
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
2023
|
-
if (!replyTo || !correlationId) {
|
|
2024
|
-
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2025
|
-
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2026
|
-
return EMPTY3;
|
|
2027
|
-
}
|
|
2028
|
-
let data;
|
|
2139
|
+
/** Handle a single RPC command message with error isolation. */
|
|
2140
|
+
async handleSafe(msg) {
|
|
2029
2141
|
try {
|
|
2030
|
-
|
|
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);
|
|
2031
2166
|
} catch (err) {
|
|
2032
|
-
|
|
2033
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2034
|
-
return EMPTY3;
|
|
2167
|
+
this.logger.error("Unexpected error in RPC router", err);
|
|
2035
2168
|
}
|
|
2036
|
-
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "rpc" /* Rpc */);
|
|
2037
|
-
return from3(this.executeHandler(handler, data, msg, replyTo, correlationId));
|
|
2038
2169
|
}
|
|
2039
2170
|
/** Execute handler, publish response, settle message. */
|
|
2040
2171
|
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
2041
|
-
const nc = await this.connection.getConnection();
|
|
2172
|
+
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
2042
2173
|
const ctx = new RpcContext([msg]);
|
|
2043
|
-
const hdrs = headers();
|
|
2044
|
-
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2045
2174
|
let settled = false;
|
|
2046
2175
|
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
2047
2176
|
const timeoutId = setTimeout(() => {
|
|
@@ -2060,6 +2189,8 @@ var RpcRouter = class {
|
|
|
2060
2189
|
stopAckExtension?.();
|
|
2061
2190
|
msg.ack();
|
|
2062
2191
|
try {
|
|
2192
|
+
const hdrs = headers();
|
|
2193
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2063
2194
|
nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
|
|
2064
2195
|
} catch (publishErr) {
|
|
2065
2196
|
this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
|
|
@@ -2070,6 +2201,8 @@ var RpcRouter = class {
|
|
|
2070
2201
|
clearTimeout(timeoutId);
|
|
2071
2202
|
stopAckExtension?.();
|
|
2072
2203
|
try {
|
|
2204
|
+
const hdrs = headers();
|
|
2205
|
+
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2073
2206
|
hdrs.set("x-error" /* Error */, "true");
|
|
2074
2207
|
nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
|
|
2075
2208
|
} catch (encodeErr) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@horizon-republic/nestjs-jetstream",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "A NestJS transport for NATS with JetStream events, broadcast fan-out, and Core/JetStream RPC.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/HorizonRepublic/nestjs-jetstream.git"
|
|
8
8
|
},
|
|
9
|
-
"homepage": "https://github.
|
|
9
|
+
"homepage": "https://horizonrepublic.github.io/nestjs-jetstream/",
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/HorizonRepublic/nestjs-jetstream/issues"
|
|
12
12
|
},
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@nestjs/platform-express": "^11.1.17",
|
|
68
68
|
"@nestjs/testing": "^11.1.17",
|
|
69
69
|
"@types/node": "^25.5.0",
|
|
70
|
-
"@vitest/coverage-v8": "^4.1.
|
|
70
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
71
71
|
"eslint": "^10.1.0",
|
|
72
72
|
"eslint-config-prettier": "^10.1.8",
|
|
73
73
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
"prettier": "^3.8.1",
|
|
78
78
|
"tsup": "^8.5.1",
|
|
79
79
|
"tsx": "^4.21.0",
|
|
80
|
-
"typescript": "~5.9.
|
|
80
|
+
"typescript": "~5.9.3",
|
|
81
81
|
"typescript-eslint": "^8.57.2",
|
|
82
|
-
"vitest": "^4.1.
|
|
82
|
+
"vitest": "^4.1.2"
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "tsup",
|