@amqp-contract/contract 0.9.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 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.durable - If true, the queue survives broker restarts (default: false)
32
- * @param options.exclusive - If true, the queue can only be used by the declaring connection (default: false)
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). Sets x-max-priority argument.
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
- * // Basic queue
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
- * // Queue with dead letter exchange
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
- * durable: true,
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 { maxPriority, ...queueOptions } = options ?? {};
68
- if (maxPriority !== void 0) {
69
- if (maxPriority < 1 || maxPriority > 255) throw new Error(`Invalid maxPriority: ${maxPriority}. Must be between 1 and 255. Recommended range: 1-10.`);
70
- return {
71
- name,
72
- ...queueOptions,
73
- arguments: {
74
- ...queueOptions.arguments,
75
- "x-max-priority": maxPriority
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
- name,
81
- ...queueOptions
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;