@horizon-republic/nestjs-jetstream 2.11.0 → 2.12.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 +725 -168
- package/dist/index.d.cts +95 -22
- package/dist/index.d.ts +95 -22
- package/dist/index.js +720 -158
- package/package.json +16 -13
package/dist/index.cjs
CHANGED
|
@@ -64,6 +64,7 @@ __export(index_exports, {
|
|
|
64
64
|
JetstreamHeader: () => JetstreamHeader,
|
|
65
65
|
JetstreamHealthIndicator: () => JetstreamHealthIndicator,
|
|
66
66
|
JetstreamModule: () => JetstreamModule,
|
|
67
|
+
JetstreamProvisioningError: () => JetstreamProvisioningError,
|
|
67
68
|
JetstreamRecord: () => JetstreamRecord,
|
|
68
69
|
JetstreamRecordBuilder: () => JetstreamRecordBuilder,
|
|
69
70
|
JetstreamStrategy: () => JetstreamStrategy,
|
|
@@ -210,7 +211,7 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
|
210
211
|
};
|
|
211
212
|
var DEFAULT_DLQ_STREAM_CONFIG = {
|
|
212
213
|
...baseStreamConfig,
|
|
213
|
-
retention: import_jetstream.RetentionPolicy.
|
|
214
|
+
retention: import_jetstream.RetentionPolicy.Limits,
|
|
214
215
|
allow_rollup_hdrs: false,
|
|
215
216
|
max_consumers: 100,
|
|
216
217
|
max_msg_size: 10 * MB,
|
|
@@ -274,6 +275,7 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
|
274
275
|
"x-reply-to" /* ReplyTo */,
|
|
275
276
|
"x-error" /* Error */
|
|
276
277
|
]);
|
|
278
|
+
var NATS_CONTROL_HEADER_PREFIX = "nats-";
|
|
277
279
|
var internalName = (name) => `${name}__microservice`;
|
|
278
280
|
var buildSubject = (serviceName, kind, pattern) => `${internalName(serviceName)}.${kind}.${pattern}`;
|
|
279
281
|
var buildBroadcastSubject = (pattern) => `broadcast.${pattern}`;
|
|
@@ -326,6 +328,9 @@ var ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE = "jetstream.rpc.reply.error.code";
|
|
|
326
328
|
var ATTR_JETSTREAM_PROVISIONING_ENTITY = "jetstream.provisioning.entity";
|
|
327
329
|
var ATTR_JETSTREAM_PROVISIONING_ACTION = "jetstream.provisioning.action";
|
|
328
330
|
var ATTR_JETSTREAM_PROVISIONING_NAME = "jetstream.provisioning.name";
|
|
331
|
+
var ATTR_JETSTREAM_PROVISIONING_MAX_BYTES = "jetstream.provisioning.max_bytes";
|
|
332
|
+
var ATTR_JETSTREAM_PROVISIONING_NUM_REPLICAS = "jetstream.provisioning.num_replicas";
|
|
333
|
+
var ATTR_JETSTREAM_PROVISIONING_RESERVATION = "jetstream.provisioning.reservation_bytes";
|
|
329
334
|
var ATTR_JETSTREAM_SELF_HEALING_REASON = "jetstream.self_healing.reason";
|
|
330
335
|
var ATTR_JETSTREAM_MIGRATION_REASON = "jetstream.migration.reason";
|
|
331
336
|
var ATTR_JETSTREAM_DEAD_LETTER_REASON = "jetstream.dead_letter.reason";
|
|
@@ -512,6 +517,8 @@ var resolveCaptureBody = (option) => {
|
|
|
512
517
|
};
|
|
513
518
|
};
|
|
514
519
|
var resolveOtelOptions = (options = {}) => {
|
|
520
|
+
if (options === true) options = {};
|
|
521
|
+
if (options === false) options = { enabled: false };
|
|
515
522
|
return {
|
|
516
523
|
enabled: options.enabled ?? true,
|
|
517
524
|
traces: expandTracesOption(options.traces),
|
|
@@ -591,7 +598,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
|
|
|
591
598
|
|
|
592
599
|
// src/otel/tracer.ts
|
|
593
600
|
var import_api2 = require("@opentelemetry/api");
|
|
594
|
-
var PACKAGE_VERSION = true ? "2.
|
|
601
|
+
var PACKAGE_VERSION = true ? "2.12.0" : "0.0.0";
|
|
595
602
|
var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
|
|
596
603
|
|
|
597
604
|
// src/otel/carrier.ts
|
|
@@ -1180,7 +1187,10 @@ var withProvisioningSpan = (config, ctx, op) => wrapInfra(
|
|
|
1180
1187
|
{
|
|
1181
1188
|
[ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
|
|
1182
1189
|
[ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
|
|
1183
|
-
[ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name
|
|
1190
|
+
[ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name,
|
|
1191
|
+
[ATTR_JETSTREAM_PROVISIONING_MAX_BYTES]: ctx.maxBytes,
|
|
1192
|
+
[ATTR_JETSTREAM_PROVISIONING_NUM_REPLICAS]: ctx.numReplicas,
|
|
1193
|
+
[ATTR_JETSTREAM_PROVISIONING_RESERVATION]: ctx.reservation
|
|
1184
1194
|
},
|
|
1185
1195
|
op
|
|
1186
1196
|
);
|
|
@@ -1356,7 +1366,13 @@ var JetstreamRecordBuilder = class {
|
|
|
1356
1366
|
* lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
|
|
1357
1367
|
*/
|
|
1358
1368
|
validateHeaderKey(key) {
|
|
1359
|
-
|
|
1369
|
+
const normalized = key.toLowerCase();
|
|
1370
|
+
if (normalized.startsWith(NATS_CONTROL_HEADER_PREFIX)) {
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
`Header "${key}" is reserved for the NATS server and cannot be set manually. Use setMessageId() for deduplication, ttl() for per-message expiry, and scheduleAt() for delayed delivery.`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (RESERVED_HEADERS.has(normalized)) {
|
|
1360
1376
|
throw new Error(
|
|
1361
1377
|
`Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
|
|
1362
1378
|
);
|
|
@@ -1502,13 +1518,18 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1502
1518
|
async dispatchEvent(packet) {
|
|
1503
1519
|
if (!this.readyForPublish) await this.connect();
|
|
1504
1520
|
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
1521
|
+
const publishKind = detectEventKind(packet.pattern);
|
|
1522
|
+
if (schedule && publishKind === "ordered" /* Ordered */) {
|
|
1523
|
+
throw new Error(
|
|
1524
|
+
`scheduleAt() is not supported for ordered events (pattern: ${packet.pattern}). Scheduled delivery is available for workqueue events and broadcasts.`
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1505
1527
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
1506
1528
|
const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
|
|
1507
1529
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
1508
1530
|
const encoded = this.codec.encode(data);
|
|
1509
1531
|
const effectiveMsgId = messageId ?? import_nuid.nuid.next();
|
|
1510
1532
|
const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
|
|
1511
|
-
const publishKind = detectEventKind(packet.pattern);
|
|
1512
1533
|
const declaredPattern = declaredEventPattern(packet.pattern);
|
|
1513
1534
|
const streamKind = eventStreamKind(publishKind);
|
|
1514
1535
|
const startedAt = performance.now();
|
|
@@ -1540,10 +1561,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1540
1561
|
const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1541
1562
|
headers: msgHeaders,
|
|
1542
1563
|
msgID: effectiveMsgId,
|
|
1543
|
-
ttl,
|
|
1544
1564
|
schedule: {
|
|
1545
1565
|
specification: schedule.at,
|
|
1546
|
-
target: eventSubject
|
|
1566
|
+
target: eventSubject,
|
|
1567
|
+
ttl
|
|
1547
1568
|
}
|
|
1548
1569
|
});
|
|
1549
1570
|
warnIfDuplicate("scheduled", ack2);
|
|
@@ -1733,13 +1754,18 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1733
1754
|
});
|
|
1734
1755
|
return;
|
|
1735
1756
|
}
|
|
1736
|
-
await import_api8.context.with(
|
|
1757
|
+
const ack = await import_api8.context.with(
|
|
1737
1758
|
spanHandle.activeContext,
|
|
1738
1759
|
() => this.connection.getJetStreamClient().publish(subject, encoded, {
|
|
1739
1760
|
headers: hdrs,
|
|
1740
1761
|
msgID: messageId ?? import_nuid.nuid.next()
|
|
1741
1762
|
})
|
|
1742
1763
|
);
|
|
1764
|
+
if (ack.duplicate) {
|
|
1765
|
+
throw new Error(
|
|
1766
|
+
`Duplicate RPC publish for ${subject}: the messageId was already used within the stream dedup window, so the reply belongs to the original request`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1743
1769
|
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1744
1770
|
} catch (err) {
|
|
1745
1771
|
const existingTimeout = this.pendingTimeouts.get(correlationId);
|
|
@@ -1909,13 +1935,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1909
1935
|
* uses a separate `_sch` namespace that is NOT matched by any consumer filter.
|
|
1910
1936
|
* NATS holds the message and publishes it to the target subject after the delay.
|
|
1911
1937
|
*
|
|
1938
|
+
* A unique per-message suffix is appended because the server stores schedules
|
|
1939
|
+
* as rollup messages — one active schedule per subject (ADR-51). Without it,
|
|
1940
|
+
* concurrent schedules of the same pattern would silently replace each other.
|
|
1941
|
+
*
|
|
1912
1942
|
* Examples:
|
|
1913
|
-
* - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder
|
|
1914
|
-
* - `broadcast.config.updated` → `broadcast._sch.config.updated
|
|
1943
|
+
* - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
|
|
1944
|
+
* - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
|
|
1915
1945
|
*/
|
|
1916
1946
|
buildScheduleSubject(eventSubject) {
|
|
1917
1947
|
if (eventSubject.startsWith("broadcast.")) {
|
|
1918
|
-
return eventSubject.replace("broadcast.", "broadcast._sch.")
|
|
1948
|
+
return `${eventSubject.replace("broadcast.", "broadcast._sch.")}.${import_nuid.nuid.next()}`;
|
|
1919
1949
|
}
|
|
1920
1950
|
const targetPrefix = `${internalName(this.targetName)}.`;
|
|
1921
1951
|
if (!eventSubject.startsWith(targetPrefix)) {
|
|
@@ -1927,7 +1957,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1927
1957
|
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
1928
1958
|
}
|
|
1929
1959
|
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
1930
|
-
return `${targetPrefix}_sch.${pattern}`;
|
|
1960
|
+
return `${targetPrefix}_sch.${pattern}.${import_nuid.nuid.next()}`;
|
|
1931
1961
|
}
|
|
1932
1962
|
};
|
|
1933
1963
|
|
|
@@ -1939,6 +1969,7 @@ var JsonCodec = class {
|
|
|
1939
1969
|
return encoder.encode(JSON.stringify(data));
|
|
1940
1970
|
}
|
|
1941
1971
|
decode(data) {
|
|
1972
|
+
if (data.length === 0) return void 0;
|
|
1942
1973
|
return JSON.parse(decoder.decode(data));
|
|
1943
1974
|
}
|
|
1944
1975
|
};
|
|
@@ -1952,6 +1983,7 @@ var MsgpackCodec = class {
|
|
|
1952
1983
|
return this.packr.pack(data);
|
|
1953
1984
|
}
|
|
1954
1985
|
decode(data) {
|
|
1986
|
+
if (data.length === 0) return void 0;
|
|
1955
1987
|
return this.packr.unpack(data);
|
|
1956
1988
|
}
|
|
1957
1989
|
};
|
|
@@ -2751,6 +2783,7 @@ var JetstreamMetricsService = class {
|
|
|
2751
2783
|
activeServers = /* @__PURE__ */ new Set();
|
|
2752
2784
|
async onApplicationBootstrap() {
|
|
2753
2785
|
if (this.metrics !== null) return;
|
|
2786
|
+
if (!this.options.metrics || !this.config || !this.promClient) return;
|
|
2754
2787
|
if (!this.config.register) {
|
|
2755
2788
|
throw new Error(
|
|
2756
2789
|
"JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
|
|
@@ -2776,7 +2809,7 @@ var JetstreamMetricsService = class {
|
|
|
2776
2809
|
}
|
|
2777
2810
|
/** @internal Visible for tests. `0` disables polling. */
|
|
2778
2811
|
getEffectivePollInterval() {
|
|
2779
|
-
return this.config
|
|
2812
|
+
return this.config?.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
|
|
2780
2813
|
}
|
|
2781
2814
|
/**
|
|
2782
2815
|
* NATS connects during early bootstrap, before this service subscribes to
|
|
@@ -2942,28 +2975,29 @@ var normalizeMetricsConfig = (option, promClient) => {
|
|
|
2942
2975
|
};
|
|
2943
2976
|
};
|
|
2944
2977
|
var JetstreamMetricsModule = class {
|
|
2945
|
-
static forFeature(
|
|
2946
|
-
if (!metricsOption) {
|
|
2947
|
-
return { module: JetstreamMetricsModule, providers: [], exports: [] };
|
|
2948
|
-
}
|
|
2978
|
+
static forFeature() {
|
|
2949
2979
|
const promClientProvider = {
|
|
2950
2980
|
provide: JETSTREAM_METRICS_PROM_CLIENT,
|
|
2951
|
-
|
|
2981
|
+
inject: [JETSTREAM_OPTIONS],
|
|
2982
|
+
useFactory: async (opts) => {
|
|
2983
|
+
if (!opts.metrics) return null;
|
|
2952
2984
|
const mod = await resolvePromClient();
|
|
2953
2985
|
return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
|
|
2954
2986
|
}
|
|
2955
2987
|
};
|
|
2956
2988
|
const configProvider = {
|
|
2957
2989
|
provide: JETSTREAM_METRICS_CONFIG,
|
|
2958
|
-
|
|
2990
|
+
inject: [JETSTREAM_OPTIONS],
|
|
2991
|
+
useFactory: async (opts) => {
|
|
2992
|
+
if (!opts.metrics) return null;
|
|
2959
2993
|
const mod = await resolvePromClient();
|
|
2960
|
-
return normalizeMetricsConfig(
|
|
2994
|
+
return normalizeMetricsConfig(opts.metrics, mod);
|
|
2961
2995
|
}
|
|
2962
2996
|
};
|
|
2963
2997
|
const registryProvider = {
|
|
2964
2998
|
provide: JETSTREAM_METRICS_REGISTRY,
|
|
2965
2999
|
inject: [JETSTREAM_METRICS_CONFIG],
|
|
2966
|
-
useFactory: (cfg) => cfg
|
|
3000
|
+
useFactory: (cfg) => cfg?.register ?? null
|
|
2967
3001
|
};
|
|
2968
3002
|
const serviceProvider = {
|
|
2969
3003
|
provide: JetstreamMetricsService,
|
|
@@ -3020,43 +3054,11 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3020
3054
|
* Called by NestJS when `connectMicroservice()` is used, or internally by the module.
|
|
3021
3055
|
*/
|
|
3022
3056
|
async listen(callback) {
|
|
3023
|
-
|
|
3024
|
-
this.
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
this.started = true;
|
|
3028
|
-
this.patternRegistry.registerHandlers(this.getHandlers());
|
|
3029
|
-
const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
|
|
3030
|
-
if (streamKinds.length > 0) {
|
|
3031
|
-
await this.streamProvider.ensureStreams(streamKinds);
|
|
3032
|
-
if (durableKinds.length > 0) {
|
|
3033
|
-
const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
|
|
3034
|
-
this.populateAckWaitMap(consumers);
|
|
3035
|
-
this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
|
|
3036
|
-
this.messageProvider.start(consumers);
|
|
3037
|
-
}
|
|
3038
|
-
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
3039
|
-
const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
|
|
3040
|
-
await this.messageProvider.startOrdered(
|
|
3041
|
-
orderedStreamName,
|
|
3042
|
-
this.patternRegistry.getOrderedSubjects(),
|
|
3043
|
-
this.options.ordered
|
|
3044
|
-
);
|
|
3045
|
-
}
|
|
3046
|
-
if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
|
|
3047
|
-
this.eventRouter.start();
|
|
3048
|
-
}
|
|
3049
|
-
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
3050
|
-
await this.rpcRouter.start();
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
3054
|
-
await this.coreRpcServer.start();
|
|
3055
|
-
}
|
|
3056
|
-
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
3057
|
-
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
3057
|
+
try {
|
|
3058
|
+
await this.doListen(callback);
|
|
3059
|
+
} catch (err) {
|
|
3060
|
+
callback(err);
|
|
3058
3061
|
}
|
|
3059
|
-
callback();
|
|
3060
3062
|
}
|
|
3061
3063
|
/** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
|
|
3062
3064
|
close() {
|
|
@@ -3115,6 +3117,33 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3115
3117
|
getPatternRegistry() {
|
|
3116
3118
|
return this.patternRegistry;
|
|
3117
3119
|
}
|
|
3120
|
+
async doListen(callback) {
|
|
3121
|
+
if (this.started) {
|
|
3122
|
+
this.logger.warn("listen() called more than once \u2014 ignoring");
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
this.started = true;
|
|
3126
|
+
this.patternRegistry.registerHandlers(this.getHandlers());
|
|
3127
|
+
const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
|
|
3128
|
+
if (streamKinds.length > 0) {
|
|
3129
|
+
await this.streamProvider.ensureStreams(streamKinds);
|
|
3130
|
+
let consumers = null;
|
|
3131
|
+
if (durableKinds.length > 0) {
|
|
3132
|
+
consumers = await this.consumerProvider.ensureConsumers(durableKinds);
|
|
3133
|
+
this.populateAckWaitMap(consumers);
|
|
3134
|
+
this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
|
|
3135
|
+
}
|
|
3136
|
+
await this.startRouters();
|
|
3137
|
+
await this.startConsumption(consumers);
|
|
3138
|
+
}
|
|
3139
|
+
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
3140
|
+
await this.coreRpcServer.start();
|
|
3141
|
+
}
|
|
3142
|
+
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
3143
|
+
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
3144
|
+
}
|
|
3145
|
+
callback();
|
|
3146
|
+
}
|
|
3118
3147
|
/** Determine which streams and durable consumers are needed. */
|
|
3119
3148
|
resolveRequiredKinds() {
|
|
3120
3149
|
const streams = [];
|
|
@@ -3136,7 +3165,29 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3136
3165
|
}
|
|
3137
3166
|
return { streams, durableConsumers };
|
|
3138
3167
|
}
|
|
3139
|
-
/**
|
|
3168
|
+
/** Subscribe the event and RPC routers to the message subjects. */
|
|
3169
|
+
async startRouters() {
|
|
3170
|
+
if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
|
|
3171
|
+
this.eventRouter.start();
|
|
3172
|
+
}
|
|
3173
|
+
if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
3174
|
+
await this.rpcRouter.start();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
/** Begin durable and ordered consumption; routers must already be subscribed. */
|
|
3178
|
+
async startConsumption(consumers) {
|
|
3179
|
+
if (consumers !== null) {
|
|
3180
|
+
this.messageProvider.start(consumers);
|
|
3181
|
+
}
|
|
3182
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
3183
|
+
const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
|
|
3184
|
+
await this.messageProvider.startOrdered(
|
|
3185
|
+
orderedStreamName,
|
|
3186
|
+
this.patternRegistry.getOrderedSubjects(),
|
|
3187
|
+
this.options.ordered
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3140
3191
|
populateAckWaitMap(consumers) {
|
|
3141
3192
|
for (const [kind, info] of consumers) {
|
|
3142
3193
|
if (info.config.ack_wait) {
|
|
@@ -3375,6 +3426,15 @@ var serializeError = (err) => {
|
|
|
3375
3426
|
return err;
|
|
3376
3427
|
};
|
|
3377
3428
|
|
|
3429
|
+
// src/utils/settle-quietly.ts
|
|
3430
|
+
var settleQuietly = (logger5, label, action) => {
|
|
3431
|
+
try {
|
|
3432
|
+
action();
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
logger5.error(label, err);
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
|
|
3378
3438
|
// src/utils/unwrap-result.ts
|
|
3379
3439
|
var import_rxjs2 = require("rxjs");
|
|
3380
3440
|
var unwrapResult = (result) => {
|
|
@@ -3540,16 +3600,164 @@ var CoreRpcServer = class {
|
|
|
3540
3600
|
|
|
3541
3601
|
// src/server/infrastructure/stream.provider.ts
|
|
3542
3602
|
var import_common14 = require("@nestjs/common");
|
|
3543
|
-
var
|
|
3603
|
+
var import_jetstream19 = require("@nats-io/jetstream");
|
|
3544
3604
|
|
|
3545
3605
|
// src/server/infrastructure/nats-error-codes.ts
|
|
3546
3606
|
var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
3547
3607
|
NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
|
|
3548
3608
|
NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
|
|
3549
3609
|
NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
|
|
3610
|
+
NatsErrorCode2[NatsErrorCode2["StorageResourcesExceeded"] = 10047] = "StorageResourcesExceeded";
|
|
3611
|
+
NatsErrorCode2[NatsErrorCode2["NoSuitablePeers"] = 10005] = "NoSuitablePeers";
|
|
3550
3612
|
return NatsErrorCode2;
|
|
3551
3613
|
})(NatsErrorCode || {});
|
|
3552
3614
|
|
|
3615
|
+
// src/server/infrastructure/provisioning-budget.ts
|
|
3616
|
+
var import_jetstream16 = require("@nats-io/jetstream");
|
|
3617
|
+
var GIB = 1024 ** 3;
|
|
3618
|
+
var fmt = (bytes) => `${(bytes / GIB).toFixed(2)} GiB`;
|
|
3619
|
+
var resolveTierBudget = (info, replicas) => {
|
|
3620
|
+
const tier = info.tiers?.[`R${replicas}`];
|
|
3621
|
+
const limits = tier?.limits ?? info.limits;
|
|
3622
|
+
return {
|
|
3623
|
+
maxStorage: limits?.max_storage ?? 0,
|
|
3624
|
+
reserved: tier?.reserved_storage ?? info.reserved_storage ?? 0,
|
|
3625
|
+
tiered: tier !== void 0
|
|
3626
|
+
};
|
|
3627
|
+
};
|
|
3628
|
+
var groupByReplicas = (reservations) => {
|
|
3629
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3630
|
+
for (const r of reservations) {
|
|
3631
|
+
if (r.storage !== import_jetstream16.StorageType.File) continue;
|
|
3632
|
+
const prev = groups.get(r.numReplicas) ?? 0;
|
|
3633
|
+
groups.set(r.numReplicas, prev + r.maxBytes * r.numReplicas);
|
|
3634
|
+
}
|
|
3635
|
+
return groups;
|
|
3636
|
+
};
|
|
3637
|
+
var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
|
|
3638
|
+
try {
|
|
3639
|
+
const info = await jsm.getAccountInfo();
|
|
3640
|
+
const groups = groupByReplicas(reservations);
|
|
3641
|
+
let limitNotSetWarned = false;
|
|
3642
|
+
let okReserved = 0;
|
|
3643
|
+
let anyWarned = false;
|
|
3644
|
+
for (const [replicas, incremental] of groups) {
|
|
3645
|
+
const { maxStorage, reserved, tiered } = resolveTierBudget(info, replicas);
|
|
3646
|
+
const tierNote = tiered ? ` (tier R${replicas})` : "";
|
|
3647
|
+
if (maxStorage <= 0) {
|
|
3648
|
+
if (!limitNotSetWarned) {
|
|
3649
|
+
limitNotSetWarned = true;
|
|
3650
|
+
logger5.warn(
|
|
3651
|
+
`Storage preflight for "${serviceName}": account file-storage limit not set (max_storage=${maxStorage}); the server max_file_store cannot be verified from the client.`
|
|
3652
|
+
);
|
|
3653
|
+
}
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3656
|
+
const remaining = maxStorage - reserved;
|
|
3657
|
+
if (incremental > remaining) {
|
|
3658
|
+
anyWarned = true;
|
|
3659
|
+
logger5.warn(
|
|
3660
|
+
`Storage preflight for "${serviceName}"${tierNote}: needs ~${fmt(incremental)} but only ~${fmt(remaining)} remains (reserved ${fmt(reserved)} / limit ${fmt(maxStorage)}). Provisioning will likely fail with insufficient storage. Lower max_bytes/num_replicas, or raise the account/server storage limit.`
|
|
3661
|
+
);
|
|
3662
|
+
continue;
|
|
3663
|
+
}
|
|
3664
|
+
okReserved += incremental;
|
|
3665
|
+
}
|
|
3666
|
+
if (!anyWarned && !limitNotSetWarned && okReserved > 0) {
|
|
3667
|
+
logger5.log(
|
|
3668
|
+
`Storage preflight for "${serviceName}" OK: reserving ~${fmt(okReserved)} across file-backed streams within account limits.`
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
} catch (err) {
|
|
3672
|
+
logger5.debug(`Storage preflight skipped \u2014 account info unavailable: ${String(err)}`);
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
|
|
3676
|
+
// src/server/infrastructure/provisioning-error.ts
|
|
3677
|
+
var REMEDIATION = {
|
|
3678
|
+
[10047 /* StorageResourcesExceeded */]: "Aggregate stream reservation exceeds the server `max_file_store` (or account `max_storage`). Lower `max_bytes`/`num_replicas` for this service, or raise `max_file_store` on the NATS servers.",
|
|
3679
|
+
[10005 /* NoSuitablePeers */]: "Fewer healthy peers than `num_replicas`, or no peer has enough reserved storage headroom. Reduce replicas or add/repair cluster nodes."
|
|
3680
|
+
};
|
|
3681
|
+
var GENERIC_REMEDIATION = "Inspect the NATS server logs and JetStream account limits for the underlying cause.";
|
|
3682
|
+
var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error {
|
|
3683
|
+
entity;
|
|
3684
|
+
target;
|
|
3685
|
+
kind;
|
|
3686
|
+
errCode;
|
|
3687
|
+
errDescription;
|
|
3688
|
+
remediation;
|
|
3689
|
+
maxBytes;
|
|
3690
|
+
numReplicas;
|
|
3691
|
+
reservation;
|
|
3692
|
+
constructor(fields) {
|
|
3693
|
+
const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B \xD7 replicas=${fields.numReplicas}).` : "";
|
|
3694
|
+
super(
|
|
3695
|
+
`JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
|
|
3696
|
+
{ cause: fields.cause }
|
|
3697
|
+
);
|
|
3698
|
+
this.name = "JetstreamProvisioningError";
|
|
3699
|
+
this.entity = fields.entity;
|
|
3700
|
+
this.target = fields.target;
|
|
3701
|
+
this.kind = fields.kind;
|
|
3702
|
+
this.errCode = fields.errCode;
|
|
3703
|
+
this.errDescription = fields.errDescription;
|
|
3704
|
+
this.remediation = fields.remediation;
|
|
3705
|
+
this.maxBytes = fields.maxBytes;
|
|
3706
|
+
this.numReplicas = fields.numReplicas;
|
|
3707
|
+
this.reservation = fields.reservation;
|
|
3708
|
+
Object.setPrototypeOf(this, _JetstreamProvisioningError.prototype);
|
|
3709
|
+
}
|
|
3710
|
+
};
|
|
3711
|
+
var mapProvisioningError = (err, ctx) => {
|
|
3712
|
+
const api = err.apiError();
|
|
3713
|
+
const remediation = REMEDIATION[api.err_code] ?? GENERIC_REMEDIATION;
|
|
3714
|
+
const reservation = ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0;
|
|
3715
|
+
return new JetstreamProvisioningError({
|
|
3716
|
+
entity: ctx.entity,
|
|
3717
|
+
target: ctx.name,
|
|
3718
|
+
kind: ctx.kind,
|
|
3719
|
+
errCode: api.err_code,
|
|
3720
|
+
errDescription: api.description,
|
|
3721
|
+
remediation,
|
|
3722
|
+
maxBytes: ctx.maxBytes,
|
|
3723
|
+
numReplicas: ctx.numReplicas,
|
|
3724
|
+
reservation,
|
|
3725
|
+
cause: err
|
|
3726
|
+
});
|
|
3727
|
+
};
|
|
3728
|
+
|
|
3729
|
+
// src/server/infrastructure/provisioning-summary.ts
|
|
3730
|
+
var import_jetstream17 = require("@nats-io/jetstream");
|
|
3731
|
+
var GIB2 = 1024 ** 3;
|
|
3732
|
+
var NANOS_PER_SECOND = 1e9;
|
|
3733
|
+
var NANOS_PER_HOUR = 3600 * NANOS_PER_SECOND;
|
|
3734
|
+
var NANOS_PER_DAY = 86400 * NANOS_PER_SECOND;
|
|
3735
|
+
var formatBytes = (bytes) => {
|
|
3736
|
+
if (bytes <= 0) return "0 B";
|
|
3737
|
+
return `${(bytes / GIB2).toFixed(2)} GiB`;
|
|
3738
|
+
};
|
|
3739
|
+
var formatAge = (nanos) => {
|
|
3740
|
+
if (nanos <= 0) return "unlimited";
|
|
3741
|
+
if (nanos >= NANOS_PER_DAY) return `${(nanos / NANOS_PER_DAY).toFixed(1)}d`;
|
|
3742
|
+
if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
|
|
3743
|
+
return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
|
|
3744
|
+
};
|
|
3745
|
+
var formatProvisioningSummary = (serviceName, reservations) => {
|
|
3746
|
+
const lines = [`Provisioning ${reservations.length} stream(s) for "${serviceName}":`];
|
|
3747
|
+
let totalFileMaxBytes = 0;
|
|
3748
|
+
for (const r of reservations) {
|
|
3749
|
+
if (r.storage === import_jetstream17.StorageType.File) totalFileMaxBytes += r.maxBytes;
|
|
3750
|
+
const clusterReservation = r.maxBytes * r.numReplicas;
|
|
3751
|
+
lines.push(
|
|
3752
|
+
` \u2022 ${r.name} [${r.kind}] storage=${r.storage} replicas=${r.numReplicas} max_bytes=${formatBytes(r.maxBytes)} max_age=${formatAge(r.maxAge)} retention=${r.retention} \u2192 cluster reservation ${formatBytes(clusterReservation)}`
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3755
|
+
lines.push(
|
|
3756
|
+
` \u03A3 per-node file-backed footprint \u2248 ${formatBytes(totalFileMaxBytes)} (sum of max_bytes; worst case replicas = nodes). Ensure the NATS server max_file_store accommodates the sum across ALL services.`
|
|
3757
|
+
);
|
|
3758
|
+
return lines.join("\n");
|
|
3759
|
+
};
|
|
3760
|
+
|
|
3553
3761
|
// src/server/infrastructure/stream-config-diff.ts
|
|
3554
3762
|
var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
3555
3763
|
"retention"
|
|
@@ -3607,85 +3815,199 @@ var isEqual = (a, b) => {
|
|
|
3607
3815
|
|
|
3608
3816
|
// src/server/infrastructure/stream-migration.ts
|
|
3609
3817
|
var import_common13 = require("@nestjs/common");
|
|
3610
|
-
var
|
|
3818
|
+
var import_jetstream18 = require("@nats-io/jetstream");
|
|
3611
3819
|
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
3612
3820
|
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
3613
3821
|
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
3822
|
+
var DEFAULT_PEER_WAIT_MS = 6e4;
|
|
3823
|
+
var ACTIVE_MIGRATION_GRACE_MS = 9e4;
|
|
3824
|
+
var MIGRATION_STARTED_AT_KEY = "nestjs-jetstream-migration-started-at";
|
|
3614
3825
|
var StreamMigration = class {
|
|
3615
|
-
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
3826
|
+
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS, peerWaitMs = DEFAULT_PEER_WAIT_MS) {
|
|
3616
3827
|
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
3828
|
+
this.peerWaitMs = peerWaitMs;
|
|
3617
3829
|
}
|
|
3618
3830
|
logger = new import_common13.Logger("Jetstream:Stream");
|
|
3619
3831
|
async migrate(jsm, streamName2, newConfig) {
|
|
3620
3832
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
3621
3833
|
const startTime = Date.now();
|
|
3834
|
+
const peerFinished = await this.waitOutPeerMigration(jsm, backupName);
|
|
3622
3835
|
const currentInfo = await jsm.streams.info(streamName2);
|
|
3623
|
-
|
|
3624
|
-
|
|
3836
|
+
if (peerFinished && !compareStreamConfig(currentInfo.config, newConfig).hasImmutableChanges) {
|
|
3837
|
+
this.logger.log(`Stream ${streamName2}: migration completed by another instance`);
|
|
3838
|
+
await jsm.streams.update(streamName2, newConfig);
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3625
3841
|
this.logger.log(`Stream ${streamName2}: destructive migration started`);
|
|
3626
3842
|
let originalDeleted = false;
|
|
3843
|
+
let drainedCount = 0;
|
|
3627
3844
|
try {
|
|
3628
|
-
|
|
3629
|
-
|
|
3845
|
+
this.logger.log(` Phase 1/4: Quiescing ${streamName2} (publishes rejected during migration)`);
|
|
3846
|
+
await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
|
|
3847
|
+
drainedCount = (await jsm.streams.info(streamName2)).state.messages;
|
|
3848
|
+
if (drainedCount > 0) {
|
|
3849
|
+
this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages \u2192 ${backupName}`);
|
|
3630
3850
|
await jsm.streams.add({
|
|
3631
3851
|
...currentInfo.config,
|
|
3632
3852
|
name: backupName,
|
|
3633
3853
|
subjects: [],
|
|
3634
|
-
sources: [{ name: streamName2 }]
|
|
3854
|
+
sources: [{ name: streamName2 }],
|
|
3855
|
+
metadata: { [MIGRATION_STARTED_AT_KEY]: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3635
3856
|
});
|
|
3636
|
-
await this.
|
|
3857
|
+
await this.waitForSourceDrained(jsm, backupName, streamName2, drainedCount);
|
|
3637
3858
|
}
|
|
3638
|
-
this.logger.log(` Phase
|
|
3859
|
+
this.logger.log(` Phase 3/4: Recreating ${streamName2} with the new config`);
|
|
3639
3860
|
await jsm.streams.delete(streamName2);
|
|
3640
3861
|
originalDeleted = true;
|
|
3641
|
-
this.logger.log(` Phase 3/4: Creating stream with new config`);
|
|
3642
3862
|
await jsm.streams.add(newConfig);
|
|
3643
|
-
if (
|
|
3644
|
-
|
|
3645
|
-
await
|
|
3646
|
-
this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
|
|
3647
|
-
await jsm.streams.update(streamName2, {
|
|
3648
|
-
...newConfig,
|
|
3649
|
-
sources: [{ name: backupName }]
|
|
3650
|
-
});
|
|
3651
|
-
await this.waitForSourcing(jsm, streamName2, messageCount);
|
|
3652
|
-
await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
|
|
3653
|
-
await jsm.streams.delete(backupName);
|
|
3863
|
+
if (drainedCount > 0) {
|
|
3864
|
+
this.logger.log(` Phase 4/4: Restoring ${drainedCount} messages from backup`);
|
|
3865
|
+
await this.restoreFromBackup(jsm, streamName2, newConfig, backupName);
|
|
3654
3866
|
}
|
|
3655
3867
|
} catch (err) {
|
|
3656
|
-
if (originalDeleted
|
|
3868
|
+
if (originalDeleted) {
|
|
3657
3869
|
this.logger.error(
|
|
3658
|
-
`Migration failed after
|
|
3870
|
+
`Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved \u2014 restoration resumes on the next startup.`
|
|
3659
3871
|
);
|
|
3660
3872
|
} else {
|
|
3661
|
-
await this.
|
|
3873
|
+
await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
|
|
3662
3874
|
}
|
|
3663
3875
|
throw err;
|
|
3664
3876
|
}
|
|
3665
3877
|
const durationMs = Date.now() - startTime;
|
|
3666
3878
|
this.logger.log(
|
|
3667
|
-
`Stream ${streamName2}: migration complete (${
|
|
3879
|
+
`Stream ${streamName2}: migration complete (${drainedCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
|
|
3880
|
+
);
|
|
3881
|
+
}
|
|
3882
|
+
/**
|
|
3883
|
+
* Detect and finish a migration that a previous process left unfinished.
|
|
3884
|
+
* Safe against concurrent instances: a backup fresh enough to belong to a
|
|
3885
|
+
* live migration is left alone.
|
|
3886
|
+
*
|
|
3887
|
+
* @returns true when recovery work was performed.
|
|
3888
|
+
*/
|
|
3889
|
+
async recoverInterrupted(jsm, streamName2, desiredConfig) {
|
|
3890
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
3891
|
+
const backupInfo = await this.tryInfo(jsm, backupName);
|
|
3892
|
+
if (backupInfo === null) return false;
|
|
3893
|
+
if (this.isPeerMigrationActive(backupInfo)) return false;
|
|
3894
|
+
const streamInfo = await this.tryInfo(jsm, streamName2);
|
|
3895
|
+
if (streamInfo === null) {
|
|
3896
|
+
this.logger.warn(`Stream ${streamName2}: resuming interrupted migration from ${backupName}`);
|
|
3897
|
+
await jsm.streams.add(desiredConfig);
|
|
3898
|
+
if (backupInfo.state.messages > 0) {
|
|
3899
|
+
await this.restoreFromBackup(jsm, streamName2, desiredConfig, backupName);
|
|
3900
|
+
} else {
|
|
3901
|
+
await jsm.streams.delete(backupName);
|
|
3902
|
+
}
|
|
3903
|
+
return true;
|
|
3904
|
+
}
|
|
3905
|
+
const hasBackupSource = (streamInfo.config.sources ?? []).some((s) => s.name === backupName);
|
|
3906
|
+
if (hasBackupSource) {
|
|
3907
|
+
this.logger.warn(`Stream ${streamName2}: finishing interrupted restore from ${backupName}`);
|
|
3908
|
+
await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
|
|
3909
|
+
await jsm.streams.delete(backupName);
|
|
3910
|
+
await jsm.streams.update(streamName2, { ...streamInfo.config, sources: [] });
|
|
3911
|
+
return true;
|
|
3912
|
+
}
|
|
3913
|
+
if (backupInfo.state.messages === 0) {
|
|
3914
|
+
this.logger.warn(`Removing empty migration backup ${backupName}`);
|
|
3915
|
+
await jsm.streams.delete(backupName);
|
|
3916
|
+
return true;
|
|
3917
|
+
}
|
|
3918
|
+
this.logger.warn(
|
|
3919
|
+
`Stream ${streamName2}: restoring ${backupInfo.state.messages} messages from stale ${backupName}`
|
|
3920
|
+
);
|
|
3921
|
+
await this.restoreFromBackup(
|
|
3922
|
+
jsm,
|
|
3923
|
+
streamName2,
|
|
3924
|
+
{ ...streamInfo.config, name: streamName2, subjects: streamInfo.config.subjects },
|
|
3925
|
+
backupName
|
|
3668
3926
|
);
|
|
3927
|
+
return true;
|
|
3928
|
+
}
|
|
3929
|
+
/** Attach the backup as a source, drain it fully, then clean up. */
|
|
3930
|
+
async restoreFromBackup(jsm, streamName2, streamConfig, backupName) {
|
|
3931
|
+
const backupInfo = await jsm.streams.info(backupName);
|
|
3932
|
+
if ((backupInfo.config.sources ?? []).length > 0) {
|
|
3933
|
+
await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
|
|
3934
|
+
}
|
|
3935
|
+
await jsm.streams.update(streamName2, { ...streamConfig, sources: [{ name: backupName }] });
|
|
3936
|
+
await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
|
|
3937
|
+
await jsm.streams.delete(backupName);
|
|
3938
|
+
await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
|
|
3669
3939
|
}
|
|
3670
|
-
|
|
3940
|
+
/**
|
|
3941
|
+
* Wait until `sourceName` is fully drained into `streamName`. Lag-based, so
|
|
3942
|
+
* concurrent live publishes to the target cannot fake completion the way a
|
|
3943
|
+
* bare message-count comparison could. A freshly attached source reports
|
|
3944
|
+
* `lag: 0, active: -1` before its first sync — `active >= 0` filters that
|
|
3945
|
+
* false positive out (verified against NATS 2.12.6).
|
|
3946
|
+
*/
|
|
3947
|
+
async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
|
|
3671
3948
|
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
3672
3949
|
while (Date.now() < deadline) {
|
|
3673
3950
|
const info = await jsm.streams.info(streamName2);
|
|
3674
|
-
|
|
3951
|
+
const source = (info.sources ?? []).find((s) => s.name === sourceName);
|
|
3952
|
+
if (source !== void 0 && source.active >= 0 && source.lag === 0 && info.state.messages >= minimumMessages) {
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3675
3955
|
await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
|
|
3676
3956
|
}
|
|
3677
3957
|
throw new Error(
|
|
3678
|
-
`Stream sourcing timeout: ${
|
|
3958
|
+
`Stream sourcing timeout: ${sourceName} has not drained into ${streamName2} within ${this.sourcingTimeoutMs / 1e3}s. The backup is preserved; restoration resumes on the next startup.`
|
|
3679
3959
|
);
|
|
3680
3960
|
}
|
|
3681
|
-
|
|
3961
|
+
/**
|
|
3962
|
+
* A backup already present when migrate() begins belongs to another
|
|
3963
|
+
* instance migrating right now (rolling deploy) — wait for it to finish.
|
|
3964
|
+
* Stale leftovers are handled by recoverInterrupted() before migrate() runs,
|
|
3965
|
+
* so a timeout here means something is genuinely stuck.
|
|
3966
|
+
*
|
|
3967
|
+
* @returns true when a peer's backup was observed and cleared.
|
|
3968
|
+
*/
|
|
3969
|
+
async waitOutPeerMigration(jsm, backupName) {
|
|
3970
|
+
if (await this.tryInfo(jsm, backupName) === null) return false;
|
|
3971
|
+
this.logger.warn(
|
|
3972
|
+
`Migration backup ${backupName} exists \u2014 another instance appears to be migrating; waiting`
|
|
3973
|
+
);
|
|
3974
|
+
const deadline = Date.now() + this.peerWaitMs;
|
|
3975
|
+
while (Date.now() < deadline) {
|
|
3976
|
+
if (await this.tryInfo(jsm, backupName) === null) return true;
|
|
3977
|
+
await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS * 5));
|
|
3978
|
+
}
|
|
3979
|
+
throw new Error(
|
|
3980
|
+
`Migration backup ${backupName} did not clear within ${this.peerWaitMs / 1e3}s. If no other instance is migrating, recover or remove the backup manually.`
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
/** Failure before the original was deleted: undo the quiesce, drop our backup. */
|
|
3984
|
+
async rollbackBeforeDelete(jsm, streamName2, originalInfo, backupName) {
|
|
3682
3985
|
try {
|
|
3683
|
-
await jsm.streams.
|
|
3684
|
-
|
|
3685
|
-
|
|
3986
|
+
await jsm.streams.update(streamName2, { ...originalInfo.config });
|
|
3987
|
+
const backupInfo = await this.tryInfo(jsm, backupName);
|
|
3988
|
+
if (backupInfo !== null) {
|
|
3989
|
+
await jsm.streams.delete(backupName);
|
|
3990
|
+
}
|
|
3991
|
+
} catch (rollbackErr) {
|
|
3992
|
+
this.logger.error(
|
|
3993
|
+
`Rollback of ${streamName2} after a failed migration also failed \u2014 the stream may be left quiesced:`,
|
|
3994
|
+
rollbackErr
|
|
3995
|
+
);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
isPeerMigrationActive(backupInfo) {
|
|
3999
|
+
const startedAt = backupInfo.config.metadata?.[MIGRATION_STARTED_AT_KEY];
|
|
4000
|
+
if (!startedAt) return false;
|
|
4001
|
+
const startedMs = Date.parse(startedAt);
|
|
4002
|
+
if (Number.isNaN(startedMs)) return false;
|
|
4003
|
+
return Date.now() - startedMs < ACTIVE_MIGRATION_GRACE_MS;
|
|
4004
|
+
}
|
|
4005
|
+
async tryInfo(jsm, name) {
|
|
4006
|
+
try {
|
|
4007
|
+
return await jsm.streams.info(name);
|
|
3686
4008
|
} catch (err) {
|
|
3687
|
-
if (err instanceof
|
|
3688
|
-
return;
|
|
4009
|
+
if (err instanceof import_jetstream18.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4010
|
+
return null;
|
|
3689
4011
|
}
|
|
3690
4012
|
throw err;
|
|
3691
4013
|
}
|
|
@@ -3716,6 +4038,15 @@ var StreamProvider = class {
|
|
|
3716
4038
|
*/
|
|
3717
4039
|
async ensureStreams(kinds) {
|
|
3718
4040
|
const jsm = await this.connection.getJetStreamManager();
|
|
4041
|
+
const reservations = kinds.map((kind) => this.buildReservation(kind, this.buildConfig(kind)));
|
|
4042
|
+
if (this.options.dlq) {
|
|
4043
|
+
reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
|
|
4044
|
+
}
|
|
4045
|
+
this.logger.log(`
|
|
4046
|
+
${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
4047
|
+
if (this.options.provisioning?.preflightStorageCheck) {
|
|
4048
|
+
await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
|
|
4049
|
+
}
|
|
3719
4050
|
await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
|
|
3720
4051
|
if (this.options.dlq) {
|
|
3721
4052
|
await this.ensureDlqStream(jsm);
|
|
@@ -3752,6 +4083,7 @@ var StreamProvider = class {
|
|
|
3752
4083
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
3753
4084
|
async ensureStream(jsm, kind) {
|
|
3754
4085
|
const config = this.buildConfig(kind);
|
|
4086
|
+
const ctx = this.errorContext(kind, config);
|
|
3755
4087
|
return withProvisioningSpan(
|
|
3756
4088
|
this.otel,
|
|
3757
4089
|
{
|
|
@@ -3759,17 +4091,21 @@ var StreamProvider = class {
|
|
|
3759
4091
|
endpoint: this.otelEndpoint,
|
|
3760
4092
|
entity: "stream",
|
|
3761
4093
|
name: config.name,
|
|
3762
|
-
action: "ensure"
|
|
4094
|
+
action: "ensure",
|
|
4095
|
+
maxBytes: ctx.maxBytes,
|
|
4096
|
+
numReplicas: ctx.numReplicas,
|
|
4097
|
+
reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
|
|
3763
4098
|
},
|
|
3764
4099
|
async () => {
|
|
3765
4100
|
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
4101
|
+
await this.migration.recoverInterrupted(jsm, config.name, config);
|
|
3766
4102
|
try {
|
|
3767
4103
|
const currentInfo = await jsm.streams.info(config.name);
|
|
3768
|
-
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
4104
|
+
return await this.handleExistingStream(jsm, currentInfo, config, ctx);
|
|
3769
4105
|
} catch (err) {
|
|
3770
|
-
if (err instanceof
|
|
4106
|
+
if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
3771
4107
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
3772
|
-
return await jsm.streams.add(config);
|
|
4108
|
+
return await this.runStreamOp(ctx, () => jsm.streams.add(config));
|
|
3773
4109
|
}
|
|
3774
4110
|
throw err;
|
|
3775
4111
|
}
|
|
@@ -3779,6 +4115,7 @@ var StreamProvider = class {
|
|
|
3779
4115
|
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
3780
4116
|
async ensureDlqStream(jsm) {
|
|
3781
4117
|
const config = this.buildDlqConfig();
|
|
4118
|
+
const ctx = this.errorContext("dlq", config);
|
|
3782
4119
|
return withProvisioningSpan(
|
|
3783
4120
|
this.otel,
|
|
3784
4121
|
{
|
|
@@ -3786,24 +4123,30 @@ var StreamProvider = class {
|
|
|
3786
4123
|
endpoint: this.otelEndpoint,
|
|
3787
4124
|
entity: "stream",
|
|
3788
4125
|
name: config.name,
|
|
3789
|
-
action: "ensure"
|
|
4126
|
+
action: "ensure",
|
|
4127
|
+
maxBytes: ctx.maxBytes,
|
|
4128
|
+
numReplicas: ctx.numReplicas,
|
|
4129
|
+
reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
|
|
3790
4130
|
},
|
|
3791
4131
|
async () => {
|
|
3792
4132
|
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
3793
4133
|
try {
|
|
3794
4134
|
const currentInfo = await jsm.streams.info(config.name);
|
|
3795
|
-
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
4135
|
+
return await this.handleExistingStream(jsm, currentInfo, config, ctx);
|
|
3796
4136
|
} catch (err) {
|
|
3797
|
-
if (err instanceof
|
|
4137
|
+
if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
3798
4138
|
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
3799
|
-
return await jsm.streams.add(config);
|
|
4139
|
+
return await this.runStreamOp(ctx, () => jsm.streams.add(config));
|
|
3800
4140
|
}
|
|
3801
4141
|
throw err;
|
|
3802
4142
|
}
|
|
3803
4143
|
}
|
|
3804
4144
|
);
|
|
3805
4145
|
}
|
|
3806
|
-
async handleExistingStream(jsm, currentInfo, config) {
|
|
4146
|
+
async handleExistingStream(jsm, currentInfo, config, ctx) {
|
|
4147
|
+
if (this.isSharedStream(config.name)) {
|
|
4148
|
+
config.subjects = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
|
|
4149
|
+
}
|
|
3807
4150
|
const diff = compareStreamConfig(currentInfo.config, config);
|
|
3808
4151
|
if (!diff.hasChanges) {
|
|
3809
4152
|
this.logger.debug(`Stream ${config.name}: no config changes`);
|
|
@@ -3818,7 +4161,7 @@ var StreamProvider = class {
|
|
|
3818
4161
|
}
|
|
3819
4162
|
if (!diff.hasImmutableChanges) {
|
|
3820
4163
|
this.logger.debug(`Stream exists, updating: ${config.name}`);
|
|
3821
|
-
return await jsm.streams.update(config.name, config);
|
|
4164
|
+
return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, config));
|
|
3822
4165
|
}
|
|
3823
4166
|
if (!this.options.allowDestructiveMigration) {
|
|
3824
4167
|
this.logger.warn(
|
|
@@ -3826,10 +4169,15 @@ var StreamProvider = class {
|
|
|
3826
4169
|
);
|
|
3827
4170
|
if (diff.hasMutableChanges) {
|
|
3828
4171
|
const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
|
|
3829
|
-
return await jsm.streams.update(config.name, mutableConfig);
|
|
4172
|
+
return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, mutableConfig));
|
|
3830
4173
|
}
|
|
3831
4174
|
return currentInfo;
|
|
3832
4175
|
}
|
|
4176
|
+
if (this.isSharedStream(config.name)) {
|
|
4177
|
+
throw new Error(
|
|
4178
|
+
`Stream ${config.name} is shared across services and cannot be destructively migrated: recreating it would delete every other service's durable broadcast consumers and replay retained history to them. Coordinate a manual migration instead.`
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
3833
4181
|
await withMigrationSpan(
|
|
3834
4182
|
this.otel,
|
|
3835
4183
|
{
|
|
@@ -3868,11 +4216,47 @@ var StreamProvider = class {
|
|
|
3868
4216
|
}
|
|
3869
4217
|
}
|
|
3870
4218
|
}
|
|
4219
|
+
buildReservation(kind, config) {
|
|
4220
|
+
const mb = config.max_bytes;
|
|
4221
|
+
return {
|
|
4222
|
+
kind,
|
|
4223
|
+
name: config.name,
|
|
4224
|
+
storage: config.storage ?? import_jetstream19.StorageType.File,
|
|
4225
|
+
numReplicas: config.num_replicas ?? 1,
|
|
4226
|
+
maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
|
|
4227
|
+
// NATS uses -1 for unlimited
|
|
4228
|
+
maxAge: config.max_age ?? 0,
|
|
4229
|
+
retention: config.retention ?? import_jetstream19.RetentionPolicy.Limits
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
errorContext(kind, config) {
|
|
4233
|
+
return {
|
|
4234
|
+
entity: "stream",
|
|
4235
|
+
name: config.name,
|
|
4236
|
+
kind,
|
|
4237
|
+
maxBytes: config.max_bytes,
|
|
4238
|
+
numReplicas: config.num_replicas ?? 1
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
async runStreamOp(ctx, op) {
|
|
4242
|
+
try {
|
|
4243
|
+
return await op();
|
|
4244
|
+
} catch (err) {
|
|
4245
|
+
if (err instanceof import_jetstream19.JetStreamApiError) {
|
|
4246
|
+
throw mapProvisioningError(err, ctx);
|
|
4247
|
+
}
|
|
4248
|
+
throw err;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
/** The broadcast stream is global — every service in the cluster shares it. */
|
|
4252
|
+
isSharedStream(name) {
|
|
4253
|
+
return name === this.getStreamName("broadcast" /* Broadcast */);
|
|
4254
|
+
}
|
|
3871
4255
|
/** Build the full stream config by merging defaults with user overrides. */
|
|
3872
4256
|
buildConfig(kind) {
|
|
3873
4257
|
const name = this.getStreamName(kind);
|
|
3874
4258
|
const subjects = this.getSubjects(kind);
|
|
3875
|
-
const description = `JetStream ${kind} stream for ${this.options.name}`;
|
|
4259
|
+
const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
|
|
3876
4260
|
const defaults = this.getDefaults(kind);
|
|
3877
4261
|
const overrides = this.getOverrides(kind);
|
|
3878
4262
|
return {
|
|
@@ -3958,7 +4342,7 @@ var StreamProvider = class {
|
|
|
3958
4342
|
|
|
3959
4343
|
// src/server/infrastructure/consumer.provider.ts
|
|
3960
4344
|
var import_common15 = require("@nestjs/common");
|
|
3961
|
-
var
|
|
4345
|
+
var import_jetstream21 = require("@nats-io/jetstream");
|
|
3962
4346
|
var ConsumerProvider = class {
|
|
3963
4347
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
3964
4348
|
this.options = options;
|
|
@@ -4014,15 +4398,16 @@ var ConsumerProvider = class {
|
|
|
4014
4398
|
},
|
|
4015
4399
|
async () => {
|
|
4016
4400
|
this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
|
|
4401
|
+
const ctx = { entity: "consumer", name, kind };
|
|
4017
4402
|
try {
|
|
4018
4403
|
await jsm.consumers.info(stream, name);
|
|
4019
4404
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
4020
|
-
return await jsm.consumers.update(stream, name, config);
|
|
4405
|
+
return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
|
|
4021
4406
|
} catch (err) {
|
|
4022
|
-
if (!(err instanceof
|
|
4407
|
+
if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4023
4408
|
throw err;
|
|
4024
4409
|
}
|
|
4025
|
-
return await this.createConsumer(jsm, stream, name, config);
|
|
4410
|
+
return await this.createConsumer(jsm, stream, name, kind, config);
|
|
4026
4411
|
}
|
|
4027
4412
|
}
|
|
4028
4413
|
);
|
|
@@ -4059,10 +4444,10 @@ var ConsumerProvider = class {
|
|
|
4059
4444
|
try {
|
|
4060
4445
|
return await jsm.consumers.info(stream, name);
|
|
4061
4446
|
} catch (err) {
|
|
4062
|
-
if (!(err instanceof
|
|
4447
|
+
if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4063
4448
|
throw err;
|
|
4064
4449
|
}
|
|
4065
|
-
return await this.createConsumer(jsm, stream, name, config);
|
|
4450
|
+
return await this.createConsumer(jsm, stream, name, kind, config);
|
|
4066
4451
|
}
|
|
4067
4452
|
}
|
|
4068
4453
|
);
|
|
@@ -4080,7 +4465,7 @@ var ConsumerProvider = class {
|
|
|
4080
4465
|
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
4081
4466
|
);
|
|
4082
4467
|
} catch (err) {
|
|
4083
|
-
if (err instanceof
|
|
4468
|
+
if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4084
4469
|
return;
|
|
4085
4470
|
}
|
|
4086
4471
|
throw err;
|
|
@@ -4089,18 +4474,32 @@ var ConsumerProvider = class {
|
|
|
4089
4474
|
/**
|
|
4090
4475
|
* Create a consumer, handling the race where another pod creates it first.
|
|
4091
4476
|
*/
|
|
4092
|
-
async createConsumer(jsm, stream, name, config) {
|
|
4477
|
+
async createConsumer(jsm, stream, name, kind, config) {
|
|
4093
4478
|
this.logger.log(`Creating consumer: ${name}`);
|
|
4479
|
+
const ctx = { entity: "consumer", name, kind };
|
|
4094
4480
|
try {
|
|
4095
4481
|
return await jsm.consumers.add(stream, config);
|
|
4096
4482
|
} catch (addErr) {
|
|
4097
|
-
if (addErr instanceof
|
|
4483
|
+
if (addErr instanceof import_jetstream21.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
4098
4484
|
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
4099
4485
|
return await jsm.consumers.info(stream, name);
|
|
4100
4486
|
}
|
|
4487
|
+
if (addErr instanceof import_jetstream21.JetStreamApiError) {
|
|
4488
|
+
throw mapProvisioningError(addErr, ctx);
|
|
4489
|
+
}
|
|
4101
4490
|
throw addErr;
|
|
4102
4491
|
}
|
|
4103
4492
|
}
|
|
4493
|
+
async runConsumerOp(ctx, op) {
|
|
4494
|
+
try {
|
|
4495
|
+
return await op();
|
|
4496
|
+
} catch (err) {
|
|
4497
|
+
if (err instanceof import_jetstream21.JetStreamApiError) {
|
|
4498
|
+
throw mapProvisioningError(err, ctx);
|
|
4499
|
+
}
|
|
4500
|
+
throw err;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4104
4503
|
/** Build consumer config by merging defaults with user overrides. */
|
|
4105
4504
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
4106
4505
|
buildConfig(kind) {
|
|
@@ -4182,7 +4581,7 @@ var ConsumerProvider = class {
|
|
|
4182
4581
|
|
|
4183
4582
|
// src/server/infrastructure/message.provider.ts
|
|
4184
4583
|
var import_common16 = require("@nestjs/common");
|
|
4185
|
-
var
|
|
4584
|
+
var import_jetstream23 = require("@nats-io/jetstream");
|
|
4186
4585
|
var import_rxjs3 = require("rxjs");
|
|
4187
4586
|
var MessageProvider = class {
|
|
4188
4587
|
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
@@ -4243,7 +4642,7 @@ var MessageProvider = class {
|
|
|
4243
4642
|
*/
|
|
4244
4643
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
4245
4644
|
const consumerOpts = { filter_subjects: filterSubjects };
|
|
4246
|
-
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !==
|
|
4645
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream23.DeliverPolicy.All) {
|
|
4247
4646
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
4248
4647
|
}
|
|
4249
4648
|
if (orderedConfig?.optStartSeq !== void 0) {
|
|
@@ -4552,6 +4951,7 @@ var MetadataProvider = class {
|
|
|
4552
4951
|
// src/server/routing/event.router.ts
|
|
4553
4952
|
var import_common18 = require("@nestjs/common");
|
|
4554
4953
|
var import_transport_node4 = require("@nats-io/transport-node");
|
|
4954
|
+
var DLQ_PUBLISH_ATTEMPTS = 3;
|
|
4555
4955
|
var eventConsumeKindFor = (kind) => {
|
|
4556
4956
|
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
4557
4957
|
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
@@ -4638,33 +5038,80 @@ var EventRouter = class {
|
|
|
4638
5038
|
return msg.info.deliveryCount >= maxDeliver;
|
|
4639
5039
|
};
|
|
4640
5040
|
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
4641
|
-
const settleSuccess = (msg, ctx) => {
|
|
4642
|
-
if (ctx.shouldTerminate)
|
|
4643
|
-
|
|
4644
|
-
|
|
5041
|
+
const settleSuccess = (msg, ctx, data) => {
|
|
5042
|
+
if (ctx.shouldTerminate) {
|
|
5043
|
+
settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
|
|
5044
|
+
msg.term(ctx.terminateReason);
|
|
5045
|
+
});
|
|
5046
|
+
return void 0;
|
|
5047
|
+
}
|
|
5048
|
+
if (ctx.shouldRetry) {
|
|
5049
|
+
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
5050
|
+
return handleDeadLetter(
|
|
5051
|
+
msg,
|
|
5052
|
+
data,
|
|
5053
|
+
new Error("Retry requested on the final delivery attempt")
|
|
5054
|
+
);
|
|
5055
|
+
}
|
|
5056
|
+
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5057
|
+
msg.nak(ctx.retryDelay);
|
|
5058
|
+
});
|
|
5059
|
+
return void 0;
|
|
5060
|
+
}
|
|
5061
|
+
settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
|
|
5062
|
+
msg.ack();
|
|
5063
|
+
});
|
|
5064
|
+
return void 0;
|
|
4645
5065
|
};
|
|
4646
5066
|
const settleFailure = async (msg, data, err) => {
|
|
4647
5067
|
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
4648
5068
|
await handleDeadLetter(msg, data, err);
|
|
4649
5069
|
return;
|
|
4650
5070
|
}
|
|
4651
|
-
msg.
|
|
5071
|
+
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5072
|
+
msg.nak();
|
|
5073
|
+
});
|
|
5074
|
+
};
|
|
5075
|
+
const captureUnroutable = (capture, msg, err) => {
|
|
5076
|
+
let data;
|
|
5077
|
+
try {
|
|
5078
|
+
data = codec.decode(msg.data);
|
|
5079
|
+
} catch {
|
|
5080
|
+
data = void 0;
|
|
5081
|
+
}
|
|
5082
|
+
return capture(msg, data, err).catch((captureErr) => {
|
|
5083
|
+
logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
|
|
5084
|
+
});
|
|
4652
5085
|
};
|
|
4653
5086
|
const resolveEvent = (msg) => {
|
|
4654
5087
|
const subject = msg.subject;
|
|
4655
5088
|
try {
|
|
4656
5089
|
const handler = patternRegistry.getHandler(subject);
|
|
4657
5090
|
if (!handler) {
|
|
4658
|
-
msg.term(`No handler for event: ${subject}`);
|
|
4659
5091
|
logger5.error(`No handler for subject: ${subject}`);
|
|
5092
|
+
if (handleDeadLetter !== null) {
|
|
5093
|
+
return captureUnroutable(
|
|
5094
|
+
handleDeadLetter,
|
|
5095
|
+
msg,
|
|
5096
|
+
new Error(`No handler for event: ${subject}`)
|
|
5097
|
+
);
|
|
5098
|
+
}
|
|
5099
|
+
msg.term(`No handler for event: ${subject}`);
|
|
4660
5100
|
return null;
|
|
4661
5101
|
}
|
|
4662
5102
|
let data;
|
|
4663
5103
|
try {
|
|
4664
5104
|
data = codec.decode(msg.data);
|
|
4665
5105
|
} catch (err) {
|
|
4666
|
-
msg.term("Decode error");
|
|
4667
5106
|
logger5.error(`Decode error for ${subject}:`, err);
|
|
5107
|
+
if (handleDeadLetter !== null) {
|
|
5108
|
+
return captureUnroutable(
|
|
5109
|
+
handleDeadLetter,
|
|
5110
|
+
msg,
|
|
5111
|
+
new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
msg.term("Decode error");
|
|
4668
5115
|
return null;
|
|
4669
5116
|
}
|
|
4670
5117
|
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
@@ -4687,6 +5134,7 @@ var EventRouter = class {
|
|
|
4687
5134
|
const handleSafe = (msg) => {
|
|
4688
5135
|
const resolved = resolveEvent(msg);
|
|
4689
5136
|
if (resolved === null) return void 0;
|
|
5137
|
+
if (isPromiseLike2(resolved)) return resolved;
|
|
4690
5138
|
const { handler, data } = resolved;
|
|
4691
5139
|
const ctx = new RpcContext([msg]);
|
|
4692
5140
|
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
@@ -4719,16 +5167,24 @@ var EventRouter = class {
|
|
|
4719
5167
|
});
|
|
4720
5168
|
}
|
|
4721
5169
|
if (!isPromiseLike2(pending)) {
|
|
4722
|
-
settleSuccess(msg, ctx);
|
|
5170
|
+
const settled = settleSuccess(msg, ctx, data);
|
|
4723
5171
|
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
4724
|
-
if (
|
|
4725
|
-
|
|
5172
|
+
if (settled === void 0) {
|
|
5173
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5174
|
+
return void 0;
|
|
5175
|
+
}
|
|
5176
|
+
return settled.finally(() => {
|
|
5177
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5178
|
+
});
|
|
4726
5179
|
}
|
|
4727
5180
|
return pending.then(
|
|
4728
|
-
() => {
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
5181
|
+
async () => {
|
|
5182
|
+
try {
|
|
5183
|
+
await settleSuccess(msg, ctx, data);
|
|
5184
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5185
|
+
} finally {
|
|
5186
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5187
|
+
}
|
|
4732
5188
|
},
|
|
4733
5189
|
async (err) => {
|
|
4734
5190
|
eventBus.emit(
|
|
@@ -4822,14 +5278,28 @@ var EventRouter = class {
|
|
|
4822
5278
|
active--;
|
|
4823
5279
|
drainBacklog();
|
|
4824
5280
|
};
|
|
5281
|
+
const routeSafely = (msg) => {
|
|
5282
|
+
try {
|
|
5283
|
+
return route(msg);
|
|
5284
|
+
} catch (err) {
|
|
5285
|
+
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5286
|
+
return void 0;
|
|
5287
|
+
}
|
|
5288
|
+
};
|
|
5289
|
+
const trackAsync = (result, msg) => {
|
|
5290
|
+
void result.catch((err) => {
|
|
5291
|
+
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5292
|
+
}).finally(onAsyncDone);
|
|
5293
|
+
};
|
|
4825
5294
|
const drainBacklog = () => {
|
|
4826
5295
|
while (active < maxActive) {
|
|
4827
5296
|
const next = backlog.shift();
|
|
4828
5297
|
if (next === void 0) return;
|
|
5298
|
+
next.stopAckExtension?.();
|
|
4829
5299
|
active++;
|
|
4830
|
-
const result =
|
|
5300
|
+
const result = routeSafely(next.msg);
|
|
4831
5301
|
if (result !== void 0) {
|
|
4832
|
-
|
|
5302
|
+
trackAsync(result, next.msg);
|
|
4833
5303
|
} else {
|
|
4834
5304
|
active--;
|
|
4835
5305
|
}
|
|
@@ -4839,7 +5309,10 @@ var EventRouter = class {
|
|
|
4839
5309
|
const subscription = stream$.subscribe({
|
|
4840
5310
|
next: (msg) => {
|
|
4841
5311
|
if (active >= maxActive) {
|
|
4842
|
-
backlog.push(
|
|
5312
|
+
backlog.push({
|
|
5313
|
+
msg,
|
|
5314
|
+
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5315
|
+
});
|
|
4843
5316
|
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
4844
5317
|
backlogWarned = true;
|
|
4845
5318
|
logger5.warn(
|
|
@@ -4849,9 +5322,9 @@ var EventRouter = class {
|
|
|
4849
5322
|
return;
|
|
4850
5323
|
}
|
|
4851
5324
|
active++;
|
|
4852
|
-
const result =
|
|
5325
|
+
const result = routeSafely(msg);
|
|
4853
5326
|
if (result !== void 0) {
|
|
4854
|
-
|
|
5327
|
+
trackAsync(result, msg);
|
|
4855
5328
|
} else {
|
|
4856
5329
|
active--;
|
|
4857
5330
|
if (backlog.length > 0) drainBacklog();
|
|
@@ -4861,6 +5334,12 @@ var EventRouter = class {
|
|
|
4861
5334
|
logger5.error(`Stream error in ${kind} router`, err);
|
|
4862
5335
|
}
|
|
4863
5336
|
});
|
|
5337
|
+
subscription.add(() => {
|
|
5338
|
+
for (const queued of backlog) {
|
|
5339
|
+
queued.stopAckExtension?.();
|
|
5340
|
+
}
|
|
5341
|
+
backlog.length = 0;
|
|
5342
|
+
});
|
|
4864
5343
|
this.subscriptions.push(subscription);
|
|
4865
5344
|
}
|
|
4866
5345
|
getConcurrency(kind) {
|
|
@@ -4875,25 +5354,78 @@ var EventRouter = class {
|
|
|
4875
5354
|
}
|
|
4876
5355
|
/**
|
|
4877
5356
|
* Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
|
|
4878
|
-
* success
|
|
4879
|
-
*
|
|
4880
|
-
*
|
|
5357
|
+
* success. On failure the message is nak'd to release it, but the server
|
|
5358
|
+
* never redelivers past `max_deliver` — it stays in the stream for manual
|
|
5359
|
+
* recovery. Used when the DLQ stream isn't configured, or when publishing
|
|
5360
|
+
* to it failed and we still have to surface the message somewhere.
|
|
4881
5361
|
*/
|
|
4882
5362
|
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
4883
|
-
|
|
4884
|
-
|
|
5363
|
+
const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
|
|
5364
|
+
if (!onDeadLetter) {
|
|
5365
|
+
this.logger.error(
|
|
5366
|
+
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
|
|
5367
|
+
);
|
|
5368
|
+
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5369
|
+
msg.nak();
|
|
5370
|
+
});
|
|
4885
5371
|
return;
|
|
4886
5372
|
}
|
|
4887
5373
|
try {
|
|
4888
|
-
await
|
|
4889
|
-
|
|
5374
|
+
await onDeadLetter(info);
|
|
5375
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5376
|
+
msg.term("Dead letter processed via fallback callback");
|
|
5377
|
+
});
|
|
4890
5378
|
} catch (hookErr) {
|
|
4891
5379
|
this.logger.error(
|
|
4892
|
-
`Fallback onDeadLetter callback failed for ${msg.subject}
|
|
5380
|
+
`Fallback onDeadLetter callback failed for ${msg.subject} \u2014 the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
|
|
4893
5381
|
hookErr
|
|
4894
5382
|
);
|
|
4895
|
-
msg.
|
|
5383
|
+
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5384
|
+
msg.nak();
|
|
5385
|
+
});
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
/**
|
|
5389
|
+
* Copy the original message headers for the DLQ republish, dropping NATS
|
|
5390
|
+
* server control headers: a copied Nats-TTL expires the DLQ entry (or gets
|
|
5391
|
+
* the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
|
|
5392
|
+
* Nats-Msg-Id collides with the DLQ dedup window.
|
|
5393
|
+
*/
|
|
5394
|
+
buildDlqHeaders(msg) {
|
|
5395
|
+
const hdrs = (0, import_transport_node4.headers)();
|
|
5396
|
+
if (!msg.headers) return hdrs;
|
|
5397
|
+
for (const [k, v] of msg.headers) {
|
|
5398
|
+
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5399
|
+
for (const val of v) {
|
|
5400
|
+
hdrs.append(k, val);
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
return hdrs;
|
|
5404
|
+
}
|
|
5405
|
+
/**
|
|
5406
|
+
* Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
|
|
5407
|
+
*
|
|
5408
|
+
* Past `max_deliver` the server never redelivers, so an in-process retry is
|
|
5409
|
+
* the only second chance a dead letter gets. There is no artificial delay
|
|
5410
|
+
* between attempts: when the broker is unreachable each publish already
|
|
5411
|
+
* spends its own request timeout, which spaces the attempts naturally.
|
|
5412
|
+
*/
|
|
5413
|
+
async publishToDlqWithRetry(connection, subject, data, headers2) {
|
|
5414
|
+
let lastErr;
|
|
5415
|
+
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5416
|
+
try {
|
|
5417
|
+
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5418
|
+
return;
|
|
5419
|
+
} catch (err) {
|
|
5420
|
+
lastErr = err;
|
|
5421
|
+
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5422
|
+
this.logger.warn(
|
|
5423
|
+
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5424
|
+
);
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
4896
5427
|
}
|
|
5428
|
+
throw lastErr;
|
|
4897
5429
|
}
|
|
4898
5430
|
/**
|
|
4899
5431
|
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
@@ -4913,14 +5445,7 @@ var EventRouter = class {
|
|
|
4913
5445
|
return;
|
|
4914
5446
|
}
|
|
4915
5447
|
const destinationSubject = dlqStreamName(serviceName);
|
|
4916
|
-
const hdrs =
|
|
4917
|
-
if (msg.headers) {
|
|
4918
|
-
for (const [k, v] of msg.headers) {
|
|
4919
|
-
for (const val of v) {
|
|
4920
|
-
hdrs.append(k, val);
|
|
4921
|
-
}
|
|
4922
|
-
}
|
|
4923
|
-
}
|
|
5448
|
+
const hdrs = this.buildDlqHeaders(msg);
|
|
4924
5449
|
let reason = String(error);
|
|
4925
5450
|
if (error instanceof Error) {
|
|
4926
5451
|
reason = error.message;
|
|
@@ -4933,8 +5458,7 @@ var EventRouter = class {
|
|
|
4933
5458
|
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
4934
5459
|
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
4935
5460
|
try {
|
|
4936
|
-
|
|
4937
|
-
await js.publish(destinationSubject, msg.data, { headers: hdrs });
|
|
5461
|
+
await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
4938
5462
|
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
4939
5463
|
if (this.deadLetterConfig?.onDeadLetter) {
|
|
4940
5464
|
try {
|
|
@@ -4946,7 +5470,9 @@ var EventRouter = class {
|
|
|
4946
5470
|
);
|
|
4947
5471
|
}
|
|
4948
5472
|
}
|
|
4949
|
-
|
|
5473
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5474
|
+
msg.term("Moved to DLQ stream");
|
|
5475
|
+
});
|
|
4950
5476
|
} catch (publishErr) {
|
|
4951
5477
|
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
4952
5478
|
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
@@ -5140,7 +5666,9 @@ var RpcRouter = class {
|
|
|
5140
5666
|
`rpc-handler:${subject}`
|
|
5141
5667
|
);
|
|
5142
5668
|
publishErrorReply(replyTo, correlationId, subject, err);
|
|
5143
|
-
|
|
5669
|
+
settleQuietly(logger5, `Failed to term ${subject}:`, () => {
|
|
5670
|
+
msg.term(`Handler error: ${subject}`);
|
|
5671
|
+
});
|
|
5144
5672
|
};
|
|
5145
5673
|
const abortController = new AbortController();
|
|
5146
5674
|
let pending;
|
|
@@ -5168,7 +5696,9 @@ var RpcRouter = class {
|
|
|
5168
5696
|
}
|
|
5169
5697
|
if (!isPromiseLike2(pending)) {
|
|
5170
5698
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5171
|
-
|
|
5699
|
+
settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
|
|
5700
|
+
msg.ack();
|
|
5701
|
+
});
|
|
5172
5702
|
publishReply(replyTo, correlationId, pending);
|
|
5173
5703
|
reportHandlerCompleted(msg, startedAt, "success");
|
|
5174
5704
|
return void 0;
|
|
@@ -5180,7 +5710,9 @@ var RpcRouter = class {
|
|
|
5180
5710
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5181
5711
|
abortController.abort();
|
|
5182
5712
|
emitRpcTimeout(subject, correlationId);
|
|
5183
|
-
|
|
5713
|
+
settleQuietly(logger5, `Failed to term ${subject}:`, () => {
|
|
5714
|
+
msg.term("Handler timeout");
|
|
5715
|
+
});
|
|
5184
5716
|
reportHandlerCompleted(msg, startedAt, "terminated");
|
|
5185
5717
|
}, timeout);
|
|
5186
5718
|
return pending.then(
|
|
@@ -5189,7 +5721,9 @@ var RpcRouter = class {
|
|
|
5189
5721
|
settled = true;
|
|
5190
5722
|
clearTimeout(timeoutId);
|
|
5191
5723
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5192
|
-
|
|
5724
|
+
settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
|
|
5725
|
+
msg.ack();
|
|
5726
|
+
});
|
|
5193
5727
|
publishReply(replyTo, correlationId, result);
|
|
5194
5728
|
reportHandlerCompleted(msg, startedAt, "success");
|
|
5195
5729
|
},
|
|
@@ -5211,14 +5745,28 @@ var RpcRouter = class {
|
|
|
5211
5745
|
active--;
|
|
5212
5746
|
drainBacklog();
|
|
5213
5747
|
};
|
|
5748
|
+
const routeSafely = (msg) => {
|
|
5749
|
+
try {
|
|
5750
|
+
return handleSafe(msg);
|
|
5751
|
+
} catch (err) {
|
|
5752
|
+
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5753
|
+
return void 0;
|
|
5754
|
+
}
|
|
5755
|
+
};
|
|
5756
|
+
const trackAsync = (result, msg) => {
|
|
5757
|
+
void result.catch((err) => {
|
|
5758
|
+
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5759
|
+
}).finally(onAsyncDone);
|
|
5760
|
+
};
|
|
5214
5761
|
const drainBacklog = () => {
|
|
5215
5762
|
while (active < maxActive) {
|
|
5216
5763
|
const next = backlog.shift();
|
|
5217
5764
|
if (next === void 0) return;
|
|
5765
|
+
next.stopAckExtension?.();
|
|
5218
5766
|
active++;
|
|
5219
|
-
const result =
|
|
5767
|
+
const result = routeSafely(next.msg);
|
|
5220
5768
|
if (result !== void 0) {
|
|
5221
|
-
|
|
5769
|
+
trackAsync(result, next.msg);
|
|
5222
5770
|
} else {
|
|
5223
5771
|
active--;
|
|
5224
5772
|
}
|
|
@@ -5228,7 +5776,10 @@ var RpcRouter = class {
|
|
|
5228
5776
|
this.subscription = this.messageProvider.commands$.subscribe({
|
|
5229
5777
|
next: (msg) => {
|
|
5230
5778
|
if (active >= maxActive) {
|
|
5231
|
-
backlog.push(
|
|
5779
|
+
backlog.push({
|
|
5780
|
+
msg,
|
|
5781
|
+
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5782
|
+
});
|
|
5232
5783
|
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5233
5784
|
backlogWarned = true;
|
|
5234
5785
|
logger5.warn(
|
|
@@ -5238,9 +5789,9 @@ var RpcRouter = class {
|
|
|
5238
5789
|
return;
|
|
5239
5790
|
}
|
|
5240
5791
|
active++;
|
|
5241
|
-
const result =
|
|
5792
|
+
const result = routeSafely(msg);
|
|
5242
5793
|
if (result !== void 0) {
|
|
5243
|
-
|
|
5794
|
+
trackAsync(result, msg);
|
|
5244
5795
|
} else {
|
|
5245
5796
|
active--;
|
|
5246
5797
|
if (backlog.length > 0) drainBacklog();
|
|
@@ -5250,6 +5801,12 @@ var RpcRouter = class {
|
|
|
5250
5801
|
logger5.error("Stream error in RPC router", err);
|
|
5251
5802
|
}
|
|
5252
5803
|
});
|
|
5804
|
+
this.subscription.add(() => {
|
|
5805
|
+
for (const queued of backlog) {
|
|
5806
|
+
queued.stopAckExtension?.();
|
|
5807
|
+
}
|
|
5808
|
+
backlog.length = 0;
|
|
5809
|
+
});
|
|
5253
5810
|
}
|
|
5254
5811
|
/** Stop routing and unsubscribe. */
|
|
5255
5812
|
destroy() {
|
|
@@ -5320,7 +5877,7 @@ var JetstreamModule = class {
|
|
|
5320
5877
|
return {
|
|
5321
5878
|
module: JetstreamModule,
|
|
5322
5879
|
global: true,
|
|
5323
|
-
imports:
|
|
5880
|
+
imports: [JetstreamMetricsModule.forFeature()],
|
|
5324
5881
|
providers,
|
|
5325
5882
|
exports: [
|
|
5326
5883
|
JETSTREAM_CONNECTION,
|
|
@@ -5346,11 +5903,10 @@ var JetstreamModule = class {
|
|
|
5346
5903
|
static forRootAsync(asyncOptions) {
|
|
5347
5904
|
const asyncProviders = this.createAsyncOptionsProvider(asyncOptions);
|
|
5348
5905
|
const coreProviders = this.createCoreDependentProviders();
|
|
5349
|
-
const metricsImports = asyncOptions.metrics ? [JetstreamMetricsModule.forFeature(asyncOptions.metrics)] : [];
|
|
5350
5906
|
return {
|
|
5351
5907
|
module: JetstreamModule,
|
|
5352
5908
|
global: true,
|
|
5353
|
-
imports: [...asyncOptions.imports ?? [],
|
|
5909
|
+
imports: [...asyncOptions.imports ?? [], JetstreamMetricsModule.forFeature()],
|
|
5354
5910
|
providers: [...asyncProviders, ...coreProviders],
|
|
5355
5911
|
exports: [
|
|
5356
5912
|
JETSTREAM_CONNECTION,
|
|
@@ -5533,7 +6089,7 @@ var JetstreamModule = class {
|
|
|
5533
6089
|
],
|
|
5534
6090
|
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
5535
6091
|
if (options.consumer === false) return null;
|
|
5536
|
-
const deadLetterConfig = options.onDeadLetter ? {
|
|
6092
|
+
const deadLetterConfig = options.onDeadLetter || options.dlq ? {
|
|
5537
6093
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
5538
6094
|
onDeadLetter: options.onDeadLetter
|
|
5539
6095
|
} : void 0;
|
|
@@ -5734,6 +6290,7 @@ JetstreamModule = __decorateClass([
|
|
|
5734
6290
|
JetstreamHeader,
|
|
5735
6291
|
JetstreamHealthIndicator,
|
|
5736
6292
|
JetstreamModule,
|
|
6293
|
+
JetstreamProvisioningError,
|
|
5737
6294
|
JetstreamRecord,
|
|
5738
6295
|
JetstreamRecordBuilder,
|
|
5739
6296
|
JetstreamStrategy,
|