@horizon-republic/nestjs-jetstream 2.7.1 → 2.9.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 +968 -148
- package/dist/index.d.cts +396 -27
- package/dist/index.d.ts +396 -27
- package/dist/index.js +935 -128
- package/package.json +19 -14
package/dist/index.cjs
CHANGED
|
@@ -29,12 +29,17 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
29
29
|
// src/index.ts
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
+
DEFAULT_METADATA_BUCKET: () => DEFAULT_METADATA_BUCKET,
|
|
33
|
+
DEFAULT_METADATA_HISTORY: () => DEFAULT_METADATA_HISTORY,
|
|
34
|
+
DEFAULT_METADATA_REPLICAS: () => DEFAULT_METADATA_REPLICAS,
|
|
35
|
+
DEFAULT_METADATA_TTL: () => DEFAULT_METADATA_TTL,
|
|
32
36
|
EventBus: () => EventBus,
|
|
33
37
|
JETSTREAM_CODEC: () => JETSTREAM_CODEC,
|
|
34
38
|
JETSTREAM_CONNECTION: () => JETSTREAM_CONNECTION,
|
|
35
39
|
JETSTREAM_EVENT_BUS: () => JETSTREAM_EVENT_BUS,
|
|
36
40
|
JETSTREAM_OPTIONS: () => JETSTREAM_OPTIONS,
|
|
37
41
|
JetstreamClient: () => JetstreamClient,
|
|
42
|
+
JetstreamDlqHeader: () => JetstreamDlqHeader,
|
|
38
43
|
JetstreamHeader: () => JetstreamHeader,
|
|
39
44
|
JetstreamHealthIndicator: () => JetstreamHealthIndicator,
|
|
40
45
|
JetstreamModule: () => JetstreamModule,
|
|
@@ -42,29 +47,34 @@ __export(index_exports, {
|
|
|
42
47
|
JetstreamRecordBuilder: () => JetstreamRecordBuilder,
|
|
43
48
|
JetstreamStrategy: () => JetstreamStrategy,
|
|
44
49
|
JsonCodec: () => JsonCodec,
|
|
50
|
+
MIN_METADATA_TTL: () => MIN_METADATA_TTL,
|
|
45
51
|
MessageKind: () => MessageKind,
|
|
46
52
|
PatternPrefix: () => PatternPrefix,
|
|
47
53
|
RpcContext: () => RpcContext,
|
|
48
54
|
StreamKind: () => StreamKind,
|
|
49
55
|
TransportEvent: () => TransportEvent,
|
|
56
|
+
buildBroadcastSubject: () => buildBroadcastSubject,
|
|
50
57
|
buildSubject: () => buildSubject,
|
|
51
58
|
consumerName: () => consumerName,
|
|
59
|
+
dlqStreamName: () => dlqStreamName,
|
|
52
60
|
getClientToken: () => getClientToken,
|
|
53
61
|
internalName: () => internalName,
|
|
54
62
|
isCoreRpcMode: () => isCoreRpcMode,
|
|
55
63
|
isJetStreamRpcMode: () => isJetStreamRpcMode,
|
|
64
|
+
metadataKey: () => metadataKey,
|
|
56
65
|
streamName: () => streamName,
|
|
57
66
|
toNanos: () => toNanos
|
|
58
67
|
});
|
|
59
68
|
module.exports = __toCommonJS(index_exports);
|
|
60
69
|
|
|
61
70
|
// src/jetstream.module.ts
|
|
62
|
-
var
|
|
71
|
+
var import_common14 = require("@nestjs/common");
|
|
63
72
|
|
|
64
73
|
// src/client/jetstream.client.ts
|
|
65
74
|
var import_common = require("@nestjs/common");
|
|
66
75
|
var import_microservices = require("@nestjs/microservices");
|
|
67
|
-
var
|
|
76
|
+
var import_transport_node = require("@nats-io/transport-node");
|
|
77
|
+
var import_nuid = require("@nats-io/nuid");
|
|
68
78
|
|
|
69
79
|
// src/interfaces/hooks.interface.ts
|
|
70
80
|
var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
|
|
@@ -95,7 +105,7 @@ var StreamKind = /* @__PURE__ */ ((StreamKind2) => {
|
|
|
95
105
|
})(StreamKind || {});
|
|
96
106
|
|
|
97
107
|
// src/jetstream.constants.ts
|
|
98
|
-
var
|
|
108
|
+
var import_jetstream = require("@nats-io/jetstream");
|
|
99
109
|
var JETSTREAM_OPTIONS = /* @__PURE__ */ Symbol("JETSTREAM_OPTIONS");
|
|
100
110
|
var JETSTREAM_CONNECTION = /* @__PURE__ */ Symbol("JETSTREAM_CONNECTION");
|
|
101
111
|
var JETSTREAM_CODEC = /* @__PURE__ */ Symbol("JETSTREAM_CODEC");
|
|
@@ -113,12 +123,12 @@ var NANOS_PER = {
|
|
|
113
123
|
};
|
|
114
124
|
var toNanos = (value, unit) => value * NANOS_PER[unit];
|
|
115
125
|
var baseStreamConfig = {
|
|
116
|
-
retention:
|
|
117
|
-
storage:
|
|
126
|
+
retention: import_jetstream.RetentionPolicy.Workqueue,
|
|
127
|
+
storage: import_jetstream.StorageType.File,
|
|
118
128
|
num_replicas: 1,
|
|
119
|
-
discard:
|
|
129
|
+
discard: import_jetstream.DiscardPolicy.Old,
|
|
120
130
|
allow_direct: true,
|
|
121
|
-
compression:
|
|
131
|
+
compression: import_jetstream.StoreCompression.S2
|
|
122
132
|
};
|
|
123
133
|
var DEFAULT_EVENT_STREAM_CONFIG = {
|
|
124
134
|
...baseStreamConfig,
|
|
@@ -144,19 +154,19 @@ var DEFAULT_COMMAND_STREAM_CONFIG = {
|
|
|
144
154
|
};
|
|
145
155
|
var DEFAULT_BROADCAST_STREAM_CONFIG = {
|
|
146
156
|
...baseStreamConfig,
|
|
147
|
-
retention:
|
|
157
|
+
retention: import_jetstream.RetentionPolicy.Limits,
|
|
148
158
|
allow_rollup_hdrs: true,
|
|
149
159
|
max_consumers: 200,
|
|
150
160
|
max_msg_size: 10 * MB,
|
|
151
161
|
max_msgs_per_subject: 1e6,
|
|
152
162
|
max_msgs: 1e7,
|
|
153
163
|
max_bytes: 2 * GB,
|
|
154
|
-
max_age: toNanos(1, "
|
|
164
|
+
max_age: toNanos(1, "hours"),
|
|
155
165
|
duplicate_window: toNanos(2, "minutes")
|
|
156
166
|
};
|
|
157
167
|
var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
158
168
|
...baseStreamConfig,
|
|
159
|
-
retention:
|
|
169
|
+
retention: import_jetstream.RetentionPolicy.Limits,
|
|
160
170
|
allow_rollup_hdrs: false,
|
|
161
171
|
max_consumers: 100,
|
|
162
172
|
max_msg_size: 10 * MB,
|
|
@@ -166,33 +176,51 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
|
|
|
166
176
|
max_age: toNanos(1, "days"),
|
|
167
177
|
duplicate_window: toNanos(2, "minutes")
|
|
168
178
|
};
|
|
179
|
+
var DEFAULT_DLQ_STREAM_CONFIG = {
|
|
180
|
+
...baseStreamConfig,
|
|
181
|
+
retention: import_jetstream.RetentionPolicy.Workqueue,
|
|
182
|
+
allow_rollup_hdrs: false,
|
|
183
|
+
max_consumers: 100,
|
|
184
|
+
max_msg_size: 10 * MB,
|
|
185
|
+
max_msgs_per_subject: 5e6,
|
|
186
|
+
max_msgs: 5e7,
|
|
187
|
+
max_bytes: 5 * GB,
|
|
188
|
+
max_age: toNanos(30, "days"),
|
|
189
|
+
duplicate_window: toNanos(2, "minutes")
|
|
190
|
+
};
|
|
169
191
|
var DEFAULT_EVENT_CONSUMER_CONFIG = {
|
|
170
192
|
ack_wait: toNanos(10, "seconds"),
|
|
171
193
|
max_deliver: 3,
|
|
172
194
|
max_ack_pending: 100,
|
|
173
|
-
ack_policy:
|
|
174
|
-
deliver_policy:
|
|
175
|
-
replay_policy:
|
|
195
|
+
ack_policy: import_jetstream.AckPolicy.Explicit,
|
|
196
|
+
deliver_policy: import_jetstream.DeliverPolicy.All,
|
|
197
|
+
replay_policy: import_jetstream.ReplayPolicy.Instant
|
|
176
198
|
};
|
|
177
199
|
var DEFAULT_COMMAND_CONSUMER_CONFIG = {
|
|
178
200
|
ack_wait: toNanos(5, "minutes"),
|
|
179
201
|
max_deliver: 1,
|
|
180
202
|
max_ack_pending: 100,
|
|
181
|
-
ack_policy:
|
|
182
|
-
deliver_policy:
|
|
183
|
-
replay_policy:
|
|
203
|
+
ack_policy: import_jetstream.AckPolicy.Explicit,
|
|
204
|
+
deliver_policy: import_jetstream.DeliverPolicy.All,
|
|
205
|
+
replay_policy: import_jetstream.ReplayPolicy.Instant
|
|
184
206
|
};
|
|
185
207
|
var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
|
|
186
208
|
ack_wait: toNanos(10, "seconds"),
|
|
187
209
|
max_deliver: 3,
|
|
188
210
|
max_ack_pending: 100,
|
|
189
|
-
ack_policy:
|
|
190
|
-
deliver_policy:
|
|
191
|
-
replay_policy:
|
|
211
|
+
ack_policy: import_jetstream.AckPolicy.Explicit,
|
|
212
|
+
deliver_policy: import_jetstream.DeliverPolicy.All,
|
|
213
|
+
replay_policy: import_jetstream.ReplayPolicy.Instant
|
|
192
214
|
};
|
|
193
215
|
var DEFAULT_RPC_TIMEOUT = 3e4;
|
|
194
216
|
var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
|
|
195
217
|
var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
|
|
218
|
+
var DEFAULT_METADATA_BUCKET = "handler_registry";
|
|
219
|
+
var DEFAULT_METADATA_REPLICAS = 1;
|
|
220
|
+
var DEFAULT_METADATA_HISTORY = 1;
|
|
221
|
+
var DEFAULT_METADATA_TTL = 3e4;
|
|
222
|
+
var MIN_METADATA_TTL = 5e3;
|
|
223
|
+
var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
|
|
196
224
|
var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
197
225
|
JetstreamHeader2["CorrelationId"] = "x-correlation-id";
|
|
198
226
|
JetstreamHeader2["ReplyTo"] = "x-reply-to";
|
|
@@ -201,6 +229,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
|
|
|
201
229
|
JetstreamHeader2["Error"] = "x-error";
|
|
202
230
|
return JetstreamHeader2;
|
|
203
231
|
})(JetstreamHeader || {});
|
|
232
|
+
var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
|
|
233
|
+
JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
|
|
234
|
+
JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
|
|
235
|
+
JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
|
|
236
|
+
JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
|
|
237
|
+
JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
|
|
238
|
+
return JetstreamDlqHeader2;
|
|
239
|
+
})(JetstreamDlqHeader || {});
|
|
204
240
|
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
205
241
|
"x-correlation-id" /* CorrelationId */,
|
|
206
242
|
"x-reply-to" /* ReplyTo */,
|
|
@@ -213,6 +249,9 @@ var streamName = (serviceName, kind) => {
|
|
|
213
249
|
if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
|
|
214
250
|
return `${internalName(serviceName)}_${kind}-stream`;
|
|
215
251
|
};
|
|
252
|
+
var dlqStreamName = (serviceName) => {
|
|
253
|
+
return `${internalName(serviceName)}_dlq-stream`;
|
|
254
|
+
};
|
|
216
255
|
var consumerName = (serviceName, kind) => {
|
|
217
256
|
if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
|
|
218
257
|
return `${internalName(serviceName)}_${kind}-consumer`;
|
|
@@ -227,11 +266,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
|
227
266
|
|
|
228
267
|
// src/client/jetstream.record.ts
|
|
229
268
|
var JetstreamRecord = class {
|
|
230
|
-
constructor(data, headers2, timeout, messageId) {
|
|
269
|
+
constructor(data, headers2, timeout, messageId, schedule, ttl) {
|
|
231
270
|
this.data = data;
|
|
232
271
|
this.headers = headers2;
|
|
233
272
|
this.timeout = timeout;
|
|
234
273
|
this.messageId = messageId;
|
|
274
|
+
this.schedule = schedule;
|
|
275
|
+
this.ttl = ttl;
|
|
235
276
|
}
|
|
236
277
|
};
|
|
237
278
|
var JetstreamRecordBuilder = class {
|
|
@@ -239,6 +280,8 @@ var JetstreamRecordBuilder = class {
|
|
|
239
280
|
headers = /* @__PURE__ */ new Map();
|
|
240
281
|
timeout;
|
|
241
282
|
messageId;
|
|
283
|
+
scheduleOptions;
|
|
284
|
+
ttlDuration;
|
|
242
285
|
constructor(data) {
|
|
243
286
|
this.data = data;
|
|
244
287
|
}
|
|
@@ -306,17 +349,71 @@ var JetstreamRecordBuilder = class {
|
|
|
306
349
|
this.timeout = ms;
|
|
307
350
|
return this;
|
|
308
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Schedule one-shot delayed delivery.
|
|
354
|
+
*
|
|
355
|
+
* The message is held by NATS and delivered to the event consumer
|
|
356
|
+
* at the specified time. Requires NATS >= 2.12 and `allow_msg_schedules: true`
|
|
357
|
+
* on the event stream (via `events: { stream: { allow_msg_schedules: true } }`).
|
|
358
|
+
*
|
|
359
|
+
* Only meaningful for events (`client.emit()`). If used with RPC
|
|
360
|
+
* (`client.send()`), a warning is logged and the schedule is ignored.
|
|
361
|
+
*
|
|
362
|
+
* @param date - Delivery time. Must be in the future.
|
|
363
|
+
* @throws Error if the date is not in the future.
|
|
364
|
+
*/
|
|
365
|
+
scheduleAt(date) {
|
|
366
|
+
const ts = date.getTime();
|
|
367
|
+
if (Number.isNaN(ts)) {
|
|
368
|
+
throw new Error("Schedule date is invalid");
|
|
369
|
+
}
|
|
370
|
+
if (ts <= Date.now()) {
|
|
371
|
+
throw new Error("Schedule date must be in the future");
|
|
372
|
+
}
|
|
373
|
+
this.scheduleOptions = { at: new Date(ts) };
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Set per-message TTL (time-to-live).
|
|
378
|
+
*
|
|
379
|
+
* The message expires individually after the specified duration,
|
|
380
|
+
* independent of the stream's `max_age`. Requires NATS >= 2.11 and
|
|
381
|
+
* `allow_msg_ttl: true` on the stream.
|
|
382
|
+
*
|
|
383
|
+
* Only meaningful for events (`client.emit()`). If used with RPC
|
|
384
|
+
* (`client.send()`), a warning is logged and the TTL is ignored.
|
|
385
|
+
*
|
|
386
|
+
* @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```typescript
|
|
390
|
+
* import { toNanos } from '@horizon-republic/nestjs-jetstream';
|
|
391
|
+
*
|
|
392
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
|
|
393
|
+
* new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
ttl(nanos) {
|
|
397
|
+
if (!Number.isFinite(nanos) || nanos <= 0) {
|
|
398
|
+
throw new Error("TTL must be a positive finite value");
|
|
399
|
+
}
|
|
400
|
+
this.ttlDuration = nanosToGoDuration(nanos);
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
309
403
|
/**
|
|
310
404
|
* Build the immutable {@link JetstreamRecord}.
|
|
311
405
|
*
|
|
312
406
|
* @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
|
|
313
407
|
*/
|
|
314
408
|
build() {
|
|
409
|
+
const schedule = this.scheduleOptions ? { at: new Date(this.scheduleOptions.at.getTime()) } : void 0;
|
|
315
410
|
return new JetstreamRecord(
|
|
316
411
|
this.data,
|
|
317
412
|
new Map(this.headers),
|
|
318
413
|
this.timeout,
|
|
319
|
-
this.messageId
|
|
414
|
+
this.messageId,
|
|
415
|
+
schedule,
|
|
416
|
+
this.ttlDuration
|
|
320
417
|
);
|
|
321
418
|
}
|
|
322
419
|
/** Validate that a header key is not reserved. */
|
|
@@ -328,6 +425,17 @@ var JetstreamRecordBuilder = class {
|
|
|
328
425
|
}
|
|
329
426
|
}
|
|
330
427
|
};
|
|
428
|
+
var NS_PER_MS = 1e6;
|
|
429
|
+
var NS_PER_S = 1e9;
|
|
430
|
+
var NS_PER_M = 60 * NS_PER_S;
|
|
431
|
+
var NS_PER_H = 60 * NS_PER_M;
|
|
432
|
+
var nanosToGoDuration = (nanos) => {
|
|
433
|
+
if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
|
|
434
|
+
if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
|
|
435
|
+
if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
|
|
436
|
+
if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
|
|
437
|
+
return `${nanos}ns`;
|
|
438
|
+
};
|
|
331
439
|
|
|
332
440
|
// src/client/jetstream.client.ts
|
|
333
441
|
var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
@@ -368,7 +476,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
368
476
|
this.setupInbox(nc);
|
|
369
477
|
}
|
|
370
478
|
this.statusSubscription ??= this.connection.status$.subscribe((status) => {
|
|
371
|
-
if (status.type ===
|
|
479
|
+
if (status.type === "disconnect") {
|
|
372
480
|
this.handleDisconnect();
|
|
373
481
|
}
|
|
374
482
|
});
|
|
@@ -396,19 +504,40 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
396
504
|
* Publish a fire-and-forget event to JetStream.
|
|
397
505
|
*
|
|
398
506
|
* Events are published to either the workqueue stream or broadcast stream
|
|
399
|
-
* depending on the subject prefix.
|
|
507
|
+
* depending on the subject prefix. When a schedule is present the message
|
|
508
|
+
* is published to a `_sch` subject within the same stream, with the target
|
|
509
|
+
* set to the original event subject.
|
|
400
510
|
*/
|
|
401
511
|
async dispatchEvent(packet) {
|
|
402
512
|
await this.connect();
|
|
403
|
-
const { data, hdrs, messageId } = this.extractRecordData(packet.data);
|
|
404
|
-
const
|
|
405
|
-
const msgHeaders = this.buildHeaders(hdrs, { subject });
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
513
|
+
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
514
|
+
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
515
|
+
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
516
|
+
if (schedule) {
|
|
517
|
+
const scheduleSubject = this.buildScheduleSubject(eventSubject);
|
|
518
|
+
const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
|
|
519
|
+
headers: msgHeaders,
|
|
520
|
+
msgID: messageId ?? import_nuid.nuid.next(),
|
|
521
|
+
ttl,
|
|
522
|
+
schedule: {
|
|
523
|
+
specification: schedule.at,
|
|
524
|
+
target: eventSubject
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
if (ack.duplicate) {
|
|
528
|
+
this.logger.warn(
|
|
529
|
+
`Duplicate scheduled publish detected: ${scheduleSubject} (seq: ${ack.seq})`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
534
|
+
headers: msgHeaders,
|
|
535
|
+
msgID: messageId ?? import_nuid.nuid.next(),
|
|
536
|
+
ttl
|
|
537
|
+
});
|
|
538
|
+
if (ack.duplicate) {
|
|
539
|
+
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
540
|
+
}
|
|
412
541
|
}
|
|
413
542
|
return void 0;
|
|
414
543
|
}
|
|
@@ -420,7 +549,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
420
549
|
*/
|
|
421
550
|
publish(packet, callback) {
|
|
422
551
|
const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
|
|
423
|
-
const { data, hdrs, timeout, messageId } = this.extractRecordData(packet.data);
|
|
552
|
+
const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
553
|
+
if (schedule) {
|
|
554
|
+
this.logger.warn(
|
|
555
|
+
"scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (ttl) {
|
|
559
|
+
this.logger.warn(
|
|
560
|
+
"ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
|
|
561
|
+
);
|
|
562
|
+
}
|
|
424
563
|
const onUnhandled = (err) => {
|
|
425
564
|
this.logger.error("Unhandled publish error:", err);
|
|
426
565
|
callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
|
|
@@ -429,7 +568,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
429
568
|
if (isCoreRpcMode(this.rootOptions.rpc)) {
|
|
430
569
|
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
|
|
431
570
|
} else {
|
|
432
|
-
jetStreamCorrelationId =
|
|
571
|
+
jetStreamCorrelationId = import_nuid.nuid.next();
|
|
433
572
|
this.publishJetStreamRpc(subject, data, callback, {
|
|
434
573
|
headers: hdrs,
|
|
435
574
|
timeout,
|
|
@@ -504,7 +643,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
504
643
|
});
|
|
505
644
|
await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
|
|
506
645
|
headers: hdrs,
|
|
507
|
-
msgID: messageId ??
|
|
646
|
+
msgID: messageId ?? import_nuid.nuid.next()
|
|
508
647
|
});
|
|
509
648
|
} catch (err) {
|
|
510
649
|
const existingTimeout = this.pendingTimeouts.get(correlationId);
|
|
@@ -536,10 +675,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
536
675
|
this.pendingTimeouts.clear();
|
|
537
676
|
this.inboxSubscription?.unsubscribe();
|
|
538
677
|
this.inboxSubscription = null;
|
|
678
|
+
this.inbox = null;
|
|
539
679
|
}
|
|
540
680
|
/** Setup shared inbox subscription for JetStream RPC responses. */
|
|
541
681
|
setupInbox(nc) {
|
|
542
|
-
this.inbox = (0,
|
|
682
|
+
this.inbox = (0, import_transport_node.createInbox)(internalName(this.rootOptions.name));
|
|
543
683
|
this.inboxSubscription = nc.subscribe(this.inbox, {
|
|
544
684
|
callback: (err, msg) => {
|
|
545
685
|
if (err) {
|
|
@@ -601,7 +741,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
601
741
|
}
|
|
602
742
|
/** Build NATS headers merging custom headers with transport headers. */
|
|
603
743
|
buildHeaders(customHeaders, transport) {
|
|
604
|
-
const hdrs = (0,
|
|
744
|
+
const hdrs = (0, import_transport_node.headers)();
|
|
605
745
|
hdrs.set("x-subject" /* Subject */, transport.subject);
|
|
606
746
|
hdrs.set("x-caller-name" /* CallerName */, this.callerName);
|
|
607
747
|
if (transport.correlationId) {
|
|
@@ -617,17 +757,53 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
617
757
|
}
|
|
618
758
|
return hdrs;
|
|
619
759
|
}
|
|
620
|
-
/** Extract data, headers, and
|
|
760
|
+
/** Extract data, headers, timeout, and schedule from raw packet data or JetstreamRecord. */
|
|
621
761
|
extractRecordData(rawData) {
|
|
622
762
|
if (rawData instanceof JetstreamRecord) {
|
|
623
763
|
return {
|
|
624
764
|
data: rawData.data,
|
|
625
765
|
hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
|
|
626
766
|
timeout: rawData.timeout,
|
|
627
|
-
messageId: rawData.messageId
|
|
767
|
+
messageId: rawData.messageId,
|
|
768
|
+
schedule: rawData.schedule,
|
|
769
|
+
ttl: rawData.ttl
|
|
628
770
|
};
|
|
629
771
|
}
|
|
630
|
-
return {
|
|
772
|
+
return {
|
|
773
|
+
data: rawData,
|
|
774
|
+
hdrs: null,
|
|
775
|
+
timeout: void 0,
|
|
776
|
+
messageId: void 0,
|
|
777
|
+
schedule: void 0,
|
|
778
|
+
ttl: void 0
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Build a schedule-holder subject for NATS message scheduling.
|
|
783
|
+
*
|
|
784
|
+
* The schedule-holder subject resides in the same stream as the target but
|
|
785
|
+
* uses a separate `_sch` namespace that is NOT matched by any consumer filter.
|
|
786
|
+
* NATS holds the message and publishes it to the target subject after the delay.
|
|
787
|
+
*
|
|
788
|
+
* Examples:
|
|
789
|
+
* - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
|
|
790
|
+
* - `broadcast.config.updated` → `broadcast._sch.config.updated`
|
|
791
|
+
*/
|
|
792
|
+
buildScheduleSubject(eventSubject) {
|
|
793
|
+
if (eventSubject.startsWith("broadcast.")) {
|
|
794
|
+
return eventSubject.replace("broadcast.", "broadcast._sch.");
|
|
795
|
+
}
|
|
796
|
+
const targetPrefix = `${internalName(this.targetName)}.`;
|
|
797
|
+
if (!eventSubject.startsWith(targetPrefix)) {
|
|
798
|
+
throw new Error(`Unexpected event subject format: ${eventSubject}`);
|
|
799
|
+
}
|
|
800
|
+
const withoutPrefix = eventSubject.slice(targetPrefix.length);
|
|
801
|
+
const dotIndex = withoutPrefix.indexOf(".");
|
|
802
|
+
if (dotIndex === -1) {
|
|
803
|
+
throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
|
|
804
|
+
}
|
|
805
|
+
const pattern = withoutPrefix.slice(dotIndex + 1);
|
|
806
|
+
return `${targetPrefix}_sch.${pattern}`;
|
|
631
807
|
}
|
|
632
808
|
getRpcTimeout() {
|
|
633
809
|
if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
|
|
@@ -637,20 +813,21 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
637
813
|
};
|
|
638
814
|
|
|
639
815
|
// src/codec/json.codec.ts
|
|
640
|
-
var
|
|
816
|
+
var encoder = new TextEncoder();
|
|
817
|
+
var decoder = new TextDecoder();
|
|
641
818
|
var JsonCodec = class {
|
|
642
|
-
inner = (0, import_nats3.JSONCodec)();
|
|
643
819
|
encode(data) {
|
|
644
|
-
return
|
|
820
|
+
return encoder.encode(JSON.stringify(data));
|
|
645
821
|
}
|
|
646
822
|
decode(data) {
|
|
647
|
-
return
|
|
823
|
+
return JSON.parse(decoder.decode(data));
|
|
648
824
|
}
|
|
649
825
|
};
|
|
650
826
|
|
|
651
827
|
// src/connection/connection.provider.ts
|
|
652
828
|
var import_common2 = require("@nestjs/common");
|
|
653
|
-
var
|
|
829
|
+
var import_transport_node2 = require("@nats-io/transport-node");
|
|
830
|
+
var import_jetstream7 = require("@nats-io/jetstream");
|
|
654
831
|
var import_rxjs = require("rxjs");
|
|
655
832
|
var DEFAULT_OPTIONS = {
|
|
656
833
|
maxReconnectAttempts: -1,
|
|
@@ -704,14 +881,7 @@ var ConnectionProvider = class {
|
|
|
704
881
|
async getJetStreamManager() {
|
|
705
882
|
if (this.jsmInstance) return this.jsmInstance;
|
|
706
883
|
if (this.jsmPromise) return this.jsmPromise;
|
|
707
|
-
this.jsmPromise = (
|
|
708
|
-
const nc = await this.getConnection();
|
|
709
|
-
this.jsmInstance = await nc.jetstreamManager();
|
|
710
|
-
this.logger.log("JetStream manager initialized");
|
|
711
|
-
return this.jsmInstance;
|
|
712
|
-
})().finally(() => {
|
|
713
|
-
this.jsmPromise = null;
|
|
714
|
-
});
|
|
884
|
+
this.jsmPromise = this.initJetStreamManager();
|
|
715
885
|
return this.jsmPromise;
|
|
716
886
|
}
|
|
717
887
|
/**
|
|
@@ -727,7 +897,7 @@ var ConnectionProvider = class {
|
|
|
727
897
|
if (!this.connection || this.connection.isClosed()) {
|
|
728
898
|
throw new Error("Not connected \u2014 call getConnection() before getJetStreamClient()");
|
|
729
899
|
}
|
|
730
|
-
this.jsClient ??= this.connection
|
|
900
|
+
this.jsClient ??= (0, import_jetstream7.jetstream)(this.connection);
|
|
731
901
|
return this.jsClient;
|
|
732
902
|
}
|
|
733
903
|
/** Direct access to the raw NATS connection, or `null` if not yet connected. */
|
|
@@ -763,11 +933,21 @@ var ConnectionProvider = class {
|
|
|
763
933
|
this.jsmPromise = null;
|
|
764
934
|
}
|
|
765
935
|
}
|
|
936
|
+
async initJetStreamManager() {
|
|
937
|
+
try {
|
|
938
|
+
const nc = await this.getConnection();
|
|
939
|
+
this.jsmInstance = await (0, import_jetstream7.jetstreamManager)(nc);
|
|
940
|
+
this.logger.log("JetStream manager initialized");
|
|
941
|
+
return this.jsmInstance;
|
|
942
|
+
} finally {
|
|
943
|
+
this.jsmPromise = null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
766
946
|
/** Internal: establish the physical connection with reconnect monitoring. */
|
|
767
947
|
async establish() {
|
|
768
948
|
const name = internalName(this.options.name);
|
|
769
949
|
try {
|
|
770
|
-
const nc = await (0,
|
|
950
|
+
const nc = await (0, import_transport_node2.connect)({
|
|
771
951
|
...DEFAULT_OPTIONS,
|
|
772
952
|
...this.options.connectionOptions,
|
|
773
953
|
servers: this.options.servers,
|
|
@@ -779,7 +959,7 @@ var ConnectionProvider = class {
|
|
|
779
959
|
this.monitorStatus(nc);
|
|
780
960
|
return nc;
|
|
781
961
|
} catch (err) {
|
|
782
|
-
if (err instanceof
|
|
962
|
+
if (err instanceof Error && err.message.includes("REFUSED")) {
|
|
783
963
|
throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
|
|
784
964
|
}
|
|
785
965
|
throw err;
|
|
@@ -787,27 +967,33 @@ var ConnectionProvider = class {
|
|
|
787
967
|
}
|
|
788
968
|
/** Subscribe to connection status events and emit hooks. */
|
|
789
969
|
monitorStatus(nc) {
|
|
790
|
-
(async () => {
|
|
970
|
+
void (async () => {
|
|
791
971
|
for await (const status of nc.status()) {
|
|
792
972
|
switch (status.type) {
|
|
793
|
-
case
|
|
973
|
+
case "disconnect":
|
|
794
974
|
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
795
975
|
break;
|
|
796
|
-
case
|
|
976
|
+
case "reconnect":
|
|
797
977
|
this.jsClient = null;
|
|
798
978
|
this.jsmInstance = null;
|
|
799
979
|
this.jsmPromise = null;
|
|
800
980
|
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
801
981
|
break;
|
|
802
|
-
case
|
|
803
|
-
this.eventBus.emit(
|
|
982
|
+
case "error":
|
|
983
|
+
this.eventBus.emit(
|
|
984
|
+
"error" /* Error */,
|
|
985
|
+
status.error,
|
|
986
|
+
"connection"
|
|
987
|
+
);
|
|
804
988
|
break;
|
|
805
|
-
case
|
|
806
|
-
case
|
|
807
|
-
case
|
|
808
|
-
case
|
|
809
|
-
case
|
|
810
|
-
case
|
|
989
|
+
case "update":
|
|
990
|
+
case "ldm":
|
|
991
|
+
case "reconnecting":
|
|
992
|
+
case "ping":
|
|
993
|
+
case "staleConnection":
|
|
994
|
+
case "forceReconnect":
|
|
995
|
+
case "slowConsumer":
|
|
996
|
+
case "close":
|
|
811
997
|
break;
|
|
812
998
|
}
|
|
813
999
|
}
|
|
@@ -904,9 +1090,14 @@ var JetstreamHealthIndicator = class {
|
|
|
904
1090
|
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
905
1091
|
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
906
1092
|
*
|
|
1093
|
+
* The thrown error sets `isHealthCheckError: true` and `causes` — the
|
|
1094
|
+
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
1095
|
+
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
1096
|
+
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
1097
|
+
*
|
|
907
1098
|
* @param key - Health indicator key (default: `'jetstream'`).
|
|
908
1099
|
* @returns Object with status, server, and latency under the given key.
|
|
909
|
-
* @throws Error with `{ [key]: { status: 'down' } }
|
|
1100
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
910
1101
|
*/
|
|
911
1102
|
async isHealthy(key = "jetstream") {
|
|
912
1103
|
const status = await this.check();
|
|
@@ -916,8 +1107,10 @@ var JetstreamHealthIndicator = class {
|
|
|
916
1107
|
latency: status.latency
|
|
917
1108
|
};
|
|
918
1109
|
if (!status.connected) {
|
|
1110
|
+
const causes = { [key]: details };
|
|
919
1111
|
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
920
|
-
|
|
1112
|
+
causes,
|
|
1113
|
+
isHealthCheckError: true
|
|
921
1114
|
});
|
|
922
1115
|
}
|
|
923
1116
|
return { [key]: details };
|
|
@@ -930,7 +1123,7 @@ JetstreamHealthIndicator = __decorateClass([
|
|
|
930
1123
|
// src/server/strategy.ts
|
|
931
1124
|
var import_microservices2 = require("@nestjs/microservices");
|
|
932
1125
|
var JetstreamStrategy = class extends import_microservices2.Server {
|
|
933
|
-
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
|
|
1126
|
+
constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
|
|
934
1127
|
super();
|
|
935
1128
|
this.options = options;
|
|
936
1129
|
this.connection = connection;
|
|
@@ -942,6 +1135,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
942
1135
|
this.rpcRouter = rpcRouter;
|
|
943
1136
|
this.coreRpcServer = coreRpcServer;
|
|
944
1137
|
this.ackWaitMap = ackWaitMap;
|
|
1138
|
+
this.metadataProvider = metadataProvider;
|
|
945
1139
|
}
|
|
946
1140
|
transportId = /* @__PURE__ */ Symbol("jetstream-transport");
|
|
947
1141
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -986,10 +1180,14 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
986
1180
|
if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
|
|
987
1181
|
await this.coreRpcServer.start();
|
|
988
1182
|
}
|
|
1183
|
+
if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
|
|
1184
|
+
await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
|
|
1185
|
+
}
|
|
989
1186
|
callback();
|
|
990
1187
|
}
|
|
991
|
-
/** Stop all consumers, routers, and
|
|
1188
|
+
/** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
|
|
992
1189
|
close() {
|
|
1190
|
+
this.metadataProvider?.destroy();
|
|
993
1191
|
this.eventRouter.destroy();
|
|
994
1192
|
this.rpcRouter.destroy();
|
|
995
1193
|
this.coreRpcServer.stop();
|
|
@@ -1069,7 +1267,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
1069
1267
|
|
|
1070
1268
|
// src/server/core-rpc.server.ts
|
|
1071
1269
|
var import_common4 = require("@nestjs/common");
|
|
1072
|
-
var
|
|
1270
|
+
var import_transport_node3 = require("@nats-io/transport-node");
|
|
1073
1271
|
|
|
1074
1272
|
// src/context/rpc.context.ts
|
|
1075
1273
|
var import_microservices3 = require("@nestjs/microservices");
|
|
@@ -1355,7 +1553,7 @@ var CoreRpcServer = class {
|
|
|
1355
1553
|
/** Send an error response back to the caller with x-error header. */
|
|
1356
1554
|
respondWithError(msg, error) {
|
|
1357
1555
|
try {
|
|
1358
|
-
const hdrs = (0,
|
|
1556
|
+
const hdrs = (0, import_transport_node3.headers)();
|
|
1359
1557
|
hdrs.set("x-error" /* Error */, "true");
|
|
1360
1558
|
msg.respond(this.codec.encode(serializeError(error)), { headers: hdrs });
|
|
1361
1559
|
} catch {
|
|
@@ -1365,24 +1563,172 @@ var CoreRpcServer = class {
|
|
|
1365
1563
|
};
|
|
1366
1564
|
|
|
1367
1565
|
// src/server/infrastructure/stream.provider.ts
|
|
1566
|
+
var import_common6 = require("@nestjs/common");
|
|
1567
|
+
var import_jetstream14 = require("@nats-io/jetstream");
|
|
1568
|
+
|
|
1569
|
+
// src/server/infrastructure/stream-config-diff.ts
|
|
1570
|
+
var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1571
|
+
"retention"
|
|
1572
|
+
]);
|
|
1573
|
+
var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1574
|
+
"storage"
|
|
1575
|
+
]);
|
|
1576
|
+
var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1577
|
+
"allow_msg_schedules",
|
|
1578
|
+
"allow_msg_ttl",
|
|
1579
|
+
"deny_delete",
|
|
1580
|
+
"deny_purge"
|
|
1581
|
+
]);
|
|
1582
|
+
var compareStreamConfig = (current, desired) => {
|
|
1583
|
+
const changes = [];
|
|
1584
|
+
for (const key of Object.keys(desired)) {
|
|
1585
|
+
const currentVal = current[key];
|
|
1586
|
+
const desiredVal = desired[key];
|
|
1587
|
+
if (isEqual(currentVal, desiredVal)) continue;
|
|
1588
|
+
changes.push({
|
|
1589
|
+
property: key,
|
|
1590
|
+
current: currentVal,
|
|
1591
|
+
desired: desiredVal,
|
|
1592
|
+
mutability: classifyMutability(key, currentVal, desiredVal)
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
|
|
1596
|
+
const hasMutableChanges = changes.some(
|
|
1597
|
+
(c) => c.mutability === "mutable" || c.mutability === "enable-only"
|
|
1598
|
+
);
|
|
1599
|
+
const hasTransportControlledConflicts = changes.some(
|
|
1600
|
+
(c) => c.mutability === "transport-controlled"
|
|
1601
|
+
);
|
|
1602
|
+
return {
|
|
1603
|
+
hasChanges: changes.length > 0,
|
|
1604
|
+
hasMutableChanges,
|
|
1605
|
+
hasImmutableChanges,
|
|
1606
|
+
hasTransportControlledConflicts,
|
|
1607
|
+
changes
|
|
1608
|
+
};
|
|
1609
|
+
};
|
|
1610
|
+
var classifyMutability = (key, current, desired) => {
|
|
1611
|
+
if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
|
|
1612
|
+
if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
|
|
1613
|
+
if (ENABLE_ONLY_PROPERTIES.has(key)) {
|
|
1614
|
+
return current === true && desired === false ? "immutable" : "enable-only";
|
|
1615
|
+
}
|
|
1616
|
+
return "mutable";
|
|
1617
|
+
};
|
|
1618
|
+
var isEqual = (a, b) => {
|
|
1619
|
+
if (a === b) return true;
|
|
1620
|
+
if (a == null && b == null) return true;
|
|
1621
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
// src/server/infrastructure/stream-migration.ts
|
|
1368
1625
|
var import_common5 = require("@nestjs/common");
|
|
1369
|
-
var
|
|
1370
|
-
var
|
|
1626
|
+
var import_jetstream13 = require("@nats-io/jetstream");
|
|
1627
|
+
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
1628
|
+
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
1629
|
+
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
1630
|
+
var StreamMigration = class {
|
|
1631
|
+
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
1632
|
+
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
1633
|
+
}
|
|
1634
|
+
logger = new import_common5.Logger("Jetstream:Stream");
|
|
1635
|
+
async migrate(jsm, streamName2, newConfig) {
|
|
1636
|
+
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1637
|
+
const startTime = Date.now();
|
|
1638
|
+
const currentInfo = await jsm.streams.info(streamName2);
|
|
1639
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1640
|
+
const messageCount = currentInfo.state.messages;
|
|
1641
|
+
this.logger.log(`Stream ${streamName2}: destructive migration started`);
|
|
1642
|
+
let originalDeleted = false;
|
|
1643
|
+
try {
|
|
1644
|
+
if (messageCount > 0) {
|
|
1645
|
+
this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
|
|
1646
|
+
await jsm.streams.add({
|
|
1647
|
+
...currentInfo.config,
|
|
1648
|
+
name: backupName,
|
|
1649
|
+
subjects: [],
|
|
1650
|
+
sources: [{ name: streamName2 }]
|
|
1651
|
+
});
|
|
1652
|
+
await this.waitForSourcing(jsm, backupName, messageCount);
|
|
1653
|
+
}
|
|
1654
|
+
this.logger.log(` Phase 2/4: Deleting old stream`);
|
|
1655
|
+
await jsm.streams.delete(streamName2);
|
|
1656
|
+
originalDeleted = true;
|
|
1657
|
+
this.logger.log(` Phase 3/4: Creating stream with new config`);
|
|
1658
|
+
await jsm.streams.add(newConfig);
|
|
1659
|
+
if (messageCount > 0) {
|
|
1660
|
+
const backupInfo = await jsm.streams.info(backupName);
|
|
1661
|
+
await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
|
|
1662
|
+
this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
|
|
1663
|
+
await jsm.streams.update(streamName2, {
|
|
1664
|
+
...newConfig,
|
|
1665
|
+
sources: [{ name: backupName }]
|
|
1666
|
+
});
|
|
1667
|
+
await this.waitForSourcing(jsm, streamName2, messageCount);
|
|
1668
|
+
await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
|
|
1669
|
+
await jsm.streams.delete(backupName);
|
|
1670
|
+
}
|
|
1671
|
+
} catch (err) {
|
|
1672
|
+
if (originalDeleted && messageCount > 0) {
|
|
1673
|
+
this.logger.error(
|
|
1674
|
+
`Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
|
|
1675
|
+
);
|
|
1676
|
+
} else {
|
|
1677
|
+
await this.cleanupOrphanedBackup(jsm, backupName);
|
|
1678
|
+
}
|
|
1679
|
+
throw err;
|
|
1680
|
+
}
|
|
1681
|
+
const durationMs = Date.now() - startTime;
|
|
1682
|
+
this.logger.log(
|
|
1683
|
+
`Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
async waitForSourcing(jsm, streamName2, expectedCount) {
|
|
1687
|
+
const deadline = Date.now() + this.sourcingTimeoutMs;
|
|
1688
|
+
while (Date.now() < deadline) {
|
|
1689
|
+
const info = await jsm.streams.info(streamName2);
|
|
1690
|
+
if (info.state.messages >= expectedCount) return;
|
|
1691
|
+
await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
|
|
1692
|
+
}
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
`Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
async cleanupOrphanedBackup(jsm, backupName) {
|
|
1698
|
+
try {
|
|
1699
|
+
await jsm.streams.info(backupName);
|
|
1700
|
+
this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
|
|
1701
|
+
await jsm.streams.delete(backupName);
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
throw err;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
// src/server/infrastructure/stream.provider.ts
|
|
1371
1712
|
var StreamProvider = class {
|
|
1372
1713
|
constructor(options, connection) {
|
|
1373
1714
|
this.options = options;
|
|
1374
1715
|
this.connection = connection;
|
|
1375
1716
|
}
|
|
1376
|
-
logger = new
|
|
1717
|
+
logger = new import_common6.Logger("Jetstream:Stream");
|
|
1718
|
+
migration = new StreamMigration();
|
|
1377
1719
|
/**
|
|
1378
1720
|
* Ensure all required streams exist with correct configuration.
|
|
1379
1721
|
*
|
|
1380
1722
|
* @param kinds Which stream kinds to create. Determined by the module based
|
|
1381
1723
|
* on RPC mode and registered handler patterns.
|
|
1724
|
+
* If the dlq option is enabled, also ensures the DLQ stream exists.
|
|
1382
1725
|
*/
|
|
1383
1726
|
async ensureStreams(kinds) {
|
|
1384
1727
|
const jsm = await this.connection.getJetStreamManager();
|
|
1385
1728
|
await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
|
|
1729
|
+
if (this.options.dlq) {
|
|
1730
|
+
await this.ensureDlqStream(jsm);
|
|
1731
|
+
}
|
|
1386
1732
|
}
|
|
1387
1733
|
/** Get the stream name for a given kind. */
|
|
1388
1734
|
getStreamName(kind) {
|
|
@@ -1392,12 +1738,22 @@ var StreamProvider = class {
|
|
|
1392
1738
|
getSubjects(kind) {
|
|
1393
1739
|
const name = internalName(this.options.name);
|
|
1394
1740
|
switch (kind) {
|
|
1395
|
-
case "ev" /* Event */:
|
|
1396
|
-
|
|
1741
|
+
case "ev" /* Event */: {
|
|
1742
|
+
const subjects = [`${name}.${"ev" /* Event */}.>`];
|
|
1743
|
+
if (this.isSchedulingEnabled(kind)) {
|
|
1744
|
+
subjects.push(`${name}._sch.>`);
|
|
1745
|
+
}
|
|
1746
|
+
return subjects;
|
|
1747
|
+
}
|
|
1397
1748
|
case "cmd" /* Command */:
|
|
1398
1749
|
return [`${name}.${"cmd" /* Command */}.>`];
|
|
1399
|
-
case "broadcast" /* Broadcast */:
|
|
1400
|
-
|
|
1750
|
+
case "broadcast" /* Broadcast */: {
|
|
1751
|
+
const subjects = ["broadcast.>"];
|
|
1752
|
+
if (this.isSchedulingEnabled(kind)) {
|
|
1753
|
+
subjects.push("broadcast._sch.>");
|
|
1754
|
+
}
|
|
1755
|
+
return subjects;
|
|
1756
|
+
}
|
|
1401
1757
|
case "ordered" /* Ordered */:
|
|
1402
1758
|
return [`${name}.${"ordered" /* Ordered */}.>`];
|
|
1403
1759
|
}
|
|
@@ -1407,17 +1763,85 @@ var StreamProvider = class {
|
|
|
1407
1763
|
const config = this.buildConfig(kind);
|
|
1408
1764
|
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
1409
1765
|
try {
|
|
1410
|
-
await jsm.streams.info(config.name);
|
|
1411
|
-
this.
|
|
1412
|
-
return await jsm.streams.update(config.name, config);
|
|
1766
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1767
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1413
1768
|
} catch (err) {
|
|
1414
|
-
if (err instanceof
|
|
1769
|
+
if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1415
1770
|
this.logger.log(`Creating stream: ${config.name}`);
|
|
1416
1771
|
return await jsm.streams.add(config);
|
|
1417
1772
|
}
|
|
1418
1773
|
throw err;
|
|
1419
1774
|
}
|
|
1420
1775
|
}
|
|
1776
|
+
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
1777
|
+
async ensureDlqStream(jsm) {
|
|
1778
|
+
const config = this.buildDlqConfig();
|
|
1779
|
+
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
1780
|
+
try {
|
|
1781
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
1782
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1785
|
+
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
1786
|
+
return await jsm.streams.add(config);
|
|
1787
|
+
}
|
|
1788
|
+
throw err;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
async handleExistingStream(jsm, currentInfo, config) {
|
|
1792
|
+
const diff = compareStreamConfig(currentInfo.config, config);
|
|
1793
|
+
if (!diff.hasChanges) {
|
|
1794
|
+
this.logger.debug(`Stream ${config.name}: no config changes`);
|
|
1795
|
+
return currentInfo;
|
|
1796
|
+
}
|
|
1797
|
+
this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
|
|
1798
|
+
if (diff.hasTransportControlledConflicts) {
|
|
1799
|
+
const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
|
|
1800
|
+
throw new Error(
|
|
1801
|
+
`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.`
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
if (!diff.hasImmutableChanges) {
|
|
1805
|
+
this.logger.debug(`Stream exists, updating: ${config.name}`);
|
|
1806
|
+
return await jsm.streams.update(config.name, config);
|
|
1807
|
+
}
|
|
1808
|
+
if (!this.options.allowDestructiveMigration) {
|
|
1809
|
+
this.logger.warn(
|
|
1810
|
+
`Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
|
|
1811
|
+
);
|
|
1812
|
+
if (diff.hasMutableChanges) {
|
|
1813
|
+
const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
|
|
1814
|
+
return await jsm.streams.update(config.name, mutableConfig);
|
|
1815
|
+
}
|
|
1816
|
+
return currentInfo;
|
|
1817
|
+
}
|
|
1818
|
+
await this.migration.migrate(jsm, config.name, config);
|
|
1819
|
+
return await jsm.streams.info(config.name);
|
|
1820
|
+
}
|
|
1821
|
+
buildMutableOnlyConfig(config, currentConfig, diff) {
|
|
1822
|
+
const nonMutableKeys = new Set(
|
|
1823
|
+
diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
|
|
1824
|
+
);
|
|
1825
|
+
const filtered = { ...config };
|
|
1826
|
+
for (const key of nonMutableKeys) {
|
|
1827
|
+
filtered[key] = currentConfig[key];
|
|
1828
|
+
}
|
|
1829
|
+
return filtered;
|
|
1830
|
+
}
|
|
1831
|
+
logChanges(streamName2, diff, migrationEnabled) {
|
|
1832
|
+
for (const c of diff.changes) {
|
|
1833
|
+
const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
|
|
1834
|
+
if (c.mutability === "transport-controlled") {
|
|
1835
|
+
this.logger.error(
|
|
1836
|
+
`Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
|
|
1837
|
+
);
|
|
1838
|
+
} else if (c.mutability === "immutable" && !migrationEnabled) {
|
|
1839
|
+
this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
|
|
1840
|
+
} else {
|
|
1841
|
+
this.logger.log(`Stream ${streamName2}: ${detail}`);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1421
1845
|
/** Build the full stream config by merging defaults with user overrides. */
|
|
1422
1846
|
buildConfig(kind) {
|
|
1423
1847
|
const name = this.getStreamName(kind);
|
|
@@ -1433,6 +1857,26 @@ var StreamProvider = class {
|
|
|
1433
1857
|
description
|
|
1434
1858
|
};
|
|
1435
1859
|
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Build the stream configuration for the Dead-Letter Queue (DLQ).
|
|
1862
|
+
*
|
|
1863
|
+
* Merges the library default DLQ config with user-provided overrides.
|
|
1864
|
+
* Ensures transport-controlled settings like retention are safely decoupled.
|
|
1865
|
+
*/
|
|
1866
|
+
buildDlqConfig() {
|
|
1867
|
+
const name = dlqStreamName(this.options.name);
|
|
1868
|
+
const subjects = [name];
|
|
1869
|
+
const description = `JetStream DLQ stream for ${this.options.name}`;
|
|
1870
|
+
const overrides = this.options.dlq?.stream ?? {};
|
|
1871
|
+
const safeOverrides = this.stripTransportControlled(overrides);
|
|
1872
|
+
return {
|
|
1873
|
+
...DEFAULT_DLQ_STREAM_CONFIG,
|
|
1874
|
+
...safeOverrides,
|
|
1875
|
+
name,
|
|
1876
|
+
subjects,
|
|
1877
|
+
description
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1436
1880
|
/** Get default config for a stream kind. */
|
|
1437
1881
|
getDefaults(kind) {
|
|
1438
1882
|
switch (kind) {
|
|
@@ -1446,25 +1890,49 @@ var StreamProvider = class {
|
|
|
1446
1890
|
return DEFAULT_ORDERED_STREAM_CONFIG;
|
|
1447
1891
|
}
|
|
1448
1892
|
}
|
|
1449
|
-
/**
|
|
1893
|
+
/** Check if scheduling is enabled for a stream kind via `allow_msg_schedules` override. */
|
|
1894
|
+
isSchedulingEnabled(kind) {
|
|
1895
|
+
const overrides = this.getOverrides(kind);
|
|
1896
|
+
return overrides.allow_msg_schedules === true;
|
|
1897
|
+
}
|
|
1898
|
+
/** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
|
|
1450
1899
|
getOverrides(kind) {
|
|
1900
|
+
let overrides;
|
|
1451
1901
|
switch (kind) {
|
|
1452
1902
|
case "ev" /* Event */:
|
|
1453
|
-
|
|
1903
|
+
overrides = this.options.events?.stream ?? {};
|
|
1904
|
+
break;
|
|
1454
1905
|
case "cmd" /* Command */:
|
|
1455
|
-
|
|
1906
|
+
overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
|
|
1907
|
+
break;
|
|
1456
1908
|
case "broadcast" /* Broadcast */:
|
|
1457
|
-
|
|
1909
|
+
overrides = this.options.broadcast?.stream ?? {};
|
|
1910
|
+
break;
|
|
1458
1911
|
case "ordered" /* Ordered */:
|
|
1459
|
-
|
|
1912
|
+
overrides = this.options.ordered?.stream ?? {};
|
|
1913
|
+
break;
|
|
1460
1914
|
}
|
|
1915
|
+
return this.stripTransportControlled(overrides);
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Remove transport-controlled properties from user overrides.
|
|
1919
|
+
* `retention` is managed by the transport (Workqueue/Limits per stream kind)
|
|
1920
|
+
* and silently stripped to protect users from misconfiguration.
|
|
1921
|
+
*/
|
|
1922
|
+
stripTransportControlled(overrides) {
|
|
1923
|
+
if (!("retention" in overrides)) return overrides;
|
|
1924
|
+
this.logger.debug(
|
|
1925
|
+
"Stripping user-provided retention override \u2014 retention is managed by the transport"
|
|
1926
|
+
);
|
|
1927
|
+
const cleaned = { ...overrides };
|
|
1928
|
+
delete cleaned.retention;
|
|
1929
|
+
return cleaned;
|
|
1461
1930
|
}
|
|
1462
1931
|
};
|
|
1463
1932
|
|
|
1464
1933
|
// src/server/infrastructure/consumer.provider.ts
|
|
1465
|
-
var
|
|
1466
|
-
var
|
|
1467
|
-
var CONSUMER_NOT_FOUND = 10014;
|
|
1934
|
+
var import_common7 = require("@nestjs/common");
|
|
1935
|
+
var import_jetstream16 = require("@nats-io/jetstream");
|
|
1468
1936
|
var ConsumerProvider = class {
|
|
1469
1937
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
1470
1938
|
this.options = options;
|
|
@@ -1472,7 +1940,7 @@ var ConsumerProvider = class {
|
|
|
1472
1940
|
this.streamProvider = streamProvider;
|
|
1473
1941
|
this.patternRegistry = patternRegistry;
|
|
1474
1942
|
}
|
|
1475
|
-
logger = new
|
|
1943
|
+
logger = new import_common7.Logger("Jetstream:Consumer");
|
|
1476
1944
|
/**
|
|
1477
1945
|
* Ensure consumers exist for the specified kinds.
|
|
1478
1946
|
*
|
|
@@ -1493,7 +1961,11 @@ var ConsumerProvider = class {
|
|
|
1493
1961
|
getConsumerName(kind) {
|
|
1494
1962
|
return consumerName(this.options.name, kind);
|
|
1495
1963
|
}
|
|
1496
|
-
/**
|
|
1964
|
+
/**
|
|
1965
|
+
* Ensure a single consumer exists with the desired config.
|
|
1966
|
+
* Used at **startup** — creates or updates the consumer to match
|
|
1967
|
+
* the current pod's configuration.
|
|
1968
|
+
*/
|
|
1497
1969
|
async ensureConsumer(jsm, kind) {
|
|
1498
1970
|
const stream = this.streamProvider.getStreamName(kind);
|
|
1499
1971
|
const config = this.buildConfig(kind);
|
|
@@ -1504,13 +1976,74 @@ var ConsumerProvider = class {
|
|
|
1504
1976
|
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
1505
1977
|
return await jsm.consumers.update(stream, name, config);
|
|
1506
1978
|
} catch (err) {
|
|
1507
|
-
if (err instanceof
|
|
1508
|
-
|
|
1509
|
-
|
|
1979
|
+
if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
1980
|
+
throw err;
|
|
1981
|
+
}
|
|
1982
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Recover a consumer that disappeared during runtime.
|
|
1987
|
+
* Used by **self-healing** — creates if missing, but NEVER updates config.
|
|
1988
|
+
*
|
|
1989
|
+
* If a migration backup stream exists, another pod is mid-migration — we
|
|
1990
|
+
* throw so the self-healing retry loop waits with backoff until migration
|
|
1991
|
+
* completes and the backup is cleaned up.
|
|
1992
|
+
*
|
|
1993
|
+
* This prevents old pods from:
|
|
1994
|
+
* - Overwriting a newer pod's consumer config during rolling updates
|
|
1995
|
+
* - Creating consumers during migration (which would consume and delete
|
|
1996
|
+
* workqueue messages while they're being restored)
|
|
1997
|
+
*/
|
|
1998
|
+
async recoverConsumer(jsm, kind) {
|
|
1999
|
+
const stream = this.streamProvider.getStreamName(kind);
|
|
2000
|
+
const config = this.buildConfig(kind);
|
|
2001
|
+
const name = config.durable_name;
|
|
2002
|
+
this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
|
|
2003
|
+
await this.assertNoMigrationInProgress(jsm, stream);
|
|
2004
|
+
try {
|
|
2005
|
+
return await jsm.consumers.info(stream, name);
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
2008
|
+
throw err;
|
|
2009
|
+
}
|
|
2010
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Throw if a migration backup stream exists for this stream.
|
|
2015
|
+
* The self-healing retry loop catches the error and retries with backoff,
|
|
2016
|
+
* naturally waiting until the migrating pod finishes and cleans up the backup.
|
|
2017
|
+
*/
|
|
2018
|
+
async assertNoMigrationInProgress(jsm, stream) {
|
|
2019
|
+
const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
|
|
2020
|
+
try {
|
|
2021
|
+
await jsm.streams.info(backupName);
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
2024
|
+
);
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
2027
|
+
return;
|
|
1510
2028
|
}
|
|
1511
2029
|
throw err;
|
|
1512
2030
|
}
|
|
1513
2031
|
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Create a consumer, handling the race where another pod creates it first.
|
|
2034
|
+
*/
|
|
2035
|
+
async createConsumer(jsm, stream, name, config) {
|
|
2036
|
+
this.logger.log(`Creating consumer: ${name}`);
|
|
2037
|
+
try {
|
|
2038
|
+
return await jsm.consumers.add(stream, config);
|
|
2039
|
+
} catch (addErr) {
|
|
2040
|
+
if (addErr instanceof import_jetstream16.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
2041
|
+
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
2042
|
+
return await jsm.consumers.info(stream, name);
|
|
2043
|
+
}
|
|
2044
|
+
throw addErr;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1514
2047
|
/** Build consumer config by merging defaults with user overrides. */
|
|
1515
2048
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
|
|
1516
2049
|
buildConfig(kind) {
|
|
@@ -1563,6 +2096,11 @@ var ConsumerProvider = class {
|
|
|
1563
2096
|
return DEFAULT_BROADCAST_CONSUMER_CONFIG;
|
|
1564
2097
|
case "ordered" /* Ordered */:
|
|
1565
2098
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2099
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
2100
|
+
default: {
|
|
2101
|
+
const _exhaustive = kind;
|
|
2102
|
+
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
2103
|
+
}
|
|
1566
2104
|
}
|
|
1567
2105
|
}
|
|
1568
2106
|
/** Get user-provided overrides for a consumer kind. */
|
|
@@ -1576,21 +2114,27 @@ var ConsumerProvider = class {
|
|
|
1576
2114
|
return this.options.broadcast?.consumer ?? {};
|
|
1577
2115
|
case "ordered" /* Ordered */:
|
|
1578
2116
|
throw new Error("Ordered consumers are ephemeral and should not use durable config");
|
|
2117
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
2118
|
+
default: {
|
|
2119
|
+
const _exhaustive = kind;
|
|
2120
|
+
throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
|
|
2121
|
+
}
|
|
1579
2122
|
}
|
|
1580
2123
|
}
|
|
1581
2124
|
};
|
|
1582
2125
|
|
|
1583
2126
|
// src/server/infrastructure/message.provider.ts
|
|
1584
|
-
var
|
|
1585
|
-
var
|
|
2127
|
+
var import_common8 = require("@nestjs/common");
|
|
2128
|
+
var import_jetstream18 = require("@nats-io/jetstream");
|
|
1586
2129
|
var import_rxjs3 = require("rxjs");
|
|
1587
2130
|
var MessageProvider = class {
|
|
1588
|
-
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
|
|
2131
|
+
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
1589
2132
|
this.connection = connection;
|
|
1590
2133
|
this.eventBus = eventBus;
|
|
1591
2134
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2135
|
+
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
1592
2136
|
}
|
|
1593
|
-
logger = new
|
|
2137
|
+
logger = new import_common8.Logger("Jetstream:Message");
|
|
1594
2138
|
activeIterators = /* @__PURE__ */ new Set();
|
|
1595
2139
|
orderedReadyResolve = null;
|
|
1596
2140
|
orderedReadyReject = null;
|
|
@@ -1641,8 +2185,8 @@ var MessageProvider = class {
|
|
|
1641
2185
|
* @param orderedConfig - Optional overrides for ordered consumer options.
|
|
1642
2186
|
*/
|
|
1643
2187
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
1644
|
-
const consumerOpts = { filterSubjects };
|
|
1645
|
-
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !==
|
|
2188
|
+
const consumerOpts = { filter_subjects: filterSubjects };
|
|
2189
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream18.DeliverPolicy.All) {
|
|
1646
2190
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
1647
2191
|
}
|
|
1648
2192
|
if (orderedConfig?.optStartSeq !== void 0) {
|
|
@@ -1693,12 +2237,26 @@ var MessageProvider = class {
|
|
|
1693
2237
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
1694
2238
|
async consumeOnce(kind, info, target$) {
|
|
1695
2239
|
const js = this.connection.getJetStreamClient();
|
|
1696
|
-
|
|
2240
|
+
let consumer;
|
|
2241
|
+
let consumerName2 = info.name;
|
|
2242
|
+
try {
|
|
2243
|
+
consumer = await js.consumers.get(info.stream_name, info.name);
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
|
|
2246
|
+
this.logger.warn(`Consumer ${info.name} not found, recreating...`);
|
|
2247
|
+
const recovered = await this.consumerRecoveryFn(kind);
|
|
2248
|
+
consumerName2 = recovered.name;
|
|
2249
|
+
this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
|
|
2250
|
+
consumer = await js.consumers.get(recovered.stream_name, consumerName2);
|
|
2251
|
+
} else {
|
|
2252
|
+
throw err;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
1697
2255
|
const defaults = { idle_heartbeat: 5e3 };
|
|
1698
2256
|
const userOptions = this.consumeOptionsMap.get(kind) ?? {};
|
|
1699
2257
|
const messages = await consumer.consume({ ...defaults, ...userOptions });
|
|
1700
2258
|
this.activeIterators.add(messages);
|
|
1701
|
-
this.monitorConsumerHealth(messages,
|
|
2259
|
+
this.monitorConsumerHealth(messages, consumerName2);
|
|
1702
2260
|
try {
|
|
1703
2261
|
for await (const msg of messages) {
|
|
1704
2262
|
target$.next(msg);
|
|
@@ -1707,6 +2265,17 @@ var MessageProvider = class {
|
|
|
1707
2265
|
this.activeIterators.delete(messages);
|
|
1708
2266
|
}
|
|
1709
2267
|
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Detect "consumer not found" errors from `js.consumers.get()`.
|
|
2270
|
+
*
|
|
2271
|
+
* Unlike JetStream Manager calls (which throw `JetStreamApiError`),
|
|
2272
|
+
* the JetStream client's `consumers.get()` throws a plain `Error`
|
|
2273
|
+
* with the error code embedded in the message text.
|
|
2274
|
+
*/
|
|
2275
|
+
isConsumerNotFound(err) {
|
|
2276
|
+
if (!(err instanceof Error)) return false;
|
|
2277
|
+
return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
|
|
2278
|
+
}
|
|
1710
2279
|
/** Get the target subject for a consumer kind. */
|
|
1711
2280
|
getTargetSubject(kind) {
|
|
1712
2281
|
switch (kind) {
|
|
@@ -1718,6 +2287,7 @@ var MessageProvider = class {
|
|
|
1718
2287
|
return this.broadcastMessages$;
|
|
1719
2288
|
case "ordered" /* Ordered */:
|
|
1720
2289
|
return this.orderedMessages$;
|
|
2290
|
+
/* v8 ignore next 5 -- exhaustive switch guard, unreachable */
|
|
1721
2291
|
default: {
|
|
1722
2292
|
const _exhaustive = kind;
|
|
1723
2293
|
throw new Error(`Unknown stream kind: ${_exhaustive}`);
|
|
@@ -1726,10 +2296,10 @@ var MessageProvider = class {
|
|
|
1726
2296
|
}
|
|
1727
2297
|
/** Monitor heartbeats and restart the consumer iterator on prolonged silence. */
|
|
1728
2298
|
monitorConsumerHealth(messages, name) {
|
|
1729
|
-
(async () => {
|
|
1730
|
-
for await (const status of
|
|
1731
|
-
if (status.type ===
|
|
1732
|
-
this.logger.warn(`Consumer ${name}: ${status.
|
|
2299
|
+
void (async () => {
|
|
2300
|
+
for await (const status of messages.status()) {
|
|
2301
|
+
if (status.type === "heartbeats_missed" && status.count >= 2) {
|
|
2302
|
+
this.logger.warn(`Consumer ${name}: ${status.count} heartbeats missed, restarting`);
|
|
1733
2303
|
messages.stop();
|
|
1734
2304
|
break;
|
|
1735
2305
|
}
|
|
@@ -1803,8 +2373,110 @@ var MessageProvider = class {
|
|
|
1803
2373
|
}
|
|
1804
2374
|
};
|
|
1805
2375
|
|
|
2376
|
+
// src/server/infrastructure/metadata.provider.ts
|
|
2377
|
+
var import_common9 = require("@nestjs/common");
|
|
2378
|
+
var import_kv = require("@nats-io/kv");
|
|
2379
|
+
var MetadataProvider = class {
|
|
2380
|
+
constructor(options, connection) {
|
|
2381
|
+
this.connection = connection;
|
|
2382
|
+
this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
|
|
2383
|
+
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
2384
|
+
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
2385
|
+
}
|
|
2386
|
+
logger = new import_common9.Logger("Jetstream:Metadata");
|
|
2387
|
+
bucketName;
|
|
2388
|
+
replicas;
|
|
2389
|
+
ttl;
|
|
2390
|
+
currentEntries;
|
|
2391
|
+
heartbeatTimer;
|
|
2392
|
+
cachedKv;
|
|
2393
|
+
/**
|
|
2394
|
+
* Write handler metadata entries to the KV bucket and start heartbeat.
|
|
2395
|
+
*
|
|
2396
|
+
* Creates the bucket if it doesn't exist (idempotent).
|
|
2397
|
+
* Skips silently when entries map is empty.
|
|
2398
|
+
* Starts a heartbeat interval that refreshes entries every `ttl / 2`
|
|
2399
|
+
* to prevent TTL expiry while the pod is alive.
|
|
2400
|
+
*
|
|
2401
|
+
* Non-critical — errors are logged but do not prevent transport startup.
|
|
2402
|
+
*
|
|
2403
|
+
* @param entries Map of KV key → metadata object.
|
|
2404
|
+
*/
|
|
2405
|
+
async publish(entries) {
|
|
2406
|
+
if (entries.size === 0) return;
|
|
2407
|
+
try {
|
|
2408
|
+
const kv = await this.openBucket();
|
|
2409
|
+
await this.writeEntries(kv, entries);
|
|
2410
|
+
this.currentEntries = entries;
|
|
2411
|
+
this.startHeartbeat();
|
|
2412
|
+
this.logger.log(
|
|
2413
|
+
`Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
|
|
2414
|
+
);
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
this.logger.error("Failed to publish handler metadata to KV", err);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Stop the heartbeat timer.
|
|
2421
|
+
*
|
|
2422
|
+
* After this call, entries will expire via TTL once the heartbeat window passes.
|
|
2423
|
+
* Called during transport shutdown (strategy.close()).
|
|
2424
|
+
*/
|
|
2425
|
+
destroy() {
|
|
2426
|
+
if (this.heartbeatTimer) {
|
|
2427
|
+
clearInterval(this.heartbeatTimer);
|
|
2428
|
+
this.heartbeatTimer = void 0;
|
|
2429
|
+
}
|
|
2430
|
+
this.currentEntries = void 0;
|
|
2431
|
+
this.cachedKv = void 0;
|
|
2432
|
+
}
|
|
2433
|
+
/** Write entries to KV with per-entry error handling. */
|
|
2434
|
+
async writeEntries(kv, entries) {
|
|
2435
|
+
for (const [key, meta] of entries) {
|
|
2436
|
+
try {
|
|
2437
|
+
await kv.put(key, JSON.stringify(meta));
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
this.logger.error(`Failed to write metadata entry "${key}"`, err);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
/** Start heartbeat interval that refreshes entries every ttl/2. */
|
|
2444
|
+
startHeartbeat() {
|
|
2445
|
+
if (this.heartbeatTimer) {
|
|
2446
|
+
clearInterval(this.heartbeatTimer);
|
|
2447
|
+
}
|
|
2448
|
+
const interval = Math.floor(this.ttl / 2);
|
|
2449
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2450
|
+
void this.refreshEntries();
|
|
2451
|
+
}, interval);
|
|
2452
|
+
this.heartbeatTimer.unref();
|
|
2453
|
+
}
|
|
2454
|
+
/** Refresh all current entries in KV (heartbeat tick). */
|
|
2455
|
+
async refreshEntries() {
|
|
2456
|
+
if (!this.currentEntries || this.currentEntries.size === 0) return;
|
|
2457
|
+
try {
|
|
2458
|
+
const kv = await this.openBucket();
|
|
2459
|
+
await this.writeEntries(kv, this.currentEntries);
|
|
2460
|
+
} catch (err) {
|
|
2461
|
+
this.logger.error("Failed to refresh handler metadata in KV", err);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
/** Create or open the KV bucket (cached after first call). */
|
|
2465
|
+
async openBucket() {
|
|
2466
|
+
if (this.cachedKv) return this.cachedKv;
|
|
2467
|
+
const js = this.connection.getJetStreamClient();
|
|
2468
|
+
const kvm = new import_kv.Kvm(js);
|
|
2469
|
+
this.cachedKv = await kvm.create(this.bucketName, {
|
|
2470
|
+
history: DEFAULT_METADATA_HISTORY,
|
|
2471
|
+
replicas: this.replicas,
|
|
2472
|
+
ttl: this.ttl
|
|
2473
|
+
});
|
|
2474
|
+
return this.cachedKv;
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
|
|
1806
2478
|
// src/server/routing/pattern-registry.ts
|
|
1807
|
-
var
|
|
2479
|
+
var import_common10 = require("@nestjs/common");
|
|
1808
2480
|
var HANDLER_LABELS = {
|
|
1809
2481
|
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
1810
2482
|
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
@@ -1815,7 +2487,7 @@ var PatternRegistry = class {
|
|
|
1815
2487
|
constructor(options) {
|
|
1816
2488
|
this.options = options;
|
|
1817
2489
|
}
|
|
1818
|
-
logger = new
|
|
2490
|
+
logger = new import_common10.Logger("Jetstream:PatternRegistry");
|
|
1819
2491
|
registry = /* @__PURE__ */ new Map();
|
|
1820
2492
|
// Cached after registerHandlers() — the registry is immutable from that point
|
|
1821
2493
|
cachedPatterns = null;
|
|
@@ -1823,6 +2495,7 @@ var PatternRegistry = class {
|
|
|
1823
2495
|
_hasCommands = false;
|
|
1824
2496
|
_hasBroadcasts = false;
|
|
1825
2497
|
_hasOrdered = false;
|
|
2498
|
+
_hasMetadata = false;
|
|
1826
2499
|
/**
|
|
1827
2500
|
* Register all handlers from the NestJS strategy.
|
|
1828
2501
|
*
|
|
@@ -1835,6 +2508,7 @@ var PatternRegistry = class {
|
|
|
1835
2508
|
const isEvent = handler.isEventHandler ?? false;
|
|
1836
2509
|
const isBroadcast = !!extras?.broadcast;
|
|
1837
2510
|
const isOrdered = !!extras?.ordered;
|
|
2511
|
+
const meta = extras?.meta;
|
|
1838
2512
|
if (isBroadcast && isOrdered) {
|
|
1839
2513
|
throw new Error(
|
|
1840
2514
|
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
@@ -1851,7 +2525,8 @@ var PatternRegistry = class {
|
|
|
1851
2525
|
pattern,
|
|
1852
2526
|
isEvent: isEvent && !isOrdered,
|
|
1853
2527
|
isBroadcast,
|
|
1854
|
-
isOrdered
|
|
2528
|
+
isOrdered,
|
|
2529
|
+
meta
|
|
1855
2530
|
});
|
|
1856
2531
|
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
1857
2532
|
}
|
|
@@ -1860,6 +2535,7 @@ var PatternRegistry = class {
|
|
|
1860
2535
|
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
1861
2536
|
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
1862
2537
|
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2538
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
1863
2539
|
this.logSummary();
|
|
1864
2540
|
}
|
|
1865
2541
|
/** Find handler for a full NATS subject. */
|
|
@@ -1888,6 +2564,26 @@ var PatternRegistry = class {
|
|
|
1888
2564
|
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
1889
2565
|
);
|
|
1890
2566
|
}
|
|
2567
|
+
/** Check if any registered handler has metadata. */
|
|
2568
|
+
hasMetadata() {
|
|
2569
|
+
return this._hasMetadata;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Get handler metadata entries for KV publishing.
|
|
2573
|
+
*
|
|
2574
|
+
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2575
|
+
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2576
|
+
*/
|
|
2577
|
+
getMetadataEntries() {
|
|
2578
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2579
|
+
for (const entry of this.registry.values()) {
|
|
2580
|
+
if (!entry.meta) continue;
|
|
2581
|
+
const kind = this.resolveStreamKind(entry);
|
|
2582
|
+
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2583
|
+
entries.set(key, entry.meta);
|
|
2584
|
+
}
|
|
2585
|
+
return entries;
|
|
2586
|
+
}
|
|
1891
2587
|
/** Get patterns grouped by kind (cached after registration). */
|
|
1892
2588
|
getPatternsByKind() {
|
|
1893
2589
|
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
@@ -1927,6 +2623,12 @@ var PatternRegistry = class {
|
|
|
1927
2623
|
}
|
|
1928
2624
|
return { events, commands, broadcasts, ordered };
|
|
1929
2625
|
}
|
|
2626
|
+
resolveStreamKind(entry) {
|
|
2627
|
+
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2628
|
+
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2629
|
+
if (entry.isEvent) return "ev" /* Event */;
|
|
2630
|
+
return "cmd" /* Command */;
|
|
2631
|
+
}
|
|
1930
2632
|
logSummary() {
|
|
1931
2633
|
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
1932
2634
|
const parts = [
|
|
@@ -1942,10 +2644,11 @@ var PatternRegistry = class {
|
|
|
1942
2644
|
};
|
|
1943
2645
|
|
|
1944
2646
|
// src/server/routing/event.router.ts
|
|
1945
|
-
var
|
|
2647
|
+
var import_common11 = require("@nestjs/common");
|
|
1946
2648
|
var import_rxjs4 = require("rxjs");
|
|
2649
|
+
var import_transport_node4 = require("@nats-io/transport-node");
|
|
1947
2650
|
var EventRouter = class {
|
|
1948
|
-
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
|
|
2651
|
+
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
1949
2652
|
this.messageProvider = messageProvider;
|
|
1950
2653
|
this.patternRegistry = patternRegistry;
|
|
1951
2654
|
this.codec = codec;
|
|
@@ -1953,8 +2656,10 @@ var EventRouter = class {
|
|
|
1953
2656
|
this.deadLetterConfig = deadLetterConfig;
|
|
1954
2657
|
this.processingConfig = processingConfig;
|
|
1955
2658
|
this.ackWaitMap = ackWaitMap;
|
|
2659
|
+
this.connection = connection;
|
|
2660
|
+
this.options = options;
|
|
1956
2661
|
}
|
|
1957
|
-
logger = new
|
|
2662
|
+
logger = new import_common11.Logger("Jetstream:EventRouter");
|
|
1958
2663
|
subscriptions = [];
|
|
1959
2664
|
/**
|
|
1960
2665
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
@@ -2081,6 +2786,93 @@ var EventRouter = class {
|
|
|
2081
2786
|
return msg.info.deliveryCount >= maxDeliver;
|
|
2082
2787
|
}
|
|
2083
2788
|
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
2789
|
+
/**
|
|
2790
|
+
* Fallback execution for a dead letter when DLQ is disabled, or when
|
|
2791
|
+
* publishing to the DLQ stream fails (due to network or NATS errors).
|
|
2792
|
+
*
|
|
2793
|
+
* Triggers the user-provided `onDeadLetter` hook for logging/alerting.
|
|
2794
|
+
* On success, terminates the message. On error, leaves it unacknowledged (nak)
|
|
2795
|
+
* so NATS can retry the delivery on the next cycle.
|
|
2796
|
+
*/
|
|
2797
|
+
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
2798
|
+
if (!this.deadLetterConfig) {
|
|
2799
|
+
msg.term("Dead letter config unavailable");
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
try {
|
|
2803
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
2804
|
+
msg.term("Dead letter processed via fallback callback");
|
|
2805
|
+
} catch (hookErr) {
|
|
2806
|
+
this.logger.error(
|
|
2807
|
+
`Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
|
|
2808
|
+
hookErr
|
|
2809
|
+
);
|
|
2810
|
+
msg.nak();
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
|
|
2815
|
+
*
|
|
2816
|
+
* Appends diagnostic metadata headers to the original message and preserves
|
|
2817
|
+
* the primary payload. If publishing succeeds, it notifies the standard
|
|
2818
|
+
* `onDeadLetter` callback and terminates the message. If it fails, it falls
|
|
2819
|
+
* back to the callback entirely to prevent silent data loss.
|
|
2820
|
+
*/
|
|
2821
|
+
async publishToDlq(msg, info, error) {
|
|
2822
|
+
const serviceName = this.options?.name;
|
|
2823
|
+
if (!this.connection || !serviceName) {
|
|
2824
|
+
this.logger.error(
|
|
2825
|
+
`Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
|
|
2826
|
+
);
|
|
2827
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const destinationSubject = dlqStreamName(serviceName);
|
|
2831
|
+
const hdrs = (0, import_transport_node4.headers)();
|
|
2832
|
+
if (msg.headers) {
|
|
2833
|
+
for (const [k, v] of msg.headers) {
|
|
2834
|
+
for (const val of v) {
|
|
2835
|
+
hdrs.append(k, val);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
let reason = String(error);
|
|
2840
|
+
if (error instanceof Error) {
|
|
2841
|
+
reason = error.message;
|
|
2842
|
+
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
2843
|
+
reason = String(error.message);
|
|
2844
|
+
}
|
|
2845
|
+
hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
|
|
2846
|
+
hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
|
|
2847
|
+
hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
|
|
2848
|
+
hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
|
|
2849
|
+
hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
|
|
2850
|
+
try {
|
|
2851
|
+
const js = this.connection.getJetStreamClient();
|
|
2852
|
+
await js.publish(destinationSubject, msg.data, { headers: hdrs });
|
|
2853
|
+
this.logger.log(`Message sent to DLQ: ${msg.subject}`);
|
|
2854
|
+
if (this.deadLetterConfig?.onDeadLetter) {
|
|
2855
|
+
try {
|
|
2856
|
+
await this.deadLetterConfig.onDeadLetter(info);
|
|
2857
|
+
} catch (hookErr) {
|
|
2858
|
+
this.logger.warn(
|
|
2859
|
+
`onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
|
|
2860
|
+
hookErr
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
msg.term("Moved to DLQ stream");
|
|
2865
|
+
} catch (publishErr) {
|
|
2866
|
+
this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
|
|
2867
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Orchestrates the handling of a message that has exhausted delivery limits.
|
|
2872
|
+
*
|
|
2873
|
+
* Emits a system event and delegates either to the robust DLQ stream publisher
|
|
2874
|
+
* or directly to the fallback callback based on the active module configuration.
|
|
2875
|
+
*/
|
|
2084
2876
|
async handleDeadLetter(msg, data, error) {
|
|
2085
2877
|
const info = {
|
|
2086
2878
|
subject: msg.subject,
|
|
@@ -2093,23 +2885,17 @@ var EventRouter = class {
|
|
|
2093
2885
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
2094
2886
|
};
|
|
2095
2887
|
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
2096
|
-
if (!this.
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
try {
|
|
2101
|
-
await this.deadLetterConfig.onDeadLetter(info);
|
|
2102
|
-
msg.term("Dead letter processed");
|
|
2103
|
-
} catch (hookErr) {
|
|
2104
|
-
this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
|
|
2105
|
-
msg.nak();
|
|
2888
|
+
if (!this.options?.dlq) {
|
|
2889
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
2890
|
+
} else {
|
|
2891
|
+
await this.publishToDlq(msg, info, error);
|
|
2106
2892
|
}
|
|
2107
2893
|
}
|
|
2108
2894
|
};
|
|
2109
2895
|
|
|
2110
2896
|
// src/server/routing/rpc.router.ts
|
|
2111
|
-
var
|
|
2112
|
-
var
|
|
2897
|
+
var import_common12 = require("@nestjs/common");
|
|
2898
|
+
var import_transport_node5 = require("@nats-io/transport-node");
|
|
2113
2899
|
var import_rxjs5 = require("rxjs");
|
|
2114
2900
|
var RpcRouter = class {
|
|
2115
2901
|
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
@@ -2123,7 +2909,7 @@ var RpcRouter = class {
|
|
|
2123
2909
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
2124
2910
|
this.concurrency = rpcOptions?.concurrency;
|
|
2125
2911
|
}
|
|
2126
|
-
logger = new
|
|
2912
|
+
logger = new import_common12.Logger("Jetstream:RpcRouter");
|
|
2127
2913
|
timeout;
|
|
2128
2914
|
concurrency;
|
|
2129
2915
|
resolvedAckExtensionInterval;
|
|
@@ -2201,7 +2987,7 @@ var RpcRouter = class {
|
|
|
2201
2987
|
stopAckExtension?.();
|
|
2202
2988
|
msg.ack();
|
|
2203
2989
|
try {
|
|
2204
|
-
const hdrs = (0,
|
|
2990
|
+
const hdrs = (0, import_transport_node5.headers)();
|
|
2205
2991
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2206
2992
|
nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
|
|
2207
2993
|
} catch (publishErr) {
|
|
@@ -2213,7 +2999,7 @@ var RpcRouter = class {
|
|
|
2213
2999
|
clearTimeout(timeoutId);
|
|
2214
3000
|
stopAckExtension?.();
|
|
2215
3001
|
try {
|
|
2216
|
-
const hdrs = (0,
|
|
3002
|
+
const hdrs = (0, import_transport_node5.headers)();
|
|
2217
3003
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
2218
3004
|
hdrs.set("x-error" /* Error */, "true");
|
|
2219
3005
|
nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
|
|
@@ -2226,20 +3012,27 @@ var RpcRouter = class {
|
|
|
2226
3012
|
};
|
|
2227
3013
|
|
|
2228
3014
|
// src/shutdown/shutdown.manager.ts
|
|
2229
|
-
var
|
|
3015
|
+
var import_common13 = require("@nestjs/common");
|
|
2230
3016
|
var ShutdownManager = class {
|
|
2231
3017
|
constructor(connection, eventBus, timeout) {
|
|
2232
3018
|
this.connection = connection;
|
|
2233
3019
|
this.eventBus = eventBus;
|
|
2234
3020
|
this.timeout = timeout;
|
|
2235
3021
|
}
|
|
2236
|
-
logger = new
|
|
3022
|
+
logger = new import_common13.Logger("Jetstream:Shutdown");
|
|
3023
|
+
shutdownPromise;
|
|
2237
3024
|
/**
|
|
2238
3025
|
* Execute the full shutdown sequence.
|
|
2239
3026
|
*
|
|
3027
|
+
* Idempotent — concurrent or repeated calls return the same promise.
|
|
3028
|
+
*
|
|
2240
3029
|
* @param strategy Optional stoppable to close (stops consumers and subscriptions).
|
|
2241
3030
|
*/
|
|
2242
3031
|
async shutdown(strategy) {
|
|
3032
|
+
this.shutdownPromise ??= this.doShutdown(strategy);
|
|
3033
|
+
return this.shutdownPromise;
|
|
3034
|
+
}
|
|
3035
|
+
async doShutdown(strategy) {
|
|
2243
3036
|
this.eventBus.emit("shutdownStart" /* ShutdownStart */);
|
|
2244
3037
|
this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
|
|
2245
3038
|
strategy?.close();
|
|
@@ -2374,7 +3167,7 @@ var JetstreamModule = class {
|
|
|
2374
3167
|
provide: JETSTREAM_EVENT_BUS,
|
|
2375
3168
|
inject: [JETSTREAM_OPTIONS],
|
|
2376
3169
|
useFactory: (options) => {
|
|
2377
|
-
const logger = new
|
|
3170
|
+
const logger = new import_common14.Logger("Jetstream:Module");
|
|
2378
3171
|
return new EventBus(logger, options.hooks);
|
|
2379
3172
|
}
|
|
2380
3173
|
},
|
|
@@ -2453,8 +3246,8 @@ var JetstreamModule = class {
|
|
|
2453
3246
|
// MessageProvider — pull-based message consumption
|
|
2454
3247
|
{
|
|
2455
3248
|
provide: MessageProvider,
|
|
2456
|
-
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
|
|
2457
|
-
useFactory: (options, connection, eventBus) => {
|
|
3249
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
|
|
3250
|
+
useFactory: (options, connection, eventBus, consumerProvider) => {
|
|
2458
3251
|
if (options.consumer === false) return null;
|
|
2459
3252
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
2460
3253
|
if (options.events?.consume)
|
|
@@ -2464,7 +3257,11 @@ var JetstreamModule = class {
|
|
|
2464
3257
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
2465
3258
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
2466
3259
|
}
|
|
2467
|
-
|
|
3260
|
+
const consumerRecoveryFn = consumerProvider ? async (kind) => {
|
|
3261
|
+
const jsm = await connection.getJetStreamManager();
|
|
3262
|
+
return consumerProvider.recoverConsumer(jsm, kind);
|
|
3263
|
+
} : void 0;
|
|
3264
|
+
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
2468
3265
|
}
|
|
2469
3266
|
},
|
|
2470
3267
|
// EventRouter — routes event and broadcast messages to handlers
|
|
@@ -2476,9 +3273,10 @@ var JetstreamModule = class {
|
|
|
2476
3273
|
PatternRegistry,
|
|
2477
3274
|
JETSTREAM_CODEC,
|
|
2478
3275
|
JETSTREAM_EVENT_BUS,
|
|
2479
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3276
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3277
|
+
JETSTREAM_CONNECTION
|
|
2480
3278
|
],
|
|
2481
|
-
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
|
|
3279
|
+
useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
|
|
2482
3280
|
if (options.consumer === false) return null;
|
|
2483
3281
|
const deadLetterConfig = options.onDeadLetter ? {
|
|
2484
3282
|
maxDeliverByStream: /* @__PURE__ */ new Map(),
|
|
@@ -2501,7 +3299,9 @@ var JetstreamModule = class {
|
|
|
2501
3299
|
eventBus,
|
|
2502
3300
|
deadLetterConfig,
|
|
2503
3301
|
processingConfig,
|
|
2504
|
-
ackWaitMap
|
|
3302
|
+
ackWaitMap,
|
|
3303
|
+
connection,
|
|
3304
|
+
options
|
|
2505
3305
|
);
|
|
2506
3306
|
}
|
|
2507
3307
|
},
|
|
@@ -2550,6 +3350,15 @@ var JetstreamModule = class {
|
|
|
2550
3350
|
return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
|
|
2551
3351
|
}
|
|
2552
3352
|
},
|
|
3353
|
+
// MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
|
|
3354
|
+
{
|
|
3355
|
+
provide: MetadataProvider,
|
|
3356
|
+
inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
|
|
3357
|
+
useFactory: (options, connection) => {
|
|
3358
|
+
if (options.consumer === false) return null;
|
|
3359
|
+
return new MetadataProvider(options, connection);
|
|
3360
|
+
}
|
|
3361
|
+
},
|
|
2553
3362
|
// JetstreamStrategy — server-side transport (only when consumer enabled)
|
|
2554
3363
|
{
|
|
2555
3364
|
provide: JetstreamStrategy,
|
|
@@ -2563,9 +3372,10 @@ var JetstreamModule = class {
|
|
|
2563
3372
|
EventRouter,
|
|
2564
3373
|
RpcRouter,
|
|
2565
3374
|
CoreRpcServer,
|
|
2566
|
-
JETSTREAM_ACK_WAIT_MAP
|
|
3375
|
+
JETSTREAM_ACK_WAIT_MAP,
|
|
3376
|
+
MetadataProvider
|
|
2567
3377
|
],
|
|
2568
|
-
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
|
|
3378
|
+
useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
|
|
2569
3379
|
if (options.consumer === false) return null;
|
|
2570
3380
|
return new JetstreamStrategy(
|
|
2571
3381
|
options,
|
|
@@ -2577,7 +3387,8 @@ var JetstreamModule = class {
|
|
|
2577
3387
|
eventRouter,
|
|
2578
3388
|
rpcRouter,
|
|
2579
3389
|
coreRpcServer,
|
|
2580
|
-
ackWaitMap
|
|
3390
|
+
ackWaitMap,
|
|
3391
|
+
metadataProvider
|
|
2581
3392
|
);
|
|
2582
3393
|
}
|
|
2583
3394
|
}
|
|
@@ -2636,21 +3447,26 @@ var JetstreamModule = class {
|
|
|
2636
3447
|
}
|
|
2637
3448
|
};
|
|
2638
3449
|
JetstreamModule = __decorateClass([
|
|
2639
|
-
(0,
|
|
2640
|
-
(0,
|
|
2641
|
-
__decorateParam(0, (0,
|
|
2642
|
-
__decorateParam(0, (0,
|
|
2643
|
-
__decorateParam(1, (0,
|
|
2644
|
-
__decorateParam(1, (0,
|
|
3450
|
+
(0, import_common14.Global)(),
|
|
3451
|
+
(0, import_common14.Module)({}),
|
|
3452
|
+
__decorateParam(0, (0, import_common14.Optional)()),
|
|
3453
|
+
__decorateParam(0, (0, import_common14.Inject)(ShutdownManager)),
|
|
3454
|
+
__decorateParam(1, (0, import_common14.Optional)()),
|
|
3455
|
+
__decorateParam(1, (0, import_common14.Inject)(JetstreamStrategy))
|
|
2645
3456
|
], JetstreamModule);
|
|
2646
3457
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2647
3458
|
0 && (module.exports = {
|
|
3459
|
+
DEFAULT_METADATA_BUCKET,
|
|
3460
|
+
DEFAULT_METADATA_HISTORY,
|
|
3461
|
+
DEFAULT_METADATA_REPLICAS,
|
|
3462
|
+
DEFAULT_METADATA_TTL,
|
|
2648
3463
|
EventBus,
|
|
2649
3464
|
JETSTREAM_CODEC,
|
|
2650
3465
|
JETSTREAM_CONNECTION,
|
|
2651
3466
|
JETSTREAM_EVENT_BUS,
|
|
2652
3467
|
JETSTREAM_OPTIONS,
|
|
2653
3468
|
JetstreamClient,
|
|
3469
|
+
JetstreamDlqHeader,
|
|
2654
3470
|
JetstreamHeader,
|
|
2655
3471
|
JetstreamHealthIndicator,
|
|
2656
3472
|
JetstreamModule,
|
|
@@ -2658,17 +3474,21 @@ JetstreamModule = __decorateClass([
|
|
|
2658
3474
|
JetstreamRecordBuilder,
|
|
2659
3475
|
JetstreamStrategy,
|
|
2660
3476
|
JsonCodec,
|
|
3477
|
+
MIN_METADATA_TTL,
|
|
2661
3478
|
MessageKind,
|
|
2662
3479
|
PatternPrefix,
|
|
2663
3480
|
RpcContext,
|
|
2664
3481
|
StreamKind,
|
|
2665
3482
|
TransportEvent,
|
|
3483
|
+
buildBroadcastSubject,
|
|
2666
3484
|
buildSubject,
|
|
2667
3485
|
consumerName,
|
|
3486
|
+
dlqStreamName,
|
|
2668
3487
|
getClientToken,
|
|
2669
3488
|
internalName,
|
|
2670
3489
|
isCoreRpcMode,
|
|
2671
3490
|
isJetStreamRpcMode,
|
|
3491
|
+
metadataKey,
|
|
2672
3492
|
streamName,
|
|
2673
3493
|
toNanos
|
|
2674
3494
|
});
|