@crossdelta/cloudevents 0.5.6 → 0.6.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/README.md +48 -28
- package/dist/index.cjs +1602 -0
- package/dist/index.d.mts +812 -0
- package/dist/index.d.ts +812 -9
- package/dist/index.js +1574 -6
- package/package.json +20 -18
- package/dist/adapters/cloudevents/cloudevents.d.ts +0 -14
- package/dist/adapters/cloudevents/cloudevents.js +0 -58
- package/dist/adapters/cloudevents/index.d.ts +0 -8
- package/dist/adapters/cloudevents/index.js +0 -7
- package/dist/adapters/cloudevents/parsers/binary-mode.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/binary-mode.js +0 -32
- package/dist/adapters/cloudevents/parsers/pubsub.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/pubsub.js +0 -54
- package/dist/adapters/cloudevents/parsers/raw-event.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/raw-event.js +0 -17
- package/dist/adapters/cloudevents/parsers/structured-mode.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/structured-mode.js +0 -18
- package/dist/adapters/cloudevents/types.d.ts +0 -29
- package/dist/adapters/cloudevents/types.js +0 -1
- package/dist/domain/contract-helper.d.ts +0 -63
- package/dist/domain/contract-helper.js +0 -61
- package/dist/domain/discovery.d.ts +0 -24
- package/dist/domain/discovery.js +0 -201
- package/dist/domain/handler-factory.d.ts +0 -49
- package/dist/domain/handler-factory.js +0 -169
- package/dist/domain/index.d.ts +0 -6
- package/dist/domain/index.js +0 -4
- package/dist/domain/types.d.ts +0 -108
- package/dist/domain/types.js +0 -6
- package/dist/domain/validation.d.ts +0 -37
- package/dist/domain/validation.js +0 -53
- package/dist/infrastructure/errors.d.ts +0 -53
- package/dist/infrastructure/errors.js +0 -54
- package/dist/infrastructure/index.d.ts +0 -4
- package/dist/infrastructure/index.js +0 -2
- package/dist/infrastructure/logging.d.ts +0 -18
- package/dist/infrastructure/logging.js +0 -27
- package/dist/middlewares/cloudevents-middleware.d.ts +0 -171
- package/dist/middlewares/cloudevents-middleware.js +0 -276
- package/dist/middlewares/index.d.ts +0 -1
- package/dist/middlewares/index.js +0 -1
- package/dist/processing/dlq-safe.d.ts +0 -82
- package/dist/processing/dlq-safe.js +0 -108
- package/dist/processing/handler-cache.d.ts +0 -36
- package/dist/processing/handler-cache.js +0 -94
- package/dist/processing/idempotency.d.ts +0 -51
- package/dist/processing/idempotency.js +0 -112
- package/dist/processing/index.d.ts +0 -4
- package/dist/processing/index.js +0 -4
- package/dist/processing/validation.d.ts +0 -41
- package/dist/processing/validation.js +0 -48
- package/dist/publishing/index.d.ts +0 -2
- package/dist/publishing/index.js +0 -2
- package/dist/publishing/nats.publisher.d.ts +0 -19
- package/dist/publishing/nats.publisher.js +0 -115
- package/dist/publishing/pubsub.publisher.d.ts +0 -39
- package/dist/publishing/pubsub.publisher.js +0 -84
- package/dist/transports/nats/base-message-processor.d.ts +0 -44
- package/dist/transports/nats/base-message-processor.js +0 -118
- package/dist/transports/nats/index.d.ts +0 -5
- package/dist/transports/nats/index.js +0 -5
- package/dist/transports/nats/jetstream-consumer.d.ts +0 -217
- package/dist/transports/nats/jetstream-consumer.js +0 -367
- package/dist/transports/nats/jetstream-message-processor.d.ts +0 -9
- package/dist/transports/nats/jetstream-message-processor.js +0 -32
- package/dist/transports/nats/nats-consumer.d.ts +0 -36
- package/dist/transports/nats/nats-consumer.js +0 -84
- package/dist/transports/nats/nats-message-processor.d.ts +0 -11
- package/dist/transports/nats/nats-message-processor.js +0 -32
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { connect, StringCodec, } from 'nats';
|
|
2
|
-
import { discoverHandlers } from '../../domain';
|
|
3
|
-
// String literals matching NATS enum values (enums not reliably exported in CI/Bun)
|
|
4
|
-
// AckPolicy.Explicit = "explicit", DeliverPolicy.All/Last/New/StartTime, RetentionPolicy.Limits = "limits", StorageType.File = "file"
|
|
5
|
-
const ACK_EXPLICIT = 'explicit';
|
|
6
|
-
const DELIVER_ALL = 'all';
|
|
7
|
-
const DELIVER_LAST = 'last';
|
|
8
|
-
const DELIVER_NEW = 'new';
|
|
9
|
-
const DELIVER_START_TIME = 'by_start_time';
|
|
10
|
-
const RETENTION_LIMITS = 'limits';
|
|
11
|
-
const STORAGE_FILE = 'file';
|
|
12
|
-
import { logger } from '../../infrastructure/logging';
|
|
13
|
-
import { processHandler } from '../../processing/handler-cache';
|
|
14
|
-
import { checkAndMarkProcessed, getDefaultIdempotencyStore } from '../../processing/idempotency';
|
|
15
|
-
import { createJetStreamMessageProcessor } from './jetstream-message-processor';
|
|
16
|
-
const sc = StringCodec();
|
|
17
|
-
// Default stream configuration
|
|
18
|
-
const DEFAULT_STREAM_CONFIG = {
|
|
19
|
-
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
|
|
20
|
-
maxBytes: 1024 * 1024 * 1024, // 1 GB
|
|
21
|
-
replicas: 1,
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Ensures a JetStream stream exists with the given configuration.
|
|
25
|
-
* This is typically called once during application startup or in infrastructure setup.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* // In infrastructure/setup code:
|
|
30
|
-
* await ensureJetStreamStream({
|
|
31
|
-
* stream: 'ORDERS',
|
|
32
|
-
* subjects: ['orders.>'],
|
|
33
|
-
* config: {
|
|
34
|
-
* maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
35
|
-
* replicas: 3
|
|
36
|
-
* }
|
|
37
|
-
* })
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export async function ensureJetStreamStream(options) {
|
|
41
|
-
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
42
|
-
const user = options.user ?? process.env.NATS_USER;
|
|
43
|
-
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
44
|
-
const nc = await connect({
|
|
45
|
-
servers,
|
|
46
|
-
...(user && pass ? { user, pass } : {}),
|
|
47
|
-
});
|
|
48
|
-
try {
|
|
49
|
-
const jsm = await nc.jetstreamManager();
|
|
50
|
-
await ensureStream(jsm, options.stream, options.subjects, options.config);
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
await nc.close();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Ensures stream exists with the given configuration (internal helper)
|
|
58
|
-
*/
|
|
59
|
-
async function ensureStream(jsm, name, subjects, config = {}) {
|
|
60
|
-
const streamConfig = { ...DEFAULT_STREAM_CONFIG, ...config };
|
|
61
|
-
try {
|
|
62
|
-
const stream = await jsm.streams.info(name);
|
|
63
|
-
// Update subjects if needed
|
|
64
|
-
const existingSubjects = new Set(stream.config.subjects);
|
|
65
|
-
const newSubjects = subjects.filter((s) => !existingSubjects.has(s));
|
|
66
|
-
if (newSubjects.length > 0) {
|
|
67
|
-
await jsm.streams.update(name, {
|
|
68
|
-
subjects: [...stream.config.subjects, ...newSubjects],
|
|
69
|
-
});
|
|
70
|
-
logger.info(`[jetstream] updated stream ${name} with subjects: ${newSubjects.join(', ')}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Stream doesn't exist, create it
|
|
75
|
-
await jsm.streams.add({
|
|
76
|
-
name,
|
|
77
|
-
subjects,
|
|
78
|
-
retention: RETENTION_LIMITS,
|
|
79
|
-
storage: STORAGE_FILE,
|
|
80
|
-
max_age: streamConfig.maxAge * 1_000_000, // Convert ms to nanoseconds
|
|
81
|
-
max_bytes: streamConfig.maxBytes,
|
|
82
|
-
num_replicas: streamConfig.replicas,
|
|
83
|
-
});
|
|
84
|
-
logger.info(`[jetstream] created stream ${name} with subjects: ${subjects.join(', ')}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Ensures multiple JetStream streams exist with a single NATS connection.
|
|
89
|
-
* More efficient than calling ensureJetStreamStream multiple times.
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* ```typescript
|
|
93
|
-
* await ensureJetStreamStreams({
|
|
94
|
-
* streams: [
|
|
95
|
-
* { stream: 'ORDERS', subjects: ['orders.*'] },
|
|
96
|
-
* { stream: 'CUSTOMERS', subjects: ['customers.*'] },
|
|
97
|
-
* ]
|
|
98
|
-
* })
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
export async function ensureJetStreamStreams(options) {
|
|
102
|
-
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
103
|
-
const user = options.user ?? process.env.NATS_USER;
|
|
104
|
-
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
105
|
-
const nc = await connect({
|
|
106
|
-
servers,
|
|
107
|
-
...(user && pass ? { user, pass } : {}),
|
|
108
|
-
});
|
|
109
|
-
try {
|
|
110
|
-
const jsm = await nc.jetstreamManager();
|
|
111
|
-
for (const { stream, subjects, config } of options.streams) {
|
|
112
|
-
await ensureStream(jsm, stream, subjects, config);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
await nc.close();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Ensures durable consumer exists
|
|
121
|
-
*/
|
|
122
|
-
async function ensureConsumer(jsm, streamName, consumerName, options) {
|
|
123
|
-
const deliverPolicy = (() => {
|
|
124
|
-
switch (options.startFrom) {
|
|
125
|
-
case 'all':
|
|
126
|
-
return DELIVER_ALL;
|
|
127
|
-
case 'last':
|
|
128
|
-
return DELIVER_LAST;
|
|
129
|
-
default:
|
|
130
|
-
return DELIVER_NEW;
|
|
131
|
-
}
|
|
132
|
-
})();
|
|
133
|
-
const optStartTime = options.startFrom instanceof Date ? options.startFrom : undefined;
|
|
134
|
-
try {
|
|
135
|
-
await jsm.consumers.info(streamName, consumerName);
|
|
136
|
-
// Consumer exists, no update needed for durable consumers
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// Consumer doesn't exist, create it
|
|
140
|
-
await jsm.consumers.add(streamName, {
|
|
141
|
-
durable_name: consumerName,
|
|
142
|
-
ack_policy: ACK_EXPLICIT,
|
|
143
|
-
deliver_policy: optStartTime ? DELIVER_START_TIME : deliverPolicy,
|
|
144
|
-
opt_start_time: optStartTime?.toISOString(),
|
|
145
|
-
// replay_policy defaults to 'instant', no need to specify explicitly
|
|
146
|
-
ack_wait: (options.ackWait ?? 30_000) * 1_000_000, // Convert to nanoseconds
|
|
147
|
-
max_deliver: options.maxDeliver ?? 3,
|
|
148
|
-
// Filter subjects at consumer level (optional)
|
|
149
|
-
filter_subjects: options.filterSubjects,
|
|
150
|
-
});
|
|
151
|
-
logger.info(`[jetstream] created durable consumer ${consumerName} on stream ${streamName}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Consume CloudEvents from NATS JetStream with persistence and guaranteed delivery.
|
|
156
|
-
*
|
|
157
|
-
* Features:
|
|
158
|
-
* - Automatic stream and consumer creation
|
|
159
|
-
* - Durable subscriptions (survive restarts)
|
|
160
|
-
* - Automatic acknowledgments on successful processing
|
|
161
|
-
* - Configurable retry with max redelivery
|
|
162
|
-
* - Dead letter queue support
|
|
163
|
-
*
|
|
164
|
-
* @example
|
|
165
|
-
* ```typescript
|
|
166
|
-
* await consumeJetStreamEvents({
|
|
167
|
-
* stream: 'ORDERS',
|
|
168
|
-
* subjects: ['orders.>'],
|
|
169
|
-
* consumer: 'notifications',
|
|
170
|
-
* discover: `./src/events/**\/*.handler.ts`,
|
|
171
|
-
* })
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
export async function consumeJetStreamEvents(options) {
|
|
175
|
-
const name = options.consumer;
|
|
176
|
-
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
177
|
-
// Authentication (from options or env vars)
|
|
178
|
-
const user = options.user ?? process.env.NATS_USER;
|
|
179
|
-
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
180
|
-
// 1) Discover handlers
|
|
181
|
-
const handlerConstructors = await discoverHandlers(options.discover);
|
|
182
|
-
const processedHandlers = handlerConstructors
|
|
183
|
-
.map(processHandler)
|
|
184
|
-
.filter((h) => h !== null);
|
|
185
|
-
const handlerNames = processedHandlers.map((h) => h.name).join(', ');
|
|
186
|
-
logger.info(`[${name}] discovered ${processedHandlers.length} handler(s): ${handlerNames}`);
|
|
187
|
-
// 2) Connect to NATS
|
|
188
|
-
const nc = await connect({
|
|
189
|
-
servers,
|
|
190
|
-
...(user && pass ? { user, pass } : {}),
|
|
191
|
-
});
|
|
192
|
-
logger.info(`[${name}] connected to NATS: ${servers}${user ? ' (authenticated)' : ''}`);
|
|
193
|
-
// 3) Setup JetStream
|
|
194
|
-
const jsm = await nc.jetstreamManager();
|
|
195
|
-
const js = nc.jetstream();
|
|
196
|
-
// 4) Ensure durable consumer exists (stream must already exist)
|
|
197
|
-
await ensureConsumer(jsm, options.stream, name, options);
|
|
198
|
-
// 5) Get consumer and start consuming
|
|
199
|
-
const consumer = await js.consumers.get(options.stream, name);
|
|
200
|
-
const messages = await consumer.consume({
|
|
201
|
-
max_messages: options.maxMessages ?? 100,
|
|
202
|
-
});
|
|
203
|
-
logger.info(`[${name}] consuming from stream ${options.stream}`);
|
|
204
|
-
const dlqEnabled = Boolean(options.quarantineTopic || options.errorTopic);
|
|
205
|
-
// Setup idempotency store
|
|
206
|
-
const idempotencyStore = options.idempotencyStore === false ? null : (options.idempotencyStore ?? getDefaultIdempotencyStore());
|
|
207
|
-
const idempotencyTtl = options.idempotencyTtl;
|
|
208
|
-
const { handleMessage, handleUnhandledProcessingError } = createJetStreamMessageProcessor({
|
|
209
|
-
name,
|
|
210
|
-
dlqEnabled,
|
|
211
|
-
options,
|
|
212
|
-
processedHandlers,
|
|
213
|
-
decode: (data) => sc.decode(data),
|
|
214
|
-
logger,
|
|
215
|
-
});
|
|
216
|
-
// Check idempotency and skip duplicates
|
|
217
|
-
const checkIdempotency = async (msg) => {
|
|
218
|
-
if (!idempotencyStore)
|
|
219
|
-
return true;
|
|
220
|
-
const messageId = `${msg.info.stream}:${msg.seq}`;
|
|
221
|
-
const shouldProcess = await checkAndMarkProcessed(idempotencyStore, messageId, idempotencyTtl);
|
|
222
|
-
if (!shouldProcess) {
|
|
223
|
-
logger.debug(`[${name}] skipping duplicate message: ${messageId}`);
|
|
224
|
-
msg.ack();
|
|
225
|
-
}
|
|
226
|
-
return shouldProcess;
|
|
227
|
-
};
|
|
228
|
-
// Process a single message
|
|
229
|
-
const processSingleMessage = async (msg) => {
|
|
230
|
-
const shouldProcess = await checkIdempotency(msg);
|
|
231
|
-
if (!shouldProcess)
|
|
232
|
-
return;
|
|
233
|
-
try {
|
|
234
|
-
const success = await handleMessage(msg);
|
|
235
|
-
if (success) {
|
|
236
|
-
msg.ack();
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
msg.nak();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
await handleUnhandledProcessingError(msg, error);
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
(async () => {
|
|
247
|
-
try {
|
|
248
|
-
for await (const msg of messages) {
|
|
249
|
-
await processSingleMessage(msg);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
logger.error(`[${name}] message processing loop crashed`, err);
|
|
254
|
-
}
|
|
255
|
-
})();
|
|
256
|
-
return messages;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Consume CloudEvents from multiple JetStream streams with a single connection.
|
|
260
|
-
* More efficient than calling consumeJetStreamEvents multiple times.
|
|
261
|
-
*
|
|
262
|
-
* @example
|
|
263
|
-
* ```typescript
|
|
264
|
-
* await consumeJetStreamStreams({
|
|
265
|
-
* streams: ['ORDERS', 'CUSTOMERS'],
|
|
266
|
-
* consumer: 'notifications',
|
|
267
|
-
* discover: `./src/events/**\/*.handler.ts`,
|
|
268
|
-
* })
|
|
269
|
-
* ```
|
|
270
|
-
*/
|
|
271
|
-
export async function consumeJetStreamStreams(options) {
|
|
272
|
-
const name = options.consumer;
|
|
273
|
-
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
274
|
-
const user = options.user ?? process.env.NATS_USER;
|
|
275
|
-
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
276
|
-
// 1) Discover handlers once (shared across all streams)
|
|
277
|
-
const handlerConstructors = await discoverHandlers(options.discover);
|
|
278
|
-
const processedHandlers = handlerConstructors
|
|
279
|
-
.map(processHandler)
|
|
280
|
-
.filter((h) => h !== null);
|
|
281
|
-
const handlerNames = processedHandlers.map((h) => h.name).join(', ');
|
|
282
|
-
logger.info(`[${name}] discovered ${processedHandlers.length} handler(s): ${handlerNames}`);
|
|
283
|
-
// 2) Connect to NATS once
|
|
284
|
-
const nc = await connect({
|
|
285
|
-
servers,
|
|
286
|
-
...(user && pass ? { user, pass } : {}),
|
|
287
|
-
});
|
|
288
|
-
logger.info(`[${name}] connected to NATS: ${servers}${user ? ' (authenticated)' : ''}`);
|
|
289
|
-
// 3) Setup JetStream
|
|
290
|
-
const jsm = await nc.jetstreamManager();
|
|
291
|
-
const js = nc.jetstream();
|
|
292
|
-
const dlqEnabled = Boolean(options.quarantineTopic || options.errorTopic);
|
|
293
|
-
const idempotencyStore = options.idempotencyStore === false ? null : (options.idempotencyStore ?? getDefaultIdempotencyStore());
|
|
294
|
-
const idempotencyTtl = options.idempotencyTtl;
|
|
295
|
-
const { handleMessage, handleUnhandledProcessingError } = createJetStreamMessageProcessor({
|
|
296
|
-
name,
|
|
297
|
-
dlqEnabled,
|
|
298
|
-
options,
|
|
299
|
-
processedHandlers,
|
|
300
|
-
decode: (data) => sc.decode(data),
|
|
301
|
-
logger,
|
|
302
|
-
});
|
|
303
|
-
const checkIdempotency = async (msg) => {
|
|
304
|
-
if (!idempotencyStore)
|
|
305
|
-
return true;
|
|
306
|
-
const messageId = `${msg.info.stream}:${msg.seq}`;
|
|
307
|
-
const shouldProcess = await checkAndMarkProcessed(idempotencyStore, messageId, idempotencyTtl);
|
|
308
|
-
if (!shouldProcess) {
|
|
309
|
-
logger.debug(`[${name}] skipping duplicate message: ${messageId}`);
|
|
310
|
-
msg.ack();
|
|
311
|
-
}
|
|
312
|
-
return shouldProcess;
|
|
313
|
-
};
|
|
314
|
-
const processSingleMessage = async (msg) => {
|
|
315
|
-
const shouldProcess = await checkIdempotency(msg);
|
|
316
|
-
if (!shouldProcess)
|
|
317
|
-
return;
|
|
318
|
-
try {
|
|
319
|
-
const success = await handleMessage(msg);
|
|
320
|
-
if (success) {
|
|
321
|
-
msg.ack();
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
msg.nak();
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
await handleUnhandledProcessingError(msg, error);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
// 4) Setup consumer for each stream
|
|
332
|
-
const allMessages = [];
|
|
333
|
-
for (const stream of options.streams) {
|
|
334
|
-
const consumerOpts = {
|
|
335
|
-
...options,
|
|
336
|
-
stream,
|
|
337
|
-
};
|
|
338
|
-
await ensureConsumer(jsm, stream, name, consumerOpts);
|
|
339
|
-
const consumer = await js.consumers.get(stream, name);
|
|
340
|
-
const messages = await consumer.consume({
|
|
341
|
-
max_messages: options.maxMessages ?? 100,
|
|
342
|
-
});
|
|
343
|
-
logger.info(`[${name}] consuming from stream ${stream}`);
|
|
344
|
-
(async () => {
|
|
345
|
-
try {
|
|
346
|
-
for await (const msg of messages) {
|
|
347
|
-
await processSingleMessage(msg);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
catch (err) {
|
|
351
|
-
logger.error(`[${name}] message processing loop crashed for stream ${stream}`, err);
|
|
352
|
-
}
|
|
353
|
-
})();
|
|
354
|
-
allMessages.push(messages);
|
|
355
|
-
}
|
|
356
|
-
return allMessages;
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Alias for ensureJetStreamStreams - shorter name
|
|
360
|
-
* @see ensureJetStreamStreams
|
|
361
|
-
*/
|
|
362
|
-
export const ensureJetStreams = ensureJetStreamStreams;
|
|
363
|
-
/**
|
|
364
|
-
* Alias for consumeJetStreamStreams - shorter name
|
|
365
|
-
* @see consumeJetStreamStreams
|
|
366
|
-
*/
|
|
367
|
-
export const consumeJetStreams = consumeJetStreamStreams;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { JsMsg } from 'nats';
|
|
2
|
-
import { type BaseMessageProcessorDeps } from './base-message-processor';
|
|
3
|
-
export type JetStreamMessageProcessorDeps = BaseMessageProcessorDeps;
|
|
4
|
-
export interface JetStreamMessageProcessor {
|
|
5
|
-
/** Returns true if message was handled successfully (should ack), false for retry (should nak) */
|
|
6
|
-
handleMessage(msg: JsMsg): Promise<boolean>;
|
|
7
|
-
handleUnhandledProcessingError(msg: JsMsg, error: unknown): Promise<void>;
|
|
8
|
-
}
|
|
9
|
-
export declare const createJetStreamMessageProcessor: (deps: JetStreamMessageProcessorDeps) => JetStreamMessageProcessor;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createProcessingContext } from '../../processing/dlq-safe';
|
|
2
|
-
import { createBaseMessageProcessor } from './base-message-processor';
|
|
3
|
-
export const createJetStreamMessageProcessor = (deps) => {
|
|
4
|
-
const { decode } = deps;
|
|
5
|
-
const base = createBaseMessageProcessor(deps);
|
|
6
|
-
const toUnknownContext = (msg) => ({
|
|
7
|
-
eventType: 'unknown',
|
|
8
|
-
source: `jetstream://${msg.info.stream}`,
|
|
9
|
-
subject: msg.subject,
|
|
10
|
-
time: new Date().toISOString(),
|
|
11
|
-
messageId: `${msg.info.stream}:${msg.seq}`,
|
|
12
|
-
data: decode(msg.data),
|
|
13
|
-
});
|
|
14
|
-
const handleMessage = async (msg) => {
|
|
15
|
-
try {
|
|
16
|
-
const cloudEvent = base.parseCloudEvent(msg.data);
|
|
17
|
-
const enriched = base.toEnrichedEvent(cloudEvent);
|
|
18
|
-
return base.processEvent(cloudEvent, enriched);
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
const unknownCtx = toUnknownContext(msg);
|
|
22
|
-
const context = createProcessingContext('unknown', decode(msg.data), unknownCtx, undefined);
|
|
23
|
-
return base.handleParseError(error, context, msg.info.deliveryCount);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const handleUnhandledProcessingError = async (msg, error) => {
|
|
27
|
-
const unknownCtx = toUnknownContext(msg);
|
|
28
|
-
const context = createProcessingContext('unknown', decode(msg.data), unknownCtx, undefined);
|
|
29
|
-
await base.handleUnhandledError(error, context, () => msg.ack());
|
|
30
|
-
};
|
|
31
|
-
return { handleMessage, handleUnhandledProcessingError };
|
|
32
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { type Subscription } from 'nats';
|
|
2
|
-
import type { CloudEventsOptions } from '../../middlewares/cloudevents-middleware';
|
|
3
|
-
/**
|
|
4
|
-
* Describes the configuration required to bootstrap the NATS event consumer.
|
|
5
|
-
*
|
|
6
|
-
* @property servers - Optional NATS connection string; defaults to `NATS_URL` or the local instance.
|
|
7
|
-
* @property subject - NATS subject to subscribe to for incoming events.
|
|
8
|
-
* @property discover - Glob pattern or directory used to discover event handler classes.
|
|
9
|
-
* @property consumerName - Optional identifier appended to log output and the consumer name.
|
|
10
|
-
* @property user - Optional NATS username for authentication (can also use `NATS_USER` env var).
|
|
11
|
-
* @property pass - Optional NATS password for authentication (can also use `NATS_PASSWORD` env var).
|
|
12
|
-
* @property quarantineTopic - Optional Pub/Sub topic for quarantining malformed messages when DLQ mode is enabled.
|
|
13
|
-
* @property errorTopic - Optional Pub/Sub topic for recovering handler errors when DLQ mode is enabled.
|
|
14
|
-
* @property projectId - Optional Google Cloud project identifier used for DLQ publishing.
|
|
15
|
-
* @property source - Optional CloudEvent source identifier applied to DLQ messages.
|
|
16
|
-
*/
|
|
17
|
-
export interface NatsConsumerOptions extends Pick<CloudEventsOptions, 'quarantineTopic' | 'errorTopic' | 'projectId' | 'source'> {
|
|
18
|
-
servers?: string;
|
|
19
|
-
subject: string;
|
|
20
|
-
discover: string;
|
|
21
|
-
consumerName?: string;
|
|
22
|
-
/** NATS username for authentication (defaults to NATS_USER env var) */
|
|
23
|
-
user?: string;
|
|
24
|
-
/** NATS password for authentication (defaults to NATS_PASSWORD env var) */
|
|
25
|
-
pass?: string;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Connects to NATS, discovers matching event handlers, and processes incoming CloudEvents.
|
|
29
|
-
*
|
|
30
|
-
* @param options - Consumer configuration describing connection, discovery, and subscription details.
|
|
31
|
-
* @returns The active NATS subscription for the configured subject.
|
|
32
|
-
*
|
|
33
|
-
* When `quarantineTopic` or `errorTopic` is provided, the consumer forwards malformed messages
|
|
34
|
-
* and handler failures to the configured DLQ topics instead of throwing errors.
|
|
35
|
-
*/
|
|
36
|
-
export declare function consumeNatsEvents(options: NatsConsumerOptions): Promise<Subscription>;
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { connect, StringCodec } from 'nats';
|
|
2
|
-
import { discoverHandlers } from '../../domain';
|
|
3
|
-
import { logger } from '../../infrastructure/logging';
|
|
4
|
-
import { processHandler } from '../../processing/handler-cache';
|
|
5
|
-
import { createNatsMessageProcessor } from './nats-message-processor';
|
|
6
|
-
const sc = StringCodec();
|
|
7
|
-
// Use globalThis to persist across hot-reloads
|
|
8
|
-
const CONSUMER_REGISTRY_KEY = '__crossdelta_nats_consumers__';
|
|
9
|
-
function getConsumerRegistry() {
|
|
10
|
-
if (!globalThis[CONSUMER_REGISTRY_KEY]) {
|
|
11
|
-
;
|
|
12
|
-
globalThis[CONSUMER_REGISTRY_KEY] = new Map();
|
|
13
|
-
}
|
|
14
|
-
return globalThis[CONSUMER_REGISTRY_KEY];
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Cleanup function to close a consumer by name
|
|
18
|
-
*/
|
|
19
|
-
async function cleanupConsumer(name) {
|
|
20
|
-
const registry = getConsumerRegistry();
|
|
21
|
-
const consumer = registry.get(name);
|
|
22
|
-
if (consumer) {
|
|
23
|
-
logger.info(`[${name}] cleaning up subscription...`);
|
|
24
|
-
consumer.subscription.unsubscribe();
|
|
25
|
-
await consumer.connection.drain();
|
|
26
|
-
registry.delete(name);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Connects to NATS, discovers matching event handlers, and processes incoming CloudEvents.
|
|
31
|
-
*
|
|
32
|
-
* @param options - Consumer configuration describing connection, discovery, and subscription details.
|
|
33
|
-
* @returns The active NATS subscription for the configured subject.
|
|
34
|
-
*
|
|
35
|
-
* When `quarantineTopic` or `errorTopic` is provided, the consumer forwards malformed messages
|
|
36
|
-
* and handler failures to the configured DLQ topics instead of throwing errors.
|
|
37
|
-
*/
|
|
38
|
-
export async function consumeNatsEvents(options) {
|
|
39
|
-
const servers = options.servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
40
|
-
const subject = options.subject;
|
|
41
|
-
const name = options.consumerName ?? `nats-consumer:${subject}`;
|
|
42
|
-
// Authentication (from options or env vars)
|
|
43
|
-
const user = options.user ?? process.env.NATS_USER;
|
|
44
|
-
const pass = options.pass ?? process.env.NATS_PASSWORD;
|
|
45
|
-
// Cleanup existing consumer with same name (handles hot-reload)
|
|
46
|
-
await cleanupConsumer(name);
|
|
47
|
-
// 1) Discover handler classes from *.handler.ts files
|
|
48
|
-
const handlerConstructors = await discoverHandlers(options.discover);
|
|
49
|
-
const processedHandlers = handlerConstructors
|
|
50
|
-
.map(processHandler)
|
|
51
|
-
.filter((h) => h !== null);
|
|
52
|
-
const handlerNames = processedHandlers.map((h) => h.name).join(', ');
|
|
53
|
-
logger.info(`[${name}] discovered ${processedHandlers.length} handler(s): ${handlerNames}`);
|
|
54
|
-
// 2) Connect to NATS
|
|
55
|
-
const nc = await connect({
|
|
56
|
-
servers,
|
|
57
|
-
...(user && pass ? { user, pass } : {}),
|
|
58
|
-
});
|
|
59
|
-
logger.info(`[${name}] connected to NATS: ${servers}${user ? ' (authenticated)' : ''}`);
|
|
60
|
-
// 3) Subscribe to the subject
|
|
61
|
-
const sub = nc.subscribe(subject);
|
|
62
|
-
logger.info(`[${name}] subscribed to subject: ${subject}`);
|
|
63
|
-
// Track this consumer for cleanup
|
|
64
|
-
getConsumerRegistry().set(name, { subscription: sub, connection: nc });
|
|
65
|
-
const dlqEnabled = Boolean(options.quarantineTopic || options.errorTopic);
|
|
66
|
-
const { handleMessage, handleUnhandledProcessingError } = createNatsMessageProcessor({
|
|
67
|
-
name,
|
|
68
|
-
subject,
|
|
69
|
-
dlqEnabled,
|
|
70
|
-
options,
|
|
71
|
-
processedHandlers,
|
|
72
|
-
decode: (data) => sc.decode(data),
|
|
73
|
-
logger,
|
|
74
|
-
});
|
|
75
|
-
const processSubscription = async () => {
|
|
76
|
-
for await (const msg of sub) {
|
|
77
|
-
await handleMessage(msg).catch((error) => handleUnhandledProcessingError(msg, error));
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
processSubscription().catch((err) => {
|
|
81
|
-
logger.error(`[${name}] subscription loop crashed`, err);
|
|
82
|
-
});
|
|
83
|
-
return sub;
|
|
84
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Msg } from 'nats';
|
|
2
|
-
import { type BaseMessageProcessorDeps, type LoggerLike } from './base-message-processor';
|
|
3
|
-
export type { LoggerLike };
|
|
4
|
-
export interface NatsMessageProcessorDeps extends BaseMessageProcessorDeps {
|
|
5
|
-
subject: string;
|
|
6
|
-
}
|
|
7
|
-
export interface NatsMessageProcessor {
|
|
8
|
-
handleMessage(msg: Msg): Promise<void>;
|
|
9
|
-
handleUnhandledProcessingError(msg: Msg, error: unknown): Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
export declare const createNatsMessageProcessor: (deps: NatsMessageProcessorDeps) => NatsMessageProcessor;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createProcessingContext } from '../../processing/dlq-safe';
|
|
2
|
-
import { createBaseMessageProcessor } from './base-message-processor';
|
|
3
|
-
export const createNatsMessageProcessor = (deps) => {
|
|
4
|
-
const { subject, decode } = deps;
|
|
5
|
-
const base = createBaseMessageProcessor(deps);
|
|
6
|
-
const toUnknownContext = (msg) => ({
|
|
7
|
-
eventType: 'unknown',
|
|
8
|
-
source: `nats://${subject}`,
|
|
9
|
-
subject,
|
|
10
|
-
time: new Date().toISOString(),
|
|
11
|
-
messageId: msg.headers?.get('Nats-Msg-Id') ?? 'unknown',
|
|
12
|
-
data: decode(msg.data),
|
|
13
|
-
});
|
|
14
|
-
const handleMessage = async (msg) => {
|
|
15
|
-
try {
|
|
16
|
-
const cloudEvent = base.parseCloudEvent(msg.data);
|
|
17
|
-
const enriched = base.toEnrichedEvent(cloudEvent);
|
|
18
|
-
await base.processEvent(cloudEvent, enriched);
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
const unknownCtx = toUnknownContext(msg);
|
|
22
|
-
const context = createProcessingContext('unknown', decode(msg.data), unknownCtx, undefined);
|
|
23
|
-
await base.handleParseError(error, context);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const handleUnhandledProcessingError = async (msg, error) => {
|
|
27
|
-
const unknownCtx = toUnknownContext(msg);
|
|
28
|
-
const context = createProcessingContext('unknown', decode(msg.data), unknownCtx, undefined);
|
|
29
|
-
await base.handleUnhandledError(error, context);
|
|
30
|
-
};
|
|
31
|
-
return { handleMessage, handleUnhandledProcessingError };
|
|
32
|
-
};
|