@amqp-contract/contract 0.10.0 → 0.11.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/dist/index.cjs +219 -33
- package/dist/index.d.cts +556 -68
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +556 -68
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +217 -33
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +534 -92
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -26,33 +26,30 @@ function defineExchange(name, type, options) {
|
|
|
26
26
|
* A queue stores messages until they are consumed by workers. Queues can be bound to exchanges
|
|
27
27
|
* to receive messages based on routing rules.
|
|
28
28
|
*
|
|
29
|
+
* By default, queues are created as quorum queues which provide better durability and
|
|
30
|
+
* high-availability. Use `type: 'classic'` for special cases like non-durable queues
|
|
31
|
+
* or priority queues.
|
|
32
|
+
*
|
|
29
33
|
* @param name - The name of the queue
|
|
30
34
|
* @param options - Optional queue configuration
|
|
31
|
-
* @param options.
|
|
32
|
-
* @param options.
|
|
35
|
+
* @param options.type - Queue type: 'quorum' (default, recommended) or 'classic'
|
|
36
|
+
* @param options.durable - If true, the queue survives broker restarts. Quorum queues are always durable.
|
|
37
|
+
* @param options.exclusive - If true, the queue can only be used by the declaring connection. Only supported with classic queues.
|
|
33
38
|
* @param options.autoDelete - If true, the queue is deleted when the last consumer unsubscribes (default: false)
|
|
34
39
|
* @param options.deadLetter - Dead letter configuration for handling failed messages
|
|
35
|
-
* @param options.maxPriority - Maximum priority level for priority queue (1-255, recommended: 1-10).
|
|
40
|
+
* @param options.maxPriority - Maximum priority level for priority queue (1-255, recommended: 1-10). Only supported with classic queues.
|
|
36
41
|
* @param options.arguments - Additional AMQP arguments (e.g., x-message-ttl)
|
|
37
42
|
* @returns A queue definition
|
|
38
43
|
*
|
|
39
44
|
* @example
|
|
40
45
|
* ```typescript
|
|
41
|
-
* //
|
|
42
|
-
* const orderQueue = defineQueue('order-processing'
|
|
43
|
-
* durable: true,
|
|
44
|
-
* });
|
|
45
|
-
*
|
|
46
|
-
* // Priority queue with max priority of 10
|
|
47
|
-
* const taskQueue = defineQueue('urgent-tasks', {
|
|
48
|
-
* durable: true,
|
|
49
|
-
* maxPriority: 10,
|
|
50
|
-
* });
|
|
46
|
+
* // Quorum queue (default, recommended for production)
|
|
47
|
+
* const orderQueue = defineQueue('order-processing');
|
|
51
48
|
*
|
|
52
|
-
* //
|
|
49
|
+
* // Explicit quorum queue with dead letter exchange
|
|
53
50
|
* const dlx = defineExchange('orders-dlx', 'topic', { durable: true });
|
|
54
51
|
* const orderQueueWithDLX = defineQueue('order-processing', {
|
|
55
|
-
*
|
|
52
|
+
* type: 'quorum',
|
|
56
53
|
* deadLetter: {
|
|
57
54
|
* exchange: dlx,
|
|
58
55
|
* routingKey: 'order.failed'
|
|
@@ -61,24 +58,92 @@ function defineExchange(name, type, options) {
|
|
|
61
58
|
* 'x-message-ttl': 86400000, // 24 hours
|
|
62
59
|
* }
|
|
63
60
|
* });
|
|
61
|
+
*
|
|
62
|
+
* // Classic queue (for special cases)
|
|
63
|
+
* const tempQueue = defineQueue('temp-queue', {
|
|
64
|
+
* type: 'classic',
|
|
65
|
+
* durable: false,
|
|
66
|
+
* autoDelete: true,
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* // Priority queue (requires classic type)
|
|
70
|
+
* const taskQueue = defineQueue('urgent-tasks', {
|
|
71
|
+
* type: 'classic',
|
|
72
|
+
* durable: true,
|
|
73
|
+
* maxPriority: 10,
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Queue with TTL-backoff retry (returns infrastructure automatically)
|
|
77
|
+
* const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
|
|
78
|
+
* const orderQueue = defineQueue('order-processing', {
|
|
79
|
+
* deadLetter: { exchange: dlx },
|
|
80
|
+
* retry: { mode: 'ttl-backoff', maxRetries: 5 },
|
|
81
|
+
* });
|
|
82
|
+
* // orderQueue is QueueWithTtlBackoffInfrastructure, pass directly to defineContract
|
|
64
83
|
* ```
|
|
65
84
|
*/
|
|
66
85
|
function defineQueue(name, options) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
const opts = options ?? {};
|
|
87
|
+
const type = opts.type ?? "quorum";
|
|
88
|
+
const baseProps = { name };
|
|
89
|
+
if (opts.durable !== void 0) baseProps.durable = opts.durable;
|
|
90
|
+
if (opts.autoDelete !== void 0) baseProps.autoDelete = opts.autoDelete;
|
|
91
|
+
if (opts.deadLetter !== void 0) baseProps.deadLetter = opts.deadLetter;
|
|
92
|
+
if (opts.arguments !== void 0) baseProps.arguments = opts.arguments;
|
|
93
|
+
if (type === "quorum") {
|
|
94
|
+
const quorumOpts = opts;
|
|
95
|
+
const queueDefinition$1 = {
|
|
96
|
+
...baseProps,
|
|
97
|
+
type: "quorum"
|
|
98
|
+
};
|
|
99
|
+
if (quorumOpts.deliveryLimit !== void 0) {
|
|
100
|
+
if (quorumOpts.deliveryLimit < 1 || !Number.isInteger(quorumOpts.deliveryLimit)) throw new Error(`Invalid deliveryLimit: ${quorumOpts.deliveryLimit}. Must be a positive integer.`);
|
|
101
|
+
queueDefinition$1.deliveryLimit = quorumOpts.deliveryLimit;
|
|
102
|
+
}
|
|
103
|
+
if (quorumOpts.retry !== void 0) queueDefinition$1.retry = quorumOpts.retry;
|
|
104
|
+
if (quorumOpts.retry?.mode === "ttl-backoff" && queueDefinition$1.deadLetter) return wrapWithTtlBackoffInfrastructure(queueDefinition$1);
|
|
105
|
+
return queueDefinition$1;
|
|
106
|
+
}
|
|
107
|
+
const classicOpts = opts;
|
|
108
|
+
const queueDefinition = {
|
|
109
|
+
...baseProps,
|
|
110
|
+
type: "classic"
|
|
111
|
+
};
|
|
112
|
+
if (classicOpts.exclusive !== void 0) queueDefinition.exclusive = classicOpts.exclusive;
|
|
113
|
+
if (classicOpts.maxPriority !== void 0) {
|
|
114
|
+
if (classicOpts.maxPriority < 1 || classicOpts.maxPriority > 255) throw new Error(`Invalid maxPriority: ${classicOpts.maxPriority}. Must be between 1 and 255. Recommended range: 1-10.`);
|
|
115
|
+
queueDefinition.arguments = {
|
|
116
|
+
...queueDefinition.arguments,
|
|
117
|
+
"x-max-priority": classicOpts.maxPriority
|
|
77
118
|
};
|
|
78
119
|
}
|
|
120
|
+
if (classicOpts.retry !== void 0) queueDefinition.retry = classicOpts.retry;
|
|
121
|
+
if (classicOpts.retry?.mode === "ttl-backoff" && queueDefinition.deadLetter) return wrapWithTtlBackoffInfrastructure(queueDefinition);
|
|
122
|
+
return queueDefinition;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Wrap a queue definition with TTL-backoff retry infrastructure.
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
function wrapWithTtlBackoffInfrastructure(queue) {
|
|
129
|
+
if (!queue.deadLetter) throw new Error(`Queue "${queue.name}" does not have a dead letter exchange configured. TTL-backoff retry requires deadLetter to be set on the queue.`);
|
|
130
|
+
const dlx = queue.deadLetter.exchange;
|
|
131
|
+
const waitQueueName = `${queue.name}-wait`;
|
|
132
|
+
const waitQueue = {
|
|
133
|
+
name: waitQueueName,
|
|
134
|
+
type: "quorum",
|
|
135
|
+
durable: queue.durable ?? true,
|
|
136
|
+
deadLetter: {
|
|
137
|
+
exchange: dlx,
|
|
138
|
+
routingKey: queue.name
|
|
139
|
+
}
|
|
140
|
+
};
|
|
79
141
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
142
|
+
__brand: "QueueWithTtlBackoffInfrastructure",
|
|
143
|
+
queue,
|
|
144
|
+
waitQueue,
|
|
145
|
+
waitQueueBinding: callDefineQueueBinding(waitQueue, dlx, { routingKey: waitQueueName }),
|
|
146
|
+
mainQueueRetryBinding: callDefineQueueBinding(queue, dlx, { routingKey: queue.name })
|
|
82
147
|
};
|
|
83
148
|
}
|
|
84
149
|
/**
|
|
@@ -127,22 +192,23 @@ function defineMessage(payload, options) {
|
|
|
127
192
|
*
|
|
128
193
|
* This is the implementation function - use the type-specific overloads for better type safety.
|
|
129
194
|
*
|
|
130
|
-
* @param queue - The queue definition to bind
|
|
195
|
+
* @param queue - The queue definition or queue with infrastructure to bind
|
|
131
196
|
* @param exchange - The exchange definition
|
|
132
197
|
* @param options - Optional binding configuration
|
|
133
198
|
* @returns A queue binding definition
|
|
134
199
|
* @internal
|
|
135
200
|
*/
|
|
136
201
|
function defineQueueBinding(queue, exchange, options) {
|
|
202
|
+
const queueDef = extractQueue(queue);
|
|
137
203
|
if (exchange.type === "fanout") return {
|
|
138
204
|
type: "queue",
|
|
139
|
-
queue,
|
|
205
|
+
queue: queueDef,
|
|
140
206
|
exchange,
|
|
141
207
|
...options?.arguments && { arguments: options.arguments }
|
|
142
208
|
};
|
|
143
209
|
return {
|
|
144
210
|
type: "queue",
|
|
145
|
-
queue,
|
|
211
|
+
queue: queueDef,
|
|
146
212
|
exchange,
|
|
147
213
|
routingKey: options?.routingKey,
|
|
148
214
|
...options?.arguments && { arguments: options.arguments }
|
|
@@ -240,7 +306,7 @@ function definePublisher(exchange, message, options) {
|
|
|
240
306
|
*/
|
|
241
307
|
function defineConsumer(queue, message, options) {
|
|
242
308
|
return {
|
|
243
|
-
queue,
|
|
309
|
+
queue: extractQueue(queue),
|
|
244
310
|
message,
|
|
245
311
|
...options
|
|
246
312
|
};
|
|
@@ -315,7 +381,56 @@ function defineConsumer(queue, message, options) {
|
|
|
315
381
|
* ```
|
|
316
382
|
*/
|
|
317
383
|
function defineContract(definition) {
|
|
318
|
-
return definition;
|
|
384
|
+
if (!definition.queues || Object.keys(definition.queues).length === 0) return definition;
|
|
385
|
+
const queues = definition.queues;
|
|
386
|
+
const expandedQueues = {};
|
|
387
|
+
const autoBindings = {};
|
|
388
|
+
for (const [name, entry] of Object.entries(queues)) if (isQueueWithTtlBackoffInfrastructure(entry)) {
|
|
389
|
+
expandedQueues[name] = entry.queue;
|
|
390
|
+
expandedQueues[`${name}Wait`] = entry.waitQueue;
|
|
391
|
+
autoBindings[`${name}WaitBinding`] = entry.waitQueueBinding;
|
|
392
|
+
autoBindings[`${name}RetryBinding`] = entry.mainQueueRetryBinding;
|
|
393
|
+
} else expandedQueues[name] = entry;
|
|
394
|
+
if (Object.keys(autoBindings).length > 0) {
|
|
395
|
+
const mergedBindings = {
|
|
396
|
+
...definition.bindings,
|
|
397
|
+
...autoBindings
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
...definition,
|
|
401
|
+
queues: expandedQueues,
|
|
402
|
+
bindings: mergedBindings
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
...definition,
|
|
407
|
+
queues: expandedQueues
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Type guard to check if a queue entry is a QueueWithTtlBackoffInfrastructure.
|
|
412
|
+
* @internal
|
|
413
|
+
*/
|
|
414
|
+
function isQueueWithTtlBackoffInfrastructure(entry) {
|
|
415
|
+
return typeof entry === "object" && entry !== null && "__brand" in entry && entry.__brand === "QueueWithTtlBackoffInfrastructure";
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Extract the plain QueueDefinition from a QueueEntry.
|
|
419
|
+
* If the entry is a QueueWithTtlBackoffInfrastructure, returns the inner queue.
|
|
420
|
+
* Otherwise, returns the entry as-is.
|
|
421
|
+
*
|
|
422
|
+
* @param entry - The queue entry (either plain QueueDefinition or QueueWithTtlBackoffInfrastructure)
|
|
423
|
+
* @returns The plain QueueDefinition
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* const queue = defineQueue('orders', { retry: { mode: 'ttl-backoff' }, deadLetter: { exchange: dlx } });
|
|
428
|
+
* const plainQueue = extractQueue(queue); // Returns the inner QueueDefinition
|
|
429
|
+
* ```
|
|
430
|
+
*/
|
|
431
|
+
function extractQueue(entry) {
|
|
432
|
+
if (isQueueWithTtlBackoffInfrastructure(entry)) return entry.queue;
|
|
433
|
+
return entry;
|
|
319
434
|
}
|
|
320
435
|
/**
|
|
321
436
|
* Helper to call definePublisher with proper type handling.
|
|
@@ -398,6 +513,75 @@ function defineConsumerFirst(queue, exchange, message, options) {
|
|
|
398
513
|
createPublisher
|
|
399
514
|
};
|
|
400
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Create TTL-backoff retry infrastructure for a queue.
|
|
518
|
+
*
|
|
519
|
+
* This builder helper generates the wait queue and bindings needed for TTL-backoff retry.
|
|
520
|
+
* The generated infrastructure can be spread into a contract definition.
|
|
521
|
+
*
|
|
522
|
+
* TTL-backoff retry works by:
|
|
523
|
+
* 1. Failed messages are sent to the DLX with routing key `{queueName}-wait`
|
|
524
|
+
* 2. The wait queue receives these messages and holds them for a TTL period
|
|
525
|
+
* 3. After TTL expires, messages are dead-lettered back to the DLX with routing key `{queueName}`
|
|
526
|
+
* 4. The main queue receives the retried message via its binding to the DLX
|
|
527
|
+
*
|
|
528
|
+
* @param queue - The main queue definition (must have deadLetter configured)
|
|
529
|
+
* @param options - Optional configuration for the wait queue
|
|
530
|
+
* @param options.waitQueueDurable - Whether the wait queue should be durable (default: same as main queue)
|
|
531
|
+
* @returns TTL-backoff retry infrastructure containing wait queue and bindings
|
|
532
|
+
* @throws {Error} If the queue does not have a dead letter exchange configured
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```typescript
|
|
536
|
+
* const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
|
|
537
|
+
* const orderQueue = defineQueue('order-processing', {
|
|
538
|
+
* type: 'quorum',
|
|
539
|
+
* deadLetter: { exchange: dlx },
|
|
540
|
+
* retry: {
|
|
541
|
+
* mode: 'ttl-backoff',
|
|
542
|
+
* maxRetries: 5,
|
|
543
|
+
* initialDelayMs: 1000,
|
|
544
|
+
* },
|
|
545
|
+
* });
|
|
546
|
+
*
|
|
547
|
+
* // Generate TTL-backoff infrastructure
|
|
548
|
+
* const retryInfra = defineTtlBackoffRetryInfrastructure(orderQueue);
|
|
549
|
+
*
|
|
550
|
+
* // Spread into contract
|
|
551
|
+
* const contract = defineContract({
|
|
552
|
+
* exchanges: { dlx },
|
|
553
|
+
* queues: {
|
|
554
|
+
* orderProcessing: orderQueue,
|
|
555
|
+
* orderProcessingWait: retryInfra.waitQueue,
|
|
556
|
+
* },
|
|
557
|
+
* bindings: {
|
|
558
|
+
* ...// your other bindings
|
|
559
|
+
* orderWaitBinding: retryInfra.waitQueueBinding,
|
|
560
|
+
* orderRetryBinding: retryInfra.mainQueueRetryBinding,
|
|
561
|
+
* },
|
|
562
|
+
* // ... publishers and consumers
|
|
563
|
+
* });
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
function defineTtlBackoffRetryInfrastructure(queueEntry, options) {
|
|
567
|
+
const queue = extractQueue(queueEntry);
|
|
568
|
+
if (!queue.deadLetter) throw new Error(`Queue "${queue.name}" does not have a dead letter exchange configured. TTL-backoff retry requires deadLetter to be set on the queue.`);
|
|
569
|
+
const dlx = queue.deadLetter.exchange;
|
|
570
|
+
const waitQueueName = `${queue.name}-wait`;
|
|
571
|
+
const waitQueue = defineQueue(waitQueueName, {
|
|
572
|
+
type: "quorum",
|
|
573
|
+
durable: options?.waitQueueDurable ?? queue.durable ?? true,
|
|
574
|
+
deadLetter: {
|
|
575
|
+
exchange: dlx,
|
|
576
|
+
routingKey: queue.name
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return {
|
|
580
|
+
waitQueue,
|
|
581
|
+
waitQueueBinding: callDefineQueueBinding(waitQueue, dlx, { routingKey: waitQueueName }),
|
|
582
|
+
mainQueueRetryBinding: callDefineQueueBinding(queue, dlx, { routingKey: queue.name })
|
|
583
|
+
};
|
|
584
|
+
}
|
|
401
585
|
|
|
402
586
|
//#endregion
|
|
403
587
|
exports.defineConsumer = defineConsumer;
|
|
@@ -409,4 +593,6 @@ exports.defineMessage = defineMessage;
|
|
|
409
593
|
exports.definePublisher = definePublisher;
|
|
410
594
|
exports.definePublisherFirst = definePublisherFirst;
|
|
411
595
|
exports.defineQueue = defineQueue;
|
|
412
|
-
exports.defineQueueBinding = defineQueueBinding;
|
|
596
|
+
exports.defineQueueBinding = defineQueueBinding;
|
|
597
|
+
exports.defineTtlBackoffRetryInfrastructure = defineTtlBackoffRetryInfrastructure;
|
|
598
|
+
exports.extractQueue = extractQueue;
|