@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/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(this.data, new Map(this.headers), this.timeout);
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 broadcast. */
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
- const consumers = await this.consumerProvider.ensureConsumers(streamKinds);
844
- this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
845
- this.messageProvider.start(consumers);
846
- if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers()) {
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 stream kinds are needed. */
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 = !!handler.extras?.broadcast;
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((r) => !r.isEvent && !r.isBroadcast);
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
- this.logger.log(
1499
- `Registered handlers: ${commands.length} RPC, ${events.length} events, ${broadcasts.length} broadcasts`
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 broadcast messages to handlers. */
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 subscription = stream$.pipe(
1540
- (0, import_rxjs4.mergeMap)(
1541
- (msg) => (0, import_rxjs4.defer)(() => this.handle(msg)).pipe(
1542
- (0, import_rxjs4.catchError)((err) => {
1543
- this.logger.error(`Unexpected error in ${label} event router`, err);
1544
- return import_rxjs4.EMPTY;
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 import_nats8 = require("nats");
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, import_nats8.headers)();
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(() => {