@horizon-republic/nestjs-jetstream 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1538 -1070
- package/dist/index.d.cts +291 -276
- package/dist/index.d.ts +291 -276
- package/dist/index.js +1479 -1011
- package/package.json +1 -1
package/dist/index.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);
|
|
@@ -3842,11 +4005,8 @@ var StreamMigration = class {
|
|
|
3842
4005
|
);
|
|
3843
4006
|
}
|
|
3844
4007
|
/**
|
|
3845
|
-
*
|
|
3846
|
-
*
|
|
3847
|
-
* live migration is left alone.
|
|
3848
|
-
*
|
|
3849
|
-
* @returns true when recovery work was performed.
|
|
4008
|
+
* Finish a migration a previous process left unfinished; a backup fresh
|
|
4009
|
+
* enough to belong to a live peer migration is left alone.
|
|
3850
4010
|
*/
|
|
3851
4011
|
async recoverInterrupted(jsm, streamName2, desiredConfig) {
|
|
3852
4012
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
@@ -3900,11 +4060,9 @@ var StreamMigration = class {
|
|
|
3900
4060
|
await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
|
|
3901
4061
|
}
|
|
3902
4062
|
/**
|
|
3903
|
-
*
|
|
3904
|
-
*
|
|
3905
|
-
*
|
|
3906
|
-
* `lag: 0, active: -1` before its first sync — `active >= 0` filters that
|
|
3907
|
-
* false positive out (verified against NATS 2.12.6).
|
|
4063
|
+
* Lag-based drain check: live publishes cannot fake completion. A fresh
|
|
4064
|
+
* source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
|
|
4065
|
+
* hence the active guard.
|
|
3908
4066
|
*/
|
|
3909
4067
|
async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
|
|
3910
4068
|
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
@@ -3921,17 +4079,13 @@ var StreamMigration = class {
|
|
|
3921
4079
|
);
|
|
3922
4080
|
}
|
|
3923
4081
|
/**
|
|
3924
|
-
* A backup
|
|
3925
|
-
*
|
|
3926
|
-
* Stale leftovers are handled by recoverInterrupted() before migrate() runs,
|
|
3927
|
-
* so a timeout here means something is genuinely stuck.
|
|
3928
|
-
*
|
|
3929
|
-
* @returns true when a peer's backup was observed and cleared.
|
|
4082
|
+
* A backup present at migrate() start is a live peer migration; wait it
|
|
4083
|
+
* out. Stale leftovers were already handled by recoverInterrupted().
|
|
3930
4084
|
*/
|
|
3931
4085
|
async waitOutPeerMigration(jsm, backupName) {
|
|
3932
4086
|
if (await this.tryInfo(jsm, backupName) === null) return false;
|
|
3933
4087
|
this.logger.warn(
|
|
3934
|
-
`Migration backup ${backupName} exists
|
|
4088
|
+
`Migration backup ${backupName} exists; another instance appears to be migrating; waiting`
|
|
3935
4089
|
);
|
|
3936
4090
|
const deadline = Date.now() + this.peerWaitMs;
|
|
3937
4091
|
while (Date.now() < deadline) {
|
|
@@ -3952,7 +4106,7 @@ var StreamMigration = class {
|
|
|
3952
4106
|
}
|
|
3953
4107
|
} catch (rollbackErr) {
|
|
3954
4108
|
this.logger.error(
|
|
3955
|
-
`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:`,
|
|
3956
4110
|
rollbackErr
|
|
3957
4111
|
);
|
|
3958
4112
|
}
|
|
@@ -3976,17 +4130,33 @@ var StreamMigration = class {
|
|
|
3976
4130
|
}
|
|
3977
4131
|
};
|
|
3978
4132
|
|
|
4133
|
+
// src/server/infrastructure/subject-utils.ts
|
|
4134
|
+
var subjectCovers = (broad, narrow) => {
|
|
4135
|
+
if (broad === narrow) return false;
|
|
4136
|
+
const broadTokens = broad.split(".");
|
|
4137
|
+
const narrowTokens = narrow.split(".");
|
|
4138
|
+
for (let i = 0; i < broadTokens.length; i += 1) {
|
|
4139
|
+
if (broadTokens[i] === ">") return i < narrowTokens.length;
|
|
4140
|
+
if (i >= narrowTokens.length || narrowTokens[i] === ">") return false;
|
|
4141
|
+
if (broadTokens[i] !== "*" && broadTokens[i] !== narrowTokens[i]) return false;
|
|
4142
|
+
}
|
|
4143
|
+
return broadTokens.length === narrowTokens.length;
|
|
4144
|
+
};
|
|
4145
|
+
var coversOrEquals = (broad, subject) => broad === subject || subjectCovers(broad, subject);
|
|
4146
|
+
|
|
3979
4147
|
// src/server/infrastructure/stream.provider.ts
|
|
3980
4148
|
var StreamProvider = class {
|
|
3981
|
-
constructor(options, connection) {
|
|
4149
|
+
constructor(options, connection, names, binder) {
|
|
3982
4150
|
this.options = options;
|
|
3983
4151
|
this.connection = connection;
|
|
4152
|
+
this.names = names;
|
|
4153
|
+
this.binder = binder;
|
|
3984
4154
|
const derived = deriveOtelAttrs(options);
|
|
3985
4155
|
this.otel = derived.otel;
|
|
3986
4156
|
this.otelServiceName = derived.serviceName;
|
|
3987
4157
|
this.otelEndpoint = derived.serverEndpoint;
|
|
3988
4158
|
}
|
|
3989
|
-
logger = new
|
|
4159
|
+
logger = new Logger14("Jetstream:Stream");
|
|
3990
4160
|
migration = new StreamMigration();
|
|
3991
4161
|
otel;
|
|
3992
4162
|
otelServiceName;
|
|
@@ -4000,47 +4170,48 @@ var StreamProvider = class {
|
|
|
4000
4170
|
*/
|
|
4001
4171
|
async ensureStreams(kinds) {
|
|
4002
4172
|
const jsm = await this.connection.getJetStreamManager();
|
|
4003
|
-
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 */;
|
|
4004
4182
|
if (this.options.dlq) {
|
|
4005
|
-
|
|
4183
|
+
if (dlqIsManual) {
|
|
4184
|
+
external.push({ kind: "dlq", name: this.names.dlqStreamName() });
|
|
4185
|
+
} else {
|
|
4186
|
+
reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
|
|
4187
|
+
}
|
|
4006
4188
|
}
|
|
4007
4189
|
this.logger.log(`
|
|
4008
|
-
${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
4009
|
-
if (this.options.provisioning?.preflightStorageCheck) {
|
|
4190
|
+
${formatProvisioningSummary(this.options.name, reservations, external)}`);
|
|
4191
|
+
if (this.options.provisioning?.preflightStorageCheck && reservations.length > 0) {
|
|
4010
4192
|
await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
|
|
4011
4193
|
}
|
|
4012
|
-
await Promise.all(
|
|
4194
|
+
await Promise.all([
|
|
4195
|
+
...autoKinds.map((kind) => this.ensureStream(jsm, kind)),
|
|
4196
|
+
...externalKinds.map((kind) => this.bindStream(jsm, kind))
|
|
4197
|
+
]);
|
|
4013
4198
|
if (this.options.dlq) {
|
|
4014
|
-
|
|
4199
|
+
if (dlqIsManual) {
|
|
4200
|
+
await this.bindDlqStream(jsm);
|
|
4201
|
+
} else {
|
|
4202
|
+
await this.ensureDlqStream(jsm);
|
|
4203
|
+
}
|
|
4015
4204
|
}
|
|
4016
4205
|
}
|
|
4017
4206
|
/** Get the stream name for a given kind. */
|
|
4018
4207
|
getStreamName(kind) {
|
|
4019
|
-
return
|
|
4208
|
+
return this.names.streamName(kind);
|
|
4020
4209
|
}
|
|
4021
4210
|
/** Get the subjects pattern for a given kind. */
|
|
4022
4211
|
getSubjects(kind) {
|
|
4023
|
-
const
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
const subjects = [`${name}.${"ev" /* Event */}.>`];
|
|
4027
|
-
if (this.isSchedulingEnabled(kind)) {
|
|
4028
|
-
subjects.push(`${name}._sch.>`);
|
|
4029
|
-
}
|
|
4030
|
-
return subjects;
|
|
4031
|
-
}
|
|
4032
|
-
case "cmd" /* Command */:
|
|
4033
|
-
return [`${name}.${"cmd" /* Command */}.>`];
|
|
4034
|
-
case "broadcast" /* Broadcast */: {
|
|
4035
|
-
const subjects = ["broadcast.>"];
|
|
4036
|
-
if (this.isSchedulingEnabled(kind)) {
|
|
4037
|
-
subjects.push("broadcast._sch.>");
|
|
4038
|
-
}
|
|
4039
|
-
return subjects;
|
|
4040
|
-
}
|
|
4041
|
-
case "ordered" /* Ordered */:
|
|
4042
|
-
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
4043
|
-
}
|
|
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];
|
|
4044
4215
|
}
|
|
4045
4216
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
4046
4217
|
async ensureStream(jsm, kind) {
|
|
@@ -4107,7 +4278,8 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4107
4278
|
}
|
|
4108
4279
|
async handleExistingStream(jsm, currentInfo, config, ctx) {
|
|
4109
4280
|
if (this.isSharedStream(config.name)) {
|
|
4110
|
-
|
|
4281
|
+
const merged = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
|
|
4282
|
+
config.subjects = merged.filter((s) => !merged.some((other) => subjectCovers(other, s)));
|
|
4111
4283
|
}
|
|
4112
4284
|
const diff = compareStreamConfig(currentInfo.config, config);
|
|
4113
4285
|
if (!diff.hasChanges) {
|
|
@@ -4116,7 +4288,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4116
4288
|
}
|
|
4117
4289
|
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
4118
4290
|
if (diff.hasTransportControlledConflicts) {
|
|
4119
|
-
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(", ");
|
|
4120
4292
|
throw new Error(
|
|
4121
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.`
|
|
4122
4294
|
);
|
|
@@ -4166,13 +4338,13 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4166
4338
|
}
|
|
4167
4339
|
logChanges(streamName2, diff, migrationEnabled) {
|
|
4168
4340
|
for (const c of diff.changes) {
|
|
4169
|
-
const detail = `${c.property}: ${JSON.stringify(c.current)}
|
|
4341
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`;
|
|
4170
4342
|
if (c.mutability === "transport-controlled") {
|
|
4171
4343
|
this.logger.error(
|
|
4172
|
-
`Stream ${streamName2}: ${detail}
|
|
4344
|
+
`Stream ${streamName2}: ${detail}; transport-controlled, cannot be changed`
|
|
4173
4345
|
);
|
|
4174
4346
|
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
4175
|
-
this.logger.warn(`Stream ${streamName2}: ${detail}
|
|
4347
|
+
this.logger.warn(`Stream ${streamName2}: ${detail}; requires allowDestructiveMigration`);
|
|
4176
4348
|
} else {
|
|
4177
4349
|
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
4178
4350
|
}
|
|
@@ -4210,7 +4382,47 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4210
4382
|
throw err;
|
|
4211
4383
|
}
|
|
4212
4384
|
}
|
|
4213
|
-
|
|
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. */
|
|
4214
4426
|
isSharedStream(name) {
|
|
4215
4427
|
return name === this.getStreamName("broadcast" /* Broadcast */);
|
|
4216
4428
|
}
|
|
@@ -4230,13 +4442,11 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4230
4442
|
};
|
|
4231
4443
|
}
|
|
4232
4444
|
/**
|
|
4233
|
-
* Build the stream
|
|
4234
|
-
*
|
|
4235
|
-
* Merges the library default DLQ config with user-provided overrides.
|
|
4236
|
-
* 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.
|
|
4237
4447
|
*/
|
|
4238
4448
|
buildDlqConfig() {
|
|
4239
|
-
const name =
|
|
4449
|
+
const name = this.names.dlqStreamName();
|
|
4240
4450
|
const subjects = [name];
|
|
4241
4451
|
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
4242
4452
|
const overrides = this.options.dlq?.stream ?? {};
|
|
@@ -4269,22 +4479,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4269
4479
|
}
|
|
4270
4480
|
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
4271
4481
|
getOverrides(kind) {
|
|
4272
|
-
|
|
4273
|
-
switch (kind) {
|
|
4274
|
-
case "ev" /* Event */:
|
|
4275
|
-
overrides = this.options.events?.stream ?? {};
|
|
4276
|
-
break;
|
|
4277
|
-
case "cmd" /* Command */:
|
|
4278
|
-
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
4279
|
-
break;
|
|
4280
|
-
case "broadcast" /* Broadcast */:
|
|
4281
|
-
overrides = this.options.broadcast?.stream ?? {};
|
|
4282
|
-
break;
|
|
4283
|
-
case "ordered" /* Ordered */:
|
|
4284
|
-
overrides = this.options.ordered?.stream ?? {};
|
|
4285
|
-
break;
|
|
4286
|
-
}
|
|
4287
|
-
return this.stripTransportControlled(overrides);
|
|
4482
|
+
return this.stripTransportControlled(kindOptionsBlock(this.options, kind)?.stream ?? {});
|
|
4288
4483
|
}
|
|
4289
4484
|
/**
|
|
4290
4485
|
* Remove transport-controlled properties from user overrides.
|
|
@@ -4294,7 +4489,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4294
4489
|
stripTransportControlled(overrides) {
|
|
4295
4490
|
if (!("retention" in overrides)) return overrides;
|
|
4296
4491
|
this.logger.debug(
|
|
4297
|
-
"Stripping user-provided retention override
|
|
4492
|
+
"Stripping user-provided retention override; retention is managed by the transport"
|
|
4298
4493
|
);
|
|
4299
4494
|
const cleaned = { ...overrides };
|
|
4300
4495
|
delete cleaned.retention;
|
|
@@ -4303,20 +4498,22 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
|
|
|
4303
4498
|
};
|
|
4304
4499
|
|
|
4305
4500
|
// src/server/infrastructure/consumer.provider.ts
|
|
4306
|
-
import { Logger as
|
|
4501
|
+
import { Logger as Logger15 } from "@nestjs/common";
|
|
4307
4502
|
import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
|
|
4308
4503
|
var ConsumerProvider = class {
|
|
4309
|
-
constructor(options, connection, streamProvider, patternRegistry) {
|
|
4504
|
+
constructor(options, connection, streamProvider, patternRegistry, names, binder) {
|
|
4310
4505
|
this.options = options;
|
|
4311
4506
|
this.connection = connection;
|
|
4312
4507
|
this.streamProvider = streamProvider;
|
|
4313
4508
|
this.patternRegistry = patternRegistry;
|
|
4509
|
+
this.names = names;
|
|
4510
|
+
this.binder = binder;
|
|
4314
4511
|
const derived = deriveOtelAttrs(options);
|
|
4315
4512
|
this.otel = derived.otel;
|
|
4316
4513
|
this.otelServiceName = derived.serviceName;
|
|
4317
4514
|
this.otelEndpoint = derived.serverEndpoint;
|
|
4318
4515
|
}
|
|
4319
|
-
logger = new
|
|
4516
|
+
logger = new Logger15("Jetstream:Consumer");
|
|
4320
4517
|
otel;
|
|
4321
4518
|
otelServiceName;
|
|
4322
4519
|
otelEndpoint;
|
|
@@ -4338,24 +4535,33 @@ var ConsumerProvider = class {
|
|
|
4338
4535
|
}
|
|
4339
4536
|
/** Get the consumer name for a given kind. */
|
|
4340
4537
|
getConsumerName(kind) {
|
|
4341
|
-
return
|
|
4538
|
+
return this.names.consumerName(kind);
|
|
4342
4539
|
}
|
|
4343
4540
|
/**
|
|
4344
4541
|
* Ensure a single consumer exists with the desired config.
|
|
4345
|
-
*
|
|
4346
|
-
* the current pod's configuration.
|
|
4542
|
+
* Startup path: creates or updates the consumer to match the current pod's configuration.
|
|
4347
4543
|
*/
|
|
4348
4544
|
async ensureConsumer(jsm, kind) {
|
|
4349
4545
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4350
4546
|
const config = this.buildConfig(kind);
|
|
4351
4547
|
const name = config.durable_name;
|
|
4352
|
-
|
|
4353
|
-
this.
|
|
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
|
+
}
|
|
4561
|
+
return withProvisioningSpan(
|
|
4562
|
+
this.otel,
|
|
4354
4563
|
{
|
|
4355
|
-
|
|
4356
|
-
endpoint: this.otelEndpoint,
|
|
4357
|
-
entity: "consumer",
|
|
4358
|
-
name,
|
|
4564
|
+
...spanAttrs,
|
|
4359
4565
|
action: "ensure"
|
|
4360
4566
|
},
|
|
4361
4567
|
async () => {
|
|
@@ -4376,28 +4582,40 @@ var ConsumerProvider = class {
|
|
|
4376
4582
|
}
|
|
4377
4583
|
/**
|
|
4378
4584
|
* Recover a consumer that disappeared during runtime.
|
|
4379
|
-
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
4380
|
-
*
|
|
4381
|
-
* If a migration backup stream exists, another pod is mid-migration — we
|
|
4382
|
-
* throw so the self-healing retry loop waits with backoff until migration
|
|
4383
|
-
* completes and the backup is cleaned up.
|
|
4384
4585
|
*
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4387
|
-
*
|
|
4388
|
-
*
|
|
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.
|
|
4389
4590
|
*/
|
|
4390
4591
|
async recoverConsumer(jsm, kind) {
|
|
4391
4592
|
const stream = this.streamProvider.getStreamName(kind);
|
|
4392
4593
|
const config = this.buildConfig(kind);
|
|
4393
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
|
+
}
|
|
4394
4615
|
return withProvisioningSpan(
|
|
4395
4616
|
this.otel,
|
|
4396
4617
|
{
|
|
4397
|
-
|
|
4398
|
-
endpoint: this.otelEndpoint,
|
|
4399
|
-
entity: "consumer",
|
|
4400
|
-
name,
|
|
4618
|
+
...spanAttrs,
|
|
4401
4619
|
action: "recover"
|
|
4402
4620
|
},
|
|
4403
4621
|
async () => {
|
|
@@ -4433,9 +4651,7 @@ var ConsumerProvider = class {
|
|
|
4433
4651
|
throw err;
|
|
4434
4652
|
}
|
|
4435
4653
|
}
|
|
4436
|
-
/**
|
|
4437
|
-
* Create a consumer, handling the race where another pod creates it first.
|
|
4438
|
-
*/
|
|
4654
|
+
/** Create a consumer, handling the race where another pod creates it first. */
|
|
4439
4655
|
async createConsumer(jsm, stream, name, kind, config) {
|
|
4440
4656
|
this.logger.log(`Creating consumer: ${name}`);
|
|
4441
4657
|
const ctx = { entity: "consumer", name, kind };
|
|
@@ -4463,10 +4679,8 @@ var ConsumerProvider = class {
|
|
|
4463
4679
|
}
|
|
4464
4680
|
}
|
|
4465
4681
|
/** Build consumer config by merging defaults with user overrides. */
|
|
4466
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
4467
4682
|
buildConfig(kind) {
|
|
4468
|
-
const
|
|
4469
|
-
const serviceName = internalName(this.options.name);
|
|
4683
|
+
const durableName = this.getConsumerName(kind);
|
|
4470
4684
|
const defaults = this.getDefaults(kind);
|
|
4471
4685
|
const overrides = this.getOverrides(kind);
|
|
4472
4686
|
if (kind === "broadcast" /* Broadcast */) {
|
|
@@ -4478,31 +4692,54 @@ var ConsumerProvider = class {
|
|
|
4478
4692
|
return {
|
|
4479
4693
|
...defaults,
|
|
4480
4694
|
...overrides,
|
|
4481
|
-
name,
|
|
4482
|
-
durable_name:
|
|
4695
|
+
name: durableName,
|
|
4696
|
+
durable_name: durableName,
|
|
4483
4697
|
filter_subject: broadcastPatterns[0]
|
|
4484
4698
|
};
|
|
4485
4699
|
}
|
|
4486
4700
|
return {
|
|
4487
4701
|
...defaults,
|
|
4488
4702
|
...overrides,
|
|
4489
|
-
name,
|
|
4490
|
-
durable_name:
|
|
4703
|
+
name: durableName,
|
|
4704
|
+
durable_name: durableName,
|
|
4491
4705
|
filter_subjects: broadcastPatterns
|
|
4492
4706
|
};
|
|
4493
4707
|
}
|
|
4494
4708
|
if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
|
|
4495
4709
|
throw new Error(`Unexpected durable consumer kind: ${kind}`);
|
|
4496
4710
|
}
|
|
4497
|
-
|
|
4711
|
+
if (this.names.hasCustomPrefix(kind)) {
|
|
4712
|
+
return this.buildCustomPrefixConfig(kind, durableName, defaults, overrides);
|
|
4713
|
+
}
|
|
4714
|
+
const filter_subject = this.names.filterSubject(kind);
|
|
4498
4715
|
return {
|
|
4499
4716
|
...defaults,
|
|
4500
4717
|
...overrides,
|
|
4501
|
-
name,
|
|
4502
|
-
durable_name:
|
|
4718
|
+
name: durableName,
|
|
4719
|
+
durable_name: durableName,
|
|
4503
4720
|
filter_subject
|
|
4504
4721
|
};
|
|
4505
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
|
+
}
|
|
4506
4743
|
/** Get default config for a consumer kind. */
|
|
4507
4744
|
getDefaults(kind) {
|
|
4508
4745
|
switch (kind) {
|
|
@@ -4523,26 +4760,12 @@ var ConsumerProvider = class {
|
|
|
4523
4760
|
}
|
|
4524
4761
|
/** Get user-provided overrides for a consumer kind. */
|
|
4525
4762
|
getOverrides(kind) {
|
|
4526
|
-
|
|
4527
|
-
case "ev" /* Event */:
|
|
4528
|
-
return this.options.events?.consumer ?? {};
|
|
4529
|
-
case "cmd" /* Command */:
|
|
4530
|
-
return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
|
|
4531
|
-
case "broadcast" /* Broadcast */:
|
|
4532
|
-
return this.options.broadcast?.consumer ?? {};
|
|
4533
|
-
case "ordered" /* Ordered */:
|
|
4534
|
-
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
4535
|
-
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
4536
|
-
default: {
|
|
4537
|
-
const _exhaustive = kind;
|
|
4538
|
-
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
4539
|
-
}
|
|
4540
|
-
}
|
|
4763
|
+
return kindOptionsBlock(this.options, kind)?.consumer ?? {};
|
|
4541
4764
|
}
|
|
4542
4765
|
};
|
|
4543
4766
|
|
|
4544
4767
|
// src/server/infrastructure/message.provider.ts
|
|
4545
|
-
import { Logger as
|
|
4768
|
+
import { Logger as Logger16 } from "@nestjs/common";
|
|
4546
4769
|
import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
|
|
4547
4770
|
import {
|
|
4548
4771
|
catchError,
|
|
@@ -4561,7 +4784,7 @@ var MessageProvider = class {
|
|
|
4561
4784
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
4562
4785
|
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
4563
4786
|
}
|
|
4564
|
-
logger = new
|
|
4787
|
+
logger = new Logger16("Jetstream:Message");
|
|
4565
4788
|
activeIterators = /* @__PURE__ */ new Set();
|
|
4566
4789
|
orderedReadyResolve = null;
|
|
4567
4790
|
orderedReadyReject = null;
|
|
@@ -4604,7 +4827,7 @@ var MessageProvider = class {
|
|
|
4604
4827
|
/**
|
|
4605
4828
|
* Start an ordered consumer for strict sequential delivery.
|
|
4606
4829
|
*
|
|
4607
|
-
* Unlike durable consumers, ordered consumers are ephemeral
|
|
4830
|
+
* Unlike durable consumers, ordered consumers are ephemeral: created at
|
|
4608
4831
|
* consumption time, no durable state. nats.js handles auto-recreation.
|
|
4609
4832
|
*
|
|
4610
4833
|
* @param streamName - JetStream stream to consume from.
|
|
@@ -4818,7 +5041,7 @@ var MessageProvider = class {
|
|
|
4818
5041
|
};
|
|
4819
5042
|
|
|
4820
5043
|
// src/server/infrastructure/metadata.provider.ts
|
|
4821
|
-
import { Logger as
|
|
5044
|
+
import { Logger as Logger17 } from "@nestjs/common";
|
|
4822
5045
|
import { Kvm } from "@nats-io/kv";
|
|
4823
5046
|
var MetadataProvider = class {
|
|
4824
5047
|
constructor(options, connection) {
|
|
@@ -4827,7 +5050,7 @@ var MetadataProvider = class {
|
|
|
4827
5050
|
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
4828
5051
|
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
4829
5052
|
}
|
|
4830
|
-
logger = new
|
|
5053
|
+
logger = new Logger17("Jetstream:Metadata");
|
|
4831
5054
|
bucketName;
|
|
4832
5055
|
replicas;
|
|
4833
5056
|
ttl;
|
|
@@ -4835,16 +5058,12 @@ var MetadataProvider = class {
|
|
|
4835
5058
|
heartbeatTimer;
|
|
4836
5059
|
cachedKv;
|
|
4837
5060
|
/**
|
|
4838
|
-
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
5061
|
+
* Write handler metadata entries to the KV bucket and start the heartbeat.
|
|
4839
5062
|
*
|
|
4840
|
-
* Creates the bucket if
|
|
4841
|
-
*
|
|
4842
|
-
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
4843
|
-
* 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.
|
|
4844
5065
|
*
|
|
4845
|
-
*
|
|
4846
|
-
*
|
|
4847
|
-
* @param entries Map of KV key → metadata object.
|
|
5066
|
+
* @param entries Map of KV key to metadata object.
|
|
4848
5067
|
*/
|
|
4849
5068
|
async publish(entries) {
|
|
4850
5069
|
if (entries.size === 0) return;
|
|
@@ -4920,396 +5139,626 @@ var MetadataProvider = class {
|
|
|
4920
5139
|
};
|
|
4921
5140
|
|
|
4922
5141
|
// src/server/routing/event.router.ts
|
|
4923
|
-
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";
|
|
4924
5225
|
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
4925
5226
|
var DLQ_PUBLISH_ATTEMPTS = 3;
|
|
4926
|
-
var
|
|
4927
|
-
|
|
4928
|
-
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
4929
|
-
return "event" /* Event */;
|
|
4930
|
-
};
|
|
4931
|
-
var EventRouter = class {
|
|
4932
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
4933
|
-
this.messageProvider = messageProvider;
|
|
5227
|
+
var DeadLetterCapture = class {
|
|
5228
|
+
constructor(patternRegistry, eventBus, deadLetterConfig, otel, serviceName, serverEndpoint, connection, options, names) {
|
|
4934
5229
|
this.patternRegistry = patternRegistry;
|
|
4935
|
-
this.codec = codec;
|
|
4936
5230
|
this.eventBus = eventBus;
|
|
4937
5231
|
this.deadLetterConfig = deadLetterConfig;
|
|
4938
|
-
this.
|
|
4939
|
-
this.
|
|
5232
|
+
this.otel = otel;
|
|
5233
|
+
this.serviceName = serviceName;
|
|
5234
|
+
this.serverEndpoint = serverEndpoint;
|
|
4940
5235
|
this.connection = connection;
|
|
4941
5236
|
this.options = options;
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
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
|
+
);
|
|
4952
5279
|
}
|
|
4953
|
-
logger = new Logger17("Jetstream:EventRouter");
|
|
4954
|
-
subscriptions = [];
|
|
4955
|
-
otel;
|
|
4956
|
-
serviceName;
|
|
4957
|
-
serverEndpoint;
|
|
4958
5280
|
/**
|
|
4959
|
-
*
|
|
4960
|
-
*
|
|
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.
|
|
4961
5284
|
*/
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
this.
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
if (this.patternRegistry.hasOrderedHandlers()) {
|
|
4971
|
-
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;
|
|
4972
5293
|
}
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
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);
|
|
4978
5309
|
}
|
|
4979
|
-
this.subscriptions.length = 0;
|
|
4980
5310
|
}
|
|
4981
|
-
/**
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
4998
|
-
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
4999
|
-
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
5000
|
-
const pattern = declared?.pattern ?? msg.subject;
|
|
5001
|
-
const declaredKind = declared?.kind ?? kind;
|
|
5002
|
-
const durationMs = performance.now() - startedAt;
|
|
5003
|
-
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
5004
|
-
};
|
|
5005
|
-
const isDeadLetter = (msg) => {
|
|
5006
|
-
if (!hasDlqCheck) return false;
|
|
5007
|
-
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
5008
|
-
if (maxDeliver === void 0 || maxDeliver <= 0) return false;
|
|
5009
|
-
return msg.info.deliveryCount >= maxDeliver;
|
|
5010
|
-
};
|
|
5011
|
-
const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
|
|
5012
|
-
const settleSuccess = (msg, ctx, data) => {
|
|
5013
|
-
if (ctx.shouldTerminate) {
|
|
5014
|
-
settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
|
|
5015
|
-
msg.term(ctx.terminateReason);
|
|
5016
|
-
});
|
|
5017
|
-
return void 0;
|
|
5018
|
-
}
|
|
5019
|
-
if (ctx.shouldRetry) {
|
|
5020
|
-
if (handleDeadLetter !== null && isDeadLetter(msg)) {
|
|
5021
|
-
return handleDeadLetter(
|
|
5022
|
-
msg,
|
|
5023
|
-
data,
|
|
5024
|
-
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`
|
|
5025
5327
|
);
|
|
5026
5328
|
}
|
|
5027
|
-
settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
|
|
5028
|
-
msg.nak(ctx.retryDelay);
|
|
5029
|
-
});
|
|
5030
|
-
return void 0;
|
|
5031
5329
|
}
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
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);
|
|
5041
5344
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
let data;
|
|
5345
|
+
}
|
|
5346
|
+
return hdrs;
|
|
5347
|
+
}
|
|
5348
|
+
async notifyDeadLetterCallback(info, msg) {
|
|
5349
|
+
if (this.deadLetterConfig.onDeadLetter) {
|
|
5048
5350
|
try {
|
|
5049
|
-
|
|
5050
|
-
} catch {
|
|
5051
|
-
|
|
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
|
+
);
|
|
5052
5357
|
}
|
|
5053
|
-
|
|
5054
|
-
|
|
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();
|
|
5055
5375
|
});
|
|
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
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
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}`));
|
|
5087
5481
|
}
|
|
5088
|
-
|
|
5089
|
-
return
|
|
5482
|
+
msg.term(`No handler for event: ${subject}`);
|
|
5483
|
+
return null;
|
|
5484
|
+
}
|
|
5485
|
+
let data;
|
|
5486
|
+
try {
|
|
5487
|
+
data = codec.decode(msg.data);
|
|
5090
5488
|
} catch (err) {
|
|
5091
|
-
logger5.error(`
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
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
|
+
);
|
|
5096
5496
|
}
|
|
5497
|
+
msg.term("Decode error");
|
|
5097
5498
|
return null;
|
|
5098
5499
|
}
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
return "success";
|
|
5104
|
-
};
|
|
5105
|
-
const handleSafe = (msg) => {
|
|
5106
|
-
const resolved = resolveEvent(msg);
|
|
5107
|
-
if (resolved === null) return void 0;
|
|
5108
|
-
if (isPromiseLike2(resolved)) return resolved;
|
|
5109
|
-
const { handler, data } = resolved;
|
|
5110
|
-
const ctx = new RpcContext([msg]);
|
|
5111
|
-
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5112
|
-
const startedAt = performance.now();
|
|
5113
|
-
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);
|
|
5114
5504
|
try {
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
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) => {
|
|
5130
5576
|
eventBus.emit(
|
|
5131
5577
|
"error" /* Error */,
|
|
5132
5578
|
err instanceof Error ? err : new Error(String(err)),
|
|
5133
5579
|
`${kind}-handler:${msg.subject}`
|
|
5134
5580
|
);
|
|
5135
5581
|
reportHandlerCompleted(msg, startedAt, "error");
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
}
|
|
5139
|
-
}
|
|
5140
|
-
if (!isPromiseLike2(pending)) {
|
|
5141
|
-
const settled = settleSuccess(msg, ctx, data);
|
|
5142
|
-
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
5143
|
-
if (settled === void 0) {
|
|
5582
|
+
try {
|
|
5583
|
+
await settleFailure(msg, data, err);
|
|
5584
|
+
} finally {
|
|
5144
5585
|
if (stopAckExtension !== null) stopAckExtension();
|
|
5145
|
-
return void 0;
|
|
5146
5586
|
}
|
|
5147
|
-
return settled.finally(() => {
|
|
5148
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5149
|
-
});
|
|
5150
5587
|
}
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5167
|
-
try {
|
|
5168
|
-
await settleFailure(msg, data, err);
|
|
5169
|
-
} finally {
|
|
5170
|
-
if (stopAckExtension !== null) stopAckExtension();
|
|
5171
|
-
}
|
|
5172
|
-
}
|
|
5173
|
-
);
|
|
5174
|
-
};
|
|
5175
|
-
const handleOrderedSafe = (msg) => {
|
|
5176
|
-
const subject = msg.subject;
|
|
5177
|
-
let handler;
|
|
5178
|
-
let data;
|
|
5179
|
-
try {
|
|
5180
|
-
handler = patternRegistry.getHandler(subject);
|
|
5181
|
-
if (!handler) {
|
|
5182
|
-
logger5.error(`No handler for subject: ${subject}`);
|
|
5183
|
-
return void 0;
|
|
5184
|
-
}
|
|
5185
|
-
try {
|
|
5186
|
-
data = codec.decode(msg.data);
|
|
5187
|
-
} catch (err) {
|
|
5188
|
-
logger5.error(`Decode error for ${subject}:`, err);
|
|
5189
|
-
return void 0;
|
|
5190
|
-
}
|
|
5191
|
-
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
5192
|
-
} catch (err) {
|
|
5193
|
-
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}`);
|
|
5194
5603
|
return void 0;
|
|
5195
5604
|
}
|
|
5196
|
-
const ctx = new RpcContext([msg]);
|
|
5197
|
-
const warnIfSettlementAttempted = () => {
|
|
5198
|
-
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
5199
|
-
logger5.warn(
|
|
5200
|
-
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
5201
|
-
);
|
|
5202
|
-
}
|
|
5203
|
-
};
|
|
5204
|
-
const startedAt = performance.now();
|
|
5205
|
-
let pending;
|
|
5206
5605
|
try {
|
|
5207
|
-
|
|
5208
|
-
{
|
|
5209
|
-
subject: msg.subject,
|
|
5210
|
-
msg,
|
|
5211
|
-
info: msg.info,
|
|
5212
|
-
kind: spanKind,
|
|
5213
|
-
payloadBytes: msg.data.length,
|
|
5214
|
-
handlerMetadata: { pattern: msg.subject },
|
|
5215
|
-
serviceName,
|
|
5216
|
-
endpoint: serverEndpoint
|
|
5217
|
-
},
|
|
5218
|
-
otel,
|
|
5219
|
-
() => unwrapResult(handler(data, ctx))
|
|
5220
|
-
);
|
|
5606
|
+
data = codec.decode(msg.data);
|
|
5221
5607
|
} catch (err) {
|
|
5222
|
-
logger5.error(`
|
|
5223
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5608
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
5224
5609
|
return void 0;
|
|
5225
5610
|
}
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
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
|
+
);
|
|
5230
5622
|
}
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
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
|
|
5235
5637
|
},
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
reportHandlerCompleted(msg, startedAt, "error");
|
|
5239
|
-
}
|
|
5638
|
+
otel,
|
|
5639
|
+
() => unwrapResult(handler(data, ctx))
|
|
5240
5640
|
);
|
|
5241
|
-
}
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
}
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
};
|
|
5260
|
-
const trackAsync = (result, msg) => {
|
|
5261
|
-
void result.catch((err) => {
|
|
5262
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5263
|
-
}).finally(onAsyncDone);
|
|
5264
|
-
};
|
|
5265
|
-
const drainBacklog = () => {
|
|
5266
|
-
while (active < maxActive) {
|
|
5267
|
-
const next = backlog.shift();
|
|
5268
|
-
if (next === void 0) return;
|
|
5269
|
-
next.stopAckExtension?.();
|
|
5270
|
-
active++;
|
|
5271
|
-
const result = routeSafely(next.msg);
|
|
5272
|
-
if (result !== void 0) {
|
|
5273
|
-
trackAsync(result, next.msg);
|
|
5274
|
-
} else {
|
|
5275
|
-
active--;
|
|
5276
|
-
}
|
|
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");
|
|
5277
5659
|
}
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
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
|
|
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);
|
|
5752
|
+
const subscription = stream$.subscribe({
|
|
5753
|
+
next: (msg) => {
|
|
5754
|
+
gate.push(msg);
|
|
5303
5755
|
},
|
|
5304
5756
|
error: (err) => {
|
|
5305
|
-
|
|
5757
|
+
this.logger.error(`Stream error in ${kind} router`, err);
|
|
5306
5758
|
}
|
|
5307
5759
|
});
|
|
5308
5760
|
subscription.add(() => {
|
|
5309
|
-
|
|
5310
|
-
queued.stopAckExtension?.();
|
|
5311
|
-
}
|
|
5312
|
-
backlog.length = 0;
|
|
5761
|
+
gate.dispose();
|
|
5313
5762
|
});
|
|
5314
5763
|
this.subscriptions.push(subscription);
|
|
5315
5764
|
}
|
|
@@ -5323,177 +5772,10 @@ var EventRouter = class {
|
|
|
5323
5772
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
5324
5773
|
return void 0;
|
|
5325
5774
|
}
|
|
5326
|
-
/**
|
|
5327
|
-
* Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
|
|
5328
|
-
* success. On failure the message is nak'd to release it, but the server
|
|
5329
|
-
* never redelivers past `max_deliver` — it stays in the stream for manual
|
|
5330
|
-
* recovery. Used when the DLQ stream isn't configured, or when publishing
|
|
5331
|
-
* to it failed and we still have to surface the message somewhere.
|
|
5332
|
-
*/
|
|
5333
|
-
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
5334
|
-
const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
|
|
5335
|
-
if (!onDeadLetter) {
|
|
5336
|
-
this.logger.error(
|
|
5337
|
-
`Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
|
|
5338
|
-
);
|
|
5339
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5340
|
-
msg.nak();
|
|
5341
|
-
});
|
|
5342
|
-
return;
|
|
5343
|
-
}
|
|
5344
|
-
try {
|
|
5345
|
-
await onDeadLetter(info);
|
|
5346
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5347
|
-
msg.term("Dead letter processed via fallback callback");
|
|
5348
|
-
});
|
|
5349
|
-
} catch (hookErr) {
|
|
5350
|
-
this.logger.error(
|
|
5351
|
-
`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:`,
|
|
5352
|
-
hookErr
|
|
5353
|
-
);
|
|
5354
|
-
settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
|
|
5355
|
-
msg.nak();
|
|
5356
|
-
});
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
/**
|
|
5360
|
-
* Copy the original message headers for the DLQ republish, dropping NATS
|
|
5361
|
-
* server control headers: a copied Nats-TTL expires the DLQ entry (or gets
|
|
5362
|
-
* the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
|
|
5363
|
-
* Nats-Msg-Id collides with the DLQ dedup window.
|
|
5364
|
-
*/
|
|
5365
|
-
buildDlqHeaders(msg) {
|
|
5366
|
-
const hdrs = natsHeaders3();
|
|
5367
|
-
if (!msg.headers) return hdrs;
|
|
5368
|
-
for (const [k, v] of msg.headers) {
|
|
5369
|
-
if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
|
|
5370
|
-
for (const val of v) {
|
|
5371
|
-
hdrs.append(k, val);
|
|
5372
|
-
}
|
|
5373
|
-
}
|
|
5374
|
-
return hdrs;
|
|
5375
|
-
}
|
|
5376
|
-
/**
|
|
5377
|
-
* Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
|
|
5378
|
-
*
|
|
5379
|
-
* Past `max_deliver` the server never redelivers, so an in-process retry is
|
|
5380
|
-
* the only second chance a dead letter gets. There is no artificial delay
|
|
5381
|
-
* between attempts: when the broker is unreachable each publish already
|
|
5382
|
-
* spends its own request timeout, which spaces the attempts naturally.
|
|
5383
|
-
*/
|
|
5384
|
-
async publishToDlqWithRetry(connection, subject, data, headers2) {
|
|
5385
|
-
let lastErr;
|
|
5386
|
-
for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
5387
|
-
try {
|
|
5388
|
-
await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
|
|
5389
|
-
return;
|
|
5390
|
-
} catch (err) {
|
|
5391
|
-
lastErr = err;
|
|
5392
|
-
if (attempt < DLQ_PUBLISH_ATTEMPTS) {
|
|
5393
|
-
this.logger.warn(
|
|
5394
|
-
`DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
|
|
5395
|
-
);
|
|
5396
|
-
}
|
|
5397
|
-
}
|
|
5398
|
-
}
|
|
5399
|
-
throw lastErr;
|
|
5400
|
-
}
|
|
5401
|
-
/**
|
|
5402
|
-
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
5403
|
-
*
|
|
5404
|
-
* Appends diagnostic metadata headers to the original message and preserves
|
|
5405
|
-
* the primary payload. If publishing succeeds, it notifies the standard
|
|
5406
|
-
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
5407
|
-
* back to the callback entirely to prevent silent data loss.
|
|
5408
|
-
*/
|
|
5409
|
-
async publishToDlq(msg, info, error) {
|
|
5410
|
-
const serviceName = this.options?.name;
|
|
5411
|
-
if (!this.connection || !serviceName) {
|
|
5412
|
-
this.logger.error(
|
|
5413
|
-
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
5414
|
-
);
|
|
5415
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5416
|
-
return;
|
|
5417
|
-
}
|
|
5418
|
-
const destinationSubject = dlqStreamName(serviceName);
|
|
5419
|
-
const hdrs = this.buildDlqHeaders(msg);
|
|
5420
|
-
let reason = String(error);
|
|
5421
|
-
if (error instanceof Error) {
|
|
5422
|
-
reason = error.message;
|
|
5423
|
-
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
5424
|
-
reason = String(error.message);
|
|
5425
|
-
}
|
|
5426
|
-
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
5427
|
-
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
5428
|
-
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
5429
|
-
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
5430
|
-
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
5431
|
-
try {
|
|
5432
|
-
await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
|
|
5433
|
-
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
5434
|
-
if (this.deadLetterConfig?.onDeadLetter) {
|
|
5435
|
-
try {
|
|
5436
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
5437
|
-
} catch (hookErr) {
|
|
5438
|
-
this.logger.warn(
|
|
5439
|
-
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
5440
|
-
hookErr
|
|
5441
|
-
);
|
|
5442
|
-
}
|
|
5443
|
-
}
|
|
5444
|
-
settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
|
|
5445
|
-
msg.term("Moved to DLQ stream");
|
|
5446
|
-
});
|
|
5447
|
-
} catch (publishErr) {
|
|
5448
|
-
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
5449
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5450
|
-
}
|
|
5451
|
-
}
|
|
5452
|
-
/**
|
|
5453
|
-
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
5454
|
-
*
|
|
5455
|
-
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
5456
|
-
* or directly to the fallback callback based on the active module configuration.
|
|
5457
|
-
*/
|
|
5458
|
-
async handleDeadLetter(msg, data, error) {
|
|
5459
|
-
const info = {
|
|
5460
|
-
subject: msg.subject,
|
|
5461
|
-
data,
|
|
5462
|
-
headers: msg.headers,
|
|
5463
|
-
error,
|
|
5464
|
-
deliveryCount: msg.info.deliveryCount,
|
|
5465
|
-
stream: msg.info.stream,
|
|
5466
|
-
streamSequence: msg.info.streamSequence,
|
|
5467
|
-
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
5468
|
-
};
|
|
5469
|
-
await withDeadLetterSpan(
|
|
5470
|
-
{
|
|
5471
|
-
msg,
|
|
5472
|
-
// Pattern resolution mirrors event-routing: when a registered
|
|
5473
|
-
// pattern matches, surface it on the DLQ span so APM can filter
|
|
5474
|
-
// dead letters by handler without parsing the subject. Falls back
|
|
5475
|
-
// to the subject itself when no glob handler is in play.
|
|
5476
|
-
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
5477
|
-
finalDeliveryCount: msg.info.deliveryCount,
|
|
5478
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
5479
|
-
serviceName: this.serviceName,
|
|
5480
|
-
endpoint: this.serverEndpoint
|
|
5481
|
-
},
|
|
5482
|
-
this.otel,
|
|
5483
|
-
async () => {
|
|
5484
|
-
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
5485
|
-
if (!this.options?.dlq) {
|
|
5486
|
-
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
5487
|
-
} else {
|
|
5488
|
-
await this.publishToDlq(msg, info, error);
|
|
5489
|
-
}
|
|
5490
|
-
}
|
|
5491
|
-
);
|
|
5492
|
-
}
|
|
5493
5775
|
};
|
|
5494
5776
|
|
|
5495
5777
|
// src/server/routing/rpc.router.ts
|
|
5496
|
-
import { Logger as
|
|
5778
|
+
import { Logger as Logger20 } from "@nestjs/common";
|
|
5497
5779
|
import { headers } from "@nats-io/transport-node";
|
|
5498
5780
|
var RpcRouter = class {
|
|
5499
5781
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
|
|
@@ -5517,7 +5799,7 @@ var RpcRouter = class {
|
|
|
5517
5799
|
this.serverEndpoint = null;
|
|
5518
5800
|
}
|
|
5519
5801
|
}
|
|
5520
|
-
logger = new
|
|
5802
|
+
logger = new Logger20("Jetstream:RpcRouter");
|
|
5521
5803
|
timeout;
|
|
5522
5804
|
concurrency;
|
|
5523
5805
|
resolvedAckExtensionInterval;
|
|
@@ -5708,75 +5990,18 @@ var RpcRouter = class {
|
|
|
5708
5990
|
}
|
|
5709
5991
|
);
|
|
5710
5992
|
};
|
|
5711
|
-
const
|
|
5712
|
-
|
|
5713
|
-
let backlogWarned = false;
|
|
5714
|
-
const backlog = [];
|
|
5715
|
-
const onAsyncDone = () => {
|
|
5716
|
-
active--;
|
|
5717
|
-
drainBacklog();
|
|
5718
|
-
};
|
|
5719
|
-
const routeSafely = (msg) => {
|
|
5720
|
-
try {
|
|
5721
|
-
return handleSafe(msg);
|
|
5722
|
-
} catch (err) {
|
|
5723
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5724
|
-
return void 0;
|
|
5725
|
-
}
|
|
5726
|
-
};
|
|
5727
|
-
const trackAsync = (result, msg) => {
|
|
5728
|
-
void result.catch((err) => {
|
|
5729
|
-
logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
|
|
5730
|
-
}).finally(onAsyncDone);
|
|
5731
|
-
};
|
|
5732
|
-
const drainBacklog = () => {
|
|
5733
|
-
while (active < maxActive) {
|
|
5734
|
-
const next = backlog.shift();
|
|
5735
|
-
if (next === void 0) return;
|
|
5736
|
-
next.stopAckExtension?.();
|
|
5737
|
-
active++;
|
|
5738
|
-
const result = routeSafely(next.msg);
|
|
5739
|
-
if (result !== void 0) {
|
|
5740
|
-
trackAsync(result, next.msg);
|
|
5741
|
-
} else {
|
|
5742
|
-
active--;
|
|
5743
|
-
}
|
|
5744
|
-
}
|
|
5745
|
-
if (backlog.length < backlogWarnThreshold) backlogWarned = false;
|
|
5746
|
-
};
|
|
5993
|
+
const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5994
|
+
const gate = new ConcurrencyGate(maxActive, handleSafe, parkTimer, logger5, "RPC");
|
|
5747
5995
|
this.subscription = this.messageProvider.commands$.subscribe({
|
|
5748
5996
|
next: (msg) => {
|
|
5749
|
-
|
|
5750
|
-
backlog.push({
|
|
5751
|
-
msg,
|
|
5752
|
-
stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
|
|
5753
|
-
});
|
|
5754
|
-
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
5755
|
-
backlogWarned = true;
|
|
5756
|
-
logger5.warn(
|
|
5757
|
-
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
5758
|
-
);
|
|
5759
|
-
}
|
|
5760
|
-
return;
|
|
5761
|
-
}
|
|
5762
|
-
active++;
|
|
5763
|
-
const result = routeSafely(msg);
|
|
5764
|
-
if (result !== void 0) {
|
|
5765
|
-
trackAsync(result, msg);
|
|
5766
|
-
} else {
|
|
5767
|
-
active--;
|
|
5768
|
-
if (backlog.length > 0) drainBacklog();
|
|
5769
|
-
}
|
|
5997
|
+
gate.push(msg);
|
|
5770
5998
|
},
|
|
5771
5999
|
error: (err) => {
|
|
5772
6000
|
logger5.error("Stream error in RPC router", err);
|
|
5773
6001
|
}
|
|
5774
6002
|
});
|
|
5775
6003
|
this.subscription.add(() => {
|
|
5776
|
-
|
|
5777
|
-
queued.stopAckExtension?.();
|
|
5778
|
-
}
|
|
5779
|
-
backlog.length = 0;
|
|
6004
|
+
gate.dispose();
|
|
5780
6005
|
});
|
|
5781
6006
|
}
|
|
5782
6007
|
/** Stop routing and unsubscribe. */
|
|
@@ -5786,20 +6011,220 @@ var RpcRouter = class {
|
|
|
5786
6011
|
}
|
|
5787
6012
|
};
|
|
5788
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
|
+
|
|
5789
6214
|
// src/shutdown/shutdown.manager.ts
|
|
5790
|
-
import { Logger as
|
|
6215
|
+
import { Logger as Logger22 } from "@nestjs/common";
|
|
5791
6216
|
var ShutdownManager = class {
|
|
5792
6217
|
constructor(connection, eventBus, timeout) {
|
|
5793
6218
|
this.connection = connection;
|
|
5794
6219
|
this.eventBus = eventBus;
|
|
5795
6220
|
this.timeout = timeout;
|
|
5796
6221
|
}
|
|
5797
|
-
logger = new
|
|
6222
|
+
logger = new Logger22("Jetstream:Shutdown");
|
|
5798
6223
|
shutdownPromise;
|
|
5799
6224
|
/**
|
|
5800
6225
|
* Execute the full shutdown sequence.
|
|
5801
6226
|
*
|
|
5802
|
-
* Idempotent
|
|
6227
|
+
* Idempotent: concurrent or repeated calls return the same promise.
|
|
5803
6228
|
*
|
|
5804
6229
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
5805
6230
|
*/
|
|
@@ -5829,6 +6254,12 @@ var ShutdownManager = class {
|
|
|
5829
6254
|
|
|
5830
6255
|
// src/jetstream.module.ts
|
|
5831
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
|
+
};
|
|
5832
6263
|
var JetstreamModule = class {
|
|
5833
6264
|
constructor(shutdownManager, strategy) {
|
|
5834
6265
|
this.shutdownManager = shutdownManager;
|
|
@@ -5858,7 +6289,8 @@ var JetstreamModule = class {
|
|
|
5858
6289
|
PatternRegistry,
|
|
5859
6290
|
ShutdownManager,
|
|
5860
6291
|
JetstreamStrategy,
|
|
5861
|
-
JetstreamHealthIndicator
|
|
6292
|
+
JetstreamHealthIndicator,
|
|
6293
|
+
NameResolver
|
|
5862
6294
|
]
|
|
5863
6295
|
};
|
|
5864
6296
|
}
|
|
@@ -5887,7 +6319,8 @@ var JetstreamModule = class {
|
|
|
5887
6319
|
PatternRegistry,
|
|
5888
6320
|
ShutdownManager,
|
|
5889
6321
|
JetstreamStrategy,
|
|
5890
|
-
JetstreamHealthIndicator
|
|
6322
|
+
JetstreamHealthIndicator,
|
|
6323
|
+
NameResolver
|
|
5891
6324
|
]
|
|
5892
6325
|
};
|
|
5893
6326
|
}
|
|
@@ -5904,10 +6337,23 @@ var JetstreamModule = class {
|
|
|
5904
6337
|
const clientToken = getClientToken(options.name);
|
|
5905
6338
|
const clientProvider = {
|
|
5906
6339
|
provide: clientToken,
|
|
5907
|
-
inject: [
|
|
5908
|
-
|
|
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) => {
|
|
5909
6348
|
const codec = options.codec ?? rootCodec;
|
|
5910
|
-
return new JetstreamClient(
|
|
6349
|
+
return new JetstreamClient(
|
|
6350
|
+
rootOptions,
|
|
6351
|
+
options.name,
|
|
6352
|
+
connection,
|
|
6353
|
+
codec,
|
|
6354
|
+
eventBus,
|
|
6355
|
+
names ?? void 0
|
|
6356
|
+
);
|
|
5911
6357
|
}
|
|
5912
6358
|
};
|
|
5913
6359
|
return {
|
|
@@ -5928,16 +6374,14 @@ var JetstreamModule = class {
|
|
|
5928
6374
|
/** Create providers that depend on JETSTREAM_OPTIONS (shared by sync and async). */
|
|
5929
6375
|
static createCoreDependentProviders() {
|
|
5930
6376
|
return [
|
|
5931
|
-
// EventBus — hook system with Logger fallback
|
|
5932
6377
|
{
|
|
5933
6378
|
provide: JETSTREAM_EVENT_BUS,
|
|
5934
6379
|
inject: [JETSTREAM_OPTIONS],
|
|
5935
6380
|
useFactory: (options) => {
|
|
5936
|
-
const logger5 = new
|
|
6381
|
+
const logger5 = new Logger23("Jetstream:Module");
|
|
5937
6382
|
return new EventBus(logger5, options.hooks);
|
|
5938
6383
|
}
|
|
5939
6384
|
},
|
|
5940
|
-
// Codec — global encode/decode
|
|
5941
6385
|
{
|
|
5942
6386
|
provide: JETSTREAM_CODEC,
|
|
5943
6387
|
inject: [JETSTREAM_OPTIONS],
|
|
@@ -5945,7 +6389,6 @@ var JetstreamModule = class {
|
|
|
5945
6389
|
return options.codec ?? new JsonCodec();
|
|
5946
6390
|
}
|
|
5947
6391
|
},
|
|
5948
|
-
// ConnectionProvider — single NATS connection
|
|
5949
6392
|
{
|
|
5950
6393
|
provide: JETSTREAM_CONNECTION,
|
|
5951
6394
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_EVENT_BUS],
|
|
@@ -5953,7 +6396,6 @@ var JetstreamModule = class {
|
|
|
5953
6396
|
return new ConnectionProvider(options, eventBus);
|
|
5954
6397
|
}
|
|
5955
6398
|
},
|
|
5956
|
-
// JetstreamHealthIndicator — health check for NATS connection
|
|
5957
6399
|
{
|
|
5958
6400
|
provide: JetstreamHealthIndicator,
|
|
5959
6401
|
inject: [JETSTREAM_CONNECTION],
|
|
@@ -5961,7 +6403,6 @@ var JetstreamModule = class {
|
|
|
5961
6403
|
return new JetstreamHealthIndicator(connection);
|
|
5962
6404
|
}
|
|
5963
6405
|
},
|
|
5964
|
-
// ShutdownManager — graceful shutdown orchestration
|
|
5965
6406
|
{
|
|
5966
6407
|
provide: ShutdownManager,
|
|
5967
6408
|
inject: [JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS],
|
|
@@ -5973,41 +6414,69 @@ var JetstreamModule = class {
|
|
|
5973
6414
|
);
|
|
5974
6415
|
}
|
|
5975
6416
|
},
|
|
5976
|
-
// Consumer infrastructure
|
|
5977
|
-
//
|
|
5978
|
-
// 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.
|
|
5979
6419
|
{
|
|
5980
|
-
provide:
|
|
6420
|
+
provide: NameResolver,
|
|
5981
6421
|
inject: [JETSTREAM_OPTIONS],
|
|
5982
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) => {
|
|
5983
6440
|
if (options.consumer === false) return null;
|
|
5984
|
-
return new
|
|
6441
|
+
return new InfrastructureBinder(options, names, registry);
|
|
5985
6442
|
}
|
|
5986
6443
|
},
|
|
5987
|
-
// StreamProvider — JetStream stream lifecycle
|
|
5988
6444
|
{
|
|
5989
6445
|
provide: StreamProvider,
|
|
5990
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
5991
|
-
useFactory: (options, connection) => {
|
|
6446
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, NameResolver, InfrastructureBinder],
|
|
6447
|
+
useFactory: (options, connection, names, binder) => {
|
|
5992
6448
|
if (options.consumer === false) return null;
|
|
5993
|
-
return new StreamProvider(options, connection);
|
|
6449
|
+
return new StreamProvider(options, connection, names, binder);
|
|
5994
6450
|
}
|
|
5995
6451
|
},
|
|
5996
|
-
// ConsumerProvider
|
|
6452
|
+
// ConsumerProvider needs PatternRegistry for broadcast filtering.
|
|
5997
6453
|
{
|
|
5998
6454
|
provide: ConsumerProvider,
|
|
5999
|
-
inject: [
|
|
6000
|
-
|
|
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) => {
|
|
6001
6464
|
if (options.consumer === false) return null;
|
|
6002
|
-
return new ConsumerProvider(
|
|
6465
|
+
return new ConsumerProvider(
|
|
6466
|
+
options,
|
|
6467
|
+
connection,
|
|
6468
|
+
streamProvider,
|
|
6469
|
+
patternRegistry,
|
|
6470
|
+
names,
|
|
6471
|
+
binder
|
|
6472
|
+
);
|
|
6003
6473
|
}
|
|
6004
6474
|
},
|
|
6005
|
-
// Shared ack_wait map
|
|
6475
|
+
// Shared ack_wait map, populated by the strategy after ensureConsumers().
|
|
6006
6476
|
{
|
|
6007
6477
|
provide: JETSTREAM_ACK_WAIT_MAP,
|
|
6008
6478
|
useFactory: () => /* @__PURE__ */ new Map()
|
|
6009
6479
|
},
|
|
6010
|
-
// MessageProvider — pull-based message consumption
|
|
6011
6480
|
{
|
|
6012
6481
|
provide: MessageProvider,
|
|
6013
6482
|
inject: [
|
|
@@ -6046,7 +6515,6 @@ var JetstreamModule = class {
|
|
|
6046
6515
|
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
6047
6516
|
}
|
|
6048
6517
|
},
|
|
6049
|
-
// EventRouter — routes event and broadcast messages to handlers
|
|
6050
6518
|
{
|
|
6051
6519
|
provide: EventRouter,
|
|
6052
6520
|
inject: [
|
|
@@ -6056,9 +6524,10 @@ var JetstreamModule = class {
|
|
|
6056
6524
|
JETSTREAM_CODEC,
|
|
6057
6525
|
JETSTREAM_EVENT_BUS,
|
|
6058
6526
|
JETSTREAM_ACK_WAIT_MAP,
|
|
6059
|
-
JETSTREAM_CONNECTION
|
|
6527
|
+
JETSTREAM_CONNECTION,
|
|
6528
|
+
NameResolver
|
|
6060
6529
|
],
|
|
6061
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
6530
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection, names) => {
|
|
6062
6531
|
if (options.consumer === false) return null;
|
|
6063
6532
|
const deadLetterConfig = options.onDeadLetter || options.dlq ? {
|
|
6064
6533
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -6083,11 +6552,11 @@ var JetstreamModule = class {
|
|
|
6083
6552
|
processingConfig,
|
|
6084
6553
|
ackWaitMap,
|
|
6085
6554
|
connection,
|
|
6086
|
-
options
|
|
6555
|
+
options,
|
|
6556
|
+
names
|
|
6087
6557
|
);
|
|
6088
6558
|
}
|
|
6089
6559
|
},
|
|
6090
|
-
// RpcRouter — routes RPC command messages in JetStream mode
|
|
6091
6560
|
{
|
|
6092
6561
|
provide: RpcRouter,
|
|
6093
6562
|
inject: [
|
|
@@ -6118,7 +6587,6 @@ var JetstreamModule = class {
|
|
|
6118
6587
|
);
|
|
6119
6588
|
}
|
|
6120
6589
|
},
|
|
6121
|
-
// CoreRpcServer — RPC via NATS Core request/reply
|
|
6122
6590
|
{
|
|
6123
6591
|
provide: CoreRpcServer,
|
|
6124
6592
|
inject: [
|
|
@@ -6126,14 +6594,14 @@ var JetstreamModule = class {
|
|
|
6126
6594
|
JETSTREAM_CONNECTION,
|
|
6127
6595
|
PatternRegistry,
|
|
6128
6596
|
JETSTREAM_CODEC,
|
|
6129
|
-
JETSTREAM_EVENT_BUS
|
|
6597
|
+
JETSTREAM_EVENT_BUS,
|
|
6598
|
+
NameResolver
|
|
6130
6599
|
],
|
|
6131
|
-
useFactory: (options, connection, patternRegistry, codec, eventBus) => {
|
|
6600
|
+
useFactory: (options, connection, patternRegistry, codec, eventBus, names) => {
|
|
6132
6601
|
if (options.consumer === false) return null;
|
|
6133
|
-
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
6602
|
+
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus, names);
|
|
6134
6603
|
}
|
|
6135
6604
|
},
|
|
6136
|
-
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
6137
6605
|
{
|
|
6138
6606
|
provide: MetadataProvider,
|
|
6139
6607
|
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
@@ -6142,7 +6610,6 @@ var JetstreamModule = class {
|
|
|
6142
6610
|
return new MetadataProvider(options, connection);
|
|
6143
6611
|
}
|
|
6144
6612
|
},
|
|
6145
|
-
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
6146
6613
|
{
|
|
6147
6614
|
provide: JetstreamStrategy,
|
|
6148
6615
|
inject: [
|
|
@@ -6267,6 +6734,7 @@ export {
|
|
|
6267
6734
|
JetstreamTrace,
|
|
6268
6735
|
JsonCodec,
|
|
6269
6736
|
MIN_METADATA_TTL,
|
|
6737
|
+
ManagementMode,
|
|
6270
6738
|
MessageKind,
|
|
6271
6739
|
MsgpackCodec,
|
|
6272
6740
|
NatsErrorCode,
|