@upyo/pool 0.5.0-dev.136 → 0.5.0-dev.156

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 CHANGED
@@ -1,3 +1,27 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const __upyo_core = __toESM(require("@upyo/core"));
1
25
 
2
26
  //#region src/config.ts
3
27
  /**
@@ -273,6 +297,7 @@ var SelectorStrategy = class {
273
297
  * @since 0.3.0
274
298
  */
275
299
  var PoolTransport = class {
300
+ id = "pool";
276
301
  /**
277
302
  * The resolved configuration used by this pool transport.
278
303
  */
@@ -301,27 +326,54 @@ var PoolTransport = class {
301
326
  */
302
327
  async send(message, options) {
303
328
  const attemptedIndices = /* @__PURE__ */ new Set();
329
+ const errorMessages = [];
304
330
  const errors = [];
305
331
  for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
306
332
  if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
307
333
  const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
308
334
  if (!selection) break;
309
335
  attemptedIndices.add(selection.index);
336
+ let abortedByCaller = false;
310
337
  try {
311
338
  const sendOptions = this.createSendOptions(options);
312
- const receipt = await selection.entry.transport.send(message, sendOptions);
339
+ let receipt;
340
+ try {
341
+ receipt = await selection.entry.transport.send(message, sendOptions.options);
342
+ } catch (error) {
343
+ if (isAbortError(error) && sendOptions.abortedByCaller()) {
344
+ abortedByCaller = true;
345
+ throw error;
346
+ }
347
+ throw error;
348
+ } finally {
349
+ sendOptions.cleanup();
350
+ }
313
351
  if (receipt.successful) return receipt;
314
- errors.push(...receipt.errorMessages);
352
+ errorMessages.push(...receipt.errorMessages);
353
+ errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
315
354
  } catch (error) {
316
- if (error instanceof DOMException && error.name === "AbortError") throw error;
317
- const errorMessage = error instanceof Error ? error.message : String(error);
318
- errors.push(errorMessage);
355
+ if (isAbortError(error) && abortedByCaller) throw error;
356
+ const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
357
+ if (thrownErrors.length > 0) {
358
+ errorMessages.push(...thrownErrors.map((item) => item.message));
359
+ errors.push(...thrownErrors);
360
+ continue;
361
+ }
362
+ const timeoutMessage = "Transport send timed out.";
363
+ const errorMessage = isAbortError(error) ? timeoutMessage : error instanceof Error ? error.message : String(error);
364
+ errorMessages.push(errorMessage);
365
+ errors.push((0, __upyo_core.createReceiptError)(errorMessage, {
366
+ provider: selection.entry.transport.id,
367
+ category: isAbortError(error) ? "timeout" : void 0,
368
+ retryable: isAbortError(error) ? true : void 0
369
+ }));
319
370
  }
320
371
  }
321
- return {
322
- successful: false,
323
- errorMessages: errors.length > 0 ? errors : ["All transports failed to send the message"]
324
- };
372
+ return (0, __upyo_core.createFailedReceipt)(errorMessages.length > 0 ? errorMessages : ["All transports failed to send the message."], {
373
+ provider: "pool",
374
+ errors: errors.length > 0 ? errors : void 0,
375
+ attempts: attemptedIndices.size
376
+ });
325
377
  }
326
378
  /**
327
379
  * Sends multiple email messages using the pool strategy.
@@ -376,22 +428,68 @@ var PoolTransport = class {
376
428
  * Creates send options with timeout if configured.
377
429
  */
378
430
  createSendOptions(options) {
379
- if (!this.config.timeout) return options;
431
+ if (!this.config.timeout) return {
432
+ options,
433
+ abortedByCaller: () => options?.signal?.aborted ?? false,
434
+ cleanup: () => {}
435
+ };
380
436
  const controller = new AbortController();
381
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
382
- if (options?.signal) options.signal.addEventListener("abort", () => {
383
- clearTimeout(timeoutId);
437
+ let abortSource;
438
+ const timeoutId = setTimeout(() => {
439
+ abortSource ??= "timeout";
384
440
  controller.abort();
385
- });
441
+ }, this.config.timeout);
442
+ let cleanup = () => clearTimeout(timeoutId);
443
+ if (options?.signal) {
444
+ const abort = () => {
445
+ abortSource ??= "caller";
446
+ 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
+ }
386
456
  controller.signal.addEventListener("abort", () => {
387
457
  clearTimeout(timeoutId);
388
458
  });
389
459
  return {
390
- ...options,
391
- signal: controller.signal
460
+ options: {
461
+ ...options,
462
+ signal: controller.signal
463
+ },
464
+ abortedByCaller: () => abortSource === "caller",
465
+ cleanup
392
466
  };
393
467
  }
394
468
  };
469
+ function getReceiptErrors(receipt, provider) {
470
+ if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => error.provider == null ? {
471
+ ...error,
472
+ provider
473
+ } : error);
474
+ return receipt.errorMessages.map((message) => (0, __upyo_core.createReceiptError)(message, { provider: receipt.provider ?? provider }));
475
+ }
476
+ function getThrownReceiptErrors(error, provider) {
477
+ if (isReceiptError(error)) return [error.provider == null ? {
478
+ ...error,
479
+ provider
480
+ } : error];
481
+ if (isFailedReceipt(error)) return getReceiptErrors(error, provider);
482
+ return [];
483
+ }
484
+ function isReceiptError(value) {
485
+ return typeof value === "object" && value != null && typeof value.message === "string" && typeof value.code === "string" && typeof value.retryable === "boolean" && typeof value.category === "string";
486
+ }
487
+ function isFailedReceipt(value) {
488
+ return typeof value === "object" && value != null && value.successful === false && Array.isArray(value.errorMessages);
489
+ }
490
+ function isAbortError(error) {
491
+ return error instanceof Error && error.name === "AbortError";
492
+ }
395
493
 
