@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.cjs
CHANGED
|
@@ -7,11 +7,11 @@ var __export = (target, all) => {
|
|
|
7
7
|
for (var name in all)
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
|
-
var __copyProps = (to,
|
|
11
|
-
if (
|
|
12
|
-
for (let key of __getOwnPropNames(
|
|
10
|
+
var __copyProps = (to, from2, except, desc) => {
|
|
11
|
+
if (from2 && typeof from2 === "object" || typeof from2 === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from2))
|
|
13
13
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () =>
|
|
14
|
+
__defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
|
|
15
15
|
}
|
|
16
16
|
return to;
|
|
17
17
|
};
|
|
@@ -29,12 +29,26 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
29
29
|
// src/index.ts
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
-
|
|
32
|
+
DEFAULT_BROADCAST_CONSUMER_CONFIG: () => DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
33
|
+
DEFAULT_BROADCAST_STREAM_CONFIG: () => DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
34
|
+
DEFAULT_COMMAND_CONSUMER_CONFIG: () => DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
35
|
+
DEFAULT_COMMAND_STREAM_CONFIG: () => DEFAULT_COMMAND_STREAM_CONFIG,
|
|
36
|
+
DEFAULT_DLQ_STREAM_CONFIG: () => DEFAULT_DLQ_STREAM_CONFIG,
|
|
37
|
+
DEFAULT_EVENT_CONSUMER_CONFIG: () => DEFAULT_EVENT_CONSUMER_CONFIG,
|
|
38
|
+
DEFAULT_EVENT_STREAM_CONFIG: () => DEFAULT_EVENT_STREAM_CONFIG,
|
|
39
|
+
DEFAULT_JETSTREAM_RPC_TIMEOUT: () => DEFAULT_JETSTREAM_RPC_TIMEOUT,
|
|
40
|
+
DEFAULT_METADATA_BUCKET: () => DEFAULT_METADATA_BUCKET,
|
|
41
|
+
DEFAULT_METADATA_HISTORY: () => DEFAULT_METADATA_HISTORY,
|
|
42
|
+
DEFAULT_METADATA_REPLICAS: () => DEFAULT_METADATA_REPLICAS,
|
|
43
|
+
DEFAULT_METADATA_TTL: () => DEFAULT_METADATA_TTL,
|
|
44
|
+
DEFAULT_ORDERED_STREAM_CONFIG: () => DEFAULT_ORDERED_STREAM_CONFIG,
|
|
45
|
+
DEFAULT_RPC_TIMEOUT: () => DEFAULT_RPC_TIMEOUT,
|
|
46
|
+
DEFAULT_SHUTDOWN_TIMEOUT: () => DEFAULT_SHUTDOWN_TIMEOUT,
|
|
33
47
|
JETSTREAM_CODEC: () => JETSTREAM_CODEC,
|
|
34
48
|
JETSTREAM_CONNECTION: () => JETSTREAM_CONNECTION,
|
|
35
|
-
JETSTREAM_EVENT_BUS: () => JETSTREAM_EVENT_BUS,
|
|
36
49
|
JETSTREAM_OPTIONS: () => JETSTREAM_OPTIONS,
|
|
37
50
|
JetstreamClient: () => JetstreamClient,
|
|
51
|
+
JetstreamDlqHeader: () => JetstreamDlqHeader,
|
|
38
52
|
JetstreamHeader: () => JetstreamHeader,
|
|
39
53
|
JetstreamHealthIndicator: () => JetstreamHealthIndicator,
|
|
40
54
|
JetstreamModule: () => JetstreamModule,
|
|
@@ -42,24 +56,31 @@ __export(index_exports, {
|
|
|
42
56
|
JetstreamRecordBuilder: () => JetstreamRecordBuilder,
|
|
43
57
|
JetstreamStrategy: () => JetstreamStrategy,
|
|
44
58
|
JsonCodec: () => JsonCodec,
|
|
59
|
+
MIN_METADATA_TTL: () => MIN_METADATA_TTL,
|
|
45
60
|
MessageKind: () => MessageKind,
|
|
61
|
+
MsgpackCodec: () => MsgpackCodec,
|
|
62
|
+
NatsErrorCode: () => NatsErrorCode,
|
|
46
63
|
PatternPrefix: () => PatternPrefix,
|
|
64
|
+
RESERVED_HEADERS: () => RESERVED_HEADERS,
|
|
47
65
|
RpcContext: () => RpcContext,
|
|
48
66
|
StreamKind: () => StreamKind,
|
|
49
67
|
TransportEvent: () => TransportEvent,
|
|
68
|
+
buildBroadcastSubject: () => buildBroadcastSubject,
|
|
50
69
|
buildSubject: () => buildSubject,
|
|
51
70
|
consumerName: () => consumerName,
|
|
71
|
+
dlqStreamName: () => dlqStreamName,
|
|
52
72
|
getClientToken: () => getClientToken,
|
|
53
73
|
internalName: () => internalName,
|
|
54
74
|
isCoreRpcMode: () => isCoreRpcMode,
|
|
55
75
|
isJetStreamRpcMode: () => isJetStreamRpcMode,
|
|
76
|
+
metadataKey: () => metadataKey,
|
|
56
77
|
streamName: () => streamName,
|
|
57
78
|
toNanos: () => toNanos
|
|
58
79
|
});
|
|
59
80
|
module.exports = __toCommonJS(index_exports);
|
|
60
81
|
|
|
61
82
|
// src/jetstream.module.ts
|
|
62
|
-
var
|
|
83
|
+
var import_common14 = require("@nestjs/common");
|
|
63
84
|
|
|
64
85
|
// src/client/jetstream.client.ts
|
|
65
86
|
var import_common = require("@nestjs/common");
|
|
@@ -152,7 +173,7 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
|
152
173
|
max_msgs_per_subject: 1e6,
|
|
153
174
|
max_msgs: 1e7,
|
|
154
175
|
max_bytes: 2 * GB,
|
|
155
|
-
max_age: toNanos(1, "
|
|
176
|
+
max_age: toNanos(1, "hours"),
|
|
156
177
|
duplicate_window: toNanos(2, "minutes")
|
|
157
178
|
};
|
|
158
179
|
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
@@ -167,6 +188,18 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
|
167
188
|
max_age: toNanos(1, "days"),
|
|
168
189
|
duplicate_window: toNanos(2, "minutes")
|
|
169
190
|
};
|
|
191
|
+
var DEFAULT_DLQ_STREAM_CONFIG = {
|
|
192
|
+
...baseStreamConfig,
|
|
193
|
+
retention: import_jetstream.RetentionPolicy.Workqueue,
|
|
194
|
+
allow_rollup_hdrs: false,
|
|
195
|
+
max_consumers: 100,
|
|
196
|
+
max_msg_size: 10 * MB,
|
|
197
|
+
max_msgs_per_subject: 5e6,
|
|
198
|
+
max_msgs: 5e7,
|
|
199
|
+
max_bytes: 5 * GB,
|
|
200
|
+
max_age: toNanos(30, "days"),
|
|
201
|
+
duplicate_window: toNanos(2, "minutes")
|
|
202
|
+
};
|
|
170
203
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
171
204
|
ack_wait: toNanos(10, "seconds"),
|
|
172
205
|
max_deliver: 3,
|
|
@@ -194,6 +227,12 @@ var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
|
|
|
194
227
|
var DEFAULT_RPC_TIMEOUT = 3e4;
|
|
195
228
|
var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
|
|
196
229
|
var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
|
|
230
|
+
var DEFAULT_METADATA_BUCKET = "handler_registry";
|
|
231
|
+
var DEFAULT_METADATA_REPLICAS = 1;
|
|
232
|
+
var DEFAULT_METADATA_HISTORY = 1;
|
|
233
|
+
var DEFAULT_METADATA_TTL = 3e4;
|
|
234
|
+
var MIN_METADATA_TTL = 5e3;
|
|
235
|
+
var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
|
|
197
236
|
var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
198
237
|
JetstreamHeader2["CorrelationId"] = "x-correlation-id";
|
|
199
238
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
@@ -202,6 +241,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
202
241
|
JetstreamHeader2["Error"] = "x-error";
|
|
203
242
|
return JetstreamHeader2;
|
|
204
243
|
})(JetstreamHeader || {});
|
|
244
|
+
var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
|
|
245
|
+
JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
|
|
246
|
+
JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
|
|
247
|
+
JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
|
|
248
|
+
JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
|
|
249
|
+
JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
|
|
250
|
+
return JetstreamDlqHeader2;
|
|
251
|
+
})(JetstreamDlqHeader || {});
|
|
205
252
|
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
206
253
|
"x-correlation-id" /* CorrelationId */,
|
|
207
254
|
"x-reply-to" /* ReplyTo */,
|
|
@@ -214,6 +261,9 @@ var streamName = (serviceName, kind) => {
|
|
|
214
261
|
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
215
262
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
216
263
|
};
|
|
264
|
+
var dlqStreamName = (serviceName) => {
|
|
265
|
+
return `${internalName(serviceName)}_dlq-stream`;
|
|
266
|
+
};
|
|
217
267
|
var consumerName = (serviceName, kind) => {
|
|
218
268
|
if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
|
|
219
269
|
return `${internalName(serviceName)}_${kind}-consumer`;
|
|
@@ -228,12 +278,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
|
228
278
|
|
|
229
279
|
// src/client/jetstream.record.ts
|
|
230
280
|
var JetstreamRecord = class {
|
|
231
|
-
constructor(data, headers2, timeout, messageId, schedule) {
|
|
281
|
+
constructor(data, headers2, timeout, messageId, schedule, ttl) {
|
|
232
282
|
this.data = data;
|
|
233
283
|
this.headers = headers2;
|
|
234
284
|
this.timeout = timeout;
|
|
235
285
|
this.messageId = messageId;
|
|
236
286
|
this.schedule = schedule;
|
|
287
|
+
this.ttl = ttl;
|
|
237
288
|
}
|
|
238
289
|
};
|
|
239
290
|
var JetstreamRecordBuilder = class {
|
|
@@ -242,6 +293,7 @@ var JetstreamRecordBuilder = class {
|
|
|
242
293
|
timeout;
|
|
243
294
|
messageId;
|
|
244
295
|
scheduleOptions;
|
|
296
|
+
ttlDuration;
|
|
245
297
|
constructor(data) {
|
|
246
298
|
this.data = data;
|
|
247
299
|
}
|
|
@@ -333,6 +385,33 @@ var JetstreamRecordBuilder = class {
|
|
|
333
385
|
this.scheduleOptions = { at: new Date(ts) };
|
|
334
386
|
return this;
|
|
335
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Set per-message TTL (time-to-live).
|
|
390
|
+
*
|
|
391
|
+
* The message expires individually after the specified duration,
|
|
392
|
+
* independent of the stream's `max_age`. Requires NATS >= 2.11 and
|
|
393
|
+
* `allow_msg_ttl: true` on the stream.
|
|
394
|
+
*
|
|
395
|
+
* Only meaningful for events (`client.emit()`). If used with RPC
|
|
396
|
+
* (`client.send()`), a warning is logged and the TTL is ignored.
|
|
397
|
+
*
|
|
398
|
+
* @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* import { toNanos } from '@horizon-republic/nestjs-jetstream';
|
|
403
|
+
*
|
|
404
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
|
|
405
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
ttl(nanos) {
|
|
409
|
+
if (!Number.isFinite(nanos) || nanos <= 0) {
|
|
410
|
+
throw new Error("TTL must be a positive finite value");
|
|
411
|
+
}
|
|
412
|
+
this.ttlDuration = nanosToGoDuration(nanos);
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
336
415
|
/**
|
|
337
416
|
* Build the immutable {@link JetstreamRecord}.
|
|
338
417
|
*
|
|
@@ -345,7 +424,8 @@ var JetstreamRecordBuilder = class {
|
|
|
345
424
|
new Map(this.headers),
|
|
346
425
|
this.timeout,
|
|
347
426
|
this.messageId,
|
|
348
|
-
schedule
|
|
427
|
+
schedule,
|
|
428
|
+
this.ttlDuration
|
|
349
429
|
);
|
|
350
430
|
}
|
|
351
431
|
/** Validate that a header key is not reserved. */
|
|
@@ -357,8 +437,20 @@ var JetstreamRecordBuilder = class {
|
|
|
357
437
|
}
|
|
358
438
|
}
|
|
359
439
|
};
|
|
440
|
+
var NS_PER_MS = 1e6;
|
|
441
|
+
var NS_PER_S = 1e9;
|
|
442
|
+
var NS_PER_M = 60 * NS_PER_S;
|
|
443
|
+
var NS_PER_H = 60 * NS_PER_M;
|
|
444
|
+
var nanosToGoDuration = (nanos) => {
|
|
445
|
+
if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
|
|
446
|
+
if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
|
|
447
|
+
if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
|
|
448
|
+
if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
|
|
449
|
+
return `${nanos}ns`;
|
|
450
|
+
};
|
|
360
451
|
|
|
361
452
|
// src/client/jetstream.client.ts
|
|
453
|
+
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
362
454
|
var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
363
455
|
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
364
456
|
super();
|
|
@@ -368,12 +460,33 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
368
460
|
this.eventBus = eventBus;
|
|
369
461
|
this.targetName = targetServiceName;
|
|
370
462
|
this.callerName = internalName(this.rootOptions.name);
|
|
463
|
+
const targetInternal = internalName(targetServiceName);
|
|
464
|
+
this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
|
|
465
|
+
this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
|
|
466
|
+
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
467
|
+
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
468
|
+
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
371
469
|
}
|
|
372
470
|
logger = new import_common.Logger("Jetstream:Client");
|
|
373
471
|
/** Target service name this client sends messages to. */
|
|
374
472
|
targetName;
|
|
375
473
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
376
474
|
callerName;
|
|
475
|
+
/**
|
|
476
|
+
* Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
|
|
477
|
+
* per stream kind this client may publish to. Built once in the constructor
|
|
478
|
+
* so producing a full subject is a single string concat with the user pattern.
|
|
479
|
+
*/
|
|
480
|
+
eventSubjectPrefix;
|
|
481
|
+
commandSubjectPrefix;
|
|
482
|
+
orderedSubjectPrefix;
|
|
483
|
+
/**
|
|
484
|
+
* RPC configuration snapshots. The values are derived from rootOptions at
|
|
485
|
+
* construction time so the publish hot path never has to re-run
|
|
486
|
+
* isCoreRpcMode / getRpcTimeout on every call.
|
|
487
|
+
*/
|
|
488
|
+
isCoreMode;
|
|
489
|
+
defaultRpcTimeout;
|
|
377
490
|
/** Shared inbox for JetStream-mode RPC responses. */
|
|
378
491
|
inbox = null;
|
|
379
492
|
inboxSubscription = null;
|
|
@@ -383,6 +496,12 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
383
496
|
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
384
497
|
/** Subscription to connection status events for disconnect handling. */
|
|
385
498
|
statusSubscription = null;
|
|
499
|
+
/**
|
|
500
|
+
* Cached readiness flag. Once `connect()` has wired the inbox and status
|
|
501
|
+
* subscription, subsequent publishes skip the `await connect()` microtask
|
|
502
|
+
* and reach for the underlying connection synchronously instead.
|
|
503
|
+
*/
|
|
504
|
+
readyForPublish = false;
|
|
386
505
|
/**
|
|
387
506
|
* Establish connection. Called automatically by NestJS on first use.
|
|
388
507
|
*
|
|
@@ -393,7 +512,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
393
512
|
*/
|
|
394
513
|
async connect() {
|
|
395
514
|
const nc = await this.connection.getConnection();
|
|
396
|
-
if (
|
|
515
|
+
if (!this.isCoreMode && !this.inboxSubscription) {
|
|
397
516
|
this.setupInbox(nc);
|
|
398
517
|
}
|
|
399
518
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
@@ -401,12 +520,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
401
520
|
this.handleDisconnect();
|
|
402
521
|
}
|
|
403
522
|
});
|
|
523
|
+
this.readyForPublish = true;
|
|
404
524
|
return nc;
|
|
405
525
|
}
|
|
406
526
|
/** Clean up resources: reject pending RPCs, unsubscribe from status events. */
|
|
407
527
|
async close() {
|
|
408
528
|
this.statusSubscription?.unsubscribe();
|
|
409
529
|
this.statusSubscription = null;
|
|
530
|
+
this.readyForPublish = false;
|
|
410
531
|
this.rejectPendingRpcs(new Error("Client closed"));
|
|
411
532
|
}
|
|
412
533
|
/**
|
|
@@ -430,8 +551,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
430
551
|
* set to the original event subject.
|
|
431
552
|
*/
|
|
432
553
|
async dispatchEvent(packet) {
|
|
433
|
-
await this.connect();
|
|
434
|
-
const { data, hdrs, messageId, schedule } = this.extractRecordData(packet.data);
|
|
554
|
+
if (!this.readyForPublish) await this.connect();
|
|
555
|
+
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
435
556
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
436
557
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
437
558
|
if (schedule) {
|
|
@@ -439,6 +560,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
439
560
|
const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
|
|
440
561
|
headers: msgHeaders,
|
|
441
562
|
msgID: messageId ?? import_nuid.nuid.next(),
|
|
563
|
+
ttl,
|
|
442
564
|
schedule: {
|
|
443
565
|
specification: schedule.at,
|
|
444
566
|
target: eventSubject
|
|
@@ -452,7 +574,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
452
574
|
} else {
|
|
453
575
|
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
454
576
|
headers: msgHeaders,
|
|
455
|
-
msgID: messageId ?? import_nuid.nuid.next()
|
|
577
|
+
msgID: messageId ?? import_nuid.nuid.next(),
|
|
578
|
+
ttl
|
|
456
579
|
});
|
|
457
580
|
if (ack.duplicate) {
|
|
458
581
|
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
@@ -467,19 +590,24 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
467
590
|
* JetStream mode: publishes to stream + waits for inbox response.
|
|
468
591
|
*/
|
|
469
592
|
publish(packet, callback) {
|
|
470
|
-
const subject =
|
|
471
|
-
const { data, hdrs, timeout, messageId, schedule } = this.extractRecordData(packet.data);
|
|
593
|
+
const subject = this.commandSubjectPrefix + packet.pattern;
|
|
594
|
+
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
472
595
|
if (schedule) {
|
|
473
596
|
this.logger.warn(
|
|
474
597
|
"scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
|
|
475
598
|
);
|
|
476
599
|
}
|
|
600
|
+
if (ttl) {
|
|
601
|
+
this.logger.warn(
|
|
602
|
+
"ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
|
|
603
|
+
);
|
|
604
|
+
}
|
|
477
605
|
const onUnhandled = (err) => {
|
|
478
606
|
this.logger.error("Unhandled publish error:", err);
|
|
479
607
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
480
608
|
};
|
|
481
609
|
let jetStreamCorrelationId = null;
|
|
482
|
-
if (
|
|
610
|
+
if (this.isCoreMode) {
|
|
483
611
|
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
|
|
484
612
|
} else {
|
|
485
613
|
jetStreamCorrelationId = import_nuid.nuid.next();
|
|
@@ -504,8 +632,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
504
632
|
/** Core mode: nc.request() with timeout. */
|
|
505
633
|
async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
|
|
506
634
|
try {
|
|
507
|
-
const nc = await this.connect();
|
|
508
|
-
const effectiveTimeout = timeout ?? this.
|
|
635
|
+
const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
|
|
636
|
+
const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
|
|
509
637
|
const hdrs = this.buildHeaders(customHeaders, { subject });
|
|
510
638
|
const response = await nc.request(subject, this.codec.encode(data), {
|
|
511
639
|
timeout: effectiveTimeout,
|
|
@@ -527,10 +655,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
527
655
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
528
656
|
async publishJetStreamRpc(subject, data, callback, options) {
|
|
529
657
|
const { headers: customHeaders, correlationId, messageId } = options;
|
|
530
|
-
const effectiveTimeout = options.timeout ?? this.
|
|
658
|
+
const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
|
|
531
659
|
this.pendingMessages.set(correlationId, callback);
|
|
532
660
|
try {
|
|
533
|
-
await this.connect();
|
|
661
|
+
if (!this.readyForPublish) await this.connect();
|
|
534
662
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
535
663
|
if (!this.inbox) {
|
|
536
664
|
this.pendingMessages.delete(correlationId);
|
|
@@ -576,6 +704,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
576
704
|
handleDisconnect() {
|
|
577
705
|
this.rejectPendingRpcs(new Error("Connection lost"));
|
|
578
706
|
this.inbox = null;
|
|
707
|
+
this.readyForPublish = false;
|
|
579
708
|
}
|
|
580
709
|
/** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
|
|
581
710
|
rejectPendingRpcs(error) {
|
|
@@ -589,6 +718,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
589
718
|
this.pendingTimeouts.clear();
|
|
590
719
|
this.inboxSubscription?.unsubscribe();
|
|
591
720
|
this.inboxSubscription = null;
|
|
721
|
+
this.inbox = null;
|
|
592
722
|
}
|
|
593
723
|
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
594
724
|
setupInbox(nc) {
|
|
@@ -638,19 +768,22 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
638
768
|
this.pendingMessages.delete(correlationId);
|
|
639
769
|
}
|
|
640
770
|
}
|
|
641
|
-
/**
|
|
771
|
+
/**
|
|
772
|
+
* Resolve a user pattern to a fully-qualified NATS subject, dispatching
|
|
773
|
+
* between the event, broadcast, and ordered prefixes.
|
|
774
|
+
*
|
|
775
|
+
* The leading-char check short-circuits the `startsWith` comparisons for
|
|
776
|
+
* patterns that cannot possibly carry a broadcast/ordered marker, which is
|
|
777
|
+
* the overwhelmingly common case.
|
|
778
|
+
*/
|
|
642
779
|
buildEventSubject(pattern) {
|
|
643
|
-
if (pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
644
|
-
return
|
|
645
|
-
}
|
|
646
|
-
if (pattern.startsWith("ordered:" /* Ordered */)) {
|
|
647
|
-
return buildSubject(
|
|
648
|
-
this.targetName,
|
|
649
|
-
"ordered" /* Ordered */,
|
|
650
|
-
pattern.slice("ordered:" /* Ordered */.length)
|
|
651
|
-
);
|
|
780
|
+
if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
781
|
+
return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
|
|
652
782
|
}
|
|
653
|
-
|
|
783
|
+
if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
|
|
784
|
+
return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
|
|
785
|
+
}
|
|
786
|
+
return this.eventSubjectPrefix + pattern;
|
|
654
787
|
}
|
|
655
788
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
656
789
|
buildHeaders(customHeaders, transport) {
|
|
@@ -675,10 +808,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
675
808
|
if (rawData instanceof JetstreamRecord) {
|
|
676
809
|
return {
|
|
677
810
|
data: rawData.data,
|
|
678
|
-
hdrs: rawData.headers.size > 0 ?
|
|
811
|
+
hdrs: rawData.headers.size > 0 ? rawData.headers : null,
|
|
679
812
|
timeout: rawData.timeout,
|
|
680
813
|
messageId: rawData.messageId,
|
|
681
|
-
schedule: rawData.schedule
|
|
814
|
+
schedule: rawData.schedule,
|
|
815
|
+
ttl: rawData.ttl
|
|
682
816
|
};
|
|
683
817
|
}
|
|
684
818
|
return {
|
|
@@ -686,7 +820,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
686
820
|
hdrs: null,
|
|
687
821
|
timeout: void 0,
|
|
688
822
|
messageId: void 0,
|
|
689
|
-
schedule: void 0
|
|
823
|
+
schedule: void 0,
|
|
824
|
+
ttl: void 0
|
|
690
825
|
};
|
|
691
826
|
}
|
|
692
827
|
/**
|
|
@@ -716,11 +851,6 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
716
851
|
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
717
852
|
return `${targetPrefix}_sch.${pattern}`;
|
|
718
853
|
}
|
|
719
|
-
getRpcTimeout() {
|
|
720
|
-
if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
|
|
721
|
-
const defaultTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? DEFAULT_JETSTREAM_RPC_TIMEOUT : DEFAULT_RPC_TIMEOUT;
|
|
722
|
-
return this.rootOptions.rpc.timeout ?? defaultTimeout;
|
|
723
|
-
}
|
|
724
854
|
};
|
|
725
855
|
|
|
726
856
|
// src/codec/json.codec.ts
|
|
@@ -735,6 +865,19 @@ var JsonCodec = class {
|
|
|
735
865
|
}
|
|
736
866
|
};
|
|
737
867
|
|
|
868
|
+
// src/codec/msgpack.codec.ts
|
|
869
|
+
var MsgpackCodec = class {
|
|
870
|
+
constructor(packr) {
|
|
871
|
+
this.packr = packr;
|
|
872
|
+
}
|
|
873
|
+
encode(data) {
|
|
874
|
+
return this.packr.pack(data);
|
|
875
|
+
}
|
|
876
|
+
decode(data) {
|
|
877
|
+
return this.packr.unpack(data);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
738
881
|
// src/connection/connection.provider.ts
|
|
739
882
|
var import_common2 = require("@nestjs/common");
|
|
740
883
|
var import_transport_node2 = require("@nats-io/transport-node");
|
|
@@ -947,6 +1090,15 @@ var EventBus = class {
|
|
|
947
1090
|
kind
|
|
948
1091
|
);
|
|
949
1092
|
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Check whether a hook is registered for the given event.
|
|
1095
|
+
*
|
|
1096
|
+
* Used by the routing hot path to elide the emit call entirely when the
|
|
1097
|
+
* transport owner did not register a listener.
|
|
1098
|
+
*/
|
|
1099
|
+
hasHook(event) {
|
|
1100
|
+
return this.hooks[event] !== void 0;
|
|
1101
|
+
}
|
|
950
1102
|
callHook(event, hook, ...args) {
|
|
951
1103
|
try {
|
|
952
1104
|
const result = hook(...args);
|
|
@@ -1001,9 +1153,14 @@ var JetstreamHealthIndicator = class {
|
|
|
1001
1153
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
1002
1154
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
1003
1155
|
*
|
|
1156
|
+
* The thrown error sets `isHealthCheckError: true` and `causes` — the
|
|
1157
|
+
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
1158
|
+
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
1159
|
+
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
1160
|
+
*
|
|
1004
1161
|
* @param key - Health indicator key (default: `'jetstream'`).
|
|
1005
1162
|
* @returns Object with status, server, and latency under the given key.
|
|
1006
|
-
* @throws Error with `{ [key]: { status: 'down' } }
|
|
1163
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
1007
1164
|
*/
|
|
1008
1165
|
async isHealthy(key = "jetstream") {
|
|
1009
1166
|
const status = await this.check();
|
|
@@ -1013,8 +1170,10 @@ var JetstreamHealthIndicator = class {
|
|
|
1013
1170
|
latency: status.latency
|
|
1014
1171
|
};
|
|
1015
1172
|
if (!status.connected) {
|
|
1173
|
+
const causes = { [key]: details };
|
|
1016
1174
|
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
1017
|
-
|
|
1175
|
+
causes,
|
|
1176
|
+
isHealthCheckError: true
|
|
1018
1177
|
});
|
|
1019
1178
|
}
|
|
1020
1179
|
return { [key]: details };
|
|
@@ -1027,7 +1186,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
1027
1186
|
// src/server/strategy.ts
|
|
1028
1187
|
var import_microservices2 = require("@nestjs/microservices");
|
|
1029
1188
|
var JetstreamStrategy = class extends import_microservices2.Server {
|
|
1030
|
-
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
|
|
1189
|
+
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
|
|
1031
1190
|
super();
|
|
1032
1191
|
this.options = options;
|
|
1033
1192
|
this.connection = connection;
|
|
@@ -1039,6 +1198,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
1039
1198
|
this.rpcRouter = rpcRouter;
|
|
1040
1199
|
this.coreRpcServer = coreRpcServer;
|
|
1041
1200
|
this.ackWaitMap = ackWaitMap;
|
|
1201
|
+
this.metadataProvider = metadataProvider;
|
|
1042
1202
|
}
|
|
1043
1203
|
transportId = /* @__PURE__ */ Symbol("jetstream-transport");
|
|
1044
1204
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -1083,10 +1243,14 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
1083
1243
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
1084
1244
|
await this.coreRpcServer.start();
|
|
1085
1245
|
}
|
|
1246
|
+
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
1247
|
+
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
1248
|
+
}
|
|
1086
1249
|
callback();
|
|
1087
1250
|
}
|
|
1088
|
-
/** Stop all consumers, routers, and
|
|
1251
|
+
/** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
|
|
1089
1252
|
close() {
|
|
1253
|
+
this.metadataProvider?.destroy();
|
|
1090
1254
|
this.eventRouter.destroy();
|
|
1091
1255
|
this.rpcRouter.destroy();
|
|
1092
1256
|
this.coreRpcServer.stop();
|
|
@@ -1317,16 +1481,71 @@ var resolveAckExtensionInterval = (config, ackWaitNanos) => {
|
|
|
1317
1481
|
const interval = Math.floor(ackWaitNanos / 1e6 / 2);
|
|
1318
1482
|
return Math.max(interval, MIN_ACK_EXTENSION_INTERVAL);
|
|
1319
1483
|
};
|
|
1484
|
+
var AckExtensionPool = class {
|
|
1485
|
+
entries = /* @__PURE__ */ new Set();
|
|
1486
|
+
handle = null;
|
|
1487
|
+
handleFireAt = 0;
|
|
1488
|
+
schedule(msg, interval) {
|
|
1489
|
+
const entry = {
|
|
1490
|
+
msg,
|
|
1491
|
+
interval,
|
|
1492
|
+
nextFireAt: Date.now() + interval,
|
|
1493
|
+
active: true
|
|
1494
|
+
};
|
|
1495
|
+
this.entries.add(entry);
|
|
1496
|
+
this.ensureWake(entry.nextFireAt);
|
|
1497
|
+
return entry;
|
|
1498
|
+
}
|
|
1499
|
+
cancel(entry) {
|
|
1500
|
+
if (!entry.active) return;
|
|
1501
|
+
entry.active = false;
|
|
1502
|
+
this.entries.delete(entry);
|
|
1503
|
+
if (this.entries.size === 0 && this.handle !== null) {
|
|
1504
|
+
clearTimeout(this.handle);
|
|
1505
|
+
this.handle = null;
|
|
1506
|
+
this.handleFireAt = 0;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Ensure the shared timer will fire no later than `dueAt`. If an earlier
|
|
1511
|
+
* wake is already scheduled, leave it; otherwise replace it with a tighter one.
|
|
1512
|
+
*/
|
|
1513
|
+
ensureWake(dueAt) {
|
|
1514
|
+
if (this.handle !== null && this.handleFireAt <= dueAt) return;
|
|
1515
|
+
if (this.handle !== null) clearTimeout(this.handle);
|
|
1516
|
+
const delay = Math.max(0, dueAt - Date.now());
|
|
1517
|
+
const handle = setTimeout(() => {
|
|
1518
|
+
this.tick();
|
|
1519
|
+
}, delay);
|
|
1520
|
+
if (typeof handle.unref === "function") handle.unref();
|
|
1521
|
+
this.handle = handle;
|
|
1522
|
+
this.handleFireAt = dueAt;
|
|
1523
|
+
}
|
|
1524
|
+
tick() {
|
|
1525
|
+
this.handle = null;
|
|
1526
|
+
this.handleFireAt = 0;
|
|
1527
|
+
const now = Date.now();
|
|
1528
|
+
let earliest = Infinity;
|
|
1529
|
+
for (const entry of this.entries) {
|
|
1530
|
+
if (!entry.active) continue;
|
|
1531
|
+
if (entry.nextFireAt <= now) {
|
|
1532
|
+
try {
|
|
1533
|
+
entry.msg.working();
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
entry.nextFireAt = now + entry.interval;
|
|
1537
|
+
}
|
|
1538
|
+
if (entry.nextFireAt < earliest) earliest = entry.nextFireAt;
|
|
1539
|
+
}
|
|
1540
|
+
if (earliest !== Infinity) this.ensureWake(earliest);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
var pool = new AckExtensionPool();
|
|
1320
1544
|
var startAckExtensionTimer = (msg, interval) => {
|
|
1321
1545
|
if (interval === null || interval <= 0) return null;
|
|
1322
|
-
const
|
|
1323
|
-
try {
|
|
1324
|
-
msg.working();
|
|
1325
|
-
} catch {
|
|
1326
|
-
}
|
|
1327
|
-
}, interval);
|
|
1546
|
+
const entry = pool.schedule(msg, interval);
|
|
1328
1547
|
return () => {
|
|
1329
|
-
|
|
1548
|
+
pool.cancel(entry);
|
|
1330
1549
|
};
|
|
1331
1550
|
};
|
|
1332
1551
|
|
|
@@ -1340,11 +1559,8 @@ var serializeError = (err) => {
|
|
|
1340
1559
|
|
|
1341
1560
|
// src/utils/unwrap-result.ts
|
|
1342
1561
|
var import_rxjs2 = require("rxjs");
|
|
1343
|
-
var RESOLVED_VOID = Promise.resolve(void 0);
|
|
1344
|
-
var RESOLVED_NULL = Promise.resolve(null);
|
|
1345
1562
|
var unwrapResult = (result) => {
|
|
1346
|
-
if (result === void 0) return
|
|
1347
|
-
if (result === null) return RESOLVED_NULL;
|
|
1563
|
+
if (result === void 0 || result === null) return result;
|
|
1348
1564
|
if ((0, import_rxjs2.isObservable)(result)) {
|
|
1349
1565
|
return subscribeToFirst(result);
|
|
1350
1566
|
}
|
|
@@ -1353,8 +1569,9 @@ var unwrapResult = (result) => {
|
|
|
1353
1569
|
(resolved) => (0, import_rxjs2.isObservable)(resolved) ? subscribeToFirst(resolved) : resolved
|
|
1354
1570
|
);
|
|
1355
1571
|
}
|
|
1356
|
-
return
|
|
1572
|
+
return result;
|
|
1357
1573
|
};
|
|
1574
|
+
var isPromiseLike = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
1358
1575
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1359
1576
|
let done = false;
|
|
1360
1577
|
let subscription = null;
|
|
@@ -1442,7 +1659,8 @@ var CoreRpcServer = class {
|
|
|
1442
1659
|
}
|
|
1443
1660
|
const ctx = new RpcContext([msg]);
|
|
1444
1661
|
try {
|
|
1445
|
-
const
|
|
1662
|
+
const raw = unwrapResult(handler(data, ctx));
|
|
1663
|
+
const result = isPromiseLike(raw) ? await raw : raw;
|
|
1446
1664
|
msg.respond(this.codec.encode(result));
|
|
1447
1665
|
} catch (err) {
|
|
1448
1666
|
this.logger.error(`Handler error for Core RPC ${msg.subject}:`, err);
|
|
@@ -1462,24 +1680,180 @@ var CoreRpcServer = class {
|
|
|
1462
1680
|
};
|
|
1463
1681
|
|
|
1464
1682
|
// src/server/infrastructure/stream.provider.ts
|
|
1683
|
+
var import_common6 = require("@nestjs/common");
|
|
1684
|
+
var import_jetstream14 = require("@nats-io/jetstream");
|
|
1685
|
+
|
|
1686
|
+
// src/server/infrastructure/nats-error-codes.ts
|
|
1687
|
+
var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
1688
|
+
NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
|
|
1689
|
+
NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
|
|
1690
|
+
NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
|
|
1691
|
+
return NatsErrorCode2;
|
|
1692
|
+
})(NatsErrorCode || {});
|
|
1693
|
+
|
|
1694
|
+
// src/server/infrastructure/stream-config-diff.ts
|
|
1695
|
+
var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1696
|
+
"retention"
|
|
1697
|
+
]);
|
|
1698
|
+
var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1699
|
+
"storage"
|
|
1700
|
+
]);
|
|
1701
|
+
var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1702
|
+
"allow_msg_schedules",
|
|
1703
|
+
"allow_msg_ttl",
|
|
1704
|
+
"deny_delete",
|
|
1705
|
+
"deny_purge"
|
|
1706
|
+
]);
|
|
1707
|
+
var compareStreamConfig = (current, desired) => {
|
|
1708
|
+
const changes = [];
|
|
1709
|
+
for (const key of Object.keys(desired)) {
|
|
1710
|
+
const currentVal = current[key];
|
|
1711
|
+
const desiredVal = desired[key];
|
|
1712
|
+
if (isEqual(currentVal, desiredVal)) continue;
|
|
1713
|
+
changes.push({
|
|
1714
|
+
property: key,
|
|
1715
|
+
current: currentVal,
|
|
1716
|
+
desired: desiredVal,
|
|
1717
|
+
mutability: classifyMutability(key, currentVal, desiredVal)
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
|
|
1721
|
+
const hasMutableChanges = changes.some(
|
|
1722
|
+
(c) => c.mutability === "mutable" || c.mutability === "enable-only"
|
|
1723
|
+
);
|
|
1724
|
+
const hasTransportControlledConflicts = changes.some(
|
|
1725
|
+
(c) => c.mutability === "transport-controlled"
|
|
1726
|
+
);
|
|
1727
|
+
return {
|
|
1728
|
+
hasChanges: changes.length > 0,
|
|
1729
|
+
hasMutableChanges,
|
|
1730
|
+
hasImmutableChanges,
|
|
1731
|
+
hasTransportControlledConflicts,
|
|
1732
|
+
changes
|
|
1733
|
+
};
|
|
1734
|
+
};
|
|
1735
|
+
var classifyMutability = (key, current, desired) => {
|
|
1736
|
+
if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
|
|
1737
|
+
if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
|
|
1738
|
+
if (ENABLE_ONLY_PROPERTIES.has(key)) {
|
|
1739
|
+
return current === true && desired === false ? "immutable" : "enable-only";
|
|
1740
|
+
}
|
|
1741
|
+
return "mutable";
|
|
1742
|
+
};
|
|
1743
|
+
var isEqual = (a, b) => {
|
|
1744
|
+
if (a === b) return true;
|
|
1745
|
+
if (a == null && b == null) return true;
|
|
1746
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
// src/server/infrastructure/stream-migration.ts
|
|
1465
1750
|
var import_common5 = require("@nestjs/common");
|
|
1466
1751
|
var import_jetstream13 = require("@nats-io/jetstream");
|
|
1467
|
-
var
|
|
1752
|
+
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
1753
|
+
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
1754
|
+
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
1755
|
+
var StreamMigration = class {
|
|
1756
|
+
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
1757
|
+
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
1758
|
+
}
|
|
1759
|
+
logger = new import_common5.Logger("Jetstream:Stream");
|
|
1760
|
+
async migrate(jsm, streamName2, newConfig) {
|
|
1761
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1762
|
+
const startTime = Date.now();
|
|
1763
|
+
const currentInfo = await jsm.streams.info(streamName2);
|
|
1764
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1765
|
+
const messageCount = currentInfo.state.messages;
|
|
1766
|
+
this.logger.log(`Stream ${streamName2}: destructive migration started`);
|
|
1767
|
+
let originalDeleted = false;
|
|
1768
|
+
try {
|
|
1769
|
+
if (messageCount > 0) {
|
|
1770
|
+
this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
|
|
1771
|
+
await jsm.streams.add({
|
|
1772
|
+
...currentInfo.config,
|
|
1773
|
+
name: backupName,
|
|
1774
|
+
subjects: [],
|
|
1775
|
+
sources: [{ name: streamName2 }]
|
|
1776
|
+
});
|
|
1777
|
+
await this.waitForSourcing(jsm, backupName, messageCount);
|
|
1778
|
+
}
|
|
1779
|
+
this.logger.log(` Phase 2/4: Deleting old stream`);
|
|
1780
|
+
await jsm.streams.delete(streamName2);
|
|
1781
|
+
originalDeleted = true;
|
|
1782
|
+
this.logger.log(` Phase 3/4: Creating stream with new config`);
|
|
1783
|
+
await jsm.streams.add(newConfig);
|
|
1784
|
+
if (messageCount > 0) {
|
|
1785
|
+
const backupInfo = await jsm.streams.info(backupName);
|
|
1786
|
+
await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
|
|
1787
|
+
this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
|
|
1788
|
+
await jsm.streams.update(streamName2, {
|
|
1789
|
+
...newConfig,
|
|
1790
|
+
sources: [{ name: backupName }]
|
|
1791
|
+
});
|
|
1792
|
+
await this.waitForSourcing(jsm, streamName2, messageCount);
|
|
1793
|
+
await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
|
|
1794
|
+
await jsm.streams.delete(backupName);
|
|
1795
|
+
}
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
if (originalDeleted && messageCount > 0) {
|
|
1798
|
+
this.logger.error(
|
|
1799
|
+
`Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
|
|
1800
|
+
);
|
|
1801
|
+
} else {
|
|
1802
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1803
|
+
}
|
|
1804
|
+
throw err;
|
|
1805
|
+
}
|
|
1806
|
+
const durationMs = Date.now() - startTime;
|
|
1807
|
+
this.logger.log(
|
|
1808
|
+
`Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
async waitForSourcing(jsm, streamName2, expectedCount) {
|
|
1812
|
+
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
1813
|
+
while (Date.now() < deadline) {
|
|
1814
|
+
const info = await jsm.streams.info(streamName2);
|
|
1815
|
+
if (info.state.messages >= expectedCount) return;
|
|
1816
|
+
await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
|
|
1817
|
+
}
|
|
1818
|
+
throw new Error(
|
|
1819
|
+
`Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
async cleanupOrphanedBackup(jsm, backupName) {
|
|
1823
|
+
try {
|
|
1824
|
+
await jsm.streams.info(backupName);
|
|
1825
|
+
this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
|
|
1826
|
+
await jsm.streams.delete(backupName);
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
throw err;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
// src/server/infrastructure/stream.provider.ts
|
|
1468
1837
|
var StreamProvider = class {
|
|
1469
1838
|
constructor(options, connection) {
|
|
1470
1839
|
this.options = options;
|
|
1471
1840
|
this.connection = connection;
|
|
1472
1841
|
}
|
|
1473
|
-
logger = new
|
|
1842
|
+
logger = new import_common6.Logger("Jetstream:Stream");
|
|
1843
|
+
migration = new StreamMigration();
|
|
1474
1844
|
/**
|
|
1475
1845
|
* Ensure all required streams exist with correct configuration.
|
|
1476
1846
|
*
|
|
1477
1847
|
* @param kinds Which stream kinds to create. Determined by the module based
|
|
1478
1848
|
* on RPC mode and registered handler patterns.
|
|
1849
|
+
* If the dlq option is enabled, also ensures the DLQ stream exists.
|
|
1479
1850
|
*/
|
|
1480
1851
|
async ensureStreams(kinds) {
|
|
1481
1852
|
const jsm = await this.connection.getJetStreamManager();
|
|
1482
1853
|
await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
|
|
1854
|
+
if (this.options.dlq) {
|
|
1855
|
+
await this.ensureDlqStream(jsm);
|
|
1856
|
+
}
|
|
1483
1857
|
}
|
|
1484
1858
|
/** Get the stream name for a given kind. */
|
|
1485
1859
|
getStreamName(kind) {
|
|
@@ -1514,17 +1888,85 @@ var StreamProvider = class {
|
|
|
1514
1888
|
const config = this.buildConfig(kind);
|
|
1515
1889
|
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
1516
1890
|
try {
|
|
1517
|
-
await jsm.streams.info(config.name);
|
|
1518
|
-
this.
|
|
1519
|
-
return await jsm.streams.update(config.name, config);
|
|
1891
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1892
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1520
1893
|
} catch (err) {
|
|
1521
|
-
if (err instanceof
|
|
1894
|
+
if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1522
1895
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
1523
1896
|
return await jsm.streams.add(config);
|
|
1524
1897
|
}
|
|
1525
1898
|
throw err;
|
|
1526
1899
|
}
|
|
1527
1900
|
}
|
|
1901
|
+
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
1902
|
+
async ensureDlqStream(jsm) {
|
|
1903
|
+
const config = this.buildDlqConfig();
|
|
1904
|
+
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
1905
|
+
try {
|
|
1906
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1907
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1910
|
+
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
1911
|
+
return await jsm.streams.add(config);
|
|
1912
|
+
}
|
|
1913
|
+
throw err;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
async handleExistingStream(jsm, currentInfo, config) {
|
|
1917
|
+
const diff = compareStreamConfig(currentInfo.config, config);
|
|
1918
|
+
if (!diff.hasChanges) {
|
|
1919
|
+
this.logger.debug(`Stream ${config.name}: no config changes`);
|
|
1920
|
+
return currentInfo;
|
|
1921
|
+
}
|
|
1922
|
+
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
1923
|
+
if (diff.hasTransportControlledConflicts) {
|
|
1924
|
+
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
|
|
1925
|
+
throw new Error(
|
|
1926
|
+
`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.`
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
if (!diff.hasImmutableChanges) {
|
|
1930
|
+
this.logger.debug(`Stream exists, updating: ${config.name}`);
|
|
1931
|
+
return await jsm.streams.update(config.name, config);
|
|
1932
|
+
}
|
|
1933
|
+
if (!this.options.allowDestructiveMigration) {
|
|
1934
|
+
this.logger.warn(
|
|
1935
|
+
`Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
|
|
1936
|
+
);
|
|
1937
|
+
if (diff.hasMutableChanges) {
|
|
1938
|
+
const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
|
|
1939
|
+
return await jsm.streams.update(config.name, mutableConfig);
|
|
1940
|
+
}
|
|
1941
|
+
return currentInfo;
|
|
1942
|
+
}
|
|
1943
|
+
await this.migration.migrate(jsm, config.name, config);
|
|
1944
|
+
return await jsm.streams.info(config.name);
|
|
1945
|
+
}
|
|
1946
|
+
buildMutableOnlyConfig(config, currentConfig, diff) {
|
|
1947
|
+
const nonMutableKeys = new Set(
|
|
1948
|
+
diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
|
|
1949
|
+
);
|
|
1950
|
+
const filtered = { ...config };
|
|
1951
|
+
for (const key of nonMutableKeys) {
|
|
1952
|
+
filtered[key] = currentConfig[key];
|
|
1953
|
+
}
|
|
1954
|
+
return filtered;
|
|
1955
|
+
}
|
|
1956
|
+
logChanges(streamName2, diff, migrationEnabled) {
|
|
1957
|
+
for (const c of diff.changes) {
|
|
1958
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
|
|
1959
|
+
if (c.mutability === "transport-controlled") {
|
|
1960
|
+
this.logger.error(
|
|
1961
|
+
`Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
|
|
1962
|
+
);
|
|
1963
|
+
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
1964
|
+
this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
|
|
1965
|
+
} else {
|
|
1966
|
+
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1528
1970
|
/** Build the full stream config by merging defaults with user overrides. */
|
|
1529
1971
|
buildConfig(kind) {
|
|
1530
1972
|
const name = this.getStreamName(kind);
|
|
@@ -1540,6 +1982,26 @@ var StreamProvider = class {
|
|
|
1540
1982
|
description
|
|
1541
1983
|
};
|
|
1542
1984
|
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Build the stream configuration for the Dead-Letter Queue (DLQ).
|
|
1987
|
+
*
|
|
1988
|
+
* Merges the library default DLQ config with user-provided overrides.
|
|
1989
|
+
* Ensures transport-controlled settings like retention are safely decoupled.
|
|
1990
|
+
*/
|
|
1991
|
+
buildDlqConfig() {
|
|
1992
|
+
const name = dlqStreamName(this.options.name);
|
|
1993
|
+
const subjects = [name];
|
|
1994
|
+
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
1995
|
+
const overrides = this.options.dlq?.stream ?? {};
|
|
1996
|
+
const safeOverrides = this.stripTransportControlled(overrides);
|
|
1997
|
+
return {
|
|
1998
|
+
...DEFAULT_DLQ_STREAM_CONFIG,
|
|
1999
|
+
...safeOverrides,
|
|
2000
|
+
name,
|
|
2001
|
+
subjects,
|
|
2002
|
+
description
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
1543
2005
|
/** Get default config for a stream kind. */
|
|
1544
2006
|
getDefaults(kind) {
|
|
1545
2007
|
switch (kind) {
|
|
@@ -1558,25 +2020,44 @@ var StreamProvider = class {
|
|
|
1558
2020
|
const overrides = this.getOverrides(kind);
|
|
1559
2021
|
return overrides.allow_msg_schedules === true;
|
|
1560
2022
|
}
|
|
1561
|
-
/** Get user-provided overrides for a stream kind. */
|
|
2023
|
+
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
1562
2024
|
getOverrides(kind) {
|
|
2025
|
+
let overrides;
|
|
1563
2026
|
switch (kind) {
|
|
1564
2027
|
case "ev" /* Event */:
|
|
1565
|
-
|
|
2028
|
+
overrides = this.options.events?.stream ?? {};
|
|
2029
|
+
break;
|
|
1566
2030
|
case "cmd" /* Command */:
|
|
1567
|
-
|
|
2031
|
+
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
2032
|
+
break;
|
|
1568
2033
|
case "broadcast" /* Broadcast */:
|
|
1569
|
-
|
|
2034
|
+
overrides = this.options.broadcast?.stream ?? {};
|
|
2035
|
+
break;
|
|
1570
2036
|
case "ordered" /* Ordered */:
|
|
1571
|
-
|
|
2037
|
+
overrides = this.options.ordered?.stream ?? {};
|
|
2038
|
+
break;
|
|
1572
2039
|
}
|
|
2040
|
+
return this.stripTransportControlled(overrides);
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Remove transport-controlled properties from user overrides.
|
|
2044
|
+
* `retention` is managed by the transport (Workqueue/Limits per stream kind)
|
|
2045
|
+
* and silently stripped to protect users from misconfiguration.
|
|
2046
|
+
*/
|
|
2047
|
+
stripTransportControlled(overrides) {
|
|
2048
|
+
if (!("retention" in overrides)) return overrides;
|
|
2049
|
+
this.logger.debug(
|
|
2050
|
+
"Stripping user-provided retention override \u2014 retention is managed by the transport"
|
|
2051
|
+
);
|
|
2052
|
+
const cleaned = { ...overrides };
|
|
2053
|
+
delete cleaned.retention;
|
|
2054
|
+
return cleaned;
|
|
1573
2055
|
}
|
|
1574
2056
|
};
|
|
1575
2057
|
|
|
1576
2058
|
// src/server/infrastructure/consumer.provider.ts
|
|
1577
|
-
var
|
|
1578
|
-
var
|
|
1579
|
-
var CONSUMER_NOT_FOUND = 10014;
|
|
2059
|
+
var import_common7 = require("@nestjs/common");
|
|
2060
|
+
var import_jetstream16 = require("@nats-io/jetstream");
|
|
1580
2061
|
var ConsumerProvider = class {
|
|
1581
2062
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
1582
2063
|
this.options = options;
|
|
@@ -1584,7 +2065,7 @@ var ConsumerProvider = class {
|
|
|
1584
2065
|
this.streamProvider = streamProvider;
|
|
1585
2066
|
this.patternRegistry = patternRegistry;
|
|
1586
2067
|
}
|
|
1587
|
-
logger = new
|
|
2068
|
+
logger = new import_common7.Logger("Jetstream:Consumer");
|
|
1588
2069
|
/**
|
|
1589
2070
|
* Ensure consumers exist for the specified kinds.
|
|
1590
2071
|
*
|
|
@@ -1605,7 +2086,11 @@ var ConsumerProvider = class {
|
|
|
1605
2086
|
getConsumerName(kind) {
|
|
1606
2087
|
return consumerName(this.options.name, kind);
|
|
1607
2088
|
}
|
|
1608
|
-
/**
|
|
2089
|
+
/**
|
|
2090
|
+
* Ensure a single consumer exists with the desired config.
|
|
2091
|
+
* Used at **startup** — creates or updates the consumer to match
|
|
2092
|
+
* the current pod's configuration.
|
|
2093
|
+
*/
|
|
1609
2094
|
async ensureConsumer(jsm, kind) {
|
|
1610
2095
|
const stream = this.streamProvider.getStreamName(kind);
|
|
1611
2096
|
const config = this.buildConfig(kind);
|
|
@@ -1616,13 +2101,74 @@ var ConsumerProvider = class {
|
|
|
1616
2101
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
1617
2102
|
return await jsm.consumers.update(stream, name, config);
|
|
1618
2103
|
} catch (err) {
|
|
1619
|
-
if (err instanceof
|
|
1620
|
-
|
|
1621
|
-
|
|
2104
|
+
if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
2105
|
+
throw err;
|
|
2106
|
+
}
|
|
2107
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Recover a consumer that disappeared during runtime.
|
|
2112
|
+
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
2113
|
+
*
|
|
2114
|
+
* If a migration backup stream exists, another pod is mid-migration — we
|
|
2115
|
+
* throw so the self-healing retry loop waits with backoff until migration
|
|
2116
|
+
* completes and the backup is cleaned up.
|
|
2117
|
+
*
|
|
2118
|
+
* This prevents old pods from:
|
|
2119
|
+
* - Overwriting a newer pod's consumer config during rolling updates
|
|
2120
|
+
* - Creating consumers during migration (which would consume and delete
|
|
2121
|
+
* workqueue messages while they're being restored)
|
|
2122
|
+
*/
|
|
2123
|
+
async recoverConsumer(jsm, kind) {
|
|
2124
|
+
const stream = this.streamProvider.getStreamName(kind);
|
|
2125
|
+
const config = this.buildConfig(kind);
|
|
2126
|
+
const name = config.durable_name;
|
|
2127
|
+
this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
|
|
2128
|
+
await this.assertNoMigrationInProgress(jsm, stream);
|
|
2129
|
+
try {
|
|
2130
|
+
return await jsm.consumers.info(stream, name);
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
2133
|
+
throw err;
|
|
2134
|
+
}
|
|
2135
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Throw if a migration backup stream exists for this stream.
|
|
2140
|
+
* The self-healing retry loop catches the error and retries with backoff,
|
|
2141
|
+
* naturally waiting until the migrating pod finishes and cleans up the backup.
|
|
2142
|
+
*/
|
|
2143
|
+
async assertNoMigrationInProgress(jsm, stream) {
|
|
2144
|
+
const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
|
|
2145
|
+
try {
|
|
2146
|
+
await jsm.streams.info(backupName);
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
2149
|
+
);
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
2152
|
+
return;
|
|
1622
2153
|
}
|
|
1623
2154
|
throw err;
|
|
1624
2155
|
}
|
|
1625
2156
|
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Create a consumer, handling the race where another pod creates it first.
|
|
2159
|
+
*/
|
|
2160
|
+
async createConsumer(jsm, stream, name, config) {
|
|
2161
|
+
this.logger.log(`Creating consumer: ${name}`);
|
|
2162
|
+
try {
|
|
2163
|
+
return await jsm.consumers.add(stream, config);
|
|
2164
|
+
} catch (addErr) {
|
|
2165
|
+
if (addErr instanceof import_jetstream16.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
2166
|
+
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
2167
|
+
return await jsm.consumers.info(stream, name);
|
|
2168
|
+
}
|
|
2169
|
+
throw addErr;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
1626
2172
|
/** Build consumer config by merging defaults with user overrides. */
|
|
1627
2173
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
1628
2174
|
buildConfig(kind) {
|
|
@@ -1675,6 +2221,7 @@ var ConsumerProvider = class {
|
|
|
1675
2221
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1676
2222
|
case "ordered" /* Ordered */:
|
|
1677
2223
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2224
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1678
2225
|
default: {
|
|
1679
2226
|
const _exhaustive = kind;
|
|
1680
2227
|
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
@@ -1692,6 +2239,7 @@ var ConsumerProvider = class {
|
|
|
1692
2239
|
return this.options.broadcast?.consumer ?? {};
|
|
1693
2240
|
case "ordered" /* Ordered */:
|
|
1694
2241
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2242
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1695
2243
|
default: {
|
|
1696
2244
|
const _exhaustive = kind;
|
|
1697
2245
|
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
@@ -1701,16 +2249,17 @@ var ConsumerProvider = class {
|
|
|
1701
2249
|
};
|
|
1702
2250
|
|
|
1703
2251
|
// src/server/infrastructure/message.provider.ts
|
|
1704
|
-
var
|
|
1705
|
-
var
|
|
2252
|
+
var import_common8 = require("@nestjs/common");
|
|
2253
|
+
var import_jetstream18 = require("@nats-io/jetstream");
|
|
1706
2254
|
var import_rxjs3 = require("rxjs");
|
|
1707
2255
|
var MessageProvider = class {
|
|
1708
|
-
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
|
|
2256
|
+
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
1709
2257
|
this.connection = connection;
|
|
1710
2258
|
this.eventBus = eventBus;
|
|
1711
2259
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2260
|
+
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
1712
2261
|
}
|
|
1713
|
-
logger = new
|
|
2262
|
+
logger = new import_common8.Logger("Jetstream:Message");
|
|
1714
2263
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1715
2264
|
orderedReadyResolve = null;
|
|
1716
2265
|
orderedReadyReject = null;
|
|
@@ -1762,7 +2311,7 @@ var MessageProvider = class {
|
|
|
1762
2311
|
*/
|
|
1763
2312
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1764
2313
|
const consumerOpts = { filter_subjects: filterSubjects };
|
|
1765
|
-
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !==
|
|
2314
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream18.DeliverPolicy.All) {
|
|
1766
2315
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
1767
2316
|
}
|
|
1768
2317
|
if (orderedConfig?.optStartSeq !== void 0) {
|
|
@@ -1813,20 +2362,49 @@ var MessageProvider = class {
|
|
|
1813
2362
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
1814
2363
|
async consumeOnce(kind, info, target$) {
|
|
1815
2364
|
const js = this.connection.getJetStreamClient();
|
|
1816
|
-
|
|
2365
|
+
let consumer;
|
|
2366
|
+
let consumerName2 = info.name;
|
|
2367
|
+
try {
|
|
2368
|
+
consumer = await js.consumers.get(info.stream_name, info.name);
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
|
|
2371
|
+
this.logger.warn(`Consumer ${info.name} not found, recreating...`);
|
|
2372
|
+
const recovered = await this.consumerRecoveryFn(kind);
|
|
2373
|
+
consumerName2 = recovered.name;
|
|
2374
|
+
this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
|
|
2375
|
+
consumer = await js.consumers.get(recovered.stream_name, consumerName2);
|
|
2376
|
+
} else {
|
|
2377
|
+
throw err;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
1817
2380
|
const defaults = { idle_heartbeat: 5e3 };
|
|
1818
2381
|
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
1819
|
-
const messages = await consumer.consume({
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
for await (const msg of messages) {
|
|
2382
|
+
const messages = await consumer.consume({
|
|
2383
|
+
...defaults,
|
|
2384
|
+
...userOptions,
|
|
2385
|
+
callback: (msg) => {
|
|
1824
2386
|
target$.next(msg);
|
|
1825
2387
|
}
|
|
2388
|
+
});
|
|
2389
|
+
this.activeIterators.add(messages);
|
|
2390
|
+
this.monitorConsumerHealth(messages, consumerName2);
|
|
2391
|
+
try {
|
|
2392
|
+
await messages.closed();
|
|
1826
2393
|
} finally {
|
|
1827
2394
|
this.activeIterators.delete(messages);
|
|
1828
2395
|
}
|
|
1829
2396
|
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Detect "consumer not found" errors from `js.consumers.get()`.
|
|
2399
|
+
*
|
|
2400
|
+
* Unlike JetStream Manager calls (which throw `JetStreamApiError`),
|
|
2401
|
+
* the JetStream client's `consumers.get()` throws a plain `Error`
|
|
2402
|
+
* with the error code embedded in the message text.
|
|
2403
|
+
*/
|
|
2404
|
+
isConsumerNotFound(err) {
|
|
2405
|
+
if (!(err instanceof Error)) return false;
|
|
2406
|
+
return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
|
|
2407
|
+
}
|
|
1830
2408
|
/** Get the target subject for a consumer kind. */
|
|
1831
2409
|
getTargetSubject(kind) {
|
|
1832
2410
|
switch (kind) {
|
|
@@ -1838,6 +2416,7 @@ var MessageProvider = class {
|
|
|
1838
2416
|
return this.broadcastMessages$;
|
|
1839
2417
|
case "ordered" /* Ordered */:
|
|
1840
2418
|
return this.orderedMessages$;
|
|
2419
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1841
2420
|
default: {
|
|
1842
2421
|
const _exhaustive = kind;
|
|
1843
2422
|
throw new Error(`Unknown stream kind: ${_exhaustive}`);
|
|
@@ -1902,11 +2481,16 @@ var MessageProvider = class {
|
|
|
1902
2481
|
})
|
|
1903
2482
|
);
|
|
1904
2483
|
}
|
|
1905
|
-
/** Single iteration: create ordered consumer ->
|
|
2484
|
+
/** Single iteration: create ordered consumer -> push messages into the subject. */
|
|
1906
2485
|
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
1907
2486
|
const js = this.connection.getJetStreamClient();
|
|
1908
2487
|
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
1909
|
-
const
|
|
2488
|
+
const orderedMessages$ = this.orderedMessages$;
|
|
2489
|
+
const messages = await consumer.consume({
|
|
2490
|
+
callback: (msg) => {
|
|
2491
|
+
orderedMessages$.next(msg);
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
1910
2494
|
if (this.orderedReadyResolve) {
|
|
1911
2495
|
this.orderedReadyResolve();
|
|
1912
2496
|
this.orderedReadyResolve = null;
|
|
@@ -1914,17 +2498,117 @@ var MessageProvider = class {
|
|
|
1914
2498
|
}
|
|
1915
2499
|
this.activeIterators.add(messages);
|
|
1916
2500
|
try {
|
|
1917
|
-
|
|
1918
|
-
this.orderedMessages$.next(msg);
|
|
1919
|
-
}
|
|
2501
|
+
await messages.closed();
|
|
1920
2502
|
} finally {
|
|
1921
2503
|
this.activeIterators.delete(messages);
|
|
1922
2504
|
}
|
|
1923
2505
|
}
|
|
1924
2506
|
};
|
|
1925
2507
|
|
|
2508
|
+
// src/server/infrastructure/metadata.provider.ts
|
|
2509
|
+
var import_common9 = require("@nestjs/common");
|
|
2510
|
+
var import_kv = require("@nats-io/kv");
|
|
2511
|
+
var MetadataProvider = class {
|
|
2512
|
+
constructor(options, connection) {
|
|
2513
|
+
this.connection = connection;
|
|
2514
|
+
this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
|
|
2515
|
+
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
2516
|
+
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
2517
|
+
}
|
|
2518
|
+
logger = new import_common9.Logger("Jetstream:Metadata");
|
|
2519
|
+
bucketName;
|
|
2520
|
+
replicas;
|
|
2521
|
+
ttl;
|
|
2522
|
+
currentEntries;
|
|
2523
|
+
heartbeatTimer;
|
|
2524
|
+
cachedKv;
|
|
2525
|
+
/**
|
|
2526
|
+
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
2527
|
+
*
|
|
2528
|
+
* Creates the bucket if it doesn't exist (idempotent).
|
|
2529
|
+
* Skips silently when entries map is empty.
|
|
2530
|
+
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
2531
|
+
* to prevent TTL expiry while the pod is alive.
|
|
2532
|
+
*
|
|
2533
|
+
* Non-critical — errors are logged but do not prevent transport startup.
|
|
2534
|
+
*
|
|
2535
|
+
* @param entries Map of KV key → metadata object.
|
|
2536
|
+
*/
|
|
2537
|
+
async publish(entries) {
|
|
2538
|
+
if (entries.size === 0) return;
|
|
2539
|
+
try {
|
|
2540
|
+
const kv = await this.openBucket();
|
|
2541
|
+
await this.writeEntries(kv, entries);
|
|
2542
|
+
this.currentEntries = entries;
|
|
2543
|
+
this.startHeartbeat();
|
|
2544
|
+
this.logger.log(
|
|
2545
|
+
`Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
|
|
2546
|
+
);
|
|
2547
|
+
} catch (err) {
|
|
2548
|
+
this.logger.error("Failed to publish handler metadata to KV", err);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Stop the heartbeat timer.
|
|
2553
|
+
*
|
|
2554
|
+
* After this call, entries will expire via TTL once the heartbeat window passes.
|
|
2555
|
+
* Called during transport shutdown (strategy.close()).
|
|
2556
|
+
*/
|
|
2557
|
+
destroy() {
|
|
2558
|
+
if (this.heartbeatTimer) {
|
|
2559
|
+
clearInterval(this.heartbeatTimer);
|
|
2560
|
+
this.heartbeatTimer = void 0;
|
|
2561
|
+
}
|
|
2562
|
+
this.currentEntries = void 0;
|
|
2563
|
+
this.cachedKv = void 0;
|
|
2564
|
+
}
|
|
2565
|
+
/** Write entries to KV with per-entry error handling. */
|
|
2566
|
+
async writeEntries(kv, entries) {
|
|
2567
|
+
for (const [key, meta] of entries) {
|
|
2568
|
+
try {
|
|
2569
|
+
await kv.put(key, JSON.stringify(meta));
|
|
2570
|
+
} catch (err) {
|
|
2571
|
+
this.logger.error(`Failed to write metadata entry "${key}"`, err);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
/** Start heartbeat interval that refreshes entries every ttl/2. */
|
|
2576
|
+
startHeartbeat() {
|
|
2577
|
+
if (this.heartbeatTimer) {
|
|
2578
|
+
clearInterval(this.heartbeatTimer);
|
|
2579
|
+
}
|
|
2580
|
+
const interval = Math.floor(this.ttl / 2);
|
|
2581
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2582
|
+
void this.refreshEntries();
|
|
2583
|
+
}, interval);
|
|
2584
|
+
this.heartbeatTimer.unref();
|
|
2585
|
+
}
|
|
2586
|
+
/** Refresh all current entries in KV (heartbeat tick). */
|
|
2587
|
+
async refreshEntries() {
|
|
2588
|
+
if (!this.currentEntries || this.currentEntries.size === 0) return;
|
|
2589
|
+
try {
|
|
2590
|
+
const kv = await this.openBucket();
|
|
2591
|
+
await this.writeEntries(kv, this.currentEntries);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
this.logger.error("Failed to refresh handler metadata in KV", err);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
/** Create or open the KV bucket (cached after first call). */
|
|
2597
|
+
async openBucket() {
|
|
2598
|
+
if (this.cachedKv) return this.cachedKv;
|
|
2599
|
+
const js = this.connection.getJetStreamClient();
|
|
2600
|
+
const kvm = new import_kv.Kvm(js);
|
|
2601
|
+
this.cachedKv = await kvm.create(this.bucketName, {
|
|
2602
|
+
history: DEFAULT_METADATA_HISTORY,
|
|
2603
|
+
replicas: this.replicas,
|
|
2604
|
+
ttl: this.ttl
|
|
2605
|
+
});
|
|
2606
|
+
return this.cachedKv;
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
|
|
1926
2610
|
// src/server/routing/pattern-registry.ts
|
|
1927
|
-
var
|
|
2611
|
+
var import_common10 = require("@nestjs/common");
|
|
1928
2612
|
var HANDLER_LABELS = {
|
|
1929
2613
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
1930
2614
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -1935,7 +2619,7 @@ var PatternRegistry = class {
|
|
|
1935
2619
|
constructor(options) {
|
|
1936
2620
|
this.options = options;
|
|
1937
2621
|
}
|
|
1938
|
-
logger = new
|
|
2622
|
+
logger = new import_common10.Logger("Jetstream:PatternRegistry");
|
|
1939
2623
|
registry = /* @__PURE__ */ new Map();
|
|
1940
2624
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1941
2625
|
cachedPatterns = null;
|
|
@@ -1943,6 +2627,7 @@ var PatternRegistry = class {
|
|
|
1943
2627
|
_hasCommands = false;
|
|
1944
2628
|
_hasBroadcasts = false;
|
|
1945
2629
|
_hasOrdered = false;
|
|
2630
|
+
_hasMetadata = false;
|
|
1946
2631
|
/**
|
|
1947
2632
|
* Register all handlers from the NestJS strategy.
|
|
1948
2633
|
*
|
|
@@ -1955,6 +2640,7 @@ var PatternRegistry = class {
|
|
|
1955
2640
|
const isEvent = handler.isEventHandler ?? false;
|
|
1956
2641
|
const isBroadcast = !!extras?.broadcast;
|
|
1957
2642
|
const isOrdered = !!extras?.ordered;
|
|
2643
|
+
const meta = extras?.meta;
|
|
1958
2644
|
if (isBroadcast && isOrdered) {
|
|
1959
2645
|
throw new Error(
|
|
1960
2646
|
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
@@ -1971,7 +2657,8 @@ var PatternRegistry = class {
|
|
|
1971
2657
|
pattern,
|
|
1972
2658
|
isEvent: isEvent && !isOrdered,
|
|
1973
2659
|
isBroadcast,
|
|
1974
|
-
isOrdered
|
|
2660
|
+
isOrdered,
|
|
2661
|
+
meta
|
|
1975
2662
|
});
|
|
1976
2663
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1977
2664
|
}
|
|
@@ -1980,6 +2667,7 @@ var PatternRegistry = class {
|
|
|
1980
2667
|
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1981
2668
|
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1982
2669
|
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2670
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
1983
2671
|
this.logSummary();
|
|
1984
2672
|
}
|
|
1985
2673
|
/** Find handler for a full NATS subject. */
|
|
@@ -2008,6 +2696,26 @@ var PatternRegistry = class {
|
|
|
2008
2696
|
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2009
2697
|
);
|
|
2010
2698
|
}
|
|
2699
|
+
/** Check if any registered handler has metadata. */
|
|
2700
|
+
hasMetadata() {
|
|
2701
|
+
return this._hasMetadata;
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Get handler metadata entries for KV publishing.
|
|
2705
|
+
*
|
|
2706
|
+
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2707
|
+
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2708
|
+
*/
|
|
2709
|
+
getMetadataEntries() {
|
|
2710
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2711
|
+
for (const entry of this.registry.values()) {
|
|
2712
|
+
if (!entry.meta) continue;
|
|
2713
|
+
const kind = this.resolveStreamKind(entry);
|
|
2714
|
+
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2715
|
+
entries.set(key, entry.meta);
|
|
2716
|
+
}
|
|
2717
|
+
return entries;
|
|
2718
|
+
}
|
|
2011
2719
|
/** Get patterns grouped by kind (cached after registration). */
|
|
2012
2720
|
getPatternsByKind() {
|
|
2013
2721
|
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
@@ -2047,6 +2755,12 @@ var PatternRegistry = class {
|
|
|
2047
2755
|
}
|
|
2048
2756
|
return { events, commands, broadcasts, ordered };
|
|
2049
2757
|
}
|
|
2758
|
+
resolveStreamKind(entry) {
|
|
2759
|
+
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2760
|
+
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2761
|
+
if (entry.isEvent) return "ev" /* Event */;
|
|
2762
|
+
return "cmd" /* Command */;
|
|
2763
|
+
}
|
|
2050
2764
|
logSummary() {
|
|
2051
2765
|
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2052
2766
|
const parts = [
|
|
@@ -2062,10 +2776,10 @@ var PatternRegistry = class {
|
|
|
2062
2776
|
};
|
|
2063
2777
|
|
|
2064
2778
|
// src/server/routing/event.router.ts
|
|
2065
|
-
var
|
|
2066
|
-
var
|
|
2779
|
+
var import_common11 = require("@nestjs/common");
|
|
2780
|
+
var import_transport_node4 = require("@nats-io/transport-node");
|
|
2067
2781
|
var EventRouter = class {
|
|
2068
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
2782
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
2069
2783
|
this.messageProvider = messageProvider;
|
|
2070
2784
|
this.patternRegistry = patternRegistry;
|
|
2071
2785
|
this.codec = codec;
|
|
@@ -2073,8 +2787,10 @@ var EventRouter = class {
|
|
|
2073
2787
|
this.deadLetterConfig = deadLetterConfig;
|
|
2074
2788
|
this.processingConfig = processingConfig;
|
|
2075
2789
|
this.ackWaitMap = ackWaitMap;
|
|
2790
|
+
this.connection = connection;
|
|
2791
|
+
this.options = options;
|
|
2076
2792
|
}
|
|
2077
|
-
logger = new
|
|
2793
|
+
logger = new import_common11.Logger("Jetstream:EventRouter");
|
|
2078
2794
|
subscriptions = [];
|
|
2079
2795
|
/**
|
|
2080
2796
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
@@ -2099,15 +2815,194 @@ var EventRouter = class {
|
|
|
2099
2815
|
}
|
|
2100
2816
|
this.subscriptions.length = 0;
|
|
2101
2817
|
}
|
|
2102
|
-
/** Subscribe to a message stream and route each message. */
|
|
2818
|
+
/** Subscribe to a message stream and route each message to its handler. */
|
|
2103
2819
|
subscribeToStream(stream$, kind) {
|
|
2104
2820
|
const isOrdered = kind === "ordered" /* Ordered */;
|
|
2821
|
+
const patternRegistry = this.patternRegistry;
|
|
2822
|
+
const codec = this.codec;
|
|
2823
|
+
const eventBus = this.eventBus;
|
|
2824
|
+
const logger = this.logger;
|
|
2825
|
+
const deadLetterConfig = this.deadLetterConfig;
|
|
2105
2826
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
2827
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
2106
2828
|
const concurrency = this.getConcurrency(kind);
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
)
|
|
2110
|
-
|
|
2829
|
+
const hasDlqCheck = deadLetterConfig !== void 0;
|
|
2830
|
+
const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
|
|
2831
|
+
const isDeadLetter = (msg) => {
|
|
2832
|
+
if (!hasDlqCheck) return false;
|
|
2833
|
+
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
2834
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
2835
|
+
return msg.info.deliveryCount >= maxDeliver;
|
|
2836
|
+
};
|
|
2837
|
+
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
2838
|
+
const settleSuccess = (msg, ctx) => {
|
|
2839
|
+
if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
|
|
2840
|
+
else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
|
|
2841
|
+
else msg.ack();
|
|
2842
|
+
};
|
|
2843
|
+
const settleFailure = async (msg, data, err) => {
|
|
2844
|
+
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
2845
|
+
await handleDeadLetter(msg, data, err);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
msg.nak();
|
|
2849
|
+
};
|
|
2850
|
+
const resolveEvent = (msg) => {
|
|
2851
|
+
const subject = msg.subject;
|
|
2852
|
+
try {
|
|
2853
|
+
const handler = patternRegistry.getHandler(subject);
|
|
2854
|
+
if (!handler) {
|
|
2855
|
+
msg.term(`No handler for event: ${subject}`);
|
|
2856
|
+
logger.error(`No handler for subject: ${subject}`);
|
|
2857
|
+
return null;
|
|
2858
|
+
}
|
|
2859
|
+
let data;
|
|
2860
|
+
try {
|
|
2861
|
+
data = codec.decode(msg.data);
|
|
2862
|
+
} catch (err) {
|
|
2863
|
+
msg.term("Decode error");
|
|
2864
|
+
logger.error(`Decode error for ${subject}:`, err);
|
|
2865
|
+
return null;
|
|
2866
|
+
}
|
|
2867
|
+
if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2868
|
+
return { handler, data };
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
logger.error(`Unexpected error in ${kind} event router`, err);
|
|
2871
|
+
try {
|
|
2872
|
+
msg.term("Unexpected router error");
|
|
2873
|
+
} catch (termErr) {
|
|
2874
|
+
logger.error(`Failed to terminate message ${subject}:`, termErr);
|
|
2875
|
+
}
|
|
2876
|
+
return null;
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
const handleSafe = (msg) => {
|
|
2880
|
+
const resolved = resolveEvent(msg);
|
|
2881
|
+
if (resolved === null) return void 0;
|
|
2882
|
+
const { handler, data } = resolved;
|
|
2883
|
+
const ctx = new RpcContext([msg]);
|
|
2884
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
2885
|
+
let pending;
|
|
2886
|
+
try {
|
|
2887
|
+
pending = unwrapResult(handler(data, ctx));
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
|
|
2890
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2891
|
+
return settleFailure(msg, data, err);
|
|
2892
|
+
}
|
|
2893
|
+
if (!isPromiseLike(pending)) {
|
|
2894
|
+
settleSuccess(msg, ctx);
|
|
2895
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2896
|
+
return void 0;
|
|
2897
|
+
}
|
|
2898
|
+
return pending.then(
|
|
2899
|
+
() => {
|
|
2900
|
+
settleSuccess(msg, ctx);
|
|
2901
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2902
|
+
},
|
|
2903
|
+
async (err) => {
|
|
2904
|
+
logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
|
|
2905
|
+
try {
|
|
2906
|
+
await settleFailure(msg, data, err);
|
|
2907
|
+
} finally {
|
|
2908
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
);
|
|
2912
|
+
};
|
|
2913
|
+
const handleOrderedSafe = (msg) => {
|
|
2914
|
+
const subject = msg.subject;
|
|
2915
|
+
let handler;
|
|
2916
|
+
let data;
|
|
2917
|
+
try {
|
|
2918
|
+
handler = patternRegistry.getHandler(subject);
|
|
2919
|
+
if (!handler) {
|
|
2920
|
+
logger.error(`No handler for subject: ${subject}`);
|
|
2921
|
+
return void 0;
|
|
2922
|
+
}
|
|
2923
|
+
try {
|
|
2924
|
+
data = codec.decode(msg.data);
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
logger.error(`Decode error for ${subject}:`, err);
|
|
2927
|
+
return void 0;
|
|
2928
|
+
}
|
|
2929
|
+
if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2932
|
+
return void 0;
|
|
2933
|
+
}
|
|
2934
|
+
const ctx = new RpcContext([msg]);
|
|
2935
|
+
const warnIfSettlementAttempted = () => {
|
|
2936
|
+
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
2937
|
+
logger.warn(
|
|
2938
|
+
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2942
|
+
let pending;
|
|
2943
|
+
try {
|
|
2944
|
+
pending = unwrapResult(handler(data, ctx));
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2947
|
+
return void 0;
|
|
2948
|
+
}
|
|
2949
|
+
if (!isPromiseLike(pending)) {
|
|
2950
|
+
warnIfSettlementAttempted();
|
|
2951
|
+
return void 0;
|
|
2952
|
+
}
|
|
2953
|
+
return pending.then(warnIfSettlementAttempted, (err) => {
|
|
2954
|
+
logger.error(`Ordered handler error (${subject}):`, err);
|
|
2955
|
+
});
|
|
2956
|
+
};
|
|
2957
|
+
const route = isOrdered ? handleOrderedSafe : handleSafe;
|
|
2958
|
+
const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
|
|
2959
|
+
const backlogWarnThreshold = 1e3;
|
|
2960
|
+
let active = 0;
|
|
2961
|
+
let backlogWarned = false;
|
|
2962
|
+
const backlog = [];
|
|
2963
|
+
const onAsyncDone = () => {
|
|
2964
|
+
active--;
|
|
2965
|
+
drainBacklog();
|
|
2966
|
+
};
|
|
2967
|
+
const drainBacklog = () => {
|
|
2968
|
+
while (active < maxActive) {
|
|
2969
|
+
const next = backlog.shift();
|
|
2970
|
+
if (next === void 0) return;
|
|
2971
|
+
active++;
|
|
2972
|
+
const result = route(next);
|
|
2973
|
+
if (result !== void 0) {
|
|
2974
|
+
void result.finally(onAsyncDone);
|
|
2975
|
+
} else {
|
|
2976
|
+
active--;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
2980
|
+
};
|
|
2981
|
+
const subscription = stream$.subscribe({
|
|
2982
|
+
next: (msg) => {
|
|
2983
|
+
if (active >= maxActive) {
|
|
2984
|
+
backlog.push(msg);
|
|
2985
|
+
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
2986
|
+
backlogWarned = true;
|
|
2987
|
+
logger.warn(
|
|
2988
|
+
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
active++;
|
|
2994
|
+
const result = route(msg);
|
|
2995
|
+
if (result !== void 0) {
|
|
2996
|
+
void result.finally(onAsyncDone);
|
|
2997
|
+
} else {
|
|
2998
|
+
active--;
|
|
2999
|
+
if (backlog.length > 0) drainBacklog();
|
|
3000
|
+
}
|
|
3001
|
+
},
|
|
3002
|
+
error: (err) => {
|
|
3003
|
+
logger.error(`Stream error in ${kind} router`, err);
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
2111
3006
|
this.subscriptions.push(subscription);
|
|
2112
3007
|
}
|
|
2113
3008
|
getConcurrency(kind) {
|
|
@@ -2120,87 +3015,94 @@ var EventRouter = class {
|
|
|
2120
3015
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
2121
3016
|
return void 0;
|
|
2122
3017
|
}
|
|
2123
|
-
/** Handle a
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
);
|
|
2135
|
-
|
|
2136
|
-
this.logger.error(`Unexpected error in ${kind} event router`, err);
|
|
3018
|
+
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
3019
|
+
/**
|
|
3020
|
+
* Fallback execution for a dead letter when DLQ is disabled, or when
|
|
3021
|
+
* publishing to the DLQ stream fails (due to network or NATS errors).
|
|
3022
|
+
*
|
|
3023
|
+
* Triggers the user-provided `onDeadLetter` hook for logging/alerting.
|
|
3024
|
+
* On success, terminates the message. On error, leaves it unacknowledged (nak)
|
|
3025
|
+
* so NATS can retry the delivery on the next cycle.
|
|
3026
|
+
*/
|
|
3027
|
+
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
3028
|
+
if (!this.deadLetterConfig) {
|
|
3029
|
+
msg.term("Dead letter config unavailable");
|
|
3030
|
+
return;
|
|
2137
3031
|
}
|
|
2138
|
-
}
|
|
2139
|
-
/** Handle an ordered message with error isolation. */
|
|
2140
|
-
async handleOrderedSafe(msg) {
|
|
2141
3032
|
try {
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
} catch (err) {
|
|
2151
|
-
this.logger.error(`Ordered handler error (${msg.subject}):`, err);
|
|
3033
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
3034
|
+
msg.term("Dead letter processed via fallback callback");
|
|
3035
|
+
} catch (hookErr) {
|
|
3036
|
+
this.logger.error(
|
|
3037
|
+
`Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
|
|
3038
|
+
hookErr
|
|
3039
|
+
);
|
|
3040
|
+
msg.nak();
|
|
2152
3041
|
}
|
|
2153
3042
|
}
|
|
2154
|
-
/**
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
3043
|
+
/**
|
|
3044
|
+
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
3045
|
+
*
|
|
3046
|
+
* Appends diagnostic metadata headers to the original message and preserves
|
|
3047
|
+
* the primary payload. If publishing succeeds, it notifies the standard
|
|
3048
|
+
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
3049
|
+
* back to the callback entirely to prevent silent data loss.
|
|
3050
|
+
*/
|
|
3051
|
+
async publishToDlq(msg, info, error) {
|
|
3052
|
+
const serviceName = this.options?.name;
|
|
3053
|
+
if (!this.connection || !serviceName) {
|
|
3054
|
+
this.logger.error(
|
|
3055
|
+
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
3056
|
+
);
|
|
3057
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
3058
|
+
return;
|
|
2161
3059
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
3060
|
+
const destinationSubject = dlqStreamName(serviceName);
|
|
3061
|
+
const hdrs = (0, import_transport_node4.headers)();
|
|
3062
|
+
if (msg.headers) {
|
|
3063
|
+
for (const [k, v] of msg.headers) {
|
|
3064
|
+
for (const val of v) {
|
|
3065
|
+
hdrs.append(k, val);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
2169
3068
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
3069
|
+
let reason = String(error);
|
|
3070
|
+
if (error instanceof Error) {
|
|
3071
|
+
reason = error.message;
|
|
3072
|
+
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
3073
|
+
reason = String(error.message);
|
|
3074
|
+
}
|
|
3075
|
+
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
3076
|
+
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
3077
|
+
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
3078
|
+
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
3079
|
+
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
2176
3080
|
try {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
} else {
|
|
2190
|
-
msg.nak();
|
|
3081
|
+
const js = this.connection.getJetStreamClient();
|
|
3082
|
+
await js.publish(destinationSubject, msg.data, { headers: hdrs });
|
|
3083
|
+
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
3084
|
+
if (this.deadLetterConfig?.onDeadLetter) {
|
|
3085
|
+
try {
|
|
3086
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
3087
|
+
} catch (hookErr) {
|
|
3088
|
+
this.logger.warn(
|
|
3089
|
+
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
3090
|
+
hookErr
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
2191
3093
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
3094
|
+
msg.term("Moved to DLQ stream");
|
|
3095
|
+
} catch (publishErr) {
|
|
3096
|
+
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
3097
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2194
3098
|
}
|
|
2195
3099
|
}
|
|
2196
|
-
/**
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
}
|
|
2203
|
-
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
3100
|
+
/**
|
|
3101
|
+
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
3102
|
+
*
|
|
3103
|
+
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
3104
|
+
* or directly to the fallback callback based on the active module configuration.
|
|
3105
|
+
*/
|
|
2204
3106
|
async handleDeadLetter(msg, data, error) {
|
|
2205
3107
|
const info = {
|
|
2206
3108
|
subject: msg.subject,
|
|
@@ -2213,24 +3115,17 @@ var EventRouter = class {
|
|
|
2213
3115
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
2214
3116
|
};
|
|
2215
3117
|
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
2216
|
-
if (!this.
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
try {
|
|
2221
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
2222
|
-
msg.term("Dead letter processed");
|
|
2223
|
-
} catch (hookErr) {
|
|
2224
|
-
this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
|
|
2225
|
-
msg.nak();
|
|
3118
|
+
if (!this.options?.dlq) {
|
|
3119
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
3120
|
+
} else {
|
|
3121
|
+
await this.publishToDlq(msg, info, error);
|
|
2226
3122
|
}
|
|
2227
3123
|
}
|
|
2228
3124
|
};
|
|
2229
3125
|
|
|
2230
3126
|
// src/server/routing/rpc.router.ts
|
|
2231
|
-
var
|
|
2232
|
-
var
|
|
2233
|
-
var import_rxjs5 = require("rxjs");
|
|
3127
|
+
var import_common12 = require("@nestjs/common");
|
|
3128
|
+
var import_transport_node5 = require("@nats-io/transport-node");
|
|
2234
3129
|
var RpcRouter = class {
|
|
2235
3130
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
2236
3131
|
this.messageProvider = messageProvider;
|
|
@@ -2243,7 +3138,7 @@ var RpcRouter = class {
|
|
|
2243
3138
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
2244
3139
|
this.concurrency = rpcOptions?.concurrency;
|
|
2245
3140
|
}
|
|
2246
|
-
logger = new
|
|
3141
|
+
logger = new import_common12.Logger("Jetstream:RpcRouter");
|
|
2247
3142
|
timeout;
|
|
2248
3143
|
concurrency;
|
|
2249
3144
|
resolvedAckExtensionInterval;
|
|
@@ -2261,105 +3156,203 @@ var RpcRouter = class {
|
|
|
2261
3156
|
/** Start routing command messages to handlers. */
|
|
2262
3157
|
async start() {
|
|
2263
3158
|
this.cachedNc = await this.connection.getConnection();
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
this.
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
this.logger.error(`No handler for RPC subject: ${msg.subject}`);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
const { headers: msgHeaders } = msg;
|
|
2281
|
-
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
2282
|
-
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
2283
|
-
if (!replyTo || !correlationId) {
|
|
2284
|
-
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
2285
|
-
this.logger.error(`Missing headers for RPC: ${msg.subject}`);
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
let data;
|
|
2289
|
-
try {
|
|
2290
|
-
data = this.codec.decode(msg.data);
|
|
2291
|
-
} catch (err) {
|
|
2292
|
-
msg.term("Decode error");
|
|
2293
|
-
this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
|
|
2297
|
-
await this.executeHandler(handler, data, msg, replyTo, correlationId);
|
|
2298
|
-
} catch (err) {
|
|
2299
|
-
this.logger.error("Unexpected error in RPC router", err);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
/** Execute handler, publish response, settle message. */
|
|
2303
|
-
async executeHandler(handler, data, msg, replyTo, correlationId) {
|
|
2304
|
-
const nc = this.cachedNc ?? await this.connection.getConnection();
|
|
2305
|
-
const ctx = new RpcContext([msg]);
|
|
2306
|
-
let settled = false;
|
|
2307
|
-
const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
|
|
2308
|
-
const timeoutId = setTimeout(() => {
|
|
2309
|
-
if (settled) return;
|
|
2310
|
-
settled = true;
|
|
2311
|
-
stopAckExtension?.();
|
|
2312
|
-
this.logger.error(`RPC timeout (${this.timeout}ms): ${msg.subject}`);
|
|
2313
|
-
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, msg.subject, correlationId);
|
|
2314
|
-
msg.term("Handler timeout");
|
|
2315
|
-
}, this.timeout);
|
|
2316
|
-
try {
|
|
2317
|
-
const result = await unwrapResult(handler(data, ctx));
|
|
2318
|
-
if (settled) return;
|
|
2319
|
-
settled = true;
|
|
2320
|
-
clearTimeout(timeoutId);
|
|
2321
|
-
stopAckExtension?.();
|
|
2322
|
-
msg.ack();
|
|
3159
|
+
const nc = this.cachedNc;
|
|
3160
|
+
const patternRegistry = this.patternRegistry;
|
|
3161
|
+
const codec = this.codec;
|
|
3162
|
+
const eventBus = this.eventBus;
|
|
3163
|
+
const logger = this.logger;
|
|
3164
|
+
const timeout = this.timeout;
|
|
3165
|
+
const ackExtensionInterval = this.ackExtensionInterval;
|
|
3166
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
3167
|
+
const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
|
|
3168
|
+
const emitRpcTimeout = (subject, correlationId) => {
|
|
3169
|
+
eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
3170
|
+
};
|
|
3171
|
+
const publishReply = (replyTo, correlationId, payload) => {
|
|
2323
3172
|
try {
|
|
2324
|
-
const hdrs = (0,
|
|
3173
|
+
const hdrs = (0, import_transport_node5.headers)();
|
|
2325
3174
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2326
|
-
nc.publish(replyTo,
|
|
3175
|
+
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
2327
3176
|
} catch (publishErr) {
|
|
2328
|
-
|
|
3177
|
+
logger.error(`Failed to publish RPC response`, publishErr);
|
|
2329
3178
|
}
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
settled = true;
|
|
2333
|
-
clearTimeout(timeoutId);
|
|
2334
|
-
stopAckExtension?.();
|
|
3179
|
+
};
|
|
3180
|
+
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
2335
3181
|
try {
|
|
2336
|
-
const hdrs = (0,
|
|
3182
|
+
const hdrs = (0, import_transport_node5.headers)();
|
|
2337
3183
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2338
3184
|
hdrs.set("x-error" /* Error */, "true");
|
|
2339
|
-
nc.publish(replyTo,
|
|
3185
|
+
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
2340
3186
|
} catch (encodeErr) {
|
|
2341
|
-
|
|
3187
|
+
logger.error(`Failed to encode RPC error for ${subject}`, encodeErr);
|
|
2342
3188
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
3189
|
+
};
|
|
3190
|
+
const resolveCommand = (msg) => {
|
|
3191
|
+
const subject = msg.subject;
|
|
3192
|
+
try {
|
|
3193
|
+
const handler = patternRegistry.getHandler(subject);
|
|
3194
|
+
if (!handler) {
|
|
3195
|
+
msg.term(`No handler for RPC: ${subject}`);
|
|
3196
|
+
logger.error(`No handler for RPC subject: ${subject}`);
|
|
3197
|
+
return null;
|
|
3198
|
+
}
|
|
3199
|
+
const msgHeaders = msg.headers;
|
|
3200
|
+
const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
|
|
3201
|
+
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
3202
|
+
if (!replyTo || !correlationId) {
|
|
3203
|
+
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
3204
|
+
logger.error(`Missing headers for RPC: ${subject}`);
|
|
3205
|
+
return null;
|
|
3206
|
+
}
|
|
3207
|
+
let data;
|
|
3208
|
+
try {
|
|
3209
|
+
data = codec.decode(msg.data);
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
msg.term("Decode error");
|
|
3212
|
+
logger.error(`Decode error for RPC ${subject}:`, err);
|
|
3213
|
+
return null;
|
|
3214
|
+
}
|
|
3215
|
+
eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
|
|
3216
|
+
return { handler, data, replyTo, correlationId };
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
logger.error("Unexpected error in RPC router", err);
|
|
3219
|
+
try {
|
|
3220
|
+
msg.term("Unexpected router error");
|
|
3221
|
+
} catch (termErr) {
|
|
3222
|
+
logger.error(`Failed to terminate RPC message ${subject}:`, termErr);
|
|
3223
|
+
}
|
|
3224
|
+
return null;
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
const handleSafe = (msg) => {
|
|
3228
|
+
const resolved = resolveCommand(msg);
|
|
3229
|
+
if (resolved === null) return void 0;
|
|
3230
|
+
const { handler, data, replyTo, correlationId } = resolved;
|
|
3231
|
+
const subject = msg.subject;
|
|
3232
|
+
const ctx = new RpcContext([msg]);
|
|
3233
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
3234
|
+
let pending;
|
|
3235
|
+
try {
|
|
3236
|
+
pending = unwrapResult(handler(data, ctx));
|
|
3237
|
+
} catch (err) {
|
|
3238
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3239
|
+
logger.error(`RPC handler error (${subject}):`, err);
|
|
3240
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
3241
|
+
msg.term(`Handler error: ${subject}`);
|
|
3242
|
+
return void 0;
|
|
3243
|
+
}
|
|
3244
|
+
if (!isPromiseLike(pending)) {
|
|
3245
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3246
|
+
msg.ack();
|
|
3247
|
+
publishReply(replyTo, correlationId, pending);
|
|
3248
|
+
return void 0;
|
|
3249
|
+
}
|
|
3250
|
+
let settled = false;
|
|
3251
|
+
const timeoutId = setTimeout(() => {
|
|
3252
|
+
if (settled) return;
|
|
3253
|
+
settled = true;
|
|
3254
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3255
|
+
logger.error(`RPC timeout (${timeout}ms): ${subject}`);
|
|
3256
|
+
emitRpcTimeout(subject, correlationId);
|
|
3257
|
+
msg.term("Handler timeout");
|
|
3258
|
+
}, timeout);
|
|
3259
|
+
return pending.then(
|
|
3260
|
+
(result) => {
|
|
3261
|
+
if (settled) return;
|
|
3262
|
+
settled = true;
|
|
3263
|
+
clearTimeout(timeoutId);
|
|
3264
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3265
|
+
msg.ack();
|
|
3266
|
+
publishReply(replyTo, correlationId, result);
|
|
3267
|
+
},
|
|
3268
|
+
(err) => {
|
|
3269
|
+
if (settled) return;
|
|
3270
|
+
settled = true;
|
|
3271
|
+
clearTimeout(timeoutId);
|
|
3272
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
3273
|
+
logger.error(`RPC handler error (${subject}):`, err);
|
|
3274
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
3275
|
+
msg.term(`Handler error: ${subject}`);
|
|
3276
|
+
}
|
|
3277
|
+
);
|
|
3278
|
+
};
|
|
3279
|
+
const backlogWarnThreshold = 1e3;
|
|
3280
|
+
let active = 0;
|
|
3281
|
+
let backlogWarned = false;
|
|
3282
|
+
const backlog = [];
|
|
3283
|
+
const onAsyncDone = () => {
|
|
3284
|
+
active--;
|
|
3285
|
+
drainBacklog();
|
|
3286
|
+
};
|
|
3287
|
+
const drainBacklog = () => {
|
|
3288
|
+
while (active < maxActive) {
|
|
3289
|
+
const next = backlog.shift();
|
|
3290
|
+
if (next === void 0) return;
|
|
3291
|
+
active++;
|
|
3292
|
+
const result = handleSafe(next);
|
|
3293
|
+
if (result !== void 0) {
|
|
3294
|
+
void result.finally(onAsyncDone);
|
|
3295
|
+
} else {
|
|
3296
|
+
active--;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
3300
|
+
};
|
|
3301
|
+
this.subscription = this.messageProvider.commands$.subscribe({
|
|
3302
|
+
next: (msg) => {
|
|
3303
|
+
if (active >= maxActive) {
|
|
3304
|
+
backlog.push(msg);
|
|
3305
|
+
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
3306
|
+
backlogWarned = true;
|
|
3307
|
+
logger.warn(
|
|
3308
|
+
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
active++;
|
|
3314
|
+
const result = handleSafe(msg);
|
|
3315
|
+
if (result !== void 0) {
|
|
3316
|
+
void result.finally(onAsyncDone);
|
|
3317
|
+
} else {
|
|
3318
|
+
active--;
|
|
3319
|
+
if (backlog.length > 0) drainBacklog();
|
|
3320
|
+
}
|
|
3321
|
+
},
|
|
3322
|
+
error: (err) => {
|
|
3323
|
+
logger.error("Stream error in RPC router", err);
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
/** Stop routing and unsubscribe. */
|
|
3328
|
+
destroy() {
|
|
3329
|
+
this.subscription?.unsubscribe();
|
|
3330
|
+
this.subscription = null;
|
|
2345
3331
|
}
|
|
2346
3332
|
};
|
|
2347
3333
|
|
|
2348
3334
|
// src/shutdown/shutdown.manager.ts
|
|
2349
|
-
var
|
|
3335
|
+
var import_common13 = require("@nestjs/common");
|
|
2350
3336
|
var ShutdownManager = class {
|
|
2351
3337
|
constructor(connection, eventBus, timeout) {
|
|
2352
3338
|
this.connection = connection;
|
|
2353
3339
|
this.eventBus = eventBus;
|
|
2354
3340
|
this.timeout = timeout;
|
|
2355
3341
|
}
|
|
2356
|
-
logger = new
|
|
3342
|
+
logger = new import_common13.Logger("Jetstream:Shutdown");
|
|
3343
|
+
shutdownPromise;
|
|
2357
3344
|
/**
|
|
2358
3345
|
* Execute the full shutdown sequence.
|
|
2359
3346
|
*
|
|
3347
|
+
* Idempotent — concurrent or repeated calls return the same promise.
|
|
3348
|
+
*
|
|
2360
3349
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
2361
3350
|
*/
|
|
2362
3351
|
async shutdown(strategy) {
|
|
3352
|
+
this.shutdownPromise ??= this.doShutdown(strategy);
|
|
3353
|
+
return this.shutdownPromise;
|
|
3354
|
+
}
|
|
3355
|
+
async doShutdown(strategy) {
|
|
2363
3356
|
this.eventBus.emit("shutdownStart" /* ShutdownStart */);
|
|
2364
3357
|
this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
|
|
2365
3358
|
strategy?.close();
|
|
@@ -2494,7 +3487,7 @@ var JetstreamModule = class {
|
|
|
2494
3487
|
provide: JETSTREAM_EVENT_BUS,
|
|
2495
3488
|
inject: [JETSTREAM_OPTIONS],
|
|
2496
3489
|
useFactory: (options) => {
|
|
2497
|
-
const logger = new
|
|
3490
|
+
const logger = new import_common14.Logger("Jetstream:Module");
|
|
2498
3491
|
return new EventBus(logger, options.hooks);
|
|
2499
3492
|
}
|
|
2500
3493
|
},
|
|
@@ -2573,8 +3566,8 @@ var JetstreamModule = class {
|
|
|
2573
3566
|
// MessageProvider — pull-based message consumption
|
|
2574
3567
|
{
|
|
2575
3568
|
provide: MessageProvider,
|
|
2576
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
|
|
2577
|
-
useFactory: (options, connection, eventBus) => {
|
|
3569
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
|
|
3570
|
+
useFactory: (options, connection, eventBus, consumerProvider) => {
|
|
2578
3571
|
if (options.consumer === false) return null;
|
|
2579
3572
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
2580
3573
|
if (options.events?.consume)
|
|
@@ -2584,7 +3577,11 @@ var JetstreamModule = class {
|
|
|
2584
3577
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
2585
3578
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
2586
3579
|
}
|
|
2587
|
-
|
|
3580
|
+
const consumerRecoveryFn = consumerProvider ? async (kind) => {
|
|
3581
|
+
const jsm = await connection.getJetStreamManager();
|
|
3582
|
+
return consumerProvider.recoverConsumer(jsm, kind);
|
|
3583
|
+
} : void 0;
|
|
3584
|
+
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
2588
3585
|
}
|
|
2589
3586
|
},
|
|
2590
3587
|
// EventRouter — routes event and broadcast messages to handlers
|
|
@@ -2596,9 +3593,10 @@ var JetstreamModule = class {
|
|
|
2596
3593
|
PatternRegistry,
|
|
2597
3594
|
JETSTREAM_CODEC,
|
|
2598
3595
|
JETSTREAM_EVENT_BUS,
|
|
2599
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3596
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3597
|
+
JETSTREAM_CONNECTION
|
|
2600
3598
|
],
|
|
2601
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
|
|
3599
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
2602
3600
|
if (options.consumer === false) return null;
|
|
2603
3601
|
const deadLetterConfig = options.onDeadLetter ? {
|
|
2604
3602
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -2621,7 +3619,9 @@ var JetstreamModule = class {
|
|
|
2621
3619
|
eventBus,
|
|
2622
3620
|
deadLetterConfig,
|
|
2623
3621
|
processingConfig,
|
|
2624
|
-
ackWaitMap
|
|
3622
|
+
ackWaitMap,
|
|
3623
|
+
connection,
|
|
3624
|
+
options
|
|
2625
3625
|
);
|
|
2626
3626
|
}
|
|
2627
3627
|
},
|
|
@@ -2670,6 +3670,15 @@ var JetstreamModule = class {
|
|
|
2670
3670
|
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
2671
3671
|
}
|
|
2672
3672
|
},
|
|
3673
|
+
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
3674
|
+
{
|
|
3675
|
+
provide: MetadataProvider,
|
|
3676
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
3677
|
+
useFactory: (options, connection) => {
|
|
3678
|
+
if (options.consumer === false) return null;
|
|
3679
|
+
return new MetadataProvider(options, connection);
|
|
3680
|
+
}
|
|
3681
|
+
},
|
|
2673
3682
|
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
2674
3683
|
{
|
|
2675
3684
|
provide: JetstreamStrategy,
|
|
@@ -2683,9 +3692,10 @@ var JetstreamModule = class {
|
|
|
2683
3692
|
EventRouter,
|
|
2684
3693
|
RpcRouter,
|
|
2685
3694
|
CoreRpcServer,
|
|
2686
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3695
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3696
|
+
MetadataProvider
|
|
2687
3697
|
],
|
|
2688
|
-
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
|
|
3698
|
+
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
|
|
2689
3699
|
if (options.consumer === false) return null;
|
|
2690
3700
|
return new JetstreamStrategy(
|
|
2691
3701
|
options,
|
|
@@ -2697,7 +3707,8 @@ var JetstreamModule = class {
|
|
|
2697
3707
|
eventRouter,
|
|
2698
3708
|
rpcRouter,
|
|
2699
3709
|
coreRpcServer,
|
|
2700
|
-
ackWaitMap
|
|
3710
|
+
ackWaitMap,
|
|
3711
|
+
metadataProvider
|
|
2701
3712
|
);
|
|
2702
3713
|
}
|
|
2703
3714
|
}
|
|
@@ -2756,21 +3767,35 @@ var JetstreamModule = class {
|
|
|
2756
3767
|
}
|
|
2757
3768
|
};
|
|
2758
3769
|
JetstreamModule = __decorateClass([
|
|
2759
|
-
(0,
|
|
2760
|
-
(0,
|
|
2761
|
-
__decorateParam(0, (0,
|
|
2762
|
-
__decorateParam(0, (0,
|
|
2763
|
-
__decorateParam(1, (0,
|
|
2764
|
-
__decorateParam(1, (0,
|
|
3770
|
+
(0, import_common14.Global)(),
|
|
3771
|
+
(0, import_common14.Module)({}),
|
|
3772
|
+
__decorateParam(0, (0, import_common14.Optional)()),
|
|
3773
|
+
__decorateParam(0, (0, import_common14.Inject)(ShutdownManager)),
|
|
3774
|
+
__decorateParam(1, (0, import_common14.Optional)()),
|
|
3775
|
+
__decorateParam(1, (0, import_common14.Inject)(JetstreamStrategy))
|
|
2765
3776
|
], JetstreamModule);
|
|
2766
3777
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2767
3778
|
0 && (module.exports = {
|
|
2768
|
-
|
|
3779
|
+
DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
3780
|
+
DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
3781
|
+
DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
3782
|
+
DEFAULT_COMMAND_STREAM_CONFIG,
|
|
3783
|
+
DEFAULT_DLQ_STREAM_CONFIG,
|
|
3784
|
+
DEFAULT_EVENT_CONSUMER_CONFIG,
|
|
3785
|
+
DEFAULT_EVENT_STREAM_CONFIG,
|
|
3786
|
+
DEFAULT_JETSTREAM_RPC_TIMEOUT,
|
|
3787
|
+
DEFAULT_METADATA_BUCKET,
|
|
3788
|
+
DEFAULT_METADATA_HISTORY,
|
|
3789
|
+
DEFAULT_METADATA_REPLICAS,
|
|
3790
|
+
DEFAULT_METADATA_TTL,
|
|
3791
|
+
DEFAULT_ORDERED_STREAM_CONFIG,
|
|
3792
|
+
DEFAULT_RPC_TIMEOUT,
|
|
3793
|
+
DEFAULT_SHUTDOWN_TIMEOUT,
|
|
2769
3794
|
JETSTREAM_CODEC,
|
|
2770
3795
|
JETSTREAM_CONNECTION,
|
|
2771
|
-
JETSTREAM_EVENT_BUS,
|
|
2772
3796
|
JETSTREAM_OPTIONS,
|
|
2773
3797
|
JetstreamClient,
|
|
3798
|
+
JetstreamDlqHeader,
|
|
2774
3799
|
JetstreamHeader,
|
|
2775
3800
|
JetstreamHealthIndicator,
|
|
2776
3801
|
JetstreamModule,
|
|
@@ -2778,17 +3803,24 @@ JetstreamModule = __decorateClass([
|
|
|
2778
3803
|
JetstreamRecordBuilder,
|
|
2779
3804
|
JetstreamStrategy,
|
|
2780
3805
|
JsonCodec,
|
|
3806
|
+
MIN_METADATA_TTL,
|
|
2781
3807
|
MessageKind,
|
|
3808
|
+
MsgpackCodec,
|
|
3809
|
+
NatsErrorCode,
|
|
2782
3810
|
PatternPrefix,
|
|
3811
|
+
RESERVED_HEADERS,
|
|
2783
3812
|
RpcContext,
|
|
2784
3813
|
StreamKind,
|
|
2785
3814
|
TransportEvent,
|
|
3815
|
+
buildBroadcastSubject,
|
|
2786
3816
|
buildSubject,
|
|
2787
3817
|
consumerName,
|
|
3818
|
+
dlqStreamName,
|
|
2788
3819
|
getClientToken,
|
|
2789
3820
|
internalName,
|
|
2790
3821
|
isCoreRpcMode,
|
|
2791
3822
|
isJetStreamRpcMode,
|
|
3823
|
+
metadataKey,
|
|
2792
3824
|
streamName,
|
|
2793
3825
|
toNanos
|
|
2794
3826
|
});
|