@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.cjs CHANGED
@@ -45,7 +45,7 @@ __export(index_exports, {
45
45
  RpcContext: () => RpcContext,
46
46
  TransportEvent: () => TransportEvent,
47
47
  getClientToken: () => getClientToken,
48
- nanos: () => nanos
48
+ toNanos: () => toNanos
49
49
  });
50
50
  module.exports = __toCommonJS(index_exports);
51
51
 
@@ -81,7 +81,14 @@ var getClientToken = (name) => name;
81
81
  var KB = 1024;
82
82
  var MB = 1024 * KB;
83
83
  var GB = 1024 * MB;
84
- var nanos = (ms) => ms * 1e6;
84
+ var NANOS_PER = {
85
+ ms: 1e6,
86
+ seconds: 1e9,
87
+ minutes: 6e10,
88
+ hours: 36e11,
89
+ days: 864e11
90
+ };
91
+ var toNanos = (value, unit) => value * NANOS_PER[unit];
85
92
  var baseStreamConfig = {
86
93
  retention: import_nats.RetentionPolicy.Workqueue,
87
94
  storage: import_nats.StorageType.File,
@@ -98,10 +105,8 @@ var DEFAULT_EVENT_STREAM_CONFIG = {
98
105
  max_msgs_per_subject: 5e6,
99
106
  max_msgs: 5e7,
100
107
  max_bytes: 5 * GB,
101
- max_age: nanos(7 * 24 * 60 * 60 * 1e3),
102
- // 7 days
103
- duplicate_window: nanos(2 * 60 * 1e3)
104
- // 2 min
108
+ max_age: toNanos(7, "days"),
109
+ duplicate_window: toNanos(2, "minutes")
105
110
  };
106
111
  var DEFAULT_COMMAND_STREAM_CONFIG = {
107
112
  ...baseStreamConfig,
@@ -111,10 +116,8 @@ var DEFAULT_COMMAND_STREAM_CONFIG = {
111
116
  max_msgs_per_subject: 1e5,
112
117
  max_msgs: 1e6,
113
118
  max_bytes: 100 * MB,
114
- max_age: nanos(3 * 60 * 1e3),
115
- // 3 min
116
- duplicate_window: nanos(30 * 1e3)
117
- // 30s
119
+ max_age: toNanos(3, "minutes"),
120
+ duplicate_window: toNanos(30, "seconds")
118
121
  };
119
122
  var DEFAULT_BROADCAST_STREAM_CONFIG = {
120
123
  ...baseStreamConfig,
@@ -125,14 +128,23 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
125
128
  max_msgs_per_subject: 1e6,
126
129
  max_msgs: 1e7,
127
130
  max_bytes: 2 * GB,
128
- max_age: nanos(24 * 60 * 60 * 1e3),
129
- // 1 day
130
- duplicate_window: nanos(2 * 60 * 1e3)
131
- // 2 min
131
+ max_age: toNanos(1, "days"),
132
+ duplicate_window: toNanos(2, "minutes")
133
+ };
134
+ var DEFAULT_ORDERED_STREAM_CONFIG = {
135
+ ...baseStreamConfig,
136
+ retention: import_nats.RetentionPolicy.Limits,
137
+ allow_rollup_hdrs: false,
138
+ max_consumers: 100,
139
+ max_msg_size: 10 * MB,
140
+ max_msgs_per_subject: 5e6,
141
+ max_msgs: 5e7,
142
+ max_bytes: 5 * GB,
143
+ max_age: toNanos(1, "days"),
144
+ duplicate_window: toNanos(2, "minutes")
132
145
  };
133
146
  var DEFAULT_EVENT_CONSUMER_CONFIG = {
134
- ack_wait: nanos(10 * 1e3),
135
- // 10s
147
+ ack_wait: toNanos(10, "seconds"),
136
148
  max_deliver: 3,
137
149
  max_ack_pending: 100,
138
150
  ack_policy: import_nats.AckPolicy.Explicit,
@@ -140,8 +152,7 @@ var DEFAULT_EVENT_CONSUMER_CONFIG = {
140
152
  replay_policy: import_nats.ReplayPolicy.Instant
141
153
  };
142
154
  var DEFAULT_COMMAND_CONSUMER_CONFIG = {
143
- ack_wait: nanos(5 * 60 * 1e3),
144
- // 5 min
155
+ ack_wait: toNanos(5, "minutes"),
145
156
  max_deliver: 1,
146
157
  max_ack_pending: 100,
147
158
  ack_policy: import_nats.AckPolicy.Explicit,
@@ -149,8 +160,7 @@ var DEFAULT_COMMAND_CONSUMER_CONFIG = {
149
160
  replay_policy: import_nats.ReplayPolicy.Instant
150
161
  };
151
162
  var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
152
- ack_wait: nanos(10 * 1e3),
153
- // 10s
163
+ ack_wait: toNanos(10, "seconds"),
154
164
  max_deliver: 3,
155
165
  max_ack_pending: 100,
156
166
  ack_policy: import_nats.AckPolicy.Explicit,
@@ -165,9 +175,6 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
165
175
  JetstreamHeader2["ReplyTo"] = "x-reply-to";
166
176
  JetstreamHeader2["Subject"] = "x-subject";
167
177
  JetstreamHeader2["CallerName"] = "x-caller-name";
168
- JetstreamHeader2["RequestId"] = "x-request-id";
169
- JetstreamHeader2["TraceId"] = "x-trace-id";
170
- JetstreamHeader2["SpanId"] = "x-span-id";
171
178
  JetstreamHeader2["Error"] = "x-error";
172
179
  return JetstreamHeader2;
173
180
  })(JetstreamHeader || {});
@@ -190,16 +197,18 @@ var consumerName = (serviceName, kind) => {
190
197
 
191
198
  // src/client/jetstream.record.ts
192
199
  var JetstreamRecord = class {
193
- constructor(data, headers2, timeout) {
200
+ constructor(data, headers2, timeout, messageId) {
194
201
  this.data = data;
195
202
  this.headers = headers2;
196
203
  this.timeout = timeout;
204
+ this.messageId = messageId;
197
205
  }
198
206
  };
199
207
  var JetstreamRecordBuilder = class {
200
208
  data;
201
209
  headers = /* @__PURE__ */ new Map();
202
210
  timeout;
211
+ messageId;
203
212
  constructor(data) {
204
213
  this.data = data;
205
214
  }
@@ -236,6 +245,28 @@ var JetstreamRecordBuilder = class {
236
245
  }
237
246
  return this;
238
247
  }
248
+ /**
249
+ * Set a custom message ID for JetStream deduplication.
250
+ *
251
+ * NATS JetStream uses this ID to detect duplicate publishes within the
252
+ * stream's `duplicate_window`. If two messages with the same ID arrive
253
+ * within the window, the second is silently dropped.
254
+ *
255
+ * When not set, a random UUID is generated automatically.
256
+ *
257
+ * @param id - Unique message identifier (e.g. order ID, idempotency key).
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * new JetstreamRecordBuilder(data)
262
+ * .setMessageId(`order-${order.id}`)
263
+ * .build();
264
+ * ```
265
+ */
266
+ setMessageId(id) {
267
+ this.messageId = id;
268
+ return this;
269
+ }
239
270
  /**
240
271
  * Set per-request RPC timeout.
241
272
  *
@@ -251,7 +282,12 @@ var JetstreamRecordBuilder = class {
251
282
  * @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
252
283
  */
253
284
  build() {
254
- return new JetstreamRecord(this.data, new Map(this.headers), this.timeout);
285
+ return new JetstreamRecord(
286
+ this.data,
287
+ new Map(this.headers),
288
+ this.timeout,
289
+ this.messageId
290
+ );
255
291
  }
256
292
  /** Validate that a header key is not reserved. */
257
293
  validateHeaderKey(key) {
@@ -331,12 +367,12 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
331
367
  */
332
368
  async dispatchEvent(packet) {
333
369
  const nc = await this.connect();
334
- const { data, hdrs } = this.extractRecordData(packet.data);
370
+ const { data, hdrs, messageId } = this.extractRecordData(packet.data);
335
371
  const subject = this.buildEventSubject(packet.pattern);
336
372
  const msgHeaders = this.buildHeaders(hdrs, { subject });
337
373
  const ack = await nc.jetstream().publish(subject, this.codec.encode(data), {
338
374
  headers: msgHeaders,
339
- msgID: crypto.randomUUID()
375
+ msgID: messageId ?? crypto.randomUUID()
340
376
  });
341
377
  if (ack.duplicate) {
342
378
  this.logger.warn(`Duplicate event publish detected: ${subject} (seq: ${ack.seq})`);
@@ -351,7 +387,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
351
387
  */
352
388
  publish(packet, callback) {
353
389
  const subject = buildSubject(this.targetName, "cmd", packet.pattern);
354
- const { data, hdrs, timeout } = this.extractRecordData(packet.data);
390
+ const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
355
391
  const onUnhandled = (err) => {
356
392
  this.logger.error("Unhandled publish error:", err);
357
393
  callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
@@ -367,7 +403,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
367
403
  hdrs,
368
404
  timeout,
369
405
  callback,
370
- jetStreamCorrelationId
406
+ jetStreamCorrelationId,
407
+ messageId
371
408
  ).catch(onUnhandled);
372
409
  }
373
410
  return () => {
@@ -405,7 +442,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
405
442
  }
406
443
  }
407
444
  /** JetStream mode: publish to stream + wait for inbox response. */
408
- async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID()) {
445
+ async publishJetStreamRpc(subject, data, customHeaders, timeout, callback, correlationId = crypto.randomUUID(), messageId) {
409
446
  const effectiveTimeout = timeout ?? this.getRpcTimeout();
410
447
  this.pendingMessages.set(correlationId, callback);
411
448
  const timeoutId = setTimeout(() => {
@@ -429,7 +466,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
429
466
  });
430
467
  await nc.jetstream().publish(subject, this.codec.encode(data), {
431
468
  headers: hdrs,
432
- msgID: crypto.randomUUID()
469
+ msgID: messageId ?? crypto.randomUUID()
433
470
  });
434
471
  } catch (err) {
435
472
  clearTimeout(timeoutId);
@@ -507,11 +544,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
507
544
  this.pendingMessages.delete(correlationId);
508
545
  }
509
546
  }
510
- /** Build event subject — workqueue or broadcast. */
547
+ /** Build event subject — workqueue, broadcast, or ordered. */
511
548
  buildEventSubject(pattern) {
512
549
  if (pattern.startsWith("broadcast:")) {
513
550
  return buildBroadcastSubject(pattern.slice("broadcast:".length));
514
551
  }
552
+ if (pattern.startsWith("ordered:")) {
553
+ return buildSubject(this.targetName, "ordered", pattern.slice("ordered:".length));
554
+ }
515
555
  return buildSubject(this.targetName, "ev", pattern);
516
556
  }
517
557
  /** Build NATS headers merging custom headers with transport headers. */
@@ -538,10 +578,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
538
578
  return {
539
579
  data: rawData.data,
540
580
  hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
541
- timeout: rawData.timeout
581
+ timeout: rawData.timeout,
582
+ messageId: rawData.messageId
542
583
  };
543
584
  }
544
- return { data: rawData, hdrs: null, timeout: void 0 };
585
+ return { data: rawData, hdrs: null, timeout: void 0, messageId: void 0 };
545
586
  }
546
587
  isCoreRpcMode() {
547
588
  return !this.rootOptions.rpc || this.rootOptions.rpc.mode === "core";
@@ -838,12 +879,23 @@ var JetstreamStrategy = class extends import_microservices2.Server {
838
879
  this.started = true;
839
880
  this.patternRegistry.registerHandlers(this.getHandlers());
840
881
  const streamKinds = this.resolveStreamKinds();
882
+ const durableKinds = this.resolveDurableConsumerKinds();
841
883
  if (streamKinds.length > 0) {
842
884
  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()) {
885
+ if (durableKinds.length > 0) {
886
+ const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
887
+ this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
888
+ this.messageProvider.start(consumers);
889
+ }
890
+ if (this.patternRegistry.hasOrderedHandlers()) {
891
+ const orderedStreamName = this.streamProvider.getStreamName("ordered");
892
+ await this.messageProvider.startOrdered(
893
+ orderedStreamName,
894
+ this.patternRegistry.getOrderedSubjects(),
895
+ this.options.ordered
896
+ );
897
+ }
898
+ if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
847
899
  this.eventRouter.start();
848
900
  }
849
901
  if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
@@ -891,8 +943,25 @@ var JetstreamStrategy = class extends import_microservices2.Server {
891
943
  getPatternRegistry() {
892
944
  return this.patternRegistry;
893
945
  }
894
- /** Determine which JetStream stream kinds are needed. */
946
+ /** Determine which JetStream streams are needed. */
895
947
  resolveStreamKinds() {
948
+ const kinds = [];
949
+ if (this.patternRegistry.hasEventHandlers()) {
950
+ kinds.push("ev");
951
+ }
952
+ if (this.patternRegistry.hasOrderedHandlers()) {
953
+ kinds.push("ordered");
954
+ }
955
+ if (this.isJetStreamRpcMode() && this.patternRegistry.hasRpcHandlers()) {
956
+ kinds.push("cmd");
957
+ }
958
+ if (this.patternRegistry.hasBroadcastHandlers()) {
959
+ kinds.push("broadcast");
960
+ }
961
+ return kinds;
962
+ }
963
+ /** Determine which stream kinds need durable consumers (ordered consumers are ephemeral). */
964
+ resolveDurableConsumerKinds() {
896
965
  const kinds = [];
897
966
  if (this.patternRegistry.hasEventHandlers()) {
898
967
  kinds.push("ev");
@@ -1119,6 +1188,8 @@ var StreamProvider = class {
1119
1188
  return [`${name}.cmd.>`];
1120
1189
  case "broadcast":
1121
1190
  return ["broadcast.>"];
1191
+ case "ordered":
1192
+ return [`${name}.ordered.>`];
1122
1193
  }
1123
1194
  }
1124
1195
  /** Ensure a single stream exists, creating or updating as needed. */
@@ -1161,6 +1232,8 @@ var StreamProvider = class {
1161
1232
  return DEFAULT_COMMAND_STREAM_CONFIG;
1162
1233
  case "broadcast":
1163
1234
  return DEFAULT_BROADCAST_STREAM_CONFIG;
1235
+ case "ordered":
1236
+ return DEFAULT_ORDERED_STREAM_CONFIG;
1164
1237
  }
1165
1238
  }
1166
1239
  /** Get user-provided overrides for a stream kind. */
@@ -1172,6 +1245,8 @@ var StreamProvider = class {
1172
1245
  return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
1173
1246
  case "broadcast":
1174
1247
  return this.options.broadcast?.stream ?? {};
1248
+ case "ordered":
1249
+ return this.options.ordered?.stream ?? {};
1175
1250
  }
1176
1251
  }
1177
1252
  };
@@ -1273,6 +1348,8 @@ var ConsumerProvider = class {
1273
1348
  return DEFAULT_COMMAND_CONSUMER_CONFIG;
1274
1349
  case "broadcast":
1275
1350
  return DEFAULT_BROADCAST_CONSUMER_CONFIG;
1351
+ case "ordered":
1352
+ throw new Error("Ordered consumers are ephemeral and should not use durable config");
1276
1353
  }
1277
1354
  }
1278
1355
  /** Get user-provided overrides for a consumer kind. */
@@ -1284,12 +1361,15 @@ var ConsumerProvider = class {
1284
1361
  return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
1285
1362
  case "broadcast":
1286
1363
  return this.options.broadcast?.consumer ?? {};
1364
+ case "ordered":
1365
+ throw new Error("Ordered consumers are ephemeral and should not use durable config");
1287
1366
  }
1288
1367
  }
1289
1368
  };
1290
1369
 
1291
1370
  // src/server/infrastructure/message.provider.ts
1292
1371
  var import_common7 = require("@nestjs/common");
1372
+ var import_nats8 = require("nats");
1293
1373
  var import_rxjs3 = require("rxjs");
1294
1374
  var MessageProvider = class {
1295
1375
  constructor(connection, eventBus) {
@@ -1298,10 +1378,13 @@ var MessageProvider = class {
1298
1378
  }
1299
1379
  logger = new import_common7.Logger("Jetstream:Message");
1300
1380
  activeIterators = /* @__PURE__ */ new Set();
1381
+ orderedReadyResolve = null;
1382
+ orderedReadyReject = null;
1301
1383
  destroy$ = new import_rxjs3.Subject();
1302
1384
  eventMessages$ = new import_rxjs3.Subject();
1303
1385
  commandMessages$ = new import_rxjs3.Subject();
1304
1386
  broadcastMessages$ = new import_rxjs3.Subject();
1387
+ orderedMessages$ = new import_rxjs3.Subject();
1305
1388
  /** Observable stream of workqueue event messages. */
1306
1389
  get events$() {
1307
1390
  return this.eventMessages$.asObservable();
@@ -1314,6 +1397,10 @@ var MessageProvider = class {
1314
1397
  get broadcasts$() {
1315
1398
  return this.broadcastMessages$.asObservable();
1316
1399
  }
1400
+ /** Observable stream of ordered event messages (strict sequential delivery). */
1401
+ get ordered$() {
1402
+ return this.orderedMessages$.asObservable();
1403
+ }
1317
1404
  /**
1318
1405
  * Start consuming messages from the given consumer infos.
1319
1406
  *
@@ -1329,6 +1416,37 @@ var MessageProvider = class {
1329
1416
  (0, import_rxjs3.merge)(...flows).pipe((0, import_rxjs3.takeUntil)(this.destroy$)).subscribe();
1330
1417
  }
1331
1418
  }
1419
+ /**
1420
+ * Start an ordered consumer for strict sequential delivery.
1421
+ *
1422
+ * Unlike durable consumers, ordered consumers are ephemeral — created at
1423
+ * consumption time, no durable state. nats.js handles auto-recreation.
1424
+ *
1425
+ * @param streamName - JetStream stream to consume from.
1426
+ * @param filterSubjects - NATS subjects to filter on.
1427
+ */
1428
+ async startOrdered(streamName2, filterSubjects, orderedConfig) {
1429
+ const consumerOpts = { filterSubjects };
1430
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_nats8.DeliverPolicy.All) {
1431
+ consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
1432
+ }
1433
+ if (orderedConfig?.optStartSeq !== void 0) {
1434
+ consumerOpts.opt_start_seq = orderedConfig.optStartSeq;
1435
+ }
1436
+ if (orderedConfig?.optStartTime !== void 0) {
1437
+ consumerOpts.opt_start_time = orderedConfig.optStartTime;
1438
+ }
1439
+ if (orderedConfig?.replayPolicy !== void 0) {
1440
+ consumerOpts.replay_policy = orderedConfig.replayPolicy;
1441
+ }
1442
+ const ready = new Promise((resolve, reject) => {
1443
+ this.orderedReadyResolve = resolve;
1444
+ this.orderedReadyReject = reject;
1445
+ });
1446
+ const flow = this.createOrderedFlow(streamName2, consumerOpts);
1447
+ flow.pipe((0, import_rxjs3.takeUntil)(this.destroy$)).subscribe();
1448
+ return ready;
1449
+ }
1332
1450
  /** Stop all consumer flows and reinitialize subjects for potential restart. */
1333
1451
  destroy() {
1334
1452
  this.destroy$.next();
@@ -1340,10 +1458,12 @@ var MessageProvider = class {
1340
1458
  this.eventMessages$.complete();
1341
1459
  this.commandMessages$.complete();
1342
1460
  this.broadcastMessages$.complete();
1461
+ this.orderedMessages$.complete();
1343
1462
  this.destroy$ = new import_rxjs3.Subject();
1344
1463
  this.eventMessages$ = new import_rxjs3.Subject();
1345
1464
  this.commandMessages$ = new import_rxjs3.Subject();
1346
1465
  this.broadcastMessages$ = new import_rxjs3.Subject();
1466
+ this.orderedMessages$ = new import_rxjs3.Subject();
1347
1467
  }
1348
1468
  /** Create a self-healing consumer flow for a specific kind. */
1349
1469
  createFlow(kind, info) {
@@ -1401,6 +1521,63 @@ var MessageProvider = class {
1401
1521
  return this.commandMessages$;
1402
1522
  case "broadcast":
1403
1523
  return this.broadcastMessages$;
1524
+ case "ordered":
1525
+ return this.orderedMessages$;
1526
+ }
1527
+ }
1528
+ /** Create a self-healing ordered consumer flow. */
1529
+ createOrderedFlow(streamName2, consumerOpts) {
1530
+ let consecutiveFailures = 0;
1531
+ let lastRunFailed = false;
1532
+ return (0, import_rxjs3.defer)(() => this.consumeOrderedOnce(streamName2, consumerOpts)).pipe(
1533
+ (0, import_rxjs3.tap)(() => {
1534
+ lastRunFailed = false;
1535
+ }),
1536
+ (0, import_rxjs3.catchError)((err) => {
1537
+ consecutiveFailures++;
1538
+ lastRunFailed = true;
1539
+ this.logger.error("Ordered consumer error, will restart:", err);
1540
+ this.eventBus.emit(
1541
+ "error" /* Error */,
1542
+ err instanceof Error ? err : new Error(String(err)),
1543
+ "message-provider"
1544
+ );
1545
+ if (this.orderedReadyReject) {
1546
+ this.orderedReadyReject(err);
1547
+ this.orderedReadyReject = null;
1548
+ this.orderedReadyResolve = null;
1549
+ }
1550
+ return import_rxjs3.EMPTY;
1551
+ }),
1552
+ (0, import_rxjs3.repeat)({
1553
+ delay: () => {
1554
+ if (!lastRunFailed) {
1555
+ consecutiveFailures = 0;
1556
+ }
1557
+ const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
1558
+ this.logger.warn(`Ordered consumer stream ended, restarting in ${delay}ms...`);
1559
+ return (0, import_rxjs3.timer)(delay);
1560
+ }
1561
+ })
1562
+ );
1563
+ }
1564
+ /** Single iteration: create ordered consumer -> iterate messages. */
1565
+ async consumeOrderedOnce(streamName2, consumerOpts) {
1566
+ const js = (await this.connection.getConnection()).jetstream();
1567
+ const consumer = await js.consumers.get(streamName2, consumerOpts);
1568
+ const messages = await consumer.consume();
1569
+ if (this.orderedReadyResolve) {
1570
+ this.orderedReadyResolve();
1571
+ this.orderedReadyResolve = null;
1572
+ this.orderedReadyReject = null;
1573
+ }
1574
+ this.activeIterators.add(messages);
1575
+ try {
1576
+ for await (const msg of messages) {
1577
+ this.orderedMessages$.next(msg);
1578
+ }
1579
+ } finally {
1580
+ this.activeIterators.delete(messages);
1404
1581
  }
1405
1582
  }
1406
1583
  };
@@ -1421,11 +1598,20 @@ var PatternRegistry = class {
1421
1598
  registerHandlers(handlers) {
1422
1599
  const serviceName = this.options.name;
1423
1600
  for (const [pattern, handler] of handlers) {
1601
+ const extras = handler.extras;
1424
1602
  const isEvent = handler.isEventHandler ?? false;
1425
- const isBroadcast = !!handler.extras?.broadcast;
1603
+ const isBroadcast = !!extras?.broadcast;
1604
+ const isOrdered = !!extras?.ordered;
1605
+ if (isBroadcast && isOrdered) {
1606
+ throw new Error(
1607
+ `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
1608
+ );
1609
+ }
1426
1610
  let fullSubject;
1427
1611
  if (isBroadcast) {
1428
1612
  fullSubject = buildBroadcastSubject(pattern);
1613
+ } else if (isOrdered) {
1614
+ fullSubject = buildSubject(serviceName, "ordered", pattern);
1429
1615
  } else if (isEvent) {
1430
1616
  fullSubject = buildSubject(serviceName, "ev", pattern);
1431
1617
  } else {
@@ -1434,12 +1620,15 @@ var PatternRegistry = class {
1434
1620
  this.registry.set(fullSubject, {
1435
1621
  handler,
1436
1622
  pattern,
1437
- isEvent,
1438
- isBroadcast
1623
+ isEvent: isEvent && !isOrdered,
1624
+ isBroadcast,
1625
+ isOrdered
1439
1626
  });
1440
1627
  let kind;
1441
1628
  if (isBroadcast) {
1442
1629
  kind = "broadcast";
1630
+ } else if (isOrdered) {
1631
+ kind = "ordered";
1443
1632
  } else if (isEvent) {
1444
1633
  kind = "event";
1445
1634
  } else {
@@ -1463,28 +1652,41 @@ var PatternRegistry = class {
1463
1652
  }
1464
1653
  /** Check if any RPC (command) handlers are registered. */
1465
1654
  hasRpcHandlers() {
1466
- return Array.from(this.registry.values()).some((r) => !r.isEvent && !r.isBroadcast);
1655
+ return Array.from(this.registry.values()).some(
1656
+ (r) => !r.isEvent && !r.isBroadcast && !r.isOrdered
1657
+ );
1467
1658
  }
1468
1659
  /** Check if any workqueue event handlers are registered. */
1469
1660
  hasEventHandlers() {
1470
1661
  return Array.from(this.registry.values()).some((r) => r.isEvent && !r.isBroadcast);
1471
1662
  }
1663
+ /** Check if any ordered event handlers are registered. */
1664
+ hasOrderedHandlers() {
1665
+ return Array.from(this.registry.values()).some((r) => r.isOrdered);
1666
+ }
1667
+ /** Get fully-qualified NATS subjects for ordered handlers. */
1668
+ getOrderedSubjects() {
1669
+ const name = internalName(this.options.name);
1670
+ return Array.from(this.registry.values()).filter((r) => r.isOrdered).map((r) => `${name}.ordered.${r.pattern}`);
1671
+ }
1472
1672
  /** Get patterns grouped by kind. */
1473
1673
  getPatternsByKind() {
1474
1674
  const events = [];
1475
1675
  const commands = [];
1476
1676
  const broadcasts = [];
1677
+ const ordered = [];
1477
1678
  for (const entry of this.registry.values()) {
1478
1679
  if (entry.isBroadcast) broadcasts.push(entry.pattern);
1680
+ else if (entry.isOrdered) ordered.push(entry.pattern);
1479
1681
  else if (entry.isEvent) events.push(entry.pattern);
1480
1682
  else commands.push(entry.pattern);
1481
1683
  }
1482
- return { events, commands, broadcasts };
1684
+ return { events, commands, broadcasts, ordered };
1483
1685
  }
1484
1686
  /** Normalize a full NATS subject back to the user-facing pattern. */
1485
1687
  normalizeSubject(subject) {
1486
1688
  const name = internalName(this.options.name);
1487
- const prefixes = [`${name}.cmd.`, `${name}.ev.`, "broadcast."];
1689
+ const prefixes = [`${name}.cmd.`, `${name}.ev.`, `${name}.ordered.`, "broadcast."];
1488
1690
  for (const prefix of prefixes) {
1489
1691
  if (subject.startsWith(prefix)) {
1490
1692
  return subject.slice(prefix.length);
@@ -1494,10 +1696,16 @@ var PatternRegistry = class {
1494
1696
  }
1495
1697
  /** Log a summary of all registered handlers. */
1496
1698
  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
- );
1699
+ const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
1700
+ const parts = [
1701
+ `${commands.length} RPC`,
1702
+ `${events.length} events`,
1703
+ `${broadcasts.length} broadcasts`
1704
+ ];
1705
+ if (ordered.length > 0) {
1706
+ parts.push(`${ordered.length} ordered`);
1707
+ }
1708
+ this.logger.log(`Registered handlers: ${parts.join(", ")}`);
1501
1709
  }
1502
1710
  };
1503
1711
 
@@ -1522,10 +1730,13 @@ var EventRouter = class {
1522
1730
  if (!this.deadLetterConfig) return;
1523
1731
  this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
1524
1732
  }
1525
- /** Start routing event and broadcast messages to handlers. */
1733
+ /** Start routing event, broadcast, and ordered messages to handlers. */
1526
1734
  start() {
1527
1735
  this.subscribeToStream(this.messageProvider.events$, "workqueue");
1528
1736
  this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast");
1737
+ if (this.patternRegistry.hasOrderedHandlers()) {
1738
+ this.subscribeToStream(this.messageProvider.ordered$, "ordered", true);
1739
+ }
1529
1740
  }
1530
1741
  /** Stop routing and unsubscribe from all streams. */
1531
1742
  destroy() {
@@ -1535,17 +1746,14 @@ var EventRouter = class {
1535
1746
  this.subscriptions.length = 0;
1536
1747
  }
1537
1748
  /** 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();
1749
+ subscribeToStream(stream$, label, isOrdered = false) {
1750
+ const route = (msg) => (0, import_rxjs4.defer)(() => isOrdered ? this.handleOrdered(msg) : this.handle(msg)).pipe(
1751
+ (0, import_rxjs4.catchError)((err) => {
1752
+ this.logger.error(`Unexpected error in ${label} event router`, err);
1753
+ return import_rxjs4.EMPTY;
1754
+ })
1755
+ );
1756
+ const subscription = stream$.pipe(isOrdered ? (0, import_rxjs4.concatMap)(route) : (0, import_rxjs4.mergeMap)(route)).subscribe();
1549
1757
  this.subscriptions.push(subscription);
1550
1758
  }
1551
1759
  /** Handle a single event message: decode -> execute handler -> ack/nak. */
@@ -1582,6 +1790,28 @@ var EventRouter = class {
1582
1790
  }
1583
1791
  }
1584
1792
  }
1793
+ /** Handle an ordered message: decode -> execute handler -> no ack/nak. */
1794
+ handleOrdered(msg) {
1795
+ const handler = this.patternRegistry.getHandler(msg.subject);
1796
+ if (!handler) {
1797
+ this.logger.error(`No handler for ordered subject: ${msg.subject}`);
1798
+ return import_rxjs4.EMPTY;
1799
+ }
1800
+ let data;
1801
+ try {
1802
+ data = this.codec.decode(msg.data);
1803
+ } catch (err) {
1804
+ this.logger.error(`Decode error for ordered ${msg.subject}:`, err);
1805
+ return import_rxjs4.EMPTY;
1806
+ }
1807
+ this.eventBus.emit("messageRouted" /* MessageRouted */, msg.subject, "event");
1808
+ const ctx = new RpcContext([msg]);
1809
+ return (0, import_rxjs4.from)(
1810
+ unwrapResult(handler(data, ctx)).catch((err) => {
1811
+ this.logger.error(`Ordered handler error (${msg.subject}):`, err);
1812
+ })
1813
+ );
1814
+ }
1585
1815
  /** Check if the message has exhausted all delivery attempts. */
1586
1816
  isDeadLetter(msg) {
1587
1817
  if (!this.deadLetterConfig) return false;
@@ -1615,7 +1845,7 @@ var EventRouter = class {
1615
1845
 
1616
1846
  // src/server/routing/rpc.router.ts
1617
1847
  var import_common10 = require("@nestjs/common");
1618
- var import_nats8 = require("nats");
1848
+ var import_nats9 = require("nats");
1619
1849
  var import_rxjs5 = require("rxjs");
1620
1850
  var RpcRouter = class {
1621
1851
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, timeout) {
@@ -1677,7 +1907,7 @@ var RpcRouter = class {
1677
1907
  async executeHandler(handler, data, msg, replyTo, correlationId) {
1678
1908
  const nc = await this.connection.getConnection();
1679
1909
  const ctx = new RpcContext([msg]);
1680
- const hdrs = (0, import_nats8.headers)();
1910
+ const hdrs = (0, import_nats9.headers)();
1681
1911
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
1682
1912
  let settled = false;
1683
1913
  const timeoutId = setTimeout(() => {
@@ -2114,6 +2344,5 @@ JetstreamModule = __decorateClass([
2114
2344
  RpcContext,
2115
2345
  TransportEvent,
2116
2346
  getClientToken,
2117
- nanos
2347
+ toNanos
2118
2348
  });
2119
- //# sourceMappingURL=index.cjs.map