@upyo/pool 0.5.0-dev.87 → 0.5.0
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 +142 -21
- package/dist/index.d.cts +31 -29
- package/dist/index.d.ts +31 -29
- package/dist/index.js +120 -21
- package/package.json +4 -9
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
|
@@ -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
|
/**
|
|
@@ -26,7 +50,7 @@ function createPoolConfig(config) {
|
|
|
26
50
|
return {
|
|
27
51
|
strategy: config.strategy,
|
|
28
52
|
transports: resolvedTransports,
|
|
29
|
-
maxRetries: config.maxRetries ?? enabledTransports.length,
|
|
53
|
+
maxRetries: config.maxRetries ?? enabledTransports.length - 1,
|
|
30
54
|
timeout: config.timeout,
|
|
31
55
|
continueOnSuccess: config.continueOnSuccess ?? false
|
|
32
56
|
};
|
|
@@ -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,57 @@ 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
|
+
let checkCallerAbortBeforeFailure = false;
|
|
332
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
306
333
|
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
307
334
|
const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
|
|
308
335
|
if (!selection) break;
|
|
309
336
|
attemptedIndices.add(selection.index);
|
|
337
|
+
let abortedByCaller = false;
|
|
310
338
|
try {
|
|
311
339
|
const sendOptions = this.createSendOptions(options);
|
|
312
|
-
|
|
340
|
+
let receipt;
|
|
341
|
+
try {
|
|
342
|
+
receipt = await selection.entry.transport.send(message, sendOptions.options);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (sendOptions.abortedByCaller() && isCallerAbort(error, options?.signal)) abortedByCaller = true;
|
|
345
|
+
throw error;
|
|
346
|
+
} finally {
|
|
347
|
+
sendOptions.cleanup();
|
|
348
|
+
}
|
|
313
349
|
if (receipt.successful) return receipt;
|
|
314
|
-
|
|
350
|
+
errorMessages.push(...receipt.errorMessages);
|
|
351
|
+
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
352
|
+
checkCallerAbortBeforeFailure = true;
|
|
315
353
|
} catch (error) {
|
|
316
|
-
if (
|
|
317
|
-
const
|
|
318
|
-
|
|
354
|
+
if (abortedByCaller && isCallerAbort(error, options?.signal)) throw error;
|
|
355
|
+
const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
|
|
356
|
+
if (thrownErrors.length > 0) {
|
|
357
|
+
errorMessages.push(...thrownErrors.map((item) => item.message));
|
|
358
|
+
errors.push(...thrownErrors);
|
|
359
|
+
checkCallerAbortBeforeFailure = true;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const timeoutMessage = "Transport send timed out.";
|
|
363
|
+
const abortError = isAbortError(error);
|
|
364
|
+
const errorMessage = abortError ? timeoutMessage : error instanceof Error ? error.message : String(error);
|
|
365
|
+
errorMessages.push(errorMessage);
|
|
366
|
+
errors.push((0, __upyo_core.createReceiptError)(errorMessage, {
|
|
367
|
+
provider: selection.entry.transport.id,
|
|
368
|
+
category: abortError ? "timeout" : void 0,
|
|
369
|
+
retryable: abortError ? true : void 0
|
|
370
|
+
}));
|
|
371
|
+
checkCallerAbortBeforeFailure ||= !abortError;
|
|
319
372
|
}
|
|
320
373
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
374
|
+
if (checkCallerAbortBeforeFailure) options?.signal?.throwIfAborted();
|
|
375
|
+
return (0, __upyo_core.createFailedReceipt)(errorMessages.length > 0 ? errorMessages : ["All transports failed to send the message."], {
|
|
376
|
+
provider: "pool",
|
|
377
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
378
|
+
attempts: attemptedIndices.size
|
|
379
|
+
});
|
|
325
380
|
}
|
|
326
381
|
/**
|
|
327
382
|
* Sends multiple email messages using the pool strategy.
|
|
@@ -376,22 +431,88 @@ var PoolTransport = class {
|
|
|
376
431
|
* Creates send options with timeout if configured.
|
|
377
432
|
*/
|
|
378
433
|
createSendOptions(options) {
|
|
379
|
-
if (!this.config.timeout) return
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
434
|
+
if (!this.config.timeout) return {
|
|
435
|
+
options,
|
|
436
|
+
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
437
|
+
cleanup: () => {}
|
|
438
|
+
};
|
|
439
|
+
const timeoutController = new AbortController();
|
|
440
|
+
let abortSource;
|
|
441
|
+
const timeoutId = setTimeout(() => {
|
|
442
|
+
abortSource ??= "timeout";
|
|
443
|
+
timeoutController.abort(createAbortError());
|
|
444
|
+
}, this.config.timeout);
|
|
445
|
+
let cleanupAbortSource = () => {};
|
|
446
|
+
let signal = timeoutController.signal;
|
|
447
|
+
let cleanupCombinedSignal = () => {};
|
|
448
|
+
if (options?.signal) {
|
|
449
|
+
const markCallerAbort = () => {
|
|
450
|
+
abortSource ??= "caller";
|
|
451
|
+
clearTimeout(timeoutId);
|
|
452
|
+
};
|
|
453
|
+
if (options.signal.aborted) markCallerAbort();
|
|
454
|
+
else options.signal.addEventListener("abort", markCallerAbort, { once: true });
|
|
455
|
+
cleanupAbortSource = () => options.signal?.removeEventListener("abort", markCallerAbort);
|
|
456
|
+
const combinedSignal = (0, __upyo_core.combineSignals)(timeoutController.signal, options.signal);
|
|
457
|
+
signal = combinedSignal.signal;
|
|
458
|
+
cleanupCombinedSignal = combinedSignal.cleanup;
|
|
459
|
+
}
|
|
460
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
387
461
|
clearTimeout(timeoutId);
|
|
388
462
|
});
|
|
389
463
|
return {
|
|
390
|
-
|
|
391
|
-
|
|
464
|
+
options: {
|
|
465
|
+
...options,
|
|
466
|
+
signal
|
|
467
|
+
},
|
|
468
|
+
abortedByCaller: () => abortSource === "caller",
|
|
469
|
+
cleanup: () => {
|
|
470
|
+
clearTimeout(timeoutId);
|
|
471
|
+
cleanupAbortSource();
|
|
472
|
+
cleanupCombinedSignal();
|
|
473
|
+
}
|
|
392
474
|
};
|
|
393
475
|
}
|
|
394
476
|
};
|
|
477
|
+
function createAbortError() {
|
|
478
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
479
|
+
}
|
|
480
|
+
function isCallerAbort(error, signal) {
|
|
481
|
+
return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
|
|
482
|
+
}
|
|
483
|
+
function getReceiptErrors(receipt, provider) {
|
|
484
|
+
if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => withReceiptErrorProvider(error, provider));
|
|
485
|
+
return receipt.errorMessages.map((message) => (0, __upyo_core.createReceiptError)(message, {
|
|
486
|
+
provider: receipt.provider ?? provider,
|
|
487
|
+
retryable: receipt.retryable
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
function getThrownReceiptErrors(error, provider) {
|
|
491
|
+
if (isReceiptError(error)) return [withReceiptErrorProvider(error, provider)];
|
|
492
|
+
if (isFailedReceipt(error)) return getReceiptErrors(error, provider);
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
function withReceiptErrorProvider(error, provider) {
|
|
496
|
+
return {
|
|
497
|
+
message: error.message,
|
|
498
|
+
code: error.code,
|
|
499
|
+
category: error.category,
|
|
500
|
+
retryable: error.retryable,
|
|
501
|
+
provider: error.provider ?? provider,
|
|
502
|
+
statusCode: error.statusCode,
|
|
503
|
+
retryAfterMilliseconds: error.retryAfterMilliseconds,
|
|
504
|
+
providerDetails: error.providerDetails
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function isReceiptError(value) {
|
|
508
|
+
return typeof value === "object" && value != null && typeof value.message === "string" && typeof value.code === "string" && typeof value.retryable === "boolean" && typeof value.category === "string";
|
|
509
|
+
}
|
|
510
|
+
function isFailedReceipt(value) {
|
|
511
|
+
return typeof value === "object" && value != null && value.successful === false && Array.isArray(value.errorMessages);
|
|
512
|
+
}
|
|
513
|
+
function isAbortError(error) {
|
|
514
|
+
return error instanceof Error && error.name === "AbortError";
|
|
515
|
+
}
|
|
395
516
|
|
|
396
517
|
//#endregion
|
|
397
518
|
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,19 +84,20 @@ 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
|
-
* 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
|
/**
|
|
@@ -114,9 +115,9 @@ interface PoolConfig {
|
|
|
114
115
|
* Resolved pool configuration with defaults applied.
|
|
115
116
|
* @since 0.3.0
|
|
116
117
|
*/
|
|
117
|
-
interface ResolvedPoolConfig {
|
|
118
|
-
readonly strategy: PoolStrategy | Strategy
|
|
119
|
-
readonly transports: readonly ResolvedTransportEntry[];
|
|
118
|
+
interface ResolvedPoolConfig<TProviderId extends string = string> {
|
|
119
|
+
readonly strategy: PoolStrategy | Strategy<TProviderId>;
|
|
120
|
+
readonly transports: readonly ResolvedTransportEntry<TProviderId>[];
|
|
120
121
|
readonly maxRetries: number;
|
|
121
122
|
readonly timeout?: number;
|
|
122
123
|
readonly continueOnSuccess: boolean;
|
|
@@ -125,8 +126,8 @@ interface ResolvedPoolConfig {
|
|
|
125
126
|
* Resolved transport entry with defaults applied.
|
|
126
127
|
* @since 0.3.0
|
|
127
128
|
*/
|
|
128
|
-
interface ResolvedTransportEntry {
|
|
129
|
-
readonly transport: Transport
|
|
129
|
+
interface ResolvedTransportEntry<TProviderId extends string = string> {
|
|
130
|
+
readonly transport: Transport<TProviderId>;
|
|
130
131
|
readonly weight: number;
|
|
131
132
|
readonly priority: number;
|
|
132
133
|
readonly selector?: TransportSelector;
|
|
@@ -196,11 +197,12 @@ interface ResolvedTransportEntry {
|
|
|
196
197
|
*
|
|
197
198
|
* @since 0.3.0
|
|
198
199
|
*/
|
|
199
|
-
declare class PoolTransport implements Transport
|
|
200
|
+
declare class PoolTransport<TProviderId extends string = string> implements Transport<TProviderId | "pool">, AsyncDisposable {
|
|
201
|
+
readonly id = "pool";
|
|
200
202
|
/**
|
|
201
203
|
* The resolved configuration used by this pool transport.
|
|
202
204
|
*/
|
|
203
|
-
readonly config: ResolvedPoolConfig
|
|
205
|
+
readonly config: ResolvedPoolConfig<TProviderId>;
|
|
204
206
|
private readonly strategy;
|
|
205
207
|
/**
|
|
206
208
|
* Creates a new PoolTransport instance.
|
|
@@ -208,7 +210,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
208
210
|
* @param config Configuration options for the pool transport.
|
|
209
211
|
* @throws {Error} If the configuration is invalid.
|
|
210
212
|
*/
|
|
211
|
-
constructor(config: PoolConfig);
|
|
213
|
+
constructor(config: PoolConfig<TProviderId>);
|
|
212
214
|
/**
|
|
213
215
|
* Sends a single email message using the pool strategy.
|
|
214
216
|
*
|
|
@@ -220,7 +222,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
220
222
|
* @param options Optional transport options including abort signal.
|
|
221
223
|
* @returns A promise that resolves to a receipt indicating success or failure.
|
|
222
224
|
*/
|
|
223
|
-
send(message: Message, options?: TransportOptions): Promise<Receipt
|
|
225
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId | "pool">>;
|
|
224
226
|
/**
|
|
225
227
|
* Sends multiple email messages using the pool strategy.
|
|
226
228
|
*
|
|
@@ -231,7 +233,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
231
233
|
* @param options Optional transport options including abort signal.
|
|
232
234
|
* @returns An async iterable of receipts, one for each message.
|
|
233
235
|
*/
|
|
234
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
236
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId | "pool">>;
|
|
235
237
|
/**
|
|
236
238
|
* Disposes of all underlying transports that support disposal.
|
|
237
239
|
*
|
|
@@ -258,7 +260,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
258
260
|
* all enabled transports.
|
|
259
261
|
* @since 0.3.0
|
|
260
262
|
*/
|
|
261
|
-
declare class RoundRobinStrategy implements Strategy {
|
|
263
|
+
declare class RoundRobinStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
262
264
|
private currentIndex;
|
|
263
265
|
/**
|
|
264
266
|
* Selects the next transport in round-robin order.
|
|
@@ -270,7 +272,7 @@ declare class RoundRobinStrategy implements Strategy {
|
|
|
270
272
|
* @returns The selected transport or `undefined` if all transports have been
|
|
271
273
|
* attempted.
|
|
272
274
|
*/
|
|
273
|
-
select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
275
|
+
select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
274
276
|
/**
|
|
275
277
|
* Resets the round-robin counter to start from the beginning.
|
|
276
278
|
*/
|
|
@@ -287,7 +289,7 @@ declare class RoundRobinStrategy implements Strategy {
|
|
|
287
289
|
* messages as a transport with weight 1.
|
|
288
290
|
* @since 0.3.0
|
|
289
291
|
*/
|
|
290
|
-
declare class WeightedStrategy implements Strategy {
|
|
292
|
+
declare class WeightedStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
291
293
|
/**
|
|
292
294
|
* Selects a transport based on weighted random distribution.
|
|
293
295
|
*
|
|
@@ -298,7 +300,7 @@ declare class WeightedStrategy implements Strategy {
|
|
|
298
300
|
* @returns The selected transport or `undefined` if all transports have been
|
|
299
301
|
* attempted.
|
|
300
302
|
*/
|
|
301
|
-
select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
303
|
+
select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
302
304
|
/**
|
|
303
305
|
* Resets the strategy (no-op for weighted strategy as it's stateless).
|
|
304
306
|
*/
|
|
@@ -315,7 +317,7 @@ declare class WeightedStrategy implements Strategy {
|
|
|
315
317
|
* equivalent and one is selected randomly.
|
|
316
318
|
* @since 0.3.0
|
|
317
319
|
*/
|
|
318
|
-
declare class PriorityStrategy implements Strategy {
|
|
320
|
+
declare class PriorityStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
319
321
|
/**
|
|
320
322
|
* Selects the highest priority transport that hasn't been attempted.
|
|
321
323
|
*
|
|
@@ -326,7 +328,7 @@ declare class PriorityStrategy implements Strategy {
|
|
|
326
328
|
* @returns The selected transport or `undefined` if all transports have been
|
|
327
329
|
* attempted.
|
|
328
330
|
*/
|
|
329
|
-
select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
331
|
+
select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
330
332
|
/**
|
|
331
333
|
* Resets the strategy (no-op for priority strategy as it's stateless).
|
|
332
334
|
*/
|
|
@@ -343,7 +345,7 @@ declare class PriorityStrategy implements Strategy {
|
|
|
343
345
|
* one is selected randomly.
|
|
344
346
|
* @since 0.3.0
|
|
345
347
|
*/
|
|
346
|
-
declare class SelectorStrategy implements Strategy {
|
|
348
|
+
declare class SelectorStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
347
349
|
/**
|
|
348
350
|
* Selects a transport based on selector function matching.
|
|
349
351
|
*
|
|
@@ -353,7 +355,7 @@ declare class SelectorStrategy implements Strategy {
|
|
|
353
355
|
* attempted.
|
|
354
356
|
* @returns The selected transport or `undefined` if no transport matches.
|
|
355
357
|
*/
|
|
356
|
-
select(message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
358
|
+
select(message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
357
359
|
/**
|
|
358
360
|
* Resets the strategy (no-op for selector strategy as it's stateless).
|
|
359
361
|
*/
|
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,19 +84,20 @@ 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
|
-
* 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
|
/**
|
|
@@ -114,9 +115,9 @@ interface PoolConfig {
|
|
|
114
115
|
* Resolved pool configuration with defaults applied.
|
|
115
116
|
* @since 0.3.0
|
|
116
117
|
*/
|
|
117
|
-
interface ResolvedPoolConfig {
|
|
118
|
-
readonly strategy: PoolStrategy | Strategy
|
|
119
|
-
readonly transports: readonly ResolvedTransportEntry[];
|
|
118
|
+
interface ResolvedPoolConfig<TProviderId extends string = string> {
|
|
119
|
+
readonly strategy: PoolStrategy | Strategy<TProviderId>;
|
|
120
|
+
readonly transports: readonly ResolvedTransportEntry<TProviderId>[];
|
|
120
121
|
readonly maxRetries: number;
|
|
121
122
|
readonly timeout?: number;
|
|
122
123
|
readonly continueOnSuccess: boolean;
|
|
@@ -125,8 +126,8 @@ interface ResolvedPoolConfig {
|
|
|
125
126
|
* Resolved transport entry with defaults applied.
|
|
126
127
|
* @since 0.3.0
|
|
127
128
|
*/
|
|
128
|
-
interface ResolvedTransportEntry {
|
|
129
|
-
readonly transport: Transport
|
|
129
|
+
interface ResolvedTransportEntry<TProviderId extends string = string> {
|
|
130
|
+
readonly transport: Transport<TProviderId>;
|
|
130
131
|
readonly weight: number;
|
|
131
132
|
readonly priority: number;
|
|
132
133
|
readonly selector?: TransportSelector;
|
|
@@ -196,11 +197,12 @@ interface ResolvedTransportEntry {
|
|
|
196
197
|
*
|
|
197
198
|
* @since 0.3.0
|
|
198
199
|
*/
|
|
199
|
-
declare class PoolTransport implements Transport
|
|
200
|
+
declare class PoolTransport<TProviderId extends string = string> implements Transport<TProviderId | "pool">, AsyncDisposable {
|
|
201
|
+
readonly id = "pool";
|
|
200
202
|
/**
|
|
201
203
|
* The resolved configuration used by this pool transport.
|
|
202
204
|
*/
|
|
203
|
-
readonly config: ResolvedPoolConfig
|
|
205
|
+
readonly config: ResolvedPoolConfig<TProviderId>;
|
|
204
206
|
private readonly strategy;
|
|
205
207
|
/**
|
|
206
208
|
* Creates a new PoolTransport instance.
|
|
@@ -208,7 +210,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
208
210
|
* @param config Configuration options for the pool transport.
|
|
209
211
|
* @throws {Error} If the configuration is invalid.
|
|
210
212
|
*/
|
|
211
|
-
constructor(config: PoolConfig);
|
|
213
|
+
constructor(config: PoolConfig<TProviderId>);
|
|
212
214
|
/**
|
|
213
215
|
* Sends a single email message using the pool strategy.
|
|
214
216
|
*
|
|
@@ -220,7 +222,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
220
222
|
* @param options Optional transport options including abort signal.
|
|
221
223
|
* @returns A promise that resolves to a receipt indicating success or failure.
|
|
222
224
|
*/
|
|
223
|
-
send(message: Message, options?: TransportOptions): Promise<Receipt
|
|
225
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId | "pool">>;
|
|
224
226
|
/**
|
|
225
227
|
* Sends multiple email messages using the pool strategy.
|
|
226
228
|
*
|
|
@@ -231,7 +233,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
231
233
|
* @param options Optional transport options including abort signal.
|
|
232
234
|
* @returns An async iterable of receipts, one for each message.
|
|
233
235
|
*/
|
|
234
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
236
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId | "pool">>;
|
|
235
237
|
/**
|
|
236
238
|
* Disposes of all underlying transports that support disposal.
|
|
237
239
|
*
|
|
@@ -258,7 +260,7 @@ declare class PoolTransport implements Transport, AsyncDisposable {
|
|
|
258
260
|
* all enabled transports.
|
|
259
261
|
* @since 0.3.0
|
|
260
262
|
*/
|
|
261
|
-
declare class RoundRobinStrategy implements Strategy {
|
|
263
|
+
declare class RoundRobinStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
262
264
|
private currentIndex;
|
|
263
265
|
/**
|
|
264
266
|
* Selects the next transport in round-robin order.
|
|
@@ -270,7 +272,7 @@ declare class RoundRobinStrategy implements Strategy {
|
|
|
270
272
|
* @returns The selected transport or `undefined` if all transports have been
|
|
271
273
|
* attempted.
|
|
272
274
|
*/
|
|
273
|
-
select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
275
|
+
select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
274
276
|
/**
|
|
275
277
|
* Resets the round-robin counter to start from the beginning.
|
|
276
278
|
*/
|
|
@@ -287,7 +289,7 @@ declare class RoundRobinStrategy implements Strategy {
|
|
|
287
289
|
* messages as a transport with weight 1.
|
|
288
290
|
* @since 0.3.0
|
|
289
291
|
*/
|
|
290
|
-
declare class WeightedStrategy implements Strategy {
|
|
292
|
+
declare class WeightedStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
291
293
|
/**
|
|
292
294
|
* Selects a transport based on weighted random distribution.
|
|
293
295
|
*
|
|
@@ -298,7 +300,7 @@ declare class WeightedStrategy implements Strategy {
|
|
|
298
300
|
* @returns The selected transport or `undefined` if all transports have been
|
|
299
301
|
* attempted.
|
|
300
302
|
*/
|
|
301
|
-
select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
303
|
+
select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
302
304
|
/**
|
|
303
305
|
* Resets the strategy (no-op for weighted strategy as it's stateless).
|
|
304
306
|
*/
|
|
@@ -315,7 +317,7 @@ declare class WeightedStrategy implements Strategy {
|
|
|
315
317
|
* equivalent and one is selected randomly.
|
|
316
318
|
* @since 0.3.0
|
|
317
319
|
*/
|
|
318
|
-
declare class PriorityStrategy implements Strategy {
|
|
320
|
+
declare class PriorityStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
319
321
|
/**
|
|
320
322
|
* Selects the highest priority transport that hasn't been attempted.
|
|
321
323
|
*
|
|
@@ -326,7 +328,7 @@ declare class PriorityStrategy implements Strategy {
|
|
|
326
328
|
* @returns The selected transport or `undefined` if all transports have been
|
|
327
329
|
* attempted.
|
|
328
330
|
*/
|
|
329
|
-
select(_message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
331
|
+
select(_message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
330
332
|
/**
|
|
331
333
|
* Resets the strategy (no-op for priority strategy as it's stateless).
|
|
332
334
|
*/
|
|
@@ -343,7 +345,7 @@ declare class PriorityStrategy implements Strategy {
|
|
|
343
345
|
* one is selected randomly.
|
|
344
346
|
* @since 0.3.0
|
|
345
347
|
*/
|
|
346
|
-
declare class SelectorStrategy implements Strategy {
|
|
348
|
+
declare class SelectorStrategy<TProviderId extends string = string> implements Strategy<TProviderId> {
|
|
347
349
|
/**
|
|
348
350
|
* Selects a transport based on selector function matching.
|
|
349
351
|
*
|
|
@@ -353,7 +355,7 @@ declare class SelectorStrategy implements Strategy {
|
|
|
353
355
|
* attempted.
|
|
354
356
|
* @returns The selected transport or `undefined` if no transport matches.
|
|
355
357
|
*/
|
|
356
|
-
select(message: Message, transports: readonly ResolvedTransportEntry[], attemptedIndices:
|
|
358
|
+
select(message: Message, transports: readonly ResolvedTransportEntry<TProviderId>[], attemptedIndices: ReadonlySet<number>): TransportSelection<TProviderId> | undefined;
|
|
357
359
|
/**
|
|
358
360
|
* Resets the strategy (no-op for selector strategy as it's stateless).
|
|
359
361
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { combineSignals, createFailedReceipt, createReceiptError } from "@upyo/core";
|
|
2
|
+
|
|
1
3
|
//#region src/config.ts
|
|
2
4
|
/**
|
|
3
5
|
* Creates a resolved pool configuration with defaults applied.
|
|
@@ -25,7 +27,7 @@ function createPoolConfig(config) {
|
|
|
25
27
|
return {
|
|
26
28
|
strategy: config.strategy,
|
|
27
29
|
transports: resolvedTransports,
|
|
28
|
-
maxRetries: config.maxRetries ?? enabledTransports.length,
|
|
30
|
+
maxRetries: config.maxRetries ?? enabledTransports.length - 1,
|
|
29
31
|
timeout: config.timeout,
|
|
30
32
|
continueOnSuccess: config.continueOnSuccess ?? false
|
|
31
33
|
};
|
|
@@ -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,57 @@ 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
|
+
let checkCallerAbortBeforeFailure = false;
|
|
309
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
305
310
|
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
306
311
|
const selection = this.strategy.select(message, this.config.transports, attemptedIndices);
|
|
307
312
|
if (!selection) break;
|
|
308
313
|
attemptedIndices.add(selection.index);
|
|
314
|
+
let abortedByCaller = false;
|
|
309
315
|
try {
|
|
310
316
|
const sendOptions = this.createSendOptions(options);
|
|
311
|
-
|
|
317
|
+
let receipt;
|
|
318
|
+
try {
|
|
319
|
+
receipt = await selection.entry.transport.send(message, sendOptions.options);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (sendOptions.abortedByCaller() && isCallerAbort(error, options?.signal)) abortedByCaller = true;
|
|
322
|
+
throw error;
|
|
323
|
+
} finally {
|
|
324
|
+
sendOptions.cleanup();
|
|
325
|
+
}
|
|
312
326
|
if (receipt.successful) return receipt;
|
|
313
|
-
|
|
327
|
+
errorMessages.push(...receipt.errorMessages);
|
|
328
|
+
errors.push(...getReceiptErrors(receipt, selection.entry.transport.id));
|
|
329
|
+
checkCallerAbortBeforeFailure = true;
|
|
314
330
|
} catch (error) {
|
|
315
|
-
if (
|
|
316
|
-
const
|
|
317
|
-
|
|
331
|
+
if (abortedByCaller && isCallerAbort(error, options?.signal)) throw error;
|
|
332
|
+
const thrownErrors = getThrownReceiptErrors(error, selection.entry.transport.id);
|
|
333
|
+
if (thrownErrors.length > 0) {
|
|
334
|
+
errorMessages.push(...thrownErrors.map((item) => item.message));
|
|
335
|
+
errors.push(...thrownErrors);
|
|
336
|
+
checkCallerAbortBeforeFailure = true;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const timeoutMessage = "Transport send timed out.";
|
|
340
|
+
const abortError = isAbortError(error);
|
|
341
|
+
const errorMessage = abortError ? timeoutMessage : error instanceof Error ? error.message : String(error);
|
|
342
|
+
errorMessages.push(errorMessage);
|
|
343
|
+
errors.push(createReceiptError(errorMessage, {
|
|
344
|
+
provider: selection.entry.transport.id,
|
|
345
|
+
category: abortError ? "timeout" : void 0,
|
|
346
|
+
retryable: abortError ? true : void 0
|
|
347
|
+
}));
|
|
348
|
+
checkCallerAbortBeforeFailure ||= !abortError;
|
|
318
349
|
}
|
|
319
350
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
351
|
+
if (checkCallerAbortBeforeFailure) options?.signal?.throwIfAborted();
|
|
352
|
+
return createFailedReceipt(errorMessages.length > 0 ? errorMessages : ["All transports failed to send the message."], {
|
|
353
|
+
provider: "pool",
|
|
354
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
355
|
+
attempts: attemptedIndices.size
|
|
356
|
+
});
|
|
324
357
|
}
|
|
325
358
|
/**
|
|
326
359
|
* Sends multiple email messages using the pool strategy.
|
|
@@ -375,22 +408,88 @@ var PoolTransport = class {
|
|
|
375
408
|
* Creates send options with timeout if configured.
|
|
376
409
|
*/
|
|
377
410
|
createSendOptions(options) {
|
|
378
|
-
if (!this.config.timeout) return
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
411
|
+
if (!this.config.timeout) return {
|
|
412
|
+
options,
|
|
413
|
+
abortedByCaller: () => options?.signal?.aborted ?? false,
|
|
414
|
+
cleanup: () => {}
|
|
415
|
+
};
|
|
416
|
+
const timeoutController = new AbortController();
|
|
417
|
+
let abortSource;
|
|
418
|
+
const timeoutId = setTimeout(() => {
|
|
419
|
+
abortSource ??= "timeout";
|
|
420
|
+
timeoutController.abort(createAbortError());
|
|
421
|
+
}, this.config.timeout);
|
|
422
|
+
let cleanupAbortSource = () => {};
|
|
423
|
+
let signal = timeoutController.signal;
|
|
424
|
+
let cleanupCombinedSignal = () => {};
|
|
425
|
+
if (options?.signal) {
|
|
426
|
+
const markCallerAbort = () => {
|
|
427
|
+
abortSource ??= "caller";
|
|
428
|
+
clearTimeout(timeoutId);
|
|
429
|
+
};
|
|
430
|
+
if (options.signal.aborted) markCallerAbort();
|
|
431
|
+
else options.signal.addEventListener("abort", markCallerAbort, { once: true });
|
|
432
|
+
cleanupAbortSource = () => options.signal?.removeEventListener("abort", markCallerAbort);
|
|
433
|
+
const combinedSignal = combineSignals(timeoutController.signal, options.signal);
|
|
434
|
+
signal = combinedSignal.signal;
|
|
435
|
+
cleanupCombinedSignal = combinedSignal.cleanup;
|
|
436
|
+
}
|
|
437
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
386
438
|
clearTimeout(timeoutId);
|
|
387
439
|
});
|
|
388
440
|
return {
|
|
389
|
-
|
|
390
|
-
|
|
441
|
+
options: {
|
|
442
|
+
...options,
|
|
443
|
+
signal
|
|
444
|
+
},
|
|
445
|
+
abortedByCaller: () => abortSource === "caller",
|
|
446
|
+
cleanup: () => {
|
|
447
|
+
clearTimeout(timeoutId);
|
|
448
|
+
cleanupAbortSource();
|
|
449
|
+
cleanupCombinedSignal();
|
|
450
|
+
}
|
|
391
451
|
};
|
|
392
452
|
}
|
|
393
453
|
};
|
|
454
|
+
function createAbortError() {
|
|
455
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
456
|
+
}
|
|
457
|
+
function isCallerAbort(error, signal) {
|
|
458
|
+
return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
|
|
459
|
+
}
|
|
460
|
+
function getReceiptErrors(receipt, provider) {
|
|
461
|
+
if (receipt.errors != null && receipt.errors.length > 0) return receipt.errors.map((error) => withReceiptErrorProvider(error, provider));
|
|
462
|
+
return receipt.errorMessages.map((message) => createReceiptError(message, {
|
|
463
|
+
provider: receipt.provider ?? provider,
|
|
464
|
+
retryable: receipt.retryable
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
function getThrownReceiptErrors(error, provider) {
|
|
468
|
+
if (isReceiptError(error)) return [withReceiptErrorProvider(error, provider)];
|
|
469
|
+
if (isFailedReceipt(error)) return getReceiptErrors(error, provider);
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
function withReceiptErrorProvider(error, provider) {
|
|
473
|
+
return {
|
|
474
|
+
message: error.message,
|
|
475
|
+
code: error.code,
|
|
476
|
+
category: error.category,
|
|
477
|
+
retryable: error.retryable,
|
|
478
|
+
provider: error.provider ?? provider,
|
|
479
|
+
statusCode: error.statusCode,
|
|
480
|
+
retryAfterMilliseconds: error.retryAfterMilliseconds,
|
|
481
|
+
providerDetails: error.providerDetails
|
|
482
|
+
};
|
|
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
|
+
}
|
|
394
493
|
|
|
395
494
|
//#endregion
|
|
396
495
|
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
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
|
59
|
+
"@upyo/core": "0.5.0"
|
|
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
|
|
64
|
+
"@upyo/mock": "0.5.0"
|
|
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
|
}
|