@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.cjs
CHANGED
|
@@ -130,6 +130,20 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
|
130
130
|
duplicate_window: nanos(2 * 60 * 1e3)
|
|
131
131
|
// 2 min
|
|
132
132
|
};
|
|
133
|
+
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
134
|
+
...baseStreamConfig,
|
|
135
|
+
retention: import_nats.RetentionPolicy.Limits,
|
|
136
|
+
allow_rollup_hdrs: false,
|
|
137
|
+
max_consumers: 100,
|
|
138
|
+
max_msg_size: 10 * MB,
|
|
139
|
+
max_msgs_per_subject: 5e6,
|
|
140
|
+
max_msgs: 5e7,
|
|
141
|
+
max_bytes: 5 * GB,
|
|
142
|
+
max_age: nanos(24 * 60 * 60 * 1e3),
|
|
143
|
+
// 1 day
|
|
144
|
+
duplicate_window: nanos(2 * 60 * 1e3)
|
|
145
|
+
// 2 min
|
|
146
|
+
};
|
|
133
147
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
134
148
|
ack_wait: nanos(10 * 1e3),
|
|
135
149
|
// 10s
|
|
@@ -165,9 +179,6 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
165
179
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
166
180
|
JetstreamHeader2["Subject"] = "x-subject";
|
|
167
181
|
JetstreamHeader2["CallerName"] = "x-caller-name";
|
|
168
|
-
JetstreamHeader2["RequestId"] = "x-request-id";
|
|
169
|
-
JetstreamHeader2["TraceId"] = "x-trace-id";
|
|
170
|
-
JetstreamHeader2["SpanId"] = "x-span-id";
|
|
171
182
|
JetstreamHeader2["Error"] = "x-error";
|
|
172
183
|
return JetstreamHeader2;
|
|
173
184
|
})(JetstreamHeader || {});
|
|
@@ -190,16 +201,18 @@ var consumerName = (serviceName, kind) => {
|
|
|
190
201
|
|
|
191
202
|
// src/client/jetstream.record.ts
|
|
192
203
|
var JetstreamRecord = class {
|
|
193
|
-
constructor(data, headers2, timeout) {
|
|
204
|
+
constructor(data, headers2, timeout, messageId) {
|
|
194
205
|
this.data = data;
|
|
195
206
|
this.headers = headers2;
|
|
196
207
|
this.timeout = timeout;
|
|
208
|
+
this.messageId = messageId;
|
|
197
209
|
}
|
|
198
210
|
};
|
|
199
211
|
var JetstreamRecordBuilder = class {
|
|
200
212
|
data;
|
|
201
213
|
headers = /* @__PURE__ */ new Map();
|
|
202
214
|
timeout;
|
|
215
|
+
messageId;
|
|
203
216
|
constructor(data) {
|
|
204
217
|
this.data = data;
|
|
205
218
|
}
|
|
@@ -236,6 +249,28 @@ var JetstreamRecordBuilder = class {
|
|
|
236
249
|
}
|
|
237
250
|
return this;
|
|
238
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Set a custom message ID for JetStream deduplication.
|
|
254
|
+
*
|
|
255
|
+
* NATS JetStream uses this ID to detect duplicate publishes within the
|
|
256
|
+
* stream's `duplicate_window`. If two messages with the same ID arrive
|
|
257
|
+
* within the window, the second is silently dropped.
|
|
258
|
+
*
|
|
259
|
+
* When not set, a random UUID is generated automatically.
|
|
260
|
+
*
|
|
261
|
+
* @param id - Unique message identifier (e.g. order ID, idempotency key).
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* new JetstreamRecordBuilder(data)
|
|
266
|
+
* .setMessageId(`order-${order.id}`)
|
|
267
|
+
* .build();
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
setMessageId(id) {
|
|
271
|
+
this.messageId = id;
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
239
274
|
/**
|
|
240
275
|
* Set per-request RPC timeout.
|
|
241
276
|
*
|
|
@@ -251,7 +286,12 @@ var JetstreamRecordBuilder = class {
|
|
|
251
286
|
* @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
|
|
252
287
|
*/
|
|
253
288
|
build() {
|
|
254
|
-
return new JetstreamRecord(
|
|
289
|
+
return new JetstreamRecord(
|
|
290
|
+
this.data,
|
|
291
|
+
new Map(this.headers),
|
|
292
|
+
this.timeout,
|
|
293
|
+
this.messageId
|
|
294
|
+
);
|
|
255
295
|
}
|
|
256
296
|
/** Validate that a header key is not reserved. */
|
|
257
297
|
validateHeaderKey(key) {
|
|
@@ -331,12 +371,12 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
331
371
|
*/
|
|
332
372
|
async dispatchEvent(packet) {
|
|
333
373
|
const nc = await this.connect();
|
|
334
|
-
const { data, hdrs } = this.extractRecordData(packet.data);
|
|
374
|
+
const { data, hdrs, messageId } = this.extractRecordData(packet.data);
|
|
335
375
|
const subject = this.buildEventSubject(packet.pattern);
|
|
336
376
|
const msgHeaders = this.buildHeaders(hdrs, { subject });
|
|
337
377
|
const ack = await nc.jetstream().publish(subject, this.codec.encode(data), {
|
|
338
378
|
headers: msgHeaders,
|
|
339
|
-
msgID: crypto.randomUUID()
|
|
379
|
+
msgID: messageId ?? crypto.randomUUID()
|
|
340
380
|
});
|
|
341
381
|
if (ack.duplicate) {
|
|
342
382
|
this.logger.warn(`Duplicate event publish detected: ${subject} (seq: ${ack.seq})`);
|
|
@@ -351,7 +391,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
351
391
|
*/
|
|
352
392
|
publish(packet, callback) {
|
|
353
393
|
const subject = buildSubject(this.targetName, "cmd", packet.pattern);
|
|
354
|
-
const { data, hdrs, timeout } = this.extractRecordData(packet.data);
|
|
394
|
+
const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
|
|
355
395
|
const onUnhandled = (err) => {
|
|
356
396
|
this.logger.error("Unhandled publish error:", err);
|
|
357
397
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
@@ -367,7 +407,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
367
407
|
hdrs,
|
|
368
408
|
timeout,
|
|
369
409
|
callback,
|
|
370
|
-
jetStreamCorrelationId
|
|
410
|
+
jetStreamCorrelationId,
|
|
411
|
+
messageId
|
|
371
412
|
).catch(onUnhandled);
|
|
372
413
|
}
|
|
373
414
|
return () => {
|
|
@@ -405,7 +446,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
405
446
|
}
|
|
406
447
|
}
|
|
407
448
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
408
|
-
async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID()) {
|
|
449
|
+
async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID(), messageId) {
|
|
409
450
|
const effectiveTimeout = timeout ?? this.getRpcTimeout();
|
|
410
451
|
this.pendingMessages.set(correlationId, callback);
|
|
411
452
|
const timeoutId = setTimeout(() => {
|
|
@@ -429,7 +470,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
429
470
|
});
|
|
430
471
|
await nc.jetstream().publish(subject, this.codec.encode(data), {
|
|
431
472
|
headers: hdrs,
|
|
432
|
-
msgID: crypto.randomUUID()
|
|
473
|
+
msgID: messageId ?? crypto.randomUUID()
|
|
433
474
|
});
|
|
434
475
|
} catch (err) {
|
|
435
476
|
clearTimeout(timeoutId);
|
|
@@ -507,11 +548,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
507
548
|
this.pendingMessages.delete(correlationId);
|
|
508
549
|
}
|
|
509
550
|
}
|
|
510
|
-
/** Build event subject — workqueue or
|
|
551
|
+
/** Build event subject — workqueue, broadcast, or ordered. */
|
|
511
552
|
buildEventSubject(pattern) {
|
|
512
553
|
if (pattern.startsWith("broadcast:")) {
|
|
513
554
|
return buildBroadcastSubject(pattern.slice("broadcast:".length));
|
|
514
555
|
}
|
|
556
|
+
if (pattern.startsWith("ordered:")) {
|
|
557
|
+
return buildSubject(this.targetName, "ordered", pattern.slice("ordered:".length));
|
|
558
|
+
}
|
|
515
559
|
return buildSubject(this.targetName, "ev", pattern);
|
|
516
560
|
}
|
|
517
561
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
@@ -538,10 +582,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
538
582
|
return {
|
|
539
583
|
data: rawData.data,
|
|
540
584
|
hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
|
|
541
|
-
timeout: rawData.timeout
|
|
585
|
+
timeout: rawData.timeout,
|
|
586
|
+
messageId: rawData.messageId
|
|
542
587
|
};
|
|
543
588
|
}
|
|
544
|
-
return { data: rawData, hdrs: null, timeout: void 0 };
|
|
589
|
+
return { data: rawData, hdrs: null, timeout: void 0, messageId: void 0 };
|
|
545
590
|
}
|
|
546
591
|
isCoreRpcMode() {
|
|
547
592
|
return !this.rootOptions.rpc || this.rootOptions.rpc.mode === "core";
|
|
@@ -838,12 +883,23 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
838
883
|
this.started = true;
|
|
839
884
|
this.patternRegistry.registerHandlers(this.getHandlers());
|
|
840
885
|
const streamKinds = this.resolveStreamKinds();
|
|
886
|
+
const durableKinds = this.resolveDurableConsumerKinds();
|
|
841
887
|
if (streamKinds.length > 0) {
|
|
842
888
|
await this.streamProvider.ensureStreams(streamKinds);
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
889
|
+
if (durableKinds.length > 0) {
|
|
890
|
+
const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
|
|
891
|
+
this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
|
|
892
|
+
this.messageProvider.start(consumers);
|
|
893
|
+
}
|
|
894
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
895
|
+
const orderedStreamName = this.streamProvider.getStreamName("ordered");
|
|
896
|
+
await this.messageProvider.startOrdered(
|
|
897
|
+
orderedStreamName,
|
|
898
|
+
this.patternRegistry.getOrderedSubjects(),
|
|
899
|
+
this.options.ordered
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
|
|
847
903
|
this.eventRouter.start();
|
|
848
904
|
}
|
|
849
905
|
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
@@ -891,8 +947,25 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
891
947
|
getPatternRegistry() {
|
|
892
948
|
return this.patternRegistry;
|
|
893
949
|
}
|
|
894
|
-
/** Determine which JetStream
|
|
950
|
+
/** Determine which JetStream streams are needed. */
|
|
895
951
|
resolveStreamKinds() {
|
|
952
|
+
const kinds = [];
|
|
953
|
+
if (this.patternRegistry.hasEventHandlers()) {
|
|
954
|
+
kinds.push("ev");
|
|
955
|
+
}
|
|
956
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
957
|
+
kinds.push("ordered");
|
|
958
|
+
}
|
|
959
|
+
if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
|
|
960
|
+
kinds.push("cmd");
|
|
961
|
+
}
|
|
962
|
+
if (this.patternRegistry.hasBroadcastHandlers()) {
|
|
963
|
+
kinds.push("broadcast");
|
|
964
|
+
}
|
|
965
|
+
return kinds;
|
|
966
|
+
}
|
|
967
|
+
/** Determine which stream kinds need durable consumers (ordered consumers are ephemeral). */
|
|
968
|
+
resolveDurableConsumerKinds() {
|
|
896
969
|
const kinds = [];
|
|
897
970
|
if (this.patternRegistry.hasEventHandlers()) {
|
|
898
971
|
kinds.push("ev");
|
|
@@ -1119,6 +1192,8 @@ var StreamProvider = class {
|
|
|
1119
1192
|
return [`${name}.cmd.>`];
|
|
1120
1193
|
case "broadcast":
|
|
1121
1194
|
return ["broadcast.>"];
|
|
1195
|
+
case "ordered":
|
|
1196
|
+
return [`${name}.ordered.>`];
|
|
1122
1197
|
}
|
|
1123
1198
|
}
|
|
1124
1199
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
@@ -1161,6 +1236,8 @@ var StreamProvider = class {
|
|
|
1161
1236
|
return DEFAULT_COMMAND_STREAM_CONFIG;
|
|
1162
1237
|
case "broadcast":
|
|
1163
1238
|
return DEFAULT_BROADCAST_STREAM_CONFIG;
|
|
1239
|
+
case "ordered":
|
|
1240
|
+
return DEFAULT_ORDERED_STREAM_CONFIG;
|
|
1164
1241
|
}
|
|
1165
1242
|
}
|
|
1166
1243
|
/** Get user-provided overrides for a stream kind. */
|
|
@@ -1172,6 +1249,8 @@ var StreamProvider = class {
|
|
|
1172
1249
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1173
1250
|
case "broadcast":
|
|
1174
1251
|
return this.options.broadcast?.stream ?? {};
|
|
1252
|
+
case "ordered":
|
|
1253
|
+
return this.options.ordered?.stream ?? {};
|
|
1175
1254
|
}
|
|
1176
1255
|
}
|
|
1177
1256
|
};
|
|
@@ -1273,6 +1352,8 @@ var ConsumerProvider = class {
|
|
|
1273
1352
|
return DEFAULT_COMMAND_CONSUMER_CONFIG;
|
|
1274
1353
|
case "broadcast":
|
|
1275
1354
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1355
|
+
case "ordered":
|
|
1356
|
+
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1276
1357
|
}
|
|
1277
1358
|
}
|
|
1278
1359
|
/** Get user-provided overrides for a consumer kind. */
|
|
@@ -1284,12 +1365,15 @@ var ConsumerProvider = class {
|
|
|
1284
1365
|
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
1285
1366
|
case "broadcast":
|
|
1286
1367
|
return this.options.broadcast?.consumer ?? {};
|
|
1368
|
+
case "ordered":
|
|
1369
|
+
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
1287
1370
|
}
|
|
1288
1371
|
}
|
|
1289
1372
|
};
|
|
1290
1373
|
|
|
1291
1374
|
// src/server/infrastructure/message.provider.ts
|
|
1292
1375
|
var import_common7 = require("@nestjs/common");
|
|
1376
|
+
var import_nats8 = require("nats");
|
|
1293
1377
|
var import_rxjs3 = require("rxjs");
|
|
1294
1378
|
var MessageProvider = class {
|
|
1295
1379
|
constructor(connection, eventBus) {
|
|
@@ -1298,10 +1382,13 @@ var MessageProvider = class {
|
|
|
1298
1382
|
}
|
|
1299
1383
|
logger = new import_common7.Logger("Jetstream:Message");
|
|
1300
1384
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1385
|
+
orderedReadyResolve = null;
|
|
1386
|
+
orderedReadyReject = null;
|
|
1301
1387
|
destroy$ = new import_rxjs3.Subject();
|
|
1302
1388
|
eventMessages$ = new import_rxjs3.Subject();
|
|
1303
1389
|
commandMessages$ = new import_rxjs3.Subject();
|
|
1304
1390
|
broadcastMessages$ = new import_rxjs3.Subject();
|
|
1391
|
+
orderedMessages$ = new import_rxjs3.Subject();
|
|
1305
1392
|
/** Observable stream of workqueue event messages. */
|
|
1306
1393
|
get events$() {
|
|
1307
1394
|
return this.eventMessages$.asObservable();
|
|
@@ -1314,6 +1401,10 @@ var MessageProvider = class {
|
|
|
1314
1401
|
get broadcasts$() {
|
|
1315
1402
|
return this.broadcastMessages$.asObservable();
|
|
1316
1403
|
}
|
|
1404
|
+
/** Observable stream of ordered event messages (strict sequential delivery). */
|
|
1405
|
+
get ordered$() {
|
|
1406
|
+
return this.orderedMessages$.asObservable();
|
|
1407
|
+
}
|
|
1317
1408
|
/**
|
|
1318
1409
|
* Start consuming messages from the given consumer infos.
|
|
1319
1410
|
*
|
|
@@ -1329,6 +1420,37 @@ var MessageProvider = class {
|
|
|
1329
1420
|
(0, import_rxjs3.merge)(...flows).pipe((0, import_rxjs3.takeUntil)(this.destroy$)).subscribe();
|
|
1330
1421
|
}
|
|
1331
1422
|
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Start an ordered consumer for strict sequential delivery.
|
|
1425
|
+
*
|
|
1426
|
+
* Unlike durable consumers, ordered consumers are ephemeral — created at
|
|
1427
|
+
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
1428
|
+
*
|
|
1429
|
+
* @param streamName - JetStream stream to consume from.
|
|
1430
|
+
* @param filterSubjects - NATS subjects to filter on.
|
|
1431
|
+
*/
|
|
1432
|
+
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1433
|
+
const consumerOpts = { filterSubjects };
|
|
1434
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_nats8.DeliverPolicy.All) {
|
|
1435
|
+
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
1436
|
+
}
|
|
1437
|
+
if (orderedConfig?.optStartSeq !== void 0) {
|
|
1438
|
+
consumerOpts.opt_start_seq = orderedConfig.optStartSeq;
|
|
1439
|
+
}
|
|
1440
|
+
if (orderedConfig?.optStartTime !== void 0) {
|
|
1441
|
+
consumerOpts.opt_start_time = orderedConfig.optStartTime;
|
|
1442
|
+
}
|
|
1443
|
+
if (orderedConfig?.replayPolicy !== void 0) {
|
|
1444
|
+
consumerOpts.replay_policy = orderedConfig.replayPolicy;
|
|
1445
|
+
}
|
|
1446
|
+
const ready = new Promise((resolve, reject) => {
|
|
1447
|
+
this.orderedReadyResolve = resolve;
|
|
1448
|
+
this.orderedReadyReject = reject;
|
|
1449
|
+
});
|
|
1450
|
+
const flow = this.createOrderedFlow(streamName2, consumerOpts);
|
|
1451
|
+
flow.pipe((0, import_rxjs3.takeUntil)(this.destroy$)).subscribe();
|
|
1452
|
+
return ready;
|
|
1453
|
+
}
|
|
1332
1454
|
/** Stop all consumer flows and reinitialize subjects for potential restart. */
|
|
1333
1455
|
destroy() {
|
|
1334
1456
|
this.destroy$.next();
|
|
@@ -1340,10 +1462,12 @@ var MessageProvider = class {
|
|
|
1340
1462
|
this.eventMessages$.complete();
|
|
1341
1463
|
this.commandMessages$.complete();
|
|
1342
1464
|
this.broadcastMessages$.complete();
|
|
1465
|
+
this.orderedMessages$.complete();
|
|
1343
1466
|
this.destroy$ = new import_rxjs3.Subject();
|
|
1344
1467
|
this.eventMessages$ = new import_rxjs3.Subject();
|
|
1345
1468
|
this.commandMessages$ = new import_rxjs3.Subject();
|
|
1346
1469
|
this.broadcastMessages$ = new import_rxjs3.Subject();
|
|
1470
|
+
this.orderedMessages$ = new import_rxjs3.Subject();
|
|
1347
1471
|
}
|
|
1348
1472
|
/** Create a self-healing consumer flow for a specific kind. */
|
|
1349
1473
|
createFlow(kind, info) {
|
|
@@ -1401,6 +1525,63 @@ var MessageProvider = class {
|
|
|
1401
1525
|
return this.commandMessages$;
|
|
1402
1526
|
case "broadcast":
|
|
1403
1527
|
return this.broadcastMessages$;
|
|
1528
|
+
case "ordered":
|
|
1529
|
+
return this.orderedMessages$;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
/** Create a self-healing ordered consumer flow. */
|
|
1533
|
+
createOrderedFlow(streamName2, consumerOpts) {
|
|
1534
|
+
let consecutiveFailures = 0;
|
|
1535
|
+
let lastRunFailed = false;
|
|
1536
|
+
return (0, import_rxjs3.defer)(() => this.consumeOrderedOnce(streamName2, consumerOpts)).pipe(
|
|
1537
|
+
(0, import_rxjs3.tap)(() => {
|
|
1538
|
+
lastRunFailed = false;
|
|
1539
|
+
}),
|
|
1540
|
+
(0, import_rxjs3.catchError)((err) => {
|
|
1541
|
+
consecutiveFailures++;
|
|
1542
|
+
lastRunFailed = true;
|
|
1543
|
+
this.logger.error("Ordered consumer error, will restart:", err);
|
|
1544
|
+
this.eventBus.emit(
|
|
1545
|
+
"error" /* Error */,
|
|
1546
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
1547
|
+
"message-provider"
|
|
1548
|
+
);
|
|
1549
|
+
if (this.orderedReadyReject) {
|
|
1550
|
+
this.orderedReadyReject(err);
|
|
1551
|
+
this.orderedReadyReject = null;
|
|
1552
|
+
this.orderedReadyResolve = null;
|
|
1553
|
+
}
|
|
1554
|
+
return import_rxjs3.EMPTY;
|
|
1555
|
+
}),
|
|
1556
|
+
(0, import_rxjs3.repeat)({
|
|
1557
|
+
delay: () => {
|
|
1558
|
+
if (!lastRunFailed) {
|
|
1559
|
+
consecutiveFailures = 0;
|
|
1560
|
+
}
|
|
1561
|
+
const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
|
|
1562
|
+
this.logger.warn(`Ordered consumer stream ended, restarting in ${delay}ms...`);
|
|
1563
|
+
return (0, import_rxjs3.timer)(delay);
|
|
1564
|
+
}
|
|
1565
|
+
})
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
/** Single iteration: create ordered consumer -> iterate messages. */
|
|
1569
|
+
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
1570
|
+
const js = (await this.connection.getConnection()).jetstream();
|
|
1571
|
+
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
1572
|
+
const messages = await consumer.consume();
|
|
1573
|
+
if (this.orderedReadyResolve) {
|
|
1574
|
+
this.orderedReadyResolve();
|
|
1575
|
+
this.orderedReadyResolve = null;
|
|
1576
|
+
this.orderedReadyReject = null;
|
|
1577
|
+
}
|
|
1578
|
+
this.activeIterators.add(messages);
|
|
1579
|
+
try {
|
|
1580
|
+
for await (const msg of messages) {
|
|
1581
|
+
this.orderedMessages$.next(msg);
|
|
1582
|
+
}
|
|
1583
|
+
} finally {
|
|
1584
|
+
this.activeIterators.delete(messages);
|
|
1404
1585
|
}
|
|
1405
1586
|
}
|
|
1406
1587
|
};
|
|
@@ -1421,11 +1602,20 @@ var PatternRegistry = class {
|
|
|
1421
1602
|
registerHandlers(handlers) {
|
|
1422
1603
|
const serviceName = this.options.name;
|
|
1423
1604
|
for (const [pattern, handler] of handlers) {
|
|
1605
|
+
const extras = handler.extras;
|
|
1424
1606
|
const isEvent = handler.isEventHandler ?? false;
|
|
1425
|
-
const isBroadcast = !!
|
|
1607
|
+
const isBroadcast = !!extras?.broadcast;
|
|
1608
|
+
const isOrdered = !!extras?.ordered;
|
|
1609
|
+
if (isBroadcast && isOrdered) {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1426
1614
|
let fullSubject;
|
|
1427
1615
|
if (isBroadcast) {
|
|
1428
1616
|
fullSubject = buildBroadcastSubject(pattern);
|
|
1617
|
+
} else if (isOrdered) {
|
|
1618
|
+
fullSubject = buildSubject(serviceName, "ordered", pattern);
|
|
1429
1619
|
} else if (isEvent) {
|
|
1430
1620
|
fullSubject = buildSubject(serviceName, "ev", pattern);
|
|
1431
1621
|
} else {
|
|
@@ -1434,12 +1624,15 @@ var PatternRegistry = class {
|
|
|
1434
1624
|
this.registry.set(fullSubject, {
|
|
1435
1625
|
handler,
|
|
1436
1626
|
pattern,
|
|
1437
|
-
isEvent,
|
|
1438
|
-
isBroadcast
|
|
1627
|
+
isEvent: isEvent && !isOrdered,
|
|
1628
|
+
isBroadcast,
|
|
1629
|
+
isOrdered
|
|
1439
1630
|
});
|
|
1440
1631
|
let kind;
|
|
1441
1632
|
if (isBroadcast) {
|
|
1442
1633
|
kind = "broadcast";
|
|
1634
|
+
} else if (isOrdered) {
|
|
1635
|
+
kind = "ordered";
|
|
1443
1636
|
} else if (isEvent) {
|
|
1444
1637
|
kind = "event";
|
|
1445
1638
|
} else {
|
|
@@ -1463,28 +1656,41 @@ var PatternRegistry = class {
|
|
|
1463
1656
|
}
|
|
1464
1657
|
/** Check if any RPC (command) handlers are registered. */
|
|
1465
1658
|
hasRpcHandlers() {
|
|
1466
|
-
return Array.from(this.registry.values()).some(
|
|
1659
|
+
return Array.from(this.registry.values()).some(
|
|
1660
|
+
(r) => !r.isEvent && !r.isBroadcast && !r.isOrdered
|
|
1661
|
+
);
|
|
1467
1662
|
}
|
|
1468
1663
|
/** Check if any workqueue event handlers are registered. */
|
|
1469
1664
|
hasEventHandlers() {
|
|
1470
1665
|
return Array.from(this.registry.values()).some((r) => r.isEvent && !r.isBroadcast);
|
|
1471
1666
|
}
|
|
1667
|
+
/** Check if any ordered event handlers are registered. */
|
|
1668
|
+
hasOrderedHandlers() {
|
|
1669
|
+
return Array.from(this.registry.values()).some((r) => r.isOrdered);
|
|
1670
|
+
}
|
|
1671
|
+
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
1672
|
+
getOrderedSubjects() {
|
|
1673
|
+
const name = internalName(this.options.name);
|
|
1674
|
+
return Array.from(this.registry.values()).filter((r) => r.isOrdered).map((r) => `${name}.ordered.${r.pattern}`);
|
|
1675
|
+
}
|
|
1472
1676
|
/** Get patterns grouped by kind. */
|
|
1473
1677
|
getPatternsByKind() {
|
|
1474
1678
|
const events = [];
|
|
1475
1679
|
const commands = [];
|
|
1476
1680
|
const broadcasts = [];
|
|
1681
|
+
const ordered = [];
|
|
1477
1682
|
for (const entry of this.registry.values()) {
|
|
1478
1683
|
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
1684
|
+
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
1479
1685
|
else if (entry.isEvent) events.push(entry.pattern);
|
|
1480
1686
|
else commands.push(entry.pattern);
|
|
1481
1687
|
}
|
|
1482
|
-
return { events, commands, broadcasts };
|
|
1688
|
+
return { events, commands, broadcasts, ordered };
|
|
1483
1689
|
}
|
|
1484
1690
|
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
1485
1691
|
normalizeSubject(subject) {
|
|
1486
1692
|
const name = internalName(this.options.name);
|
|
1487
|
-
const prefixes = [`${name}.cmd.`, `${name}.ev.`, "broadcast."];
|
|
1693
|
+
const prefixes = [`${name}.cmd.`, `${name}.ev.`, `${name}.ordered.`, "broadcast."];
|
|
1488
1694
|
for (const prefix of prefixes) {
|
|
1489
1695
|
if (subject.startsWith(prefix)) {
|
|
1490
1696
|
return subject.slice(prefix.length);
|
|
@@ -1494,10 +1700,16 @@ var PatternRegistry = class {
|
|
|
1494
1700
|
}
|
|
1495
1701
|
/** Log a summary of all registered handlers. */
|
|
1496
1702
|
logSummary() {
|
|
1497
|
-
const { events, commands, broadcasts } = this.getPatternsByKind();
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1703
|
+
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
1704
|
+
const parts = [
|
|
1705
|
+
`${commands.length} RPC`,
|
|
1706
|
+
`${events.length} events`,
|
|
1707
|
+
`${broadcasts.length} broadcasts`
|
|
1708
|
+
];
|
|
1709
|
+
if (ordered.length > 0) {
|
|
1710
|
+
parts.push(`${ordered.length} ordered`);
|
|
1711
|
+
}
|
|
1712
|
+
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
1501
1713
|
}
|
|
1502
1714
|
};
|
|
1503
1715
|
|
|
@@ -1522,10 +1734,13 @@ var EventRouter = class {
|
|
|
1522
1734
|
if (!this.deadLetterConfig) return;
|
|
1523
1735
|
this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
|
|
1524
1736
|
}
|
|
1525
|
-
/** Start routing event and
|
|
1737
|
+
/** Start routing event, broadcast, and ordered messages to handlers. */
|
|
1526
1738
|
start() {
|
|
1527
1739
|
this.subscribeToStream(this.messageProvider.events$, "workqueue");
|
|
1528
1740
|
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast");
|
|
1741
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
1742
|
+
this.subscribeToStream(this.messageProvider.ordered$, "ordered", true);
|
|
1743
|
+
}
|
|
1529
1744
|
}
|
|
1530
1745
|
/** Stop routing and unsubscribe from all streams. */
|
|
1531
1746
|
destroy() {
|
|
@@ -1535,17 +1750,14 @@ var EventRouter = class {
|
|
|
1535
1750
|
this.subscriptions.length = 0;
|
|
1536
1751
|
}
|
|
1537
1752
|
/** Subscribe to a message stream and route each message. */
|
|
1538
|
-
subscribeToStream(stream$, label) {
|
|
1539
|
-
const
|
|
1540
|
-
(0, import_rxjs4.
|
|
1541
|
-
(
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
)
|
|
1547
|
-
)
|
|
1548
|
-
).subscribe();
|
|
1753
|
+
subscribeToStream(stream$, label, isOrdered = false) {
|
|
1754
|
+
const route = (msg) => (0, import_rxjs4.defer)(() => isOrdered ? this.handleOrdered(msg) : this.handle(msg)).pipe(
|
|
1755
|
+
(0, import_rxjs4.catchError)((err) => {
|
|
1756
|
+
this.logger.error(`Unexpected error in ${label} event router`, err);
|
|
1757
|
+
return import_rxjs4.EMPTY;
|
|
1758
|
+
})
|
|
1759
|
+
);
|
|
1760
|
+
const subscription = stream$.pipe(isOrdered ? (0, import_rxjs4.concatMap)(route) : (0, import_rxjs4.mergeMap)(route)).subscribe();
|
|
1549
1761
|
this.subscriptions.push(subscription);
|
|
1550
1762
|
}
|
|
1551
1763
|
/** Handle a single event message: decode -> execute handler -> ack/nak. */
|
|
@@ -1582,6 +1794,28 @@ var EventRouter = class {
|
|
|
1582
1794
|
}
|
|
1583
1795
|
}
|
|
1584
1796
|
}
|
|
1797
|
+
/** Handle an ordered message: decode -> execute handler -> no ack/nak. */
|
|
1798
|
+
handleOrdered(msg) {
|
|
1799
|
+
const handler = this.patternRegistry.getHandler(msg.subject);
|
|
1800
|
+
if (!handler) {
|
|
1801
|
+
this.logger.error(`No handler for ordered subject: ${msg.subject}`);
|
|
1802
|
+
return import_rxjs4.EMPTY;
|
|
1803
|
+
}
|
|
1804
|
+
let data;
|
|
1805
|
+
try {
|
|
1806
|
+
data = this.codec.decode(msg.data);
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
this.logger.error(`Decode error for ordered ${msg.subject}:`, err);
|
|
1809
|
+
return import_rxjs4.EMPTY;
|
|
1810
|
+
}
|
|
1811
|
+
this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "event");
|
|
1812
|
+
const ctx = new RpcContext([msg]);
|
|
1813
|
+
return (0, import_rxjs4.from)(
|
|
1814
|
+
unwrapResult(handler(data, ctx)).catch((err) => {
|
|
1815
|
+
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
1816
|
+
})
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1585
1819
|
/** Check if the message has exhausted all delivery attempts. */
|
|
1586
1820
|
isDeadLetter(msg) {
|
|
1587
1821
|
if (!this.deadLetterConfig) return false;
|
|
@@ -1615,7 +1849,7 @@ var EventRouter = class {
|
|
|
1615
1849
|
|
|
1616
1850
|
// src/server/routing/rpc.router.ts
|
|
1617
1851
|
var import_common10 = require("@nestjs/common");
|
|
1618
|
-
var
|
|
1852
|
+
var import_nats9 = require("nats");
|
|
1619
1853
|
var import_rxjs5 = require("rxjs");
|
|
1620
1854
|
var RpcRouter = class {
|
|
1621
1855
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, timeout) {
|
|
@@ -1677,7 +1911,7 @@ var RpcRouter = class {
|
|
|
1677
1911
|
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
1678
1912
|
const nc = await this.connection.getConnection();
|
|
1679
1913
|
const ctx = new RpcContext([msg]);
|
|
1680
|
-
const hdrs = (0,
|
|
1914
|
+
const hdrs = (0, import_nats9.headers)();
|
|
1681
1915
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
1682
1916
|
let settled = false;
|
|
1683
1917
|
const timeoutId = setTimeout(() => {
|