@amqp-contract/worker 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 +6 -7
- package/dist/index.cjs +415 -291
- package/dist/index.d.cts +123 -149
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +122 -148
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +409 -284
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +90 -115
- package/package.json +29 -29
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value:
|
|
2
|
-
let _amqp_contract_core = require("@amqp-contract/core");
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
2
|
let _amqp_contract_contract = require("@amqp-contract/contract");
|
|
3
|
+
let _amqp_contract_core = require("@amqp-contract/core");
|
|
4
4
|
let _swan_io_boxed = require("@swan-io/boxed");
|
|
5
5
|
let node_zlib = require("node:zlib");
|
|
6
6
|
let node_util = require("node:util");
|
|
7
|
-
|
|
7
|
+
//#region src/decompression.ts
|
|
8
|
+
const gunzipAsync = (0, node_util.promisify)(node_zlib.gunzip);
|
|
9
|
+
const inflateAsync = (0, node_util.promisify)(node_zlib.inflate);
|
|
10
|
+
/**
|
|
11
|
+
* Supported content encodings for message decompression.
|
|
12
|
+
*/
|
|
13
|
+
const SUPPORTED_ENCODINGS = ["gzip", "deflate"];
|
|
14
|
+
/**
|
|
15
|
+
* Type guard to check if a string is a supported encoding.
|
|
16
|
+
*/
|
|
17
|
+
function isSupportedEncoding(encoding) {
|
|
18
|
+
return SUPPORTED_ENCODINGS.includes(encoding.toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decompress a buffer based on the content-encoding header.
|
|
22
|
+
*
|
|
23
|
+
* @param buffer - The buffer to decompress
|
|
24
|
+
* @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')
|
|
25
|
+
* @returns A Future with the decompressed buffer or a TechnicalError
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
function decompressBuffer(buffer, contentEncoding) {
|
|
30
|
+
if (!contentEncoding) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(buffer));
|
|
31
|
+
const normalizedEncoding = contentEncoding.toLowerCase();
|
|
32
|
+
if (!isSupportedEncoding(normalizedEncoding)) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`Unsupported content-encoding: "${contentEncoding}". Supported encodings are: ${SUPPORTED_ENCODINGS.join(", ")}. Please check your publisher configuration.`)));
|
|
33
|
+
switch (normalizedEncoding) {
|
|
34
|
+
case "gzip": return _swan_io_boxed.Future.fromPromise(gunzipAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to decompress gzip", error));
|
|
35
|
+
case "deflate": return _swan_io_boxed.Future.fromPromise(inflateAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to decompress deflate", error));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
8
39
|
//#region src/errors.ts
|
|
9
40
|
/**
|
|
10
41
|
* Retryable errors - transient failures that may succeed on retry
|
|
@@ -165,40 +196,231 @@ function retryable(message, cause) {
|
|
|
165
196
|
function nonRetryable(message, cause) {
|
|
166
197
|
return new NonRetryableError(message, cause);
|
|
167
198
|
}
|
|
168
|
-
|
|
169
199
|
//#endregion
|
|
170
|
-
//#region src/
|
|
171
|
-
const gunzipAsync = (0, node_util.promisify)(node_zlib.gunzip);
|
|
172
|
-
const inflateAsync = (0, node_util.promisify)(node_zlib.inflate);
|
|
200
|
+
//#region src/retry.ts
|
|
173
201
|
/**
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
*
|
|
202
|
+
* Handle error in message processing with retry logic.
|
|
203
|
+
*
|
|
204
|
+
* Flow depends on retry mode:
|
|
205
|
+
*
|
|
206
|
+
* **immediate-requeue mode:**
|
|
207
|
+
* 1. If NonRetryableError -> send directly to DLQ (no retry)
|
|
208
|
+
* 2. If max retries exceeded -> send to DLQ
|
|
209
|
+
* 3. Otherwise -> requeue immediately for retry
|
|
210
|
+
*
|
|
211
|
+
* **ttl-backoff mode:**
|
|
212
|
+
* 1. If NonRetryableError -> send directly to DLQ (no retry)
|
|
213
|
+
* 2. If max retries exceeded -> send to DLQ
|
|
214
|
+
* 3. Otherwise -> publish to wait queue with TTL for retry
|
|
215
|
+
*
|
|
216
|
+
* **none mode (no retry config):**
|
|
217
|
+
* 1. send directly to DLQ (no retry)
|
|
179
218
|
*/
|
|
180
|
-
function
|
|
181
|
-
|
|
219
|
+
function handleError(ctx, error, msg, consumerName, consumer) {
|
|
220
|
+
if (error instanceof NonRetryableError) {
|
|
221
|
+
ctx.logger?.error("Non-retryable error, sending to DLQ immediately", {
|
|
222
|
+
consumerName,
|
|
223
|
+
errorType: error.name,
|
|
224
|
+
error: error.message
|
|
225
|
+
});
|
|
226
|
+
sendToDLQ(ctx, msg, consumer);
|
|
227
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
228
|
+
}
|
|
229
|
+
const config = (0, _amqp_contract_contract.extractQueue)(consumer.queue).retry;
|
|
230
|
+
if (config.mode === "immediate-requeue") return handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, config);
|
|
231
|
+
if (config.mode === "ttl-backoff") return handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config);
|
|
232
|
+
ctx.logger?.warn("Retry disabled (none mode), sending to DLQ", {
|
|
233
|
+
consumerName,
|
|
234
|
+
error: error.message
|
|
235
|
+
});
|
|
236
|
+
sendToDLQ(ctx, msg, consumer);
|
|
237
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
182
238
|
}
|
|
183
239
|
/**
|
|
184
|
-
*
|
|
240
|
+
* Handle error by requeuing immediately.
|
|
185
241
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
242
|
+
* For quorum queues, messages are requeued with `nack(requeue=true)`, and the worker tracks delivery count via the native RabbitMQ `x-delivery-count` header.
|
|
243
|
+
* For classic queues, messages are re-published on the same queue, and the worker tracks delivery count via a custom `x-retry-count` header.
|
|
244
|
+
* When the count exceeds `maxRetries`, the message is automatically dead-lettered (if DLX is configured) or dropped.
|
|
189
245
|
*
|
|
190
|
-
*
|
|
246
|
+
* This is simpler than TTL-based retry but provides immediate retries only.
|
|
191
247
|
*/
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
248
|
+
function handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, config) {
|
|
249
|
+
const queue = (0, _amqp_contract_contract.extractQueue)(consumer.queue);
|
|
250
|
+
const queueName = queue.name;
|
|
251
|
+
const retryCount = queue.type === "quorum" ? msg.properties.headers?.["x-delivery-count"] ?? 0 : msg.properties.headers?.["x-retry-count"] ?? 0;
|
|
252
|
+
if (retryCount >= config.maxRetries) {
|
|
253
|
+
ctx.logger?.error("Max retries exceeded, sending to DLQ (immediate-requeue mode)", {
|
|
254
|
+
consumerName,
|
|
255
|
+
queueName,
|
|
256
|
+
retryCount,
|
|
257
|
+
maxRetries: config.maxRetries,
|
|
258
|
+
error: error.message
|
|
259
|
+
});
|
|
260
|
+
sendToDLQ(ctx, msg, consumer);
|
|
261
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
199
262
|
}
|
|
263
|
+
ctx.logger?.warn("Retrying message (immediate-requeue mode)", {
|
|
264
|
+
consumerName,
|
|
265
|
+
queueName,
|
|
266
|
+
retryCount,
|
|
267
|
+
maxRetries: config.maxRetries,
|
|
268
|
+
error: error.message
|
|
269
|
+
});
|
|
270
|
+
if (queue.type === "quorum") {
|
|
271
|
+
ctx.amqpClient.nack(msg, false, true);
|
|
272
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
273
|
+
} else return publishForRetry(ctx, {
|
|
274
|
+
msg,
|
|
275
|
+
exchange: msg.fields.exchange,
|
|
276
|
+
routingKey: msg.fields.routingKey,
|
|
277
|
+
queueName,
|
|
278
|
+
error
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Handle error using TTL + wait queue pattern for exponential backoff.
|
|
283
|
+
*
|
|
284
|
+
* ┌─────────────────────────────────────────────────────────────────┐
|
|
285
|
+
* │ Retry Flow (Native RabbitMQ TTL + Wait queue pattern) │
|
|
286
|
+
* ├─────────────────────────────────────────────────────────────────┤
|
|
287
|
+
* │ │
|
|
288
|
+
* │ 1. Handler throws any Error │
|
|
289
|
+
* │ ↓ │
|
|
290
|
+
* │ 2. Worker publishes to wait exchange |
|
|
291
|
+
* | (with header `x-wait-queue` set to the wait queue name) │
|
|
292
|
+
* │ ↓ │
|
|
293
|
+
* │ 3. Wait exchange routes to wait queue │
|
|
294
|
+
* │ (with expiration: calculated backoff delay) │
|
|
295
|
+
* │ ↓ │
|
|
296
|
+
* │ 4. Message waits in queue until TTL expires │
|
|
297
|
+
* │ ↓ │
|
|
298
|
+
* │ 5. Expired message dead-lettered to retry exchange |
|
|
299
|
+
* | (with header `x-retry-queue` set to the main queue name) │
|
|
300
|
+
* │ ↓ │
|
|
301
|
+
* │ 6. Retry exchange routes back to main queue → RETRY │
|
|
302
|
+
* │ ↓ │
|
|
303
|
+
* │ 7. If retries exhausted: nack without requeue → DLQ │
|
|
304
|
+
* │ │
|
|
305
|
+
* └─────────────────────────────────────────────────────────────────┘
|
|
306
|
+
*/
|
|
307
|
+
function handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config) {
|
|
308
|
+
if (!(0, _amqp_contract_contract.isQueueWithTtlBackoffInfrastructure)(consumer.queue)) {
|
|
309
|
+
ctx.logger?.error("Queue does not have TTL-backoff infrastructure", {
|
|
310
|
+
consumerName,
|
|
311
|
+
queueName: consumer.queue.name
|
|
312
|
+
});
|
|
313
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("Queue does not have TTL-backoff infrastructure")));
|
|
314
|
+
}
|
|
315
|
+
const queueEntry = consumer.queue;
|
|
316
|
+
const queueName = (0, _amqp_contract_contract.extractQueue)(queueEntry).name;
|
|
317
|
+
const retryCount = msg.properties.headers?.["x-retry-count"] ?? 0;
|
|
318
|
+
if (retryCount >= config.maxRetries) {
|
|
319
|
+
ctx.logger?.error("Max retries exceeded, sending to DLQ (ttl-backoff mode)", {
|
|
320
|
+
consumerName,
|
|
321
|
+
queueName,
|
|
322
|
+
retryCount,
|
|
323
|
+
maxRetries: config.maxRetries,
|
|
324
|
+
error: error.message
|
|
325
|
+
});
|
|
326
|
+
sendToDLQ(ctx, msg, consumer);
|
|
327
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
328
|
+
}
|
|
329
|
+
const delayMs = calculateRetryDelay(retryCount, config);
|
|
330
|
+
ctx.logger?.warn("Retrying message (ttl-backoff mode)", {
|
|
331
|
+
consumerName,
|
|
332
|
+
queueName,
|
|
333
|
+
retryCount: retryCount + 1,
|
|
334
|
+
maxRetries: config.maxRetries,
|
|
335
|
+
delayMs,
|
|
336
|
+
error: error.message
|
|
337
|
+
});
|
|
338
|
+
return publishForRetry(ctx, {
|
|
339
|
+
msg,
|
|
340
|
+
exchange: queueEntry.waitExchange.name,
|
|
341
|
+
routingKey: msg.fields.routingKey,
|
|
342
|
+
waitQueueName: queueEntry.waitQueue.name,
|
|
343
|
+
queueName,
|
|
344
|
+
delayMs,
|
|
345
|
+
error
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Calculate retry delay with exponential backoff and optional jitter.
|
|
350
|
+
*/
|
|
351
|
+
function calculateRetryDelay(retryCount, config) {
|
|
352
|
+
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = config;
|
|
353
|
+
let delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, retryCount), maxDelayMs);
|
|
354
|
+
if (jitter) delay = delay * (.5 + Math.random() * .5);
|
|
355
|
+
return Math.floor(delay);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Parse message content for republishing.
|
|
359
|
+
* Prevents double JSON serialization by converting Buffer to object when possible.
|
|
360
|
+
*/
|
|
361
|
+
function parseMessageContentForRetry(ctx, msg, queueName) {
|
|
362
|
+
let content = msg.content;
|
|
363
|
+
if (!msg.properties.contentEncoding) try {
|
|
364
|
+
content = JSON.parse(msg.content.toString());
|
|
365
|
+
} catch (err) {
|
|
366
|
+
ctx.logger?.warn("Failed to parse message for retry, using original buffer", {
|
|
367
|
+
queueName,
|
|
368
|
+
error: err
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return content;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Publish message with an incremented x-retry-count header and optional TTL.
|
|
375
|
+
*/
|
|
376
|
+
function publishForRetry(ctx, { msg, exchange, routingKey, queueName, waitQueueName, delayMs, error }) {
|
|
377
|
+
const newRetryCount = (msg.properties.headers?.["x-retry-count"] ?? 0) + 1;
|
|
378
|
+
ctx.amqpClient.ack(msg);
|
|
379
|
+
const content = parseMessageContentForRetry(ctx, msg, queueName);
|
|
380
|
+
return ctx.amqpClient.publish(exchange, routingKey, content, {
|
|
381
|
+
...msg.properties,
|
|
382
|
+
...delayMs !== void 0 ? { expiration: delayMs.toString() } : {},
|
|
383
|
+
headers: {
|
|
384
|
+
...msg.properties.headers,
|
|
385
|
+
"x-retry-count": newRetryCount,
|
|
386
|
+
"x-last-error": error.message,
|
|
387
|
+
"x-first-failure-timestamp": msg.properties.headers?.["x-first-failure-timestamp"] ?? Date.now(),
|
|
388
|
+
...waitQueueName !== void 0 ? {
|
|
389
|
+
"x-wait-queue": waitQueueName,
|
|
390
|
+
"x-retry-queue": queueName
|
|
391
|
+
} : {}
|
|
392
|
+
}
|
|
393
|
+
}).mapOkToResult((published) => {
|
|
394
|
+
if (!published) {
|
|
395
|
+
ctx.logger?.error("Failed to publish message for retry (write buffer full)", {
|
|
396
|
+
queueName,
|
|
397
|
+
retryCount: newRetryCount,
|
|
398
|
+
...delayMs !== void 0 ? { delayMs } : {}
|
|
399
|
+
});
|
|
400
|
+
return _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("Failed to publish message for retry (write buffer full)"));
|
|
401
|
+
}
|
|
402
|
+
ctx.logger?.info("Message published for retry", {
|
|
403
|
+
queueName,
|
|
404
|
+
retryCount: newRetryCount,
|
|
405
|
+
...delayMs !== void 0 ? { delayMs } : {}
|
|
406
|
+
});
|
|
407
|
+
return _swan_io_boxed.Result.Ok(void 0);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Send message to dead letter queue.
|
|
412
|
+
* Nacks the message without requeue, relying on DLX configuration.
|
|
413
|
+
*/
|
|
414
|
+
function sendToDLQ(ctx, msg, consumer) {
|
|
415
|
+
const queue = (0, _amqp_contract_contract.extractQueue)(consumer.queue);
|
|
416
|
+
const queueName = queue.name;
|
|
417
|
+
if (!(queue.deadLetter !== void 0)) ctx.logger?.warn("Queue does not have DLX configured - message will be lost on nack", { queueName });
|
|
418
|
+
ctx.logger?.info("Sending message to DLQ", {
|
|
419
|
+
queueName,
|
|
420
|
+
deliveryTag: msg.fields.deliveryTag
|
|
421
|
+
});
|
|
422
|
+
ctx.amqpClient.nack(msg, false, false);
|
|
200
423
|
}
|
|
201
|
-
|
|
202
424
|
//#endregion
|
|
203
425
|
//#region src/worker.ts
|
|
204
426
|
/**
|
|
@@ -221,7 +443,7 @@ function isHandlerTuple(entry) {
|
|
|
221
443
|
* import { defineQueue, defineMessage, defineContract, defineConsumer } from '@amqp-contract/contract';
|
|
222
444
|
* import { z } from 'zod';
|
|
223
445
|
*
|
|
224
|
-
* const orderQueue = defineQueue('order-processing'
|
|
446
|
+
* const orderQueue = defineQueue('order-processing');
|
|
225
447
|
* const orderMessage = defineMessage(z.object({
|
|
226
448
|
* orderId: z.string(),
|
|
227
449
|
* amount: z.number()
|
|
@@ -250,31 +472,67 @@ function isHandlerTuple(entry) {
|
|
|
250
472
|
*/
|
|
251
473
|
var TypedAmqpWorker = class TypedAmqpWorker {
|
|
252
474
|
/**
|
|
253
|
-
* Internal handler storage
|
|
475
|
+
* Internal handler storage. Keyed by handler name (consumer or RPC); the
|
|
476
|
+
* stored function signature is widened so the dispatch loop can call it
|
|
477
|
+
* uniformly. The actual handler is type-checked at the worker's public API
|
|
478
|
+
* boundary via `WorkerInferHandlers<TContract>`.
|
|
254
479
|
*/
|
|
255
480
|
actualHandlers;
|
|
256
481
|
consumerOptions;
|
|
257
482
|
consumerTags = /* @__PURE__ */ new Set();
|
|
258
483
|
telemetry;
|
|
259
|
-
constructor(contract, amqpClient, handlers, logger, telemetry) {
|
|
484
|
+
constructor(contract, amqpClient, handlers, defaultConsumerOptions, logger, telemetry) {
|
|
260
485
|
this.contract = contract;
|
|
261
486
|
this.amqpClient = amqpClient;
|
|
487
|
+
this.defaultConsumerOptions = defaultConsumerOptions;
|
|
262
488
|
this.logger = logger;
|
|
263
489
|
this.telemetry = telemetry ?? _amqp_contract_core.defaultTelemetryProvider;
|
|
264
490
|
this.actualHandlers = {};
|
|
265
491
|
this.consumerOptions = {};
|
|
266
492
|
const handlersRecord = handlers;
|
|
267
|
-
for (const
|
|
268
|
-
const handlerEntry = handlersRecord[
|
|
269
|
-
const
|
|
493
|
+
for (const handlerName of Object.keys(handlersRecord)) {
|
|
494
|
+
const handlerEntry = handlersRecord[handlerName];
|
|
495
|
+
const typedName = handlerName;
|
|
270
496
|
if (isHandlerTuple(handlerEntry)) {
|
|
271
497
|
const [handler, options] = handlerEntry;
|
|
272
|
-
this.actualHandlers[
|
|
273
|
-
this.consumerOptions[
|
|
274
|
-
|
|
498
|
+
this.actualHandlers[typedName] = handler;
|
|
499
|
+
this.consumerOptions[typedName] = {
|
|
500
|
+
...this.defaultConsumerOptions,
|
|
501
|
+
...options
|
|
502
|
+
};
|
|
503
|
+
} else {
|
|
504
|
+
this.actualHandlers[typedName] = handlerEntry;
|
|
505
|
+
this.consumerOptions[typedName] = this.defaultConsumerOptions;
|
|
506
|
+
}
|
|
275
507
|
}
|
|
276
508
|
}
|
|
277
509
|
/**
|
|
510
|
+
* Build a `ConsumerDefinition`-shaped view for a handler name, regardless
|
|
511
|
+
* of whether it came from `contract.consumers` or `contract.rpcs`. The
|
|
512
|
+
* dispatch path treats both uniformly; the returned `isRpc` flag (and the
|
|
513
|
+
* accompanying `responseSchema`) tells `processMessage` whether to validate
|
|
514
|
+
* the handler return value and publish a reply.
|
|
515
|
+
*/
|
|
516
|
+
resolveConsumerView(name) {
|
|
517
|
+
const rpcs = this.contract.rpcs;
|
|
518
|
+
if (rpcs && Object.hasOwn(rpcs, name)) {
|
|
519
|
+
const rpc = rpcs[name];
|
|
520
|
+
return {
|
|
521
|
+
consumer: {
|
|
522
|
+
queue: rpc.queue,
|
|
523
|
+
message: rpc.request
|
|
524
|
+
},
|
|
525
|
+
isRpc: true,
|
|
526
|
+
responseSchema: rpc.response.payload
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const consumerEntry = this.contract.consumers[name];
|
|
530
|
+
return {
|
|
531
|
+
consumer: (0, _amqp_contract_contract.extractConsumer)(consumerEntry),
|
|
532
|
+
isRpc: false
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
278
536
|
* Create a type-safe AMQP worker from a contract.
|
|
279
537
|
*
|
|
280
538
|
* Connection management (including automatic reconnection) is handled internally
|
|
@@ -299,12 +557,18 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
299
557
|
* }).resultToPromise();
|
|
300
558
|
* ```
|
|
301
559
|
*/
|
|
302
|
-
static create({ contract, handlers, urls, connectionOptions, logger, telemetry }) {
|
|
560
|
+
static create({ contract, handlers, urls, connectionOptions, defaultConsumerOptions, logger, telemetry, connectTimeoutMs }) {
|
|
303
561
|
const worker = new TypedAmqpWorker(contract, new _amqp_contract_core.AmqpClient(contract, {
|
|
304
562
|
urls,
|
|
305
|
-
connectionOptions
|
|
306
|
-
|
|
307
|
-
|
|
563
|
+
connectionOptions,
|
|
564
|
+
connectTimeoutMs
|
|
565
|
+
}), handlers, defaultConsumerOptions ?? {}, logger, telemetry);
|
|
566
|
+
return worker.waitForConnectionReady().flatMapOk(() => worker.consumeAll()).flatMap((result) => result.match({
|
|
567
|
+
Ok: () => _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(worker)),
|
|
568
|
+
Error: (error) => worker.close().tapError((closeError) => {
|
|
569
|
+
logger?.warn("Failed to close worker after setup failure", { error: closeError });
|
|
570
|
+
}).map(() => _swan_io_boxed.Result.Error(error))
|
|
571
|
+
}));
|
|
308
572
|
}
|
|
309
573
|
/**
|
|
310
574
|
* Close the AMQP channel and connection.
|
|
@@ -334,40 +598,25 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
334
598
|
}).flatMapOk(() => this.amqpClient.close()).mapOk(() => void 0);
|
|
335
599
|
}
|
|
336
600
|
/**
|
|
337
|
-
*
|
|
338
|
-
* Defaults are applied in the contract's defineQueue, so we just return the config.
|
|
339
|
-
*/
|
|
340
|
-
getRetryConfigForConsumer(consumer) {
|
|
341
|
-
return consumer.queue.retry;
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Start consuming messages for all consumers.
|
|
345
|
-
* TypeScript guarantees consumers exist (handlers require matching consumers).
|
|
601
|
+
* Start consuming for every entry in `contract.consumers` and `contract.rpcs`.
|
|
346
602
|
*/
|
|
347
603
|
consumeAll() {
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
return prefetch ? Math.max(max, prefetch) : max;
|
|
353
|
-
}, 0);
|
|
354
|
-
if (maxPrefetch > 0) this.amqpClient.addSetup(async (channel) => {
|
|
355
|
-
await channel.prefetch(maxPrefetch);
|
|
356
|
-
});
|
|
357
|
-
return _swan_io_boxed.Future.all(consumerNames.map((name) => this.consume(name))).map(_swan_io_boxed.Result.all).mapOk(() => void 0);
|
|
604
|
+
const consumerNames = Object.keys(this.contract.consumers ?? {});
|
|
605
|
+
const rpcNames = Object.keys(this.contract.rpcs ?? {});
|
|
606
|
+
const allNames = [...consumerNames, ...rpcNames];
|
|
607
|
+
return _swan_io_boxed.Future.all(allNames.map((name) => this.consume(name))).map(_swan_io_boxed.Result.all).mapOk(() => void 0);
|
|
358
608
|
}
|
|
359
609
|
waitForConnectionReady() {
|
|
360
610
|
return this.amqpClient.waitForConnect();
|
|
361
611
|
}
|
|
362
612
|
/**
|
|
363
|
-
* Start consuming messages for a specific
|
|
364
|
-
*
|
|
613
|
+
* Start consuming messages for a specific handler — either a `consumers`
|
|
614
|
+
* entry (regular event/command consumer) or an `rpcs` entry (RPC server).
|
|
365
615
|
*/
|
|
366
|
-
consume(
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
return this.consumeSingle(consumerName, consumer, handler);
|
|
616
|
+
consume(name) {
|
|
617
|
+
const view = this.resolveConsumerView(name);
|
|
618
|
+
const handler = this.actualHandlers[name];
|
|
619
|
+
return this.consumeSingle(name, view, handler);
|
|
371
620
|
}
|
|
372
621
|
/**
|
|
373
622
|
* Validate data against a Standard Schema and handle errors.
|
|
@@ -392,9 +641,10 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
392
641
|
* @returns Ok with validated message (payload + headers), or Error (message already nacked)
|
|
393
642
|
*/
|
|
394
643
|
parseAndValidateMessage(msg, consumer, consumerName) {
|
|
644
|
+
const queue = (0, _amqp_contract_contract.extractQueue)(consumer.queue);
|
|
395
645
|
const context = {
|
|
396
646
|
consumerName: String(consumerName),
|
|
397
|
-
queueName:
|
|
647
|
+
queueName: queue.name
|
|
398
648
|
};
|
|
399
649
|
const nackAndError = (message, error) => {
|
|
400
650
|
this.logger?.error(message, {
|
|
@@ -424,244 +674,119 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
424
674
|
}).map(_swan_io_boxed.Result.allFromDict);
|
|
425
675
|
}
|
|
426
676
|
/**
|
|
427
|
-
*
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return this.amqpClient.consume(queueName, async (msg) => {
|
|
432
|
-
if (msg === null) {
|
|
433
|
-
this.logger?.warn("Consumer cancelled by server", {
|
|
434
|
-
consumerName: String(consumerName),
|
|
435
|
-
queueName
|
|
436
|
-
});
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
const startTime = Date.now();
|
|
440
|
-
const span = (0, _amqp_contract_core.startConsumeSpan)(this.telemetry, queueName, String(consumerName), { "messaging.rabbitmq.message.delivery_tag": msg.fields.deliveryTag });
|
|
441
|
-
await this.parseAndValidateMessage(msg, consumer, consumerName).flatMapOk((validatedMessage) => handler(validatedMessage, msg).flatMapOk(() => {
|
|
442
|
-
this.logger?.info("Message consumed successfully", {
|
|
443
|
-
consumerName: String(consumerName),
|
|
444
|
-
queueName
|
|
445
|
-
});
|
|
446
|
-
this.amqpClient.ack(msg);
|
|
447
|
-
const durationMs = Date.now() - startTime;
|
|
448
|
-
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
449
|
-
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(consumerName), true, durationMs);
|
|
450
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
451
|
-
}).flatMapError((handlerError) => {
|
|
452
|
-
this.logger?.error("Error processing message", {
|
|
453
|
-
consumerName: String(consumerName),
|
|
454
|
-
queueName,
|
|
455
|
-
errorType: handlerError.name,
|
|
456
|
-
error: handlerError.message
|
|
457
|
-
});
|
|
458
|
-
const durationMs = Date.now() - startTime;
|
|
459
|
-
(0, _amqp_contract_core.endSpanError)(span, handlerError);
|
|
460
|
-
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(consumerName), false, durationMs);
|
|
461
|
-
return this.handleError(handlerError, msg, String(consumerName), consumer);
|
|
462
|
-
})).tapError(() => {
|
|
463
|
-
const durationMs = Date.now() - startTime;
|
|
464
|
-
(0, _amqp_contract_core.endSpanError)(span, /* @__PURE__ */ new Error("Message validation failed"));
|
|
465
|
-
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(consumerName), false, durationMs);
|
|
466
|
-
}).toPromise();
|
|
467
|
-
}).tapOk((consumerTag) => {
|
|
468
|
-
this.consumerTags.add(consumerTag);
|
|
469
|
-
}).mapError((error) => new _amqp_contract_core.TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Handle error in message processing with retry logic.
|
|
677
|
+
* Validate an RPC handler's response and publish it back to the caller's reply
|
|
678
|
+
* queue with the same `correlationId`. Published via the AMQP default exchange
|
|
679
|
+
* with `routingKey = msg.properties.replyTo`, which works for both
|
|
680
|
+
* `amq.rabbitmq.reply-to` and any anonymous queue declared by the caller.
|
|
473
681
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* 1. If NonRetryableError -> send directly to DLQ (no retry)
|
|
478
|
-
* 2. Otherwise -> nack with requeue=true (RabbitMQ handles delivery count)
|
|
479
|
-
*
|
|
480
|
-
* **ttl-backoff mode:**
|
|
481
|
-
* 1. If NonRetryableError -> send directly to DLQ (no retry)
|
|
482
|
-
* 2. If max retries exceeded -> send to DLQ
|
|
483
|
-
* 3. Otherwise -> publish to wait queue with TTL for retry
|
|
484
|
-
*
|
|
485
|
-
* **Legacy mode (no retry config):**
|
|
486
|
-
* 1. nack with requeue=true (immediate requeue)
|
|
682
|
+
* Validation errors are surfaced as NonRetryableError (handler returned the
|
|
683
|
+
* wrong shape — retrying the same input will not fix it). Publish errors are
|
|
684
|
+
* surfaced as RetryableError so the worker's existing retry logic applies.
|
|
487
685
|
*/
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
686
|
+
publishRpcResponse(msg, queueName, rpcName, responseSchema, response) {
|
|
687
|
+
const replyTo = msg.properties.replyTo;
|
|
688
|
+
const correlationId = msg.properties.correlationId;
|
|
689
|
+
if (typeof replyTo !== "string" || replyTo.length === 0) {
|
|
690
|
+
this.logger?.warn("RPC handler returned a response but the incoming message has no replyTo; dropping response", {
|
|
691
|
+
rpcName: String(rpcName),
|
|
692
|
+
queueName
|
|
494
693
|
});
|
|
495
|
-
this.sendToDLQ(msg, consumer);
|
|
496
694
|
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
497
695
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
* Handle error using quorum queue's native delivery limit feature.
|
|
504
|
-
*
|
|
505
|
-
* Simply requeues the message with nack(requeue=true). RabbitMQ automatically:
|
|
506
|
-
* - Increments x-delivery-count header
|
|
507
|
-
* - Dead-letters the message when count exceeds x-delivery-limit
|
|
508
|
-
*
|
|
509
|
-
* This is simpler than TTL-based retry but provides immediate retries only.
|
|
510
|
-
*/
|
|
511
|
-
handleErrorQuorumNative(error, msg, consumerName, consumer) {
|
|
512
|
-
const queue = consumer.queue;
|
|
513
|
-
const queueName = queue.name;
|
|
514
|
-
const deliveryCount = msg.properties.headers?.["x-delivery-count"] ?? 0;
|
|
515
|
-
const deliveryLimit = queue.type === "quorum" ? queue.deliveryLimit : void 0;
|
|
516
|
-
const attemptsBeforeDeadLetter = deliveryLimit !== void 0 ? Math.max(0, deliveryLimit - deliveryCount - 1) : "unknown";
|
|
517
|
-
if (deliveryLimit !== void 0 && deliveryCount >= deliveryLimit - 1) this.logger?.warn("Message at final delivery attempt (quorum-native mode)", {
|
|
518
|
-
consumerName,
|
|
519
|
-
queueName,
|
|
520
|
-
deliveryCount,
|
|
521
|
-
deliveryLimit,
|
|
522
|
-
willDeadLetterOnNextFailure: deliveryCount === deliveryLimit - 1,
|
|
523
|
-
alreadyExceededLimit: deliveryCount >= deliveryLimit,
|
|
524
|
-
error: error.message
|
|
525
|
-
});
|
|
526
|
-
else this.logger?.warn("Retrying message (quorum-native mode)", {
|
|
527
|
-
consumerName,
|
|
528
|
-
queueName,
|
|
529
|
-
deliveryCount,
|
|
530
|
-
deliveryLimit,
|
|
531
|
-
attemptsBeforeDeadLetter,
|
|
532
|
-
error: error.message
|
|
533
|
-
});
|
|
534
|
-
this.amqpClient.nack(msg, false, true);
|
|
535
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Handle error using TTL + wait queue pattern for exponential backoff.
|
|
539
|
-
*/
|
|
540
|
-
handleErrorTtlBackoff(error, msg, consumerName, consumer, config) {
|
|
541
|
-
const retryCount = msg.properties.headers?.["x-retry-count"] ?? 0;
|
|
542
|
-
if (retryCount >= config.maxRetries) {
|
|
543
|
-
this.logger?.error("Max retries exceeded, sending to DLQ", {
|
|
544
|
-
consumerName,
|
|
545
|
-
retryCount,
|
|
546
|
-
maxRetries: config.maxRetries,
|
|
547
|
-
error: error.message
|
|
696
|
+
if (typeof correlationId !== "string" || correlationId.length === 0) {
|
|
697
|
+
this.logger?.warn("RPC handler returned a response but the incoming message has no correlationId; dropping response", {
|
|
698
|
+
rpcName: String(rpcName),
|
|
699
|
+
queueName,
|
|
700
|
+
replyTo
|
|
548
701
|
});
|
|
549
|
-
this.sendToDLQ(msg, consumer);
|
|
550
702
|
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
551
703
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
error: error.message
|
|
558
|
-
});
|
|
559
|
-
return this.publishForRetry(msg, consumer, retryCount + 1, delayMs, error);
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Calculate retry delay with exponential backoff and optional jitter.
|
|
563
|
-
*/
|
|
564
|
-
calculateRetryDelay(retryCount, config) {
|
|
565
|
-
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = config;
|
|
566
|
-
let delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, retryCount), maxDelayMs);
|
|
567
|
-
if (jitter) delay = delay * (.5 + Math.random() * .5);
|
|
568
|
-
return Math.floor(delay);
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Parse message content for republishing.
|
|
572
|
-
* Prevents double JSON serialization by converting Buffer to object when possible.
|
|
573
|
-
*/
|
|
574
|
-
parseMessageContentForRetry(msg, queueName) {
|
|
575
|
-
let content = msg.content;
|
|
576
|
-
if (!msg.properties.contentEncoding) try {
|
|
577
|
-
content = JSON.parse(msg.content.toString());
|
|
578
|
-
} catch (err) {
|
|
579
|
-
this.logger?.warn("Failed to parse message for retry, using original buffer", {
|
|
580
|
-
queueName,
|
|
581
|
-
error: err
|
|
582
|
-
});
|
|
704
|
+
let rawValidation;
|
|
705
|
+
try {
|
|
706
|
+
rawValidation = responseSchema["~standard"].validate(response);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new NonRetryableError("RPC response schema validation threw", error)));
|
|
583
709
|
}
|
|
584
|
-
|
|
710
|
+
const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
|
|
711
|
+
return _swan_io_boxed.Future.fromPromise(validationPromise).mapError((error) => new NonRetryableError("RPC response schema validation threw", error)).mapOkToResult((validation) => {
|
|
712
|
+
if (validation.issues) return _swan_io_boxed.Result.Error(new NonRetryableError(`RPC response for "${String(rpcName)}" failed schema validation`, new _amqp_contract_core.MessageValidationError(String(rpcName), validation.issues)));
|
|
713
|
+
return _swan_io_boxed.Result.Ok(validation.value);
|
|
714
|
+
}).flatMapOk((validatedResponse) => this.amqpClient.publish("", replyTo, validatedResponse, {
|
|
715
|
+
correlationId,
|
|
716
|
+
contentType: "application/json"
|
|
717
|
+
}).mapErrorToResult((error) => _swan_io_boxed.Result.Error(new RetryableError("Failed to publish RPC response", error))).mapOkToResult((published) => published ? _swan_io_boxed.Result.Ok(void 0) : _swan_io_boxed.Result.Error(new RetryableError("Failed to publish RPC response: channel buffer full"))));
|
|
585
718
|
}
|
|
586
719
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* ┌─────────────────────────────────────────────────────────────────┐
|
|
590
|
-
* │ Retry Flow (Native RabbitMQ TTL + DLX Pattern) │
|
|
591
|
-
* ├─────────────────────────────────────────────────────────────────┤
|
|
592
|
-
* │ │
|
|
593
|
-
* │ 1. Handler throws any Error │
|
|
594
|
-
* │ ↓ │
|
|
595
|
-
* │ 2. Worker publishes to DLX with routing key: {queue}-wait │
|
|
596
|
-
* │ ↓ │
|
|
597
|
-
* │ 3. DLX routes to wait queue: {queue}-wait │
|
|
598
|
-
* │ (with expiration: calculated backoff delay) │
|
|
599
|
-
* │ ↓ │
|
|
600
|
-
* │ 4. Message waits in queue until TTL expires │
|
|
601
|
-
* │ ↓ │
|
|
602
|
-
* │ 5. Expired message dead-lettered to DLX │
|
|
603
|
-
* │ (with routing key: {queue}) │
|
|
604
|
-
* │ ↓ │
|
|
605
|
-
* │ 6. DLX routes back to main queue → RETRY │
|
|
606
|
-
* │ ↓ │
|
|
607
|
-
* │ 7. If retries exhausted: nack without requeue → DLQ │
|
|
608
|
-
* │ │
|
|
609
|
-
* └─────────────────────────────────────────────────────────────────┘
|
|
720
|
+
* Process a single consumed message: validate, invoke handler, optionally
|
|
721
|
+
* publish the RPC response, record telemetry, and handle errors.
|
|
610
722
|
*/
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return this.amqpClient.publish(dlxName, waitRoutingKey, content, {
|
|
624
|
-
...msg.properties,
|
|
625
|
-
expiration: delayMs.toString(),
|
|
626
|
-
headers: {
|
|
627
|
-
...msg.properties.headers,
|
|
628
|
-
"x-retry-count": newRetryCount,
|
|
629
|
-
"x-last-error": error.message,
|
|
630
|
-
"x-first-failure-timestamp": msg.properties.headers?.["x-first-failure-timestamp"] ?? Date.now()
|
|
631
|
-
}
|
|
632
|
-
}).mapOkToResult((published) => {
|
|
633
|
-
if (!published) {
|
|
634
|
-
this.logger?.error("Failed to publish message for retry (write buffer full)", {
|
|
635
|
-
queueName,
|
|
636
|
-
waitRoutingKey,
|
|
637
|
-
retryCount: newRetryCount
|
|
723
|
+
processMessage(msg, view, name, handler) {
|
|
724
|
+
const { consumer, isRpc, responseSchema } = view;
|
|
725
|
+
const queueName = (0, _amqp_contract_contract.extractQueue)(consumer.queue).name;
|
|
726
|
+
const startTime = Date.now();
|
|
727
|
+
const span = (0, _amqp_contract_core.startConsumeSpan)(this.telemetry, queueName, String(name), { "messaging.rabbitmq.message.delivery_tag": msg.fields.deliveryTag });
|
|
728
|
+
let messageHandled = false;
|
|
729
|
+
let firstError;
|
|
730
|
+
return this.parseAndValidateMessage(msg, consumer, name).flatMapOk((validatedMessage) => handler(validatedMessage, msg).flatMapOk((handlerResponse) => {
|
|
731
|
+
if (isRpc && responseSchema) return this.publishRpcResponse(msg, queueName, name, responseSchema, handlerResponse).flatMapOk(() => {
|
|
732
|
+
this.logger?.info("Message consumed successfully", {
|
|
733
|
+
consumerName: String(name),
|
|
734
|
+
queueName
|
|
638
735
|
});
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
736
|
+
this.amqpClient.ack(msg);
|
|
737
|
+
messageHandled = true;
|
|
738
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
739
|
+
});
|
|
740
|
+
this.logger?.info("Message consumed successfully", {
|
|
741
|
+
consumerName: String(name),
|
|
742
|
+
queueName
|
|
743
|
+
});
|
|
744
|
+
this.amqpClient.ack(msg);
|
|
745
|
+
messageHandled = true;
|
|
746
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
747
|
+
}).flatMapError((handlerError) => {
|
|
748
|
+
this.logger?.error("Error processing message", {
|
|
749
|
+
consumerName: String(name),
|
|
642
750
|
queueName,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
delayMs
|
|
751
|
+
errorType: handlerError.name,
|
|
752
|
+
error: handlerError.message
|
|
646
753
|
});
|
|
647
|
-
|
|
754
|
+
firstError = handlerError;
|
|
755
|
+
return handleError({
|
|
756
|
+
amqpClient: this.amqpClient,
|
|
757
|
+
logger: this.logger
|
|
758
|
+
}, handlerError, msg, String(name), consumer);
|
|
759
|
+
})).map((result) => {
|
|
760
|
+
const durationMs = Date.now() - startTime;
|
|
761
|
+
if (messageHandled) {
|
|
762
|
+
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
763
|
+
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(name), true, durationMs);
|
|
764
|
+
} else {
|
|
765
|
+
(0, _amqp_contract_core.endSpanError)(span, result.isError() ? result.error : firstError ?? /* @__PURE__ */ new Error("Unknown error"));
|
|
766
|
+
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(name), false, durationMs);
|
|
767
|
+
}
|
|
768
|
+
return result;
|
|
648
769
|
});
|
|
649
770
|
}
|
|
650
771
|
/**
|
|
651
|
-
*
|
|
652
|
-
* Nacks the message without requeue, relying on DLX configuration.
|
|
772
|
+
* Consume messages one at a time.
|
|
653
773
|
*/
|
|
654
|
-
|
|
655
|
-
const queueName = consumer.queue.name;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
774
|
+
consumeSingle(name, view, handler) {
|
|
775
|
+
const queueName = (0, _amqp_contract_contract.extractQueue)(view.consumer.queue).name;
|
|
776
|
+
return this.amqpClient.consume(queueName, async (msg) => {
|
|
777
|
+
if (msg === null) {
|
|
778
|
+
this.logger?.warn("Consumer cancelled by server", {
|
|
779
|
+
consumerName: String(name),
|
|
780
|
+
queueName
|
|
781
|
+
});
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
await this.processMessage(msg, view, name, handler).toPromise();
|
|
785
|
+
}, this.consumerOptions[name]).tapOk((consumerTag) => {
|
|
786
|
+
this.consumerTags.add(consumerTag);
|
|
787
|
+
}).mapError((error) => new _amqp_contract_core.TechnicalError(`Failed to start consuming for "${String(name)}"`, error)).mapOk(() => void 0);
|
|
662
788
|
}
|
|
663
789
|
};
|
|
664
|
-
|
|
665
790
|
//#endregion
|
|
666
791
|
//#region src/handlers.ts
|
|
667
792
|
/**
|
|
@@ -722,13 +847,12 @@ function defineHandlers(contract, handlers) {
|
|
|
722
847
|
validateHandlers(contract, handlers);
|
|
723
848
|
return handlers;
|
|
724
849
|
}
|
|
725
|
-
|
|
726
850
|
//#endregion
|
|
727
|
-
Object.defineProperty(exports,
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
851
|
+
Object.defineProperty(exports, "MessageValidationError", {
|
|
852
|
+
enumerable: true,
|
|
853
|
+
get: function() {
|
|
854
|
+
return _amqp_contract_core.MessageValidationError;
|
|
855
|
+
}
|
|
732
856
|
});
|
|
733
857
|
exports.NonRetryableError = NonRetryableError;
|
|
734
858
|
exports.RetryableError = RetryableError;
|
|
@@ -739,4 +863,4 @@ exports.isHandlerError = isHandlerError;
|
|
|
739
863
|
exports.isNonRetryableError = isNonRetryableError;
|
|
740
864
|
exports.isRetryableError = isRetryableError;
|
|
741
865
|
exports.nonRetryable = nonRetryable;
|
|
742
|
-
exports.retryable = retryable;
|
|
866
|
+
exports.retryable = retryable;
|