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