@horizon-republic/nestjs-jetstream 2.3.6 → 2.5.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 +21 -996
- package/dist/index.cjs +294 -65
- package/dist/index.d.cts +116 -31
- package/dist/index.d.ts +116 -31
- package/dist/index.js +299 -63
- package/package.json +6 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -60,7 +60,14 @@ var getClientToken = (name) => name;
|
|
|
60
60
|
var KB = 1024;
|
|
61
61
|
var MB = 1024 * KB;
|
|
62
62
|
var GB = 1024 * MB;
|
|
63
|
-
var
|
|
63
|
+
var NANOS_PER = {
|
|
64
|
+
ms: 1e6,
|
|
65
|
+
seconds: 1e9,
|
|
66
|
+
minutes: 6e10,
|
|
67
|
+
hours: 36e11,
|
|
68
|
+
days: 864e11
|
|
69
|
+
};
|
|
70
|
+
var toNanos = (value, unit) => value * NANOS_PER[unit];
|
|
64
71
|
var baseStreamConfig = {
|
|
65
72
|
retention: RetentionPolicy.Workqueue,
|
|
66
73
|
storage: StorageType.File,
|
|
@@ -77,10 +84,8 @@ var DEFAULT_EVENT_STREAM_CONFIG = {
|
|
|
77
84
|
max_msgs_per_subject: 5e6,
|
|
78
85
|
max_msgs: 5e7,
|
|
79
86
|
max_bytes: 5 * GB,
|
|
80
|
-
max_age:
|
|
81
|
-
|
|
82
|
-
duplicate_window: nanos(2 * 60 * 1e3)
|
|
83
|
-
// 2 min
|
|
87
|
+
max_age: toNanos(7, "days"),
|
|
88
|
+
duplicate_window: toNanos(2, "minutes")
|
|
84
89
|
};
|
|
85
90
|
var DEFAULT_COMMAND_STREAM_CONFIG = {
|
|
86
91
|
...baseStreamConfig,
|
|
@@ -90,10 +95,8 @@ var DEFAULT_COMMAND_STREAM_CONFIG = {
|
|
|
90
95
|
max_msgs_per_subject: 1e5,
|
|
91
96
|
max_msgs: 1e6,
|
|
92
97
|
max_bytes: 100 * MB,
|
|
93
|
-
max_age:
|
|
94
|
-
|
|
95
|
-
duplicate_window: nanos(30 * 1e3)
|
|
96
|
-
// 30s
|
|
98
|
+
max_age: toNanos(3, "minutes"),
|
|
99
|
+
duplicate_window: toNanos(30, "seconds")
|
|
97
100
|
};
|
|
98
101
|
var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
99
102
|
...baseStreamConfig,
|
|
@@ -104,14 +107,23 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
|
104
107
|
max_msgs_per_subject: 1e6,
|
|
105
108
|
max_msgs: 1e7,
|
|
106
109
|
max_bytes: 2 * GB,
|
|
107
|
-
max_age:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
max_age: toNanos(1, "days"),
|
|
111
|
+
duplicate_window: toNanos(2, "minutes")
|
|
112
|
+
};
|
|
113
|
+
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
114
|
+
...baseStreamConfig,
|
|
115
|
+
retention: RetentionPolicy.Limits,
|
|
116
|
+
allow_rollup_hdrs: false,
|
|
117
|
+
max_consumers: 100,
|
|
118
|
+
max_msg_size: 10 * MB,
|
|
119
|
+
max_msgs_per_subject: 5e6,
|
|
120
|
+
max_msgs: 5e7,
|
|
121
|
+
max_bytes: 5 * GB,
|
|
122
|
+
max_age: toNanos(1, "days"),
|
|
123
|
+
duplicate_window: toNanos(2, "minutes")
|
|
111
124
|
};
|
|
112
125
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
113
|
-
ack_wait:
|
|
114
|
-
// 10s
|
|
126
|
+
ack_wait: toNanos(10, "seconds"),
|
|
115
127
|
max_deliver: 3,
|
|
116
128
|
max_ack_pending: 100,
|
|
117
129
|
ack_policy: AckPolicy.Explicit,
|
|
@@ -119,8 +131,7 @@ var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
|
119
131
|
replay_policy: ReplayPolicy.Instant
|
|
120
132
|
};
|
|
121
133
|
var DEFAULT_COMMAND_CONSUMER_CONFIG = {
|
|
122
|
-
ack_wait:
|
|
123
|
-
// 5 min
|
|
134
|
+
ack_wait: toNanos(5, "minutes"),
|
|
124
135
|
max_deliver: 1,
|
|
125
136
|
max_ack_pending: 100,
|
|
126
137
|
ack_policy: AckPolicy.Explicit,
|
|
@@ -128,8 +139,7 @@ var DEFAULT_COMMAND_CONSUMER_CONFIG = {
|
|
|
128
139
|
replay_policy: ReplayPolicy.Instant
|
|
129
140
|
};
|
|
130
141
|
var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
|
|
131
|
-
ack_wait:
|
|
132
|
-
// 10s
|
|
142
|
+
ack_wait: toNanos(10, "seconds"),
|
|
133
143
|
max_deliver: 3,
|
|
134
144
|
max_ack_pending: 100,
|
|
135
145
|
ack_policy: AckPolicy.Explicit,
|
|
@@ -144,9 +154,6 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
144
154
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
145
155
|
JetstreamHeader2["Subject"] = "x-subject";
|
|
146
156
|
JetstreamHeader2["CallerName"] = "x-caller-name";
|
|
147
|
-
JetstreamHeader2["RequestId"] = "x-request-id";
|
|
148
|
-
JetstreamHeader2["TraceId"] = "x-trace-id";
|
|
149
|
-
JetstreamHeader2["SpanId"] = "x-span-id";
|
|
150
157
|
JetstreamHeader2["Error"] = "x-error";
|
|
151
158
|
return JetstreamHeader2;
|
|
152
159
|
})(JetstreamHeader || {});
|
|
@@ -169,16 +176,18 @@ var consumerName = (serviceName, kind) => {
|
|
|
169
176
|
|
|
170
177
|
// src/client/jetstream.record.ts
|
|
171
178
|
var JetstreamRecord = class {
|
|
172
|
-
constructor(data, headers2, timeout) {
|
|
179
|
+
constructor(data, headers2, timeout, messageId) {
|
|
173
180
|
this.data = data;
|
|
174
181
|
this.headers = headers2;
|
|
175
182
|
this.timeout = timeout;
|
|
183
|
+
this.messageId = messageId;
|
|
176
184
|
}
|
|
177
185
|
};
|
|
178
186
|
var JetstreamRecordBuilder = class {
|
|
179
187
|
data;
|
|
180
188
|
headers = /* @__PURE__ */ new Map();
|
|
181
189
|
timeout;
|
|
190
|
+
messageId;
|
|
182
191
|
constructor(data) {
|
|
183
192
|
this.data = data;
|
|
184
193
|
}
|
|
@@ -215,6 +224,28 @@ var JetstreamRecordBuilder = class {
|
|
|
215
224
|
}
|
|
216
225
|
return this;
|
|
217
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Set a custom message ID for JetStream deduplication.
|
|
229
|
+
*
|
|
230
|
+
* NATS JetStream uses this ID to detect duplicate publishes within the
|
|
231
|
+
* stream's `duplicate_window`. If two messages with the same ID arrive
|
|
232
|
+
* within the window, the second is silently dropped.
|
|
233
|
+
*
|
|
234
|
+
* When not set, a random UUID is generated automatically.
|
|
235
|
+
*
|
|
236
|
+
* @param id - Unique message identifier (e.g. order ID, idempotency key).
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* new JetstreamRecordBuilder(data)
|
|
241
|
+
* .setMessageId(`order-${order.id}`)
|
|
242
|
+
* .build();
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
setMessageId(id) {
|
|
246
|
+
this.messageId = id;
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
218
249
|
/**
|
|
219
250
|
* Set per-request RPC timeout.
|
|
220
251
|
*
|
|
@@ -230,7 +261,12 @@ var JetstreamRecordBuilder = class {
|
|
|
230
261
|
* @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
|
|
231
262
|
*/
|
|
232
263
|
build() {
|
|
233
|
-
return new JetstreamRecord(
|
|
264
|
+
return new JetstreamRecord(
|
|
265
|
+
this.data,
|
|
266
|
+
new Map(this.headers),
|
|
267
|
+
this.timeout,
|
|
268
|
+
this.messageId
|
|
269
|
+
);
|
|
234
270
|
}
|
|
235
271
|
/** Validate that a header key is not reserved. */
|
|
236
272
|
validateHeaderKey(key) {
|
|
@@ -310,12 +346,12 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
310
346
|
*/
|
|
311
347
|
async dispatchEvent(packet) {
|
|
312
348
|
const nc = await this.connect();
|
|
313
|
-
const { data, hdrs } = this.extractRecordData(packet.data);
|
|
349
|
+
const { data, hdrs, messageId } = this.extractRecordData(packet.data);
|
|
314
350
|
const subject = this.buildEventSubject(packet.pattern);
|
|
315
351
|
const msgHeaders = this.buildHeaders(hdrs, { subject });
|
|
316
352
|
const ack = await nc.jetstream().publish(subject, this.codec.encode(data), {
|
|
317
353
|
headers: msgHeaders,
|
|
318
|
-
msgID: crypto.randomUUID()
|
|
354
|
+
msgID: messageId ?? crypto.randomUUID()
|
|
319
355
|
});
|
|
320
356
|
if (ack.duplicate) {
|
|
321
357
|
this.logger.warn(`Duplicate event publish detected: ${subject} (seq: ${ack.seq})`);
|
|
@@ -330,7 +366,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
330
366
|
*/
|
|
331
367
|
publish(packet, callback) {
|
|
332
368
|
const subject = buildSubject(this.targetName, "cmd", packet.pattern);
|
|
333
|
-
const { data, hdrs, timeout } = this.extractRecordData(packet.data);
|
|
369
|
+
const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
|
|
334
370
|
const onUnhandled = (err) => {
|
|
335
371
|
this.logger.error("Unhandled publish error:", err);
|
|
336
372
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
@@ -346,7 +382,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
346
382
|
hdrs,
|
|
347
383
|
timeout,
|
|
348
384
|
callback,
|
|
349
|
-
jetStreamCorrelationId
|
|
385
|
+
jetStreamCorrelationId,
|
|
386
|
+
messageId
|
|
350
387
|
).catch(onUnhandled);
|
|
351
388
|
}
|
|
352
389
|
return () => {
|
|
@@ -384,7 +421,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
384
421
|
}
|
|
385
422
|
}
|
|
386
423
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
387
|
-
async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID()) {
|
|
424
|
+
async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID(), messageId) {
|
|
388
425
|
const effectiveTimeout = timeout ?? this.getRpcTimeout();
|
|
389
426
|
this.pendingMessages.set(correlationId, callback);
|
|
390
427
|
const timeoutId = setTimeout(() => {
|
|
@@ -408,7 +445,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
408
445
|
});
|
|
409
446
|
await nc.jetstream().publish(subject, this.codec.encode(data), {
|
|
410
447
|
headers: hdrs,
|
|
411
|
-
msgID: crypto.randomUUID()
|
|
448
|
+
msgID: messageId ?? crypto.randomUUID()
|
|
412
449
|
});
|
|
413
450
|
} catch (err) {
|
|
414
451
|
clearTimeout(timeoutId);
|
|
@@ -486,11 +523,14 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
486
523
|
this.pendingMessages.delete(correlationId);
|
|
487
524
|
}
|
|
488
525
|
}
|
|
489
|
-
/** Build event subject — workqueue or
|
|
526
|
+
/** Build event subject — workqueue, broadcast, or ordered. */
|
|
490
527
|
buildEventSubject(pattern) {
|
|
491
528
|
if (pattern.startsWith("broadcast:")) {
|
|
492
529
|
return buildBroadcastSubject(pattern.slice("broadcast:".length));
|
|
493
530
|
}
|
|
531
|
+
if (pattern.startsWith("ordered:")) {
|
|
532
|
+
return buildSubject(this.targetName, "ordered", pattern.slice("ordered:".length));
|
|
533
|
+
}
|
|
494
534
|
return buildSubject(this.targetName, "ev", pattern);
|
|
495
535
|
}
|
|
496
536
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
@@ -517,10 +557,11 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
517
557
|
return {
|
|
518
558
|
data: rawData.data,
|
|
519
559
|
hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
|
|
520
|
-
timeout: rawData.timeout
|
|
560
|
+
timeout: rawData.timeout,
|
|
561
|
+
messageId: rawData.messageId
|
|
521
562
|
};
|
|
522
563
|
}
|
|
523
|
-
return { data: rawData, hdrs: null, timeout: void 0 };
|
|
564
|
+
return { data: rawData, hdrs: null, timeout: void 0, messageId: void 0 };
|
|
524
565
|
}
|
|
525
566
|
isCoreRpcMode() {
|
|
526
567
|
return !this.rootOptions.rpc || this.rootOptions.rpc.mode === "core";
|
|
@@ -822,12 +863,23 @@ var JetstreamStrategy = class extends Server {
|
|
|
822
863
|
this.started = true;
|
|
823
864
|
this.patternRegistry.registerHandlers(this.getHandlers());
|
|
824
865
|
const streamKinds = this.resolveStreamKinds();
|
|
866
|
+
const durableKinds = this.resolveDurableConsumerKinds();
|
|
825
867
|
if (streamKinds.length > 0) {
|
|
826
868
|
await this.streamProvider.ensureStreams(streamKinds);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
869
|
+
if (durableKinds.length > 0) {
|
|
870
|
+
const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
|
|
871
|
+
this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
|
|
872
|
+
this.messageProvider.start(consumers);
|
|
873
|
+
}
|
|
874
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
875
|
+
const orderedStreamName = this.streamProvider.getStreamName("ordered");
|
|
876
|
+
await this.messageProvider.startOrdered(
|
|
877
|
+
orderedStreamName,
|
|
878
|
+
this.patternRegistry.getOrderedSubjects(),
|
|
879
|
+
this.options.ordered
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
|
|
831
883
|
this.eventRouter.start();
|
|
832
884
|
}
|
|
833
885
|
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
@@ -875,8 +927,25 @@ var JetstreamStrategy = class extends Server {
|
|
|
875
927
|
getPatternRegistry() {
|
|
876
928
|
return this.patternRegistry;
|
|
877
929
|
}
|
|
878
|
-
/** Determine which JetStream
|
|
930
|
+
/** Determine which JetStream streams are needed. */
|
|
879
931
|
resolveStreamKinds() {
|
|
932
|
+
const kinds = [];
|
|
933
|
+
if (this.patternRegistry.hasEventHandlers()) {
|
|
934
|
+
kinds.push("ev");
|
|
935
|
+
}
|
|
936
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
937
|
+
kinds.push("ordered");
|
|
938
|
+
}
|
|
939
|
+
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
940
|
+
kinds.push("cmd");
|
|
941
|
+
}
|
|
942
|
+
if (this.patternRegistry.hasBroadcastHandlers()) {
|
|
943
|
+
kinds.push("broadcast");
|
|
944
|
+
}
|
|
945
|
+
return kinds;
|
|
946
|
+
}
|
|
947
|
+
/** Determine which stream kinds need durable consumers (ordered consumers are ephemeral). */
|
|
948
|
+
resolveDurableConsumerKinds() {
|
|
880
949
|
const kinds = [];
|
|
881
950
|
if (this.patternRegistry.hasEventHandlers()) {
|
|
882
951
|
kinds.push("ev");
|
|
@@ -1103,6 +1172,8 @@ var StreamProvider = class {
|
|
|
1103
1172
|
return [`${name}.cmd.>`];
|
|
1104
1173
|
case "broadcast":
|
|
1105
1174
|
return ["broadcast.>"];
|
|
1175
|
+
case "ordered":
|
|
1176
|
+
return [`${name}.ordered.>`];
|
|
1106
1177
|
}
|
|
1107
1178
|
}
|
|
1108
1179
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
@@ -1145,6 +1216,8 @@ var StreamProvider = class {
|
|
|
1145
1216
|
return DEFAULT_COMMAND_STREAM_CONFIG;
|
|
1146
1217
|
case "broadcast":
|
|
1147
1218
|
return DEFAULT_BROADCAST_STREAM_CONFIG;
|
|
1219
|
+
case "ordered":
|
|
1220
|
+
return DEFAULT_ORDERED_STREAM_CONFIG;
|
|
1148
1221
|
}
|
|
1149
1222
|
}
|
|
1150
1223
|
/** Get user-provided overrides for a stream kind. */
|
|
@@ -1156,6 +1229,8 @@ var StreamProvider = class {
|
|
|
1156
1229
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1157
1230
|
case "broadcast":
|
|
1158
1231
|
return this.options.broadcast?.stream ?? {};
|
|
1232
|
+
case "ordered":
|
|
1233
|
+
return this.options.ordered?.stream ?? {};
|
|
1159
1234
|
}
|
|
1160
1235
|
}
|
|
1161
1236
|
};
|
|
@@ -1257,6 +1332,8 @@ var ConsumerProvider = class {
|
|
|
1257
1332
|
return DEFAULT_COMMAND_CONSUMER_CONFIG;
|
|
1258
1333
|
case "broadcast":
|
|
1259
1334
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1335
|
+
case "ordered":
|
|
1336
|
+
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1260
1337
|
}
|
|
1261
1338
|
}
|
|
1262
1339
|
/** Get user-provided overrides for a consumer kind. */
|
|
@@ -1268,12 +1345,15 @@ var ConsumerProvider = class {
|
|
|
1268
1345
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
1269
1346
|
case "broadcast":
|
|
1270
1347
|
return this.options.broadcast?.consumer ?? {};
|
|
1348
|
+
case "ordered":
|
|
1349
|
+
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1271
1350
|
}
|
|
1272
1351
|
}
|
|
1273
1352
|
};
|
|
1274
1353
|
|
|
1275
1354
|
// src/server/infrastructure/message.provider.ts
|
|
1276
1355
|
import { Logger as Logger7 } from "@nestjs/common";
|
|
1356
|
+
import { DeliverPolicy as DeliverPolicy2 } from "nats";
|
|
1277
1357
|
import {
|
|
1278
1358
|
catchError,
|
|
1279
1359
|
defer as defer2,
|
|
@@ -1292,10 +1372,13 @@ var MessageProvider = class {
|
|
|
1292
1372
|
}
|
|
1293
1373
|
logger = new Logger7("Jetstream:Message");
|
|
1294
1374
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1375
|
+
orderedReadyResolve = null;
|
|
1376
|
+
orderedReadyReject = null;
|
|
1295
1377
|
destroy$ = new Subject();
|
|
1296
1378
|
eventMessages$ = new Subject();
|
|
1297
1379
|
commandMessages$ = new Subject();
|
|
1298
1380
|
broadcastMessages$ = new Subject();
|
|
1381
|
+
orderedMessages$ = new Subject();
|
|
1299
1382
|
/** Observable stream of workqueue event messages. */
|
|
1300
1383
|
get events$() {
|
|
1301
1384
|
return this.eventMessages$.asObservable();
|
|
@@ -1308,6 +1391,10 @@ var MessageProvider = class {
|
|
|
1308
1391
|
get broadcasts$() {
|
|
1309
1392
|
return this.broadcastMessages$.asObservable();
|
|
1310
1393
|
}
|
|
1394
|
+
/** Observable stream of ordered event messages (strict sequential delivery). */
|
|
1395
|
+
get ordered$() {
|
|
1396
|
+
return this.orderedMessages$.asObservable();
|
|
1397
|
+
}
|
|
1311
1398
|
/**
|
|
1312
1399
|
* Start consuming messages from the given consumer infos.
|
|
1313
1400
|
*
|
|
@@ -1323,6 +1410,37 @@ var MessageProvider = class {
|
|
|
1323
1410
|
merge(...flows).pipe(takeUntil(this.destroy$)).subscribe();
|
|
1324
1411
|
}
|
|
1325
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Start an ordered consumer for strict sequential delivery.
|
|
1415
|
+
*
|
|
1416
|
+
* Unlike durable consumers, ordered consumers are ephemeral — created at
|
|
1417
|
+
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
1418
|
+
*
|
|
1419
|
+
* @param streamName - JetStream stream to consume from.
|
|
1420
|
+
* @param filterSubjects - NATS subjects to filter on.
|
|
1421
|
+
*/
|
|
1422
|
+
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1423
|
+
const consumerOpts = { filterSubjects };
|
|
1424
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== DeliverPolicy2.All) {
|
|
1425
|
+
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
1426
|
+
}
|
|
1427
|
+
if (orderedConfig?.optStartSeq !== void 0) {
|
|
1428
|
+
consumerOpts.opt_start_seq = orderedConfig.optStartSeq;
|
|
1429
|
+
}
|
|
1430
|
+
if (orderedConfig?.optStartTime !== void 0) {
|
|
1431
|
+
consumerOpts.opt_start_time = orderedConfig.optStartTime;
|
|
1432
|
+
}
|
|
1433
|
+
if (orderedConfig?.replayPolicy !== void 0) {
|
|
1434
|
+
consumerOpts.replay_policy = orderedConfig.replayPolicy;
|
|
1435
|
+
}
|
|
1436
|
+
const ready = new Promise((resolve, reject) => {
|
|
1437
|
+
this.orderedReadyResolve = resolve;
|
|
1438
|
+
this.orderedReadyReject = reject;
|
|
1439
|
+
});
|
|
1440
|
+
const flow = this.createOrderedFlow(streamName2, consumerOpts);
|
|
1441
|
+
flow.pipe(takeUntil(this.destroy$)).subscribe();
|
|
1442
|
+
return ready;
|
|
1443
|
+
}
|
|
1326
1444
|
/** Stop all consumer flows and reinitialize subjects for potential restart. */
|
|
1327
1445
|
destroy() {
|
|
1328
1446
|
this.destroy$.next();
|
|
@@ -1334,10 +1452,12 @@ var MessageProvider = class {
|
|
|
1334
1452
|
this.eventMessages$.complete();
|
|
1335
1453
|
this.commandMessages$.complete();
|
|
1336
1454
|
this.broadcastMessages$.complete();
|
|
1455
|
+
this.orderedMessages$.complete();
|
|
1337
1456
|
this.destroy$ = new Subject();
|
|
1338
1457
|
this.eventMessages$ = new Subject();
|
|
1339
1458
|
this.commandMessages$ = new Subject();
|
|
1340
1459
|
this.broadcastMessages$ = new Subject();
|
|
1460
|
+
this.orderedMessages$ = new Subject();
|
|
1341
1461
|
}
|
|
1342
1462
|
/** Create a self-healing consumer flow for a specific kind. */
|
|
1343
1463
|
createFlow(kind, info) {
|
|
@@ -1395,6 +1515,63 @@ var MessageProvider = class {
|
|
|
1395
1515
|
return this.commandMessages$;
|
|
1396
1516
|
case "broadcast":
|
|
1397
1517
|
return this.broadcastMessages$;
|
|
1518
|
+
case "ordered":
|
|
1519
|
+
return this.orderedMessages$;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/** Create a self-healing ordered consumer flow. */
|
|
1523
|
+
createOrderedFlow(streamName2, consumerOpts) {
|
|
1524
|
+
let consecutiveFailures = 0;
|
|
1525
|
+
let lastRunFailed = false;
|
|
1526
|
+
return defer2(() => this.consumeOrderedOnce(streamName2, consumerOpts)).pipe(
|
|
1527
|
+
tap(() => {
|
|
1528
|
+
lastRunFailed = false;
|
|
1529
|
+
}),
|
|
1530
|
+
catchError((err) => {
|
|
1531
|
+
consecutiveFailures++;
|
|
1532
|
+
lastRunFailed = true;
|
|
1533
|
+
this.logger.error("Ordered consumer error, will restart:", err);
|
|
1534
|
+
this.eventBus.emit(
|
|
1535
|
+
"error" /* Error */,
|
|
1536
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
1537
|
+
"message-provider"
|
|
1538
|
+
);
|
|
1539
|
+
if (this.orderedReadyReject) {
|
|
1540
|
+
this.orderedReadyReject(err);
|
|
1541
|
+
this.orderedReadyReject = null;
|
|
1542
|
+
this.orderedReadyResolve = null;
|
|
1543
|
+
}
|
|
1544
|
+
return EMPTY;
|
|
1545
|
+
}),
|
|
1546
|
+
repeat({
|
|
1547
|
+
delay: () => {
|
|
1548
|
+
if (!lastRunFailed) {
|
|
1549
|
+
consecutiveFailures = 0;
|
|
1550
|
+
}
|
|
1551
|
+
const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
|
|
1552
|
+
this.logger.warn(`Ordered consumer stream ended, restarting in ${delay}ms...`);
|
|
1553
|
+
return timer(delay);
|
|
1554
|
+
}
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
/** Single iteration: create ordered consumer -> iterate messages. */
|
|
1559
|
+
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
1560
|
+
const js = (await this.connection.getConnection()).jetstream();
|
|
1561
|
+
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
1562
|
+
const messages = await consumer.consume();
|
|
1563
|
+
if (this.orderedReadyResolve) {
|
|
1564
|
+
this.orderedReadyResolve();
|
|
1565
|
+
this.orderedReadyResolve = null;
|
|
1566
|
+
this.orderedReadyReject = null;
|
|
1567
|
+
}
|
|
1568
|
+
this.activeIterators.add(messages);
|
|
1569
|
+
try {
|
|
1570
|
+
for await (const msg of messages) {
|
|
1571
|
+
this.orderedMessages$.next(msg);
|
|
1572
|
+
}
|
|
1573
|
+
} finally {
|
|
1574
|
+
this.activeIterators.delete(messages);
|
|
1398
1575
|
}
|
|
1399
1576
|
}
|
|
1400
1577
|
};
|
|
@@ -1415,11 +1592,20 @@ var PatternRegistry = class {
|
|
|
1415
1592
|
registerHandlers(handlers) {
|
|
1416
1593
|
const serviceName = this.options.name;
|
|
1417
1594
|
for (const [pattern, handler] of handlers) {
|
|
1595
|
+
const extras = handler.extras;
|
|
1418
1596
|
const isEvent = handler.isEventHandler ?? false;
|
|
1419
|
-
const isBroadcast = !!
|
|
1597
|
+
const isBroadcast = !!extras?.broadcast;
|
|
1598
|
+
const isOrdered = !!extras?.ordered;
|
|
1599
|
+
if (isBroadcast && isOrdered) {
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1420
1604
|
let fullSubject;
|
|
1421
1605
|
if (isBroadcast) {
|
|
1422
1606
|
fullSubject = buildBroadcastSubject(pattern);
|
|
1607
|
+
} else if (isOrdered) {
|
|
1608
|
+
fullSubject = buildSubject(serviceName, "ordered", pattern);
|
|
1423
1609
|
} else if (isEvent) {
|
|
1424
1610
|
fullSubject = buildSubject(serviceName, "ev", pattern);
|
|
1425
1611
|
} else {
|
|
@@ -1428,12 +1614,15 @@ var PatternRegistry = class {
|
|
|
1428
1614
|
this.registry.set(fullSubject, {
|
|
1429
1615
|
handler,
|
|
1430
1616
|
pattern,
|
|
1431
|
-
isEvent,
|
|
1432
|
-
isBroadcast
|
|
1617
|
+
isEvent: isEvent && !isOrdered,
|
|
1618
|
+
isBroadcast,
|
|
1619
|
+
isOrdered
|
|
1433
1620
|
});
|
|
1434
1621
|
let kind;
|
|
1435
1622
|
if (isBroadcast) {
|
|
1436
1623
|
kind = "broadcast";
|
|
1624
|
+
} else if (isOrdered) {
|
|
1625
|
+
kind = "ordered";
|
|
1437
1626
|
} else if (isEvent) {
|
|
1438
1627
|
kind = "event";
|
|
1439
1628
|
} else {
|
|
@@ -1457,28 +1646,41 @@ var PatternRegistry = class {
|
|
|
1457
1646
|
}
|
|
1458
1647
|
/** Check if any RPC (command) handlers are registered. */
|
|
1459
1648
|
hasRpcHandlers() {
|
|
1460
|
-
return Array.from(this.registry.values()).some(
|
|
1649
|
+
return Array.from(this.registry.values()).some(
|
|
1650
|
+
(r) => !r.isEvent && !r.isBroadcast && !r.isOrdered
|
|
1651
|
+
);
|
|
1461
1652
|
}
|
|
1462
1653
|
/** Check if any workqueue event handlers are registered. */
|
|
1463
1654
|
hasEventHandlers() {
|
|
1464
1655
|
return Array.from(this.registry.values()).some((r) => r.isEvent && !r.isBroadcast);
|
|
1465
1656
|
}
|
|
1657
|
+
/** Check if any ordered event handlers are registered. */
|
|
1658
|
+
hasOrderedHandlers() {
|
|
1659
|
+
return Array.from(this.registry.values()).some((r) => r.isOrdered);
|
|
1660
|
+
}
|
|
1661
|
+
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1662
|
+
getOrderedSubjects() {
|
|
1663
|
+
const name = internalName(this.options.name);
|
|
1664
|
+
return Array.from(this.registry.values()).filter((r) => r.isOrdered).map((r) => `${name}.ordered.${r.pattern}`);
|
|
1665
|
+
}
|
|
1466
1666
|
/** Get patterns grouped by kind. */
|
|
1467
1667
|
getPatternsByKind() {
|
|
1468
1668
|
const events = [];
|
|
1469
1669
|
const commands = [];
|
|
1470
1670
|
const broadcasts = [];
|
|
1671
|
+
const ordered = [];
|
|
1471
1672
|
for (const entry of this.registry.values()) {
|
|
1472
1673
|
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
1674
|
+
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
1473
1675
|
else if (entry.isEvent) events.push(entry.pattern);
|
|
1474
1676
|
else commands.push(entry.pattern);
|
|
1475
1677
|
}
|
|
1476
|
-
return { events, commands, broadcasts };
|
|
1678
|
+
return { events, commands, broadcasts, ordered };
|
|
1477
1679
|
}
|
|
1478
1680
|
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
1479
1681
|
normalizeSubject(subject) {
|
|
1480
1682
|
const name = internalName(this.options.name);
|
|
1481
|
-
const prefixes = [`${name}.cmd.`, `${name}.ev.`, "broadcast."];
|
|
1683
|
+
const prefixes = [`${name}.cmd.`, `${name}.ev.`, `${name}.ordered.`, "broadcast."];
|
|
1482
1684
|
for (const prefix of prefixes) {
|
|
1483
1685
|
if (subject.startsWith(prefix)) {
|
|
1484
1686
|
return subject.slice(prefix.length);
|
|
@@ -1488,16 +1690,29 @@ var PatternRegistry = class {
|
|
|
1488
1690
|
}
|
|
1489
1691
|
/** Log a summary of all registered handlers. */
|
|
1490
1692
|
logSummary() {
|
|
1491
|
-
const { events, commands, broadcasts } = this.getPatternsByKind();
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1693
|
+
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
1694
|
+
const parts = [
|
|
1695
|
+
`${commands.length} RPC`,
|
|
1696
|
+
`${events.length} events`,
|
|
1697
|
+
`${broadcasts.length} broadcasts`
|
|
1698
|
+
];
|
|
1699
|
+
if (ordered.length > 0) {
|
|
1700
|
+
parts.push(`${ordered.length} ordered`);
|
|
1701
|
+
}
|
|
1702
|
+
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
1495
1703
|
}
|
|
1496
1704
|
};
|
|
1497
1705
|
|
|
1498
1706
|
// src/server/routing/event.router.ts
|
|
1499
1707
|
import { Logger as Logger9 } from "@nestjs/common";
|
|
1500
|
-
import {
|
|
1708
|
+
import {
|
|
1709
|
+
catchError as catchError2,
|
|
1710
|
+
concatMap,
|
|
1711
|
+
defer as defer3,
|
|
1712
|
+
EMPTY as EMPTY2,
|
|
1713
|
+
from as from2,
|
|
1714
|
+
mergeMap
|
|
1715
|
+
} from "rxjs";
|
|
1501
1716
|
var EventRouter = class {
|
|
1502
1717
|
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig) {
|
|
1503
1718
|
this.messageProvider = messageProvider;
|
|
@@ -1516,10 +1731,13 @@ var EventRouter = class {
|
|
|
1516
1731
|
if (!this.deadLetterConfig) return;
|
|
1517
1732
|
this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
|
|
1518
1733
|
}
|
|
1519
|
-
/** Start routing event and
|
|
1734
|
+
/** Start routing event, broadcast, and ordered messages to handlers. */
|
|
1520
1735
|
start() {
|
|
1521
1736
|
this.subscribeToStream(this.messageProvider.events$, "workqueue");
|
|
1522
1737
|
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast");
|
|
1738
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
1739
|
+
this.subscribeToStream(this.messageProvider.ordered$, "ordered", true);
|
|
1740
|
+
}
|
|
1523
1741
|
}
|
|
1524
1742
|
/** Stop routing and unsubscribe from all streams. */
|
|
1525
1743
|
destroy() {
|
|
@@ -1529,17 +1747,14 @@ var EventRouter = class {
|
|
|
1529
1747
|
this.subscriptions.length = 0;
|
|
1530
1748
|
}
|
|
1531
1749
|
/** Subscribe to a message stream and route each message. */
|
|
1532
|
-
subscribeToStream(stream$, label) {
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
(
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
)
|
|
1541
|
-
)
|
|
1542
|
-
).subscribe();
|
|
1750
|
+
subscribeToStream(stream$, label, isOrdered = false) {
|
|
1751
|
+
const route = (msg) => defer3(() => isOrdered ? this.handleOrdered(msg) : this.handle(msg)).pipe(
|
|
1752
|
+
catchError2((err) => {
|
|
1753
|
+
this.logger.error(`Unexpected error in ${label} event router`, err);
|
|
1754
|
+
return EMPTY2;
|
|
1755
|
+
})
|
|
1756
|
+
);
|
|
1757
|
+
const subscription = stream$.pipe(isOrdered ? concatMap(route) : mergeMap(route)).subscribe();
|
|
1543
1758
|
this.subscriptions.push(subscription);
|
|
1544
1759
|
}
|
|
1545
1760
|
/** Handle a single event message: decode -> execute handler -> ack/nak. */
|
|
@@ -1576,6 +1791,28 @@ var EventRouter = class {
|
|
|
1576
1791
|
}
|
|
1577
1792
|
}
|
|
1578
1793
|
}
|
|
1794
|
+
/** Handle an ordered message: decode -> execute handler -> no ack/nak. */
|
|
1795
|
+
handleOrdered(msg) {
|
|
1796
|
+
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1797
|
+
if (!handler) {
|
|
1798
|
+
this.logger.error(`No handler for ordered subject: ${msg.subject}`);
|
|
1799
|
+
return EMPTY2;
|
|
1800
|
+
}
|
|
1801
|
+
let data;
|
|
1802
|
+
try {
|
|
1803
|
+
data = this.codec.decode(msg.data);
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
this.logger.error(`Decode error for ordered ${msg.subject}:`, err);
|
|
1806
|
+
return EMPTY2;
|
|
1807
|
+
}
|
|
1808
|
+
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "event");
|
|
1809
|
+
const ctx = new RpcContext([msg]);
|
|
1810
|
+
return from2(
|
|
1811
|
+
unwrapResult(handler(data, ctx)).catch((err) => {
|
|
1812
|
+
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
1813
|
+
})
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1579
1816
|
/** Check if the message has exhausted all delivery attempts. */
|
|
1580
1817
|
isDeadLetter(msg) {
|
|
1581
1818
|
if (!this.deadLetterConfig) return false;
|
|
@@ -2107,6 +2344,5 @@ export {
|
|
|
2107
2344
|
RpcContext,
|
|
2108
2345
|
TransportEvent,
|
|
2109
2346
|
getClientToken,
|
|
2110
|
-
|
|
2347
|
+
toNanos
|
|
2111
2348
|
};
|
|
2112
|
-
//# sourceMappingURL=index.js.map
|