@horizon-republic/nestjs-jetstream 2.8.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/index.cjs +1313 -281
- package/dist/index.d.cts +467 -37
- package/dist/index.d.ts +467 -37
- package/dist/index.js +1275 -264
- package/package.json +31 -16
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
14
14
|
import {
|
|
15
15
|
Global,
|
|
16
16
|
Inject,
|
|
17
|
-
Logger as
|
|
17
|
+
Logger as Logger14,
|
|
18
18
|
Module,
|
|
19
19
|
Optional
|
|
20
20
|
} from "@nestjs/common";
|
|
@@ -121,7 +121,7 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
|
121
121
|
max_msgs_per_subject: 1e6,
|
|
122
122
|
max_msgs: 1e7,
|
|
123
123
|
max_bytes: 2 * GB,
|
|
124
|
-
max_age: toNanos(1, "
|
|
124
|
+
max_age: toNanos(1, "hours"),
|
|
125
125
|
duplicate_window: toNanos(2, "minutes")
|
|
126
126
|
};
|
|
127
127
|
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
@@ -136,6 +136,18 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
|
136
136
|
max_age: toNanos(1, "days"),
|
|
137
137
|
duplicate_window: toNanos(2, "minutes")
|
|
138
138
|
};
|
|
139
|
+
var DEFAULT_DLQ_STREAM_CONFIG = {
|
|
140
|
+
...baseStreamConfig,
|
|
141
|
+
retention: RetentionPolicy.Workqueue,
|
|
142
|
+
allow_rollup_hdrs: false,
|
|
143
|
+
max_consumers: 100,
|
|
144
|
+
max_msg_size: 10 * MB,
|
|
145
|
+
max_msgs_per_subject: 5e6,
|
|
146
|
+
max_msgs: 5e7,
|
|
147
|
+
max_bytes: 5 * GB,
|
|
148
|
+
max_age: toNanos(30, "days"),
|
|
149
|
+
duplicate_window: toNanos(2, "minutes")
|
|
150
|
+
};
|
|
139
151
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
140
152
|
ack_wait: toNanos(10, "seconds"),
|
|
141
153
|
max_deliver: 3,
|
|
@@ -163,6 +175,12 @@ var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
|
|
|
163
175
|
var DEFAULT_RPC_TIMEOUT = 3e4;
|
|
164
176
|
var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
|
|
165
177
|
var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
|
|
178
|
+
var DEFAULT_METADATA_BUCKET = "handler_registry";
|
|
179
|
+
var DEFAULT_METADATA_REPLICAS = 1;
|
|
180
|
+
var DEFAULT_METADATA_HISTORY = 1;
|
|
181
|
+
var DEFAULT_METADATA_TTL = 3e4;
|
|
182
|
+
var MIN_METADATA_TTL = 5e3;
|
|
183
|
+
var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
|
|
166
184
|
var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
167
185
|
JetstreamHeader2["CorrelationId"] = "x-correlation-id";
|
|
168
186
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
@@ -171,6 +189,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
171
189
|
JetstreamHeader2["Error"] = "x-error";
|
|
172
190
|
return JetstreamHeader2;
|
|
173
191
|
})(JetstreamHeader || {});
|
|
192
|
+
var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
|
|
193
|
+
JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
|
|
194
|
+
JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
|
|
195
|
+
JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
|
|
196
|
+
JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
|
|
197
|
+
JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
|
|
198
|
+
return JetstreamDlqHeader2;
|
|
199
|
+
})(JetstreamDlqHeader || {});
|
|
174
200
|
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
175
201
|
"x-correlation-id" /* CorrelationId */,
|
|
176
202
|
"x-reply-to" /* ReplyTo */,
|
|
@@ -183,6 +209,9 @@ var streamName = (serviceName, kind) => {
|
|
|
183
209
|
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
184
210
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
185
211
|
};
|
|
212
|
+
var dlqStreamName = (serviceName) => {
|
|
213
|
+
return `${internalName(serviceName)}_dlq-stream`;
|
|
214
|
+
};
|
|
186
215
|
var consumerName = (serviceName, kind) => {
|
|
187
216
|
if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
|
|
188
217
|
return `${internalName(serviceName)}_${kind}-consumer`;
|
|
@@ -197,12 +226,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
|
197
226
|
|
|
198
227
|
// src/client/jetstream.record.ts
|
|
199
228
|
var JetstreamRecord = class {
|
|
200
|
-
constructor(data, headers2, timeout, messageId, schedule) {
|
|
229
|
+
constructor(data, headers2, timeout, messageId, schedule, ttl) {
|
|
201
230
|
this.data = data;
|
|
202
231
|
this.headers = headers2;
|
|
203
232
|
this.timeout = timeout;
|
|
204
233
|
this.messageId = messageId;
|
|
205
234
|
this.schedule = schedule;
|
|
235
|
+
this.ttl = ttl;
|
|
206
236
|
}
|
|
207
237
|
};
|
|
208
238
|
var JetstreamRecordBuilder = class {
|
|
@@ -211,6 +241,7 @@ var JetstreamRecordBuilder = class {
|
|
|
211
241
|
timeout;
|
|
212
242
|
messageId;
|
|
213
243
|
scheduleOptions;
|
|
244
|
+
ttlDuration;
|
|
214
245
|
constructor(data) {
|
|
215
246
|
this.data = data;
|
|
216
247
|
}
|
|
@@ -302,6 +333,33 @@ var JetstreamRecordBuilder = class {
|
|
|
302
333
|
this.scheduleOptions = { at: new Date(ts) };
|
|
303
334
|
return this;
|
|
304
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Set per-message TTL (time-to-live).
|
|
338
|
+
*
|
|
339
|
+
* The message expires individually after the specified duration,
|
|
340
|
+
* independent of the stream's `max_age`. Requires NATS >= 2.11 and
|
|
341
|
+
* `allow_msg_ttl: true` on the stream.
|
|
342
|
+
*
|
|
343
|
+
* Only meaningful for events (`client.emit()`). If used with RPC
|
|
344
|
+
* (`client.send()`), a warning is logged and the TTL is ignored.
|
|
345
|
+
*
|
|
346
|
+
* @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* import { toNanos } from '@horizon-republic/nestjs-jetstream';
|
|
351
|
+
*
|
|
352
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
|
|
353
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
ttl(nanos) {
|
|
357
|
+
if (!Number.isFinite(nanos) || nanos <= 0) {
|
|
358
|
+
throw new Error("TTL must be a positive finite value");
|
|
359
|
+
}
|
|
360
|
+
this.ttlDuration = nanosToGoDuration(nanos);
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
305
363
|
/**
|
|
306
364
|
* Build the immutable {@link JetstreamRecord}.
|
|
307
365
|
*
|
|
@@ -314,7 +372,8 @@ var JetstreamRecordBuilder = class {
|
|
|
314
372
|
new Map(this.headers),
|
|
315
373
|
this.timeout,
|
|
316
374
|
this.messageId,
|
|
317
|
-
schedule
|
|
375
|
+
schedule,
|
|
376
|
+
this.ttlDuration
|
|
318
377
|
);
|
|
319
378
|
}
|
|
320
379
|
/** Validate that a header key is not reserved. */
|
|
@@ -326,8 +385,20 @@ var JetstreamRecordBuilder = class {
|
|
|
326
385
|
}
|
|
327
386
|
}
|
|
328
387
|
};
|
|
388
|
+
var NS_PER_MS = 1e6;
|
|
389
|
+
var NS_PER_S = 1e9;
|
|
390
|
+
var NS_PER_M = 60 * NS_PER_S;
|
|
391
|
+
var NS_PER_H = 60 * NS_PER_M;
|
|
392
|
+
var nanosToGoDuration = (nanos) => {
|
|
393
|
+
if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
|
|
394
|
+
if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
|
|
395
|
+
if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
|
|
396
|
+
if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
|
|
397
|
+
return `${nanos}ns`;
|
|
398
|
+
};
|
|
329
399
|
|
|
330
400
|
// src/client/jetstream.client.ts
|
|
401
|
+
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
331
402
|
var JetstreamClient = class extends ClientProxy {
|
|
332
403
|
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
333
404
|
super();
|
|
@@ -337,12 +408,33 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
337
408
|
this.eventBus = eventBus;
|
|
338
409
|
this.targetName = targetServiceName;
|
|
339
410
|
this.callerName = internalName(this.rootOptions.name);
|
|
411
|
+
const targetInternal = internalName(targetServiceName);
|
|
412
|
+
this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
|
|
413
|
+
this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
|
|
414
|
+
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
415
|
+
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
416
|
+
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
340
417
|
}
|
|
341
418
|
logger = new Logger("Jetstream:Client");
|
|
342
419
|
/** Target service name this client sends messages to. */
|
|
343
420
|
targetName;
|
|
344
421
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
345
422
|
callerName;
|
|
423
|
+
/**
|
|
424
|
+
* Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
|
|
425
|
+
* per stream kind this client may publish to. Built once in the constructor
|
|
426
|
+
* so producing a full subject is a single string concat with the user pattern.
|
|
427
|
+
*/
|
|
428
|
+
eventSubjectPrefix;
|
|
429
|
+
commandSubjectPrefix;
|
|
430
|
+
orderedSubjectPrefix;
|
|
431
|
+
/**
|
|
432
|
+
* RPC configuration snapshots. The values are derived from rootOptions at
|
|
433
|
+
* construction time so the publish hot path never has to re-run
|
|
434
|
+
* isCoreRpcMode / getRpcTimeout on every call.
|
|
435
|
+
*/
|
|
436
|
+
isCoreMode;
|
|
437
|
+
defaultRpcTimeout;
|
|
346
438
|
/** Shared inbox for JetStream-mode RPC responses. */
|
|
347
439
|
inbox = null;
|
|
348
440
|
inboxSubscription = null;
|
|
@@ -352,6 +444,12 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
352
444
|
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
353
445
|
/** Subscription to connection status events for disconnect handling. */
|
|
354
446
|
statusSubscription = null;
|
|
447
|
+
/**
|
|
448
|
+
* Cached readiness flag. Once `connect()` has wired the inbox and status
|
|
449
|
+
* subscription, subsequent publishes skip the `await connect()` microtask
|
|
450
|
+
* and reach for the underlying connection synchronously instead.
|
|
451
|
+
*/
|
|
452
|
+
readyForPublish = false;
|
|
355
453
|
/**
|
|
356
454
|
* Establish connection. Called automatically by NestJS on first use.
|
|
357
455
|
*
|
|
@@ -362,7 +460,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
362
460
|
*/
|
|
363
461
|
async connect() {
|
|
364
462
|
const nc = await this.connection.getConnection();
|
|
365
|
-
if (
|
|
463
|
+
if (!this.isCoreMode && !this.inboxSubscription) {
|
|
366
464
|
this.setupInbox(nc);
|
|
367
465
|
}
|
|
368
466
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
@@ -370,12 +468,14 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
370
468
|
this.handleDisconnect();
|
|
371
469
|
}
|
|
372
470
|
});
|
|
471
|
+
this.readyForPublish = true;
|
|
373
472
|
return nc;
|
|
374
473
|
}
|
|
375
474
|
/** Clean up resources: reject pending RPCs, unsubscribe from status events. */
|
|
376
475
|
async close() {
|
|
377
476
|
this.statusSubscription?.unsubscribe();
|
|
378
477
|
this.statusSubscription = null;
|
|
478
|
+
this.readyForPublish = false;
|
|
379
479
|
this.rejectPendingRpcs(new Error("Client closed"));
|
|
380
480
|
}
|
|
381
481
|
/**
|
|
@@ -399,8 +499,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
399
499
|
* set to the original event subject.
|
|
400
500
|
*/
|
|
401
501
|
async dispatchEvent(packet) {
|
|
402
|
-
await this.connect();
|
|
403
|
-
const { data, hdrs, messageId, schedule } = this.extractRecordData(packet.data);
|
|
502
|
+
if (!this.readyForPublish) await this.connect();
|
|
503
|
+
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
404
504
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
405
505
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
406
506
|
if (schedule) {
|
|
@@ -408,6 +508,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
408
508
|
const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
|
|
409
509
|
headers: msgHeaders,
|
|
410
510
|
msgID: messageId ?? nuid.next(),
|
|
511
|
+
ttl,
|
|
411
512
|
schedule: {
|
|
412
513
|
specification: schedule.at,
|
|
413
514
|
target: eventSubject
|
|
@@ -421,7 +522,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
421
522
|
} else {
|
|
422
523
|
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
423
524
|
headers: msgHeaders,
|
|
424
|
-
msgID: messageId ?? nuid.next()
|
|
525
|
+
msgID: messageId ?? nuid.next(),
|
|
526
|
+
ttl
|
|
425
527
|
});
|
|
426
528
|
if (ack.duplicate) {
|
|
427
529
|
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
@@ -436,19 +538,24 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
436
538
|
* JetStream mode: publishes to stream + waits for inbox response.
|
|
437
539
|
*/
|
|
438
540
|
publish(packet, callback) {
|
|
439
|
-
const subject =
|
|
440
|
-
const { data, hdrs, timeout, messageId, schedule } = this.extractRecordData(packet.data);
|
|
541
|
+
const subject = this.commandSubjectPrefix + packet.pattern;
|
|
542
|
+
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
441
543
|
if (schedule) {
|
|
442
544
|
this.logger.warn(
|
|
443
545
|
"scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
|
|
444
546
|
);
|
|
445
547
|
}
|
|
548
|
+
if (ttl) {
|
|
549
|
+
this.logger.warn(
|
|
550
|
+
"ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
|
|
551
|
+
);
|
|
552
|
+
}
|
|
446
553
|
const onUnhandled = (err) => {
|
|
447
554
|
this.logger.error("Unhandled publish error:", err);
|
|
448
555
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
449
556
|
};
|
|
450
557
|
let jetStreamCorrelationId = null;
|
|
451
|
-
if (
|
|
558
|
+
if (this.isCoreMode) {
|
|
452
559
|
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
|
|
453
560
|
} else {
|
|
454
561
|
jetStreamCorrelationId = nuid.next();
|
|
@@ -473,8 +580,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
473
580
|
/** Core mode: nc.request() with timeout. */
|
|
474
581
|
async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
|
|
475
582
|
try {
|
|
476
|
-
const nc = await this.connect();
|
|
477
|
-
const effectiveTimeout = timeout ?? this.
|
|
583
|
+
const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
|
|
584
|
+
const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
|
|
478
585
|
const hdrs = this.buildHeaders(customHeaders, { subject });
|
|
479
586
|
const response = await nc.request(subject, this.codec.encode(data), {
|
|
480
587
|
timeout: effectiveTimeout,
|
|
@@ -496,10 +603,10 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
496
603
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
497
604
|
async publishJetStreamRpc(subject, data, callback, options) {
|
|
498
605
|
const { headers: customHeaders, correlationId, messageId } = options;
|
|
499
|
-
const effectiveTimeout = options.timeout ?? this.
|
|
606
|
+
const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
|
|
500
607
|
this.pendingMessages.set(correlationId, callback);
|
|
501
608
|
try {
|
|
502
|
-
await this.connect();
|
|
609
|
+
if (!this.readyForPublish) await this.connect();
|
|
503
610
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
504
611
|
if (!this.inbox) {
|
|
505
612
|
this.pendingMessages.delete(correlationId);
|
|
@@ -545,6 +652,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
545
652
|
handleDisconnect() {
|
|
546
653
|
this.rejectPendingRpcs(new Error("Connection lost"));
|
|
547
654
|
this.inbox = null;
|
|
655
|
+
this.readyForPublish = false;
|
|
548
656
|
}
|
|
549
657
|
/** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
|
|
550
658
|
rejectPendingRpcs(error) {
|
|
@@ -558,6 +666,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
558
666
|
this.pendingTimeouts.clear();
|
|
559
667
|
this.inboxSubscription?.unsubscribe();
|
|
560
668
|
this.inboxSubscription = null;
|
|
669
|
+
this.inbox = null;
|
|
561
670
|
}
|
|
562
671
|
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
563
672
|
setupInbox(nc) {
|
|
@@ -607,19 +716,22 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
607
716
|
this.pendingMessages.delete(correlationId);
|
|
608
717
|
}
|
|
609
718
|
}
|
|
610
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* Resolve a user pattern to a fully-qualified NATS subject, dispatching
|
|
721
|
+
* between the event, broadcast, and ordered prefixes.
|
|
722
|
+
*
|
|
723
|
+
* The leading-char check short-circuits the `startsWith` comparisons for
|
|
724
|
+
* patterns that cannot possibly carry a broadcast/ordered marker, which is
|
|
725
|
+
* the overwhelmingly common case.
|
|
726
|
+
*/
|
|
611
727
|
buildEventSubject(pattern) {
|
|
612
|
-
if (pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
613
|
-
return
|
|
614
|
-
}
|
|
615
|
-
if (pattern.startsWith("ordered:" /* Ordered */)) {
|
|
616
|
-
return
|
|
617
|
-
this.targetName,
|
|
618
|
-
"ordered" /* Ordered */,
|
|
619
|
-
pattern.slice("ordered:" /* Ordered */.length)
|
|
620
|
-
);
|
|
728
|
+
if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
729
|
+
return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
|
|
730
|
+
}
|
|
731
|
+
if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
|
|
732
|
+
return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
|
|
621
733
|
}
|
|
622
|
-
return
|
|
734
|
+
return this.eventSubjectPrefix + pattern;
|
|
623
735
|
}
|
|
624
736
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
625
737
|
buildHeaders(customHeaders, transport) {
|
|
@@ -644,10 +756,11 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
644
756
|
if (rawData instanceof JetstreamRecord) {
|
|
645
757
|
return {
|
|
646
758
|
data: rawData.data,
|
|
647
|
-
hdrs: rawData.headers.size > 0 ?
|
|
759
|
+
hdrs: rawData.headers.size > 0 ? rawData.headers : null,
|
|
648
760
|
timeout: rawData.timeout,
|
|
649
761
|
messageId: rawData.messageId,
|
|
650
|
-
schedule: rawData.schedule
|
|
762
|
+
schedule: rawData.schedule,
|
|
763
|
+
ttl: rawData.ttl
|
|
651
764
|
};
|
|
652
765
|
}
|
|
653
766
|
return {
|
|
@@ -655,7 +768,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
655
768
|
hdrs: null,
|
|
656
769
|
timeout: void 0,
|
|
657
770
|
messageId: void 0,
|
|
658
|
-
schedule: void 0
|
|
771
|
+
schedule: void 0,
|
|
772
|
+
ttl: void 0
|
|
659
773
|
};
|
|
660
774
|
}
|
|
661
775
|
/**
|
|
@@ -685,11 +799,6 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
685
799
|
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
686
800
|
return `${targetPrefix}_sch.${pattern}`;
|
|
687
801
|
}
|
|
688
|
-
getRpcTimeout() {
|
|
689
|
-
if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
|
|
690
|
-
const defaultTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? DEFAULT_JETSTREAM_RPC_TIMEOUT : DEFAULT_RPC_TIMEOUT;
|
|
691
|
-
return this.rootOptions.rpc.timeout ?? defaultTimeout;
|
|
692
|
-
}
|
|
693
802
|
};
|
|
694
803
|
|
|
695
804
|
// src/codec/json.codec.ts
|
|
@@ -704,6 +813,19 @@ var JsonCodec = class {
|
|
|
704
813
|
}
|
|
705
814
|
};
|
|
706
815
|
|
|
816
|
+
// src/codec/msgpack.codec.ts
|
|
817
|
+
var MsgpackCodec = class {
|
|
818
|
+
constructor(packr) {
|
|
819
|
+
this.packr = packr;
|
|
820
|
+
}
|
|
821
|
+
encode(data) {
|
|
822
|
+
return this.packr.pack(data);
|
|
823
|
+
}
|
|
824
|
+
decode(data) {
|
|
825
|
+
return this.packr.unpack(data);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
707
829
|
// src/connection/connection.provider.ts
|
|
708
830
|
import { Logger as Logger2 } from "@nestjs/common";
|
|
709
831
|
import {
|
|
@@ -921,6 +1043,15 @@ var EventBus = class {
|
|
|
921
1043
|
kind
|
|
922
1044
|
);
|
|
923
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Check whether a hook is registered for the given event.
|
|
1048
|
+
*
|
|
1049
|
+
* Used by the routing hot path to elide the emit call entirely when the
|
|
1050
|
+
* transport owner did not register a listener.
|
|
1051
|
+
*/
|
|
1052
|
+
hasHook(event) {
|
|
1053
|
+
return this.hooks[event] !== void 0;
|
|
1054
|
+
}
|
|
924
1055
|
callHook(event, hook, ...args) {
|
|
925
1056
|
try {
|
|
926
1057
|
const result = hook(...args);
|
|
@@ -975,9 +1106,14 @@ var JetstreamHealthIndicator = class {
|
|
|
975
1106
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
976
1107
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
977
1108
|
*
|
|
1109
|
+
* The thrown error sets `isHealthCheckError: true` and `causes` — the
|
|
1110
|
+
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
1111
|
+
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
1112
|
+
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
1113
|
+
*
|
|
978
1114
|
* @param key - Health indicator key (default: `'jetstream'`).
|
|
979
1115
|
* @returns Object with status, server, and latency under the given key.
|
|
980
|
-
* @throws Error with `{ [key]: { status: 'down' } }
|
|
1116
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
981
1117
|
*/
|
|
982
1118
|
async isHealthy(key = "jetstream") {
|
|
983
1119
|
const status = await this.check();
|
|
@@ -987,8 +1123,10 @@ var JetstreamHealthIndicator = class {
|
|
|
987
1123
|
latency: status.latency
|
|
988
1124
|
};
|
|
989
1125
|
if (!status.connected) {
|
|
1126
|
+
const causes = { [key]: details };
|
|
990
1127
|
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
991
|
-
|
|
1128
|
+
causes,
|
|
1129
|
+
isHealthCheckError: true
|
|
992
1130
|
});
|
|
993
1131
|
}
|
|
994
1132
|
return { [key]: details };
|
|
@@ -1001,7 +1139,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
1001
1139
|
// src/server/strategy.ts
|
|
1002
1140
|
import { Server } from "@nestjs/microservices";
|
|
1003
1141
|
var JetstreamStrategy = class extends Server {
|
|
1004
|
-
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
|
|
1142
|
+
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
|
|
1005
1143
|
super();
|
|
1006
1144
|
this.options = options;
|
|
1007
1145
|
this.connection = connection;
|
|
@@ -1013,6 +1151,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
1013
1151
|
this.rpcRouter = rpcRouter;
|
|
1014
1152
|
this.coreRpcServer = coreRpcServer;
|
|
1015
1153
|
this.ackWaitMap = ackWaitMap;
|
|
1154
|
+
this.metadataProvider = metadataProvider;
|
|
1016
1155
|
}
|
|
1017
1156
|
transportId = /* @__PURE__ */ Symbol("jetstream-transport");
|
|
1018
1157
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -1057,10 +1196,14 @@ var JetstreamStrategy = class extends Server {
|
|
|
1057
1196
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
1058
1197
|
await this.coreRpcServer.start();
|
|
1059
1198
|
}
|
|
1199
|
+
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
1200
|
+
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
1201
|
+
}
|
|
1060
1202
|
callback();
|
|
1061
1203
|
}
|
|
1062
|
-
/** Stop all consumers, routers, and
|
|
1204
|
+
/** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
|
|
1063
1205
|
close() {
|
|
1206
|
+
this.metadataProvider?.destroy();
|
|
1064
1207
|
this.eventRouter.destroy();
|
|
1065
1208
|
this.rpcRouter.destroy();
|
|
1066
1209
|
this.coreRpcServer.stop();
|
|
@@ -1291,16 +1434,71 @@ var resolveAckExtensionInterval = (config, ackWaitNanos) => {
|
|
|
1291
1434
|
const interval = Math.floor(ackWaitNanos / 1e6 / 2);
|
|
1292
1435
|
return Math.max(interval, MIN_ACK_EXTENSION_INTERVAL);
|
|
1293
1436
|
};
|
|
1437
|
+
var AckExtensionPool = class {
|
|
1438
|
+
entries = /* @__PURE__ */ new Set();
|
|
1439
|
+
handle = null;
|
|
1440
|
+
handleFireAt = 0;
|
|
1441
|
+
schedule(msg, interval) {
|
|
1442
|
+
const entry = {
|
|
1443
|
+
msg,
|
|
1444
|
+
interval,
|
|
1445
|
+
nextFireAt: Date.now() + interval,
|
|
1446
|
+
active: true
|
|
1447
|
+
};
|
|
1448
|
+
this.entries.add(entry);
|
|
1449
|
+
this.ensureWake(entry.nextFireAt);
|
|
1450
|
+
return entry;
|
|
1451
|
+
}
|
|
1452
|
+
cancel(entry) {
|
|
1453
|
+
if (!entry.active) return;
|
|
1454
|
+
entry.active = false;
|
|
1455
|
+
this.entries.delete(entry);
|
|
1456
|
+
if (this.entries.size === 0 && this.handle !== null) {
|
|
1457
|
+
clearTimeout(this.handle);
|
|
1458
|
+
this.handle = null;
|
|
1459
|
+
this.handleFireAt = 0;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Ensure the shared timer will fire no later than `dueAt`. If an earlier
|
|
1464
|
+
* wake is already scheduled, leave it; otherwise replace it with a tighter one.
|
|
1465
|
+
*/
|
|
1466
|
+
ensureWake(dueAt) {
|
|
1467
|
+
if (this.handle !== null && this.handleFireAt <= dueAt) return;
|
|
1468
|
+
if (this.handle !== null) clearTimeout(this.handle);
|
|
1469
|
+
const delay = Math.max(0, dueAt - Date.now());
|
|
1470
|
+
const handle = setTimeout(() => {
|
|
1471
|
+
this.tick();
|
|
1472
|
+
}, delay);
|
|
1473
|
+
if (typeof handle.unref === "function") handle.unref();
|
|
1474
|
+
this.handle = handle;
|
|
1475
|
+
this.handleFireAt = dueAt;
|
|
1476
|
+
}
|
|
1477
|
+
tick() {
|
|
1478
|
+
this.handle = null;
|
|
1479
|
+
this.handleFireAt = 0;
|
|
1480
|
+
const now = Date.now();
|
|
1481
|
+
let earliest = Infinity;
|
|
1482
|
+
for (const entry of this.entries) {
|
|
1483
|
+
if (!entry.active) continue;
|
|
1484
|
+
if (entry.nextFireAt <= now) {
|
|
1485
|
+
try {
|
|
1486
|
+
entry.msg.working();
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
entry.nextFireAt = now + entry.interval;
|
|
1490
|
+
}
|
|
1491
|
+
if (entry.nextFireAt < earliest) earliest = entry.nextFireAt;
|
|
1492
|
+
}
|
|
1493
|
+
if (earliest !== Infinity) this.ensureWake(earliest);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
var pool = new AckExtensionPool();
|
|
1294
1497
|
var startAckExtensionTimer = (msg, interval) => {
|
|
1295
1498
|
if (interval === null || interval <= 0) return null;
|
|
1296
|
-
const
|
|
1297
|
-
try {
|
|
1298
|
-
msg.working();
|
|
1299
|
-
} catch {
|
|
1300
|
-
}
|
|
1301
|
-
}, interval);
|
|
1499
|
+
const entry = pool.schedule(msg, interval);
|
|
1302
1500
|
return () => {
|
|
1303
|
-
|
|
1501
|
+
pool.cancel(entry);
|
|
1304
1502
|
};
|
|
1305
1503
|
};
|
|
1306
1504
|
|
|
@@ -1314,11 +1512,8 @@ var serializeError = (err) => {
|
|
|
1314
1512
|
|
|
1315
1513
|
// src/utils/unwrap-result.ts
|
|
1316
1514
|
import { isObservable } from "rxjs";
|
|
1317
|
-
var RESOLVED_VOID = Promise.resolve(void 0);
|
|
1318
|
-
var RESOLVED_NULL = Promise.resolve(null);
|
|
1319
1515
|
var unwrapResult = (result) => {
|
|
1320
|
-
if (result === void 0) return
|
|
1321
|
-
if (result === null) return RESOLVED_NULL;
|
|
1516
|
+
if (result === void 0 || result === null) return result;
|
|
1322
1517
|
if (isObservable(result)) {
|
|
1323
1518
|
return subscribeToFirst(result);
|
|
1324
1519
|
}
|
|
@@ -1327,8 +1522,9 @@ var unwrapResult = (result) => {
|
|
|
1327
1522
|
(resolved) => isObservable(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1328
1523
|
);
|
|
1329
1524
|
}
|
|
1330
|
-
return
|
|
1525
|
+
return result;
|
|
1331
1526
|
};
|
|
1527
|
+
var isPromiseLike = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
1332
1528
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1333
1529
|
let done = false;
|
|
1334
1530
|
let subscription = null;
|
|
@@ -1416,7 +1612,8 @@ var CoreRpcServer = class {
|
|
|
1416
1612
|
}
|
|
1417
1613
|
const ctx = new RpcContext([msg]);
|
|
1418
1614
|
try {
|
|
1419
|
-
const
|
|
1615
|
+
const raw = unwrapResult(handler(data, ctx));
|
|
1616
|
+
const result = isPromiseLike(raw) ? await raw : raw;
|
|
1420
1617
|
msg.respond(this.codec.encode(result));
|
|
1421
1618
|
} catch (err) {
|
|
1422
1619
|
this.logger.error(`Handler error for Core RPC ${msg.subject}:`, err);
|
|
@@ -1436,24 +1633,180 @@ var CoreRpcServer = class {
|
|
|
1436
1633
|
};
|
|
1437
1634
|
|
|
1438
1635
|
// src/server/infrastructure/stream.provider.ts
|
|
1636
|
+
import { Logger as Logger6 } from "@nestjs/common";
|
|
1637
|
+
import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
|
|
1638
|
+
|
|
1639
|
+
// src/server/infrastructure/nats-error-codes.ts
|
|
1640
|
+
var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
1641
|
+
NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
|
|
1642
|
+
NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
|
|
1643
|
+
NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
|
|
1644
|
+
return NatsErrorCode2;
|
|
1645
|
+
})(NatsErrorCode || {});
|
|
1646
|
+
|
|
1647
|
+
// src/server/infrastructure/stream-config-diff.ts
|
|
1648
|
+
var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1649
|
+
"retention"
|
|
1650
|
+
]);
|
|
1651
|
+
var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1652
|
+
"storage"
|
|
1653
|
+
]);
|
|
1654
|
+
var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1655
|
+
"allow_msg_schedules",
|
|
1656
|
+
"allow_msg_ttl",
|
|
1657
|
+
"deny_delete",
|
|
1658
|
+
"deny_purge"
|
|
1659
|
+
]);
|
|
1660
|
+
var compareStreamConfig = (current, desired) => {
|
|
1661
|
+
const changes = [];
|
|
1662
|
+
for (const key of Object.keys(desired)) {
|
|
1663
|
+
const currentVal = current[key];
|
|
1664
|
+
const desiredVal = desired[key];
|
|
1665
|
+
if (isEqual(currentVal, desiredVal)) continue;
|
|
1666
|
+
changes.push({
|
|
1667
|
+
property: key,
|
|
1668
|
+
current: currentVal,
|
|
1669
|
+
desired: desiredVal,
|
|
1670
|
+
mutability: classifyMutability(key, currentVal, desiredVal)
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
|
|
1674
|
+
const hasMutableChanges = changes.some(
|
|
1675
|
+
(c) => c.mutability === "mutable" || c.mutability === "enable-only"
|
|
1676
|
+
);
|
|
1677
|
+
const hasTransportControlledConflicts = changes.some(
|
|
1678
|
+
(c) => c.mutability === "transport-controlled"
|
|
1679
|
+
);
|
|
1680
|
+
return {
|
|
1681
|
+
hasChanges: changes.length > 0,
|
|
1682
|
+
hasMutableChanges,
|
|
1683
|
+
hasImmutableChanges,
|
|
1684
|
+
hasTransportControlledConflicts,
|
|
1685
|
+
changes
|
|
1686
|
+
};
|
|
1687
|
+
};
|
|
1688
|
+
var classifyMutability = (key, current, desired) => {
|
|
1689
|
+
if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
|
|
1690
|
+
if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
|
|
1691
|
+
if (ENABLE_ONLY_PROPERTIES.has(key)) {
|
|
1692
|
+
return current === true && desired === false ? "immutable" : "enable-only";
|
|
1693
|
+
}
|
|
1694
|
+
return "mutable";
|
|
1695
|
+
};
|
|
1696
|
+
var isEqual = (a, b) => {
|
|
1697
|
+
if (a === b) return true;
|
|
1698
|
+
if (a == null && b == null) return true;
|
|
1699
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
// src/server/infrastructure/stream-migration.ts
|
|
1439
1703
|
import { Logger as Logger5 } from "@nestjs/common";
|
|
1440
1704
|
import { JetStreamApiError } from "@nats-io/jetstream";
|
|
1441
|
-
var
|
|
1705
|
+
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
1706
|
+
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
1707
|
+
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
1708
|
+
var StreamMigration = class {
|
|
1709
|
+
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
1710
|
+
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
1711
|
+
}
|
|
1712
|
+
logger = new Logger5("Jetstream:Stream");
|
|
1713
|
+
async migrate(jsm, streamName2, newConfig) {
|
|
1714
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1715
|
+
const startTime = Date.now();
|
|
1716
|
+
const currentInfo = await jsm.streams.info(streamName2);
|
|
1717
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1718
|
+
const messageCount = currentInfo.state.messages;
|
|
1719
|
+
this.logger.log(`Stream ${streamName2}: destructive migration started`);
|
|
1720
|
+
let originalDeleted = false;
|
|
1721
|
+
try {
|
|
1722
|
+
if (messageCount > 0) {
|
|
1723
|
+
this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
|
|
1724
|
+
await jsm.streams.add({
|
|
1725
|
+
...currentInfo.config,
|
|
1726
|
+
name: backupName,
|
|
1727
|
+
subjects: [],
|
|
1728
|
+
sources: [{ name: streamName2 }]
|
|
1729
|
+
});
|
|
1730
|
+
await this.waitForSourcing(jsm, backupName, messageCount);
|
|
1731
|
+
}
|
|
1732
|
+
this.logger.log(` Phase 2/4: Deleting old stream`);
|
|
1733
|
+
await jsm.streams.delete(streamName2);
|
|
1734
|
+
originalDeleted = true;
|
|
1735
|
+
this.logger.log(` Phase 3/4: Creating stream with new config`);
|
|
1736
|
+
await jsm.streams.add(newConfig);
|
|
1737
|
+
if (messageCount > 0) {
|
|
1738
|
+
const backupInfo = await jsm.streams.info(backupName);
|
|
1739
|
+
await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
|
|
1740
|
+
this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
|
|
1741
|
+
await jsm.streams.update(streamName2, {
|
|
1742
|
+
...newConfig,
|
|
1743
|
+
sources: [{ name: backupName }]
|
|
1744
|
+
});
|
|
1745
|
+
await this.waitForSourcing(jsm, streamName2, messageCount);
|
|
1746
|
+
await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
|
|
1747
|
+
await jsm.streams.delete(backupName);
|
|
1748
|
+
}
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
if (originalDeleted && messageCount > 0) {
|
|
1751
|
+
this.logger.error(
|
|
1752
|
+
`Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
|
|
1753
|
+
);
|
|
1754
|
+
} else {
|
|
1755
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1756
|
+
}
|
|
1757
|
+
throw err;
|
|
1758
|
+
}
|
|
1759
|
+
const durationMs = Date.now() - startTime;
|
|
1760
|
+
this.logger.log(
|
|
1761
|
+
`Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
async waitForSourcing(jsm, streamName2, expectedCount) {
|
|
1765
|
+
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
1766
|
+
while (Date.now() < deadline) {
|
|
1767
|
+
const info = await jsm.streams.info(streamName2);
|
|
1768
|
+
if (info.state.messages >= expectedCount) return;
|
|
1769
|
+
await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
|
|
1770
|
+
}
|
|
1771
|
+
throw new Error(
|
|
1772
|
+
`Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
async cleanupOrphanedBackup(jsm, backupName) {
|
|
1776
|
+
try {
|
|
1777
|
+
await jsm.streams.info(backupName);
|
|
1778
|
+
this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
|
|
1779
|
+
await jsm.streams.delete(backupName);
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
if (err instanceof JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
throw err;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
// src/server/infrastructure/stream.provider.ts
|
|
1442
1790
|
var StreamProvider = class {
|
|
1443
1791
|
constructor(options, connection) {
|
|
1444
1792
|
this.options = options;
|
|
1445
1793
|
this.connection = connection;
|
|
1446
1794
|
}
|
|
1447
|
-
logger = new
|
|
1795
|
+
logger = new Logger6("Jetstream:Stream");
|
|
1796
|
+
migration = new StreamMigration();
|
|
1448
1797
|
/**
|
|
1449
1798
|
* Ensure all required streams exist with correct configuration.
|
|
1450
1799
|
*
|
|
1451
1800
|
* @param kinds Which stream kinds to create. Determined by the module based
|
|
1452
1801
|
* on RPC mode and registered handler patterns.
|
|
1802
|
+
* If the dlq option is enabled, also ensures the DLQ stream exists.
|
|
1453
1803
|
*/
|
|
1454
1804
|
async ensureStreams(kinds) {
|
|
1455
1805
|
const jsm = await this.connection.getJetStreamManager();
|
|
1456
1806
|
await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
|
|
1807
|
+
if (this.options.dlq) {
|
|
1808
|
+
await this.ensureDlqStream(jsm);
|
|
1809
|
+
}
|
|
1457
1810
|
}
|
|
1458
1811
|
/** Get the stream name for a given kind. */
|
|
1459
1812
|
getStreamName(kind) {
|
|
@@ -1488,17 +1841,85 @@ var StreamProvider = class {
|
|
|
1488
1841
|
const config = this.buildConfig(kind);
|
|
1489
1842
|
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
1490
1843
|
try {
|
|
1491
|
-
await jsm.streams.info(config.name);
|
|
1492
|
-
this.
|
|
1493
|
-
return await jsm.streams.update(config.name, config);
|
|
1844
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1845
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1494
1846
|
} catch (err) {
|
|
1495
|
-
if (err instanceof
|
|
1847
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1496
1848
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
1497
1849
|
return await jsm.streams.add(config);
|
|
1498
1850
|
}
|
|
1499
1851
|
throw err;
|
|
1500
1852
|
}
|
|
1501
1853
|
}
|
|
1854
|
+
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
1855
|
+
async ensureDlqStream(jsm) {
|
|
1856
|
+
const config = this.buildDlqConfig();
|
|
1857
|
+
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
1858
|
+
try {
|
|
1859
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1860
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1863
|
+
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
1864
|
+
return await jsm.streams.add(config);
|
|
1865
|
+
}
|
|
1866
|
+
throw err;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async handleExistingStream(jsm, currentInfo, config) {
|
|
1870
|
+
const diff = compareStreamConfig(currentInfo.config, config);
|
|
1871
|
+
if (!diff.hasChanges) {
|
|
1872
|
+
this.logger.debug(`Stream ${config.name}: no config changes`);
|
|
1873
|
+
return currentInfo;
|
|
1874
|
+
}
|
|
1875
|
+
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
1876
|
+
if (diff.hasTransportControlledConflicts) {
|
|
1877
|
+
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
`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.`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
if (!diff.hasImmutableChanges) {
|
|
1883
|
+
this.logger.debug(`Stream exists, updating: ${config.name}`);
|
|
1884
|
+
return await jsm.streams.update(config.name, config);
|
|
1885
|
+
}
|
|
1886
|
+
if (!this.options.allowDestructiveMigration) {
|
|
1887
|
+
this.logger.warn(
|
|
1888
|
+
`Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
|
|
1889
|
+
);
|
|
1890
|
+
if (diff.hasMutableChanges) {
|
|
1891
|
+
const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
|
|
1892
|
+
return await jsm.streams.update(config.name, mutableConfig);
|
|
1893
|
+
}
|
|
1894
|
+
return currentInfo;
|
|
1895
|
+
}
|
|
1896
|
+
await this.migration.migrate(jsm, config.name, config);
|
|
1897
|
+
return await jsm.streams.info(config.name);
|
|
1898
|
+
}
|
|
1899
|
+
buildMutableOnlyConfig(config, currentConfig, diff) {
|
|
1900
|
+
const nonMutableKeys = new Set(
|
|
1901
|
+
diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
|
|
1902
|
+
);
|
|
1903
|
+
const filtered = { ...config };
|
|
1904
|
+
for (const key of nonMutableKeys) {
|
|
1905
|
+
filtered[key] = currentConfig[key];
|
|
1906
|
+
}
|
|
1907
|
+
return filtered;
|
|
1908
|
+
}
|
|
1909
|
+
logChanges(streamName2, diff, migrationEnabled) {
|
|
1910
|
+
for (const c of diff.changes) {
|
|
1911
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
|
|
1912
|
+
if (c.mutability === "transport-controlled") {
|
|
1913
|
+
this.logger.error(
|
|
1914
|
+
`Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
|
|
1915
|
+
);
|
|
1916
|
+
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
1917
|
+
this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
|
|
1918
|
+
} else {
|
|
1919
|
+
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1502
1923
|
/** Build the full stream config by merging defaults with user overrides. */
|
|
1503
1924
|
buildConfig(kind) {
|
|
1504
1925
|
const name = this.getStreamName(kind);
|
|
@@ -1514,6 +1935,26 @@ var StreamProvider = class {
|
|
|
1514
1935
|
description
|
|
1515
1936
|
};
|
|
1516
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Build the stream configuration for the Dead-Letter Queue (DLQ).
|
|
1940
|
+
*
|
|
1941
|
+
* Merges the library default DLQ config with user-provided overrides.
|
|
1942
|
+
* Ensures transport-controlled settings like retention are safely decoupled.
|
|
1943
|
+
*/
|
|
1944
|
+
buildDlqConfig() {
|
|
1945
|
+
const name = dlqStreamName(this.options.name);
|
|
1946
|
+
const subjects = [name];
|
|
1947
|
+
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
1948
|
+
const overrides = this.options.dlq?.stream ?? {};
|
|
1949
|
+
const safeOverrides = this.stripTransportControlled(overrides);
|
|
1950
|
+
return {
|
|
1951
|
+
...DEFAULT_DLQ_STREAM_CONFIG,
|
|
1952
|
+
...safeOverrides,
|
|
1953
|
+
name,
|
|
1954
|
+
subjects,
|
|
1955
|
+
description
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1517
1958
|
/** Get default config for a stream kind. */
|
|
1518
1959
|
getDefaults(kind) {
|
|
1519
1960
|
switch (kind) {
|
|
@@ -1532,25 +1973,44 @@ var StreamProvider = class {
|
|
|
1532
1973
|
const overrides = this.getOverrides(kind);
|
|
1533
1974
|
return overrides.allow_msg_schedules === true;
|
|
1534
1975
|
}
|
|
1535
|
-
/** Get user-provided overrides for a stream kind. */
|
|
1976
|
+
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
1536
1977
|
getOverrides(kind) {
|
|
1978
|
+
let overrides;
|
|
1537
1979
|
switch (kind) {
|
|
1538
1980
|
case "ev" /* Event */:
|
|
1539
|
-
|
|
1981
|
+
overrides = this.options.events?.stream ?? {};
|
|
1982
|
+
break;
|
|
1540
1983
|
case "cmd" /* Command */:
|
|
1541
|
-
|
|
1984
|
+
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1985
|
+
break;
|
|
1542
1986
|
case "broadcast" /* Broadcast */:
|
|
1543
|
-
|
|
1987
|
+
overrides = this.options.broadcast?.stream ?? {};
|
|
1988
|
+
break;
|
|
1544
1989
|
case "ordered" /* Ordered */:
|
|
1545
|
-
|
|
1990
|
+
overrides = this.options.ordered?.stream ?? {};
|
|
1991
|
+
break;
|
|
1546
1992
|
}
|
|
1993
|
+
return this.stripTransportControlled(overrides);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Remove transport-controlled properties from user overrides.
|
|
1997
|
+
* `retention` is managed by the transport (Workqueue/Limits per stream kind)
|
|
1998
|
+
* and silently stripped to protect users from misconfiguration.
|
|
1999
|
+
*/
|
|
2000
|
+
stripTransportControlled(overrides) {
|
|
2001
|
+
if (!("retention" in overrides)) return overrides;
|
|
2002
|
+
this.logger.debug(
|
|
2003
|
+
"Stripping user-provided retention override \u2014 retention is managed by the transport"
|
|
2004
|
+
);
|
|
2005
|
+
const cleaned = { ...overrides };
|
|
2006
|
+
delete cleaned.retention;
|
|
2007
|
+
return cleaned;
|
|
1547
2008
|
}
|
|
1548
2009
|
};
|
|
1549
2010
|
|
|
1550
2011
|
// src/server/infrastructure/consumer.provider.ts
|
|
1551
|
-
import { Logger as
|
|
1552
|
-
import { JetStreamApiError as
|
|
1553
|
-
var CONSUMER_NOT_FOUND = 10014;
|
|
2012
|
+
import { Logger as Logger7 } from "@nestjs/common";
|
|
2013
|
+
import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
|
|
1554
2014
|
var ConsumerProvider = class {
|
|
1555
2015
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
1556
2016
|
this.options = options;
|
|
@@ -1558,7 +2018,7 @@ var ConsumerProvider = class {
|
|
|
1558
2018
|
this.streamProvider = streamProvider;
|
|
1559
2019
|
this.patternRegistry = patternRegistry;
|
|
1560
2020
|
}
|
|
1561
|
-
logger = new
|
|
2021
|
+
logger = new Logger7("Jetstream:Consumer");
|
|
1562
2022
|
/**
|
|
1563
2023
|
* Ensure consumers exist for the specified kinds.
|
|
1564
2024
|
*
|
|
@@ -1579,7 +2039,11 @@ var ConsumerProvider = class {
|
|
|
1579
2039
|
getConsumerName(kind) {
|
|
1580
2040
|
return consumerName(this.options.name, kind);
|
|
1581
2041
|
}
|
|
1582
|
-
/**
|
|
2042
|
+
/**
|
|
2043
|
+
* Ensure a single consumer exists with the desired config.
|
|
2044
|
+
* Used at **startup** — creates or updates the consumer to match
|
|
2045
|
+
* the current pod's configuration.
|
|
2046
|
+
*/
|
|
1583
2047
|
async ensureConsumer(jsm, kind) {
|
|
1584
2048
|
const stream = this.streamProvider.getStreamName(kind);
|
|
1585
2049
|
const config = this.buildConfig(kind);
|
|
@@ -1590,13 +2054,74 @@ var ConsumerProvider = class {
|
|
|
1590
2054
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
1591
2055
|
return await jsm.consumers.update(stream, name, config);
|
|
1592
2056
|
} catch (err) {
|
|
1593
|
-
if (err instanceof
|
|
1594
|
-
|
|
1595
|
-
|
|
2057
|
+
if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
2058
|
+
throw err;
|
|
2059
|
+
}
|
|
2060
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Recover a consumer that disappeared during runtime.
|
|
2065
|
+
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
2066
|
+
*
|
|
2067
|
+
* If a migration backup stream exists, another pod is mid-migration — we
|
|
2068
|
+
* throw so the self-healing retry loop waits with backoff until migration
|
|
2069
|
+
* completes and the backup is cleaned up.
|
|
2070
|
+
*
|
|
2071
|
+
* This prevents old pods from:
|
|
2072
|
+
* - Overwriting a newer pod's consumer config during rolling updates
|
|
2073
|
+
* - Creating consumers during migration (which would consume and delete
|
|
2074
|
+
* workqueue messages while they're being restored)
|
|
2075
|
+
*/
|
|
2076
|
+
async recoverConsumer(jsm, kind) {
|
|
2077
|
+
const stream = this.streamProvider.getStreamName(kind);
|
|
2078
|
+
const config = this.buildConfig(kind);
|
|
2079
|
+
const name = config.durable_name;
|
|
2080
|
+
this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
|
|
2081
|
+
await this.assertNoMigrationInProgress(jsm, stream);
|
|
2082
|
+
try {
|
|
2083
|
+
return await jsm.consumers.info(stream, name);
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
2086
|
+
throw err;
|
|
2087
|
+
}
|
|
2088
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Throw if a migration backup stream exists for this stream.
|
|
2093
|
+
* The self-healing retry loop catches the error and retries with backoff,
|
|
2094
|
+
* naturally waiting until the migrating pod finishes and cleans up the backup.
|
|
2095
|
+
*/
|
|
2096
|
+
async assertNoMigrationInProgress(jsm, stream) {
|
|
2097
|
+
const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
|
|
2098
|
+
try {
|
|
2099
|
+
await jsm.streams.info(backupName);
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
2102
|
+
);
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
if (err instanceof JetStreamApiError3 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
2105
|
+
return;
|
|
1596
2106
|
}
|
|
1597
2107
|
throw err;
|
|
1598
2108
|
}
|
|
1599
2109
|
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Create a consumer, handling the race where another pod creates it first.
|
|
2112
|
+
*/
|
|
2113
|
+
async createConsumer(jsm, stream, name, config) {
|
|
2114
|
+
this.logger.log(`Creating consumer: ${name}`);
|
|
2115
|
+
try {
|
|
2116
|
+
return await jsm.consumers.add(stream, config);
|
|
2117
|
+
} catch (addErr) {
|
|
2118
|
+
if (addErr instanceof JetStreamApiError3 && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
2119
|
+
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
2120
|
+
return await jsm.consumers.info(stream, name);
|
|
2121
|
+
}
|
|
2122
|
+
throw addErr;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
1600
2125
|
/** Build consumer config by merging defaults with user overrides. */
|
|
1601
2126
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
1602
2127
|
buildConfig(kind) {
|
|
@@ -1649,6 +2174,7 @@ var ConsumerProvider = class {
|
|
|
1649
2174
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1650
2175
|
case "ordered" /* Ordered */:
|
|
1651
2176
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2177
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1652
2178
|
default: {
|
|
1653
2179
|
const _exhaustive = kind;
|
|
1654
2180
|
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
@@ -1666,6 +2192,7 @@ var ConsumerProvider = class {
|
|
|
1666
2192
|
return this.options.broadcast?.consumer ?? {};
|
|
1667
2193
|
case "ordered" /* Ordered */:
|
|
1668
2194
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2195
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1669
2196
|
default: {
|
|
1670
2197
|
const _exhaustive = kind;
|
|
1671
2198
|
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
@@ -1675,7 +2202,7 @@ var ConsumerProvider = class {
|
|
|
1675
2202
|
};
|
|
1676
2203
|
|
|
1677
2204
|
// src/server/infrastructure/message.provider.ts
|
|
1678
|
-
import { Logger as
|
|
2205
|
+
import { Logger as Logger8 } from "@nestjs/common";
|
|
1679
2206
|
import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
|
|
1680
2207
|
import {
|
|
1681
2208
|
catchError,
|
|
@@ -1689,12 +2216,13 @@ import {
|
|
|
1689
2216
|
timer
|
|
1690
2217
|
} from "rxjs";
|
|
1691
2218
|
var MessageProvider = class {
|
|
1692
|
-
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
|
|
2219
|
+
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
1693
2220
|
this.connection = connection;
|
|
1694
2221
|
this.eventBus = eventBus;
|
|
1695
2222
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2223
|
+
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
1696
2224
|
}
|
|
1697
|
-
logger = new
|
|
2225
|
+
logger = new Logger8("Jetstream:Message");
|
|
1698
2226
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1699
2227
|
orderedReadyResolve = null;
|
|
1700
2228
|
orderedReadyReject = null;
|
|
@@ -1797,20 +2325,49 @@ var MessageProvider = class {
|
|
|
1797
2325
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
1798
2326
|
async consumeOnce(kind, info, target$) {
|
|
1799
2327
|
const js = this.connection.getJetStreamClient();
|
|
1800
|
-
|
|
2328
|
+
let consumer;
|
|
2329
|
+
let consumerName2 = info.name;
|
|
2330
|
+
try {
|
|
2331
|
+
consumer = await js.consumers.get(info.stream_name, info.name);
|
|
2332
|
+
} catch (err) {
|
|
2333
|
+
if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
|
|
2334
|
+
this.logger.warn(`Consumer ${info.name} not found, recreating...`);
|
|
2335
|
+
const recovered = await this.consumerRecoveryFn(kind);
|
|
2336
|
+
consumerName2 = recovered.name;
|
|
2337
|
+
this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
|
|
2338
|
+
consumer = await js.consumers.get(recovered.stream_name, consumerName2);
|
|
2339
|
+
} else {
|
|
2340
|
+
throw err;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
1801
2343
|
const defaults = { idle_heartbeat: 5e3 };
|
|
1802
2344
|
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
1803
|
-
const messages = await consumer.consume({
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
for await (const msg of messages) {
|
|
2345
|
+
const messages = await consumer.consume({
|
|
2346
|
+
...defaults,
|
|
2347
|
+
...userOptions,
|
|
2348
|
+
callback: (msg) => {
|
|
1808
2349
|
target$.next(msg);
|
|
1809
2350
|
}
|
|
2351
|
+
});
|
|
2352
|
+
this.activeIterators.add(messages);
|
|
2353
|
+
this.monitorConsumerHealth(messages, consumerName2);
|
|
2354
|
+
try {
|
|
2355
|
+
await messages.closed();
|
|
1810
2356
|
} finally {
|
|
1811
2357
|
this.activeIterators.delete(messages);
|
|
1812
2358
|
}
|
|
1813
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Detect "consumer not found" errors from `js.consumers.get()`.
|
|
2362
|
+
*
|
|
2363
|
+
* Unlike JetStream Manager calls (which throw `JetStreamApiError`),
|
|
2364
|
+
* the JetStream client's `consumers.get()` throws a plain `Error`
|
|
2365
|
+
* with the error code embedded in the message text.
|
|
2366
|
+
*/
|
|
2367
|
+
isConsumerNotFound(err) {
|
|
2368
|
+
if (!(err instanceof Error)) return false;
|
|
2369
|
+
return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
|
|
2370
|
+
}
|
|
1814
2371
|
/** Get the target subject for a consumer kind. */
|
|
1815
2372
|
getTargetSubject(kind) {
|
|
1816
2373
|
switch (kind) {
|
|
@@ -1822,6 +2379,7 @@ var MessageProvider = class {
|
|
|
1822
2379
|
return this.broadcastMessages$;
|
|
1823
2380
|
case "ordered" /* Ordered */:
|
|
1824
2381
|
return this.orderedMessages$;
|
|
2382
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1825
2383
|
default: {
|
|
1826
2384
|
const _exhaustive = kind;
|
|
1827
2385
|
throw new Error(`Unknown stream kind: ${_exhaustive}`);
|
|
@@ -1886,11 +2444,16 @@ var MessageProvider = class {
|
|
|
1886
2444
|
})
|
|
1887
2445
|
);
|
|
1888
2446
|
}
|
|
1889
|
-
/** Single iteration: create ordered consumer ->
|
|
2447
|
+
/** Single iteration: create ordered consumer -> push messages into the subject. */
|
|
1890
2448
|
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
1891
2449
|
const js = this.connection.getJetStreamClient();
|
|
1892
2450
|
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
1893
|
-
const
|
|
2451
|
+
const orderedMessages$ = this.orderedMessages$;
|
|
2452
|
+
const messages = await consumer.consume({
|
|
2453
|
+
callback: (msg) => {
|
|
2454
|
+
orderedMessages$.next(msg);
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
1894
2457
|
if (this.orderedReadyResolve) {
|
|
1895
2458
|
this.orderedReadyResolve();
|
|
1896
2459
|
this.orderedReadyResolve = null;
|
|
@@ -1898,17 +2461,117 @@ var MessageProvider = class {
|
|
|
1898
2461
|
}
|
|
1899
2462
|
this.activeIterators.add(messages);
|
|
1900
2463
|
try {
|
|
1901
|
-
|
|
1902
|
-
this.orderedMessages$.next(msg);
|
|
1903
|
-
}
|
|
2464
|
+
await messages.closed();
|
|
1904
2465
|
} finally {
|
|
1905
2466
|
this.activeIterators.delete(messages);
|
|
1906
2467
|
}
|
|
1907
2468
|
}
|
|
1908
2469
|
};
|
|
1909
2470
|
|
|
2471
|
+
// src/server/infrastructure/metadata.provider.ts
|
|
2472
|
+
import { Logger as Logger9 } from "@nestjs/common";
|
|
2473
|
+
import { Kvm } from "@nats-io/kv";
|
|
2474
|
+
var MetadataProvider = class {
|
|
2475
|
+
constructor(options, connection) {
|
|
2476
|
+
this.connection = connection;
|
|
2477
|
+
this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
|
|
2478
|
+
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
2479
|
+
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
2480
|
+
}
|
|
2481
|
+
logger = new Logger9("Jetstream:Metadata");
|
|
2482
|
+
bucketName;
|
|
2483
|
+
replicas;
|
|
2484
|
+
ttl;
|
|
2485
|
+
currentEntries;
|
|
2486
|
+
heartbeatTimer;
|
|
2487
|
+
cachedKv;
|
|
2488
|
+
/**
|
|
2489
|
+
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
2490
|
+
*
|
|
2491
|
+
* Creates the bucket if it doesn't exist (idempotent).
|
|
2492
|
+
* Skips silently when entries map is empty.
|
|
2493
|
+
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
2494
|
+
* to prevent TTL expiry while the pod is alive.
|
|
2495
|
+
*
|
|
2496
|
+
* Non-critical — errors are logged but do not prevent transport startup.
|
|
2497
|
+
*
|
|
2498
|
+
* @param entries Map of KV key → metadata object.
|
|
2499
|
+
*/
|
|
2500
|
+
async publish(entries) {
|
|
2501
|
+
if (entries.size === 0) return;
|
|
2502
|
+
try {
|
|
2503
|
+
const kv = await this.openBucket();
|
|
2504
|
+
await this.writeEntries(kv, entries);
|
|
2505
|
+
this.currentEntries = entries;
|
|
2506
|
+
this.startHeartbeat();
|
|
2507
|
+
this.logger.log(
|
|
2508
|
+
`Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
|
|
2509
|
+
);
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
this.logger.error("Failed to publish handler metadata to KV", err);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* Stop the heartbeat timer.
|
|
2516
|
+
*
|
|
2517
|
+
* After this call, entries will expire via TTL once the heartbeat window passes.
|
|
2518
|
+
* Called during transport shutdown (strategy.close()).
|
|
2519
|
+
*/
|
|
2520
|
+
destroy() {
|
|
2521
|
+
if (this.heartbeatTimer) {
|
|
2522
|
+
clearInterval(this.heartbeatTimer);
|
|
2523
|
+
this.heartbeatTimer = void 0;
|
|
2524
|
+
}
|
|
2525
|
+
this.currentEntries = void 0;
|
|
2526
|
+
this.cachedKv = void 0;
|
|
2527
|
+
}
|
|
2528
|
+
/** Write entries to KV with per-entry error handling. */
|
|
2529
|
+
async writeEntries(kv, entries) {
|
|
2530
|
+
for (const [key, meta] of entries) {
|
|
2531
|
+
try {
|
|
2532
|
+
await kv.put(key, JSON.stringify(meta));
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
this.logger.error(`Failed to write metadata entry "${key}"`, err);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
/** Start heartbeat interval that refreshes entries every ttl/2. */
|
|
2539
|
+
startHeartbeat() {
|
|
2540
|
+
if (this.heartbeatTimer) {
|
|
2541
|
+
clearInterval(this.heartbeatTimer);
|
|
2542
|
+
}
|
|
2543
|
+
const interval = Math.floor(this.ttl / 2);
|
|
2544
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2545
|
+
void this.refreshEntries();
|
|
2546
|
+
}, interval);
|
|
2547
|
+
this.heartbeatTimer.unref();
|
|
2548
|
+
}
|
|
2549
|
+
/** Refresh all current entries in KV (heartbeat tick). */
|
|
2550
|
+
async refreshEntries() {
|
|
2551
|
+
if (!this.currentEntries || this.currentEntries.size === 0) return;
|
|
2552
|
+
try {
|
|
2553
|
+
const kv = await this.openBucket();
|
|
2554
|
+
await this.writeEntries(kv, this.currentEntries);
|
|
2555
|
+
} catch (err) {
|
|
2556
|
+
this.logger.error("Failed to refresh handler metadata in KV", err);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
/** Create or open the KV bucket (cached after first call). */
|
|
2560
|
+
async openBucket() {
|
|
2561
|
+
if (this.cachedKv) return this.cachedKv;
|
|
2562
|
+
const js = this.connection.getJetStreamClient();
|
|
2563
|
+
const kvm = new Kvm(js);
|
|
2564
|
+
this.cachedKv = await kvm.create(this.bucketName, {
|
|
2565
|
+
history: DEFAULT_METADATA_HISTORY,
|
|
2566
|
+
replicas: this.replicas,
|
|
2567
|
+
ttl: this.ttl
|
|
2568
|
+
});
|
|
2569
|
+
return this.cachedKv;
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
|
|
1910
2573
|
// src/server/routing/pattern-registry.ts
|
|
1911
|
-
import { Logger as
|
|
2574
|
+
import { Logger as Logger10 } from "@nestjs/common";
|
|
1912
2575
|
var HANDLER_LABELS = {
|
|
1913
2576
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
1914
2577
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -1919,7 +2582,7 @@ var PatternRegistry = class {
|
|
|
1919
2582
|
constructor(options) {
|
|
1920
2583
|
this.options = options;
|
|
1921
2584
|
}
|
|
1922
|
-
logger = new
|
|
2585
|
+
logger = new Logger10("Jetstream:PatternRegistry");
|
|
1923
2586
|
registry = /* @__PURE__ */ new Map();
|
|
1924
2587
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1925
2588
|
cachedPatterns = null;
|
|
@@ -1927,6 +2590,7 @@ var PatternRegistry = class {
|
|
|
1927
2590
|
_hasCommands = false;
|
|
1928
2591
|
_hasBroadcasts = false;
|
|
1929
2592
|
_hasOrdered = false;
|
|
2593
|
+
_hasMetadata = false;
|
|
1930
2594
|
/**
|
|
1931
2595
|
* Register all handlers from the NestJS strategy.
|
|
1932
2596
|
*
|
|
@@ -1939,6 +2603,7 @@ var PatternRegistry = class {
|
|
|
1939
2603
|
const isEvent = handler.isEventHandler ?? false;
|
|
1940
2604
|
const isBroadcast = !!extras?.broadcast;
|
|
1941
2605
|
const isOrdered = !!extras?.ordered;
|
|
2606
|
+
const meta = extras?.meta;
|
|
1942
2607
|
if (isBroadcast && isOrdered) {
|
|
1943
2608
|
throw new Error(
|
|
1944
2609
|
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
@@ -1955,7 +2620,8 @@ var PatternRegistry = class {
|
|
|
1955
2620
|
pattern,
|
|
1956
2621
|
isEvent: isEvent && !isOrdered,
|
|
1957
2622
|
isBroadcast,
|
|
1958
|
-
isOrdered
|
|
2623
|
+
isOrdered,
|
|
2624
|
+
meta
|
|
1959
2625
|
});
|
|
1960
2626
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1961
2627
|
}
|
|
@@ -1964,6 +2630,7 @@ var PatternRegistry = class {
|
|
|
1964
2630
|
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1965
2631
|
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1966
2632
|
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2633
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
1967
2634
|
this.logSummary();
|
|
1968
2635
|
}
|
|
1969
2636
|
/** Find handler for a full NATS subject. */
|
|
@@ -1992,6 +2659,26 @@ var PatternRegistry = class {
|
|
|
1992
2659
|
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
1993
2660
|
);
|
|
1994
2661
|
}
|
|
2662
|
+
/** Check if any registered handler has metadata. */
|
|
2663
|
+
hasMetadata() {
|
|
2664
|
+
return this._hasMetadata;
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Get handler metadata entries for KV publishing.
|
|
2668
|
+
*
|
|
2669
|
+
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2670
|
+
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2671
|
+
*/
|
|
2672
|
+
getMetadataEntries() {
|
|
2673
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2674
|
+
for (const entry of this.registry.values()) {
|
|
2675
|
+
if (!entry.meta) continue;
|
|
2676
|
+
const kind = this.resolveStreamKind(entry);
|
|
2677
|
+
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2678
|
+
entries.set(key, entry.meta);
|
|
2679
|
+
}
|
|
2680
|
+
return entries;
|
|
2681
|
+
}
|
|
1995
2682
|
/** Get patterns grouped by kind (cached after registration). */
|
|
1996
2683
|
getPatternsByKind() {
|
|
1997
2684
|
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
@@ -2031,6 +2718,12 @@ var PatternRegistry = class {
|
|
|
2031
2718
|
}
|
|
2032
2719
|
return { events, commands, broadcasts, ordered };
|
|
2033
2720
|
}
|
|
2721
|
+
resolveStreamKind(entry) {
|
|
2722
|
+
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2723
|
+
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2724
|
+
if (entry.isEvent) return "ev" /* Event */;
|
|
2725
|
+
return "cmd" /* Command */;
|
|
2726
|
+
}
|
|
2034
2727
|
logSummary() {
|
|
2035
2728
|
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2036
2729
|
const parts = [
|
|
@@ -2046,10 +2739,10 @@ var PatternRegistry = class {
|
|
|
2046
2739
|
};
|
|
2047
2740
|
|
|
2048
2741
|
// src/server/routing/event.router.ts
|
|
2049
|
-
import { Logger as
|
|
2050
|
-
import {
|
|
2742
|
+
import { Logger as Logger11 } from "@nestjs/common";
|
|
2743
|
+
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
2051
2744
|
var EventRouter = class {
|
|
2052
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
2745
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
2053
2746
|
this.messageProvider = messageProvider;
|
|
2054
2747
|
this.patternRegistry = patternRegistry;
|
|
2055
2748
|
this.codec = codec;
|
|
@@ -2057,8 +2750,10 @@ var EventRouter = class {
|
|
|
2057
2750
|
this.deadLetterConfig = deadLetterConfig;
|
|
2058
2751
|
this.processingConfig = processingConfig;
|
|
2059
2752
|
this.ackWaitMap = ackWaitMap;
|
|
2753
|
+
this.connection = connection;
|
|
2754
|
+
this.options = options;
|
|
2060
2755
|
}
|
|
2061
|
-
logger = new
|
|
2756
|
+
logger = new Logger11("Jetstream:EventRouter");
|
|
2062
2757
|
subscriptions = [];
|
|
2063
2758
|
/**
|
|
2064
2759
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
@@ -2083,15 +2778,194 @@ var EventRouter = class {
|
|
|
2083
2778
|
}
|
|
2084
2779
|
this.subscriptions.length = 0;
|
|
2085
2780
|
}
|
|
2086
|
-
/** Subscribe to a message stream and route each message. */
|
|
2781
|
+
/** Subscribe to a message stream and route each message to its handler. */
|
|
2087
2782
|
subscribeToStream(stream$, kind) {
|
|
2088
2783
|
const isOrdered = kind === "ordered" /* Ordered */;
|
|
2784
|
+
const patternRegistry = this.patternRegistry;
|
|
2785
|
+
const codec = this.codec;
|
|
2786
|
+
const eventBus = this.eventBus;
|
|
2787
|
+
const logger = this.logger;
|
|
2788
|
+
const deadLetterConfig = this.deadLetterConfig;
|
|
2089
2789
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
2790
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
2090
2791
|
const concurrency = this.getConcurrency(kind);
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
)
|
|
2094
|
-
|
|
2792
|
+
const hasDlqCheck = deadLetterConfig !== void 0;
|
|
2793
|
+
const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
|
|
2794
|
+
const isDeadLetter = (msg) => {
|
|
2795
|
+
if (!hasDlqCheck) return false;
|
|
2796
|
+
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
2797
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
2798
|
+
return msg.info.deliveryCount >= maxDeliver;
|
|
2799
|
+
};
|
|
2800
|
+
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
2801
|
+
const settleSuccess = (msg, ctx) => {
|
|
2802
|
+
if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
|
|
2803
|
+
else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
|
|
2804
|
+
else msg.ack();
|
|
2805
|
+
};
|
|
2806
|
+
const settleFailure = async (msg, data, err) => {
|
|
2807
|
+
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
2808
|
+
await handleDeadLetter(msg, data, err);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
msg.nak();
|
|
2812
|
+
};
|
|
2813
|
+
const resolveEvent = (msg) => {
|
|
2814
|
+
const subject = msg.subject;
|
|
2815
|
+
try {
|
|
2816
|
+
const handler = patternRegistry.getHandler(subject);
|
|
2817
|
+
if (!handler) {
|
|
2818
|
+
msg.term(`No handler for event: ${subject}`);
|
|
2819
|
+
logger.error(`No handler for subject: ${subject}`);
|
|
2820
|
+
return null;
|
|
2821
|
+
}
|
|
2822
|
+
let data;
|
|
2823
|
+
try {
|
|
2824
|
+
data = codec.decode(msg.data);
|
|
2825
|
+
} catch (err) {
|
|
2826
|
+
msg.term("Decode error");
|
|
2827
|
+
logger.error(`Decode error for ${subject}:`, err);
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2831
|
+
return { handler, data };
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2834
|
+
try {
|
|
2835
|
+
msg.term("Unexpected router error");
|
|
2836
|
+
} catch (termErr) {
|
|
2837
|
+
logger.error(`Failed to terminate message ${subject}:`, termErr);
|
|
2838
|
+
}
|
|
2839
|
+
return null;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
const handleSafe = (msg) => {
|
|
2843
|
+
const resolved = resolveEvent(msg);
|
|
2844
|
+
if (resolved === null) return void 0;
|
|
2845
|
+
const { handler, data } = resolved;
|
|
2846
|
+
const ctx = new RpcContext([msg]);
|
|
2847
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
2848
|
+
let pending;
|
|
2849
|
+
try {
|
|
2850
|
+
pending = unwrapResult(handler(data, ctx));
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
|
|
2853
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2854
|
+
return settleFailure(msg, data, err);
|
|
2855
|
+
}
|
|
2856
|
+
if (!isPromiseLike(pending)) {
|
|
2857
|
+
settleSuccess(msg, ctx);
|
|
2858
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2859
|
+
return void 0;
|
|
2860
|
+
}
|
|
2861
|
+
return pending.then(
|
|
2862
|
+
() => {
|
|
2863
|
+
settleSuccess(msg, ctx);
|
|
2864
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2865
|
+
},
|
|
2866
|
+
async (err) => {
|
|
2867
|
+
logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
|
|
2868
|
+
try {
|
|
2869
|
+
await settleFailure(msg, data, err);
|
|
2870
|
+
} finally {
|
|
2871
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
);
|
|
2875
|
+
};
|
|
2876
|
+
const handleOrderedSafe = (msg) => {
|
|
2877
|
+
const subject = msg.subject;
|
|
2878
|
+
let handler;
|
|
2879
|
+
let data;
|
|
2880
|
+
try {
|
|
2881
|
+
handler = patternRegistry.getHandler(subject);
|
|
2882
|
+
if (!handler) {
|
|
2883
|
+
logger.error(`No handler for subject: ${subject}`);
|
|
2884
|
+
return void 0;
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
data = codec.decode(msg.data);
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
logger.error(`Decode error for ${subject}:`, err);
|
|
2890
|
+
return void 0;
|
|
2891
|
+
}
|
|
2892
|
+
if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2895
|
+
return void 0;
|
|
2896
|
+
}
|
|
2897
|
+
const ctx = new RpcContext([msg]);
|
|
2898
|
+
const warnIfSettlementAttempted = () => {
|
|
2899
|
+
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
2900
|
+
logger.warn(
|
|
2901
|
+
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
let pending;
|
|
2906
|
+
try {
|
|
2907
|
+
pending = unwrapResult(handler(data, ctx));
|
|
2908
|
+
} catch (err) {
|
|
2909
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2910
|
+
return void 0;
|
|
2911
|
+
}
|
|
2912
|
+
if (!isPromiseLike(pending)) {
|
|
2913
|
+
warnIfSettlementAttempted();
|
|
2914
|
+
return void 0;
|
|
2915
|
+
}
|
|
2916
|
+
return pending.then(warnIfSettlementAttempted, (err) => {
|
|
2917
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2918
|
+
});
|
|
2919
|
+
};
|
|
2920
|
+
const route = isOrdered ? handleOrderedSafe : handleSafe;
|
|
2921
|
+
const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
|
|
2922
|
+
const backlogWarnThreshold = 1e3;
|
|
2923
|
+
let active = 0;
|
|
2924
|
+
let backlogWarned = false;
|
|
2925
|
+
const backlog = [];
|
|
2926
|
+
const onAsyncDone = () => {
|
|
2927
|
+
active--;
|
|
2928
|
+
drainBacklog();
|
|
2929
|
+
};
|
|
2930
|
+
const drainBacklog = () => {
|
|
2931
|
+
while (active < maxActive) {
|
|
2932
|
+
const next = backlog.shift();
|
|
2933
|
+
if (next === void 0) return;
|
|
2934
|
+
active++;
|
|
2935
|
+
const result = route(next);
|
|
2936
|
+
if (result !== void 0) {
|
|
2937
|
+
void result.finally(onAsyncDone);
|
|
2938
|
+
} else {
|
|
2939
|
+
active--;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
2943
|
+
};
|
|
2944
|
+
const subscription = stream$.subscribe({
|
|
2945
|
+
next: (msg) => {
|
|
2946
|
+
if (active >= maxActive) {
|
|
2947
|
+
backlog.push(msg);
|
|
2948
|
+
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
2949
|
+
backlogWarned = true;
|
|
2950
|
+
logger.warn(
|
|
2951
|
+
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
active++;
|
|
2957
|
+
const result = route(msg);
|
|
2958
|
+
if (result !== void 0) {
|
|
2959
|
+
void result.finally(onAsyncDone);
|
|
2960
|
+
} else {
|
|
2961
|
+
active--;
|
|
2962
|
+
if (backlog.length > 0) drainBacklog();
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
error: (err) => {
|
|
2966
|
+
logger.error(`Stream error in ${kind} router`, err);
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2095
2969
|
this.subscriptions.push(subscription);
|
|
2096
2970
|
}
|
|
2097
2971
|
getConcurrency(kind) {
|
|
@@ -2104,87 +2978,94 @@ var EventRouter = class {
|
|
|
2104
2978
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
2105
2979
|
return void 0;
|
|
2106
2980
|
}
|
|
2107
|
-
/** Handle a
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
);
|
|
2119
|
-
|
|
2120
|
-
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2981
|
+
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
2982
|
+
/**
|
|
2983
|
+
* Fallback execution for a dead letter when DLQ is disabled, or when
|
|
2984
|
+
* publishing to the DLQ stream fails (due to network or NATS errors).
|
|
2985
|
+
*
|
|
2986
|
+
* Triggers the user-provided `onDeadLetter` hook for logging/alerting.
|
|
2987
|
+
* On success, terminates the message. On error, leaves it unacknowledged (nak)
|
|
2988
|
+
* so NATS can retry the delivery on the next cycle.
|
|
2989
|
+
*/
|
|
2990
|
+
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
2991
|
+
if (!this.deadLetterConfig) {
|
|
2992
|
+
msg.term("Dead letter config unavailable");
|
|
2993
|
+
return;
|
|
2121
2994
|
}
|
|
2122
|
-
}
|
|
2123
|
-
/** Handle an ordered message with error isolation. */
|
|
2124
|
-
async handleOrderedSafe(msg) {
|
|
2125
2995
|
try {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
} catch (err) {
|
|
2135
|
-
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
2996
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
2997
|
+
msg.term("Dead letter processed via fallback callback");
|
|
2998
|
+
} catch (hookErr) {
|
|
2999
|
+
this.logger.error(
|
|
3000
|
+
`Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
|
|
3001
|
+
hookErr
|
|
3002
|
+
);
|
|
3003
|
+
msg.nak();
|
|
2136
3004
|
}
|
|
2137
3005
|
}
|
|
2138
|
-
/**
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
3006
|
+
/**
|
|
3007
|
+
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
3008
|
+
*
|
|
3009
|
+
* Appends diagnostic metadata headers to the original message and preserves
|
|
3010
|
+
* the primary payload. If publishing succeeds, it notifies the standard
|
|
3011
|
+
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
3012
|
+
* back to the callback entirely to prevent silent data loss.
|
|
3013
|
+
*/
|
|
3014
|
+
async publishToDlq(msg, info, error) {
|
|
3015
|
+
const serviceName = this.options?.name;
|
|
3016
|
+
if (!this.connection || !serviceName) {
|
|
3017
|
+
this.logger.error(
|
|
3018
|
+
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
3019
|
+
);
|
|
3020
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
3021
|
+
return;
|
|
2145
3022
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
3023
|
+
const destinationSubject = dlqStreamName(serviceName);
|
|
3024
|
+
const hdrs = natsHeaders3();
|
|
3025
|
+
if (msg.headers) {
|
|
3026
|
+
for (const [k, v] of msg.headers) {
|
|
3027
|
+
for (const val of v) {
|
|
3028
|
+
hdrs.append(k, val);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
2153
3031
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
3032
|
+
let reason = String(error);
|
|
3033
|
+
if (error instanceof Error) {
|
|
3034
|
+
reason = error.message;
|
|
3035
|
+
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
3036
|
+
reason = String(error.message);
|
|
3037
|
+
}
|
|
3038
|
+
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
3039
|
+
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
3040
|
+
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
3041
|
+
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
3042
|
+
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
2160
3043
|
try {
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
} else {
|
|
2174
|
-
msg.nak();
|
|
3044
|
+
const js = this.connection.getJetStreamClient();
|
|
3045
|
+
await js.publish(destinationSubject, msg.data, { headers: hdrs });
|
|
3046
|
+
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
3047
|
+
if (this.deadLetterConfig?.onDeadLetter) {
|
|
3048
|
+
try {
|
|
3049
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
3050
|
+
} catch (hookErr) {
|
|
3051
|
+
this.logger.warn(
|
|
3052
|
+
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
3053
|
+
hookErr
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
2175
3056
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
3057
|
+
msg.term("Moved to DLQ stream");
|
|
3058
|
+
} catch (publishErr) {
|
|
3059
|
+
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
3060
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2178
3061
|
}
|
|
2179
3062
|
}
|
|
2180
|
-
/**
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
3063
|
+
/**
|
|
3064
|
+
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
3065
|
+
*
|
|
3066
|
+
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
3067
|
+
* or directly to the fallback callback based on the active module configuration.
|
|
3068
|
+
*/
|
|
2188
3069
|
async handleDeadLetter(msg, data, error) {
|
|
2189
3070
|
const info = {
|
|
2190
3071
|
subject: msg.subject,
|
|
@@ -2197,24 +3078,17 @@ var EventRouter = class {
|
|
|
2197
3078
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
2198
3079
|
};
|
|
2199
3080
|
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
2200
|
-
if (!this.
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
try {
|
|
2205
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
2206
|
-
msg.term("Dead letter processed");
|
|
2207
|
-
} catch (hookErr) {
|
|
2208
|
-
this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
|
|
2209
|
-
msg.nak();
|
|
3081
|
+
if (!this.options?.dlq) {
|
|
3082
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
3083
|
+
} else {
|
|
3084
|
+
await this.publishToDlq(msg, info, error);
|
|
2210
3085
|
}
|
|
2211
3086
|
}
|
|
2212
3087
|
};
|
|
2213
3088
|
|
|
2214
3089
|
// src/server/routing/rpc.router.ts
|
|
2215
|
-
import { Logger as
|
|
3090
|
+
import { Logger as Logger12 } from "@nestjs/common";
|
|
2216
3091
|
import { headers } from "@nats-io/transport-node";
|
|
2217
|
-
import { from as from3, mergeMap as mergeMap2 } from "rxjs";
|
|
2218
3092
|
var RpcRouter = class {
|
|
2219
3093
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
2220
3094
|
this.messageProvider = messageProvider;
|
|
@@ -2227,7 +3101,7 @@ var RpcRouter = class {
|
|
|
2227
3101
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
2228
3102
|
this.concurrency = rpcOptions?.concurrency;
|
|
2229
3103
|
}
|
|
2230
|
-
logger = new
|
|
3104
|
+
logger = new Logger12("Jetstream:RpcRouter");
|
|
2231
3105
|
timeout;
|
|
2232
3106
|
concurrency;
|
|
2233
3107
|
resolvedAckExtensionInterval;
|
|
@@ -2245,105 +3119,203 @@ var RpcRouter = class {
|
|
|
2245
3119
|
/** Start routing command messages to handlers. */
|
|
2246
3120
|
async start() {
|
|
2247
3121
|
this.cachedNc = await this.connection.getConnection();
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
this.
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2262
|
-
return;
|
|
2263
|
-
}
|
|
2264
|
-
const { headers: msgHeaders } = msg;
|
|
2265
|
-
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
2266
|
-
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
2267
|
-
if (!replyTo || !correlationId) {
|
|
2268
|
-
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2269
|
-
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2270
|
-
return;
|
|
2271
|
-
}
|
|
2272
|
-
let data;
|
|
2273
|
-
try {
|
|
2274
|
-
data = this.codec.decode(msg.data);
|
|
2275
|
-
} catch (err) {
|
|
2276
|
-
msg.term("Decode error");
|
|
2277
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
2281
|
-
await this.executeHandler(handler, data, msg, replyTo, correlationId);
|
|
2282
|
-
} catch (err) {
|
|
2283
|
-
this.logger.error("Unexpected error in RPC router", err);
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
/** Execute handler, publish response, settle message. */
|
|
2287
|
-
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
2288
|
-
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
2289
|
-
const ctx = new RpcContext([msg]);
|
|
2290
|
-
let settled = false;
|
|
2291
|
-
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
2292
|
-
const timeoutId = setTimeout(() => {
|
|
2293
|
-
if (settled) return;
|
|
2294
|
-
settled = true;
|
|
2295
|
-
stopAckExtension?.();
|
|
2296
|
-
this.logger.error(`RPC timeout (${this.timeout}ms): ${msg.subject}`);
|
|
2297
|
-
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, msg.subject, correlationId);
|
|
2298
|
-
msg.term("Handler timeout");
|
|
2299
|
-
}, this.timeout);
|
|
2300
|
-
try {
|
|
2301
|
-
const result = await unwrapResult(handler(data, ctx));
|
|
2302
|
-
if (settled) return;
|
|
2303
|
-
settled = true;
|
|
2304
|
-
clearTimeout(timeoutId);
|
|
2305
|
-
stopAckExtension?.();
|
|
2306
|
-
msg.ack();
|
|
3122
|
+
const nc = this.cachedNc;
|
|
3123
|
+
const patternRegistry = this.patternRegistry;
|
|
3124
|
+
const codec = this.codec;
|
|
3125
|
+
const eventBus = this.eventBus;
|
|
3126
|
+
const logger = this.logger;
|
|
3127
|
+
const timeout = this.timeout;
|
|
3128
|
+
const ackExtensionInterval = this.ackExtensionInterval;
|
|
3129
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
3130
|
+
const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
|
|
3131
|
+
const emitRpcTimeout = (subject, correlationId) => {
|
|
3132
|
+
eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
3133
|
+
};
|
|
3134
|
+
const publishReply = (replyTo, correlationId, payload) => {
|
|
2307
3135
|
try {
|
|
2308
3136
|
const hdrs = headers();
|
|
2309
3137
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2310
|
-
nc.publish(replyTo,
|
|
3138
|
+
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
2311
3139
|
} catch (publishErr) {
|
|
2312
|
-
|
|
3140
|
+
logger.error(`Failed to publish RPC response`, publishErr);
|
|
2313
3141
|
}
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
settled = true;
|
|
2317
|
-
clearTimeout(timeoutId);
|
|
2318
|
-
stopAckExtension?.();
|
|
3142
|
+
};
|
|
3143
|
+
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
2319
3144
|
try {
|
|
2320
3145
|
const hdrs = headers();
|
|
2321
3146
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2322
3147
|
hdrs.set("x-error" /* Error */, "true");
|
|
2323
|
-
nc.publish(replyTo,
|
|
3148
|
+
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
2324
3149
|
} catch (encodeErr) {
|
|
2325
|
-
|
|
3150
|
+
logger.error(`Failed to encode RPC error for ${subject}`, encodeErr);
|
|
2326
3151
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
3152
|
+
};
|
|
3153
|
+
const resolveCommand = (msg) => {
|
|
3154
|
+
const subject = msg.subject;
|
|
3155
|
+
try {
|
|
3156
|
+
const handler = patternRegistry.getHandler(subject);
|
|
3157
|
+
if (!handler) {
|
|
3158
|
+
msg.term(`No handler for RPC: ${subject}`);
|
|
3159
|
+
logger.error(`No handler for RPC subject: ${subject}`);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
const msgHeaders = msg.headers;
|
|
3163
|
+
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
3164
|
+
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
3165
|
+
if (!replyTo || !correlationId) {
|
|
3166
|
+
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
3167
|
+
logger.error(`Missing headers for RPC: ${subject}`);
|
|
3168
|
+
return null;
|
|
3169
|
+
}
|
|
3170
|
+
let data;
|
|
3171
|
+
try {
|
|
3172
|
+
data = codec.decode(msg.data);
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
msg.term("Decode error");
|
|
3175
|
+
logger.error(`Decode error for RPC ${subject}:`, err);
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
|
|
3179
|
+
return { handler, data, replyTo, correlationId };
|
|
3180
|
+
} catch (err) {
|
|
3181
|
+
logger.error("Unexpected error in RPC router", err);
|
|
3182
|
+
try {
|
|
3183
|
+
msg.term("Unexpected router error");
|
|
3184
|
+
} catch (termErr) {
|
|
3185
|
+
logger.error(`Failed to terminate RPC message ${subject}:`, termErr);
|
|
3186
|
+
}
|
|
3187
|
+
return null;
|
|
3188
|
+
}
|
|
3189
|
+
};
|
|
3190
|
+
const handleSafe = (msg) => {
|
|
3191
|
+
const resolved = resolveCommand(msg);
|
|
3192
|
+
if (resolved === null) return void 0;
|
|
3193
|
+
const { handler, data, replyTo, correlationId } = resolved;
|
|
3194
|
+
const subject = msg.subject;
|
|
3195
|
+
const ctx = new RpcContext([msg]);
|
|
3196
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
3197
|
+
let pending;
|
|
3198
|
+
try {
|
|
3199
|
+
pending = unwrapResult(handler(data, ctx));
|
|
3200
|
+
} catch (err) {
|
|
3201
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3202
|
+
logger.error(`RPC handler error (${subject}):`, err);
|
|
3203
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
3204
|
+
msg.term(`Handler error: ${subject}`);
|
|
3205
|
+
return void 0;
|
|
3206
|
+
}
|
|
3207
|
+
if (!isPromiseLike(pending)) {
|
|
3208
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3209
|
+
msg.ack();
|
|
3210
|
+
publishReply(replyTo, correlationId, pending);
|
|
3211
|
+
return void 0;
|
|
3212
|
+
}
|
|
3213
|
+
let settled = false;
|
|
3214
|
+
const timeoutId = setTimeout(() => {
|
|
3215
|
+
if (settled) return;
|
|
3216
|
+
settled = true;
|
|
3217
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3218
|
+
logger.error(`RPC timeout (${timeout}ms): ${subject}`);
|
|
3219
|
+
emitRpcTimeout(subject, correlationId);
|
|
3220
|
+
msg.term("Handler timeout");
|
|
3221
|
+
}, timeout);
|
|
3222
|
+
return pending.then(
|
|
3223
|
+
(result) => {
|
|
3224
|
+
if (settled) return;
|
|
3225
|
+
settled = true;
|
|
3226
|
+
clearTimeout(timeoutId);
|
|
3227
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3228
|
+
msg.ack();
|
|
3229
|
+
publishReply(replyTo, correlationId, result);
|
|
3230
|
+
},
|
|
3231
|
+
(err) => {
|
|
3232
|
+
if (settled) return;
|
|
3233
|
+
settled = true;
|
|
3234
|
+
clearTimeout(timeoutId);
|
|
3235
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3236
|
+
logger.error(`RPC handler error (${subject}):`, err);
|
|
3237
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
3238
|
+
msg.term(`Handler error: ${subject}`);
|
|
3239
|
+
}
|
|
3240
|
+
);
|
|
3241
|
+
};
|
|
3242
|
+
const backlogWarnThreshold = 1e3;
|
|
3243
|
+
let active = 0;
|
|
3244
|
+
let backlogWarned = false;
|
|
3245
|
+
const backlog = [];
|
|
3246
|
+
const onAsyncDone = () => {
|
|
3247
|
+
active--;
|
|
3248
|
+
drainBacklog();
|
|
3249
|
+
};
|
|
3250
|
+
const drainBacklog = () => {
|
|
3251
|
+
while (active < maxActive) {
|
|
3252
|
+
const next = backlog.shift();
|
|
3253
|
+
if (next === void 0) return;
|
|
3254
|
+
active++;
|
|
3255
|
+
const result = handleSafe(next);
|
|
3256
|
+
if (result !== void 0) {
|
|
3257
|
+
void result.finally(onAsyncDone);
|
|
3258
|
+
} else {
|
|
3259
|
+
active--;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
3263
|
+
};
|
|
3264
|
+
this.subscription = this.messageProvider.commands$.subscribe({
|
|
3265
|
+
next: (msg) => {
|
|
3266
|
+
if (active >= maxActive) {
|
|
3267
|
+
backlog.push(msg);
|
|
3268
|
+
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
3269
|
+
backlogWarned = true;
|
|
3270
|
+
logger.warn(
|
|
3271
|
+
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
active++;
|
|
3277
|
+
const result = handleSafe(msg);
|
|
3278
|
+
if (result !== void 0) {
|
|
3279
|
+
void result.finally(onAsyncDone);
|
|
3280
|
+
} else {
|
|
3281
|
+
active--;
|
|
3282
|
+
if (backlog.length > 0) drainBacklog();
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
error: (err) => {
|
|
3286
|
+
logger.error("Stream error in RPC router", err);
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
/** Stop routing and unsubscribe. */
|
|
3291
|
+
destroy() {
|
|
3292
|
+
this.subscription?.unsubscribe();
|
|
3293
|
+
this.subscription = null;
|
|
2329
3294
|
}
|
|
2330
3295
|
};
|
|
2331
3296
|
|
|
2332
3297
|
// src/shutdown/shutdown.manager.ts
|
|
2333
|
-
import { Logger as
|
|
3298
|
+
import { Logger as Logger13 } from "@nestjs/common";
|
|
2334
3299
|
var ShutdownManager = class {
|
|
2335
3300
|
constructor(connection, eventBus, timeout) {
|
|
2336
3301
|
this.connection = connection;
|
|
2337
3302
|
this.eventBus = eventBus;
|
|
2338
3303
|
this.timeout = timeout;
|
|
2339
3304
|
}
|
|
2340
|
-
logger = new
|
|
3305
|
+
logger = new Logger13("Jetstream:Shutdown");
|
|
3306
|
+
shutdownPromise;
|
|
2341
3307
|
/**
|
|
2342
3308
|
* Execute the full shutdown sequence.
|
|
2343
3309
|
*
|
|
3310
|
+
* Idempotent — concurrent or repeated calls return the same promise.
|
|
3311
|
+
*
|
|
2344
3312
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
2345
3313
|
*/
|
|
2346
3314
|
async shutdown(strategy) {
|
|
3315
|
+
this.shutdownPromise ??= this.doShutdown(strategy);
|
|
3316
|
+
return this.shutdownPromise;
|
|
3317
|
+
}
|
|
3318
|
+
async doShutdown(strategy) {
|
|
2347
3319
|
this.eventBus.emit("shutdownStart" /* ShutdownStart */);
|
|
2348
3320
|
this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
|
|
2349
3321
|
strategy?.close();
|
|
@@ -2478,7 +3450,7 @@ var JetstreamModule = class {
|
|
|
2478
3450
|
provide: JETSTREAM_EVENT_BUS,
|
|
2479
3451
|
inject: [JETSTREAM_OPTIONS],
|
|
2480
3452
|
useFactory: (options) => {
|
|
2481
|
-
const logger = new
|
|
3453
|
+
const logger = new Logger14("Jetstream:Module");
|
|
2482
3454
|
return new EventBus(logger, options.hooks);
|
|
2483
3455
|
}
|
|
2484
3456
|
},
|
|
@@ -2557,8 +3529,8 @@ var JetstreamModule = class {
|
|
|
2557
3529
|
// MessageProvider — pull-based message consumption
|
|
2558
3530
|
{
|
|
2559
3531
|
provide: MessageProvider,
|
|
2560
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
|
|
2561
|
-
useFactory: (options, connection, eventBus) => {
|
|
3532
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
|
|
3533
|
+
useFactory: (options, connection, eventBus, consumerProvider) => {
|
|
2562
3534
|
if (options.consumer === false) return null;
|
|
2563
3535
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
2564
3536
|
if (options.events?.consume)
|
|
@@ -2568,7 +3540,11 @@ var JetstreamModule = class {
|
|
|
2568
3540
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
2569
3541
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
2570
3542
|
}
|
|
2571
|
-
|
|
3543
|
+
const consumerRecoveryFn = consumerProvider ? async (kind) => {
|
|
3544
|
+
const jsm = await connection.getJetStreamManager();
|
|
3545
|
+
return consumerProvider.recoverConsumer(jsm, kind);
|
|
3546
|
+
} : void 0;
|
|
3547
|
+
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
2572
3548
|
}
|
|
2573
3549
|
},
|
|
2574
3550
|
// EventRouter — routes event and broadcast messages to handlers
|
|
@@ -2580,9 +3556,10 @@ var JetstreamModule = class {
|
|
|
2580
3556
|
PatternRegistry,
|
|
2581
3557
|
JETSTREAM_CODEC,
|
|
2582
3558
|
JETSTREAM_EVENT_BUS,
|
|
2583
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3559
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3560
|
+
JETSTREAM_CONNECTION
|
|
2584
3561
|
],
|
|
2585
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
|
|
3562
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
2586
3563
|
if (options.consumer === false) return null;
|
|
2587
3564
|
const deadLetterConfig = options.onDeadLetter ? {
|
|
2588
3565
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -2605,7 +3582,9 @@ var JetstreamModule = class {
|
|
|
2605
3582
|
eventBus,
|
|
2606
3583
|
deadLetterConfig,
|
|
2607
3584
|
processingConfig,
|
|
2608
|
-
ackWaitMap
|
|
3585
|
+
ackWaitMap,
|
|
3586
|
+
connection,
|
|
3587
|
+
options
|
|
2609
3588
|
);
|
|
2610
3589
|
}
|
|
2611
3590
|
},
|
|
@@ -2654,6 +3633,15 @@ var JetstreamModule = class {
|
|
|
2654
3633
|
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
2655
3634
|
}
|
|
2656
3635
|
},
|
|
3636
|
+
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
3637
|
+
{
|
|
3638
|
+
provide: MetadataProvider,
|
|
3639
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
3640
|
+
useFactory: (options, connection) => {
|
|
3641
|
+
if (options.consumer === false) return null;
|
|
3642
|
+
return new MetadataProvider(options, connection);
|
|
3643
|
+
}
|
|
3644
|
+
},
|
|
2657
3645
|
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
2658
3646
|
{
|
|
2659
3647
|
provide: JetstreamStrategy,
|
|
@@ -2667,9 +3655,10 @@ var JetstreamModule = class {
|
|
|
2667
3655
|
EventRouter,
|
|
2668
3656
|
RpcRouter,
|
|
2669
3657
|
CoreRpcServer,
|
|
2670
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3658
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3659
|
+
MetadataProvider
|
|
2671
3660
|
],
|
|
2672
|
-
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
|
|
3661
|
+
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
|
|
2673
3662
|
if (options.consumer === false) return null;
|
|
2674
3663
|
return new JetstreamStrategy(
|
|
2675
3664
|
options,
|
|
@@ -2681,7 +3670,8 @@ var JetstreamModule = class {
|
|
|
2681
3670
|
eventRouter,
|
|
2682
3671
|
rpcRouter,
|
|
2683
3672
|
coreRpcServer,
|
|
2684
|
-
ackWaitMap
|
|
3673
|
+
ackWaitMap,
|
|
3674
|
+
metadataProvider
|
|
2685
3675
|
);
|
|
2686
3676
|
}
|
|
2687
3677
|
}
|
|
@@ -2748,12 +3738,26 @@ JetstreamModule = __decorateClass([
|
|
|
2748
3738
|
__decorateParam(1, Inject(JetstreamStrategy))
|
|
2749
3739
|
], JetstreamModule);
|
|
2750
3740
|
export {
|
|
2751
|
-
|
|
3741
|
+
DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
3742
|
+
DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
3743
|
+
DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
3744
|
+
DEFAULT_COMMAND_STREAM_CONFIG,
|
|
3745
|
+
DEFAULT_DLQ_STREAM_CONFIG,
|
|
3746
|
+
DEFAULT_EVENT_CONSUMER_CONFIG,
|
|
3747
|
+
DEFAULT_EVENT_STREAM_CONFIG,
|
|
3748
|
+
DEFAULT_JETSTREAM_RPC_TIMEOUT,
|
|
3749
|
+
DEFAULT_METADATA_BUCKET,
|
|
3750
|
+
DEFAULT_METADATA_HISTORY,
|
|
3751
|
+
DEFAULT_METADATA_REPLICAS,
|
|
3752
|
+
DEFAULT_METADATA_TTL,
|
|
3753
|
+
DEFAULT_ORDERED_STREAM_CONFIG,
|
|
3754
|
+
DEFAULT_RPC_TIMEOUT,
|
|
3755
|
+
DEFAULT_SHUTDOWN_TIMEOUT,
|
|
2752
3756
|
JETSTREAM_CODEC,
|
|
2753
3757
|
JETSTREAM_CONNECTION,
|
|
2754
|
-
JETSTREAM_EVENT_BUS,
|
|
2755
3758
|
JETSTREAM_OPTIONS,
|
|
2756
3759
|
JetstreamClient,
|
|
3760
|
+
JetstreamDlqHeader,
|
|
2757
3761
|
JetstreamHeader,
|
|
2758
3762
|
JetstreamHealthIndicator,
|
|
2759
3763
|
JetstreamModule,
|
|
@@ -2761,17 +3765,24 @@ export {
|
|
|
2761
3765
|
JetstreamRecordBuilder,
|
|
2762
3766
|
JetstreamStrategy,
|
|
2763
3767
|
JsonCodec,
|
|
3768
|
+
MIN_METADATA_TTL,
|
|
2764
3769
|
MessageKind,
|
|
3770
|
+
MsgpackCodec,
|
|
3771
|
+
NatsErrorCode,
|
|
2765
3772
|
PatternPrefix,
|
|
3773
|
+
RESERVED_HEADERS,
|
|
2766
3774
|
RpcContext,
|
|
2767
3775
|
StreamKind,
|
|
2768
3776
|
TransportEvent,
|
|
3777
|
+
buildBroadcastSubject,
|
|
2769
3778
|
buildSubject,
|
|
2770
3779
|
consumerName,
|
|
3780
|
+
dlqStreamName,
|
|
2771
3781
|
getClientToken,
|
|
2772
3782
|
internalName,
|
|
2773
3783
|
isCoreRpcMode,
|
|
2774
3784
|
isJetStreamRpcMode,
|
|
3785
|
+
metadataKey,
|
|
2775
3786
|
streamName,
|
|
2776
3787
|
toNanos
|
|
2777
3788
|
};
|