@horizon-republic/nestjs-jetstream 2.8.0 → 2.9.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 +778 -78
- package/dist/index.d.cts +344 -17
- package/dist/index.d.ts +344 -17
- package/dist/index.js +758 -67
- package/package.json +14 -14
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
14
14
|
import {
|
|
15
15
|
Global,
|
|
16
16
|
Inject,
|
|
17
|
-
Logger as
|
|
17
|
+
Logger as Logger14,
|
|
18
18
|
Module,
|
|
19
19
|
Optional
|
|
20
20
|
} from "@nestjs/common";
|
|
@@ -121,7 +121,7 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
|
121
121
|
max_msgs_per_subject: 1e6,
|
|
122
122
|
max_msgs: 1e7,
|
|
123
123
|
max_bytes: 2 * GB,
|
|
124
|
-
max_age: toNanos(1, "
|
|
124
|
+
max_age: toNanos(1, "hours"),
|
|
125
125
|
duplicate_window: toNanos(2, "minutes")
|
|
126
126
|
};
|
|
127
127
|
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
@@ -136,6 +136,18 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
|
136
136
|
max_age: toNanos(1, "days"),
|
|
137
137
|
duplicate_window: toNanos(2, "minutes")
|
|
138
138
|
};
|
|
139
|
+
var DEFAULT_DLQ_STREAM_CONFIG = {
|
|
140
|
+
...baseStreamConfig,
|
|
141
|
+
retention: RetentionPolicy.Workqueue,
|
|
142
|
+
allow_rollup_hdrs: false,
|
|
143
|
+
max_consumers: 100,
|
|
144
|
+
max_msg_size: 10 * MB,
|
|
145
|
+
max_msgs_per_subject: 5e6,
|
|
146
|
+
max_msgs: 5e7,
|
|
147
|
+
max_bytes: 5 * GB,
|
|
148
|
+
max_age: toNanos(30, "days"),
|
|
149
|
+
duplicate_window: toNanos(2, "minutes")
|
|
150
|
+
};
|
|
139
151
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
140
152
|
ack_wait: toNanos(10, "seconds"),
|
|
141
153
|
max_deliver: 3,
|
|
@@ -163,6 +175,12 @@ var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
|
|
|
163
175
|
var DEFAULT_RPC_TIMEOUT = 3e4;
|
|
164
176
|
var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
|
|
165
177
|
var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
|
|
178
|
+
var DEFAULT_METADATA_BUCKET = "handler_registry";
|
|
179
|
+
var DEFAULT_METADATA_REPLICAS = 1;
|
|
180
|
+
var DEFAULT_METADATA_HISTORY = 1;
|
|
181
|
+
var DEFAULT_METADATA_TTL = 3e4;
|
|
182
|
+
var MIN_METADATA_TTL = 5e3;
|
|
183
|
+
var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
|
|
166
184
|
var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
167
185
|
JetstreamHeader2["CorrelationId"] = "x-correlation-id";
|
|
168
186
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
@@ -171,6 +189,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
171
189
|
JetstreamHeader2["Error"] = "x-error";
|
|
172
190
|
return JetstreamHeader2;
|
|
173
191
|
})(JetstreamHeader || {});
|
|
192
|
+
var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
|
|
193
|
+
JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
|
|
194
|
+
JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
|
|
195
|
+
JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
|
|
196
|
+
JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
|
|
197
|
+
JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
|
|
198
|
+
return JetstreamDlqHeader2;
|
|
199
|
+
})(JetstreamDlqHeader || {});
|
|
174
200
|
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
175
201
|
"x-correlation-id" /* CorrelationId */,
|
|
176
202
|
"x-reply-to" /* ReplyTo */,
|
|
@@ -183,6 +209,9 @@ var streamName = (serviceName, kind) => {
|
|
|
183
209
|
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
184
210
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
185
211
|
};
|
|
212
|
+
var dlqStreamName = (serviceName) => {
|
|
213
|
+
return `${internalName(serviceName)}_dlq-stream`;
|
|
214
|
+
};
|
|
186
215
|
var consumerName = (serviceName, kind) => {
|
|
187
216
|
if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
|
|
188
217
|
return `${internalName(serviceName)}_${kind}-consumer`;
|
|
@@ -197,12 +226,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
|
197
226
|
|
|
198
227
|
// src/client/jetstream.record.ts
|
|
199
228
|
var JetstreamRecord = class {
|
|
200
|
-
constructor(data, headers2, timeout, messageId, schedule) {
|
|
229
|
+
constructor(data, headers2, timeout, messageId, schedule, ttl) {
|
|
201
230
|
this.data = data;
|
|
202
231
|
this.headers = headers2;
|
|
203
232
|
this.timeout = timeout;
|
|
204
233
|
this.messageId = messageId;
|
|
205
234
|
this.schedule = schedule;
|
|
235
|
+
this.ttl = ttl;
|
|
206
236
|
}
|
|
207
237
|
};
|
|
208
238
|
var JetstreamRecordBuilder = class {
|
|
@@ -211,6 +241,7 @@ var JetstreamRecordBuilder = class {
|
|
|
211
241
|
timeout;
|
|
212
242
|
messageId;
|
|
213
243
|
scheduleOptions;
|
|
244
|
+
ttlDuration;
|
|
214
245
|
constructor(data) {
|
|
215
246
|
this.data = data;
|
|
216
247
|
}
|
|
@@ -302,6 +333,33 @@ var JetstreamRecordBuilder = class {
|
|
|
302
333
|
this.scheduleOptions = { at: new Date(ts) };
|
|
303
334
|
return this;
|
|
304
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Set per-message TTL (time-to-live).
|
|
338
|
+
*
|
|
339
|
+
* The message expires individually after the specified duration,
|
|
340
|
+
* independent of the stream's `max_age`. Requires NATS >= 2.11 and
|
|
341
|
+
* `allow_msg_ttl: true` on the stream.
|
|
342
|
+
*
|
|
343
|
+
* Only meaningful for events (`client.emit()`). If used with RPC
|
|
344
|
+
* (`client.send()`), a warning is logged and the TTL is ignored.
|
|
345
|
+
*
|
|
346
|
+
* @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* import { toNanos } from '@horizon-republic/nestjs-jetstream';
|
|
351
|
+
*
|
|
352
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
|
|
353
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
ttl(nanos) {
|
|
357
|
+
if (!Number.isFinite(nanos) || nanos <= 0) {
|
|
358
|
+
throw new Error("TTL must be a positive finite value");
|
|
359
|
+
}
|
|
360
|
+
this.ttlDuration = nanosToGoDuration(nanos);
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
305
363
|
/**
|
|
306
364
|
* Build the immutable {@link JetstreamRecord}.
|
|
307
365
|
*
|
|
@@ -314,7 +372,8 @@ var JetstreamRecordBuilder = class {
|
|
|
314
372
|
new Map(this.headers),
|
|
315
373
|
this.timeout,
|
|
316
374
|
this.messageId,
|
|
317
|
-
schedule
|
|
375
|
+
schedule,
|
|
376
|
+
this.ttlDuration
|
|
318
377
|
);
|
|
319
378
|
}
|
|
320
379
|
/** Validate that a header key is not reserved. */
|
|
@@ -326,6 +385,17 @@ var JetstreamRecordBuilder = class {
|
|
|
326
385
|
}
|
|
327
386
|
}
|
|
328
387
|
};
|
|
388
|
+
var NS_PER_MS = 1e6;
|
|
389
|
+
var NS_PER_S = 1e9;
|
|
390
|
+
var NS_PER_M = 60 * NS_PER_S;
|
|
391
|
+
var NS_PER_H = 60 * NS_PER_M;
|
|
392
|
+
var nanosToGoDuration = (nanos) => {
|
|
393
|
+
if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
|
|
394
|
+
if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
|
|
395
|
+
if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
|
|
396
|
+
if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
|
|
397
|
+
return `${nanos}ns`;
|
|
398
|
+
};
|
|
329
399
|
|
|
330
400
|
// src/client/jetstream.client.ts
|
|
331
401
|
var JetstreamClient = class extends ClientProxy {
|
|
@@ -400,7 +470,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
400
470
|
*/
|
|
401
471
|
async dispatchEvent(packet) {
|
|
402
472
|
await this.connect();
|
|
403
|
-
const { data, hdrs, messageId, schedule } = this.extractRecordData(packet.data);
|
|
473
|
+
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
404
474
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
405
475
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
406
476
|
if (schedule) {
|
|
@@ -408,6 +478,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
408
478
|
const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
|
|
409
479
|
headers: msgHeaders,
|
|
410
480
|
msgID: messageId ?? nuid.next(),
|
|
481
|
+
ttl,
|
|
411
482
|
schedule: {
|
|
412
483
|
specification: schedule.at,
|
|
413
484
|
target: eventSubject
|
|
@@ -421,7 +492,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
421
492
|
} else {
|
|
422
493
|
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
423
494
|
headers: msgHeaders,
|
|
424
|
-
msgID: messageId ?? nuid.next()
|
|
495
|
+
msgID: messageId ?? nuid.next(),
|
|
496
|
+
ttl
|
|
425
497
|
});
|
|
426
498
|
if (ack.duplicate) {
|
|
427
499
|
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
@@ -437,12 +509,17 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
437
509
|
*/
|
|
438
510
|
publish(packet, callback) {
|
|
439
511
|
const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
|
|
440
|
-
const { data, hdrs, timeout, messageId, schedule } = this.extractRecordData(packet.data);
|
|
512
|
+
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
441
513
|
if (schedule) {
|
|
442
514
|
this.logger.warn(
|
|
443
515
|
"scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
|
|
444
516
|
);
|
|
445
517
|
}
|
|
518
|
+
if (ttl) {
|
|
519
|
+
this.logger.warn(
|
|
520
|
+
"ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
|
|
521
|
+
);
|
|
522
|
+
}
|
|
446
523
|
const onUnhandled = (err) => {
|
|
447
524
|
this.logger.error("Unhandled publish error:", err);
|
|
448
525
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
@@ -558,6 +635,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
558
635
|
this.pendingTimeouts.clear();
|
|
559
636
|
this.inboxSubscription?.unsubscribe();
|
|
560
637
|
this.inboxSubscription = null;
|
|
638
|
+
this.inbox = null;
|
|
561
639
|
}
|
|
562
640
|
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
563
641
|
setupInbox(nc) {
|
|
@@ -647,7 +725,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
647
725
|
hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
|
|
648
726
|
timeout: rawData.timeout,
|
|
649
727
|
messageId: rawData.messageId,
|
|
650
|
-
schedule: rawData.schedule
|
|
728
|
+
schedule: rawData.schedule,
|
|
729
|
+
ttl: rawData.ttl
|
|
651
730
|
};
|
|
652
731
|
}
|
|
653
732
|
return {
|
|
@@ -655,7 +734,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
655
734
|
hdrs: null,
|
|
656
735
|
timeout: void 0,
|
|
657
736
|
messageId: void 0,
|
|
658
|
-
schedule: void 0
|
|
737
|
+
schedule: void 0,
|
|
738
|
+
ttl: void 0
|
|
659
739
|
};
|
|
660
740
|
}
|
|
661
741
|
/**
|
|
@@ -975,9 +1055,14 @@ var JetstreamHealthIndicator = class {
|
|
|
975
1055
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
976
1056
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
977
1057
|
*
|
|
1058
|
+
* The thrown error sets `isHealthCheckError: true` and `causes` — the
|
|
1059
|
+
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
1060
|
+
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
1061
|
+
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
1062
|
+
*
|
|
978
1063
|
* @param key - Health indicator key (default: `'jetstream'`).
|
|
979
1064
|
* @returns Object with status, server, and latency under the given key.
|
|
980
|
-
* @throws Error with `{ [key]: { status: 'down' } }
|
|
1065
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
981
1066
|
*/
|
|
982
1067
|
async isHealthy(key = "jetstream") {
|
|
983
1068
|
const status = await this.check();
|
|
@@ -987,8 +1072,10 @@ var JetstreamHealthIndicator = class {
|
|
|
987
1072
|
latency: status.latency
|
|
988
1073
|
};
|
|
989
1074
|
if (!status.connected) {
|
|
1075
|
+
const causes = { [key]: details };
|
|
990
1076
|
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
991
|
-
|
|
1077
|
+
causes,
|
|
1078
|
+
isHealthCheckError: true
|
|
992
1079
|
});
|
|
993
1080
|
}
|
|
994
1081
|
return { [key]: details };
|
|
@@ -1001,7 +1088,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
1001
1088
|
// src/server/strategy.ts
|
|
1002
1089
|
import { Server } from "@nestjs/microservices";
|
|
1003
1090
|
var JetstreamStrategy = class extends Server {
|
|
1004
|
-
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
|
|
1091
|
+
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
|
|
1005
1092
|
super();
|
|
1006
1093
|
this.options = options;
|
|
1007
1094
|
this.connection = connection;
|
|
@@ -1013,6 +1100,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
1013
1100
|
this.rpcRouter = rpcRouter;
|
|
1014
1101
|
this.coreRpcServer = coreRpcServer;
|
|
1015
1102
|
this.ackWaitMap = ackWaitMap;
|
|
1103
|
+
this.metadataProvider = metadataProvider;
|
|
1016
1104
|
}
|
|
1017
1105
|
transportId = /* @__PURE__ */ Symbol("jetstream-transport");
|
|
1018
1106
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -1057,10 +1145,14 @@ var JetstreamStrategy = class extends Server {
|
|
|
1057
1145
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
1058
1146
|
await this.coreRpcServer.start();
|
|
1059
1147
|
}
|
|
1148
|
+
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
1149
|
+
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
1150
|
+
}
|
|
1060
1151
|
callback();
|
|
1061
1152
|
}
|
|
1062
|
-
/** Stop all consumers, routers, and
|
|
1153
|
+
/** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
|
|
1063
1154
|
close() {
|
|
1155
|
+
this.metadataProvider?.destroy();
|
|
1064
1156
|
this.eventRouter.destroy();
|
|
1065
1157
|
this.rpcRouter.destroy();
|
|
1066
1158
|
this.coreRpcServer.stop();
|
|
@@ -1436,24 +1528,172 @@ var CoreRpcServer = class {
|
|
|
1436
1528
|
};
|
|
1437
1529
|
|
|
1438
1530
|
// src/server/infrastructure/stream.provider.ts
|
|
1531
|
+
import { Logger as Logger6 } from "@nestjs/common";
|
|
1532
|
+
import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
|
|
1533
|
+
|
|
1534
|
+
// src/server/infrastructure/stream-config-diff.ts
|
|
1535
|
+
var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1536
|
+
"retention"
|
|
1537
|
+
]);
|
|
1538
|
+
var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1539
|
+
"storage"
|
|
1540
|
+
]);
|
|
1541
|
+
var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1542
|
+
"allow_msg_schedules",
|
|
1543
|
+
"allow_msg_ttl",
|
|
1544
|
+
"deny_delete",
|
|
1545
|
+
"deny_purge"
|
|
1546
|
+
]);
|
|
1547
|
+
var compareStreamConfig = (current, desired) => {
|
|
1548
|
+
const changes = [];
|
|
1549
|
+
for (const key of Object.keys(desired)) {
|
|
1550
|
+
const currentVal = current[key];
|
|
1551
|
+
const desiredVal = desired[key];
|
|
1552
|
+
if (isEqual(currentVal, desiredVal)) continue;
|
|
1553
|
+
changes.push({
|
|
1554
|
+
property: key,
|
|
1555
|
+
current: currentVal,
|
|
1556
|
+
desired: desiredVal,
|
|
1557
|
+
mutability: classifyMutability(key, currentVal, desiredVal)
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
|
|
1561
|
+
const hasMutableChanges = changes.some(
|
|
1562
|
+
(c) => c.mutability === "mutable" || c.mutability === "enable-only"
|
|
1563
|
+
);
|
|
1564
|
+
const hasTransportControlledConflicts = changes.some(
|
|
1565
|
+
(c) => c.mutability === "transport-controlled"
|
|
1566
|
+
);
|
|
1567
|
+
return {
|
|
1568
|
+
hasChanges: changes.length > 0,
|
|
1569
|
+
hasMutableChanges,
|
|
1570
|
+
hasImmutableChanges,
|
|
1571
|
+
hasTransportControlledConflicts,
|
|
1572
|
+
changes
|
|
1573
|
+
};
|
|
1574
|
+
};
|
|
1575
|
+
var classifyMutability = (key, current, desired) => {
|
|
1576
|
+
if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
|
|
1577
|
+
if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
|
|
1578
|
+
if (ENABLE_ONLY_PROPERTIES.has(key)) {
|
|
1579
|
+
return current === true && desired === false ? "immutable" : "enable-only";
|
|
1580
|
+
}
|
|
1581
|
+
return "mutable";
|
|
1582
|
+
};
|
|
1583
|
+
var isEqual = (a, b) => {
|
|
1584
|
+
if (a === b) return true;
|
|
1585
|
+
if (a == null && b == null) return true;
|
|
1586
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/server/infrastructure/stream-migration.ts
|
|
1439
1590
|
import { Logger as Logger5 } from "@nestjs/common";
|
|
1440
1591
|
import { JetStreamApiError } from "@nats-io/jetstream";
|
|
1441
|
-
var
|
|
1592
|
+
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
1593
|
+
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
1594
|
+
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
1595
|
+
var StreamMigration = class {
|
|
1596
|
+
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
1597
|
+
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
1598
|
+
}
|
|
1599
|
+
logger = new Logger5("Jetstream:Stream");
|
|
1600
|
+
async migrate(jsm, streamName2, newConfig) {
|
|
1601
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1602
|
+
const startTime = Date.now();
|
|
1603
|
+
const currentInfo = await jsm.streams.info(streamName2);
|
|
1604
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1605
|
+
const messageCount = currentInfo.state.messages;
|
|
1606
|
+
this.logger.log(`Stream ${streamName2}: destructive migration started`);
|
|
1607
|
+
let originalDeleted = false;
|
|
1608
|
+
try {
|
|
1609
|
+
if (messageCount > 0) {
|
|
1610
|
+
this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
|
|
1611
|
+
await jsm.streams.add({
|
|
1612
|
+
...currentInfo.config,
|
|
1613
|
+
name: backupName,
|
|
1614
|
+
subjects: [],
|
|
1615
|
+
sources: [{ name: streamName2 }]
|
|
1616
|
+
});
|
|
1617
|
+
await this.waitForSourcing(jsm, backupName, messageCount);
|
|
1618
|
+
}
|
|
1619
|
+
this.logger.log(` Phase 2/4: Deleting old stream`);
|
|
1620
|
+
await jsm.streams.delete(streamName2);
|
|
1621
|
+
originalDeleted = true;
|
|
1622
|
+
this.logger.log(` Phase 3/4: Creating stream with new config`);
|
|
1623
|
+
await jsm.streams.add(newConfig);
|
|
1624
|
+
if (messageCount > 0) {
|
|
1625
|
+
const backupInfo = await jsm.streams.info(backupName);
|
|
1626
|
+
await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
|
|
1627
|
+
this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
|
|
1628
|
+
await jsm.streams.update(streamName2, {
|
|
1629
|
+
...newConfig,
|
|
1630
|
+
sources: [{ name: backupName }]
|
|
1631
|
+
});
|
|
1632
|
+
await this.waitForSourcing(jsm, streamName2, messageCount);
|
|
1633
|
+
await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
|
|
1634
|
+
await jsm.streams.delete(backupName);
|
|
1635
|
+
}
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
if (originalDeleted && messageCount > 0) {
|
|
1638
|
+
this.logger.error(
|
|
1639
|
+
`Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
|
|
1640
|
+
);
|
|
1641
|
+
} else {
|
|
1642
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1643
|
+
}
|
|
1644
|
+
throw err;
|
|
1645
|
+
}
|
|
1646
|
+
const durationMs = Date.now() - startTime;
|
|
1647
|
+
this.logger.log(
|
|
1648
|
+
`Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
async waitForSourcing(jsm, streamName2, expectedCount) {
|
|
1652
|
+
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
1653
|
+
while (Date.now() < deadline) {
|
|
1654
|
+
const info = await jsm.streams.info(streamName2);
|
|
1655
|
+
if (info.state.messages >= expectedCount) return;
|
|
1656
|
+
await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
|
|
1657
|
+
}
|
|
1658
|
+
throw new Error(
|
|
1659
|
+
`Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
async cleanupOrphanedBackup(jsm, backupName) {
|
|
1663
|
+
try {
|
|
1664
|
+
await jsm.streams.info(backupName);
|
|
1665
|
+
this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
|
|
1666
|
+
await jsm.streams.delete(backupName);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
if (err instanceof JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
throw err;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// src/server/infrastructure/stream.provider.ts
|
|
1442
1677
|
var StreamProvider = class {
|
|
1443
1678
|
constructor(options, connection) {
|
|
1444
1679
|
this.options = options;
|
|
1445
1680
|
this.connection = connection;
|
|
1446
1681
|
}
|
|
1447
|
-
logger = new
|
|
1682
|
+
logger = new Logger6("Jetstream:Stream");
|
|
1683
|
+
migration = new StreamMigration();
|
|
1448
1684
|
/**
|
|
1449
1685
|
* Ensure all required streams exist with correct configuration.
|
|
1450
1686
|
*
|
|
1451
1687
|
* @param kinds Which stream kinds to create. Determined by the module based
|
|
1452
1688
|
* on RPC mode and registered handler patterns.
|
|
1689
|
+
* If the dlq option is enabled, also ensures the DLQ stream exists.
|
|
1453
1690
|
*/
|
|
1454
1691
|
async ensureStreams(kinds) {
|
|
1455
1692
|
const jsm = await this.connection.getJetStreamManager();
|
|
1456
1693
|
await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
|
|
1694
|
+
if (this.options.dlq) {
|
|
1695
|
+
await this.ensureDlqStream(jsm);
|
|
1696
|
+
}
|
|
1457
1697
|
}
|
|
1458
1698
|
/** Get the stream name for a given kind. */
|
|
1459
1699
|
getStreamName(kind) {
|
|
@@ -1488,17 +1728,85 @@ var StreamProvider = class {
|
|
|
1488
1728
|
const config = this.buildConfig(kind);
|
|
1489
1729
|
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
1490
1730
|
try {
|
|
1491
|
-
await jsm.streams.info(config.name);
|
|
1492
|
-
this.
|
|
1493
|
-
return await jsm.streams.update(config.name, config);
|
|
1731
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1732
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1494
1733
|
} catch (err) {
|
|
1495
|
-
if (err instanceof
|
|
1734
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1496
1735
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
1497
1736
|
return await jsm.streams.add(config);
|
|
1498
1737
|
}
|
|
1499
1738
|
throw err;
|
|
1500
1739
|
}
|
|
1501
1740
|
}
|
|
1741
|
+
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
1742
|
+
async ensureDlqStream(jsm) {
|
|
1743
|
+
const config = this.buildDlqConfig();
|
|
1744
|
+
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
1745
|
+
try {
|
|
1746
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1747
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1750
|
+
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
1751
|
+
return await jsm.streams.add(config);
|
|
1752
|
+
}
|
|
1753
|
+
throw err;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async handleExistingStream(jsm, currentInfo, config) {
|
|
1757
|
+
const diff = compareStreamConfig(currentInfo.config, config);
|
|
1758
|
+
if (!diff.hasChanges) {
|
|
1759
|
+
this.logger.debug(`Stream ${config.name}: no config changes`);
|
|
1760
|
+
return currentInfo;
|
|
1761
|
+
}
|
|
1762
|
+
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
1763
|
+
if (diff.hasTransportControlledConflicts) {
|
|
1764
|
+
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
|
|
1765
|
+
throw new Error(
|
|
1766
|
+
`Stream ${config.name} has transport-controlled config conflicts that cannot be migrated: ${conflicts}. The retention policy is managed by the transport and must match the stream kind.`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
if (!diff.hasImmutableChanges) {
|
|
1770
|
+
this.logger.debug(`Stream exists, updating: ${config.name}`);
|
|
1771
|
+
return await jsm.streams.update(config.name, config);
|
|
1772
|
+
}
|
|
1773
|
+
if (!this.options.allowDestructiveMigration) {
|
|
1774
|
+
this.logger.warn(
|
|
1775
|
+
`Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
|
|
1776
|
+
);
|
|
1777
|
+
if (diff.hasMutableChanges) {
|
|
1778
|
+
const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
|
|
1779
|
+
return await jsm.streams.update(config.name, mutableConfig);
|
|
1780
|
+
}
|
|
1781
|
+
return currentInfo;
|
|
1782
|
+
}
|
|
1783
|
+
await this.migration.migrate(jsm, config.name, config);
|
|
1784
|
+
return await jsm.streams.info(config.name);
|
|
1785
|
+
}
|
|
1786
|
+
buildMutableOnlyConfig(config, currentConfig, diff) {
|
|
1787
|
+
const nonMutableKeys = new Set(
|
|
1788
|
+
diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
|
|
1789
|
+
);
|
|
1790
|
+
const filtered = { ...config };
|
|
1791
|
+
for (const key of nonMutableKeys) {
|
|
1792
|
+
filtered[key] = currentConfig[key];
|
|
1793
|
+
}
|
|
1794
|
+
return filtered;
|
|
1795
|
+
}
|
|
1796
|
+
logChanges(streamName2, diff, migrationEnabled) {
|
|
1797
|
+
for (const c of diff.changes) {
|
|
1798
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
|
|
1799
|
+
if (c.mutability === "transport-controlled") {
|
|
1800
|
+
this.logger.error(
|
|
1801
|
+
`Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
|
|
1802
|
+
);
|
|
1803
|
+
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
1804
|
+
this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
|
|
1805
|
+
} else {
|
|
1806
|
+
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1502
1810
|
/** Build the full stream config by merging defaults with user overrides. */
|
|
1503
1811
|
buildConfig(kind) {
|
|
1504
1812
|
const name = this.getStreamName(kind);
|
|
@@ -1514,6 +1822,26 @@ var StreamProvider = class {
|
|
|
1514
1822
|
description
|
|
1515
1823
|
};
|
|
1516
1824
|
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Build the stream configuration for the Dead-Letter Queue (DLQ).
|
|
1827
|
+
*
|
|
1828
|
+
* Merges the library default DLQ config with user-provided overrides.
|
|
1829
|
+
* Ensures transport-controlled settings like retention are safely decoupled.
|
|
1830
|
+
*/
|
|
1831
|
+
buildDlqConfig() {
|
|
1832
|
+
const name = dlqStreamName(this.options.name);
|
|
1833
|
+
const subjects = [name];
|
|
1834
|
+
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
1835
|
+
const overrides = this.options.dlq?.stream ?? {};
|
|
1836
|
+
const safeOverrides = this.stripTransportControlled(overrides);
|
|
1837
|
+
return {
|
|
1838
|
+
...DEFAULT_DLQ_STREAM_CONFIG,
|
|
1839
|
+
...safeOverrides,
|
|
1840
|
+
name,
|
|
1841
|
+
subjects,
|
|
1842
|
+
description
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1517
1845
|
/** Get default config for a stream kind. */
|
|
1518
1846
|
getDefaults(kind) {
|
|
1519
1847
|
switch (kind) {
|
|
@@ -1532,25 +1860,44 @@ var StreamProvider = class {
|
|
|
1532
1860
|
const overrides = this.getOverrides(kind);
|
|
1533
1861
|
return overrides.allow_msg_schedules === true;
|
|
1534
1862
|
}
|
|
1535
|
-
/** Get user-provided overrides for a stream kind. */
|
|
1863
|
+
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
1536
1864
|
getOverrides(kind) {
|
|
1865
|
+
let overrides;
|
|
1537
1866
|
switch (kind) {
|
|
1538
1867
|
case "ev" /* Event */:
|
|
1539
|
-
|
|
1868
|
+
overrides = this.options.events?.stream ?? {};
|
|
1869
|
+
break;
|
|
1540
1870
|
case "cmd" /* Command */:
|
|
1541
|
-
|
|
1871
|
+
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1872
|
+
break;
|
|
1542
1873
|
case "broadcast" /* Broadcast */:
|
|
1543
|
-
|
|
1874
|
+
overrides = this.options.broadcast?.stream ?? {};
|
|
1875
|
+
break;
|
|
1544
1876
|
case "ordered" /* Ordered */:
|
|
1545
|
-
|
|
1877
|
+
overrides = this.options.ordered?.stream ?? {};
|
|
1878
|
+
break;
|
|
1546
1879
|
}
|
|
1880
|
+
return this.stripTransportControlled(overrides);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Remove transport-controlled properties from user overrides.
|
|
1884
|
+
* `retention` is managed by the transport (Workqueue/Limits per stream kind)
|
|
1885
|
+
* and silently stripped to protect users from misconfiguration.
|
|
1886
|
+
*/
|
|
1887
|
+
stripTransportControlled(overrides) {
|
|
1888
|
+
if (!("retention" in overrides)) return overrides;
|
|
1889
|
+
this.logger.debug(
|
|
1890
|
+
"Stripping user-provided retention override \u2014 retention is managed by the transport"
|
|
1891
|
+
);
|
|
1892
|
+
const cleaned = { ...overrides };
|
|
1893
|
+
delete cleaned.retention;
|
|
1894
|
+
return cleaned;
|
|
1547
1895
|
}
|
|
1548
1896
|
};
|
|
1549
1897
|
|
|
1550
1898
|
// src/server/infrastructure/consumer.provider.ts
|
|
1551
|
-
import { Logger as
|
|
1552
|
-
import { JetStreamApiError as
|
|
1553
|
-
var CONSUMER_NOT_FOUND = 10014;
|
|
1899
|
+
import { Logger as Logger7 } from "@nestjs/common";
|
|
1900
|
+
import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
|
|
1554
1901
|
var ConsumerProvider = class {
|
|
1555
1902
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
1556
1903
|
this.options = options;
|
|
@@ -1558,7 +1905,7 @@ var ConsumerProvider = class {
|
|
|
1558
1905
|
this.streamProvider = streamProvider;
|
|
1559
1906
|
this.patternRegistry = patternRegistry;
|
|
1560
1907
|
}
|
|
1561
|
-
logger = new
|
|
1908
|
+
logger = new Logger7("Jetstream:Consumer");
|
|
1562
1909
|
/**
|
|
1563
1910
|
* Ensure consumers exist for the specified kinds.
|
|
1564
1911
|
*
|
|
@@ -1579,7 +1926,11 @@ var ConsumerProvider = class {
|
|
|
1579
1926
|
getConsumerName(kind) {
|
|
1580
1927
|
return consumerName(this.options.name, kind);
|
|
1581
1928
|
}
|
|
1582
|
-
/**
|
|
1929
|
+
/**
|
|
1930
|
+
* Ensure a single consumer exists with the desired config.
|
|
1931
|
+
* Used at **startup** — creates or updates the consumer to match
|
|
1932
|
+
* the current pod's configuration.
|
|
1933
|
+
*/
|
|
1583
1934
|
async ensureConsumer(jsm, kind) {
|
|
1584
1935
|
const stream = this.streamProvider.getStreamName(kind);
|
|
1585
1936
|
const config = this.buildConfig(kind);
|
|
@@ -1590,13 +1941,74 @@ var ConsumerProvider = class {
|
|
|
1590
1941
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
1591
1942
|
return await jsm.consumers.update(stream, name, config);
|
|
1592
1943
|
} catch (err) {
|
|
1593
|
-
if (err instanceof
|
|
1594
|
-
|
|
1595
|
-
|
|
1944
|
+
if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
1945
|
+
throw err;
|
|
1946
|
+
}
|
|
1947
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Recover a consumer that disappeared during runtime.
|
|
1952
|
+
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
1953
|
+
*
|
|
1954
|
+
* If a migration backup stream exists, another pod is mid-migration — we
|
|
1955
|
+
* throw so the self-healing retry loop waits with backoff until migration
|
|
1956
|
+
* completes and the backup is cleaned up.
|
|
1957
|
+
*
|
|
1958
|
+
* This prevents old pods from:
|
|
1959
|
+
* - Overwriting a newer pod's consumer config during rolling updates
|
|
1960
|
+
* - Creating consumers during migration (which would consume and delete
|
|
1961
|
+
* workqueue messages while they're being restored)
|
|
1962
|
+
*/
|
|
1963
|
+
async recoverConsumer(jsm, kind) {
|
|
1964
|
+
const stream = this.streamProvider.getStreamName(kind);
|
|
1965
|
+
const config = this.buildConfig(kind);
|
|
1966
|
+
const name = config.durable_name;
|
|
1967
|
+
this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
|
|
1968
|
+
await this.assertNoMigrationInProgress(jsm, stream);
|
|
1969
|
+
try {
|
|
1970
|
+
return await jsm.consumers.info(stream, name);
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
1973
|
+
throw err;
|
|
1974
|
+
}
|
|
1975
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Throw if a migration backup stream exists for this stream.
|
|
1980
|
+
* The self-healing retry loop catches the error and retries with backoff,
|
|
1981
|
+
* naturally waiting until the migrating pod finishes and cleans up the backup.
|
|
1982
|
+
*/
|
|
1983
|
+
async assertNoMigrationInProgress(jsm, stream) {
|
|
1984
|
+
const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1985
|
+
try {
|
|
1986
|
+
await jsm.streams.info(backupName);
|
|
1987
|
+
throw new Error(
|
|
1988
|
+
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
1989
|
+
);
|
|
1990
|
+
} catch (err) {
|
|
1991
|
+
if (err instanceof JetStreamApiError3 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1992
|
+
return;
|
|
1596
1993
|
}
|
|
1597
1994
|
throw err;
|
|
1598
1995
|
}
|
|
1599
1996
|
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Create a consumer, handling the race where another pod creates it first.
|
|
1999
|
+
*/
|
|
2000
|
+
async createConsumer(jsm, stream, name, config) {
|
|
2001
|
+
this.logger.log(`Creating consumer: ${name}`);
|
|
2002
|
+
try {
|
|
2003
|
+
return await jsm.consumers.add(stream, config);
|
|
2004
|
+
} catch (addErr) {
|
|
2005
|
+
if (addErr instanceof JetStreamApiError3 && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
2006
|
+
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
2007
|
+
return await jsm.consumers.info(stream, name);
|
|
2008
|
+
}
|
|
2009
|
+
throw addErr;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1600
2012
|
/** Build consumer config by merging defaults with user overrides. */
|
|
1601
2013
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
1602
2014
|
buildConfig(kind) {
|
|
@@ -1649,6 +2061,7 @@ var ConsumerProvider = class {
|
|
|
1649
2061
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1650
2062
|
case "ordered" /* Ordered */:
|
|
1651
2063
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2064
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1652
2065
|
default: {
|
|
1653
2066
|
const _exhaustive = kind;
|
|
1654
2067
|
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
@@ -1666,6 +2079,7 @@ var ConsumerProvider = class {
|
|
|
1666
2079
|
return this.options.broadcast?.consumer ?? {};
|
|
1667
2080
|
case "ordered" /* Ordered */:
|
|
1668
2081
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2082
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1669
2083
|
default: {
|
|
1670
2084
|
const _exhaustive = kind;
|
|
1671
2085
|
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
@@ -1675,7 +2089,7 @@ var ConsumerProvider = class {
|
|
|
1675
2089
|
};
|
|
1676
2090
|
|
|
1677
2091
|
// src/server/infrastructure/message.provider.ts
|
|
1678
|
-
import { Logger as
|
|
2092
|
+
import { Logger as Logger8 } from "@nestjs/common";
|
|
1679
2093
|
import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
|
|
1680
2094
|
import {
|
|
1681
2095
|
catchError,
|
|
@@ -1689,12 +2103,13 @@ import {
|
|
|
1689
2103
|
timer
|
|
1690
2104
|
} from "rxjs";
|
|
1691
2105
|
var MessageProvider = class {
|
|
1692
|
-
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
|
|
2106
|
+
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
1693
2107
|
this.connection = connection;
|
|
1694
2108
|
this.eventBus = eventBus;
|
|
1695
2109
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2110
|
+
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
1696
2111
|
}
|
|
1697
|
-
logger = new
|
|
2112
|
+
logger = new Logger8("Jetstream:Message");
|
|
1698
2113
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1699
2114
|
orderedReadyResolve = null;
|
|
1700
2115
|
orderedReadyReject = null;
|
|
@@ -1797,12 +2212,26 @@ var MessageProvider = class {
|
|
|
1797
2212
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
1798
2213
|
async consumeOnce(kind, info, target$) {
|
|
1799
2214
|
const js = this.connection.getJetStreamClient();
|
|
1800
|
-
|
|
2215
|
+
let consumer;
|
|
2216
|
+
let consumerName2 = info.name;
|
|
2217
|
+
try {
|
|
2218
|
+
consumer = await js.consumers.get(info.stream_name, info.name);
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
|
|
2221
|
+
this.logger.warn(`Consumer ${info.name} not found, recreating...`);
|
|
2222
|
+
const recovered = await this.consumerRecoveryFn(kind);
|
|
2223
|
+
consumerName2 = recovered.name;
|
|
2224
|
+
this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
|
|
2225
|
+
consumer = await js.consumers.get(recovered.stream_name, consumerName2);
|
|
2226
|
+
} else {
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
1801
2230
|
const defaults = { idle_heartbeat: 5e3 };
|
|
1802
2231
|
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
1803
2232
|
const messages = await consumer.consume({ ...defaults, ...userOptions });
|
|
1804
2233
|
this.activeIterators.add(messages);
|
|
1805
|
-
this.monitorConsumerHealth(messages,
|
|
2234
|
+
this.monitorConsumerHealth(messages, consumerName2);
|
|
1806
2235
|
try {
|
|
1807
2236
|
for await (const msg of messages) {
|
|
1808
2237
|
target$.next(msg);
|
|
@@ -1811,6 +2240,17 @@ var MessageProvider = class {
|
|
|
1811
2240
|
this.activeIterators.delete(messages);
|
|
1812
2241
|
}
|
|
1813
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Detect "consumer not found" errors from `js.consumers.get()`.
|
|
2245
|
+
*
|
|
2246
|
+
* Unlike JetStream Manager calls (which throw `JetStreamApiError`),
|
|
2247
|
+
* the JetStream client's `consumers.get()` throws a plain `Error`
|
|
2248
|
+
* with the error code embedded in the message text.
|
|
2249
|
+
*/
|
|
2250
|
+
isConsumerNotFound(err) {
|
|
2251
|
+
if (!(err instanceof Error)) return false;
|
|
2252
|
+
return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
|
|
2253
|
+
}
|
|
1814
2254
|
/** Get the target subject for a consumer kind. */
|
|
1815
2255
|
getTargetSubject(kind) {
|
|
1816
2256
|
switch (kind) {
|
|
@@ -1822,6 +2262,7 @@ var MessageProvider = class {
|
|
|
1822
2262
|
return this.broadcastMessages$;
|
|
1823
2263
|
case "ordered" /* Ordered */:
|
|
1824
2264
|
return this.orderedMessages$;
|
|
2265
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1825
2266
|
default: {
|
|
1826
2267
|
const _exhaustive = kind;
|
|
1827
2268
|
throw new Error(`Unknown stream kind: ${_exhaustive}`);
|
|
@@ -1907,8 +2348,110 @@ var MessageProvider = class {
|
|
|
1907
2348
|
}
|
|
1908
2349
|
};
|
|
1909
2350
|
|
|
2351
|
+
// src/server/infrastructure/metadata.provider.ts
|
|
2352
|
+
import { Logger as Logger9 } from "@nestjs/common";
|
|
2353
|
+
import { Kvm } from "@nats-io/kv";
|
|
2354
|
+
var MetadataProvider = class {
|
|
2355
|
+
constructor(options, connection) {
|
|
2356
|
+
this.connection = connection;
|
|
2357
|
+
this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
|
|
2358
|
+
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
2359
|
+
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
2360
|
+
}
|
|
2361
|
+
logger = new Logger9("Jetstream:Metadata");
|
|
2362
|
+
bucketName;
|
|
2363
|
+
replicas;
|
|
2364
|
+
ttl;
|
|
2365
|
+
currentEntries;
|
|
2366
|
+
heartbeatTimer;
|
|
2367
|
+
cachedKv;
|
|
2368
|
+
/**
|
|
2369
|
+
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
2370
|
+
*
|
|
2371
|
+
* Creates the bucket if it doesn't exist (idempotent).
|
|
2372
|
+
* Skips silently when entries map is empty.
|
|
2373
|
+
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
2374
|
+
* to prevent TTL expiry while the pod is alive.
|
|
2375
|
+
*
|
|
2376
|
+
* Non-critical — errors are logged but do not prevent transport startup.
|
|
2377
|
+
*
|
|
2378
|
+
* @param entries Map of KV key → metadata object.
|
|
2379
|
+
*/
|
|
2380
|
+
async publish(entries) {
|
|
2381
|
+
if (entries.size === 0) return;
|
|
2382
|
+
try {
|
|
2383
|
+
const kv = await this.openBucket();
|
|
2384
|
+
await this.writeEntries(kv, entries);
|
|
2385
|
+
this.currentEntries = entries;
|
|
2386
|
+
this.startHeartbeat();
|
|
2387
|
+
this.logger.log(
|
|
2388
|
+
`Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
|
|
2389
|
+
);
|
|
2390
|
+
} catch (err) {
|
|
2391
|
+
this.logger.error("Failed to publish handler metadata to KV", err);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Stop the heartbeat timer.
|
|
2396
|
+
*
|
|
2397
|
+
* After this call, entries will expire via TTL once the heartbeat window passes.
|
|
2398
|
+
* Called during transport shutdown (strategy.close()).
|
|
2399
|
+
*/
|
|
2400
|
+
destroy() {
|
|
2401
|
+
if (this.heartbeatTimer) {
|
|
2402
|
+
clearInterval(this.heartbeatTimer);
|
|
2403
|
+
this.heartbeatTimer = void 0;
|
|
2404
|
+
}
|
|
2405
|
+
this.currentEntries = void 0;
|
|
2406
|
+
this.cachedKv = void 0;
|
|
2407
|
+
}
|
|
2408
|
+
/** Write entries to KV with per-entry error handling. */
|
|
2409
|
+
async writeEntries(kv, entries) {
|
|
2410
|
+
for (const [key, meta] of entries) {
|
|
2411
|
+
try {
|
|
2412
|
+
await kv.put(key, JSON.stringify(meta));
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
this.logger.error(`Failed to write metadata entry "${key}"`, err);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
/** Start heartbeat interval that refreshes entries every ttl/2. */
|
|
2419
|
+
startHeartbeat() {
|
|
2420
|
+
if (this.heartbeatTimer) {
|
|
2421
|
+
clearInterval(this.heartbeatTimer);
|
|
2422
|
+
}
|
|
2423
|
+
const interval = Math.floor(this.ttl / 2);
|
|
2424
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2425
|
+
void this.refreshEntries();
|
|
2426
|
+
}, interval);
|
|
2427
|
+
this.heartbeatTimer.unref();
|
|
2428
|
+
}
|
|
2429
|
+
/** Refresh all current entries in KV (heartbeat tick). */
|
|
2430
|
+
async refreshEntries() {
|
|
2431
|
+
if (!this.currentEntries || this.currentEntries.size === 0) return;
|
|
2432
|
+
try {
|
|
2433
|
+
const kv = await this.openBucket();
|
|
2434
|
+
await this.writeEntries(kv, this.currentEntries);
|
|
2435
|
+
} catch (err) {
|
|
2436
|
+
this.logger.error("Failed to refresh handler metadata in KV", err);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
/** Create or open the KV bucket (cached after first call). */
|
|
2440
|
+
async openBucket() {
|
|
2441
|
+
if (this.cachedKv) return this.cachedKv;
|
|
2442
|
+
const js = this.connection.getJetStreamClient();
|
|
2443
|
+
const kvm = new Kvm(js);
|
|
2444
|
+
this.cachedKv = await kvm.create(this.bucketName, {
|
|
2445
|
+
history: DEFAULT_METADATA_HISTORY,
|
|
2446
|
+
replicas: this.replicas,
|
|
2447
|
+
ttl: this.ttl
|
|
2448
|
+
});
|
|
2449
|
+
return this.cachedKv;
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
|
|
1910
2453
|
// src/server/routing/pattern-registry.ts
|
|
1911
|
-
import { Logger as
|
|
2454
|
+
import { Logger as Logger10 } from "@nestjs/common";
|
|
1912
2455
|
var HANDLER_LABELS = {
|
|
1913
2456
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
1914
2457
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -1919,7 +2462,7 @@ var PatternRegistry = class {
|
|
|
1919
2462
|
constructor(options) {
|
|
1920
2463
|
this.options = options;
|
|
1921
2464
|
}
|
|
1922
|
-
logger = new
|
|
2465
|
+
logger = new Logger10("Jetstream:PatternRegistry");
|
|
1923
2466
|
registry = /* @__PURE__ */ new Map();
|
|
1924
2467
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1925
2468
|
cachedPatterns = null;
|
|
@@ -1927,6 +2470,7 @@ var PatternRegistry = class {
|
|
|
1927
2470
|
_hasCommands = false;
|
|
1928
2471
|
_hasBroadcasts = false;
|
|
1929
2472
|
_hasOrdered = false;
|
|
2473
|
+
_hasMetadata = false;
|
|
1930
2474
|
/**
|
|
1931
2475
|
* Register all handlers from the NestJS strategy.
|
|
1932
2476
|
*
|
|
@@ -1939,6 +2483,7 @@ var PatternRegistry = class {
|
|
|
1939
2483
|
const isEvent = handler.isEventHandler ?? false;
|
|
1940
2484
|
const isBroadcast = !!extras?.broadcast;
|
|
1941
2485
|
const isOrdered = !!extras?.ordered;
|
|
2486
|
+
const meta = extras?.meta;
|
|
1942
2487
|
if (isBroadcast && isOrdered) {
|
|
1943
2488
|
throw new Error(
|
|
1944
2489
|
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
@@ -1955,7 +2500,8 @@ var PatternRegistry = class {
|
|
|
1955
2500
|
pattern,
|
|
1956
2501
|
isEvent: isEvent && !isOrdered,
|
|
1957
2502
|
isBroadcast,
|
|
1958
|
-
isOrdered
|
|
2503
|
+
isOrdered,
|
|
2504
|
+
meta
|
|
1959
2505
|
});
|
|
1960
2506
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1961
2507
|
}
|
|
@@ -1964,6 +2510,7 @@ var PatternRegistry = class {
|
|
|
1964
2510
|
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1965
2511
|
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1966
2512
|
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2513
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
1967
2514
|
this.logSummary();
|
|
1968
2515
|
}
|
|
1969
2516
|
/** Find handler for a full NATS subject. */
|
|
@@ -1992,6 +2539,26 @@ var PatternRegistry = class {
|
|
|
1992
2539
|
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
1993
2540
|
);
|
|
1994
2541
|
}
|
|
2542
|
+
/** Check if any registered handler has metadata. */
|
|
2543
|
+
hasMetadata() {
|
|
2544
|
+
return this._hasMetadata;
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Get handler metadata entries for KV publishing.
|
|
2548
|
+
*
|
|
2549
|
+
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2550
|
+
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2551
|
+
*/
|
|
2552
|
+
getMetadataEntries() {
|
|
2553
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2554
|
+
for (const entry of this.registry.values()) {
|
|
2555
|
+
if (!entry.meta) continue;
|
|
2556
|
+
const kind = this.resolveStreamKind(entry);
|
|
2557
|
+
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2558
|
+
entries.set(key, entry.meta);
|
|
2559
|
+
}
|
|
2560
|
+
return entries;
|
|
2561
|
+
}
|
|
1995
2562
|
/** Get patterns grouped by kind (cached after registration). */
|
|
1996
2563
|
getPatternsByKind() {
|
|
1997
2564
|
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
@@ -2031,6 +2598,12 @@ var PatternRegistry = class {
|
|
|
2031
2598
|
}
|
|
2032
2599
|
return { events, commands, broadcasts, ordered };
|
|
2033
2600
|
}
|
|
2601
|
+
resolveStreamKind(entry) {
|
|
2602
|
+
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2603
|
+
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2604
|
+
if (entry.isEvent) return "ev" /* Event */;
|
|
2605
|
+
return "cmd" /* Command */;
|
|
2606
|
+
}
|
|
2034
2607
|
logSummary() {
|
|
2035
2608
|
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2036
2609
|
const parts = [
|
|
@@ -2046,10 +2619,11 @@ var PatternRegistry = class {
|
|
|
2046
2619
|
};
|
|
2047
2620
|
|
|
2048
2621
|
// src/server/routing/event.router.ts
|
|
2049
|
-
import { Logger as
|
|
2622
|
+
import { Logger as Logger11 } from "@nestjs/common";
|
|
2050
2623
|
import { concatMap, from as from2, mergeMap } from "rxjs";
|
|
2624
|
+
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
2051
2625
|
var EventRouter = class {
|
|
2052
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
2626
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
2053
2627
|
this.messageProvider = messageProvider;
|
|
2054
2628
|
this.patternRegistry = patternRegistry;
|
|
2055
2629
|
this.codec = codec;
|
|
@@ -2057,8 +2631,10 @@ var EventRouter = class {
|
|
|
2057
2631
|
this.deadLetterConfig = deadLetterConfig;
|
|
2058
2632
|
this.processingConfig = processingConfig;
|
|
2059
2633
|
this.ackWaitMap = ackWaitMap;
|
|
2634
|
+
this.connection = connection;
|
|
2635
|
+
this.options = options;
|
|
2060
2636
|
}
|
|
2061
|
-
logger = new
|
|
2637
|
+
logger = new Logger11("Jetstream:EventRouter");
|
|
2062
2638
|
subscriptions = [];
|
|
2063
2639
|
/**
|
|
2064
2640
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
@@ -2185,6 +2761,93 @@ var EventRouter = class {
|
|
|
2185
2761
|
return msg.info.deliveryCount >= maxDeliver;
|
|
2186
2762
|
}
|
|
2187
2763
|
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
2764
|
+
/**
|
|
2765
|
+
* Fallback execution for a dead letter when DLQ is disabled, or when
|
|
2766
|
+
* publishing to the DLQ stream fails (due to network or NATS errors).
|
|
2767
|
+
*
|
|
2768
|
+
* Triggers the user-provided `onDeadLetter` hook for logging/alerting.
|
|
2769
|
+
* On success, terminates the message. On error, leaves it unacknowledged (nak)
|
|
2770
|
+
* so NATS can retry the delivery on the next cycle.
|
|
2771
|
+
*/
|
|
2772
|
+
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
2773
|
+
if (!this.deadLetterConfig) {
|
|
2774
|
+
msg.term("Dead letter config unavailable");
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
try {
|
|
2778
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
2779
|
+
msg.term("Dead letter processed via fallback callback");
|
|
2780
|
+
} catch (hookErr) {
|
|
2781
|
+
this.logger.error(
|
|
2782
|
+
`Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
|
|
2783
|
+
hookErr
|
|
2784
|
+
);
|
|
2785
|
+
msg.nak();
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
2790
|
+
*
|
|
2791
|
+
* Appends diagnostic metadata headers to the original message and preserves
|
|
2792
|
+
* the primary payload. If publishing succeeds, it notifies the standard
|
|
2793
|
+
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
2794
|
+
* back to the callback entirely to prevent silent data loss.
|
|
2795
|
+
*/
|
|
2796
|
+
async publishToDlq(msg, info, error) {
|
|
2797
|
+
const serviceName = this.options?.name;
|
|
2798
|
+
if (!this.connection || !serviceName) {
|
|
2799
|
+
this.logger.error(
|
|
2800
|
+
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
2801
|
+
);
|
|
2802
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
const destinationSubject = dlqStreamName(serviceName);
|
|
2806
|
+
const hdrs = natsHeaders3();
|
|
2807
|
+
if (msg.headers) {
|
|
2808
|
+
for (const [k, v] of msg.headers) {
|
|
2809
|
+
for (const val of v) {
|
|
2810
|
+
hdrs.append(k, val);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
let reason = String(error);
|
|
2815
|
+
if (error instanceof Error) {
|
|
2816
|
+
reason = error.message;
|
|
2817
|
+
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
2818
|
+
reason = String(error.message);
|
|
2819
|
+
}
|
|
2820
|
+
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
2821
|
+
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
2822
|
+
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
2823
|
+
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
2824
|
+
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
2825
|
+
try {
|
|
2826
|
+
const js = this.connection.getJetStreamClient();
|
|
2827
|
+
await js.publish(destinationSubject, msg.data, { headers: hdrs });
|
|
2828
|
+
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
2829
|
+
if (this.deadLetterConfig?.onDeadLetter) {
|
|
2830
|
+
try {
|
|
2831
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
2832
|
+
} catch (hookErr) {
|
|
2833
|
+
this.logger.warn(
|
|
2834
|
+
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
2835
|
+
hookErr
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
msg.term("Moved to DLQ stream");
|
|
2840
|
+
} catch (publishErr) {
|
|
2841
|
+
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
2842
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
2847
|
+
*
|
|
2848
|
+
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
2849
|
+
* or directly to the fallback callback based on the active module configuration.
|
|
2850
|
+
*/
|
|
2188
2851
|
async handleDeadLetter(msg, data, error) {
|
|
2189
2852
|
const info = {
|
|
2190
2853
|
subject: msg.subject,
|
|
@@ -2197,22 +2860,16 @@ var EventRouter = class {
|
|
|
2197
2860
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
2198
2861
|
};
|
|
2199
2862
|
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
2200
|
-
if (!this.
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
try {
|
|
2205
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
2206
|
-
msg.term("Dead letter processed");
|
|
2207
|
-
} catch (hookErr) {
|
|
2208
|
-
this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
|
|
2209
|
-
msg.nak();
|
|
2863
|
+
if (!this.options?.dlq) {
|
|
2864
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2865
|
+
} else {
|
|
2866
|
+
await this.publishToDlq(msg, info, error);
|
|
2210
2867
|
}
|
|
2211
2868
|
}
|
|
2212
2869
|
};
|
|
2213
2870
|
|
|
2214
2871
|
// src/server/routing/rpc.router.ts
|
|
2215
|
-
import { Logger as
|
|
2872
|
+
import { Logger as Logger12 } from "@nestjs/common";
|
|
2216
2873
|
import { headers } from "@nats-io/transport-node";
|
|
2217
2874
|
import { from as from3, mergeMap as mergeMap2 } from "rxjs";
|
|
2218
2875
|
var RpcRouter = class {
|
|
@@ -2227,7 +2884,7 @@ var RpcRouter = class {
|
|
|
2227
2884
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
2228
2885
|
this.concurrency = rpcOptions?.concurrency;
|
|
2229
2886
|
}
|
|
2230
|
-
logger = new
|
|
2887
|
+
logger = new Logger12("Jetstream:RpcRouter");
|
|
2231
2888
|
timeout;
|
|
2232
2889
|
concurrency;
|
|
2233
2890
|
resolvedAckExtensionInterval;
|
|
@@ -2330,20 +2987,27 @@ var RpcRouter = class {
|
|
|
2330
2987
|
};
|
|
2331
2988
|
|
|
2332
2989
|
// src/shutdown/shutdown.manager.ts
|
|
2333
|
-
import { Logger as
|
|
2990
|
+
import { Logger as Logger13 } from "@nestjs/common";
|
|
2334
2991
|
var ShutdownManager = class {
|
|
2335
2992
|
constructor(connection, eventBus, timeout) {
|
|
2336
2993
|
this.connection = connection;
|
|
2337
2994
|
this.eventBus = eventBus;
|
|
2338
2995
|
this.timeout = timeout;
|
|
2339
2996
|
}
|
|
2340
|
-
logger = new
|
|
2997
|
+
logger = new Logger13("Jetstream:Shutdown");
|
|
2998
|
+
shutdownPromise;
|
|
2341
2999
|
/**
|
|
2342
3000
|
* Execute the full shutdown sequence.
|
|
2343
3001
|
*
|
|
3002
|
+
* Idempotent — concurrent or repeated calls return the same promise.
|
|
3003
|
+
*
|
|
2344
3004
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
2345
3005
|
*/
|
|
2346
3006
|
async shutdown(strategy) {
|
|
3007
|
+
this.shutdownPromise ??= this.doShutdown(strategy);
|
|
3008
|
+
return this.shutdownPromise;
|
|
3009
|
+
}
|
|
3010
|
+
async doShutdown(strategy) {
|
|
2347
3011
|
this.eventBus.emit("shutdownStart" /* ShutdownStart */);
|
|
2348
3012
|
this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
|
|
2349
3013
|
strategy?.close();
|
|
@@ -2478,7 +3142,7 @@ var JetstreamModule = class {
|
|
|
2478
3142
|
provide: JETSTREAM_EVENT_BUS,
|
|
2479
3143
|
inject: [JETSTREAM_OPTIONS],
|
|
2480
3144
|
useFactory: (options) => {
|
|
2481
|
-
const logger = new
|
|
3145
|
+
const logger = new Logger14("Jetstream:Module");
|
|
2482
3146
|
return new EventBus(logger, options.hooks);
|
|
2483
3147
|
}
|
|
2484
3148
|
},
|
|
@@ -2557,8 +3221,8 @@ var JetstreamModule = class {
|
|
|
2557
3221
|
// MessageProvider — pull-based message consumption
|
|
2558
3222
|
{
|
|
2559
3223
|
provide: MessageProvider,
|
|
2560
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
|
|
2561
|
-
useFactory: (options, connection, eventBus) => {
|
|
3224
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
|
|
3225
|
+
useFactory: (options, connection, eventBus, consumerProvider) => {
|
|
2562
3226
|
if (options.consumer === false) return null;
|
|
2563
3227
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
2564
3228
|
if (options.events?.consume)
|
|
@@ -2568,7 +3232,11 @@ var JetstreamModule = class {
|
|
|
2568
3232
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
2569
3233
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
2570
3234
|
}
|
|
2571
|
-
|
|
3235
|
+
const consumerRecoveryFn = consumerProvider ? async (kind) => {
|
|
3236
|
+
const jsm = await connection.getJetStreamManager();
|
|
3237
|
+
return consumerProvider.recoverConsumer(jsm, kind);
|
|
3238
|
+
} : void 0;
|
|
3239
|
+
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
2572
3240
|
}
|
|
2573
3241
|
},
|
|
2574
3242
|
// EventRouter — routes event and broadcast messages to handlers
|
|
@@ -2580,9 +3248,10 @@ var JetstreamModule = class {
|
|
|
2580
3248
|
PatternRegistry,
|
|
2581
3249
|
JETSTREAM_CODEC,
|
|
2582
3250
|
JETSTREAM_EVENT_BUS,
|
|
2583
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3251
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3252
|
+
JETSTREAM_CONNECTION
|
|
2584
3253
|
],
|
|
2585
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
|
|
3254
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
2586
3255
|
if (options.consumer === false) return null;
|
|
2587
3256
|
const deadLetterConfig = options.onDeadLetter ? {
|
|
2588
3257
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -2605,7 +3274,9 @@ var JetstreamModule = class {
|
|
|
2605
3274
|
eventBus,
|
|
2606
3275
|
deadLetterConfig,
|
|
2607
3276
|
processingConfig,
|
|
2608
|
-
ackWaitMap
|
|
3277
|
+
ackWaitMap,
|
|
3278
|
+
connection,
|
|
3279
|
+
options
|
|
2609
3280
|
);
|
|
2610
3281
|
}
|
|
2611
3282
|
},
|
|
@@ -2654,6 +3325,15 @@ var JetstreamModule = class {
|
|
|
2654
3325
|
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
2655
3326
|
}
|
|
2656
3327
|
},
|
|
3328
|
+
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
3329
|
+
{
|
|
3330
|
+
provide: MetadataProvider,
|
|
3331
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
3332
|
+
useFactory: (options, connection) => {
|
|
3333
|
+
if (options.consumer === false) return null;
|
|
3334
|
+
return new MetadataProvider(options, connection);
|
|
3335
|
+
}
|
|
3336
|
+
},
|
|
2657
3337
|
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
2658
3338
|
{
|
|
2659
3339
|
provide: JetstreamStrategy,
|
|
@@ -2667,9 +3347,10 @@ var JetstreamModule = class {
|
|
|
2667
3347
|
EventRouter,
|
|
2668
3348
|
RpcRouter,
|
|
2669
3349
|
CoreRpcServer,
|
|
2670
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3350
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3351
|
+
MetadataProvider
|
|
2671
3352
|
],
|
|
2672
|
-
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
|
|
3353
|
+
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
|
|
2673
3354
|
if (options.consumer === false) return null;
|
|
2674
3355
|
return new JetstreamStrategy(
|
|
2675
3356
|
options,
|
|
@@ -2681,7 +3362,8 @@ var JetstreamModule = class {
|
|
|
2681
3362
|
eventRouter,
|
|
2682
3363
|
rpcRouter,
|
|
2683
3364
|
coreRpcServer,
|
|
2684
|
-
ackWaitMap
|
|
3365
|
+
ackWaitMap,
|
|
3366
|
+
metadataProvider
|
|
2685
3367
|
);
|
|
2686
3368
|
}
|
|
2687
3369
|
}
|
|
@@ -2748,12 +3430,17 @@ JetstreamModule = __decorateClass([
|
|
|
2748
3430
|
__decorateParam(1, Inject(JetstreamStrategy))
|
|
2749
3431
|
], JetstreamModule);
|
|
2750
3432
|
export {
|
|
3433
|
+
DEFAULT_METADATA_BUCKET,
|
|
3434
|
+
DEFAULT_METADATA_HISTORY,
|
|
3435
|
+
DEFAULT_METADATA_REPLICAS,
|
|
3436
|
+
DEFAULT_METADATA_TTL,
|
|
2751
3437
|
EventBus,
|
|
2752
3438
|
JETSTREAM_CODEC,
|
|
2753
3439
|
JETSTREAM_CONNECTION,
|
|
2754
3440
|
JETSTREAM_EVENT_BUS,
|
|
2755
3441
|
JETSTREAM_OPTIONS,
|
|
2756
3442
|
JetstreamClient,
|
|
3443
|
+
JetstreamDlqHeader,
|
|
2757
3444
|
JetstreamHeader,
|
|
2758
3445
|
JetstreamHealthIndicator,
|
|
2759
3446
|
JetstreamModule,
|
|
@@ -2761,17 +3448,21 @@ export {
|
|
|
2761
3448
|
JetstreamRecordBuilder,
|
|
2762
3449
|
JetstreamStrategy,
|
|
2763
3450
|
JsonCodec,
|
|
3451
|
+
MIN_METADATA_TTL,
|
|
2764
3452
|
MessageKind,
|
|
2765
3453
|
PatternPrefix,
|
|
2766
3454
|
RpcContext,
|
|
2767
3455
|
StreamKind,
|
|
2768
3456
|
TransportEvent,
|
|
3457
|
+
buildBroadcastSubject,
|
|
2769
3458
|
buildSubject,
|
|
2770
3459
|
consumerName,
|
|
3460
|
+
dlqStreamName,
|
|
2771
3461
|
getClientToken,
|
|
2772
3462
|
internalName,
|
|
2773
3463
|
isCoreRpcMode,
|
|
2774
3464
|
isJetStreamRpcMode,
|
|
3465
|
+
metadataKey,
|
|
2775
3466
|
streamName,
|
|
2776
3467
|
toNanos
|
|
2777
3468
|
};
|