@horizon-republic/nestjs-jetstream 2.7.1 → 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 +968 -148
- package/dist/index.d.cts +396 -27
- package/dist/index.d.ts +396 -27
- package/dist/index.js +935 -128
- package/package.json +19 -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";
|
|
@@ -24,9 +24,9 @@ import { Logger } from "@nestjs/common";
|
|
|
24
24
|
import { ClientProxy } from "@nestjs/microservices";
|
|
25
25
|
import {
|
|
26
26
|
createInbox,
|
|
27
|
-
Events,
|
|
28
27
|
headers as natsHeaders
|
|
29
|
-
} from "nats";
|
|
28
|
+
} from "@nats-io/transport-node";
|
|
29
|
+
import { nuid } from "@nats-io/nuid";
|
|
30
30
|
|
|
31
31
|
// src/interfaces/hooks.interface.ts
|
|
32
32
|
var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
RetentionPolicy,
|
|
66
66
|
StorageType,
|
|
67
67
|
StoreCompression
|
|
68
|
-
} from "nats";
|
|
68
|
+
} from "@nats-io/jetstream";
|
|
69
69
|
var JETSTREAM_OPTIONS = /* @__PURE__ */ Symbol("JETSTREAM_OPTIONS");
|
|
70
70
|
var JETSTREAM_CONNECTION = /* @__PURE__ */ Symbol("JETSTREAM_CONNECTION");
|
|
71
71
|
var JETSTREAM_CODEC = /* @__PURE__ */ Symbol("JETSTREAM_CODEC");
|
|
@@ -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,11 +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) {
|
|
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;
|
|
234
|
+
this.schedule = schedule;
|
|
235
|
+
this.ttl = ttl;
|
|
205
236
|
}
|
|
206
237
|
};
|
|
207
238
|
var JetstreamRecordBuilder = class {
|
|
@@ -209,6 +240,8 @@ var JetstreamRecordBuilder = class {
|
|
|
209
240
|
headers = /* @__PURE__ */ new Map();
|
|
210
241
|
timeout;
|
|
211
242
|
messageId;
|
|
243
|
+
scheduleOptions;
|
|
244
|
+
ttlDuration;
|
|
212
245
|
constructor(data) {
|
|
213
246
|
this.data = data;
|
|
214
247
|
}
|
|
@@ -276,17 +309,71 @@ var JetstreamRecordBuilder = class {
|
|
|
276
309
|
this.timeout = ms;
|
|
277
310
|
return this;
|
|
278
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Schedule one-shot delayed delivery.
|
|
314
|
+
*
|
|
315
|
+
* The message is held by NATS and delivered to the event consumer
|
|
316
|
+
* at the specified time. Requires NATS >= 2.12 and `allow_msg_schedules: true`
|
|
317
|
+
* on the event stream (via `events: { stream: { allow_msg_schedules: true } }`).
|
|
318
|
+
*
|
|
319
|
+
* Only meaningful for events (`client.emit()`). If used with RPC
|
|
320
|
+
* (`client.send()`), a warning is logged and the schedule is ignored.
|
|
321
|
+
*
|
|
322
|
+
* @param date - Delivery time. Must be in the future.
|
|
323
|
+
* @throws Error if the date is not in the future.
|
|
324
|
+
*/
|
|
325
|
+
scheduleAt(date) {
|
|
326
|
+
const ts = date.getTime();
|
|
327
|
+
if (Number.isNaN(ts)) {
|
|
328
|
+
throw new Error("Schedule date is invalid");
|
|
329
|
+
}
|
|
330
|
+
if (ts <= Date.now()) {
|
|
331
|
+
throw new Error("Schedule date must be in the future");
|
|
332
|
+
}
|
|
333
|
+
this.scheduleOptions = { at: new Date(ts) };
|
|
334
|
+
return this;
|
|
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
|
+
}
|
|
279
363
|
/**
|
|
280
364
|
* Build the immutable {@link JetstreamRecord}.
|
|
281
365
|
*
|
|
282
366
|
* @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
|
|
283
367
|
*/
|
|
284
368
|
build() {
|
|
369
|
+
const schedule = this.scheduleOptions ? { at: new Date(this.scheduleOptions.at.getTime()) } : void 0;
|
|
285
370
|
return new JetstreamRecord(
|
|
286
371
|
this.data,
|
|
287
372
|
new Map(this.headers),
|
|
288
373
|
this.timeout,
|
|
289
|
-
this.messageId
|
|
374
|
+
this.messageId,
|
|
375
|
+
schedule,
|
|
376
|
+
this.ttlDuration
|
|
290
377
|
);
|
|
291
378
|
}
|
|
292
379
|
/** Validate that a header key is not reserved. */
|
|
@@ -298,6 +385,17 @@ var JetstreamRecordBuilder = class {
|
|
|
298
385
|
}
|
|
299
386
|
}
|
|
300
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
|
+
};
|
|
301
399
|
|
|
302
400
|
// src/client/jetstream.client.ts
|
|
303
401
|
var JetstreamClient = class extends ClientProxy {
|
|
@@ -338,7 +436,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
338
436
|
this.setupInbox(nc);
|
|
339
437
|
}
|
|
340
438
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
341
|
-
if (status.type ===
|
|
439
|
+
if (status.type === "disconnect") {
|
|
342
440
|
this.handleDisconnect();
|
|
343
441
|
}
|
|
344
442
|
});
|
|
@@ -366,19 +464,40 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
366
464
|
* Publish a fire-and-forget event to JetStream.
|
|
367
465
|
*
|
|
368
466
|
* Events are published to either the workqueue stream or broadcast stream
|
|
369
|
-
* depending on the subject prefix.
|
|
467
|
+
* depending on the subject prefix. When a schedule is present the message
|
|
468
|
+
* is published to a `_sch` subject within the same stream, with the target
|
|
469
|
+
* set to the original event subject.
|
|
370
470
|
*/
|
|
371
471
|
async dispatchEvent(packet) {
|
|
372
472
|
await this.connect();
|
|
373
|
-
const { data, hdrs, messageId } = this.extractRecordData(packet.data);
|
|
374
|
-
const
|
|
375
|
-
const msgHeaders = this.buildHeaders(hdrs, { subject });
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
473
|
+
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
474
|
+
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
475
|
+
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
476
|
+
if (schedule) {
|
|
477
|
+
const scheduleSubject = this.buildScheduleSubject(eventSubject);
|
|
478
|
+
const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
|
|
479
|
+
headers: msgHeaders,
|
|
480
|
+
msgID: messageId ?? nuid.next(),
|
|
481
|
+
ttl,
|
|
482
|
+
schedule: {
|
|
483
|
+
specification: schedule.at,
|
|
484
|
+
target: eventSubject
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
if (ack.duplicate) {
|
|
488
|
+
this.logger.warn(
|
|
489
|
+
`Duplicate scheduled publish detected: ${scheduleSubject} (seq: ${ack.seq})`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
494
|
+
headers: msgHeaders,
|
|
495
|
+
msgID: messageId ?? nuid.next(),
|
|
496
|
+
ttl
|
|
497
|
+
});
|
|
498
|
+
if (ack.duplicate) {
|
|
499
|
+
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
500
|
+
}
|
|
382
501
|
}
|
|
383
502
|
return void 0;
|
|
384
503
|
}
|
|
@@ -390,7 +509,17 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
390
509
|
*/
|
|
391
510
|
publish(packet, callback) {
|
|
392
511
|
const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
|
|
393
|
-
const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
|
|
512
|
+
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
513
|
+
if (schedule) {
|
|
514
|
+
this.logger.warn(
|
|
515
|
+
"scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
|
|
516
|
+
);
|
|
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
|
+
}
|
|
394
523
|
const onUnhandled = (err) => {
|
|
395
524
|
this.logger.error("Unhandled publish error:", err);
|
|
396
525
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
@@ -399,7 +528,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
399
528
|
if (isCoreRpcMode(this.rootOptions.rpc)) {
|
|
400
529
|
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
|
|
401
530
|
} else {
|
|
402
|
-
jetStreamCorrelationId =
|
|
531
|
+
jetStreamCorrelationId = nuid.next();
|
|
403
532
|
this.publishJetStreamRpc(subject, data, callback, {
|
|
404
533
|
headers: hdrs,
|
|
405
534
|
timeout,
|
|
@@ -474,7 +603,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
474
603
|
});
|
|
475
604
|
await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
|
|
476
605
|
headers: hdrs,
|
|
477
|
-
msgID: messageId ??
|
|
606
|
+
msgID: messageId ?? nuid.next()
|
|
478
607
|
});
|
|
479
608
|
} catch (err) {
|
|
480
609
|
const existingTimeout = this.pendingTimeouts.get(correlationId);
|
|
@@ -506,6 +635,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
506
635
|
this.pendingTimeouts.clear();
|
|
507
636
|
this.inboxSubscription?.unsubscribe();
|
|
508
637
|
this.inboxSubscription = null;
|
|
638
|
+
this.inbox = null;
|
|
509
639
|
}
|
|
510
640
|
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
511
641
|
setupInbox(nc) {
|
|
@@ -587,17 +717,53 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
587
717
|
}
|
|
588
718
|
return hdrs;
|
|
589
719
|
}
|
|
590
|
-
/** Extract data, headers, and
|
|
720
|
+
/** Extract data, headers, timeout, and schedule from raw packet data or JetstreamRecord. */
|
|
591
721
|
extractRecordData(rawData) {
|
|
592
722
|
if (rawData instanceof JetstreamRecord) {
|
|
593
723
|
return {
|
|
594
724
|
data: rawData.data,
|
|
595
725
|
hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
|
|
596
726
|
timeout: rawData.timeout,
|
|
597
|
-
messageId: rawData.messageId
|
|
727
|
+
messageId: rawData.messageId,
|
|
728
|
+
schedule: rawData.schedule,
|
|
729
|
+
ttl: rawData.ttl
|
|
598
730
|
};
|
|
599
731
|
}
|
|
600
|
-
return {
|
|
732
|
+
return {
|
|
733
|
+
data: rawData,
|
|
734
|
+
hdrs: null,
|
|
735
|
+
timeout: void 0,
|
|
736
|
+
messageId: void 0,
|
|
737
|
+
schedule: void 0,
|
|
738
|
+
ttl: void 0
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Build a schedule-holder subject for NATS message scheduling.
|
|
743
|
+
*
|
|
744
|
+
* The schedule-holder subject resides in the same stream as the target but
|
|
745
|
+
* uses a separate `_sch` namespace that is NOT matched by any consumer filter.
|
|
746
|
+
* NATS holds the message and publishes it to the target subject after the delay.
|
|
747
|
+
*
|
|
748
|
+
* Examples:
|
|
749
|
+
* - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
|
|
750
|
+
* - `broadcast.config.updated` → `broadcast._sch.config.updated`
|
|
751
|
+
*/
|
|
752
|
+
buildScheduleSubject(eventSubject) {
|
|
753
|
+
if (eventSubject.startsWith("broadcast.")) {
|
|
754
|
+
return eventSubject.replace("broadcast.", "broadcast._sch.");
|
|
755
|
+
}
|
|
756
|
+
const targetPrefix = `${internalName(this.targetName)}.`;
|
|
757
|
+
if (!eventSubject.startsWith(targetPrefix)) {
|
|
758
|
+
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
759
|
+
}
|
|
760
|
+
const withoutPrefix = eventSubject.slice(targetPrefix.length);
|
|
761
|
+
const dotIndex = withoutPrefix.indexOf(".");
|
|
762
|
+
if (dotIndex === -1) {
|
|
763
|
+
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
764
|
+
}
|
|
765
|
+
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
766
|
+
return `${targetPrefix}_sch.${pattern}`;
|
|
601
767
|
}
|
|
602
768
|
getRpcTimeout() {
|
|
603
769
|
if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
|
|
@@ -607,25 +773,26 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
607
773
|
};
|
|
608
774
|
|
|
609
775
|
// src/codec/json.codec.ts
|
|
610
|
-
|
|
776
|
+
var encoder = new TextEncoder();
|
|
777
|
+
var decoder = new TextDecoder();
|
|
611
778
|
var JsonCodec = class {
|
|
612
|
-
inner = NatsJSONCodec();
|
|
613
779
|
encode(data) {
|
|
614
|
-
return
|
|
780
|
+
return encoder.encode(JSON.stringify(data));
|
|
615
781
|
}
|
|
616
782
|
decode(data) {
|
|
617
|
-
return
|
|
783
|
+
return JSON.parse(decoder.decode(data));
|
|
618
784
|
}
|
|
619
785
|
};
|
|
620
786
|
|
|
621
787
|
// src/connection/connection.provider.ts
|
|
622
788
|
import { Logger as Logger2 } from "@nestjs/common";
|
|
623
789
|
import {
|
|
624
|
-
connect
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
790
|
+
connect
|
|
791
|
+
} from "@nats-io/transport-node";
|
|
792
|
+
import {
|
|
793
|
+
jetstream,
|
|
794
|
+
jetstreamManager
|
|
795
|
+
} from "@nats-io/jetstream";
|
|
629
796
|
import { defer, from, share, shareReplay, switchMap } from "rxjs";
|
|
630
797
|
var DEFAULT_OPTIONS = {
|
|
631
798
|
maxReconnectAttempts: -1,
|
|
@@ -679,14 +846,7 @@ var ConnectionProvider = class {
|
|
|
679
846
|
async getJetStreamManager() {
|
|
680
847
|
if (this.jsmInstance) return this.jsmInstance;
|
|
681
848
|
if (this.jsmPromise) return this.jsmPromise;
|
|
682
|
-
this.jsmPromise = (
|
|
683
|
-
const nc = await this.getConnection();
|
|
684
|
-
this.jsmInstance = await nc.jetstreamManager();
|
|
685
|
-
this.logger.log("JetStream manager initialized");
|
|
686
|
-
return this.jsmInstance;
|
|
687
|
-
})().finally(() => {
|
|
688
|
-
this.jsmPromise = null;
|
|
689
|
-
});
|
|
849
|
+
this.jsmPromise = this.initJetStreamManager();
|
|
690
850
|
return this.jsmPromise;
|
|
691
851
|
}
|
|
692
852
|
/**
|
|
@@ -702,7 +862,7 @@ var ConnectionProvider = class {
|
|
|
702
862
|
if (!this.connection || this.connection.isClosed()) {
|
|
703
863
|
throw new Error("Not connected \u2014 call getConnection() before getJetStreamClient()");
|
|
704
864
|
}
|
|
705
|
-
this.jsClient ??= this.connection
|
|
865
|
+
this.jsClient ??= jetstream(this.connection);
|
|
706
866
|
return this.jsClient;
|
|
707
867
|
}
|
|
708
868
|
/** Direct access to the raw NATS connection, or `null` if not yet connected. */
|
|
@@ -738,6 +898,16 @@ var ConnectionProvider = class {
|
|
|
738
898
|
this.jsmPromise = null;
|
|
739
899
|
}
|
|
740
900
|
}
|
|
901
|
+
async initJetStreamManager() {
|
|
902
|
+
try {
|
|
903
|
+
const nc = await this.getConnection();
|
|
904
|
+
this.jsmInstance = await jetstreamManager(nc);
|
|
905
|
+
this.logger.log("JetStream manager initialized");
|
|
906
|
+
return this.jsmInstance;
|
|
907
|
+
} finally {
|
|
908
|
+
this.jsmPromise = null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
741
911
|
/** Internal: establish the physical connection with reconnect monitoring. */
|
|
742
912
|
async establish() {
|
|
743
913
|
const name = internalName(this.options.name);
|
|
@@ -754,7 +924,7 @@ var ConnectionProvider = class {
|
|
|
754
924
|
this.monitorStatus(nc);
|
|
755
925
|
return nc;
|
|
756
926
|
} catch (err) {
|
|
757
|
-
if (err instanceof
|
|
927
|
+
if (err instanceof Error && err.message.includes("REFUSED")) {
|
|
758
928
|
throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
|
|
759
929
|
}
|
|
760
930
|
throw err;
|
|
@@ -762,27 +932,33 @@ var ConnectionProvider = class {
|
|
|
762
932
|
}
|
|
763
933
|
/** Subscribe to connection status events and emit hooks. */
|
|
764
934
|
monitorStatus(nc) {
|
|
765
|
-
(async () => {
|
|
935
|
+
void (async () => {
|
|
766
936
|
for await (const status of nc.status()) {
|
|
767
937
|
switch (status.type) {
|
|
768
|
-
case
|
|
938
|
+
case "disconnect":
|
|
769
939
|
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
770
940
|
break;
|
|
771
|
-
case
|
|
941
|
+
case "reconnect":
|
|
772
942
|
this.jsClient = null;
|
|
773
943
|
this.jsmInstance = null;
|
|
774
944
|
this.jsmPromise = null;
|
|
775
945
|
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
776
946
|
break;
|
|
777
|
-
case
|
|
778
|
-
this.eventBus.emit(
|
|
947
|
+
case "error":
|
|
948
|
+
this.eventBus.emit(
|
|
949
|
+
"error" /* Error */,
|
|
950
|
+
status.error,
|
|
951
|
+
"connection"
|
|
952
|
+
);
|
|
779
953
|
break;
|
|
780
|
-
case
|
|
781
|
-
case
|
|
782
|
-
case
|
|
783
|
-
case
|
|
784
|
-
case
|
|
785
|
-
case
|
|
954
|
+
case "update":
|
|
955
|
+
case "ldm":
|
|
956
|
+
case "reconnecting":
|
|
957
|
+
case "ping":
|
|
958
|
+
case "staleConnection":
|
|
959
|
+
case "forceReconnect":
|
|
960
|
+
case "slowConsumer":
|
|
961
|
+
case "close":
|
|
786
962
|
break;
|
|
787
963
|
}
|
|
788
964
|
}
|
|
@@ -879,9 +1055,14 @@ var JetstreamHealthIndicator = class {
|
|
|
879
1055
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
880
1056
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
881
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
|
+
*
|
|
882
1063
|
* @param key - Health indicator key (default: `'jetstream'`).
|
|
883
1064
|
* @returns Object with status, server, and latency under the given key.
|
|
884
|
-
* @throws Error with `{ [key]: { status: 'down' } }
|
|
1065
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
885
1066
|
*/
|
|
886
1067
|
async isHealthy(key = "jetstream") {
|
|
887
1068
|
const status = await this.check();
|
|
@@ -891,8 +1072,10 @@ var JetstreamHealthIndicator = class {
|
|
|
891
1072
|
latency: status.latency
|
|
892
1073
|
};
|
|
893
1074
|
if (!status.connected) {
|
|
1075
|
+
const causes = { [key]: details };
|
|
894
1076
|
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
895
|
-
|
|
1077
|
+
causes,
|
|
1078
|
+
isHealthCheckError: true
|
|
896
1079
|
});
|
|
897
1080
|
}
|
|
898
1081
|
return { [key]: details };
|
|
@@ -905,7 +1088,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
905
1088
|
// src/server/strategy.ts
|
|
906
1089
|
import { Server } from "@nestjs/microservices";
|
|
907
1090
|
var JetstreamStrategy = class extends Server {
|
|
908
|
-
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) {
|
|
909
1092
|
super();
|
|
910
1093
|
this.options = options;
|
|
911
1094
|
this.connection = connection;
|
|
@@ -917,6 +1100,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
917
1100
|
this.rpcRouter = rpcRouter;
|
|
918
1101
|
this.coreRpcServer = coreRpcServer;
|
|
919
1102
|
this.ackWaitMap = ackWaitMap;
|
|
1103
|
+
this.metadataProvider = metadataProvider;
|
|
920
1104
|
}
|
|
921
1105
|
transportId = /* @__PURE__ */ Symbol("jetstream-transport");
|
|
922
1106
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -961,10 +1145,14 @@ var JetstreamStrategy = class extends Server {
|
|
|
961
1145
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
962
1146
|
await this.coreRpcServer.start();
|
|
963
1147
|
}
|
|
1148
|
+
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
1149
|
+
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
1150
|
+
}
|
|
964
1151
|
callback();
|
|
965
1152
|
}
|
|
966
|
-
/** Stop all consumers, routers, and
|
|
1153
|
+
/** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
|
|
967
1154
|
close() {
|
|
1155
|
+
this.metadataProvider?.destroy();
|
|
968
1156
|
this.eventRouter.destroy();
|
|
969
1157
|
this.rpcRouter.destroy();
|
|
970
1158
|
this.coreRpcServer.stop();
|
|
@@ -1044,7 +1232,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
1044
1232
|
|
|
1045
1233
|
// src/server/core-rpc.server.ts
|
|
1046
1234
|
import { Logger as Logger4 } from "@nestjs/common";
|
|
1047
|
-
import { headers as natsHeaders2 } from "nats";
|
|
1235
|
+
import { headers as natsHeaders2 } from "@nats-io/transport-node";
|
|
1048
1236
|
|
|
1049
1237
|
// src/context/rpc.context.ts
|
|
1050
1238
|
import { BaseRpcContext } from "@nestjs/microservices";
|
|
@@ -1340,24 +1528,172 @@ var CoreRpcServer = class {
|
|
|
1340
1528
|
};
|
|
1341
1529
|
|
|
1342
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
|
|
1343
1590
|
import { Logger as Logger5 } from "@nestjs/common";
|
|
1344
|
-
import {
|
|
1345
|
-
var
|
|
1591
|
+
import { JetStreamApiError } from "@nats-io/jetstream";
|
|
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
|
|
1346
1677
|
var StreamProvider = class {
|
|
1347
1678
|
constructor(options, connection) {
|
|
1348
1679
|
this.options = options;
|
|
1349
1680
|
this.connection = connection;
|
|
1350
1681
|
}
|
|
1351
|
-
logger = new
|
|
1682
|
+
logger = new Logger6("Jetstream:Stream");
|
|
1683
|
+
migration = new StreamMigration();
|
|
1352
1684
|
/**
|
|
1353
1685
|
* Ensure all required streams exist with correct configuration.
|
|
1354
1686
|
*
|
|
1355
1687
|
* @param kinds Which stream kinds to create. Determined by the module based
|
|
1356
1688
|
* on RPC mode and registered handler patterns.
|
|
1689
|
+
* If the dlq option is enabled, also ensures the DLQ stream exists.
|
|
1357
1690
|
*/
|
|
1358
1691
|
async ensureStreams(kinds) {
|
|
1359
1692
|
const jsm = await this.connection.getJetStreamManager();
|
|
1360
1693
|
await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
|
|
1694
|
+
if (this.options.dlq) {
|
|
1695
|
+
await this.ensureDlqStream(jsm);
|
|
1696
|
+
}
|
|
1361
1697
|
}
|
|
1362
1698
|
/** Get the stream name for a given kind. */
|
|
1363
1699
|
getStreamName(kind) {
|
|
@@ -1367,12 +1703,22 @@ var StreamProvider = class {
|
|
|
1367
1703
|
getSubjects(kind) {
|
|
1368
1704
|
const name = internalName(this.options.name);
|
|
1369
1705
|
switch (kind) {
|
|
1370
|
-
case "ev" /* Event */:
|
|
1371
|
-
|
|
1706
|
+
case "ev" /* Event */: {
|
|
1707
|
+
const subjects = [`${name}.${"ev" /* Event */}.>`];
|
|
1708
|
+
if (this.isSchedulingEnabled(kind)) {
|
|
1709
|
+
subjects.push(`${name}._sch.>`);
|
|
1710
|
+
}
|
|
1711
|
+
return subjects;
|
|
1712
|
+
}
|
|
1372
1713
|
case "cmd" /* Command */:
|
|
1373
1714
|
return [`${name}.${"cmd" /* Command */}.>`];
|
|
1374
|
-
case "broadcast" /* Broadcast */:
|
|
1375
|
-
|
|
1715
|
+
case "broadcast" /* Broadcast */: {
|
|
1716
|
+
const subjects = ["broadcast.>"];
|
|
1717
|
+
if (this.isSchedulingEnabled(kind)) {
|
|
1718
|
+
subjects.push("broadcast._sch.>");
|
|
1719
|
+
}
|
|
1720
|
+
return subjects;
|
|
1721
|
+
}
|
|
1376
1722
|
case "ordered" /* Ordered */:
|
|
1377
1723
|
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
1378
1724
|
}
|
|
@@ -1382,17 +1728,85 @@ var StreamProvider = class {
|
|
|
1382
1728
|
const config = this.buildConfig(kind);
|
|
1383
1729
|
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
1384
1730
|
try {
|
|
1385
|
-
await jsm.streams.info(config.name);
|
|
1386
|
-
this.
|
|
1387
|
-
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);
|
|
1388
1733
|
} catch (err) {
|
|
1389
|
-
if (err instanceof
|
|
1734
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1390
1735
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
1391
1736
|
return await jsm.streams.add(config);
|
|
1392
1737
|
}
|
|
1393
1738
|
throw err;
|
|
1394
1739
|
}
|
|
1395
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
|
+
}
|
|
1396
1810
|
/** Build the full stream config by merging defaults with user overrides. */
|
|
1397
1811
|
buildConfig(kind) {
|
|
1398
1812
|
const name = this.getStreamName(kind);
|
|
@@ -1408,6 +1822,26 @@ var StreamProvider = class {
|
|
|
1408
1822
|
description
|
|
1409
1823
|
};
|
|
1410
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
|
+
}
|
|
1411
1845
|
/** Get default config for a stream kind. */
|
|
1412
1846
|
getDefaults(kind) {
|
|
1413
1847
|
switch (kind) {
|
|
@@ -1421,25 +1855,49 @@ var StreamProvider = class {
|
|
|
1421
1855
|
return DEFAULT_ORDERED_STREAM_CONFIG;
|
|
1422
1856
|
}
|
|
1423
1857
|
}
|
|
1424
|
-
/**
|
|
1858
|
+
/** Check if scheduling is enabled for a stream kind via `allow_msg_schedules` override. */
|
|
1859
|
+
isSchedulingEnabled(kind) {
|
|
1860
|
+
const overrides = this.getOverrides(kind);
|
|
1861
|
+
return overrides.allow_msg_schedules === true;
|
|
1862
|
+
}
|
|
1863
|
+
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
1425
1864
|
getOverrides(kind) {
|
|
1865
|
+
let overrides;
|
|
1426
1866
|
switch (kind) {
|
|
1427
1867
|
case "ev" /* Event */:
|
|
1428
|
-
|
|
1868
|
+
overrides = this.options.events?.stream ?? {};
|
|
1869
|
+
break;
|
|
1429
1870
|
case "cmd" /* Command */:
|
|
1430
|
-
|
|
1871
|
+
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1872
|
+
break;
|
|
1431
1873
|
case "broadcast" /* Broadcast */:
|
|
1432
|
-
|
|
1874
|
+
overrides = this.options.broadcast?.stream ?? {};
|
|
1875
|
+
break;
|
|
1433
1876
|
case "ordered" /* Ordered */:
|
|
1434
|
-
|
|
1877
|
+
overrides = this.options.ordered?.stream ?? {};
|
|
1878
|
+
break;
|
|
1435
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;
|
|
1436
1895
|
}
|
|
1437
1896
|
};
|
|
1438
1897
|
|
|
1439
1898
|
// src/server/infrastructure/consumer.provider.ts
|
|
1440
|
-
import { Logger as
|
|
1441
|
-
import {
|
|
1442
|
-
var CONSUMER_NOT_FOUND = 10014;
|
|
1899
|
+
import { Logger as Logger7 } from "@nestjs/common";
|
|
1900
|
+
import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
|
|
1443
1901
|
var ConsumerProvider = class {
|
|
1444
1902
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
1445
1903
|
this.options = options;
|
|
@@ -1447,7 +1905,7 @@ var ConsumerProvider = class {
|
|
|
1447
1905
|
this.streamProvider = streamProvider;
|
|
1448
1906
|
this.patternRegistry = patternRegistry;
|
|
1449
1907
|
}
|
|
1450
|
-
logger = new
|
|
1908
|
+
logger = new Logger7("Jetstream:Consumer");
|
|
1451
1909
|
/**
|
|
1452
1910
|
* Ensure consumers exist for the specified kinds.
|
|
1453
1911
|
*
|
|
@@ -1468,7 +1926,11 @@ var ConsumerProvider = class {
|
|
|
1468
1926
|
getConsumerName(kind) {
|
|
1469
1927
|
return consumerName(this.options.name, kind);
|
|
1470
1928
|
}
|
|
1471
|
-
/**
|
|
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
|
+
*/
|
|
1472
1934
|
async ensureConsumer(jsm, kind) {
|
|
1473
1935
|
const stream = this.streamProvider.getStreamName(kind);
|
|
1474
1936
|
const config = this.buildConfig(kind);
|
|
@@ -1479,13 +1941,74 @@ var ConsumerProvider = class {
|
|
|
1479
1941
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
1480
1942
|
return await jsm.consumers.update(stream, name, config);
|
|
1481
1943
|
} catch (err) {
|
|
1482
|
-
if (err instanceof
|
|
1483
|
-
|
|
1484
|
-
|
|
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;
|
|
1485
1993
|
}
|
|
1486
1994
|
throw err;
|
|
1487
1995
|
}
|
|
1488
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
|
+
}
|
|
1489
2012
|
/** Build consumer config by merging defaults with user overrides. */
|
|
1490
2013
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
1491
2014
|
buildConfig(kind) {
|
|
@@ -1538,6 +2061,11 @@ var ConsumerProvider = class {
|
|
|
1538
2061
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1539
2062
|
case "ordered" /* Ordered */:
|
|
1540
2063
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2064
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
2065
|
+
default: {
|
|
2066
|
+
const _exhaustive = kind;
|
|
2067
|
+
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
2068
|
+
}
|
|
1541
2069
|
}
|
|
1542
2070
|
}
|
|
1543
2071
|
/** Get user-provided overrides for a consumer kind. */
|
|
@@ -1551,16 +2079,18 @@ var ConsumerProvider = class {
|
|
|
1551
2079
|
return this.options.broadcast?.consumer ?? {};
|
|
1552
2080
|
case "ordered" /* Ordered */:
|
|
1553
2081
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2082
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
2083
|
+
default: {
|
|
2084
|
+
const _exhaustive = kind;
|
|
2085
|
+
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
2086
|
+
}
|
|
1554
2087
|
}
|
|
1555
2088
|
}
|
|
1556
2089
|
};
|
|
1557
2090
|
|
|
1558
2091
|
// src/server/infrastructure/message.provider.ts
|
|
1559
|
-
import { Logger as
|
|
1560
|
-
import {
|
|
1561
|
-
ConsumerEvents,
|
|
1562
|
-
DeliverPolicy as DeliverPolicy2
|
|
1563
|
-
} from "nats";
|
|
2092
|
+
import { Logger as Logger8 } from "@nestjs/common";
|
|
2093
|
+
import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
|
|
1564
2094
|
import {
|
|
1565
2095
|
catchError,
|
|
1566
2096
|
defer as defer2,
|
|
@@ -1573,12 +2103,13 @@ import {
|
|
|
1573
2103
|
timer
|
|
1574
2104
|
} from "rxjs";
|
|
1575
2105
|
var MessageProvider = class {
|
|
1576
|
-
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
|
|
2106
|
+
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
1577
2107
|
this.connection = connection;
|
|
1578
2108
|
this.eventBus = eventBus;
|
|
1579
2109
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2110
|
+
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
1580
2111
|
}
|
|
1581
|
-
logger = new
|
|
2112
|
+
logger = new Logger8("Jetstream:Message");
|
|
1582
2113
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1583
2114
|
orderedReadyResolve = null;
|
|
1584
2115
|
orderedReadyReject = null;
|
|
@@ -1629,7 +2160,7 @@ var MessageProvider = class {
|
|
|
1629
2160
|
* @param orderedConfig - Optional overrides for ordered consumer options.
|
|
1630
2161
|
*/
|
|
1631
2162
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1632
|
-
const consumerOpts = { filterSubjects };
|
|
2163
|
+
const consumerOpts = { filter_subjects: filterSubjects };
|
|
1633
2164
|
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== DeliverPolicy2.All) {
|
|
1634
2165
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
1635
2166
|
}
|
|
@@ -1681,12 +2212,26 @@ var MessageProvider = class {
|
|
|
1681
2212
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
1682
2213
|
async consumeOnce(kind, info, target$) {
|
|
1683
2214
|
const js = this.connection.getJetStreamClient();
|
|
1684
|
-
|
|
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
|
+
}
|
|
1685
2230
|
const defaults = { idle_heartbeat: 5e3 };
|
|
1686
2231
|
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
1687
2232
|
const messages = await consumer.consume({ ...defaults, ...userOptions });
|
|
1688
2233
|
this.activeIterators.add(messages);
|
|
1689
|
-
this.monitorConsumerHealth(messages,
|
|
2234
|
+
this.monitorConsumerHealth(messages, consumerName2);
|
|
1690
2235
|
try {
|
|
1691
2236
|
for await (const msg of messages) {
|
|
1692
2237
|
target$.next(msg);
|
|
@@ -1695,6 +2240,17 @@ var MessageProvider = class {
|
|
|
1695
2240
|
this.activeIterators.delete(messages);
|
|
1696
2241
|
}
|
|
1697
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
|
+
}
|
|
1698
2254
|
/** Get the target subject for a consumer kind. */
|
|
1699
2255
|
getTargetSubject(kind) {
|
|
1700
2256
|
switch (kind) {
|
|
@@ -1706,6 +2262,7 @@ var MessageProvider = class {
|
|
|
1706
2262
|
return this.broadcastMessages$;
|
|
1707
2263
|
case "ordered" /* Ordered */:
|
|
1708
2264
|
return this.orderedMessages$;
|
|
2265
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1709
2266
|
default: {
|
|
1710
2267
|
const _exhaustive = kind;
|
|
1711
2268
|
throw new Error(`Unknown stream kind: ${_exhaustive}`);
|
|
@@ -1714,10 +2271,10 @@ var MessageProvider = class {
|
|
|
1714
2271
|
}
|
|
1715
2272
|
/** Monitor heartbeats and restart the consumer iterator on prolonged silence. */
|
|
1716
2273
|
monitorConsumerHealth(messages, name) {
|
|
1717
|
-
(async () => {
|
|
1718
|
-
for await (const status of
|
|
1719
|
-
if (status.type ===
|
|
1720
|
-
this.logger.warn(`Consumer ${name}: ${status.
|
|
2274
|
+
void (async () => {
|
|
2275
|
+
for await (const status of messages.status()) {
|
|
2276
|
+
if (status.type === "heartbeats_missed" && status.count >= 2) {
|
|
2277
|
+
this.logger.warn(`Consumer ${name}: ${status.count} heartbeats missed, restarting`);
|
|
1721
2278
|
messages.stop();
|
|
1722
2279
|
break;
|
|
1723
2280
|
}
|
|
@@ -1791,8 +2348,110 @@ var MessageProvider = class {
|
|
|
1791
2348
|
}
|
|
1792
2349
|
};
|
|
1793
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
|
+
|
|
1794
2453
|
// src/server/routing/pattern-registry.ts
|
|
1795
|
-
import { Logger as
|
|
2454
|
+
import { Logger as Logger10 } from "@nestjs/common";
|
|
1796
2455
|
var HANDLER_LABELS = {
|
|
1797
2456
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
1798
2457
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -1803,7 +2462,7 @@ var PatternRegistry = class {
|
|
|
1803
2462
|
constructor(options) {
|
|
1804
2463
|
this.options = options;
|
|
1805
2464
|
}
|
|
1806
|
-
logger = new
|
|
2465
|
+
logger = new Logger10("Jetstream:PatternRegistry");
|
|
1807
2466
|
registry = /* @__PURE__ */ new Map();
|
|
1808
2467
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1809
2468
|
cachedPatterns = null;
|
|
@@ -1811,6 +2470,7 @@ var PatternRegistry = class {
|
|
|
1811
2470
|
_hasCommands = false;
|
|
1812
2471
|
_hasBroadcasts = false;
|
|
1813
2472
|
_hasOrdered = false;
|
|
2473
|
+
_hasMetadata = false;
|
|
1814
2474
|
/**
|
|
1815
2475
|
* Register all handlers from the NestJS strategy.
|
|
1816
2476
|
*
|
|
@@ -1823,6 +2483,7 @@ var PatternRegistry = class {
|
|
|
1823
2483
|
const isEvent = handler.isEventHandler ?? false;
|
|
1824
2484
|
const isBroadcast = !!extras?.broadcast;
|
|
1825
2485
|
const isOrdered = !!extras?.ordered;
|
|
2486
|
+
const meta = extras?.meta;
|
|
1826
2487
|
if (isBroadcast && isOrdered) {
|
|
1827
2488
|
throw new Error(
|
|
1828
2489
|
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
@@ -1839,7 +2500,8 @@ var PatternRegistry = class {
|
|
|
1839
2500
|
pattern,
|
|
1840
2501
|
isEvent: isEvent && !isOrdered,
|
|
1841
2502
|
isBroadcast,
|
|
1842
|
-
isOrdered
|
|
2503
|
+
isOrdered,
|
|
2504
|
+
meta
|
|
1843
2505
|
});
|
|
1844
2506
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1845
2507
|
}
|
|
@@ -1848,6 +2510,7 @@ var PatternRegistry = class {
|
|
|
1848
2510
|
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1849
2511
|
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1850
2512
|
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2513
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
1851
2514
|
this.logSummary();
|
|
1852
2515
|
}
|
|
1853
2516
|
/** Find handler for a full NATS subject. */
|
|
@@ -1876,6 +2539,26 @@ var PatternRegistry = class {
|
|
|
1876
2539
|
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
1877
2540
|
);
|
|
1878
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
|
+
}
|
|
1879
2562
|
/** Get patterns grouped by kind (cached after registration). */
|
|
1880
2563
|
getPatternsByKind() {
|
|
1881
2564
|
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
@@ -1915,6 +2598,12 @@ var PatternRegistry = class {
|
|
|
1915
2598
|
}
|
|
1916
2599
|
return { events, commands, broadcasts, ordered };
|
|
1917
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
|
+
}
|
|
1918
2607
|
logSummary() {
|
|
1919
2608
|
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
1920
2609
|
const parts = [
|
|
@@ -1930,10 +2619,11 @@ var PatternRegistry = class {
|
|
|
1930
2619
|
};
|
|
1931
2620
|
|
|
1932
2621
|
// src/server/routing/event.router.ts
|
|
1933
|
-
import { Logger as
|
|
2622
|
+
import { Logger as Logger11 } from "@nestjs/common";
|
|
1934
2623
|
import { concatMap, from as from2, mergeMap } from "rxjs";
|
|
2624
|
+
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
1935
2625
|
var EventRouter = class {
|
|
1936
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
2626
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
1937
2627
|
this.messageProvider = messageProvider;
|
|
1938
2628
|
this.patternRegistry = patternRegistry;
|
|
1939
2629
|
this.codec = codec;
|
|
@@ -1941,8 +2631,10 @@ var EventRouter = class {
|
|
|
1941
2631
|
this.deadLetterConfig = deadLetterConfig;
|
|
1942
2632
|
this.processingConfig = processingConfig;
|
|
1943
2633
|
this.ackWaitMap = ackWaitMap;
|
|
2634
|
+
this.connection = connection;
|
|
2635
|
+
this.options = options;
|
|
1944
2636
|
}
|
|
1945
|
-
logger = new
|
|
2637
|
+
logger = new Logger11("Jetstream:EventRouter");
|
|
1946
2638
|
subscriptions = [];
|
|
1947
2639
|
/**
|
|
1948
2640
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
@@ -2069,6 +2761,93 @@ var EventRouter = class {
|
|
|
2069
2761
|
return msg.info.deliveryCount >= maxDeliver;
|
|
2070
2762
|
}
|
|
2071
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
|
+
*/
|
|
2072
2851
|
async handleDeadLetter(msg, data, error) {
|
|
2073
2852
|
const info = {
|
|
2074
2853
|
subject: msg.subject,
|
|
@@ -2081,23 +2860,17 @@ var EventRouter = class {
|
|
|
2081
2860
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
2082
2861
|
};
|
|
2083
2862
|
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
2084
|
-
if (!this.
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
try {
|
|
2089
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
2090
|
-
msg.term("Dead letter processed");
|
|
2091
|
-
} catch (hookErr) {
|
|
2092
|
-
this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
|
|
2093
|
-
msg.nak();
|
|
2863
|
+
if (!this.options?.dlq) {
|
|
2864
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2865
|
+
} else {
|
|
2866
|
+
await this.publishToDlq(msg, info, error);
|
|
2094
2867
|
}
|
|
2095
2868
|
}
|
|
2096
2869
|
};
|
|
2097
2870
|
|
|
2098
2871
|
// src/server/routing/rpc.router.ts
|
|
2099
|
-
import { Logger as
|
|
2100
|
-
import { headers } from "nats";
|
|
2872
|
+
import { Logger as Logger12 } from "@nestjs/common";
|
|
2873
|
+
import { headers } from "@nats-io/transport-node";
|
|
2101
2874
|
import { from as from3, mergeMap as mergeMap2 } from "rxjs";
|
|
2102
2875
|
var RpcRouter = class {
|
|
2103
2876
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
@@ -2111,7 +2884,7 @@ var RpcRouter = class {
|
|
|
2111
2884
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
2112
2885
|
this.concurrency = rpcOptions?.concurrency;
|
|
2113
2886
|
}
|
|
2114
|
-
logger = new
|
|
2887
|
+
logger = new Logger12("Jetstream:RpcRouter");
|
|
2115
2888
|
timeout;
|
|
2116
2889
|
concurrency;
|
|
2117
2890
|
resolvedAckExtensionInterval;
|
|
@@ -2214,20 +2987,27 @@ var RpcRouter = class {
|
|
|
2214
2987
|
};
|
|
2215
2988
|
|
|
2216
2989
|
// src/shutdown/shutdown.manager.ts
|
|
2217
|
-
import { Logger as
|
|
2990
|
+
import { Logger as Logger13 } from "@nestjs/common";
|
|
2218
2991
|
var ShutdownManager = class {
|
|
2219
2992
|
constructor(connection, eventBus, timeout) {
|
|
2220
2993
|
this.connection = connection;
|
|
2221
2994
|
this.eventBus = eventBus;
|
|
2222
2995
|
this.timeout = timeout;
|
|
2223
2996
|
}
|
|
2224
|
-
logger = new
|
|
2997
|
+
logger = new Logger13("Jetstream:Shutdown");
|
|
2998
|
+
shutdownPromise;
|
|
2225
2999
|
/**
|
|
2226
3000
|
* Execute the full shutdown sequence.
|
|
2227
3001
|
*
|
|
3002
|
+
* Idempotent — concurrent or repeated calls return the same promise.
|
|
3003
|
+
*
|
|
2228
3004
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
2229
3005
|
*/
|
|
2230
3006
|
async shutdown(strategy) {
|
|
3007
|
+
this.shutdownPromise ??= this.doShutdown(strategy);
|
|
3008
|
+
return this.shutdownPromise;
|
|
3009
|
+
}
|
|
3010
|
+
async doShutdown(strategy) {
|
|
2231
3011
|
this.eventBus.emit("shutdownStart" /* ShutdownStart */);
|
|
2232
3012
|
this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
|
|
2233
3013
|
strategy?.close();
|
|
@@ -2362,7 +3142,7 @@ var JetstreamModule = class {
|
|
|
2362
3142
|
provide: JETSTREAM_EVENT_BUS,
|
|
2363
3143
|
inject: [JETSTREAM_OPTIONS],
|
|
2364
3144
|
useFactory: (options) => {
|
|
2365
|
-
const logger = new
|
|
3145
|
+
const logger = new Logger14("Jetstream:Module");
|
|
2366
3146
|
return new EventBus(logger, options.hooks);
|
|
2367
3147
|
}
|
|
2368
3148
|
},
|
|
@@ -2441,8 +3221,8 @@ var JetstreamModule = class {
|
|
|
2441
3221
|
// MessageProvider — pull-based message consumption
|
|
2442
3222
|
{
|
|
2443
3223
|
provide: MessageProvider,
|
|
2444
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
|
|
2445
|
-
useFactory: (options, connection, eventBus) => {
|
|
3224
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
|
|
3225
|
+
useFactory: (options, connection, eventBus, consumerProvider) => {
|
|
2446
3226
|
if (options.consumer === false) return null;
|
|
2447
3227
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
2448
3228
|
if (options.events?.consume)
|
|
@@ -2452,7 +3232,11 @@ var JetstreamModule = class {
|
|
|
2452
3232
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
2453
3233
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
2454
3234
|
}
|
|
2455
|
-
|
|
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);
|
|
2456
3240
|
}
|
|
2457
3241
|
},
|
|
2458
3242
|
// EventRouter — routes event and broadcast messages to handlers
|
|
@@ -2464,9 +3248,10 @@ var JetstreamModule = class {
|
|
|
2464
3248
|
PatternRegistry,
|
|
2465
3249
|
JETSTREAM_CODEC,
|
|
2466
3250
|
JETSTREAM_EVENT_BUS,
|
|
2467
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3251
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3252
|
+
JETSTREAM_CONNECTION
|
|
2468
3253
|
],
|
|
2469
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
|
|
3254
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
2470
3255
|
if (options.consumer === false) return null;
|
|
2471
3256
|
const deadLetterConfig = options.onDeadLetter ? {
|
|
2472
3257
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -2489,7 +3274,9 @@ var JetstreamModule = class {
|
|
|
2489
3274
|
eventBus,
|
|
2490
3275
|
deadLetterConfig,
|
|
2491
3276
|
processingConfig,
|
|
2492
|
-
ackWaitMap
|
|
3277
|
+
ackWaitMap,
|
|
3278
|
+
connection,
|
|
3279
|
+
options
|
|
2493
3280
|
);
|
|
2494
3281
|
}
|
|
2495
3282
|
},
|
|
@@ -2538,6 +3325,15 @@ var JetstreamModule = class {
|
|
|
2538
3325
|
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
2539
3326
|
}
|
|
2540
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
|
+
},
|
|
2541
3337
|
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
2542
3338
|
{
|
|
2543
3339
|
provide: JetstreamStrategy,
|
|
@@ -2551,9 +3347,10 @@ var JetstreamModule = class {
|
|
|
2551
3347
|
EventRouter,
|
|
2552
3348
|
RpcRouter,
|
|
2553
3349
|
CoreRpcServer,
|
|
2554
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3350
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3351
|
+
MetadataProvider
|
|
2555
3352
|
],
|
|
2556
|
-
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
|
|
3353
|
+
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
|
|
2557
3354
|
if (options.consumer === false) return null;
|
|
2558
3355
|
return new JetstreamStrategy(
|
|
2559
3356
|
options,
|
|
@@ -2565,7 +3362,8 @@ var JetstreamModule = class {
|
|
|
2565
3362
|
eventRouter,
|
|
2566
3363
|
rpcRouter,
|
|
2567
3364
|
coreRpcServer,
|
|
2568
|
-
ackWaitMap
|
|
3365
|
+
ackWaitMap,
|
|
3366
|
+
metadataProvider
|
|
2569
3367
|
);
|
|
2570
3368
|
}
|
|
2571
3369
|
}
|
|
@@ -2632,12 +3430,17 @@ JetstreamModule = __decorateClass([
|
|
|
2632
3430
|
__decorateParam(1, Inject(JetstreamStrategy))
|
|
2633
3431
|
], JetstreamModule);
|
|
2634
3432
|
export {
|
|
3433
|
+
DEFAULT_METADATA_BUCKET,
|
|
3434
|
+
DEFAULT_METADATA_HISTORY,
|
|
3435
|
+
DEFAULT_METADATA_REPLICAS,
|
|
3436
|
+
DEFAULT_METADATA_TTL,
|
|
2635
3437
|
EventBus,
|
|
2636
3438
|
JETSTREAM_CODEC,
|
|
2637
3439
|
JETSTREAM_CONNECTION,
|
|
2638
3440
|
JETSTREAM_EVENT_BUS,
|
|
2639
3441
|
JETSTREAM_OPTIONS,
|
|
2640
3442
|
JetstreamClient,
|
|
3443
|
+
JetstreamDlqHeader,
|
|
2641
3444
|
JetstreamHeader,
|
|
2642
3445
|
JetstreamHealthIndicator,
|
|
2643
3446
|
JetstreamModule,
|
|
@@ -2645,17 +3448,21 @@ export {
|
|
|
2645
3448
|
JetstreamRecordBuilder,
|
|
2646
3449
|
JetstreamStrategy,
|
|
2647
3450
|
JsonCodec,
|
|
3451
|
+
MIN_METADATA_TTL,
|
|
2648
3452
|
MessageKind,
|
|
2649
3453
|
PatternPrefix,
|
|
2650
3454
|
RpcContext,
|
|
2651
3455
|
StreamKind,
|
|
2652
3456
|
TransportEvent,
|
|
3457
|
+
buildBroadcastSubject,
|
|
2653
3458
|
buildSubject,
|
|
2654
3459
|
consumerName,
|
|
3460
|
+
dlqStreamName,
|
|
2655
3461
|
getClientToken,
|
|
2656
3462
|
internalName,
|
|
2657
3463
|
isCoreRpcMode,
|
|
2658
3464
|
isJetStreamRpcMode,
|
|
3465
|
+
metadataKey,
|
|
2659
3466
|
streamName,
|
|
2660
3467
|
toNanos
|
|
2661
3468
|
};
|