@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/README.md +21 -996
- package/dist/index.cjs +294 -65
- package/dist/index.d.cts +116 -31
- package/dist/index.d.ts +116 -31
- package/dist/index.js +299 -63
- package/package.json +6 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
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
|
-
|
|
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
|
|
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:
|
|
102
|
-
|
|
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:
|
|
115
|
-
|
|
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:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
|
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 = !!
|
|
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(
|
|
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
|
-
|
|
1499
|
-
|
|
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
|
|
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
|
|
1540
|
-
(0, import_rxjs4.
|
|
1541
|
-
(
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
2347
|
+
toNanos
|
|
2118
2348
|
});
|
|
2119
|
-
//# sourceMappingURL=index.cjs.map
|