@message-queue-toolkit/sqs 22.2.1 → 22.4.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 ADDED
@@ -0,0 +1,2146 @@
1
+ # @message-queue-toolkit/sqs
2
+
3
+ AWS SQS (Simple Queue Service) implementation for the message-queue-toolkit. Provides a robust, type-safe abstraction for publishing and consuming messages from both standard and FIFO SQS queues.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Features](#features)
9
+ - [Core Concepts](#core-concepts)
10
+ - [Quick Start](#quick-start)
11
+ - [Standard Queue Publisher](#standard-queue-publisher)
12
+ - [Standard Queue Consumer](#standard-queue-consumer)
13
+ - [FIFO Queue Publisher](#fifo-queue-publisher)
14
+ - [FIFO Queue Consumer](#fifo-queue-consumer)
15
+ - [Configuration](#configuration)
16
+ - [Queue Creation](#queue-creation)
17
+ - [Queue Locator](#queue-locator)
18
+ - [Publisher Options](#publisher-options)
19
+ - [Consumer Options](#consumer-options)
20
+ - [Advanced Features](#advanced-features)
21
+ - [Custom Message Field Names](#custom-message-field-names)
22
+ - [Dead Letter Queue (DLQ)](#dead-letter-queue-dlq)
23
+ - [Message Retry Logic](#message-retry-logic)
24
+ - [Message Deduplication](#message-deduplication)
25
+ - [Payload Offloading](#payload-offloading)
26
+ - [Message Handlers](#message-handlers)
27
+ - [Pre-handlers and Barriers](#pre-handlers-and-barriers)
28
+ - [Handler Spies](#handler-spies)
29
+ - [FIFO Queues](#fifo-queues)
30
+ - [FIFO Queue Requirements](#fifo-queue-requirements)
31
+ - [Message Ordering](#message-ordering)
32
+ - [Message Groups](#message-groups)
33
+ - [FIFO-Specific Configuration](#fifo-specific-configuration)
34
+ - [Policy Configuration](#policy-configuration)
35
+ - [Testing](#testing)
36
+ - [API Reference](#api-reference)
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm install @message-queue-toolkit/sqs @message-queue-toolkit/core
42
+ ```
43
+
44
+ **Peer Dependencies:**
45
+ - `@aws-sdk/client-sqs` - AWS SDK for SQS
46
+ - `zod` - Schema validation
47
+
48
+ ## Features
49
+
50
+ - ✅ **Type-safe message handling** with Zod schema validation
51
+ - ✅ **Standard and FIFO queue support**
52
+ - ✅ **Automatic retry logic** with exponential backoff
53
+ - ✅ **Dead Letter Queue (DLQ)** support
54
+ - ✅ **Message deduplication** (publisher and consumer level)
55
+ - ✅ **Payload offloading** for large messages (S3 integration)
56
+ - ✅ **Concurrent consumers** for high throughput
57
+ - ✅ **Policy-based access control**
58
+ - ✅ **Handler spies** for testing
59
+ - ✅ **Pre-handlers and barriers** for complex message processing
60
+ - ✅ **Automatic queue creation** with validation
61
+
62
+ ## Core Concepts
63
+
64
+ ### Publishers
65
+
66
+ Publishers send messages to SQS queues. They handle:
67
+ - Message validation against Zod schemas
68
+ - Automatic serialization
69
+ - Optional deduplication (preventing duplicate sends)
70
+ - Optional payload offloading (for messages > 256KB)
71
+ - FIFO-specific concerns (MessageGroupId, MessageDeduplicationId)
72
+
73
+ ### Consumers
74
+
75
+ Consumers receive and process messages from SQS queues. They handle:
76
+ - Message deserialization and validation
77
+ - Routing to appropriate handlers based on message type
78
+ - Automatic retry with exponential backoff
79
+ - Dead letter queue integration
80
+ - Optional deduplication (preventing duplicate processing)
81
+ - FIFO ordering guarantees
82
+
83
+ ### Message Schemas
84
+
85
+ Messages are validated using Zod schemas. Each message must have:
86
+ - A unique message type field (discriminator for routing) - configurable via `messageTypeField` (required)
87
+ - A message ID field (for tracking and deduplication) - configurable via `messageIdField` (default: `'id'`)
88
+ - A timestamp field (added automatically if missing) - configurable via `messageTimestampField` (default: `'timestamp'`)
89
+
90
+ **Note:** All field names are configurable, allowing you to adapt the library to your existing message schemas without modification.
91
+
92
+ ## Quick Start
93
+
94
+ ### Standard Queue Publisher
95
+
96
+ ```typescript
97
+ import { AbstractSqsPublisher } from '@message-queue-toolkit/sqs'
98
+ import { SQSClient } from '@aws-sdk/client-sqs'
99
+ import z from 'zod'
100
+
101
+ // Define your message schemas
102
+ const UserCreatedSchema = z.object({
103
+ id: z.string(),
104
+ messageType: z.literal('user.created'),
105
+ userId: z.string(),
106
+ email: z.string().email(),
107
+ timestamp: z.string().optional(),
108
+ })
109
+
110
+ const UserUpdatedSchema = z.object({
111
+ id: z.string(),
112
+ messageType: z.literal('user.updated'),
113
+ userId: z.string(),
114
+ changes: z.record(z.unknown()),
115
+ timestamp: z.string().optional(),
116
+ })
117
+
118
+ type UserCreated = z.infer<typeof UserCreatedSchema>
119
+ type UserUpdated = z.infer<typeof UserUpdatedSchema>
120
+ type SupportedMessages = UserCreated | UserUpdated
121
+
122
+ // Create your publisher class
123
+ class UserEventsPublisher extends AbstractSqsPublisher<SupportedMessages> {
124
+ constructor(sqsClient: SQSClient) {
125
+ super(
126
+ {
127
+ sqsClient,
128
+ logger: console,
129
+ errorReporter: { report: (error) => console.error(error) },
130
+ },
131
+ {
132
+ messageSchemas: [UserCreatedSchema, UserUpdatedSchema],
133
+ messageTypeField: 'messageType',
134
+ creationConfig: {
135
+ queue: {
136
+ QueueName: 'user-events-queue',
137
+ },
138
+ },
139
+ deletionConfig: {
140
+ deleteIfExists: false,
141
+ },
142
+ }
143
+ )
144
+ }
145
+ }
146
+
147
+ // Use the publisher
148
+ const sqsClient = new SQSClient({ region: 'us-east-1' })
149
+ const publisher = new UserEventsPublisher(sqsClient)
150
+
151
+ await publisher.init()
152
+
153
+ await publisher.publish({
154
+ id: '123',
155
+ messageType: 'user.created',
156
+ userId: 'user-456',
157
+ email: 'user@example.com',
158
+ })
159
+
160
+ await publisher.close()
161
+ ```
162
+
163
+ ### Standard Queue Consumer
164
+
165
+ ```typescript
166
+ import { AbstractSqsConsumer } from '@message-queue-toolkit/sqs'
167
+ import { MessageHandlerConfigBuilder } from '@message-queue-toolkit/core'
168
+ import type { Either } from '@lokalise/node-core'
169
+
170
+ type ExecutionContext = {
171
+ userService: UserService
172
+ }
173
+
174
+ class UserEventsConsumer extends AbstractSqsConsumer<
175
+ SupportedMessages,
176
+ ExecutionContext
177
+ > {
178
+ constructor(sqsClient: SQSClient, userService: UserService) {
179
+ super(
180
+ {
181
+ sqsClient,
182
+ logger: console,
183
+ errorReporter: { report: (error) => console.error(error) },
184
+ consumerErrorResolver: {
185
+ resolveError: () => ({ resolve: 'retryLater' as const }),
186
+ },
187
+ transactionObservabilityManager: {
188
+ start: () => {},
189
+ stop: () => {},
190
+ },
191
+ },
192
+ {
193
+ messageTypeField: 'messageType',
194
+ handlers: new MessageHandlerConfigBuilder<SupportedMessages, ExecutionContext>()
195
+ .addConfig(
196
+ UserCreatedSchema,
197
+ async (message, context): Promise<Either<'retryLater', 'success'>> => {
198
+ await context.userService.createUser(message.userId, message.email)
199
+ return { result: 'success' }
200
+ }
201
+ )
202
+ .addConfig(
203
+ UserUpdatedSchema,
204
+ async (message, context): Promise<Either<'retryLater', 'success'>> => {
205
+ await context.userService.updateUser(message.userId, message.changes)
206
+ return { result: 'success' }
207
+ }
208
+ )
209
+ .build(),
210
+ creationConfig: {
211
+ queue: {
212
+ QueueName: 'user-events-queue',
213
+ },
214
+ },
215
+ deletionConfig: {
216
+ deleteIfExists: false,
217
+ },
218
+ },
219
+ { userService } // Execution context
220
+ )
221
+ }
222
+ }
223
+
224
+ // Use the consumer
225
+ const consumer = new UserEventsConsumer(sqsClient, userService)
226
+ await consumer.start() // Initializes and starts consuming
227
+
228
+ // Later, to stop
229
+ await consumer.close()
230
+ ```
231
+
232
+ ### FIFO Queue Publisher
233
+
234
+ FIFO (First-In-First-Out) queues guarantee that messages are processed exactly once and in order within a message group.
235
+
236
+ ```typescript
237
+ class UserEventsFifoPublisher extends AbstractSqsPublisher<SupportedMessages> {
238
+ constructor(sqsClient: SQSClient) {
239
+ super(
240
+ {
241
+ sqsClient,
242
+ logger: console,
243
+ errorReporter: { report: (error) => console.error(error) },
244
+ },
245
+ {
246
+ messageSchemas: [UserCreatedSchema, UserUpdatedSchema],
247
+ messageTypeField: 'messageType',
248
+ fifoQueue: true, // Enable FIFO mode
249
+
250
+ // Option 1: Use a field from the message as MessageGroupId
251
+ messageGroupIdField: 'userId',
252
+
253
+ // Option 2: Use a default MessageGroupId for all messages
254
+ // defaultMessageGroupId: 'user-events',
255
+
256
+ creationConfig: {
257
+ queue: {
258
+ QueueName: 'user-events-queue.fifo', // Must end with .fifo
259
+ Attributes: {
260
+ FifoQueue: 'true',
261
+ ContentBasedDeduplication: 'false', // or 'true' for automatic deduplication
262
+ },
263
+ },
264
+ },
265
+ }
266
+ )
267
+ }
268
+ }
269
+
270
+ // Publishing to FIFO queue
271
+ const fifoPublisher = new UserEventsFifoPublisher(sqsClient)
272
+ await fifoPublisher.init()
273
+
274
+ // Messages with the same userId will be processed in order
275
+ await fifoPublisher.publish({
276
+ id: '123',
277
+ messageType: 'user.created',
278
+ userId: 'user-456', // Used as MessageGroupId
279
+ email: 'user@example.com',
280
+ })
281
+
282
+ await fifoPublisher.publish({
283
+ id: '124',
284
+ messageType: 'user.updated',
285
+ userId: 'user-456', // Same group - processed after the first message
286
+ changes: { name: 'John Doe' },
287
+ })
288
+
289
+ // You can also explicitly provide MessageGroupId
290
+ await fifoPublisher.publish(
291
+ {
292
+ id: '125',
293
+ messageType: 'user.created',
294
+ userId: 'user-789',
295
+ email: 'other@example.com',
296
+ },
297
+ {
298
+ MessageGroupId: 'custom-group-id',
299
+ MessageDeduplicationId: 'unique-dedup-id', // Optional
300
+ }
301
+ )
302
+ ```
303
+
304
+ ### FIFO Queue Consumer
305
+
306
+ ```typescript
307
+ class UserEventsFifoConsumer extends AbstractSqsConsumer<
308
+ SupportedMessages,
309
+ ExecutionContext
310
+ > {
311
+ constructor(sqsClient: SQSClient, userService: UserService) {
312
+ super(
313
+ {
314
+ sqsClient,
315
+ logger: console,
316
+ errorReporter: { report: (error) => console.error(error) },
317
+ consumerErrorResolver: {
318
+ resolveError: () => ({ resolve: 'retryLater' as const }),
319
+ },
320
+ transactionObservabilityManager: {
321
+ start: () => {},
322
+ stop: () => {},
323
+ },
324
+ },
325
+ {
326
+ fifoQueue: true, // Enable FIFO mode
327
+ messageTypeField: 'messageType',
328
+ handlers: new MessageHandlerConfigBuilder<SupportedMessages, ExecutionContext>()
329
+ .addConfig(UserCreatedSchema, handleUserCreated)
330
+ .addConfig(UserUpdatedSchema, handleUserUpdated)
331
+ .build(),
332
+ creationConfig: {
333
+ queue: {
334
+ QueueName: 'user-events-queue.fifo',
335
+ Attributes: {
336
+ FifoQueue: 'true',
337
+ ContentBasedDeduplication: 'false',
338
+ VisibilityTimeout: '30',
339
+ },
340
+ },
341
+ },
342
+ // Optional: Configure concurrent consumers for parallel processing of different groups
343
+ concurrentConsumersAmount: 3, // Process 3 different message groups in parallel
344
+ },
345
+ { userService }
346
+ )
347
+ }
348
+ }
349
+ ```
350
+
351
+ ## Configuration
352
+
353
+ ### Queue Creation
354
+
355
+ When using `creationConfig`, the queue will be created automatically if it doesn't exist:
356
+
357
+ ```typescript
358
+ {
359
+ creationConfig: {
360
+ queue: {
361
+ QueueName: 'my-queue',
362
+ Attributes: {
363
+ // Standard Queue attributes
364
+ VisibilityTimeout: '30', // Seconds a message is invisible after being received
365
+ MessageRetentionPeriod: '345600', // 4 days (in seconds)
366
+ ReceiveMessageWaitTimeSeconds: '20', // Long polling duration
367
+
368
+ // FIFO Queue attributes (only for .fifo queues)
369
+ FifoQueue: 'true', // Must be 'true' for FIFO queues
370
+ ContentBasedDeduplication: 'false', // Automatic deduplication based on message body
371
+ DeduplicationScope: 'queue', // 'queue' or 'messageGroup'
372
+ FifoThroughputLimit: 'perQueue', // 'perQueue' or 'perMessageGroupId'
373
+
374
+ // Encryption
375
+ KmsMasterKeyId: 'alias/aws/sqs', // KMS key for encryption
376
+
377
+ // Other attributes
378
+ DelaySeconds: '0', // Default delay for all messages
379
+ MaximumMessageSize: '262144', // 256 KB (default maximum)
380
+ },
381
+ tags: {
382
+ Environment: 'production',
383
+ Team: 'backend',
384
+ },
385
+ },
386
+ updateAttributesIfExists: true, // Update attributes if queue exists
387
+ forceTagUpdate: false, // Force tag update even if unchanged
388
+
389
+ // Policy configuration (see Policy Configuration section)
390
+ policyConfig: {
391
+ resource: 'arn:aws:sqs:us-east-1:123456789012:my-queue',
392
+ statements: [
393
+ {
394
+ Effect: 'Allow',
395
+ Principal: '*',
396
+ Action: ['sqs:SendMessage'],
397
+ },
398
+ ],
399
+ },
400
+ },
401
+ }
402
+ ```
403
+
404
+ ### Queue Locator
405
+
406
+ When using `locatorConfig`, you connect to an existing queue without creating it:
407
+
408
+ ```typescript
409
+ {
410
+ locatorConfig: {
411
+ // Option 1: By queue URL
412
+ queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
413
+
414
+ // Option 2: By queue name (URL will be resolved)
415
+ // queueName: 'my-queue',
416
+ },
417
+ }
418
+ ```
419
+
420
+ ### Publisher Options
421
+
422
+ ```typescript
423
+ {
424
+ // Required - Message Schema Configuration
425
+ messageSchemas: [Schema1, Schema2], // Array of Zod schemas
426
+ messageTypeField: 'messageType', // Field containing message type discriminator
427
+
428
+ // Queue Configuration (one of these required)
429
+ creationConfig: { /* ... */ }, // Create queue if doesn't exist
430
+ locatorConfig: { /* ... */ }, // Use existing queue
431
+
432
+ // Optional - FIFO Configuration
433
+ fifoQueue: false, // Set to true for FIFO queues
434
+ messageGroupIdField: 'userId', // Field to use as MessageGroupId
435
+ defaultMessageGroupId: 'default', // Default MessageGroupId if field not present
436
+
437
+ // Optional - Message Field Configuration
438
+ messageIdField: 'id', // Field containing message ID (default: 'id')
439
+ messageTimestampField: 'timestamp', // Field containing timestamp (default: 'timestamp')
440
+ messageDeduplicationIdField: 'deduplicationId', // Field for deduplication ID (default: 'deduplicationId')
441
+ messageDeduplicationOptionsField: 'deduplicationOptions', // Field for deduplication options (default: 'deduplicationOptions')
442
+
443
+ // Optional - Features
444
+ logMessages: false, // Log all published messages
445
+ handlerSpy: true, // Enable handler spy for testing
446
+
447
+ // Optional - Deduplication
448
+ enablePublisherDeduplication: false, // Enable store-based deduplication
449
+ messageDeduplicationConfig: {
450
+ deduplicationStore: redisStore, // Redis-based deduplication store
451
+ },
452
+
453
+ // Optional - Payload Offloading
454
+ payloadStoreConfig: {
455
+ payloadStore: s3Store, // S3-based payload store
456
+ maxPayloadSize: 256 * 1024, // 256 KB
457
+ },
458
+
459
+ // Optional - Deletion
460
+ deletionConfig: {
461
+ deleteIfExists: false, // Delete queue on init
462
+ waitForConfirmation: true, // Wait for deletion to complete
463
+ forceDeleteInProduction: false, // Allow deletion in production
464
+ },
465
+ }
466
+ ```
467
+
468
+ ### Consumer Options
469
+
470
+ ```typescript
471
+ {
472
+ // Required - Message Handling Configuration
473
+ handlers: MessageHandlerConfigBuilder.build(), // Message handlers configuration
474
+ messageTypeField: 'messageType', // Field containing message type discriminator
475
+
476
+ // Queue Configuration (one of these required)
477
+ creationConfig: { /* ... */ },
478
+ locatorConfig: { /* ... */ },
479
+
480
+ // Optional - FIFO Configuration
481
+ fifoQueue: false, // Set to true for FIFO queues
482
+
483
+ // Optional - Message Field Configuration
484
+ messageIdField: 'id', // Field containing message ID (default: 'id')
485
+ messageTimestampField: 'timestamp', // Field containing timestamp (default: 'timestamp')
486
+ messageDeduplicationIdField: 'deduplicationId', // Field for deduplication ID (default: 'deduplicationId')
487
+ messageDeduplicationOptionsField: 'deduplicationOptions', // Field for deduplication options (default: 'deduplicationOptions')
488
+
489
+ // Optional - Concurrency
490
+ concurrentConsumersAmount: 1, // Number of concurrent consumer instances
491
+
492
+ // Optional - Retry Configuration
493
+ maxRetryDuration: 345600, // 4 days in seconds (default)
494
+
495
+ // Optional - Dead Letter Queue
496
+ deadLetterQueue: {
497
+ creationConfig: {
498
+ queue: {
499
+ QueueName: 'my-queue-dlq',
500
+ // For FIFO queues, DLQ must also be FIFO
501
+ Attributes: {
502
+ FifoQueue: 'true', // Match source queue type
503
+ },
504
+ },
505
+ },
506
+ redrivePolicy: {
507
+ maxReceiveCount: 3, // Move to DLQ after 3 receive attempts
508
+ },
509
+ },
510
+
511
+ // Optional - Consumer Behavior
512
+ consumerOverrides: {
513
+ batchSize: 10, // Messages per receive (1-10)
514
+ pollingWaitTimeMs: 0, // Time between polls
515
+ terminateVisibilityTimeout: true, // Reset visibility on error
516
+ heartbeatInterval: 300, // Heartbeat interval in seconds
517
+ },
518
+
519
+ // Optional - Deduplication
520
+ enableConsumerDeduplication: false,
521
+ messageDeduplicationConfig: {
522
+ deduplicationStore: redisStore,
523
+ },
524
+
525
+ // Optional - Payload Offloading
526
+ payloadStoreConfig: {
527
+ payloadStore: s3Store,
528
+ },
529
+
530
+ // Optional - Other
531
+ logMessages: false,
532
+ handlerSpy: true,
533
+ deletionConfig: { /* ... */ },
534
+ }
535
+ ```
536
+
537
+ ## Advanced Features
538
+
539
+ ### Custom Message Field Names
540
+
541
+ All message field names are configurable, allowing you to adapt the library to your existing message schemas:
542
+
543
+ ```typescript
544
+ // Your existing message schema with custom field names
545
+ const CustomMessageSchema = z.object({
546
+ messageId: z.string(), // Custom ID field
547
+ eventType: z.literal('order.created'), // Custom type field
548
+ createdAt: z.string(), // Custom timestamp field
549
+ txId: z.string(), // Custom deduplication ID
550
+ txOptions: z.object({ // Custom deduplication options
551
+ deduplicationWindowSeconds: z.number().optional(),
552
+ }).optional(),
553
+ orderId: z.string(),
554
+ amount: z.number(),
555
+ })
556
+
557
+ // Configure the publisher to use your custom field names
558
+ class OrderPublisher extends AbstractSqsPublisher<CustomMessage> {
559
+ constructor(sqsClient: SQSClient) {
560
+ super(
561
+ { sqsClient, logger: console, errorReporter: { report: console.error } },
562
+ {
563
+ messageSchemas: [CustomMessageSchema],
564
+
565
+ // Map library's internal fields to your custom fields
566
+ messageIdField: 'messageId', // Default: 'id'
567
+ messageTypeField: 'eventType', // Required
568
+ messageTimestampField: 'createdAt', // Default: 'timestamp'
569
+ messageDeduplicationIdField: 'txId', // Default: 'deduplicationId'
570
+ messageDeduplicationOptionsField: 'txOptions', // Default: 'deduplicationOptions'
571
+
572
+ creationConfig: {
573
+ queue: { QueueName: 'orders-queue' },
574
+ },
575
+ }
576
+ )
577
+ }
578
+ }
579
+
580
+ // Use with your custom schema
581
+ await publisher.publish({
582
+ messageId: 'msg-123', // Library will use this for tracking
583
+ eventType: 'order.created', // Library will use this for routing
584
+ createdAt: new Date().toISOString(), // Library will use this for retry tracking
585
+ txId: 'tx-456', // Library will use this for deduplication
586
+ orderId: 'order-789',
587
+ amount: 99.99,
588
+ })
589
+ ```
590
+
591
+ **Benefits:**
592
+ - ✅ No need to modify existing message schemas
593
+ - ✅ Maintain consistency with your domain model
594
+ - ✅ Gradual migration from legacy systems
595
+ - ✅ Works with all features (retry, deduplication, offloading)
596
+
597
+ ### Dead Letter Queue (DLQ)
598
+
599
+ Dead Letter Queues capture messages that cannot be processed after multiple attempts:
600
+
601
+ ```typescript
602
+ {
603
+ deadLetterQueue: {
604
+ creationConfig: {
605
+ queue: {
606
+ QueueName: 'my-queue-dlq',
607
+ // For FIFO source queues, DLQ must also be FIFO
608
+ Attributes: {
609
+ FifoQueue: 'true', // Match source queue type
610
+ MessageRetentionPeriod: '1209600', // 14 days
611
+ },
612
+ },
613
+ },
614
+ redrivePolicy: {
615
+ maxReceiveCount: 3, // Send to DLQ after 3 failed attempts
616
+ },
617
+ },
618
+ }
619
+ ```
620
+
621
+ **How it works:**
622
+ 1. Message fails processing (handler returns error or throws)
623
+ 2. Message becomes visible again (visibility timeout expires)
624
+ 3. Consumer receives message again (receive count increments)
625
+ 4. After `maxReceiveCount` attempts, SQS automatically moves message to DLQ
626
+ 5. DLQ messages can be inspected, reprocessed, or deleted
627
+
628
+ ### Message Retry Logic
629
+
630
+ The library implements intelligent retry logic with exponential backoff:
631
+
632
+ ```typescript
633
+ {
634
+ maxRetryDuration: 345600, // 4 days in seconds (default)
635
+ }
636
+ ```
637
+
638
+ **Retry Flow:**
639
+
640
+ 1. **Handler returns `{ error: 'retryLater' }`** or **throws an error**
641
+ 2. Consumer checks if message should be retried:
642
+ - Calculates how long the message has been retrying
643
+ - If within `maxRetryDuration`, re-queues message
644
+ - If exceeded, sends to DLQ (if configured) or marks as failed
645
+
646
+ 3. **Exponential Backoff (Standard Queues):**
647
+ ```
648
+ Attempt 1: 2^0 = 1 second delay
649
+ Attempt 2: 2^1 = 2 seconds delay
650
+ Attempt 3: 2^2 = 4 seconds delay
651
+ Attempt 4: 2^3 = 8 seconds delay
652
+ ...
653
+ Max: 900 seconds (15 minutes) per AWS limits
654
+ ```
655
+
656
+ 4. **FIFO Queues:**
657
+ - No delay support (AWS limitation)
658
+ - Messages retry immediately
659
+ - Order preserved within message group
660
+
661
+ **Handler Return Types:**
662
+
663
+ ```typescript
664
+ type HandlerResult = Either<'retryLater', 'success'>
665
+
666
+ // Success - message is deleted from queue
667
+ return { result: 'success' }
668
+
669
+ // Retry - message is re-queued with delay
670
+ return { error: 'retryLater' }
671
+
672
+ // Error thrown - automatically retries
673
+ throw new Error('Database connection failed')
674
+ ```
675
+
676
+ ### Message Deduplication
677
+
678
+ Prevent duplicate message publishing or processing:
679
+
680
+ #### Publisher-Level Deduplication
681
+
682
+ Prevents sending the same message multiple times:
683
+
684
+ ```typescript
685
+ import { InMemoryDeduplicationStore } from '@message-queue-toolkit/core'
686
+ // or
687
+ import { RedisMessageDeduplicationStore } from '@message-queue-toolkit/redis-message-deduplication-store'
688
+
689
+ const deduplicationStore = new RedisMessageDeduplicationStore(redisClient)
690
+
691
+ // Publisher configuration
692
+ {
693
+ enablePublisherDeduplication: true,
694
+ messageDeduplicationIdField: 'deduplicationId',
695
+ messageDeduplicationConfig: {
696
+ deduplicationStore,
697
+ },
698
+ }
699
+
700
+ // Publishing with deduplication
701
+ await publisher.publish({
702
+ id: '123',
703
+ messageType: 'user.created',
704
+ deduplicationId: 'user-456-creation', // Unique key for deduplication
705
+ deduplicationOptions: {
706
+ deduplicationWindowSeconds: 60, // Prevent duplicates for 60 seconds
707
+ },
708
+ })
709
+
710
+ // Second publish with same deduplicationId within 60s is skipped
711
+ await publisher.publish({
712
+ id: '124',
713
+ messageType: 'user.created',
714
+ deduplicationId: 'user-456-creation', // Duplicate - won't be sent
715
+ })
716
+ ```
717
+
718
+ #### Consumer-Level Deduplication
719
+
720
+ Prevents processing the same message multiple times:
721
+
722
+ ```typescript
723
+ {
724
+ enableConsumerDeduplication: true,
725
+ messageDeduplicationIdField: 'deduplicationId',
726
+ messageDeduplicationConfig: {
727
+ deduplicationStore,
728
+ },
729
+ }
730
+
731
+ // Message configuration
732
+ {
733
+ deduplicationId: 'unique-operation-id',
734
+ deduplicationOptions: {
735
+ deduplicationWindowSeconds: 3600, // 1 hour
736
+ lockTimeoutSeconds: 20, // Lock duration while processing
737
+ acquireTimeoutSeconds: 20, // Max wait time to acquire lock
738
+ refreshIntervalSeconds: 10, // Lock refresh interval
739
+ },
740
+ }
741
+ ```
742
+
743
+ **How it works:**
744
+ 1. Consumer receives message
745
+ 2. Checks deduplication store for duplicate
746
+ 3. If duplicate found (within window), skips processing
747
+ 4. If not duplicate, acquires exclusive lock
748
+ 5. Processes message
749
+ 6. Releases lock and marks as processed
750
+ 7. Subsequent messages with same ID are skipped
751
+
752
+ ### Payload Offloading
753
+
754
+ For messages larger than 256 KB, store the payload externally (e.g., S3):
755
+
756
+ ```typescript
757
+ import { S3PayloadStore } from '@message-queue-toolkit/s3-payload-store'
758
+
759
+ const payloadStore = new S3PayloadStore({
760
+ s3Client,
761
+ bucketName: 'my-message-payloads',
762
+ })
763
+
764
+ // Publisher configuration
765
+ {
766
+ payloadStoreConfig: {
767
+ payloadStore,
768
+ maxPayloadSize: 256 * 1024, // 256 KB threshold
769
+ },
770
+ }
771
+
772
+ // Large message is automatically offloaded
773
+ await publisher.publish({
774
+ id: '123',
775
+ messageType: 'document.processed',
776
+ largeData: hugeArrayOfData, // If total size > 256 KB, stored in S3
777
+ })
778
+ ```
779
+
780
+ **How it works:**
781
+ 1. Publisher checks message size before sending
782
+ 2. If size exceeds `maxPayloadSize`, stores payload in S3
783
+ 3. Replaces payload with pointer: `{ _offloadedPayload: { bucketName, key, size } }`
784
+ 4. Sends pointer message to SQS
785
+ 5. Consumer detects pointer, fetches payload from S3
786
+ 6. Processes message with full payload
787
+
788
+ **Note:** Payload cleanup is the responsibility of the store (e.g., S3 lifecycle policies).
789
+
790
+ ### Message Handlers
791
+
792
+ Handlers process messages based on their type. Messages are routed to the appropriate handler using the discriminator field (configurable via `messageTypeField`):
793
+
794
+ ```typescript
795
+ import { MessageHandlerConfigBuilder } from '@message-queue-toolkit/core'
796
+
797
+ const handlers = new MessageHandlerConfigBuilder<
798
+ SupportedMessages,
799
+ ExecutionContext,
800
+ PrehandlerOutput
801
+ >()
802
+ .addConfig(
803
+ UserCreatedSchema,
804
+ async (message, context, preHandlingOutputs) => {
805
+ // Access execution context
806
+ await context.userService.createUser(message.userId)
807
+
808
+ // Access pre-handler outputs
809
+ console.log('Pre-handler result:', preHandlingOutputs.preHandlerOutput)
810
+ console.log('Barrier result:', preHandlingOutputs.barrierOutput)
811
+
812
+ return { result: 'success' }
813
+ },
814
+ {
815
+ // Optional: Pre-handlers (run before main handler)
816
+ preHandlers: [
817
+ (message, context, output, next) => {
818
+ console.log('Pre-processing message:', message.id)
819
+ output.processedAt = Date.now()
820
+ next({ result: 'success' })
821
+ },
822
+ ],
823
+
824
+ // Optional: Barrier (controls whether message should be processed)
825
+ preHandlerBarrier: async (message, context, preHandlerOutput) => {
826
+ const isReady = await context.userService.isSystemReady()
827
+ return {
828
+ isPassing: isReady,
829
+ output: { systemStatus: 'ready' },
830
+ }
831
+ },
832
+
833
+ // Optional: Custom message log formatter
834
+ messageLogFormatter: (message) => ({
835
+ userId: message.userId,
836
+ action: 'create',
837
+ }),
838
+ }
839
+ )
840
+ .addConfig(UserUpdatedSchema, handleUserUpdated)
841
+ .build()
842
+ ```
843
+
844
+ ### Pre-handlers and Barriers
845
+
846
+ #### Pre-handlers
847
+
848
+ Pre-handlers are middleware functions that run before the main message handler, allowing you to:
849
+ - Enrich the execution context with additional data
850
+ - Set up scoped resources (child loggers, database transactions)
851
+ - Validate prerequisites
852
+ - Transform message data
853
+ - Implement cross-cutting concerns (logging, metrics, caching)
854
+
855
+ The output from pre-handlers is passed to both the barrier and the main handler, enabling a powerful data flow pattern.
856
+
857
+ **Type Signature:**
858
+
859
+ ```typescript
860
+ type Prehandler<Message, Context, Output> = (
861
+ message: Message,
862
+ context: Context,
863
+ output: Output,
864
+ next: (result: PrehandlerResult) => void
865
+ ) => void
866
+ ```
867
+
868
+ **Common Use Cases:**
869
+
870
+ ##### 1. Child Logger Resolution
871
+
872
+ Create message-specific loggers with contextual information:
873
+
874
+ ```typescript
875
+ type PrehandlerOutput = {
876
+ logger: Logger
877
+ }
878
+
879
+ const preHandlers: Prehandler<UserMessage, ExecutionContext, PrehandlerOutput>[] = [
880
+ (message, context, output, next) => {
881
+ // Create child logger with message context
882
+ output.logger = context.logger.child({
883
+ messageId: message.id,
884
+ messageType: message.messageType,
885
+ userId: message.userId,
886
+ correlationId: message.correlationId,
887
+ })
888
+
889
+ output.logger.info('Message processing started')
890
+ next({ result: 'success' })
891
+ },
892
+ ]
893
+
894
+ // In your handler
895
+ const handler = async (message, context, preHandlingOutputs) => {
896
+ const logger = preHandlingOutputs.preHandlerOutput.logger
897
+
898
+ logger.info('Processing user update') // Automatically includes message context
899
+ logger.error({ error: someError }, 'Failed to update user')
900
+
901
+ return { result: 'success' }
902
+ }
903
+ ```
904
+
905
+ ##### 2. User Data and Permissions Resolution
906
+
907
+ Fetch and cache user information needed by the handler:
908
+
909
+ ```typescript
910
+ type PrehandlerOutput = {
911
+ user: User
912
+ permissions: string[]
913
+ organizationId: string
914
+ }
915
+
916
+ const preHandlers: Prehandler<OrderMessage, ExecutionContext, PrehandlerOutput>[] = [
917
+ // Fetch user data
918
+ async (message, context, output, next) => {
919
+ try {
920
+ const user = await context.userRepository.findById(message.userId)
921
+ if (!user) {
922
+ next({ error: new Error(`User ${message.userId} not found`) })
923
+ return
924
+ }
925
+ output.user = user
926
+ next({ result: 'success' })
927
+ } catch (error) {
928
+ next({ error })
929
+ }
930
+ },
931
+
932
+ // Resolve permissions
933
+ async (message, context, output, next) => {
934
+ try {
935
+ output.permissions = await context.permissionService.getPermissions(output.user.id)
936
+ output.organizationId = output.user.organizationId
937
+ next({ result: 'success' })
938
+ } catch (error) {
939
+ next({ error })
940
+ }
941
+ },
942
+ ]
943
+
944
+ // In your handler - user data is already fetched
945
+ const handler = async (message, context, preHandlingOutputs) => {
946
+ const { user, permissions, organizationId } = preHandlingOutputs.preHandlerOutput
947
+
948
+ // Check permissions
949
+ if (!permissions.includes('orders:create')) {
950
+ throw new Error('Insufficient permissions')
951
+ }
952
+
953
+ // Use pre-fetched data
954
+ await context.orderService.createOrder({
955
+ orderId: message.orderId,
956
+ userId: user.id,
957
+ organizationId,
958
+ userEmail: user.email, // Already available, no need to fetch again
959
+ })
960
+
961
+ return { result: 'success' }
962
+ }
963
+ ```
964
+
965
+ ##### 3. Database Transaction Management
966
+
967
+ Set up scoped database transactions:
968
+
969
+ ```typescript
970
+ type PrehandlerOutput = {
971
+ transaction: DatabaseTransaction
972
+ }
973
+
974
+ const preHandlers = [
975
+ async (message, context, output, next) => {
976
+ const transaction = await context.database.beginTransaction()
977
+ output.transaction = transaction
978
+
979
+ try {
980
+ next({ result: 'success' })
981
+ } catch (error) {
982
+ await transaction.rollback()
983
+ throw error
984
+ }
985
+ },
986
+ ]
987
+
988
+ const handler = async (message, context, preHandlingOutputs) => {
989
+ const { transaction } = preHandlingOutputs.preHandlerOutput
990
+
991
+ try {
992
+ await context.userRepository.create(message.userData, { transaction })
993
+ await context.auditRepository.log(message.action, { transaction })
994
+
995
+ await transaction.commit()
996
+ return { result: 'success' }
997
+ } catch (error) {
998
+ await transaction.rollback()
999
+ throw error
1000
+ }
1001
+ }
1002
+ ```
1003
+
1004
+ ##### 4. Caching and Deduplication
1005
+
1006
+ Implement custom caching logic:
1007
+
1008
+ ```typescript
1009
+ type PrehandlerOutput = {
1010
+ cachedData?: ProductData
1011
+ cacheHit: boolean
1012
+ }
1013
+
1014
+ const preHandlers = [
1015
+ async (message, context, output, next) => {
1016
+ const cacheKey = `product:${message.productId}`
1017
+ const cached = await context.cache.get(cacheKey)
1018
+
1019
+ if (cached) {
1020
+ output.cachedData = cached
1021
+ output.cacheHit = true
1022
+ context.logger.info('Cache hit', { productId: message.productId })
1023
+ } else {
1024
+ output.cacheHit = false
1025
+ }
1026
+
1027
+ next({ result: 'success' })
1028
+ },
1029
+ ]
1030
+
1031
+ const handler = async (message, context, preHandlingOutputs) => {
1032
+ const { cachedData, cacheHit } = preHandlingOutputs.preHandlerOutput
1033
+
1034
+ if (cacheHit && cachedData) {
1035
+ // Use cached data
1036
+ return processWithCache(cachedData)
1037
+ }
1038
+
1039
+ // Fetch fresh data
1040
+ const data = await context.productService.fetch(message.productId)
1041
+ await context.cache.set(`product:${message.productId}`, data, { ttl: 3600 })
1042
+
1043
+ return processWithCache(data)
1044
+ }
1045
+ ```
1046
+
1047
+ ##### 5. Metrics and Monitoring
1048
+
1049
+ Track message processing metrics:
1050
+
1051
+ ```typescript
1052
+ type PrehandlerOutput = {
1053
+ startTime: number
1054
+ metricsLabels: Record<string, string>
1055
+ }
1056
+
1057
+ const preHandlers = [
1058
+ (message, context, output, next) => {
1059
+ output.startTime = Date.now()
1060
+ output.metricsLabels = {
1061
+ messageType: message.messageType,
1062
+ userId: message.userId,
1063
+ source: message.source || 'unknown',
1064
+ }
1065
+
1066
+ context.metrics.increment('messages.received', output.metricsLabels)
1067
+ next({ result: 'success' })
1068
+ },
1069
+ ]
1070
+
1071
+ const handler = async (message, context, preHandlingOutputs) => {
1072
+ const { startTime, metricsLabels } = preHandlingOutputs.preHandlerOutput
1073
+
1074
+ try {
1075
+ await processMessage(message)
1076
+
1077
+ const duration = Date.now() - startTime
1078
+ context.metrics.histogram('message.processing.duration', duration, metricsLabels)
1079
+ context.metrics.increment('messages.processed', { ...metricsLabels, status: 'success' })
1080
+
1081
+ return { result: 'success' }
1082
+ } catch (error) {
1083
+ context.metrics.increment('messages.processed', { ...metricsLabels, status: 'error' })
1084
+ throw error
1085
+ }
1086
+ }
1087
+ ```
1088
+
1089
+ **Configuration:**
1090
+
1091
+ ```typescript
1092
+ new MessageHandlerConfigBuilder<SupportedMessages, ExecutionContext, PrehandlerOutput>()
1093
+ .addConfig(
1094
+ MessageSchema,
1095
+ handler,
1096
+ {
1097
+ preHandlers: [
1098
+ loggerPreHandler,
1099
+ userDataPreHandler,
1100
+ permissionsPreHandler,
1101
+ ],
1102
+ }
1103
+ )
1104
+ .build()
1105
+ ```
1106
+
1107
+ #### Barriers
1108
+
1109
+ Barriers are async functions that determine whether a message should be processed immediately or retried later. They are essential for handling message dependencies and ensuring prerequisites are met.
1110
+
1111
+ **Type Signature:**
1112
+
1113
+ ```typescript
1114
+ type BarrierCallback<Message, Context, PrehandlerOutput, BarrierOutput> = (
1115
+ message: Message,
1116
+ context: Context,
1117
+ preHandlerOutput: PrehandlerOutput
1118
+ ) => Promise<BarrierResult<BarrierOutput>>
1119
+
1120
+ type BarrierResult<Output> = {
1121
+ isPassing: boolean // true = process now, false = retry later
1122
+ output: Output // Additional data passed to the handler
1123
+ }
1124
+ ```
1125
+
1126
+ **Common Use Cases:**
1127
+
1128
+ ##### 1. Message Ordering Dependencies
1129
+
1130
+ Ensure messages are processed in the correct order when they arrive out of sequence:
1131
+
1132
+ ```typescript
1133
+ // Scenario: Process order.updated only after order.created
1134
+ const preHandlerBarrier = async (message: OrderUpdatedMessage, context, preHandlerOutput) => {
1135
+ // Check if the order exists (created event was processed)
1136
+ const orderExists = await context.orderRepository.exists(message.orderId)
1137
+
1138
+ if (!orderExists) {
1139
+ context.logger.warn('Order not found, retrying later', {
1140
+ orderId: message.orderId,
1141
+ messageId: message.id,
1142
+ })
1143
+
1144
+ return {
1145
+ isPassing: false,
1146
+ output: { reason: 'order_not_created_yet' },
1147
+ }
1148
+ }
1149
+
1150
+ return {
1151
+ isPassing: true,
1152
+ output: { orderExists: true },
1153
+ }
1154
+ }
1155
+
1156
+ // Message will be automatically retried until order.created is processed
1157
+ ```
1158
+
1159
+ ##### 2. External Resource Availability
1160
+
1161
+ Wait for external systems to be ready:
1162
+
1163
+ ```typescript
1164
+ // Scenario: Process message only when third-party API is available
1165
+ const preHandlerBarrier = async (message, context, preHandlerOutput) => {
1166
+ try {
1167
+ // Check if external service is healthy
1168
+ const isHealthy = await context.externalApiClient.healthCheck()
1169
+
1170
+ if (!isHealthy) {
1171
+ context.logger.info('External API unhealthy, retrying later')
1172
+ return {
1173
+ isPassing: false,
1174
+ output: { reason: 'external_api_unavailable' },
1175
+ }
1176
+ }
1177
+
1178
+ // Check rate limit
1179
+ const rateLimitOk = await context.rateLimiter.checkLimit(message.userId)
1180
+ if (!rateLimitOk) {
1181
+ context.logger.info('Rate limit exceeded, retrying later')
1182
+ return {
1183
+ isPassing: false,
1184
+ output: { reason: 'rate_limit_exceeded' },
1185
+ }
1186
+ }
1187
+
1188
+ return {
1189
+ isPassing: true,
1190
+ output: { apiAvailable: true },
1191
+ }
1192
+ } catch (error) {
1193
+ context.logger.error({ error }, 'Barrier check failed')
1194
+ return {
1195
+ isPassing: false,
1196
+ output: { reason: 'barrier_error', error },
1197
+ }
1198
+ }
1199
+ }
1200
+ ```
1201
+
1202
+ ##### 3. Business Workflow Prerequisites
1203
+
1204
+ Implement complex business logic gates:
1205
+
1206
+ ```typescript
1207
+ // Scenario: Process payment only after KYC verification is complete
1208
+ const preHandlerBarrier = async (
1209
+ message: PaymentMessage,
1210
+ context,
1211
+ preHandlerOutput
1212
+ ) => {
1213
+ const { user } = preHandlerOutput // From pre-handler
1214
+
1215
+ // Check KYC status
1216
+ const kycStatus = await context.kycService.getStatus(user.id)
1217
+
1218
+ if (kycStatus !== 'approved') {
1219
+ context.logger.info('KYC not approved, retrying later', {
1220
+ userId: user.id,
1221
+ kycStatus,
1222
+ })
1223
+
1224
+ return {
1225
+ isPassing: false,
1226
+ output: {
1227
+ reason: 'kyc_pending',
1228
+ kycStatus,
1229
+ retriedAt: new Date(),
1230
+ },
1231
+ }
1232
+ }
1233
+
1234
+ // Check account balance
1235
+ const balance = await context.accountService.getBalance(user.id)
1236
+ if (balance < message.amount) {
1237
+ context.logger.info('Insufficient balance, retrying later', {
1238
+ userId: user.id,
1239
+ balance,
1240
+ required: message.amount,
1241
+ })
1242
+
1243
+ return {
1244
+ isPassing: false,
1245
+ output: {
1246
+ reason: 'insufficient_balance',
1247
+ balance,
1248
+ required: message.amount,
1249
+ },
1250
+ }
1251
+ }
1252
+
1253
+ return {
1254
+ isPassing: true,
1255
+ output: {
1256
+ kycApproved: true,
1257
+ currentBalance: balance,
1258
+ },
1259
+ }
1260
+ }
1261
+
1262
+ const handler = async (message, context, preHandlingOutputs) => {
1263
+ const { kycApproved, currentBalance } = preHandlingOutputs.barrierOutput
1264
+
1265
+ // Safe to process payment - all prerequisites met
1266
+ await context.paymentService.processPayment({
1267
+ userId: message.userId,
1268
+ amount: message.amount,
1269
+ currentBalance, // From barrier
1270
+ })
1271
+
1272
+ return { result: 'success' }
1273
+ }
1274
+ ```
1275
+
1276
+ ##### 4. Multi-Message Dependencies
1277
+
1278
+ Wait for multiple related messages to be processed:
1279
+
1280
+ ```typescript
1281
+ // Scenario: Process shipment only after all items are packed
1282
+ const preHandlerBarrier = async (
1283
+ message: ShipmentMessage,
1284
+ context,
1285
+ preHandlerOutput
1286
+ ) => {
1287
+ const orderId = message.orderId
1288
+
1289
+ // Check if all items are packed
1290
+ const orderItems = await context.orderRepository.getItems(orderId)
1291
+ const packedItems = await context.packingRepository.getPackedItems(orderId)
1292
+
1293
+ const allItemsPacked = orderItems.every(item =>
1294
+ packedItems.some(packed => packed.itemId === item.id)
1295
+ )
1296
+
1297
+ if (!allItemsPacked) {
1298
+ const pendingItems = orderItems.filter(item =>
1299
+ !packedItems.some(packed => packed.itemId === item.id)
1300
+ )
1301
+
1302
+ context.logger.info('Not all items packed, retrying later', {
1303
+ orderId,
1304
+ totalItems: orderItems.length,
1305
+ packedItems: packedItems.length,
1306
+ pendingItems: pendingItems.map(i => i.id),
1307
+ })
1308
+
1309
+ return {
1310
+ isPassing: false,
1311
+ output: {
1312
+ reason: 'items_not_packed',
1313
+ pendingItemsCount: pendingItems.length,
1314
+ },
1315
+ }
1316
+ }
1317
+
1318
+ return {
1319
+ isPassing: true,
1320
+ output: {
1321
+ allItemsPacked: true,
1322
+ totalWeight: packedItems.reduce((sum, item) => sum + item.weight, 0),
1323
+ },
1324
+ }
1325
+ }
1326
+ ```
1327
+
1328
+ ##### 5. Time-Based Gating
1329
+
1330
+ Delay processing until a specific time:
1331
+
1332
+ ```typescript
1333
+ // Scenario: Process scheduled messages only after their scheduled time
1334
+ const preHandlerBarrier = async (message: ScheduledMessage, context, preHandlerOutput) => {
1335
+ const scheduledTime = new Date(message.scheduledFor)
1336
+ const now = new Date()
1337
+
1338
+ if (now < scheduledTime) {
1339
+ const delayMs = scheduledTime.getTime() - now.getTime()
1340
+ context.logger.info('Message scheduled for future, retrying later', {
1341
+ messageId: message.id,
1342
+ scheduledFor: scheduledTime,
1343
+ delayMs,
1344
+ })
1345
+
1346
+ return {
1347
+ isPassing: false,
1348
+ output: {
1349
+ reason: 'scheduled_for_future',
1350
+ scheduledFor: scheduledTime,
1351
+ },
1352
+ }
1353
+ }
1354
+
1355
+ return {
1356
+ isPassing: true,
1357
+ output: {
1358
+ scheduledFor: scheduledTime,
1359
+ actualProcessingTime: now,
1360
+ },
1361
+ }
1362
+ }
1363
+ ```
1364
+
1365
+ **Configuration:**
1366
+
1367
+ ```typescript
1368
+ new MessageHandlerConfigBuilder<SupportedMessages, ExecutionContext, PrehandlerOutput>()
1369
+ .addConfig(
1370
+ MessageSchema,
1371
+ handler,
1372
+ {
1373
+ preHandlers: [userDataPreHandler, permissionsPreHandler],
1374
+ preHandlerBarrier: orderDependencyBarrier,
1375
+ }
1376
+ )
1377
+ .build()
1378
+ ```
1379
+
1380
+ **Important Notes:**
1381
+
1382
+ - **Barriers return `isPassing: false`** → Message is automatically retried with exponential backoff
1383
+ - **Barriers throw errors** → Message follows normal error handling (retry or DLQ)
1384
+ - **Barrier output** → Available in handler via `preHandlingOutputs.barrierOutput`
1385
+ - **Retry limits apply** → Messages exceeding `maxRetryDuration` will be sent to DLQ even if barrier keeps returning false
1386
+ - **FIFO queues** → Barriers are especially important for FIFO queues to handle out-of-order delivery within message groups
1387
+
1388
+ ### Handler Spies
1389
+
1390
+ Handler spies solve the fundamental challenge of testing asynchronous message-based systems.
1391
+
1392
+ **The Problem:**
1393
+
1394
+ Testing message queues is complex because:
1395
+ 1. **Asynchronous processing** - Messages are published and consumed asynchronously with unpredictable timing
1396
+ 2. **Indirect interactions** - Business logic may trigger message publishing without explicit calls to the publisher
1397
+ 3. **Non-deterministic order** - Messages may be processed in different orders across test runs
1398
+ 4. **Hard to verify** - Traditional mocking/stubbing doesn't work well for async pub/sub patterns
1399
+
1400
+ **The Solution:**
1401
+
1402
+ Handler spies provide a way to wait for and inspect messages during tests without having to:
1403
+ - Poll the queue directly
1404
+ - Add artificial delays (`setTimeout`)
1405
+ - Mock the entire message infrastructure
1406
+ - Modify production code for testing
1407
+
1408
+ #### Configuration
1409
+
1410
+ ```typescript
1411
+ // Enable handler spy for publisher and/or consumer
1412
+ const publisher = new UserEventsPublisher(sqsClient, {
1413
+ handlerSpy: true, // Track published messages
1414
+ })
1415
+
1416
+ const consumer = new UserEventsConsumer(sqsClient, {
1417
+ handlerSpy: true, // Track consumed messages
1418
+ })
1419
+ ```
1420
+
1421
+ #### Example 1: Testing Direct Message Publishing
1422
+
1423
+ ```typescript
1424
+ import { describe, it, expect, beforeEach } from 'vitest'
1425
+
1426
+ describe('UserEventsPublisher', () => {
1427
+ let publisher: UserEventsPublisher
1428
+
1429
+ beforeEach(async () => {
1430
+ publisher = new UserEventsPublisher(sqsClient, { handlerSpy: true })
1431
+ await publisher.init()
1432
+ })
1433
+
1434
+ it('publishes user.created event', async () => {
1435
+ // Act: Publish message
1436
+ await publisher.publish({
1437
+ id: 'msg-123',
1438
+ messageType: 'user.created',
1439
+ userId: 'user-456',
1440
+ email: 'test@example.com',
1441
+ })
1442
+
1443
+ // Assert: Wait for message to be tracked by publisher spy
1444
+ const publishedMessage = await publisher.handlerSpy.waitForMessageWithId(
1445
+ 'msg-123',
1446
+ 'published',
1447
+ 5000 // 5 second timeout
1448
+ )
1449
+
1450
+ expect(publishedMessage).toMatchObject({
1451
+ id: 'msg-123',
1452
+ userId: 'user-456',
1453
+ email: 'test@example.com',
1454
+ })
1455
+ })
1456
+ })
1457
+ ```
1458
+
1459
+ #### Example 2: Testing Indirect Message Publishing via API
1460
+
1461
+ This example demonstrates testing business logic that publishes messages internally:
1462
+
1463
+ ```typescript
1464
+ import { describe, it, expect, beforeEach } from 'vitest'
1465
+ import request from 'supertest'
1466
+
1467
+ // Your API endpoint that creates a user and publishes an event
1468
+ class UserController {
1469
+ constructor(
1470
+ private userRepository: UserRepository,
1471
+ private eventPublisher: UserEventsPublisher
1472
+ ) {}
1473
+
1474
+ async createUser(req, res) {
1475
+ const user = await this.userRepository.create(req.body)
1476
+
1477
+ // Publish event internally - not directly exposed to the test
1478
+ await this.eventPublisher.publish({
1479
+ id: crypto.randomUUID(),
1480
+ messageType: 'user.created',
1481
+ userId: user.id,
1482
+ email: user.email,
1483
+ })
1484
+
1485
+ res.status(201).json(user)
1486
+ }
1487
+ }
1488
+
1489
+ describe('User Creation Flow', () => {
1490
+ let app: Express
1491
+ let publisher: UserEventsPublisher
1492
+ let consumer: UserEventsConsumer
1493
+
1494
+ beforeEach(async () => {
1495
+ // Set up publisher with handler spy
1496
+ publisher = new UserEventsPublisher(sqsClient, { handlerSpy: true })
1497
+ await publisher.init()
1498
+
1499
+ // Set up consumer with handler spy
1500
+ consumer = new UserEventsConsumer(sqsClient, userService, { handlerSpy: true })
1501
+ await consumer.start()
1502
+
1503
+ // Create API with real publisher
1504
+ app = createApp({ eventPublisher: publisher })
1505
+ })
1506
+
1507
+ it('publishes event when user is created via API', async () => {
1508
+ // Act: Make API call (no direct interaction with publisher)
1509
+ const response = await request(app)
1510
+ .post('/api/users')
1511
+ .send({
1512
+ email: 'newuser@example.com',
1513
+ name: 'John Doe',
1514
+ })
1515
+
1516
+ expect(response.status).toBe(201)
1517
+ const createdUserId = response.body.id
1518
+
1519
+ // Assert: Wait for message to be published (by internal business logic)
1520
+ const publishedMessage = await publisher.handlerSpy.waitForMessage(
1521
+ (msg) => msg.userId === createdUserId && msg.messageType === 'user.created',
1522
+ 'published',
1523
+ 5000
1524
+ )
1525
+
1526
+ expect(publishedMessage).toMatchObject({
1527
+ messageType: 'user.created',
1528
+ userId: createdUserId,
1529
+ email: 'newuser@example.com',
1530
+ })
1531
+
1532
+ // Assert: Wait for message to be consumed and processed
1533
+ const consumedMessage = await consumer.handlerSpy.waitForMessage(
1534
+ (msg) => msg.userId === createdUserId,
1535
+ 'consumed',
1536
+ 10000 // Allow more time for async processing
1537
+ )
1538
+
1539
+ expect(consumedMessage.userId).toBe(createdUserId)
1540
+
1541
+ // Verify side effects in your user service
1542
+ expect(userService.onUserCreated).toHaveBeenCalledWith(createdUserId)
1543
+ })
1544
+
1545
+ it('handles complex multi-step workflows', async () => {
1546
+ // Create user via API
1547
+ const createResponse = await request(app)
1548
+ .post('/api/users')
1549
+ .send({ email: 'user@example.com', name: 'Jane Doe' })
1550
+
1551
+ const userId = createResponse.body.id
1552
+
1553
+ // Wait for user.created event
1554
+ await consumer.handlerSpy.waitForMessage(
1555
+ (msg) => msg.userId === userId && msg.messageType === 'user.created',
1556
+ 'consumed'
1557
+ )
1558
+
1559
+ // Update user via API (triggers another event)
1560
+ await request(app)
1561
+ .patch(`/api/users/${userId}`)
1562
+ .send({ name: 'Jane Smith' })
1563
+
1564
+ // Wait for user.updated event
1565
+ const updatedMessage = await consumer.handlerSpy.waitForMessage(
1566
+ (msg) => msg.userId === userId && msg.messageType === 'user.updated',
1567
+ 'consumed',
1568
+ 5000
1569
+ )
1570
+
1571
+ expect(updatedMessage.changes).toMatchObject({ name: 'Jane Smith' })
1572
+ })
1573
+ })
1574
+ ```
1575
+
1576
+ #### Example 3: Non-Waiting Checks
1577
+
1578
+ For scenarios where you don't want to wait:
1579
+
1580
+ ```typescript
1581
+ it('checks message without waiting', async () => {
1582
+ await publisher.publish({
1583
+ id: 'msg-789',
1584
+ messageType: 'user.deleted',
1585
+ userId: 'user-123',
1586
+ })
1587
+
1588
+ // Wait briefly for async processing
1589
+ await new Promise(resolve => setTimeout(resolve, 100))
1590
+
1591
+ // Check without waiting
1592
+ const result = consumer.handlerSpy.checkMessage(
1593
+ (msg) => msg.id === 'msg-789'
1594
+ )
1595
+
1596
+ if (result) {
1597
+ expect(result.message.userId).toBe('user-123')
1598
+ expect(result.processingResult.status).toBe('consumed')
1599
+ } else {
1600
+ throw new Error('Message not found')
1601
+ }
1602
+ })
1603
+ ```
1604
+
1605
+ #### Example 4: Inspecting All Messages
1606
+
1607
+ Useful for debugging or verifying batch operations:
1608
+
1609
+ ```typescript
1610
+ it('processes batch of user events', async () => {
1611
+ // Publish multiple messages
1612
+ for (let i = 0; i < 10; i++) {
1613
+ await publisher.publish({
1614
+ id: `msg-${i}`,
1615
+ messageType: 'user.created',
1616
+ userId: `user-${i}`,
1617
+ email: `user${i}@example.com`,
1618
+ })
1619
+ }
1620
+
1621
+ // Wait for the last message
1622
+ await consumer.handlerSpy.waitForMessageWithId('msg-9', 'consumed')
1623
+
1624
+ // Inspect all processed messages
1625
+ const allMessages = consumer.handlerSpy.getAllMessages()
1626
+
1627
+ expect(allMessages.length).toBeGreaterThanOrEqual(10)
1628
+
1629
+ const successfulMessages = allMessages.filter(
1630
+ ({ processingResult }) => processingResult.status === 'consumed'
1631
+ )
1632
+
1633
+ expect(successfulMessages.length).toBe(10)
1634
+ })
1635
+ ```
1636
+
1637
+ #### Handler Spy API Reference
1638
+
1639
+ ```typescript
1640
+ interface HandlerSpy<Message> {
1641
+ // Wait for message by ID (with timeout)
1642
+ waitForMessageWithId(
1643
+ messageId: string,
1644
+ state: 'consumed' | 'published' | 'retryLater',
1645
+ timeout?: number // Default: 15000ms
1646
+ ): Promise<Message>
1647
+
1648
+ // Wait for message matching predicate (with timeout)
1649
+ waitForMessage(
1650
+ predicate: (message: Message) => boolean,
1651
+ state: 'consumed' | 'published' | 'retryLater',
1652
+ timeout?: number // Default: 15000ms
1653
+ ): Promise<Message>
1654
+
1655
+ // Check if message exists without waiting
1656
+ checkMessage(
1657
+ predicate: (message: Message) => boolean
1658
+ ): { message: Message; processingResult: ProcessingResult } | undefined
1659
+
1660
+ // Get all tracked messages (circular buffer, limited size)
1661
+ getAllMessages(): Array<{ message: Message; processingResult: ProcessingResult }>
1662
+ }
1663
+ ```
1664
+
1665
+ **Best Practices:**
1666
+
1667
+ 1. **Always set timeouts** - Tests can hang indefinitely if messages don't arrive
1668
+ 2. **Use specific predicates** - Avoid overly broad matchers that could match wrong messages
1669
+ 3. **Clean up between tests** - Reset handler spies or recreate publishers/consumers
1670
+ 4. **Use in integration tests** - Handler spies are most valuable for integration tests, not unit tests
1671
+ 5. **Don't use in production** - Handler spies add memory overhead (circular buffer of messages)
1672
+
1673
+ ## FIFO Queues
1674
+
1675
+ FIFO (First-In-First-Out) queues provide message ordering and exactly-once processing.
1676
+
1677
+ ### FIFO Queue Requirements
1678
+
1679
+ 1. **Queue name must end with `.fifo`**
1680
+ ```typescript
1681
+ QueueName: 'my-queue.fifo' // ✅ Valid
1682
+ QueueName: 'my-queue' // ❌ Invalid for FIFO
1683
+ ```
1684
+
1685
+ 2. **FifoQueue attribute must be 'true'**
1686
+ ```typescript
1687
+ Attributes: {
1688
+ FifoQueue: 'true',
1689
+ }
1690
+ ```
1691
+
1692
+ 3. **MessageGroupId required for all messages**
1693
+ ```typescript
1694
+ // Option 1: From message field
1695
+ messageGroupIdField: 'userId'
1696
+
1697
+ // Option 2: Default value
1698
+ defaultMessageGroupId: 'default-group'
1699
+
1700
+ // Option 3: Explicit in publish call
1701
+ await publisher.publish(message, {
1702
+ MessageGroupId: 'custom-group',
1703
+ })
1704
+ ```
1705
+
1706
+ 4. **DLQ must also be FIFO**
1707
+ ```typescript
1708
+ deadLetterQueue: {
1709
+ creationConfig: {
1710
+ queue: {
1711
+ QueueName: 'my-queue-dlq.fifo', // Must be FIFO
1712
+ Attributes: {
1713
+ FifoQueue: 'true',
1714
+ },
1715
+ },
1716
+ },
1717
+ }
1718
+ ```
1719
+
1720
+ ### Message Ordering
1721
+
1722
+ **Ordering guarantees:**
1723
+ - ✅ Messages within the same MessageGroupId are delivered in order
1724
+ - ✅ Messages are processed exactly once (no duplicates)
1725
+ - ❌ No ordering guarantee across different MessageGroupIds
1726
+
1727
+ **Example:**
1728
+
1729
+ ```typescript
1730
+ // All messages for user-123 are processed in order
1731
+ await publisher.publish({ id: '1', userId: 'user-123', action: 'create' })
1732
+ await publisher.publish({ id: '2', userId: 'user-123', action: 'update' })
1733
+ await publisher.publish({ id: '3', userId: 'user-123', action: 'delete' })
1734
+
1735
+ // Messages for user-456 can be processed in parallel
1736
+ await publisher.publish({ id: '4', userId: 'user-456', action: 'create' })
1737
+ ```
1738
+
1739
+ **Processing order:**
1740
+ ```
1741
+ Message 1 (user-123) → Message 2 (user-123) → Message 3 (user-123)
1742
+ Message 4 (user-456) can process in parallel with above
1743
+ ```
1744
+
1745
+ ### Message Groups
1746
+
1747
+ Message groups enable parallel processing while maintaining order:
1748
+
1749
+ **Scenario: E-commerce orders**
1750
+
1751
+ ```typescript
1752
+ // Each customer's orders are processed in order
1753
+ await publisher.publish({
1754
+ orderId: 'order-1',
1755
+ customerId: 'customer-A',
1756
+ action: 'created',
1757
+ }, {
1758
+ MessageGroupId: 'customer-A', // Group by customer
1759
+ })
1760
+
1761
+ await publisher.publish({
1762
+ orderId: 'order-2',
1763
+ customerId: 'customer-A',
1764
+ action: 'paid',
1765
+ }, {
1766
+ MessageGroupId: 'customer-A', // Same group - processed in order
1767
+ })
1768
+
1769
+ await publisher.publish({
1770
+ orderId: 'order-3',
1771
+ customerId: 'customer-B',
1772
+ action: 'created',
1773
+ }, {
1774
+ MessageGroupId: 'customer-B', // Different group - parallel processing
1775
+ })
1776
+ ```
1777
+
1778
+ **Group Assignment:**
1779
+ - AWS SQS automatically assigns groups to consumers
1780
+ - Assignment is dynamic (groups can move between consumers)
1781
+ - Assignment is sticky (SQS prefers consistency)
1782
+ - You cannot control which consumer processes which group
1783
+
1784
+ **Parallel Processing:**
1785
+
1786
+ ```typescript
1787
+ {
1788
+ concurrentConsumersAmount: 5, // 5 consumers can process 5 groups in parallel
1789
+ }
1790
+ ```
1791
+
1792
+ **Best Practices:**
1793
+
1794
+ 1. **Design groups for parallelism**
1795
+ ```typescript
1796
+ // ✅ Good: Many groups = high parallelism
1797
+ MessageGroupId: `customer-${customerId}`
1798
+
1799
+ // ❌ Bad: One group = no parallelism
1800
+ MessageGroupId: 'all-customers'
1801
+ ```
1802
+
1803
+ 2. **Balance group sizes**
1804
+ - Avoid one group having 90% of messages
1805
+ - Aim for relatively balanced message distribution
1806
+
1807
+ 3. **Size concurrent consumers appropriately**
1808
+ ```typescript
1809
+ // If you have 10 active customer groups at peak
1810
+ concurrentConsumersAmount: 10
1811
+ ```
1812
+
1813
+ ### FIFO-Specific Configuration
1814
+
1815
+ ```typescript
1816
+ // Publisher
1817
+ {
1818
+ fifoQueue: true,
1819
+
1820
+ // Choose one or more:
1821
+ messageGroupIdField: 'userId', // Use field from message
1822
+ defaultMessageGroupId: 'default', // Fallback value
1823
+ // or provide in publish call
1824
+ }
1825
+
1826
+ // Consumer
1827
+ {
1828
+ fifoQueue: true,
1829
+ concurrentConsumersAmount: 3, // Process 3 groups in parallel
1830
+
1831
+ // Note: Retry behavior is different for FIFO
1832
+ // - No DelaySeconds support (AWS limitation)
1833
+ // - Messages retry immediately
1834
+ // - Order is preserved
1835
+ }
1836
+
1837
+ // Queue Configuration
1838
+ {
1839
+ creationConfig: {
1840
+ queue: {
1841
+ QueueName: 'my-queue.fifo',
1842
+ Attributes: {
1843
+ FifoQueue: 'true',
1844
+
1845
+ // Optional: Automatic deduplication based on message body
1846
+ ContentBasedDeduplication: 'false', // or 'true'
1847
+
1848
+ // Optional: Deduplication scope
1849
+ DeduplicationScope: 'queue', // or 'messageGroup'
1850
+
1851
+ // Optional: Throughput limit
1852
+ FifoThroughputLimit: 'perQueue', // or 'perMessageGroupId'
1853
+ },
1854
+ },
1855
+ },
1856
+ }
1857
+ ```
1858
+
1859
+ **FIFO vs Standard Queues:**
1860
+
1861
+ | Feature | Standard Queue | FIFO Queue |
1862
+ |---------|---------------|------------|
1863
+ | Ordering | Best-effort | Guaranteed within group |
1864
+ | Delivery | At-least-once | Exactly-once |
1865
+ | Throughput | Unlimited | 3,000 msg/s (per queue) or 300 msg/s (per group) |
1866
+ | Retry Delay | Exponential backoff | Immediate (no delay) |
1867
+ | Message Groups | N/A | Required (MessageGroupId) |
1868
+ | Naming | Any name | Must end with `.fifo` |
1869
+
1870
+ ## Policy Configuration
1871
+
1872
+ Configure queue access policies for SNS topic subscriptions or cross-account access:
1873
+
1874
+ ### Allow SNS Topic to Send Messages
1875
+
1876
+ ```typescript
1877
+ import { SQS_RESOURCE_CURRENT_QUEUE } from '@message-queue-toolkit/sqs'
1878
+
1879
+ {
1880
+ creationConfig: {
1881
+ queue: { QueueName: 'my-queue' },
1882
+ policyConfig: {
1883
+ resource: SQS_RESOURCE_CURRENT_QUEUE, // Use queue's ARN
1884
+ statements: {
1885
+ Effect: 'Allow',
1886
+ Principal: '*',
1887
+ Action: ['sqs:SendMessage'],
1888
+ },
1889
+ },
1890
+ },
1891
+ }
1892
+ ```
1893
+
1894
+ ### Allow Specific SNS Topics
1895
+
1896
+ ```typescript
1897
+ {
1898
+ creationConfig: {
1899
+ queue: { QueueName: 'my-queue' },
1900
+ topicArnsWithPublishPermissionsPrefix: 'arn:aws:sns:us-east-1:123456789012:my-topic',
1901
+ },
1902
+ }
1903
+ ```
1904
+
1905
+ ### Custom Policy
1906
+
1907
+ ```typescript
1908
+ {
1909
+ creationConfig: {
1910
+ queue: { QueueName: 'my-queue' },
1911
+ policyConfig: {
1912
+ resource: 'arn:aws:sqs:us-east-1:123456789012:my-queue',
1913
+ statements: [
1914
+ {
1915
+ Effect: 'Allow',
1916
+ Principal: { AWS: 'arn:aws:iam::111111111111:root' },
1917
+ Action: ['sqs:SendMessage', 'sqs:ReceiveMessage'],
1918
+ },
1919
+ {
1920
+ Effect: 'Deny',
1921
+ Principal: '*',
1922
+ Action: ['sqs:DeleteQueue'],
1923
+ },
1924
+ ],
1925
+ },
1926
+ },
1927
+ }
1928
+ ```
1929
+
1930
+ ## Testing
1931
+
1932
+ The library is designed to be testable:
1933
+
1934
+ ### Integration Tests
1935
+
1936
+ ```typescript
1937
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
1938
+ import { SQSClient } from '@aws-sdk/client-sqs'
1939
+
1940
+ describe('UserEventsConsumer', () => {
1941
+ let sqsClient: SQSClient
1942
+ let publisher: UserEventsPublisher
1943
+ let consumer: UserEventsConsumer
1944
+
1945
+ beforeEach(async () => {
1946
+ sqsClient = new SQSClient({
1947
+ endpoint: 'http://localhost:4566', // LocalStack
1948
+ region: 'us-east-1',
1949
+ })
1950
+
1951
+ publisher = new UserEventsPublisher(sqsClient)
1952
+ consumer = new UserEventsConsumer(sqsClient, userService)
1953
+
1954
+ await publisher.init()
1955
+ await consumer.start()
1956
+ })
1957
+
1958
+ afterEach(async () => {
1959
+ await consumer.close()
1960
+ await publisher.close()
1961
+ })
1962
+
1963
+ it('processes user.created message', async () => {
1964
+ await publisher.publish({
1965
+ id: '123',
1966
+ messageType: 'user.created',
1967
+ userId: 'user-456',
1968
+ email: 'test@example.com',
1969
+ })
1970
+
1971
+ // Wait for message to be processed
1972
+ await consumer.handlerSpy.waitForMessageWithId('123', 'consumed')
1973
+
1974
+ // Verify side effects
1975
+ expect(userService.createUser).toHaveBeenCalledWith('user-456', 'test@example.com')
1976
+ })
1977
+
1978
+ it('retries failed messages', async () => {
1979
+ let attempts = 0
1980
+ userService.createUser.mockImplementation(() => {
1981
+ attempts++
1982
+ if (attempts < 3) throw new Error('Temporary failure')
1983
+ return Promise.resolve()
1984
+ })
1985
+
1986
+ await publisher.publish({
1987
+ id: '124',
1988
+ messageType: 'user.created',
1989
+ userId: 'user-789',
1990
+ email: 'test2@example.com',
1991
+ })
1992
+
1993
+ await consumer.handlerSpy.waitForMessageWithId('124', 'consumed')
1994
+
1995
+ expect(attempts).toBe(3)
1996
+ })
1997
+ })
1998
+ ```
1999
+
2000
+ ### Unit Tests with Handler Spies
2001
+
2002
+ ```typescript
2003
+ it('publishes message', async () => {
2004
+ await publisher.publish({
2005
+ id: '123',
2006
+ messageType: 'user.created',
2007
+ userId: 'user-456',
2008
+ email: 'test@example.com',
2009
+ })
2010
+
2011
+ const publishedMessage = await publisher.handlerSpy.waitForMessageWithId('123', 'published')
2012
+
2013
+ expect(publishedMessage).toMatchObject({
2014
+ id: '123',
2015
+ userId: 'user-456',
2016
+ })
2017
+ })
2018
+ ```
2019
+
2020
+ ## API Reference
2021
+
2022
+ ### AbstractSqsPublisher
2023
+
2024
+ ```typescript
2025
+ class AbstractSqsPublisher<MessagePayloadType extends object> {
2026
+ constructor(
2027
+ dependencies: SQSDependencies,
2028
+ options: SQSPublisherOptions<MessagePayloadType>
2029
+ )
2030
+
2031
+ async init(): Promise<void>
2032
+ async close(): Promise<void>
2033
+
2034
+ async publish(
2035
+ message: MessagePayloadType,
2036
+ options?: SQSMessageOptions
2037
+ ): Promise<void>
2038
+
2039
+ readonly handlerSpy: HandlerSpy<MessagePayloadType>
2040
+ }
2041
+ ```
2042
+
2043
+ ### AbstractSqsConsumer
2044
+
2045
+ ```typescript
2046
+ class AbstractSqsConsumer<
2047
+ MessagePayloadType extends object,
2048
+ ExecutionContext,
2049
+ PrehandlerOutput = undefined
2050
+ > {
2051
+ constructor(
2052
+ dependencies: SQSConsumerDependencies,
2053
+ options: SQSConsumerOptions<MessagePayloadType, ExecutionContext, PrehandlerOutput>,
2054
+ executionContext: ExecutionContext
2055
+ )
2056
+
2057
+ async init(): Promise<void>
2058
+ async start(): Promise<void>
2059
+ async close(abort?: boolean): Promise<void>
2060
+
2061
+ readonly handlerSpy: HandlerSpy<MessagePayloadType>
2062
+ }
2063
+ ```
2064
+
2065
+ ### Types
2066
+
2067
+ ```typescript
2068
+ // Message options for publishing
2069
+ type SQSMessageOptions = {
2070
+ MessageGroupId?: string // Required for FIFO queues
2071
+ MessageDeduplicationId?: string // Optional for FIFO queues
2072
+ }
2073
+
2074
+ // Handler result
2075
+ type HandlerResult = Either<'retryLater', 'success'>
2076
+
2077
+ // Dependencies
2078
+ type SQSDependencies = {
2079
+ sqsClient: SQSClient
2080
+ logger: Logger
2081
+ errorReporter: ErrorReporter
2082
+ messageMetricsManager?: MessageMetricsManager
2083
+ }
2084
+
2085
+ // Consumer dependencies (extends SQSDependencies)
2086
+ type SQSConsumerDependencies = SQSDependencies & {
2087
+ consumerErrorResolver: ErrorResolver
2088
+ transactionObservabilityManager: TransactionObservabilityManager
2089
+ }
2090
+ ```
2091
+
2092
+ ### Utility Functions
2093
+
2094
+ ```typescript
2095
+ // Queue validation
2096
+ function isFifoQueueName(queueName: string): boolean
2097
+ function validateFifoQueueName(queueName: string, isFifoQueue: boolean): void
2098
+ function validateFifoQueueConfiguration(
2099
+ queueName: string,
2100
+ attributes?: Record<string, string>,
2101
+ isFifoQueue?: boolean
2102
+ ): void
2103
+
2104
+ // Queue operations
2105
+ async function assertQueue(
2106
+ sqsClient: SQSClient,
2107
+ queueConfig: CreateQueueCommandInput,
2108
+ extraParams?: ExtraSQSCreationParams,
2109
+ isFifoQueue?: boolean
2110
+ ): Promise<{ queueUrl: string; queueArn: string; queueName: string }>
2111
+
2112
+ async function deleteQueue(
2113
+ client: SQSClient,
2114
+ queueName: string,
2115
+ waitForConfirmation?: boolean
2116
+ ): Promise<void>
2117
+
2118
+ async function getQueueUrl(
2119
+ sqsClient: SQSClient,
2120
+ queueName: string
2121
+ ): Promise<Either<'not_found', string>>
2122
+
2123
+ async function getQueueAttributes(
2124
+ sqsClient: SQSClient,
2125
+ queueUrl: string,
2126
+ attributeNames?: QueueAttributeName[]
2127
+ ): Promise<Either<'not_found', { attributes?: Record<string, string> }>>
2128
+
2129
+ // Message size calculation
2130
+ function calculateOutgoingMessageSize(message: unknown): number
2131
+ ```
2132
+
2133
+ ## License
2134
+
2135
+ MIT
2136
+
2137
+ ## Contributing
2138
+
2139
+ Contributions are welcome! Please see the main repository for guidelines.
2140
+
2141
+ ## Links
2142
+
2143
+ - [Main Repository](https://github.com/kibertoad/message-queue-toolkit)
2144
+ - [Core Package](https://www.npmjs.com/package/@message-queue-toolkit/core)
2145
+ - [AWS SQS Documentation](https://docs.aws.amazon.com/sqs/)
2146
+ - [FIFO Queue Documentation](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html)