@horizon-republic/nestjs-jetstream 2.3.6 → 2.4.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 +22 -996
- package/dist/index.cjs +277 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -26
- package/dist/index.d.ts +107 -26
- package/dist/index.js +283 -42
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -109,6 +109,20 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
|
109
109
|
duplicate_window: nanos(2 * 60 * 1e3)
|
|
110
110
|
// 2 min
|
|
111
111
|
};
|
|
112
|
+
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
113
|
+
...baseStreamConfig,
|
|
114
|
+
retention: RetentionPolicy.Limits,
|
|
115
|
+
allow_rollup_hdrs: false,
|
|
116
|
+
max_consumers: 100,
|
|
117
|
+
max_msg_size: 10 * MB,
|
|
118
|
+
max_msgs_per_subject: 5e6,
|
|
119
|
+
max_msgs: 5e7,
|
|
120
|
+
max_bytes: 5 * GB,
|
|
121
|
+
max_age: nanos(24 * 60 * 60 * 1e3),
|
|
122
|
+
// 1 day
|
|
123
|
+
duplicate_window: nanos(2 * 60 * 1e3)
|
|
124
|
+
// 2 min
|
|
125
|
+
};
|
|
112
126
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
113
127
|
ack_wait: nanos(10 * 1e3),
|
|
114
128
|
// 10s
|
|
@@ -144,9 +158,6 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
144
158
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
145
159
|
JetstreamHeader2["Subject"] = "x-subject";
|
|
146
160
|
JetstreamHeader2["CallerName"] = "x-caller-name";
|
|
147
|
-
JetstreamHeader2["RequestId"] = "x-request-id";
|
|
148
|
-
JetstreamHeader2["TraceId"] = "x-trace-id";
|
|
149
|
-
JetstreamHeader2["SpanId"] = "x-span-id";
|
|
150
161
|
JetstreamHeader2["Error"] = "x-error";
|
|
151
162
|
return JetstreamHeader2;
|
|
152
163
|
})(JetstreamHeader || {});
|
|
@@ -169,16 +180,18 @@ var consumerName = (serviceName, kind) => {
|
|
|
169
180
|
|
|
170
181
|
// src/client/jetstream.record.ts
|
|
171
182
|
var JetstreamRecord = class {
|
|
172
|
-
constructor(data, headers2, timeout) {
|
|
183
|
+
constructor(data, headers2, timeout, messageId) {
|
|
173
184
|
this.data = data;
|
|
174
185
|
this.headers = headers2;
|
|
175
186
|
this.timeout = timeout;
|
|
187
|
+
this.messageId = messageId;
|
|
176
188
|
}
|
|
177
189
|
};
|
|
178
190
|
var JetstreamRecordBuilder = class {
|
|
179
191
|
data;
|
|
180
192
|
headers = /* @__PURE__ */ new Map();
|
|
181
193
|
timeout;
|
|
194
|
+
messageId;
|
|
182
195
|
constructor(data) {
|
|
183
196
|
this.data = data;
|
|
184
197
|
}
|
|
@@ -215,6 +228,28 @@ var JetstreamRecordBuilder = class {
|
|
|
215
228
|
}
|
|
216
229
|
return this;
|
|
217
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Set a custom message ID for JetStream deduplication.
|
|
233
|
+
*
|
|
234
|
+
* NATS JetStream uses this ID to detect duplicate publishes within the
|
|
235
|
+
* stream's `duplicate_window`. If two messages with the same ID arrive
|
|
236
|
+
* within the window, the second is silently dropped.
|
|
237
|
+
*
|
|
238
|
+
* When not set, a random UUID is generated automatically.
|
|
239
|
+
*
|
|
240
|
+
* @param id - Unique message identifier (e.g. order ID, idempotency key).
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* new JetstreamRecordBuilder(data)
|
|
245
|
+
* .setMessageId(`order-${order.id}`)
|
|
246
|
+
* .build();
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
setMessageId(id) {
|
|
250
|
+
this.messageId = id;
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
218
253
|
/**
|
|
219
254
|
* Set per-request RPC timeout.
|
|
220
255
|
*
|
|
@@ -230,7 +265,12 @@ var JetstreamRecordBuilder = class {
|
|
|
230
265
|
* @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
|
|
231
266
|
*/
|
|
232
267
|
build() {
|
|
233
|
-
return new JetstreamRecord(
|
|
268
|
+
return new JetstreamRecord(
|
|
269
|
+
this.data,
|
|
270
|
+
new Map(this.headers),
|
|
271
|
+
this.timeout,
|
|
272
|
+
this.messageId
|
|
273
|
+
);
|
|
234
274
|
}
|
|
235
275
|
/** Validate that a header key is not reserved. */
|
|
236
276
|
validateHeaderKey(key) {
|
|
@@ -310,12 +350,12 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
310
350
|
*/
|
|
311
351
|
async dispatchEvent(packet) {
|
|
312
352
|
const nc = await this.connect();
|
|
313
|
-
const { data, hdrs } = this.extractRecordData(packet.data);
|
|
353
|
+
const { data, hdrs, messageId } = this.extractRecordData(packet.data);
|
|
314
354
|
const subject = this.buildEventSubject(packet.pattern);
|
|
315
355
|
const msgHeaders = this.buildHeaders(hdrs, { subject });
|
|
316
356
|
const ack = await nc.jetstream().publish(subject, this.codec.encode(data), {
|
|
317
357
|
headers: msgHeaders,
|
|
318
|
-
msgID: crypto.randomUUID()
|
|
358
|
+
msgID: messageId ?? crypto.randomUUID()
|
|
319
359
|
});
|
|
320
360
|
if (ack.duplicate) {
|
|
321
361
|
this.logger.warn(`Duplicate event publish detected: ${subject} (seq: ${ack.seq})`);
|
|
@@ -330,7 +370,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
330
370
|
*/
|
|
331
371
|
publish(packet, callback) {
|
|
332
372
|
const subject = buildSubject(this.targetName, "cmd", packet.pattern);
|
|
333
|
-
const { data, hdrs, timeout } = this.extractRecordData(packet.data);
|
|
373
|
+
const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
|
|
334
374
|
const onUnhandled = (err) => {
|
|
335
375
|
this.logger.error("Unhandled publish error:", err);
|
|
336
376
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
@@ -346,7 +386,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
346
386
|
hdrs,
|
|
347
387
|
timeout,
|
|
348
388
|
callback,
|
|
349
|
-
jetStreamCorrelationId
|
|
389
|
+
jetStreamCorrelationId,
|
|
390
|
+
messageId
|
|
350
391
|
).catch(onUnhandled);
|
|
351
392
|
}
|
|
352
393
|
return () => {
|
|
@@ -384,7 +425,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
384
425
|
}
|
|
385
426
|
}
|
|
386
427
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
387
|
-
async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID()) {
|
|
428
|
+
async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID(), messageId) {
|
|
388
429
|
const effectiveTimeout = timeout ?? this.getRpcTimeout();
|
|
389
430
|
this.pendingMessages.set(correlationId, callback);
|
|
390
431
|
const timeoutId = setTimeout(() => {
|
|
@@ -408,7 +449,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
408
449
|
});
|
|
409
450
|
await nc.jetstream().publish(subject, this.codec.encode(data), {
|
|
410
451
|
headers: hdrs,
|
|
411
|
-
msgID: crypto.randomUUID()
|
|
452
|
+
msgID: messageId ?? crypto.randomUUID()
|
|
412
453
|
});
|
|
413
454
|
} catch (err) {
|
|
414
455
|
clearTimeout(timeoutId);
|
|
@@ -486,11 +527,14 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
486
527
|
this.pendingMessages.delete(correlationId);
|
|
487
528
|
}
|
|
488
529
|
}
|
|
489
|
-
/** Build event subject — workqueue or
|
|
530
|
+
/** Build event subject — workqueue, broadcast, or ordered. */
|
|
490
531
|
buildEventSubject(pattern) {
|
|
491
532
|
if (pattern.startsWith("broadcast:")) {
|
|
492
533
|
return buildBroadcastSubject(pattern.slice("broadcast:".length));
|
|
493
534
|
}
|
|
535
|
+
if (pattern.startsWith("ordered:")) {
|
|
536
|
+
return buildSubject(this.targetName, "ordered", pattern.slice("ordered:".length));
|
|
537
|
+
}
|
|
494
538
|
return buildSubject(this.targetName, "ev", pattern);
|
|
495
539
|
}
|
|
496
540
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
@@ -517,10 +561,11 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
517
561
|
return {
|
|
518
562
|
data: rawData.data,
|
|
519
563
|
hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
|
|
520
|
-
timeout: rawData.timeout
|
|
564
|
+
timeout: rawData.timeout,
|
|
565
|
+
messageId: rawData.messageId
|
|
521
566
|
};
|
|
522
567
|
}
|
|
523
|
-
return { data: rawData, hdrs: null, timeout: void 0 };
|
|
568
|
+
return { data: rawData, hdrs: null, timeout: void 0, messageId: void 0 };
|
|
524
569
|
}
|
|
525
570
|
isCoreRpcMode() {
|
|
526
571
|
return !this.rootOptions.rpc || this.rootOptions.rpc.mode === "core";
|
|
@@ -822,12 +867,23 @@ var JetstreamStrategy = class extends Server {
|
|
|
822
867
|
this.started = true;
|
|
823
868
|
this.patternRegistry.registerHandlers(this.getHandlers());
|
|
824
869
|
const streamKinds = this.resolveStreamKinds();
|
|
870
|
+
const durableKinds = this.resolveDurableConsumerKinds();
|
|
825
871
|
if (streamKinds.length > 0) {
|
|
826
872
|
await this.streamProvider.ensureStreams(streamKinds);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
873
|
+
if (durableKinds.length > 0) {
|
|
874
|
+
const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
|
|
875
|
+
this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
|
|
876
|
+
this.messageProvider.start(consumers);
|
|
877
|
+
}
|
|
878
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
879
|
+
const orderedStreamName = this.streamProvider.getStreamName("ordered");
|
|
880
|
+
await this.messageProvider.startOrdered(
|
|
881
|
+
orderedStreamName,
|
|
882
|
+
this.patternRegistry.getOrderedSubjects(),
|
|
883
|
+
this.options.ordered
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
|
|
831
887
|
this.eventRouter.start();
|
|
832
888
|
}
|
|
833
889
|
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
@@ -875,8 +931,25 @@ var JetstreamStrategy = class extends Server {
|
|
|
875
931
|
getPatternRegistry() {
|
|
876
932
|
return this.patternRegistry;
|
|
877
933
|
}
|
|
878
|
-
/** Determine which JetStream
|
|
934
|
+
/** Determine which JetStream streams are needed. */
|
|
879
935
|
resolveStreamKinds() {
|
|
936
|
+
const kinds = [];
|
|
937
|
+
if (this.patternRegistry.hasEventHandlers()) {
|
|
938
|
+
kinds.push("ev");
|
|
939
|
+
}
|
|
940
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
941
|
+
kinds.push("ordered");
|
|
942
|
+
}
|
|
943
|
+
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
944
|
+
kinds.push("cmd");
|
|
945
|
+
}
|
|
946
|
+
if (this.patternRegistry.hasBroadcastHandlers()) {
|
|
947
|
+
kinds.push("broadcast");
|
|
948
|
+
}
|
|
949
|
+
return kinds;
|
|
950
|
+
}
|
|
951
|
+
/** Determine which stream kinds need durable consumers (ordered consumers are ephemeral). */
|
|
952
|
+
resolveDurableConsumerKinds() {
|
|
880
953
|
const kinds = [];
|
|
881
954
|
if (this.patternRegistry.hasEventHandlers()) {
|
|
882
955
|
kinds.push("ev");
|
|
@@ -1103,6 +1176,8 @@ var StreamProvider = class {
|
|
|
1103
1176
|
return [`${name}.cmd.>`];
|
|
1104
1177
|
case "broadcast":
|
|
1105
1178
|
return ["broadcast.>"];
|
|
1179
|
+
case "ordered":
|
|
1180
|
+
return [`${name}.ordered.>`];
|
|
1106
1181
|
}
|
|
1107
1182
|
}
|
|
1108
1183
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
@@ -1145,6 +1220,8 @@ var StreamProvider = class {
|
|
|
1145
1220
|
return DEFAULT_COMMAND_STREAM_CONFIG;
|
|
1146
1221
|
case "broadcast":
|
|
1147
1222
|
return DEFAULT_BROADCAST_STREAM_CONFIG;
|
|
1223
|
+
case "ordered":
|
|
1224
|
+
return DEFAULT_ORDERED_STREAM_CONFIG;
|
|
1148
1225
|
}
|
|
1149
1226
|
}
|
|
1150
1227
|
/** Get user-provided overrides for a stream kind. */
|
|
@@ -1156,6 +1233,8 @@ var StreamProvider = class {
|
|
|
1156
1233
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1157
1234
|
case "broadcast":
|
|
1158
1235
|
return this.options.broadcast?.stream ?? {};
|
|
1236
|
+
case "ordered":
|
|
1237
|
+
return this.options.ordered?.stream ?? {};
|
|
1159
1238
|
}
|
|
1160
1239
|
}
|
|
1161
1240
|
};
|
|
@@ -1257,6 +1336,8 @@ var ConsumerProvider = class {
|
|
|
1257
1336
|
return DEFAULT_COMMAND_CONSUMER_CONFIG;
|
|
1258
1337
|
case "broadcast":
|
|
1259
1338
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1339
|
+
case "ordered":
|
|
1340
|
+
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1260
1341
|
}
|
|
1261
1342
|
}
|
|
1262
1343
|
/** Get user-provided overrides for a consumer kind. */
|
|
@@ -1268,12 +1349,15 @@ var ConsumerProvider = class {
|
|
|
1268
1349
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
1269
1350
|
case "broadcast":
|
|
1270
1351
|
return this.options.broadcast?.consumer ?? {};
|
|
1352
|
+
case "ordered":
|
|
1353
|
+
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1271
1354
|
}
|
|
1272
1355
|
}
|
|
1273
1356
|
};
|
|
1274
1357
|
|
|
1275
1358
|
// src/server/infrastructure/message.provider.ts
|
|
1276
1359
|
import { Logger as Logger7 } from "@nestjs/common";
|
|
1360
|
+
import { DeliverPolicy as DeliverPolicy2 } from "nats";
|
|
1277
1361
|
import {
|
|
1278
1362
|
catchError,
|
|
1279
1363
|
defer as defer2,
|
|
@@ -1292,10 +1376,13 @@ var MessageProvider = class {
|
|
|
1292
1376
|
}
|
|
1293
1377
|
logger = new Logger7("Jetstream:Message");
|
|
1294
1378
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1379
|
+
orderedReadyResolve = null;
|
|
1380
|
+
orderedReadyReject = null;
|
|
1295
1381
|
destroy$ = new Subject();
|
|
1296
1382
|
eventMessages$ = new Subject();
|
|
1297
1383
|
commandMessages$ = new Subject();
|
|
1298
1384
|
broadcastMessages$ = new Subject();
|
|
1385
|
+
orderedMessages$ = new Subject();
|
|
1299
1386
|
/** Observable stream of workqueue event messages. */
|
|
1300
1387
|
get events$() {
|
|
1301
1388
|
return this.eventMessages$.asObservable();
|
|
@@ -1308,6 +1395,10 @@ var MessageProvider = class {
|
|
|
1308
1395
|
get broadcasts$() {
|
|
1309
1396
|
return this.broadcastMessages$.asObservable();
|
|
1310
1397
|
}
|
|
1398
|
+
/** Observable stream of ordered event messages (strict sequential delivery). */
|
|
1399
|
+
get ordered$() {
|
|
1400
|
+
return this.orderedMessages$.asObservable();
|
|
1401
|
+
}
|
|
1311
1402
|
/**
|
|
1312
1403
|
* Start consuming messages from the given consumer infos.
|
|
1313
1404
|
*
|
|
@@ -1323,6 +1414,37 @@ var MessageProvider = class {
|
|
|
1323
1414
|
merge(...flows).pipe(takeUntil(this.destroy$)).subscribe();
|
|
1324
1415
|
}
|
|
1325
1416
|
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Start an ordered consumer for strict sequential delivery.
|
|
1419
|
+
*
|
|
1420
|
+
* Unlike durable consumers, ordered consumers are ephemeral — created at
|
|
1421
|
+
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
1422
|
+
*
|
|
1423
|
+
* @param streamName - JetStream stream to consume from.
|
|
1424
|
+
* @param filterSubjects - NATS subjects to filter on.
|
|
1425
|
+
*/
|
|
1426
|
+
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1427
|
+
const consumerOpts = { filterSubjects };
|
|
1428
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== DeliverPolicy2.All) {
|
|
1429
|
+
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
1430
|
+
}
|
|
1431
|
+
if (orderedConfig?.optStartSeq !== void 0) {
|
|
1432
|
+
consumerOpts.opt_start_seq = orderedConfig.optStartSeq;
|
|
1433
|
+
}
|
|
1434
|
+
if (orderedConfig?.optStartTime !== void 0) {
|
|
1435
|
+
consumerOpts.opt_start_time = orderedConfig.optStartTime;
|
|
1436
|
+
}
|
|
1437
|
+
if (orderedConfig?.replayPolicy !== void 0) {
|
|
1438
|
+
consumerOpts.replay_policy = orderedConfig.replayPolicy;
|
|
1439
|
+
}
|
|
1440
|
+
const ready = new Promise((resolve, reject) => {
|
|
1441
|
+
this.orderedReadyResolve = resolve;
|
|
1442
|
+
this.orderedReadyReject = reject;
|
|
1443
|
+
});
|
|
1444
|
+
const flow = this.createOrderedFlow(streamName2, consumerOpts);
|
|
1445
|
+
flow.pipe(takeUntil(this.destroy$)).subscribe();
|
|
1446
|
+
return ready;
|
|
1447
|
+
}
|
|
1326
1448
|
/** Stop all consumer flows and reinitialize subjects for potential restart. */
|
|
1327
1449
|
destroy() {
|
|
1328
1450
|
this.destroy$.next();
|
|
@@ -1334,10 +1456,12 @@ var MessageProvider = class {
|
|
|
1334
1456
|
this.eventMessages$.complete();
|
|
1335
1457
|
this.commandMessages$.complete();
|
|
1336
1458
|
this.broadcastMessages$.complete();
|
|
1459
|
+
this.orderedMessages$.complete();
|
|
1337
1460
|
this.destroy$ = new Subject();
|
|
1338
1461
|
this.eventMessages$ = new Subject();
|
|
1339
1462
|
this.commandMessages$ = new Subject();
|
|
1340
1463
|
this.broadcastMessages$ = new Subject();
|
|
1464
|
+
this.orderedMessages$ = new Subject();
|
|
1341
1465
|
}
|
|
1342
1466
|
/** Create a self-healing consumer flow for a specific kind. */
|
|
1343
1467
|
createFlow(kind, info) {
|
|
@@ -1395,6 +1519,63 @@ var MessageProvider = class {
|
|
|
1395
1519
|
return this.commandMessages$;
|
|
1396
1520
|
case "broadcast":
|
|
1397
1521
|
return this.broadcastMessages$;
|
|
1522
|
+
case "ordered":
|
|
1523
|
+
return this.orderedMessages$;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/** Create a self-healing ordered consumer flow. */
|
|
1527
|
+
createOrderedFlow(streamName2, consumerOpts) {
|
|
1528
|
+
let consecutiveFailures = 0;
|
|
1529
|
+
let lastRunFailed = false;
|
|
1530
|
+
return defer2(() => this.consumeOrderedOnce(streamName2, consumerOpts)).pipe(
|
|
1531
|
+
tap(() => {
|
|
1532
|
+
lastRunFailed = false;
|
|
1533
|
+
}),
|
|
1534
|
+
catchError((err) => {
|
|
1535
|
+
consecutiveFailures++;
|
|
1536
|
+
lastRunFailed = true;
|
|
1537
|
+
this.logger.error("Ordered consumer error, will restart:", err);
|
|
1538
|
+
this.eventBus.emit(
|
|
1539
|
+
"error" /* Error */,
|
|
1540
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
1541
|
+
"message-provider"
|
|
1542
|
+
);
|
|
1543
|
+
if (this.orderedReadyReject) {
|
|
1544
|
+
this.orderedReadyReject(err);
|
|
1545
|
+
this.orderedReadyReject = null;
|
|
1546
|
+
this.orderedReadyResolve = null;
|
|
1547
|
+
}
|
|
1548
|
+
return EMPTY;
|
|
1549
|
+
}),
|
|
1550
|
+
repeat({
|
|
1551
|
+
delay: () => {
|
|
1552
|
+
if (!lastRunFailed) {
|
|
1553
|
+
consecutiveFailures = 0;
|
|
1554
|
+
}
|
|
1555
|
+
const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
|
|
1556
|
+
this.logger.warn(`Ordered consumer stream ended, restarting in ${delay}ms...`);
|
|
1557
|
+
return timer(delay);
|
|
1558
|
+
}
|
|
1559
|
+
})
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
/** Single iteration: create ordered consumer -> iterate messages. */
|
|
1563
|
+
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
1564
|
+
const js = (await this.connection.getConnection()).jetstream();
|
|
1565
|
+
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
1566
|
+
const messages = await consumer.consume();
|
|
1567
|
+
if (this.orderedReadyResolve) {
|
|
1568
|
+
this.orderedReadyResolve();
|
|
1569
|
+
this.orderedReadyResolve = null;
|
|
1570
|
+
this.orderedReadyReject = null;
|
|
1571
|
+
}
|
|
1572
|
+
this.activeIterators.add(messages);
|
|
1573
|
+
try {
|
|
1574
|
+
for await (const msg of messages) {
|
|
1575
|
+
this.orderedMessages$.next(msg);
|
|
1576
|
+
}
|
|
1577
|
+
} finally {
|
|
1578
|
+
this.activeIterators.delete(messages);
|
|
1398
1579
|
}
|
|
1399
1580
|
}
|
|
1400
1581
|
};
|
|
@@ -1415,11 +1596,20 @@ var PatternRegistry = class {
|
|
|
1415
1596
|
registerHandlers(handlers) {
|
|
1416
1597
|
const serviceName = this.options.name;
|
|
1417
1598
|
for (const [pattern, handler] of handlers) {
|
|
1599
|
+
const extras = handler.extras;
|
|
1418
1600
|
const isEvent = handler.isEventHandler ?? false;
|
|
1419
|
-
const isBroadcast = !!
|
|
1601
|
+
const isBroadcast = !!extras?.broadcast;
|
|
1602
|
+
const isOrdered = !!extras?.ordered;
|
|
1603
|
+
if (isBroadcast && isOrdered) {
|
|
1604
|
+
throw new Error(
|
|
1605
|
+
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1420
1608
|
let fullSubject;
|
|
1421
1609
|
if (isBroadcast) {
|
|
1422
1610
|
fullSubject = buildBroadcastSubject(pattern);
|
|
1611
|
+
} else if (isOrdered) {
|
|
1612
|
+
fullSubject = buildSubject(serviceName, "ordered", pattern);
|
|
1423
1613
|
} else if (isEvent) {
|
|
1424
1614
|
fullSubject = buildSubject(serviceName, "ev", pattern);
|
|
1425
1615
|
} else {
|
|
@@ -1428,12 +1618,15 @@ var PatternRegistry = class {
|
|
|
1428
1618
|
this.registry.set(fullSubject, {
|
|
1429
1619
|
handler,
|
|
1430
1620
|
pattern,
|
|
1431
|
-
isEvent,
|
|
1432
|
-
isBroadcast
|
|
1621
|
+
isEvent: isEvent && !isOrdered,
|
|
1622
|
+
isBroadcast,
|
|
1623
|
+
isOrdered
|
|
1433
1624
|
});
|
|
1434
1625
|
let kind;
|
|
1435
1626
|
if (isBroadcast) {
|
|
1436
1627
|
kind = "broadcast";
|
|
1628
|
+
} else if (isOrdered) {
|
|
1629
|
+
kind = "ordered";
|
|
1437
1630
|
} else if (isEvent) {
|
|
1438
1631
|
kind = "event";
|
|
1439
1632
|
} else {
|
|
@@ -1457,28 +1650,41 @@ var PatternRegistry = class {
|
|
|
1457
1650
|
}
|
|
1458
1651
|
/** Check if any RPC (command) handlers are registered. */
|
|
1459
1652
|
hasRpcHandlers() {
|
|
1460
|
-
return Array.from(this.registry.values()).some(
|
|
1653
|
+
return Array.from(this.registry.values()).some(
|
|
1654
|
+
(r) => !r.isEvent && !r.isBroadcast && !r.isOrdered
|
|
1655
|
+
);
|
|
1461
1656
|
}
|
|
1462
1657
|
/** Check if any workqueue event handlers are registered. */
|
|
1463
1658
|
hasEventHandlers() {
|
|
1464
1659
|
return Array.from(this.registry.values()).some((r) => r.isEvent && !r.isBroadcast);
|
|
1465
1660
|
}
|
|
1661
|
+
/** Check if any ordered event handlers are registered. */
|
|
1662
|
+
hasOrderedHandlers() {
|
|
1663
|
+
return Array.from(this.registry.values()).some((r) => r.isOrdered);
|
|
1664
|
+
}
|
|
1665
|
+
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1666
|
+
getOrderedSubjects() {
|
|
1667
|
+
const name = internalName(this.options.name);
|
|
1668
|
+
return Array.from(this.registry.values()).filter((r) => r.isOrdered).map((r) => `${name}.ordered.${r.pattern}`);
|
|
1669
|
+
}
|
|
1466
1670
|
/** Get patterns grouped by kind. */
|
|
1467
1671
|
getPatternsByKind() {
|
|
1468
1672
|
const events = [];
|
|
1469
1673
|
const commands = [];
|
|
1470
1674
|
const broadcasts = [];
|
|
1675
|
+
const ordered = [];
|
|
1471
1676
|
for (const entry of this.registry.values()) {
|
|
1472
1677
|
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
1678
|
+
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
1473
1679
|
else if (entry.isEvent) events.push(entry.pattern);
|
|
1474
1680
|
else commands.push(entry.pattern);
|
|
1475
1681
|
}
|
|
1476
|
-
return { events, commands, broadcasts };
|
|
1682
|
+
return { events, commands, broadcasts, ordered };
|
|
1477
1683
|
}
|
|
1478
1684
|
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
1479
1685
|
normalizeSubject(subject) {
|
|
1480
1686
|
const name = internalName(this.options.name);
|
|
1481
|
-
const prefixes = [`${name}.cmd.`, `${name}.ev.`, "broadcast."];
|
|
1687
|
+
const prefixes = [`${name}.cmd.`, `${name}.ev.`, `${name}.ordered.`, "broadcast."];
|
|
1482
1688
|
for (const prefix of prefixes) {
|
|
1483
1689
|
if (subject.startsWith(prefix)) {
|
|
1484
1690
|
return subject.slice(prefix.length);
|
|
@@ -1488,16 +1694,29 @@ var PatternRegistry = class {
|
|
|
1488
1694
|
}
|
|
1489
1695
|
/** Log a summary of all registered handlers. */
|
|
1490
1696
|
logSummary() {
|
|
1491
|
-
const { events, commands, broadcasts } = this.getPatternsByKind();
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1697
|
+
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
1698
|
+
const parts = [
|
|
1699
|
+
`${commands.length} RPC`,
|
|
1700
|
+
`${events.length} events`,
|
|
1701
|
+
`${broadcasts.length} broadcasts`
|
|
1702
|
+
];
|
|
1703
|
+
if (ordered.length > 0) {
|
|
1704
|
+
parts.push(`${ordered.length} ordered`);
|
|
1705
|
+
}
|
|
1706
|
+
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
1495
1707
|
}
|
|
1496
1708
|
};
|
|
1497
1709
|
|
|
1498
1710
|
// src/server/routing/event.router.ts
|
|
1499
1711
|
import { Logger as Logger9 } from "@nestjs/common";
|
|
1500
|
-
import {
|
|
1712
|
+
import {
|
|
1713
|
+
catchError as catchError2,
|
|
1714
|
+
concatMap,
|
|
1715
|
+
defer as defer3,
|
|
1716
|
+
EMPTY as EMPTY2,
|
|
1717
|
+
from as from2,
|
|
1718
|
+
mergeMap
|
|
1719
|
+
} from "rxjs";
|
|
1501
1720
|
var EventRouter = class {
|
|
1502
1721
|
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig) {
|
|
1503
1722
|
this.messageProvider = messageProvider;
|
|
@@ -1516,10 +1735,13 @@ var EventRouter = class {
|
|
|
1516
1735
|
if (!this.deadLetterConfig) return;
|
|
1517
1736
|
this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
|
|
1518
1737
|
}
|
|
1519
|
-
/** Start routing event and
|
|
1738
|
+
/** Start routing event, broadcast, and ordered messages to handlers. */
|
|
1520
1739
|
start() {
|
|
1521
1740
|
this.subscribeToStream(this.messageProvider.events$, "workqueue");
|
|
1522
1741
|
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast");
|
|
1742
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
1743
|
+
this.subscribeToStream(this.messageProvider.ordered$, "ordered", true);
|
|
1744
|
+
}
|
|
1523
1745
|
}
|
|
1524
1746
|
/** Stop routing and unsubscribe from all streams. */
|
|
1525
1747
|
destroy() {
|
|
@@ -1529,17 +1751,14 @@ var EventRouter = class {
|
|
|
1529
1751
|
this.subscriptions.length = 0;
|
|
1530
1752
|
}
|
|
1531
1753
|
/** 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();
|
|
1754
|
+
subscribeToStream(stream$, label, isOrdered = false) {
|
|
1755
|
+
const route = (msg) => defer3(() => isOrdered ? this.handleOrdered(msg) : this.handle(msg)).pipe(
|
|
1756
|
+
catchError2((err) => {
|
|
1757
|
+
this.logger.error(`Unexpected error in ${label} event router`, err);
|
|
1758
|
+
return EMPTY2;
|
|
1759
|
+
})
|
|
1760
|
+
);
|
|
1761
|
+
const subscription = stream$.pipe(isOrdered ? concatMap(route) : mergeMap(route)).subscribe();
|
|
1543
1762
|
this.subscriptions.push(subscription);
|
|
1544
1763
|
}
|
|
1545
1764
|
/** Handle a single event message: decode -> execute handler -> ack/nak. */
|
|
@@ -1576,6 +1795,28 @@ var EventRouter = class {
|
|
|
1576
1795
|
}
|
|
1577
1796
|
}
|
|
1578
1797
|
}
|
|
1798
|
+
/** Handle an ordered message: decode -> execute handler -> no ack/nak. */
|
|
1799
|
+
handleOrdered(msg) {
|
|
1800
|
+
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1801
|
+
if (!handler) {
|
|
1802
|
+
this.logger.error(`No handler for ordered subject: ${msg.subject}`);
|
|
1803
|
+
return EMPTY2;
|
|
1804
|
+
}
|
|
1805
|
+
let data;
|
|
1806
|
+
try {
|
|
1807
|
+
data = this.codec.decode(msg.data);
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
this.logger.error(`Decode error for ordered ${msg.subject}:`, err);
|
|
1810
|
+
return EMPTY2;
|
|
1811
|
+
}
|
|
1812
|
+
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "event");
|
|
1813
|
+
const ctx = new RpcContext([msg]);
|
|
1814
|
+
return from2(
|
|
1815
|
+
unwrapResult(handler(data, ctx)).catch((err) => {
|
|
1816
|
+
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
1817
|
+
})
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1579
1820
|
/** Check if the message has exhausted all delivery attempts. */
|
|
1580
1821
|
isDeadLetter(msg) {
|
|
1581
1822
|
if (!this.deadLetterConfig) return false;
|