@cloudwerk/queue 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Squirrelsoft Dev Tools
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,723 @@
1
+ import { ZodType } from 'zod';
2
+
3
+ /**
4
+ * @cloudwerk/queue - Type Definitions
5
+ *
6
+ * Core types for queue producers and consumers.
7
+ */
8
+
9
+ /**
10
+ * Type that can be either a value or a Promise of that value.
11
+ * Used throughout the queue package for async-friendly APIs.
12
+ */
13
+ type Awaitable<T> = T | Promise<T>;
14
+ /**
15
+ * Represents a message received from a queue for processing.
16
+ *
17
+ * @typeParam T - The message body type
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * export default defineQueue<EmailMessage>({
22
+ * async process(message) {
23
+ * console.log(message.id) // Unique message ID
24
+ * console.log(message.body) // { to, subject, body }
25
+ * console.log(message.attempts) // Number of delivery attempts
26
+ *
27
+ * await sendEmail(message.body)
28
+ * message.ack() // Acknowledge successful processing
29
+ * }
30
+ * })
31
+ * ```
32
+ */
33
+ interface QueueMessage<T = unknown> {
34
+ /** Unique identifier for the message */
35
+ readonly id: string;
36
+ /** The message payload */
37
+ readonly body: T;
38
+ /** When the message was originally sent */
39
+ readonly timestamp: Date;
40
+ /** Number of delivery attempts for this message */
41
+ readonly attempts: number;
42
+ /**
43
+ * Acknowledge successful message processing.
44
+ * The message will be removed from the queue.
45
+ */
46
+ ack(): void;
47
+ /**
48
+ * Request retry of this message.
49
+ * The message will be requeued with optional delay.
50
+ *
51
+ * @param options - Retry options
52
+ * @param options.delaySeconds - Delay before retry (default: queue's retryDelay)
53
+ */
54
+ retry(options?: {
55
+ delaySeconds?: number;
56
+ }): void;
57
+ /**
58
+ * Mark this message as failed and send to dead letter queue.
59
+ * Only works if DLQ is configured for this queue.
60
+ *
61
+ * @param reason - Reason for sending to DLQ
62
+ */
63
+ deadLetter(reason?: string): void;
64
+ }
65
+ /**
66
+ * Message sent to a dead letter queue when processing fails.
67
+ *
68
+ * @typeParam T - The original message body type
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * export default defineQueue<DeadLetterMessage<EmailMessage>>({
73
+ * name: 'email-dlq',
74
+ * async process(message) {
75
+ * // Log failed message for manual inspection
76
+ * await logFailedMessage({
77
+ * originalQueue: message.body.originalQueue,
78
+ * originalMessage: message.body.originalMessage,
79
+ * error: message.body.error,
80
+ * attempts: message.body.attempts,
81
+ * })
82
+ * message.ack()
83
+ * }
84
+ * })
85
+ * ```
86
+ */
87
+ interface DeadLetterMessage<T = unknown> {
88
+ /** Name of the original queue that failed processing */
89
+ originalQueue: string;
90
+ /** The original message that failed */
91
+ originalMessage: T;
92
+ /** Error message from the last failure */
93
+ error: string;
94
+ /** Error stack trace (if available) */
95
+ stack?: string;
96
+ /** Number of processing attempts before DLQ */
97
+ attempts: number;
98
+ /** ISO timestamp when the message was sent to DLQ */
99
+ failedAt: string;
100
+ /** Original message ID */
101
+ originalMessageId: string;
102
+ }
103
+ /**
104
+ * Configuration for queue processing behavior.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * export default defineQueue({
109
+ * config: {
110
+ * batchSize: 10,
111
+ * maxRetries: 5,
112
+ * retryDelay: '2m',
113
+ * deadLetterQueue: 'my-dlq',
114
+ * batchTimeout: '30s',
115
+ * },
116
+ * async process(message) {
117
+ * // ...
118
+ * }
119
+ * })
120
+ * ```
121
+ */
122
+ interface QueueProcessingConfig {
123
+ /**
124
+ * Maximum number of messages to deliver in a batch.
125
+ * @default 10
126
+ */
127
+ batchSize?: number;
128
+ /**
129
+ * Maximum number of retry attempts before sending to DLQ.
130
+ * @default 3
131
+ */
132
+ maxRetries?: number;
133
+ /**
134
+ * Delay between retries. Supports duration strings like '1m', '30s', '1h'.
135
+ * @default '1m'
136
+ */
137
+ retryDelay?: string | number;
138
+ /**
139
+ * Dead letter queue name for failed messages.
140
+ * Messages exceeding maxRetries will be sent here.
141
+ */
142
+ deadLetterQueue?: string;
143
+ /**
144
+ * Maximum time to wait for a batch to fill.
145
+ * Supports duration strings like '5s', '30s'.
146
+ * @default '5s'
147
+ */
148
+ batchTimeout?: string | number;
149
+ }
150
+ /**
151
+ * Full configuration for defining a queue consumer.
152
+ *
153
+ * @typeParam T - The message body type
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * // Simple queue with single message processing
158
+ * export default defineQueue<EmailMessage>({
159
+ * async process(message) {
160
+ * await sendEmail(message.body)
161
+ * message.ack()
162
+ * }
163
+ * })
164
+ *
165
+ * // Queue with batch processing and configuration
166
+ * export default defineQueue<ImageJob>({
167
+ * config: {
168
+ * batchSize: 50,
169
+ * maxRetries: 5,
170
+ * deadLetterQueue: 'image-dlq',
171
+ * },
172
+ * async processBatch(messages) {
173
+ * const jobs = messages.map(m => m.body)
174
+ * await processImageBatch(jobs)
175
+ * messages.forEach(m => m.ack())
176
+ * }
177
+ * })
178
+ *
179
+ * // Queue with Zod schema validation
180
+ * export default defineQueue({
181
+ * schema: z.object({
182
+ * to: z.string().email(),
183
+ * subject: z.string(),
184
+ * body: z.string(),
185
+ * }),
186
+ * async process(message) {
187
+ * // message.body is validated and typed
188
+ * await sendEmail(message.body)
189
+ * message.ack()
190
+ * }
191
+ * })
192
+ * ```
193
+ */
194
+ interface QueueConfig<T = unknown> {
195
+ /**
196
+ * Optional queue name override.
197
+ * By default, the queue name is derived from the filename.
198
+ * - `app/queues/email.ts` -> `email`
199
+ * - `app/queues/image-processing.ts` -> `imageProcessing`
200
+ */
201
+ name?: string;
202
+ /**
203
+ * Optional Zod schema for runtime validation of message bodies.
204
+ * If provided, messages that fail validation will be rejected.
205
+ */
206
+ schema?: ZodType<T>;
207
+ /**
208
+ * Queue processing configuration.
209
+ */
210
+ config?: QueueProcessingConfig;
211
+ /**
212
+ * Process a single message.
213
+ * Called for each message in the batch unless processBatch is defined.
214
+ *
215
+ * @param message - The message to process
216
+ */
217
+ process?: (message: QueueMessage<T>) => Awaitable<void>;
218
+ /**
219
+ * Process a batch of messages.
220
+ * If defined, this is called instead of process() for the entire batch.
221
+ * More efficient for bulk operations.
222
+ *
223
+ * @param messages - Array of messages to process
224
+ */
225
+ processBatch?: (messages: QueueMessage<T>[]) => Awaitable<void>;
226
+ /**
227
+ * Error handler for processing failures.
228
+ * Called when process() or processBatch() throws an error.
229
+ *
230
+ * @param error - The error that occurred
231
+ * @param message - The message that was being processed (or first message in batch)
232
+ */
233
+ onError?: (error: Error, message: QueueMessage<T>) => Awaitable<void>;
234
+ }
235
+ /**
236
+ * A defined queue consumer, returned by defineQueue().
237
+ *
238
+ * @typeParam T - The message body type
239
+ */
240
+ interface QueueDefinition<T = unknown> {
241
+ /** Internal marker identifying this as a queue definition */
242
+ readonly __brand: 'cloudwerk-queue';
243
+ /** Queue name (derived from filename or explicitly set) */
244
+ readonly name: string | undefined;
245
+ /** Zod schema for validation (if provided) */
246
+ readonly schema: ZodType<T> | undefined;
247
+ /** Processing configuration */
248
+ readonly config: QueueProcessingConfig;
249
+ /** Single message processor */
250
+ readonly process: ((message: QueueMessage<T>) => Awaitable<void>) | undefined;
251
+ /** Batch message processor */
252
+ readonly processBatch: ((messages: QueueMessage<T>[]) => Awaitable<void>) | undefined;
253
+ /** Error handler */
254
+ readonly onError: ((error: Error, message: QueueMessage<T>) => Awaitable<void>) | undefined;
255
+ }
256
+ /**
257
+ * Options for sending messages to a queue.
258
+ */
259
+ interface SendOptions {
260
+ /**
261
+ * Delay delivery of this message by the specified number of seconds.
262
+ * The message will not be available for processing until after this delay.
263
+ */
264
+ delaySeconds?: number;
265
+ /**
266
+ * Content type of the message body.
267
+ * @default 'json'
268
+ */
269
+ contentType?: 'json' | 'text' | 'bytes' | 'v8';
270
+ }
271
+ /**
272
+ * A typed queue producer for sending messages.
273
+ *
274
+ * @typeParam T - The message body type
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * import { queues } from '@cloudwerk/core/bindings'
279
+ *
280
+ * // Send a single message
281
+ * await queues.email.send({
282
+ * to: 'user@example.com',
283
+ * subject: 'Welcome!',
284
+ * body: 'Thanks for signing up.',
285
+ * })
286
+ *
287
+ * // Send with delay
288
+ * await queues.email.send(message, { delaySeconds: 60 })
289
+ *
290
+ * // Send a batch
291
+ * await queues.notifications.sendBatch([
292
+ * { userId: '1', event: 'login' },
293
+ * { userId: '2', event: 'purchase' },
294
+ * ])
295
+ * ```
296
+ */
297
+ interface Queue<T = unknown> {
298
+ /**
299
+ * Send a single message to the queue.
300
+ *
301
+ * @param message - The message body to send
302
+ * @param options - Optional send options
303
+ */
304
+ send(message: T, options?: SendOptions): Promise<void>;
305
+ /**
306
+ * Send multiple messages to the queue in a single operation.
307
+ *
308
+ * @param messages - Array of message bodies to send
309
+ * @param options - Optional send options (applied to all messages)
310
+ */
311
+ sendBatch(messages: T[], options?: SendOptions): Promise<void>;
312
+ }
313
+ /**
314
+ * A scanned queue file from the app/queues/ directory.
315
+ */
316
+ interface ScannedQueue {
317
+ /** Relative path from app/queues/ (e.g., 'email.ts') */
318
+ relativePath: string;
319
+ /** Absolute filesystem path */
320
+ absolutePath: string;
321
+ /** File name without extension (e.g., 'email') */
322
+ name: string;
323
+ /** File extension (e.g., '.ts') */
324
+ extension: string;
325
+ }
326
+ /**
327
+ * Result of scanning the app/queues/ directory.
328
+ */
329
+ interface QueueScanResult {
330
+ /** All discovered queue files */
331
+ queues: ScannedQueue[];
332
+ }
333
+ /**
334
+ * A compiled queue entry in the manifest.
335
+ */
336
+ interface QueueEntry {
337
+ /** Queue name derived from filename (e.g., 'email', 'imageProcessing') */
338
+ name: string;
339
+ /** Binding name for wrangler.toml (e.g., 'EMAIL_QUEUE') */
340
+ bindingName: string;
341
+ /** Actual queue name in Cloudflare (e.g., 'cloudwerk-email') */
342
+ queueName: string;
343
+ /** Relative path to the queue definition file */
344
+ filePath: string;
345
+ /** Absolute path to the queue definition file */
346
+ absolutePath: string;
347
+ /** Processing configuration from the queue definition */
348
+ config: QueueProcessingConfig;
349
+ /** Whether processBatch is defined */
350
+ hasProcessBatch: boolean;
351
+ /** Whether onError handler is defined */
352
+ hasOnError: boolean;
353
+ /** TypeScript type name for the message body (if extractable) */
354
+ messageType?: string;
355
+ }
356
+ /**
357
+ * Validation error for a queue definition.
358
+ */
359
+ interface QueueValidationError$1 {
360
+ /** Queue file path */
361
+ file: string;
362
+ /** Error message */
363
+ message: string;
364
+ /** Error code for programmatic handling */
365
+ code: 'NO_HANDLER' | 'INVALID_CONFIG' | 'DUPLICATE_NAME' | 'INVALID_NAME';
366
+ }
367
+ /**
368
+ * Validation warning for a queue definition.
369
+ */
370
+ interface QueueValidationWarning {
371
+ /** Queue file path */
372
+ file: string;
373
+ /** Warning message */
374
+ message: string;
375
+ /** Warning code */
376
+ code: 'NO_DLQ' | 'LOW_RETRIES' | 'MISSING_ERROR_HANDLER';
377
+ }
378
+ /**
379
+ * Complete queue manifest generated during build.
380
+ */
381
+ interface QueueManifest {
382
+ /** All compiled queue entries */
383
+ queues: QueueEntry[];
384
+ /** Validation errors (queue won't be registered) */
385
+ errors: QueueValidationError$1[];
386
+ /** Validation warnings (queue will be registered with warning) */
387
+ warnings: QueueValidationWarning[];
388
+ /** When the manifest was generated */
389
+ generatedAt: Date;
390
+ /** Root directory of the app */
391
+ rootDir: string;
392
+ }
393
+
394
+ /**
395
+ * @cloudwerk/queue - Error Classes
396
+ *
397
+ * Custom error classes for queue processing.
398
+ */
399
+ /**
400
+ * Base error class for queue-related errors.
401
+ */
402
+ declare class QueueError extends Error {
403
+ /** Error code for programmatic handling */
404
+ readonly code: string;
405
+ constructor(code: string, message: string, options?: ErrorOptions);
406
+ }
407
+ /**
408
+ * Error thrown when a queue message fails schema validation.
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * export default defineQueue({
413
+ * schema: z.object({ email: z.string().email() }),
414
+ * async process(message) {
415
+ * // If message.body doesn't match schema, QueueValidationError is thrown
416
+ * }
417
+ * })
418
+ * ```
419
+ */
420
+ declare class QueueValidationError extends QueueError {
421
+ /** The validation errors from Zod */
422
+ readonly validationErrors: unknown[];
423
+ constructor(message: string, validationErrors: unknown[]);
424
+ }
425
+ /**
426
+ * Error thrown when queue message processing fails.
427
+ */
428
+ declare class QueueProcessingError extends QueueError {
429
+ /** The message ID that failed processing */
430
+ readonly messageId: string;
431
+ /** Number of attempts made */
432
+ readonly attempts: number;
433
+ constructor(message: string, messageId: string, attempts: number, options?: ErrorOptions);
434
+ }
435
+ /**
436
+ * Error thrown when max retries are exceeded.
437
+ */
438
+ declare class QueueMaxRetriesError extends QueueError {
439
+ /** The message ID that exceeded retries */
440
+ readonly messageId: string;
441
+ /** Maximum retries configured */
442
+ readonly maxRetries: number;
443
+ constructor(messageId: string, maxRetries: number);
444
+ }
445
+ /**
446
+ * Error thrown when queue configuration is invalid.
447
+ */
448
+ declare class QueueConfigError extends QueueError {
449
+ /** The configuration field that is invalid */
450
+ readonly field?: string;
451
+ constructor(message: string, field?: string);
452
+ }
453
+ /**
454
+ * Error thrown when no message handler is defined.
455
+ */
456
+ declare class QueueNoHandlerError extends QueueError {
457
+ constructor(queueName: string);
458
+ }
459
+ /**
460
+ * Error thrown when accessing a queue outside of request context.
461
+ */
462
+ declare class QueueContextError extends QueueError {
463
+ constructor();
464
+ }
465
+ /**
466
+ * Error thrown when a queue binding is not found.
467
+ */
468
+ declare class QueueNotFoundError extends QueueError {
469
+ /** The queue name that was not found */
470
+ readonly queueName: string;
471
+ /** Available queue names */
472
+ readonly availableQueues: string[];
473
+ constructor(queueName: string, availableQueues: string[]);
474
+ }
475
+
476
+ /**
477
+ * @cloudwerk/queue - defineQueue()
478
+ *
479
+ * Factory function for creating queue consumer definitions.
480
+ */
481
+
482
+ /**
483
+ * Parse a duration string into seconds.
484
+ *
485
+ * Supports formats like:
486
+ * - '30s' - 30 seconds
487
+ * - '5m' - 5 minutes
488
+ * - '1h' - 1 hour
489
+ * - 60 - number of seconds
490
+ *
491
+ * @param duration - Duration string or number
492
+ * @returns Duration in seconds
493
+ */
494
+ declare function parseDuration(duration: string | number): number;
495
+ /**
496
+ * Define a queue consumer.
497
+ *
498
+ * This function creates a queue definition that will be automatically
499
+ * discovered and registered by Cloudwerk during build.
500
+ *
501
+ * @typeParam T - The message body type
502
+ * @param config - Queue configuration
503
+ * @returns Queue definition
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * // app/queues/email.ts
508
+ * import { defineQueue } from '@cloudwerk/queue'
509
+ *
510
+ * interface EmailMessage {
511
+ * to: string
512
+ * subject: string
513
+ * body: string
514
+ * }
515
+ *
516
+ * export default defineQueue<EmailMessage>({
517
+ * async process(message) {
518
+ * await sendEmail(message.body)
519
+ * message.ack()
520
+ * }
521
+ * })
522
+ * ```
523
+ *
524
+ * @example
525
+ * ```typescript
526
+ * // With Zod schema validation
527
+ * import { defineQueue } from '@cloudwerk/queue'
528
+ * import { z } from 'zod'
529
+ *
530
+ * const EmailSchema = z.object({
531
+ * to: z.string().email(),
532
+ * subject: z.string().min(1),
533
+ * body: z.string(),
534
+ * })
535
+ *
536
+ * export default defineQueue({
537
+ * schema: EmailSchema,
538
+ * config: {
539
+ * maxRetries: 5,
540
+ * deadLetterQueue: 'email-dlq',
541
+ * },
542
+ * async process(message) {
543
+ * // message.body is validated and typed as { to: string, subject: string, body: string }
544
+ * await sendEmail(message.body)
545
+ * message.ack()
546
+ * },
547
+ * async onError(error, message) {
548
+ * console.error(`Failed to send email to ${message.body.to}:`, error)
549
+ * }
550
+ * })
551
+ * ```
552
+ *
553
+ * @example
554
+ * ```typescript
555
+ * // Batch processing
556
+ * import { defineQueue } from '@cloudwerk/queue'
557
+ *
558
+ * interface ImageJob {
559
+ * imageId: string
560
+ * operation: 'resize' | 'crop' | 'compress'
561
+ * params: Record<string, unknown>
562
+ * }
563
+ *
564
+ * export default defineQueue<ImageJob>({
565
+ * config: {
566
+ * batchSize: 50,
567
+ * batchTimeout: '30s',
568
+ * },
569
+ * async processBatch(messages) {
570
+ * // Process all images in parallel for efficiency
571
+ * await Promise.all(
572
+ * messages.map(async (msg) => {
573
+ * await processImage(msg.body)
574
+ * msg.ack()
575
+ * })
576
+ * )
577
+ * }
578
+ * })
579
+ * ```
580
+ */
581
+ declare function defineQueue<T = unknown>(config: QueueConfig<T>): QueueDefinition<T>;
582
+ /**
583
+ * Check if a value is a queue definition created by defineQueue().
584
+ *
585
+ * @param value - Value to check
586
+ * @returns true if value is a QueueDefinition
587
+ */
588
+ declare function isQueueDefinition(value: unknown): value is QueueDefinition;
589
+
590
+ /**
591
+ * @cloudwerk/queue - Dead Letter Queue Utilities
592
+ *
593
+ * Utilities for working with dead letter queues.
594
+ */
595
+
596
+ /**
597
+ * Create a dead letter message from a failed queue message.
598
+ *
599
+ * @param originalMessage - The original message that failed processing
600
+ * @param originalQueue - Name of the queue where the message failed
601
+ * @param error - The error that caused the failure
602
+ * @returns Dead letter message ready to be sent to DLQ
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * import { createDeadLetterMessage } from '@cloudwerk/queue'
607
+ *
608
+ * export default defineQueue<EmailMessage>({
609
+ * config: {
610
+ * deadLetterQueue: 'email-dlq',
611
+ * },
612
+ * async process(message) {
613
+ * try {
614
+ * await sendEmail(message.body)
615
+ * message.ack()
616
+ * } catch (error) {
617
+ * // Message will automatically go to DLQ after max retries
618
+ * // Or manually send to DLQ:
619
+ * message.deadLetter(error.message)
620
+ * }
621
+ * }
622
+ * })
623
+ * ```
624
+ */
625
+ declare function createDeadLetterMessage<T>(originalMessage: QueueMessage<T>, originalQueue: string, error: Error): DeadLetterMessage<T>;
626
+ /**
627
+ * Configuration for dead letter queue handling.
628
+ */
629
+ interface DLQConfig {
630
+ /**
631
+ * Name of the dead letter queue.
632
+ */
633
+ queueName: string;
634
+ /**
635
+ * Maximum retries before sending to DLQ.
636
+ * @default 3
637
+ */
638
+ maxRetries?: number;
639
+ /**
640
+ * Custom handler called before sending to DLQ.
641
+ * Return false to prevent the message from being sent to DLQ.
642
+ */
643
+ beforeDLQ?: <T>(message: QueueMessage<T>, error: Error) => Awaitable<boolean | void>;
644
+ /**
645
+ * Custom handler called after sending to DLQ.
646
+ */
647
+ afterDLQ?: <T>(dlqMessage: DeadLetterMessage<T>) => Awaitable<void>;
648
+ }
649
+ /**
650
+ * Create DLQ configuration with defaults.
651
+ *
652
+ * @param queueName - Name of the dead letter queue
653
+ * @param options - Optional configuration overrides
654
+ * @returns Complete DLQ configuration
655
+ */
656
+ declare function createDLQConfig(queueName: string, options?: Partial<Omit<DLQConfig, 'queueName'>>): DLQConfig;
657
+ /**
658
+ * Validate DLQ configuration.
659
+ *
660
+ * @param config - DLQ configuration to validate
661
+ * @returns Array of validation error messages (empty if valid)
662
+ */
663
+ declare function validateDLQConfig(config: DLQConfig): string[];
664
+ /**
665
+ * Check if a message should be sent to DLQ based on attempts.
666
+ *
667
+ * @param message - The queue message
668
+ * @param maxRetries - Maximum retry attempts (default: 3)
669
+ * @returns true if message has exceeded max retries
670
+ */
671
+ declare function shouldSendToDLQ<T>(message: QueueMessage<T>, maxRetries?: number): boolean;
672
+ /**
673
+ * Extract original message from a DLQ message.
674
+ *
675
+ * @param dlqMessage - Dead letter message
676
+ * @returns The original message body
677
+ */
678
+ declare function extractOriginalMessage<T>(dlqMessage: DeadLetterMessage<T>): T;
679
+ /**
680
+ * Check if a message is a dead letter message.
681
+ *
682
+ * @param message - Message to check
683
+ * @returns true if message is a DeadLetterMessage
684
+ */
685
+ declare function isDeadLetterMessage(message: unknown): message is DeadLetterMessage;
686
+ /**
687
+ * Create a DLQ consumer configuration.
688
+ *
689
+ * This is a convenience helper for defining DLQ consumers with appropriate
690
+ * defaults and error handling.
691
+ *
692
+ * @param handler - Handler for processing dead letter messages
693
+ * @returns Queue config for the DLQ consumer
694
+ *
695
+ * @example
696
+ * ```typescript
697
+ * // app/queues/email-dlq.ts
698
+ * import { defineDLQConsumer } from '@cloudwerk/queue'
699
+ *
700
+ * export default defineDLQConsumer<EmailMessage>(async (dlqMessage) => {
701
+ * // Log failed message for manual inspection
702
+ * await logFailedMessage({
703
+ * queue: dlqMessage.originalQueue,
704
+ * error: dlqMessage.error,
705
+ * attempts: dlqMessage.attempts,
706
+ * message: dlqMessage.originalMessage,
707
+ * })
708
+ *
709
+ * // Optionally alert on critical failures
710
+ * if (dlqMessage.attempts > 10) {
711
+ * await sendAlert(`Critical: ${dlqMessage.originalQueue} message failed`)
712
+ * }
713
+ * })
714
+ * ```
715
+ */
716
+ declare function defineDLQConsumer<T>(handler: (dlqMessage: DeadLetterMessage<T>) => Awaitable<void>): {
717
+ config: {
718
+ maxRetries: number;
719
+ };
720
+ process: (message: QueueMessage<DeadLetterMessage<T>>) => Promise<void>;
721
+ };
722
+
723
+ export { type Awaitable, type DLQConfig, type DeadLetterMessage, type Queue, type QueueConfig, QueueConfigError, QueueContextError, type QueueDefinition, type QueueEntry, QueueError, type QueueManifest, QueueMaxRetriesError, type QueueMessage, QueueNoHandlerError, QueueNotFoundError, type QueueProcessingConfig, QueueProcessingError, type QueueScanResult, QueueValidationError, type QueueValidationWarning, type ScannedQueue, type SendOptions, createDLQConfig, createDeadLetterMessage, defineDLQConsumer, defineQueue, extractOriginalMessage, isDeadLetterMessage, isQueueDefinition, parseDuration, shouldSendToDLQ, validateDLQConfig };
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ // src/errors.ts
2
+ var QueueError = class extends Error {
3
+ /** Error code for programmatic handling */
4
+ code;
5
+ constructor(code, message, options) {
6
+ super(message, options);
7
+ this.name = "QueueError";
8
+ this.code = code;
9
+ }
10
+ };
11
+ var QueueValidationError = class extends QueueError {
12
+ /** The validation errors from Zod */
13
+ validationErrors;
14
+ constructor(message, validationErrors) {
15
+ super("VALIDATION_ERROR", message);
16
+ this.name = "QueueValidationError";
17
+ this.validationErrors = validationErrors;
18
+ }
19
+ };
20
+ var QueueProcessingError = class extends QueueError {
21
+ /** The message ID that failed processing */
22
+ messageId;
23
+ /** Number of attempts made */
24
+ attempts;
25
+ constructor(message, messageId, attempts, options) {
26
+ super("PROCESSING_ERROR", message, options);
27
+ this.name = "QueueProcessingError";
28
+ this.messageId = messageId;
29
+ this.attempts = attempts;
30
+ }
31
+ };
32
+ var QueueMaxRetriesError = class extends QueueError {
33
+ /** The message ID that exceeded retries */
34
+ messageId;
35
+ /** Maximum retries configured */
36
+ maxRetries;
37
+ constructor(messageId, maxRetries) {
38
+ super(
39
+ "MAX_RETRIES_EXCEEDED",
40
+ `Message ${messageId} exceeded maximum retries (${maxRetries})`
41
+ );
42
+ this.name = "QueueMaxRetriesError";
43
+ this.messageId = messageId;
44
+ this.maxRetries = maxRetries;
45
+ }
46
+ };
47
+ var QueueConfigError = class extends QueueError {
48
+ /** The configuration field that is invalid */
49
+ field;
50
+ constructor(message, field) {
51
+ super("CONFIG_ERROR", message);
52
+ this.name = "QueueConfigError";
53
+ this.field = field;
54
+ }
55
+ };
56
+ var QueueNoHandlerError = class extends QueueError {
57
+ constructor(queueName) {
58
+ super(
59
+ "NO_HANDLER",
60
+ `Queue '${queueName}' must define either process() or processBatch()`
61
+ );
62
+ this.name = "QueueNoHandlerError";
63
+ }
64
+ };
65
+ var QueueContextError = class extends QueueError {
66
+ constructor() {
67
+ super(
68
+ "CONTEXT_ERROR",
69
+ "Queue accessed outside of request handler. Queues can only be accessed during request handling."
70
+ );
71
+ this.name = "QueueContextError";
72
+ }
73
+ };
74
+ var QueueNotFoundError = class extends QueueError {
75
+ /** The queue name that was not found */
76
+ queueName;
77
+ /** Available queue names */
78
+ availableQueues;
79
+ constructor(queueName, availableQueues) {
80
+ const available = availableQueues.length > 0 ? `Available queues: ${availableQueues.join(", ")}` : "No queues are configured";
81
+ super(
82
+ "QUEUE_NOT_FOUND",
83
+ `Queue '${queueName}' not found in environment. ${available}`
84
+ );
85
+ this.name = "QueueNotFoundError";
86
+ this.queueName = queueName;
87
+ this.availableQueues = availableQueues;
88
+ }
89
+ };
90
+
91
+ // src/define-queue.ts
92
+ var DEFAULT_CONFIG = {
93
+ batchSize: 10,
94
+ maxRetries: 3,
95
+ retryDelay: "1m",
96
+ deadLetterQueue: "",
97
+ batchTimeout: "5s"
98
+ };
99
+ function parseDuration(duration) {
100
+ if (typeof duration === "number") {
101
+ return duration;
102
+ }
103
+ const match = duration.match(/^(\d+)(s|m|h)$/);
104
+ if (!match) {
105
+ throw new QueueConfigError(
106
+ `Invalid duration format: '${duration}'. Expected format like '30s', '5m', or '1h'`,
107
+ "retryDelay"
108
+ );
109
+ }
110
+ const value = parseInt(match[1], 10);
111
+ const unit = match[2];
112
+ switch (unit) {
113
+ case "s":
114
+ return value;
115
+ case "m":
116
+ return value * 60;
117
+ case "h":
118
+ return value * 3600;
119
+ default:
120
+ throw new QueueConfigError(`Unknown duration unit: ${unit}`, "retryDelay");
121
+ }
122
+ }
123
+ function validateConfig(config) {
124
+ if (!config.process && !config.processBatch) {
125
+ throw new QueueNoHandlerError(config.name || "unknown");
126
+ }
127
+ if (config.config) {
128
+ const { batchSize, maxRetries, retryDelay, batchTimeout } = config.config;
129
+ if (batchSize !== void 0) {
130
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 100) {
131
+ throw new QueueConfigError(
132
+ "batchSize must be an integer between 1 and 100",
133
+ "batchSize"
134
+ );
135
+ }
136
+ }
137
+ if (maxRetries !== void 0) {
138
+ if (!Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 100) {
139
+ throw new QueueConfigError(
140
+ "maxRetries must be an integer between 0 and 100",
141
+ "maxRetries"
142
+ );
143
+ }
144
+ }
145
+ if (retryDelay !== void 0) {
146
+ parseDuration(retryDelay);
147
+ }
148
+ if (batchTimeout !== void 0) {
149
+ parseDuration(batchTimeout);
150
+ }
151
+ }
152
+ if (config.name !== void 0) {
153
+ if (typeof config.name !== "string" || config.name.length === 0) {
154
+ throw new QueueConfigError("name must be a non-empty string", "name");
155
+ }
156
+ if (!/^[a-z][a-z0-9-]*$/.test(config.name)) {
157
+ throw new QueueConfigError(
158
+ "name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens",
159
+ "name"
160
+ );
161
+ }
162
+ }
163
+ }
164
+ function defineQueue(config) {
165
+ validateConfig(config);
166
+ const mergedConfig = {
167
+ ...DEFAULT_CONFIG,
168
+ ...config.config
169
+ };
170
+ const definition = {
171
+ __brand: "cloudwerk-queue",
172
+ name: config.name,
173
+ schema: config.schema,
174
+ config: mergedConfig,
175
+ process: config.process,
176
+ processBatch: config.processBatch,
177
+ onError: config.onError
178
+ };
179
+ return definition;
180
+ }
181
+ function isQueueDefinition(value) {
182
+ return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "cloudwerk-queue";
183
+ }
184
+
185
+ // src/dlq.ts
186
+ function createDeadLetterMessage(originalMessage, originalQueue, error) {
187
+ return {
188
+ originalQueue,
189
+ originalMessage: originalMessage.body,
190
+ error: error.message,
191
+ stack: error.stack,
192
+ attempts: originalMessage.attempts,
193
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
194
+ originalMessageId: originalMessage.id
195
+ };
196
+ }
197
+ function createDLQConfig(queueName, options) {
198
+ return {
199
+ queueName,
200
+ maxRetries: options?.maxRetries ?? 3,
201
+ beforeDLQ: options?.beforeDLQ,
202
+ afterDLQ: options?.afterDLQ
203
+ };
204
+ }
205
+ function validateDLQConfig(config) {
206
+ const errors = [];
207
+ if (!config.queueName || config.queueName.trim() === "") {
208
+ errors.push("DLQ queue name is required");
209
+ }
210
+ if (config.queueName && !/^[a-z][a-z0-9-]*$/.test(config.queueName)) {
211
+ errors.push(
212
+ "DLQ queue name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens"
213
+ );
214
+ }
215
+ if (config.maxRetries !== void 0) {
216
+ if (!Number.isInteger(config.maxRetries) || config.maxRetries < 0) {
217
+ errors.push("maxRetries must be a non-negative integer");
218
+ }
219
+ }
220
+ return errors;
221
+ }
222
+ function shouldSendToDLQ(message, maxRetries = 3) {
223
+ return message.attempts > maxRetries;
224
+ }
225
+ function extractOriginalMessage(dlqMessage) {
226
+ return dlqMessage.originalMessage;
227
+ }
228
+ function isDeadLetterMessage(message) {
229
+ if (typeof message !== "object" || message === null) {
230
+ return false;
231
+ }
232
+ const dlm = message;
233
+ return typeof dlm.originalQueue === "string" && "originalMessage" in dlm && typeof dlm.error === "string" && typeof dlm.attempts === "number" && typeof dlm.failedAt === "string" && typeof dlm.originalMessageId === "string";
234
+ }
235
+ function defineDLQConsumer(handler) {
236
+ return {
237
+ config: {
238
+ // DLQ consumers typically shouldn't retry much
239
+ // to avoid infinite loops
240
+ maxRetries: 1
241
+ },
242
+ async process(message) {
243
+ await handler(message.body);
244
+ message.ack();
245
+ }
246
+ };
247
+ }
248
+ export {
249
+ QueueConfigError,
250
+ QueueContextError,
251
+ QueueError,
252
+ QueueMaxRetriesError,
253
+ QueueNoHandlerError,
254
+ QueueNotFoundError,
255
+ QueueProcessingError,
256
+ QueueValidationError,
257
+ createDLQConfig,
258
+ createDeadLetterMessage,
259
+ defineDLQConsumer,
260
+ defineQueue,
261
+ extractOriginalMessage,
262
+ isDeadLetterMessage,
263
+ isQueueDefinition,
264
+ parseDuration,
265
+ shouldSendToDLQ,
266
+ validateDLQConfig
267
+ };
268
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/define-queue.ts","../src/dlq.ts"],"sourcesContent":["/**\n * @cloudwerk/queue - Error Classes\n *\n * Custom error classes for queue processing.\n */\n\n// ============================================================================\n// Base Error\n// ============================================================================\n\n/**\n * Base error class for queue-related errors.\n */\nexport class QueueError extends Error {\n /** Error code for programmatic handling */\n readonly code: string\n\n constructor(code: string, message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = 'QueueError'\n this.code = code\n }\n}\n\n// ============================================================================\n// Validation Errors\n// ============================================================================\n\n/**\n * Error thrown when a queue message fails schema validation.\n *\n * @example\n * ```typescript\n * export default defineQueue({\n * schema: z.object({ email: z.string().email() }),\n * async process(message) {\n * // If message.body doesn't match schema, QueueValidationError is thrown\n * }\n * })\n * ```\n */\nexport class QueueValidationError extends QueueError {\n /** The validation errors from Zod */\n readonly validationErrors: unknown[]\n\n constructor(message: string, validationErrors: unknown[]) {\n super('VALIDATION_ERROR', message)\n this.name = 'QueueValidationError'\n this.validationErrors = validationErrors\n }\n}\n\n// ============================================================================\n// Processing Errors\n// ============================================================================\n\n/**\n * Error thrown when queue message processing fails.\n */\nexport class QueueProcessingError extends QueueError {\n /** The message ID that failed processing */\n readonly messageId: string\n\n /** Number of attempts made */\n readonly attempts: number\n\n constructor(\n message: string,\n messageId: string,\n attempts: number,\n options?: ErrorOptions\n ) {\n super('PROCESSING_ERROR', message, options)\n this.name = 'QueueProcessingError'\n this.messageId = messageId\n this.attempts = attempts\n }\n}\n\n/**\n * Error thrown when max retries are exceeded.\n */\nexport class QueueMaxRetriesError extends QueueError {\n /** The message ID that exceeded retries */\n readonly messageId: string\n\n /** Maximum retries configured */\n readonly maxRetries: number\n\n constructor(messageId: string, maxRetries: number) {\n super(\n 'MAX_RETRIES_EXCEEDED',\n `Message ${messageId} exceeded maximum retries (${maxRetries})`\n )\n this.name = 'QueueMaxRetriesError'\n this.messageId = messageId\n this.maxRetries = maxRetries\n }\n}\n\n// ============================================================================\n// Configuration Errors\n// ============================================================================\n\n/**\n * Error thrown when queue configuration is invalid.\n */\nexport class QueueConfigError extends QueueError {\n /** The configuration field that is invalid */\n readonly field?: string\n\n constructor(message: string, field?: string) {\n super('CONFIG_ERROR', message)\n this.name = 'QueueConfigError'\n this.field = field\n }\n}\n\n/**\n * Error thrown when no message handler is defined.\n */\nexport class QueueNoHandlerError extends QueueError {\n constructor(queueName: string) {\n super(\n 'NO_HANDLER',\n `Queue '${queueName}' must define either process() or processBatch()`\n )\n this.name = 'QueueNoHandlerError'\n }\n}\n\n// ============================================================================\n// Runtime Errors\n// ============================================================================\n\n/**\n * Error thrown when accessing a queue outside of request context.\n */\nexport class QueueContextError extends QueueError {\n constructor() {\n super(\n 'CONTEXT_ERROR',\n 'Queue accessed outside of request handler. Queues can only be accessed during request handling.'\n )\n this.name = 'QueueContextError'\n }\n}\n\n/**\n * Error thrown when a queue binding is not found.\n */\nexport class QueueNotFoundError extends QueueError {\n /** The queue name that was not found */\n readonly queueName: string\n\n /** Available queue names */\n readonly availableQueues: string[]\n\n constructor(queueName: string, availableQueues: string[]) {\n const available =\n availableQueues.length > 0\n ? `Available queues: ${availableQueues.join(', ')}`\n : 'No queues are configured'\n\n super(\n 'QUEUE_NOT_FOUND',\n `Queue '${queueName}' not found in environment. ${available}`\n )\n this.name = 'QueueNotFoundError'\n this.queueName = queueName\n this.availableQueues = availableQueues\n }\n}\n","/**\n * @cloudwerk/queue - defineQueue()\n *\n * Factory function for creating queue consumer definitions.\n */\n\nimport type {\n QueueConfig,\n QueueDefinition,\n QueueProcessingConfig,\n} from './types.js'\nimport { QueueConfigError, QueueNoHandlerError } from './errors.js'\n\n// ============================================================================\n// Default Configuration\n// ============================================================================\n\nconst DEFAULT_CONFIG: Required<QueueProcessingConfig> = {\n batchSize: 10,\n maxRetries: 3,\n retryDelay: '1m',\n deadLetterQueue: '',\n batchTimeout: '5s',\n}\n\n// ============================================================================\n// Validation\n// ============================================================================\n\n/**\n * Parse a duration string into seconds.\n *\n * Supports formats like:\n * - '30s' - 30 seconds\n * - '5m' - 5 minutes\n * - '1h' - 1 hour\n * - 60 - number of seconds\n *\n * @param duration - Duration string or number\n * @returns Duration in seconds\n */\nexport function parseDuration(duration: string | number): number {\n if (typeof duration === 'number') {\n return duration\n }\n\n const match = duration.match(/^(\\d+)(s|m|h)$/)\n if (!match) {\n throw new QueueConfigError(\n `Invalid duration format: '${duration}'. Expected format like '30s', '5m', or '1h'`,\n 'retryDelay'\n )\n }\n\n const value = parseInt(match[1], 10)\n const unit = match[2]\n\n switch (unit) {\n case 's':\n return value\n case 'm':\n return value * 60\n case 'h':\n return value * 3600\n default:\n throw new QueueConfigError(`Unknown duration unit: ${unit}`, 'retryDelay')\n }\n}\n\n/**\n * Validate queue configuration.\n *\n * @param config - Queue configuration to validate\n * @throws QueueConfigError if configuration is invalid\n * @throws QueueNoHandlerError if no handler is defined\n */\nfunction validateConfig<T>(config: QueueConfig<T>): void {\n // Must have either process or processBatch\n if (!config.process && !config.processBatch) {\n throw new QueueNoHandlerError(config.name || 'unknown')\n }\n\n // Validate processing config\n if (config.config) {\n const { batchSize, maxRetries, retryDelay, batchTimeout } = config.config\n\n if (batchSize !== undefined) {\n if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 100) {\n throw new QueueConfigError(\n 'batchSize must be an integer between 1 and 100',\n 'batchSize'\n )\n }\n }\n\n if (maxRetries !== undefined) {\n if (!Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 100) {\n throw new QueueConfigError(\n 'maxRetries must be an integer between 0 and 100',\n 'maxRetries'\n )\n }\n }\n\n if (retryDelay !== undefined) {\n // This will throw if invalid\n parseDuration(retryDelay)\n }\n\n if (batchTimeout !== undefined) {\n // This will throw if invalid\n parseDuration(batchTimeout)\n }\n }\n\n // Validate name if provided\n if (config.name !== undefined) {\n if (typeof config.name !== 'string' || config.name.length === 0) {\n throw new QueueConfigError('name must be a non-empty string', 'name')\n }\n\n // Queue names should be lowercase alphanumeric with hyphens\n if (!/^[a-z][a-z0-9-]*$/.test(config.name)) {\n throw new QueueConfigError(\n 'name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens',\n 'name'\n )\n }\n }\n}\n\n// ============================================================================\n// defineQueue()\n// ============================================================================\n\n/**\n * Define a queue consumer.\n *\n * This function creates a queue definition that will be automatically\n * discovered and registered by Cloudwerk during build.\n *\n * @typeParam T - The message body type\n * @param config - Queue configuration\n * @returns Queue definition\n *\n * @example\n * ```typescript\n * // app/queues/email.ts\n * import { defineQueue } from '@cloudwerk/queue'\n *\n * interface EmailMessage {\n * to: string\n * subject: string\n * body: string\n * }\n *\n * export default defineQueue<EmailMessage>({\n * async process(message) {\n * await sendEmail(message.body)\n * message.ack()\n * }\n * })\n * ```\n *\n * @example\n * ```typescript\n * // With Zod schema validation\n * import { defineQueue } from '@cloudwerk/queue'\n * import { z } from 'zod'\n *\n * const EmailSchema = z.object({\n * to: z.string().email(),\n * subject: z.string().min(1),\n * body: z.string(),\n * })\n *\n * export default defineQueue({\n * schema: EmailSchema,\n * config: {\n * maxRetries: 5,\n * deadLetterQueue: 'email-dlq',\n * },\n * async process(message) {\n * // message.body is validated and typed as { to: string, subject: string, body: string }\n * await sendEmail(message.body)\n * message.ack()\n * },\n * async onError(error, message) {\n * console.error(`Failed to send email to ${message.body.to}:`, error)\n * }\n * })\n * ```\n *\n * @example\n * ```typescript\n * // Batch processing\n * import { defineQueue } from '@cloudwerk/queue'\n *\n * interface ImageJob {\n * imageId: string\n * operation: 'resize' | 'crop' | 'compress'\n * params: Record<string, unknown>\n * }\n *\n * export default defineQueue<ImageJob>({\n * config: {\n * batchSize: 50,\n * batchTimeout: '30s',\n * },\n * async processBatch(messages) {\n * // Process all images in parallel for efficiency\n * await Promise.all(\n * messages.map(async (msg) => {\n * await processImage(msg.body)\n * msg.ack()\n * })\n * )\n * }\n * })\n * ```\n */\nexport function defineQueue<T = unknown>(\n config: QueueConfig<T>\n): QueueDefinition<T> {\n // Validate configuration\n validateConfig(config)\n\n // Merge with defaults\n const mergedConfig: QueueProcessingConfig = {\n ...DEFAULT_CONFIG,\n ...config.config,\n }\n\n // Create the definition object\n const definition: QueueDefinition<T> = {\n __brand: 'cloudwerk-queue',\n name: config.name,\n schema: config.schema,\n config: mergedConfig,\n process: config.process,\n processBatch: config.processBatch,\n onError: config.onError,\n }\n\n return definition\n}\n\n/**\n * Check if a value is a queue definition created by defineQueue().\n *\n * @param value - Value to check\n * @returns true if value is a QueueDefinition\n */\nexport function isQueueDefinition(value: unknown): value is QueueDefinition {\n return (\n typeof value === 'object' &&\n value !== null &&\n '__brand' in value &&\n (value as QueueDefinition).__brand === 'cloudwerk-queue'\n )\n}\n","/**\n * @cloudwerk/queue - Dead Letter Queue Utilities\n *\n * Utilities for working with dead letter queues.\n */\n\nimport type { DeadLetterMessage, QueueMessage, Awaitable } from './types.js'\n\n// ============================================================================\n// DLQ Message Creation\n// ============================================================================\n\n/**\n * Create a dead letter message from a failed queue message.\n *\n * @param originalMessage - The original message that failed processing\n * @param originalQueue - Name of the queue where the message failed\n * @param error - The error that caused the failure\n * @returns Dead letter message ready to be sent to DLQ\n *\n * @example\n * ```typescript\n * import { createDeadLetterMessage } from '@cloudwerk/queue'\n *\n * export default defineQueue<EmailMessage>({\n * config: {\n * deadLetterQueue: 'email-dlq',\n * },\n * async process(message) {\n * try {\n * await sendEmail(message.body)\n * message.ack()\n * } catch (error) {\n * // Message will automatically go to DLQ after max retries\n * // Or manually send to DLQ:\n * message.deadLetter(error.message)\n * }\n * }\n * })\n * ```\n */\nexport function createDeadLetterMessage<T>(\n originalMessage: QueueMessage<T>,\n originalQueue: string,\n error: Error\n): DeadLetterMessage<T> {\n return {\n originalQueue,\n originalMessage: originalMessage.body,\n error: error.message,\n stack: error.stack,\n attempts: originalMessage.attempts,\n failedAt: new Date().toISOString(),\n originalMessageId: originalMessage.id,\n }\n}\n\n// ============================================================================\n// DLQ Configuration\n// ============================================================================\n\n/**\n * Configuration for dead letter queue handling.\n */\nexport interface DLQConfig {\n /**\n * Name of the dead letter queue.\n */\n queueName: string\n\n /**\n * Maximum retries before sending to DLQ.\n * @default 3\n */\n maxRetries?: number\n\n /**\n * Custom handler called before sending to DLQ.\n * Return false to prevent the message from being sent to DLQ.\n */\n beforeDLQ?: <T>(\n message: QueueMessage<T>,\n error: Error\n ) => Awaitable<boolean | void>\n\n /**\n * Custom handler called after sending to DLQ.\n */\n afterDLQ?: <T>(\n dlqMessage: DeadLetterMessage<T>\n ) => Awaitable<void>\n}\n\n/**\n * Create DLQ configuration with defaults.\n *\n * @param queueName - Name of the dead letter queue\n * @param options - Optional configuration overrides\n * @returns Complete DLQ configuration\n */\nexport function createDLQConfig(\n queueName: string,\n options?: Partial<Omit<DLQConfig, 'queueName'>>\n): DLQConfig {\n return {\n queueName,\n maxRetries: options?.maxRetries ?? 3,\n beforeDLQ: options?.beforeDLQ,\n afterDLQ: options?.afterDLQ,\n }\n}\n\n// ============================================================================\n// DLQ Validation\n// ============================================================================\n\n/**\n * Validate DLQ configuration.\n *\n * @param config - DLQ configuration to validate\n * @returns Array of validation error messages (empty if valid)\n */\nexport function validateDLQConfig(config: DLQConfig): string[] {\n const errors: string[] = []\n\n if (!config.queueName || config.queueName.trim() === '') {\n errors.push('DLQ queue name is required')\n }\n\n if (config.queueName && !/^[a-z][a-z0-9-]*$/.test(config.queueName)) {\n errors.push(\n 'DLQ queue name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens'\n )\n }\n\n if (config.maxRetries !== undefined) {\n if (!Number.isInteger(config.maxRetries) || config.maxRetries < 0) {\n errors.push('maxRetries must be a non-negative integer')\n }\n }\n\n return errors\n}\n\n// ============================================================================\n// DLQ Processing Helpers\n// ============================================================================\n\n/**\n * Check if a message should be sent to DLQ based on attempts.\n *\n * @param message - The queue message\n * @param maxRetries - Maximum retry attempts (default: 3)\n * @returns true if message has exceeded max retries\n */\nexport function shouldSendToDLQ<T>(\n message: QueueMessage<T>,\n maxRetries: number = 3\n): boolean {\n return message.attempts > maxRetries\n}\n\n/**\n * Extract original message from a DLQ message.\n *\n * @param dlqMessage - Dead letter message\n * @returns The original message body\n */\nexport function extractOriginalMessage<T>(\n dlqMessage: DeadLetterMessage<T>\n): T {\n return dlqMessage.originalMessage\n}\n\n/**\n * Check if a message is a dead letter message.\n *\n * @param message - Message to check\n * @returns true if message is a DeadLetterMessage\n */\nexport function isDeadLetterMessage(\n message: unknown\n): message is DeadLetterMessage {\n if (typeof message !== 'object' || message === null) {\n return false\n }\n\n const dlm = message as Record<string, unknown>\n return (\n typeof dlm.originalQueue === 'string' &&\n 'originalMessage' in dlm &&\n typeof dlm.error === 'string' &&\n typeof dlm.attempts === 'number' &&\n typeof dlm.failedAt === 'string' &&\n typeof dlm.originalMessageId === 'string'\n )\n}\n\n// ============================================================================\n// DLQ Queue Definition Helper\n// ============================================================================\n\n/**\n * Create a DLQ consumer configuration.\n *\n * This is a convenience helper for defining DLQ consumers with appropriate\n * defaults and error handling.\n *\n * @param handler - Handler for processing dead letter messages\n * @returns Queue config for the DLQ consumer\n *\n * @example\n * ```typescript\n * // app/queues/email-dlq.ts\n * import { defineDLQConsumer } from '@cloudwerk/queue'\n *\n * export default defineDLQConsumer<EmailMessage>(async (dlqMessage) => {\n * // Log failed message for manual inspection\n * await logFailedMessage({\n * queue: dlqMessage.originalQueue,\n * error: dlqMessage.error,\n * attempts: dlqMessage.attempts,\n * message: dlqMessage.originalMessage,\n * })\n *\n * // Optionally alert on critical failures\n * if (dlqMessage.attempts > 10) {\n * await sendAlert(`Critical: ${dlqMessage.originalQueue} message failed`)\n * }\n * })\n * ```\n */\nexport function defineDLQConsumer<T>(\n handler: (dlqMessage: DeadLetterMessage<T>) => Awaitable<void>\n): {\n config: { maxRetries: number }\n process: (message: QueueMessage<DeadLetterMessage<T>>) => Promise<void>\n} {\n return {\n config: {\n // DLQ consumers typically shouldn't retry much\n // to avoid infinite loops\n maxRetries: 1,\n },\n async process(message: QueueMessage<DeadLetterMessage<T>>) {\n await handler(message.body)\n message.ack()\n },\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@cloudwerk/queue",
3
+ "version": "0.0.1",
4
+ "description": "Queue producers and consumers for Cloudwerk",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/squirrelsoft-dev/cloudwerk.git",
8
+ "directory": "packages/queue"
9
+ },
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@cloudwerk/core": "0.12.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.4.0",
25
+ "vitest": "^1.0.0",
26
+ "tsup": "^8.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "typescript": "^5.0.0",
30
+ "zod": "^3.0.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "zod": {
34
+ "optional": true
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "test": "vitest --run",
40
+ "test:watch": "vitest"
41
+ }
42
+ }