396
494
  //#endregion
397
495
  exports.PoolTransport = PoolTransport;
package/dist/index.d.cts CHANGED
@@ -6,11 +6,11 @@ import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
6
6
  * Result of transport selection by a strategy.
7
7
  * @since 0.3.0
8
8
  */
9
- interface TransportSelection {
9
+ interface TransportSelection<TProviderId extends string = string> {
10
10
  /**
11
11
  * The selected transport entry.
12
12
  */
13
- readonly entry: ResolvedTransportEntry;
13
+ readonly entry: ResolvedTransportEntry<TProviderId>;
14
14
  /**
15
15
  * Index of the selected transport in the original list.
16
16
  */
@@ -20,7 +20,7 @@ interface TransportSelection {
20
20
  * Base interface for transport selection strategies.
21
21
  * @since 0.3.0
22
22
  */
23
- interface Strategy {
23
+ interface Strategy<TProviderId extends string = string> {
24
24
  /**
25
25
  * Selects a transport for sending a message.
26
26
  *
@@ -31,7 +31,7 @@ interface Strategy {
31
31
  * @returns The selected transport or `undefined` if no suitable transport is
32
32
  * available.
33
33
  */
34
- select(message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
34
+ select(message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
35
35
  /**
36
36
  * Resets any internal state of the strategy.
37
37
  */
@@ -53,11 +53,11 @@ type TransportSelector = (message: Message) => boolean;
53
53
  * Configuration for a transport entry in the pool.
54
54
  * @since 0.3.0
55
55
  */
56
- interface TransportEntry {
56
+ interface TransportEntry<TProviderId extends string = string> {
57
57
  /**
58
58
  * The transport instance to use.
59
59
  */
60
- readonly transport: Transport;
60
+ readonly transport: Transport<TProviderId>;
61
61
  /**
62
62
  * Weight for weighted distribution strategy.
63
63
  * Higher values mean more traffic. Defaults to 1.
@@ -84,16 +84,16 @@ interface TransportEntry {
84
84
  * Configuration options for the pool transport.
85
85
  * @since 0.3.0
86
86
  */
87
- interface PoolConfig {
87
+ interface PoolConfig<TProviderId extends string = string> {
88
88
  /**
89
89
  * The strategy to use for selecting transports.
90
90
  * Can be a built-in strategy name or a custom Strategy instance.
91
91
  */
92
- readonly strategy: PoolStrategy | Strategy;
92
+ readonly strategy: PoolStrategy | Strategy<TProviderId>;
93
93
  /**
94
94
  * The transports in the pool.
95
95
  */
96
- readonly transports: readonly TransportEntry[];
96
+ readonly transports: readonly TransportEntry<TProviderId>[];
97
97
  /**
98
98
  * Maximum number of retry attempts when a transport fails.
99
99
  * Set to 0 to disable retries. Defaults to the number of transports.
@@ -114,9 +114,9 @@ interface PoolConfig {
114
114
  * Resolved pool configuration with defaults applied.
115
115
  * @since 0.3.0
116
116
  */
117
- interface ResolvedPoolConfig {
118
- readonly strategy: PoolStrategy | Strategy;
119
- readonly transports: readonly ResolvedTransportEntry[];
117
+ interface ResolvedPoolConfig<TProviderId extends string = string> {
118
+ readonly strategy: PoolStrategy | Strategy<TProviderId>;
119
+ readonly transports: readonly ResolvedTransportEntry<TProviderId>[];
120
120
  readonly maxRetries: number;
121
121
  readonly timeout?: number;
122
122
  readonly continueOnSuccess: boolean;
@@ -125,8 +125,8 @@ interface ResolvedPoolConfig {
125
125
  * Resolved transport entry with defaults applied.
126
126
  * @since 0.3.0
127
127
  */
128
- interface ResolvedTransportEntry {
129
- readonly transport: Transport;
128
+ interface ResolvedTransportEntry<TProviderId extends string = string> {
129
+ readonly transport: Transport<TProviderId>;
130
130
  readonly weight: number;
131
131
  readonly priority: number;
132
132
  readonly selector?: TransportSelector;
@@ -196,11 +196,12 @@ interface ResolvedTransportEntry {
196
196
  *
197
197
  * @since 0.3.0
198
198
  */
199
- declare class PoolTransport implements Transport, AsyncDisposable {
199
+ declare class PoolTransport<TProviderId extends string = string> implements Transport<TProviderId | "pool">, AsyncDisposable {
200
+ readonly id = "pool";
200
201
  /**
201
202
  * The resolved configuration used by this pool transport.
202
203
  */
203
- readonly config: ResolvedPoolConfig;
204
+ readonly config: ResolvedPoolConfig<TProviderId>;
204
205
  private readonly strategy;
205
206
  /**
206
207
  * Creates a new PoolTransport instance.
@@ -208,7 +209,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
208
209
  * @param config Configuration options for the pool transport.
209
210
  * @throws {Error} If the configuration is invalid.
210
211
  */
211
- constructor(config: PoolConfig);
212
+ constructor(config: PoolConfig<TProviderId>);
212
213
  /**
213
214
  * Sends a single email message using the pool strategy.
214
215
  *
@@ -220,7 +221,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
220
221
  * @param options Optional transport options including abort signal.
221
222
  * @returns A promise that resolves to a receipt indicating success or failure.
222
223
  */
223
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
224
+ send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId | "pool">>;
224
225
  /**
225
226
  * Sends multiple email messages using the pool strategy.
226
227
  *
@@ -231,7 +232,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
231
232
  * @param options Optional transport options including abort signal.
232
233
  * @returns An async iterable of receipts, one for each message.
233
234
  */
234
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
235
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId | "pool">>;
235
236
  /**
236
237
  * Disposes of all underlying transports that support disposal.
237
238
  *
@@ -258,7 +259,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
258
259
  * all enabled transports.
259
260
  * @since 0.3.0
260
261
  */
261
- declare class RoundRobinStrategy implements Strategy {
262
+ declare class RoundRobinStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
262
263
  private currentIndex;
263
264
  /**
264
265
  * Selects the next transport in round-robin order.
@@ -270,7 +271,7 @@ declare class RoundRobinStrategy implements Strategy {
270
271
  * @returns The selected transport or `undefined` if all transports have been
271
272
  * attempted.
272
273
  */
273
- select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
274
+ select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
274
275
  /**
275
276
  * Resets the round-robin counter to start from the beginning.
276
277
  */
@@ -287,7 +288,7 @@ declare class RoundRobinStrategy implements Strategy {
287
288
  * messages as a transport with weight 1.
288
289
  * @since 0.3.0
289
290
  */
290
- declare class WeightedStrategy implements Strategy {
291
+ declare class WeightedStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
291
292
  /**
292
293
  * Selects a transport based on weighted random distribution.
293
294
  *
@@ -298,7 +299,7 @@ declare class WeightedStrategy implements Strategy {
298
299
  * @returns The selected transport or `undefined` if all transports have been
299
300
  * attempted.
300
301
  */
301
- select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
302
+ select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
302
303
  /**
303
304
  * Resets the strategy (no-op for weighted strategy as it's stateless).
304
305
  */
@@ -315,7 +316,7 @@ declare class WeightedStrategy implements Strategy {
315
316
  * equivalent and one is selected randomly.
316
317
  * @since 0.3.0
317
318
  */
318
- declare class PriorityStrategy implements Strategy {
319
+ declare class PriorityStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
319
320
  /**
320
321
  * Selects the highest priority transport that hasn't been attempted.
321
322
  *
@@ -326,7 +327,7 @@ declare class PriorityStrategy implements Strategy {
326
327
  * @returns The selected transport or `undefined` if all transports have been
327
328
  * attempted.
328
329
  */
329
- select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
330
+ select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
330
331
  /**
331
332
  * Resets the strategy (no-op for priority strategy as it's stateless).
332
333
  */
@@ -343,7 +344,7 @@ declare class PriorityStrategy implements Strategy {
343
344
  * one is selected randomly.
344
345
  * @since 0.3.0
345
346
  */
346
- declare class SelectorStrategy implements Strategy {
347
+ declare class SelectorStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
347
348
  /**
348
349
  * Selects a transport based on selector function matching.
349
350
  *
@@ -353,7 +354,7 @@ declare class SelectorStrategy implements Strategy {
353
354
  * attempted.
354
355
  * @returns The selected transport or `undefined` if no transport matches.
355
356
  */
356
- select(message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
357
+ select(message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
357
358
  /**
358
359
  * Resets the strategy (no-op for selector strategy as it's stateless).
359
360
  */
package/dist/index.d.ts CHANGED
@@ -6,11 +6,11 @@ import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
6
6
  * Result of transport selection by a strategy.
7
7
  * @since 0.3.0
8
8
  */
9
- interface TransportSelection {
9
+ interface TransportSelection<TProviderId extends string = string> {
10
10
  /**
11
11
  * The selected transport entry.
12
12
  */
13
- readonly entry: ResolvedTransportEntry;
13
+ readonly entry: ResolvedTransportEntry<TProviderId>;
14
14
  /**
15
15
  * Index of the selected transport in the original list.
16
16
  */
@@ -20,7 +20,7 @@ interface TransportSelection {
20
20
  * Base interface for transport selection strategies.
21
21
  * @since 0.3.0
22
22
  */
23
- interface Strategy {
23
+ interface Strategy<TProviderId extends string = string> {
24
24
  /**
25
25
  * Selects a transport for sending a message.
26
26
  *
@@ -31,7 +31,7 @@ interface Strategy {
31
31
  * @returns The selected transport or `undefined` if no suitable transport is
32
32
  * available.
33
33
  */
34
- select(message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
34
+ select(message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
35
35
  /**
36
36
  * Resets any internal state of the strategy.
37
37
  */
@@ -53,11 +53,11 @@ type TransportSelector = (message: Message) => boolean;
53
53
  * Configuration for a transport entry in the pool.
54
54
  * @since 0.3.0
55
55
  */
56
- interface TransportEntry {
56
+ interface TransportEntry<TProviderId extends string = string> {
57
57
  /**
58
58
  * The transport instance to use.
59
59
  */
60
- readonly transport: Transport;
60
+ readonly transport: Transport<TProviderId>;
61
61
  /**
62
62
  * Weight for weighted distribution strategy.
63
63
  * Higher values mean more traffic. Defaults to 1.
@@ -84,16 +84,16 @@ interface TransportEntry {
84
84
  * Configuration options for the pool transport.
85
85
  * @since 0.3.0
86
86
  */
87
- interface PoolConfig {
87
+ interface PoolConfig<TProviderId extends string = string> {
88
88
  /**
89
89
  * The strategy to use for selecting transports.
90
90
  * Can be a built-in strategy name or a custom Strategy instance.
91
91
  */
92
- readonly strategy: PoolStrategy | Strategy;
92
+ readonly strategy: PoolStrategy | Strategy<TProviderId>;
93
93
  /**
94
94
  * The transports in the pool.
95
95
  */
96
- readonly transports: readonly TransportEntry[];
96
+ readonly transports: readonly TransportEntry<TProviderId>[];
97
97
  /**
98
98
  * Maximum number of retry attempts when a transport fails.
99
99
  * Set to 0 to disable retries. Defaults to the number of transports.
@@ -114,9 +114,9 @@ interface PoolConfig {
114
114
  * Resolved pool configuration with defaults applied.
115
115
  * @since 0.3.0
116
116
  */
117
- interface ResolvedPoolConfig {
118
- readonly strategy: PoolStrategy | Strategy;
119
- readonly transports: readonly ResolvedTransportEntry[];
117
+ interface ResolvedPoolConfig<TProviderId extends string = string> {
118
+ readonly strategy: PoolStrategy | Strategy<TProviderId>;
119
+ readonly transports: readonly ResolvedTransportEntry<TProviderId>[];
120
120
  readonly maxRetries: number;
121
121
  readonly timeout?: number;
122
122
  readonly continueOnSuccess: boolean;
@@ -125,8 +125,8 @@ interface ResolvedPoolConfig {
125
125
  * Resolved transport entry with defaults applied.
126
126
  * @since 0.3.0
127
127
  */
128
- interface ResolvedTransportEntry {
129
- readonly transport: Transport;
128
+ interface ResolvedTransportEntry<TProviderId extends string = string> {
129
+ readonly transport: Transport<TProviderId>;
130
130
  readonly weight: number;
131
131
  readonly priority: number;
132
132
  readonly selector?: TransportSelector;
@@ -196,11 +196,12 @@ interface ResolvedTransportEntry {
196
196
  *
197
197
  * @since 0.3.0
198
198
  */
199
- declare class PoolTransport implements Transport, AsyncDisposable {
199
+ declare class PoolTransport<TProviderId extends string = string> implements Transport<TProviderId | "pool">, AsyncDisposable {
200
+ readonly id = "pool";
200
201
  /**
201
202
  * The resolved configuration used by this pool transport.
202
203
  */
203
- readonly config: ResolvedPoolConfig;
204
+ readonly config: ResolvedPoolConfig<TProviderId>;
204
205
  private readonly strategy;
205
206
  /**
206
207
  * Creates a new PoolTransport instance.
@@ -208,7 +209,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
208
209
  * @param config Configuration options for the pool transport.
209
210
  * @throws {Error} If the configuration is invalid.
210
211
  */
211
- constructor(config: PoolConfig);
212
+ constructor(config: PoolConfig<TProviderId>);
212
213
  /**
213
214
  * Sends a single email message using the pool strategy.
214
215
  *
@@ -220,7 +221,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
220
221
  * @param options Optional transport options including abort signal.
221
222
  * @returns A promise that resolves to a receipt indicating success or failure.
222
223
  */
223
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
224
+ send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId | "pool">>;
224
225
  /**
225
226
  * Sends multiple email messages using the pool strategy.
226
227
  *
@@ -231,7 +232,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
231
232
  * @param options Optional transport options including abort signal.
232
233
  * @returns An async iterable of receipts, one for each message.
233
234
  */
234
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
235
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId | "pool">>;
235
236
  /**
236
237
  * Disposes of all underlying transports that support disposal.
237
238
  *
@@ -258,7 +259,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
258
259
  * all enabled transports.
259
260
  * @since 0.3.0
260
261
  */
261
- declare class RoundRobinStrategy implements Strategy {
262
+ declare class RoundRobinStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
262
263
  private currentIndex;
263
264
  /**
264
265
  * Selects the next transport in round-robin order.
@@ -270,7 +271,7 @@ declare class RoundRobinStrategy implements Strategy {
270
271
  * @returns The selected transport or `undefined` if all transports have been
271
272
  * attempted.
272
273
  */
273
- select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
274
+ select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
274
275
  /**
275
276
  * Resets the round-robin counter to start from the beginning.
276
277
  */
@@ -287,7 +288,7 @@ declare class RoundRobinStrategy implements Strategy {
287
288
  * messages as a transport with weight 1.
288
289
  * @since 0.3.0
289
290
  */
290
- declare class WeightedStrategy implements Strategy {
291
+ declare class WeightedStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
291
292
  /**
292
293
  * Selects a transport based on weighted random distribution.
293
294
  *
@@ -298,7 +299,7 @@ declare class WeightedStrategy implements Strategy {
298
299
  * @returns The selected transport or `undefined` if all transports have been
299
300
  * attempted.
300
301
  */
301
- select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
302
+ select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
302
303
  /**
303
304
  * Resets the strategy (no-op for weighted strategy as it's stateless).
304
305
  */
@@ -315,7 +316,7 @@ declare class WeightedStrategy implements Strategy {
315
316
  * equivalent and one is selected randomly.
316
317
  * @since 0.3.0
317
318
  */
318
- declare class PriorityStrategy implements Strategy {
319
+ declare class PriorityStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
319
320
  /**
320
321
  * Selects the highest priority transport that hasn't been attempted.
321
322
  *
@@ -326,7 +327,7 @@ declare class PriorityStrategy implements Strategy {
326
327
  * @returns The selected transport or `undefined` if all transports have been
327
328
  * attempted.
328
329
  */
329
- select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
330
+ select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
330
331
  /**
331
332
  * Resets the strategy (no-op for priority strategy as it's stateless).
332
333
  */
@@ -343,7 +344,7 @@ declare class PriorityStrategy implements Strategy {
343
344
  * one is selected randomly.
344
345
  * @since 0.3.0
345
346
  */
346
- declare class SelectorStrategy implements Strategy {
347
+ declare class SelectorStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
347
348
  /**
348
349
  * Selects a transport based on selector function matching.
349
350
  *
@@ -353,7 +354,7 @@ declare class SelectorStrategy implements Strategy {
353
354
  * attempted.
354
355
  * @returns The selected transport or `undefined` if no transport matches.
355
356
  */
356
- select(message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices: Set<number>): TransportSelection | undefined;
357
+ select(message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
357
358
  /**
358
359
  * Resets the strategy (no-op for selector strategy as it's stateless).
359
360
  */
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createFailedReceipt, createReceiptError } from "@upyo/core";
2
+
1
3
  //#region src/config.ts
2
4
  /**
3
5
  * Creates a resolved pool configuration with defaults applied.
@@ -272,6 +274,7 @@ var SelectorStrategy = class {
272
274
  * @since 0.3.0
273
275
  */
274
276
  var PoolTransport = class {
277
+ id = "pool";
275
278
  /**
276
279
  * The resolved configuration used by this pool transport.
277
280
  */
@@ -300,27 +303,54 @@ var PoolTransport = class {
300
303
  */
301
304
  async send(message, options) {
302
305
  const attemptedIndices = /* @__PURE__ */ new Set();
306
+ const errorMessages = [];
303
307
  const errors = [];
304
308
  for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
305
309
  if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
306
310
  const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
307
311
  if (!selection) break;
308
312
  attemptedIndices.add(selection.index);
313
+ let abortedByCaller = false;
309
314
  try {
310
315
  const sendOptions = this.createSendOptions(options);
311
- const receipt = await selection.entry.transport.send(message, sendOptions);
316
+ let receipt;
317
+ try {
318
+ receipt = await selection.entry.transport.send(message, sendOptions.options);
319
+ } catch (error) {
320
+ if (isAbortError(error) && sendOptions.abortedByCaller()) {
321
+ abortedByCaller = true;
322
+ throw error;
323
+ }
324
+ throw error;
325
+ } finally {
326
+ sendOptions.cleanup();
327
+ }
312
328
  if (receipt.successful) return receipt;
313
- errors.push(...receipt.errorMessages);
329
+ errorMessages.push(...receipt.errorMessages);
330
+ errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
314
331
  } catch (error) {
315
- if (error instanceof DOMException && error.name === "AbortError") throw error;
316
- const errorMessage = error instanceof Error ? error.message : String(error);
317
- errors.push(errorMessage);
332
+ if (isAbortError(error) && abortedByCaller) throw error;
333
+ const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
334
+ if (thrownErrors.length > 0) {
335
+ errorMessages.push(...thrownErrors.map((item) => item.message));
336
+ errors.push(...thrownErrors);
337
+ continue;
338
+ }
339
+ const timeoutMessage = "Transport send timed out.";
340
+ const errorMessage = isAbortError(error) ? timeoutMessage : error instanceof Error ? error.message : String(error);
341
+ errorMessages.push(errorMessage);
342
+ errors.push(createReceiptError(errorMessage, {
343
+ provider: selection.entry.transport.id,
344
+ category: isAbortError(error) ? "timeout" : void 0,
345
+ retryable: isAbortError(error) ? true : void 0
346
+ }));
318
347
  }
319
348
  }
320
- return {
321
- successful: false,
322
- errorMessages: errors.length > 0 ? errors : ["All transports failed to send the message"]
323
- };
349
+ return createFailedReceipt(errorMessages.length > 0 ? errorMessages : ["All transports failed to send the message."], {
350
+ provider: "pool",
351
+ errors: errors.length > 0 ? errors : void 0,
352
+ attempts: attemptedIndices.size
353
+ });
324
354
  }
325
355
  /**
326
356
  * Sends multiple email messages using the pool strategy.
@@ -375,22 +405,68 @@ var PoolTransport = class {
375
405
  * Creates send options with timeout if configured.
376
406
  */
377
407
  createSendOptions(options) {
378
- if (!this.config.timeout) return options;
408
+ if (!this.config.timeout) return {
409
+ options,
410
+ abortedByCaller: () => options?.signal?.aborted ?? false,
411
+ cleanup: () => {}
412
+ };
379
413
  const controller = new AbortController();
380
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
381
- if (options?.signal) options.signal.addEventListener("abort", () => {
382
- clearTimeout(timeoutId);
414
+ let abortSource;
415
+ const timeoutId = setTimeout(() => {
416
+ abortSource ??= "timeout";
383
417
  controller.abort();
384
- });
418
+ }, this.config.timeout);
419
+ let cleanup = () => clearTimeout(timeoutId);
420
+ if (options?.signal) {
421
+ const abort = () => {
422
+ abortSource ??= "caller";
423
+ 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
+ }
385
433
  controller.signal.addEventListener("abort", () => {
386
434
  clearTimeout(timeoutId);
387
435
  });
388
436
  return {
389
- ...options,
390
- signal: controller.signal
437
+ options: {
438
+ ...options,
439
+ signal: controller.signal
440
+ },
441
+ abortedByCaller: () => abortSource === "caller",
442
+ cleanup
391
443
  };
392
444
  }
393
445
  };
446
+ function getReceiptErrors(receipt, provider) {
447
+ if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => error.provider == null ? {
448
+ ...error,
449
+ provider
450
+ } : error);
451
+ return receipt.errorMessages.map((message) => createReceiptError(message, { provider: receipt.provider ?? provider }));
452
+ }
453
+ function getThrownReceiptErrors(error, provider) {
454
+ if (isReceiptError(error)) return [error.provider == null ? {
455
+ ...error,
456
+ provider
457
+ } : error];
458
+ if (isFailedReceipt(error)) return getReceiptErrors(error, provider);
459
+ return [];
460
+ }
461
+ function isReceiptError(value) {
462
+ return typeof value === "object" && value != null && typeof value.message === "string" && typeof value.code === "string" && typeof value.retryable === "boolean" && typeof value.category === "string";
463
+ }
464
+ function isFailedReceipt(value) {
465
+ return typeof value === "object" && value != null && value.successful === false && Array.isArray(value.errorMessages);
466
+ }
467
+ function isAbortError(error) {
468
+ return error instanceof Error && error.name === "AbortError";
469
+ }
394
470
 
395
471
  //#endregion
396
472
  export { PoolTransport, PriorityStrategy, RoundRobinStrategy, SelectorStrategy, WeightedStrategy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/pool",
3
- "version": "0.5.0-dev.136",
3
+ "version": "0.5.0-dev.156",
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,19 +56,14 @@
56
56
  },
57
57
  "sideEffects": false,
58
58
  "peerDependencies": {
59
- "@upyo/core": "0.5.0-dev.136+adacf579"
59
+ "@upyo/core": "0.5.0-dev.156+edad9790"
60
60
  },
61
61
  "devDependencies": {
62
- "@dotenvx/dotenvx": "^1.47.3",
63
62
  "tsdown": "^0.12.7",
64
63
  "typescript": "5.8.3",
65
- "@upyo/mock": "0.5.0-dev.136+adacf579"
64
+ "@upyo/mock": "0.5.0-dev.156+edad9790"
66
65
  },
67
66
  "scripts": {
68
- "build": "tsdown",
69
- "prepublish": "tsdown",
70
- "test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
71
- "test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
72
- "test:deno": "deno test --allow-env --env-file=.env"
67
+ "prepublish": "mise run --no-deps :build"
73
68
  }
74
69
  }