@crossdelta/cloudevents 0.5.7 → 0.6.1

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.
Files changed (70) hide show
  1. package/README.md +22 -2
  2. package/dist/index.cjs +1602 -0
  3. package/dist/index.d.mts +812 -0
  4. package/dist/index.d.ts +812 -9
  5. package/dist/index.js +1574 -6
  6. package/package.json +20 -18
  7. package/dist/adapters/cloudevents/cloudevents.d.ts +0 -14
  8. package/dist/adapters/cloudevents/cloudevents.js +0 -58
  9. package/dist/adapters/cloudevents/index.d.ts +0 -8
  10. package/dist/adapters/cloudevents/index.js +0 -7
  11. package/dist/adapters/cloudevents/parsers/binary-mode.d.ts +0 -5
  12. package/dist/adapters/cloudevents/parsers/binary-mode.js +0 -32
  13. package/dist/adapters/cloudevents/parsers/pubsub.d.ts +0 -5
  14. package/dist/adapters/cloudevents/parsers/pubsub.js +0 -54
  15. package/dist/adapters/cloudevents/parsers/raw-event.d.ts +0 -5
  16. package/dist/adapters/cloudevents/parsers/raw-event.js +0 -17
  17. package/dist/adapters/cloudevents/parsers/structured-mode.d.ts +0 -5
  18. package/dist/adapters/cloudevents/parsers/structured-mode.js +0 -18
  19. package/dist/adapters/cloudevents/types.d.ts +0 -29
  20. package/dist/adapters/cloudevents/types.js +0 -1
  21. package/dist/domain/contract-helper.d.ts +0 -63
  22. package/dist/domain/contract-helper.js +0 -61
  23. package/dist/domain/discovery.d.ts +0 -24
  24. package/dist/domain/discovery.js +0 -201
  25. package/dist/domain/handler-factory.d.ts +0 -49
  26. package/dist/domain/handler-factory.js +0 -169
  27. package/dist/domain/index.d.ts +0 -6
  28. package/dist/domain/index.js +0 -4
  29. package/dist/domain/types.d.ts +0 -108
  30. package/dist/domain/types.js +0 -6
  31. package/dist/domain/validation.d.ts +0 -37
  32. package/dist/domain/validation.js +0 -53
  33. package/dist/infrastructure/errors.d.ts +0 -53
  34. package/dist/infrastructure/errors.js +0 -54
  35. package/dist/infrastructure/index.d.ts +0 -4
  36. package/dist/infrastructure/index.js +0 -2
  37. package/dist/infrastructure/logging.d.ts +0 -18
  38. package/dist/infrastructure/logging.js +0 -27
  39. package/dist/middlewares/cloudevents-middleware.d.ts +0 -171
  40. package/dist/middlewares/cloudevents-middleware.js +0 -276
  41. package/dist/middlewares/index.d.ts +0 -1
  42. package/dist/middlewares/index.js +0 -1
  43. package/dist/processing/dlq-safe.d.ts +0 -82
  44. package/dist/processing/dlq-safe.js +0 -108
  45. package/dist/processing/handler-cache.d.ts +0 -36
  46. package/dist/processing/handler-cache.js +0 -94
  47. package/dist/processing/idempotency.d.ts +0 -51
  48. package/dist/processing/idempotency.js +0 -112
  49. package/dist/processing/index.d.ts +0 -4
  50. package/dist/processing/index.js +0 -4
  51. package/dist/processing/validation.d.ts +0 -41
  52. package/dist/processing/validation.js +0 -48
  53. package/dist/publishing/index.d.ts +0 -2
  54. package/dist/publishing/index.js +0 -2
  55. package/dist/publishing/nats.publisher.d.ts +0 -19
  56. package/dist/publishing/nats.publisher.js +0 -115
  57. package/dist/publishing/pubsub.publisher.d.ts +0 -39
  58. package/dist/publishing/pubsub.publisher.js +0 -84
  59. package/dist/transports/nats/base-message-processor.d.ts +0 -44
  60. package/dist/transports/nats/base-message-processor.js +0 -118
  61. package/dist/transports/nats/index.d.ts +0 -5
  62. package/dist/transports/nats/index.js +0 -5
  63. package/dist/transports/nats/jetstream-consumer.d.ts +0 -217
  64. package/dist/transports/nats/jetstream-consumer.js +0 -367
  65. package/dist/transports/nats/jetstream-message-processor.d.ts +0 -9
  66. package/dist/transports/nats/jetstream-message-processor.js +0 -32
  67. package/dist/transports/nats/nats-consumer.d.ts +0 -36
  68. package/dist/transports/nats/nats-consumer.js +0 -84
  69. package/dist/transports/nats/nats-message-processor.d.ts +0 -11
  70. 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
- };