@horizon-republic/nestjs-jetstream 2.3.6 → 2.5.0

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