@upyo/pool 0.5.0-dev.164 → 0.5.0-dev.170
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 +8 -8
- package/dist/index.cjs +38 -21
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +39 -22
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ const transport = new PoolTransport({
|
|
|
97
97
|
{ transport: backupTransport, priority: 50 },
|
|
98
98
|
{ transport: emergencyTransport, priority: 10 },
|
|
99
99
|
],
|
|
100
|
-
maxRetries:
|
|
100
|
+
maxRetries: 2, // Retry up to 2 other transports
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
// Always tries primary first, only uses backup if primary fails
|
|
@@ -219,13 +219,13 @@ Configuration
|
|
|
219
219
|
|
|
220
220
|
### `PoolConfig`
|
|
221
221
|
|
|
222
|
-
| Property | Type | Required | Default
|
|
223
|
-
| ------------------- | --------------------------------------------------------------------------- | -------- |
|
|
224
|
-
| `strategy` | `"round-robin" \| "weighted" \| "priority" \| "selector-based" \| Strategy` | Yes |
|
|
225
|
-
| `transports` | `TransportEntry[]` | Yes |
|
|
226
|
-
| `maxRetries` | `number` | No |
|
|
227
|
-
| `timeout` | `number` | No |
|
|
228
|
-
| `continueOnSuccess` | `boolean` | No | `false`
|
|
222
|
+
| Property | Type | Required | Default | Description |
|
|
223
|
+
| ------------------- | --------------------------------------------------------------------------- | -------- | --------------------------------- | ----------------------------------------------------------------- |
|
|
224
|
+
| `strategy` | `"round-robin" \| "weighted" \| "priority" \| "selector-based" \| Strategy` | Yes | | The strategy for selecting transports |
|
|
225
|
+
| `transports` | `TransportEntry[]` | Yes | | Array of transport configurations |
|
|
226
|
+
| `maxRetries` | `number` | No | Enough to try each transport once | Maximum retry attempts after the initial attempt |
|
|
227
|
+
| `timeout` | `number` | No | | Timeout in milliseconds for each send attempt |
|
|
228
|
+
| `continueOnSuccess` | `boolean` | No | `false` | Continue trying transports after success (selector strategy only) |
|
|
229
229
|
|
|
230
230
|
### `TransportEntry`
|
|
231
231
|
|
package/dist/index.cjs
CHANGED
|
@@ -50,7 +50,7 @@ function createPoolConfig(config) {
|
|
|
50
50
|
return {
|
|
51
51
|
strategy: config.strategy,
|
|
52
52
|
transports: resolvedTransports,
|
|
53
|
-
maxRetries: config.maxRetries ?? enabledTransports.length,
|
|
53
|
+
maxRetries: config.maxRetries ?? enabledTransports.length - 1,
|
|
54
54
|
timeout: config.timeout,
|
|
55
55
|
continueOnSuccess: config.continueOnSuccess ?? false
|
|
56
56
|
};
|
|
@@ -328,7 +328,8 @@ var PoolTransport = class {
|
|
|
328
328
|
const attemptedIndices = /* @__PURE__ */ new Set();
|
|
329
329
|
const errorMessages = [];
|
|
330
330
|
const errors = [];
|
|
331
|
-
|
|
331
|
+
let checkCallerAbortBeforeFailure = false;
|
|
332
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
332
333
|
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
333
334
|
const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
|
|
334
335
|
if (!selection) break;
|
|
@@ -340,7 +341,7 @@ var PoolTransport = class {
|
|
|
340
341
|
try {
|
|
341
342
|
receipt = await selection.entry.transport.send(message, sendOptions.options);
|
|
342
343
|
} catch (error) {
|
|
343
|
-
if (
|
|
344
|
+
if (sendOptions.abortedByCaller() && isCallerAbort(error, options?.signal)) {
|
|
344
345
|
abortedByCaller = true;
|
|
345
346
|
throw error;
|
|
346
347
|
}
|
|
@@ -351,24 +352,29 @@ var PoolTransport = class {
|
|
|
351
352
|
if (receipt.successful) return receipt;
|
|
352
353
|
errorMessages.push(...receipt.errorMessages);
|
|
353
354
|
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
355
|
+
checkCallerAbortBeforeFailure = true;
|
|
354
356
|
} catch (error) {
|
|
355
|
-
if (
|
|
357
|
+
if (abortedByCaller && isCallerAbort(error, options?.signal)) throw error;
|
|
356
358
|
const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
|
|
357
359
|
if (thrownErrors.length > 0) {
|
|
358
360
|
errorMessages.push(...thrownErrors.map((item) => item.message));
|
|
359
361
|
errors.push(...thrownErrors);
|
|
362
|
+
checkCallerAbortBeforeFailure = true;
|
|
360
363
|
continue;
|
|
361
364
|
}
|
|
362
365
|
const timeoutMessage = "Transport send timed out.";
|
|
363
|
-
const
|
|
366
|
+
const abortError = isAbortError(error);
|
|
367
|
+
const errorMessage = abortError ? timeoutMessage : error instanceof Error ? error.message : String(error);
|
|
364
368
|
errorMessages.push(errorMessage);
|
|
365
369
|
errors.push((0, __upyo_core.createReceiptError)(errorMessage, {
|
|
366
370
|
provider: selection.entry.transport.id,
|
|
367
|
-
category:
|
|
368
|
-
retryable:
|
|
371
|
+
category: abortError ? "timeout" : void 0,
|
|
372
|
+
retryable: abortError ? true : void 0
|
|
369
373
|
}));
|
|
374
|
+
checkCallerAbortBeforeFailure ||= !abortError;
|
|
370
375
|
}
|
|
371
376
|
}
|
|
377
|
+
if (checkCallerAbortBeforeFailure) options?.signal?.throwIfAborted();
|
|
372
378
|
return (0, __upyo_core.createFailedReceipt)(errorMessages.length > 0 ? errorMessages : ["All transports failed to send the message."], {
|
|
373
379
|
provider: "pool",
|
|
374
380
|
errors: errors.length > 0 ? errors : void 0,
|
|
@@ -433,39 +439,50 @@ var PoolTransport = class {
|
|
|
433
439
|
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
434
440
|
cleanup: () => {}
|
|
435
441
|
};
|
|
436
|
-
const
|
|
442
|
+
const timeoutController = new AbortController();
|
|
437
443
|
let abortSource;
|
|
438
444
|
const timeoutId = setTimeout(() => {
|
|
439
445
|
abortSource ??= "timeout";
|
|
440
|
-
|
|
446
|
+
timeoutController.abort(createAbortError());
|
|
441
447
|
}, this.config.timeout);
|
|
442
|
-
let
|
|
448
|
+
let cleanupAbortSource = () => {};
|
|
449
|
+
let signal = timeoutController.signal;
|
|
450
|
+
let cleanupCombinedSignal = () => {};
|
|
443
451
|
if (options?.signal) {
|
|
444
|
-
const
|
|
452
|
+
const markCallerAbort = () => {
|
|
445
453
|
abortSource ??= "caller";
|
|
446
454
|
clearTimeout(timeoutId);
|
|
447
|
-
controller.abort();
|
|
448
|
-
};
|
|
449
|
-
if (options.signal.aborted) abort();
|
|
450
|
-
else options.signal.addEventListener("abort", abort, { once: true });
|
|
451
|
-
cleanup = () => {
|
|
452
|
-
clearTimeout(timeoutId);
|
|
453
|
-
options.signal?.removeEventListener("abort", abort);
|
|
454
455
|
};
|
|
456
|
+
if (options.signal.aborted) markCallerAbort();
|
|
457
|
+
else options.signal.addEventListener("abort", markCallerAbort, { once: true });
|
|
458
|
+
cleanupAbortSource = () => options.signal?.removeEventListener("abort", markCallerAbort);
|
|
459
|
+
const combinedSignal = (0, __upyo_core.combineSignals)(timeoutController.signal, options.signal);
|
|
460
|
+
signal = combinedSignal.signal;
|
|
461
|
+
cleanupCombinedSignal = combinedSignal.cleanup;
|
|
455
462
|
}
|
|
456
|
-
|
|
463
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
457
464
|
clearTimeout(timeoutId);
|
|
458
465
|
});
|
|
459
466
|
return {
|
|
460
467
|
options: {
|
|
461
468
|
...options,
|
|
462
|
-
signal
|
|
469
|
+
signal
|
|
463
470
|
},
|
|
464
471
|
abortedByCaller: () => abortSource === "caller",
|
|
465
|
-
cleanup
|
|
472
|
+
cleanup: () => {
|
|
473
|
+
clearTimeout(timeoutId);
|
|
474
|
+
cleanupAbortSource();
|
|
475
|
+
cleanupCombinedSignal();
|
|
476
|
+
}
|
|
466
477
|
};
|
|
467
478
|
}
|
|
468
479
|
};
|
|
480
|
+
function createAbortError() {
|
|
481
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
482
|
+
}
|
|
483
|
+
function isCallerAbort(error, signal) {
|
|
484
|
+
return isAbortError(error) || error === signal?.reason;
|
|
485
|
+
}
|
|
469
486
|
function getReceiptErrors(receipt, provider) {
|
|
470
487
|
if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => error.provider == null ? {
|
|
471
488
|
...error,
|
package/dist/index.d.cts
CHANGED
|
@@ -95,8 +95,9 @@ interface PoolConfig<TProviderId extends string = string> {
|
|
|
95
95
|
*/
|
|
96
96
|
readonly transports: readonly TransportEntry<TProviderId>[];
|
|
97
97
|
/**
|
|
98
|
-
* Maximum number of retry attempts
|
|
99
|
-
* Set to 0 to disable retries. Defaults to
|
|
98
|
+
* Maximum number of retry attempts after the initial send attempt fails.
|
|
99
|
+
* Set to 0 to disable retries. Defaults to enough retries to try every
|
|
100
|
+
* enabled transport once.
|
|
100
101
|
*/
|
|
101
102
|
readonly maxRetries?: number;
|
|
102
103
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -95,8 +95,9 @@ interface PoolConfig<TProviderId extends string = string> {
|
|
|
95
95
|
*/
|
|
96
96
|
readonly transports: readonly TransportEntry<TProviderId>[];
|
|
97
97
|
/**
|
|
98
|
-
* Maximum number of retry attempts
|
|
99
|
-
* Set to 0 to disable retries. Defaults to
|
|
98
|
+
* Maximum number of retry attempts after the initial send attempt fails.
|
|
99
|
+
* Set to 0 to disable retries. Defaults to enough retries to try every
|
|
100
|
+
* enabled transport once.
|
|
100
101
|
*/
|
|
101
102
|
readonly maxRetries?: number;
|
|
102
103
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createFailedReceipt, createReceiptError } from "@upyo/core";
|
|
1
|
+
import { combineSignals, createFailedReceipt, createReceiptError } from "@upyo/core";
|
|
2
2
|
|
|
3
3
|
//#region src/config.ts
|
|
4
4
|
/**
|
|
@@ -27,7 +27,7 @@ function createPoolConfig(config) {
|
|
|
27
27
|
return {
|
|
28
28
|
strategy: config.strategy,
|
|
29
29
|
transports: resolvedTransports,
|
|
30
|
-
maxRetries: config.maxRetries ?? enabledTransports.length,
|
|
30
|
+
maxRetries: config.maxRetries ?? enabledTransports.length - 1,
|
|
31
31
|
timeout: config.timeout,
|
|
32
32
|
continueOnSuccess: config.continueOnSuccess ?? false
|
|
33
33
|
};
|
|
@@ -305,7 +305,8 @@ var PoolTransport = class {
|
|
|
305
305
|
const attemptedIndices = /* @__PURE__ */ new Set();
|
|
306
306
|
const errorMessages = [];
|
|
307
307
|
const errors = [];
|
|
308
|
-
|
|
308
|
+
let checkCallerAbortBeforeFailure = false;
|
|
309
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
309
310
|
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
310
311
|
const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
|
|
311
312
|
if (!selection) break;
|
|
@@ -317,7 +318,7 @@ var PoolTransport = class {
|
|
|
317
318
|
try {
|
|
318
319
|
receipt = await selection.entry.transport.send(message, sendOptions.options);
|
|
319
320
|
} catch (error) {
|
|
320
|
-
if (
|
|
321
|
+
if (sendOptions.abortedByCaller() && isCallerAbort(error, options?.signal)) {
|
|
321
322
|
abortedByCaller = true;
|
|
322
323
|
throw error;
|
|
323
324
|
}
|
|
@@ -328,24 +329,29 @@ var PoolTransport = class {
|
|
|
328
329
|
if (receipt.successful) return receipt;
|
|
329
330
|
errorMessages.push(...receipt.errorMessages);
|
|
330
331
|
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
332
|
+
checkCallerAbortBeforeFailure = true;
|
|
331
333
|
} catch (error) {
|
|
332
|
-
if (
|
|
334
|
+
if (abortedByCaller && isCallerAbort(error, options?.signal)) throw error;
|
|
333
335
|
const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
|
|
334
336
|
if (thrownErrors.length > 0) {
|
|
335
337
|
errorMessages.push(...thrownErrors.map((item) => item.message));
|
|
336
338
|
errors.push(...thrownErrors);
|
|
339
|
+
checkCallerAbortBeforeFailure = true;
|
|
337
340
|
continue;
|
|
338
341
|
}
|
|
339
342
|
const timeoutMessage = "Transport send timed out.";
|
|
340
|
-
const
|
|
343
|
+
const abortError = isAbortError(error);
|
|
344
|
+
const errorMessage = abortError ? timeoutMessage : error instanceof Error ? error.message : String(error);
|
|
341
345
|
errorMessages.push(errorMessage);
|
|
342
346
|
errors.push(createReceiptError(errorMessage, {
|
|
343
347
|
provider: selection.entry.transport.id,
|
|
344
|
-
category:
|
|
345
|
-
retryable:
|
|
348
|
+
category: abortError ? "timeout" : void 0,
|
|
349
|
+
retryable: abortError ? true : void 0
|
|
346
350
|
}));
|
|
351
|
+
checkCallerAbortBeforeFailure ||= !abortError;
|
|
347
352
|
}
|
|
348
353
|
}
|
|
354
|
+
if (checkCallerAbortBeforeFailure) options?.signal?.throwIfAborted();
|
|
349
355
|
return createFailedReceipt(errorMessages.length > 0 ? errorMessages : ["All transports failed to send the message."], {
|
|
350
356
|
provider: "pool",
|
|
351
357
|
errors: errors.length > 0 ? errors : void 0,
|
|
@@ -410,39 +416,50 @@ var PoolTransport = class {
|
|
|
410
416
|
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
411
417
|
cleanup: () => {}
|
|
412
418
|
};
|
|
413
|
-
const
|
|
419
|
+
const timeoutController = new AbortController();
|
|
414
420
|
let abortSource;
|
|
415
421
|
const timeoutId = setTimeout(() => {
|
|
416
422
|
abortSource ??= "timeout";
|
|
417
|
-
|
|
423
|
+
timeoutController.abort(createAbortError());
|
|
418
424
|
}, this.config.timeout);
|
|
419
|
-
let
|
|
425
|
+
let cleanupAbortSource = () => {};
|
|
426
|
+
let signal = timeoutController.signal;
|
|
427
|
+
let cleanupCombinedSignal = () => {};
|
|
420
428
|
if (options?.signal) {
|
|
421
|
-
const
|
|
429
|
+
const markCallerAbort = () => {
|
|
422
430
|
abortSource ??= "caller";
|
|
423
431
|
clearTimeout(timeoutId);
|
|
424
|
-
controller.abort();
|
|
425
|
-
};
|
|
426
|
-
if (options.signal.aborted) abort();
|
|
427
|
-
else options.signal.addEventListener("abort", abort, { once: true });
|
|
428
|
-
cleanup = () => {
|
|
429
|
-
clearTimeout(timeoutId);
|
|
430
|
-
options.signal?.removeEventListener("abort", abort);
|
|
431
432
|
};
|
|
433
|
+
if (options.signal.aborted) markCallerAbort();
|
|
434
|
+
else options.signal.addEventListener("abort", markCallerAbort, { once: true });
|
|
435
|
+
cleanupAbortSource = () => options.signal?.removeEventListener("abort", markCallerAbort);
|
|
436
|
+
const combinedSignal = combineSignals(timeoutController.signal, options.signal);
|
|
437
|
+
signal = combinedSignal.signal;
|
|
438
|
+
cleanupCombinedSignal = combinedSignal.cleanup;
|
|
432
439
|
}
|
|
433
|
-
|
|
440
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
434
441
|
clearTimeout(timeoutId);
|
|
435
442
|
});
|
|
436
443
|
return {
|
|
437
444
|
options: {
|
|
438
445
|
...options,
|
|
439
|
-
signal
|
|
446
|
+
signal
|
|
440
447
|
},
|
|
441
448
|
abortedByCaller: () => abortSource === "caller",
|
|
442
|
-
cleanup
|
|
449
|
+
cleanup: () => {
|
|
450
|
+
clearTimeout(timeoutId);
|
|
451
|
+
cleanupAbortSource();
|
|
452
|
+
cleanupCombinedSignal();
|
|
453
|
+
}
|
|
443
454
|
};
|
|
444
455
|
}
|
|
445
456
|
};
|
|
457
|
+
function createAbortError() {
|
|
458
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
459
|
+
}
|
|
460
|
+
function isCallerAbort(error, signal) {
|
|
461
|
+
return isAbortError(error) || error === signal?.reason;
|
|
462
|
+
}
|
|
446
463
|
function getReceiptErrors(receipt, provider) {
|
|
447
464
|
if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => error.provider == null ? {
|
|
448
465
|
...error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/pool",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.170",
|
|
4
4
|
"description": "Pool transport for Upyo email library—provides load balancing and failover for multiple email providers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
},
|
|
57
57
|
"sideEffects": false,
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@upyo/core": "0.5.0-dev.
|
|
59
|
+
"@upyo/core": "0.5.0-dev.170+643994fd"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"tsdown": "^0.12.7",
|
|
63
63
|
"typescript": "5.8.3",
|
|
64
|
-
"@upyo/mock": "0.5.0-dev.
|
|
64
|
+
"@upyo/mock": "0.5.0-dev.170+643994fd"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"prepublish": "mise run --no-deps :build"
|