@onebun/nats 0.1.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 +141 -0
- package/package.json +52 -0
- package/src/index.ts +26 -0
- package/src/jetstream.adapter.ts +588 -0
- package/src/nats-client.ts +198 -0
- package/src/nats.adapter.ts +467 -0
- package/src/types.ts +81 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JetStream Queue Adapter
|
|
3
|
+
*
|
|
4
|
+
* Queue adapter using NATS JetStream for persistent messaging.
|
|
5
|
+
* Provides reliable message delivery with persistence and acknowledgments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { JetStreamAdapterOptions } from './types';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
QueueAdapter,
|
|
12
|
+
QueueAdapterType,
|
|
13
|
+
QueueFeature,
|
|
14
|
+
QueueEvents,
|
|
15
|
+
Message,
|
|
16
|
+
MessageMetadata,
|
|
17
|
+
PublishOptions,
|
|
18
|
+
SubscribeOptions,
|
|
19
|
+
Subscription,
|
|
20
|
+
ScheduledJobOptions,
|
|
21
|
+
ScheduledJobInfo,
|
|
22
|
+
MessageHandler,
|
|
23
|
+
QueueScheduler,
|
|
24
|
+
} from '@onebun/core';
|
|
25
|
+
import {
|
|
26
|
+
createQueuePatternMatcher,
|
|
27
|
+
createQueueScheduler,
|
|
28
|
+
type QueuePatternMatch,
|
|
29
|
+
} from '@onebun/core';
|
|
30
|
+
|
|
31
|
+
import { NatsClient } from './nats-client';
|
|
32
|
+
|
|
33
|
+
// Import JetStream types dynamically
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
let jetstreamModule: any = null;
|
|
36
|
+
|
|
37
|
+
async function getJetStreamModule() {
|
|
38
|
+
if (!jetstreamModule) {
|
|
39
|
+
jetstreamModule = await import('@nats-io/jetstream');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return jetstreamModule;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// JetStream Message Implementation
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
class JetStreamMessage<T> implements Message<T> {
|
|
50
|
+
id: string;
|
|
51
|
+
pattern: string;
|
|
52
|
+
data: T;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
redelivered: boolean;
|
|
55
|
+
metadata: MessageMetadata;
|
|
56
|
+
attempt?: number;
|
|
57
|
+
maxAttempts?: number;
|
|
58
|
+
|
|
59
|
+
private acked = false;
|
|
60
|
+
private nacked = false;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
private jsMsg: any;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
id: string,
|
|
66
|
+
pattern: string,
|
|
67
|
+
data: T,
|
|
68
|
+
timestamp: number,
|
|
69
|
+
metadata: MessageMetadata,
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
jsMsg: any,
|
|
72
|
+
) {
|
|
73
|
+
this.id = id;
|
|
74
|
+
this.pattern = pattern;
|
|
75
|
+
this.data = data;
|
|
76
|
+
this.timestamp = timestamp;
|
|
77
|
+
this.metadata = metadata;
|
|
78
|
+
this.jsMsg = jsMsg;
|
|
79
|
+
this.redelivered = jsMsg?.info?.redelivered ?? false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async ack(): Promise<void> {
|
|
83
|
+
if (this.acked || this.nacked) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.acked = true;
|
|
87
|
+
if (this.jsMsg?.ack) {
|
|
88
|
+
this.jsMsg.ack();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async nack(requeue = false): Promise<void> {
|
|
93
|
+
if (this.acked || this.nacked) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.nacked = true;
|
|
97
|
+
if (this.jsMsg?.nak) {
|
|
98
|
+
// JetStream will requeue automatically based on consumer config
|
|
99
|
+
this.jsMsg.nak(requeue ? undefined : { delay: -1 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// JetStream Subscription Implementation
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
interface JetStreamSubscriptionEntry {
|
|
109
|
+
pattern: string;
|
|
110
|
+
handler: MessageHandler;
|
|
111
|
+
options?: SubscribeOptions;
|
|
112
|
+
matcher: (topic: string) => QueuePatternMatch;
|
|
113
|
+
paused: boolean;
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
consumer?: any;
|
|
116
|
+
running: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class JetStreamSubscription implements Subscription {
|
|
120
|
+
private active = true;
|
|
121
|
+
|
|
122
|
+
constructor(
|
|
123
|
+
private readonly entry: JetStreamSubscriptionEntry,
|
|
124
|
+
private readonly onUnsubscribe: () => Promise<void>,
|
|
125
|
+
) {}
|
|
126
|
+
|
|
127
|
+
async unsubscribe(): Promise<void> {
|
|
128
|
+
this.active = false;
|
|
129
|
+
this.entry.running = false;
|
|
130
|
+
await this.onUnsubscribe();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pause(): void {
|
|
134
|
+
this.entry.paused = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
resume(): void {
|
|
138
|
+
this.entry.paused = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get pattern(): string {
|
|
142
|
+
return this.entry.pattern;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get isActive(): boolean {
|
|
146
|
+
return this.active && !this.entry.paused;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// JetStream Queue Adapter
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* JetStream Queue Adapter
|
|
156
|
+
*
|
|
157
|
+
* Uses NATS JetStream for persistent, reliable message delivery.
|
|
158
|
+
*
|
|
159
|
+
* Features:
|
|
160
|
+
* - Pattern subscriptions
|
|
161
|
+
* - Consumer groups (durable consumers)
|
|
162
|
+
* - Scheduled jobs (via in-process scheduler)
|
|
163
|
+
* - Dead letter queue support
|
|
164
|
+
* - Retry with acknowledgment
|
|
165
|
+
* - Message persistence
|
|
166
|
+
*
|
|
167
|
+
* Not supported:
|
|
168
|
+
* - Priority (JetStream doesn't support priority)
|
|
169
|
+
* - Delayed messages (can be simulated with headers)
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const adapter = new JetStreamQueueAdapter({
|
|
174
|
+
* servers: 'nats://localhost:4222',
|
|
175
|
+
* stream: 'EVENTS',
|
|
176
|
+
* createStream: true,
|
|
177
|
+
* streamConfig: {
|
|
178
|
+
* subjects: ['events.>'],
|
|
179
|
+
* retention: 'limits',
|
|
180
|
+
* maxMsgs: 1000000,
|
|
181
|
+
* },
|
|
182
|
+
* });
|
|
183
|
+
* await adapter.connect();
|
|
184
|
+
*
|
|
185
|
+
* await adapter.subscribe('events.*', async (message) => {
|
|
186
|
+
* console.log('Received:', message.data);
|
|
187
|
+
* await message.ack();
|
|
188
|
+
* }, { ackMode: 'manual', group: 'event-processor' });
|
|
189
|
+
*
|
|
190
|
+
* await adapter.publish('events.created', { id: 123 });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export class JetStreamQueueAdapter implements QueueAdapter {
|
|
194
|
+
readonly name = 'jetstream';
|
|
195
|
+
readonly type: QueueAdapterType = 'jetstream';
|
|
196
|
+
|
|
197
|
+
private client: NatsClient;
|
|
198
|
+
private connected = false;
|
|
199
|
+
private scheduler: QueueScheduler | null = null;
|
|
200
|
+
private subscriptions: JetStreamSubscriptionEntry[] = [];
|
|
201
|
+
private messageIdCounter = 0;
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
|
+
private js: any = null;
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
205
|
+
private jsm: any = null;
|
|
206
|
+
|
|
207
|
+
// Event handlers
|
|
208
|
+
private eventHandlers: Map<keyof QueueEvents, Set<(...args: unknown[]) => void>> = new Map();
|
|
209
|
+
|
|
210
|
+
constructor(private readonly options: JetStreamAdapterOptions) {
|
|
211
|
+
this.client = new NatsClient(options);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Lifecycle
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
async connect(): Promise<void> {
|
|
219
|
+
if (this.connected) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await this.client.connect();
|
|
225
|
+
|
|
226
|
+
const jsModule = await getJetStreamModule();
|
|
227
|
+
const nc = this.client.getConnection();
|
|
228
|
+
|
|
229
|
+
// Get JetStream context
|
|
230
|
+
this.js = jsModule.jetstream(nc);
|
|
231
|
+
this.jsm = await jsModule.jetstreamManager(nc);
|
|
232
|
+
|
|
233
|
+
// Create stream if needed
|
|
234
|
+
if (this.options.createStream) {
|
|
235
|
+
await this.ensureStream();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.connected = true;
|
|
239
|
+
this.scheduler = createQueueScheduler(this);
|
|
240
|
+
|
|
241
|
+
this.emit('onReady');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.emit('onError', error as Error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async disconnect(): Promise<void> {
|
|
249
|
+
if (!this.connected) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Stop scheduler
|
|
254
|
+
if (this.scheduler) {
|
|
255
|
+
this.scheduler.stop();
|
|
256
|
+
this.scheduler = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Stop all consumers
|
|
260
|
+
for (const entry of this.subscriptions) {
|
|
261
|
+
entry.running = false;
|
|
262
|
+
}
|
|
263
|
+
this.subscriptions = [];
|
|
264
|
+
|
|
265
|
+
await this.client.disconnect();
|
|
266
|
+
this.connected = false;
|
|
267
|
+
this.js = null;
|
|
268
|
+
this.jsm = null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
isConnected(): boolean {
|
|
272
|
+
return this.connected && this.client.isConnected();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Publishing
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
|
|
280
|
+
this.ensureConnected();
|
|
281
|
+
|
|
282
|
+
const messageId = options?.messageId ?? this.generateMessageId();
|
|
283
|
+
const timestamp = Date.now();
|
|
284
|
+
|
|
285
|
+
const messageData = {
|
|
286
|
+
id: messageId,
|
|
287
|
+
pattern,
|
|
288
|
+
data,
|
|
289
|
+
timestamp,
|
|
290
|
+
metadata: options?.metadata ?? {},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const encoder = new TextEncoder();
|
|
294
|
+
|
|
295
|
+
// Convert OneBun subject to NATS subject (replace # with >)
|
|
296
|
+
const natsSubject = pattern.replace(/#/g, '>');
|
|
297
|
+
|
|
298
|
+
await this.js.publish(natsSubject, encoder.encode(JSON.stringify(messageData)));
|
|
299
|
+
|
|
300
|
+
return messageId;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async publishBatch<T>(
|
|
304
|
+
messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
|
|
305
|
+
): Promise<string[]> {
|
|
306
|
+
const ids: string[] = [];
|
|
307
|
+
|
|
308
|
+
for (const msg of messages) {
|
|
309
|
+
const id = await this.publish(msg.pattern, msg.data, msg.options);
|
|
310
|
+
ids.push(id);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return ids;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Subscribing
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
async subscribe<T>(
|
|
321
|
+
pattern: string,
|
|
322
|
+
handler: MessageHandler<T>,
|
|
323
|
+
options?: SubscribeOptions,
|
|
324
|
+
): Promise<Subscription> {
|
|
325
|
+
this.ensureConnected();
|
|
326
|
+
|
|
327
|
+
const jsModule = await getJetStreamModule();
|
|
328
|
+
|
|
329
|
+
// Create consumer name from group or generate one
|
|
330
|
+
const consumerName = options?.group ?? `consumer-${Date.now()}`;
|
|
331
|
+
|
|
332
|
+
// Convert pattern to filter subject (replace # with >)
|
|
333
|
+
const filterSubject = pattern.replace(/#/g, '>');
|
|
334
|
+
|
|
335
|
+
// Determine ack policy
|
|
336
|
+
const ackPolicy = options?.ackMode === 'manual'
|
|
337
|
+
? jsModule.AckPolicy.Explicit
|
|
338
|
+
: jsModule.AckPolicy.None;
|
|
339
|
+
|
|
340
|
+
// Create or get consumer
|
|
341
|
+
try {
|
|
342
|
+
await this.jsm.consumers.add(this.options.stream, {
|
|
343
|
+
durable_name: options?.group ? consumerName : undefined,
|
|
344
|
+
name: consumerName,
|
|
345
|
+
ack_policy: ackPolicy,
|
|
346
|
+
filter_subject: filterSubject,
|
|
347
|
+
max_ack_pending: options?.prefetch ?? 100,
|
|
348
|
+
// eslint-disable-next-line no-magic-numbers
|
|
349
|
+
ack_wait: this.options.consumerConfig?.ackWait ?? 30000000000, // 30s in nanoseconds
|
|
350
|
+
max_deliver: options?.retry?.attempts ?? this.options.consumerConfig?.maxDeliver ?? 3,
|
|
351
|
+
});
|
|
352
|
+
} catch {
|
|
353
|
+
// Consumer might already exist, try to get it
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const consumer = await this.js.consumers.get(this.options.stream, consumerName);
|
|
357
|
+
|
|
358
|
+
const entry: JetStreamSubscriptionEntry = {
|
|
359
|
+
pattern,
|
|
360
|
+
handler: handler as MessageHandler,
|
|
361
|
+
options,
|
|
362
|
+
matcher: createQueuePatternMatcher(pattern),
|
|
363
|
+
paused: false,
|
|
364
|
+
consumer,
|
|
365
|
+
running: true,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
this.subscriptions.push(entry);
|
|
369
|
+
|
|
370
|
+
// Start consuming messages
|
|
371
|
+
this.consumeMessages(entry);
|
|
372
|
+
|
|
373
|
+
const subscription = new JetStreamSubscription(entry, async () => {
|
|
374
|
+
entry.running = false;
|
|
375
|
+
const index = this.subscriptions.indexOf(entry);
|
|
376
|
+
if (index !== -1) {
|
|
377
|
+
this.subscriptions.splice(index, 1);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return subscription;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Scheduled Jobs
|
|
386
|
+
// ============================================================================
|
|
387
|
+
|
|
388
|
+
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
389
|
+
this.ensureConnected();
|
|
390
|
+
|
|
391
|
+
if (!this.scheduler) {
|
|
392
|
+
throw new Error('Scheduler not initialized');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (options.schedule.cron) {
|
|
396
|
+
this.scheduler.addCronJob(name, options.schedule.cron, options.pattern, () => options.data, {
|
|
397
|
+
metadata: options.metadata,
|
|
398
|
+
overlapStrategy: options.overlapStrategy,
|
|
399
|
+
});
|
|
400
|
+
} else if (options.schedule.every) {
|
|
401
|
+
this.scheduler.addIntervalJob(name, options.schedule.every, options.pattern, () => options.data, {
|
|
402
|
+
metadata: options.metadata,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async removeScheduledJob(name: string): Promise<boolean> {
|
|
408
|
+
if (!this.scheduler) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return this.scheduler.removeJob(name);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
416
|
+
if (!this.scheduler) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return this.scheduler.getJobs();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Features
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
supports(feature: QueueFeature): boolean {
|
|
428
|
+
switch (feature) {
|
|
429
|
+
case 'pattern-subscriptions':
|
|
430
|
+
case 'consumer-groups':
|
|
431
|
+
case 'scheduled-jobs':
|
|
432
|
+
case 'dead-letter-queue':
|
|
433
|
+
case 'retry':
|
|
434
|
+
return true;
|
|
435
|
+
case 'delayed-messages':
|
|
436
|
+
case 'priority':
|
|
437
|
+
return false;
|
|
438
|
+
default:
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Events
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
448
|
+
if (!this.eventHandlers.has(event)) {
|
|
449
|
+
this.eventHandlers.set(event, new Set());
|
|
450
|
+
}
|
|
451
|
+
this.eventHandlers.get(event)!.add(handler as (...args: unknown[]) => void);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
455
|
+
const handlers = this.eventHandlers.get(event);
|
|
456
|
+
if (handlers) {
|
|
457
|
+
handlers.delete(handler as (...args: unknown[]) => void);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Private Methods
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
private ensureConnected(): void {
|
|
466
|
+
if (!this.connected) {
|
|
467
|
+
throw new Error('JetStreamQueueAdapter not connected. Call connect() first.');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private generateMessageId(): string {
|
|
472
|
+
// eslint-disable-next-line no-magic-numbers
|
|
473
|
+
return `js-${++this.messageIdCounter}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private emit<E extends keyof QueueEvents>(event: E, ...args: unknown[]): void {
|
|
477
|
+
const handlers = this.eventHandlers.get(event);
|
|
478
|
+
if (handlers) {
|
|
479
|
+
for (const handler of handlers) {
|
|
480
|
+
try {
|
|
481
|
+
handler(...args);
|
|
482
|
+
} catch {
|
|
483
|
+
// Silently ignore event handler errors
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async ensureStream(): Promise<void> {
|
|
490
|
+
const config = this.options.streamConfig ?? {};
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
// Try to get existing stream
|
|
494
|
+
await this.jsm.streams.info(this.options.stream);
|
|
495
|
+
} catch {
|
|
496
|
+
// Stream doesn't exist, create it
|
|
497
|
+
await this.jsm.streams.add({
|
|
498
|
+
name: this.options.stream,
|
|
499
|
+
subjects: config.subjects ?? [`${this.options.stream}.>`],
|
|
500
|
+
retention: config.retention ?? 'limits',
|
|
501
|
+
max_msgs: config.maxMsgs,
|
|
502
|
+
max_bytes: config.maxBytes,
|
|
503
|
+
max_age: config.maxAge,
|
|
504
|
+
storage: config.storage ?? 'file',
|
|
505
|
+
num_replicas: config.replicas ?? 1,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private async consumeMessages(entry: JetStreamSubscriptionEntry): Promise<void> {
|
|
511
|
+
const decoder = new TextDecoder();
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const messages = await entry.consumer.consume({
|
|
515
|
+
max_messages: entry.options?.prefetch ?? 10,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
for await (const msg of messages) {
|
|
519
|
+
if (!entry.running || entry.paused) {
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const messageData = JSON.parse(decoder.decode(msg.data));
|
|
525
|
+
|
|
526
|
+
// Check if pattern matches
|
|
527
|
+
const match = entry.matcher(messageData.pattern || msg.subject);
|
|
528
|
+
if (!match.matched) {
|
|
529
|
+
// Ack and skip non-matching messages
|
|
530
|
+
if (entry.options?.ackMode !== 'manual') {
|
|
531
|
+
msg.ack();
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const message = new JetStreamMessage(
|
|
537
|
+
messageData.id || this.generateMessageId(),
|
|
538
|
+
messageData.pattern || msg.subject,
|
|
539
|
+
messageData.data,
|
|
540
|
+
messageData.timestamp || Date.now(),
|
|
541
|
+
messageData.metadata || {},
|
|
542
|
+
msg,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// Emit received event
|
|
546
|
+
this.emit('onMessageReceived', message);
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
await entry.handler(message);
|
|
550
|
+
|
|
551
|
+
// Auto-ack if not manual mode
|
|
552
|
+
if (entry.options?.ackMode !== 'manual') {
|
|
553
|
+
msg.ack();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Emit processed event
|
|
557
|
+
this.emit('onMessageProcessed', message);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
// Emit failed event
|
|
560
|
+
this.emit('onMessageFailed', message, error as Error);
|
|
561
|
+
|
|
562
|
+
// Auto-nack if not manual mode
|
|
563
|
+
if (entry.options?.ackMode !== 'manual') {
|
|
564
|
+
msg.nak();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} catch {
|
|
568
|
+
// Message parsing error - ack to prevent redelivery
|
|
569
|
+
msg.ack();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
// Consumer error - will be handled by NATS reconnection
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Restart consumption if still running
|
|
577
|
+
if (entry.running) {
|
|
578
|
+
setTimeout(() => this.consumeMessages(entry), 100);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Create a JetStream queue adapter
|
|
585
|
+
*/
|
|
586
|
+
export function createJetStreamQueueAdapter(options: JetStreamAdapterOptions): JetStreamQueueAdapter {
|
|
587
|
+
return new JetStreamQueueAdapter(options);
|
|
588
|
+
}
|