@upyo/pool 0.3.0-dev.36

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2025 Hong Minhee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,389 @@
1
+ <!-- deno-fmt-ignore-file -->
2
+
3
+ @upyo/pool
4
+ ==========
5
+
6
+ [![JSR][JSR badge]][JSR]
7
+ [![npm][npm badge]][npm]
8
+
9
+ Pool transport for the [Upyo] email library with load balancing and failover
10
+ strategies for combining multiple email providers.
11
+
12
+ [JSR]: https://jsr.io/@upyo/pool
13
+ [JSR badge]: https://jsr.io/badges/@upyo/pool
14
+ [npm]: https://www.npmjs.com/package/@upyo/pool
15
+ [npm badge]: https://img.shields.io/npm/v/@upyo/pool?logo=npm
16
+ [Upyo]: https://upyo.org/
17
+
18
+
19
+ Features
20
+ --------
21
+
22
+ - Multiple strategies: Round-robin, weighted, priority, and custom
23
+ selector-based routing
24
+ - Automatic failover: Retry with different transports when one fails
25
+ - Load balancing: Distribute email traffic across multiple providers
26
+ - Drop-in replacement: Implements the same `Transport` interface
27
+ - Resource management: Proper cleanup with `AsyncDisposable` support
28
+ - Cross-runtime compatibility (Node.js, Deno, Bun, edge functions)
29
+ - TypeScript support
30
+
31
+
32
+ Installation
33
+ ------------
34
+
35
+ ~~~~ sh
36
+ npm add @upyo/core @upyo/pool
37
+ pnpm add @upyo/core @upyo/pool
38
+ yarn add @upyo/core @upyo/pool
39
+ deno add --jsr @upyo/core @upyo/pool
40
+ bun add @upyo/core @upyo/pool
41
+ ~~~~
42
+
43
+
44
+ Usage
45
+ -----
46
+
47
+ ### Round-robin load balancing
48
+
49
+ Distribute messages evenly across multiple transports in circular order:
50
+
51
+ ~~~~ typescript
52
+ import { PoolTransport } from "@upyo/pool";
53
+ import { createSmtpTransport } from "@upyo/smtp";
54
+ import { createMailgunTransport } from "@upyo/mailgun";
55
+ import { createSendGridTransport } from "@upyo/sendgrid";
56
+
57
+ const transport = new PoolTransport({
58
+ strategy: "round-robin",
59
+ transports: [
60
+ { transport: createSmtpTransport({ /* config */ }) },
61
+ { transport: createMailgunTransport({ /* config */ }) },
62
+ { transport: createSendGridTransport({ /* config */ }) },
63
+ ],
64
+ });
65
+
66
+ // Messages are sent in order: SMTP → Mailgun → SendGrid → SMTP → ...
67
+ await transport.send(message1); // Uses SMTP
68
+ await transport.send(message2); // Uses Mailgun
69
+ await transport.send(message3); // Uses SendGrid
70
+ await transport.send(message4); // Uses SMTP again
71
+ ~~~~
72
+
73
+ ### Weighted distribution
74
+
75
+ Distribute traffic proportionally based on configured weights:
76
+
77
+ ~~~~ typescript
78
+ const transport = new PoolTransport({
79
+ strategy: "weighted",
80
+ transports: [
81
+ { transport: primaryTransport, weight: 3 }, // Gets ~60% of traffic
82
+ { transport: secondaryTransport, weight: 2 }, // Gets ~40% of traffic
83
+ ],
84
+ });
85
+ ~~~~
86
+
87
+ ### Priority-based failover
88
+
89
+ Always use the highest priority transport, falling back to lower priorities
90
+ only on failure:
91
+
92
+ ~~~~ typescript
93
+ const transport = new PoolTransport({
94
+ strategy: "priority",
95
+ transports: [
96
+ { transport: primaryTransport, priority: 100 },
97
+ { transport: backupTransport, priority: 50 },
98
+ { transport: emergencyTransport, priority: 10 },
99
+ ],
100
+ maxRetries: 3, // Try up to 3 transports
101
+ });
102
+
103
+ // Always tries primary first, only uses backup if primary fails
104
+ const receipt = await transport.send(message);
105
+ ~~~~
106
+
107
+ ### Custom routing with selectors
108
+
109
+ Route messages based on custom logic:
110
+
111
+ ~~~~ typescript
112
+ const transport = new PoolTransport({
113
+ strategy: "selector-based",
114
+ transports: [
115
+ {
116
+ transport: bulkEmailTransport,
117
+ selector: (msg) => msg.tags?.includes("newsletter"),
118
+ },
119
+ {
120
+ transport: transactionalTransport,
121
+ selector: (msg) => msg.priority === "high",
122
+ },
123
+ {
124
+ transport: euTransport,
125
+ selector: (msg) => msg.metadata?.region === "EU",
126
+ },
127
+ {
128
+ transport: defaultTransport, // No selector - catches everything else
129
+ },
130
+ ],
131
+ });
132
+
133
+ // Newsletter goes through bulk provider
134
+ await transport.send({
135
+ ...message,
136
+ tags: ["newsletter", "marketing"],
137
+ });
138
+
139
+ // Important email goes through premium provider
140
+ await transport.send({
141
+ ...message,
142
+ priority: "high",
143
+ });
144
+ ~~~~
145
+
146
+ ### Custom strategies
147
+
148
+ You can implement custom routing strategies by creating a class that implements
149
+ the `Strategy` interface:
150
+
151
+ ~~~~ typescript
152
+ import { PoolTransport, type Strategy, type TransportSelection } from "@upyo/pool";
153
+
154
+ class TimeBasedStrategy implements Strategy {
155
+ select(message, transports, attemptedIndices) {
156
+ const hour = new Date().getHours();
157
+
158
+ // Use different transports based on time of day
159
+ const preferredIndex = hour < 12 ? 0 : 1; // Morning vs afternoon
160
+
161
+ if (!attemptedIndices.has(preferredIndex) &&
162
+ transports[preferredIndex]?.enabled) {
163
+ return {
164
+ entry: transports[preferredIndex],
165
+ index: preferredIndex,
166
+ };
167
+ }
168
+
169
+ // Fallback to any available transport
170
+ for (let i = 0; i < transports.length; i++) {
171
+ if (!attemptedIndices.has(i) && transports[i].enabled) {
172
+ return { entry: transports[i], index: i };
173
+ }
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ reset() {
180
+ // Custom reset logic if needed
181
+ }
182
+ }
183
+
184
+ const transport = new PoolTransport({
185
+ strategy: new TimeBasedStrategy(),
186
+ transports: [
187
+ { transport: morningTransport },
188
+ { transport: afternoonTransport },
189
+ ],
190
+ });
191
+ ~~~~
192
+
193
+ ### Resource management
194
+
195
+ The pool transport implements `AsyncDisposable` for automatic cleanup:
196
+
197
+ ~~~~ typescript
198
+ // Automatic cleanup with 'using' statement
199
+ await using transport = new PoolTransport({
200
+ strategy: "round-robin",
201
+ transports: [/* ... */],
202
+ });
203
+
204
+ await transport.send(message);
205
+ // All underlying transports are disposed automatically
206
+
207
+ // Or manual cleanup
208
+ const transport = new PoolTransport(config);
209
+ try {
210
+ await transport.send(message);
211
+ } finally {
212
+ await transport[Symbol.asyncDispose]();
213
+ }
214
+ ~~~~
215
+
216
+
217
+ Configuration
218
+ -------------
219
+
220
+ ### `PoolConfig`
221
+
222
+ | Property | Type | Required | Default | Description |
223
+ |---------------------|-------------------------------------------------------------------------|----------|----------------------|-------------------------------------------------------------------|
224
+ | `strategy` | `"round-robin" | "weighted" | "priority" | "selector-based" | Strategy` | Yes | | The strategy for selecting transports |
225
+ | `transports` | `TransportEntry[]` | Yes | | Array of transport configurations |
226
+ | `maxRetries` | `number` | No | Number of transports | Maximum retry attempts on failure |
227
+ | `timeout` | `number` | No | | Timeout in milliseconds for each send attempt |
228
+ | `continueOnSuccess` | `boolean` | No | `false` | Continue trying transports after success (selector strategy only) |
229
+
230
+ ### `TransportEntry`
231
+
232
+ | Property | Type | Required | Default | Description |
233
+ |-------------|---------------------------------|----------|---------|-----------------------------------------------------|
234
+ | `transport` | `Transport` | Yes | | The transport instance |
235
+ | `weight` | `number` | No | `1` | Weight for weighted distribution |
236
+ | `priority` | `number` | No | `0` | Priority for priority strategy (higher = preferred) |
237
+ | `selector` | `(message: Message) => boolean` | No | | Custom selector function |
238
+ | `enabled` | `boolean` | No | `true` | Whether this transport is enabled |
239
+
240
+
241
+ Strategies
242
+ ----------
243
+
244
+ ### Round-robin
245
+
246
+ Cycles through transports in order, ensuring even distribution:
247
+
248
+ - Maintains internal counter
249
+ - Skips disabled transports
250
+ - Wraps around at the end of the list
251
+ - Best for: Even load distribution
252
+
253
+ ### Weighted
254
+
255
+ Randomly selects transports based on configured weights:
256
+
257
+ - Higher weight = higher probability of selection
258
+ - Supports fractional weights
259
+ - Stateless random selection
260
+ - Best for: Proportional traffic distribution
261
+
262
+ ### Priority
263
+
264
+ Always attempts highest priority transport first:
265
+
266
+ - Sorts by priority value (descending)
267
+ - Falls back to lower priorities on failure
268
+ - Random selection among same priority
269
+ - Best for: Primary/backup scenarios
270
+
271
+ ### Selector-based
272
+
273
+ Routes messages based on custom logic:
274
+
275
+ - Evaluates selector functions in order
276
+ - Transports without selectors act as catch-all
277
+ - Falls back to default if no selector matches
278
+ - Best for: Content-based routing
279
+
280
+
281
+ Error handling
282
+ --------------
283
+
284
+ The pool transport aggregates errors from all failed attempts:
285
+
286
+ ~~~~ typescript
287
+ const receipt = await transport.send(message);
288
+
289
+ if (!receipt.successful) {
290
+ // Contains errors from all attempted transports
291
+ console.error("Failed to send:", receipt.errorMessages);
292
+ }
293
+ ~~~~
294
+
295
+
296
+ Testing
297
+ -------
298
+
299
+ Use `MockTransport` for testing pool behavior:
300
+
301
+ ~~~~ typescript
302
+ import { PoolTransport } from "@upyo/pool";
303
+ import { MockTransport } from "@upyo/mock";
304
+
305
+ const mockTransport1 = new MockTransport();
306
+ const mockTransport2 = new MockTransport();
307
+
308
+ const pool = new PoolTransport({
309
+ strategy: "round-robin",
310
+ transports: [
311
+ { transport: mockTransport1 },
312
+ { transport: mockTransport2 },
313
+ ],
314
+ });
315
+
316
+ await pool.send(message);
317
+
318
+ // Verify distribution
319
+ assert.equal(mockTransport1.getSentMessagesCount(), 1);
320
+ assert.equal(mockTransport2.getSentMessagesCount(), 0);
321
+ ~~~~
322
+
323
+
324
+ Use cases
325
+ ---------
326
+
327
+ ### High availability
328
+
329
+ Ensure email delivery even when providers have outages:
330
+
331
+ ~~~~ typescript
332
+ const transport = new PoolTransport({
333
+ strategy: "priority",
334
+ transports: [
335
+ { transport: primaryProvider, priority: 100 },
336
+ { transport: backupProvider1, priority: 50 },
337
+ { transport: backupProvider2, priority: 50 },
338
+ ],
339
+ });
340
+ ~~~~
341
+
342
+ ### Cost optimization
343
+
344
+ Route different types of emails through appropriate providers:
345
+
346
+ ~~~~ typescript
347
+ const transport = new PoolTransport({
348
+ strategy: "selector-based",
349
+ transports: [
350
+ {
351
+ transport: cheapBulkProvider,
352
+ selector: (msg) => msg.tags?.includes("newsletter"),
353
+ },
354
+ {
355
+ transport: premiumProvider,
356
+ selector: (msg) => msg.priority === "high" ||
357
+ msg.tags?.includes("transactional"),
358
+ },
359
+ ],
360
+ });
361
+ ~~~~
362
+
363
+ ### Rate limit management
364
+
365
+ Distribute load when approaching provider limits:
366
+
367
+ ~~~~ typescript
368
+ const transport = new PoolTransport({
369
+ strategy: "weighted",
370
+ transports: [
371
+ { transport: provider1, weight: 1 }, // 1000 emails/hour limit
372
+ { transport: provider2, weight: 2 }, // 2000 emails/hour limit
373
+ ],
374
+ });
375
+ ~~~~
376
+
377
+ ### Gradual migration
378
+
379
+ Shift traffic from old to new provider:
380
+
381
+ ~~~~ typescript
382
+ const transport = new PoolTransport({
383
+ strategy: "weighted",
384
+ transports: [
385
+ { transport: oldProvider, weight: 90 }, // Start with 90%
386
+ { transport: newProvider, weight: 10 }, // Gradually increase
387
+ ],
388
+ });
389
+ ~~~~