@horizon-republic/nestjs-jetstream 2.12.1 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1521 -1043
- package/dist/index.d.cts +291 -268
- package/dist/index.d.ts +291 -268
- package/dist/index.js +1458 -980
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,18 +14,17 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
14
14
|
import {
|
|
15
15
|
Global,
|
|
16
16
|
Inject as Inject2,
|
|
17
|
-
Logger as
|
|
17
|
+
Logger as Logger23,
|
|
18
18
|
Module as Module2,
|
|
19
19
|
Optional as Optional2
|
|
20
20
|
} from "@nestjs/common";
|
|
21
21
|
|
|
22
22
|
// src/client/jetstream.client.ts
|
|
23
|
-
import { Logger as
|
|
23
|
+
import { Logger as Logger6 } from "@nestjs/common";
|
|
24
24
|
import { ClientProxy } from "@nestjs/microservices";
|
|
25
25
|
import { context as context6 } from "@opentelemetry/api";
|
|
26
26
|
import { nuid } from "@nats-io/nuid";
|
|
27
27
|
import {
|
|
28
|
-
createInbox,
|
|
29
28
|
headers as natsHeaders,
|
|
30
29
|
TimeoutError
|
|
31
30
|
} from "@nats-io/transport-node";
|
|
@@ -53,6 +52,13 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
|
|
|
53
52
|
return TransportEvent2;
|
|
54
53
|
})(TransportEvent || {});
|
|
55
54
|
|
|
55
|
+
// src/interfaces/options.interface.ts
|
|
56
|
+
var ManagementMode = /* @__PURE__ */ ((ManagementMode2) => {
|
|
57
|
+
ManagementMode2["Auto"] = "auto";
|
|
58
|
+
ManagementMode2["Manual"] = "manual";
|
|
59
|
+
return ManagementMode2;
|
|
60
|
+
})(ManagementMode || {});
|
|
61
|
+
|
|
56
62
|
// src/interfaces/stream.interface.ts
|
|
57
63
|
var StreamKind = /* @__PURE__ */ ((StreamKind2) => {
|
|
58
64
|
StreamKind2["Event"] = "ev";
|
|
@@ -210,8 +216,27 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
|
210
216
|
]);
|
|
211
217
|
var NATS_CONTROL_HEADER_PREFIX = "nats-";
|
|
212
218
|
var internalName = (name) => `${name}__microservice`;
|
|
213
|
-
var
|
|
214
|
-
var
|
|
219
|
+
var subjectPrefix = (serviceName, kind) => `${internalName(serviceName)}.${kind}.`;
|
|
220
|
+
var buildSubject = (serviceName, kind, pattern) => `${subjectPrefix(serviceName, kind)}${pattern}`;
|
|
221
|
+
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
222
|
+
var SCHEDULE_SEGMENT = "_sch.";
|
|
223
|
+
var buildBroadcastSubject = (pattern) => `${BROADCAST_SUBJECT_PREFIX}${pattern}`;
|
|
224
|
+
var conventionScheduleSubjectBase = (targetName, eventSubject) => {
|
|
225
|
+
if (eventSubject.startsWith(BROADCAST_SUBJECT_PREFIX)) {
|
|
226
|
+
const bare = eventSubject.slice(BROADCAST_SUBJECT_PREFIX.length);
|
|
227
|
+
return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}${bare}`;
|
|
228
|
+
}
|
|
229
|
+
const targetPrefix = `${internalName(targetName)}.`;
|
|
230
|
+
if (!eventSubject.startsWith(targetPrefix)) {
|
|
231
|
+
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
232
|
+
}
|
|
233
|
+
const withoutPrefix = eventSubject.slice(targetPrefix.length);
|
|
234
|
+
const dotIndex = withoutPrefix.indexOf(".");
|
|
235
|
+
if (dotIndex === -1) {
|
|
236
|
+
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
237
|
+
}
|
|
238
|
+
return `${targetPrefix}${SCHEDULE_SEGMENT}${withoutPrefix.slice(dotIndex + 1)}`;
|
|
239
|
+
};
|
|
215
240
|
var streamName = (serviceName, kind) => {
|
|
216
241
|
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
217
242
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
@@ -231,6 +256,118 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
|
|
|
231
256
|
var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
|
|
232
257
|
var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
233
258
|
|
|
259
|
+
// src/server/infrastructure/management.ts
|
|
260
|
+
var kindOptionsBlock = (options, kind) => {
|
|
261
|
+
switch (kind) {
|
|
262
|
+
case "ev" /* Event */:
|
|
263
|
+
return options.events;
|
|
264
|
+
case "broadcast" /* Broadcast */:
|
|
265
|
+
return options.broadcast;
|
|
266
|
+
case "ordered" /* Ordered */:
|
|
267
|
+
return options.ordered;
|
|
268
|
+
case "cmd" /* Command */:
|
|
269
|
+
return isJetStreamRpcMode(options.rpc) ? options.rpc : void 0;
|
|
270
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
271
|
+
default: {
|
|
272
|
+
const _exhaustive = kind;
|
|
273
|
+
throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var entityManagementFor = (options, kind) => {
|
|
278
|
+
if (kind === "dlq") return options.dlq?.management;
|
|
279
|
+
return kindOptionsBlock(options, kind)?.management;
|
|
280
|
+
};
|
|
281
|
+
var resolveManagementMode = (options, kind, entity) => entityManagementFor(options, kind)?.[entity] ?? options.provisioning?.management ?? "auto" /* Auto */;
|
|
282
|
+
|
|
283
|
+
// src/server/infrastructure/name-resolver.ts
|
|
284
|
+
var NameResolver = class {
|
|
285
|
+
constructor(options) {
|
|
286
|
+
this.options = options;
|
|
287
|
+
this.dlq = options.dlq?.stream?.name ?? dlqStreamName(options.name);
|
|
288
|
+
this.kinds = this.buildKindMap();
|
|
289
|
+
}
|
|
290
|
+
kinds;
|
|
291
|
+
dlq;
|
|
292
|
+
streamName(kind) {
|
|
293
|
+
return this.get(kind).stream;
|
|
294
|
+
}
|
|
295
|
+
consumerName(kind) {
|
|
296
|
+
return this.get(kind).consumer;
|
|
297
|
+
}
|
|
298
|
+
dlqStreamName() {
|
|
299
|
+
return this.dlq;
|
|
300
|
+
}
|
|
301
|
+
subject(kind, pattern) {
|
|
302
|
+
return `${this.get(kind).prefix}${pattern}`;
|
|
303
|
+
}
|
|
304
|
+
filterSubject(kind) {
|
|
305
|
+
return `${this.get(kind).prefix}>`;
|
|
306
|
+
}
|
|
307
|
+
schedulePrefix(kind) {
|
|
308
|
+
return this.get(kind).schedulePrefix;
|
|
309
|
+
}
|
|
310
|
+
hasCustomPrefix(kind) {
|
|
311
|
+
return this.get(kind).custom;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Map a resolved event subject back to its schedule-holder base subject
|
|
315
|
+
* (the `_sch` namespace twin, without the per-message unique suffix).
|
|
316
|
+
*/
|
|
317
|
+
scheduleSubjectBase(eventSubject) {
|
|
318
|
+
for (const kind of ["broadcast" /* Broadcast */, "ev" /* Event */, "ordered" /* Ordered */]) {
|
|
319
|
+
const { prefix, schedulePrefix } = this.get(kind);
|
|
320
|
+
if (eventSubject.startsWith(prefix)) {
|
|
321
|
+
return `${schedulePrefix}${eventSubject.slice(prefix.length)}`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
325
|
+
}
|
|
326
|
+
get(kind) {
|
|
327
|
+
const entry = this.kinds.get(kind);
|
|
328
|
+
if (!entry) throw new Error(`Unknown StreamKind: ${String(kind)}`);
|
|
329
|
+
return entry;
|
|
330
|
+
}
|
|
331
|
+
buildKindMap() {
|
|
332
|
+
const map = /* @__PURE__ */ new Map();
|
|
333
|
+
const { name } = this.options;
|
|
334
|
+
for (const kind of Object.values(StreamKind)) {
|
|
335
|
+
const block = kindOptionsBlock(this.options, kind);
|
|
336
|
+
const customPrefix = block?.subjectPrefix;
|
|
337
|
+
const custom = customPrefix !== void 0;
|
|
338
|
+
const prefix = custom ? this.normalizePrefix(customPrefix) : this.conventionPrefix(name, kind);
|
|
339
|
+
map.set(kind, {
|
|
340
|
+
stream: block?.stream?.name ?? streamName(name, kind),
|
|
341
|
+
consumer: block?.consumer?.durable_name ?? consumerName(name, kind),
|
|
342
|
+
prefix,
|
|
343
|
+
schedulePrefix: custom ? `${prefix}${SCHEDULE_SEGMENT}` : this.conventionSchedulePrefix(name, kind),
|
|
344
|
+
custom
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return map;
|
|
348
|
+
}
|
|
349
|
+
normalizePrefix(raw) {
|
|
350
|
+
let end = raw.length;
|
|
351
|
+
while (end > 0 && raw[end - 1] === ".") end -= 1;
|
|
352
|
+
const trimmed = raw.slice(0, end);
|
|
353
|
+
const tokens = trimmed.split(".");
|
|
354
|
+
if (trimmed.length === 0 || tokens.some((t) => t.length === 0 || t === ">")) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Invalid subjectPrefix "${raw}": expected non-empty dot-separated tokens without ">".`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return `${trimmed}.`;
|
|
360
|
+
}
|
|
361
|
+
conventionPrefix(name, kind) {
|
|
362
|
+
if (kind === "broadcast" /* Broadcast */) return BROADCAST_SUBJECT_PREFIX;
|
|
363
|
+
return subjectPrefix(name, kind);
|
|
364
|
+
}
|
|
365
|
+
conventionSchedulePrefix(name, kind) {
|
|
366
|
+
if (kind === "broadcast" /* Broadcast */) return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}`;
|
|
367
|
+
return `${internalName(name)}.${SCHEDULE_SEGMENT}`;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
234
371
|
// src/otel/constants.ts
|
|
235
372
|
var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
|
|
236
373
|
|
|
@@ -533,7 +670,7 @@ var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier,
|
|
|
533
670
|
|
|
534
671
|
// src/otel/tracer.ts
|
|
535
672
|
import { trace } from "@opentelemetry/api";
|
|
536
|
-
var PACKAGE_VERSION = true ? "2.
|
|
673
|
+
var PACKAGE_VERSION = true ? "2.13.0" : "0.0.0";
|
|
537
674
|
var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
|
|
538
675
|
|
|
539
676
|
// src/otel/carrier.ts
|
|
@@ -1337,17 +1474,137 @@ var nanosToGoDuration = (nanos) => {
|
|
|
1337
1474
|
return `${nanos}ns`;
|
|
1338
1475
|
};
|
|
1339
1476
|
|
|
1477
|
+
// src/client/rpc-reply-inbox.ts
|
|
1478
|
+
import { Logger as Logger5 } from "@nestjs/common";
|
|
1479
|
+
import {
|
|
1480
|
+
createInbox
|
|
1481
|
+
} from "@nats-io/transport-node";
|
|
1482
|
+
var RpcReplyInbox = class {
|
|
1483
|
+
constructor(codec, inboxPrefix) {
|
|
1484
|
+
this.codec = codec;
|
|
1485
|
+
this.inboxPrefix = inboxPrefix;
|
|
1486
|
+
}
|
|
1487
|
+
logger = new Logger5("Jetstream:RpcInbox");
|
|
1488
|
+
inbox = null;
|
|
1489
|
+
subscription = null;
|
|
1490
|
+
pending = /* @__PURE__ */ new Map();
|
|
1491
|
+
timeouts = /* @__PURE__ */ new Map();
|
|
1492
|
+
/** Reply-to subject for outgoing requests; null until {@link setup} ran. */
|
|
1493
|
+
get address() {
|
|
1494
|
+
return this.inbox;
|
|
1495
|
+
}
|
|
1496
|
+
/** True while the inbox subscription is live. */
|
|
1497
|
+
get active() {
|
|
1498
|
+
return this.subscription !== null;
|
|
1499
|
+
}
|
|
1500
|
+
/** Create the inbox subject and subscribe reply routing on it. */
|
|
1501
|
+
setup(nc) {
|
|
1502
|
+
this.inbox = createInbox(this.inboxPrefix);
|
|
1503
|
+
this.subscription = nc.subscribe(this.inbox, {
|
|
1504
|
+
callback: (err, msg) => {
|
|
1505
|
+
if (err) {
|
|
1506
|
+
this.logger.error("Inbox subscription error:", err);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
this.route(msg);
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
this.logger.debug(`Inbox subscription: ${this.inbox}`);
|
|
1513
|
+
}
|
|
1514
|
+
/** Register the callback that settles the round-trip for `correlationId`. */
|
|
1515
|
+
register(correlationId, callback) {
|
|
1516
|
+
this.pending.set(correlationId, callback);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Arm the request deadline. When it fires while the request is still
|
|
1520
|
+
* pending, both registry entries are removed before `onExpired` runs.
|
|
1521
|
+
*/
|
|
1522
|
+
armTimeout(correlationId, ms, onExpired) {
|
|
1523
|
+
const existing = this.timeouts.get(correlationId);
|
|
1524
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
1525
|
+
const timeoutId = setTimeout(() => {
|
|
1526
|
+
if (!this.pending.has(correlationId)) return;
|
|
1527
|
+
this.timeouts.delete(correlationId);
|
|
1528
|
+
this.pending.delete(correlationId);
|
|
1529
|
+
onExpired();
|
|
1530
|
+
}, ms);
|
|
1531
|
+
this.timeouts.set(correlationId, timeoutId);
|
|
1532
|
+
}
|
|
1533
|
+
/** True while the request has not been settled or discarded. */
|
|
1534
|
+
has(correlationId) {
|
|
1535
|
+
return this.pending.has(correlationId);
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Drop a request without invoking its callback: clears the deadline and
|
|
1539
|
+
* returns whether the request was still pending.
|
|
1540
|
+
*/
|
|
1541
|
+
discard(correlationId) {
|
|
1542
|
+
const timeoutId = this.timeouts.get(correlationId);
|
|
1543
|
+
if (timeoutId !== void 0) {
|
|
1544
|
+
clearTimeout(timeoutId);
|
|
1545
|
+
this.timeouts.delete(correlationId);
|
|
1546
|
+
}
|
|
1547
|
+
return this.pending.delete(correlationId);
|
|
1548
|
+
}
|
|
1549
|
+
/** Fail every pending request with `error` and tear the inbox down. */
|
|
1550
|
+
rejectAll(error) {
|
|
1551
|
+
for (const callback of this.pending.values()) {
|
|
1552
|
+
callback({ err: error, response: null, isDisposed: true });
|
|
1553
|
+
}
|
|
1554
|
+
for (const timeoutId of this.timeouts.values()) {
|
|
1555
|
+
clearTimeout(timeoutId);
|
|
1556
|
+
}
|
|
1557
|
+
this.pending.clear();
|
|
1558
|
+
this.timeouts.clear();
|
|
1559
|
+
this.subscription?.unsubscribe();
|
|
1560
|
+
this.subscription = null;
|
|
1561
|
+
this.inbox = null;
|
|
1562
|
+
}
|
|
1563
|
+
/** Route an inbox reply to the matching pending callback. */
|
|
1564
|
+
route(msg) {
|
|
1565
|
+
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
1566
|
+
if (!correlationId) {
|
|
1567
|
+
this.logger.warn("Inbox reply without correlation-id, ignoring");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const callback = this.pending.get(correlationId);
|
|
1571
|
+
if (!callback) {
|
|
1572
|
+
this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const timeoutId = this.timeouts.get(correlationId);
|
|
1576
|
+
if (timeoutId) {
|
|
1577
|
+
clearTimeout(timeoutId);
|
|
1578
|
+
this.timeouts.delete(correlationId);
|
|
1579
|
+
}
|
|
1580
|
+
try {
|
|
1581
|
+
const decoded = this.codec.decode(msg.data);
|
|
1582
|
+
if (msg.headers?.get("x-error" /* Error */)) {
|
|
1583
|
+
callback({ err: decoded, response: null, isDisposed: true });
|
|
1584
|
+
} else {
|
|
1585
|
+
callback({ err: null, response: decoded, isDisposed: true });
|
|
1586
|
+
}
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
callback({
|
|
1589
|
+
err: err instanceof Error ? err : new Error("Decode error"),
|
|
1590
|
+
response: null,
|
|
1591
|
+
isDisposed: true
|
|
1592
|
+
});
|
|
1593
|
+
} finally {
|
|
1594
|
+
this.pending.delete(correlationId);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1340
1599
|
// src/client/jetstream.client.ts
|
|
1341
|
-
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
1342
1600
|
var detectEventKind = (pattern) => {
|
|
1343
1601
|
if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
|
|
1344
1602
|
if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
|
|
1345
1603
|
return "event" /* Event */;
|
|
1346
1604
|
};
|
|
1347
|
-
var declaredEventPattern = (pattern) => {
|
|
1348
|
-
if (pattern.
|
|
1349
|
-
|
|
1350
|
-
if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
|
|
1605
|
+
var declaredEventPattern = (pattern, kind) => {
|
|
1606
|
+
if (kind === "broadcast" /* Broadcast */) return pattern.slice("broadcast:" /* Broadcast */.length);
|
|
1607
|
+
if (kind === "ordered" /* Ordered */) return pattern.slice("ordered:" /* Ordered */.length);
|
|
1351
1608
|
return pattern;
|
|
1352
1609
|
};
|
|
1353
1610
|
var eventStreamKind = (kind) => {
|
|
@@ -1356,7 +1613,7 @@ var eventStreamKind = (kind) => {
|
|
|
1356
1613
|
return "ev" /* Event */;
|
|
1357
1614
|
};
|
|
1358
1615
|
var JetstreamClient = class extends ClientProxy {
|
|
1359
|
-
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
1616
|
+
constructor(rootOptions, targetServiceName, connection, codec, eventBus, names) {
|
|
1360
1617
|
super();
|
|
1361
1618
|
this.rootOptions = rootOptions;
|
|
1362
1619
|
this.connection = connection;
|
|
@@ -1364,29 +1621,34 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1364
1621
|
this.eventBus = eventBus;
|
|
1365
1622
|
this.targetName = targetServiceName;
|
|
1366
1623
|
this.callerName = internalName(this.rootOptions.name);
|
|
1624
|
+
const isSelf = targetServiceName === rootOptions.name;
|
|
1625
|
+
const ownNames = names ?? (isSelf ? new NameResolver(rootOptions) : null);
|
|
1626
|
+
this.selfNames = isSelf ? ownNames : null;
|
|
1627
|
+
this.broadcastPrefix = ownNames ? ownNames.subject("broadcast" /* Broadcast */, "") : BROADCAST_SUBJECT_PREFIX;
|
|
1367
1628
|
const targetInternal = internalName(targetServiceName);
|
|
1368
1629
|
this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
|
|
1369
1630
|
this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
|
|
1370
1631
|
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
1632
|
+
this.rpcInbox = new RpcReplyInbox(codec, this.callerName);
|
|
1371
1633
|
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
1372
|
-
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc
|
|
1634
|
+
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
1373
1635
|
const derived = deriveOtelAttrs(this.rootOptions);
|
|
1374
1636
|
this.otel = derived.otel;
|
|
1375
1637
|
this.serverEndpoint = derived.serverEndpoint;
|
|
1376
1638
|
}
|
|
1377
|
-
logger = new
|
|
1639
|
+
logger = new Logger6("Jetstream:Client");
|
|
1378
1640
|
/** Target service name this client sends messages to. */
|
|
1379
1641
|
targetName;
|
|
1380
1642
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
1381
1643
|
callerName;
|
|
1382
|
-
/**
|
|
1383
|
-
* Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
|
|
1384
|
-
* per stream kind this client may publish to. Built once in the constructor
|
|
1385
|
-
* so producing a full subject is a single string concat with the user pattern.
|
|
1386
|
-
*/
|
|
1644
|
+
/** Convention subject prefixes for foreign targets; one per stream kind. */
|
|
1387
1645
|
eventSubjectPrefix;
|
|
1388
1646
|
commandSubjectPrefix;
|
|
1389
1647
|
orderedSubjectPrefix;
|
|
1648
|
+
/** Broadcast subject prefix derived from the own resolver; never null. */
|
|
1649
|
+
broadcastPrefix;
|
|
1650
|
+
/** Resolver for self-target subjects; null when targeting a foreign service. */
|
|
1651
|
+
selfNames;
|
|
1390
1652
|
/**
|
|
1391
1653
|
* RPC configuration snapshots. The values are derived from rootOptions at
|
|
1392
1654
|
* construction time so the publish hot path never has to re-run
|
|
@@ -1398,13 +1660,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1398
1660
|
otel;
|
|
1399
1661
|
/** Server endpoint parts used for `server.address` / `server.port` span attributes. */
|
|
1400
1662
|
serverEndpoint;
|
|
1401
|
-
/**
|
|
1402
|
-
|
|
1403
|
-
inboxSubscription = null;
|
|
1404
|
-
/** Pending JetStream-mode RPC callbacks, keyed by correlation ID. */
|
|
1405
|
-
pendingMessages = /* @__PURE__ */ new Map();
|
|
1406
|
-
/** Pending JetStream-mode RPC timeouts, keyed by correlation ID. */
|
|
1407
|
-
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
1663
|
+
/** Reply inbox and pending-request registry for JetStream-mode RPC. */
|
|
1664
|
+
rpcInbox;
|
|
1408
1665
|
/** Subscription to connection status events for disconnect handling. */
|
|
1409
1666
|
statusSubscription = null;
|
|
1410
1667
|
/**
|
|
@@ -1423,8 +1680,8 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1423
1680
|
*/
|
|
1424
1681
|
async connect() {
|
|
1425
1682
|
const nc = await this.connection.getConnection();
|
|
1426
|
-
if (!this.isCoreMode && !this.
|
|
1427
|
-
this.
|
|
1683
|
+
if (!this.isCoreMode && !this.rpcInbox.active) {
|
|
1684
|
+
this.rpcInbox.setup(nc);
|
|
1428
1685
|
}
|
|
1429
1686
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
1430
1687
|
if (status.type === "disconnect") {
|
|
@@ -1439,7 +1696,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1439
1696
|
this.statusSubscription?.unsubscribe();
|
|
1440
1697
|
this.statusSubscription = null;
|
|
1441
1698
|
this.readyForPublish = false;
|
|
1442
|
-
this.
|
|
1699
|
+
this.rpcInbox.rejectAll(new Error("Client closed"));
|
|
1443
1700
|
}
|
|
1444
1701
|
/**
|
|
1445
1702
|
* Direct access to the raw NATS connection.
|
|
@@ -1449,7 +1706,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1449
1706
|
unwrap() {
|
|
1450
1707
|
const nc = this.connection.unwrap;
|
|
1451
1708
|
if (!nc) {
|
|
1452
|
-
throw new Error("Not connected
|
|
1709
|
+
throw new Error("Not connected; call connect() before unwrap()");
|
|
1453
1710
|
}
|
|
1454
1711
|
return nc;
|
|
1455
1712
|
}
|
|
@@ -1476,7 +1733,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1476
1733
|
const encoded = this.codec.encode(data);
|
|
1477
1734
|
const effectiveMsgId = messageId ?? nuid.next();
|
|
1478
1735
|
const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
|
|
1479
|
-
const declaredPattern = declaredEventPattern(packet.pattern);
|
|
1736
|
+
const declaredPattern = declaredEventPattern(packet.pattern, publishKind);
|
|
1480
1737
|
const streamKind = eventStreamKind(publishKind);
|
|
1481
1738
|
const startedAt = performance.now();
|
|
1482
1739
|
try {
|
|
@@ -1496,13 +1753,6 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1496
1753
|
},
|
|
1497
1754
|
this.otel,
|
|
1498
1755
|
async () => {
|
|
1499
|
-
const warnIfDuplicate = (kindLabel, ack2) => {
|
|
1500
|
-
if (ack2.duplicate) {
|
|
1501
|
-
this.logger.warn(
|
|
1502
|
-
`Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
};
|
|
1506
1756
|
if (schedule) {
|
|
1507
1757
|
const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1508
1758
|
headers: msgHeaders,
|
|
@@ -1513,7 +1763,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1513
1763
|
ttl
|
|
1514
1764
|
}
|
|
1515
1765
|
});
|
|
1516
|
-
warnIfDuplicate("scheduled", ack2);
|
|
1766
|
+
this.warnIfDuplicate("scheduled", publishSubject, ack2);
|
|
1517
1767
|
return;
|
|
1518
1768
|
}
|
|
1519
1769
|
const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
@@ -1521,7 +1771,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1521
1771
|
msgID: effectiveMsgId,
|
|
1522
1772
|
ttl
|
|
1523
1773
|
});
|
|
1524
|
-
warnIfDuplicate("event", ack);
|
|
1774
|
+
this.warnIfDuplicate("event", publishSubject, ack);
|
|
1525
1775
|
}
|
|
1526
1776
|
);
|
|
1527
1777
|
this.reportPublished(declaredPattern, streamKind, startedAt, "success");
|
|
@@ -1538,7 +1788,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1538
1788
|
* JetStream mode: publishes to stream + waits for inbox response.
|
|
1539
1789
|
*/
|
|
1540
1790
|
publish(packet, callback) {
|
|
1541
|
-
const subject = this.commandSubjectPrefix + packet.pattern;
|
|
1791
|
+
const subject = this.selfNames ? this.selfNames.subject("cmd" /* Command */, packet.pattern) : this.commandSubjectPrefix + packet.pattern;
|
|
1542
1792
|
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
1543
1793
|
if (schedule) {
|
|
1544
1794
|
this.logger.warn(
|
|
@@ -1571,12 +1821,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1571
1821
|
}
|
|
1572
1822
|
return () => {
|
|
1573
1823
|
if (jetStreamCorrelationId) {
|
|
1574
|
-
|
|
1575
|
-
if (timeoutId) {
|
|
1576
|
-
clearTimeout(timeoutId);
|
|
1577
|
-
this.pendingTimeouts.delete(jetStreamCorrelationId);
|
|
1578
|
-
}
|
|
1579
|
-
this.pendingMessages.delete(jetStreamCorrelationId);
|
|
1824
|
+
this.rpcInbox.discard(jetStreamCorrelationId);
|
|
1580
1825
|
}
|
|
1581
1826
|
};
|
|
1582
1827
|
}
|
|
@@ -1641,7 +1886,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1641
1886
|
const hdrs = this.buildHeaders(customHeaders, {
|
|
1642
1887
|
subject,
|
|
1643
1888
|
correlationId,
|
|
1644
|
-
replyTo: this.
|
|
1889
|
+
replyTo: this.rpcInbox.address ?? ""
|
|
1645
1890
|
});
|
|
1646
1891
|
const encoded = this.codec.encode(data);
|
|
1647
1892
|
const spanHandle = beginRpcClientSpan(
|
|
@@ -1658,7 +1903,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1658
1903
|
this.otel
|
|
1659
1904
|
);
|
|
1660
1905
|
const startedAt = performance.now();
|
|
1661
|
-
|
|
1906
|
+
const settleRoundTrip = (packet) => {
|
|
1662
1907
|
if (packet.err) {
|
|
1663
1908
|
if (packet.err instanceof Error) {
|
|
1664
1909
|
spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
|
|
@@ -1671,30 +1916,25 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1671
1916
|
this.reportRpcCompleted(declaredPattern, startedAt, "success");
|
|
1672
1917
|
}
|
|
1673
1918
|
callback(packet);
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1678
|
-
this.pendingMessages.delete(correlationId);
|
|
1919
|
+
};
|
|
1920
|
+
this.rpcInbox.register(correlationId, settleRoundTrip);
|
|
1921
|
+
this.rpcInbox.armTimeout(correlationId, effectiveTimeout, () => {
|
|
1679
1922
|
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1680
1923
|
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
1681
1924
|
this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
|
|
1682
1925
|
callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
|
|
1683
|
-
}
|
|
1684
|
-
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
1926
|
+
});
|
|
1685
1927
|
try {
|
|
1686
1928
|
if (!this.readyForPublish) await this.connect();
|
|
1687
|
-
if (!this.
|
|
1688
|
-
if (!this.
|
|
1689
|
-
|
|
1690
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1691
|
-
this.pendingMessages.delete(correlationId);
|
|
1929
|
+
if (!this.rpcInbox.has(correlationId)) return;
|
|
1930
|
+
if (!this.rpcInbox.address) {
|
|
1931
|
+
this.rpcInbox.discard(correlationId);
|
|
1692
1932
|
const inboxError = new Error("Inbox not initialized");
|
|
1693
1933
|
spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
|
|
1694
1934
|
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1695
1935
|
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
1696
1936
|
callback({
|
|
1697
|
-
err: new Error("Inbox not initialized
|
|
1937
|
+
err: new Error("Inbox not initialized; JetStream RPC mode requires a connected inbox"),
|
|
1698
1938
|
response: null,
|
|
1699
1939
|
isDisposed: true
|
|
1700
1940
|
});
|
|
@@ -1714,13 +1954,7 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1714
1954
|
}
|
|
1715
1955
|
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1716
1956
|
} catch (err) {
|
|
1717
|
-
|
|
1718
|
-
if (existingTimeout) {
|
|
1719
|
-
clearTimeout(existingTimeout);
|
|
1720
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1721
|
-
}
|
|
1722
|
-
if (!this.pendingMessages.has(correlationId)) return;
|
|
1723
|
-
this.pendingMessages.delete(correlationId);
|
|
1957
|
+
if (!this.rpcInbox.discard(correlationId)) return;
|
|
1724
1958
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
1725
1959
|
spanHandle.finish({ kind: "error" /* Error */, error });
|
|
1726
1960
|
this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
|
|
@@ -1729,6 +1963,11 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1729
1963
|
callback({ err: error, response: null, isDisposed: true });
|
|
1730
1964
|
}
|
|
1731
1965
|
}
|
|
1966
|
+
warnIfDuplicate(kindLabel, subject, ack) {
|
|
1967
|
+
if (ack.duplicate) {
|
|
1968
|
+
this.logger.warn(`Duplicate ${kindLabel} publish detected: ${subject} (seq: ${ack.seq})`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1732
1971
|
// hasHook is per-emit so late subscribers (JetstreamMetricsService during
|
|
1733
1972
|
// OnApplicationBootstrap) still receive events.
|
|
1734
1973
|
reportPublished(declaredPattern, kind, startedAt, status) {
|
|
@@ -1752,87 +1991,23 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1752
1991
|
}
|
|
1753
1992
|
/** Fail-fast all pending JetStream RPC callbacks on connection loss. */
|
|
1754
1993
|
handleDisconnect() {
|
|
1755
|
-
this.
|
|
1756
|
-
this.inbox = null;
|
|
1994
|
+
this.rpcInbox.rejectAll(new Error("Connection lost"));
|
|
1757
1995
|
this.readyForPublish = false;
|
|
1758
1996
|
}
|
|
1759
|
-
/** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
|
|
1760
|
-
rejectPendingRpcs(error) {
|
|
1761
|
-
for (const callback of this.pendingMessages.values()) {
|
|
1762
|
-
callback({ err: error, response: null, isDisposed: true });
|
|
1763
|
-
}
|
|
1764
|
-
for (const timeoutId of this.pendingTimeouts.values()) {
|
|
1765
|
-
clearTimeout(timeoutId);
|
|
1766
|
-
}
|
|
1767
|
-
this.pendingMessages.clear();
|
|
1768
|
-
this.pendingTimeouts.clear();
|
|
1769
|
-
this.inboxSubscription?.unsubscribe();
|
|
1770
|
-
this.inboxSubscription = null;
|
|
1771
|
-
this.inbox = null;
|
|
1772
|
-
}
|
|
1773
|
-
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
1774
|
-
setupInbox(nc) {
|
|
1775
|
-
this.inbox = createInbox(internalName(this.rootOptions.name));
|
|
1776
|
-
this.inboxSubscription = nc.subscribe(this.inbox, {
|
|
1777
|
-
callback: (err, msg) => {
|
|
1778
|
-
if (err) {
|
|
1779
|
-
this.logger.error("Inbox subscription error:", err);
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
this.routeInboxReply(msg);
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
1785
|
-
this.logger.debug(`Inbox subscription: ${this.inbox}`);
|
|
1786
|
-
}
|
|
1787
|
-
/** Route an inbox reply to the matching pending callback. */
|
|
1788
|
-
routeInboxReply(msg) {
|
|
1789
|
-
const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
|
|
1790
|
-
if (!correlationId) {
|
|
1791
|
-
this.logger.warn("Inbox reply without correlation-id, ignoring");
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
const callback = this.pendingMessages.get(correlationId);
|
|
1795
|
-
if (!callback) {
|
|
1796
|
-
this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
const timeoutId = this.pendingTimeouts.get(correlationId);
|
|
1800
|
-
if (timeoutId) {
|
|
1801
|
-
clearTimeout(timeoutId);
|
|
1802
|
-
this.pendingTimeouts.delete(correlationId);
|
|
1803
|
-
}
|
|
1804
|
-
try {
|
|
1805
|
-
const decoded = this.codec.decode(msg.data);
|
|
1806
|
-
if (msg.headers?.get("x-error" /* Error */)) {
|
|
1807
|
-
callback({ err: decoded, response: null, isDisposed: true });
|
|
1808
|
-
} else {
|
|
1809
|
-
callback({ err: null, response: decoded, isDisposed: true });
|
|
1810
|
-
}
|
|
1811
|
-
} catch (err) {
|
|
1812
|
-
callback({
|
|
1813
|
-
err: err instanceof Error ? err : new Error("Decode error"),
|
|
1814
|
-
response: null,
|
|
1815
|
-
isDisposed: true
|
|
1816
|
-
});
|
|
1817
|
-
} finally {
|
|
1818
|
-
this.pendingMessages.delete(correlationId);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
1997
|
/**
|
|
1822
|
-
* Resolve a user pattern to a fully-qualified NATS subject
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
* The leading-char check short-circuits the `startsWith` comparisons for
|
|
1826
|
-
* patterns that cannot possibly carry a broadcast/ordered marker, which is
|
|
1827
|
-
* the overwhelmingly common case.
|
|
1998
|
+
* Resolve a user pattern to a fully-qualified NATS subject. Self-targets go
|
|
1999
|
+
* through the resolver so custom subjectPrefix options are honoured.
|
|
1828
2000
|
*/
|
|
1829
2001
|
buildEventSubject(pattern) {
|
|
1830
|
-
if (pattern.
|
|
1831
|
-
return
|
|
2002
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */)) {
|
|
2003
|
+
return this.broadcastPrefix + pattern.slice("broadcast:" /* Broadcast */.length);
|
|
1832
2004
|
}
|
|
1833
|
-
if (pattern.
|
|
1834
|
-
|
|
2005
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) {
|
|
2006
|
+
const bare = pattern.slice("ordered:" /* Ordered */.length);
|
|
2007
|
+
if (this.selfNames) return this.selfNames.subject("ordered" /* Ordered */, bare);
|
|
2008
|
+
return this.orderedSubjectPrefix + bare;
|
|
1835
2009
|
}
|
|
2010
|
+
if (this.selfNames) return this.selfNames.subject("ev" /* Event */, pattern);
|
|
1836
2011
|
return this.eventSubjectPrefix + pattern;
|
|
1837
2012
|
}
|
|
1838
2013
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
@@ -1877,33 +2052,16 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
1877
2052
|
/**
|
|
1878
2053
|
* Build a schedule-holder subject for NATS message scheduling.
|
|
1879
2054
|
*
|
|
1880
|
-
* The
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
* concurrent schedules of the same pattern would silently replace each other.
|
|
1887
|
-
*
|
|
1888
|
-
* Examples:
|
|
1889
|
-
* - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
|
|
1890
|
-
* - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
|
|
2055
|
+
* The holder lives in the same stream as the target but under the `_sch`
|
|
2056
|
+
* namespace no consumer filter matches; the server publishes it to the
|
|
2057
|
+
* target subject when the schedule fires. The unique per-message suffix
|
|
2058
|
+
* exists because the server stores schedules as rollup messages, one
|
|
2059
|
+
* active schedule per subject (ADR-51): without it, concurrent schedules
|
|
2060
|
+
* of the same pattern would silently replace each other.
|
|
1891
2061
|
*/
|
|
1892
2062
|
buildScheduleSubject(eventSubject) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1896
|
-
const targetPrefix = `${internalName(this.targetName)}.`;
|
|
1897
|
-
if (!eventSubject.startsWith(targetPrefix)) {
|
|
1898
|
-
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
1899
|
-
}
|
|
1900
|
-
const withoutPrefix = eventSubject.slice(targetPrefix.length);
|
|
1901
|
-
const dotIndex = withoutPrefix.indexOf(".");
|
|
1902
|
-
if (dotIndex === -1) {
|
|
1903
|
-
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
1904
|
-
}
|
|
1905
|
-
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
1906
|
-
return `${targetPrefix}_sch.${pattern}.${nuid.next()}`;
|
|
2063
|
+
const base = this.selfNames ? this.selfNames.scheduleSubjectBase(eventSubject) : conventionScheduleSubjectBase(this.targetName, eventSubject);
|
|
2064
|
+
return `${base}.${nuid.next()}`;
|
|
1907
2065
|
}
|
|
1908
2066
|
};
|
|
1909
2067
|
|
|
@@ -1935,7 +2093,7 @@ var MsgpackCodec = class {
|
|
|
1935
2093
|
};
|
|
1936
2094
|
|
|
1937
2095
|
// src/connection/connection.provider.ts
|
|
1938
|
-
import { Logger as
|
|
2096
|
+
import { Logger as Logger7 } from "@nestjs/common";
|
|
1939
2097
|
import {
|
|
1940
2098
|
connect
|
|
1941
2099
|
} from "@nats-io/transport-node";
|
|
@@ -1968,7 +2126,7 @@ var ConnectionProvider = class {
|
|
|
1968
2126
|
nc$;
|
|
1969
2127
|
/** Live stream of connection status events (no replay). */
|
|
1970
2128
|
status$;
|
|
1971
|
-
logger = new
|
|
2129
|
+
logger = new Logger7("Jetstream:Connection");
|
|
1972
2130
|
connection = null;
|
|
1973
2131
|
connectionPromise = null;
|
|
1974
2132
|
jsClient = null;
|
|
@@ -1979,7 +2137,7 @@ var ConnectionProvider = class {
|
|
|
1979
2137
|
otelEndpoint;
|
|
1980
2138
|
lifecycleSpan = null;
|
|
1981
2139
|
/**
|
|
1982
|
-
* Establish NATS connection. Idempotent
|
|
2140
|
+
* Establish NATS connection. Idempotent: returns cached connection on subsequent calls.
|
|
1983
2141
|
*
|
|
1984
2142
|
* @throws Error if connection is refused (fail fast).
|
|
1985
2143
|
*/
|
|
@@ -2018,7 +2176,7 @@ var ConnectionProvider = class {
|
|
|
2018
2176
|
*/
|
|
2019
2177
|
getJetStreamClient() {
|
|
2020
2178
|
if (!this.connection || this.connection.isClosed()) {
|
|
2021
|
-
throw new Error("Not connected
|
|
2179
|
+
throw new Error("Not connected; call getConnection() before getJetStreamClient()");
|
|
2022
2180
|
}
|
|
2023
2181
|
this.jsClient ??= jetstream(this.connection);
|
|
2024
2182
|
return this.jsClient;
|
|
@@ -2030,7 +2188,7 @@ var ConnectionProvider = class {
|
|
|
2030
2188
|
/**
|
|
2031
2189
|
* Gracefully shut down the connection.
|
|
2032
2190
|
*
|
|
2033
|
-
* Sequence: drain
|
|
2191
|
+
* Sequence: drain -> wait for close. Falls back to force-close on error.
|
|
2034
2192
|
*/
|
|
2035
2193
|
async shutdown() {
|
|
2036
2194
|
if (this.connectionPromise) {
|
|
@@ -2233,12 +2391,12 @@ var EventBus = class {
|
|
|
2233
2391
|
};
|
|
2234
2392
|
|
|
2235
2393
|
// src/health/jetstream.health-indicator.ts
|
|
2236
|
-
import { Injectable, Logger as
|
|
2394
|
+
import { Injectable, Logger as Logger8 } from "@nestjs/common";
|
|
2237
2395
|
var JetstreamHealthIndicator = class {
|
|
2238
2396
|
constructor(connection) {
|
|
2239
2397
|
this.connection = connection;
|
|
2240
2398
|
}
|
|
2241
|
-
logger = new
|
|
2399
|
+
logger = new Logger8("Jetstream:Health");
|
|
2242
2400
|
/**
|
|
2243
2401
|
* Plain health status check.
|
|
2244
2402
|
*
|
|
@@ -2268,7 +2426,7 @@ var JetstreamHealthIndicator = class {
|
|
|
2268
2426
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
2269
2427
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
2270
2428
|
*
|
|
2271
|
-
* The thrown error sets `isHealthCheckError: true` and `causes
|
|
2429
|
+
* The thrown error sets `isHealthCheckError: true` and `causes`, the
|
|
2272
2430
|
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
2273
2431
|
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
2274
2432
|
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
@@ -2302,7 +2460,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
2302
2460
|
import { Module } from "@nestjs/common";
|
|
2303
2461
|
|
|
2304
2462
|
// src/server/routing/pattern-registry.ts
|
|
2305
|
-
import { Logger as
|
|
2463
|
+
import { Logger as Logger9 } from "@nestjs/common";
|
|
2306
2464
|
var HANDLER_LABELS = {
|
|
2307
2465
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
2308
2466
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -2310,12 +2468,13 @@ var HANDLER_LABELS = {
|
|
|
2310
2468
|
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
2311
2469
|
};
|
|
2312
2470
|
var PatternRegistry = class {
|
|
2313
|
-
constructor(options) {
|
|
2471
|
+
constructor(options, names) {
|
|
2314
2472
|
this.options = options;
|
|
2473
|
+
this.names = names;
|
|
2315
2474
|
}
|
|
2316
|
-
logger = new
|
|
2475
|
+
logger = new Logger9("Jetstream:PatternRegistry");
|
|
2317
2476
|
registry = /* @__PURE__ */ new Map();
|
|
2318
|
-
// Cached after registerHandlers()
|
|
2477
|
+
// Cached after registerHandlers(); the registry is immutable from that point
|
|
2319
2478
|
cachedPatterns = null;
|
|
2320
2479
|
_hasEvents = false;
|
|
2321
2480
|
_hasCommands = false;
|
|
@@ -2328,7 +2487,6 @@ var PatternRegistry = class {
|
|
|
2328
2487
|
* @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
|
|
2329
2488
|
*/
|
|
2330
2489
|
registerHandlers(handlers) {
|
|
2331
|
-
const serviceName = this.options.name;
|
|
2332
2490
|
for (const [pattern, handler] of handlers) {
|
|
2333
2491
|
const extras = handler.extras;
|
|
2334
2492
|
const isEvent = handler.isEventHandler ?? false;
|
|
@@ -2345,7 +2503,7 @@ var PatternRegistry = class {
|
|
|
2345
2503
|
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
2346
2504
|
else if (isEvent) kind = "ev" /* Event */;
|
|
2347
2505
|
else kind = "cmd" /* Command */;
|
|
2348
|
-
const fullSubject =
|
|
2506
|
+
const fullSubject = this.names.subject(kind, pattern);
|
|
2349
2507
|
this.registry.set(fullSubject, {
|
|
2350
2508
|
handler,
|
|
2351
2509
|
pattern,
|
|
@@ -2372,7 +2530,7 @@ var PatternRegistry = class {
|
|
|
2372
2530
|
* Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
|
|
2373
2531
|
*
|
|
2374
2532
|
* Returns `null` when the subject is not registered. The declared pattern is
|
|
2375
|
-
* the value the user passed to `@EventPattern`/`@MessagePattern
|
|
2533
|
+
* the value the user passed to `@EventPattern`/`@MessagePattern`: stable and
|
|
2376
2534
|
* bounded, suitable for use as a Prometheus label without cardinality risk.
|
|
2377
2535
|
*/
|
|
2378
2536
|
resolveDeclared(subject) {
|
|
@@ -2382,7 +2540,17 @@ var PatternRegistry = class {
|
|
|
2382
2540
|
}
|
|
2383
2541
|
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
2384
2542
|
getBroadcastPatterns() {
|
|
2385
|
-
return this.getPatternsByKind().broadcasts.map(
|
|
2543
|
+
return this.getPatternsByKind().broadcasts.map(
|
|
2544
|
+
(p) => this.names.subject("broadcast" /* Broadcast */, p)
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
/** Get registered event patterns as raw user-declared patterns. */
|
|
2548
|
+
getEventPatterns() {
|
|
2549
|
+
return this.getPatternsByKind().events;
|
|
2550
|
+
}
|
|
2551
|
+
/** Get registered command patterns as raw user-declared patterns. */
|
|
2552
|
+
getCommandPatterns() {
|
|
2553
|
+
return this.getPatternsByKind().commands;
|
|
2386
2554
|
}
|
|
2387
2555
|
hasBroadcastHandlers() {
|
|
2388
2556
|
return this._hasBroadcasts;
|
|
@@ -2398,9 +2566,7 @@ var PatternRegistry = class {
|
|
|
2398
2566
|
}
|
|
2399
2567
|
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
2400
2568
|
getOrderedSubjects() {
|
|
2401
|
-
return this.getPatternsByKind().ordered.map(
|
|
2402
|
-
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2403
|
-
);
|
|
2569
|
+
return this.getPatternsByKind().ordered.map((p) => this.names.subject("ordered" /* Ordered */, p));
|
|
2404
2570
|
}
|
|
2405
2571
|
/** Check if any registered handler has metadata. */
|
|
2406
2572
|
hasMetadata() {
|
|
@@ -2432,22 +2598,6 @@ var PatternRegistry = class {
|
|
|
2432
2598
|
ordered: [...patterns.ordered]
|
|
2433
2599
|
};
|
|
2434
2600
|
}
|
|
2435
|
-
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
2436
|
-
normalizeSubject(subject) {
|
|
2437
|
-
const name = internalName(this.options.name);
|
|
2438
|
-
const prefixes = [
|
|
2439
|
-
`${name}.${"cmd" /* Command */}.`,
|
|
2440
|
-
`${name}.${"ev" /* Event */}.`,
|
|
2441
|
-
`${name}.${"ordered" /* Ordered */}.`,
|
|
2442
|
-
`${"broadcast" /* Broadcast */}.`
|
|
2443
|
-
];
|
|
2444
|
-
for (const prefix of prefixes) {
|
|
2445
|
-
if (subject.startsWith(prefix)) {
|
|
2446
|
-
return subject.slice(prefix.length);
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
return subject;
|
|
2450
|
-
}
|
|
2451
2601
|
buildPatternsByKind() {
|
|
2452
2602
|
const events = [];
|
|
2453
2603
|
const commands = [];
|
|
@@ -2502,7 +2652,7 @@ var ERROR_CONTEXT_PREFIXES = [
|
|
|
2502
2652
|
["consume", "consume"],
|
|
2503
2653
|
["core-rpc-handler", "handler"],
|
|
2504
2654
|
["rpc-handler", "handler"],
|
|
2505
|
-
// EventRouter formats contexts as `${StreamKind.*}-handler
|
|
2655
|
+
// EventRouter formats contexts as `${StreamKind.*}-handler:...`; the enum
|
|
2506
2656
|
// uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
|
|
2507
2657
|
["ev-handler", "handler"],
|
|
2508
2658
|
["event-handler", "handler"],
|
|
@@ -2523,7 +2673,7 @@ var STREAM_KIND_LABEL = {
|
|
|
2523
2673
|
import {
|
|
2524
2674
|
Inject,
|
|
2525
2675
|
Injectable as Injectable2,
|
|
2526
|
-
Logger as
|
|
2676
|
+
Logger as Logger11,
|
|
2527
2677
|
Optional
|
|
2528
2678
|
} from "@nestjs/common";
|
|
2529
2679
|
|
|
@@ -2648,12 +2798,12 @@ var createMetrics = (opts) => {
|
|
|
2648
2798
|
};
|
|
2649
2799
|
|
|
2650
2800
|
// src/metrics/poll-runner.ts
|
|
2651
|
-
import { Logger as
|
|
2801
|
+
import { Logger as Logger10 } from "@nestjs/common";
|
|
2652
2802
|
var PollRunner = class {
|
|
2653
2803
|
constructor(opts) {
|
|
2654
2804
|
this.opts = opts;
|
|
2655
2805
|
}
|
|
2656
|
-
logger = new
|
|
2806
|
+
logger = new Logger10("Jetstream:Metrics:Poll");
|
|
2657
2807
|
timer = null;
|
|
2658
2808
|
inFlight = null;
|
|
2659
2809
|
start() {
|
|
@@ -2662,7 +2812,7 @@ var PollRunner = class {
|
|
|
2662
2812
|
if (this.opts.targets.length === 0) return;
|
|
2663
2813
|
this.timer = setInterval(() => {
|
|
2664
2814
|
if (this.inFlight !== null) {
|
|
2665
|
-
this.logger.warn("Skipping poll tick
|
|
2815
|
+
this.logger.warn("Skipping poll tick; previous cycle still in flight");
|
|
2666
2816
|
return;
|
|
2667
2817
|
}
|
|
2668
2818
|
this.inFlight = this.tick().finally(() => {
|
|
@@ -2725,15 +2875,16 @@ var PollRunner = class {
|
|
|
2725
2875
|
|
|
2726
2876
|
// src/metrics/metrics.service.ts
|
|
2727
2877
|
var JetstreamMetricsService = class {
|
|
2728
|
-
constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
|
|
2878
|
+
constructor(eventBus, config, promClient, options, patternRegistry, connection = null, names = null) {
|
|
2729
2879
|
this.eventBus = eventBus;
|
|
2730
2880
|
this.config = config;
|
|
2731
2881
|
this.promClient = promClient;
|
|
2732
2882
|
this.options = options;
|
|
2733
2883
|
this.patternRegistry = patternRegistry;
|
|
2734
2884
|
this.connection = connection;
|
|
2885
|
+
this.names = names;
|
|
2735
2886
|
}
|
|
2736
|
-
logger = new
|
|
2887
|
+
logger = new Logger11("Jetstream:Metrics");
|
|
2737
2888
|
metrics = null;
|
|
2738
2889
|
pollRunner = null;
|
|
2739
2890
|
activeServers = /* @__PURE__ */ new Set();
|
|
@@ -2742,7 +2893,7 @@ var JetstreamMetricsService = class {
|
|
|
2742
2893
|
if (!this.options.metrics || !this.config || !this.promClient) return;
|
|
2743
2894
|
if (!this.config.register) {
|
|
2744
2895
|
throw new Error(
|
|
2745
|
-
"JetstreamMetricsService requires a prom-client Registry
|
|
2896
|
+
"JetstreamMetricsService requires a prom-client Registry; none was resolved by JetstreamMetricsModule."
|
|
2746
2897
|
);
|
|
2747
2898
|
}
|
|
2748
2899
|
this.metrics = createMetrics({
|
|
@@ -2769,7 +2920,7 @@ var JetstreamMetricsService = class {
|
|
|
2769
2920
|
}
|
|
2770
2921
|
/**
|
|
2771
2922
|
* NATS connects during early bootstrap, before this service subscribes to
|
|
2772
|
-
* the EventBus
|
|
2923
|
+
* the EventBus, so the initial `Connect` emission misses us. Mirror the
|
|
2773
2924
|
* current state here so `connection_up` reflects reality the moment metrics
|
|
2774
2925
|
* come online; later disconnects/reconnects update it normally.
|
|
2775
2926
|
*/
|
|
@@ -2802,26 +2953,32 @@ var JetstreamMetricsService = class {
|
|
|
2802
2953
|
if (registry.hasEventHandlers()) {
|
|
2803
2954
|
targets.push({
|
|
2804
2955
|
kind: "ev" /* Event */,
|
|
2805
|
-
stream:
|
|
2806
|
-
consumer:
|
|
2956
|
+
stream: this.resolveStreamName("ev" /* Event */),
|
|
2957
|
+
consumer: this.resolveConsumerName("ev" /* Event */)
|
|
2807
2958
|
});
|
|
2808
2959
|
}
|
|
2809
2960
|
if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
|
|
2810
2961
|
targets.push({
|
|
2811
2962
|
kind: "cmd" /* Command */,
|
|
2812
|
-
stream:
|
|
2813
|
-
consumer:
|
|
2963
|
+
stream: this.resolveStreamName("cmd" /* Command */),
|
|
2964
|
+
consumer: this.resolveConsumerName("cmd" /* Command */)
|
|
2814
2965
|
});
|
|
2815
2966
|
}
|
|
2816
2967
|
if (registry.hasBroadcastHandlers()) {
|
|
2817
2968
|
targets.push({
|
|
2818
2969
|
kind: "broadcast" /* Broadcast */,
|
|
2819
|
-
stream:
|
|
2820
|
-
consumer:
|
|
2970
|
+
stream: this.resolveStreamName("broadcast" /* Broadcast */),
|
|
2971
|
+
consumer: this.resolveConsumerName("broadcast" /* Broadcast */)
|
|
2821
2972
|
});
|
|
2822
2973
|
}
|
|
2823
2974
|
return targets;
|
|
2824
2975
|
}
|
|
2976
|
+
resolveStreamName(kind) {
|
|
2977
|
+
return this.names ? this.names.streamName(kind) : streamName(this.options.name, kind);
|
|
2978
|
+
}
|
|
2979
|
+
resolveConsumerName(kind) {
|
|
2980
|
+
return this.names ? this.names.consumerName(kind) : consumerName(this.options.name, kind);
|
|
2981
|
+
}
|
|
2825
2982
|
subscribeToEvents() {
|
|
2826
2983
|
this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
|
|
2827
2984
|
this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
|
|
@@ -2856,7 +3013,7 @@ var JetstreamMetricsService = class {
|
|
|
2856
3013
|
const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
|
|
2857
3014
|
this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
|
|
2858
3015
|
};
|
|
2859
|
-
// `_kind` collapses broadcast/ordered into MessageKind.Event
|
|
3016
|
+
// `_kind` collapses broadcast/ordered into MessageKind.Event; we use
|
|
2860
3017
|
// declared.kind from PatternRegistry for the precise label instead.
|
|
2861
3018
|
onMessageRouted = (subject, _kind) => {
|
|
2862
3019
|
if (!this.metrics) return;
|
|
@@ -2866,7 +3023,7 @@ var JetstreamMetricsService = class {
|
|
|
2866
3023
|
return;
|
|
2867
3024
|
}
|
|
2868
3025
|
this.metrics.messagesReceivedTotal.labels({
|
|
2869
|
-
stream:
|
|
3026
|
+
stream: this.resolveStreamName(declared.kind),
|
|
2870
3027
|
subject: declared.pattern,
|
|
2871
3028
|
kind: STREAM_KIND_LABEL[declared.kind]
|
|
2872
3029
|
}).inc();
|
|
@@ -2882,7 +3039,7 @@ var JetstreamMetricsService = class {
|
|
|
2882
3039
|
};
|
|
2883
3040
|
onHandlerCompleted = (pattern, kind, durationMs, status) => {
|
|
2884
3041
|
if (!this.metrics) return;
|
|
2885
|
-
const stream =
|
|
3042
|
+
const stream = this.resolveStreamName(kind);
|
|
2886
3043
|
const kindLabel = STREAM_KIND_LABEL[kind];
|
|
2887
3044
|
const labels = { stream, subject: pattern, kind: kindLabel, status };
|
|
2888
3045
|
this.metrics.messagesProcessedTotal.labels(labels).inc();
|
|
@@ -2908,7 +3065,8 @@ JetstreamMetricsService = __decorateClass([
|
|
|
2908
3065
|
__decorateParam(3, Inject(JETSTREAM_OPTIONS)),
|
|
2909
3066
|
__decorateParam(4, Optional()),
|
|
2910
3067
|
__decorateParam(5, Optional()),
|
|
2911
|
-
__decorateParam(5, Inject(JETSTREAM_CONNECTION))
|
|
3068
|
+
__decorateParam(5, Inject(JETSTREAM_CONNECTION)),
|
|
3069
|
+
__decorateParam(6, Optional())
|
|
2912
3070
|
], JetstreamMetricsService);
|
|
2913
3071
|
|
|
2914
3072
|
// src/metrics/metrics.module.ts
|
|
@@ -2963,9 +3121,18 @@ var JetstreamMetricsModule = class {
|
|
|
2963
3121
|
JETSTREAM_METRICS_PROM_CLIENT,
|
|
2964
3122
|
JETSTREAM_OPTIONS,
|
|
2965
3123
|
{ token: PatternRegistry, optional: true },
|
|
2966
|
-
{ token: JETSTREAM_CONNECTION, optional: true }
|
|
3124
|
+
{ token: JETSTREAM_CONNECTION, optional: true },
|
|
3125
|
+
{ token: NameResolver, optional: true }
|
|
2967
3126
|
],
|
|
2968
|
-
useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(
|
|
3127
|
+
useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection, names) => new JetstreamMetricsService(
|
|
3128
|
+
eventBus,
|
|
3129
|
+
cfg,
|
|
3130
|
+
runtime,
|
|
3131
|
+
opts,
|
|
3132
|
+
patternRegistry,
|
|
3133
|
+
connection,
|
|
3134
|
+
names
|
|
3135
|
+
)
|
|
2969
3136
|
};
|
|
2970
3137
|
return {
|
|
2971
3138
|
module: JetstreamMetricsModule,
|
|
@@ -3026,31 +3193,22 @@ var JetstreamStrategy = class extends Server {
|
|
|
3026
3193
|
this.started = false;
|
|
3027
3194
|
}
|
|
3028
3195
|
/**
|
|
3029
|
-
* Override NestJS `Server.addHandler` to fail
|
|
3196
|
+
* Override NestJS `Server.addHandler` to fail fast on duplicate pattern registration.
|
|
3030
3197
|
*
|
|
3031
|
-
* The base class silently overwrites duplicate RPC handlers
|
|
3032
|
-
*
|
|
3033
|
-
*
|
|
3034
|
-
* event dispatch double-acks/double-processes the same JetStream message.
|
|
3035
|
-
*
|
|
3036
|
-
* We treat any pattern collision as a fatal misconfiguration so it surfaces at
|
|
3037
|
-
* bootstrap instead of in production traffic.
|
|
3198
|
+
* The base class silently overwrites duplicate RPC handlers and chains duplicate event
|
|
3199
|
+
* handlers, which would double-ack the same JetStream message. Any collision is treated
|
|
3200
|
+
* as a fatal misconfiguration so it surfaces at bootstrap, not in production traffic.
|
|
3038
3201
|
*/
|
|
3039
3202
|
addHandler(pattern, callback, isEventHandler = false, extras = {}) {
|
|
3040
3203
|
const normalizedPattern = this.normalizePattern(pattern);
|
|
3041
3204
|
if (this.messageHandlers.has(normalizedPattern)) {
|
|
3042
3205
|
throw new Error(
|
|
3043
|
-
`Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice
|
|
3206
|
+
`Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice; find and remove the second declaration.`
|
|
3044
3207
|
);
|
|
3045
3208
|
}
|
|
3046
3209
|
super.addHandler(pattern, callback, isEventHandler, extras);
|
|
3047
3210
|
}
|
|
3048
|
-
/**
|
|
3049
|
-
* Register event listener (required by Server base class).
|
|
3050
|
-
*
|
|
3051
|
-
* Stores callbacks for client use. Primary lifecycle events
|
|
3052
|
-
* are routed through EventBus.
|
|
3053
|
-
*/
|
|
3211
|
+
/** Register event listener (required by Server base class); lifecycle events use EventBus. */
|
|
3054
3212
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
3055
3213
|
on(event, callback) {
|
|
3056
3214
|
const existing = this.listeners.get(event) ?? [];
|
|
@@ -3065,7 +3223,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
3065
3223
|
unwrap() {
|
|
3066
3224
|
const nc = this.connection.unwrap;
|
|
3067
3225
|
if (!nc) {
|
|
3068
|
-
throw new Error("Not connected
|
|
3226
|
+
throw new Error("Not connected; transport has not started");
|
|
3069
3227
|
}
|
|
3070
3228
|
return nc;
|
|
3071
3229
|
}
|
|
@@ -3075,7 +3233,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
3075
3233
|
}
|
|
3076
3234
|
async doListen(callback) {
|
|
3077
3235
|
if (this.started) {
|
|
3078
|
-
this.logger.warn("listen() called more than once
|
|
3236
|
+
this.logger.warn("listen() called more than once; ignoring");
|
|
3079
3237
|
return;
|
|
3080
3238
|
}
|
|
3081
3239
|
this.started = true;
|
|
@@ -3166,7 +3324,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
3166
3324
|
};
|
|
3167
3325
|
|
|
3168
3326
|
// src/server/core-rpc.server.ts
|
|
3169
|
-
import { Logger as
|
|
3327
|
+
import { Logger as Logger12 } from "@nestjs/common";
|
|
3170
3328
|
import { headers as natsHeaders2 } from "@nats-io/transport-node";
|
|
3171
3329
|
|
|
3172
3330
|
// src/context/rpc.context.ts
|
|
@@ -3243,7 +3401,7 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
3243
3401
|
retry(opts) {
|
|
3244
3402
|
this.assertJetStream("retry");
|
|
3245
3403
|
if (this._shouldTerminate) {
|
|
3246
|
-
throw new Error("Cannot retry
|
|
3404
|
+
throw new Error("Cannot retry; terminate() was already called");
|
|
3247
3405
|
}
|
|
3248
3406
|
this._shouldRetry = true;
|
|
3249
3407
|
this._retryDelay = opts?.delayMs;
|
|
@@ -3260,7 +3418,7 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
3260
3418
|
terminate(reason) {
|
|
3261
3419
|
this.assertJetStream("terminate");
|
|
3262
3420
|
if (this._shouldRetry) {
|
|
3263
|
-
throw new Error("Cannot terminate
|
|
3421
|
+
throw new Error("Cannot terminate; retry() was already called");
|
|
3264
3422
|
}
|
|
3265
3423
|
this._shouldTerminate = true;
|
|
3266
3424
|
this._terminateReason = reason;
|
|
@@ -3269,7 +3427,7 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
3269
3427
|
asJetStream() {
|
|
3270
3428
|
return this.isJetStream() ? this.args[0] : null;
|
|
3271
3429
|
}
|
|
3272
|
-
/** Ensure the message is JetStream
|
|
3430
|
+
/** Ensure the message is JetStream; settlement actions are not available for Core NATS. */
|
|
3273
3431
|
assertJetStream(method) {
|
|
3274
3432
|
if (!this.isJetStream()) {
|
|
3275
3433
|
throw new Error(`${method}() is only available for JetStream messages`);
|
|
@@ -3434,17 +3592,18 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
|
3434
3592
|
|
|
3435
3593
|
// src/server/core-rpc.server.ts
|
|
3436
3594
|
var CoreRpcServer = class {
|
|
3437
|
-
constructor(options, connection, patternRegistry, codec, eventBus) {
|
|
3595
|
+
constructor(options, connection, patternRegistry, codec, eventBus, names) {
|
|
3438
3596
|
this.connection = connection;
|
|
3439
3597
|
this.patternRegistry = patternRegistry;
|
|
3440
3598
|
this.codec = codec;
|
|
3441
3599
|
this.eventBus = eventBus;
|
|
3600
|
+
this.names = names;
|
|
3442
3601
|
const derived = deriveOtelAttrs(options);
|
|
3443
3602
|
this.otel = derived.otel;
|
|
3444
3603
|
this.serviceName = derived.serviceName;
|
|
3445
3604
|
this.serverEndpoint = derived.serverEndpoint;
|
|
3446
3605
|
}
|
|
3447
|
-
logger = new
|
|
3606
|
+
logger = new Logger12("Jetstream:CoreRpc");
|
|
3448
3607
|
subscription = null;
|
|
3449
3608
|
otel;
|
|
3450
3609
|
serviceName;
|
|
@@ -3452,7 +3611,7 @@ var CoreRpcServer = class {
|
|
|
3452
3611
|
/** Start listening for RPC requests on the command subject. */
|
|
3453
3612
|
async start() {
|
|
3454
3613
|
const nc = await this.connection.getConnection();
|
|
3455
|
-
const subject = `${this.serviceName}.cmd.>`;
|
|
3614
|
+
const subject = this.names ? this.names.filterSubject("cmd" /* Command */) : `${this.serviceName}.cmd.>`;
|
|
3456
3615
|
const queue = `${this.serviceName}_cmd_queue`;
|
|
3457
3616
|
this.subscription = nc.subscribe(subject, {
|
|
3458
3617
|
queue,
|
|
@@ -3555,7 +3714,7 @@ var CoreRpcServer = class {
|
|
|
3555
3714
|
};
|
|
3556
3715
|
|
|
3557
3716
|
// src/server/infrastructure/stream.provider.ts
|
|
3558
|
-
import { Logger as
|
|
3717
|
+
import { Logger as Logger14 } from "@nestjs/common";
|
|
3559
3718
|
import {
|
|
3560
3719
|
JetStreamApiError as JetStreamApiError2,
|
|
3561
3720
|
RetentionPolicy as RetentionPolicy2,
|
|
@@ -3629,7 +3788,7 @@ var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
|
|
|
3629
3788
|
);
|
|
3630
3789
|
}
|
|
3631
3790
|
} catch (err) {
|
|
3632
|
-
logger5.debug(`Storage preflight skipped
|
|
3791
|
+
logger5.debug(`Storage preflight skipped; account info unavailable: ${String(err)}`);
|
|
3633
3792
|
}
|
|
3634
3793
|
};
|
|
3635
3794
|
|
|
@@ -3650,7 +3809,7 @@ var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error
|
|
|
3650
3809
|
numReplicas;
|
|
3651
3810
|
reservation;
|
|
3652
3811
|
constructor(fields) {
|
|
3653
|
-
const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B
|
|
3812
|
+
const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B x replicas=${fields.numReplicas}).` : "";
|
|
3654
3813
|
super(
|
|
3655
3814
|
`JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
|
|
3656
3815
|
{ cause: fields.cause }
|
|
@@ -3702,18 +3861,22 @@ var formatAge = (nanos) => {
|
|
|
3702
3861
|
if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
|
|
3703
3862
|
return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
|
|
3704
3863
|
};
|
|
3705
|
-
var formatProvisioningSummary = (serviceName, reservations) => {
|
|
3706
|
-
const
|
|
3864
|
+
var formatProvisioningSummary = (serviceName, reservations, external = []) => {
|
|
3865
|
+
const totalStreams = reservations.length + external.length;
|
|
3866
|
+
const lines = [`Provisioning ${totalStreams} stream(s) for "${serviceName}":`];
|
|
3707
3867
|
let totalFileMaxBytes = 0;
|
|
3708
3868
|
for (const r of reservations) {
|
|
3709
3869
|
if (r.storage === StorageType3.File) totalFileMaxBytes += r.maxBytes;
|
|
3710
3870
|
const clusterReservation = r.maxBytes * r.numReplicas;
|
|
3711
3871
|
lines.push(
|
|
3712
|
-
`
|
|
3872
|
+
` - ${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)}`
|
|
3713
3873
|
);
|
|
3714
3874
|
}
|
|
3875
|
+
for (const e of external) {
|
|
3876
|
+
lines.push(` - ${e.name} [${e.kind}] external (bound)`);
|
|
3877
|
+
}
|
|
3715
3878
|
lines.push(
|
|
3716
|
-
`
|
|
3879
|
+
` 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.`
|
|
3717
3880
|
);
|
|
3718
3881
|
return lines.join("\n");
|
|
3719
3882
|
};
|
|
@@ -3774,7 +3937,7 @@ var isEqual = (a, b) => {
|
|
|
3774
3937
|
};
|
|
3775
3938
|
|
|
3776
3939
|
// src/server/infrastructure/stream-migration.ts
|
|
3777
|
-
import { Logger as
|
|
3940
|
+
import { Logger as Logger13 } from "@nestjs/common";
|
|
3778
3941
|
import {
|
|
3779
3942
|
JetStreamApiError
|
|
3780
3943
|
} from "@nats-io/jetstream";
|
|
@@ -3789,7 +3952,7 @@ var StreamMigration = class {
|
|
|
3789
3952
|
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
3790
3953
|
this.peerWaitMs = peerWaitMs;
|
|
3791
3954
|
}
|
|
3792
|
-
logger = new
|
|
3955
|
+
logger = new Logger13("Jetstream:Stream");
|
|
3793
3956
|
async migrate(jsm, streamName2, newConfig) {
|
|
3794
3957
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
3795
3958
|
const startTime = Date.now();
|
|
@@ -3808,7 +3971,7 @@ var StreamMigration = class {
|
|
|
3808
3971
|
await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
|
|
3809
3972
|
drainedCount = (await jsm.streams.info(streamName2)).state.messages;
|
|
3810
3973
|
if (drainedCount > 0) {
|
|
3811
|
-
this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages
|
|
3974
|
+
this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages -> ${backupName}`);
|
|
3812
3975
|
await jsm.streams.add({
|
|
3813
3976
|
...currentInfo.config,
|
|
3814
3977
|
name: backupName,
|
|
@@ -3829,7 +3992,7 @@ var StreamMigration = class {
|
|
|
3829
3992
|
} catch (err) {
|
|
3830
3993
|
if (originalDeleted) {
|
|
3831
3994
|
this.logger.error(
|
|
3832
|
-
`Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved
|
|
3995
|
+
`Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved; restoration resumes on the next startup.`
|
|
3833
3996
|
);
|
|
3834
3997
|
} else {
|
|
3835
3998
|
await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
|
|
@@ -3897,7 +4060,7 @@ var StreamMigration = class {
|
|
|
3897
4060
|
await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
|
|
3898
4061
|
}
|
|
3899
4062
|
/**
|
|
3900
|
-
* Lag-based drain check
|
|
4063
|
+
* Lag-based drain check: live publishes cannot fake completion. A fresh
|
|
3901
4064
|
* source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
|
|
3902
4065
|
* hence the active guard.
|
|
3903
4066
|
*/
|
|
@@ -3916,13 +4079,13 @@ var StreamMigration = class {
|
|
|
3916
4079
|
);
|
|
3917
4080
|
}
|
|
3918
4081
|
/**
|
|
3919
|
-
* A backup present at migrate() start is a live peer migration
|
|
4082
|
+
* A backup present at migrate() start is a live peer migration; wait it
|
|
3920
4083
|
* out. Stale leftovers were already handled by recoverInterrupted().
|
|
3921
4084
|
*/
|
|
3922
4085
|
async waitOutPeerMigration(jsm, backupName) {
|
|
3923
4086
|
if (await this.tryInfo(jsm, backupName) === null) return false;
|
|
3924
4087
|
this.logger.warn(
|
|
3925
|
-
`Migration backup ${backupName} exists
|
|
4088
|
+
`Migration backup ${backupName} exists; another instance appears to be migrating; waiting`
|
|
3926
4089
|
);
|
|
3927
4090
|
const deadline = Date.now() + this.peerWaitMs;
|
|
3928
4091
|
while (Date.now() < deadline) {
|
|
@@ -3943,7 +4106,7 @@ var StreamMigration = class {
|
|
|
3943
4106
|
}
|
|
3944
4107
|
} catch (rollbackErr) {
|
|
3945
4108
|
this.logger.error(
|
|
3946
|
-
`Rollback of ${streamName2} after a failed migration also failed
|
|
4109
|
+
`Rollback of ${streamName2} after a failed migration also failed; the stream may be left quiesced:`,
|
|
3947
4110
|
rollbackErr
|
|
3948
4111
|
);
|
|
3949
4112
|
}
|
|
@@ -3967,7 +4130,7 @@ var StreamMigration = class {
|
|
|
3967
4130
|
}
|
|
3968
4131
|
};
|
|
3969
4132
|
|
|
3970
|
-
// src/server/infrastructure/
|
|
4133
|
+
// src/server/infrastructure/subject-utils.ts
|
|
3971
4134
|
var subjectCovers = (broad, narrow) => {
|
|
3972
4135
|
if (broad === narrow) return false;
|
|
3973
4136
|
const broadTokens = broad.split(".");
|
|
@@ -3979,16 +4142,21 @@ var subjectCovers = (broad, narrow) => {
|
|
|
3979
4142
|
}
|
|
3980
4143
|
return broadTokens.length === narrowTokens.length;
|
|
3981
4144
|
};
|
|
4145
|
+
var coversOrEquals = (broad, subject) => broad === subject || subjectCovers(broad, subject);
|
|
4146
|
+
|
|
4147
|
+
// src/server/infrastructure/stream.provider.ts
|
|
3982
4148
|
var StreamProvider = class {
|
|
3983
|
-
constructor(options, connection) {
|
|
4149
|
+
constructor(options, connection, names, binder) {
|
|
3984
4150
|
this.options = options;
|
|
3985
4151
|
this.connection = connection;
|
|
4152
|
+
this.names = names;
|
|
4153
|
+
this.binder = binder;
|
|
3986
4154
|
const derived = deriveOtelAttrs(options);
|
|
3987
4155
|
this.otel = derived.otel;
|
|
3988
4156
|
this.otelServiceName = derived.serviceName;
|
|
3989
4157
|
this.otelEndpoint = derived.serverEndpoint;
|
|
3990
4158
|
}
|
|
3991
|
-
logger = new
|
|
4159
|
+
logger = new Logger14("Jetstream:Stream");
|
|
3992
4160
|
migration = new StreamMigration();
|
|
3993
4161
|
otel;
|
|
3994
4162
|
otelServiceName;
|
|
@@ -4002,42 +4170,48 @@ var StreamProvider = class {
|
|
|
4002
4170
|
*/
|
|
4003
4171
|
async ensureStreams(kinds) {
|
|
4004
4172
|
const jsm = await this.connection.getJetStreamManager();
|
|
4005
|
-
const
|
|
4173
|
+
const { autoKinds, externalKinds } = this.partitionByManagement(kinds);
|
|
4174
|
+
const reservations = autoKinds.map(
|
|
4175
|
+
(kind) => this.buildReservation(kind, this.buildConfig(kind))
|
|
4176
|
+
);
|
|
4177
|
+
const external = externalKinds.map((kind) => ({
|
|
4178
|
+
kind,
|
|
4179
|
+
name: this.names.streamName(kind)
|
|
4180
|
+
}));
|
|
4181
|
+
const dlqIsManual = !!this.options.dlq && resolveManagementMode(this.options, "dlq", "stream") === "manual" /* Manual */;
|
|
4006
4182
|
if (this.options.dlq) {
|
|
4007
|
-
|
|
4183
|
+
if (dlqIsManual) {
|
|
4184
|
+
external.push({ kind: "dlq", name: this.names.dlqStreamName() });
|
|
4185
|
+
} else {
|
|
4186
|
+
reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
|
|
4187
|
+
}
|
|
4008
4188
|
}
|
|
4009
4189
|
this.logger.log(`
|
|
4010
|
-
${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
4011
|
-
if (this.options.provisioning?.preflightStorageCheck) {
|
|
4190
|
+
${formatProvisioningSummary(this.options.name, reservations, external)}`);
|
|
4191
|
+
if (this.options.provisioning?.preflightStorageCheck && reservations.length > 0) {
|
|
4012
4192
|
await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
|
|
4013
4193
|
}
|
|
4014
|
-
await Promise.all(
|
|
4194
|
+
await Promise.all([
|
|
4195
|
+
...autoKinds.map((kind) => this.ensureStream(jsm, kind)),
|
|
4196
|
+
...externalKinds.map((kind) => this.bindStream(jsm, kind))
|
|
4197
|
+
]);
|
|
4015
4198
|
if (this.options.dlq) {
|
|
4016
|
-
|
|
4199
|
+
if (dlqIsManual) {
|
|
4200
|
+
await this.bindDlqStream(jsm);
|
|
4201
|
+
} else {
|
|
4202
|
+
await this.ensureDlqStream(jsm);
|
|
4203
|
+
}
|
|
4017
4204
|
}
|
|
4018
4205
|
}
|
|
4019
4206
|
/** Get the stream name for a given kind. */
|
|
4020
4207
|
getStreamName(kind) {
|
|
4021
|
-
return
|
|
4208
|
+
return this.names.streamName(kind);
|
|
4022
4209
|
}
|
|
4023
4210
|
/** Get the subjects pattern for a given kind. */
|
|
4024
4211
|
getSubjects(kind) {
|
|
4025
|
-
const
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
const subjects = [`${name}.${"ev" /* Event */}.>`];
|
|
4029
|
-
if (this.isSchedulingEnabled(kind)) {
|
|
4030
|
-
subjects.push(`${name}._sch.>`);
|
|
4031
|
-
}
|
|
4032
|
-
return subjects;
|
|
4033
|
-
}
|
|
4034
|
-
case "cmd" /* Command */:
|
|
4035
|
-
return [`${name}.${"cmd" /* Command */}.>`];
|
|
4036
|
-
case "broadcast" /* Broadcast */:
|
|
4037
|
-
return ["broadcast.>"];
|
|
4038
|
-
case "ordered" /* Ordered */:
|
|
4039
|
-
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
4040
|
-
}
|
|
4212
|
+
const filter = this.names.filterSubject(kind);
|
|
4213
|
+
const dedicatedSchedule = kind === "ev" /* Event */ && this.isSchedulingEnabled(kind) && !this.names.hasCustomPrefix(kind);
|
|
4214
|
+
return dedicatedSchedule ? [filter, `${this.names.schedulePrefix(kind)}>`] : [filter];
|
|
4041
4215
|
}
|
|
4042
4216
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
4043
4217
|
async ensureStream(jsm, kind) {
|
|
@@ -4114,7 +4288,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4114
4288
|
}
|
|
4115
4289
|
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
4116
4290
|
if (diff.hasTransportControlledConflicts) {
|
|
4117
|
-
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)}
|
|
4291
|
+
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`).join(", ");
|
|
4118
4292
|
throw new Error(
|
|
4119
4293
|
`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.`
|
|
4120
4294
|
);
|
|
@@ -4164,13 +4338,13 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4164
4338
|
}
|
|
4165
4339
|
logChanges(streamName2, diff, migrationEnabled) {
|
|
4166
4340
|
for (const c of diff.changes) {
|
|
4167
|
-
const detail = `${c.property}: ${JSON.stringify(c.current)}
|
|
4341
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`;
|
|
4168
4342
|
if (c.mutability === "transport-controlled") {
|
|
4169
4343
|
this.logger.error(
|
|
4170
|
-
`Stream ${streamName2}: ${detail}
|
|
4344
|
+
`Stream ${streamName2}: ${detail}; transport-controlled, cannot be changed`
|
|
4171
4345
|
);
|
|
4172
4346
|
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
4173
|
-
this.logger.warn(`Stream ${streamName2}: ${detail}
|
|
4347
|
+
this.logger.warn(`Stream ${streamName2}: ${detail}; requires allowDestructiveMigration`);
|
|
4174
4348
|
} else {
|
|
4175
4349
|
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
4176
4350
|
}
|
|
@@ -4208,7 +4382,47 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4208
4382
|
throw err;
|
|
4209
4383
|
}
|
|
4210
4384
|
}
|
|
4211
|
-
|
|
4385
|
+
partitionByManagement(kinds) {
|
|
4386
|
+
const autoKinds = [];
|
|
4387
|
+
const externalKinds = [];
|
|
4388
|
+
for (const kind of kinds) {
|
|
4389
|
+
if (resolveManagementMode(this.options, kind, "stream") === "manual" /* Manual */) {
|
|
4390
|
+
externalKinds.push(kind);
|
|
4391
|
+
} else {
|
|
4392
|
+
autoKinds.push(kind);
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
return { autoKinds, externalKinds };
|
|
4396
|
+
}
|
|
4397
|
+
async bindStream(jsm, kind) {
|
|
4398
|
+
const name = this.names.streamName(kind);
|
|
4399
|
+
return withProvisioningSpan(
|
|
4400
|
+
this.otel,
|
|
4401
|
+
{
|
|
4402
|
+
serviceName: this.otelServiceName,
|
|
4403
|
+
endpoint: this.otelEndpoint,
|
|
4404
|
+
entity: "stream",
|
|
4405
|
+
name,
|
|
4406
|
+
action: "bind"
|
|
4407
|
+
},
|
|
4408
|
+
() => this.binder.bindStream(jsm, kind)
|
|
4409
|
+
);
|
|
4410
|
+
}
|
|
4411
|
+
async bindDlqStream(jsm) {
|
|
4412
|
+
const name = this.names.dlqStreamName();
|
|
4413
|
+
return withProvisioningSpan(
|
|
4414
|
+
this.otel,
|
|
4415
|
+
{
|
|
4416
|
+
serviceName: this.otelServiceName,
|
|
4417
|
+
endpoint: this.otelEndpoint,
|
|
4418
|
+
entity: "stream",
|
|
4419
|
+
name,
|
|
4420
|
+
action: "bind"
|
|
4421
|
+
},
|
|
4422
|
+
() => this.binder.bindDlqStream(jsm)
|
|
4423
|
+
);
|
|
4424
|
+
}
|
|
4425
|
+
/** The broadcast stream is global; every service in the cluster shares it. */
|
|
4212
4426
|
isSharedStream(name) {
|
|
4213
4427
|
return name === this.getStreamName("broadcast" /* Broadcast */);
|
|
4214
4428
|
}
|
|
@@ -4228,13 +4442,11 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4228
4442
|
};
|
|
4229
4443
|
}
|
|
4230
4444
|
/**
|
|
4231
|
-
* Build the stream
|
|
4232
|
-
*
|
|
4233
|
-
* Merges the library default DLQ config with user-provided overrides.
|
|
4234
|
-
* Ensures transport-controlled settings like retention are safely decoupled.
|
|
4445
|
+
* Build the DLQ stream config: library defaults merged with user overrides,
|
|
4446
|
+
* with transport-controlled settings like retention stripped.
|
|
4235
4447
|
*/
|
|
4236
4448
|
buildDlqConfig() {
|
|
4237
|
-
const name =
|
|
4449
|
+
const name = this.names.dlqStreamName();
|
|
4238
4450
|
const subjects = [name];
|
|
4239
4451
|
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
4240
4452
|
const overrides = this.options.dlq?.stream ?? {};
|
|
@@ -4267,22 +4479,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4267
4479
|
}
|
|
4268
4480
|
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
4269
4481
|
getOverrides(kind) {
|
|
4270
|
-
|
|
4271
|
-
switch (kind) {
|
|
4272
|
-
case "ev" /* Event */:
|
|
4273
|
-
overrides = this.options.events?.stream ?? {};
|
|
4274
|
-
break;
|
|
4275
|
-
case "cmd" /* Command */:
|
|
4276
|
-
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
4277
|
-
break;
|
|
4278
|
-
case "broadcast" /* Broadcast */:
|
|
4279
|
-
overrides = this.options.broadcast?.stream ?? {};
|
|
4280
|
-
break;
|
|
4281
|
-
case "ordered" /* Ordered */:
|
|
4282
|
-
overrides = this.options.ordered?.stream ?? {};
|
|
4283
|
-
break;
|
|
4284
|
-
}
|
|
4285
|
-
return this.stripTransportControlled(overrides);
|
|
4482
|
+
return this.stripTransportControlled(kindOptionsBlock(this.options, kind)?.stream ?? {});
|
|
4286
4483
|
}
|
|
4287
4484
|
/**
|
|
4288
4485
|
* Remove transport-controlled properties from user overrides.
|
|
@@ -4292,7 +4489,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4292
4489
|
stripTransportControlled(overrides) {
|
|
4293
4490
|
if (!("retention" in overrides)) return overrides;
|
|
4294
4491
|
this.logger.debug(
|
|
4295
|
-
"Stripping user-provided retention override
|
|
4492
|
+
"Stripping user-provided retention override; retention is managed by the transport"
|
|
4296
4493
|
);
|
|
4297
4494
|
const cleaned = { ...overrides };
|
|
4298
4495
|
delete cleaned.retention;
|
|
@@ -4301,20 +4498,22 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4301
4498
|
};
|
|
4302
4499
|
|
|
4303
4500
|
// src/server/infrastructure/consumer.provider.ts
|
|
4304
|
-
import { Logger as
|
|
4501
|
+
import { Logger as Logger15 } from "@nestjs/common";
|
|
4305
4502
|
import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
|
|
4306
4503
|
var ConsumerProvider = class {
|
|
4307
|
-
constructor(options, connection, streamProvider, patternRegistry) {
|
|
4504
|
+
constructor(options, connection, streamProvider, patternRegistry, names, binder) {
|
|
4308
4505
|
this.options = options;
|
|
4309
4506
|
this.connection = connection;
|
|
4310
4507
|
this.streamProvider = streamProvider;
|
|
4311
4508
|
this.patternRegistry = patternRegistry;
|
|
4509
|
+
this.names = names;
|
|
4510
|
+
this.binder = binder;
|
|
4312
4511
|
const derived = deriveOtelAttrs(options);
|
|
4313
4512
|
this.otel = derived.otel;
|
|
4314
4513
|
this.otelServiceName = derived.serviceName;
|
|
4315
4514
|
this.otelEndpoint = derived.serverEndpoint;
|
|
4316
4515
|
}
|
|
4317
|
-
logger = new
|
|
4516
|
+
logger = new Logger15("Jetstream:Consumer");
|
|
4318
4517
|
otel;
|
|
4319
4518
|
otelServiceName;
|
|
4320
4519
|
otelEndpoint;
|
|
@@ -4336,24 +4535,33 @@ var ConsumerProvider = class {
|
|
|
4336
4535
|
}
|
|
4337
4536
|
/** Get the consumer name for a given kind. */
|
|
4338
4537
|
getConsumerName(kind) {
|
|
4339
|
-
return
|
|
4538
|
+
return this.names.consumerName(kind);
|
|
4340
4539
|
}
|
|
4341
4540
|
/**
|
|
4342
4541
|
* Ensure a single consumer exists with the desired config.
|
|
4343
|
-
*
|
|
4344
|
-
* the current pod's configuration.
|
|
4542
|
+
* Startup path: creates or updates the consumer to match the current pod's configuration.
|
|
4345
4543
|
*/
|
|
4346
4544
|
async ensureConsumer(jsm, kind) {
|
|
4347
4545
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4348
4546
|
const config = this.buildConfig(kind);
|
|
4349
4547
|
const name = config.durable_name;
|
|
4548
|
+
const spanAttrs = {
|
|
4549
|
+
serviceName: this.otelServiceName,
|
|
4550
|
+
endpoint: this.otelEndpoint,
|
|
4551
|
+
entity: "consumer",
|
|
4552
|
+
name
|
|
4553
|
+
};
|
|
4554
|
+
if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
|
|
4555
|
+
return withProvisioningSpan(
|
|
4556
|
+
this.otel,
|
|
4557
|
+
{ ...spanAttrs, action: "bind" },
|
|
4558
|
+
() => this.binder.bindConsumer(jsm, kind)
|
|
4559
|
+
);
|
|
4560
|
+
}
|
|
4350
4561
|
return withProvisioningSpan(
|
|
4351
4562
|
this.otel,
|
|
4352
4563
|
{
|
|
4353
|
-
|
|
4354
|
-
endpoint: this.otelEndpoint,
|
|
4355
|
-
entity: "consumer",
|
|
4356
|
-
name,
|
|
4564
|
+
...spanAttrs,
|
|
4357
4565
|
action: "ensure"
|
|
4358
4566
|
},
|
|
4359
4567
|
async () => {
|
|
@@ -4374,28 +4582,40 @@ var ConsumerProvider = class {
|
|
|
4374
4582
|
}
|
|
4375
4583
|
/**
|
|
4376
4584
|
* Recover a consumer that disappeared during runtime.
|
|
4377
|
-
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
4378
|
-
*
|
|
4379
|
-
* If a migration backup stream exists, another pod is mid-migration — we
|
|
4380
|
-
* throw so the self-healing retry loop waits with backoff until migration
|
|
4381
|
-
* completes and the backup is cleaned up.
|
|
4382
4585
|
*
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4586
|
+
* Self-healing path: creates if missing but never updates config, so an old pod
|
|
4587
|
+
* cannot overwrite a newer pod's config during rolling updates. If a migration
|
|
4588
|
+
* backup stream exists, throws so the retry loop backs off until migration completes;
|
|
4589
|
+
* creating a consumer mid-migration would eat workqueue messages being restored.
|
|
4387
4590
|
*/
|
|
4388
4591
|
async recoverConsumer(jsm, kind) {
|
|
4389
4592
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4390
4593
|
const config = this.buildConfig(kind);
|
|
4391
4594
|
const name = config.durable_name;
|
|
4595
|
+
const spanAttrs = {
|
|
4596
|
+
serviceName: this.otelServiceName,
|
|
4597
|
+
endpoint: this.otelEndpoint,
|
|
4598
|
+
entity: "consumer",
|
|
4599
|
+
name
|
|
4600
|
+
};
|
|
4601
|
+
if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
|
|
4602
|
+
return withProvisioningSpan(this.otel, { ...spanAttrs, action: "rebind" }, async () => {
|
|
4603
|
+
try {
|
|
4604
|
+
return await jsm.consumers.info(stream, name);
|
|
4605
|
+
} catch (err) {
|
|
4606
|
+
if (err instanceof JetStreamApiError3 && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
|
|
4607
|
+
throw new Error(
|
|
4608
|
+
`Consumer ${name} on ${stream} is externally managed and currently absent; waiting for it to be restored.`
|
|
4609
|
+
);
|
|
4610
|
+
}
|
|
4611
|
+
throw err;
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4392
4615
|
return withProvisioningSpan(
|
|
4393
4616
|
this.otel,
|
|
4394
4617
|
{
|
|
4395
|
-
|
|
4396
|
-
endpoint: this.otelEndpoint,
|
|
4397
|
-
entity: "consumer",
|
|
4398
|
-
name,
|
|
4618
|
+
...spanAttrs,
|
|
4399
4619
|
action: "recover"
|
|
4400
4620
|
},
|
|
4401
4621
|
async () => {
|
|
@@ -4431,9 +4651,7 @@ var ConsumerProvider = class {
|
|
|
4431
4651
|
throw err;
|
|
4432
4652
|
}
|
|
4433
4653
|
}
|
|
4434
|
-
/**
|
|
4435
|
-
* Create a consumer, handling the race where another pod creates it first.
|
|
4436
|
-
*/
|
|
4654
|
+
/** Create a consumer, handling the race where another pod creates it first. */
|
|
4437
4655
|
async createConsumer(jsm, stream, name, kind, config) {
|
|
4438
4656
|
this.logger.log(`Creating consumer: ${name}`);
|
|
4439
4657
|
const ctx = { entity: "consumer", name, kind };
|
|
@@ -4461,10 +4679,8 @@ var ConsumerProvider = class {
|
|
|
4461
4679
|
}
|
|
4462
4680
|
}
|
|
4463
4681
|
/** Build consumer config by merging defaults with user overrides. */
|
|
4464
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
4465
4682
|
buildConfig(kind) {
|
|
4466
|
-
const
|
|
4467
|
-
const serviceName = internalName(this.options.name);
|
|
4683
|
+
const durableName = this.getConsumerName(kind);
|
|
4468
4684
|
const defaults = this.getDefaults(kind);
|
|
4469
4685
|
const overrides = this.getOverrides(kind);
|
|
4470
4686
|
if (kind === "broadcast" /* Broadcast */) {
|
|
@@ -4476,31 +4692,54 @@ var ConsumerProvider = class {
|
|
|
4476
4692
|
return {
|
|
4477
4693
|
...defaults,
|
|
4478
4694
|
...overrides,
|
|
4479
|
-
name,
|
|
4480
|
-
durable_name:
|
|
4695
|
+
name: durableName,
|
|
4696
|
+
durable_name: durableName,
|
|
4481
4697
|
filter_subject: broadcastPatterns[0]
|
|
4482
4698
|
};
|
|
4483
4699
|
}
|
|
4484
4700
|
return {
|
|
4485
4701
|
...defaults,
|
|
4486
4702
|
...overrides,
|
|
4487
|
-
name,
|
|
4488
|
-
durable_name:
|
|
4703
|
+
name: durableName,
|
|
4704
|
+
durable_name: durableName,
|
|
4489
4705
|
filter_subjects: broadcastPatterns
|
|
4490
4706
|
};
|
|
4491
4707
|
}
|
|
4492
4708
|
if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
|
|
4493
4709
|
throw new Error(`Unexpected durable consumer kind: ${kind}`);
|
|
4494
4710
|
}
|
|
4495
|
-
|
|
4711
|
+
if (this.names.hasCustomPrefix(kind)) {
|
|
4712
|
+
return this.buildCustomPrefixConfig(kind, durableName, defaults, overrides);
|
|
4713
|
+
}
|
|
4714
|
+
const filter_subject = this.names.filterSubject(kind);
|
|
4496
4715
|
return {
|
|
4497
4716
|
...defaults,
|
|
4498
4717
|
...overrides,
|
|
4499
|
-
name,
|
|
4500
|
-
durable_name:
|
|
4718
|
+
name: durableName,
|
|
4719
|
+
durable_name: durableName,
|
|
4501
4720
|
filter_subject
|
|
4502
4721
|
};
|
|
4503
4722
|
}
|
|
4723
|
+
buildCustomPrefixConfig(kind, durableName, defaults, overrides) {
|
|
4724
|
+
const patterns = kind === "ev" /* Event */ ? this.patternRegistry.getEventPatterns() : this.patternRegistry.getCommandPatterns();
|
|
4725
|
+
const subjects = patterns.map((p) => this.names.subject(kind, p));
|
|
4726
|
+
if (subjects.length === 1) {
|
|
4727
|
+
return {
|
|
4728
|
+
...defaults,
|
|
4729
|
+
...overrides,
|
|
4730
|
+
name: durableName,
|
|
4731
|
+
durable_name: durableName,
|
|
4732
|
+
filter_subject: subjects[0]
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
return {
|
|
4736
|
+
...defaults,
|
|
4737
|
+
...overrides,
|
|
4738
|
+
name: durableName,
|
|
4739
|
+
durable_name: durableName,
|
|
4740
|
+
filter_subjects: subjects
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4504
4743
|
/** Get default config for a consumer kind. */
|
|
4505
4744
|
getDefaults(kind) {
|
|
4506
4745
|
switch (kind) {
|
|
@@ -4521,26 +4760,12 @@ var ConsumerProvider = class {
|
|
|
4521
4760
|
}
|
|
4522
4761
|
/** Get user-provided overrides for a consumer kind. */
|
|
4523
4762
|
getOverrides(kind) {
|
|
4524
|
-
|
|
4525
|
-
case "ev" /* Event */:
|
|
4526
|
-
return this.options.events?.consumer ?? {};
|
|
4527
|
-
case "cmd" /* Command */:
|
|
4528
|
-
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
4529
|
-
case "broadcast" /* Broadcast */:
|
|
4530
|
-
return this.options.broadcast?.consumer ?? {};
|
|
4531
|
-
case "ordered" /* Ordered */:
|
|
4532
|
-
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
4533
|
-
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
4534
|
-
default: {
|
|
4535
|
-
const _exhaustive = kind;
|
|
4536
|
-
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
4537
|
-
}
|
|
4538
|
-
}
|
|
4763
|
+
return kindOptionsBlock(this.options, kind)?.consumer ?? {};
|
|
4539
4764
|
}
|
|
4540
4765
|
};
|
|
4541
4766
|
|
|
4542
4767
|
// src/server/infrastructure/message.provider.ts
|
|
4543
|
-
import { Logger as
|
|
4768
|
+
import { Logger as Logger16 } from "@nestjs/common";
|
|
4544
4769
|
import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
|
|
4545
4770
|
import {
|
|
4546
4771
|
catchError,
|
|
@@ -4559,7 +4784,7 @@ var MessageProvider = class {
|
|
|
4559
4784
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
4560
4785
|
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
4561
4786
|
}
|
|
4562
|
-
logger = new
|
|
4787
|
+
logger = new Logger16("Jetstream:Message");
|
|
4563
4788
|
activeIterators = /* @__PURE__ */ new Set();
|
|
4564
4789
|
orderedReadyResolve = null;
|
|
4565
4790
|
orderedReadyReject = null;
|
|
@@ -4602,7 +4827,7 @@ var MessageProvider = class {
|
|
|
4602
4827
|
/**
|
|
4603
4828
|
* Start an ordered consumer for strict sequential delivery.
|
|
4604
4829
|
*
|
|
4605
|
-
* Unlike durable consumers, ordered consumers are ephemeral
|
|
4830
|
+
* Unlike durable consumers, ordered consumers are ephemeral: created at
|
|
4606
4831
|
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
4607
4832
|
*
|
|
4608
4833
|
* @param streamName - JetStream stream to consume from.
|
|
@@ -4816,7 +5041,7 @@ var MessageProvider = class {
|
|
|
4816
5041
|
};
|
|
4817
5042
|
|
|
4818
5043
|
// src/server/infrastructure/metadata.provider.ts
|
|
4819
|
-
import { Logger as
|
|
5044
|
+
import { Logger as Logger17 } from "@nestjs/common";
|
|
4820
5045
|
import { Kvm } from "@nats-io/kv";
|
|
4821
5046
|
var MetadataProvider = class {
|
|
4822
5047
|
constructor(options, connection) {
|
|
@@ -4825,7 +5050,7 @@ var MetadataProvider = class {
|
|
|
4825
5050
|
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
4826
5051
|
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
4827
5052
|
}
|
|
4828
|
-
logger = new
|
|
5053
|
+
logger = new Logger17("Jetstream:Metadata");
|
|
4829
5054
|
bucketName;
|
|
4830
5055
|
replicas;
|
|
4831
5056
|
ttl;
|
|
@@ -4833,16 +5058,12 @@ var MetadataProvider = class {
|
|
|
4833
5058
|
heartbeatTimer;
|
|
4834
5059
|
cachedKv;
|
|
4835
5060
|
/**
|
|
4836
|
-
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
5061
|
+
* Write handler metadata entries to the KV bucket and start the heartbeat.
|
|
4837
5062
|
*
|
|
4838
|
-
* Creates the bucket if
|
|
4839
|
-
*
|
|
4840
|
-
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
4841
|
-
* to prevent TTL expiry while the pod is alive.
|
|
5063
|
+
* Creates the bucket if missing; skips silently when the map is empty.
|
|
5064
|
+
* Non-critical: errors are logged but do not prevent transport startup.
|
|
4842
5065
|
*
|
|
4843
|
-
*
|
|
4844
|
-
*
|
|
4845
|
-
* @param entries Map of KV key → metadata object.
|
|
5066
|
+
* @param entries Map of KV key to metadata object.
|
|
4846
5067
|
*/
|
|
4847
5068
|
async publish(entries) {
|
|
4848
5069
|
if (entries.size === 0) return;
|
|
@@ -4918,396 +5139,626 @@ var MetadataProvider = class {
|
|
|
4918
5139
|
};
|
|
4919
5140
|
|
|
4920
5141
|
// src/server/routing/event.router.ts
|
|
4921
|
-
import { Logger as
|
|
5142
|
+
import { Logger as Logger19 } from "@nestjs/common";
|
|
5143
|
+
|
|
5144
|
+
// src/server/routing/concurrency-gate.ts
|
|
5145
|
+
var BACKLOG_WARN_THRESHOLD = 1e3;
|
|
5146
|
+
var ConcurrencyGate = class {
|
|
5147
|
+
constructor(maxActive, route, parkTimer, logger5, label) {
|
|
5148
|
+
this.maxActive = maxActive;
|
|
5149
|
+
this.route = route;
|
|
5150
|
+
this.parkTimer = parkTimer;
|
|
5151
|
+
this.logger = logger5;
|
|
5152
|
+
this.label = label;
|
|
5153
|
+
}
|
|
5154
|
+
active = 0;
|
|
5155
|
+
backlogWarned = false;
|
|
5156
|
+
backlog = [];
|
|
5157
|
+
/** Entry point for each incoming message. */
|
|
5158
|
+
push(msg) {
|
|
5159
|
+
if (this.active >= this.maxActive) {
|
|
5160
|
+
this.backlog.push({
|
|
5161
|
+
msg,
|
|
5162
|
+
stopAckExtension: this.parkTimer ? this.parkTimer(msg) : null
|
|
5163
|
+
});
|
|
5164
|
+
if (!this.backlogWarned && this.backlog.length >= BACKLOG_WARN_THRESHOLD) {
|
|
5165
|
+
this.backlogWarned = true;
|
|
5166
|
+
this.logger.warn(
|
|
5167
|
+
`${this.label} backlog reached ${this.backlog.length} messages; consumer may be falling behind`
|
|
5168
|
+
);
|
|
5169
|
+
}
|
|
5170
|
+
return;
|
|
5171
|
+
}
|
|
5172
|
+
this.active++;
|
|
5173
|
+
const result = this.routeSafely(msg);
|
|
5174
|
+
if (result !== void 0) {
|
|
5175
|
+
this.trackAsync(result, msg);
|
|
5176
|
+
} else {
|
|
5177
|
+
this.active--;
|
|
5178
|
+
if (this.backlog.length > 0) this.drainBacklog();
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
/** Stop parked timers and drop the backlog. */
|
|
5182
|
+
dispose() {
|
|
5183
|
+
for (const queued of this.backlog) {
|
|
5184
|
+
queued.stopAckExtension?.();
|
|
5185
|
+
}
|
|
5186
|
+
this.backlog.length = 0;
|
|
5187
|
+
}
|
|
5188
|
+
onAsyncDone = () => {
|
|
5189
|
+
this.active--;
|
|
5190
|
+
this.drainBacklog();
|
|
5191
|
+
};
|
|
5192
|
+
/** A throw here must not leak the concurrency slot or kill the subscription. */
|
|
5193
|
+
routeSafely(msg) {
|
|
5194
|
+
try {
|
|
5195
|
+
return this.route(msg);
|
|
5196
|
+
} catch (err) {
|
|
5197
|
+
this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5198
|
+
return void 0;
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
trackAsync(result, msg) {
|
|
5202
|
+
void result.catch((err) => {
|
|
5203
|
+
this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5204
|
+
}).finally(this.onAsyncDone);
|
|
5205
|
+
}
|
|
5206
|
+
drainBacklog() {
|
|
5207
|
+
while (this.active < this.maxActive) {
|
|
5208
|
+
const next = this.backlog.shift();
|
|
5209
|
+
if (next === void 0) break;
|
|
5210
|
+
next.stopAckExtension?.();
|
|
5211
|
+
this.active++;
|
|
5212
|
+
const result = this.routeSafely(next.msg);
|
|
5213
|
+
if (result !== void 0) {
|
|
5214
|
+
this.trackAsync(result, next.msg);
|
|
5215
|
+
} else {
|
|
5216
|
+
this.active--;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
if (this.backlog.length < BACKLOG_WARN_THRESHOLD) this.backlogWarned = false;
|
|
5220
|
+
}
|
|
5221
|
+
};
|
|
5222
|
+
|
|
5223
|
+
// src/server/routing/dead-letter-capture.ts
|
|
5224
|
+
import { Logger as Logger18 } from "@nestjs/common";
|
|
4922
5225
|
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
4923
5226
|
var DLQ_PUBLISH_ATTEMPTS = 3;
|
|
4924
|
-
var
|
|
4925
|
-
|
|
4926
|
-
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
4927
|
-
return "event" /* Event */;
|
|
4928
|
-
};
|
|
4929
|
-
var EventRouter = class {
|
|
4930
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
4931
|
-
this.messageProvider = messageProvider;
|
|
5227
|
+
var DeadLetterCapture = class {
|
|
5228
|
+
constructor(patternRegistry, eventBus, deadLetterConfig, otel, serviceName, serverEndpoint, connection, options, names) {
|
|
4932
5229
|
this.patternRegistry = patternRegistry;
|
|
4933
|
-
this.codec = codec;
|
|
4934
5230
|
this.eventBus = eventBus;
|
|
4935
5231
|
this.deadLetterConfig = deadLetterConfig;
|
|
4936
|
-
this.
|
|
4937
|
-
this.
|
|
5232
|
+
this.otel = otel;
|
|
5233
|
+
this.serviceName = serviceName;
|
|
5234
|
+
this.serverEndpoint = serverEndpoint;
|
|
4938
5235
|
this.connection = connection;
|
|
4939
5236
|
this.options = options;
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5237
|
+
this.names = names;
|
|
5238
|
+
}
|
|
5239
|
+
logger = new Logger18("Jetstream:DeadLetter");
|
|
5240
|
+
/** True when this delivery is the consumer's last attempt for the message. */
|
|
5241
|
+
isFinalDelivery(msg) {
|
|
5242
|
+
const maxDeliver = this.deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
5243
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
5244
|
+
return msg.info.deliveryCount >= maxDeliver;
|
|
5245
|
+
}
|
|
5246
|
+
/** Emit the dead-letter event and route the message to the DLQ or the fallback callback. */
|
|
5247
|
+
async capture(msg, data, error) {
|
|
5248
|
+
const info = {
|
|
5249
|
+
subject: msg.subject,
|
|
5250
|
+
data,
|
|
5251
|
+
headers: msg.headers,
|
|
5252
|
+
error,
|
|
5253
|
+
deliveryCount: msg.info.deliveryCount,
|
|
5254
|
+
stream: msg.info.stream,
|
|
5255
|
+
streamSequence: msg.info.streamSequence,
|
|
5256
|
+
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
5257
|
+
};
|
|
5258
|
+
await withDeadLetterSpan(
|
|
5259
|
+
{
|
|
5260
|
+
msg,
|
|
5261
|
+
// Surface the registered pattern so APM can filter dead letters by
|
|
5262
|
+
// handler; falls back to the raw subject when no handler matches.
|
|
5263
|
+
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
5264
|
+
finalDeliveryCount: msg.info.deliveryCount,
|
|
5265
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
5266
|
+
serviceName: this.serviceName,
|
|
5267
|
+
endpoint: this.serverEndpoint
|
|
5268
|
+
},
|
|
5269
|
+
this.otel,
|
|
5270
|
+
async () => {
|
|
5271
|
+
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
5272
|
+
if (!this.options?.dlq) {
|
|
5273
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5274
|
+
} else {
|
|
5275
|
+
await this.publishToDlq(msg, info, error);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
);
|
|
4950
5279
|
}
|
|
4951
|
-
logger = new Logger17("Jetstream:EventRouter");
|
|
4952
|
-
subscriptions = [];
|
|
4953
|
-
otel;
|
|
4954
|
-
serviceName;
|
|
4955
|
-
serverEndpoint;
|
|
4956
5280
|
/**
|
|
4957
|
-
*
|
|
4958
|
-
*
|
|
5281
|
+
* Publish the dead letter to the DLQ stream with diagnostic headers. On
|
|
5282
|
+
* success the `onDeadLetter` callback is notified and the message termed;
|
|
5283
|
+
* on failure everything falls back to the callback to avoid silent loss.
|
|
4959
5284
|
*/
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
this.
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
4969
|
-
this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
|
|
5285
|
+
async publishToDlq(msg, info, error) {
|
|
5286
|
+
const serviceName = this.options?.name;
|
|
5287
|
+
if (!this.connection || !serviceName) {
|
|
5288
|
+
this.logger.error(
|
|
5289
|
+
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
5290
|
+
);
|
|
5291
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5292
|
+
return;
|
|
4970
5293
|
}
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
5294
|
+
const dlqStreamOverride = this.options.dlq?.stream?.name;
|
|
5295
|
+
const destinationSubject = this.names ? this.names.dlqStreamName() : dlqStreamOverride ?? dlqStreamName(serviceName);
|
|
5296
|
+
const hdrs = this.buildDlqHeaders(msg);
|
|
5297
|
+
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, this.extractErrorReason(error));
|
|
5298
|
+
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
5299
|
+
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
5300
|
+
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
5301
|
+
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
5302
|
+
try {
|
|
5303
|
+
await this.publishWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
5304
|
+
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
5305
|
+
await this.notifyDeadLetterCallback(info, msg);
|
|
5306
|
+
} catch (publishErr) {
|
|
5307
|
+
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
5308
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
4976
5309
|
}
|
|
4977
|
-
this.subscriptions.length = 0;
|
|
4978
5310
|
}
|
|
4979
|
-
/**
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
4996
|
-
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
4997
|
-
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
4998
|
-
const pattern = declared?.pattern ?? msg.subject;
|
|
4999
|
-
const declaredKind = declared?.kind ?? kind;
|
|
5000
|
-
const durationMs = performance.now() - startedAt;
|
|
5001
|
-
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
5002
|
-
};
|
|
5003
|
-
const isDeadLetter = (msg) => {
|
|
5004
|
-
if (!hasDlqCheck) return false;
|
|
5005
|
-
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
5006
|
-
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
5007
|
-
return msg.info.deliveryCount >= maxDeliver;
|
|
5008
|
-
};
|
|
5009
|
-
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
5010
|
-
const settleSuccess = (msg, ctx, data) => {
|
|
5011
|
-
if (ctx.shouldTerminate) {
|
|
5012
|
-
settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
|
|
5013
|
-
msg.term(ctx.terminateReason);
|
|
5014
|
-
});
|
|
5015
|
-
return void 0;
|
|
5016
|
-
}
|
|
5017
|
-
if (ctx.shouldRetry) {
|
|
5018
|
-
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
5019
|
-
return handleDeadLetter(
|
|
5020
|
-
msg,
|
|
5021
|
-
data,
|
|
5022
|
-
new Error("Retry requested on the final delivery attempt")
|
|
5311
|
+
/**
|
|
5312
|
+
* Past max_deliver the server never redelivers, so these in-process attempts
|
|
5313
|
+
* are the only second chance a dead letter gets. No artificial delay: an
|
|
5314
|
+
* unreachable broker already spaces attempts via its own request timeout.
|
|
5315
|
+
*/
|
|
5316
|
+
async publishWithRetry(connection, subject, data, headers2) {
|
|
5317
|
+
let lastErr;
|
|
5318
|
+
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5319
|
+
try {
|
|
5320
|
+
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5321
|
+
return;
|
|
5322
|
+
} catch (err) {
|
|
5323
|
+
lastErr = err;
|
|
5324
|
+
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5325
|
+
this.logger.warn(
|
|
5326
|
+
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5023
5327
|
);
|
|
5024
5328
|
}
|
|
5025
|
-
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5026
|
-
msg.nak(ctx.retryDelay);
|
|
5027
|
-
});
|
|
5028
|
-
return void 0;
|
|
5029
5329
|
}
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5330
|
+
}
|
|
5331
|
+
throw lastErr;
|
|
5332
|
+
}
|
|
5333
|
+
/**
|
|
5334
|
+
* Copy headers for the DLQ republish, dropping NATS control headers: a
|
|
5335
|
+
* copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
|
|
5336
|
+
*/
|
|
5337
|
+
buildDlqHeaders(msg) {
|
|
5338
|
+
const hdrs = natsHeaders3();
|
|
5339
|
+
if (!msg.headers) return hdrs;
|
|
5340
|
+
for (const [k, v] of msg.headers) {
|
|
5341
|
+
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5342
|
+
for (const val of v) {
|
|
5343
|
+
hdrs.append(k, val);
|
|
5039
5344
|
}
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
let data;
|
|
5345
|
+
}
|
|
5346
|
+
return hdrs;
|
|
5347
|
+
}
|
|
5348
|
+
async notifyDeadLetterCallback(info, msg) {
|
|
5349
|
+
if (this.deadLetterConfig.onDeadLetter) {
|
|
5046
5350
|
try {
|
|
5047
|
-
|
|
5048
|
-
} catch {
|
|
5049
|
-
|
|
5351
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
5352
|
+
} catch (hookErr) {
|
|
5353
|
+
this.logger.warn(
|
|
5354
|
+
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
5355
|
+
hookErr
|
|
5356
|
+
);
|
|
5050
5357
|
}
|
|
5051
|
-
|
|
5052
|
-
|
|
5358
|
+
}
|
|
5359
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5360
|
+
msg.term("Moved to DLQ stream");
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
/**
|
|
5364
|
+
* Last resort: invoke onDeadLetter, then term on success. On failure the
|
|
5365
|
+
* message is nak'd: never redelivered past max_deliver, but preserved.
|
|
5366
|
+
*/
|
|
5367
|
+
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
5368
|
+
const onDeadLetter = this.deadLetterConfig.onDeadLetter;
|
|
5369
|
+
if (!onDeadLetter) {
|
|
5370
|
+
this.logger.error(
|
|
5371
|
+
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback); leaving the message in the stream`
|
|
5372
|
+
);
|
|
5373
|
+
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5374
|
+
msg.nak();
|
|
5053
5375
|
});
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5376
|
+
return;
|
|
5377
|
+
}
|
|
5378
|
+
try {
|
|
5379
|
+
await onDeadLetter(info);
|
|
5380
|
+
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5381
|
+
msg.term("Dead letter processed via fallback callback");
|
|
5382
|
+
});
|
|
5383
|
+
} catch (hookErr) {
|
|
5384
|
+
this.logger.error(
|
|
5385
|
+
`Fallback onDeadLetter callback failed for ${msg.subject}; the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
|
|
5386
|
+
hookErr
|
|
5387
|
+
);
|
|
5388
|
+
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5389
|
+
msg.nak();
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
extractErrorReason(error) {
|
|
5394
|
+
if (error instanceof Error) {
|
|
5395
|
+
return error.message;
|
|
5396
|
+
}
|
|
5397
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
5398
|
+
return String(error.message);
|
|
5399
|
+
}
|
|
5400
|
+
return String(error);
|
|
5401
|
+
}
|
|
5402
|
+
};
|
|
5403
|
+
|
|
5404
|
+
// src/server/routing/settlement.ts
|
|
5405
|
+
var statusForContext = (ctx) => {
|
|
5406
|
+
if (ctx.shouldTerminate) return "terminated";
|
|
5407
|
+
if (ctx.shouldRetry) return "retried";
|
|
5408
|
+
return "success";
|
|
5409
|
+
};
|
|
5410
|
+
var createSettlement = (logger5, capture) => {
|
|
5411
|
+
const settleSuccess = (msg, ctx, data) => {
|
|
5412
|
+
if (ctx.shouldTerminate) {
|
|
5413
|
+
settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
|
|
5414
|
+
msg.term(ctx.terminateReason);
|
|
5415
|
+
});
|
|
5416
|
+
return void 0;
|
|
5417
|
+
}
|
|
5418
|
+
if (ctx.shouldRetry) {
|
|
5419
|
+
if (capture?.isFinalDelivery(msg)) {
|
|
5420
|
+
return capture.capture(
|
|
5421
|
+
msg,
|
|
5422
|
+
data,
|
|
5423
|
+
new Error("Retry requested on the final delivery attempt")
|
|
5424
|
+
);
|
|
5425
|
+
}
|
|
5426
|
+
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5427
|
+
msg.nak(ctx.retryDelay);
|
|
5428
|
+
});
|
|
5429
|
+
return void 0;
|
|
5430
|
+
}
|
|
5431
|
+
settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
|
|
5432
|
+
msg.ack();
|
|
5433
|
+
});
|
|
5434
|
+
return void 0;
|
|
5435
|
+
};
|
|
5436
|
+
const settleFailure = async (msg, data, err) => {
|
|
5437
|
+
if (capture?.isFinalDelivery(msg)) {
|
|
5438
|
+
await capture.capture(msg, data, err);
|
|
5439
|
+
return;
|
|
5440
|
+
}
|
|
5441
|
+
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5442
|
+
msg.nak();
|
|
5443
|
+
});
|
|
5444
|
+
};
|
|
5445
|
+
return { settleSuccess, settleFailure };
|
|
5446
|
+
};
|
|
5447
|
+
|
|
5448
|
+
// src/server/routing/event-pipeline.ts
|
|
5449
|
+
var createHandlerReporter = (rctx) => {
|
|
5450
|
+
const { eventBus, patternRegistry, kind } = rctx;
|
|
5451
|
+
return (msg, startedAt, status) => {
|
|
5452
|
+
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
5453
|
+
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
5454
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
5455
|
+
const declaredKind = declared?.kind ?? kind;
|
|
5456
|
+
const durationMs = performance.now() - startedAt;
|
|
5457
|
+
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
5458
|
+
};
|
|
5459
|
+
};
|
|
5460
|
+
var createEventResolver = (rctx) => {
|
|
5461
|
+
const { patternRegistry, codec, eventBus, logger: logger5, capture, kind } = rctx;
|
|
5462
|
+
const captureUnroutable = (activeCapture, msg, err) => {
|
|
5463
|
+
let data;
|
|
5464
|
+
try {
|
|
5465
|
+
data = codec.decode(msg.data);
|
|
5466
|
+
} catch {
|
|
5467
|
+
data = void 0;
|
|
5468
|
+
}
|
|
5469
|
+
return activeCapture.capture(msg, data, err).catch((captureErr) => {
|
|
5470
|
+
logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
|
|
5471
|
+
});
|
|
5472
|
+
};
|
|
5473
|
+
return (msg) => {
|
|
5474
|
+
const subject = msg.subject;
|
|
5475
|
+
try {
|
|
5476
|
+
const handler = patternRegistry.getHandler(subject);
|
|
5477
|
+
if (!handler) {
|
|
5478
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
5479
|
+
if (capture !== null) {
|
|
5480
|
+
return captureUnroutable(capture, msg, new Error(`No handler for event: ${subject}`));
|
|
5085
5481
|
}
|
|
5086
|
-
|
|
5087
|
-
return
|
|
5482
|
+
msg.term(`No handler for event: ${subject}`);
|
|
5483
|
+
return null;
|
|
5484
|
+
}
|
|
5485
|
+
let data;
|
|
5486
|
+
try {
|
|
5487
|
+
data = codec.decode(msg.data);
|
|
5088
5488
|
} catch (err) {
|
|
5089
|
-
logger5.error(`
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5489
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
5490
|
+
if (capture !== null) {
|
|
5491
|
+
return captureUnroutable(
|
|
5492
|
+
capture,
|
|
5493
|
+
msg,
|
|
5494
|
+
new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
|
|
5495
|
+
);
|
|
5094
5496
|
}
|
|
5497
|
+
msg.term("Decode error");
|
|
5095
5498
|
return null;
|
|
5096
5499
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
return "success";
|
|
5102
|
-
};
|
|
5103
|
-
const handleSafe = (msg) => {
|
|
5104
|
-
const resolved = resolveEvent(msg);
|
|
5105
|
-
if (resolved === null) return void 0;
|
|
5106
|
-
if (isPromiseLike2(resolved)) return resolved;
|
|
5107
|
-
const { handler, data } = resolved;
|
|
5108
|
-
const ctx = new RpcContext([msg]);
|
|
5109
|
-
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5110
|
-
const startedAt = performance.now();
|
|
5111
|
-
let pending;
|
|
5500
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5501
|
+
return { handler, data };
|
|
5502
|
+
} catch (err) {
|
|
5503
|
+
logger5.error(`Unexpected error in ${kind} event router`, err);
|
|
5112
5504
|
try {
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5505
|
+
msg.term("Unexpected router error");
|
|
5506
|
+
} catch (termErr) {
|
|
5507
|
+
logger5.error(`Failed to terminate message ${subject}:`, termErr);
|
|
5508
|
+
}
|
|
5509
|
+
return null;
|
|
5510
|
+
}
|
|
5511
|
+
};
|
|
5512
|
+
};
|
|
5513
|
+
var createWorkqueuePipeline = (rctx) => {
|
|
5514
|
+
const { kind, spanKind, logger: logger5, eventBus, otel, serviceName, serverEndpoint } = rctx;
|
|
5515
|
+
const { ackExtensionInterval } = rctx;
|
|
5516
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
5517
|
+
const reportHandlerCompleted = createHandlerReporter(rctx);
|
|
5518
|
+
const resolveEvent = createEventResolver(rctx);
|
|
5519
|
+
const { settleSuccess, settleFailure } = createSettlement(logger5, rctx.capture);
|
|
5520
|
+
return (msg) => {
|
|
5521
|
+
const resolved = resolveEvent(msg);
|
|
5522
|
+
if (resolved === null) return void 0;
|
|
5523
|
+
if (isPromiseLike2(resolved)) return resolved;
|
|
5524
|
+
const { handler, data } = resolved;
|
|
5525
|
+
const ctx = new RpcContext([msg]);
|
|
5526
|
+
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5527
|
+
const startedAt = performance.now();
|
|
5528
|
+
let pending;
|
|
5529
|
+
try {
|
|
5530
|
+
pending = withConsumeSpan(
|
|
5531
|
+
{
|
|
5532
|
+
subject: msg.subject,
|
|
5533
|
+
msg,
|
|
5534
|
+
info: msg.info,
|
|
5535
|
+
kind: spanKind,
|
|
5536
|
+
payloadBytes: msg.data.length,
|
|
5537
|
+
handlerMetadata: { pattern: msg.subject },
|
|
5538
|
+
serviceName,
|
|
5539
|
+
endpoint: serverEndpoint
|
|
5540
|
+
},
|
|
5541
|
+
otel,
|
|
5542
|
+
() => unwrapResult(handler(data, ctx))
|
|
5543
|
+
);
|
|
5544
|
+
} catch (err) {
|
|
5545
|
+
eventBus.emit(
|
|
5546
|
+
"error" /* Error */,
|
|
5547
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
5548
|
+
`${kind}-handler:${msg.subject}`
|
|
5549
|
+
);
|
|
5550
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
5551
|
+
return settleFailure(msg, data, err).finally(() => {
|
|
5552
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5553
|
+
});
|
|
5554
|
+
}
|
|
5555
|
+
if (!isPromiseLike2(pending)) {
|
|
5556
|
+
const settled = settleSuccess(msg, ctx, data);
|
|
5557
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5558
|
+
if (settled === void 0) {
|
|
5559
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5560
|
+
return void 0;
|
|
5561
|
+
}
|
|
5562
|
+
return settled.finally(() => {
|
|
5563
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5564
|
+
});
|
|
5565
|
+
}
|
|
5566
|
+
return pending.then(
|
|
5567
|
+
async () => {
|
|
5568
|
+
try {
|
|
5569
|
+
await settleSuccess(msg, ctx, data);
|
|
5570
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5571
|
+
} finally {
|
|
5572
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
5573
|
+
}
|
|
5574
|
+
},
|
|
5575
|
+
async (err) => {
|
|
5128
5576
|
eventBus.emit(
|
|
5129
5577
|
"error" /* Error */,
|
|
5130
5578
|
err instanceof Error ? err : new Error(String(err)),
|
|
5131
5579
|
`${kind}-handler:${msg.subject}`
|
|
5132
5580
|
);
|
|
5133
5581
|
reportHandlerCompleted(msg, startedAt, "error");
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
}
|
|
5137
|
-
}
|
|
5138
|
-
if (!isPromiseLike2(pending)) {
|
|
5139
|
-
const settled = settleSuccess(msg, ctx, data);
|
|
5140
|
-
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5141
|
-
if (settled === void 0) {
|
|
5582
|
+
try {
|
|
5583
|
+
await settleFailure(msg, data, err);
|
|
5584
|
+
} finally {
|
|
5142
5585
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5143
|
-
return void 0;
|
|
5144
5586
|
}
|
|
5145
|
-
return settled.finally(() => {
|
|
5146
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5147
|
-
});
|
|
5148
5587
|
}
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5165
|
-
try {
|
|
5166
|
-
await settleFailure(msg, data, err);
|
|
5167
|
-
} finally {
|
|
5168
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5169
|
-
}
|
|
5170
|
-
}
|
|
5171
|
-
);
|
|
5172
|
-
};
|
|
5173
|
-
const handleOrderedSafe = (msg) => {
|
|
5174
|
-
const subject = msg.subject;
|
|
5175
|
-
let handler;
|
|
5176
|
-
let data;
|
|
5177
|
-
try {
|
|
5178
|
-
handler = patternRegistry.getHandler(subject);
|
|
5179
|
-
if (!handler) {
|
|
5180
|
-
logger5.error(`No handler for subject: ${subject}`);
|
|
5181
|
-
return void 0;
|
|
5182
|
-
}
|
|
5183
|
-
try {
|
|
5184
|
-
data = codec.decode(msg.data);
|
|
5185
|
-
} catch (err) {
|
|
5186
|
-
logger5.error(`Decode error for ${subject}:`, err);
|
|
5187
|
-
return void 0;
|
|
5188
|
-
}
|
|
5189
|
-
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5190
|
-
} catch (err) {
|
|
5191
|
-
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5588
|
+
);
|
|
5589
|
+
};
|
|
5590
|
+
};
|
|
5591
|
+
var createOrderedPipeline = (rctx) => {
|
|
5592
|
+
const { spanKind, codec, logger: logger5, eventBus, patternRegistry, otel } = rctx;
|
|
5593
|
+
const { serviceName, serverEndpoint } = rctx;
|
|
5594
|
+
const reportHandlerCompleted = createHandlerReporter(rctx);
|
|
5595
|
+
return (msg) => {
|
|
5596
|
+
const subject = msg.subject;
|
|
5597
|
+
let handler;
|
|
5598
|
+
let data;
|
|
5599
|
+
try {
|
|
5600
|
+
handler = patternRegistry.getHandler(subject);
|
|
5601
|
+
if (!handler) {
|
|
5602
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
5192
5603
|
return void 0;
|
|
5193
5604
|
}
|
|
5194
|
-
const ctx = new RpcContext([msg]);
|
|
5195
|
-
const warnIfSettlementAttempted = () => {
|
|
5196
|
-
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
5197
|
-
logger5.warn(
|
|
5198
|
-
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
5199
|
-
);
|
|
5200
|
-
}
|
|
5201
|
-
};
|
|
5202
|
-
const startedAt = performance.now();
|
|
5203
|
-
let pending;
|
|
5204
5605
|
try {
|
|
5205
|
-
|
|
5206
|
-
{
|
|
5207
|
-
subject: msg.subject,
|
|
5208
|
-
msg,
|
|
5209
|
-
info: msg.info,
|
|
5210
|
-
kind: spanKind,
|
|
5211
|
-
payloadBytes: msg.data.length,
|
|
5212
|
-
handlerMetadata: { pattern: msg.subject },
|
|
5213
|
-
serviceName,
|
|
5214
|
-
endpoint: serverEndpoint
|
|
5215
|
-
},
|
|
5216
|
-
otel,
|
|
5217
|
-
() => unwrapResult(handler(data, ctx))
|
|
5218
|
-
);
|
|
5606
|
+
data = codec.decode(msg.data);
|
|
5219
5607
|
} catch (err) {
|
|
5220
|
-
logger5.error(`
|
|
5221
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5608
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
5222
5609
|
return void 0;
|
|
5223
5610
|
}
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5611
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5612
|
+
} catch (err) {
|
|
5613
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5614
|
+
return void 0;
|
|
5615
|
+
}
|
|
5616
|
+
const ctx = new RpcContext([msg]);
|
|
5617
|
+
const warnIfSettlementAttempted = () => {
|
|
5618
|
+
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
5619
|
+
logger5.warn(
|
|
5620
|
+
`retry()/terminate() ignored for ordered message ${subject}; ordered consumers auto-acknowledge`
|
|
5621
|
+
);
|
|
5228
5622
|
}
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5623
|
+
};
|
|
5624
|
+
const startedAt = performance.now();
|
|
5625
|
+
let pending;
|
|
5626
|
+
try {
|
|
5627
|
+
pending = withConsumeSpan(
|
|
5628
|
+
{
|
|
5629
|
+
subject: msg.subject,
|
|
5630
|
+
msg,
|
|
5631
|
+
info: msg.info,
|
|
5632
|
+
kind: spanKind,
|
|
5633
|
+
payloadBytes: msg.data.length,
|
|
5634
|
+
handlerMetadata: { pattern: msg.subject },
|
|
5635
|
+
serviceName,
|
|
5636
|
+
endpoint: serverEndpoint
|
|
5233
5637
|
},
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5237
|
-
}
|
|
5638
|
+
otel,
|
|
5639
|
+
() => unwrapResult(handler(data, ctx))
|
|
5238
5640
|
);
|
|
5239
|
-
}
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
}
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
};
|
|
5258
|
-
const trackAsync = (result, msg) => {
|
|
5259
|
-
void result.catch((err) => {
|
|
5260
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5261
|
-
}).finally(onAsyncDone);
|
|
5262
|
-
};
|
|
5263
|
-
const drainBacklog = () => {
|
|
5264
|
-
while (active < maxActive) {
|
|
5265
|
-
const next = backlog.shift();
|
|
5266
|
-
if (next === void 0) return;
|
|
5267
|
-
next.stopAckExtension?.();
|
|
5268
|
-
active++;
|
|
5269
|
-
const result = routeSafely(next.msg);
|
|
5270
|
-
if (result !== void 0) {
|
|
5271
|
-
trackAsync(result, next.msg);
|
|
5272
|
-
} else {
|
|
5273
|
-
active--;
|
|
5274
|
-
}
|
|
5641
|
+
} catch (err) {
|
|
5642
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5643
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
5644
|
+
return void 0;
|
|
5645
|
+
}
|
|
5646
|
+
if (!isPromiseLike2(pending)) {
|
|
5647
|
+
warnIfSettlementAttempted();
|
|
5648
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
5649
|
+
return void 0;
|
|
5650
|
+
}
|
|
5651
|
+
return pending.then(
|
|
5652
|
+
() => {
|
|
5653
|
+
warnIfSettlementAttempted();
|
|
5654
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
5655
|
+
},
|
|
5656
|
+
(err) => {
|
|
5657
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
5658
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
5275
5659
|
}
|
|
5276
|
-
|
|
5660
|
+
);
|
|
5661
|
+
};
|
|
5662
|
+
};
|
|
5663
|
+
|
|
5664
|
+
// src/server/routing/event.router.ts
|
|
5665
|
+
var eventConsumeKindFor = (kind) => {
|
|
5666
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
5667
|
+
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
5668
|
+
return "event" /* Event */;
|
|
5669
|
+
};
|
|
5670
|
+
var EventRouter = class {
|
|
5671
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options, names) {
|
|
5672
|
+
this.messageProvider = messageProvider;
|
|
5673
|
+
this.patternRegistry = patternRegistry;
|
|
5674
|
+
this.codec = codec;
|
|
5675
|
+
this.eventBus = eventBus;
|
|
5676
|
+
this.deadLetterConfig = deadLetterConfig;
|
|
5677
|
+
this.processingConfig = processingConfig;
|
|
5678
|
+
this.ackWaitMap = ackWaitMap;
|
|
5679
|
+
if (options) {
|
|
5680
|
+
const derived = deriveOtelAttrs(options);
|
|
5681
|
+
this.otel = derived.otel;
|
|
5682
|
+
this.serviceName = derived.serviceName;
|
|
5683
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
5684
|
+
} else {
|
|
5685
|
+
this.otel = resolveOtelOptions({ enabled: false });
|
|
5686
|
+
this.serviceName = "";
|
|
5687
|
+
this.serverEndpoint = null;
|
|
5688
|
+
}
|
|
5689
|
+
this.capture = deadLetterConfig ? new DeadLetterCapture(
|
|
5690
|
+
patternRegistry,
|
|
5691
|
+
eventBus,
|
|
5692
|
+
deadLetterConfig,
|
|
5693
|
+
this.otel,
|
|
5694
|
+
this.serviceName,
|
|
5695
|
+
this.serverEndpoint,
|
|
5696
|
+
connection,
|
|
5697
|
+
options,
|
|
5698
|
+
names
|
|
5699
|
+
) : null;
|
|
5700
|
+
}
|
|
5701
|
+
logger = new Logger19("Jetstream:EventRouter");
|
|
5702
|
+
subscriptions = [];
|
|
5703
|
+
otel;
|
|
5704
|
+
serviceName;
|
|
5705
|
+
serverEndpoint;
|
|
5706
|
+
capture;
|
|
5707
|
+
/**
|
|
5708
|
+
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
5709
|
+
* Called after consumers are ensured so the DLQ map reflects reality.
|
|
5710
|
+
*/
|
|
5711
|
+
updateMaxDeliverMap(consumerMaxDelivers) {
|
|
5712
|
+
if (!this.deadLetterConfig) return;
|
|
5713
|
+
this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
|
|
5714
|
+
}
|
|
5715
|
+
/** Start routing event, broadcast, and ordered messages to handlers. */
|
|
5716
|
+
start() {
|
|
5717
|
+
this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
|
|
5718
|
+
this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
|
|
5719
|
+
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
5720
|
+
this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
/** Stop routing and unsubscribe from all streams. */
|
|
5724
|
+
destroy() {
|
|
5725
|
+
for (const sub of this.subscriptions) {
|
|
5726
|
+
sub.unsubscribe();
|
|
5727
|
+
}
|
|
5728
|
+
this.subscriptions.length = 0;
|
|
5729
|
+
}
|
|
5730
|
+
/** Assemble the pipeline and concurrency gate for one stream and subscribe. */
|
|
5731
|
+
subscribeToStream(stream$, kind) {
|
|
5732
|
+
const isOrdered = kind === "ordered" /* Ordered */;
|
|
5733
|
+
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
5734
|
+
const rctx = {
|
|
5735
|
+
kind,
|
|
5736
|
+
spanKind: eventConsumeKindFor(kind),
|
|
5737
|
+
codec: this.codec,
|
|
5738
|
+
logger: this.logger,
|
|
5739
|
+
eventBus: this.eventBus,
|
|
5740
|
+
patternRegistry: this.patternRegistry,
|
|
5741
|
+
otel: this.otel,
|
|
5742
|
+
serviceName: this.serviceName,
|
|
5743
|
+
serverEndpoint: this.serverEndpoint,
|
|
5744
|
+
ackExtensionInterval,
|
|
5745
|
+
capture: this.capture
|
|
5277
5746
|
};
|
|
5747
|
+
const route = isOrdered ? createOrderedPipeline(rctx) : createWorkqueuePipeline(rctx);
|
|
5748
|
+
const maxActive = isOrdered ? 1 : this.getConcurrency(kind) ?? Number.POSITIVE_INFINITY;
|
|
5749
|
+
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
5750
|
+
const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5751
|
+
const gate = new ConcurrencyGate(maxActive, route, parkTimer, this.logger, kind);
|
|
5278
5752
|
const subscription = stream$.subscribe({
|
|
5279
|
-
next: (msg) => {
|
|
5280
|
-
|
|
5281
|
-
backlog.push({
|
|
5282
|
-
msg,
|
|
5283
|
-
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5284
|
-
});
|
|
5285
|
-
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5286
|
-
backlogWarned = true;
|
|
5287
|
-
logger5.warn(
|
|
5288
|
-
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5289
|
-
);
|
|
5290
|
-
}
|
|
5291
|
-
return;
|
|
5292
|
-
}
|
|
5293
|
-
active++;
|
|
5294
|
-
const result = routeSafely(msg);
|
|
5295
|
-
if (result !== void 0) {
|
|
5296
|
-
trackAsync(result, msg);
|
|
5297
|
-
} else {
|
|
5298
|
-
active--;
|
|
5299
|
-
if (backlog.length > 0) drainBacklog();
|
|
5300
|
-
}
|
|
5753
|
+
next: (msg) => {
|
|
5754
|
+
gate.push(msg);
|
|
5301
5755
|
},
|
|
5302
5756
|
error: (err) => {
|
|
5303
|
-
|
|
5757
|
+
this.logger.error(`Stream error in ${kind} router`, err);
|
|
5304
5758
|
}
|
|
5305
5759
|
});
|
|
5306
5760
|
subscription.add(() => {
|
|
5307
|
-
|
|
5308
|
-
queued.stopAckExtension?.();
|
|
5309
|
-
}
|
|
5310
|
-
backlog.length = 0;
|
|
5761
|
+
gate.dispose();
|
|
5311
5762
|
});
|
|
5312
5763
|
this.subscriptions.push(subscription);
|
|
5313
5764
|
}
|
|
@@ -5321,169 +5772,10 @@ var EventRouter = class {
|
|
|
5321
5772
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
5322
5773
|
return void 0;
|
|
5323
5774
|
}
|
|
5324
|
-
/**
|
|
5325
|
-
* Last resort: invoke onDeadLetter, then term on success. On failure the
|
|
5326
|
-
* message is nak'd — never redelivered past max_deliver, but preserved.
|
|
5327
|
-
*/
|
|
5328
|
-
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
5329
|
-
const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
|
|
5330
|
-
if (!onDeadLetter) {
|
|
5331
|
-
this.logger.error(
|
|
5332
|
-
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
|
|
5333
|
-
);
|
|
5334
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5335
|
-
msg.nak();
|
|
5336
|
-
});
|
|
5337
|
-
return;
|
|
5338
|
-
}
|
|
5339
|
-
try {
|
|
5340
|
-
await onDeadLetter(info);
|
|
5341
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5342
|
-
msg.term("Dead letter processed via fallback callback");
|
|
5343
|
-
});
|
|
5344
|
-
} catch (hookErr) {
|
|
5345
|
-
this.logger.error(
|
|
5346
|
-
`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:`,
|
|
5347
|
-
hookErr
|
|
5348
|
-
);
|
|
5349
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5350
|
-
msg.nak();
|
|
5351
|
-
});
|
|
5352
|
-
}
|
|
5353
|
-
}
|
|
5354
|
-
/**
|
|
5355
|
-
* Copy headers for the DLQ republish, dropping NATS control headers — a
|
|
5356
|
-
* copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
|
|
5357
|
-
*/
|
|
5358
|
-
buildDlqHeaders(msg) {
|
|
5359
|
-
const hdrs = natsHeaders3();
|
|
5360
|
-
if (!msg.headers) return hdrs;
|
|
5361
|
-
for (const [k, v] of msg.headers) {
|
|
5362
|
-
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5363
|
-
for (const val of v) {
|
|
5364
|
-
hdrs.append(k, val);
|
|
5365
|
-
}
|
|
5366
|
-
}
|
|
5367
|
-
return hdrs;
|
|
5368
|
-
}
|
|
5369
|
-
/**
|
|
5370
|
-
* Past max_deliver the server never redelivers, so these in-process attempts
|
|
5371
|
-
* are the only second chance a dead letter gets. No artificial delay — an
|
|
5372
|
-
* unreachable broker already spaces attempts via its own request timeout.
|
|
5373
|
-
*/
|
|
5374
|
-
async publishToDlqWithRetry(connection, subject, data, headers2) {
|
|
5375
|
-
let lastErr;
|
|
5376
|
-
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5377
|
-
try {
|
|
5378
|
-
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5379
|
-
return;
|
|
5380
|
-
} catch (err) {
|
|
5381
|
-
lastErr = err;
|
|
5382
|
-
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5383
|
-
this.logger.warn(
|
|
5384
|
-
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5385
|
-
);
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5388
|
-
}
|
|
5389
|
-
throw lastErr;
|
|
5390
|
-
}
|
|
5391
|
-
/**
|
|
5392
|
-
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
5393
|
-
*
|
|
5394
|
-
* Appends diagnostic metadata headers to the original message and preserves
|
|
5395
|
-
* the primary payload. If publishing succeeds, it notifies the standard
|
|
5396
|
-
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
5397
|
-
* back to the callback entirely to prevent silent data loss.
|
|
5398
|
-
*/
|
|
5399
|
-
async publishToDlq(msg, info, error) {
|
|
5400
|
-
const serviceName = this.options?.name;
|
|
5401
|
-
if (!this.connection || !serviceName) {
|
|
5402
|
-
this.logger.error(
|
|
5403
|
-
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
5404
|
-
);
|
|
5405
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5406
|
-
return;
|
|
5407
|
-
}
|
|
5408
|
-
const destinationSubject = dlqStreamName(serviceName);
|
|
5409
|
-
const hdrs = this.buildDlqHeaders(msg);
|
|
5410
|
-
let reason = String(error);
|
|
5411
|
-
if (error instanceof Error) {
|
|
5412
|
-
reason = error.message;
|
|
5413
|
-
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
5414
|
-
reason = String(error.message);
|
|
5415
|
-
}
|
|
5416
|
-
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
5417
|
-
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
5418
|
-
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
5419
|
-
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
5420
|
-
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
5421
|
-
try {
|
|
5422
|
-
await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
5423
|
-
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
5424
|
-
if (this.deadLetterConfig?.onDeadLetter) {
|
|
5425
|
-
try {
|
|
5426
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
5427
|
-
} catch (hookErr) {
|
|
5428
|
-
this.logger.warn(
|
|
5429
|
-
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
5430
|
-
hookErr
|
|
5431
|
-
);
|
|
5432
|
-
}
|
|
5433
|
-
}
|
|
5434
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5435
|
-
msg.term("Moved to DLQ stream");
|
|
5436
|
-
});
|
|
5437
|
-
} catch (publishErr) {
|
|
5438
|
-
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
5439
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5440
|
-
}
|
|
5441
|
-
}
|
|
5442
|
-
/**
|
|
5443
|
-
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
5444
|
-
*
|
|
5445
|
-
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
5446
|
-
* or directly to the fallback callback based on the active module configuration.
|
|
5447
|
-
*/
|
|
5448
|
-
async handleDeadLetter(msg, data, error) {
|
|
5449
|
-
const info = {
|
|
5450
|
-
subject: msg.subject,
|
|
5451
|
-
data,
|
|
5452
|
-
headers: msg.headers,
|
|
5453
|
-
error,
|
|
5454
|
-
deliveryCount: msg.info.deliveryCount,
|
|
5455
|
-
stream: msg.info.stream,
|
|
5456
|
-
streamSequence: msg.info.streamSequence,
|
|
5457
|
-
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
5458
|
-
};
|
|
5459
|
-
await withDeadLetterSpan(
|
|
5460
|
-
{
|
|
5461
|
-
msg,
|
|
5462
|
-
// Pattern resolution mirrors event-routing: when a registered
|
|
5463
|
-
// pattern matches, surface it on the DLQ span so APM can filter
|
|
5464
|
-
// dead letters by handler without parsing the subject. Falls back
|
|
5465
|
-
// to the subject itself when no glob handler is in play.
|
|
5466
|
-
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
5467
|
-
finalDeliveryCount: msg.info.deliveryCount,
|
|
5468
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
5469
|
-
serviceName: this.serviceName,
|
|
5470
|
-
endpoint: this.serverEndpoint
|
|
5471
|
-
},
|
|
5472
|
-
this.otel,
|
|
5473
|
-
async () => {
|
|
5474
|
-
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
5475
|
-
if (!this.options?.dlq) {
|
|
5476
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5477
|
-
} else {
|
|
5478
|
-
await this.publishToDlq(msg, info, error);
|
|
5479
|
-
}
|
|
5480
|
-
}
|
|
5481
|
-
);
|
|
5482
|
-
}
|
|
5483
5775
|
};
|
|
5484
5776
|
|
|
5485
5777
|
// src/server/routing/rpc.router.ts
|
|
5486
|
-
import { Logger as
|
|
5778
|
+
import { Logger as Logger20 } from "@nestjs/common";
|
|
5487
5779
|
import { headers } from "@nats-io/transport-node";
|
|
5488
5780
|
var RpcRouter = class {
|
|
5489
5781
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
|
|
@@ -5507,7 +5799,7 @@ var RpcRouter = class {
|
|
|
5507
5799
|
this.serverEndpoint = null;
|
|
5508
5800
|
}
|
|
5509
5801
|
}
|
|
5510
|
-
logger = new
|
|
5802
|
+
logger = new Logger20("Jetstream:RpcRouter");
|
|
5511
5803
|
timeout;
|
|
5512
5804
|
concurrency;
|
|
5513
5805
|
resolvedAckExtensionInterval;
|
|
@@ -5698,75 +5990,18 @@ var RpcRouter = class {
|
|
|
5698
5990
|
}
|
|
5699
5991
|
);
|
|
5700
5992
|
};
|
|
5701
|
-
const
|
|
5702
|
-
|
|
5703
|
-
let backlogWarned = false;
|
|
5704
|
-
const backlog = [];
|
|
5705
|
-
const onAsyncDone = () => {
|
|
5706
|
-
active--;
|
|
5707
|
-
drainBacklog();
|
|
5708
|
-
};
|
|
5709
|
-
const routeSafely = (msg) => {
|
|
5710
|
-
try {
|
|
5711
|
-
return handleSafe(msg);
|
|
5712
|
-
} catch (err) {
|
|
5713
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5714
|
-
return void 0;
|
|
5715
|
-
}
|
|
5716
|
-
};
|
|
5717
|
-
const trackAsync = (result, msg) => {
|
|
5718
|
-
void result.catch((err) => {
|
|
5719
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5720
|
-
}).finally(onAsyncDone);
|
|
5721
|
-
};
|
|
5722
|
-
const drainBacklog = () => {
|
|
5723
|
-
while (active < maxActive) {
|
|
5724
|
-
const next = backlog.shift();
|
|
5725
|
-
if (next === void 0) return;
|
|
5726
|
-
next.stopAckExtension?.();
|
|
5727
|
-
active++;
|
|
5728
|
-
const result = routeSafely(next.msg);
|
|
5729
|
-
if (result !== void 0) {
|
|
5730
|
-
trackAsync(result, next.msg);
|
|
5731
|
-
} else {
|
|
5732
|
-
active--;
|
|
5733
|
-
}
|
|
5734
|
-
}
|
|
5735
|
-
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
5736
|
-
};
|
|
5993
|
+
const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5994
|
+
const gate = new ConcurrencyGate(maxActive, handleSafe, parkTimer, logger5, "RPC");
|
|
5737
5995
|
this.subscription = this.messageProvider.commands$.subscribe({
|
|
5738
5996
|
next: (msg) => {
|
|
5739
|
-
|
|
5740
|
-
backlog.push({
|
|
5741
|
-
msg,
|
|
5742
|
-
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5743
|
-
});
|
|
5744
|
-
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5745
|
-
backlogWarned = true;
|
|
5746
|
-
logger5.warn(
|
|
5747
|
-
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5748
|
-
);
|
|
5749
|
-
}
|
|
5750
|
-
return;
|
|
5751
|
-
}
|
|
5752
|
-
active++;
|
|
5753
|
-
const result = routeSafely(msg);
|
|
5754
|
-
if (result !== void 0) {
|
|
5755
|
-
trackAsync(result, msg);
|
|
5756
|
-
} else {
|
|
5757
|
-
active--;
|
|
5758
|
-
if (backlog.length > 0) drainBacklog();
|
|
5759
|
-
}
|
|
5997
|
+
gate.push(msg);
|
|
5760
5998
|
},
|
|
5761
5999
|
error: (err) => {
|
|
5762
6000
|
logger5.error("Stream error in RPC router", err);
|
|
5763
6001
|
}
|
|
5764
6002
|
});
|
|
5765
6003
|
this.subscription.add(() => {
|
|
5766
|
-
|
|
5767
|
-
queued.stopAckExtension?.();
|
|
5768
|
-
}
|
|
5769
|
-
backlog.length = 0;
|
|
6004
|
+
gate.dispose();
|
|
5770
6005
|
});
|
|
5771
6006
|
}
|
|
5772
6007
|
/** Stop routing and unsubscribe. */
|
|
@@ -5776,20 +6011,220 @@ var RpcRouter = class {
|
|
|
5776
6011
|
}
|
|
5777
6012
|
};
|
|
5778
6013
|
|
|
6014
|
+
// src/server/infrastructure/infrastructure-binder.ts
|
|
6015
|
+
import { Logger as Logger21 } from "@nestjs/common";
|
|
6016
|
+
import { JetStreamApiError as JetStreamApiError4, RetentionPolicy as RetentionPolicy3 } from "@nats-io/jetstream";
|
|
6017
|
+
var WORKQUEUE_KINDS = /* @__PURE__ */ new Set(["ev" /* Event */, "cmd" /* Command */]);
|
|
6018
|
+
var manualRemediation = (entity) => `Management mode is Manual; the ${entity} must be provisioned externally before boot.`;
|
|
6019
|
+
var isSchedulingEnabled = (options, kind) => kindOptionsBlock(options, kind)?.stream?.allow_msg_schedules === true;
|
|
6020
|
+
var resolveAckExtension = (options, kind) => kindOptionsBlock(options, kind)?.ackExtension;
|
|
6021
|
+
var filterCoversSubject = (filter_subject, filter_subjects, subject) => {
|
|
6022
|
+
if (filter_subject !== void 0) {
|
|
6023
|
+
return coversOrEquals(filter_subject, subject);
|
|
6024
|
+
}
|
|
6025
|
+
if (filter_subjects !== void 0) {
|
|
6026
|
+
return filter_subjects.some((f) => coversOrEquals(f, subject));
|
|
6027
|
+
}
|
|
6028
|
+
return true;
|
|
6029
|
+
};
|
|
6030
|
+
var InfrastructureBinder = class {
|
|
6031
|
+
constructor(options, names, registry) {
|
|
6032
|
+
this.options = options;
|
|
6033
|
+
this.names = names;
|
|
6034
|
+
this.registry = registry;
|
|
6035
|
+
}
|
|
6036
|
+
logger = new Logger21("Jetstream:Binder");
|
|
6037
|
+
async bindStream(jsm, kind) {
|
|
6038
|
+
const name = this.names.streamName(kind);
|
|
6039
|
+
const info = await this.fetchStream(jsm, name, kind);
|
|
6040
|
+
await this.warnOnOrphanedMigrationBackup(jsm, name);
|
|
6041
|
+
if (isSchedulingEnabled(this.options, kind)) {
|
|
6042
|
+
this.assertScheduleCoverage(info, kind);
|
|
6043
|
+
this.warnOnSchedulesDisabled(info, kind);
|
|
6044
|
+
}
|
|
6045
|
+
if (WORKQUEUE_KINDS.has(kind)) {
|
|
6046
|
+
this.warnOnRetention(info, kind);
|
|
6047
|
+
}
|
|
6048
|
+
return info;
|
|
6049
|
+
}
|
|
6050
|
+
async bindDlqStream(jsm) {
|
|
6051
|
+
const dlqName = this.names.dlqStreamName();
|
|
6052
|
+
const info = await this.fetchStream(jsm, dlqName, "dlq");
|
|
6053
|
+
await this.warnOnOrphanedMigrationBackup(jsm, dlqName);
|
|
6054
|
+
this.assertDlqSubjectCoverage(info);
|
|
6055
|
+
return info;
|
|
6056
|
+
}
|
|
6057
|
+
async bindConsumer(jsm, kind) {
|
|
6058
|
+
const info = await this.fetchConsumer(jsm, kind);
|
|
6059
|
+
this.assertHandlersCovered(info, kind);
|
|
6060
|
+
this.assertScheduleHoldersNotConsumed(info, kind);
|
|
6061
|
+
this.warnOnUnlimitedDelivery(info, kind);
|
|
6062
|
+
this.warnOnShortAckWait(info, kind);
|
|
6063
|
+
return info;
|
|
6064
|
+
}
|
|
6065
|
+
async fetchStream(jsm, name, kind) {
|
|
6066
|
+
try {
|
|
6067
|
+
return await jsm.streams.info(name);
|
|
6068
|
+
} catch (err) {
|
|
6069
|
+
if (err instanceof JetStreamApiError4 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
6070
|
+
const api = err.apiError();
|
|
6071
|
+
throw new JetstreamProvisioningError({
|
|
6072
|
+
entity: "stream",
|
|
6073
|
+
target: name,
|
|
6074
|
+
kind: String(kind),
|
|
6075
|
+
errCode: api.err_code,
|
|
6076
|
+
errDescription: api.description,
|
|
6077
|
+
remediation: manualRemediation("stream"),
|
|
6078
|
+
cause: err
|
|
6079
|
+
});
|
|
6080
|
+
}
|
|
6081
|
+
throw err;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
async fetchConsumer(jsm, kind) {
|
|
6085
|
+
const stream = this.names.streamName(kind);
|
|
6086
|
+
const consumer = this.names.consumerName(kind);
|
|
6087
|
+
try {
|
|
6088
|
+
return await jsm.consumers.info(stream, consumer);
|
|
6089
|
+
} catch (err) {
|
|
6090
|
+
if (err instanceof JetStreamApiError4 && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
|
|
6091
|
+
const api = err.apiError();
|
|
6092
|
+
throw new JetstreamProvisioningError({
|
|
6093
|
+
entity: "consumer",
|
|
6094
|
+
target: `${consumer} on stream "${stream}"`,
|
|
6095
|
+
kind: String(kind),
|
|
6096
|
+
errCode: api.err_code,
|
|
6097
|
+
errDescription: api.description,
|
|
6098
|
+
remediation: manualRemediation("consumer"),
|
|
6099
|
+
cause: err
|
|
6100
|
+
});
|
|
6101
|
+
}
|
|
6102
|
+
throw err;
|
|
6103
|
+
}
|
|
6104
|
+
}
|
|
6105
|
+
assertHandlersCovered(info, kind) {
|
|
6106
|
+
const subjects = this.resolveHandlerSubjects(kind);
|
|
6107
|
+
if (subjects.length === 0) return;
|
|
6108
|
+
const { filter_subject, filter_subjects } = info.config;
|
|
6109
|
+
const uncovered = subjects.filter(
|
|
6110
|
+
(s) => !filterCoversSubject(filter_subject, filter_subjects, s)
|
|
6111
|
+
);
|
|
6112
|
+
if (uncovered.length > 0) {
|
|
6113
|
+
throw new Error(
|
|
6114
|
+
`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.`
|
|
6115
|
+
);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
assertDlqSubjectCoverage(info) {
|
|
6119
|
+
const dlqSubject = this.names.dlqStreamName();
|
|
6120
|
+
const covered = info.config.subjects.some((s) => coversOrEquals(s, dlqSubject));
|
|
6121
|
+
if (!covered) {
|
|
6122
|
+
throw new Error(
|
|
6123
|
+
`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.`
|
|
6124
|
+
);
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
assertScheduleCoverage(info, kind) {
|
|
6128
|
+
const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
|
|
6129
|
+
const covered = info.config.subjects.some((s) => coversOrEquals(s, scheduleWildcard));
|
|
6130
|
+
if (!covered) {
|
|
6131
|
+
throw new Error(
|
|
6132
|
+
`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.`
|
|
6133
|
+
);
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
assertScheduleHoldersNotConsumed(info, kind) {
|
|
6137
|
+
if (!isSchedulingEnabled(this.options, kind)) return;
|
|
6138
|
+
const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
|
|
6139
|
+
const { filter_subject, filter_subjects } = info.config;
|
|
6140
|
+
const filters = filter_subjects ?? (filter_subject !== void 0 ? [filter_subject] : []);
|
|
6141
|
+
const swallowing = filters.length === 0 ? ["<no filter, consumes the whole stream>"] : filters.filter((f) => coversOrEquals(f, scheduleWildcard));
|
|
6142
|
+
if (swallowing.length > 0) {
|
|
6143
|
+
throw new Error(
|
|
6144
|
+
`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.`
|
|
6145
|
+
);
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
async warnOnOrphanedMigrationBackup(jsm, streamName2) {
|
|
6149
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
6150
|
+
try {
|
|
6151
|
+
await jsm.streams.info(backupName);
|
|
6152
|
+
} catch {
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6155
|
+
this.logger.warn(
|
|
6156
|
+
`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.`
|
|
6157
|
+
);
|
|
6158
|
+
}
|
|
6159
|
+
warnOnSchedulesDisabled(info, kind) {
|
|
6160
|
+
if (info.config.allow_msg_schedules === true) return;
|
|
6161
|
+
this.logger.warn(
|
|
6162
|
+
`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.`
|
|
6163
|
+
);
|
|
6164
|
+
}
|
|
6165
|
+
warnOnRetention(info, kind) {
|
|
6166
|
+
if (info.config.retention !== RetentionPolicy3.Workqueue) {
|
|
6167
|
+
this.logger.warn(
|
|
6168
|
+
`Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) retention is "${String(info.config.retention)}"; expected "workqueue" for reliable at-least-once delivery.`
|
|
6169
|
+
);
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
warnOnUnlimitedDelivery(info, kind) {
|
|
6173
|
+
if (!this.options.dlq) return;
|
|
6174
|
+
const maxDeliver = info.config.max_deliver;
|
|
6175
|
+
if (maxDeliver === void 0 || maxDeliver <= 0) {
|
|
6176
|
+
this.logger.warn(
|
|
6177
|
+
`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.`
|
|
6178
|
+
);
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
warnOnShortAckWait(info, kind) {
|
|
6182
|
+
const ackExtConfig = resolveAckExtension(this.options, kind);
|
|
6183
|
+
if (ackExtConfig === void 0 || ackExtConfig === false) return;
|
|
6184
|
+
const ackWaitNanos = info.config.ack_wait;
|
|
6185
|
+
const intervalMs = resolveAckExtensionInterval(ackExtConfig, ackWaitNanos);
|
|
6186
|
+
if (intervalMs === null) return;
|
|
6187
|
+
const ackWaitMs = ackWaitNanos !== void 0 ? ackWaitNanos / 1e6 : void 0;
|
|
6188
|
+
if (ackWaitMs !== void 0 && ackWaitMs < intervalMs) {
|
|
6189
|
+
this.logger.warn(
|
|
6190
|
+
`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.`
|
|
6191
|
+
);
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
resolveHandlerSubjects(kind) {
|
|
6195
|
+
const patterns = this.registry.getPatternsByKind();
|
|
6196
|
+
switch (kind) {
|
|
6197
|
+
case "ev" /* Event */:
|
|
6198
|
+
return patterns.events.map((p) => this.names.subject("ev" /* Event */, p));
|
|
6199
|
+
case "cmd" /* Command */:
|
|
6200
|
+
return patterns.commands.map((p) => this.names.subject("cmd" /* Command */, p));
|
|
6201
|
+
case "broadcast" /* Broadcast */:
|
|
6202
|
+
return this.registry.getBroadcastPatterns();
|
|
6203
|
+
case "ordered" /* Ordered */:
|
|
6204
|
+
return this.registry.getOrderedSubjects();
|
|
6205
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
6206
|
+
default: {
|
|
6207
|
+
const _exhaustive = kind;
|
|
6208
|
+
throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
};
|
|
6213
|
+
|
|
5779
6214
|
// src/shutdown/shutdown.manager.ts
|
|
5780
|
-
import { Logger as
|
|
6215
|
+
import { Logger as Logger22 } from "@nestjs/common";
|
|
5781
6216
|
var ShutdownManager = class {
|
|
5782
6217
|
constructor(connection, eventBus, timeout) {
|
|
5783
6218
|
this.connection = connection;
|
|
5784
6219
|
this.eventBus = eventBus;
|
|
5785
6220
|
this.timeout = timeout;
|
|
5786
6221
|
}
|
|
5787
|
-
logger = new
|
|
6222
|
+
logger = new Logger22("Jetstream:Shutdown");
|
|
5788
6223
|
shutdownPromise;
|
|
5789
6224
|
/**
|
|
5790
6225
|
* Execute the full shutdown sequence.
|
|
5791
6226
|
*
|
|
5792
|
-
* Idempotent
|
|
6227
|
+
* Idempotent: concurrent or repeated calls return the same promise.
|
|
5793
6228
|
*
|
|
5794
6229
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
5795
6230
|
*/
|
|
@@ -5819,6 +6254,12 @@ var ShutdownManager = class {
|
|
|
5819
6254
|
|
|
5820
6255
|
// src/jetstream.module.ts
|
|
5821
6256
|
var JETSTREAM_ACK_WAIT_MAP = /* @__PURE__ */ Symbol("JETSTREAM_ACK_WAIT_MAP");
|
|
6257
|
+
var DESTRUCTIVE_MIGRATION_MANUAL_WARNING = "allowDestructiveMigration has no effect under provisioning.management: Manual; the library never migrates externally managed streams.";
|
|
6258
|
+
var warnIfManualWithDestructive = (options, logger5) => {
|
|
6259
|
+
if (options.allowDestructiveMigration && options.provisioning?.management === "manual" /* Manual */) {
|
|
6260
|
+
logger5.warn(DESTRUCTIVE_MIGRATION_MANUAL_WARNING);
|
|
6261
|
+
}
|
|
6262
|
+
};
|
|
5822
6263
|
var JetstreamModule = class {
|
|
5823
6264
|
constructor(shutdownManager, strategy) {
|
|
5824
6265
|
this.shutdownManager = shutdownManager;
|
|
@@ -5848,7 +6289,8 @@ var JetstreamModule = class {
|
|
|
5848
6289
|
PatternRegistry,
|
|
5849
6290
|
ShutdownManager,
|
|
5850
6291
|
JetstreamStrategy,
|
|
5851
|
-
JetstreamHealthIndicator
|
|
6292
|
+
JetstreamHealthIndicator,
|
|
6293
|
+
NameResolver
|
|
5852
6294
|
]
|
|
5853
6295
|
};
|
|
5854
6296
|
}
|
|
@@ -5877,7 +6319,8 @@ var JetstreamModule = class {
|
|
|
5877
6319
|
PatternRegistry,
|
|
5878
6320
|
ShutdownManager,
|
|
5879
6321
|
JetstreamStrategy,
|
|
5880
|
-
JetstreamHealthIndicator
|
|
6322
|
+
JetstreamHealthIndicator,
|
|
6323
|
+
NameResolver
|
|
5881
6324
|
]
|
|
5882
6325
|
};
|
|
5883
6326
|
}
|
|
@@ -5894,10 +6337,23 @@ var JetstreamModule = class {
|
|
|
5894
6337
|
const clientToken = getClientToken(options.name);
|
|
5895
6338
|
const clientProvider = {
|
|
5896
6339
|
provide: clientToken,
|
|
5897
|
-
inject: [
|
|
5898
|
-
|
|
6340
|
+
inject: [
|
|
6341
|
+
JETSTREAM_OPTIONS,
|
|
6342
|
+
JETSTREAM_CONNECTION,
|
|
6343
|
+
JETSTREAM_CODEC,
|
|
6344
|
+
JETSTREAM_EVENT_BUS,
|
|
6345
|
+
{ token: NameResolver, optional: true }
|
|
6346
|
+
],
|
|
6347
|
+
useFactory: (rootOptions, connection, rootCodec, eventBus, names) => {
|
|
5899
6348
|
const codec = options.codec ?? rootCodec;
|
|
5900
|
-
return new JetstreamClient(
|
|
6349
|
+
return new JetstreamClient(
|
|
6350
|
+
rootOptions,
|
|
6351
|
+
options.name,
|
|
6352
|
+
connection,
|
|
6353
|
+
codec,
|
|
6354
|
+
eventBus,
|
|
6355
|
+
names ?? void 0
|
|
6356
|
+
);
|
|
5901
6357
|
}
|
|
5902
6358
|
};
|
|
5903
6359
|
return {
|
|
@@ -5918,16 +6374,14 @@ var JetstreamModule = class {
|
|
|
5918
6374
|
/** Create providers that depend on JETSTREAM_OPTIONS (shared by sync and async). */
|
|
5919
6375
|
static createCoreDependentProviders() {
|
|
5920
6376
|
return [
|
|
5921
|
-
// EventBus — hook system with Logger fallback
|
|
5922
6377
|
{
|
|
5923
6378
|
provide: JETSTREAM_EVENT_BUS,
|
|
5924
6379
|
inject: [JETSTREAM_OPTIONS],
|
|
5925
6380
|
useFactory: (options) => {
|
|
5926
|
-
const logger5 = new
|
|
6381
|
+
const logger5 = new Logger23("Jetstream:Module");
|
|
5927
6382
|
return new EventBus(logger5, options.hooks);
|
|
5928
6383
|
}
|
|
5929
6384
|
},
|
|
5930
|
-
// Codec — global encode/decode
|
|
5931
6385
|
{
|
|
5932
6386
|
provide: JETSTREAM_CODEC,
|
|
5933
6387
|
inject: [JETSTREAM_OPTIONS],
|
|
@@ -5935,7 +6389,6 @@ var JetstreamModule = class {
|
|
|
5935
6389
|
return options.codec ?? new JsonCodec();
|
|
5936
6390
|
}
|
|
5937
6391
|
},
|
|
5938
|
-
// ConnectionProvider — single NATS connection
|
|
5939
6392
|
{
|
|
5940
6393
|
provide: JETSTREAM_CONNECTION,
|
|
5941
6394
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_EVENT_BUS],
|
|
@@ -5943,7 +6396,6 @@ var JetstreamModule = class {
|
|
|
5943
6396
|
return new ConnectionProvider(options, eventBus);
|
|
5944
6397
|
}
|
|
5945
6398
|
},
|
|
5946
|
-
// JetstreamHealthIndicator — health check for NATS connection
|
|
5947
6399
|
{
|
|
5948
6400
|
provide: JetstreamHealthIndicator,
|
|
5949
6401
|
inject: [JETSTREAM_CONNECTION],
|
|
@@ -5951,7 +6403,6 @@ var JetstreamModule = class {
|
|
|
5951
6403
|
return new JetstreamHealthIndicator(connection);
|
|
5952
6404
|
}
|
|
5953
6405
|
},
|
|
5954
|
-
// ShutdownManager — graceful shutdown orchestration
|
|
5955
6406
|
{
|
|
5956
6407
|
provide: ShutdownManager,
|
|
5957
6408
|
inject: [JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS],
|
|
@@ -5963,41 +6414,69 @@ var JetstreamModule = class {
|
|
|
5963
6414
|
);
|
|
5964
6415
|
}
|
|
5965
6416
|
},
|
|
5966
|
-
// Consumer infrastructure
|
|
5967
|
-
//
|
|
5968
|
-
// PatternRegistry — subject-to-handler mapping
|
|
6417
|
+
// Consumer infrastructure providers below return null when consumer === false
|
|
6418
|
+
// (publisher-only mode). NameResolver is the exception: clients need it too.
|
|
5969
6419
|
{
|
|
5970
|
-
provide:
|
|
6420
|
+
provide: NameResolver,
|
|
5971
6421
|
inject: [JETSTREAM_OPTIONS],
|
|
5972
6422
|
useFactory: (options) => {
|
|
6423
|
+
const logger5 = new Logger23("Jetstream:Module");
|
|
6424
|
+
warnIfManualWithDestructive(options, logger5);
|
|
6425
|
+
return new NameResolver(options);
|
|
6426
|
+
}
|
|
6427
|
+
},
|
|
6428
|
+
{
|
|
6429
|
+
provide: PatternRegistry,
|
|
6430
|
+
inject: [JETSTREAM_OPTIONS, NameResolver],
|
|
6431
|
+
useFactory: (options, names) => {
|
|
6432
|
+
if (options.consumer === false) return null;
|
|
6433
|
+
return new PatternRegistry(options, names);
|
|
6434
|
+
}
|
|
6435
|
+
},
|
|
6436
|
+
{
|
|
6437
|
+
provide: InfrastructureBinder,
|
|
6438
|
+
inject: [JETSTREAM_OPTIONS, NameResolver, PatternRegistry],
|
|
6439
|
+
useFactory: (options, names, registry) => {
|
|
5973
6440
|
if (options.consumer === false) return null;
|
|
5974
|
-
return new
|
|
6441
|
+
return new InfrastructureBinder(options, names, registry);
|
|
5975
6442
|
}
|
|
5976
6443
|
},
|
|
5977
|
-
// StreamProvider — JetStream stream lifecycle
|
|
5978
6444
|
{
|
|
5979
6445
|
provide: StreamProvider,
|
|
5980
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
5981
|
-
useFactory: (options, connection) => {
|
|
6446
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, NameResolver, InfrastructureBinder],
|
|
6447
|
+
useFactory: (options, connection, names, binder) => {
|
|
5982
6448
|
if (options.consumer === false) return null;
|
|
5983
|
-
return new StreamProvider(options, connection);
|
|
6449
|
+
return new StreamProvider(options, connection, names, binder);
|
|
5984
6450
|
}
|
|
5985
6451
|
},
|
|
5986
|
-
// ConsumerProvider
|
|
6452
|
+
// ConsumerProvider needs PatternRegistry for broadcast filtering.
|
|
5987
6453
|
{
|
|
5988
6454
|
provide: ConsumerProvider,
|
|
5989
|
-
inject: [
|
|
5990
|
-
|
|
6455
|
+
inject: [
|
|
6456
|
+
JETSTREAM_OPTIONS,
|
|
6457
|
+
JETSTREAM_CONNECTION,
|
|
6458
|
+
StreamProvider,
|
|
6459
|
+
PatternRegistry,
|
|
6460
|
+
NameResolver,
|
|
6461
|
+
InfrastructureBinder
|
|
6462
|
+
],
|
|
6463
|
+
useFactory: (options, connection, streamProvider, patternRegistry, names, binder) => {
|
|
5991
6464
|
if (options.consumer === false) return null;
|
|
5992
|
-
return new ConsumerProvider(
|
|
6465
|
+
return new ConsumerProvider(
|
|
6466
|
+
options,
|
|
6467
|
+
connection,
|
|
6468
|
+
streamProvider,
|
|
6469
|
+
patternRegistry,
|
|
6470
|
+
names,
|
|
6471
|
+
binder
|
|
6472
|
+
);
|
|
5993
6473
|
}
|
|
5994
6474
|
},
|
|
5995
|
-
// Shared ack_wait map
|
|
6475
|
+
// Shared ack_wait map, populated by the strategy after ensureConsumers().
|
|
5996
6476
|
{
|
|
5997
6477
|
provide: JETSTREAM_ACK_WAIT_MAP,
|
|
5998
6478
|
useFactory: () => /* @__PURE__ */ new Map()
|
|
5999
6479
|
},
|
|
6000
|
-
// MessageProvider — pull-based message consumption
|
|
6001
6480
|
{
|
|
6002
6481
|
provide: MessageProvider,
|
|
6003
6482
|
inject: [
|
|
@@ -6036,7 +6515,6 @@ var JetstreamModule = class {
|
|
|
6036
6515
|
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
6037
6516
|
}
|
|
6038
6517
|
},
|
|
6039
|
-
// EventRouter — routes event and broadcast messages to handlers
|
|
6040
6518
|
{
|
|
6041
6519
|
provide: EventRouter,
|
|
6042
6520
|
inject: [
|
|
@@ -6046,9 +6524,10 @@ var JetstreamModule = class {
|
|
|
6046
6524
|
JETSTREAM_CODEC,
|
|
6047
6525
|
JETSTREAM_EVENT_BUS,
|
|
6048
6526
|
JETSTREAM_ACK_WAIT_MAP,
|
|
6049
|
-
JETSTREAM_CONNECTION
|
|
6527
|
+
JETSTREAM_CONNECTION,
|
|
6528
|
+
NameResolver
|
|
6050
6529
|
],
|
|
6051
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
6530
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection, names) => {
|
|
6052
6531
|
if (options.consumer === false) return null;
|
|
6053
6532
|
const deadLetterConfig = options.onDeadLetter || options.dlq ? {
|
|
6054
6533
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -6073,11 +6552,11 @@ var JetstreamModule = class {
|
|
|
6073
6552
|
processingConfig,
|
|
6074
6553
|
ackWaitMap,
|
|
6075
6554
|
connection,
|
|
6076
|
-
options
|
|
6555
|
+
options,
|
|
6556
|
+
names
|
|
6077
6557
|
);
|
|
6078
6558
|
}
|
|
6079
6559
|
},
|
|
6080
|
-
// RpcRouter — routes RPC command messages in JetStream mode
|
|
6081
6560
|
{
|
|
6082
6561
|
provide: RpcRouter,
|
|
6083
6562
|
inject: [
|
|
@@ -6108,7 +6587,6 @@ var JetstreamModule = class {
|
|
|
6108
6587
|
);
|
|
6109
6588
|
}
|
|
6110
6589
|
},
|
|
6111
|
-
// CoreRpcServer — RPC via NATS Core request/reply
|
|
6112
6590
|
{
|
|
6113
6591
|
provide: CoreRpcServer,
|
|
6114
6592
|
inject: [
|
|
@@ -6116,14 +6594,14 @@ var JetstreamModule = class {
|
|
|
6116
6594
|
JETSTREAM_CONNECTION,
|
|
6117
6595
|
PatternRegistry,
|
|
6118
6596
|
JETSTREAM_CODEC,
|
|
6119
|
-
JETSTREAM_EVENT_BUS
|
|
6597
|
+
JETSTREAM_EVENT_BUS,
|
|
6598
|
+
NameResolver
|
|
6120
6599
|
],
|
|
6121
|
-
useFactory: (options, connection, patternRegistry, codec, eventBus) => {
|
|
6600
|
+
useFactory: (options, connection, patternRegistry, codec, eventBus, names) => {
|
|
6122
6601
|
if (options.consumer === false) return null;
|
|
6123
|
-
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
6602
|
+
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus, names);
|
|
6124
6603
|
}
|
|
6125
6604
|
},
|
|
6126
|
-
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
6127
6605
|
{
|
|
6128
6606
|
provide: MetadataProvider,
|
|
6129
6607
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
@@ -6132,7 +6610,6 @@ var JetstreamModule = class {
|
|
|
6132
6610
|
return new MetadataProvider(options, connection);
|
|
6133
6611
|
}
|
|
6134
6612
|
},
|
|
6135
|
-
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
6136
6613
|
{
|
|
6137
6614
|
provide: JetstreamStrategy,
|
|
6138
6615
|
inject: [
|
|
@@ -6257,6 +6734,7 @@ export {
|
|
|
6257
6734
|
JetstreamTrace,
|
|
6258
6735
|
JsonCodec,
|
|
6259
6736
|
MIN_METADATA_TTL,
|
|
6737
|
+
ManagementMode,
|
|
6260
6738
|
MessageKind,
|
|
6261
6739
|
MsgpackCodec,
|
|
6262
6740
|
NatsErrorCode,
|