@horizon-republic/nestjs-jetstream 2.12.0 → 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 +1538 -1070
- package/dist/index.d.cts +291 -276
- package/dist/index.d.ts +291 -276
- package/dist/index.js +1479 -1011
- 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);
|
|
@@ -3880,11 +4043,8 @@ var StreamMigration = class {
|
|
|
3880
4043
|
);
|
|
3881
4044
|
}
|
|
3882
4045
|
/**
|
|
3883
|
-
*
|
|
3884
|
-
*
|
|
3885
|
-
* live migration is left alone.
|
|
3886
|
-
*
|
|
3887
|
-
* @returns true when recovery work was performed.
|
|
4046
|
+
* Finish a migration a previous process left unfinished; a backup fresh
|
|
4047
|
+
* enough to belong to a live peer migration is left alone.
|
|
3888
4048
|
*/
|
|
3889
4049
|
async recoverInterrupted(jsm, streamName2, desiredConfig) {
|
|
3890
4050
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
@@ -3938,11 +4098,9 @@ var StreamMigration = class {
|
|
|
3938
4098
|
await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
|
|
3939
4099
|
}
|
|
3940
4100
|
/**
|
|
3941
|
-
*
|
|
3942
|
-
*
|
|
3943
|
-
*
|
|
3944
|
-
* `lag: 0, active: -1` before its first sync — `active >= 0` filters that
|
|
3945
|
-
* false positive out (verified against NATS 2.12.6).
|
|
4101
|
+
* Lag-based drain check: live publishes cannot fake completion. A fresh
|
|
4102
|
+
* source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
|
|
4103
|
+
* hence the active guard.
|
|
3946
4104
|
*/
|
|
3947
4105
|
async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
|
|
3948
4106
|
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
@@ -3959,17 +4117,13 @@ var StreamMigration = class {
|
|
|
3959
4117
|
);
|
|
3960
4118
|
}
|
|
3961
4119
|
/**
|
|
3962
|
-
* A backup
|
|
3963
|
-
*
|
|
3964
|
-
* Stale leftovers are handled by recoverInterrupted() before migrate() runs,
|
|
3965
|
-
* so a timeout here means something is genuinely stuck.
|
|
3966
|
-
*
|
|
3967
|
-
* @returns true when a peer's backup was observed and cleared.
|
|
4120
|
+
* A backup present at migrate() start is a live peer migration; wait it
|
|
4121
|
+
* out. Stale leftovers were already handled by recoverInterrupted().
|
|
3968
4122
|
*/
|
|
3969
4123
|
async waitOutPeerMigration(jsm, backupName) {
|
|
3970
4124
|
if (await this.tryInfo(jsm, backupName) === null) return false;
|
|
3971
4125
|
this.logger.warn(
|
|
3972
|
-
`Migration backup ${backupName} exists
|
|
4126
|
+
`Migration backup ${backupName} exists; another instance appears to be migrating; waiting`
|
|
3973
4127
|
);
|
|
3974
4128
|
const deadline = Date.now() + this.peerWaitMs;
|
|
3975
4129
|
while (Date.now() < deadline) {
|
|
@@ -3990,7 +4144,7 @@ var StreamMigration = class {
|
|
|
3990
4144
|
}
|
|
3991
4145
|
} catch (rollbackErr) {
|
|
3992
4146
|
this.logger.error(
|
|
3993
|
-
`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:`,
|
|
3994
4148
|
rollbackErr
|
|
3995
4149
|
);
|
|
3996
4150
|
}
|
|
@@ -4006,7 +4160,7 @@ var StreamMigration = class {
|
|
|
4006
4160
|
try {
|
|
4007
4161
|
return await jsm.streams.info(name);
|
|
4008
4162
|
} catch (err) {
|
|
4009
|
-
if (err instanceof
|
|
4163
|
+
if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4010
4164
|
return null;
|
|
4011
4165
|
}
|
|
4012
4166
|
throw err;
|
|
@@ -4014,17 +4168,33 @@ var StreamMigration = class {
|
|
|
4014
4168
|
}
|
|
4015
4169
|
};
|
|
4016
4170
|
|
|
4171
|
+
// src/server/infrastructure/subject-utils.ts
|
|
4172
|
+
var subjectCovers = (broad, narrow) => {
|
|
4173
|
+
if (broad === narrow) return false;
|
|
4174
|
+
const broadTokens = broad.split(".");
|
|
4175
|
+
const narrowTokens = narrow.split(".");
|
|
4176
|
+
for (let i = 0; i < broadTokens.length; i += 1) {
|
|
4177
|
+
if (broadTokens[i] === ">") return i < narrowTokens.length;
|
|
4178
|
+
if (i >= narrowTokens.length || narrowTokens[i] === ">") return false;
|
|
4179
|
+
if (broadTokens[i] !== "*" && broadTokens[i] !== narrowTokens[i]) return false;
|
|
4180
|
+
}
|
|
4181
|
+
return broadTokens.length === narrowTokens.length;
|
|
4182
|
+
};
|
|
4183
|
+
var coversOrEquals = (broad, subject) => broad === subject || subjectCovers(broad, subject);
|
|
4184
|
+
|
|
4017
4185
|
// src/server/infrastructure/stream.provider.ts
|
|
4018
4186
|
var StreamProvider = class {
|
|
4019
|
-
constructor(options, connection) {
|
|
4187
|
+
constructor(options, connection, names, binder) {
|
|
4020
4188
|
this.options = options;
|
|
4021
4189
|
this.connection = connection;
|
|
4190
|
+
this.names = names;
|
|
4191
|
+
this.binder = binder;
|
|
4022
4192
|
const derived = deriveOtelAttrs(options);
|
|
4023
4193
|
this.otel = derived.otel;
|
|
4024
4194
|
this.otelServiceName = derived.serviceName;
|
|
4025
4195
|
this.otelEndpoint = derived.serverEndpoint;
|
|
4026
4196
|
}
|
|
4027
|
-
logger = new
|
|
4197
|
+
logger = new import_common15.Logger("Jetstream:Stream");
|
|
4028
4198
|
migration = new StreamMigration();
|
|
4029
4199
|
otel;
|
|
4030
4200
|
otelServiceName;
|
|
@@ -4038,47 +4208,48 @@ var StreamProvider = class {
|
|
|
4038
4208
|
*/
|
|
4039
4209
|
async ensureStreams(kinds) {
|
|
4040
4210
|
const jsm = await this.connection.getJetStreamManager();
|
|
4041
|
-
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 */;
|
|
4042
4220
|
if (this.options.dlq) {
|
|
4043
|
-
|
|
4221
|
+
if (dlqIsManual) {
|
|
4222
|
+
external.push({ kind: "dlq", name: this.names.dlqStreamName() });
|
|
4223
|
+
} else {
|
|
4224
|
+
reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
|
|
4225
|
+
}
|
|
4044
4226
|
}
|
|
4045
4227
|
this.logger.log(`
|
|
4046
|
-
${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
4047
|
-
if (this.options.provisioning?.preflightStorageCheck) {
|
|
4228
|
+
${formatProvisioningSummary(this.options.name, reservations, external)}`);
|
|
4229
|
+
if (this.options.provisioning?.preflightStorageCheck && reservations.length > 0) {
|
|
4048
4230
|
await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
|
|
4049
4231
|
}
|
|
4050
|
-
await Promise.all(
|
|
4232
|
+
await Promise.all([
|
|
4233
|
+
...autoKinds.map((kind) => this.ensureStream(jsm, kind)),
|
|
4234
|
+
...externalKinds.map((kind) => this.bindStream(jsm, kind))
|
|
4235
|
+
]);
|
|
4051
4236
|
if (this.options.dlq) {
|
|
4052
|
-
|
|
4237
|
+
if (dlqIsManual) {
|
|
4238
|
+
await this.bindDlqStream(jsm);
|
|
4239
|
+
} else {
|
|
4240
|
+
await this.ensureDlqStream(jsm);
|
|
4241
|
+
}
|
|
4053
4242
|
}
|
|
4054
4243
|
}
|
|
4055
4244
|
/** Get the stream name for a given kind. */
|
|
4056
4245
|
getStreamName(kind) {
|
|
4057
|
-
return
|
|
4246
|
+
return this.names.streamName(kind);
|
|
4058
4247
|
}
|
|
4059
4248
|
/** Get the subjects pattern for a given kind. */
|
|
4060
4249
|
getSubjects(kind) {
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
const subjects = [`${name}.${"ev" /* Event */}.>`];
|
|
4065
|
-
if (this.isSchedulingEnabled(kind)) {
|
|
4066
|
-
subjects.push(`${name}._sch.>`);
|
|
4067
|
-
}
|
|
4068
|
-
return subjects;
|
|
4069
|
-
}
|
|
4070
|
-
case "cmd" /* Command */:
|
|
4071
|
-
return [`${name}.${"cmd" /* Command */}.>`];
|
|
4072
|
-
case "broadcast" /* Broadcast */: {
|
|
4073
|
-
const subjects = ["broadcast.>"];
|
|
4074
|
-
if (this.isSchedulingEnabled(kind)) {
|
|
4075
|
-
subjects.push("broadcast._sch.>");
|
|
4076
|
-
}
|
|
4077
|
-
return subjects;
|
|
4078
|
-
}
|
|
4079
|
-
case "ordered" /* Ordered */:
|
|
4080
|
-
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
4081
|
-
}
|
|
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];
|
|
4082
4253
|
}
|
|
4083
4254
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
4084
4255
|
async ensureStream(jsm, kind) {
|
|
@@ -4103,7 +4274,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4103
4274
|
const currentInfo = await jsm.streams.info(config.name);
|
|
4104
4275
|
return await this.handleExistingStream(jsm, currentInfo, config, ctx);
|
|
4105
4276
|
} catch (err) {
|
|
4106
|
-
if (err instanceof
|
|
4277
|
+
if (err instanceof import_jetstream22.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4107
4278
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
4108
4279
|
return await this.runStreamOp(ctx, () => jsm.streams.add(config));
|
|
4109
4280
|
}
|
|
@@ -4134,7 +4305,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4134
4305
|
const currentInfo = await jsm.streams.info(config.name);
|
|
4135
4306
|
return await this.handleExistingStream(jsm, currentInfo, config, ctx);
|
|
4136
4307
|
} catch (err) {
|
|
4137
|
-
if (err instanceof
|
|
4308
|
+
if (err instanceof import_jetstream22.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4138
4309
|
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
4139
4310
|
return await this.runStreamOp(ctx, () => jsm.streams.add(config));
|
|
4140
4311
|
}
|
|
@@ -4145,7 +4316,8 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4145
4316
|
}
|
|
4146
4317
|
async handleExistingStream(jsm, currentInfo, config, ctx) {
|
|
4147
4318
|
if (this.isSharedStream(config.name)) {
|
|
4148
|
-
|
|
4319
|
+
const merged = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
|
|
4320
|
+
config.subjects = merged.filter((s) => !merged.some((other) => subjectCovers(other, s)));
|
|
4149
4321
|
}
|
|
4150
4322
|
const diff = compareStreamConfig(currentInfo.config, config);
|
|
4151
4323
|
if (!diff.hasChanges) {
|
|
@@ -4154,7 +4326,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4154
4326
|
}
|
|
4155
4327
|
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
4156
4328
|
if (diff.hasTransportControlledConflicts) {
|
|
4157
|
-
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(", ");
|
|
4158
4330
|
throw new Error(
|
|
4159
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.`
|
|
4160
4332
|
);
|
|
@@ -4204,13 +4376,13 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4204
4376
|
}
|
|
4205
4377
|
logChanges(streamName2, diff, migrationEnabled) {
|
|
4206
4378
|
for (const c of diff.changes) {
|
|
4207
|
-
const detail = `${c.property}: ${JSON.stringify(c.current)}
|
|
4379
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`;
|
|
4208
4380
|
if (c.mutability === "transport-controlled") {
|
|
4209
4381
|
this.logger.error(
|
|
4210
|
-
`Stream ${streamName2}: ${detail}
|
|
4382
|
+
`Stream ${streamName2}: ${detail}; transport-controlled, cannot be changed`
|
|
4211
4383
|
);
|
|
4212
4384
|
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
4213
|
-
this.logger.warn(`Stream ${streamName2}: ${detail}
|
|
4385
|
+
this.logger.warn(`Stream ${streamName2}: ${detail}; requires allowDestructiveMigration`);
|
|
4214
4386
|
} else {
|
|
4215
4387
|
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
4216
4388
|
}
|
|
@@ -4221,12 +4393,12 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4221
4393
|
return {
|
|
4222
4394
|
kind,
|
|
4223
4395
|
name: config.name,
|
|
4224
|
-
storage: config.storage ??
|
|
4396
|
+
storage: config.storage ?? import_jetstream22.StorageType.File,
|
|
4225
4397
|
numReplicas: config.num_replicas ?? 1,
|
|
4226
4398
|
maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
|
|
4227
4399
|
// NATS uses -1 for unlimited
|
|
4228
4400
|
maxAge: config.max_age ?? 0,
|
|
4229
|
-
retention: config.retention ??
|
|
4401
|
+
retention: config.retention ?? import_jetstream22.RetentionPolicy.Limits
|
|
4230
4402
|
};
|
|
4231
4403
|
}
|
|
4232
4404
|
errorContext(kind, config) {
|
|
@@ -4242,39 +4414,77 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4242
4414
|
try {
|
|
4243
4415
|
return await op();
|
|
4244
4416
|
} catch (err) {
|
|
4245
|
-
if (err instanceof
|
|
4417
|
+
if (err instanceof import_jetstream22.JetStreamApiError) {
|
|
4246
4418
|
throw mapProvisioningError(err, ctx);
|
|
4247
4419
|
}
|
|
4248
4420
|
throw err;
|
|
4249
4421
|
}
|
|
4250
4422
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
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 };
|
|
4254
4434
|
}
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
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
|
+
};
|
|
4269
4481
|
}
|
|
4270
4482
|
/**
|
|
4271
|
-
* Build the stream
|
|
4272
|
-
*
|
|
4273
|
-
* Merges the library default DLQ config with user-provided overrides.
|
|
4274
|
-
* Ensures transport-controlled settings like retention are safely decoupled.
|
|
4483
|
+
* Build the DLQ stream config: library defaults merged with user overrides,
|
|
4484
|
+
* with transport-controlled settings like retention stripped.
|
|
4275
4485
|
*/
|
|
4276
4486
|
buildDlqConfig() {
|
|
4277
|
-
const name =
|
|
4487
|
+
const name = this.names.dlqStreamName();
|
|
4278
4488
|
const subjects = [name];
|
|
4279
4489
|
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
4280
4490
|
const overrides = this.options.dlq?.stream ?? {};
|
|
@@ -4307,22 +4517,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4307
4517
|
}
|
|
4308
4518
|
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
4309
4519
|
getOverrides(kind) {
|
|
4310
|
-
|
|
4311
|
-
switch (kind) {
|
|
4312
|
-
case "ev" /* Event */:
|
|
4313
|
-
overrides = this.options.events?.stream ?? {};
|
|
4314
|
-
break;
|
|
4315
|
-
case "cmd" /* Command */:
|
|
4316
|
-
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
4317
|
-
break;
|
|
4318
|
-
case "broadcast" /* Broadcast */:
|
|
4319
|
-
overrides = this.options.broadcast?.stream ?? {};
|
|
4320
|
-
break;
|
|
4321
|
-
case "ordered" /* Ordered */:
|
|
4322
|
-
overrides = this.options.ordered?.stream ?? {};
|
|
4323
|
-
break;
|
|
4324
|
-
}
|
|
4325
|
-
return this.stripTransportControlled(overrides);
|
|
4520
|
+
return this.stripTransportControlled(kindOptionsBlock(this.options, kind)?.stream ?? {});
|
|
4326
4521
|
}
|
|
4327
4522
|
/**
|
|
4328
4523
|
* Remove transport-controlled properties from user overrides.
|
|
@@ -4332,7 +4527,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4332
4527
|
stripTransportControlled(overrides) {
|
|
4333
4528
|
if (!("retention" in overrides)) return overrides;
|
|
4334
4529
|
this.logger.debug(
|
|
4335
|
-
"Stripping user-provided retention override
|
|
4530
|
+
"Stripping user-provided retention override; retention is managed by the transport"
|
|
4336
4531
|
);
|
|
4337
4532
|
const cleaned = { ...overrides };
|
|
4338
4533
|
delete cleaned.retention;
|
|
@@ -4341,20 +4536,22 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4341
4536
|
};
|
|
4342
4537
|
|
|
4343
4538
|
// src/server/infrastructure/consumer.provider.ts
|
|
4344
|
-
var
|
|
4345
|
-
var
|
|
4539
|
+
var import_common16 = require("@nestjs/common");
|
|
4540
|
+
var import_jetstream24 = require("@nats-io/jetstream");
|
|
4346
4541
|
var ConsumerProvider = class {
|
|
4347
|
-
constructor(options, connection, streamProvider, patternRegistry) {
|
|
4542
|
+
constructor(options, connection, streamProvider, patternRegistry, names, binder) {
|
|
4348
4543
|
this.options = options;
|
|
4349
4544
|
this.connection = connection;
|
|
4350
4545
|
this.streamProvider = streamProvider;
|
|
4351
4546
|
this.patternRegistry = patternRegistry;
|
|
4547
|
+
this.names = names;
|
|
4548
|
+
this.binder = binder;
|
|
4352
4549
|
const derived = deriveOtelAttrs(options);
|
|
4353
4550
|
this.otel = derived.otel;
|
|
4354
4551
|
this.otelServiceName = derived.serviceName;
|
|
4355
4552
|
this.otelEndpoint = derived.serverEndpoint;
|
|
4356
4553
|
}
|
|
4357
|
-
logger = new
|
|
4554
|
+
logger = new import_common16.Logger("Jetstream:Consumer");
|
|
4358
4555
|
otel;
|
|
4359
4556
|
otelServiceName;
|
|
4360
4557
|
otelEndpoint;
|
|
@@ -4376,24 +4573,33 @@ var ConsumerProvider = class {
|
|
|
4376
4573
|
}
|
|
4377
4574
|
/** Get the consumer name for a given kind. */
|
|
4378
4575
|
getConsumerName(kind) {
|
|
4379
|
-
return
|
|
4576
|
+
return this.names.consumerName(kind);
|
|
4380
4577
|
}
|
|
4381
4578
|
/**
|
|
4382
4579
|
* Ensure a single consumer exists with the desired config.
|
|
4383
|
-
*
|
|
4384
|
-
* the current pod's configuration.
|
|
4580
|
+
* Startup path: creates or updates the consumer to match the current pod's configuration.
|
|
4385
4581
|
*/
|
|
4386
4582
|
async ensureConsumer(jsm, kind) {
|
|
4387
4583
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4388
4584
|
const config = this.buildConfig(kind);
|
|
4389
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
|
+
}
|
|
4390
4599
|
return withProvisioningSpan(
|
|
4391
4600
|
this.otel,
|
|
4392
4601
|
{
|
|
4393
|
-
|
|
4394
|
-
endpoint: this.otelEndpoint,
|
|
4395
|
-
entity: "consumer",
|
|
4396
|
-
name,
|
|
4602
|
+
...spanAttrs,
|
|
4397
4603
|
action: "ensure"
|
|
4398
4604
|
},
|
|
4399
4605
|
async () => {
|
|
@@ -4404,7 +4610,7 @@ var ConsumerProvider = class {
|
|
|
4404
4610
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
4405
4611
|
return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
|
|
4406
4612
|
} catch (err) {
|
|
4407
|
-
if (!(err instanceof
|
|
4613
|
+
if (!(err instanceof import_jetstream24.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4408
4614
|
throw err;
|
|
4409
4615
|
}
|
|
4410
4616
|
return await this.createConsumer(jsm, stream, name, kind, config);
|
|
@@ -4414,28 +4620,40 @@ var ConsumerProvider = class {
|
|
|
4414
4620
|
}
|
|
4415
4621
|
/**
|
|
4416
4622
|
* Recover a consumer that disappeared during runtime.
|
|
4417
|
-
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
4418
4623
|
*
|
|
4419
|
-
*
|
|
4420
|
-
*
|
|
4421
|
-
*
|
|
4422
|
-
*
|
|
4423
|
-
* This prevents old pods from:
|
|
4424
|
-
* - Overwriting a newer pod's consumer config during rolling updates
|
|
4425
|
-
* - Creating consumers during migration (which would consume and delete
|
|
4426
|
-
* 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.
|
|
4427
4628
|
*/
|
|
4428
4629
|
async recoverConsumer(jsm, kind) {
|
|
4429
4630
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4430
4631
|
const config = this.buildConfig(kind);
|
|
4431
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
|
+
}
|
|
4432
4653
|
return withProvisioningSpan(
|
|
4433
4654
|
this.otel,
|
|
4434
4655
|
{
|
|
4435
|
-
|
|
4436
|
-
endpoint: this.otelEndpoint,
|
|
4437
|
-
entity: "consumer",
|
|
4438
|
-
name,
|
|
4656
|
+
...spanAttrs,
|
|
4439
4657
|
action: "recover"
|
|
4440
4658
|
},
|
|
4441
4659
|
async () => {
|
|
@@ -4444,7 +4662,7 @@ var ConsumerProvider = class {
|
|
|
4444
4662
|
try {
|
|
4445
4663
|
return await jsm.consumers.info(stream, name);
|
|
4446
4664
|
} catch (err) {
|
|
4447
|
-
if (!(err instanceof
|
|
4665
|
+
if (!(err instanceof import_jetstream24.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4448
4666
|
throw err;
|
|
4449
4667
|
}
|
|
4450
4668
|
return await this.createConsumer(jsm, stream, name, kind, config);
|
|
@@ -4465,26 +4683,24 @@ var ConsumerProvider = class {
|
|
|
4465
4683
|
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
4466
4684
|
);
|
|
4467
4685
|
} catch (err) {
|
|
4468
|
-
if (err instanceof
|
|
4686
|
+
if (err instanceof import_jetstream24.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
4469
4687
|
return;
|
|
4470
4688
|
}
|
|
4471
4689
|
throw err;
|
|
4472
4690
|
}
|
|
4473
4691
|
}
|
|
4474
|
-
/**
|
|
4475
|
-
* Create a consumer, handling the race where another pod creates it first.
|
|
4476
|
-
*/
|
|
4692
|
+
/** Create a consumer, handling the race where another pod creates it first. */
|
|
4477
4693
|
async createConsumer(jsm, stream, name, kind, config) {
|
|
4478
4694
|
this.logger.log(`Creating consumer: ${name}`);
|
|
4479
4695
|
const ctx = { entity: "consumer", name, kind };
|
|
4480
4696
|
try {
|
|
4481
4697
|
return await jsm.consumers.add(stream, config);
|
|
4482
4698
|
} catch (addErr) {
|
|
4483
|
-
if (addErr instanceof
|
|
4699
|
+
if (addErr instanceof import_jetstream24.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
4484
4700
|
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
4485
4701
|
return await jsm.consumers.info(stream, name);
|
|
4486
4702
|
}
|
|
4487
|
-
if (addErr instanceof
|
|
4703
|
+
if (addErr instanceof import_jetstream24.JetStreamApiError) {
|
|
4488
4704
|
throw mapProvisioningError(addErr, ctx);
|
|
4489
4705
|
}
|
|
4490
4706
|
throw addErr;
|
|
@@ -4494,17 +4710,15 @@ var ConsumerProvider = class {
|
|
|
4494
4710
|
try {
|
|
4495
4711
|
return await op();
|
|
4496
4712
|
} catch (err) {
|
|
4497
|
-
if (err instanceof
|
|
4713
|
+
if (err instanceof import_jetstream24.JetStreamApiError) {
|
|
4498
4714
|
throw mapProvisioningError(err, ctx);
|
|
4499
4715
|
}
|
|
4500
4716
|
throw err;
|
|
4501
4717
|
}
|
|
4502
4718
|
}
|
|
4503
4719
|
/** Build consumer config by merging defaults with user overrides. */
|
|
4504
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
4505
4720
|
buildConfig(kind) {
|
|
4506
|
-
const
|
|
4507
|
-
const serviceName = internalName(this.options.name);
|
|
4721
|
+
const durableName = this.getConsumerName(kind);
|
|
4508
4722
|
const defaults = this.getDefaults(kind);
|
|
4509
4723
|
const overrides = this.getOverrides(kind);
|
|
4510
4724
|
if (kind === "broadcast" /* Broadcast */) {
|
|
@@ -4516,31 +4730,54 @@ var ConsumerProvider = class {
|
|
|
4516
4730
|
return {
|
|
4517
4731
|
...defaults,
|
|
4518
4732
|
...overrides,
|
|
4519
|
-
name,
|
|
4520
|
-
durable_name:
|
|
4733
|
+
name: durableName,
|
|
4734
|
+
durable_name: durableName,
|
|
4521
4735
|
filter_subject: broadcastPatterns[0]
|
|
4522
4736
|
};
|
|
4523
4737
|
}
|
|
4524
4738
|
return {
|
|
4525
4739
|
...defaults,
|
|
4526
4740
|
...overrides,
|
|
4527
|
-
name,
|
|
4528
|
-
durable_name:
|
|
4741
|
+
name: durableName,
|
|
4742
|
+
durable_name: durableName,
|
|
4529
4743
|
filter_subjects: broadcastPatterns
|
|
4530
4744
|
};
|
|
4531
4745
|
}
|
|
4532
4746
|
if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
|
|
4533
4747
|
throw new Error(`Unexpected durable consumer kind: ${kind}`);
|
|
4534
4748
|
}
|
|
4535
|
-
|
|
4749
|
+
if (this.names.hasCustomPrefix(kind)) {
|
|
4750
|
+
return this.buildCustomPrefixConfig(kind, durableName, defaults, overrides);
|
|
4751
|
+
}
|
|
4752
|
+
const filter_subject = this.names.filterSubject(kind);
|
|
4536
4753
|
return {
|
|
4537
4754
|
...defaults,
|
|
4538
4755
|
...overrides,
|
|
4539
|
-
name,
|
|
4540
|
-
durable_name:
|
|
4756
|
+
name: durableName,
|
|
4757
|
+
durable_name: durableName,
|
|
4541
4758
|
filter_subject
|
|
4542
4759
|
};
|
|
4543
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
|
+
}
|
|
4544
4781
|
/** Get default config for a consumer kind. */
|
|
4545
4782
|
getDefaults(kind) {
|
|
4546
4783
|
switch (kind) {
|
|
@@ -4561,27 +4798,13 @@ var ConsumerProvider = class {
|
|
|
4561
4798
|
}
|
|
4562
4799
|
/** Get user-provided overrides for a consumer kind. */
|
|
4563
4800
|
getOverrides(kind) {
|
|
4564
|
-
|
|
4565
|
-
case "ev" /* Event */:
|
|
4566
|
-
return this.options.events?.consumer ?? {};
|
|
4567
|
-
case "cmd" /* Command */:
|
|
4568
|
-
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
4569
|
-
case "broadcast" /* Broadcast */:
|
|
4570
|
-
return this.options.broadcast?.consumer ?? {};
|
|
4571
|
-
case "ordered" /* Ordered */:
|
|
4572
|
-
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
4573
|
-
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
4574
|
-
default: {
|
|
4575
|
-
const _exhaustive = kind;
|
|
4576
|
-
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4801
|
+
return kindOptionsBlock(this.options, kind)?.consumer ?? {};
|
|
4579
4802
|
}
|
|
4580
4803
|
};
|
|
4581
4804
|
|
|
4582
4805
|
// src/server/infrastructure/message.provider.ts
|
|
4583
|
-
var
|
|
4584
|
-
var
|
|
4806
|
+
var import_common17 = require("@nestjs/common");
|
|
4807
|
+
var import_jetstream26 = require("@nats-io/jetstream");
|
|
4585
4808
|
var import_rxjs3 = require("rxjs");
|
|
4586
4809
|
var MessageProvider = class {
|
|
4587
4810
|
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
@@ -4590,7 +4813,7 @@ var MessageProvider = class {
|
|
|
4590
4813
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
4591
4814
|
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
4592
4815
|
}
|
|
4593
|
-
logger = new
|
|
4816
|
+
logger = new import_common17.Logger("Jetstream:Message");
|
|
4594
4817
|
activeIterators = /* @__PURE__ */ new Set();
|
|
4595
4818
|
orderedReadyResolve = null;
|
|
4596
4819
|
orderedReadyReject = null;
|
|
@@ -4633,7 +4856,7 @@ var MessageProvider = class {
|
|
|
4633
4856
|
/**
|
|
4634
4857
|
* Start an ordered consumer for strict sequential delivery.
|
|
4635
4858
|
*
|
|
4636
|
-
* Unlike durable consumers, ordered consumers are ephemeral
|
|
4859
|
+
* Unlike durable consumers, ordered consumers are ephemeral: created at
|
|
4637
4860
|
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
4638
4861
|
*
|
|
4639
4862
|
* @param streamName - JetStream stream to consume from.
|
|
@@ -4642,7 +4865,7 @@ var MessageProvider = class {
|
|
|
4642
4865
|
*/
|
|
4643
4866
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
4644
4867
|
const consumerOpts = { filter_subjects: filterSubjects };
|
|
4645
|
-
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !==
|
|
4868
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream26.DeliverPolicy.All) {
|
|
4646
4869
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
4647
4870
|
}
|
|
4648
4871
|
if (orderedConfig?.optStartSeq !== void 0) {
|
|
@@ -4847,7 +5070,7 @@ var MessageProvider = class {
|
|
|
4847
5070
|
};
|
|
4848
5071
|
|
|
4849
5072
|
// src/server/infrastructure/metadata.provider.ts
|
|
4850
|
-
var
|
|
5073
|
+
var import_common18 = require("@nestjs/common");
|
|
4851
5074
|
var import_kv = require("@nats-io/kv");
|
|
4852
5075
|
var MetadataProvider = class {
|
|
4853
5076
|
constructor(options, connection) {
|
|
@@ -4856,7 +5079,7 @@ var MetadataProvider = class {
|
|
|
4856
5079
|
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
4857
5080
|
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
4858
5081
|
}
|
|
4859
|
-
logger = new
|
|
5082
|
+
logger = new import_common18.Logger("Jetstream:Metadata");
|
|
4860
5083
|
bucketName;
|
|
4861
5084
|
replicas;
|
|
4862
5085
|
ttl;
|
|
@@ -4864,16 +5087,12 @@ var MetadataProvider = class {
|
|
|
4864
5087
|
heartbeatTimer;
|
|
4865
5088
|
cachedKv;
|
|
4866
5089
|
/**
|
|
4867
|
-
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
5090
|
+
* Write handler metadata entries to the KV bucket and start the heartbeat.
|
|
4868
5091
|
*
|
|
4869
|
-
* Creates the bucket if
|
|
4870
|
-
*
|
|
4871
|
-
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
4872
|
-
* 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.
|
|
4873
5094
|
*
|
|
4874
|
-
*
|
|
4875
|
-
*
|
|
4876
|
-
* @param entries Map of KV key → metadata object.
|
|
5095
|
+
* @param entries Map of KV key to metadata object.
|
|
4877
5096
|
*/
|
|
4878
5097
|
async publish(entries) {
|
|
4879
5098
|
if (entries.size === 0) return;
|
|
@@ -4949,396 +5168,626 @@ var MetadataProvider = class {
|
|
|
4949
5168
|
};
|
|
4950
5169
|
|
|
4951
5170
|
// src/server/routing/event.router.ts
|
|
4952
|
-
var
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
var
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
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
|
+
}
|
|
4959
5250
|
};
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
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) {
|
|
4963
5258
|
this.patternRegistry = patternRegistry;
|
|
4964
|
-
this.codec = codec;
|
|
4965
5259
|
this.eventBus = eventBus;
|
|
4966
5260
|
this.deadLetterConfig = deadLetterConfig;
|
|
4967
|
-
this.
|
|
4968
|
-
this.
|
|
5261
|
+
this.otel = otel;
|
|
5262
|
+
this.serviceName = serviceName;
|
|
5263
|
+
this.serverEndpoint = serverEndpoint;
|
|
4969
5264
|
this.connection = connection;
|
|
4970
5265
|
this.options = options;
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
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
|
+
);
|
|
5308
|
+
}
|
|
5309
|
+
/**
|
|
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.
|
|
5313
|
+
*/
|
|
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);
|
|
4980
5338
|
}
|
|
4981
5339
|
}
|
|
4982
|
-
logger = new import_common18.Logger("Jetstream:EventRouter");
|
|
4983
|
-
subscriptions = [];
|
|
4984
|
-
otel;
|
|
4985
|
-
serviceName;
|
|
4986
|
-
serverEndpoint;
|
|
4987
5340
|
/**
|
|
4988
|
-
*
|
|
4989
|
-
*
|
|
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.
|
|
4990
5344
|
*/
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
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
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
throw lastErr;
|
|
4994
5361
|
}
|
|
4995
|
-
/**
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
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);
|
|
5373
|
+
}
|
|
5001
5374
|
}
|
|
5375
|
+
return hdrs;
|
|
5002
5376
|
}
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
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
|
+
);
|
|
5386
|
+
}
|
|
5007
5387
|
}
|
|
5008
|
-
this.
|
|
5388
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5389
|
+
msg.term("Moved to DLQ stream");
|
|
5390
|
+
});
|
|
5009
5391
|
}
|
|
5010
|
-
/**
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
const
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
}
|
|
5061
|
-
|
|
5062
|
-
msg.ack();
|
|
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);
|
|
5063
5444
|
});
|
|
5064
5445
|
return void 0;
|
|
5065
|
-
}
|
|
5066
|
-
|
|
5067
|
-
if (
|
|
5068
|
-
|
|
5069
|
-
|
|
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
|
+
);
|
|
5070
5454
|
}
|
|
5071
5455
|
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5072
|
-
msg.nak();
|
|
5456
|
+
msg.nak(ctx.retryDelay);
|
|
5073
5457
|
});
|
|
5074
|
-
|
|
5075
|
-
|
|
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
|
+
}
|
|
5076
5514
|
let data;
|
|
5077
5515
|
try {
|
|
5078
5516
|
data = codec.decode(msg.data);
|
|
5079
|
-
} catch {
|
|
5080
|
-
data = void 0;
|
|
5081
|
-
}
|
|
5082
|
-
return capture(msg, data, err).catch((captureErr) => {
|
|
5083
|
-
logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
|
|
5084
|
-
});
|
|
5085
|
-
};
|
|
5086
|
-
const resolveEvent = (msg) => {
|
|
5087
|
-
const subject = msg.subject;
|
|
5088
|
-
try {
|
|
5089
|
-
const handler = patternRegistry.getHandler(subject);
|
|
5090
|
-
if (!handler) {
|
|
5091
|
-
logger5.error(`No handler for subject: ${subject}`);
|
|
5092
|
-
if (handleDeadLetter !== null) {
|
|
5093
|
-
return captureUnroutable(
|
|
5094
|
-
handleDeadLetter,
|
|
5095
|
-
msg,
|
|
5096
|
-
new Error(`No handler for event: ${subject}`)
|
|
5097
|
-
);
|
|
5098
|
-
}
|
|
5099
|
-
msg.term(`No handler for event: ${subject}`);
|
|
5100
|
-
return null;
|
|
5101
|
-
}
|
|
5102
|
-
let data;
|
|
5103
|
-
try {
|
|
5104
|
-
data = codec.decode(msg.data);
|
|
5105
|
-
} catch (err) {
|
|
5106
|
-
logger5.error(`Decode error for ${subject}:`, err);
|
|
5107
|
-
if (handleDeadLetter !== null) {
|
|
5108
|
-
return captureUnroutable(
|
|
5109
|
-
handleDeadLetter,
|
|
5110
|
-
msg,
|
|
5111
|
-
new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
|
|
5112
|
-
);
|
|
5113
|
-
}
|
|
5114
|
-
msg.term("Decode error");
|
|
5115
|
-
return null;
|
|
5116
|
-
}
|
|
5117
|
-
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5118
|
-
return { handler, data };
|
|
5119
5517
|
} catch (err) {
|
|
5120
|
-
logger5.error(`
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
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
|
+
);
|
|
5125
5525
|
}
|
|
5526
|
+
msg.term("Decode error");
|
|
5126
5527
|
return null;
|
|
5127
5528
|
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
return "success";
|
|
5133
|
-
};
|
|
5134
|
-
const handleSafe = (msg) => {
|
|
5135
|
-
const resolved = resolveEvent(msg);
|
|
5136
|
-
if (resolved === null) return void 0;
|
|
5137
|
-
if (isPromiseLike2(resolved)) return resolved;
|
|
5138
|
-
const { handler, data } = resolved;
|
|
5139
|
-
const ctx = new RpcContext([msg]);
|
|
5140
|
-
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5141
|
-
const startedAt = performance.now();
|
|
5142
|
-
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);
|
|
5143
5533
|
try {
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
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) => {
|
|
5159
5605
|
eventBus.emit(
|
|
5160
5606
|
"error" /* Error */,
|
|
5161
5607
|
err instanceof Error ? err : new Error(String(err)),
|
|
5162
5608
|
`${kind}-handler:${msg.subject}`
|
|
5163
5609
|
);
|
|
5164
5610
|
reportHandlerCompleted(msg, startedAt, "error");
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
}
|
|
5168
|
-
}
|
|
5169
|
-
if (!isPromiseLike2(pending)) {
|
|
5170
|
-
const settled = settleSuccess(msg, ctx, data);
|
|
5171
|
-
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5172
|
-
if (settled === void 0) {
|
|
5611
|
+
try {
|
|
5612
|
+
await settleFailure(msg, data, err);
|
|
5613
|
+
} finally {
|
|
5173
5614
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5174
|
-
return void 0;
|
|
5175
5615
|
}
|
|
5176
|
-
return settled.finally(() => {
|
|
5177
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5178
|
-
});
|
|
5179
5616
|
}
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5196
|
-
try {
|
|
5197
|
-
await settleFailure(msg, data, err);
|
|
5198
|
-
} finally {
|
|
5199
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5200
|
-
}
|
|
5201
|
-
}
|
|
5202
|
-
);
|
|
5203
|
-
};
|
|
5204
|
-
const handleOrderedSafe = (msg) => {
|
|
5205
|
-
const subject = msg.subject;
|
|
5206
|
-
let handler;
|
|
5207
|
-
let data;
|
|
5208
|
-
try {
|
|
5209
|
-
handler = patternRegistry.getHandler(subject);
|
|
5210
|
-
if (!handler) {
|
|
5211
|
-
logger5.error(`No handler for subject: ${subject}`);
|
|
5212
|
-
return void 0;
|
|
5213
|
-
}
|
|
5214
|
-
try {
|
|
5215
|
-
data = codec.decode(msg.data);
|
|
5216
|
-
} catch (err) {
|
|
5217
|
-
logger5.error(`Decode error for ${subject}:`, err);
|
|
5218
|
-
return void 0;
|
|
5219
|
-
}
|
|
5220
|
-
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5221
|
-
} catch (err) {
|
|
5222
|
-
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}`);
|
|
5223
5632
|
return void 0;
|
|
5224
5633
|
}
|
|
5225
|
-
const ctx = new RpcContext([msg]);
|
|
5226
|
-
const warnIfSettlementAttempted = () => {
|
|
5227
|
-
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
5228
|
-
logger5.warn(
|
|
5229
|
-
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
5230
|
-
);
|
|
5231
|
-
}
|
|
5232
|
-
};
|
|
5233
|
-
const startedAt = performance.now();
|
|
5234
|
-
let pending;
|
|
5235
5634
|
try {
|
|
5236
|
-
|
|
5237
|
-
{
|
|
5238
|
-
subject: msg.subject,
|
|
5239
|
-
msg,
|
|
5240
|
-
info: msg.info,
|
|
5241
|
-
kind: spanKind,
|
|
5242
|
-
payloadBytes: msg.data.length,
|
|
5243
|
-
handlerMetadata: { pattern: msg.subject },
|
|
5244
|
-
serviceName,
|
|
5245
|
-
endpoint: serverEndpoint
|
|
5246
|
-
},
|
|
5247
|
-
otel,
|
|
5248
|
-
() => unwrapResult(handler(data, ctx))
|
|
5249
|
-
);
|
|
5635
|
+
data = codec.decode(msg.data);
|
|
5250
5636
|
} catch (err) {
|
|
5251
|
-
logger5.error(`
|
|
5252
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5637
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
5253
5638
|
return void 0;
|
|
5254
5639
|
}
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
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
|
+
);
|
|
5259
5651
|
}
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
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
|
|
5264
5666
|
},
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5268
|
-
}
|
|
5667
|
+
otel,
|
|
5668
|
+
() => unwrapResult(handler(data, ctx))
|
|
5269
5669
|
);
|
|
5270
|
-
}
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
}
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
};
|
|
5289
|
-
const trackAsync = (result, msg) => {
|
|
5290
|
-
void result.catch((err) => {
|
|
5291
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5292
|
-
}).finally(onAsyncDone);
|
|
5293
|
-
};
|
|
5294
|
-
const drainBacklog = () => {
|
|
5295
|
-
while (active < maxActive) {
|
|
5296
|
-
const next = backlog.shift();
|
|
5297
|
-
if (next === void 0) return;
|
|
5298
|
-
next.stopAckExtension?.();
|
|
5299
|
-
active++;
|
|
5300
|
-
const result = routeSafely(next.msg);
|
|
5301
|
-
if (result !== void 0) {
|
|
5302
|
-
trackAsync(result, next.msg);
|
|
5303
|
-
} else {
|
|
5304
|
-
active--;
|
|
5305
|
-
}
|
|
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");
|
|
5306
5688
|
}
|
|
5307
|
-
|
|
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
|
|
5308
5775
|
};
|
|
5309
|
-
const
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
backlogWarned = true;
|
|
5318
|
-
logger5.warn(
|
|
5319
|
-
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5320
|
-
);
|
|
5321
|
-
}
|
|
5322
|
-
return;
|
|
5323
|
-
}
|
|
5324
|
-
active++;
|
|
5325
|
-
const result = routeSafely(msg);
|
|
5326
|
-
if (result !== void 0) {
|
|
5327
|
-
trackAsync(result, msg);
|
|
5328
|
-
} else {
|
|
5329
|
-
active--;
|
|
5330
|
-
if (backlog.length > 0) drainBacklog();
|
|
5331
|
-
}
|
|
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);
|
|
5781
|
+
const subscription = stream$.subscribe({
|
|
5782
|
+
next: (msg) => {
|
|
5783
|
+
gate.push(msg);
|
|
5332
5784
|
},
|
|
5333
5785
|
error: (err) => {
|
|
5334
|
-
|
|
5786
|
+
this.logger.error(`Stream error in ${kind} router`, err);
|
|
5335
5787
|
}
|
|
5336
5788
|
});
|
|
5337
5789
|
subscription.add(() => {
|
|
5338
|
-
|
|
5339
|
-
queued.stopAckExtension?.();
|
|
5340
|
-
}
|
|
5341
|
-
backlog.length = 0;
|
|
5790
|
+
gate.dispose();
|
|
5342
5791
|
});
|
|
5343
5792
|
this.subscriptions.push(subscription);
|
|
5344
5793
|
}
|
|
@@ -5352,178 +5801,11 @@ var EventRouter = class {
|
|
|
5352
5801
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
5353
5802
|
return void 0;
|
|
5354
5803
|
}
|
|
5355
|
-
/**
|
|
5356
|
-
* Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
|
|
5357
|
-
* success. On failure the message is nak'd to release it, but the server
|
|
5358
|
-
* never redelivers past `max_deliver` — it stays in the stream for manual
|
|
5359
|
-
* recovery. Used when the DLQ stream isn't configured, or when publishing
|
|
5360
|
-
* to it failed and we still have to surface the message somewhere.
|
|
5361
|
-
*/
|
|
5362
|
-
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
5363
|
-
const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
|
|
5364
|
-
if (!onDeadLetter) {
|
|
5365
|
-
this.logger.error(
|
|
5366
|
-
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
|
|
5367
|
-
);
|
|
5368
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5369
|
-
msg.nak();
|
|
5370
|
-
});
|
|
5371
|
-
return;
|
|
5372
|
-
}
|
|
5373
|
-
try {
|
|
5374
|
-
await onDeadLetter(info);
|
|
5375
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5376
|
-
msg.term("Dead letter processed via fallback callback");
|
|
5377
|
-
});
|
|
5378
|
-
} catch (hookErr) {
|
|
5379
|
-
this.logger.error(
|
|
5380
|
-
`Fallback onDeadLetter callback failed for ${msg.subject} \u2014 the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
|
|
5381
|
-
hookErr
|
|
5382
|
-
);
|
|
5383
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5384
|
-
msg.nak();
|
|
5385
|
-
});
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5388
|
-
/**
|
|
5389
|
-
* Copy the original message headers for the DLQ republish, dropping NATS
|
|
5390
|
-
* server control headers: a copied Nats-TTL expires the DLQ entry (or gets
|
|
5391
|
-
* the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
|
|
5392
|
-
* Nats-Msg-Id collides with the DLQ dedup window.
|
|
5393
|
-
*/
|
|
5394
|
-
buildDlqHeaders(msg) {
|
|
5395
|
-
const hdrs = (0, import_transport_node4.headers)();
|
|
5396
|
-
if (!msg.headers) return hdrs;
|
|
5397
|
-
for (const [k, v] of msg.headers) {
|
|
5398
|
-
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5399
|
-
for (const val of v) {
|
|
5400
|
-
hdrs.append(k, val);
|
|
5401
|
-
}
|
|
5402
|
-
}
|
|
5403
|
-
return hdrs;
|
|
5404
|
-
}
|
|
5405
|
-
/**
|
|
5406
|
-
* Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
|
|
5407
|
-
*
|
|
5408
|
-
* Past `max_deliver` the server never redelivers, so an in-process retry is
|
|
5409
|
-
* the only second chance a dead letter gets. There is no artificial delay
|
|
5410
|
-
* between attempts: when the broker is unreachable each publish already
|
|
5411
|
-
* spends its own request timeout, which spaces the attempts naturally.
|
|
5412
|
-
*/
|
|
5413
|
-
async publishToDlqWithRetry(connection, subject, data, headers2) {
|
|
5414
|
-
let lastErr;
|
|
5415
|
-
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5416
|
-
try {
|
|
5417
|
-
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5418
|
-
return;
|
|
5419
|
-
} catch (err) {
|
|
5420
|
-
lastErr = err;
|
|
5421
|
-
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5422
|
-
this.logger.warn(
|
|
5423
|
-
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5424
|
-
);
|
|
5425
|
-
}
|
|
5426
|
-
}
|
|
5427
|
-
}
|
|
5428
|
-
throw lastErr;
|
|
5429
|
-
}
|
|
5430
|
-
/**
|
|
5431
|
-
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
5432
|
-
*
|
|
5433
|
-
* Appends diagnostic metadata headers to the original message and preserves
|
|
5434
|
-
* the primary payload. If publishing succeeds, it notifies the standard
|
|
5435
|
-
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
5436
|
-
* back to the callback entirely to prevent silent data loss.
|
|
5437
|
-
*/
|
|
5438
|
-
async publishToDlq(msg, info, error) {
|
|
5439
|
-
const serviceName = this.options?.name;
|
|
5440
|
-
if (!this.connection || !serviceName) {
|
|
5441
|
-
this.logger.error(
|
|
5442
|
-
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
5443
|
-
);
|
|
5444
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5445
|
-
return;
|
|
5446
|
-
}
|
|
5447
|
-
const destinationSubject = dlqStreamName(serviceName);
|
|
5448
|
-
const hdrs = this.buildDlqHeaders(msg);
|
|
5449
|
-
let reason = String(error);
|
|
5450
|
-
if (error instanceof Error) {
|
|
5451
|
-
reason = error.message;
|
|
5452
|
-
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
5453
|
-
reason = String(error.message);
|
|
5454
|
-
}
|
|
5455
|
-
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
5456
|
-
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
5457
|
-
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
5458
|
-
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
5459
|
-
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
5460
|
-
try {
|
|
5461
|
-
await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
5462
|
-
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
5463
|
-
if (this.deadLetterConfig?.onDeadLetter) {
|
|
5464
|
-
try {
|
|
5465
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
5466
|
-
} catch (hookErr) {
|
|
5467
|
-
this.logger.warn(
|
|
5468
|
-
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
5469
|
-
hookErr
|
|
5470
|
-
);
|
|
5471
|
-
}
|
|
5472
|
-
}
|
|
5473
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5474
|
-
msg.term("Moved to DLQ stream");
|
|
5475
|
-
});
|
|
5476
|
-
} catch (publishErr) {
|
|
5477
|
-
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
5478
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5479
|
-
}
|
|
5480
|
-
}
|
|
5481
|
-
/**
|
|
5482
|
-
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
5483
|
-
*
|
|
5484
|
-
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
5485
|
-
* or directly to the fallback callback based on the active module configuration.
|
|
5486
|
-
*/
|
|
5487
|
-
async handleDeadLetter(msg, data, error) {
|
|
5488
|
-
const info = {
|
|
5489
|
-
subject: msg.subject,
|
|
5490
|
-
data,
|
|
5491
|
-
headers: msg.headers,
|
|
5492
|
-
error,
|
|
5493
|
-
deliveryCount: msg.info.deliveryCount,
|
|
5494
|
-
stream: msg.info.stream,
|
|
5495
|
-
streamSequence: msg.info.streamSequence,
|
|
5496
|
-
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
5497
|
-
};
|
|
5498
|
-
await withDeadLetterSpan(
|
|
5499
|
-
{
|
|
5500
|
-
msg,
|
|
5501
|
-
// Pattern resolution mirrors event-routing: when a registered
|
|
5502
|
-
// pattern matches, surface it on the DLQ span so APM can filter
|
|
5503
|
-
// dead letters by handler without parsing the subject. Falls back
|
|
5504
|
-
// to the subject itself when no glob handler is in play.
|
|
5505
|
-
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
5506
|
-
finalDeliveryCount: msg.info.deliveryCount,
|
|
5507
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
5508
|
-
serviceName: this.serviceName,
|
|
5509
|
-
endpoint: this.serverEndpoint
|
|
5510
|
-
},
|
|
5511
|
-
this.otel,
|
|
5512
|
-
async () => {
|
|
5513
|
-
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
5514
|
-
if (!this.options?.dlq) {
|
|
5515
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5516
|
-
} else {
|
|
5517
|
-
await this.publishToDlq(msg, info, error);
|
|
5518
|
-
}
|
|
5519
|
-
}
|
|
5520
|
-
);
|
|
5521
|
-
}
|
|
5522
5804
|
};
|
|
5523
5805
|
|
|
5524
5806
|
// src/server/routing/rpc.router.ts
|
|
5525
|
-
var
|
|
5526
|
-
var
|
|
5807
|
+
var import_common21 = require("@nestjs/common");
|
|
5808
|
+
var import_transport_node6 = require("@nats-io/transport-node");
|
|
5527
5809
|
var RpcRouter = class {
|
|
5528
5810
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
|
|
5529
5811
|
this.messageProvider = messageProvider;
|
|
@@ -5546,7 +5828,7 @@ var RpcRouter = class {
|
|
|
5546
5828
|
this.serverEndpoint = null;
|
|
5547
5829
|
}
|
|
5548
5830
|
}
|
|
5549
|
-
logger = new
|
|
5831
|
+
logger = new import_common21.Logger("Jetstream:RpcRouter");
|
|
5550
5832
|
timeout;
|
|
5551
5833
|
concurrency;
|
|
5552
5834
|
resolvedAckExtensionInterval;
|
|
@@ -5597,7 +5879,7 @@ var RpcRouter = class {
|
|
|
5597
5879
|
};
|
|
5598
5880
|
const publishReply = (replyTo, correlationId, payload) => {
|
|
5599
5881
|
try {
|
|
5600
|
-
const hdrs = (0,
|
|
5882
|
+
const hdrs = (0, import_transport_node6.headers)();
|
|
5601
5883
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
5602
5884
|
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
5603
5885
|
} catch (publishErr) {
|
|
@@ -5606,7 +5888,7 @@ var RpcRouter = class {
|
|
|
5606
5888
|
};
|
|
5607
5889
|
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
5608
5890
|
try {
|
|
5609
|
-
const hdrs = (0,
|
|
5891
|
+
const hdrs = (0, import_transport_node6.headers)();
|
|
5610
5892
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
5611
5893
|
hdrs.set("x-error" /* Error */, "true");
|
|
5612
5894
|
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
@@ -5737,75 +6019,18 @@ var RpcRouter = class {
|
|
|
5737
6019
|
}
|
|
5738
6020
|
);
|
|
5739
6021
|
};
|
|
5740
|
-
const
|
|
5741
|
-
|
|
5742
|
-
let backlogWarned = false;
|
|
5743
|
-
const backlog = [];
|
|
5744
|
-
const onAsyncDone = () => {
|
|
5745
|
-
active--;
|
|
5746
|
-
drainBacklog();
|
|
5747
|
-
};
|
|
5748
|
-
const routeSafely = (msg) => {
|
|
5749
|
-
try {
|
|
5750
|
-
return handleSafe(msg);
|
|
5751
|
-
} catch (err) {
|
|
5752
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5753
|
-
return void 0;
|
|
5754
|
-
}
|
|
5755
|
-
};
|
|
5756
|
-
const trackAsync = (result, msg) => {
|
|
5757
|
-
void result.catch((err) => {
|
|
5758
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5759
|
-
}).finally(onAsyncDone);
|
|
5760
|
-
};
|
|
5761
|
-
const drainBacklog = () => {
|
|
5762
|
-
while (active < maxActive) {
|
|
5763
|
-
const next = backlog.shift();
|
|
5764
|
-
if (next === void 0) return;
|
|
5765
|
-
next.stopAckExtension?.();
|
|
5766
|
-
active++;
|
|
5767
|
-
const result = routeSafely(next.msg);
|
|
5768
|
-
if (result !== void 0) {
|
|
5769
|
-
trackAsync(result, next.msg);
|
|
5770
|
-
} else {
|
|
5771
|
-
active--;
|
|
5772
|
-
}
|
|
5773
|
-
}
|
|
5774
|
-
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
5775
|
-
};
|
|
6022
|
+
const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
6023
|
+
const gate = new ConcurrencyGate(maxActive, handleSafe, parkTimer, logger5, "RPC");
|
|
5776
6024
|
this.subscription = this.messageProvider.commands$.subscribe({
|
|
5777
6025
|
next: (msg) => {
|
|
5778
|
-
|
|
5779
|
-
backlog.push({
|
|
5780
|
-
msg,
|
|
5781
|
-
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5782
|
-
});
|
|
5783
|
-
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5784
|
-
backlogWarned = true;
|
|
5785
|
-
logger5.warn(
|
|
5786
|
-
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5787
|
-
);
|
|
5788
|
-
}
|
|
5789
|
-
return;
|
|
5790
|
-
}
|
|
5791
|
-
active++;
|
|
5792
|
-
const result = routeSafely(msg);
|
|
5793
|
-
if (result !== void 0) {
|
|
5794
|
-
trackAsync(result, msg);
|
|
5795
|
-
} else {
|
|
5796
|
-
active--;
|
|
5797
|
-
if (backlog.length > 0) drainBacklog();
|
|
5798
|
-
}
|
|
6026
|
+
gate.push(msg);
|
|
5799
6027
|
},
|
|
5800
6028
|
error: (err) => {
|
|
5801
6029
|
logger5.error("Stream error in RPC router", err);
|
|
5802
6030
|
}
|
|
5803
6031
|
});
|
|
5804
6032
|
this.subscription.add(() => {
|
|
5805
|
-
|
|
5806
|
-
queued.stopAckExtension?.();
|
|
5807
|
-
}
|
|
5808
|
-
backlog.length = 0;
|
|
6033
|
+
gate.dispose();
|
|
5809
6034
|
});
|
|
5810
6035
|
}
|
|
5811
6036
|
/** Stop routing and unsubscribe. */
|
|
@@ -5815,20 +6040,220 @@ var RpcRouter = class {
|
|
|
5815
6040
|
}
|
|
5816
6041
|
};
|
|
5817
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
|
+
|
|
5818
6243
|
// src/shutdown/shutdown.manager.ts
|
|
5819
|
-
var
|
|
6244
|
+
var import_common23 = require("@nestjs/common");
|
|
5820
6245
|
var ShutdownManager = class {
|
|
5821
6246
|
constructor(connection, eventBus, timeout) {
|
|
5822
6247
|
this.connection = connection;
|
|
5823
6248
|
this.eventBus = eventBus;
|
|
5824
6249
|
this.timeout = timeout;
|
|
5825
6250
|
}
|
|
5826
|
-
logger = new
|
|
6251
|
+
logger = new import_common23.Logger("Jetstream:Shutdown");
|
|
5827
6252
|
shutdownPromise;
|
|
5828
6253
|
/**
|
|
5829
6254
|
* Execute the full shutdown sequence.
|
|
5830
6255
|
*
|
|
5831
|
-
* Idempotent
|
|
6256
|
+
* Idempotent: concurrent or repeated calls return the same promise.
|
|
5832
6257
|
*
|
|
5833
6258
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
5834
6259
|
*/
|
|
@@ -5858,6 +6283,12 @@ var ShutdownManager = class {
|
|
|
5858
6283
|
|
|
5859
6284
|
// src/jetstream.module.ts
|
|
5860
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
|
+
};
|
|
5861
6292
|
var JetstreamModule = class {
|
|
5862
6293
|
constructor(shutdownManager, strategy) {
|
|
5863
6294
|
this.shutdownManager = shutdownManager;
|
|
@@ -5887,7 +6318,8 @@ var JetstreamModule = class {
|
|
|
5887
6318
|
PatternRegistry,
|
|
5888
6319
|
ShutdownManager,
|
|
5889
6320
|
JetstreamStrategy,
|
|
5890
|
-
JetstreamHealthIndicator
|
|
6321
|
+
JetstreamHealthIndicator,
|
|
6322
|
+
NameResolver
|
|
5891
6323
|
]
|
|
5892
6324
|
};
|
|
5893
6325
|
}
|
|
@@ -5916,7 +6348,8 @@ var JetstreamModule = class {
|
|
|
5916
6348
|
PatternRegistry,
|
|
5917
6349
|
ShutdownManager,
|
|
5918
6350
|
JetstreamStrategy,
|
|
5919
|
-
JetstreamHealthIndicator
|
|
6351
|
+
JetstreamHealthIndicator,
|
|
6352
|
+
NameResolver
|
|
5920
6353
|
]
|
|
5921
6354
|
};
|
|
5922
6355
|
}
|
|
@@ -5933,10 +6366,23 @@ var JetstreamModule = class {
|
|
|
5933
6366
|
const clientToken = getClientToken(options.name);
|
|
5934
6367
|
const clientProvider = {
|
|
5935
6368
|
provide: clientToken,
|
|
5936
|
-
inject: [
|
|
5937
|
-
|
|
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) => {
|
|
5938
6377
|
const codec = options.codec ?? rootCodec;
|
|
5939
|
-
return new JetstreamClient(
|
|
6378
|
+
return new JetstreamClient(
|
|
6379
|
+
rootOptions,
|
|
6380
|
+
options.name,
|
|
6381
|
+
connection,
|
|
6382
|
+
codec,
|
|
6383
|
+
eventBus,
|
|
6384
|
+
names ?? void 0
|
|
6385
|
+
);
|
|
5940
6386
|
}
|
|
5941
6387
|
};
|
|
5942
6388
|
return {
|
|
@@ -5957,16 +6403,14 @@ var JetstreamModule = class {
|
|
|
5957
6403
|
/** Create providers that depend on JETSTREAM_OPTIONS (shared by sync and async). */
|
|
5958
6404
|
static createCoreDependentProviders() {
|
|
5959
6405
|
return [
|
|
5960
|
-
// EventBus — hook system with Logger fallback
|
|
5961
6406
|
{
|
|
5962
6407
|
provide: JETSTREAM_EVENT_BUS,
|
|
5963
6408
|
inject: [JETSTREAM_OPTIONS],
|
|
5964
6409
|
useFactory: (options) => {
|
|
5965
|
-
const logger5 = new
|
|
6410
|
+
const logger5 = new import_common24.Logger("Jetstream:Module");
|
|
5966
6411
|
return new EventBus(logger5, options.hooks);
|
|
5967
6412
|
}
|
|
5968
6413
|
},
|
|
5969
|
-
// Codec — global encode/decode
|
|
5970
6414
|
{
|
|
5971
6415
|
provide: JETSTREAM_CODEC,
|
|
5972
6416
|
inject: [JETSTREAM_OPTIONS],
|
|
@@ -5974,7 +6418,6 @@ var JetstreamModule = class {
|
|
|
5974
6418
|
return options.codec ?? new JsonCodec();
|
|
5975
6419
|
}
|
|
5976
6420
|
},
|
|
5977
|
-
// ConnectionProvider — single NATS connection
|
|
5978
6421
|
{
|
|
5979
6422
|
provide: JETSTREAM_CONNECTION,
|
|
5980
6423
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_EVENT_BUS],
|
|
@@ -5982,7 +6425,6 @@ var JetstreamModule = class {
|
|
|
5982
6425
|
return new ConnectionProvider(options, eventBus);
|
|
5983
6426
|
}
|
|
5984
6427
|
},
|
|
5985
|
-
// JetstreamHealthIndicator — health check for NATS connection
|
|
5986
6428
|
{
|
|
5987
6429
|
provide: JetstreamHealthIndicator,
|
|
5988
6430
|
inject: [JETSTREAM_CONNECTION],
|
|
@@ -5990,7 +6432,6 @@ var JetstreamModule = class {
|
|
|
5990
6432
|
return new JetstreamHealthIndicator(connection);
|
|
5991
6433
|
}
|
|
5992
6434
|
},
|
|
5993
|
-
// ShutdownManager — graceful shutdown orchestration
|
|
5994
6435
|
{
|
|
5995
6436
|
provide: ShutdownManager,
|
|
5996
6437
|
inject: [JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS],
|
|
@@ -6002,41 +6443,69 @@ var JetstreamModule = class {
|
|
|
6002
6443
|
);
|
|
6003
6444
|
}
|
|
6004
6445
|
},
|
|
6005
|
-
// Consumer infrastructure
|
|
6006
|
-
//
|
|
6007
|
-
// 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.
|
|
6008
6448
|
{
|
|
6009
|
-
provide:
|
|
6449
|
+
provide: NameResolver,
|
|
6010
6450
|
inject: [JETSTREAM_OPTIONS],
|
|
6011
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) => {
|
|
6012
6469
|
if (options.consumer === false) return null;
|
|
6013
|
-
return new
|
|
6470
|
+
return new InfrastructureBinder(options, names, registry);
|
|
6014
6471
|
}
|
|
6015
6472
|
},
|
|
6016
|
-
// StreamProvider — JetStream stream lifecycle
|
|
6017
6473
|
{
|
|
6018
6474
|
provide: StreamProvider,
|
|
6019
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
6020
|
-
useFactory: (options, connection) => {
|
|
6475
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, NameResolver, InfrastructureBinder],
|
|
6476
|
+
useFactory: (options, connection, names, binder) => {
|
|
6021
6477
|
if (options.consumer === false) return null;
|
|
6022
|
-
return new StreamProvider(options, connection);
|
|
6478
|
+
return new StreamProvider(options, connection, names, binder);
|
|
6023
6479
|
}
|
|
6024
6480
|
},
|
|
6025
|
-
// ConsumerProvider
|
|
6481
|
+
// ConsumerProvider needs PatternRegistry for broadcast filtering.
|
|
6026
6482
|
{
|
|
6027
6483
|
provide: ConsumerProvider,
|
|
6028
|
-
inject: [
|
|
6029
|
-
|
|
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) => {
|
|
6030
6493
|
if (options.consumer === false) return null;
|
|
6031
|
-
return new ConsumerProvider(
|
|
6494
|
+
return new ConsumerProvider(
|
|
6495
|
+
options,
|
|
6496
|
+
connection,
|
|
6497
|
+
streamProvider,
|
|
6498
|
+
patternRegistry,
|
|
6499
|
+
names,
|
|
6500
|
+
binder
|
|
6501
|
+
);
|
|
6032
6502
|
}
|
|
6033
6503
|
},
|
|
6034
|
-
// Shared ack_wait map
|
|
6504
|
+
// Shared ack_wait map, populated by the strategy after ensureConsumers().
|
|
6035
6505
|
{
|
|
6036
6506
|
provide: JETSTREAM_ACK_WAIT_MAP,
|
|
6037
6507
|
useFactory: () => /* @__PURE__ */ new Map()
|
|
6038
6508
|
},
|
|
6039
|
-
// MessageProvider — pull-based message consumption
|
|
6040
6509
|
{
|
|
6041
6510
|
provide: MessageProvider,
|
|
6042
6511
|
inject: [
|
|
@@ -6075,7 +6544,6 @@ var JetstreamModule = class {
|
|
|
6075
6544
|
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
6076
6545
|
}
|
|
6077
6546
|
},
|
|
6078
|
-
// EventRouter — routes event and broadcast messages to handlers
|
|
6079
6547
|
{
|
|
6080
6548
|
provide: EventRouter,
|
|
6081
6549
|
inject: [
|
|
@@ -6085,9 +6553,10 @@ var JetstreamModule = class {
|
|
|
6085
6553
|
JETSTREAM_CODEC,
|
|
6086
6554
|
JETSTREAM_EVENT_BUS,
|
|
6087
6555
|
JETSTREAM_ACK_WAIT_MAP,
|
|
6088
|
-
JETSTREAM_CONNECTION
|
|
6556
|
+
JETSTREAM_CONNECTION,
|
|
6557
|
+
NameResolver
|
|
6089
6558
|
],
|
|
6090
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
6559
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection, names) => {
|
|
6091
6560
|
if (options.consumer === false) return null;
|
|
6092
6561
|
const deadLetterConfig = options.onDeadLetter || options.dlq ? {
|
|
6093
6562
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -6112,11 +6581,11 @@ var JetstreamModule = class {
|
|
|
6112
6581
|
processingConfig,
|
|
6113
6582
|
ackWaitMap,
|
|
6114
6583
|
connection,
|
|
6115
|
-
options
|
|
6584
|
+
options,
|
|
6585
|
+
names
|
|
6116
6586
|
);
|
|
6117
6587
|
}
|
|
6118
6588
|
},
|
|
6119
|
-
// RpcRouter — routes RPC command messages in JetStream mode
|
|
6120
6589
|
{
|
|
6121
6590
|
provide: RpcRouter,
|
|
6122
6591
|
inject: [
|
|
@@ -6147,7 +6616,6 @@ var JetstreamModule = class {
|
|
|
6147
6616
|
);
|
|
6148
6617
|
}
|
|
6149
6618
|
},
|
|
6150
|
-
// CoreRpcServer — RPC via NATS Core request/reply
|
|
6151
6619
|
{
|
|
6152
6620
|
provide: CoreRpcServer,
|
|
6153
6621
|
inject: [
|
|
@@ -6155,14 +6623,14 @@ var JetstreamModule = class {
|
|
|
6155
6623
|
JETSTREAM_CONNECTION,
|
|
6156
6624
|
PatternRegistry,
|
|
6157
6625
|
JETSTREAM_CODEC,
|
|
6158
|
-
JETSTREAM_EVENT_BUS
|
|
6626
|
+
JETSTREAM_EVENT_BUS,
|
|
6627
|
+
NameResolver
|
|
6159
6628
|
],
|
|
6160
|
-
useFactory: (options, connection, patternRegistry, codec, eventBus) => {
|
|
6629
|
+
useFactory: (options, connection, patternRegistry, codec, eventBus, names) => {
|
|
6161
6630
|
if (options.consumer === false) return null;
|
|
6162
|
-
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
6631
|
+
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus, names);
|
|
6163
6632
|
}
|
|
6164
6633
|
},
|
|
6165
|
-
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
6166
6634
|
{
|
|
6167
6635
|
provide: MetadataProvider,
|
|
6168
6636
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
@@ -6171,7 +6639,6 @@ var JetstreamModule = class {
|
|
|
6171
6639
|
return new MetadataProvider(options, connection);
|
|
6172
6640
|
}
|
|
6173
6641
|
},
|
|
6174
|
-
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
6175
6642
|
{
|
|
6176
6643
|
provide: JetstreamStrategy,
|
|
6177
6644
|
inject: [
|
|
@@ -6256,12 +6723,12 @@ var JetstreamModule = class {
|
|
|
6256
6723
|
}
|
|
6257
6724
|
};
|
|
6258
6725
|
JetstreamModule = __decorateClass([
|
|
6259
|
-
(0,
|
|
6260
|
-
(0,
|
|
6261
|
-
__decorateParam(0, (0,
|
|
6262
|
-
__decorateParam(0, (0,
|
|
6263
|
-
__decorateParam(1, (0,
|
|
6264
|
-
__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))
|
|
6265
6732
|
], JetstreamModule);
|
|
6266
6733
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6267
6734
|
0 && (module.exports = {
|
|
@@ -6297,6 +6764,7 @@ JetstreamModule = __decorateClass([
|
|
|
6297
6764
|
JetstreamTrace,
|
|
6298
6765
|
JsonCodec,
|
|
6299
6766
|
MIN_METADATA_TTL,
|
|
6767
|
+
ManagementMode,
|
|
6300
6768
|
MessageKind,
|
|
6301
6769
|
MsgpackCodec,
|
|
6302
6770
|
NatsErrorCode,
|