@onebun/core 0.1.2 → 0.1.3

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 (79) hide show
  1. package/package.json +1 -1
  2. package/src/{application.test.ts → application/application.test.ts} +6 -5
  3. package/src/{application.ts → application/application.ts} +131 -12
  4. package/src/application/index.ts +9 -0
  5. package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
  6. package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
  7. package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
  8. package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
  9. package/src/{decorators.ts → decorators/decorators.ts} +3 -2
  10. package/src/decorators/index.ts +15 -0
  11. package/src/index.ts +47 -134
  12. package/src/module/index.ts +12 -0
  13. package/src/{module.test.ts → module/module.test.ts} +3 -2
  14. package/src/{module.ts → module/module.ts} +6 -5
  15. package/src/queue/adapters/index.ts +8 -0
  16. package/src/queue/adapters/memory.adapter.test.ts +405 -0
  17. package/src/queue/adapters/memory.adapter.ts +509 -0
  18. package/src/queue/adapters/redis.adapter.ts +673 -0
  19. package/src/queue/cron-expression.test.ts +145 -0
  20. package/src/queue/cron-expression.ts +115 -0
  21. package/src/queue/cron-parser.test.ts +185 -0
  22. package/src/queue/cron-parser.ts +287 -0
  23. package/src/queue/decorators.test.ts +292 -0
  24. package/src/queue/decorators.ts +493 -0
  25. package/src/queue/docs-examples.test.ts +449 -0
  26. package/src/queue/guards.test.ts +309 -0
  27. package/src/queue/guards.ts +307 -0
  28. package/src/queue/index.ts +118 -0
  29. package/src/queue/pattern-matcher.test.ts +191 -0
  30. package/src/queue/pattern-matcher.ts +252 -0
  31. package/src/queue/queue.service.ts +421 -0
  32. package/src/queue/scheduler.test.ts +235 -0
  33. package/src/queue/scheduler.ts +379 -0
  34. package/src/queue/types.ts +502 -0
  35. package/src/redis/index.ts +8 -0
  36. package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
  37. package/src/service-client/index.ts +10 -0
  38. package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
  39. package/src/{service-client.ts → service-client/service-client.ts} +1 -1
  40. package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
  41. package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
  42. package/src/testing/index.ts +7 -0
  43. package/src/types.ts +34 -5
  44. package/src/websocket/index.ts +50 -0
  45. package/src/{ws-decorators.ts → websocket/ws-decorators.ts} +2 -1
  46. package/src/{ws-integration.test.ts → websocket/ws-integration.test.ts} +3 -2
  47. package/src/{ws-service-definition.ts → websocket/ws-service-definition.ts} +2 -1
  48. package/src/{ws-storage-redis.ts → websocket/ws-storage-redis.ts} +1 -1
  49. /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
  50. /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
  51. /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
  52. /package/src/{config.service.ts → module/config.service.ts} +0 -0
  53. /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
  54. /package/src/{controller.ts → module/controller.ts} +0 -0
  55. /package/src/{service.test.ts → module/service.test.ts} +0 -0
  56. /package/src/{service.ts → module/service.ts} +0 -0
  57. /package/src/{redis-client.ts → redis/redis-client.ts} +0 -0
  58. /package/src/{shared-redis.ts → redis/shared-redis.ts} +0 -0
  59. /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
  60. /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
  61. /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
  62. /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
  63. /package/src/{ws-base-gateway.test.ts → websocket/ws-base-gateway.test.ts} +0 -0
  64. /package/src/{ws-base-gateway.ts → websocket/ws-base-gateway.ts} +0 -0
  65. /package/src/{ws-client.test.ts → websocket/ws-client.test.ts} +0 -0
  66. /package/src/{ws-client.ts → websocket/ws-client.ts} +0 -0
  67. /package/src/{ws-client.types.ts → websocket/ws-client.types.ts} +0 -0
  68. /package/src/{ws-decorators.test.ts → websocket/ws-decorators.test.ts} +0 -0
  69. /package/src/{ws-guards.test.ts → websocket/ws-guards.test.ts} +0 -0
  70. /package/src/{ws-guards.ts → websocket/ws-guards.ts} +0 -0
  71. /package/src/{ws-handler.ts → websocket/ws-handler.ts} +0 -0
  72. /package/src/{ws-pattern-matcher.test.ts → websocket/ws-pattern-matcher.test.ts} +0 -0
  73. /package/src/{ws-pattern-matcher.ts → websocket/ws-pattern-matcher.ts} +0 -0
  74. /package/src/{ws-socketio-protocol.test.ts → websocket/ws-socketio-protocol.test.ts} +0 -0
  75. /package/src/{ws-socketio-protocol.ts → websocket/ws-socketio-protocol.ts} +0 -0
  76. /package/src/{ws-storage-memory.test.ts → websocket/ws-storage-memory.test.ts} +0 -0
  77. /package/src/{ws-storage-memory.ts → websocket/ws-storage-memory.ts} +0 -0
  78. /package/src/{ws-storage.ts → websocket/ws-storage.ts} +0 -0
  79. /package/src/{ws.types.ts → websocket/ws.types.ts} +0 -0
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Redis Queue Adapter
3
+ *
4
+ * Queue adapter using Redis for distributed message queuing.
5
+ * Uses SharedRedisProvider by default (like cache and websocket).
6
+ *
7
+ * Features:
8
+ * - Pub/Sub for real-time message delivery
9
+ * - Lists for persistent queues
10
+ * - Sorted sets for delayed messages and priority queues
11
+ * - Consumer groups for load balancing
12
+ */
13
+
14
+ import type {
15
+ QueueAdapter,
16
+ QueueAdapterType,
17
+ QueueFeature,
18
+ QueueEvents,
19
+ Message,
20
+ MessageMetadata,
21
+ PublishOptions,
22
+ SubscribeOptions,
23
+ Subscription,
24
+ ScheduledJobOptions,
25
+ ScheduledJobInfo,
26
+ MessageHandler,
27
+ } from '../types';
28
+
29
+ import { RedisClient } from '../../redis/redis-client';
30
+ import { SharedRedisProvider } from '../../redis/shared-redis';
31
+ import { createQueuePatternMatcher, type QueuePatternMatch } from '../pattern-matcher';
32
+ import { QueueScheduler } from '../scheduler';
33
+
34
+ // ============================================================================
35
+ // Types
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Redis queue adapter options
40
+ */
41
+ export interface RedisQueueOptions {
42
+ /** Use shared Redis client (default: true) */
43
+ useSharedClient?: boolean;
44
+
45
+ /** Redis URL (only used if useSharedClient is false) */
46
+ url?: string;
47
+
48
+ /** Key prefix for all queue operations */
49
+ keyPrefix?: string;
50
+
51
+ /** Poll interval for delayed messages (ms, default: 100) */
52
+ pollInterval?: number;
53
+ }
54
+
55
+ interface RedisSubscriptionEntry {
56
+ pattern: string;
57
+ handler: MessageHandler;
58
+ options?: SubscribeOptions;
59
+ matcher: (topic: string) => QueuePatternMatch;
60
+ paused: boolean;
61
+ consumerGroup?: string;
62
+ }
63
+
64
+ // ============================================================================
65
+ // Redis Message Implementation
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Redis message implementation
70
+ */
71
+ class RedisMessage<T> implements Message<T> {
72
+ id: string;
73
+ pattern: string;
74
+ data: T;
75
+ timestamp: number;
76
+ redelivered: boolean;
77
+ metadata: MessageMetadata;
78
+ attempt?: number;
79
+ maxAttempts?: number;
80
+
81
+ private acked = false;
82
+ private nacked = false;
83
+ private onAck?: () => Promise<void>;
84
+ private onNack?: (requeue: boolean) => Promise<void>;
85
+
86
+ constructor(
87
+ id: string,
88
+ pattern: string,
89
+ data: T,
90
+ timestamp: number,
91
+ metadata: MessageMetadata,
92
+ options?: {
93
+ redelivered?: boolean;
94
+ attempt?: number;
95
+ maxAttempts?: number;
96
+ onAck?: () => Promise<void>;
97
+ onNack?: (requeue: boolean) => Promise<void>;
98
+ },
99
+ ) {
100
+ this.id = id;
101
+ this.pattern = pattern;
102
+ this.data = data;
103
+ this.timestamp = timestamp;
104
+ this.metadata = metadata;
105
+ this.redelivered = options?.redelivered ?? false;
106
+ this.attempt = options?.attempt;
107
+ this.maxAttempts = options?.maxAttempts;
108
+ this.onAck = options?.onAck;
109
+ this.onNack = options?.onNack;
110
+ }
111
+
112
+ async ack(): Promise<void> {
113
+ if (this.acked || this.nacked) {
114
+ return;
115
+ }
116
+ this.acked = true;
117
+ await this.onAck?.();
118
+ }
119
+
120
+ async nack(requeue = false): Promise<void> {
121
+ if (this.acked || this.nacked) {
122
+ return;
123
+ }
124
+ this.nacked = true;
125
+ await this.onNack?.(requeue);
126
+ }
127
+ }
128
+
129
+ // ============================================================================
130
+ // Redis Subscription Implementation
131
+ // ============================================================================
132
+
133
+ class RedisSubscription implements Subscription {
134
+ private active = true;
135
+
136
+ constructor(
137
+ private readonly entry: RedisSubscriptionEntry,
138
+ private readonly onUnsubscribe: () => Promise<void>,
139
+ ) {}
140
+
141
+ async unsubscribe(): Promise<void> {
142
+ this.active = false;
143
+ await this.onUnsubscribe();
144
+ }
145
+
146
+ pause(): void {
147
+ this.entry.paused = true;
148
+ }
149
+
150
+ resume(): void {
151
+ this.entry.paused = false;
152
+ }
153
+
154
+ get pattern(): string {
155
+ return this.entry.pattern;
156
+ }
157
+
158
+ get isActive(): boolean {
159
+ return this.active && !this.entry.paused;
160
+ }
161
+ }
162
+
163
+ // ============================================================================
164
+ // Redis Queue Adapter
165
+ // ============================================================================
166
+
167
+ /**
168
+ * Redis Queue Adapter
169
+ *
170
+ * Uses SharedRedisProvider by default for connection sharing
171
+ * with cache and websocket modules.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * // Using shared client (default)
176
+ * const adapter = new RedisQueueAdapter();
177
+ * await adapter.connect();
178
+ *
179
+ * // Using custom connection
180
+ * const adapter = new RedisQueueAdapter({
181
+ * useSharedClient: false,
182
+ * url: 'redis://localhost:6379',
183
+ * keyPrefix: 'myapp:queue:',
184
+ * });
185
+ * await adapter.connect();
186
+ * ```
187
+ */
188
+ export class RedisQueueAdapter implements QueueAdapter {
189
+ readonly name = 'redis';
190
+ readonly type: QueueAdapterType = 'redis';
191
+
192
+ private client: RedisClient | null = null;
193
+ private ownsClient = false;
194
+ private connected = false;
195
+ private scheduler: QueueScheduler | null = null;
196
+ private subscriptions: RedisSubscriptionEntry[] = [];
197
+ private messageIdCounter = 0;
198
+ private running = false;
199
+ private delayedInterval?: ReturnType<typeof setInterval>;
200
+
201
+ // Key prefixes
202
+ private keys = {
203
+ delayed: 'queue:delayed',
204
+ priority: 'queue:priority',
205
+ queue: (pattern: string) => `queue:q:${pattern}`,
206
+ channel: (pattern: string) => `queue:ch:${pattern}`,
207
+ processing: (group: string) => `queue:processing:${group}`,
208
+ deadLetter: (pattern: string) => `queue:dlq:${pattern}`,
209
+ };
210
+
211
+ // Event handlers
212
+ private eventHandlers: Map<keyof QueueEvents, Set<(...args: unknown[]) => void>> = new Map();
213
+
214
+ private readonly options: Required<RedisQueueOptions>;
215
+
216
+ constructor(options: RedisQueueOptions = {}) {
217
+ this.options = {
218
+ useSharedClient: options.useSharedClient ?? true,
219
+ url: options.url ?? '',
220
+ keyPrefix: options.keyPrefix ?? 'onebun:',
221
+ pollInterval: options.pollInterval ?? 100,
222
+ };
223
+
224
+ // Update key prefixes
225
+ const prefix = this.options.keyPrefix;
226
+ this.keys = {
227
+ delayed: `${prefix}queue:delayed`,
228
+ priority: `${prefix}queue:priority`,
229
+ queue: (pattern: string) => `${prefix}queue:q:${pattern}`,
230
+ channel: (pattern: string) => `${prefix}queue:ch:${pattern}`,
231
+ processing: (group: string) => `${prefix}queue:processing:${group}`,
232
+ deadLetter: (pattern: string) => `${prefix}queue:dlq:${pattern}`,
233
+ };
234
+ }
235
+
236
+ // ============================================================================
237
+ // Lifecycle
238
+ // ============================================================================
239
+
240
+ async connect(): Promise<void> {
241
+ if (this.connected) {
242
+ return;
243
+ }
244
+
245
+ try {
246
+ if (this.options.useSharedClient) {
247
+ // Use shared client (default)
248
+ this.client = await SharedRedisProvider.getClient();
249
+ this.ownsClient = false;
250
+ } else {
251
+ // Create own client
252
+ if (!this.options.url) {
253
+ throw new Error('Redis URL is required when not using shared client');
254
+ }
255
+ this.client = SharedRedisProvider.createClient({
256
+ url: this.options.url,
257
+ keyPrefix: '', // We handle prefix ourselves
258
+ });
259
+ await this.client.connect();
260
+ this.ownsClient = true;
261
+ }
262
+
263
+ this.connected = true;
264
+ this.running = true;
265
+ this.scheduler = new QueueScheduler(this);
266
+
267
+ // Start delayed message processor
268
+ this.startDelayedProcessor();
269
+
270
+ // Emit ready event
271
+ this.emit('onReady');
272
+ } catch (error) {
273
+ this.emit('onError', error as Error);
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ async disconnect(): Promise<void> {
279
+ if (!this.connected) {
280
+ return;
281
+ }
282
+
283
+ this.running = false;
284
+
285
+ // Stop scheduler
286
+ if (this.scheduler) {
287
+ this.scheduler.stop();
288
+ this.scheduler = null;
289
+ }
290
+
291
+ // Stop delayed processor
292
+ if (this.delayedInterval) {
293
+ clearInterval(this.delayedInterval);
294
+ this.delayedInterval = undefined;
295
+ }
296
+
297
+ // Clear subscriptions
298
+ this.subscriptions = [];
299
+
300
+ // Disconnect client only if we own it
301
+ if (this.ownsClient && this.client) {
302
+ await this.client.disconnect();
303
+ }
304
+
305
+ this.client = null;
306
+ this.connected = false;
307
+ }
308
+
309
+ isConnected(): boolean {
310
+ return this.connected && (this.client?.isConnected() ?? false);
311
+ }
312
+
313
+ // ============================================================================
314
+ // Publishing
315
+ // ============================================================================
316
+
317
+ async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
318
+ this.ensureConnected();
319
+
320
+ const messageId = options?.messageId ?? this.generateMessageId();
321
+ const timestamp = Date.now();
322
+
323
+ const messageData = {
324
+ id: messageId,
325
+ pattern,
326
+ data,
327
+ timestamp,
328
+ metadata: options?.metadata ?? {},
329
+ };
330
+
331
+ const serialized = JSON.stringify(messageData);
332
+
333
+ if (options?.delay && options.delay > 0) {
334
+ // Delayed message - use sorted set
335
+ const score = timestamp + options.delay;
336
+ await this.client!.raw('ZADD', this.keys.delayed, String(score), serialized);
337
+ } else if (options?.priority && options.priority > 0) {
338
+ // Priority message - use sorted set with negative priority (higher = more important)
339
+ await this.client!.raw('ZADD', this.keys.priority, String(-options.priority), serialized);
340
+ } else {
341
+ // Normal message - push to list and publish to channel
342
+ await this.client!.raw('RPUSH', this.keys.queue(pattern), serialized);
343
+ await this.client!.publish(this.keys.channel(pattern), serialized);
344
+ }
345
+
346
+ return messageId;
347
+ }
348
+
349
+ async publishBatch<T>(
350
+ messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
351
+ ): Promise<string[]> {
352
+ const ids: string[] = [];
353
+
354
+ for (const msg of messages) {
355
+ const id = await this.publish(msg.pattern, msg.data, msg.options);
356
+ ids.push(id);
357
+ }
358
+
359
+ return ids;
360
+ }
361
+
362
+ // ============================================================================
363
+ // Subscribing
364
+ // ============================================================================
365
+
366
+ async subscribe<T>(
367
+ pattern: string,
368
+ handler: MessageHandler<T>,
369
+ options?: SubscribeOptions,
370
+ ): Promise<Subscription> {
371
+ this.ensureConnected();
372
+
373
+ const entry: RedisSubscriptionEntry = {
374
+ pattern,
375
+ handler: handler as MessageHandler,
376
+ options,
377
+ matcher: createQueuePatternMatcher(pattern),
378
+ paused: false,
379
+ consumerGroup: options?.group,
380
+ };
381
+
382
+ this.subscriptions.push(entry);
383
+
384
+ // Subscribe to Redis pub/sub channel
385
+ await this.client!.subscribe(this.keys.channel(pattern), (message) => {
386
+ if (entry.paused) {
387
+ return;
388
+ }
389
+
390
+ try {
391
+ const parsed = JSON.parse(message);
392
+ this.processMessage(entry, parsed);
393
+ } catch {
394
+ // Silently ignore message parsing errors
395
+ }
396
+ });
397
+
398
+ // Also start polling the queue for messages that were published before subscription
399
+ this.startQueuePolling(entry);
400
+
401
+ const subscription = new RedisSubscription(entry, async () => {
402
+ const index = this.subscriptions.indexOf(entry);
403
+ if (index !== -1) {
404
+ this.subscriptions.splice(index, 1);
405
+ }
406
+ await this.client!.unsubscribe(this.keys.channel(pattern));
407
+ });
408
+
409
+ return subscription;
410
+ }
411
+
412
+ // ============================================================================
413
+ // Scheduled Jobs
414
+ // ============================================================================
415
+
416
+ async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
417
+ this.ensureConnected();
418
+
419
+ if (!this.scheduler) {
420
+ throw new Error('Scheduler not initialized');
421
+ }
422
+
423
+ if (options.schedule.cron) {
424
+ this.scheduler.addCronJob(name, options.schedule.cron, options.pattern, () => options.data, {
425
+ metadata: options.metadata,
426
+ overlapStrategy: options.overlapStrategy,
427
+ });
428
+ } else if (options.schedule.every) {
429
+ this.scheduler.addIntervalJob(name, options.schedule.every, options.pattern, () => options.data, {
430
+ metadata: options.metadata,
431
+ });
432
+ }
433
+ }
434
+
435
+ async removeScheduledJob(name: string): Promise<boolean> {
436
+ if (!this.scheduler) {
437
+ return false;
438
+ }
439
+
440
+ return this.scheduler.removeJob(name);
441
+ }
442
+
443
+ async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
444
+ if (!this.scheduler) {
445
+ return [];
446
+ }
447
+
448
+ return this.scheduler.getJobs();
449
+ }
450
+
451
+ // ============================================================================
452
+ // Features
453
+ // ============================================================================
454
+
455
+ supports(_feature: QueueFeature): boolean {
456
+ // Redis supports all features
457
+ return true;
458
+ }
459
+
460
+ // ============================================================================
461
+ // Events
462
+ // ============================================================================
463
+
464
+ on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
465
+ if (!this.eventHandlers.has(event)) {
466
+ this.eventHandlers.set(event, new Set());
467
+ }
468
+ this.eventHandlers.get(event)!.add(handler as (...args: unknown[]) => void);
469
+ }
470
+
471
+ off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
472
+ const handlers = this.eventHandlers.get(event);
473
+ if (handlers) {
474
+ handlers.delete(handler as (...args: unknown[]) => void);
475
+ }
476
+ }
477
+
478
+ // ============================================================================
479
+ // Private Methods
480
+ // ============================================================================
481
+
482
+ private ensureConnected(): void {
483
+ if (!this.connected || !this.client) {
484
+ throw new Error('RedisQueueAdapter not connected. Call connect() first.');
485
+ }
486
+ }
487
+
488
+ private generateMessageId(): string {
489
+ // eslint-disable-next-line no-magic-numbers
490
+ return `msg-${++this.messageIdCounter}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
491
+ }
492
+
493
+ private emit<E extends keyof QueueEvents>(event: E, ...args: unknown[]): void {
494
+ const handlers = this.eventHandlers.get(event);
495
+ if (handlers) {
496
+ for (const handler of handlers) {
497
+ try {
498
+ handler(...args);
499
+ } catch {
500
+ // Silently ignore event handler errors
501
+ }
502
+ }
503
+ }
504
+ }
505
+
506
+ private async processMessage(
507
+ entry: RedisSubscriptionEntry,
508
+ messageData: {
509
+ id: string;
510
+ pattern: string;
511
+ data: unknown;
512
+ timestamp: number;
513
+ metadata?: MessageMetadata;
514
+ },
515
+ ): Promise<void> {
516
+ // Check if pattern matches
517
+ const match = entry.matcher(messageData.pattern);
518
+ if (!match.matched) {
519
+ return;
520
+ }
521
+
522
+ const message = new RedisMessage(
523
+ messageData.id,
524
+ messageData.pattern,
525
+ messageData.data,
526
+ messageData.timestamp,
527
+ messageData.metadata ?? {},
528
+ {
529
+ onAck: async () => {
530
+ // Remove from processing set if using consumer groups
531
+ if (entry.consumerGroup) {
532
+ await this.client!.srem(
533
+ this.keys.processing(entry.consumerGroup),
534
+ JSON.stringify(messageData),
535
+ );
536
+ }
537
+ },
538
+ onNack: async (requeue) => {
539
+ if (requeue) {
540
+ // Re-queue the message
541
+ await this.client!.raw(
542
+ 'LPUSH',
543
+ this.keys.queue(messageData.pattern),
544
+ JSON.stringify(messageData),
545
+ );
546
+ } else if (entry.options?.deadLetter) {
547
+ // Move to dead letter queue
548
+ await this.client!.raw(
549
+ 'RPUSH',
550
+ this.keys.deadLetter(messageData.pattern),
551
+ JSON.stringify(messageData),
552
+ );
553
+ }
554
+ },
555
+ },
556
+ );
557
+
558
+ // Emit received event
559
+ this.emit('onMessageReceived', message);
560
+
561
+ try {
562
+ await entry.handler(message);
563
+
564
+ // Auto-ack if not manual mode
565
+ if (entry.options?.ackMode !== 'manual') {
566
+ await message.ack();
567
+ }
568
+
569
+ // Emit processed event
570
+ this.emit('onMessageProcessed', message);
571
+ } catch (error) {
572
+ // Emit failed event
573
+ this.emit('onMessageFailed', message, error as Error);
574
+
575
+ // Auto-nack if not manual mode
576
+ if (entry.options?.ackMode !== 'manual') {
577
+ await message.nack(false);
578
+ }
579
+ }
580
+ }
581
+
582
+ private startQueuePolling(entry: RedisSubscriptionEntry): void {
583
+ // Poll the queue for existing messages
584
+ const poll = async () => {
585
+ if (!this.running || entry.paused) {
586
+ return;
587
+ }
588
+
589
+ try {
590
+ // Get message from queue (LPOP for FIFO)
591
+ const result = await this.client!.raw<string | null>(
592
+ 'LPOP',
593
+ this.keys.queue(entry.pattern),
594
+ );
595
+
596
+ if (result) {
597
+ const messageData = JSON.parse(result);
598
+ await this.processMessage(entry, messageData);
599
+ }
600
+ } catch {
601
+ // Silently ignore polling errors
602
+ }
603
+
604
+ // Continue polling
605
+ if (this.running && this.subscriptions.includes(entry)) {
606
+ setTimeout(poll, this.options.pollInterval);
607
+ }
608
+ };
609
+
610
+ poll();
611
+ }
612
+
613
+ private startDelayedProcessor(): void {
614
+ this.delayedInterval = setInterval(async () => {
615
+ if (!this.running || !this.client) {
616
+ return;
617
+ }
618
+
619
+ try {
620
+ const now = Date.now();
621
+
622
+ // Get delayed messages that are ready
623
+ const messages = await this.client.raw<string[]>(
624
+ 'ZRANGEBYSCORE',
625
+ this.keys.delayed,
626
+ '0',
627
+ String(now),
628
+ 'LIMIT',
629
+ '0',
630
+ '100',
631
+ );
632
+
633
+ if (messages && messages.length > 0) {
634
+ for (const msg of messages) {
635
+ // Remove from delayed set
636
+ await this.client.raw('ZREM', this.keys.delayed, msg);
637
+
638
+ // Parse and publish
639
+ const messageData = JSON.parse(msg);
640
+ await this.client.raw('RPUSH', this.keys.queue(messageData.pattern), msg);
641
+ await this.client.publish(this.keys.channel(messageData.pattern), msg);
642
+ }
643
+ }
644
+
645
+ // Also process priority queue
646
+ const priorityMessages = await this.client.raw<string[]>(
647
+ 'ZPOPMIN',
648
+ this.keys.priority,
649
+ '10',
650
+ );
651
+
652
+ if (priorityMessages && priorityMessages.length > 0) {
653
+ // ZPOPMIN returns [member, score, member, score, ...]
654
+ for (let i = 0; i < priorityMessages.length; i += 2) {
655
+ const msg = priorityMessages[i];
656
+ const messageData = JSON.parse(msg);
657
+ await this.client.raw('RPUSH', this.keys.queue(messageData.pattern), msg);
658
+ await this.client.publish(this.keys.channel(messageData.pattern), msg);
659
+ }
660
+ }
661
+ } catch {
662
+ // Silently ignore delayed message processing errors
663
+ }
664
+ }, this.options.pollInterval);
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Create a Redis queue adapter
670
+ */
671
+ export function createRedisQueueAdapter(options?: RedisQueueOptions): RedisQueueAdapter {
672
+ return new RedisQueueAdapter(options);
673
+ }