@amqp-contract/contract 0.20.0 → 0.22.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.mjs CHANGED
@@ -7,19 +7,23 @@
7
7
  * type safety.
8
8
  *
9
9
  * @param name - The name of the exchange
10
- * @param type - The type of exchange: "fanout", "direct", or "topic"
11
10
  * @param options - Optional exchange configuration
11
+ * @param options.type - Exchange type (one of "topic", "direct", "fanout", "headers") (default: "topic")
12
+ * @param options.durable - If true, the exchange survives broker restarts (default: true)
13
+ * @param options.autoDelete - If true, the exchange is deleted when no queues are bound
14
+ * @param options.internal - If true, the exchange cannot be directly published to
15
+ * @param options.arguments - Additional AMQP arguments for the exchange
12
16
  * @returns An exchange definition
13
17
  * @internal
14
18
  */
15
- function defineExchange(name, type, options) {
19
+ function defineExchange(name, options) {
16
20
  return {
17
21
  name,
18
- type,
22
+ type: options?.type ?? "topic",
23
+ durable: true,
19
24
  ...options
20
25
  };
21
26
  }
22
-
23
27
  //#endregion
24
28
  //#region src/builder/message.ts
25
29
  /**
@@ -63,25 +67,64 @@ function defineMessage(payload, options) {
63
67
  ...options
64
68
  };
65
69
  }
66
-
67
70
  //#endregion
68
71
  //#region src/builder/queue-utils.ts
69
72
  /**
70
- * Type guard to check if a queue entry is a QueueWithTtlBackoffInfrastructure.
73
+ * Extract the plain QueueDefinition from a QueueEntry.
71
74
  * @internal
72
75
  */
73
- function isQueueWithTtlBackoffInfrastructure$1(entry) {
74
- return typeof entry === "object" && entry !== null && "__brand" in entry && entry.__brand === "QueueWithTtlBackoffInfrastructure";
76
+ function extractQueueFromEntry(entry) {
77
+ if (isQueueWithTtlBackoffInfrastructure(entry)) return entry.queue;
78
+ return entry;
75
79
  }
76
80
  /**
77
81
  * Extract the plain QueueDefinition from a QueueEntry.
78
- * @internal
82
+ *
83
+ * **Why this function exists:**
84
+ * When you configure a queue with TTL-backoff retry,
85
+ * `defineQueue` returns a wrapper object that includes
86
+ * the main queue, wait queue, headers exchanges, and bindings. This function extracts the underlying
87
+ * queue definition so you can access properties like `name`, `type`, etc.
88
+ *
89
+ * **When to use:**
90
+ * - When you need to access queue properties (name, type, etc.)
91
+ * - When passing a queue to functions that expect a plain QueueDefinition
92
+ * - Works safely on both plain queues and infrastructure wrappers
93
+ *
94
+ * **How it works:**
95
+ * - If the entry is a `QueueWithTtlBackoffInfrastructure`, returns `entry.queue`
96
+ * - Otherwise, returns the entry as-is (it's already a plain QueueDefinition)
97
+ *
98
+ * @param entry - The queue entry (either plain QueueDefinition or QueueWithTtlBackoffInfrastructure)
99
+ * @returns The plain QueueDefinition
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * import { defineQueue, extractQueue } from '@amqp-contract/contract';
104
+ *
105
+ * // TTL-backoff queue returns a wrapper
106
+ * const orderQueue = defineQueue('orders', {
107
+ * retry: { mode: 'ttl-backoff', maxRetries: 3 },
108
+ * });
109
+ *
110
+ * // Use extractQueue to access the queue name
111
+ * const queueName = extractQueue(orderQueue).name; // 'orders'
112
+ *
113
+ * // Also works safely on plain queues
114
+ * const plainQueue = defineQueue('simple', { type: 'quorum', retry: { mode: 'immediate-requeue' } });
115
+ * const plainName = extractQueue(plainQueue).name; // 'simple'
116
+ *
117
+ * // Access other properties
118
+ * const queueDef = extractQueue(orderQueue);
119
+ * console.log(queueDef.name); // 'orders'
120
+ * console.log(queueDef.type); // 'quorum'
121
+ * ```
122
+ *
123
+ * @see isQueueWithTtlBackoffInfrastructure - Type guard to check if extraction is needed
79
124
  */
80
- function extractQueueFromEntry(entry) {
81
- if (isQueueWithTtlBackoffInfrastructure$1(entry)) return entry.queue;
82
- return entry;
125
+ function extractQueue(entry) {
126
+ return extractQueueFromEntry(entry);
83
127
  }
84
-
85
128
  //#endregion
86
129
  //#region src/builder/binding.ts
87
130
  /**
@@ -96,8 +139,8 @@ function extractQueueFromEntry(entry) {
96
139
  * @internal
97
140
  */
98
141
  function defineQueueBinding(queue, exchange, options) {
99
- const queueDef = extractQueueFromEntry(queue);
100
- if (exchange.type === "fanout") return {
142
+ const queueDef = extractQueue(queue);
143
+ if (exchange.type === "fanout" || exchange.type === "headers") return {
101
144
  type: "queue",
102
145
  queue: queueDef,
103
146
  exchange,
@@ -117,7 +160,7 @@ function defineQueueBinding(queue, exchange, options) {
117
160
  * @internal
118
161
  */
119
162
  function defineQueueBindingInternal(queue, exchange, options) {
120
- if (exchange.type === "fanout") return defineQueueBinding(queue, exchange, options);
163
+ if (exchange.type === "fanout" || exchange.type === "headers") return defineQueueBinding(queue, exchange, options);
121
164
  return defineQueueBinding(queue, exchange, options);
122
165
  }
123
166
  /**
@@ -132,7 +175,7 @@ function defineQueueBindingInternal(queue, exchange, options) {
132
175
  * @internal
133
176
  */
134
177
  function defineExchangeBinding(destination, source, options) {
135
- if (source.type === "fanout") return {
178
+ if (source.type === "fanout" || source.type === "headers") return {
136
179
  type: "exchange",
137
180
  source,
138
181
  destination,
@@ -146,27 +189,12 @@ function defineExchangeBinding(destination, source, options) {
146
189
  ...options?.arguments && { arguments: options.arguments }
147
190
  };
148
191
  }
149
-
150
192
  //#endregion
151
- //#region src/builder/queue.ts
152
- /**
153
- * Resolve TTL-backoff retry options with defaults applied.
154
- * @internal
155
- */
156
- function resolveTtlBackoffOptions(options) {
157
- return {
158
- mode: "ttl-backoff",
159
- maxRetries: options?.maxRetries ?? 3,
160
- initialDelayMs: options?.initialDelayMs ?? 1e3,
161
- maxDelayMs: options?.maxDelayMs ?? 3e4,
162
- backoffMultiplier: options?.backoffMultiplier ?? 2,
163
- jitter: options?.jitter ?? true
164
- };
165
- }
193
+ //#region src/builder/ttl-backoff.ts
166
194
  /**
167
195
  * Type guard to check if a queue entry is a QueueWithTtlBackoffInfrastructure.
168
196
  *
169
- * When you configure a queue with TTL-backoff retry and a dead letter exchange,
197
+ * When you configure a queue with TTL-backoff retry,
170
198
  * `defineQueue` returns a `QueueWithTtlBackoffInfrastructure` instead of a plain
171
199
  * `QueueDefinition`. This type guard helps you distinguish between the two.
172
200
  *
@@ -183,12 +211,11 @@ function resolveTtlBackoffOptions(options) {
183
211
  * @example
184
212
  * ```typescript
185
213
  * const queue = defineQueue('orders', {
186
- * deadLetter: { exchange: dlx },
187
214
  * retry: { mode: 'ttl-backoff' },
188
215
  * });
189
216
  *
190
217
  * if (isQueueWithTtlBackoffInfrastructure(queue)) {
191
- * // queue has .queue, .waitQueue, .waitQueueBinding, .mainQueueRetryBinding
218
+ * // queue has .queue, .waitQueue, .waitQueueBinding, .retryQueueBinding, .waitExchange, .retryExchange
192
219
  * console.log('Wait queue:', queue.waitQueue.name);
193
220
  * } else {
194
221
  * // queue is a plain QueueDefinition
@@ -197,256 +224,161 @@ function resolveTtlBackoffOptions(options) {
197
224
  * ```
198
225
  */
199
226
  function isQueueWithTtlBackoffInfrastructure(entry) {
200
- return isQueueWithTtlBackoffInfrastructure$1(entry);
227
+ return typeof entry === "object" && entry !== null && "__brand" in entry && entry.__brand === "QueueWithTtlBackoffInfrastructure";
201
228
  }
202
229
  /**
203
- * Extract the plain QueueDefinition from a QueueEntry.
204
- *
205
- * **Why this function exists:**
206
- * When you configure a queue with TTL-backoff retry and a dead letter exchange,
207
- * `defineQueue` (or `defineTtlBackoffQueue`) returns a wrapper object that includes
208
- * the main queue, wait queue, and bindings. This function extracts the underlying
209
- * queue definition so you can access properties like `name`, `type`, etc.
230
+ * Wrap a queue definition with TTL-backoff retry infrastructure.
231
+ */
232
+ function wrapWithTtlBackoffInfrastructure(queue) {
233
+ return {
234
+ __brand: "QueueWithTtlBackoffInfrastructure",
235
+ queue,
236
+ ...createTtlBackoffInfrastructure(queue)
237
+ };
238
+ }
239
+ /**
240
+ * Create TTL-backoff retry infrastructure for a queue.
210
241
  *
211
- * **When to use:**
212
- * - When you need to access queue properties (name, type, deadLetter, etc.)
213
- * - When passing a queue to functions that expect a plain QueueDefinition
214
- * - Works safely on both plain queues and infrastructure wrappers
242
+ * This builder helper generates the wait queue, exchanges, and bindings needed for TTL-backoff retry.
243
+ * The generated infrastructure can be spread into a contract definition.
215
244
  *
216
- * **How it works:**
217
- * - If the entry is a `QueueWithTtlBackoffInfrastructure`, returns `entry.queue`
218
- * - Otherwise, returns the entry as-is (it's already a plain QueueDefinition)
245
+ * TTL-backoff retry works by:
246
+ * 1. Failed messages are sent to the wait exchange with header `x-wait-queue` set to the wait queue name
247
+ * 2. The wait queue receives these messages and holds them for a TTL period
248
+ * 3. After TTL expires, messages are dead-lettered back to the retry exchange with header `x-retry-queue` set to the main queue name
249
+ * 4. The main queue receives the retried message via its binding to the retry exchange
219
250
  *
220
- * @param entry - The queue entry (either plain QueueDefinition or QueueWithTtlBackoffInfrastructure)
221
- * @returns The plain QueueDefinition
251
+ * @param queue - The main queue definition
252
+ * @param options - Optional configuration for the wait queue
253
+ * @returns TTL-backoff retry infrastructure containing wait queue and bindings
254
+ * @throws {Error} If the queue does not have retry mode set to `ttl-backoff`
222
255
  *
223
256
  * @example
224
257
  * ```typescript
225
- * import { defineQueue, defineTtlBackoffQueue, extractQueue } from '@amqp-contract/contract';
226
- *
227
- * // TTL-backoff queue returns a wrapper
228
- * const orderQueue = defineTtlBackoffQueue('orders', {
229
- * deadLetter: { exchange: dlx },
230
- * maxRetries: 3,
258
+ * const orderQueue = defineQueue('order-processing', {
259
+ * type: 'quorum',
260
+ * retry: {
261
+ * mode: 'ttl-backoff',
262
+ * maxRetries: 5,
263
+ * initialDelayMs: 1000,
264
+ * },
231
265
  * });
232
266
  *
233
- * // Use extractQueue to access the queue name
234
- * const queueName = extractQueue(orderQueue).name; // 'orders'
235
- *
236
- * // Also works safely on plain queues
237
- * const plainQueue = defineQueue('simple', { type: 'quorum', retry: { mode: 'quorum-native' } });
238
- * const plainName = extractQueue(plainQueue).name; // 'simple'
239
- *
240
- * // Access other properties
241
- * const queueDef = extractQueue(orderQueue);
242
- * console.log(queueDef.name); // 'orders'
243
- * console.log(queueDef.type); // 'quorum'
244
- * console.log(queueDef.deadLetter); // { exchange: dlx, ... }
267
+ * // Infrastructure is auto-extracted when using defineContract:
268
+ * const contract = defineContract({
269
+ * publishers: { ... },
270
+ * consumers: { processOrder: defineEventConsumer(event, orderQueue) },
271
+ * });
272
+ * // contract.queues includes the wait queue, contract.exchanges includes retry exchanges, contract.bindings includes retry bindings
245
273
  * ```
246
- *
247
- * @see isQueueWithTtlBackoffInfrastructure - Type guard to check if extraction is needed
248
- * @see defineTtlBackoffQueue - Creates queues with TTL-backoff infrastructure
249
274
  */
250
- function extractQueue(entry) {
251
- return extractQueueFromEntry(entry);
275
+ function createTtlBackoffInfrastructure(queue) {
276
+ if (queue.retry.mode !== "ttl-backoff") throw new Error(`Queue ${queue.name} does not have ttl-backoff retry mode. Infrastructure can only be created for queues with ttl-backoff retry.`);
277
+ const waitExchange = defineExchange(queue.retry.waitExchangeName, { type: "headers" });
278
+ const retryExchange = defineExchange(queue.retry.retryExchangeName, { type: "headers" });
279
+ const baseWaitQueue = {
280
+ name: queue.retry.waitQueueName,
281
+ deadLetter: { exchange: retryExchange },
282
+ retry: { mode: "none" }
283
+ };
284
+ const waitQueue = queue.type === "quorum" ? {
285
+ ...baseWaitQueue,
286
+ type: queue.type,
287
+ durable: true
288
+ } : {
289
+ ...baseWaitQueue,
290
+ type: queue.type,
291
+ durable: queue.durable
292
+ };
293
+ return {
294
+ waitQueue,
295
+ waitExchange,
296
+ retryExchange,
297
+ waitQueueBinding: defineQueueBindingInternal(waitQueue, waitExchange, { arguments: {
298
+ "x-match": "all",
299
+ "x-wait-queue": waitQueue.name
300
+ } }),
301
+ retryQueueBinding: defineQueueBindingInternal(queue, retryExchange, { arguments: {
302
+ "x-match": "all",
303
+ "x-retry-queue": queue.name
304
+ } })
305
+ };
252
306
  }
307
+ //#endregion
308
+ //#region src/builder/queue.ts
253
309
  /**
254
- * Create TTL-backoff retry infrastructure (wait queue + bindings) for a queue.
310
+ * Resolve immediate-requeue retry options with defaults.
255
311
  * @internal
256
312
  */
257
- function createTtlBackoffInfrastructure(queue) {
258
- 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.`);
259
- const dlx = queue.deadLetter.exchange;
260
- const waitQueueName = `${queue.name}-wait`;
261
- const waitQueue = {
262
- name: waitQueueName,
263
- type: "quorum",
264
- durable: queue.durable ?? true,
265
- deadLetter: {
266
- exchange: dlx,
267
- routingKey: queue.name
268
- },
269
- retry: resolveTtlBackoffOptions(void 0)
270
- };
313
+ function resolveImmediateRequeueOptions(options) {
271
314
  return {
272
- waitQueue,
273
- waitQueueBinding: defineQueueBindingInternal(waitQueue, dlx, { routingKey: waitQueueName }),
274
- mainQueueRetryBinding: defineQueueBindingInternal(queue, dlx, { routingKey: queue.name })
315
+ mode: "immediate-requeue",
316
+ maxRetries: options?.maxRetries ?? 3
275
317
  };
276
318
  }
277
319
  /**
278
- * Wrap a queue definition with TTL-backoff retry infrastructure.
320
+ * Resolve TTL-backoff retry options with defaults.
279
321
  * @internal
280
322
  */
281
- function wrapWithTtlBackoffInfrastructure(queue) {
282
- const infra = createTtlBackoffInfrastructure(queue);
323
+ function resolveTtlBackoffOptions(queueName, options) {
283
324
  return {
284
- __brand: "QueueWithTtlBackoffInfrastructure",
285
- queue,
286
- deadLetter: queue.deadLetter,
287
- ...infra
325
+ mode: "ttl-backoff",
326
+ maxRetries: options?.maxRetries ?? 3,
327
+ initialDelayMs: options?.initialDelayMs ?? 1e3,
328
+ maxDelayMs: options?.maxDelayMs ?? 3e4,
329
+ backoffMultiplier: options?.backoffMultiplier ?? 2,
330
+ jitter: options?.jitter ?? true,
331
+ waitQueueName: options?.waitQueueName ?? `${queueName}-wait`,
332
+ waitExchangeName: options?.waitExchangeName ?? "wait-exchange",
333
+ retryExchangeName: options?.retryExchangeName ?? "retry-exchange"
288
334
  };
289
335
  }
290
336
  function defineQueue(name, options) {
291
337
  const opts = options ?? {};
292
338
  const type = opts.type ?? "quorum";
293
- const baseProps = { name };
294
- if (opts.durable !== void 0) baseProps.durable = opts.durable;
295
- if (opts.autoDelete !== void 0) baseProps.autoDelete = opts.autoDelete;
296
- if (opts.deadLetter !== void 0) baseProps.deadLetter = opts.deadLetter;
297
- if (opts.arguments !== void 0) baseProps.arguments = opts.arguments;
339
+ const durable = opts.durable ?? true;
340
+ const baseProps = {
341
+ name,
342
+ ...opts.deadLetter !== void 0 && { deadLetter: opts.deadLetter },
343
+ ...opts.arguments !== void 0 && { arguments: opts.arguments }
344
+ };
345
+ const classicProps = {
346
+ ...opts.exclusive !== void 0 && { exclusive: opts.exclusive },
347
+ ...opts.autoDelete !== void 0 && { autoDelete: opts.autoDelete },
348
+ ...opts.maxPriority !== void 0 && { maxPriority: opts.maxPriority }
349
+ };
298
350
  if (type === "quorum") {
299
- const quorumOpts = opts;
300
- const inputRetry = quorumOpts.retry ?? { mode: "ttl-backoff" };
301
- if (inputRetry.mode === "quorum-native") {
302
- if (quorumOpts.deliveryLimit === void 0) throw new Error(`Queue "${name}" uses quorum-native retry mode but deliveryLimit is not configured. Quorum-native retry requires deliveryLimit to be set.`);
303
- }
304
- const retry = inputRetry.mode === "quorum-native" ? inputRetry : resolveTtlBackoffOptions(inputRetry);
305
- const queueDefinition = {
306
- ...baseProps,
307
- type: "quorum",
308
- retry
309
- };
310
- if (quorumOpts.deliveryLimit !== void 0) {
311
- if (quorumOpts.deliveryLimit < 1 || !Number.isInteger(quorumOpts.deliveryLimit)) throw new Error(`Invalid deliveryLimit: ${quorumOpts.deliveryLimit}. Must be a positive integer.`);
312
- queueDefinition.deliveryLimit = quorumOpts.deliveryLimit;
351
+ if (opts.durable === false) throw new Error("Non-durable queues are not supported with quorum type.");
352
+ if (opts.exclusive !== void 0) throw new Error("Exclusive queues are not supported with quorum type.");
353
+ if (opts.autoDelete !== void 0) throw new Error("Auto-deleting queues are not supported with quorum type.");
354
+ if (opts.maxPriority !== void 0) throw new Error("Priority queues are not supported with quorum type.");
355
+ } else if (opts.maxPriority !== void 0) {
356
+ if (opts.maxPriority < 1 || opts.maxPriority > 255) throw new Error(`Invalid maxPriority: ${opts.maxPriority}. Must be between 1 and 255. Recommended range: 1-10.`);
357
+ }
358
+ const inputRetry = opts.retry ?? { mode: "none" };
359
+ if (inputRetry.mode === "immediate-requeue" || inputRetry.mode === "ttl-backoff") {
360
+ if (inputRetry.maxRetries !== void 0) {
361
+ if (inputRetry.maxRetries < 1 || !Number.isInteger(inputRetry.maxRetries)) throw new Error(`Queue "${name}" uses ${inputRetry.mode} retry mode with invalid maxRetries: ${inputRetry.maxRetries}. Must be a positive integer.`);
313
362
  }
314
- if (retry.mode === "ttl-backoff" && queueDefinition.deadLetter) return wrapWithTtlBackoffInfrastructure(queueDefinition);
315
- return queueDefinition;
316
363
  }
317
- const classicOpts = opts;
318
- if (classicOpts.retry?.mode === "quorum-native") throw new Error(`Queue "${name}" uses quorum-native retry mode but is a classic queue. Quorum-native retry requires quorum queues (type: "quorum").`);
319
- const retry = resolveTtlBackoffOptions(classicOpts.retry);
320
- const queueDefinition = {
364
+ const retry = inputRetry.mode === "immediate-requeue" ? resolveImmediateRequeueOptions(inputRetry) : inputRetry.mode === "ttl-backoff" ? resolveTtlBackoffOptions(name, inputRetry) : inputRetry;
365
+ const baseQueueDefinition = {
321
366
  ...baseProps,
322
- type: "classic",
323
367
  retry
324
368
  };
325
- if (classicOpts.exclusive !== void 0) queueDefinition.exclusive = classicOpts.exclusive;
326
- if (classicOpts.maxPriority !== void 0) {
327
- if (classicOpts.maxPriority < 1 || classicOpts.maxPriority > 255) throw new Error(`Invalid maxPriority: ${classicOpts.maxPriority}. Must be between 1 and 255. Recommended range: 1-10.`);
328
- queueDefinition.arguments = {
329
- ...queueDefinition.arguments,
330
- "x-max-priority": classicOpts.maxPriority
331
- };
332
- }
333
- if (retry.mode === "ttl-backoff" && queueDefinition.deadLetter) return wrapWithTtlBackoffInfrastructure(queueDefinition);
334
- return queueDefinition;
335
- }
336
- /**
337
- * Create a quorum queue with quorum-native retry.
338
- *
339
- * This is a simplified helper that enforces best practices:
340
- * - Uses quorum queues (recommended for most use cases)
341
- * - Requires dead letter exchange for failed message handling
342
- * - Uses quorum-native retry mode (simpler than TTL-backoff)
343
- *
344
- * **When to use:**
345
- * - You want simple, immediate retries without exponential backoff
346
- * - You don't need configurable delays between retries
347
- * - You want the simplest retry configuration
348
- *
349
- * @param name - The queue name
350
- * @param options - Configuration options
351
- * @returns A quorum queue definition with quorum-native retry
352
- *
353
- * @example
354
- * ```typescript
355
- * const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
356
- *
357
- * const orderQueue = defineQuorumQueue('order-processing', {
358
- * deadLetter: { exchange: dlx },
359
- * deliveryLimit: 3, // Retry up to 3 times
360
- * });
361
- *
362
- * // Use in a contract — exchanges, queues, and bindings are auto-extracted
363
- * const contract = defineContract({
364
- * publishers: { ... },
365
- * consumers: { processOrder: defineEventConsumer(event, orderQueue) },
366
- * });
367
- * ```
368
- *
369
- * @see defineQueue - For full queue configuration options
370
- * @see defineTtlBackoffQueue - For queues with exponential backoff retry
371
- */
372
- function defineQuorumQueue(name, options) {
373
- const { deadLetter, deliveryLimit, autoDelete, arguments: args } = options;
374
- const queueOptions = {
375
- type: "quorum",
376
- deadLetter,
377
- deliveryLimit,
378
- retry: { mode: "quorum-native" }
379
- };
380
- if (autoDelete !== void 0) queueOptions.autoDelete = autoDelete;
381
- if (args !== void 0) queueOptions.arguments = args;
382
- return defineQueue(name, queueOptions);
383
- }
384
- /**
385
- * Create a queue with TTL-backoff retry (exponential backoff).
386
- *
387
- * This is a simplified helper that enforces best practices:
388
- * - Uses quorum queues (recommended for most use cases)
389
- * - Requires dead letter exchange for retry routing
390
- * - Uses TTL-backoff retry mode with configurable delays
391
- * - Automatically generates wait queue and bindings
392
- *
393
- * **When to use:**
394
- * - You need exponential backoff between retries
395
- * - You want configurable delays (initial delay, max delay, jitter)
396
- * - You're processing messages that may need time before retry
397
- *
398
- * **Returns:** A `QueueWithTtlBackoffInfrastructure` object that includes the
399
- * main queue, wait queue, and bindings. Pass this directly to `defineContract`
400
- * and it will be expanded automatically.
401
- *
402
- * @param name - The queue name
403
- * @param options - Configuration options
404
- * @returns A queue with TTL-backoff infrastructure
405
- *
406
- * @example
407
- * ```typescript
408
- * const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
409
- *
410
- * const orderQueue = defineTtlBackoffQueue('order-processing', {
411
- * deadLetter: { exchange: dlx },
412
- * maxRetries: 5,
413
- * initialDelayMs: 1000, // Start with 1s delay
414
- * maxDelayMs: 30000, // Cap at 30s
415
- * });
416
- *
417
- * // Use in a contract — wait queue, bindings, and DLX are auto-extracted
418
- * const contract = defineContract({
419
- * publishers: { ... },
420
- * consumers: { processOrder: defineEventConsumer(event, extractQueue(orderQueue)) },
421
- * });
422
- *
423
- * // To access the underlying queue definition (e.g., for the queue name):
424
- * import { extractQueue } from '@amqp-contract/contract';
425
- * const queueName = extractQueue(orderQueue).name;
426
- * ```
427
- *
428
- * @see defineQueue - For full queue configuration options
429
- * @see defineQuorumQueue - For queues with quorum-native retry (simpler, immediate retries)
430
- * @see extractQueue - To access the underlying queue definition
431
- */
432
- function defineTtlBackoffQueue(name, options) {
433
- const { deadLetter, maxRetries, initialDelayMs, maxDelayMs, backoffMultiplier, jitter, autoDelete, arguments: args } = options;
434
- const retryOptions = { mode: "ttl-backoff" };
435
- if (maxRetries !== void 0) retryOptions.maxRetries = maxRetries;
436
- if (initialDelayMs !== void 0) retryOptions.initialDelayMs = initialDelayMs;
437
- if (maxDelayMs !== void 0) retryOptions.maxDelayMs = maxDelayMs;
438
- if (backoffMultiplier !== void 0) retryOptions.backoffMultiplier = backoffMultiplier;
439
- if (jitter !== void 0) retryOptions.jitter = jitter;
440
- const queueOptions = {
441
- type: "quorum",
442
- deadLetter,
443
- retry: retryOptions
369
+ const queueDefinition = type === "quorum" ? {
370
+ ...baseQueueDefinition,
371
+ type,
372
+ durable: true
373
+ } : {
374
+ ...baseQueueDefinition,
375
+ ...classicProps,
376
+ type,
377
+ durable
444
378
  };
445
- if (autoDelete !== void 0) queueOptions.autoDelete = autoDelete;
446
- if (args !== void 0) queueOptions.arguments = args;
447
- return defineQueue(name, queueOptions);
379
+ if (retry.mode === "ttl-backoff") return wrapWithTtlBackoffInfrastructure(queueDefinition);
380
+ return queueDefinition;
448
381
  }
449
-
450
382
  //#endregion
451
383
  //#region src/builder/publisher.ts
452
384
  /**
@@ -461,7 +393,7 @@ function defineTtlBackoffQueue(name, options) {
461
393
  * @internal
462
394
  */
463
395
  function definePublisher(exchange, message, options) {
464
- if (exchange.type === "fanout") return {
396
+ if (exchange.type === "fanout" || exchange.type === "headers") return {
465
397
  exchange,
466
398
  message
467
399
  };
@@ -477,10 +409,9 @@ function definePublisher(exchange, message, options) {
477
409
  * @internal
478
410
  */
479
411
  function definePublisherInternal(exchange, message, options) {
480
- if (exchange.type === "fanout") return definePublisher(exchange, message, options);
412
+ if (exchange.type === "fanout" || exchange.type === "headers") return definePublisher(exchange, message, options);
481
413
  return definePublisher(exchange, message, options);
482
414
  }
483
-
484
415
  //#endregion
485
416
  //#region src/builder/consumer.ts
486
417
  /**
@@ -560,7 +491,7 @@ function extractConsumer(entry) {
560
491
  * ```typescript
561
492
  * import { z } from 'zod';
562
493
  *
563
- * const orderQueue = defineQueue('order-processing', { durable: true });
494
+ * const orderQueue = defineQueue('order-processing');
564
495
  * const orderMessage = defineMessage(
565
496
  * z.object({
566
497
  * orderId: z.string().uuid(),
@@ -589,12 +520,72 @@ function extractConsumer(entry) {
589
520
  */
590
521
  function defineConsumer(queue, message, options) {
591
522
  return {
592
- queue: extractQueue(queue),
523
+ queue,
593
524
  message,
594
525
  ...options
595
526
  };
596
527
  }
597
-
528
+ //#endregion
529
+ //#region src/builder/command.ts
530
+ /**
531
+ * Implementation of defineCommandConsumer.
532
+ * @internal
533
+ */
534
+ function defineCommandConsumer(queue, exchange, message, options) {
535
+ return {
536
+ __brand: "CommandConsumerConfig",
537
+ consumer: defineConsumer(queue, message),
538
+ binding: defineQueueBindingInternal(queue, exchange, options),
539
+ exchange,
540
+ queue,
541
+ message,
542
+ routingKey: options?.routingKey
543
+ };
544
+ }
545
+ /**
546
+ * Implementation of defineCommandPublisher.
547
+ * @internal
548
+ */
549
+ function defineCommandPublisher(commandConsumer, options) {
550
+ const { exchange: targetExchange, message, routingKey: consumerRoutingKey } = commandConsumer;
551
+ const publisherRoutingKey = options?.routingKey ?? consumerRoutingKey;
552
+ const bridgeExchange = options?.bridgeExchange;
553
+ if (bridgeExchange) {
554
+ const publisherOptions = {};
555
+ if (publisherRoutingKey !== void 0) publisherOptions.routingKey = publisherRoutingKey;
556
+ const publisher = definePublisherInternal(bridgeExchange, message, publisherOptions);
557
+ const e2eBindingOptions = {};
558
+ if (publisherRoutingKey !== void 0) e2eBindingOptions.routingKey = publisherRoutingKey;
559
+ return {
560
+ __brand: "BridgedPublisherConfig",
561
+ publisher,
562
+ exchangeBinding: bridgeExchange.type === "fanout" || bridgeExchange.type === "headers" ? defineExchangeBinding(targetExchange, bridgeExchange) : defineExchangeBinding(targetExchange, bridgeExchange, e2eBindingOptions),
563
+ bridgeExchange,
564
+ targetExchange
565
+ };
566
+ }
567
+ const publisherOptions = {};
568
+ if (publisherRoutingKey !== void 0) publisherOptions.routingKey = publisherRoutingKey;
569
+ return definePublisherInternal(targetExchange, message, publisherOptions);
570
+ }
571
+ /**
572
+ * Type guard to check if a value is a CommandConsumerConfig.
573
+ *
574
+ * @param value - The value to check
575
+ * @returns True if the value is a CommandConsumerConfig
576
+ */
577
+ function isCommandConsumerConfig(value) {
578
+ return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "CommandConsumerConfig";
579
+ }
580
+ /**
581
+ * Type guard to check if a value is a BridgedPublisherConfig.
582
+ *
583
+ * @param value - The value to check
584
+ * @returns True if the value is a BridgedPublisherConfig
585
+ */
586
+ function isBridgedPublisherConfig(value) {
587
+ return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "BridgedPublisherConfig";
588
+ }
598
589
  //#endregion
599
590
  //#region src/builder/event.ts
600
591
  /**
@@ -628,27 +619,23 @@ function defineEventConsumer(eventPublisher, queue, options) {
628
619
  const consumer = defineConsumer(queue, message);
629
620
  const exchangeBindingOptions = {};
630
621
  if (bindingRoutingKey !== void 0) exchangeBindingOptions.routingKey = bindingRoutingKey;
631
- const e2eBinding = sourceExchange.type === "fanout" ? defineExchangeBinding(bridgeExchange, sourceExchange) : defineExchangeBinding(bridgeExchange, sourceExchange, exchangeBindingOptions);
632
622
  return {
633
623
  __brand: "EventConsumerResult",
634
624
  consumer,
635
625
  binding,
636
626
  exchange: sourceExchange,
637
- queue: consumer.queue,
638
- deadLetterExchange: consumer.queue.deadLetter?.exchange,
639
- exchangeBinding: e2eBinding,
627
+ queue,
628
+ exchangeBinding: sourceExchange.type === "fanout" || sourceExchange.type === "headers" ? defineExchangeBinding(bridgeExchange, sourceExchange) : defineExchangeBinding(bridgeExchange, sourceExchange, exchangeBindingOptions),
640
629
  bridgeExchange
641
630
  };
642
631
  }
643
632
  const binding = defineQueueBindingInternal(queue, sourceExchange, bindingOptions);
644
- const consumer = defineConsumer(queue, message);
645
633
  return {
646
634
  __brand: "EventConsumerResult",
647
- consumer,
635
+ consumer: defineConsumer(queue, message),
648
636
  binding,
649
637
  exchange: sourceExchange,
650
- queue: consumer.queue,
651
- deadLetterExchange: consumer.queue.deadLetter?.exchange,
638
+ queue,
652
639
  exchangeBinding: void 0,
653
640
  bridgeExchange: void 0
654
641
  };
@@ -671,71 +658,6 @@ function isEventPublisherConfig(value) {
671
658
  function isEventConsumerResult(value) {
672
659
  return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "EventConsumerResult";
673
660
  }
674
-
675
- //#endregion
676
- //#region src/builder/command.ts
677
- /**
678
- * Implementation of defineCommandConsumer.
679
- * @internal
680
- */
681
- function defineCommandConsumer(queue, exchange, message, options) {
682
- const consumer = defineConsumer(queue, message);
683
- return {
684
- __brand: "CommandConsumerConfig",
685
- consumer,
686
- binding: defineQueueBindingInternal(queue, exchange, options),
687
- exchange,
688
- queue: consumer.queue,
689
- deadLetterExchange: consumer.queue.deadLetter?.exchange,
690
- message,
691
- routingKey: options?.routingKey
692
- };
693
- }
694
- /**
695
- * Implementation of defineCommandPublisher.
696
- * @internal
697
- */
698
- function defineCommandPublisher(commandConsumer, options) {
699
- const { exchange: targetExchange, message, routingKey: consumerRoutingKey } = commandConsumer;
700
- const publisherRoutingKey = options?.routingKey ?? consumerRoutingKey;
701
- const bridgeExchange = options?.bridgeExchange;
702
- if (bridgeExchange) {
703
- const publisherOptions = {};
704
- if (publisherRoutingKey !== void 0) publisherOptions.routingKey = publisherRoutingKey;
705
- const publisher = definePublisherInternal(bridgeExchange, message, publisherOptions);
706
- const e2eBindingOptions = {};
707
- if (publisherRoutingKey !== void 0) e2eBindingOptions.routingKey = publisherRoutingKey;
708
- return {
709
- __brand: "BridgedPublisherConfig",
710
- publisher,
711
- exchangeBinding: bridgeExchange.type === "fanout" ? defineExchangeBinding(targetExchange, bridgeExchange) : defineExchangeBinding(targetExchange, bridgeExchange, e2eBindingOptions),
712
- bridgeExchange,
713
- targetExchange
714
- };
715
- }
716
- const publisherOptions = {};
717
- if (publisherRoutingKey !== void 0) publisherOptions.routingKey = publisherRoutingKey;
718
- return definePublisherInternal(targetExchange, message, publisherOptions);
719
- }
720
- /**
721
- * Type guard to check if a value is a CommandConsumerConfig.
722
- *
723
- * @param value - The value to check
724
- * @returns True if the value is a CommandConsumerConfig
725
- */
726
- function isCommandConsumerConfig(value) {
727
- return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "CommandConsumerConfig";
728
- }
729
- /**
730
- * Type guard to check if a value is a BridgedPublisherConfig.
731
- *
732
- * @param value - The value to check
733
- * @returns True if the value is a BridgedPublisherConfig
734
- */
735
- function isBridgedPublisherConfig(value) {
736
- return typeof value === "object" && value !== null && "__brand" in value && value.__brand === "BridgedPublisherConfig";
737
- }
738
-
739
661
  //#endregion
740
662
  //#region src/builder/contract.ts
741
663
  /**
@@ -767,12 +689,11 @@ function isBridgedPublisherConfig(value) {
767
689
  * import { z } from 'zod';
768
690
  *
769
691
  * // Define resources
770
- * const ordersExchange = defineExchange('orders', 'topic', { durable: true });
771
- * const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
692
+ * const ordersExchange = defineExchange('orders');
693
+ * const dlx = defineExchange('orders-dlx', { type: 'direct' });
772
694
  * const orderQueue = defineQueue('order-processing', {
773
695
  * deadLetter: { exchange: dlx },
774
- * retry: { mode: 'quorum-native' },
775
- * deliveryLimit: 3,
696
+ * retry: { mode: 'immediate-requeue', maxRetries: 3 },
776
697
  * });
777
698
  * const orderMessage = defineMessage(
778
699
  * z.object({
@@ -805,13 +726,18 @@ function isBridgedPublisherConfig(value) {
805
726
  * ```
806
727
  */
807
728
  function defineContract(definition) {
808
- const { publishers: inputPublishers, consumers: inputConsumers } = definition;
729
+ const { publishers: inputPublishers, consumers: inputConsumers, rpcs: inputRpcs } = definition;
730
+ if (inputConsumers && inputRpcs) {
731
+ const collisions = Object.keys(inputConsumers).filter((name) => Object.hasOwn(inputRpcs, name));
732
+ if (collisions.length > 0) throw new Error(`defineContract: name collision between consumers and rpcs — keys must be disjoint. Conflicting names: ${collisions.join(", ")}`);
733
+ }
809
734
  const result = {
810
735
  exchanges: {},
811
736
  queues: {},
812
737
  bindings: {},
813
738
  publishers: {},
814
- consumers: {}
739
+ consumers: {},
740
+ rpcs: {}
815
741
  };
816
742
  if (inputPublishers && Object.keys(inputPublishers).length > 0) {
817
743
  const processedPublishers = {};
@@ -851,9 +777,10 @@ function defineContract(definition) {
851
777
  processedConsumers[name] = entry.consumer;
852
778
  consumerBindings[`${name}Binding`] = entry.binding;
853
779
  const queueEntry = entry.consumer.queue;
854
- queues[queueEntry.name] = queueEntry;
780
+ const queueDef = extractQueue(queueEntry);
781
+ queues[queueDef.name] = queueEntry;
855
782
  exchanges[entry.binding.exchange.name] = entry.binding.exchange;
856
- if (queueEntry.deadLetter?.exchange) exchanges[queueEntry.deadLetter.exchange.name] = queueEntry.deadLetter.exchange;
783
+ if (queueDef.deadLetter?.exchange) exchanges[queueDef.deadLetter.exchange.name] = queueDef.deadLetter.exchange;
857
784
  if (entry.exchangeBinding) consumerBindings[`${name}ExchangeBinding`] = entry.exchangeBinding;
858
785
  if (entry.bridgeExchange) exchanges[entry.bridgeExchange.name] = entry.bridgeExchange;
859
786
  if (entry.exchange) exchanges[entry.exchange.name] = entry.exchange;
@@ -861,22 +788,24 @@ function defineContract(definition) {
861
788
  processedConsumers[name] = entry.consumer;
862
789
  consumerBindings[`${name}Binding`] = entry.binding;
863
790
  const queueEntry = entry.consumer.queue;
864
- queues[queueEntry.name] = queueEntry;
791
+ const queueDef = extractQueue(queueEntry);
792
+ queues[queueDef.name] = queueEntry;
865
793
  exchanges[entry.exchange.name] = entry.exchange;
866
- if (queueEntry.deadLetter?.exchange) exchanges[queueEntry.deadLetter.exchange.name] = queueEntry.deadLetter.exchange;
794
+ if (queueDef.deadLetter?.exchange) exchanges[queueDef.deadLetter.exchange.name] = queueDef.deadLetter.exchange;
867
795
  } else {
868
796
  const consumer = entry;
869
797
  processedConsumers[name] = consumer;
870
798
  const queueEntry = consumer.queue;
871
- queues[queueEntry.name] = queueEntry;
872
- if (queueEntry.deadLetter?.exchange) exchanges[queueEntry.deadLetter.exchange.name] = queueEntry.deadLetter.exchange;
799
+ const queueDef = extractQueue(queueEntry);
800
+ queues[queueDef.name] = queueEntry;
801
+ if (queueDef.deadLetter?.exchange) exchanges[queueDef.deadLetter.exchange.name] = queueDef.deadLetter.exchange;
873
802
  }
874
- for (const queue of Object.values(queues)) if (queue.retry?.mode === "ttl-backoff" && queue.deadLetter) {
875
- const infra = createTtlBackoffInfrastructure(queue);
876
- queues[infra.waitQueue.name] = infra.waitQueue;
877
- consumerBindings[`${queue.name}WaitBinding`] = infra.waitQueueBinding;
878
- consumerBindings[`${queue.name}RetryBinding`] = infra.mainQueueRetryBinding;
879
- exchanges[queue.deadLetter.exchange.name] = queue.deadLetter.exchange;
803
+ for (const queueEntry of Object.values(queues)) if (isQueueWithTtlBackoffInfrastructure(queueEntry)) {
804
+ queues[queueEntry.waitQueue.name] = queueEntry.waitQueue;
805
+ consumerBindings[`${queueEntry.queue.name}WaitBinding`] = queueEntry.waitQueueBinding;
806
+ consumerBindings[`${queueEntry.queue.name}RetryBinding`] = queueEntry.retryQueueBinding;
807
+ exchanges[queueEntry.waitExchange.name] = queueEntry.waitExchange;
808
+ exchanges[queueEntry.retryExchange.name] = queueEntry.retryExchange;
880
809
  }
881
810
  result.consumers = processedConsumers;
882
811
  result.bindings = {
@@ -892,59 +821,76 @@ function defineContract(definition) {
892
821
  ...exchanges
893
822
  };
894
823
  }
824
+ if (inputRpcs && Object.keys(inputRpcs).length > 0) {
825
+ const processedRpcs = {};
826
+ const rpcQueues = {};
827
+ const rpcExchanges = {};
828
+ for (const [name, rpc] of Object.entries(inputRpcs)) {
829
+ processedRpcs[name] = rpc;
830
+ const queueDef = extractQueue(rpc.queue);
831
+ rpcQueues[queueDef.name] = rpc.queue;
832
+ if (queueDef.deadLetter?.exchange) rpcExchanges[queueDef.deadLetter.exchange.name] = queueDef.deadLetter.exchange;
833
+ }
834
+ result.rpcs = processedRpcs;
835
+ result.queues = {
836
+ ...result.queues,
837
+ ...rpcQueues
838
+ };
839
+ result.exchanges = {
840
+ ...result.exchanges,
841
+ ...rpcExchanges
842
+ };
843
+ }
895
844
  return result;
896
845
  }
897
-
898
846
  //#endregion
899
- //#region src/builder/ttl-backoff.ts
847
+ //#region src/builder/rpc.ts
900
848
  /**
901
- * Create TTL-backoff retry infrastructure for a queue.
849
+ * Define an RPC operation: a request/response pair flowing over a request
850
+ * queue with replies routed back via RabbitMQ direct reply-to.
902
851
  *
903
- * This builder helper generates the wait queue and bindings needed for TTL-backoff retry.
904
- * The generated infrastructure can be spread into a contract definition.
852
+ * RPC is bidirectional on both ends the worker handler consumes the request
853
+ * and produces the response; `client.call(name, request, options)` publishes
854
+ * the request and awaits the typed response. Both sides share the same
855
+ * definition, so request and response schemas cannot drift between them.
905
856
  *
906
- * TTL-backoff retry works by:
907
- * 1. Failed messages are sent to the DLX with routing key `{queueName}-wait`
908
- * 2. The wait queue receives these messages and holds them for a TTL period
909
- * 3. After TTL expires, messages are dead-lettered back to the DLX with routing key `{queueName}`
910
- * 4. The main queue receives the retried message via its binding to the DLX
857
+ * Plug the result into `defineContract({ rpcs: { name: ... } })`. RPCs do not
858
+ * appear in `publishers` or `consumers`.
911
859
  *
912
- * @param queue - The main queue definition (must have deadLetter configured)
913
- * @param options - Optional configuration for the wait queue
914
- * @param options.waitQueueDurable - Whether the wait queue should be durable (default: same as main queue)
915
- * @returns TTL-backoff retry infrastructure containing wait queue and bindings
916
- * @throws {Error} If the queue does not have a dead letter exchange configured
860
+ * @param queue - The queue that receives RPC requests. The queue name is
861
+ * used as the routing key on the AMQP default direct exchange.
862
+ * @param messages.request - Schema validated against incoming request payloads
863
+ * (server side) and outgoing requests (client side).
864
+ * @param messages.response - Schema validated against handler return values
865
+ * (server side) and incoming replies (client side).
917
866
  *
918
867
  * @example
919
868
  * ```typescript
920
- * const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
921
- * const orderQueue = defineQueue('order-processing', {
922
- * type: 'quorum',
923
- * deadLetter: { exchange: dlx },
924
- * retry: {
925
- * mode: 'ttl-backoff',
926
- * maxRetries: 5,
927
- * initialDelayMs: 1000,
928
- * },
929
- * });
869
+ * import { defineQueue, defineMessage, defineRpc, defineContract } from '@amqp-contract/contract';
870
+ * import { z } from 'zod';
930
871
  *
931
- * // Infrastructure is auto-extracted when using defineContract:
932
- * const contract = defineContract({
933
- * publishers: { ... },
934
- * consumers: { processOrder: defineEventConsumer(event, extractQueue(orderQueue)) },
872
+ * const calculate = defineRpc(defineQueue('rpc.calculate'), {
873
+ * request: defineMessage(z.object({ a: z.number(), b: z.number() })),
874
+ * response: defineMessage(z.object({ sum: z.number() })),
935
875
  * });
936
- * // contract.queues includes the wait queue, contract.bindings includes retry bindings
937
876
  *
938
- * // Or generate manually for advanced use cases:
939
- * const retryInfra = defineTtlBackoffRetryInfrastructure(orderQueue);
877
+ * const contract = defineContract({ rpcs: { calculate } });
878
+ *
879
+ * // Server (worker): handler returns the typed response
880
+ * // handlers: { calculate: ({ payload }) => Future.value(Result.Ok({ sum: payload.a + payload.b })) }
881
+ *
882
+ * // Client: typed call with required timeout
883
+ * // const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 }).toPromise();
940
884
  * ```
941
885
  */
942
- function defineTtlBackoffRetryInfrastructure(queueEntry, options) {
943
- const infra = createTtlBackoffInfrastructure(extractQueue(queueEntry));
944
- if (options?.waitQueueDurable !== void 0) infra.waitQueue.durable = options.waitQueueDurable;
945
- return infra;
886
+ function defineRpc(queue, messages) {
887
+ return {
888
+ queue,
889
+ request: messages.request,
890
+ response: messages.response
891
+ };
946
892
  }
947
-
948
893
  //#endregion
949
- export { defineCommandConsumer, defineCommandPublisher, defineConsumer, defineContract, defineEventConsumer, defineEventPublisher, defineExchange, defineExchangeBinding, defineMessage, definePublisher, defineQueue, defineQueueBinding, defineQuorumQueue, defineTtlBackoffQueue, defineTtlBackoffRetryInfrastructure, extractConsumer, extractQueue, isBridgedPublisherConfig, isCommandConsumerConfig, isEventConsumerResult, isEventPublisherConfig, isQueueWithTtlBackoffInfrastructure };
894
+ export { defineCommandConsumer, defineCommandPublisher, defineConsumer, defineContract, defineEventConsumer, defineEventPublisher, defineExchange, defineExchangeBinding, defineMessage, definePublisher, defineQueue, defineQueueBinding, defineRpc, extractConsumer, extractQueue, isBridgedPublisherConfig, isCommandConsumerConfig, isEventConsumerResult, isEventPublisherConfig, isQueueWithTtlBackoffInfrastructure };
895
+
950
896
  //# sourceMappingURL=index.mjs.map