@horizon-republic/nestjs-jetstream 2.12.1 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1521 -1043
- package/dist/index.d.cts +291 -268
- package/dist/index.d.ts +291 -268
- package/dist/index.js +1458 -980
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -71,6 +71,7 @@ __export(index_exports, {
|
|
|
71
71
|
JetstreamTrace: () => JetstreamTrace,
|
|
72
72
|
JsonCodec: () => JsonCodec,
|
|
73
73
|
MIN_METADATA_TTL: () => MIN_METADATA_TTL,
|
|
74
|
+
ManagementMode: () => ManagementMode,
|
|
74
75
|
MessageKind: () => MessageKind,
|
|
75
76
|
MsgpackCodec: () => MsgpackCodec,
|
|
76
77
|
NatsErrorCode: () => NatsErrorCode,
|
|
@@ -96,14 +97,14 @@ __export(index_exports, {
|
|
|
96
97
|
module.exports = __toCommonJS(index_exports);
|
|
97
98
|
|
|
98
99
|
// src/jetstream.module.ts
|
|
99
|
-
var
|
|
100
|
+
var import_common24 = require("@nestjs/common");
|
|
100
101
|
|
|
101
102
|
// src/client/jetstream.client.ts
|
|
102
|
-
var
|
|
103
|
+
var import_common6 = require("@nestjs/common");
|
|
103
104
|
var import_microservices = require("@nestjs/microservices");
|
|
104
105
|
var import_api8 = require("@opentelemetry/api");
|
|
105
106
|
var import_nuid = require("@nats-io/nuid");
|
|
106
|
-
var
|
|
107
|
+
var import_transport_node2 = require("@nats-io/transport-node");
|
|
107
108
|
|
|
108
109
|
// src/interfaces/hooks.interface.ts
|
|
109
110
|
var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
|
|
@@ -128,6 +129,13 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
|
|
|
128
129
|
return TransportEvent2;
|
|
129
130
|
})(TransportEvent || {});
|
|
130
131
|
|
|
132
|
+
// src/interfaces/options.interface.ts
|
|
133
|
+
var ManagementMode = /* @__PURE__ */ ((ManagementMode2) => {
|
|
134
|
+
ManagementMode2["Auto"] = "auto";
|
|
135
|
+
ManagementMode2["Manual"] = "manual";
|
|
136
|
+
return ManagementMode2;
|
|
137
|
+
})(ManagementMode || {});
|
|
138
|
+
|
|
131
139
|
// src/interfaces/stream.interface.ts
|
|
132
140
|
var StreamKind = /* @__PURE__ */ ((StreamKind2) => {
|
|
133
141
|
StreamKind2["Event"] = "ev";
|
|
@@ -277,8 +285,27 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
|
277
285
|
]);
|
|
278
286
|
var NATS_CONTROL_HEADER_PREFIX = "nats-";
|
|
279
287
|
var internalName = (name) => `${name}__microservice`;
|
|
280
|
-
var
|
|
281
|
-
var
|
|
288
|
+
var subjectPrefix = (serviceName, kind) => `${internalName(serviceName)}.${kind}.`;
|
|
289
|
+
var buildSubject = (serviceName, kind, pattern) => `${subjectPrefix(serviceName, kind)}${pattern}`;
|
|
290
|
+
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
291
|
+
var SCHEDULE_SEGMENT = "_sch.";
|
|
292
|
+
var buildBroadcastSubject = (pattern) => `${BROADCAST_SUBJECT_PREFIX}${pattern}`;
|
|
293
|
+
var conventionScheduleSubjectBase = (targetName, eventSubject) => {
|
|
294
|
+
if (eventSubject.startsWith(BROADCAST_SUBJECT_PREFIX)) {
|
|
295
|
+
const bare = eventSubject.slice(BROADCAST_SUBJECT_PREFIX.length);
|
|
296
|
+
return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}${bare}`;
|
|
297
|
+
}
|
|
298
|
+
const targetPrefix = `${internalName(targetName)}.`;
|
|
299
|
+
if (!eventSubject.startsWith(targetPrefix)) {
|
|
300
|
+
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
301
|
+
}
|
|
302
|
+
const withoutPrefix = eventSubject.slice(targetPrefix.length);
|
|
303
|
+
const dotIndex = withoutPrefix.indexOf(".");
|
|
304
|
+
if (dotIndex === -1) {
|
|
305
|
+
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
306
|
+
}
|
|
307
|
+
return `${targetPrefix}${SCHEDULE_SEGMENT}${withoutPrefix.slice(dotIndex + 1)}`;
|
|
308
|
+
};
|
|
282
309
|
var streamName = (serviceName, kind) => {
|
|
283
310
|
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
284
311
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
@@ -298,6 +325,118 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
|
|
|
298
325
|
var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
|
|
299
326
|
var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
300
327
|
|
|
328
|
+
// src/server/infrastructure/management.ts
|
|
329
|
+
var kindOptionsBlock = (options, kind) => {
|
|
330
|
+
switch (kind) {
|
|
331
|
+
case "ev" /* Event */:
|
|
332
|
+
return options.events;
|
|
333
|
+
case "broadcast" /* Broadcast */:
|
|
334
|
+
return options.broadcast;
|
|
335
|
+
case "ordered" /* Ordered */:
|
|
336
|
+
return options.ordered;
|
|
337
|
+
case "cmd" /* Command */:
|
|
338
|
+
return isJetStreamRpcMode(options.rpc) ? options.rpc : void 0;
|
|
339
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
340
|
+
default: {
|
|
341
|
+
const _exhaustive = kind;
|
|
342
|
+
throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var entityManagementFor = (options, kind) => {
|
|
347
|
+
if (kind === "dlq") return options.dlq?.management;
|
|
348
|
+
return kindOptionsBlock(options, kind)?.management;
|
|
349
|
+
};
|
|
350
|
+
var resolveManagementMode = (options, kind, entity) => entityManagementFor(options, kind)?.[entity] ?? options.provisioning?.management ?? "auto" /* Auto */;
|
|
351
|
+
|
|
352
|
+
// src/server/infrastructure/name-resolver.ts
|
|
353
|
+
var NameResolver = class {
|
|
354
|
+
constructor(options) {
|
|
355
|
+
this.options = options;
|
|
356
|
+
this.dlq = options.dlq?.stream?.name ?? dlqStreamName(options.name);
|
|
357
|
+
this.kinds = this.buildKindMap();
|
|
358
|
+
}
|
|
359
|
+
kinds;
|
|
360
|
+
dlq;
|
|
361
|
+
streamName(kind) {
|
|
362
|
+
return this.get(kind).stream;
|
|
363
|
+
}
|
|
364
|
+
consumerName(kind) {
|
|
365
|
+
return this.get(kind).consumer;
|
|
366
|
+
}
|
|
367
|
+
dlqStreamName() {
|
|
368
|
+
return this.dlq;
|
|
369
|
+
}
|
|
370
|
+
subject(kind, pattern) {
|
|
371
|
+
return `${this.get(kind).prefix}${pattern}`;
|
|
372
|
+
}
|
|
373
|
+
filterSubject(kind) {
|
|
374
|
+
return `${this.get(kind).prefix}>`;
|
|
375
|
+
}
|
|
376
|
+
schedulePrefix(kind) {
|
|
377
|
+
return this.get(kind).schedulePrefix;
|
|
378
|
+
}
|
|
379
|
+
hasCustomPrefix(kind) {
|
|
380
|
+
return this.get(kind).custom;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Map a resolved event subject back to its schedule-holder base subject
|
|
384
|
+
* (the `_sch` namespace twin, without the per-message unique suffix).
|
|
385
|
+
*/
|
|
386
|
+
scheduleSubjectBase(eventSubject) {
|
|
387
|
+
for (const kind of ["broadcast" /* Broadcast */, "ev" /* Event */, "ordered" /* Ordered */]) {
|
|
388
|
+
const { prefix, schedulePrefix } = this.get(kind);
|
|
389
|
+
if (eventSubject.startsWith(prefix)) {
|
|
390
|
+
return `${schedulePrefix}${eventSubject.slice(prefix.length)}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
394
|
+
}
|
|
395
|
+
get(kind) {
|
|
396
|
+
const entry = this.kinds.get(kind);
|
|
397
|
+
if (!entry) throw new Error(`Unknown StreamKind: ${String(kind)}`);
|
|
398
|
+
return entry;
|
|
399
|
+
}
|
|
400
|
+
buildKindMap() {
|
|
401
|
+
const map = /* @__PURE__ */ new Map();
|
|
402
|
+
const { name } = this.options;
|
|
403
|
+
for (const kind of Object.values(StreamKind)) {
|
|
404
|
+
const block = kindOptionsBlock(this.options, kind);
|
|
405
|
+
const customPrefix = block?.subjectPrefix;
|
|
406
|
+
const custom = customPrefix !== void 0;
|
|
407
|
+
const prefix = custom ? this.normalizePrefix(customPrefix) : this.conventionPrefix(name, kind);
|
|
408
|
+
map.set(kind, {
|
|
409
|
+
stream: block?.stream?.name ?? streamName(name, kind),
|
|
410
|
+
consumer: block?.consumer?.durable_name ?? consumerName(name, kind),
|
|
411
|
+
prefix,
|
|
412
|
+
schedulePrefix: custom ? `${prefix}${SCHEDULE_SEGMENT}` : this.conventionSchedulePrefix(name, kind),
|
|
413
|
+
custom
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return map;
|
|
417
|
+
}
|
|
418
|
+
normalizePrefix(raw) {
|
|
419
|
+
let end = raw.length;
|
|
420
|
+
while (end > 0 && raw[end - 1] === ".") end -= 1;
|
|
421
|
+
const trimmed = raw.slice(0, end);
|
|
422
|
+
const tokens = trimmed.split(".");
|
|
423
|
+
if (trimmed.length === 0 || tokens.some((t) => t.length === 0 || t === ">")) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Invalid subjectPrefix "${raw}": expected non-empty dot-separated tokens without ">".`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return `${trimmed}.`;
|
|
429
|
+
}
|
|
430
|
+
conventionPrefix(name, kind) {
|
|
431
|
+
if (kind === "broadcast" /* Broadcast */) return BROADCAST_SUBJECT_PREFIX;
|
|
432
|
+
return subjectPrefix(name, kind);
|
|
433
|
+
}
|
|
434
|
+
conventionSchedulePrefix(name, kind) {
|
|
435
|
+
if (kind === "broadcast" /* Broadcast */) return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}`;
|
|
436
|
+
return `${internalName(name)}.${SCHEDULE_SEGMENT}`;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
301
440
|
// src/otel/constants.ts
|
|
302
441
|
var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
|
|
303
442
|
|
|
@@ -598,7 +737,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
|
|
|
598
737
|
|
|
599
738
|
// src/otel/tracer.ts
|
|
600
739
|
var import_api2 = require("@opentelemetry/api");
|
|
601
|
-
var PACKAGE_VERSION = true ? "2.
|
|
740
|
+
var PACKAGE_VERSION = true ? "2.13.0" : "0.0.0";
|
|
602
741
|
var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
|
|
603
742
|
|
|
604
743
|
// src/otel/carrier.ts
|
|
@@ -1391,17 +1530,135 @@ var nanosToGoDuration = (nanos) => {
|
|
|
1391
1530
|
return `${nanos}ns`;
|
|
1392
1531
|
};
|
|
1393
1532
|
|
|
1533
|
+
// src/client/rpc-reply-inbox.ts
|
|
1534
|
+
var import_common5 = require("@nestjs/common");
|
|
1535
|
+
var import_transport_node = require("@nats-io/transport-node");
|
|
1536
|
+
var RpcReplyInbox = class {
|
|
1537
|
+
constructor(codec, inboxPrefix) {
|
|
1538
|
+
this.codec = codec;
|
|
1539
|
+
this.inboxPrefix = inboxPrefix;
|
|
1540
|
+
}
|
|
1541
|
+
logger = new import_common5.Logger("Jetstream:RpcInbox");
|
|
1542
|
+
inbox = null;
|
|
1543
|
+
subscription = null;
|
|
1544
|
+
pending = /* @__PURE__ */ new Map();
|
|
1545
|
+
timeouts = /* @__PURE__ */ new Map();
|
|
1546
|
+
/** Reply-to subject for outgoing requests; null until {@link setup} ran. */
|
|
1547
|
+
get address() {
|
|
1548
|
+
return this.inbox;
|
|
1549
|
+
}
|
|
1550
|
+
/** True while the inbox subscription is live. */
|
|
1551
|
+
get active() {
|
|
1552
|
+
return this.subscription !== null;
|
|
1553
|
+
}
|
|
1554
|
+
/** Create the inbox subject and subscribe reply routing on it. */
|
|
1555
|
+
setup(nc) {
|
|
1556
|
+
this.inbox = (0, import_transport_node.createInbox)(this.inboxPrefix);
|
|
1557
|
+
this.subscription = nc.subscribe(this.inbox, {
|
|
1558
|
+
callback: (err, msg) => {
|
|
1559
|
+
if (err) {
|
|
1560
|
+
this.logger.error("Inbox subscription error:", err);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
this.route(msg);
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
this.logger.debug(`Inbox subscription: ${this.inbox}`);
|
|
1567
|
+
}
|
|
1568
|
+
/** Register the callback that settles the round-trip for `correlationId`. */
|
|
1569
|
+
register(correlationId, callback) {
|
|
1570
|
+
this.pending.set(correlationId, callback);
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Arm the request deadline. When it fires while the request is still
|
|
1574
|
+
* pending, both registry entries are removed before `onExpired` runs.
|
|
1575
|
+
*/
|
|
1576
|
+
armTimeout(correlationId, ms, onExpired) {
|
|
1577
|
+
const existing = this.timeouts.get(correlationId);
|
|
1578
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
1579
|
+
const timeoutId = setTimeout(() => {
|
|
1580
|
+
if (!this.pending.has(correlationId)) return;
|
|
1581
|
+
this.timeouts.delete(correlationId);
|
|
1582
|
+
this.pending.delete(correlationId);
|
|
1583
|
+
onExpired();
|
|
1584
|
+
}, ms);
|
|
1585
|
+
this.timeouts.set(correlationId, timeoutId);
|
|
1586
|
+
}
|
|
1587
|
+
/** True while the request has not been settled or discarded. */
|
|
1588
|
+
has(correlationId) {
|
|
1589
|
+
return this.pending.has(correlationId);
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Drop a request without invoking its callback: clears the deadline and
|
|
1593
|
+
* returns whether the request was still pending.
|
|
1594
|
+
*/
|
|
1595
|
+
discard(correlationId) {
|
|
1596
|
+
const timeoutId = this.timeouts.get(correlationId);
|
|
1597
|
+
if (timeoutId !== void 0) {
|
|
1598
|
+
clearTimeout(timeoutId);
|
|
1599
|
+
this.timeouts.delete(correlationId);
|
|
1600
|
+
}
|
|
1601
|
+
return this.pending.delete(correlationId);
|
|
1602
|
+
}
|
|
1603
|
+
/** Fail every pending request with `error` and tear the inbox down. */
|
|
1604
|
+
rejectAll(error) {
|
|
1605
|
+
for (const callback of this.pending.values()) {
|
|
1606
|
+
callback({ err: error, response: null, isDisposed: true });
|
|
1607
|
+
}
|
|
1608
|
+
for (const timeoutId of this.timeouts.values()) {
|
|
1609
|
+
clearTimeout(timeoutId);
|
|
1610
|
+
}
|
|
1611
|
+
this.pending.clear();
|
|
1612
|
+
this.timeouts.clear();
|
|
1613
|
+
this.subscription?.unsubscribe();
|
|
1614
|
+
this.subscription = null;
|
|
1615
|
+
this.inbox = null;
|
|
1616
|
+
}
|
|
1617
|
+
/** Route an inbox reply to the matching pending callback. */
|
|
1618
|
+
route(msg) {
|
|
1619
|
+
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
1620
|
+
if (!correlationId) {
|
|
1621
|
+
this.logger.warn("Inbox reply without correlation-id, ignoring");
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const callback = this.pending.get(correlationId);
|
|
1625
|
+
if (!callback) {
|
|
1626
|
+
this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const timeoutId = this.timeouts.get(correlationId);
|
|
1630
|
+
if (timeoutId) {
|
|
1631
|
+
clearTimeout(timeoutId);
|
|
1632
|
+
this.timeouts.delete(correlationId);
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const decoded = this.codec.decode(msg.data);
|
|
1636
|
+
if (msg.headers?.get("x-error" /* Error */)) {
|
|
1637
|
+
callback({ err: decoded, response: null, isDisposed: true });
|
|
1638
|
+
} else {
|
|
1639
|
+
callback({ err: null, response: decoded, isDisposed: true });
|
|
1640
|
+
}
|
|
1641
|
+
} catch (err) {
|
|
1642
|
+
callback({
|
|
1643
|
+
err: err instanceof Error ? err : new Error("Decode error"),
|
|
1644
|
+
response: null,
|
|
1645
|
+
isDisposed: true
|
|
1646
|
+
});
|
|
1647
|
+
} finally {
|
|
1648
|
+
this.pending.delete(correlationId);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1394
1653
|
// src/client/jetstream.client.ts
|
|
1395
|
-
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
1396
1654
|
var detectEventKind = (pattern) => {
|
|
1397
1655
|
if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
|
|
1398
1656
|
if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
|
|
1399
1657
|
return "event" /* Event */;
|
|
1400
1658
|
};
|
|
1401
|
-
var declaredEventPattern = (pattern) => {
|
|
1402
|
-
if (pattern.
|
|
1403
|
-
|
|
1404
|
-
if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
|
|
1659
|
+
var declaredEventPattern = (pattern, kind) => {
|
|
1660
|
+
if (kind === "broadcast" /* Broadcast */) return pattern.slice("broadcast:" /* Broadcast */.length);
|
|
1661
|
+
if (kind === "ordered" /* Ordered */) return pattern.slice("ordered:" /* Ordered */.length);
|
|
1405
1662
|
return pattern;
|
|
1406
1663
|
};
|
|
1407
1664
|
var eventStreamKind = (kind) => {
|
|
@@ -1410,7 +1667,7 @@ var eventStreamKind = (kind) => {
|
|
|
1410
1667
|
return "ev" /* Event */;
|
|
1411
1668
|
};
|
|
1412
1669
|
var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
1413
|
-
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
1670
|
+
constructor(rootOptions, targetServiceName, connection, codec, eventBus, names) {
|
|
1414
1671
|
super();
|
|
1415
1672
|
this.rootOptions = rootOptions;
|
|
1416
1673
|
this.connection = connection;
|
|
@@ -1418,29 +1675,34 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1418
1675
|
this.eventBus = eventBus;
|
|
1419
1676
|
this.targetName = targetServiceName;
|
|
1420
1677
|
this.callerName = internalName(this.rootOptions.name);
|
|
1678
|
+
const isSelf = targetServiceName === rootOptions.name;
|
|
1679
|
+
const ownNames = names ?? (isSelf ? new NameResolver(rootOptions) : null);
|
|
1680
|
+
this.selfNames = isSelf ? ownNames : null;
|
|
1681
|
+
this.broadcastPrefix = ownNames ? ownNames.subject("broadcast" /* Broadcast */, "") : BROADCAST_SUBJECT_PREFIX;
|
|
1421
1682
|
const targetInternal = internalName(targetServiceName);
|
|
1422
1683
|
this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
|
|
1423
1684
|
this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
|
|
1424
1685
|
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
1686
|
+
this.rpcInbox = new RpcReplyInbox(codec, this.callerName);
|
|
1425
1687
|
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
1426
|
-
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc
|
|
1688
|
+
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
1427
1689
|
const derived = deriveOtelAttrs(this.rootOptions);
|
|
1428
1690
|
this.otel = derived.otel;
|
|
1429
1691
|
this.serverEndpoint = derived.serverEndpoint;
|
|
1430
1692
|
}
|
|
1431
|
-
logger = new
|
|
1693
|
+
logger = new import_common6.Logger("Jetstream:Client");
|
|
1432
1694
|
/** Target service name this client sends messages to. */
|
|
1433
1695
|
targetName;
|
|
1434
1696
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
1435
1697
|
callerName;
|
|
1436
|
-
/**
|
|
1437
|
-
* Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
|
|
1438
|
-
* per stream kind this client may publish to. Built once in the constructor
|
|
1439
|
-
* so producing a full subject is a single string concat with the user pattern.
|
|
1440
|
-
*/
|
|
1698
|
+
/** Convention subject prefixes for foreign targets; one per stream kind. */
|
|
1441
1699
|
eventSubjectPrefix;
|
|
1442
1700
|
commandSubjectPrefix;
|
|
1443
1701
|
orderedSubjectPrefix;
|
|
1702
|
+
/** Broadcast subject prefix derived from the own resolver; never null. */
|
|
1703
|
+
broadcastPrefix;
|
|
1704
|
+
/** Resolver for self-target subjects; null when targeting a foreign service. */
|
|
1705
|
+
selfNames;
|
|
1444
1706
|
/**
|
|
1445
1707
|
* RPC configuration snapshots. The values are derived from rootOptions at
|
|
1446
1708
|
* construction time so the publish hot path never has to re-run
|
|
@@ -1452,13 +1714,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1452
1714
|
otel;
|
|
1453
1715
|
/** Server endpoint parts used for `server.address` / `server.port` span attributes. */
|
|
1454
1716
|
serverEndpoint;
|
|
1455
|
-
/**
|
|
1456
|
-
|
|
1457
|
-
inboxSubscription = null;
|
|
1458
|
-
/** Pending JetStream-mode RPC callbacks, keyed by correlation ID. */
|
|
1459
|
-
pendingMessages = /* @__PURE__ */ new Map();
|
|
1460
|
-
/** Pending JetStream-mode RPC timeouts, keyed by correlation ID. */
|
|
1461
|
-
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
1717
|
+
/** Reply inbox and pending-request registry for JetStream-mode RPC. */
|
|
1718
|
+
rpcInbox;
|
|
1462
1719
|
/** Subscription to connection status events for disconnect handling. */
|
|
1463
1720
|
statusSubscription = null;
|
|
1464
1721
|
/**
|
|
@@ -1477,8 +1734,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1477
1734
|
*/
|
|
1478
1735
|
async connect() {
|
|
1479
1736
|
const nc = await this.connection.getConnection();
|
|
1480
|
-
if (!this.isCoreMode && !this.
|
|
1481
|
-
this.
|
|
1737
|
+
if (!this.isCoreMode && !this.rpcInbox.active) {
|
|
1738
|
+
this.rpcInbox.setup(nc);
|
|
1482
1739
|
}
|
|
1483
1740
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
1484
1741
|
if (status.type === "disconnect") {
|
|
@@ -1493,7 +1750,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1493
1750
|
this.statusSubscription?.unsubscribe();
|
|
1494
1751
|
this.statusSubscription = null;
|
|
1495
1752
|
this.readyForPublish = false;
|
|
1496
|
-
this.
|
|
1753
|
+
this.rpcInbox.rejectAll(new Error("Client closed"));
|
|
1497
1754
|
}
|
|
1498
1755
|
/**
|
|
1499
1756
|
* Direct access to the raw NATS connection.
|
|
@@ -1503,7 +1760,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1503
1760
|
unwrap() {
|
|
1504
1761
|
const nc = this.connection.unwrap;
|
|
1505
1762
|
if (!nc) {
|
|
1506
|
-
throw new Error("Not connected
|
|
1763
|
+
throw new Error("Not connected; call connect() before unwrap()");
|
|
1507
1764
|
}
|
|
1508
1765
|
return nc;
|
|
1509
1766
|
}
|
|
@@ -1530,7 +1787,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1530
1787
|
const encoded = this.codec.encode(data);
|
|
1531
1788
|
const effectiveMsgId = messageId ?? import_nuid.nuid.next();
|
|
1532
1789
|
const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
|
|
1533
|
-
const declaredPattern = declaredEventPattern(packet.pattern);
|
|
1790
|
+
const declaredPattern = declaredEventPattern(packet.pattern, publishKind);
|
|
1534
1791
|
const streamKind = eventStreamKind(publishKind);
|
|
1535
1792
|
const startedAt = performance.now();
|
|
1536
1793
|
try {
|
|
@@ -1550,13 +1807,6 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1550
1807
|
},
|
|
1551
1808
|
this.otel,
|
|
1552
1809
|
async () => {
|
|
1553
|
-
const warnIfDuplicate = (kindLabel, ack2) => {
|
|
1554
|
-
if (ack2.duplicate) {
|
|
1555
|
-
this.logger.warn(
|
|
1556
|
-
`Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
|
|
1557
|
-
);
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
1810
|
if (schedule) {
|
|
1561
1811
|
const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1562
1812
|
headers: msgHeaders,
|
|
@@ -1567,7 +1817,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1567
1817
|
ttl
|
|
1568
1818
|
}
|
|
1569
1819
|
});
|
|
1570
|
-
warnIfDuplicate("scheduled", ack2);
|
|
1820
|
+
this.warnIfDuplicate("scheduled", publishSubject, ack2);
|
|
1571
1821
|
return;
|
|
1572
1822
|
}
|
|
1573
1823
|
const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
@@ -1575,7 +1825,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1575
1825
|
msgID: effectiveMsgId,
|
|
1576
1826
|
ttl
|
|
1577
1827
|
});
|
|
1578
|
-
warnIfDuplicate("event", ack);
|
|
1828
|
+
this.warnIfDuplicate("event", publishSubject, ack);
|
|
1579
1829
|
}
|
|
1580
1830
|
);
|
|
1581
1831
|
this.reportPublished(declaredPattern, streamKind, startedAt, "success");
|
|
@@ -1592,7 +1842,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1592
1842
|
* JetStream mode: publishes to stream + waits for inbox response.
|
|
1593
1843
|
*/
|
|
1594
1844
|
publish(packet, callback) {
|
|
1595
|
-
const subject = this.commandSubjectPrefix + packet.pattern;
|
|
1845
|
+
const subject = this.selfNames ? this.selfNames.subject("cmd" /* Command */, packet.pattern) : this.commandSubjectPrefix + packet.pattern;
|
|
1596
1846
|
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
1597
1847
|
if (schedule) {
|
|
1598
1848
|
this.logger.warn(
|
|
@@ -1625,12 +1875,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1625
1875
|
}
|
|
1626
1876
|
return () => {
|
|
1627
1877
|
if (jetStreamCorrelationId) {
|
|
1628
|
-
|
|
1629
|
-
if (timeoutId) {
|
|
1630
|
-
clearTimeout(timeoutId);
|
|
1631
|
-
this.pendingTimeouts.delete(jetStreamCorrelationId);
|
|
1632
|
-
}
|
|
1633
|
-
this.pendingMessages.delete(jetStreamCorrelationId);
|
|
1878
|
+
this.rpcInbox.discard(jetStreamCorrelationId);
|
|
1634
1879
|
}
|
|
1635
1880
|
};
|
|
1636
1881
|
}
|
|
@@ -1674,7 +1919,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1674
1919
|
}
|
|
1675
1920
|
} catch (err) {
|
|
1676
1921
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
1677
|
-
if (error instanceof
|
|
1922
|
+
if (error instanceof import_transport_node2.TimeoutError) {
|
|
1678
1923
|
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1679
1924
|
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
|
|
1680
1925
|
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
@@ -1695,7 +1940,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1695
1940
|
const hdrs = this.buildHeaders(customHeaders, {
|
|
1696
1941
|
subject,
|
|
1697
1942
|
correlationId,
|
|
1698
|
-
replyTo: this.
|
|
1943
|
+
replyTo: this.rpcInbox.address ?? ""
|
|
1699
1944
|
});
|
|
1700
1945
|
const encoded = this.codec.encode(data);
|
|
1701
1946
|
const spanHandle = beginRpcClientSpan(
|
|
@@ -1712,7 +1957,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1712
1957
|
this.otel
|
|
1713
1958
|
);
|
|
1714
1959
|
const startedAt = performance.now();
|
|
1715
|
-
|
|
1960
|
+
const settleRoundTrip = (packet) => {
|
|
1716
1961
|
if (packet.err) {
|
|
1717
1962
|
if (packet.err instanceof Error) {
|
|
1718
1963
|
spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
|
|
@@ -1725,30 +1970,25 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1725
1970
|
this.reportRpcCompleted(declaredPattern, startedAt, "success");
|
|
1726
1971
|
}
|
|
1727
1972
|
callback(packet);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1732
|
-
this.pendingMessages.delete(correlationId);
|
|
1973
|
+
};
|
|
1974
|
+
this.rpcInbox.register(correlationId, settleRoundTrip);
|
|
1975
|
+
this.rpcInbox.armTimeout(correlationId, effectiveTimeout, () => {
|
|
1733
1976
|
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1734
1977
|
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
1735
1978
|
this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
|
|
1736
1979
|
callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
|
|
1737
|
-
}
|
|
1738
|
-
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
1980
|
+
});
|
|
1739
1981
|
try {
|
|
1740
1982
|
if (!this.readyForPublish) await this.connect();
|
|
1741
|
-
if (!this.
|
|
1742
|
-
if (!this.
|
|
1743
|
-
|
|
1744
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1745
|
-
this.pendingMessages.delete(correlationId);
|
|
1983
|
+
if (!this.rpcInbox.has(correlationId)) return;
|
|
1984
|
+
if (!this.rpcInbox.address) {
|
|
1985
|
+
this.rpcInbox.discard(correlationId);
|
|
1746
1986
|
const inboxError = new Error("Inbox not initialized");
|
|
1747
1987
|
spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
|
|
1748
1988
|
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1749
1989
|
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
1750
1990
|
callback({
|
|
1751
|
-
err: new Error("Inbox not initialized
|
|
1991
|
+
err: new Error("Inbox not initialized; JetStream RPC mode requires a connected inbox"),
|
|
1752
1992
|
response: null,
|
|
1753
1993
|
isDisposed: true
|
|
1754
1994
|
});
|
|
@@ -1768,13 +2008,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1768
2008
|
}
|
|
1769
2009
|
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1770
2010
|
} catch (err) {
|
|
1771
|
-
|
|
1772
|
-
if (existingTimeout) {
|
|
1773
|
-
clearTimeout(existingTimeout);
|
|
1774
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1775
|
-
}
|
|
1776
|
-
if (!this.pendingMessages.has(correlationId)) return;
|
|
1777
|
-
this.pendingMessages.delete(correlationId);
|
|
2011
|
+
if (!this.rpcInbox.discard(correlationId)) return;
|
|
1778
2012
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
1779
2013
|
spanHandle.finish({ kind: "error" /* Error */, error });
|
|
1780
2014
|
this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
|
|
@@ -1783,6 +2017,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1783
2017
|
callback({ err: error, response: null, isDisposed: true });
|
|
1784
2018
|
}
|
|
1785
2019
|
}
|
|
2020
|
+
warnIfDuplicate(kindLabel, subject, ack) {
|
|
2021
|
+
if (ack.duplicate) {
|
|
2022
|
+
this.logger.warn(`Duplicate ${kindLabel} publish detected: ${subject} (seq: ${ack.seq})`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
1786
2025
|
// hasHook is per-emit so late subscribers (JetstreamMetricsService during
|
|
1787
2026
|
// OnApplicationBootstrap) still receive events.
|
|
1788
2027
|
reportPublished(declaredPattern, kind, startedAt, status) {
|
|
@@ -1806,92 +2045,28 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1806
2045
|
}
|
|
1807
2046
|
/** Fail-fast all pending JetStream RPC callbacks on connection loss. */
|
|
1808
2047
|
handleDisconnect() {
|
|
1809
|
-
this.
|
|
1810
|
-
this.inbox = null;
|
|
2048
|
+
this.rpcInbox.rejectAll(new Error("Connection lost"));
|
|
1811
2049
|
this.readyForPublish = false;
|
|
1812
2050
|
}
|
|
1813
|
-
/** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
|
|
1814
|
-
rejectPendingRpcs(error) {
|
|
1815
|
-
for (const callback of this.pendingMessages.values()) {
|
|
1816
|
-
callback({ err: error, response: null, isDisposed: true });
|
|
1817
|
-
}
|
|
1818
|
-
for (const timeoutId of this.pendingTimeouts.values()) {
|
|
1819
|
-
clearTimeout(timeoutId);
|
|
1820
|
-
}
|
|
1821
|
-
this.pendingMessages.clear();
|
|
1822
|
-
this.pendingTimeouts.clear();
|
|
1823
|
-
this.inboxSubscription?.unsubscribe();
|
|
1824
|
-
this.inboxSubscription = null;
|
|
1825
|
-
this.inbox = null;
|
|
1826
|
-
}
|
|
1827
|
-
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
1828
|
-
setupInbox(nc) {
|
|
1829
|
-
this.inbox = (0, import_transport_node.createInbox)(internalName(this.rootOptions.name));
|
|
1830
|
-
this.inboxSubscription = nc.subscribe(this.inbox, {
|
|
1831
|
-
callback: (err, msg) => {
|
|
1832
|
-
if (err) {
|
|
1833
|
-
this.logger.error("Inbox subscription error:", err);
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
this.routeInboxReply(msg);
|
|
1837
|
-
}
|
|
1838
|
-
});
|
|
1839
|
-
this.logger.debug(`Inbox subscription: ${this.inbox}`);
|
|
1840
|
-
}
|
|
1841
|
-
/** Route an inbox reply to the matching pending callback. */
|
|
1842
|
-
routeInboxReply(msg) {
|
|
1843
|
-
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
1844
|
-
if (!correlationId) {
|
|
1845
|
-
this.logger.warn("Inbox reply without correlation-id, ignoring");
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
const callback = this.pendingMessages.get(correlationId);
|
|
1849
|
-
if (!callback) {
|
|
1850
|
-
this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
const timeoutId = this.pendingTimeouts.get(correlationId);
|
|
1854
|
-
if (timeoutId) {
|
|
1855
|
-
clearTimeout(timeoutId);
|
|
1856
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1857
|
-
}
|
|
1858
|
-
try {
|
|
1859
|
-
const decoded = this.codec.decode(msg.data);
|
|
1860
|
-
if (msg.headers?.get("x-error" /* Error */)) {
|
|
1861
|
-
callback({ err: decoded, response: null, isDisposed: true });
|
|
1862
|
-
} else {
|
|
1863
|
-
callback({ err: null, response: decoded, isDisposed: true });
|
|
1864
|
-
}
|
|
1865
|
-
} catch (err) {
|
|
1866
|
-
callback({
|
|
1867
|
-
err: err instanceof Error ? err : new Error("Decode error"),
|
|
1868
|
-
response: null,
|
|
1869
|
-
isDisposed: true
|
|
1870
|
-
});
|
|
1871
|
-
} finally {
|
|
1872
|
-
this.pendingMessages.delete(correlationId);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
2051
|
/**
|
|
1876
|
-
* Resolve a user pattern to a fully-qualified NATS subject
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
* The leading-char check short-circuits the `startsWith` comparisons for
|
|
1880
|
-
* patterns that cannot possibly carry a broadcast/ordered marker, which is
|
|
1881
|
-
* the overwhelmingly common case.
|
|
2052
|
+
* Resolve a user pattern to a fully-qualified NATS subject. Self-targets go
|
|
2053
|
+
* through the resolver so custom subjectPrefix options are honoured.
|
|
1882
2054
|
*/
|
|
1883
2055
|
buildEventSubject(pattern) {
|
|
1884
|
-
if (pattern.
|
|
1885
|
-
return
|
|
2056
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
2057
|
+
return this.broadcastPrefix + pattern.slice("broadcast:" /* Broadcast */.length);
|
|
1886
2058
|
}
|
|
1887
|
-
if (pattern.
|
|
1888
|
-
|
|
2059
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) {
|
|
2060
|
+
const bare = pattern.slice("ordered:" /* Ordered */.length);
|
|
2061
|
+
if (this.selfNames) return this.selfNames.subject("ordered" /* Ordered */, bare);
|
|
2062
|
+
return this.orderedSubjectPrefix + bare;
|
|
1889
2063
|
}
|
|
2064
|
+
if (this.selfNames) return this.selfNames.subject("ev" /* Event */, pattern);
|
|
1890
2065
|
return this.eventSubjectPrefix + pattern;
|
|
1891
2066
|
}
|
|
1892
2067
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
1893
2068
|
buildHeaders(customHeaders, transport) {
|
|
1894
|
-
const hdrs = (0,
|
|
2069
|
+
const hdrs = (0, import_transport_node2.headers)();
|
|
1895
2070
|
hdrs.set("x-subject" /* Subject */, transport.subject);
|
|
1896
2071
|
hdrs.set("x-caller-name" /* CallerName */, this.callerName);
|
|
1897
2072
|
if (transport.correlationId) {
|
|
@@ -1931,33 +2106,16 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
1931
2106
|
/**
|
|
1932
2107
|
* Build a schedule-holder subject for NATS message scheduling.
|
|
1933
2108
|
*
|
|
1934
|
-
* The
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
* concurrent schedules of the same pattern would silently replace each other.
|
|
1941
|
-
*
|
|
1942
|
-
* Examples:
|
|
1943
|
-
* - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
|
|
1944
|
-
* - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
|
|
2109
|
+
* The holder lives in the same stream as the target but under the `_sch`
|
|
2110
|
+
* namespace no consumer filter matches; the server publishes it to the
|
|
2111
|
+
* target subject when the schedule fires. The unique per-message suffix
|
|
2112
|
+
* exists because the server stores schedules as rollup messages, one
|
|
2113
|
+
* active schedule per subject (ADR-51): without it, concurrent schedules
|
|
2114
|
+
* of the same pattern would silently replace each other.
|
|
1945
2115
|
*/
|
|
1946
2116
|
buildScheduleSubject(eventSubject) {
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
}
|
|
1950
|
-
const targetPrefix = `${internalName(this.targetName)}.`;
|
|
1951
|
-
if (!eventSubject.startsWith(targetPrefix)) {
|
|
1952
|
-
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
1953
|
-
}
|
|
1954
|
-
const withoutPrefix = eventSubject.slice(targetPrefix.length);
|
|
1955
|
-
const dotIndex = withoutPrefix.indexOf(".");
|
|
1956
|
-
if (dotIndex === -1) {
|
|
1957
|
-
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
1958
|
-
}
|
|
1959
|
-
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
1960
|
-
return `${targetPrefix}_sch.${pattern}.${import_nuid.nuid.next()}`;
|
|
2117
|
+
const base = this.selfNames ? this.selfNames.scheduleSubjectBase(eventSubject) : conventionScheduleSubjectBase(this.targetName, eventSubject);
|
|
2118
|
+
return `${base}.${import_nuid.nuid.next()}`;
|
|
1961
2119
|
}
|
|
1962
2120
|
};
|
|
1963
2121
|
|
|
@@ -1989,9 +2147,9 @@ var MsgpackCodec = class {
|
|
|
1989
2147
|
};
|
|
1990
2148
|
|
|
1991
2149
|
// src/connection/connection.provider.ts
|
|
1992
|
-
var
|
|
1993
|
-
var
|
|
1994
|
-
var
|
|
2150
|
+
var import_common7 = require("@nestjs/common");
|
|
2151
|
+
var import_transport_node3 = require("@nats-io/transport-node");
|
|
2152
|
+
var import_jetstream11 = require("@nats-io/jetstream");
|
|
1995
2153
|
var import_rxjs = require("rxjs");
|
|
1996
2154
|
var DEFAULT_OPTIONS = {
|
|
1997
2155
|
maxReconnectAttempts: -1,
|
|
@@ -2017,7 +2175,7 @@ var ConnectionProvider = class {
|
|
|
2017
2175
|
nc$;
|
|
2018
2176
|
/** Live stream of connection status events (no replay). */
|
|
2019
2177
|
status$;
|
|
2020
|
-
logger = new
|
|
2178
|
+
logger = new import_common7.Logger("Jetstream:Connection");
|
|
2021
2179
|
connection = null;
|
|
2022
2180
|
connectionPromise = null;
|
|
2023
2181
|
jsClient = null;
|
|
@@ -2028,7 +2186,7 @@ var ConnectionProvider = class {
|
|
|
2028
2186
|
otelEndpoint;
|
|
2029
2187
|
lifecycleSpan = null;
|
|
2030
2188
|
/**
|
|
2031
|
-
* Establish NATS connection. Idempotent
|
|
2189
|
+
* Establish NATS connection. Idempotent: returns cached connection on subsequent calls.
|
|
2032
2190
|
*
|
|
2033
2191
|
* @throws Error if connection is refused (fail fast).
|
|
2034
2192
|
*/
|
|
@@ -2067,9 +2225,9 @@ var ConnectionProvider = class {
|
|
|
2067
2225
|
*/
|
|
2068
2226
|
getJetStreamClient() {
|
|
2069
2227
|
if (!this.connection || this.connection.isClosed()) {
|
|
2070
|
-
throw new Error("Not connected
|
|
2228
|
+
throw new Error("Not connected; call getConnection() before getJetStreamClient()");
|
|
2071
2229
|
}
|
|
2072
|
-
this.jsClient ??= (0,
|
|
2230
|
+
this.jsClient ??= (0, import_jetstream11.jetstream)(this.connection);
|
|
2073
2231
|
return this.jsClient;
|
|
2074
2232
|
}
|
|
2075
2233
|
/** Direct access to the raw NATS connection, or `null` if not yet connected. */
|
|
@@ -2079,7 +2237,7 @@ var ConnectionProvider = class {
|
|
|
2079
2237
|
/**
|
|
2080
2238
|
* Gracefully shut down the connection.
|
|
2081
2239
|
*
|
|
2082
|
-
* Sequence: drain
|
|
2240
|
+
* Sequence: drain -> wait for close. Falls back to force-close on error.
|
|
2083
2241
|
*/
|
|
2084
2242
|
async shutdown() {
|
|
2085
2243
|
if (this.connectionPromise) {
|
|
@@ -2118,7 +2276,7 @@ var ConnectionProvider = class {
|
|
|
2118
2276
|
async initJetStreamManager() {
|
|
2119
2277
|
try {
|
|
2120
2278
|
const nc = await this.getConnection();
|
|
2121
|
-
this.jsmInstance = await (0,
|
|
2279
|
+
this.jsmInstance = await (0, import_jetstream11.jetstreamManager)(nc);
|
|
2122
2280
|
this.logger.log("JetStream manager initialized");
|
|
2123
2281
|
return this.jsmInstance;
|
|
2124
2282
|
} finally {
|
|
@@ -2128,7 +2286,7 @@ var ConnectionProvider = class {
|
|
|
2128
2286
|
/** Internal: establish the physical connection with reconnect monitoring. */
|
|
2129
2287
|
async establish() {
|
|
2130
2288
|
try {
|
|
2131
|
-
const nc = await (0,
|
|
2289
|
+
const nc = await (0, import_transport_node3.connect)({
|
|
2132
2290
|
...DEFAULT_OPTIONS,
|
|
2133
2291
|
// Default the NATS connection name to the OTel-derived service name so
|
|
2134
2292
|
// `nats server info` lines up with span attributes, but let user-supplied
|
|
@@ -2282,12 +2440,12 @@ var EventBus = class {
|
|
|
2282
2440
|
};
|
|
2283
2441
|
|
|
2284
2442
|
// src/health/jetstream.health-indicator.ts
|
|
2285
|
-
var
|
|
2443
|
+
var import_common8 = require("@nestjs/common");
|
|
2286
2444
|
var JetstreamHealthIndicator = class {
|
|
2287
2445
|
constructor(connection) {
|
|
2288
2446
|
this.connection = connection;
|
|
2289
2447
|
}
|
|
2290
|
-
logger = new
|
|
2448
|
+
logger = new import_common8.Logger("Jetstream:Health");
|
|
2291
2449
|
/**
|
|
2292
2450
|
* Plain health status check.
|
|
2293
2451
|
*
|
|
@@ -2317,7 +2475,7 @@ var JetstreamHealthIndicator = class {
|
|
|
2317
2475
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
2318
2476
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
2319
2477
|
*
|
|
2320
|
-
* The thrown error sets `isHealthCheckError: true` and `causes
|
|
2478
|
+
* The thrown error sets `isHealthCheckError: true` and `causes`, the
|
|
2321
2479
|
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
2322
2480
|
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
2323
2481
|
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
@@ -2344,14 +2502,14 @@ var JetstreamHealthIndicator = class {
|
|
|
2344
2502
|
}
|
|
2345
2503
|
};
|
|
2346
2504
|
JetstreamHealthIndicator = __decorateClass([
|
|
2347
|
-
(0,
|
|
2505
|
+
(0, import_common8.Injectable)()
|
|
2348
2506
|
], JetstreamHealthIndicator);
|
|
2349
2507
|
|
|
2350
2508
|
// src/metrics/metrics.module.ts
|
|
2351
|
-
var
|
|
2509
|
+
var import_common12 = require("@nestjs/common");
|
|
2352
2510
|
|
|
2353
2511
|
// src/server/routing/pattern-registry.ts
|
|
2354
|
-
var
|
|
2512
|
+
var import_common9 = require("@nestjs/common");
|
|
2355
2513
|
var HANDLER_LABELS = {
|
|
2356
2514
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
2357
2515
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -2359,12 +2517,13 @@ var HANDLER_LABELS = {
|
|
|
2359
2517
|
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
2360
2518
|
};
|
|
2361
2519
|
var PatternRegistry = class {
|
|
2362
|
-
constructor(options) {
|
|
2520
|
+
constructor(options, names) {
|
|
2363
2521
|
this.options = options;
|
|
2522
|
+
this.names = names;
|
|
2364
2523
|
}
|
|
2365
|
-
logger = new
|
|
2524
|
+
logger = new import_common9.Logger("Jetstream:PatternRegistry");
|
|
2366
2525
|
registry = /* @__PURE__ */ new Map();
|
|
2367
|
-
// Cached after registerHandlers()
|
|
2526
|
+
// Cached after registerHandlers(); the registry is immutable from that point
|
|
2368
2527
|
cachedPatterns = null;
|
|
2369
2528
|
_hasEvents = false;
|
|
2370
2529
|
_hasCommands = false;
|
|
@@ -2377,7 +2536,6 @@ var PatternRegistry = class {
|
|
|
2377
2536
|
* @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
|
|
2378
2537
|
*/
|
|
2379
2538
|
registerHandlers(handlers) {
|
|
2380
|
-
const serviceName = this.options.name;
|
|
2381
2539
|
for (const [pattern, handler] of handlers) {
|
|
2382
2540
|
const extras = handler.extras;
|
|
2383
2541
|
const isEvent = handler.isEventHandler ?? false;
|
|
@@ -2394,7 +2552,7 @@ var PatternRegistry = class {
|
|
|
2394
2552
|
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
2395
2553
|
else if (isEvent) kind = "ev" /* Event */;
|
|
2396
2554
|
else kind = "cmd" /* Command */;
|
|
2397
|
-
const fullSubject =
|
|
2555
|
+
const fullSubject = this.names.subject(kind, pattern);
|
|
2398
2556
|
this.registry.set(fullSubject, {
|
|
2399
2557
|
handler,
|
|
2400
2558
|
pattern,
|
|
@@ -2421,7 +2579,7 @@ var PatternRegistry = class {
|
|
|
2421
2579
|
* Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
|
|
2422
2580
|
*
|
|
2423
2581
|
* Returns `null` when the subject is not registered. The declared pattern is
|
|
2424
|
-
* the value the user passed to `@EventPattern`/`@MessagePattern
|
|
2582
|
+
* the value the user passed to `@EventPattern`/`@MessagePattern`: stable and
|
|
2425
2583
|
* bounded, suitable for use as a Prometheus label without cardinality risk.
|
|
2426
2584
|
*/
|
|
2427
2585
|
resolveDeclared(subject) {
|
|
@@ -2431,7 +2589,17 @@ var PatternRegistry = class {
|
|
|
2431
2589
|
}
|
|
2432
2590
|
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
2433
2591
|
getBroadcastPatterns() {
|
|
2434
|
-
return this.getPatternsByKind().broadcasts.map(
|
|
2592
|
+
return this.getPatternsByKind().broadcasts.map(
|
|
2593
|
+
(p) => this.names.subject("broadcast" /* Broadcast */, p)
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
/** Get registered event patterns as raw user-declared patterns. */
|
|
2597
|
+
getEventPatterns() {
|
|
2598
|
+
return this.getPatternsByKind().events;
|
|
2599
|
+
}
|
|
2600
|
+
/** Get registered command patterns as raw user-declared patterns. */
|
|
2601
|
+
getCommandPatterns() {
|
|
2602
|
+
return this.getPatternsByKind().commands;
|
|
2435
2603
|
}
|
|
2436
2604
|
hasBroadcastHandlers() {
|
|
2437
2605
|
return this._hasBroadcasts;
|
|
@@ -2447,9 +2615,7 @@ var PatternRegistry = class {
|
|
|
2447
2615
|
}
|
|
2448
2616
|
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
2449
2617
|
getOrderedSubjects() {
|
|
2450
|
-
return this.getPatternsByKind().ordered.map(
|
|
2451
|
-
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2452
|
-
);
|
|
2618
|
+
return this.getPatternsByKind().ordered.map((p) => this.names.subject("ordered" /* Ordered */, p));
|
|
2453
2619
|
}
|
|
2454
2620
|
/** Check if any registered handler has metadata. */
|
|
2455
2621
|
hasMetadata() {
|
|
@@ -2481,22 +2647,6 @@ var PatternRegistry = class {
|
|
|
2481
2647
|
ordered: [...patterns.ordered]
|
|
2482
2648
|
};
|
|
2483
2649
|
}
|
|
2484
|
-
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
2485
|
-
normalizeSubject(subject) {
|
|
2486
|
-
const name = internalName(this.options.name);
|
|
2487
|
-
const prefixes = [
|
|
2488
|
-
`${name}.${"cmd" /* Command */}.`,
|
|
2489
|
-
`${name}.${"ev" /* Event */}.`,
|
|
2490
|
-
`${name}.${"ordered" /* Ordered */}.`,
|
|
2491
|
-
`${"broadcast" /* Broadcast */}.`
|
|
2492
|
-
];
|
|
2493
|
-
for (const prefix of prefixes) {
|
|
2494
|
-
if (subject.startsWith(prefix)) {
|
|
2495
|
-
return subject.slice(prefix.length);
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2498
|
-
return subject;
|
|
2499
|
-
}
|
|
2500
2650
|
buildPatternsByKind() {
|
|
2501
2651
|
const events = [];
|
|
2502
2652
|
const commands = [];
|
|
@@ -2551,7 +2701,7 @@ var ERROR_CONTEXT_PREFIXES = [
|
|
|
2551
2701
|
["consume", "consume"],
|
|
2552
2702
|
["core-rpc-handler", "handler"],
|
|
2553
2703
|
["rpc-handler", "handler"],
|
|
2554
|
-
// EventRouter formats contexts as `${StreamKind.*}-handler
|
|
2704
|
+
// EventRouter formats contexts as `${StreamKind.*}-handler:...`; the enum
|
|
2555
2705
|
// uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
|
|
2556
2706
|
["ev-handler", "handler"],
|
|
2557
2707
|
["event-handler", "handler"],
|
|
@@ -2569,7 +2719,7 @@ var STREAM_KIND_LABEL = {
|
|
|
2569
2719
|
};
|
|
2570
2720
|
|
|
2571
2721
|
// src/metrics/metrics.service.ts
|
|
2572
|
-
var
|
|
2722
|
+
var import_common11 = require("@nestjs/common");
|
|
2573
2723
|
|
|
2574
2724
|
// src/metrics/error-context-mapper.ts
|
|
2575
2725
|
var mapErrorContext = (context7) => {
|
|
@@ -2692,12 +2842,12 @@ var createMetrics = (opts) => {
|
|
|
2692
2842
|
};
|
|
2693
2843
|
|
|
2694
2844
|
// src/metrics/poll-runner.ts
|
|
2695
|
-
var
|
|
2845
|
+
var import_common10 = require("@nestjs/common");
|
|
2696
2846
|
var PollRunner = class {
|
|
2697
2847
|
constructor(opts) {
|
|
2698
2848
|
this.opts = opts;
|
|
2699
2849
|
}
|
|
2700
|
-
logger = new
|
|
2850
|
+
logger = new import_common10.Logger("Jetstream:Metrics:Poll");
|
|
2701
2851
|
timer = null;
|
|
2702
2852
|
inFlight = null;
|
|
2703
2853
|
start() {
|
|
@@ -2706,7 +2856,7 @@ var PollRunner = class {
|
|
|
2706
2856
|
if (this.opts.targets.length === 0) return;
|
|
2707
2857
|
this.timer = setInterval(() => {
|
|
2708
2858
|
if (this.inFlight !== null) {
|
|
2709
|
-
this.logger.warn("Skipping poll tick
|
|
2859
|
+
this.logger.warn("Skipping poll tick; previous cycle still in flight");
|
|
2710
2860
|
return;
|
|
2711
2861
|
}
|
|
2712
2862
|
this.inFlight = this.tick().finally(() => {
|
|
@@ -2769,15 +2919,16 @@ var PollRunner = class {
|
|
|
2769
2919
|
|
|
2770
2920
|
// src/metrics/metrics.service.ts
|
|
2771
2921
|
var JetstreamMetricsService = class {
|
|
2772
|
-
constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
|
|
2922
|
+
constructor(eventBus, config, promClient, options, patternRegistry, connection = null, names = null) {
|
|
2773
2923
|
this.eventBus = eventBus;
|
|
2774
2924
|
this.config = config;
|
|
2775
2925
|
this.promClient = promClient;
|
|
2776
2926
|
this.options = options;
|
|
2777
2927
|
this.patternRegistry = patternRegistry;
|
|
2778
2928
|
this.connection = connection;
|
|
2929
|
+
this.names = names;
|
|
2779
2930
|
}
|
|
2780
|
-
logger = new
|
|
2931
|
+
logger = new import_common11.Logger("Jetstream:Metrics");
|
|
2781
2932
|
metrics = null;
|
|
2782
2933
|
pollRunner = null;
|
|
2783
2934
|
activeServers = /* @__PURE__ */ new Set();
|
|
@@ -2786,7 +2937,7 @@ var JetstreamMetricsService = class {
|
|
|
2786
2937
|
if (!this.options.metrics || !this.config || !this.promClient) return;
|
|
2787
2938
|
if (!this.config.register) {
|
|
2788
2939
|
throw new Error(
|
|
2789
|
-
"JetstreamMetricsService requires a prom-client Registry
|
|
2940
|
+
"JetstreamMetricsService requires a prom-client Registry; none was resolved by JetstreamMetricsModule."
|
|
2790
2941
|
);
|
|
2791
2942
|
}
|
|
2792
2943
|
this.metrics = createMetrics({
|
|
@@ -2813,7 +2964,7 @@ var JetstreamMetricsService = class {
|
|
|
2813
2964
|
}
|
|
2814
2965
|
/**
|
|
2815
2966
|
* NATS connects during early bootstrap, before this service subscribes to
|
|
2816
|
-
* the EventBus
|
|
2967
|
+
* the EventBus, so the initial `Connect` emission misses us. Mirror the
|
|
2817
2968
|
* current state here so `connection_up` reflects reality the moment metrics
|
|
2818
2969
|
* come online; later disconnects/reconnects update it normally.
|
|
2819
2970
|
*/
|
|
@@ -2846,26 +2997,32 @@ var JetstreamMetricsService = class {
|
|
|
2846
2997
|
if (registry.hasEventHandlers()) {
|
|
2847
2998
|
targets.push({
|
|
2848
2999
|
kind: "ev" /* Event */,
|
|
2849
|
-
stream:
|
|
2850
|
-
consumer:
|
|
3000
|
+
stream: this.resolveStreamName("ev" /* Event */),
|
|
3001
|
+
consumer: this.resolveConsumerName("ev" /* Event */)
|
|
2851
3002
|
});
|
|
2852
3003
|
}
|
|
2853
3004
|
if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
|
|
2854
3005
|
targets.push({
|
|
2855
3006
|
kind: "cmd" /* Command */,
|
|
2856
|
-
stream:
|
|
2857
|
-
consumer:
|
|
3007
|
+
stream: this.resolveStreamName("cmd" /* Command */),
|
|
3008
|
+
consumer: this.resolveConsumerName("cmd" /* Command */)
|
|
2858
3009
|
});
|
|
2859
3010
|
}
|
|
2860
3011
|
if (registry.hasBroadcastHandlers()) {
|
|
2861
3012
|
targets.push({
|
|
2862
3013
|
kind: "broadcast" /* Broadcast */,
|
|
2863
|
-
stream:
|
|
2864
|
-
consumer:
|
|
3014
|
+
stream: this.resolveStreamName("broadcast" /* Broadcast */),
|
|
3015
|
+
consumer: this.resolveConsumerName("broadcast" /* Broadcast */)
|
|
2865
3016
|
});
|
|
2866
3017
|
}
|
|
2867
3018
|
return targets;
|
|
2868
3019
|
}
|
|
3020
|
+
resolveStreamName(kind) {
|
|
3021
|
+
return this.names ? this.names.streamName(kind) : streamName(this.options.name, kind);
|
|
3022
|
+
}
|
|
3023
|
+
resolveConsumerName(kind) {
|
|
3024
|
+
return this.names ? this.names.consumerName(kind) : consumerName(this.options.name, kind);
|
|
3025
|
+
}
|
|
2869
3026
|
subscribeToEvents() {
|
|
2870
3027
|
this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
|
|
2871
3028
|
this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
|
|
@@ -2900,7 +3057,7 @@ var JetstreamMetricsService = class {
|
|
|
2900
3057
|
const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
|
|
2901
3058
|
this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
|
|
2902
3059
|
};
|
|
2903
|
-
// `_kind` collapses broadcast/ordered into MessageKind.Event
|
|
3060
|
+
// `_kind` collapses broadcast/ordered into MessageKind.Event; we use
|
|
2904
3061
|
// declared.kind from PatternRegistry for the precise label instead.
|
|
2905
3062
|
onMessageRouted = (subject, _kind) => {
|
|
2906
3063
|
if (!this.metrics) return;
|
|
@@ -2910,7 +3067,7 @@ var JetstreamMetricsService = class {
|
|
|
2910
3067
|
return;
|
|
2911
3068
|
}
|
|
2912
3069
|
this.metrics.messagesReceivedTotal.labels({
|
|
2913
|
-
stream:
|
|
3070
|
+
stream: this.resolveStreamName(declared.kind),
|
|
2914
3071
|
subject: declared.pattern,
|
|
2915
3072
|
kind: STREAM_KIND_LABEL[declared.kind]
|
|
2916
3073
|
}).inc();
|
|
@@ -2926,7 +3083,7 @@ var JetstreamMetricsService = class {
|
|
|
2926
3083
|
};
|
|
2927
3084
|
onHandlerCompleted = (pattern, kind, durationMs, status) => {
|
|
2928
3085
|
if (!this.metrics) return;
|
|
2929
|
-
const stream =
|
|
3086
|
+
const stream = this.resolveStreamName(kind);
|
|
2930
3087
|
const kindLabel = STREAM_KIND_LABEL[kind];
|
|
2931
3088
|
const labels = { stream, subject: pattern, kind: kindLabel, status };
|
|
2932
3089
|
this.metrics.messagesProcessedTotal.labels(labels).inc();
|
|
@@ -2946,13 +3103,14 @@ var JetstreamMetricsService = class {
|
|
|
2946
3103
|
}
|
|
2947
3104
|
};
|
|
2948
3105
|
JetstreamMetricsService = __decorateClass([
|
|
2949
|
-
(0,
|
|
2950
|
-
__decorateParam(1, (0,
|
|
2951
|
-
__decorateParam(2, (0,
|
|
2952
|
-
__decorateParam(3, (0,
|
|
2953
|
-
__decorateParam(4, (0,
|
|
2954
|
-
__decorateParam(5, (0,
|
|
2955
|
-
__decorateParam(5, (0,
|
|
3106
|
+
(0, import_common11.Injectable)(),
|
|
3107
|
+
__decorateParam(1, (0, import_common11.Inject)(JETSTREAM_METRICS_CONFIG)),
|
|
3108
|
+
__decorateParam(2, (0, import_common11.Inject)(JETSTREAM_METRICS_PROM_CLIENT)),
|
|
3109
|
+
__decorateParam(3, (0, import_common11.Inject)(JETSTREAM_OPTIONS)),
|
|
3110
|
+
__decorateParam(4, (0, import_common11.Optional)()),
|
|
3111
|
+
__decorateParam(5, (0, import_common11.Optional)()),
|
|
3112
|
+
__decorateParam(5, (0, import_common11.Inject)(JETSTREAM_CONNECTION)),
|
|
3113
|
+
__decorateParam(6, (0, import_common11.Optional)())
|
|
2956
3114
|
], JetstreamMetricsService);
|
|
2957
3115
|
|
|
2958
3116
|
// src/metrics/metrics.module.ts
|
|
@@ -3007,9 +3165,18 @@ var JetstreamMetricsModule = class {
|
|
|
3007
3165
|
JETSTREAM_METRICS_PROM_CLIENT,
|
|
3008
3166
|
JETSTREAM_OPTIONS,
|
|
3009
3167
|
{ token: PatternRegistry, optional: true },
|
|
3010
|
-
{ token: JETSTREAM_CONNECTION, optional: true }
|
|
3168
|
+
{ token: JETSTREAM_CONNECTION, optional: true },
|
|
3169
|
+
{ token: NameResolver, optional: true }
|
|
3011
3170
|
],
|
|
3012
|
-
useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(
|
|
3171
|
+
useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection, names) => new JetstreamMetricsService(
|
|
3172
|
+
eventBus,
|
|
3173
|
+
cfg,
|
|
3174
|
+
runtime,
|
|
3175
|
+
opts,
|
|
3176
|
+
patternRegistry,
|
|
3177
|
+
connection,
|
|
3178
|
+
names
|
|
3179
|
+
)
|
|
3013
3180
|
};
|
|
3014
3181
|
return {
|
|
3015
3182
|
module: JetstreamMetricsModule,
|
|
@@ -3024,7 +3191,7 @@ var JetstreamMetricsModule = class {
|
|
|
3024
3191
|
}
|
|
3025
3192
|
};
|
|
3026
3193
|
JetstreamMetricsModule = __decorateClass([
|
|
3027
|
-
(0,
|
|
3194
|
+
(0, import_common12.Module)({})
|
|
3028
3195
|
], JetstreamMetricsModule);
|
|
3029
3196
|
|
|
3030
3197
|
// src/server/strategy.ts
|
|
@@ -3070,31 +3237,22 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3070
3237
|
this.started = false;
|
|
3071
3238
|
}
|
|
3072
3239
|
/**
|
|
3073
|
-
* Override NestJS `Server.addHandler` to fail
|
|
3074
|
-
*
|
|
3075
|
-
* The base class silently overwrites duplicate RPC handlers (last wins) and appends
|
|
3076
|
-
* duplicate event handlers to a linked list. Both behaviors are hazardous in a
|
|
3077
|
-
* JetStream context: silent overwrite drops a handler the user wrote, and double
|
|
3078
|
-
* event dispatch double-acks/double-processes the same JetStream message.
|
|
3240
|
+
* Override NestJS `Server.addHandler` to fail fast on duplicate pattern registration.
|
|
3079
3241
|
*
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3242
|
+
* The base class silently overwrites duplicate RPC handlers and chains duplicate event
|
|
3243
|
+
* handlers, which would double-ack the same JetStream message. Any collision is treated
|
|
3244
|
+
* as a fatal misconfiguration so it surfaces at bootstrap, not in production traffic.
|
|
3082
3245
|
*/
|
|
3083
3246
|
addHandler(pattern, callback, isEventHandler = false, extras = {}) {
|
|
3084
3247
|
const normalizedPattern = this.normalizePattern(pattern);
|
|
3085
3248
|
if (this.messageHandlers.has(normalizedPattern)) {
|
|
3086
3249
|
throw new Error(
|
|
3087
|
-
`Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice
|
|
3250
|
+
`Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice; find and remove the second declaration.`
|
|
3088
3251
|
);
|
|
3089
3252
|
}
|
|
3090
3253
|
super.addHandler(pattern, callback, isEventHandler, extras);
|
|
3091
3254
|
}
|
|
3092
|
-
/**
|
|
3093
|
-
* Register event listener (required by Server base class).
|
|
3094
|
-
*
|
|
3095
|
-
* Stores callbacks for client use. Primary lifecycle events
|
|
3096
|
-
* are routed through EventBus.
|
|
3097
|
-
*/
|
|
3255
|
+
/** Register event listener (required by Server base class); lifecycle events use EventBus. */
|
|
3098
3256
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
3099
3257
|
on(event, callback) {
|
|
3100
3258
|
const existing = this.listeners.get(event) ?? [];
|
|
@@ -3109,7 +3267,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3109
3267
|
unwrap() {
|
|
3110
3268
|
const nc = this.connection.unwrap;
|
|
3111
3269
|
if (!nc) {
|
|
3112
|
-
throw new Error("Not connected
|
|
3270
|
+
throw new Error("Not connected; transport has not started");
|
|
3113
3271
|
}
|
|
3114
3272
|
return nc;
|
|
3115
3273
|
}
|
|
@@ -3119,7 +3277,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3119
3277
|
}
|
|
3120
3278
|
async doListen(callback) {
|
|
3121
3279
|
if (this.started) {
|
|
3122
|
-
this.logger.warn("listen() called more than once
|
|
3280
|
+
this.logger.warn("listen() called more than once; ignoring");
|
|
3123
3281
|
return;
|
|
3124
3282
|
}
|
|
3125
3283
|
this.started = true;
|
|
@@ -3210,8 +3368,8 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
3210
3368
|
};
|
|
3211
3369
|
|
|
3212
3370
|
// src/server/core-rpc.server.ts
|
|
3213
|
-
var
|
|
3214
|
-
var
|
|
3371
|
+
var import_common13 = require("@nestjs/common");
|
|
3372
|
+
var import_transport_node4 = require("@nats-io/transport-node");
|
|
3215
3373
|
|
|
3216
3374
|
// src/context/rpc.context.ts
|
|
3217
3375
|
var import_microservices3 = require("@nestjs/microservices");
|
|
@@ -3287,7 +3445,7 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
3287
3445
|
retry(opts) {
|
|
3288
3446
|
this.assertJetStream("retry");
|
|
3289
3447
|
if (this._shouldTerminate) {
|
|
3290
|
-
throw new Error("Cannot retry
|
|
3448
|
+
throw new Error("Cannot retry; terminate() was already called");
|
|
3291
3449
|
}
|
|
3292
3450
|
this._shouldRetry = true;
|
|
3293
3451
|
this._retryDelay = opts?.delayMs;
|
|
@@ -3304,7 +3462,7 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
3304
3462
|
terminate(reason) {
|
|
3305
3463
|
this.assertJetStream("terminate");
|
|
3306
3464
|
if (this._shouldRetry) {
|
|
3307
|
-
throw new Error("Cannot terminate
|
|
3465
|
+
throw new Error("Cannot terminate; retry() was already called");
|
|
3308
3466
|
}
|
|
3309
3467
|
this._shouldTerminate = true;
|
|
3310
3468
|
this._terminateReason = reason;
|
|
@@ -3313,7 +3471,7 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
3313
3471
|
asJetStream() {
|
|
3314
3472
|
return this.isJetStream() ? this.args[0] : null;
|
|
3315
3473
|
}
|
|
3316
|
-
/** Ensure the message is JetStream
|
|
3474
|
+
/** Ensure the message is JetStream; settlement actions are not available for Core NATS. */
|
|
3317
3475
|
assertJetStream(method) {
|
|
3318
3476
|
if (!this.isJetStream()) {
|
|
3319
3477
|
throw new Error(`${method}() is only available for JetStream messages`);
|
|
@@ -3478,17 +3636,18 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
|
3478
3636
|
|
|
3479
3637
|
// src/server/core-rpc.server.ts
|
|
3480
3638
|
var CoreRpcServer = class {
|
|
3481
|
-
constructor(options, connection, patternRegistry, codec, eventBus) {
|
|
3639
|
+
constructor(options, connection, patternRegistry, codec, eventBus, names) {
|
|
3482
3640
|
this.connection = connection;
|
|
3483
3641
|
this.patternRegistry = patternRegistry;
|
|
3484
3642
|
this.codec = codec;
|
|
3485
3643
|
this.eventBus = eventBus;
|
|
3644
|
+
this.names = names;
|
|
3486
3645
|
const derived = deriveOtelAttrs(options);
|
|
3487
3646
|
this.otel = derived.otel;
|
|
3488
3647
|
this.serviceName = derived.serviceName;
|
|
3489
3648
|
this.serverEndpoint = derived.serverEndpoint;
|
|
3490
3649
|
}
|
|
3491
|
-
logger = new
|
|
3650
|
+
logger = new import_common13.Logger("Jetstream:CoreRpc");
|
|
3492
3651
|
subscription = null;
|
|
3493
3652
|
otel;
|
|
3494
3653
|
serviceName;
|
|
@@ -3496,7 +3655,7 @@ var CoreRpcServer = class {
|
|
|
3496
3655
|
/** Start listening for RPC requests on the command subject. */
|
|
3497
3656
|
async start() {
|
|
3498
3657
|
const nc = await this.connection.getConnection();
|
|
3499
|
-
const subject = `${this.serviceName}.cmd.>`;
|
|
3658
|
+
const subject = this.names ? this.names.filterSubject("cmd" /* Command */) : `${this.serviceName}.cmd.>`;
|
|
3500
3659
|
const queue = `${this.serviceName}_cmd_queue`;
|
|
3501
3660
|
this.subscription = nc.subscribe(subject, {
|
|
3502
3661
|
queue,
|
|
@@ -3589,7 +3748,7 @@ var CoreRpcServer = class {
|
|
|
3589
3748
|
/** Send an error response back to the caller with x-error header. */
|
|
3590
3749
|
respondWithError(msg, error) {
|
|
3591
3750
|
try {
|
|
3592
|
-
const hdrs = (0,
|
|
3751
|
+
const hdrs = (0, import_transport_node4.headers)();
|
|
3593
3752
|
hdrs.set("x-error" /* Error */, "true");
|
|
3594
3753
|
msg.respond(this.codec.encode(serializeError(error)), { headers: hdrs });
|
|
3595
3754
|
} catch {
|
|
@@ -3599,8 +3758,8 @@ var CoreRpcServer = class {
|
|
|
3599
3758
|
};
|
|
3600
3759
|
|
|
3601
3760
|
// src/server/infrastructure/stream.provider.ts
|
|
3602
|
-
var
|
|
3603
|
-
var
|
|
3761
|
+
var import_common15 = require("@nestjs/common");
|
|
3762
|
+
var import_jetstream22 = require("@nats-io/jetstream");
|
|
3604
3763
|
|
|
3605
3764
|
// src/server/infrastructure/nats-error-codes.ts
|
|
3606
3765
|
var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
@@ -3613,7 +3772,7 @@ var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
|
3613
3772
|
})(NatsErrorCode || {});
|
|
3614
3773
|
|
|
3615
3774
|
// src/server/infrastructure/provisioning-budget.ts
|
|
3616
|
-
var
|
|
3775
|
+
var import_jetstream19 = require("@nats-io/jetstream");
|
|
3617
3776
|
var GIB = 1024 ** 3;
|
|
3618
3777
|
var fmt = (bytes) => `${(bytes / GIB).toFixed(2)} GiB`;
|
|
3619
3778
|
var resolveTierBudget = (info, replicas) => {
|
|
@@ -3628,7 +3787,7 @@ var resolveTierBudget = (info, replicas) => {
|
|
|
3628
3787
|
var groupByReplicas = (reservations) => {
|
|
3629
3788
|
const groups = /* @__PURE__ */ new Map();
|
|
3630
3789
|
for (const r of reservations) {
|
|
3631
|
-
if (r.storage !==
|
|
3790
|
+
if (r.storage !== import_jetstream19.StorageType.File) continue;
|
|
3632
3791
|
const prev = groups.get(r.numReplicas) ?? 0;
|
|
3633
3792
|
groups.set(r.numReplicas, prev + r.maxBytes * r.numReplicas);
|
|
3634
3793
|
}
|
|
@@ -3669,7 +3828,7 @@ var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
|
|
|
3669
3828
|
);
|
|
3670
3829
|
}
|
|
3671
3830
|
} catch (err) {
|
|
3672
|
-
logger5.debug(`Storage preflight skipped
|
|
3831
|
+
logger5.debug(`Storage preflight skipped; account info unavailable: ${String(err)}`);
|
|
3673
3832
|
}
|
|
3674
3833
|
};
|
|
3675
3834
|
|
|
@@ -3690,7 +3849,7 @@ var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error
|
|
|
3690
3849
|
numReplicas;
|
|
3691
3850
|
reservation;
|
|
3692
3851
|
constructor(fields) {
|
|
3693
|
-
const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B
|
|
3852
|
+
const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B x replicas=${fields.numReplicas}).` : "";
|
|
3694
3853
|
super(
|
|
3695
3854
|
`JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
|
|
3696
3855
|
{ cause: fields.cause }
|
|
@@ -3727,7 +3886,7 @@ var mapProvisioningError = (err, ctx) => {
|
|
|
3727
3886
|
};
|
|
3728
3887
|
|
|
3729
3888
|
// src/server/infrastructure/provisioning-summary.ts
|
|
3730
|
-
var
|
|
3889
|
+
var import_jetstream20 = require("@nats-io/jetstream");
|
|
3731
3890
|
var GIB2 = 1024 ** 3;
|
|
3732
3891
|
var NANOS_PER_SECOND = 1e9;
|
|
3733
3892
|
var NANOS_PER_HOUR = 3600 * NANOS_PER_SECOND;
|
|
@@ -3742,18 +3901,22 @@ var formatAge = (nanos) => {
|
|
|
3742
3901
|
if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
|
|
3743
3902
|
return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
|
|
3744
3903
|
};
|
|
3745
|
-
var formatProvisioningSummary = (serviceName, reservations) => {
|
|
3746
|
-
const
|
|
3904
|
+
var formatProvisioningSummary = (serviceName, reservations, external = []) => {
|
|
3905
|
+
const totalStreams = reservations.length + external.length;
|
|
3906
|
+
const lines = [`Provisioning ${totalStreams} stream(s) for "${serviceName}":`];
|
|
3747
3907
|
let totalFileMaxBytes = 0;
|
|
3748
3908
|
for (const r of reservations) {
|
|
3749
|
-
if (r.storage ===
|
|
3909
|
+
if (r.storage === import_jetstream20.StorageType.File) totalFileMaxBytes += r.maxBytes;
|
|
3750
3910
|
const clusterReservation = r.maxBytes * r.numReplicas;
|
|
3751
3911
|
lines.push(
|
|
3752
|
-
`
|
|
3912
|
+
` - ${r.name} [${r.kind}] storage=${r.storage} replicas=${r.numReplicas} max_bytes=${formatBytes(r.maxBytes)} max_age=${formatAge(r.maxAge)} retention=${r.retention} -> cluster reservation ${formatBytes(clusterReservation)}`
|
|
3753
3913
|
);
|
|
3754
3914
|
}
|
|
3915
|
+
for (const e of external) {
|
|
3916
|
+
lines.push(` - ${e.name} [${e.kind}] external (bound)`);
|
|
3917
|
+
}
|
|
3755
3918
|
lines.push(
|
|
3756
|
-
`
|
|
3919
|
+
` Total per-node file-backed footprint ~ ${formatBytes(totalFileMaxBytes)} (sum of max_bytes; worst case replicas = nodes). Ensure the NATS server max_file_store accommodates the sum across ALL services.`
|
|
3757
3920
|
);
|
|
3758
3921
|
return lines.join("\n");
|
|
3759
3922
|
};
|
|
@@ -3814,8 +3977,8 @@ var isEqual = (a, b) => {
|
|
|
3814
3977
|
};
|
|
3815
3978
|
|
|
3816
3979
|
// src/server/infrastructure/stream-migration.ts
|
|
3817
|
-
var
|
|
3818
|
-
var
|
|
3980
|
+
var import_common14 = require("@nestjs/common");
|
|
3981
|
+
var import_jetstream21 = require("@nats-io/jetstream");
|
|
3819
3982
|
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
3820
3983
|
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
3821
3984
|
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
@@ -3827,7 +3990,7 @@ var StreamMigration = class {
|
|
|
3827
3990
|
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
3828
3991
|
this.peerWaitMs = peerWaitMs;
|
|
3829
3992
|
}
|
|
3830
|
-
logger = new
|
|
3993
|
+
logger = new import_common14.Logger("Jetstream:Stream");
|
|
3831
3994
|
async migrate(jsm, streamName2, newConfig) {
|
|
3832
3995
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
3833
3996
|
const startTime = Date.now();
|
|
@@ -3846,7 +4009,7 @@ var StreamMigration = class {
|
|
|
3846
4009
|
await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
|
|
3847
4010
|
drainedCount = (await jsm.streams.info(streamName2)).state.messages;
|
|
3848
4011
|
if (drainedCount > 0) {
|
|
3849
|
-
this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages
|
|
4012
|
+
this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages -> ${backupName}`);
|
|
3850
4013
|
await jsm.streams.add({
|
|
3851
4014
|
...currentInfo.config,
|
|
3852
4015
|
name: backupName,
|
|
@@ -3867,7 +4030,7 @@ var StreamMigration = class {
|
|
|
3867
4030
|
} catch (err) {
|
|
3868
4031
|
if (originalDeleted) {
|
|
3869
4032
|
this.logger.error(
|
|
3870
|
-
`Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved
|
|
4033
|
+
`Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved; restoration resumes on the next startup.`
|
|
3871
4034
|
);
|
|
3872
4035
|
} else {
|
|
3873
4036
|
await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
|
|
@@ -3935,7 +4098,7 @@ var StreamMigration = class {
|
|
|
3935
4098
|
await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
|
|
3936
4099
|
}
|
|
3937
4100
|
/**
|
|
3938
|
-
* Lag-based drain check
|
|
4101
|
+
* Lag-based drain check: live publishes cannot fake completion. A fresh
|
|
3939
4102
|
* source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
|
|
3940
4103
|
* hence the active guard.
|
|
3941
4104
|
*/
|
|
@@ -3954,13 +4117,13 @@ var StreamMigration = class {
|
|
|
3954
4117
|
);
|
|
3955
4118
|
}
|
|
3956
4119
|
/**
|
|
3957
|
-
* A backup present at migrate() start is a live peer migration
|
|
4120
|
+
* A backup present at migrate() start is a live peer migration; wait it
|
|
3958
4121
|
* out. Stale leftovers were already handled by recoverInterrupted().
|
|
3959
4122
|
*/
|
|
3960
4123
|
async waitOutPeerMigration(jsm, backupName) {
|
|
3961
4124
|
if (await this.tryInfo(jsm, backupName) === null) return false;
|
|
3962
4125
|
this.logger.warn(
|
|
3963
|
-
`Migration backup ${backupName} exists
|
|
4126
|
+
`Migration backup ${backupName} exists; another instance appears to be migrating; waiting`
|
|
3964
4127
|
);
|
|
3965
4128
|
const deadline = Date.now() + this.peerWaitMs;
|
|
3966
4129
|
while (Date.now() < deadline) {
|
|
@@ -3981,7 +4144,7 @@ var StreamMigration = class {
|
|
|
3981
4144
|
}
|
|
3982
4145
|
} catch (rollbackErr) {
|
|
3983
4146
|
this.logger.error(
|
|
3984
|
-
`Rollback of ${streamName2} after a failed migration also failed
|
|
4147
|
+
`Rollback of ${streamName2} after a failed migration also failed; the stream may be left quiesced:`,
|
|
3985
4148
|
rollbackErr
|
|
3986
4149
|
);
|
|
3987
4150
|
}
|
|
@@ -3997,7 +4160,7 @@ var StreamMigration = class {
|
|
|
3997
4160
|
try {
|
|
3998
4161
|
return await jsm.streams.info(name);
|
|
3999
4162
|
} catch (err) {
|
|
4000
|
-
if (err instanceof
|
|
4163
|
+
if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4001
4164
|
return null;
|
|
4002
4165
|
}
|
|
4003
4166
|
throw err;
|
|
@@ -4005,7 +4168,7 @@ var StreamMigration = class {
|
|
|
4005
4168
|
}
|
|
4006
4169
|
};
|
|
4007
4170
|
|
|
4008
|
-
// src/server/infrastructure/
|
|
4171
|
+
// src/server/infrastructure/subject-utils.ts
|
|
4009
4172
|
var subjectCovers = (broad, narrow) => {
|
|
4010
4173
|
if (broad === narrow) return false;
|
|
4011
4174
|
const broadTokens = broad.split(".");
|
|
@@ -4017,16 +4180,21 @@ var subjectCovers = (broad, narrow) => {
|
|
|
4017
4180
|
}
|
|
4018
4181
|
return broadTokens.length === narrowTokens.length;
|
|
4019
4182
|
};
|
|
4183
|
+
var coversOrEquals = (broad, subject) => broad === subject || subjectCovers(broad, subject);
|
|
4184
|
+
|
|
4185
|
+
// src/server/infrastructure/stream.provider.ts
|
|
4020
4186
|
var StreamProvider = class {
|
|
4021
|
-
constructor(options, connection) {
|
|
4187
|
+
constructor(options, connection, names, binder) {
|
|
4022
4188
|
this.options = options;
|
|
4023
4189
|
this.connection = connection;
|
|
4190
|
+
this.names = names;
|
|
4191
|
+
this.binder = binder;
|
|
4024
4192
|
const derived = deriveOtelAttrs(options);
|
|
4025
4193
|
this.otel = derived.otel;
|
|
4026
4194
|
this.otelServiceName = derived.serviceName;
|
|
4027
4195
|
this.otelEndpoint = derived.serverEndpoint;
|
|
4028
4196
|
}
|
|
4029
|
-
logger = new
|
|
4197
|
+
logger = new import_common15.Logger("Jetstream:Stream");
|
|
4030
4198
|
migration = new StreamMigration();
|
|
4031
4199
|
otel;
|
|
4032
4200
|
otelServiceName;
|
|
@@ -4040,42 +4208,48 @@ var StreamProvider = class {
|
|
|
4040
4208
|
*/
|
|
4041
4209
|
async ensureStreams(kinds) {
|
|
4042
4210
|
const jsm = await this.connection.getJetStreamManager();
|
|
4043
|
-
const
|
|
4211
|
+
const { autoKinds, externalKinds } = this.partitionByManagement(kinds);
|
|
4212
|
+
const reservations = autoKinds.map(
|
|
4213
|
+
(kind) => this.buildReservation(kind, this.buildConfig(kind))
|
|
4214
|
+
);
|
|
4215
|
+
const external = externalKinds.map((kind) => ({
|
|
4216
|
+
kind,
|
|
4217
|
+
name: this.names.streamName(kind)
|
|
4218
|
+
}));
|
|
4219
|
+
const dlqIsManual = !!this.options.dlq && resolveManagementMode(this.options, "dlq", "stream") === "manual" /* Manual */;
|
|
4044
4220
|
if (this.options.dlq) {
|
|
4045
|
-
|
|
4221
|
+
if (dlqIsManual) {
|
|
4222
|
+
external.push({ kind: "dlq", name: this.names.dlqStreamName() });
|
|
4223
|
+
} else {
|
|
4224
|
+
reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
|
|
4225
|
+
}
|
|
4046
4226
|
}
|
|
4047
4227
|
this.logger.log(`
|
|
4048
|
-
${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
4049
|
-
if (this.options.provisioning?.preflightStorageCheck) {
|
|
4228
|
+
${formatProvisioningSummary(this.options.name, reservations, external)}`);
|
|
4229
|
+
if (this.options.provisioning?.preflightStorageCheck && reservations.length > 0) {
|
|
4050
4230
|
await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
|
|
4051
4231
|
}
|
|
4052
|
-
await Promise.all(
|
|
4232
|
+
await Promise.all([
|
|
4233
|
+
...autoKinds.map((kind) => this.ensureStream(jsm, kind)),
|
|
4234
|
+
...externalKinds.map((kind) => this.bindStream(jsm, kind))
|
|
4235
|
+
]);
|
|
4053
4236
|
if (this.options.dlq) {
|
|
4054
|
-
|
|
4237
|
+
if (dlqIsManual) {
|
|
4238
|
+
await this.bindDlqStream(jsm);
|
|
4239
|
+
} else {
|
|
4240
|
+
await this.ensureDlqStream(jsm);
|
|
4241
|
+
}
|
|
4055
4242
|
}
|
|
4056
4243
|
}
|
|
4057
4244
|
/** Get the stream name for a given kind. */
|
|
4058
4245
|
getStreamName(kind) {
|
|
4059
|
-
return
|
|
4246
|
+
return this.names.streamName(kind);
|
|
4060
4247
|
}
|
|
4061
4248
|
/** Get the subjects pattern for a given kind. */
|
|
4062
4249
|
getSubjects(kind) {
|
|
4063
|
-
const
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
const subjects = [`${name}.${"ev" /* Event */}.>`];
|
|
4067
|
-
if (this.isSchedulingEnabled(kind)) {
|
|
4068
|
-
subjects.push(`${name}._sch.>`);
|
|
4069
|
-
}
|
|
4070
|
-
return subjects;
|
|
4071
|
-
}
|
|
4072
|
-
case "cmd" /* Command */:
|
|
4073
|
-
return [`${name}.${"cmd" /* Command */}.>`];
|
|
4074
|
-
case "broadcast" /* Broadcast */:
|
|
4075
|
-
return ["broadcast.>"];
|
|
4076
|
-
case "ordered" /* Ordered */:
|
|
4077
|
-
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
4078
|
-
}
|
|
4250
|
+
const filter = this.names.filterSubject(kind);
|
|
4251
|
+
const dedicatedSchedule = kind === "ev" /* Event */ && this.isSchedulingEnabled(kind) && !this.names.hasCustomPrefix(kind);
|
|
4252
|
+
return dedicatedSchedule ? [filter, `${this.names.schedulePrefix(kind)}>`] : [filter];
|
|
4079
4253
|
}
|
|
4080
4254
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
4081
4255
|
async ensureStream(jsm, kind) {
|
|
@@ -4100,7 +4274,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4100
4274
|
const currentInfo = await jsm.streams.info(config.name);
|
|
4101
4275
|
return await this.handleExistingStream(jsm, currentInfo, config, ctx);
|
|
4102
4276
|
} catch (err) {
|
|
4103
|
-
if (err instanceof
|
|
4277
|
+
if (err instanceof import_jetstream22.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4104
4278
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
4105
4279
|
return await this.runStreamOp(ctx, () => jsm.streams.add(config));
|
|
4106
4280
|
}
|
|
@@ -4131,7 +4305,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4131
4305
|
const currentInfo = await jsm.streams.info(config.name);
|
|
4132
4306
|
return await this.handleExistingStream(jsm, currentInfo, config, ctx);
|
|
4133
4307
|
} catch (err) {
|
|
4134
|
-
if (err instanceof
|
|
4308
|
+
if (err instanceof import_jetstream22.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4135
4309
|
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
4136
4310
|
return await this.runStreamOp(ctx, () => jsm.streams.add(config));
|
|
4137
4311
|
}
|
|
@@ -4152,7 +4326,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4152
4326
|
}
|
|
4153
4327
|
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
4154
4328
|
if (diff.hasTransportControlledConflicts) {
|
|
4155
|
-
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)}
|
|
4329
|
+
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`).join(", ");
|
|
4156
4330
|
throw new Error(
|
|
4157
4331
|
`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.`
|
|
4158
4332
|
);
|
|
@@ -4202,13 +4376,13 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4202
4376
|
}
|
|
4203
4377
|
logChanges(streamName2, diff, migrationEnabled) {
|
|
4204
4378
|
for (const c of diff.changes) {
|
|
4205
|
-
const detail = `${c.property}: ${JSON.stringify(c.current)}
|
|
4379
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`;
|
|
4206
4380
|
if (c.mutability === "transport-controlled") {
|
|
4207
4381
|
this.logger.error(
|
|
4208
|
-
`Stream ${streamName2}: ${detail}
|
|
4382
|
+
`Stream ${streamName2}: ${detail}; transport-controlled, cannot be changed`
|
|
4209
4383
|
);
|
|
4210
4384
|
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
4211
|
-
this.logger.warn(`Stream ${streamName2}: ${detail}
|
|
4385
|
+
this.logger.warn(`Stream ${streamName2}: ${detail}; requires allowDestructiveMigration`);
|
|
4212
4386
|
} else {
|
|
4213
4387
|
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
4214
4388
|
}
|
|
@@ -4219,12 +4393,12 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4219
4393
|
return {
|
|
4220
4394
|
kind,
|
|
4221
4395
|
name: config.name,
|
|
4222
|
-
storage: config.storage ??
|
|
4396
|
+
storage: config.storage ?? import_jetstream22.StorageType.File,
|
|
4223
4397
|
numReplicas: config.num_replicas ?? 1,
|
|
4224
4398
|
maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
|
|
4225
4399
|
// NATS uses -1 for unlimited
|
|
4226
4400
|
maxAge: config.max_age ?? 0,
|
|
4227
|
-
retention: config.retention ??
|
|
4401
|
+
retention: config.retention ?? import_jetstream22.RetentionPolicy.Limits
|
|
4228
4402
|
};
|
|
4229
4403
|
}
|
|
4230
4404
|
errorContext(kind, config) {
|
|
@@ -4240,39 +4414,77 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4240
4414
|
try {
|
|
4241
4415
|
return await op();
|
|
4242
4416
|
} catch (err) {
|
|
4243
|
-
if (err instanceof
|
|
4417
|
+
if (err instanceof import_jetstream22.JetStreamApiError) {
|
|
4244
4418
|
throw mapProvisioningError(err, ctx);
|
|
4245
4419
|
}
|
|
4246
4420
|
throw err;
|
|
4247
4421
|
}
|
|
4248
4422
|
}
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
return {
|
|
4261
|
-
...defaults,
|
|
4262
|
-
...overrides,
|
|
4263
|
-
name,
|
|
4264
|
-
subjects,
|
|
4265
|
-
description
|
|
4266
|
-
};
|
|
4423
|
+
partitionByManagement(kinds) {
|
|
4424
|
+
const autoKinds = [];
|
|
4425
|
+
const externalKinds = [];
|
|
4426
|
+
for (const kind of kinds) {
|
|
4427
|
+
if (resolveManagementMode(this.options, kind, "stream") === "manual" /* Manual */) {
|
|
4428
|
+
externalKinds.push(kind);
|
|
4429
|
+
} else {
|
|
4430
|
+
autoKinds.push(kind);
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
return { autoKinds, externalKinds };
|
|
4267
4434
|
}
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4435
|
+
async bindStream(jsm, kind) {
|
|
4436
|
+
const name = this.names.streamName(kind);
|
|
4437
|
+
return withProvisioningSpan(
|
|
4438
|
+
this.otel,
|
|
4439
|
+
{
|
|
4440
|
+
serviceName: this.otelServiceName,
|
|
4441
|
+
endpoint: this.otelEndpoint,
|
|
4442
|
+
entity: "stream",
|
|
4443
|
+
name,
|
|
4444
|
+
action: "bind"
|
|
4445
|
+
},
|
|
4446
|
+
() => this.binder.bindStream(jsm, kind)
|
|
4447
|
+
);
|
|
4448
|
+
}
|
|
4449
|
+
async bindDlqStream(jsm) {
|
|
4450
|
+
const name = this.names.dlqStreamName();
|
|
4451
|
+
return withProvisioningSpan(
|
|
4452
|
+
this.otel,
|
|
4453
|
+
{
|
|
4454
|
+
serviceName: this.otelServiceName,
|
|
4455
|
+
endpoint: this.otelEndpoint,
|
|
4456
|
+
entity: "stream",
|
|
4457
|
+
name,
|
|
4458
|
+
action: "bind"
|
|
4459
|
+
},
|
|
4460
|
+
() => this.binder.bindDlqStream(jsm)
|
|
4461
|
+
);
|
|
4462
|
+
}
|
|
4463
|
+
/** The broadcast stream is global; every service in the cluster shares it. */
|
|
4464
|
+
isSharedStream(name) {
|
|
4465
|
+
return name === this.getStreamName("broadcast" /* Broadcast */);
|
|
4466
|
+
}
|
|
4467
|
+
/** Build the full stream config by merging defaults with user overrides. */
|
|
4468
|
+
buildConfig(kind) {
|
|
4469
|
+
const name = this.getStreamName(kind);
|
|
4470
|
+
const subjects = this.getSubjects(kind);
|
|
4471
|
+
const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
|
|
4472
|
+
const defaults = this.getDefaults(kind);
|
|
4473
|
+
const overrides = this.getOverrides(kind);
|
|
4474
|
+
return {
|
|
4475
|
+
...defaults,
|
|
4476
|
+
...overrides,
|
|
4477
|
+
name,
|
|
4478
|
+
subjects,
|
|
4479
|
+
description
|
|
4480
|
+
};
|
|
4481
|
+
}
|
|
4482
|
+
/**
|
|
4483
|
+
* Build the DLQ stream config: library defaults merged with user overrides,
|
|
4484
|
+
* with transport-controlled settings like retention stripped.
|
|
4273
4485
|
*/
|
|
4274
4486
|
buildDlqConfig() {
|
|
4275
|
-
const name =
|
|
4487
|
+
const name = this.names.dlqStreamName();
|
|
4276
4488
|
const subjects = [name];
|
|
4277
4489
|
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
4278
4490
|
const overrides = this.options.dlq?.stream ?? {};
|
|
@@ -4305,22 +4517,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4305
4517
|
}
|
|
4306
4518
|
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
4307
4519
|
getOverrides(kind) {
|
|
4308
|
-
|
|
4309
|
-
switch (kind) {
|
|
4310
|
-
case "ev" /* Event */:
|
|
4311
|
-
overrides = this.options.events?.stream ?? {};
|
|
4312
|
-
break;
|
|
4313
|
-
case "cmd" /* Command */:
|
|
4314
|
-
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
4315
|
-
break;
|
|
4316
|
-
case "broadcast" /* Broadcast */:
|
|
4317
|
-
overrides = this.options.broadcast?.stream ?? {};
|
|
4318
|
-
break;
|
|
4319
|
-
case "ordered" /* Ordered */:
|
|
4320
|
-
overrides = this.options.ordered?.stream ?? {};
|
|
4321
|
-
break;
|
|
4322
|
-
}
|
|
4323
|
-
return this.stripTransportControlled(overrides);
|
|
4520
|
+
return this.stripTransportControlled(kindOptionsBlock(this.options, kind)?.stream ?? {});
|
|
4324
4521
|
}
|
|
4325
4522
|
/**
|
|
4326
4523
|
* Remove transport-controlled properties from user overrides.
|
|
@@ -4330,7 +4527,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4330
4527
|
stripTransportControlled(overrides) {
|
|
4331
4528
|
if (!("retention" in overrides)) return overrides;
|
|
4332
4529
|
this.logger.debug(
|
|
4333
|
-
"Stripping user-provided retention override
|
|
4530
|
+
"Stripping user-provided retention override; retention is managed by the transport"
|
|
4334
4531
|
);
|
|
4335
4532
|
const cleaned = { ...overrides };
|
|
4336
4533
|
delete cleaned.retention;
|
|
@@ -4339,20 +4536,22 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4339
4536
|
};
|
|
4340
4537
|
|
|
4341
4538
|
// src/server/infrastructure/consumer.provider.ts
|
|
4342
|
-
var
|
|
4343
|
-
var
|
|
4539
|
+
var import_common16 = require("@nestjs/common");
|
|
4540
|
+
var import_jetstream24 = require("@nats-io/jetstream");
|
|
4344
4541
|
var ConsumerProvider = class {
|
|
4345
|
-
constructor(options, connection, streamProvider, patternRegistry) {
|
|
4542
|
+
constructor(options, connection, streamProvider, patternRegistry, names, binder) {
|
|
4346
4543
|
this.options = options;
|
|
4347
4544
|
this.connection = connection;
|
|
4348
4545
|
this.streamProvider = streamProvider;
|
|
4349
4546
|
this.patternRegistry = patternRegistry;
|
|
4547
|
+
this.names = names;
|
|
4548
|
+
this.binder = binder;
|
|
4350
4549
|
const derived = deriveOtelAttrs(options);
|
|
4351
4550
|
this.otel = derived.otel;
|
|
4352
4551
|
this.otelServiceName = derived.serviceName;
|
|
4353
4552
|
this.otelEndpoint = derived.serverEndpoint;
|
|
4354
4553
|
}
|
|
4355
|
-
logger = new
|
|
4554
|
+
logger = new import_common16.Logger("Jetstream:Consumer");
|
|
4356
4555
|
otel;
|
|
4357
4556
|
otelServiceName;
|
|
4358
4557
|
otelEndpoint;
|
|
@@ -4374,24 +4573,33 @@ var ConsumerProvider = class {
|
|
|
4374
4573
|
}
|
|
4375
4574
|
/** Get the consumer name for a given kind. */
|
|
4376
4575
|
getConsumerName(kind) {
|
|
4377
|
-
return
|
|
4576
|
+
return this.names.consumerName(kind);
|
|
4378
4577
|
}
|
|
4379
4578
|
/**
|
|
4380
4579
|
* Ensure a single consumer exists with the desired config.
|
|
4381
|
-
*
|
|
4382
|
-
* the current pod's configuration.
|
|
4580
|
+
* Startup path: creates or updates the consumer to match the current pod's configuration.
|
|
4383
4581
|
*/
|
|
4384
4582
|
async ensureConsumer(jsm, kind) {
|
|
4385
4583
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4386
4584
|
const config = this.buildConfig(kind);
|
|
4387
4585
|
const name = config.durable_name;
|
|
4586
|
+
const spanAttrs = {
|
|
4587
|
+
serviceName: this.otelServiceName,
|
|
4588
|
+
endpoint: this.otelEndpoint,
|
|
4589
|
+
entity: "consumer",
|
|
4590
|
+
name
|
|
4591
|
+
};
|
|
4592
|
+
if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
|
|
4593
|
+
return withProvisioningSpan(
|
|
4594
|
+
this.otel,
|
|
4595
|
+
{ ...spanAttrs, action: "bind" },
|
|
4596
|
+
() => this.binder.bindConsumer(jsm, kind)
|
|
4597
|
+
);
|
|
4598
|
+
}
|
|
4388
4599
|
return withProvisioningSpan(
|
|
4389
4600
|
this.otel,
|
|
4390
4601
|
{
|
|
4391
|
-
|
|
4392
|
-
endpoint: this.otelEndpoint,
|
|
4393
|
-
entity: "consumer",
|
|
4394
|
-
name,
|
|
4602
|
+
...spanAttrs,
|
|
4395
4603
|
action: "ensure"
|
|
4396
4604
|
},
|
|
4397
4605
|
async () => {
|
|
@@ -4402,7 +4610,7 @@ var ConsumerProvider = class {
|
|
|
4402
4610
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
4403
4611
|
return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
|
|
4404
4612
|
} catch (err) {
|
|
4405
|
-
if (!(err instanceof
|
|
4613
|
+
if (!(err instanceof import_jetstream24.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4406
4614
|
throw err;
|
|
4407
4615
|
}
|
|
4408
4616
|
return await this.createConsumer(jsm, stream, name, kind, config);
|
|
@@ -4412,28 +4620,40 @@ var ConsumerProvider = class {
|
|
|
4412
4620
|
}
|
|
4413
4621
|
/**
|
|
4414
4622
|
* Recover a consumer that disappeared during runtime.
|
|
4415
|
-
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
4416
4623
|
*
|
|
4417
|
-
*
|
|
4418
|
-
*
|
|
4419
|
-
*
|
|
4420
|
-
*
|
|
4421
|
-
* This prevents old pods from:
|
|
4422
|
-
* - Overwriting a newer pod's consumer config during rolling updates
|
|
4423
|
-
* - Creating consumers during migration (which would consume and delete
|
|
4424
|
-
* workqueue messages while they're being restored)
|
|
4624
|
+
* Self-healing path: creates if missing but never updates config, so an old pod
|
|
4625
|
+
* cannot overwrite a newer pod's config during rolling updates. If a migration
|
|
4626
|
+
* backup stream exists, throws so the retry loop backs off until migration completes;
|
|
4627
|
+
* creating a consumer mid-migration would eat workqueue messages being restored.
|
|
4425
4628
|
*/
|
|
4426
4629
|
async recoverConsumer(jsm, kind) {
|
|
4427
4630
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4428
4631
|
const config = this.buildConfig(kind);
|
|
4429
4632
|
const name = config.durable_name;
|
|
4633
|
+
const spanAttrs = {
|
|
4634
|
+
serviceName: this.otelServiceName,
|
|
4635
|
+
endpoint: this.otelEndpoint,
|
|
4636
|
+
entity: "consumer",
|
|
4637
|
+
name
|
|
4638
|
+
};
|
|
4639
|
+
if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
|
|
4640
|
+
return withProvisioningSpan(this.otel, { ...spanAttrs, action: "rebind" }, async () => {
|
|
4641
|
+
try {
|
|
4642
|
+
return await jsm.consumers.info(stream, name);
|
|
4643
|
+
} catch (err) {
|
|
4644
|
+
if (err instanceof import_jetstream24.JetStreamApiError && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
|
|
4645
|
+
throw new Error(
|
|
4646
|
+
`Consumer ${name} on ${stream} is externally managed and currently absent; waiting for it to be restored.`
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4649
|
+
throw err;
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4430
4653
|
return withProvisioningSpan(
|
|
4431
4654
|
this.otel,
|
|
4432
4655
|
{
|
|
4433
|
-
|
|
4434
|
-
endpoint: this.otelEndpoint,
|
|
4435
|
-
entity: "consumer",
|
|
4436
|
-
name,
|
|
4656
|
+
...spanAttrs,
|
|
4437
4657
|
action: "recover"
|
|
4438
4658
|
},
|
|
4439
4659
|
async () => {
|
|
@@ -4442,7 +4662,7 @@ var ConsumerProvider = class {
|
|
|
4442
4662
|
try {
|
|
4443
4663
|
return await jsm.consumers.info(stream, name);
|
|
4444
4664
|
} catch (err) {
|
|
4445
|
-
if (!(err instanceof
|
|
4665
|
+
if (!(err instanceof import_jetstream24.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4446
4666
|
throw err;
|
|
4447
4667
|
}
|
|
4448
4668
|
return await this.createConsumer(jsm, stream, name, kind, config);
|
|
@@ -4463,26 +4683,24 @@ var ConsumerProvider = class {
|
|
|
4463
4683
|
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
4464
4684
|
);
|
|
4465
4685
|
} catch (err) {
|
|
4466
|
-
if (err instanceof
|
|
4686
|
+
if (err instanceof import_jetstream24.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4467
4687
|
return;
|
|
4468
4688
|
}
|
|
4469
4689
|
throw err;
|
|
4470
4690
|
}
|
|
4471
4691
|
}
|
|
4472
|
-
/**
|
|
4473
|
-
* Create a consumer, handling the race where another pod creates it first.
|
|
4474
|
-
*/
|
|
4692
|
+
/** Create a consumer, handling the race where another pod creates it first. */
|
|
4475
4693
|
async createConsumer(jsm, stream, name, kind, config) {
|
|
4476
4694
|
this.logger.log(`Creating consumer: ${name}`);
|
|
4477
4695
|
const ctx = { entity: "consumer", name, kind };
|
|
4478
4696
|
try {
|
|
4479
4697
|
return await jsm.consumers.add(stream, config);
|
|
4480
4698
|
} catch (addErr) {
|
|
4481
|
-
if (addErr instanceof
|
|
4699
|
+
if (addErr instanceof import_jetstream24.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
4482
4700
|
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
4483
4701
|
return await jsm.consumers.info(stream, name);
|
|
4484
4702
|
}
|
|
4485
|
-
if (addErr instanceof
|
|
4703
|
+
if (addErr instanceof import_jetstream24.JetStreamApiError) {
|
|
4486
4704
|
throw mapProvisioningError(addErr, ctx);
|
|
4487
4705
|
}
|
|
4488
4706
|
throw addErr;
|
|
@@ -4492,17 +4710,15 @@ var ConsumerProvider = class {
|
|
|
4492
4710
|
try {
|
|
4493
4711
|
return await op();
|
|
4494
4712
|
} catch (err) {
|
|
4495
|
-
if (err instanceof
|
|
4713
|
+
if (err instanceof import_jetstream24.JetStreamApiError) {
|
|
4496
4714
|
throw mapProvisioningError(err, ctx);
|
|
4497
4715
|
}
|
|
4498
4716
|
throw err;
|
|
4499
4717
|
}
|
|
4500
4718
|
}
|
|
4501
4719
|
/** Build consumer config by merging defaults with user overrides. */
|
|
4502
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
4503
4720
|
buildConfig(kind) {
|
|
4504
|
-
const
|
|
4505
|
-
const serviceName = internalName(this.options.name);
|
|
4721
|
+
const durableName = this.getConsumerName(kind);
|
|
4506
4722
|
const defaults = this.getDefaults(kind);
|
|
4507
4723
|
const overrides = this.getOverrides(kind);
|
|
4508
4724
|
if (kind === "broadcast" /* Broadcast */) {
|
|
@@ -4514,31 +4730,54 @@ var ConsumerProvider = class {
|
|
|
4514
4730
|
return {
|
|
4515
4731
|
...defaults,
|
|
4516
4732
|
...overrides,
|
|
4517
|
-
name,
|
|
4518
|
-
durable_name:
|
|
4733
|
+
name: durableName,
|
|
4734
|
+
durable_name: durableName,
|
|
4519
4735
|
filter_subject: broadcastPatterns[0]
|
|
4520
4736
|
};
|
|
4521
4737
|
}
|
|
4522
4738
|
return {
|
|
4523
4739
|
...defaults,
|
|
4524
4740
|
...overrides,
|
|
4525
|
-
name,
|
|
4526
|
-
durable_name:
|
|
4741
|
+
name: durableName,
|
|
4742
|
+
durable_name: durableName,
|
|
4527
4743
|
filter_subjects: broadcastPatterns
|
|
4528
4744
|
};
|
|
4529
4745
|
}
|
|
4530
4746
|
if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
|
|
4531
4747
|
throw new Error(`Unexpected durable consumer kind: ${kind}`);
|
|
4532
4748
|
}
|
|
4533
|
-
|
|
4749
|
+
if (this.names.hasCustomPrefix(kind)) {
|
|
4750
|
+
return this.buildCustomPrefixConfig(kind, durableName, defaults, overrides);
|
|
4751
|
+
}
|
|
4752
|
+
const filter_subject = this.names.filterSubject(kind);
|
|
4534
4753
|
return {
|
|
4535
4754
|
...defaults,
|
|
4536
4755
|
...overrides,
|
|
4537
|
-
name,
|
|
4538
|
-
durable_name:
|
|
4756
|
+
name: durableName,
|
|
4757
|
+
durable_name: durableName,
|
|
4539
4758
|
filter_subject
|
|
4540
4759
|
};
|
|
4541
4760
|
}
|
|
4761
|
+
buildCustomPrefixConfig(kind, durableName, defaults, overrides) {
|
|
4762
|
+
const patterns = kind === "ev" /* Event */ ? this.patternRegistry.getEventPatterns() : this.patternRegistry.getCommandPatterns();
|
|
4763
|
+
const subjects = patterns.map((p) => this.names.subject(kind, p));
|
|
4764
|
+
if (subjects.length === 1) {
|
|
4765
|
+
return {
|
|
4766
|
+
...defaults,
|
|
4767
|
+
...overrides,
|
|
4768
|
+
name: durableName,
|
|
4769
|
+
durable_name: durableName,
|
|
4770
|
+
filter_subject: subjects[0]
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
4773
|
+
return {
|
|
4774
|
+
...defaults,
|
|
4775
|
+
...overrides,
|
|
4776
|
+
name: durableName,
|
|
4777
|
+
durable_name: durableName,
|
|
4778
|
+
filter_subjects: subjects
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4542
4781
|
/** Get default config for a consumer kind. */
|
|
4543
4782
|
getDefaults(kind) {
|
|
4544
4783
|
switch (kind) {
|
|
@@ -4559,27 +4798,13 @@ var ConsumerProvider = class {
|
|
|
4559
4798
|
}
|
|
4560
4799
|
/** Get user-provided overrides for a consumer kind. */
|
|
4561
4800
|
getOverrides(kind) {
|
|
4562
|
-
|
|
4563
|
-
case "ev" /* Event */:
|
|
4564
|
-
return this.options.events?.consumer ?? {};
|
|
4565
|
-
case "cmd" /* Command */:
|
|
4566
|
-
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
4567
|
-
case "broadcast" /* Broadcast */:
|
|
4568
|
-
return this.options.broadcast?.consumer ?? {};
|
|
4569
|
-
case "ordered" /* Ordered */:
|
|
4570
|
-
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
4571
|
-
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
4572
|
-
default: {
|
|
4573
|
-
const _exhaustive = kind;
|
|
4574
|
-
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4801
|
+
return kindOptionsBlock(this.options, kind)?.consumer ?? {};
|
|
4577
4802
|
}
|
|
4578
4803
|
};
|
|
4579
4804
|
|
|
4580
4805
|
// src/server/infrastructure/message.provider.ts
|
|
4581
|
-
var
|
|
4582
|
-
var
|
|
4806
|
+
var import_common17 = require("@nestjs/common");
|
|
4807
|
+
var import_jetstream26 = require("@nats-io/jetstream");
|
|
4583
4808
|
var import_rxjs3 = require("rxjs");
|
|
4584
4809
|
var MessageProvider = class {
|
|
4585
4810
|
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
@@ -4588,7 +4813,7 @@ var MessageProvider = class {
|
|
|
4588
4813
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
4589
4814
|
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
4590
4815
|
}
|
|
4591
|
-
logger = new
|
|
4816
|
+
logger = new import_common17.Logger("Jetstream:Message");
|
|
4592
4817
|
activeIterators = /* @__PURE__ */ new Set();
|
|
4593
4818
|
orderedReadyResolve = null;
|
|
4594
4819
|
orderedReadyReject = null;
|
|
@@ -4631,7 +4856,7 @@ var MessageProvider = class {
|
|
|
4631
4856
|
/**
|
|
4632
4857
|
* Start an ordered consumer for strict sequential delivery.
|
|
4633
4858
|
*
|
|
4634
|
-
* Unlike durable consumers, ordered consumers are ephemeral
|
|
4859
|
+
* Unlike durable consumers, ordered consumers are ephemeral: created at
|
|
4635
4860
|
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
4636
4861
|
*
|
|
4637
4862
|
* @param streamName - JetStream stream to consume from.
|
|
@@ -4640,7 +4865,7 @@ var MessageProvider = class {
|
|
|
4640
4865
|
*/
|
|
4641
4866
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
4642
4867
|
const consumerOpts = { filter_subjects: filterSubjects };
|
|
4643
|
-
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !==
|
|
4868
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream26.DeliverPolicy.All) {
|
|
4644
4869
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
4645
4870
|
}
|
|
4646
4871
|
if (orderedConfig?.optStartSeq !== void 0) {
|
|
@@ -4845,7 +5070,7 @@ var MessageProvider = class {
|
|
|
4845
5070
|
};
|
|
4846
5071
|
|
|
4847
5072
|
// src/server/infrastructure/metadata.provider.ts
|
|
4848
|
-
var
|
|
5073
|
+
var import_common18 = require("@nestjs/common");
|
|
4849
5074
|
var import_kv = require("@nats-io/kv");
|
|
4850
5075
|
var MetadataProvider = class {
|
|
4851
5076
|
constructor(options, connection) {
|
|
@@ -4854,7 +5079,7 @@ var MetadataProvider = class {
|
|
|
4854
5079
|
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
4855
5080
|
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
4856
5081
|
}
|
|
4857
|
-
logger = new
|
|
5082
|
+
logger = new import_common18.Logger("Jetstream:Metadata");
|
|
4858
5083
|
bucketName;
|
|
4859
5084
|
replicas;
|
|
4860
5085
|
ttl;
|
|
@@ -4862,16 +5087,12 @@ var MetadataProvider = class {
|
|
|
4862
5087
|
heartbeatTimer;
|
|
4863
5088
|
cachedKv;
|
|
4864
5089
|
/**
|
|
4865
|
-
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
5090
|
+
* Write handler metadata entries to the KV bucket and start the heartbeat.
|
|
4866
5091
|
*
|
|
4867
|
-
* Creates the bucket if
|
|
4868
|
-
*
|
|
4869
|
-
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
4870
|
-
* to prevent TTL expiry while the pod is alive.
|
|
5092
|
+
* Creates the bucket if missing; skips silently when the map is empty.
|
|
5093
|
+
* Non-critical: errors are logged but do not prevent transport startup.
|
|
4871
5094
|
*
|
|
4872
|
-
*
|
|
4873
|
-
*
|
|
4874
|
-
* @param entries Map of KV key → metadata object.
|
|
5095
|
+
* @param entries Map of KV key to metadata object.
|
|
4875
5096
|
*/
|
|
4876
5097
|
async publish(entries) {
|
|
4877
5098
|
if (entries.size === 0) return;
|
|
@@ -4947,396 +5168,626 @@ var MetadataProvider = class {
|
|
|
4947
5168
|
};
|
|
4948
5169
|
|
|
4949
5170
|
// src/server/routing/event.router.ts
|
|
4950
|
-
var
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
var
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
5171
|
+
var import_common20 = require("@nestjs/common");
|
|
5172
|
+
|
|
5173
|
+
// src/server/routing/concurrency-gate.ts
|
|
5174
|
+
var BACKLOG_WARN_THRESHOLD = 1e3;
|
|
5175
|
+
var ConcurrencyGate = class {
|
|
5176
|
+
constructor(maxActive, route, parkTimer, logger5, label) {
|
|
5177
|
+
this.maxActive = maxActive;
|
|
5178
|
+
this.route = route;
|
|
5179
|
+
this.parkTimer = parkTimer;
|
|
5180
|
+
this.logger = logger5;
|
|
5181
|
+
this.label = label;
|
|
5182
|
+
}
|
|
5183
|
+
active = 0;
|
|
5184
|
+
backlogWarned = false;
|
|
5185
|
+
backlog = [];
|
|
5186
|
+
/** Entry point for each incoming message. */
|
|
5187
|
+
push(msg) {
|
|
5188
|
+
if (this.active >= this.maxActive) {
|
|
5189
|
+
this.backlog.push({
|
|
5190
|
+
msg,
|
|
5191
|
+
stopAckExtension: this.parkTimer ? this.parkTimer(msg) : null
|
|
5192
|
+
});
|
|
5193
|
+
if (!this.backlogWarned && this.backlog.length >= BACKLOG_WARN_THRESHOLD) {
|
|
5194
|
+
this.backlogWarned = true;
|
|
5195
|
+
this.logger.warn(
|
|
5196
|
+
`${this.label} backlog reached ${this.backlog.length} messages; consumer may be falling behind`
|
|
5197
|
+
);
|
|
5198
|
+
}
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
this.active++;
|
|
5202
|
+
const result = this.routeSafely(msg);
|
|
5203
|
+
if (result !== void 0) {
|
|
5204
|
+
this.trackAsync(result, msg);
|
|
5205
|
+
} else {
|
|
5206
|
+
this.active--;
|
|
5207
|
+
if (this.backlog.length > 0) this.drainBacklog();
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
/** Stop parked timers and drop the backlog. */
|
|
5211
|
+
dispose() {
|
|
5212
|
+
for (const queued of this.backlog) {
|
|
5213
|
+
queued.stopAckExtension?.();
|
|
5214
|
+
}
|
|
5215
|
+
this.backlog.length = 0;
|
|
5216
|
+
}
|
|
5217
|
+
onAsyncDone = () => {
|
|
5218
|
+
this.active--;
|
|
5219
|
+
this.drainBacklog();
|
|
5220
|
+
};
|
|
5221
|
+
/** A throw here must not leak the concurrency slot or kill the subscription. */
|
|
5222
|
+
routeSafely(msg) {
|
|
5223
|
+
try {
|
|
5224
|
+
return this.route(msg);
|
|
5225
|
+
} catch (err) {
|
|
5226
|
+
this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5227
|
+
return void 0;
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
trackAsync(result, msg) {
|
|
5231
|
+
void result.catch((err) => {
|
|
5232
|
+
this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5233
|
+
}).finally(this.onAsyncDone);
|
|
5234
|
+
}
|
|
5235
|
+
drainBacklog() {
|
|
5236
|
+
while (this.active < this.maxActive) {
|
|
5237
|
+
const next = this.backlog.shift();
|
|
5238
|
+
if (next === void 0) break;
|
|
5239
|
+
next.stopAckExtension?.();
|
|
5240
|
+
this.active++;
|
|
5241
|
+
const result = this.routeSafely(next.msg);
|
|
5242
|
+
if (result !== void 0) {
|
|
5243
|
+
this.trackAsync(result, next.msg);
|
|
5244
|
+
} else {
|
|
5245
|
+
this.active--;
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
if (this.backlog.length < BACKLOG_WARN_THRESHOLD) this.backlogWarned = false;
|
|
5249
|
+
}
|
|
4957
5250
|
};
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
5251
|
+
|
|
5252
|
+
// src/server/routing/dead-letter-capture.ts
|
|
5253
|
+
var import_common19 = require("@nestjs/common");
|
|
5254
|
+
var import_transport_node5 = require("@nats-io/transport-node");
|
|
5255
|
+
var DLQ_PUBLISH_ATTEMPTS = 3;
|
|
5256
|
+
var DeadLetterCapture = class {
|
|
5257
|
+
constructor(patternRegistry, eventBus, deadLetterConfig, otel, serviceName, serverEndpoint, connection, options, names) {
|
|
4961
5258
|
this.patternRegistry = patternRegistry;
|
|
4962
|
-
this.codec = codec;
|
|
4963
5259
|
this.eventBus = eventBus;
|
|
4964
5260
|
this.deadLetterConfig = deadLetterConfig;
|
|
4965
|
-
this.
|
|
4966
|
-
this.
|
|
5261
|
+
this.otel = otel;
|
|
5262
|
+
this.serviceName = serviceName;
|
|
5263
|
+
this.serverEndpoint = serverEndpoint;
|
|
4967
5264
|
this.connection = connection;
|
|
4968
5265
|
this.options = options;
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5266
|
+
this.names = names;
|
|
5267
|
+
}
|
|
5268
|
+
logger = new import_common19.Logger("Jetstream:DeadLetter");
|
|
5269
|
+
/** True when this delivery is the consumer's last attempt for the message. */
|
|
5270
|
+
isFinalDelivery(msg) {
|
|
5271
|
+
const maxDeliver = this.deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
5272
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
5273
|
+
return msg.info.deliveryCount >= maxDeliver;
|
|
5274
|
+
}
|
|
5275
|
+
/** Emit the dead-letter event and route the message to the DLQ or the fallback callback. */
|
|
5276
|
+
async capture(msg, data, error) {
|
|
5277
|
+
const info = {
|
|
5278
|
+
subject: msg.subject,
|
|
5279
|
+
data,
|
|
5280
|
+
headers: msg.headers,
|
|
5281
|
+
error,
|
|
5282
|
+
deliveryCount: msg.info.deliveryCount,
|
|
5283
|
+
stream: msg.info.stream,
|
|
5284
|
+
streamSequence: msg.info.streamSequence,
|
|
5285
|
+
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
5286
|
+
};
|
|
5287
|
+
await withDeadLetterSpan(
|
|
5288
|
+
{
|
|
5289
|
+
msg,
|
|
5290
|
+
// Surface the registered pattern so APM can filter dead letters by
|
|
5291
|
+
// handler; falls back to the raw subject when no handler matches.
|
|
5292
|
+
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
5293
|
+
finalDeliveryCount: msg.info.deliveryCount,
|
|
5294
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
5295
|
+
serviceName: this.serviceName,
|
|
5296
|
+
endpoint: this.serverEndpoint
|
|
5297
|
+
},
|
|
5298
|
+
this.otel,
|
|
5299
|
+
async () => {
|
|
5300
|
+
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
5301
|
+
if (!this.options?.dlq) {
|
|
5302
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5303
|
+
} else {
|
|
5304
|
+
await this.publishToDlq(msg, info, error);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
);
|
|
4979
5308
|
}
|
|
4980
|
-
logger = new import_common18.Logger("Jetstream:EventRouter");
|
|
4981
|
-
subscriptions = [];
|
|
4982
|
-
otel;
|
|
4983
|
-
serviceName;
|
|
4984
|
-
serverEndpoint;
|
|
4985
5309
|
/**
|
|
4986
|
-
*
|
|
4987
|
-
*
|
|
5310
|
+
* Publish the dead letter to the DLQ stream with diagnostic headers. On
|
|
5311
|
+
* success the `onDeadLetter` callback is notified and the message termed;
|
|
5312
|
+
* on failure everything falls back to the callback to avoid silent loss.
|
|
4988
5313
|
*/
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
this.
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5314
|
+
async publishToDlq(msg, info, error) {
|
|
5315
|
+
const serviceName = this.options?.name;
|
|
5316
|
+
if (!this.connection || !serviceName) {
|
|
5317
|
+
this.logger.error(
|
|
5318
|
+
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
5319
|
+
);
|
|
5320
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5321
|
+
return;
|
|
5322
|
+
}
|
|
5323
|
+
const dlqStreamOverride = this.options.dlq?.stream?.name;
|
|
5324
|
+
const destinationSubject = this.names ? this.names.dlqStreamName() : dlqStreamOverride ?? dlqStreamName(serviceName);
|
|
5325
|
+
const hdrs = this.buildDlqHeaders(msg);
|
|
5326
|
+
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, this.extractErrorReason(error));
|
|
5327
|
+
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
5328
|
+
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
5329
|
+
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
5330
|
+
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
5331
|
+
try {
|
|
5332
|
+
await this.publishWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
5333
|
+
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
5334
|
+
await this.notifyDeadLetterCallback(info, msg);
|
|
5335
|
+
} catch (publishErr) {
|
|
5336
|
+
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
5337
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
4999
5338
|
}
|
|
5000
5339
|
}
|
|
5001
|
-
/**
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5340
|
+
/**
|
|
5341
|
+
* Past max_deliver the server never redelivers, so these in-process attempts
|
|
5342
|
+
* are the only second chance a dead letter gets. No artificial delay: an
|
|
5343
|
+
* unreachable broker already spaces attempts via its own request timeout.
|
|
5344
|
+
*/
|
|
5345
|
+
async publishWithRetry(connection, subject, data, headers2) {
|
|
5346
|
+
let lastErr;
|
|
5347
|
+
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5348
|
+
try {
|
|
5349
|
+
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5350
|
+
return;
|
|
5351
|
+
} catch (err) {
|
|
5352
|
+
lastErr = err;
|
|
5353
|
+
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5354
|
+
this.logger.warn(
|
|
5355
|
+
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5356
|
+
);
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5005
5359
|
}
|
|
5006
|
-
|
|
5360
|
+
throw lastErr;
|
|
5007
5361
|
}
|
|
5008
|
-
/**
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
const
|
|
5014
|
-
|
|
5015
|
-
const
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
const spanKind = eventConsumeKindFor(kind);
|
|
5020
|
-
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
5021
|
-
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
5022
|
-
const concurrency = this.getConcurrency(kind);
|
|
5023
|
-
const hasDlqCheck = deadLetterConfig !== void 0;
|
|
5024
|
-
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
5025
|
-
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
5026
|
-
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
5027
|
-
const pattern = declared?.pattern ?? msg.subject;
|
|
5028
|
-
const declaredKind = declared?.kind ?? kind;
|
|
5029
|
-
const durationMs = performance.now() - startedAt;
|
|
5030
|
-
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
5031
|
-
};
|
|
5032
|
-
const isDeadLetter = (msg) => {
|
|
5033
|
-
if (!hasDlqCheck) return false;
|
|
5034
|
-
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
5035
|
-
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
5036
|
-
return msg.info.deliveryCount >= maxDeliver;
|
|
5037
|
-
};
|
|
5038
|
-
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
5039
|
-
const settleSuccess = (msg, ctx, data) => {
|
|
5040
|
-
if (ctx.shouldTerminate) {
|
|
5041
|
-
settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
|
|
5042
|
-
msg.term(ctx.terminateReason);
|
|
5043
|
-
});
|
|
5044
|
-
return void 0;
|
|
5362
|
+
/**
|
|
5363
|
+
* Copy headers for the DLQ republish, dropping NATS control headers: a
|
|
5364
|
+
* copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
|
|
5365
|
+
*/
|
|
5366
|
+
buildDlqHeaders(msg) {
|
|
5367
|
+
const hdrs = (0, import_transport_node5.headers)();
|
|
5368
|
+
if (!msg.headers) return hdrs;
|
|
5369
|
+
for (const [k, v] of msg.headers) {
|
|
5370
|
+
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5371
|
+
for (const val of v) {
|
|
5372
|
+
hdrs.append(k, val);
|
|
5045
5373
|
}
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
msg.
|
|
5056
|
-
|
|
5057
|
-
|
|
5374
|
+
}
|
|
5375
|
+
return hdrs;
|
|
5376
|
+
}
|
|
5377
|
+
async notifyDeadLetterCallback(info, msg) {
|
|
5378
|
+
if (this.deadLetterConfig.onDeadLetter) {
|
|
5379
|
+
try {
|
|
5380
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
5381
|
+
} catch (hookErr) {
|
|
5382
|
+
this.logger.warn(
|
|
5383
|
+
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
5384
|
+
hookErr
|
|
5385
|
+
);
|
|
5058
5386
|
}
|
|
5059
|
-
|
|
5060
|
-
|
|
5387
|
+
}
|
|
5388
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5389
|
+
msg.term("Moved to DLQ stream");
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
/**
|
|
5393
|
+
* Last resort: invoke onDeadLetter, then term on success. On failure the
|
|
5394
|
+
* message is nak'd: never redelivered past max_deliver, but preserved.
|
|
5395
|
+
*/
|
|
5396
|
+
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
5397
|
+
const onDeadLetter = this.deadLetterConfig.onDeadLetter;
|
|
5398
|
+
if (!onDeadLetter) {
|
|
5399
|
+
this.logger.error(
|
|
5400
|
+
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback); leaving the message in the stream`
|
|
5401
|
+
);
|
|
5402
|
+
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5403
|
+
msg.nak();
|
|
5404
|
+
});
|
|
5405
|
+
return;
|
|
5406
|
+
}
|
|
5407
|
+
try {
|
|
5408
|
+
await onDeadLetter(info);
|
|
5409
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5410
|
+
msg.term("Dead letter processed via fallback callback");
|
|
5411
|
+
});
|
|
5412
|
+
} catch (hookErr) {
|
|
5413
|
+
this.logger.error(
|
|
5414
|
+
`Fallback onDeadLetter callback failed for ${msg.subject}; the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
|
|
5415
|
+
hookErr
|
|
5416
|
+
);
|
|
5417
|
+
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5418
|
+
msg.nak();
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
extractErrorReason(error) {
|
|
5423
|
+
if (error instanceof Error) {
|
|
5424
|
+
return error.message;
|
|
5425
|
+
}
|
|
5426
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
5427
|
+
return String(error.message);
|
|
5428
|
+
}
|
|
5429
|
+
return String(error);
|
|
5430
|
+
}
|
|
5431
|
+
};
|
|
5432
|
+
|
|
5433
|
+
// src/server/routing/settlement.ts
|
|
5434
|
+
var statusForContext = (ctx) => {
|
|
5435
|
+
if (ctx.shouldTerminate) return "terminated";
|
|
5436
|
+
if (ctx.shouldRetry) return "retried";
|
|
5437
|
+
return "success";
|
|
5438
|
+
};
|
|
5439
|
+
var createSettlement = (logger5, capture) => {
|
|
5440
|
+
const settleSuccess = (msg, ctx, data) => {
|
|
5441
|
+
if (ctx.shouldTerminate) {
|
|
5442
|
+
settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
|
|
5443
|
+
msg.term(ctx.terminateReason);
|
|
5061
5444
|
});
|
|
5062
5445
|
return void 0;
|
|
5063
|
-
}
|
|
5064
|
-
|
|
5065
|
-
if (
|
|
5066
|
-
|
|
5067
|
-
|
|
5446
|
+
}
|
|
5447
|
+
if (ctx.shouldRetry) {
|
|
5448
|
+
if (capture?.isFinalDelivery(msg)) {
|
|
5449
|
+
return capture.capture(
|
|
5450
|
+
msg,
|
|
5451
|
+
data,
|
|
5452
|
+
new Error("Retry requested on the final delivery attempt")
|
|
5453
|
+
);
|
|
5068
5454
|
}
|
|
5069
5455
|
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5070
|
-
msg.nak();
|
|
5456
|
+
msg.nak(ctx.retryDelay);
|
|
5071
5457
|
});
|
|
5072
|
-
|
|
5073
|
-
|
|
5458
|
+
return void 0;
|
|
5459
|
+
}
|
|
5460
|
+
settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
|
|
5461
|
+
msg.ack();
|
|
5462
|
+
});
|
|
5463
|
+
return void 0;
|
|
5464
|
+
};
|
|
5465
|
+
const settleFailure = async (msg, data, err) => {
|
|
5466
|
+
if (capture?.isFinalDelivery(msg)) {
|
|
5467
|
+
await capture.capture(msg, data, err);
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5470
|
+
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5471
|
+
msg.nak();
|
|
5472
|
+
});
|
|
5473
|
+
};
|
|
5474
|
+
return { settleSuccess, settleFailure };
|
|
5475
|
+
};
|
|
5476
|
+
|
|
5477
|
+
// src/server/routing/event-pipeline.ts
|
|
5478
|
+
var createHandlerReporter = (rctx) => {
|
|
5479
|
+
const { eventBus, patternRegistry, kind } = rctx;
|
|
5480
|
+
return (msg, startedAt, status) => {
|
|
5481
|
+
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
5482
|
+
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
5483
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
5484
|
+
const declaredKind = declared?.kind ?? kind;
|
|
5485
|
+
const durationMs = performance.now() - startedAt;
|
|
5486
|
+
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
5487
|
+
};
|
|
5488
|
+
};
|
|
5489
|
+
var createEventResolver = (rctx) => {
|
|
5490
|
+
const { patternRegistry, codec, eventBus, logger: logger5, capture, kind } = rctx;
|
|
5491
|
+
const captureUnroutable = (activeCapture, msg, err) => {
|
|
5492
|
+
let data;
|
|
5493
|
+
try {
|
|
5494
|
+
data = codec.decode(msg.data);
|
|
5495
|
+
} catch {
|
|
5496
|
+
data = void 0;
|
|
5497
|
+
}
|
|
5498
|
+
return activeCapture.capture(msg, data, err).catch((captureErr) => {
|
|
5499
|
+
logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
|
|
5500
|
+
});
|
|
5501
|
+
};
|
|
5502
|
+
return (msg) => {
|
|
5503
|
+
const subject = msg.subject;
|
|
5504
|
+
try {
|
|
5505
|
+
const handler = patternRegistry.getHandler(subject);
|
|
5506
|
+
if (!handler) {
|
|
5507
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
5508
|
+
if (capture !== null) {
|
|
5509
|
+
return captureUnroutable(capture, msg, new Error(`No handler for event: ${subject}`));
|
|
5510
|
+
}
|
|
5511
|
+
msg.term(`No handler for event: ${subject}`);
|
|
5512
|
+
return null;
|
|
5513
|
+
}
|
|
5074
5514
|
let data;
|
|
5075
5515
|
try {
|
|
5076
5516
|
data = codec.decode(msg.data);
|
|
5077
|
-
} catch {
|
|
5078
|
-
data = void 0;
|
|
5079
|
-
}
|
|
5080
|
-
return capture(msg, data, err).catch((captureErr) => {
|
|
5081
|
-
logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
|
|
5082
|
-
});
|
|
5083
|
-
};
|
|
5084
|
-
const resolveEvent = (msg) => {
|
|
5085
|
-
const subject = msg.subject;
|
|
5086
|
-
try {
|
|
5087
|
-
const handler = patternRegistry.getHandler(subject);
|
|
5088
|
-
if (!handler) {
|
|
5089
|
-
logger5.error(`No handler for subject: ${subject}`);
|
|
5090
|
-
if (handleDeadLetter !== null) {
|
|
5091
|
-
return captureUnroutable(
|
|
5092
|
-
handleDeadLetter,
|
|
5093
|
-
msg,
|
|
5094
|
-
new Error(`No handler for event: ${subject}`)
|
|
5095
|
-
);
|
|
5096
|
-
}
|
|
5097
|
-
msg.term(`No handler for event: ${subject}`);
|
|
5098
|
-
return null;
|
|
5099
|
-
}
|
|
5100
|
-
let data;
|
|
5101
|
-
try {
|
|
5102
|
-
data = codec.decode(msg.data);
|
|
5103
|
-
} catch (err) {
|
|
5104
|
-
logger5.error(`Decode error for ${subject}:`, err);
|
|
5105
|
-
if (handleDeadLetter !== null) {
|
|
5106
|
-
return captureUnroutable(
|
|
5107
|
-
handleDeadLetter,
|
|
5108
|
-
msg,
|
|
5109
|
-
new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
|
|
5110
|
-
);
|
|
5111
|
-
}
|
|
5112
|
-
msg.term("Decode error");
|
|
5113
|
-
return null;
|
|
5114
|
-
}
|
|
5115
|
-
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5116
|
-
return { handler, data };
|
|
5117
5517
|
} catch (err) {
|
|
5118
|
-
logger5.error(`
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5518
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
5519
|
+
if (capture !== null) {
|
|
5520
|
+
return captureUnroutable(
|
|
5521
|
+
capture,
|
|
5522
|
+
msg,
|
|
5523
|
+
new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
|
|
5524
|
+
);
|
|
5123
5525
|
}
|
|
5526
|
+
msg.term("Decode error");
|
|
5124
5527
|
return null;
|
|
5125
5528
|
}
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
return "success";
|
|
5131
|
-
};
|
|
5132
|
-
const handleSafe = (msg) => {
|
|
5133
|
-
const resolved = resolveEvent(msg);
|
|
5134
|
-
if (resolved === null) return void 0;
|
|
5135
|
-
if (isPromiseLike2(resolved)) return resolved;
|
|
5136
|
-
const { handler, data } = resolved;
|
|
5137
|
-
const ctx = new RpcContext([msg]);
|
|
5138
|
-
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5139
|
-
const startedAt = performance.now();
|
|
5140
|
-
let pending;
|
|
5529
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5530
|
+
return { handler, data };
|
|
5531
|
+
} catch (err) {
|
|
5532
|
+
logger5.error(`Unexpected error in ${kind} event router`, err);
|
|
5141
5533
|
try {
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5534
|
+
msg.term("Unexpected router error");
|
|
5535
|
+
} catch (termErr) {
|
|
5536
|
+
logger5.error(`Failed to terminate message ${subject}:`, termErr);
|
|
5537
|
+
}
|
|
5538
|
+
return null;
|
|
5539
|
+
}
|
|
5540
|
+
};
|
|
5541
|
+
};
|
|
5542
|
+
var createWorkqueuePipeline = (rctx) => {
|
|
5543
|
+
const { kind, spanKind, logger: logger5, eventBus, otel, serviceName, serverEndpoint } = rctx;
|
|
5544
|
+
const { ackExtensionInterval } = rctx;
|
|
5545
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
5546
|
+
const reportHandlerCompleted = createHandlerReporter(rctx);
|
|
5547
|
+
const resolveEvent = createEventResolver(rctx);
|
|
5548
|
+
const { settleSuccess, settleFailure } = createSettlement(logger5, rctx.capture);
|
|
5549
|
+
return (msg) => {
|
|
5550
|
+
const resolved = resolveEvent(msg);
|
|
5551
|
+
if (resolved === null) return void 0;
|
|
5552
|
+
if (isPromiseLike2(resolved)) return resolved;
|
|
5553
|
+
const { handler, data } = resolved;
|
|
5554
|
+
const ctx = new RpcContext([msg]);
|
|
5555
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5556
|
+
const startedAt = performance.now();
|
|
5557
|
+
let pending;
|
|
5558
|
+
try {
|
|
5559
|
+
pending = withConsumeSpan(
|
|
5560
|
+
{
|
|
5561
|
+
subject: msg.subject,
|
|
5562
|
+
msg,
|
|
5563
|
+
info: msg.info,
|
|
5564
|
+
kind: spanKind,
|
|
5565
|
+
payloadBytes: msg.data.length,
|
|
5566
|
+
handlerMetadata: { pattern: msg.subject },
|
|
5567
|
+
serviceName,
|
|
5568
|
+
endpoint: serverEndpoint
|
|
5569
|
+
},
|
|
5570
|
+
otel,
|
|
5571
|
+
() => unwrapResult(handler(data, ctx))
|
|
5572
|
+
);
|
|
5573
|
+
} catch (err) {
|
|
5574
|
+
eventBus.emit(
|
|
5575
|
+
"error" /* Error */,
|
|
5576
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
5577
|
+
`${kind}-handler:${msg.subject}`
|
|
5578
|
+
);
|
|
5579
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
5580
|
+
return settleFailure(msg, data, err).finally(() => {
|
|
5581
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5582
|
+
});
|
|
5583
|
+
}
|
|
5584
|
+
if (!isPromiseLike2(pending)) {
|
|
5585
|
+
const settled = settleSuccess(msg, ctx, data);
|
|
5586
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5587
|
+
if (settled === void 0) {
|
|
5588
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5589
|
+
return void 0;
|
|
5590
|
+
}
|
|
5591
|
+
return settled.finally(() => {
|
|
5592
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
return pending.then(
|
|
5596
|
+
async () => {
|
|
5597
|
+
try {
|
|
5598
|
+
await settleSuccess(msg, ctx, data);
|
|
5599
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5600
|
+
} finally {
|
|
5601
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5602
|
+
}
|
|
5603
|
+
},
|
|
5604
|
+
async (err) => {
|
|
5157
5605
|
eventBus.emit(
|
|
5158
5606
|
"error" /* Error */,
|
|
5159
5607
|
err instanceof Error ? err : new Error(String(err)),
|
|
5160
5608
|
`${kind}-handler:${msg.subject}`
|
|
5161
5609
|
);
|
|
5162
5610
|
reportHandlerCompleted(msg, startedAt, "error");
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
if (!isPromiseLike2(pending)) {
|
|
5168
|
-
const settled = settleSuccess(msg, ctx, data);
|
|
5169
|
-
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5170
|
-
if (settled === void 0) {
|
|
5611
|
+
try {
|
|
5612
|
+
await settleFailure(msg, data, err);
|
|
5613
|
+
} finally {
|
|
5171
5614
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5172
|
-
return void 0;
|
|
5173
5615
|
}
|
|
5174
|
-
return settled.finally(() => {
|
|
5175
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5176
|
-
});
|
|
5177
5616
|
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5194
|
-
try {
|
|
5195
|
-
await settleFailure(msg, data, err);
|
|
5196
|
-
} finally {
|
|
5197
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5198
|
-
}
|
|
5199
|
-
}
|
|
5200
|
-
);
|
|
5201
|
-
};
|
|
5202
|
-
const handleOrderedSafe = (msg) => {
|
|
5203
|
-
const subject = msg.subject;
|
|
5204
|
-
let handler;
|
|
5205
|
-
let data;
|
|
5206
|
-
try {
|
|
5207
|
-
handler = patternRegistry.getHandler(subject);
|
|
5208
|
-
if (!handler) {
|
|
5209
|
-
logger5.error(`No handler for subject: ${subject}`);
|
|
5210
|
-
return void 0;
|
|
5211
|
-
}
|
|
5212
|
-
try {
|
|
5213
|
-
data = codec.decode(msg.data);
|
|
5214
|
-
} catch (err) {
|
|
5215
|
-
logger5.error(`Decode error for ${subject}:`, err);
|
|
5216
|
-
return void 0;
|
|
5217
|
-
}
|
|
5218
|
-
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5219
|
-
} catch (err) {
|
|
5220
|
-
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5617
|
+
);
|
|
5618
|
+
};
|
|
5619
|
+
};
|
|
5620
|
+
var createOrderedPipeline = (rctx) => {
|
|
5621
|
+
const { spanKind, codec, logger: logger5, eventBus, patternRegistry, otel } = rctx;
|
|
5622
|
+
const { serviceName, serverEndpoint } = rctx;
|
|
5623
|
+
const reportHandlerCompleted = createHandlerReporter(rctx);
|
|
5624
|
+
return (msg) => {
|
|
5625
|
+
const subject = msg.subject;
|
|
5626
|
+
let handler;
|
|
5627
|
+
let data;
|
|
5628
|
+
try {
|
|
5629
|
+
handler = patternRegistry.getHandler(subject);
|
|
5630
|
+
if (!handler) {
|
|
5631
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
5221
5632
|
return void 0;
|
|
5222
5633
|
}
|
|
5223
|
-
const ctx = new RpcContext([msg]);
|
|
5224
|
-
const warnIfSettlementAttempted = () => {
|
|
5225
|
-
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
5226
|
-
logger5.warn(
|
|
5227
|
-
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
5228
|
-
);
|
|
5229
|
-
}
|
|
5230
|
-
};
|
|
5231
|
-
const startedAt = performance.now();
|
|
5232
|
-
let pending;
|
|
5233
5634
|
try {
|
|
5234
|
-
|
|
5235
|
-
{
|
|
5236
|
-
subject: msg.subject,
|
|
5237
|
-
msg,
|
|
5238
|
-
info: msg.info,
|
|
5239
|
-
kind: spanKind,
|
|
5240
|
-
payloadBytes: msg.data.length,
|
|
5241
|
-
handlerMetadata: { pattern: msg.subject },
|
|
5242
|
-
serviceName,
|
|
5243
|
-
endpoint: serverEndpoint
|
|
5244
|
-
},
|
|
5245
|
-
otel,
|
|
5246
|
-
() => unwrapResult(handler(data, ctx))
|
|
5247
|
-
);
|
|
5635
|
+
data = codec.decode(msg.data);
|
|
5248
5636
|
} catch (err) {
|
|
5249
|
-
logger5.error(`
|
|
5250
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5637
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
5251
5638
|
return void 0;
|
|
5252
5639
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5640
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5641
|
+
} catch (err) {
|
|
5642
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5643
|
+
return void 0;
|
|
5644
|
+
}
|
|
5645
|
+
const ctx = new RpcContext([msg]);
|
|
5646
|
+
const warnIfSettlementAttempted = () => {
|
|
5647
|
+
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
5648
|
+
logger5.warn(
|
|
5649
|
+
`retry()/terminate() ignored for ordered message ${subject}; ordered consumers auto-acknowledge`
|
|
5650
|
+
);
|
|
5257
5651
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5652
|
+
};
|
|
5653
|
+
const startedAt = performance.now();
|
|
5654
|
+
let pending;
|
|
5655
|
+
try {
|
|
5656
|
+
pending = withConsumeSpan(
|
|
5657
|
+
{
|
|
5658
|
+
subject: msg.subject,
|
|
5659
|
+
msg,
|
|
5660
|
+
info: msg.info,
|
|
5661
|
+
kind: spanKind,
|
|
5662
|
+
payloadBytes: msg.data.length,
|
|
5663
|
+
handlerMetadata: { pattern: msg.subject },
|
|
5664
|
+
serviceName,
|
|
5665
|
+
endpoint: serverEndpoint
|
|
5262
5666
|
},
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5266
|
-
}
|
|
5667
|
+
otel,
|
|
5668
|
+
() => unwrapResult(handler(data, ctx))
|
|
5267
5669
|
);
|
|
5268
|
-
}
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
}
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
};
|
|
5287
|
-
const trackAsync = (result, msg) => {
|
|
5288
|
-
void result.catch((err) => {
|
|
5289
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5290
|
-
}).finally(onAsyncDone);
|
|
5291
|
-
};
|
|
5292
|
-
const drainBacklog = () => {
|
|
5293
|
-
while (active < maxActive) {
|
|
5294
|
-
const next = backlog.shift();
|
|
5295
|
-
if (next === void 0) return;
|
|
5296
|
-
next.stopAckExtension?.();
|
|
5297
|
-
active++;
|
|
5298
|
-
const result = routeSafely(next.msg);
|
|
5299
|
-
if (result !== void 0) {
|
|
5300
|
-
trackAsync(result, next.msg);
|
|
5301
|
-
} else {
|
|
5302
|
-
active--;
|
|
5303
|
-
}
|
|
5670
|
+
} catch (err) {
|
|
5671
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5672
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
5673
|
+
return void 0;
|
|
5674
|
+
}
|
|
5675
|
+
if (!isPromiseLike2(pending)) {
|
|
5676
|
+
warnIfSettlementAttempted();
|
|
5677
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
5678
|
+
return void 0;
|
|
5679
|
+
}
|
|
5680
|
+
return pending.then(
|
|
5681
|
+
() => {
|
|
5682
|
+
warnIfSettlementAttempted();
|
|
5683
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
5684
|
+
},
|
|
5685
|
+
(err) => {
|
|
5686
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5687
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
5304
5688
|
}
|
|
5305
|
-
|
|
5689
|
+
);
|
|
5690
|
+
};
|
|
5691
|
+
};
|
|
5692
|
+
|
|
5693
|
+
// src/server/routing/event.router.ts
|
|
5694
|
+
var eventConsumeKindFor = (kind) => {
|
|
5695
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
5696
|
+
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
5697
|
+
return "event" /* Event */;
|
|
5698
|
+
};
|
|
5699
|
+
var EventRouter = class {
|
|
5700
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options, names) {
|
|
5701
|
+
this.messageProvider = messageProvider;
|
|
5702
|
+
this.patternRegistry = patternRegistry;
|
|
5703
|
+
this.codec = codec;
|
|
5704
|
+
this.eventBus = eventBus;
|
|
5705
|
+
this.deadLetterConfig = deadLetterConfig;
|
|
5706
|
+
this.processingConfig = processingConfig;
|
|
5707
|
+
this.ackWaitMap = ackWaitMap;
|
|
5708
|
+
if (options) {
|
|
5709
|
+
const derived = deriveOtelAttrs(options);
|
|
5710
|
+
this.otel = derived.otel;
|
|
5711
|
+
this.serviceName = derived.serviceName;
|
|
5712
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
5713
|
+
} else {
|
|
5714
|
+
this.otel = resolveOtelOptions({ enabled: false });
|
|
5715
|
+
this.serviceName = "";
|
|
5716
|
+
this.serverEndpoint = null;
|
|
5717
|
+
}
|
|
5718
|
+
this.capture = deadLetterConfig ? new DeadLetterCapture(
|
|
5719
|
+
patternRegistry,
|
|
5720
|
+
eventBus,
|
|
5721
|
+
deadLetterConfig,
|
|
5722
|
+
this.otel,
|
|
5723
|
+
this.serviceName,
|
|
5724
|
+
this.serverEndpoint,
|
|
5725
|
+
connection,
|
|
5726
|
+
options,
|
|
5727
|
+
names
|
|
5728
|
+
) : null;
|
|
5729
|
+
}
|
|
5730
|
+
logger = new import_common20.Logger("Jetstream:EventRouter");
|
|
5731
|
+
subscriptions = [];
|
|
5732
|
+
otel;
|
|
5733
|
+
serviceName;
|
|
5734
|
+
serverEndpoint;
|
|
5735
|
+
capture;
|
|
5736
|
+
/**
|
|
5737
|
+
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
5738
|
+
* Called after consumers are ensured so the DLQ map reflects reality.
|
|
5739
|
+
*/
|
|
5740
|
+
updateMaxDeliverMap(consumerMaxDelivers) {
|
|
5741
|
+
if (!this.deadLetterConfig) return;
|
|
5742
|
+
this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
|
|
5743
|
+
}
|
|
5744
|
+
/** Start routing event, broadcast, and ordered messages to handlers. */
|
|
5745
|
+
start() {
|
|
5746
|
+
this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
|
|
5747
|
+
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
|
|
5748
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
5749
|
+
this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
/** Stop routing and unsubscribe from all streams. */
|
|
5753
|
+
destroy() {
|
|
5754
|
+
for (const sub of this.subscriptions) {
|
|
5755
|
+
sub.unsubscribe();
|
|
5756
|
+
}
|
|
5757
|
+
this.subscriptions.length = 0;
|
|
5758
|
+
}
|
|
5759
|
+
/** Assemble the pipeline and concurrency gate for one stream and subscribe. */
|
|
5760
|
+
subscribeToStream(stream$, kind) {
|
|
5761
|
+
const isOrdered = kind === "ordered" /* Ordered */;
|
|
5762
|
+
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
5763
|
+
const rctx = {
|
|
5764
|
+
kind,
|
|
5765
|
+
spanKind: eventConsumeKindFor(kind),
|
|
5766
|
+
codec: this.codec,
|
|
5767
|
+
logger: this.logger,
|
|
5768
|
+
eventBus: this.eventBus,
|
|
5769
|
+
patternRegistry: this.patternRegistry,
|
|
5770
|
+
otel: this.otel,
|
|
5771
|
+
serviceName: this.serviceName,
|
|
5772
|
+
serverEndpoint: this.serverEndpoint,
|
|
5773
|
+
ackExtensionInterval,
|
|
5774
|
+
capture: this.capture
|
|
5306
5775
|
};
|
|
5776
|
+
const route = isOrdered ? createOrderedPipeline(rctx) : createWorkqueuePipeline(rctx);
|
|
5777
|
+
const maxActive = isOrdered ? 1 : this.getConcurrency(kind) ?? Number.POSITIVE_INFINITY;
|
|
5778
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
5779
|
+
const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5780
|
+
const gate = new ConcurrencyGate(maxActive, route, parkTimer, this.logger, kind);
|
|
5307
5781
|
const subscription = stream$.subscribe({
|
|
5308
5782
|
next: (msg) => {
|
|
5309
|
-
|
|
5310
|
-
backlog.push({
|
|
5311
|
-
msg,
|
|
5312
|
-
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5313
|
-
});
|
|
5314
|
-
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5315
|
-
backlogWarned = true;
|
|
5316
|
-
logger5.warn(
|
|
5317
|
-
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5318
|
-
);
|
|
5319
|
-
}
|
|
5320
|
-
return;
|
|
5321
|
-
}
|
|
5322
|
-
active++;
|
|
5323
|
-
const result = routeSafely(msg);
|
|
5324
|
-
if (result !== void 0) {
|
|
5325
|
-
trackAsync(result, msg);
|
|
5326
|
-
} else {
|
|
5327
|
-
active--;
|
|
5328
|
-
if (backlog.length > 0) drainBacklog();
|
|
5329
|
-
}
|
|
5783
|
+
gate.push(msg);
|
|
5330
5784
|
},
|
|
5331
5785
|
error: (err) => {
|
|
5332
|
-
|
|
5786
|
+
this.logger.error(`Stream error in ${kind} router`, err);
|
|
5333
5787
|
}
|
|
5334
5788
|
});
|
|
5335
5789
|
subscription.add(() => {
|
|
5336
|
-
|
|
5337
|
-
queued.stopAckExtension?.();
|
|
5338
|
-
}
|
|
5339
|
-
backlog.length = 0;
|
|
5790
|
+
gate.dispose();
|
|
5340
5791
|
});
|
|
5341
5792
|
this.subscriptions.push(subscription);
|
|
5342
5793
|
}
|
|
@@ -5350,170 +5801,11 @@ var EventRouter = class {
|
|
|
5350
5801
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
5351
5802
|
return void 0;
|
|
5352
5803
|
}
|
|
5353
|
-
/**
|
|
5354
|
-
* Last resort: invoke onDeadLetter, then term on success. On failure the
|
|
5355
|
-
* message is nak'd — never redelivered past max_deliver, but preserved.
|
|
5356
|
-
*/
|
|
5357
|
-
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
5358
|
-
const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
|
|
5359
|
-
if (!onDeadLetter) {
|
|
5360
|
-
this.logger.error(
|
|
5361
|
-
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
|
|
5362
|
-
);
|
|
5363
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5364
|
-
msg.nak();
|
|
5365
|
-
});
|
|
5366
|
-
return;
|
|
5367
|
-
}
|
|
5368
|
-
try {
|
|
5369
|
-
await onDeadLetter(info);
|
|
5370
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5371
|
-
msg.term("Dead letter processed via fallback callback");
|
|
5372
|
-
});
|
|
5373
|
-
} catch (hookErr) {
|
|
5374
|
-
this.logger.error(
|
|
5375
|
-
`Fallback onDeadLetter callback failed for ${msg.subject} \u2014 the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
|
|
5376
|
-
hookErr
|
|
5377
|
-
);
|
|
5378
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5379
|
-
msg.nak();
|
|
5380
|
-
});
|
|
5381
|
-
}
|
|
5382
|
-
}
|
|
5383
|
-
/**
|
|
5384
|
-
* Copy headers for the DLQ republish, dropping NATS control headers — a
|
|
5385
|
-
* copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
|
|
5386
|
-
*/
|
|
5387
|
-
buildDlqHeaders(msg) {
|
|
5388
|
-
const hdrs = (0, import_transport_node4.headers)();
|
|
5389
|
-
if (!msg.headers) return hdrs;
|
|
5390
|
-
for (const [k, v] of msg.headers) {
|
|
5391
|
-
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5392
|
-
for (const val of v) {
|
|
5393
|
-
hdrs.append(k, val);
|
|
5394
|
-
}
|
|
5395
|
-
}
|
|
5396
|
-
return hdrs;
|
|
5397
|
-
}
|
|
5398
|
-
/**
|
|
5399
|
-
* Past max_deliver the server never redelivers, so these in-process attempts
|
|
5400
|
-
* are the only second chance a dead letter gets. No artificial delay — an
|
|
5401
|
-
* unreachable broker already spaces attempts via its own request timeout.
|
|
5402
|
-
*/
|
|
5403
|
-
async publishToDlqWithRetry(connection, subject, data, headers2) {
|
|
5404
|
-
let lastErr;
|
|
5405
|
-
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5406
|
-
try {
|
|
5407
|
-
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5408
|
-
return;
|
|
5409
|
-
} catch (err) {
|
|
5410
|
-
lastErr = err;
|
|
5411
|
-
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5412
|
-
this.logger.warn(
|
|
5413
|
-
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5414
|
-
);
|
|
5415
|
-
}
|
|
5416
|
-
}
|
|
5417
|
-
}
|
|
5418
|
-
throw lastErr;
|
|
5419
|
-
}
|
|
5420
|
-
/**
|
|
5421
|
-
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
5422
|
-
*
|
|
5423
|
-
* Appends diagnostic metadata headers to the original message and preserves
|
|
5424
|
-
* the primary payload. If publishing succeeds, it notifies the standard
|
|
5425
|
-
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
5426
|
-
* back to the callback entirely to prevent silent data loss.
|
|
5427
|
-
*/
|
|
5428
|
-
async publishToDlq(msg, info, error) {
|
|
5429
|
-
const serviceName = this.options?.name;
|
|
5430
|
-
if (!this.connection || !serviceName) {
|
|
5431
|
-
this.logger.error(
|
|
5432
|
-
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
5433
|
-
);
|
|
5434
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5435
|
-
return;
|
|
5436
|
-
}
|
|
5437
|
-
const destinationSubject = dlqStreamName(serviceName);
|
|
5438
|
-
const hdrs = this.buildDlqHeaders(msg);
|
|
5439
|
-
let reason = String(error);
|
|
5440
|
-
if (error instanceof Error) {
|
|
5441
|
-
reason = error.message;
|
|
5442
|
-
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
5443
|
-
reason = String(error.message);
|
|
5444
|
-
}
|
|
5445
|
-
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
5446
|
-
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
5447
|
-
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
5448
|
-
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
5449
|
-
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
5450
|
-
try {
|
|
5451
|
-
await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
5452
|
-
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
5453
|
-
if (this.deadLetterConfig?.onDeadLetter) {
|
|
5454
|
-
try {
|
|
5455
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
5456
|
-
} catch (hookErr) {
|
|
5457
|
-
this.logger.warn(
|
|
5458
|
-
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
5459
|
-
hookErr
|
|
5460
|
-
);
|
|
5461
|
-
}
|
|
5462
|
-
}
|
|
5463
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5464
|
-
msg.term("Moved to DLQ stream");
|
|
5465
|
-
});
|
|
5466
|
-
} catch (publishErr) {
|
|
5467
|
-
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
5468
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5469
|
-
}
|
|
5470
|
-
}
|
|
5471
|
-
/**
|
|
5472
|
-
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
5473
|
-
*
|
|
5474
|
-
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
5475
|
-
* or directly to the fallback callback based on the active module configuration.
|
|
5476
|
-
*/
|
|
5477
|
-
async handleDeadLetter(msg, data, error) {
|
|
5478
|
-
const info = {
|
|
5479
|
-
subject: msg.subject,
|
|
5480
|
-
data,
|
|
5481
|
-
headers: msg.headers,
|
|
5482
|
-
error,
|
|
5483
|
-
deliveryCount: msg.info.deliveryCount,
|
|
5484
|
-
stream: msg.info.stream,
|
|
5485
|
-
streamSequence: msg.info.streamSequence,
|
|
5486
|
-
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
5487
|
-
};
|
|
5488
|
-
await withDeadLetterSpan(
|
|
5489
|
-
{
|
|
5490
|
-
msg,
|
|
5491
|
-
// Pattern resolution mirrors event-routing: when a registered
|
|
5492
|
-
// pattern matches, surface it on the DLQ span so APM can filter
|
|
5493
|
-
// dead letters by handler without parsing the subject. Falls back
|
|
5494
|
-
// to the subject itself when no glob handler is in play.
|
|
5495
|
-
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
5496
|
-
finalDeliveryCount: msg.info.deliveryCount,
|
|
5497
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
5498
|
-
serviceName: this.serviceName,
|
|
5499
|
-
endpoint: this.serverEndpoint
|
|
5500
|
-
},
|
|
5501
|
-
this.otel,
|
|
5502
|
-
async () => {
|
|
5503
|
-
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
5504
|
-
if (!this.options?.dlq) {
|
|
5505
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5506
|
-
} else {
|
|
5507
|
-
await this.publishToDlq(msg, info, error);
|
|
5508
|
-
}
|
|
5509
|
-
}
|
|
5510
|
-
);
|
|
5511
|
-
}
|
|
5512
5804
|
};
|
|
5513
5805
|
|
|
5514
5806
|
// src/server/routing/rpc.router.ts
|
|
5515
|
-
var
|
|
5516
|
-
var
|
|
5807
|
+
var import_common21 = require("@nestjs/common");
|
|
5808
|
+
var import_transport_node6 = require("@nats-io/transport-node");
|
|
5517
5809
|
var RpcRouter = class {
|
|
5518
5810
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
|
|
5519
5811
|
this.messageProvider = messageProvider;
|
|
@@ -5536,7 +5828,7 @@ var RpcRouter = class {
|
|
|
5536
5828
|
this.serverEndpoint = null;
|
|
5537
5829
|
}
|
|
5538
5830
|
}
|
|
5539
|
-
logger = new
|
|
5831
|
+
logger = new import_common21.Logger("Jetstream:RpcRouter");
|
|
5540
5832
|
timeout;
|
|
5541
5833
|
concurrency;
|
|
5542
5834
|
resolvedAckExtensionInterval;
|
|
@@ -5587,7 +5879,7 @@ var RpcRouter = class {
|
|
|
5587
5879
|
};
|
|
5588
5880
|
const publishReply = (replyTo, correlationId, payload) => {
|
|
5589
5881
|
try {
|
|
5590
|
-
const hdrs = (0,
|
|
5882
|
+
const hdrs = (0, import_transport_node6.headers)();
|
|
5591
5883
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
5592
5884
|
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
5593
5885
|
} catch (publishErr) {
|
|
@@ -5596,7 +5888,7 @@ var RpcRouter = class {
|
|
|
5596
5888
|
};
|
|
5597
5889
|
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
5598
5890
|
try {
|
|
5599
|
-
const hdrs = (0,
|
|
5891
|
+
const hdrs = (0, import_transport_node6.headers)();
|
|
5600
5892
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
5601
5893
|
hdrs.set("x-error" /* Error */, "true");
|
|
5602
5894
|
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
@@ -5727,75 +6019,18 @@ var RpcRouter = class {
|
|
|
5727
6019
|
}
|
|
5728
6020
|
);
|
|
5729
6021
|
};
|
|
5730
|
-
const
|
|
5731
|
-
|
|
5732
|
-
let backlogWarned = false;
|
|
5733
|
-
const backlog = [];
|
|
5734
|
-
const onAsyncDone = () => {
|
|
5735
|
-
active--;
|
|
5736
|
-
drainBacklog();
|
|
5737
|
-
};
|
|
5738
|
-
const routeSafely = (msg) => {
|
|
5739
|
-
try {
|
|
5740
|
-
return handleSafe(msg);
|
|
5741
|
-
} catch (err) {
|
|
5742
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5743
|
-
return void 0;
|
|
5744
|
-
}
|
|
5745
|
-
};
|
|
5746
|
-
const trackAsync = (result, msg) => {
|
|
5747
|
-
void result.catch((err) => {
|
|
5748
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5749
|
-
}).finally(onAsyncDone);
|
|
5750
|
-
};
|
|
5751
|
-
const drainBacklog = () => {
|
|
5752
|
-
while (active < maxActive) {
|
|
5753
|
-
const next = backlog.shift();
|
|
5754
|
-
if (next === void 0) return;
|
|
5755
|
-
next.stopAckExtension?.();
|
|
5756
|
-
active++;
|
|
5757
|
-
const result = routeSafely(next.msg);
|
|
5758
|
-
if (result !== void 0) {
|
|
5759
|
-
trackAsync(result, next.msg);
|
|
5760
|
-
} else {
|
|
5761
|
-
active--;
|
|
5762
|
-
}
|
|
5763
|
-
}
|
|
5764
|
-
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
5765
|
-
};
|
|
6022
|
+
const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
6023
|
+
const gate = new ConcurrencyGate(maxActive, handleSafe, parkTimer, logger5, "RPC");
|
|
5766
6024
|
this.subscription = this.messageProvider.commands$.subscribe({
|
|
5767
6025
|
next: (msg) => {
|
|
5768
|
-
|
|
5769
|
-
backlog.push({
|
|
5770
|
-
msg,
|
|
5771
|
-
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5772
|
-
});
|
|
5773
|
-
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5774
|
-
backlogWarned = true;
|
|
5775
|
-
logger5.warn(
|
|
5776
|
-
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5777
|
-
);
|
|
5778
|
-
}
|
|
5779
|
-
return;
|
|
5780
|
-
}
|
|
5781
|
-
active++;
|
|
5782
|
-
const result = routeSafely(msg);
|
|
5783
|
-
if (result !== void 0) {
|
|
5784
|
-
trackAsync(result, msg);
|
|
5785
|
-
} else {
|
|
5786
|
-
active--;
|
|
5787
|
-
if (backlog.length > 0) drainBacklog();
|
|
5788
|
-
}
|
|
6026
|
+
gate.push(msg);
|
|
5789
6027
|
},
|
|
5790
6028
|
error: (err) => {
|
|
5791
6029
|
logger5.error("Stream error in RPC router", err);
|
|
5792
6030
|
}
|
|
5793
6031
|
});
|
|
5794
6032
|
this.subscription.add(() => {
|
|
5795
|
-
|
|
5796
|
-
queued.stopAckExtension?.();
|
|
5797
|
-
}
|
|
5798
|
-
backlog.length = 0;
|
|
6033
|
+
gate.dispose();
|
|
5799
6034
|
});
|
|
5800
6035
|
}
|
|
5801
6036
|
/** Stop routing and unsubscribe. */
|
|
@@ -5805,20 +6040,220 @@ var RpcRouter = class {
|
|
|
5805
6040
|
}
|
|
5806
6041
|
};
|
|
5807
6042
|
|
|
6043
|
+
// src/server/infrastructure/infrastructure-binder.ts
|
|
6044
|
+
var import_common22 = require("@nestjs/common");
|
|
6045
|
+
var import_jetstream30 = require("@nats-io/jetstream");
|
|
6046
|
+
var WORKQUEUE_KINDS = /* @__PURE__ */ new Set(["ev" /* Event */, "cmd" /* Command */]);
|
|
6047
|
+
var manualRemediation = (entity) => `Management mode is Manual; the ${entity} must be provisioned externally before boot.`;
|
|
6048
|
+
var isSchedulingEnabled = (options, kind) => kindOptionsBlock(options, kind)?.stream?.allow_msg_schedules === true;
|
|
6049
|
+
var resolveAckExtension = (options, kind) => kindOptionsBlock(options, kind)?.ackExtension;
|
|
6050
|
+
var filterCoversSubject = (filter_subject, filter_subjects, subject) => {
|
|
6051
|
+
if (filter_subject !== void 0) {
|
|
6052
|
+
return coversOrEquals(filter_subject, subject);
|
|
6053
|
+
}
|
|
6054
|
+
if (filter_subjects !== void 0) {
|
|
6055
|
+
return filter_subjects.some((f) => coversOrEquals(f, subject));
|
|
6056
|
+
}
|
|
6057
|
+
return true;
|
|
6058
|
+
};
|
|
6059
|
+
var InfrastructureBinder = class {
|
|
6060
|
+
constructor(options, names, registry) {
|
|
6061
|
+
this.options = options;
|
|
6062
|
+
this.names = names;
|
|
6063
|
+
this.registry = registry;
|
|
6064
|
+
}
|
|
6065
|
+
logger = new import_common22.Logger("Jetstream:Binder");
|
|
6066
|
+
async bindStream(jsm, kind) {
|
|
6067
|
+
const name = this.names.streamName(kind);
|
|
6068
|
+
const info = await this.fetchStream(jsm, name, kind);
|
|
6069
|
+
await this.warnOnOrphanedMigrationBackup(jsm, name);
|
|
6070
|
+
if (isSchedulingEnabled(this.options, kind)) {
|
|
6071
|
+
this.assertScheduleCoverage(info, kind);
|
|
6072
|
+
this.warnOnSchedulesDisabled(info, kind);
|
|
6073
|
+
}
|
|
6074
|
+
if (WORKQUEUE_KINDS.has(kind)) {
|
|
6075
|
+
this.warnOnRetention(info, kind);
|
|
6076
|
+
}
|
|
6077
|
+
return info;
|
|
6078
|
+
}
|
|
6079
|
+
async bindDlqStream(jsm) {
|
|
6080
|
+
const dlqName = this.names.dlqStreamName();
|
|
6081
|
+
const info = await this.fetchStream(jsm, dlqName, "dlq");
|
|
6082
|
+
await this.warnOnOrphanedMigrationBackup(jsm, dlqName);
|
|
6083
|
+
this.assertDlqSubjectCoverage(info);
|
|
6084
|
+
return info;
|
|
6085
|
+
}
|
|
6086
|
+
async bindConsumer(jsm, kind) {
|
|
6087
|
+
const info = await this.fetchConsumer(jsm, kind);
|
|
6088
|
+
this.assertHandlersCovered(info, kind);
|
|
6089
|
+
this.assertScheduleHoldersNotConsumed(info, kind);
|
|
6090
|
+
this.warnOnUnlimitedDelivery(info, kind);
|
|
6091
|
+
this.warnOnShortAckWait(info, kind);
|
|
6092
|
+
return info;
|
|
6093
|
+
}
|
|
6094
|
+
async fetchStream(jsm, name, kind) {
|
|
6095
|
+
try {
|
|
6096
|
+
return await jsm.streams.info(name);
|
|
6097
|
+
} catch (err) {
|
|
6098
|
+
if (err instanceof import_jetstream30.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
6099
|
+
const api = err.apiError();
|
|
6100
|
+
throw new JetstreamProvisioningError({
|
|
6101
|
+
entity: "stream",
|
|
6102
|
+
target: name,
|
|
6103
|
+
kind: String(kind),
|
|
6104
|
+
errCode: api.err_code,
|
|
6105
|
+
errDescription: api.description,
|
|
6106
|
+
remediation: manualRemediation("stream"),
|
|
6107
|
+
cause: err
|
|
6108
|
+
});
|
|
6109
|
+
}
|
|
6110
|
+
throw err;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
async fetchConsumer(jsm, kind) {
|
|
6114
|
+
const stream = this.names.streamName(kind);
|
|
6115
|
+
const consumer = this.names.consumerName(kind);
|
|
6116
|
+
try {
|
|
6117
|
+
return await jsm.consumers.info(stream, consumer);
|
|
6118
|
+
} catch (err) {
|
|
6119
|
+
if (err instanceof import_jetstream30.JetStreamApiError && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
|
|
6120
|
+
const api = err.apiError();
|
|
6121
|
+
throw new JetstreamProvisioningError({
|
|
6122
|
+
entity: "consumer",
|
|
6123
|
+
target: `${consumer} on stream "${stream}"`,
|
|
6124
|
+
kind: String(kind),
|
|
6125
|
+
errCode: api.err_code,
|
|
6126
|
+
errDescription: api.description,
|
|
6127
|
+
remediation: manualRemediation("consumer"),
|
|
6128
|
+
cause: err
|
|
6129
|
+
});
|
|
6130
|
+
}
|
|
6131
|
+
throw err;
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
assertHandlersCovered(info, kind) {
|
|
6135
|
+
const subjects = this.resolveHandlerSubjects(kind);
|
|
6136
|
+
if (subjects.length === 0) return;
|
|
6137
|
+
const { filter_subject, filter_subjects } = info.config;
|
|
6138
|
+
const uncovered = subjects.filter(
|
|
6139
|
+
(s) => !filterCoversSubject(filter_subject, filter_subjects, s)
|
|
6140
|
+
);
|
|
6141
|
+
if (uncovered.length > 0) {
|
|
6142
|
+
throw new Error(
|
|
6143
|
+
`Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) does not cover the following registered handler subjects: ${uncovered.join(", ")}. Update the consumer's filter_subject / filter_subjects to include them.`
|
|
6144
|
+
);
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
assertDlqSubjectCoverage(info) {
|
|
6148
|
+
const dlqSubject = this.names.dlqStreamName();
|
|
6149
|
+
const covered = info.config.subjects.some((s) => coversOrEquals(s, dlqSubject));
|
|
6150
|
+
if (!covered) {
|
|
6151
|
+
throw new Error(
|
|
6152
|
+
`DLQ stream "${dlqSubject}" subjects do not cover "${dlqSubject}" (dead letters publish to a subject equal to the stream name). Add it to the stream's subjects list.`
|
|
6153
|
+
);
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
assertScheduleCoverage(info, kind) {
|
|
6157
|
+
const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
|
|
6158
|
+
const covered = info.config.subjects.some((s) => coversOrEquals(s, scheduleWildcard));
|
|
6159
|
+
if (!covered) {
|
|
6160
|
+
throw new Error(
|
|
6161
|
+
`Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) has scheduling enabled (allow_msg_schedules=true) but its subjects do not cover the schedule prefix "${this.names.schedulePrefix(kind)}". Add "${scheduleWildcard}" to the stream's subjects.`
|
|
6162
|
+
);
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
assertScheduleHoldersNotConsumed(info, kind) {
|
|
6166
|
+
if (!isSchedulingEnabled(this.options, kind)) return;
|
|
6167
|
+
const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
|
|
6168
|
+
const { filter_subject, filter_subjects } = info.config;
|
|
6169
|
+
const filters = filter_subjects ?? (filter_subject !== void 0 ? [filter_subject] : []);
|
|
6170
|
+
const swallowing = filters.length === 0 ? ["<no filter, consumes the whole stream>"] : filters.filter((f) => coversOrEquals(f, scheduleWildcard));
|
|
6171
|
+
if (swallowing.length > 0) {
|
|
6172
|
+
throw new Error(
|
|
6173
|
+
`Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) filter ${swallowing.join(", ")} also matches the schedule namespace "${this.names.schedulePrefix(kind)}". Consuming schedule holders removes pending schedules from the stream. Use exact filter_subjects for the registered handler subjects instead.`
|
|
6174
|
+
);
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
async warnOnOrphanedMigrationBackup(jsm, streamName2) {
|
|
6178
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
6179
|
+
try {
|
|
6180
|
+
await jsm.streams.info(backupName);
|
|
6181
|
+
} catch {
|
|
6182
|
+
return;
|
|
6183
|
+
}
|
|
6184
|
+
this.logger.warn(
|
|
6185
|
+
`Found migration backup "${backupName}" for the externally managed stream "${streamName2}". A previous Auto-managed migration was interrupted and undelivered messages may still reside in the backup. Recover them by sourcing the backup back, or re-enable Auto management for one boot to let the library finish the recovery.`
|
|
6186
|
+
);
|
|
6187
|
+
}
|
|
6188
|
+
warnOnSchedulesDisabled(info, kind) {
|
|
6189
|
+
if (info.config.allow_msg_schedules === true) return;
|
|
6190
|
+
this.logger.warn(
|
|
6191
|
+
`Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) does not report allow_msg_schedules=true, but scheduling is enabled in the application options. Scheduled publishes will be rejected by the server until the stream allows message schedules.`
|
|
6192
|
+
);
|
|
6193
|
+
}
|
|
6194
|
+
warnOnRetention(info, kind) {
|
|
6195
|
+
if (info.config.retention !== import_jetstream30.RetentionPolicy.Workqueue) {
|
|
6196
|
+
this.logger.warn(
|
|
6197
|
+
`Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) retention is "${String(info.config.retention)}"; expected "workqueue" for reliable at-least-once delivery.`
|
|
6198
|
+
);
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
warnOnUnlimitedDelivery(info, kind) {
|
|
6202
|
+
if (!this.options.dlq) return;
|
|
6203
|
+
const maxDeliver = info.config.max_deliver;
|
|
6204
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) {
|
|
6205
|
+
this.logger.warn(
|
|
6206
|
+
`Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) has unlimited max_deliver but options.dlq is enabled; messages will never be dead-lettered. Set max_deliver > 0 on the consumer.`
|
|
6207
|
+
);
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
warnOnShortAckWait(info, kind) {
|
|
6211
|
+
const ackExtConfig = resolveAckExtension(this.options, kind);
|
|
6212
|
+
if (ackExtConfig === void 0 || ackExtConfig === false) return;
|
|
6213
|
+
const ackWaitNanos = info.config.ack_wait;
|
|
6214
|
+
const intervalMs = resolveAckExtensionInterval(ackExtConfig, ackWaitNanos);
|
|
6215
|
+
if (intervalMs === null) return;
|
|
6216
|
+
const ackWaitMs = ackWaitNanos !== void 0 ? ackWaitNanos / 1e6 : void 0;
|
|
6217
|
+
if (ackWaitMs !== void 0 && ackWaitMs < intervalMs) {
|
|
6218
|
+
this.logger.warn(
|
|
6219
|
+
`Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) ack_wait (${ackWaitMs}ms) is shorter than the ackExtension interval (${intervalMs}ms). Messages may redeliver before the handler finishes. Increase ack_wait.`
|
|
6220
|
+
);
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
resolveHandlerSubjects(kind) {
|
|
6224
|
+
const patterns = this.registry.getPatternsByKind();
|
|
6225
|
+
switch (kind) {
|
|
6226
|
+
case "ev" /* Event */:
|
|
6227
|
+
return patterns.events.map((p) => this.names.subject("ev" /* Event */, p));
|
|
6228
|
+
case "cmd" /* Command */:
|
|
6229
|
+
return patterns.commands.map((p) => this.names.subject("cmd" /* Command */, p));
|
|
6230
|
+
case "broadcast" /* Broadcast */:
|
|
6231
|
+
return this.registry.getBroadcastPatterns();
|
|
6232
|
+
case "ordered" /* Ordered */:
|
|
6233
|
+
return this.registry.getOrderedSubjects();
|
|
6234
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
6235
|
+
default: {
|
|
6236
|
+
const _exhaustive = kind;
|
|
6237
|
+
throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
};
|
|
6242
|
+
|
|
5808
6243
|
// src/shutdown/shutdown.manager.ts
|
|
5809
|
-
var
|
|
6244
|
+
var import_common23 = require("@nestjs/common");
|
|
5810
6245
|
var ShutdownManager = class {
|
|
5811
6246
|
constructor(connection, eventBus, timeout) {
|
|
5812
6247
|
this.connection = connection;
|
|
5813
6248
|
this.eventBus = eventBus;
|
|
5814
6249
|
this.timeout = timeout;
|
|
5815
6250
|
}
|
|
5816
|
-
logger = new
|
|
6251
|
+
logger = new import_common23.Logger("Jetstream:Shutdown");
|
|
5817
6252
|
shutdownPromise;
|
|
5818
6253
|
/**
|
|
5819
6254
|
* Execute the full shutdown sequence.
|
|
5820
6255
|
*
|
|
5821
|
-
* Idempotent
|
|
6256
|
+
* Idempotent: concurrent or repeated calls return the same promise.
|
|
5822
6257
|
*
|
|
5823
6258
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
5824
6259
|
*/
|
|
@@ -5848,6 +6283,12 @@ var ShutdownManager = class {
|
|
|
5848
6283
|
|
|
5849
6284
|
// src/jetstream.module.ts
|
|
5850
6285
|
var JETSTREAM_ACK_WAIT_MAP = /* @__PURE__ */ Symbol("JETSTREAM_ACK_WAIT_MAP");
|
|
6286
|
+
var DESTRUCTIVE_MIGRATION_MANUAL_WARNING = "allowDestructiveMigration has no effect under provisioning.management: Manual; the library never migrates externally managed streams.";
|
|
6287
|
+
var warnIfManualWithDestructive = (options, logger5) => {
|
|
6288
|
+
if (options.allowDestructiveMigration && options.provisioning?.management === "manual" /* Manual */) {
|
|
6289
|
+
logger5.warn(DESTRUCTIVE_MIGRATION_MANUAL_WARNING);
|
|
6290
|
+
}
|
|
6291
|
+
};
|
|
5851
6292
|
var JetstreamModule = class {
|
|
5852
6293
|
constructor(shutdownManager, strategy) {
|
|
5853
6294
|
this.shutdownManager = shutdownManager;
|
|
@@ -5877,7 +6318,8 @@ var JetstreamModule = class {
|
|
|
5877
6318
|
PatternRegistry,
|
|
5878
6319
|
ShutdownManager,
|
|
5879
6320
|
JetstreamStrategy,
|
|
5880
|
-
JetstreamHealthIndicator
|
|
6321
|
+
JetstreamHealthIndicator,
|
|
6322
|
+
NameResolver
|
|
5881
6323
|
]
|
|
5882
6324
|
};
|
|
5883
6325
|
}
|
|
@@ -5906,7 +6348,8 @@ var JetstreamModule = class {
|
|
|
5906
6348
|
PatternRegistry,
|
|
5907
6349
|
ShutdownManager,
|
|
5908
6350
|
JetstreamStrategy,
|
|
5909
|
-
JetstreamHealthIndicator
|
|
6351
|
+
JetstreamHealthIndicator,
|
|
6352
|
+
NameResolver
|
|
5910
6353
|
]
|
|
5911
6354
|
};
|
|
5912
6355
|
}
|
|
@@ -5923,10 +6366,23 @@ var JetstreamModule = class {
|
|
|
5923
6366
|
const clientToken = getClientToken(options.name);
|
|
5924
6367
|
const clientProvider = {
|
|
5925
6368
|
provide: clientToken,
|
|
5926
|
-
inject: [
|
|
5927
|
-
|
|
6369
|
+
inject: [
|
|
6370
|
+
JETSTREAM_OPTIONS,
|
|
6371
|
+
JETSTREAM_CONNECTION,
|
|
6372
|
+
JETSTREAM_CODEC,
|
|
6373
|
+
JETSTREAM_EVENT_BUS,
|
|
6374
|
+
{ token: NameResolver, optional: true }
|
|
6375
|
+
],
|
|
6376
|
+
useFactory: (rootOptions, connection, rootCodec, eventBus, names) => {
|
|
5928
6377
|
const codec = options.codec ?? rootCodec;
|
|
5929
|
-
return new JetstreamClient(
|
|
6378
|
+
return new JetstreamClient(
|
|
6379
|
+
rootOptions,
|
|
6380
|
+
options.name,
|
|
6381
|
+
connection,
|
|
6382
|
+
codec,
|
|
6383
|
+
eventBus,
|
|
6384
|
+
names ?? void 0
|
|
6385
|
+
);
|
|
5930
6386
|
}
|
|
5931
6387
|
};
|
|
5932
6388
|
return {
|
|
@@ -5947,16 +6403,14 @@ var JetstreamModule = class {
|
|
|
5947
6403
|
/** Create providers that depend on JETSTREAM_OPTIONS (shared by sync and async). */
|
|
5948
6404
|
static createCoreDependentProviders() {
|
|
5949
6405
|
return [
|
|
5950
|
-
// EventBus — hook system with Logger fallback
|
|
5951
6406
|
{
|
|
5952
6407
|
provide: JETSTREAM_EVENT_BUS,
|
|
5953
6408
|
inject: [JETSTREAM_OPTIONS],
|
|
5954
6409
|
useFactory: (options) => {
|
|
5955
|
-
const logger5 = new
|
|
6410
|
+
const logger5 = new import_common24.Logger("Jetstream:Module");
|
|
5956
6411
|
return new EventBus(logger5, options.hooks);
|
|
5957
6412
|
}
|
|
5958
6413
|
},
|
|
5959
|
-
// Codec — global encode/decode
|
|
5960
6414
|
{
|
|
5961
6415
|
provide: JETSTREAM_CODEC,
|
|
5962
6416
|
inject: [JETSTREAM_OPTIONS],
|
|
@@ -5964,7 +6418,6 @@ var JetstreamModule = class {
|
|
|
5964
6418
|
return options.codec ?? new JsonCodec();
|
|
5965
6419
|
}
|
|
5966
6420
|
},
|
|
5967
|
-
// ConnectionProvider — single NATS connection
|
|
5968
6421
|
{
|
|
5969
6422
|
provide: JETSTREAM_CONNECTION,
|
|
5970
6423
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_EVENT_BUS],
|
|
@@ -5972,7 +6425,6 @@ var JetstreamModule = class {
|
|
|
5972
6425
|
return new ConnectionProvider(options, eventBus);
|
|
5973
6426
|
}
|
|
5974
6427
|
},
|
|
5975
|
-
// JetstreamHealthIndicator — health check for NATS connection
|
|
5976
6428
|
{
|
|
5977
6429
|
provide: JetstreamHealthIndicator,
|
|
5978
6430
|
inject: [JETSTREAM_CONNECTION],
|
|
@@ -5980,7 +6432,6 @@ var JetstreamModule = class {
|
|
|
5980
6432
|
return new JetstreamHealthIndicator(connection);
|
|
5981
6433
|
}
|
|
5982
6434
|
},
|
|
5983
|
-
// ShutdownManager — graceful shutdown orchestration
|
|
5984
6435
|
{
|
|
5985
6436
|
provide: ShutdownManager,
|
|
5986
6437
|
inject: [JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS],
|
|
@@ -5992,41 +6443,69 @@ var JetstreamModule = class {
|
|
|
5992
6443
|
);
|
|
5993
6444
|
}
|
|
5994
6445
|
},
|
|
5995
|
-
// Consumer infrastructure
|
|
5996
|
-
//
|
|
5997
|
-
// PatternRegistry — subject-to-handler mapping
|
|
6446
|
+
// Consumer infrastructure providers below return null when consumer === false
|
|
6447
|
+
// (publisher-only mode). NameResolver is the exception: clients need it too.
|
|
5998
6448
|
{
|
|
5999
|
-
provide:
|
|
6449
|
+
provide: NameResolver,
|
|
6000
6450
|
inject: [JETSTREAM_OPTIONS],
|
|
6001
6451
|
useFactory: (options) => {
|
|
6452
|
+
const logger5 = new import_common24.Logger("Jetstream:Module");
|
|
6453
|
+
warnIfManualWithDestructive(options, logger5);
|
|
6454
|
+
return new NameResolver(options);
|
|
6455
|
+
}
|
|
6456
|
+
},
|
|
6457
|
+
{
|
|
6458
|
+
provide: PatternRegistry,
|
|
6459
|
+
inject: [JETSTREAM_OPTIONS, NameResolver],
|
|
6460
|
+
useFactory: (options, names) => {
|
|
6461
|
+
if (options.consumer === false) return null;
|
|
6462
|
+
return new PatternRegistry(options, names);
|
|
6463
|
+
}
|
|
6464
|
+
},
|
|
6465
|
+
{
|
|
6466
|
+
provide: InfrastructureBinder,
|
|
6467
|
+
inject: [JETSTREAM_OPTIONS, NameResolver, PatternRegistry],
|
|
6468
|
+
useFactory: (options, names, registry) => {
|
|
6002
6469
|
if (options.consumer === false) return null;
|
|
6003
|
-
return new
|
|
6470
|
+
return new InfrastructureBinder(options, names, registry);
|
|
6004
6471
|
}
|
|
6005
6472
|
},
|
|
6006
|
-
// StreamProvider — JetStream stream lifecycle
|
|
6007
6473
|
{
|
|
6008
6474
|
provide: StreamProvider,
|
|
6009
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
6010
|
-
useFactory: (options, connection) => {
|
|
6475
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, NameResolver, InfrastructureBinder],
|
|
6476
|
+
useFactory: (options, connection, names, binder) => {
|
|
6011
6477
|
if (options.consumer === false) return null;
|
|
6012
|
-
return new StreamProvider(options, connection);
|
|
6478
|
+
return new StreamProvider(options, connection, names, binder);
|
|
6013
6479
|
}
|
|
6014
6480
|
},
|
|
6015
|
-
// ConsumerProvider
|
|
6481
|
+
// ConsumerProvider needs PatternRegistry for broadcast filtering.
|
|
6016
6482
|
{
|
|
6017
6483
|
provide: ConsumerProvider,
|
|
6018
|
-
inject: [
|
|
6019
|
-
|
|
6484
|
+
inject: [
|
|
6485
|
+
JETSTREAM_OPTIONS,
|
|
6486
|
+
JETSTREAM_CONNECTION,
|
|
6487
|
+
StreamProvider,
|
|
6488
|
+
PatternRegistry,
|
|
6489
|
+
NameResolver,
|
|
6490
|
+
InfrastructureBinder
|
|
6491
|
+
],
|
|
6492
|
+
useFactory: (options, connection, streamProvider, patternRegistry, names, binder) => {
|
|
6020
6493
|
if (options.consumer === false) return null;
|
|
6021
|
-
return new ConsumerProvider(
|
|
6494
|
+
return new ConsumerProvider(
|
|
6495
|
+
options,
|
|
6496
|
+
connection,
|
|
6497
|
+
streamProvider,
|
|
6498
|
+
patternRegistry,
|
|
6499
|
+
names,
|
|
6500
|
+
binder
|
|
6501
|
+
);
|
|
6022
6502
|
}
|
|
6023
6503
|
},
|
|
6024
|
-
// Shared ack_wait map
|
|
6504
|
+
// Shared ack_wait map, populated by the strategy after ensureConsumers().
|
|
6025
6505
|
{
|
|
6026
6506
|
provide: JETSTREAM_ACK_WAIT_MAP,
|
|
6027
6507
|
useFactory: () => /* @__PURE__ */ new Map()
|
|
6028
6508
|
},
|
|
6029
|
-
// MessageProvider — pull-based message consumption
|
|
6030
6509
|
{
|
|
6031
6510
|
provide: MessageProvider,
|
|
6032
6511
|
inject: [
|
|
@@ -6065,7 +6544,6 @@ var JetstreamModule = class {
|
|
|
6065
6544
|
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
6066
6545
|
}
|
|
6067
6546
|
},
|
|
6068
|
-
// EventRouter — routes event and broadcast messages to handlers
|
|
6069
6547
|
{
|
|
6070
6548
|
provide: EventRouter,
|
|
6071
6549
|
inject: [
|
|
@@ -6075,9 +6553,10 @@ var JetstreamModule = class {
|
|
|
6075
6553
|
JETSTREAM_CODEC,
|
|
6076
6554
|
JETSTREAM_EVENT_BUS,
|
|
6077
6555
|
JETSTREAM_ACK_WAIT_MAP,
|
|
6078
|
-
JETSTREAM_CONNECTION
|
|
6556
|
+
JETSTREAM_CONNECTION,
|
|
6557
|
+
NameResolver
|
|
6079
6558
|
],
|
|
6080
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
6559
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection, names) => {
|
|
6081
6560
|
if (options.consumer === false) return null;
|
|
6082
6561
|
const deadLetterConfig = options.onDeadLetter || options.dlq ? {
|
|
6083
6562
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -6102,11 +6581,11 @@ var JetstreamModule = class {
|
|
|
6102
6581
|
processingConfig,
|
|
6103
6582
|
ackWaitMap,
|
|
6104
6583
|
connection,
|
|
6105
|
-
options
|
|
6584
|
+
options,
|
|
6585
|
+
names
|
|
6106
6586
|
);
|
|
6107
6587
|
}
|
|
6108
6588
|
},
|
|
6109
|
-
// RpcRouter — routes RPC command messages in JetStream mode
|
|
6110
6589
|
{
|
|
6111
6590
|
provide: RpcRouter,
|
|
6112
6591
|
inject: [
|
|
@@ -6137,7 +6616,6 @@ var JetstreamModule = class {
|
|
|
6137
6616
|
);
|
|
6138
6617
|
}
|
|
6139
6618
|
},
|
|
6140
|
-
// CoreRpcServer — RPC via NATS Core request/reply
|
|
6141
6619
|
{
|
|
6142
6620
|
provide: CoreRpcServer,
|
|
6143
6621
|
inject: [
|
|
@@ -6145,14 +6623,14 @@ var JetstreamModule = class {
|
|
|
6145
6623
|
JETSTREAM_CONNECTION,
|
|
6146
6624
|
PatternRegistry,
|
|
6147
6625
|
JETSTREAM_CODEC,
|
|
6148
|
-
JETSTREAM_EVENT_BUS
|
|
6626
|
+
JETSTREAM_EVENT_BUS,
|
|
6627
|
+
NameResolver
|
|
6149
6628
|
],
|
|
6150
|
-
useFactory: (options, connection, patternRegistry, codec, eventBus) => {
|
|
6629
|
+
useFactory: (options, connection, patternRegistry, codec, eventBus, names) => {
|
|
6151
6630
|
if (options.consumer === false) return null;
|
|
6152
|
-
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
6631
|
+
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus, names);
|
|
6153
6632
|
}
|
|
6154
6633
|
},
|
|
6155
|
-
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
6156
6634
|
{
|
|
6157
6635
|
provide: MetadataProvider,
|
|
6158
6636
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
@@ -6161,7 +6639,6 @@ var JetstreamModule = class {
|
|
|
6161
6639
|
return new MetadataProvider(options, connection);
|
|
6162
6640
|
}
|
|
6163
6641
|
},
|
|
6164
|
-
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
6165
6642
|
{
|
|
6166
6643
|
provide: JetstreamStrategy,
|
|
6167
6644
|
inject: [
|
|
@@ -6246,12 +6723,12 @@ var JetstreamModule = class {
|
|
|
6246
6723
|
}
|
|
6247
6724
|
};
|
|
6248
6725
|
JetstreamModule = __decorateClass([
|
|
6249
|
-
(0,
|
|
6250
|
-
(0,
|
|
6251
|
-
__decorateParam(0, (0,
|
|
6252
|
-
__decorateParam(0, (0,
|
|
6253
|
-
__decorateParam(1, (0,
|
|
6254
|
-
__decorateParam(1, (0,
|
|
6726
|
+
(0, import_common24.Global)(),
|
|
6727
|
+
(0, import_common24.Module)({}),
|
|
6728
|
+
__decorateParam(0, (0, import_common24.Optional)()),
|
|
6729
|
+
__decorateParam(0, (0, import_common24.Inject)(ShutdownManager)),
|
|
6730
|
+
__decorateParam(1, (0, import_common24.Optional)()),
|
|
6731
|
+
__decorateParam(1, (0, import_common24.Inject)(JetstreamStrategy))
|
|
6255
6732
|
], JetstreamModule);
|
|
6256
6733
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6257
6734
|
0 && (module.exports = {
|
|
@@ -6287,6 +6764,7 @@ JetstreamModule = __decorateClass([
|
|
|
6287
6764
|
JetstreamTrace,
|
|
6288
6765
|
JsonCodec,
|
|
6289
6766
|
MIN_METADATA_TTL,
|
|
6767
|
+
ManagementMode,
|
|
6290
6768
|
MessageKind,
|
|
6291
6769
|
MsgpackCodec,
|
|
6292
6770
|
NatsErrorCode,
|