@upyo/pool 0.5.0-dev.158 → 0.5.0-dev.168
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 +55 -18
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +55 -18
- 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,7 @@ var PoolTransport = class {
|
|
|
328
328
|
const attemptedIndices = /* @__PURE__ */ new Set();
|
|
329
329
|
const errorMessages = [];
|
|
330
330
|
const errors = [];
|
|
331
|
-
for (let attempt = 0; attempt
|
|
331
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
332
332
|
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
333
333
|
const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
|
|
334
334
|
if (!selection) break;
|
|
@@ -340,7 +340,7 @@ var PoolTransport = class {
|
|
|
340
340
|
try {
|
|
341
341
|
receipt = await selection.entry.transport.send(message, sendOptions.options);
|
|
342
342
|
} catch (error) {
|
|
343
|
-
if (
|
|
343
|
+
if (sendOptions.abortedByCaller() && isCallerAbort(error, options?.signal)) {
|
|
344
344
|
abortedByCaller = true;
|
|
345
345
|
throw error;
|
|
346
346
|
}
|
|
@@ -352,7 +352,7 @@ var PoolTransport = class {
|
|
|
352
352
|
errorMessages.push(...receipt.errorMessages);
|
|
353
353
|
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
354
354
|
} catch (error) {
|
|
355
|
-
if (
|
|
355
|
+
if (abortedByCaller && isCallerAbort(error, options?.signal)) throw error;
|
|
356
356
|
const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
|
|
357
357
|
if (thrownErrors.length > 0) {
|
|
358
358
|
errorMessages.push(...thrownErrors.map((item) => item.message));
|
|
@@ -433,39 +433,76 @@ var PoolTransport = class {
|
|
|
433
433
|
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
434
434
|
cleanup: () => {}
|
|
435
435
|
};
|
|
436
|
-
const
|
|
436
|
+
const timeoutController = new AbortController();
|
|
437
437
|
let abortSource;
|
|
438
438
|
const timeoutId = setTimeout(() => {
|
|
439
439
|
abortSource ??= "timeout";
|
|
440
|
-
|
|
440
|
+
timeoutController.abort(createAbortError());
|
|
441
441
|
}, this.config.timeout);
|
|
442
|
-
let
|
|
442
|
+
let cleanupAbortSource = () => {};
|
|
443
|
+
let signal = timeoutController.signal;
|
|
444
|
+
let cleanupCombinedSignal = () => {};
|
|
443
445
|
if (options?.signal) {
|
|
444
|
-
const
|
|
446
|
+
const markCallerAbort = () => {
|
|
445
447
|
abortSource ??= "caller";
|
|
446
448
|
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
449
|
};
|
|
450
|
+
if (options.signal.aborted) markCallerAbort();
|
|
451
|
+
else options.signal.addEventListener("abort", markCallerAbort, { once: true });
|
|
452
|
+
cleanupAbortSource = () => options.signal?.removeEventListener("abort", markCallerAbort);
|
|
453
|
+
const combinedSignal = combineSignals(timeoutController.signal, options.signal);
|
|
454
|
+
signal = combinedSignal.signal;
|
|
455
|
+
cleanupCombinedSignal = combinedSignal.cleanup;
|
|
455
456
|
}
|
|
456
|
-
|
|
457
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
457
458
|
clearTimeout(timeoutId);
|
|
458
459
|
});
|
|
459
460
|
return {
|
|
460
461
|
options: {
|
|
461
462
|
...options,
|
|
462
|
-
signal
|
|
463
|
+
signal
|
|
463
464
|
},
|
|
464
465
|
abortedByCaller: () => abortSource === "caller",
|
|
465
|
-
cleanup
|
|
466
|
+
cleanup: () => {
|
|
467
|
+
clearTimeout(timeoutId);
|
|
468
|
+
cleanupAbortSource();
|
|
469
|
+
cleanupCombinedSignal();
|
|
470
|
+
}
|
|
466
471
|
};
|
|
467
472
|
}
|
|
468
473
|
};
|
|
474
|
+
function combineSignals(timeoutSignal, externalSignal) {
|
|
475
|
+
if (typeof AbortSignal.any === "function") return {
|
|
476
|
+
signal: AbortSignal.any([timeoutSignal, externalSignal]),
|
|
477
|
+
cleanup: () => {}
|
|
478
|
+
};
|
|
479
|
+
const controller = new AbortController();
|
|
480
|
+
const abort = (signal) => {
|
|
481
|
+
controller.abort(getAbortReason(signal));
|
|
482
|
+
};
|
|
483
|
+
const abortTimeout = () => abort(timeoutSignal);
|
|
484
|
+
const abortExternal = () => abort(externalSignal);
|
|
485
|
+
timeoutSignal.addEventListener("abort", abortTimeout, { once: true });
|
|
486
|
+
externalSignal.addEventListener("abort", abortExternal, { once: true });
|
|
487
|
+
if (timeoutSignal.aborted) abortTimeout();
|
|
488
|
+
else if (externalSignal.aborted) abortExternal();
|
|
489
|
+
return {
|
|
490
|
+
signal: controller.signal,
|
|
491
|
+
cleanup: () => {
|
|
492
|
+
timeoutSignal.removeEventListener("abort", abortTimeout);
|
|
493
|
+
externalSignal.removeEventListener("abort", abortExternal);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function getAbortReason(signal) {
|
|
498
|
+
return signal.reason ?? createAbortError();
|
|
499
|
+
}
|
|
500
|
+
function createAbortError() {
|
|
501
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
502
|
+
}
|
|
503
|
+
function isCallerAbort(error, signal) {
|
|
504
|
+
return isAbortError(error) || error === signal?.reason;
|
|
505
|
+
}
|
|
469
506
|
function getReceiptErrors(receipt, provider) {
|
|
470
507
|
if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => error.provider == null ? {
|
|
471
508
|
...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
|
@@ -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,7 @@ var PoolTransport = class {
|
|
|
305
305
|
const attemptedIndices = /* @__PURE__ */ new Set();
|
|
306
306
|
const errorMessages = [];
|
|
307
307
|
const errors = [];
|
|
308
|
-
for (let attempt = 0; attempt
|
|
308
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
309
309
|
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
310
310
|
const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
|
|
311
311
|
if (!selection) break;
|
|
@@ -317,7 +317,7 @@ var PoolTransport = class {
|
|
|
317
317
|
try {
|
|
318
318
|
receipt = await selection.entry.transport.send(message, sendOptions.options);
|
|
319
319
|
} catch (error) {
|
|
320
|
-
if (
|
|
320
|
+
if (sendOptions.abortedByCaller() && isCallerAbort(error, options?.signal)) {
|
|
321
321
|
abortedByCaller = true;
|
|
322
322
|
throw error;
|
|
323
323
|
}
|
|
@@ -329,7 +329,7 @@ var PoolTransport = class {
|
|
|
329
329
|
errorMessages.push(...receipt.errorMessages);
|
|
330
330
|
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
331
331
|
} catch (error) {
|
|
332
|
-
if (
|
|
332
|
+
if (abortedByCaller && isCallerAbort(error, options?.signal)) throw error;
|
|
333
333
|
const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
|
|
334
334
|
if (thrownErrors.length > 0) {
|
|
335
335
|
errorMessages.push(...thrownErrors.map((item) => item.message));
|
|
@@ -410,39 +410,76 @@ var PoolTransport = class {
|
|
|
410
410
|
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
411
411
|
cleanup: () => {}
|
|
412
412
|
};
|
|
413
|
-
const
|
|
413
|
+
const timeoutController = new AbortController();
|
|
414
414
|
let abortSource;
|
|
415
415
|
const timeoutId = setTimeout(() => {
|
|
416
416
|
abortSource ??= "timeout";
|
|
417
|
-
|
|
417
|
+
timeoutController.abort(createAbortError());
|
|
418
418
|
}, this.config.timeout);
|
|
419
|
-
let
|
|
419
|
+
let cleanupAbortSource = () => {};
|
|
420
|
+
let signal = timeoutController.signal;
|
|
421
|
+
let cleanupCombinedSignal = () => {};
|
|
420
422
|
if (options?.signal) {
|
|
421
|
-
const
|
|
423
|
+
const markCallerAbort = () => {
|
|
422
424
|
abortSource ??= "caller";
|
|
423
425
|
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
426
|
};
|
|
427
|
+
if (options.signal.aborted) markCallerAbort();
|
|
428
|
+
else options.signal.addEventListener("abort", markCallerAbort, { once: true });
|
|
429
|
+
cleanupAbortSource = () => options.signal?.removeEventListener("abort", markCallerAbort);
|
|
430
|
+
const combinedSignal = combineSignals(timeoutController.signal, options.signal);
|
|
431
|
+
signal = combinedSignal.signal;
|
|
432
|
+
cleanupCombinedSignal = combinedSignal.cleanup;
|
|
432
433
|
}
|
|
433
|
-
|
|
434
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
434
435
|
clearTimeout(timeoutId);
|
|
435
436
|
});
|
|
436
437
|
return {
|
|
437
438
|
options: {
|
|
438
439
|
...options,
|
|
439
|
-
signal
|
|
440
|
+
signal
|
|
440
441
|
},
|
|
441
442
|
abortedByCaller: () => abortSource === "caller",
|
|
442
|
-
cleanup
|
|
443
|
+
cleanup: () => {
|
|
444
|
+
clearTimeout(timeoutId);
|
|
445
|
+
cleanupAbortSource();
|
|
446
|
+
cleanupCombinedSignal();
|
|
447
|
+
}
|
|
443
448
|
};
|
|
444
449
|
}
|
|
445
450
|
};
|
|
451
|
+
function combineSignals(timeoutSignal, externalSignal) {
|
|
452
|
+
if (typeof AbortSignal.any === "function") return {
|
|
453
|
+
signal: AbortSignal.any([timeoutSignal, externalSignal]),
|
|
454
|
+
cleanup: () => {}
|
|
455
|
+
};
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
const abort = (signal) => {
|
|
458
|
+
controller.abort(getAbortReason(signal));
|
|
459
|
+
};
|
|
460
|
+
const abortTimeout = () => abort(timeoutSignal);
|
|
461
|
+
const abortExternal = () => abort(externalSignal);
|
|
462
|
+
timeoutSignal.addEventListener("abort", abortTimeout, { once: true });
|
|
463
|
+
externalSignal.addEventListener("abort", abortExternal, { once: true });
|
|
464
|
+
if (timeoutSignal.aborted) abortTimeout();
|
|
465
|
+
else if (externalSignal.aborted) abortExternal();
|
|
466
|
+
return {
|
|
467
|
+
signal: controller.signal,
|
|
468
|
+
cleanup: () => {
|
|
469
|
+
timeoutSignal.removeEventListener("abort", abortTimeout);
|
|
470
|
+
externalSignal.removeEventListener("abort", abortExternal);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function getAbortReason(signal) {
|
|
475
|
+
return signal.reason ?? createAbortError();
|
|
476
|
+
}
|
|
477
|
+
function createAbortError() {
|
|
478
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
479
|
+
}
|
|
480
|
+
function isCallerAbort(error, signal) {
|
|
481
|
+
return isAbortError(error) || error === signal?.reason;
|
|
482
|
+
}
|
|
446
483
|
function getReceiptErrors(receipt, provider) {
|
|
447
484
|
if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => error.provider == null ? {
|
|
448
485
|
...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.168",
|
|
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.168+1e808a3a"
|
|
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.168+1e808a3a"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"prepublish": "mise run --no-deps :build"
|