@upyo/pool 0.5.0-dev.136 → 0.5.0-dev.154
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 +114 -16
- package/dist/index.d.cts +28 -27
- package/dist/index.d.ts +28 -27
- package/dist/index.js +92 -16
- package/package.json +4 -9
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
|
-
|
|
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
|
-
|
|
352
|
+
errorMessages.push(...receipt.errorMessages);
|
|
353
|
+
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
315
354
|
} catch (error) {
|
|
316
|
-
if (error
|
|
317
|
-
const
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
|
431
|
+
if (!this.config.timeout) return {
|
|
432
|
+
options,
|
|
433
|
+
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
434
|
+
cleanup: () => {}
|
|
435
|
+
};
|
|
380
436
|
const controller = new AbortController();
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
329
|
+
errorMessages.push(...receipt.errorMessages);
|
|
330
|
+
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
314
331
|
} catch (error) {
|
|
315
|
-
if (error
|
|
316
|
-
const
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
|
408
|
+
if (!this.config.timeout) return {
|
|
409
|
+
options,
|
|
410
|
+
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
411
|
+
cleanup: () => {}
|
|
412
|
+
};
|
|
379
413
|
const controller = new AbortController();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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.
|
|
3
|
+
"version": "0.5.0-dev.154",
|
|
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.
|
|
59
|
+
"@upyo/core": "0.5.0-dev.154+2f72d353"
|
|
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.
|
|
64
|
+
"@upyo/mock": "0.5.0-dev.154+2f72d353"
|
|
66
65
|
},
|
|
67
66
|
"scripts": {
|
|
68
|
-
"
|
|
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
|
}
|