@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.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(this.data, new Map(this.headers), this.timeout);
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 broadcast. */
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
- const consumers = await this.consumerProvider.ensureConsumers(streamKinds);
828
- this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
829
- this.messageProvider.start(consumers);
830
- if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers()) {
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 stream kinds are needed. */
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 = !!handler.extras?.broadcast;
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((r) => !r.isEvent && !r.isBroadcast);
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
- this.logger.log(
1493
- `Registered handlers: ${commands.length} RPC, ${events.length} events, ${broadcasts.length} broadcasts`
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 { catchError as catchError2, defer as defer3, EMPTY as EMPTY2, from as from2, mergeMap } from "rxjs";
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 broadcast messages to handlers. */
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 subscription = stream$.pipe(
1534
- mergeMap(
1535
- (msg) => defer3(() => this.handle(msg)).pipe(
1536
- catchError2((err) => {
1537
- this.logger.error(`Unexpected error in ${label} event router`, err);
1538
- return EMPTY2;
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;