@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 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: 3, // Try up to 3 transports
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 | 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 | Number of transports | Maximum retry attempts on failure |
227
- | `timeout` | `number` | No | | Timeout in milliseconds for each send attempt |
228
- | `continueOnSuccess` | `boolean` | No | `false` | Continue trying transports after success (selector strategy only) |
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 < this.config.maxRetries; 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 (isAbortError(error) && sendOptions.abortedByCaller()) {
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 (isAbortError(error) && abortedByCaller) throw error;
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 controller = new AbortController();
436
+ const timeoutController = new AbortController();
437
437
  let abortSource;
438
438
  const timeoutId = setTimeout(() => {
439
439
  abortSource ??= "timeout";
440
- controller.abort();
440
+ timeoutController.abort(createAbortError());
441
441
  }, this.config.timeout);
442
- let cleanup = () => clearTimeout(timeoutId);
442
+ let cleanupAbortSource = () => {};
443
+ let signal = timeoutController.signal;
444
+ let cleanupCombinedSignal = () => {};
443
445
  if (options?.signal) {
444
- const abort = () => {
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
- controller.signal.addEventListener("abort", () => {
457
+ timeoutController.signal.addEventListener("abort", () => {
457
458
  clearTimeout(timeoutId);
458
459
  });
459
460
  return {
460
461
  options: {
461
462
  ...options,
462
- signal: controller.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 when a transport fails.
99
- * Set to 0 to disable retries. Defaults to the number of transports.
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 when a transport fails.
99
- * Set to 0 to disable retries. Defaults to the number of transports.
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 < this.config.maxRetries; 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 (isAbortError(error) && sendOptions.abortedByCaller()) {
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 (isAbortError(error) && abortedByCaller) throw error;
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 controller = new AbortController();
413
+ const timeoutController = new AbortController();
414
414
  let abortSource;
415
415
  const timeoutId = setTimeout(() => {
416
416
  abortSource ??= "timeout";
417
- controller.abort();
417
+ timeoutController.abort(createAbortError());
418
418
  }, this.config.timeout);
419
- let cleanup = () => clearTimeout(timeoutId);
419
+ let cleanupAbortSource = () => {};
420
+ let signal = timeoutController.signal;
421
+ let cleanupCombinedSignal = () => {};
420
422
  if (options?.signal) {
421
- const abort = () => {
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
- controller.signal.addEventListener("abort", () => {
434
+ timeoutController.signal.addEventListener("abort", () => {
434
435
  clearTimeout(timeoutId);
435
436
  });
436
437
  return {
437
438
  options: {
438
439
  ...options,
439
- signal: controller.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.158",
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.158+468b767c"
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.158+468b767c"
64
+ "@upyo/mock": "0.5.0-dev.168+1e808a3a"
65
65
  },
66
66
  "scripts": {
67
67
  "prepublish": "mise run --no-deps :build"