@message-queue-toolkit/core 24.0.0 → 25.0.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,710 @@
1
+ # @message-queue-toolkit/core
2
+
3
+ Core library for message-queue-toolkit. Provides foundational abstractions, utilities, and base classes for building message queue publishers and consumers.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Overview](#overview)
9
+ - [Core Concepts](#core-concepts)
10
+ - [Message Schemas](#message-schemas)
11
+ - [Message Type Resolution](#message-type-resolution)
12
+ - [Handler Configuration](#handler-configuration)
13
+ - [Pre-handlers and Barriers](#pre-handlers-and-barriers)
14
+ - [Handler Spies](#handler-spies)
15
+ - [Key Classes](#key-classes)
16
+ - [AbstractQueueService](#abstractqueueservice)
17
+ - [MessageHandlerConfigBuilder](#messagehandlerconfigbuilder)
18
+ - [HandlerContainer](#handlercontainer)
19
+ - [MessageSchemaContainer](#messageschemacontainer)
20
+ - [AbstractPublisherManager](#abstractpublishermanager)
21
+ - [DomainEventEmitter](#domaineventemitter)
22
+ - [Utilities](#utilities)
23
+ - [Error Classes](#error-classes)
24
+ - [Message Deduplication](#message-deduplication)
25
+ - [Payload Offloading](#payload-offloading)
26
+ - [API Reference](#api-reference)
27
+ - [Links](#links)
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install @message-queue-toolkit/core zod
33
+ ```
34
+
35
+ **Peer Dependencies:**
36
+ - `zod` - Schema validation
37
+
38
+ ## Overview
39
+
40
+ The core package provides the foundational building blocks used by all protocol-specific implementations (SQS, SNS, AMQP, Kafka, GCP Pub/Sub). It includes:
41
+
42
+ - **Base Classes**: Abstract classes for publishers and consumers
43
+ - **Handler System**: Type-safe message routing and handling
44
+ - **Validation**: Zod schema validation and message parsing
45
+ - **Utilities**: Retry logic, date handling, environment utilities
46
+ - **Testing**: Handler spies for testing async message flows
47
+ - **Extensibility**: Interfaces for payload stores, deduplication stores, and metrics
48
+
49
+ ## Core Concepts
50
+
51
+ ### Message Schemas
52
+
53
+ Messages are validated using Zod schemas. The library uses configurable field names:
54
+
55
+ - **`messageTypeResolver`**: Configuration for resolving the message type discriminator (see [Message Type Resolution](#message-type-resolution))
56
+ - **`messageIdField`** (default: `'id'`): Field containing the message ID
57
+ - **`messageTimestampField`** (default: `'timestamp'`): Field containing the timestamp
58
+
59
+ ```typescript
60
+ import { z } from 'zod'
61
+
62
+ const UserCreatedSchema = z.object({
63
+ id: z.string(),
64
+ type: z.literal('user.created'), // Used for routing
65
+ timestamp: z.string().datetime(),
66
+ userId: z.string(),
67
+ email: z.string().email(),
68
+ })
69
+
70
+ type UserCreated = z.infer<typeof UserCreatedSchema>
71
+ ```
72
+
73
+ ### Message Type Resolution
74
+
75
+ #### What is Message Type?
76
+
77
+ The **message type** is a discriminator field that identifies what kind of event or command a message represents. It's used for:
78
+
79
+ 1. **Routing**: Directing messages to the appropriate handler based on their type
80
+ 2. **Schema validation**: Selecting the correct Zod schema to validate the message
81
+ 3. **Observability**: Tracking metrics and logs per message type
82
+
83
+ In a typical event-driven architecture, a single queue or topic may receive multiple types of messages. For example, a `user-events` queue might receive `user.created`, `user.updated`, and `user.deleted` events. The message type tells the consumer which handler should process each message.
84
+
85
+ #### Configuration Options
86
+
87
+ The `messageTypeResolver` configuration supports three modes:
88
+
89
+ ##### Mode 1: Field Path (Simple)
90
+
91
+ Use when the message type is a field in the parsed message body. Supports dot notation for nested paths:
92
+
93
+ ```typescript
94
+ {
95
+ messageTypeResolver: { messageTypePath: 'type' }, // Extracts from message.type
96
+ }
97
+
98
+ // Nested path example
99
+ {
100
+ messageTypeResolver: { messageTypePath: 'metadata.eventType' }, // Extracts from message.metadata.eventType
101
+ }
102
+ ```
103
+
104
+ ##### Mode 2: Literal (Constant)
105
+
106
+ Use when all messages are of the same type:
107
+
108
+ ```typescript
109
+ {
110
+ messageTypeResolver: { literal: 'order.created' }, // All messages treated as this type
111
+ }
112
+ ```
113
+
114
+ ##### Mode 3: Custom Resolver (Flexible)
115
+
116
+ Use for complex scenarios where the type needs to be extracted from message attributes, nested fields, or requires transformation:
117
+
118
+ ```typescript
119
+ import type { MessageTypeResolverConfig } from '@message-queue-toolkit/core'
120
+
121
+ const resolverConfig: MessageTypeResolverConfig = {
122
+ resolver: ({ messageData, messageAttributes }) => {
123
+ // Your custom logic here
124
+ return 'resolved.type'
125
+ },
126
+ }
127
+ ```
128
+
129
+ **Important:** The resolver function must always return a valid string. If the type cannot be determined, either return a default type or throw an error with a descriptive message.
130
+
131
+ #### Real-World Examples by Platform
132
+
133
+ ##### AWS SQS (Plain)
134
+
135
+ When publishing your own events directly to SQS, you control the message format:
136
+
137
+ ```typescript
138
+ // Message format you control
139
+ {
140
+ "id": "msg-123",
141
+ "type": "order.created", // Your type field
142
+ "timestamp": "2024-01-15T10:30:00Z",
143
+ "payload": {
144
+ "orderId": "order-456",
145
+ "amount": 99.99
146
+ }
147
+ }
148
+
149
+ // Configuration
150
+ {
151
+ messageTypeResolver: { messageTypePath: 'type' },
152
+ }
153
+ ```
154
+
155
+ ##### AWS EventBridge → SQS
156
+
157
+ EventBridge events have a specific structure with `detail-type`:
158
+
159
+ ```typescript
160
+ // EventBridge event structure delivered to SQS
161
+ {
162
+ "version": "0",
163
+ "id": "12345678-1234-1234-1234-123456789012",
164
+ "detail-type": "Order Created", // EventBridge uses detail-type
165
+ "source": "com.myapp.orders",
166
+ "account": "123456789012",
167
+ "time": "2024-01-15T10:30:00Z",
168
+ "region": "us-east-1",
169
+ "detail": {
170
+ "orderId": "order-456",
171
+ "amount": 99.99
172
+ }
173
+ }
174
+
175
+ // Configuration
176
+ {
177
+ messageTypeResolver: { messageTypePath: 'detail-type' },
178
+ }
179
+
180
+ // Or with resolver for normalization
181
+ {
182
+ messageTypeResolver: {
183
+ resolver: ({ messageData }) => {
184
+ const data = messageData as { 'detail-type'?: string; source?: string }
185
+ const detailType = data['detail-type']
186
+ if (!detailType) throw new Error('detail-type is required')
187
+ // Optionally normalize: "Order Created" → "order.created"
188
+ return detailType.toLowerCase().replace(/ /g, '.')
189
+ },
190
+ },
191
+ }
192
+ ```
193
+
194
+ ##### AWS SNS → SQS
195
+
196
+ SNS messages wrapped in SQS have the actual payload in the `Message` field (handled automatically by the library after unwrapping):
197
+
198
+ ```typescript
199
+ // After SNS envelope unwrapping, you get your original message
200
+ {
201
+ "id": "msg-123",
202
+ "type": "user.signup.completed",
203
+ "userId": "user-789",
204
+ "email": "user@example.com"
205
+ }
206
+
207
+ // Configuration
208
+ {
209
+ messageTypeResolver: { messageTypePath: 'type' },
210
+ }
211
+ ```
212
+
213
+ ##### Apache Kafka
214
+
215
+ Kafka typically uses topic-based routing, but you may still need message types within a topic:
216
+
217
+ ```typescript
218
+ // Kafka message value (JSON)
219
+ {
220
+ "eventType": "inventory.reserved",
221
+ "eventId": "evt-123",
222
+ "timestamp": 1705312200000,
223
+ "data": {
224
+ "sku": "PROD-001",
225
+ "quantity": 5
226
+ }
227
+ }
228
+
229
+ // Configuration
230
+ {
231
+ messageTypeResolver: { messageTypePath: 'eventType' },
232
+ }
233
+
234
+ // Or using Kafka headers (via custom resolver)
235
+ {
236
+ messageTypeResolver: {
237
+ resolver: ({ messageData, messageAttributes }) => {
238
+ // Kafka headers are passed as messageAttributes
239
+ if (messageAttributes?.['ce_type']) {
240
+ return messageAttributes['ce_type'] as string // CloudEvents header
241
+ }
242
+ const data = messageData as { eventType?: string }
243
+ if (!data.eventType) throw new Error('eventType required')
244
+ return data.eventType
245
+ },
246
+ },
247
+ }
248
+ ```
249
+
250
+ ##### Google Cloud Pub/Sub (Your Own Events)
251
+
252
+ When you control the message format in Pub/Sub:
253
+
254
+ ```typescript
255
+ // Your message (base64-decoded from data field)
256
+ {
257
+ "type": "payment.processed",
258
+ "paymentId": "pay-123",
259
+ "amount": 150.00,
260
+ "currency": "USD"
261
+ }
262
+
263
+ // Configuration
264
+ {
265
+ messageTypeResolver: { messageTypePath: 'type' },
266
+ }
267
+ ```
268
+
269
+ ##### Google Cloud Pub/Sub (Cloud Storage Notifications)
270
+
271
+ Cloud Storage notifications put the event type in message **attributes**, not the data payload:
272
+
273
+ ```typescript
274
+ // Pub/Sub message structure for Cloud Storage notifications
275
+ {
276
+ "data": "eyJraW5kIjoic3RvcmFnZSMgb2JqZWN0In0=", // Base64-encoded object metadata
277
+ "attributes": {
278
+ "eventType": "OBJECT_FINALIZE", // Type is HERE, not in data!
279
+ "bucketId": "my-bucket",
280
+ "objectId": "path/to/file.jpg",
281
+ "objectGeneration": "1705312200000"
282
+ },
283
+ "messageId": "123456789",
284
+ "publishTime": "2024-01-15T10:30:00Z"
285
+ }
286
+
287
+ // Configuration - must use resolver to access attributes
288
+ {
289
+ messageTypeResolver: {
290
+ resolver: ({ messageAttributes }) => {
291
+ const eventType = messageAttributes?.eventType as string
292
+ if (!eventType) {
293
+ throw new Error('eventType attribute required for Cloud Storage notifications')
294
+ }
295
+ // Map GCS event types to your internal types
296
+ const typeMap: Record<string, string> = {
297
+ 'OBJECT_FINALIZE': 'storage.object.created',
298
+ 'OBJECT_DELETE': 'storage.object.deleted',
299
+ 'OBJECT_ARCHIVE': 'storage.object.archived',
300
+ 'OBJECT_METADATA_UPDATE': 'storage.object.metadataUpdated',
301
+ }
302
+ return typeMap[eventType] ?? eventType
303
+ },
304
+ },
305
+ }
306
+ ```
307
+
308
+ ##### Google Cloud Pub/Sub (Eventarc / CloudEvents)
309
+
310
+ Eventarc delivers events in CloudEvents format:
311
+
312
+ ```typescript
313
+ // CloudEvents structured format
314
+ {
315
+ "specversion": "1.0",
316
+ "type": "google.cloud.storage.object.v1.finalized", // CloudEvents type
317
+ "source": "//storage.googleapis.com/projects/_/buckets/my-bucket",
318
+ "id": "1234567890",
319
+ "time": "2024-01-15T10:30:00Z",
320
+ "datacontenttype": "application/json",
321
+ "data": {
322
+ "bucket": "my-bucket",
323
+ "name": "path/to/file.jpg",
324
+ "contentType": "image/jpeg"
325
+ }
326
+ }
327
+
328
+ // Configuration
329
+ {
330
+ messageTypeResolver: { messageTypePath: 'type' }, // CloudEvents type is at root level
331
+ }
332
+
333
+ // Or with mapping to simpler types
334
+ {
335
+ messageTypeResolver: {
336
+ resolver: ({ messageData }) => {
337
+ const data = messageData as { type?: string }
338
+ const ceType = data.type
339
+ if (!ceType) throw new Error('CloudEvents type required')
340
+ // Map verbose CloudEvents types to simpler names
341
+ if (ceType.includes('storage.object') && ceType.includes('finalized')) {
342
+ return 'storage.object.created'
343
+ }
344
+ if (ceType.includes('storage.object') && ceType.includes('deleted')) {
345
+ return 'storage.object.deleted'
346
+ }
347
+ return ceType
348
+ },
349
+ },
350
+ }
351
+ ```
352
+
353
+ ##### Single-Type Queues (Any Platform)
354
+
355
+ When a queue/subscription only ever receives one type of message, use `literal`:
356
+
357
+ ```typescript
358
+ // Dedicated queue for order.created events only
359
+ {
360
+ messageTypeResolver: {
361
+ literal: 'order.created',
362
+ },
363
+ }
364
+ ```
365
+
366
+ This is useful for:
367
+ - Dedicated queues/subscriptions filtered to a single event type
368
+ - Legacy systems where messages don't have a type field
369
+ - Simple integrations where you know exactly what you're receiving
370
+
371
+ ### Handler Configuration
372
+
373
+ Use `MessageHandlerConfigBuilder` to configure handlers for different message types:
374
+
375
+ ```typescript
376
+ import { MessageHandlerConfigBuilder } from '@message-queue-toolkit/core'
377
+
378
+ const handlers = new MessageHandlerConfigBuilder<SupportedMessages, ExecutionContext>()
379
+ .addConfig(
380
+ UserCreatedSchema,
381
+ async (message, context, preHandlingOutputs) => {
382
+ await context.userService.createUser(message.userId)
383
+ return { result: 'success' }
384
+ }
385
+ )
386
+ .addConfig(
387
+ UserUpdatedSchema,
388
+ async (message, context, preHandlingOutputs) => {
389
+ await context.userService.updateUser(message.userId, message.changes)
390
+ return { result: 'success' }
391
+ }
392
+ )
393
+ .build()
394
+ ```
395
+
396
+ ### Pre-handlers and Barriers
397
+
398
+ **Pre-handlers** are middleware functions that run before the main handler:
399
+
400
+ ```typescript
401
+ const preHandlers = [
402
+ (message, context, output, next) => {
403
+ // Enrich context, validate prerequisites, etc.
404
+ output.logger = context.logger.child({ messageId: message.id })
405
+ next({ result: 'success' })
406
+ },
407
+ ]
408
+ ```
409
+
410
+ **Barriers** control whether a message should be processed or retried later:
411
+
412
+ ```typescript
413
+ const preHandlerBarrier = async (message, context, preHandlerOutput) => {
414
+ const prerequisiteMet = await checkPrerequisite(message)
415
+ return {
416
+ isPassing: prerequisiteMet,
417
+ output: { ready: true },
418
+ }
419
+ }
420
+ ```
421
+
422
+ ### Handler Spies
423
+
424
+ Handler spies enable testing of async message flows:
425
+
426
+ ```typescript
427
+ // Enable in consumer/publisher options
428
+ { handlerSpy: true }
429
+
430
+ // Wait for specific messages in tests
431
+ const result = await consumer.handlerSpy.waitForMessageWithId('msg-123', 'consumed', 5000)
432
+ expect(result.userId).toBe('user-456')
433
+ ```
434
+
435
+ ## Key Classes
436
+
437
+ ### AbstractQueueService
438
+
439
+ Base class for all queue services. Provides:
440
+
441
+ - Message serialization/deserialization
442
+ - Schema validation
443
+ - Retry logic with exponential backoff
444
+ - Payload offloading support
445
+ - Message deduplication primitives
446
+
447
+ ### MessageHandlerConfigBuilder
448
+
449
+ Fluent builder for configuring message handlers:
450
+
451
+ ```typescript
452
+ import { MessageHandlerConfigBuilder } from '@message-queue-toolkit/core'
453
+
454
+ const handlers = new MessageHandlerConfigBuilder<
455
+ SupportedMessages,
456
+ ExecutionContext,
457
+ PrehandlerOutput
458
+ >()
459
+ .addConfig(Schema1, handler1)
460
+ .addConfig(Schema2, handler2, {
461
+ preHandlers: [preHandler1, preHandler2],
462
+ preHandlerBarrier: barrierFn,
463
+ messageLogFormatter: (msg) => ({ id: msg.id }),
464
+ })
465
+ .build()
466
+ ```
467
+
468
+ #### Handler Configuration Options
469
+
470
+ The third parameter to `addConfig` accepts these options:
471
+
472
+ | Option | Type | Description |
473
+ |--------|------|-------------|
474
+ | `messageType` | `string` | Explicit message type for routing. Required when using custom resolver. |
475
+ | `messageLogFormatter` | `(message) => unknown` | Custom formatter for logging |
476
+ | `preHandlers` | `Prehandler[]` | Middleware functions run before the handler |
477
+ | `preHandlerBarrier` | `BarrierCallback` | Barrier function for out-of-order message handling |
478
+
479
+ #### Explicit Message Type
480
+
481
+ When using a custom resolver function (`messageTypeResolver: { resolver: fn }`), the message type cannot be automatically extracted from schemas at registration time. You must provide an explicit `messageType` for each handler:
482
+
483
+ ```typescript
484
+ const handlers = new MessageHandlerConfigBuilder<SupportedMessages, Context>()
485
+ .addConfig(
486
+ STORAGE_OBJECT_SCHEMA,
487
+ handleObjectCreated,
488
+ { messageType: 'storage.object.created' } // Required for custom resolver
489
+ )
490
+ .addConfig(
491
+ STORAGE_DELETE_SCHEMA,
492
+ handleObjectDeleted,
493
+ { messageType: 'storage.object.deleted' } // Required for custom resolver
494
+ )
495
+ .build()
496
+
497
+ const container = new HandlerContainer({
498
+ messageHandlers: handlers,
499
+ messageTypeResolver: {
500
+ resolver: ({ messageAttributes }) => {
501
+ // Map external event types to your internal types
502
+ const eventType = messageAttributes?.eventType as string
503
+ if (eventType === 'OBJECT_FINALIZE') return 'storage.object.created'
504
+ if (eventType === 'OBJECT_DELETE') return 'storage.object.deleted'
505
+ throw new Error(`Unknown event type: ${eventType}`)
506
+ },
507
+ },
508
+ })
509
+ ```
510
+
511
+ **Priority for determining handler message type:**
512
+ 1. Explicit `messageType` in handler options (highest priority)
513
+ 2. Literal type from `messageTypeResolver: { literal: 'type' }`
514
+ 3. Extract from schema's literal field using `messageTypePath`
515
+
516
+ If the message type cannot be determined, an error is thrown during container construction.
517
+
518
+ ### HandlerContainer
519
+
520
+ Routes messages to appropriate handlers based on message type:
521
+
522
+ ```typescript
523
+ import { HandlerContainer } from '@message-queue-toolkit/core'
524
+
525
+ const container = new HandlerContainer({
526
+ messageHandlers: handlers,
527
+ messageTypeResolver: { messageTypePath: 'type' },
528
+ })
529
+
530
+ const handler = container.resolveHandler(message.type)
531
+ ```
532
+
533
+ ### MessageSchemaContainer
534
+
535
+ Manages Zod schemas and validates messages:
536
+
537
+ ```typescript
538
+ import { MessageSchemaContainer } from '@message-queue-toolkit/core'
539
+
540
+ const container = new MessageSchemaContainer({
541
+ messageSchemas: [{ schema: Schema1 }, { schema: Schema2 }],
542
+ messageDefinitions: [],
543
+ messageTypeResolver: { messageTypePath: 'type' },
544
+ })
545
+
546
+ const result = container.resolveSchema(message)
547
+ if ('error' in result) {
548
+ // Handle error
549
+ } else {
550
+ const schema = result.result
551
+ }
552
+ ```
553
+
554
+ ### AbstractPublisherManager
555
+
556
+ Factory pattern for spawning publishers on demand:
557
+
558
+ ```typescript
559
+ import { AbstractPublisherManager } from '@message-queue-toolkit/core'
560
+
561
+ // Automatically spawns publishers and fills metadata
562
+ await publisherManager.publish('user-events-topic', {
563
+ type: 'user.created',
564
+ userId: 'user-123',
565
+ })
566
+ ```
567
+
568
+ ### DomainEventEmitter
569
+
570
+ Event emitter for domain events:
571
+
572
+ ```typescript
573
+ import { DomainEventEmitter } from '@message-queue-toolkit/core'
574
+
575
+ const emitter = new DomainEventEmitter()
576
+
577
+ emitter.on('user.created', async (event) => {
578
+ console.log('User created:', event.userId)
579
+ })
580
+
581
+ await emitter.emit('user.created', { userId: 'user-123' })
582
+ ```
583
+
584
+ ## Utilities
585
+
586
+ ### Error Classes
587
+
588
+ ```typescript
589
+ import {
590
+ MessageValidationError,
591
+ MessageInvalidFormatError,
592
+ DoNotProcessMessageError,
593
+ RetryMessageLaterError,
594
+ } from '@message-queue-toolkit/core'
595
+
596
+ // Validation failed
597
+ throw new MessageValidationError(zodError.issues)
598
+
599
+ // Message format is invalid (cannot parse)
600
+ throw new MessageInvalidFormatError({ message: 'Invalid JSON' })
601
+
602
+ // Do not process this message (skip without retry)
603
+ throw new DoNotProcessMessageError({ message: 'Duplicate message' })
604
+
605
+ // Retry this message later
606
+ throw new RetryMessageLaterError({ message: 'Dependency not ready' })
607
+ ```
608
+
609
+ ### Message Deduplication
610
+
611
+ Interfaces for implementing deduplication stores:
612
+
613
+ ```typescript
614
+ import type { MessageDeduplicationStore, ReleasableLock } from '@message-queue-toolkit/core'
615
+
616
+ // Implement custom deduplication store
617
+ class MyDeduplicationStore implements MessageDeduplicationStore {
618
+ async keyExists(key: string): Promise<boolean> { /* ... */ }
619
+ async setKey(key: string, ttlSeconds: number): Promise<void> { /* ... */ }
620
+ async acquireLock(key: string, options: AcquireLockOptions): Promise<ReleasableLock> { /* ... */ }
621
+ }
622
+ ```
623
+
624
+ ### Payload Offloading
625
+
626
+ Interfaces for implementing payload stores:
627
+
628
+ ```typescript
629
+ import type { PayloadStore, PayloadStoreConfig } from '@message-queue-toolkit/core'
630
+
631
+ // Implement custom payload store
632
+ class MyPayloadStore implements PayloadStore {
633
+ async storePayload(payload: Buffer, messageId: string): Promise<PayloadRef> { /* ... */ }
634
+ async retrievePayload(ref: PayloadRef): Promise<Buffer> { /* ... */ }
635
+ }
636
+ ```
637
+
638
+ ## API Reference
639
+
640
+ ### Types
641
+
642
+ ```typescript
643
+ // Handler result type
644
+ type HandlerResult = Either<'retryLater', 'success'>
645
+
646
+ // Pre-handler signature
647
+ type Prehandler<Message, Context, Output> = (
648
+ message: Message,
649
+ context: Context,
650
+ output: Output,
651
+ next: (result: PrehandlerResult) => void
652
+ ) => void
653
+
654
+ // Barrier signature
655
+ type BarrierCallback<Message, Context, PrehandlerOutput, BarrierOutput> = (
656
+ message: Message,
657
+ context: Context,
658
+ preHandlerOutput: PrehandlerOutput
659
+ ) => Promise<BarrierResult<BarrierOutput>>
660
+
661
+ // Barrier result
662
+ type BarrierResult<Output> =
663
+ | { isPassing: true; output: Output }
664
+ | { isPassing: false; output?: never }
665
+
666
+ // Message type resolver context
667
+ type MessageTypeResolverContext = {
668
+ messageData: unknown
669
+ messageAttributes?: Record<string, unknown>
670
+ }
671
+
672
+ // Message type resolver function
673
+ type MessageTypeResolverFn = (context: MessageTypeResolverContext) => string
674
+
675
+ // Message type resolver configuration
676
+ type MessageTypeResolverConfig =
677
+ | { messageTypePath: string } // Extract from field at root of message data
678
+ | { literal: string } // Constant type for all messages
679
+ | { resolver: MessageTypeResolverFn } // Custom resolver function
680
+ ```
681
+
682
+ ### Utility Functions
683
+
684
+ ```typescript
685
+ // Environment utilities
686
+ isProduction(): boolean
687
+ reloadConfig(): void
688
+
689
+ // Date utilities
690
+ isRetryDateExceeded(timestamp: string | Date, maxRetryDuration: number): boolean
691
+
692
+ // Message parsing
693
+ parseMessage<T>(data: unknown, schema: ZodSchema<T>): ParseMessageResult<T>
694
+
695
+ // Wait utilities
696
+ waitAndRetry<T>(fn: () => Promise<T>, options: WaitAndRetryOptions): Promise<T>
697
+
698
+ // Object utilities
699
+ objectMatches(obj: unknown, pattern: unknown): boolean
700
+ isShallowSubset(subset: object, superset: object): boolean
701
+ ```
702
+
703
+ ## Links
704
+
705
+ - [Main Repository](https://github.com/kibertoad/message-queue-toolkit)
706
+ - [SQS Package](https://www.npmjs.com/package/@message-queue-toolkit/sqs)
707
+ - [SNS Package](https://www.npmjs.com/package/@message-queue-toolkit/sns)
708
+ - [AMQP Package](https://www.npmjs.com/package/@message-queue-toolkit/amqp)
709
+ - [GCP Pub/Sub Package](https://www.npmjs.com/package/@message-queue-toolkit/gcp-pubsub)
710
+ - [Kafka Package](https://www.npmjs.com/package/@message-queue-toolkit/kafka)
@@ -23,7 +23,7 @@ export declare class DomainEventEmitter<SupportedEvents extends CommonEventDefin
23
23
  constructor(deps: DomainEventEmitterDependencies<SupportedEvents>, options?: {
24
24
  handlerSpy?: HandlerSpy<object> | HandlerSpyParams | boolean;
25
25
  });
26
- get handlerSpy(): PublicHandlerSpy<CommonEventDefinitionPublisherSchemaType<SupportedEvents[number]>>;
26
+ get handlerSpy(): PublicHandlerSpy<CommonEventDefinitionConsumerSchemaType<SupportedEvents[number]>>;
27
27
  dispose(): Promise<void>;
28
28
  emit<SupportedEvent extends SupportedEvents[number]>(supportedEvent: SupportedEvent, data: Omit<CommonEventDefinitionPublisherSchemaType<SupportedEvent>, 'type'>, precedingMessageMetadata?: Partial<ConsumerMessageMetadataType>): Promise<Omit<CommonEventDefinitionConsumerSchemaType<SupportedEvent>, 'type'>>;
29
29
  /**