@amqp-contract/worker 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +104 -232
- package/dist/index.d.cts +17 -43
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +17 -43
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +104 -231
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +76 -226
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -5,36 +5,16 @@ let node_util = require("node:util");
|
|
|
5
5
|
|
|
6
6
|
//#region src/errors.ts
|
|
7
7
|
/**
|
|
8
|
-
* Base error class for worker errors
|
|
9
|
-
*/
|
|
10
|
-
var WorkerError = class extends Error {
|
|
11
|
-
constructor(message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "WorkerError";
|
|
14
|
-
const ErrorConstructor = Error;
|
|
15
|
-
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Error for technical/runtime failures in worker operations
|
|
20
|
-
* This includes validation failures, parsing failures, and processing failures
|
|
21
|
-
*/
|
|
22
|
-
var TechnicalError = class extends WorkerError {
|
|
23
|
-
constructor(message, cause) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.cause = cause;
|
|
26
|
-
this.name = "TechnicalError";
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
8
|
* Error thrown when message validation fails
|
|
31
9
|
*/
|
|
32
|
-
var MessageValidationError = class extends
|
|
10
|
+
var MessageValidationError = class extends Error {
|
|
33
11
|
constructor(consumerName, issues) {
|
|
34
12
|
super(`Message validation failed for consumer "${consumerName}"`);
|
|
35
13
|
this.consumerName = consumerName;
|
|
36
14
|
this.issues = issues;
|
|
37
15
|
this.name = "MessageValidationError";
|
|
16
|
+
const ErrorConstructor = Error;
|
|
17
|
+
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
|
|
38
18
|
}
|
|
39
19
|
};
|
|
40
20
|
/**
|
|
@@ -44,11 +24,13 @@ var MessageValidationError = class extends WorkerError {
|
|
|
44
24
|
* Use this error type when the operation might succeed if retried.
|
|
45
25
|
* The worker will apply exponential backoff and retry the message.
|
|
46
26
|
*/
|
|
47
|
-
var RetryableError = class extends
|
|
27
|
+
var RetryableError = class extends Error {
|
|
48
28
|
constructor(message, cause) {
|
|
49
29
|
super(message);
|
|
50
30
|
this.cause = cause;
|
|
51
31
|
this.name = "RetryableError";
|
|
32
|
+
const ErrorConstructor = Error;
|
|
33
|
+
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
|
|
52
34
|
}
|
|
53
35
|
};
|
|
54
36
|
/**
|
|
@@ -58,11 +40,13 @@ var RetryableError = class extends WorkerError {
|
|
|
58
40
|
* Use this error type when retrying would not help - the message will be
|
|
59
41
|
* immediately sent to the dead letter queue (DLQ) if configured.
|
|
60
42
|
*/
|
|
61
|
-
var NonRetryableError = class extends
|
|
43
|
+
var NonRetryableError = class extends Error {
|
|
62
44
|
constructor(message, cause) {
|
|
63
45
|
super(message);
|
|
64
46
|
this.cause = cause;
|
|
65
47
|
this.name = "NonRetryableError";
|
|
48
|
+
const ErrorConstructor = Error;
|
|
49
|
+
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
|
|
66
50
|
}
|
|
67
51
|
};
|
|
68
52
|
|
|
@@ -71,21 +55,31 @@ var NonRetryableError = class extends WorkerError {
|
|
|
71
55
|
const gunzipAsync = (0, node_util.promisify)(node_zlib.gunzip);
|
|
72
56
|
const inflateAsync = (0, node_util.promisify)(node_zlib.inflate);
|
|
73
57
|
/**
|
|
58
|
+
* Supported content encodings for message decompression.
|
|
59
|
+
*/
|
|
60
|
+
const SUPPORTED_ENCODINGS = ["gzip", "deflate"];
|
|
61
|
+
/**
|
|
62
|
+
* Type guard to check if a string is a supported encoding.
|
|
63
|
+
*/
|
|
64
|
+
function isSupportedEncoding(encoding) {
|
|
65
|
+
return SUPPORTED_ENCODINGS.includes(encoding.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
74
68
|
* Decompress a buffer based on the content-encoding header.
|
|
75
69
|
*
|
|
76
70
|
* @param buffer - The buffer to decompress
|
|
77
71
|
* @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')
|
|
78
|
-
* @returns A
|
|
79
|
-
* @throws Error if decompression fails or if the encoding is unsupported
|
|
72
|
+
* @returns A Future with the decompressed buffer or a TechnicalError
|
|
80
73
|
*
|
|
81
74
|
* @internal
|
|
82
75
|
*/
|
|
83
|
-
|
|
84
|
-
if (!contentEncoding) return buffer;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
function decompressBuffer(buffer, contentEncoding) {
|
|
77
|
+
if (!contentEncoding) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(buffer));
|
|
78
|
+
const normalizedEncoding = contentEncoding.toLowerCase();
|
|
79
|
+
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.`)));
|
|
80
|
+
switch (normalizedEncoding) {
|
|
81
|
+
case "gzip": return _swan_io_boxed.Future.fromPromise(gunzipAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to decompress gzip", error));
|
|
82
|
+
case "deflate": return _swan_io_boxed.Future.fromPromise(inflateAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to decompress deflate", error));
|
|
89
83
|
}
|
|
90
84
|
}
|
|
91
85
|
|
|
@@ -98,17 +92,6 @@ function isHandlerTuple(entry) {
|
|
|
98
92
|
return Array.isArray(entry) && entry.length === 2;
|
|
99
93
|
}
|
|
100
94
|
/**
|
|
101
|
-
* Type guard to check if a value is a Standard Schema v1 compliant schema.
|
|
102
|
-
*/
|
|
103
|
-
function isStandardSchema(value) {
|
|
104
|
-
if (typeof value !== "object" || value === null) return false;
|
|
105
|
-
if (!("~standard" in value)) return false;
|
|
106
|
-
const standard = value["~standard"];
|
|
107
|
-
if (typeof standard !== "object" || standard === null) return false;
|
|
108
|
-
if (!("validate" in standard)) return false;
|
|
109
|
-
return typeof standard.validate === "function";
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
95
|
* Type-safe AMQP worker for consuming messages from RabbitMQ.
|
|
113
96
|
*
|
|
114
97
|
* This class provides automatic message validation, connection management,
|
|
@@ -204,7 +187,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
204
187
|
urls,
|
|
205
188
|
connectionOptions
|
|
206
189
|
}), handlers, logger, telemetry);
|
|
207
|
-
return worker.waitForConnectionReady().flatMapOk(() => worker.
|
|
190
|
+
return worker.waitForConnectionReady().flatMapOk(() => worker.consumeAll()).mapOk(() => worker);
|
|
208
191
|
}
|
|
209
192
|
/**
|
|
210
193
|
* Close the AMQP channel and connection.
|
|
@@ -223,7 +206,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
223
206
|
* ```
|
|
224
207
|
*/
|
|
225
208
|
close() {
|
|
226
|
-
return _swan_io_boxed.Future.all(Array.from(this.consumerTags).map((consumerTag) =>
|
|
209
|
+
return _swan_io_boxed.Future.all(Array.from(this.consumerTags).map((consumerTag) => this.amqpClient.cancel(consumerTag).mapErrorToResult((error) => {
|
|
227
210
|
this.logger?.warn("Failed to cancel consumer during close", {
|
|
228
211
|
consumerTag,
|
|
229
212
|
error
|
|
@@ -231,213 +214,103 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
231
214
|
return _swan_io_boxed.Result.Ok(void 0);
|
|
232
215
|
}))).map(_swan_io_boxed.Result.all).tapOk(() => {
|
|
233
216
|
this.consumerTags.clear();
|
|
234
|
-
}).flatMapOk(() =>
|
|
217
|
+
}).flatMapOk(() => this.amqpClient.close()).mapOk(() => void 0);
|
|
235
218
|
}
|
|
236
219
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* For quorum-native mode, validates that the queue is properly configured.
|
|
240
|
-
* For TTL-backoff mode, wait queues are created by setupAmqpTopology in the core package.
|
|
241
|
-
*/
|
|
242
|
-
validateRetryConfiguration() {
|
|
243
|
-
if (!this.contract.consumers) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
244
|
-
for (const consumerName of Object.keys(this.contract.consumers)) {
|
|
245
|
-
const consumer = this.contract.consumers[consumerName];
|
|
246
|
-
if (!consumer) continue;
|
|
247
|
-
const queue = consumer.queue;
|
|
248
|
-
if ((queue.retry?.mode ?? "ttl-backoff") === "quorum-native") {
|
|
249
|
-
const validationError = this.validateQuorumNativeConfigForConsumer(String(consumerName), consumer);
|
|
250
|
-
if (validationError) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(validationError));
|
|
251
|
-
this.logger?.info("Using quorum-native retry mode", {
|
|
252
|
-
consumerName: String(consumerName),
|
|
253
|
-
queueName: queue.name
|
|
254
|
-
});
|
|
255
|
-
} else if (queue.deadLetter) this.logger?.info("Using TTL-backoff retry mode", {
|
|
256
|
-
consumerName: String(consumerName),
|
|
257
|
-
queueName: queue.name
|
|
258
|
-
});
|
|
259
|
-
else this.logger?.warn("Queue has no deadLetter configured - retries will use nack with requeue", {
|
|
260
|
-
consumerName: String(consumerName),
|
|
261
|
-
queueName: queue.name
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Get the resolved retry configuration for a consumer's queue.
|
|
268
|
-
* Reads retry config from the queue definition in the contract.
|
|
220
|
+
* Get the retry configuration for a consumer's queue.
|
|
221
|
+
* Defaults are applied in the contract's defineQueue, so we just return the config.
|
|
269
222
|
*/
|
|
270
223
|
getRetryConfigForConsumer(consumer) {
|
|
271
|
-
|
|
272
|
-
if (retryOptions?.mode === "quorum-native") return {
|
|
273
|
-
mode: "quorum-native",
|
|
274
|
-
maxRetries: 0,
|
|
275
|
-
initialDelayMs: 0,
|
|
276
|
-
maxDelayMs: 0,
|
|
277
|
-
backoffMultiplier: 0,
|
|
278
|
-
jitter: false
|
|
279
|
-
};
|
|
280
|
-
if (retryOptions?.mode === "ttl-backoff") return {
|
|
281
|
-
mode: "ttl-backoff",
|
|
282
|
-
maxRetries: retryOptions.maxRetries ?? 3,
|
|
283
|
-
initialDelayMs: retryOptions.initialDelayMs ?? 1e3,
|
|
284
|
-
maxDelayMs: retryOptions.maxDelayMs ?? 3e4,
|
|
285
|
-
backoffMultiplier: retryOptions.backoffMultiplier ?? 2,
|
|
286
|
-
jitter: retryOptions.jitter ?? true
|
|
287
|
-
};
|
|
288
|
-
return {
|
|
289
|
-
mode: "ttl-backoff",
|
|
290
|
-
maxRetries: 3,
|
|
291
|
-
initialDelayMs: 1e3,
|
|
292
|
-
maxDelayMs: 3e4,
|
|
293
|
-
backoffMultiplier: 2,
|
|
294
|
-
jitter: true
|
|
295
|
-
};
|
|
224
|
+
return consumer.queue.retry;
|
|
296
225
|
}
|
|
297
226
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* Requirements for quorum-native mode:
|
|
301
|
-
* - Consumer queue must be a quorum queue
|
|
302
|
-
* - Consumer queue must have deliveryLimit configured
|
|
303
|
-
* - Consumer queue should have DLX configured (warning if not)
|
|
304
|
-
*
|
|
305
|
-
* @returns TechnicalError if validation fails, null if valid
|
|
306
|
-
*/
|
|
307
|
-
validateQuorumNativeConfigForConsumer(consumerName, consumer) {
|
|
308
|
-
const queue = consumer.queue;
|
|
309
|
-
if (queue.type !== "quorum") return new TechnicalError(`Consumer "${consumerName}" uses queue "${queue.name}" with type "${queue.type}". Quorum-native retry mode requires quorum queues.`);
|
|
310
|
-
if (queue.deliveryLimit === void 0) return new TechnicalError(`Consumer "${consumerName}" uses queue "${queue.name}" without deliveryLimit configured. Quorum-native retry mode requires deliveryLimit to be set on the queue definition.`);
|
|
311
|
-
if (!queue.deadLetter) this.logger?.warn(`Consumer "${consumerName}" uses queue "${queue.name}" without deadLetter configured. Messages exceeding deliveryLimit will be dropped instead of dead-lettered.`);
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Start consuming messages for all consumers
|
|
227
|
+
* Start consuming messages for all consumers.
|
|
228
|
+
* TypeScript guarantees consumers exist (handlers require matching consumers).
|
|
316
229
|
*/
|
|
317
230
|
consumeAll() {
|
|
318
|
-
|
|
319
|
-
const consumerNames = Object.keys(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
maxPrefetch = Math.max(maxPrefetch, options.prefetch);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (maxPrefetch > 0) this.amqpClient.channel.addSetup(async (channel) => {
|
|
231
|
+
const consumers = this.contract.consumers;
|
|
232
|
+
const consumerNames = Object.keys(consumers);
|
|
233
|
+
const maxPrefetch = consumerNames.reduce((max, name) => {
|
|
234
|
+
const prefetch = this.consumerOptions[name]?.prefetch;
|
|
235
|
+
return prefetch ? Math.max(max, prefetch) : max;
|
|
236
|
+
}, 0);
|
|
237
|
+
if (maxPrefetch > 0) this.amqpClient.addSetup(async (channel) => {
|
|
329
238
|
await channel.prefetch(maxPrefetch);
|
|
330
239
|
});
|
|
331
|
-
return _swan_io_boxed.Future.all(consumerNames.map((
|
|
240
|
+
return _swan_io_boxed.Future.all(consumerNames.map((name) => this.consume(name))).map(_swan_io_boxed.Result.all).mapOk(() => void 0);
|
|
332
241
|
}
|
|
333
242
|
waitForConnectionReady() {
|
|
334
|
-
return
|
|
243
|
+
return this.amqpClient.waitForConnect();
|
|
335
244
|
}
|
|
336
245
|
/**
|
|
337
|
-
* Start consuming messages for a specific consumer
|
|
246
|
+
* Start consuming messages for a specific consumer.
|
|
247
|
+
* TypeScript guarantees consumer and handler exist for valid consumer names.
|
|
338
248
|
*/
|
|
339
249
|
consume(consumerName) {
|
|
340
|
-
const
|
|
341
|
-
if (!consumers) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError("No consumers defined in contract")));
|
|
342
|
-
const consumer = consumers[consumerName];
|
|
343
|
-
if (!consumer) {
|
|
344
|
-
const availableConsumers = Object.keys(consumers);
|
|
345
|
-
const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
|
|
346
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Consumer not found: "${String(consumerName)}". Available consumers: ${available}`)));
|
|
347
|
-
}
|
|
250
|
+
const consumer = this.contract.consumers[consumerName];
|
|
348
251
|
const handler = this.actualHandlers[consumerName];
|
|
349
|
-
if (!handler) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Handler for "${String(consumerName)}" not provided`)));
|
|
350
252
|
return this.consumeSingle(consumerName, consumer, handler);
|
|
351
253
|
}
|
|
352
254
|
/**
|
|
353
|
-
*
|
|
354
|
-
* @returns `Future<Result<consumed message, void>>` - Ok with validated consumed message (payload + headers), or Error (already handled with nack)
|
|
255
|
+
* Validate data against a Standard Schema and handle errors.
|
|
355
256
|
*/
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
257
|
+
validateSchema(schema, data, context, msg) {
|
|
258
|
+
const rawValidation = schema["~standard"].validate(data);
|
|
259
|
+
const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
|
|
260
|
+
return _swan_io_boxed.Future.fromPromise(validationPromise).mapError((error) => new _amqp_contract_core.TechnicalError(`Error validating ${context.field}`, error)).mapOkToResult((result) => {
|
|
261
|
+
if (result.issues) return _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`${context.field} validation failed`, new MessageValidationError(context.consumerName, result.issues)));
|
|
262
|
+
return _swan_io_boxed.Result.Ok(result.value);
|
|
263
|
+
}).tapError((error) => {
|
|
264
|
+
this.logger?.error(`${context.field} validation failed`, {
|
|
265
|
+
consumerName: context.consumerName,
|
|
266
|
+
queueName: context.queueName,
|
|
362
267
|
error
|
|
363
268
|
});
|
|
364
|
-
this.amqpClient.
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(void 0));
|
|
376
|
-
}
|
|
377
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(parseResult.value));
|
|
378
|
-
};
|
|
379
|
-
const validatePayload = (parsedMessage) => {
|
|
380
|
-
const rawValidation = consumer.message.payload["~standard"].validate(parsedMessage);
|
|
381
|
-
return _swan_io_boxed.Future.fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).mapError(() => void 0).mapOkToResult((validationResult) => {
|
|
382
|
-
if (validationResult.issues) {
|
|
383
|
-
const error = new MessageValidationError(String(consumerName), validationResult.issues);
|
|
384
|
-
this.logger?.error("Message payload validation failed", {
|
|
385
|
-
consumerName: String(consumerName),
|
|
386
|
-
queueName: consumer.queue.name,
|
|
387
|
-
error
|
|
388
|
-
});
|
|
389
|
-
this.amqpClient.channel.nack(msg, false, false);
|
|
390
|
-
return _swan_io_boxed.Result.Error(void 0);
|
|
391
|
-
}
|
|
392
|
-
return _swan_io_boxed.Result.Ok(validationResult.value);
|
|
393
|
-
});
|
|
269
|
+
this.amqpClient.nack(msg, false, false);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Parse and validate a message from AMQP.
|
|
274
|
+
* @returns Ok with validated message (payload + headers), or Error (message already nacked)
|
|
275
|
+
*/
|
|
276
|
+
parseAndValidateMessage(msg, consumer, consumerName) {
|
|
277
|
+
const context = {
|
|
278
|
+
consumerName: String(consumerName),
|
|
279
|
+
queueName: consumer.queue.name
|
|
394
280
|
};
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const error = new MessageValidationError(String(consumerName), "Invalid headers schema: not a Standard Schema v1 compliant schema");
|
|
400
|
-
this.logger?.error("Message headers validation failed", {
|
|
401
|
-
consumerName: String(consumerName),
|
|
402
|
-
queueName: consumer.queue.name,
|
|
403
|
-
error
|
|
404
|
-
});
|
|
405
|
-
this.amqpClient.channel.nack(msg, false, false);
|
|
406
|
-
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(void 0));
|
|
407
|
-
}
|
|
408
|
-
const validSchema = headersSchema;
|
|
409
|
-
const rawHeaders = msg.properties.headers ?? {};
|
|
410
|
-
const rawValidation = validSchema["~standard"].validate(rawHeaders);
|
|
411
|
-
return _swan_io_boxed.Future.fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).mapError(() => void 0).mapOkToResult((validationResult) => {
|
|
412
|
-
if (validationResult.issues) {
|
|
413
|
-
const error = new MessageValidationError(String(consumerName), validationResult.issues);
|
|
414
|
-
this.logger?.error("Message headers validation failed", {
|
|
415
|
-
consumerName: String(consumerName),
|
|
416
|
-
queueName: consumer.queue.name,
|
|
417
|
-
error
|
|
418
|
-
});
|
|
419
|
-
this.amqpClient.channel.nack(msg, false, false);
|
|
420
|
-
return _swan_io_boxed.Result.Error(void 0);
|
|
421
|
-
}
|
|
422
|
-
return _swan_io_boxed.Result.Ok(validationResult.value);
|
|
281
|
+
const nackAndError = (message, error) => {
|
|
282
|
+
this.logger?.error(message, {
|
|
283
|
+
...context,
|
|
284
|
+
error
|
|
423
285
|
});
|
|
286
|
+
this.amqpClient.nack(msg, false, false);
|
|
287
|
+
return new _amqp_contract_core.TechnicalError(message, error);
|
|
424
288
|
};
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
headers: validatedHeaders
|
|
430
|
-
};
|
|
289
|
+
const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding).tapError((error) => {
|
|
290
|
+
this.logger?.error("Failed to decompress message", {
|
|
291
|
+
...context,
|
|
292
|
+
error
|
|
431
293
|
});
|
|
432
|
-
|
|
433
|
-
|
|
294
|
+
this.amqpClient.nack(msg, false, false);
|
|
295
|
+
}).mapOkToResult((buffer) => _swan_io_boxed.Result.fromExecution(() => JSON.parse(buffer.toString())).mapError((error) => nackAndError("Failed to parse JSON", error))).flatMapOk((parsed) => this.validateSchema(consumer.message.payload, parsed, {
|
|
296
|
+
...context,
|
|
297
|
+
field: "payload"
|
|
298
|
+
}, msg));
|
|
299
|
+
const parseHeaders = consumer.message.headers ? this.validateSchema(consumer.message.headers, msg.properties.headers ?? {}, {
|
|
300
|
+
...context,
|
|
301
|
+
field: "headers"
|
|
302
|
+
}, msg) : _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
303
|
+
return _swan_io_boxed.Future.allFromDict({
|
|
304
|
+
payload: parsePayload,
|
|
305
|
+
headers: parseHeaders
|
|
306
|
+
}).map(_swan_io_boxed.Result.allFromDict);
|
|
434
307
|
}
|
|
435
308
|
/**
|
|
436
309
|
* Consume messages one at a time
|
|
437
310
|
*/
|
|
438
311
|
consumeSingle(consumerName, consumer, handler) {
|
|
439
312
|
const queueName = consumer.queue.name;
|
|
440
|
-
return
|
|
313
|
+
return this.amqpClient.consume(queueName, async (msg) => {
|
|
441
314
|
if (msg === null) {
|
|
442
315
|
this.logger?.warn("Consumer cancelled by server", {
|
|
443
316
|
consumerName: String(consumerName),
|
|
@@ -452,7 +325,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
452
325
|
consumerName: String(consumerName),
|
|
453
326
|
queueName
|
|
454
327
|
});
|
|
455
|
-
this.amqpClient.
|
|
328
|
+
this.amqpClient.ack(msg);
|
|
456
329
|
const durationMs = Date.now() - startTime;
|
|
457
330
|
(0, _amqp_contract_core.endSpanSuccess)(span);
|
|
458
331
|
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(consumerName), true, durationMs);
|
|
@@ -473,9 +346,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
473
346
|
(0, _amqp_contract_core.endSpanError)(span, /* @__PURE__ */ new Error("Message validation failed"));
|
|
474
347
|
(0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(consumerName), false, durationMs);
|
|
475
348
|
}).toPromise();
|
|
476
|
-
})
|
|
477
|
-
this.consumerTags.add(
|
|
478
|
-
}).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
349
|
+
}).tapOk((consumerTag) => {
|
|
350
|
+
this.consumerTags.add(consumerTag);
|
|
351
|
+
}).mapError((error) => new _amqp_contract_core.TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
479
352
|
}
|
|
480
353
|
/**
|
|
481
354
|
* Handle error in message processing with retry logic.
|
|
@@ -540,7 +413,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
540
413
|
attemptsBeforeDeadLetter,
|
|
541
414
|
error: error.message
|
|
542
415
|
});
|
|
543
|
-
this.amqpClient.
|
|
416
|
+
this.amqpClient.nack(msg, false, true);
|
|
544
417
|
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
545
418
|
}
|
|
546
419
|
/**
|
|
@@ -622,14 +495,14 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
622
495
|
const deadLetter = consumer.queue.deadLetter;
|
|
623
496
|
if (!deadLetter) {
|
|
624
497
|
this.logger?.warn("Cannot retry: queue does not have DLX configured, falling back to nack with requeue", { queueName });
|
|
625
|
-
this.amqpClient.
|
|
498
|
+
this.amqpClient.nack(msg, false, true);
|
|
626
499
|
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
|
|
627
500
|
}
|
|
628
501
|
const dlxName = deadLetter.exchange.name;
|
|
629
502
|
const waitRoutingKey = `${queueName}-wait`;
|
|
630
|
-
this.amqpClient.
|
|
503
|
+
this.amqpClient.ack(msg);
|
|
631
504
|
const content = this.parseMessageContentForRetry(msg, queueName);
|
|
632
|
-
return
|
|
505
|
+
return this.amqpClient.publish(dlxName, waitRoutingKey, content, {
|
|
633
506
|
...msg.properties,
|
|
634
507
|
expiration: delayMs.toString(),
|
|
635
508
|
headers: {
|
|
@@ -638,14 +511,14 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
638
511
|
"x-last-error": error.message,
|
|
639
512
|
"x-first-failure-timestamp": msg.properties.headers?.["x-first-failure-timestamp"] ?? Date.now()
|
|
640
513
|
}
|
|
641
|
-
})
|
|
514
|
+
}).mapOkToResult((published) => {
|
|
642
515
|
if (!published) {
|
|
643
516
|
this.logger?.error("Failed to publish message for retry (write buffer full)", {
|
|
644
517
|
queueName,
|
|
645
518
|
waitRoutingKey,
|
|
646
519
|
retryCount: newRetryCount
|
|
647
520
|
});
|
|
648
|
-
return _swan_io_boxed.Result.Error(new TechnicalError("Failed to publish message for retry (write buffer full)"));
|
|
521
|
+
return _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("Failed to publish message for retry (write buffer full)"));
|
|
649
522
|
}
|
|
650
523
|
this.logger?.info("Message published for retry", {
|
|
651
524
|
queueName,
|
|
@@ -667,7 +540,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
667
540
|
queueName,
|
|
668
541
|
deliveryTag: msg.fields.deliveryTag
|
|
669
542
|
});
|
|
670
|
-
this.amqpClient.
|
|
543
|
+
this.amqpClient.nack(msg, false, false);
|
|
671
544
|
}
|
|
672
545
|
};
|
|
673
546
|
|
|
@@ -736,7 +609,6 @@ function defineHandlers(contract, handlers) {
|
|
|
736
609
|
exports.MessageValidationError = MessageValidationError;
|
|
737
610
|
exports.NonRetryableError = NonRetryableError;
|
|
738
611
|
exports.RetryableError = RetryableError;
|
|
739
|
-
exports.TechnicalError = TechnicalError;
|
|
740
612
|
exports.TypedAmqpWorker = TypedAmqpWorker;
|
|
741
613
|
exports.defineHandler = defineHandler;
|
|
742
614
|
exports.defineHandlers = defineHandlers;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger, TelemetryProvider } from "@amqp-contract/core";
|
|
1
|
+
import { Logger, TechnicalError, TelemetryProvider } from "@amqp-contract/core";
|
|
2
2
|
import { AmqpConnectionManagerOptions, ConnectionUrl } from "amqp-connection-manager";
|
|
3
3
|
import { ConsumerDefinition, ContractDefinition, InferConsumerNames, MessageDefinition } from "@amqp-contract/contract";
|
|
4
4
|
import { Future, Result } from "@swan-io/boxed";
|
|
@@ -6,24 +6,10 @@ import { ConsumeMessage } from "amqplib";
|
|
|
6
6
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
7
7
|
|
|
8
8
|
//#region src/errors.d.ts
|
|
9
|
-
/**
|
|
10
|
-
* Base error class for worker errors
|
|
11
|
-
*/
|
|
12
|
-
declare abstract class WorkerError extends Error {
|
|
13
|
-
protected constructor(message: string);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Error for technical/runtime failures in worker operations
|
|
17
|
-
* This includes validation failures, parsing failures, and processing failures
|
|
18
|
-
*/
|
|
19
|
-
declare class TechnicalError extends WorkerError {
|
|
20
|
-
readonly cause?: unknown | undefined;
|
|
21
|
-
constructor(message: string, cause?: unknown | undefined);
|
|
22
|
-
}
|
|
23
9
|
/**
|
|
24
10
|
* Error thrown when message validation fails
|
|
25
11
|
*/
|
|
26
|
-
declare class MessageValidationError extends
|
|
12
|
+
declare class MessageValidationError extends Error {
|
|
27
13
|
readonly consumerName: string;
|
|
28
14
|
readonly issues: unknown;
|
|
29
15
|
constructor(consumerName: string, issues: unknown);
|
|
@@ -35,7 +21,7 @@ declare class MessageValidationError extends WorkerError {
|
|
|
35
21
|
* Use this error type when the operation might succeed if retried.
|
|
36
22
|
* The worker will apply exponential backoff and retry the message.
|
|
37
23
|
*/
|
|
38
|
-
declare class RetryableError extends
|
|
24
|
+
declare class RetryableError extends Error {
|
|
39
25
|
readonly cause?: unknown | undefined;
|
|
40
26
|
constructor(message: string, cause?: unknown | undefined);
|
|
41
27
|
}
|
|
@@ -46,7 +32,7 @@ declare class RetryableError extends WorkerError {
|
|
|
46
32
|
* Use this error type when retrying would not help - the message will be
|
|
47
33
|
* immediately sent to the dead letter queue (DLQ) if configured.
|
|
48
34
|
*/
|
|
49
|
-
declare class NonRetryableError extends
|
|
35
|
+
declare class NonRetryableError extends Error {
|
|
50
36
|
readonly cause?: unknown | undefined;
|
|
51
37
|
constructor(message: string, cause?: unknown | undefined);
|
|
52
38
|
}
|
|
@@ -320,40 +306,28 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
320
306
|
*/
|
|
321
307
|
close(): Future<Result<void, TechnicalError>>;
|
|
322
308
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* For quorum-native mode, validates that the queue is properly configured.
|
|
326
|
-
* For TTL-backoff mode, wait queues are created by setupAmqpTopology in the core package.
|
|
327
|
-
*/
|
|
328
|
-
private validateRetryConfiguration;
|
|
329
|
-
/**
|
|
330
|
-
* Get the resolved retry configuration for a consumer's queue.
|
|
331
|
-
* Reads retry config from the queue definition in the contract.
|
|
309
|
+
* Get the retry configuration for a consumer's queue.
|
|
310
|
+
* Defaults are applied in the contract's defineQueue, so we just return the config.
|
|
332
311
|
*/
|
|
333
312
|
private getRetryConfigForConsumer;
|
|
334
313
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* Requirements for quorum-native mode:
|
|
338
|
-
* - Consumer queue must be a quorum queue
|
|
339
|
-
* - Consumer queue must have deliveryLimit configured
|
|
340
|
-
* - Consumer queue should have DLX configured (warning if not)
|
|
341
|
-
*
|
|
342
|
-
* @returns TechnicalError if validation fails, null if valid
|
|
343
|
-
*/
|
|
344
|
-
private validateQuorumNativeConfigForConsumer;
|
|
345
|
-
/**
|
|
346
|
-
* Start consuming messages for all consumers
|
|
314
|
+
* Start consuming messages for all consumers.
|
|
315
|
+
* TypeScript guarantees consumers exist (handlers require matching consumers).
|
|
347
316
|
*/
|
|
348
317
|
private consumeAll;
|
|
349
318
|
private waitForConnectionReady;
|
|
350
319
|
/**
|
|
351
|
-
* Start consuming messages for a specific consumer
|
|
320
|
+
* Start consuming messages for a specific consumer.
|
|
321
|
+
* TypeScript guarantees consumer and handler exist for valid consumer names.
|
|
352
322
|
*/
|
|
353
323
|
private consume;
|
|
354
324
|
/**
|
|
355
|
-
*
|
|
356
|
-
|
|
325
|
+
* Validate data against a Standard Schema and handle errors.
|
|
326
|
+
*/
|
|
327
|
+
private validateSchema;
|
|
328
|
+
/**
|
|
329
|
+
* Parse and validate a message from AMQP.
|
|
330
|
+
* @returns Ok with validated message (payload + headers), or Error (message already nacked)
|
|
357
331
|
*/
|
|
358
332
|
private parseAndValidateMessage;
|
|
359
333
|
/**
|
|
@@ -518,5 +492,5 @@ declare function defineHandler<TContract extends ContractDefinition, TName exten
|
|
|
518
492
|
*/
|
|
519
493
|
declare function defineHandlers<TContract extends ContractDefinition>(contract: TContract, handlers: WorkerInferSafeConsumerHandlers<TContract>): WorkerInferSafeConsumerHandlers<TContract>;
|
|
520
494
|
//#endregion
|
|
521
|
-
export { type CreateWorkerOptions, type HandlerError, MessageValidationError, NonRetryableError, RetryableError,
|
|
495
|
+
export { type CreateWorkerOptions, type HandlerError, MessageValidationError, NonRetryableError, RetryableError, TypedAmqpWorker, type WorkerConsumedMessage, type WorkerInferConsumedMessage, type WorkerInferConsumerHeaders, type WorkerInferSafeConsumerHandler, type WorkerInferSafeConsumerHandlerEntry, type WorkerInferSafeConsumerHandlers, defineHandler, defineHandlers };
|
|
522
496
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAGa,sBAAA,SAA+B,KAAA;;;;;AAA5C;AAwBA;AAwBA;AAqBA;;;;AC1DK,cDaQ,cAAA,SAAuB,KAAA,CCbf;EAAiB,SAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;EACpC,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;;;AAAgC;;;;;AAKqD;AAQtC,cDuBpC,iBAAA,SAA0B,KAAA,CCvBU;EAC/C,SAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;EAA6B,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;;;;;;AAS1B,KDkCO,YAAA,GAAe,cClCR,GDkCyB,iBClCzB;;;;;;ADnCnB,KCWK,gBDXQ,CAAA,gBCWyB,gBDXW,CAAA,GCY/C,ODZ+C,SCY/B,gBDZ+B,CAAA,KAAA,OAAA,CAAA,GAAA,MAAA,GAAA,KAAA;AAwBjD;AAwBA;AAqBA;KCpDK,4CAA4C,sBAAsB,iBACrE;;;AAZ4D;;KAmBzD,yBAbH,CAAA,kBAa+C,kBAb/C,CAAA,GAcA,SAdA,CAAA,SAAA,CAAA,SAc6B,iBAd7B,CAAA,KAAA,UAAA,EAAA,KAAA,SAAA,CAAA,GAAA,QAAA,SAeqB,gBAfrB,CAesC,MAftC,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,GAgBM,gBAhBN,CAgBuB,QAhBvB,CAAA,GAAA,SAAA,GAAA,SAAA;;;AAAgC;KAuB7B,cAlB4C,CAAA,kBAkBX,kBAlBW,CAAA,GAkBW,WAlBX,CAkBuB,SAlBvB,CAAA,WAAA,CAAA,CAAA;;;;AAAsC,KAuBlF,aAfA,CAAA,kBAgBe,kBAhBU,EAAA,cAiBd,kBAjBc,CAiBK,SAjBL,CAAA,CAAA,GAkB1B,cAlB0B,CAkBX,SAlBW,CAAA,CAkBA,KAlBA,CAAA;;;;KAuBzB,0BArBmC,CAAA,kBAsBpB,kBAtBoB,EAAA,cAuBxB,kBAvBwB,CAuBL,SAvBK,CAAA,CAAA,GAwBpC,yBAxBoC,CAwBV,aAxBU,CAwBI,SAxBJ,EAwBe,KAxBf,CAAA,CAAA;;;;;AAQnC,KAsBO,0BAtBO,CAAA,kBAuBC,kBAvBD,EAAA,cAwBH,kBAxBG,CAwBgB,SAxBhB,CAAA,CAAA,GAyBf,yBAzBe,CAyBW,aAzBX,CAyByB,SAzBzB,EAyBoC,KAzBpC,CAAA,CAAA;;;;;AAAoD;;;;;;;;AAQpC;;;;;;;;AAQN,KA+BjB,qBA/BiB,CAAA,QAAA,EAAA,aAAA,SAAA,CAAA,GAAA;EAMjB;EACQ,OAAA,EA0BT,QA1BS;EACe;EAAnB,OAAA,EA2BL,UA3BK,SAAA,SAAA,GAAA,SAAA,GA2BoC,UA3BpC;CAC4B;;;;;AAsBhC,KAWA,0BAXqB,CAAA,kBAYb,kBAZa,EAAA,cAajB,kBAbiB,CAaE,SAbF,CAAA,CAAA,GAc7B,qBAd6B,CAe/B,0BAf+B,CAeJ,SAfI,EAeO,KAfP,CAAA,EAgB/B,0BAhB+B,CAgBJ,SAhBI,EAgBO,KAhBP,CAAA,CAAA;;;;;AAWjC;;;;;;;;;;;;AAoCA;;;;;AAIiD,KAJrC,8BAIqC,CAAA,kBAH7B,kBAG6B,EAAA,cAFjC,kBAEiC,CAFd,SAEc,CAAA,CAAA,GAAA,CAAA,OAAA,EAAtC,0BAAsC,CAAX,SAAW,EAAA,KAAA,CAAA,EAAA,UAAA,EACnC,cADmC,EAAA,GAE5C,MAF4C,CAErC,MAFqC,CAAA,IAAA,EAExB,YAFwB,CAAA,CAAA;;;;;;;AAcjD;;;;AAImC,KAJvB,mCAIuB,CAAA,kBAHf,kBAGe,EAAA,cAFnB,kBAEmB,CAFA,SAEA,CAAA,CAAA,GAA/B,8BAA+B,CAAA,SAAA,EAAW,KAAX,CAAA,GAAA,SAAA,CACrB,8BADqB,CACU,SADV,EACqB,KADrB,CAAA,EAAA;EAAW,QAAA,CAAA,EAAA,MAAA;CAA1C,CAAA;;;;;AAOQ,KAAA,+BAA+B,CAAA,kBAAmB,kBAAnB,CAAA,GAAA,QACnC,kBADsD,CACnC,SADmC,CAAA,GACtB,mCADsB,CACc,SADd,EACyB,CADzB,CAAA,EACnC;;;;;;AD/J3B;AAwBA;AAwBA;AAqBA;;;;AC/D8D;;;;;AAM5B;;;;;AAKqD;;;;;;;;;AAW/D;;;;;AAO+C;AAMnD,KCgDR,mBDhDQ,CAAA,kBCgD8B,kBDhD9B,CAAA,GAAA;EACe;EAAnB,QAAA,ECiDJ,SDjDI;EACG;;;;AAAgB;EAMf,QAAA,ECgDR,+BDhDQ,CCgDwB,SDhDxB,CAAA;EACe;EAAnB,IAAA,ECiDR,aDjDQ,EAAA;EAC4B;EAAW,iBAAA,CAAA,ECkDjC,4BDlDiC,GAAA,SAAA;EAAzB;EAA1B,MAAA,CAAA,ECoDO,MDpDP,GAAA,SAAA;EAAyB;AAM7B;;;;EAG4C,SAAA,CAAA,ECiD9B,iBDjD8B,GAAA,SAAA;CAAW;;;;AAsBvD;;;;;AAWA;;;;;;;;;;;;AAoCA;;;;;;;;;;;;AAkBA;;;;;;;;AAKwD,cCA3C,eDA2C,CAAA,kBCAT,kBDAS,CAAA,CAAA;EAA1C,iBAAA,QAAA;EAA8B,iBAAA,UAAA;EAMhC,iBAAA,MAAA;EAAkD;;;EACc,iBAAA,cAAA;EAAW,iBAAA,eAAA;EAA/C,iBAAA,YAAA;EAAmC,iBAAA,SAAA;;;;ACtE3E;;;;;;;;;;AA+DA;;;;;;;;;;;;;EAoFgF,OAAA,MAAA,CAAA,kBAP9C,kBAO8C,CAAA,CAAA;IAAA,QAAA;IAAA,QAAA;IAAA,IAAA;IAAA,iBAAA;IAAA,MAAA;IAAA;EAAA,CAAA,EAA3E,mBAA2E,CAAvD,SAAuD,CAAA,CAAA,EAA1C,MAA0C,CAAnC,MAAmC,CAA5B,eAA4B,CAAZ,SAAY,CAAA,EAAA,cAAA,CAAA,CAAA;EAAnC;;;;;;;;;ACzI7C;;;;;;;EAMqD,KAAA,CAAA,CAAA,EDuK1C,MCvK0C,CDuKnC,MCvKmC,CAAA,IAAA,EDuKtB,cCvKsB,CAAA,CAAA;EAA1C;;;;EAC2B,QAAA,yBAAA;EACtB;;;;EAIJ,QAAA,UAAA;EACI,QAAA,sBAAA;EAC0B;;;;EAEQ,QAAA,OAAA;EAA/C;;AA+CH;EAAiD,QAAA,cAAA;EACrC;;;;EAET,QAAA,uBAAA;EAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AHrKlC;AAwBA;AAwBA;AAqBA;;;;AC/D8D;;;;;AAM5B;;;;;AAKqD;;;;;;;;;AAW/D;;;;;AAO+C;;;;;;;;AAQpC;;;AAOnB,iBEiDA,aFjDA,CAAA,kBEkDI,kBFlDJ,EAAA,cEmDA,kBFnDA,CEmDmB,SFnDnB,CAAA,CAAA,CAAA,QAAA,EEqDJ,SFrDI,EAAA,YAAA,EEsDA,KFtDA,EAAA,OAAA,EEuDL,8BFvDK,CEuD0B,SFvD1B,EEuDqC,KFvDrC,CAAA,CAAA,EEwDb,mCFxDa,CEwDuB,SFxDvB,EEwDkC,KFxDlC,CAAA;AAC4B,iBEwD5B,aFxD4B,CAAA,kBEyDxB,kBFzDwB,EAAA,cE0D5B,kBF1D4B,CE0DT,SF1DS,CAAA,CAAA,CAAA,QAAA,EE4DhC,SF5DgC,EAAA,YAAA,EE6D5B,KF7D4B,EAAA,OAAA,EE8DjC,8BF9DiC,CE8DF,SF9DE,EE8DS,KF9DT,CAAA,EAAA,OAAA,EAAA;EAAW,QAAA,CAAA,EAAA,MAAA;CAAzB,CAAA,EEgE3B,mCFhE2B,CEgES,SFhET,EEgEoB,KFhEpB,CAAA;;;AAM9B;;;;;;;;;AAyBA;;;;;AAWA;;;;;;;;;;;;AAoCA;AACoB,iBEgCJ,cFhCI,CAAA,kBEgC6B,kBFhC7B,CAAA,CAAA,QAAA,EEiCR,SFjCQ,EAAA,QAAA,EEkCR,+BFlCQ,CEkCwB,SFlCxB,CAAA,CAAA,EEmCjB,+BFnCiB,CEmCe,SFnCf,CAAA"}
|