@upyo/ses 0.5.0-dev.86 → 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/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
  /**
@@ -56,6 +80,16 @@ var SesHttpClient = class {
56
80
  signal
57
81
  });
58
82
  }
83
+ /**
84
+ * Makes an HTTP request to the SES API with retry logic.
85
+ *
86
+ * @param url The URL to make the request to.
87
+ * @param options Fetch options.
88
+ * @returns Promise that resolves to the parsed response.
89
+ * @throws {DOMException} If the caller aborts the request.
90
+ * @throws {SesApiError} If SES returns a client error or all retry attempts
91
+ * are exhausted.
92
+ */
59
93
  async makeRequest(url, options) {
60
94
  let lastError = null;
61
95
  for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
@@ -72,16 +106,30 @@ var SesHttpClient = class {
72
106
  } catch {
73
107
  errorData = { message: text || `HTTP ${response.status}` };
74
108
  }
75
- throw new SesApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.errors);
109
+ throw new SesApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.errors, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")), attempt + 1);
76
110
  } catch (error) {
77
111
  lastError = error instanceof Error ? error : new Error(String(error));
112
+ if (options.signal?.aborted) throw createAbortError(options.signal);
113
+ if (isAbortError$1(error)) throw error;
78
114
  if (error instanceof SesApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) throw error;
79
- if (attempt === this.config.retries) throw error;
115
+ if (attempt === this.config.retries) {
116
+ if (lastError instanceof SesApiError) throw lastError;
117
+ throw new SesApiError(lastError.message, void 0, void 0, void 0, attempt + 1);
118
+ }
80
119
  const delay = Math.pow(2, attempt) * 1e3;
81
- await new Promise((resolve) => setTimeout(resolve, delay));
120
+ await sleep(delay, options.signal);
82
121
  }
83
122
  throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
84
123
  }
124
+ /**
125
+ * Makes a signed fetch request to the SES API.
126
+ *
127
+ * @param url The URL to make the request to.
128
+ * @param options Fetch options.
129
+ * @returns Promise that resolves to the fetch response.
130
+ * @throws {Error} If the configured request timeout is reached.
131
+ * @throws {DOMException} If the caller aborts the request.
132
+ */
85
133
  async fetchWithAuth(url, options) {
86
134
  const credentials = await this.getCredentials();
87
135
  const headers = new Headers(options.headers);
@@ -92,19 +140,18 @@ var SesHttpClient = class {
92
140
  for (const [key, value] of Object.entries(this.config.headers)) signedHeaders.set(key, value);
93
141
  const controller = new AbortController();
94
142
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
95
- let signal = controller.signal;
96
- if (options.signal) {
97
- signal = options.signal;
98
- if (options.signal.aborted) controller.abort();
99
- else options.signal.addEventListener("abort", () => controller.abort());
100
- }
143
+ const combinedSignal = (0, __upyo_core.combineSignals)(controller.signal, options.signal);
101
144
  try {
102
145
  return await globalThis.fetch(url, {
103
146
  ...options,
104
147
  headers: this.headersToRecord(signedHeaders),
105
- signal
148
+ signal: combinedSignal.signal
106
149
  });
150
+ } catch (error) {
151
+ if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`SES API request timed out after ${this.config.timeout} ms.`);
152
+ throw error;
107
153
  } finally {
154
+ combinedSignal.cleanup();
108
155
  clearTimeout(timeoutId);
109
156
  }
110
157
  }
@@ -190,14 +237,39 @@ var SesHttpClient = class {
190
237
  return record;
191
238
  }
192
239
  };
240
+ function isAbortError$1(error) {
241
+ return error instanceof Error && error.name === "AbortError";
242
+ }
243
+ function sleep(milliseconds, signal) {
244
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
245
+ return new Promise((resolve, reject) => {
246
+ function abort() {
247
+ clearTimeout(timeoutId);
248
+ signal?.removeEventListener("abort", abort);
249
+ reject(createAbortError(signal));
250
+ }
251
+ const timeoutId = setTimeout(() => {
252
+ signal?.removeEventListener("abort", abort);
253
+ resolve();
254
+ }, milliseconds);
255
+ signal?.addEventListener("abort", abort, { once: true });
256
+ });
257
+ }
258
+ function createAbortError(signal) {
259
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
260
+ }
193
261
  var SesApiError = class extends Error {
194
262
  statusCode;
195
263
  errors;
196
- constructor(message, statusCode, errors) {
264
+ retryAfterMilliseconds;
265
+ attempts;
266
+ constructor(message, statusCode, errors, retryAfterMilliseconds, attempts) {
197
267
  super(message);
198
268
  this.name = "SesApiError";
199
269
  this.statusCode = statusCode;
200
270
  this.errors = errors;
271
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
272
+ this.attempts = attempts;
201
273
  }
202
274
  };
203
275
 
@@ -311,6 +383,7 @@ function formatAddress(address) {
311
383
  * @since 0.2.0
312
384
  */
313
385
  var SesTransport = class {
386
+ id = "ses";
314
387
  /** Resolved configuration with defaults applied */
315
388
  config;
316
389
  /** HTTP client for SES API requests */
@@ -360,14 +433,13 @@ var SesTransport = class {
360
433
  const messageId = this.extractMessageId(response);
361
434
  return {
362
435
  successful: true,
363
- messageId
436
+ messageId,
437
+ provider: "ses"
364
438
  };
365
439
  } catch (error) {
440
+ if (isCallerAbort(error, options?.signal)) throw error;
366
441
  const errorMessage = error instanceof Error ? error.message : String(error);
367
- return {
368
- successful: false,
369
- errorMessages: [errorMessage]
370
- };
442
+ return createSesFailure(errorMessage, error);
371
443
  }
372
444
  }
373
445
  /**
@@ -430,6 +502,22 @@ var SesTransport = class {
430
502
  return `ses-${timestamp}-${random}`;
431
503
  }
432
504
  };
505
+ function createSesFailure(message, error) {
506
+ if (error instanceof SesApiError) return (0, __upyo_core.createFailedReceipt)(message, {
507
+ provider: "ses",
508
+ statusCode: error.statusCode,
509
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
510
+ providerDetails: error.errors,
511
+ attempts: error.attempts
512
+ });
513
+ return (0, __upyo_core.createFailedReceipt)(message, { provider: "ses" });
514
+ }
515
+ function isCallerAbort(error, signal) {
516
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
517
+ }
518
+ function isAbortError(error) {
519
+ return error instanceof Error && error.name === "AbortError";
520
+ }
433
521
 
434
522
  //#endregion
435
523
  exports.SesTransport = SesTransport;
package/dist/index.d.cts CHANGED
@@ -188,7 +188,8 @@ type ResolvedSesConfig = Required<Omit<SesConfig, "configurationSetName" | "defa
188
188
  *
189
189
  * @since 0.2.0
190
190
  */
191
- declare class SesTransport implements Transport {
191
+ declare class SesTransport implements Transport<"ses"> {
192
+ readonly id = "ses";
192
193
  /** Resolved configuration with defaults applied */
193
194
  config: ResolvedSesConfig;
194
195
  /** HTTP client for SES API requests */
@@ -226,7 +227,7 @@ declare class SesTransport implements Transport {
226
227
  * @param options Optional transport options (e.g., abort signal).
227
228
  * @returns A promise that resolves to a receipt with the result.
228
229
  */
229
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
230
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"ses">>;
230
231
  /**
231
232
  * Sends multiple email messages concurrently through Amazon SES.
232
233
  *
@@ -256,7 +257,7 @@ declare class SesTransport implements Transport {
256
257
  * @param options Optional transport options (e.g., abort signal).
257
258
  * @returns Individual receipts for each message as they complete.
258
259
  */
259
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
260
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"ses">>;
260
261
  private sendConcurrent;
261
262
  private extractMessageId;
262
263
  }
package/dist/index.d.ts CHANGED
@@ -188,7 +188,8 @@ type ResolvedSesConfig = Required<Omit<SesConfig, "configurationSetName" | "defa
188
188
  *
189
189
  * @since 0.2.0
190
190
  */
191
- declare class SesTransport implements Transport {
191
+ declare class SesTransport implements Transport<"ses"> {
192
+ readonly id = "ses";
192
193
  /** Resolved configuration with defaults applied */
193
194
  config: ResolvedSesConfig;
194
195
  /** HTTP client for SES API requests */
@@ -226,7 +227,7 @@ declare class SesTransport implements Transport {
226
227
  * @param options Optional transport options (e.g., abort signal).
227
228
  * @returns A promise that resolves to a receipt with the result.
228
229
  */
229
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
230
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"ses">>;
230
231
  /**
231
232
  * Sends multiple email messages concurrently through Amazon SES.
232
233
  *
@@ -256,7 +257,7 @@ declare class SesTransport implements Transport {
256
257
  * @param options Optional transport options (e.g., abort signal).
257
258
  * @returns Individual receipts for each message as they complete.
258
259
  */
259
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
260
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"ses">>;
260
261
  private sendConcurrent;
261
262
  private extractMessageId;
262
263
  }
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { combineSignals, createFailedReceipt, parseRetryAfter } from "@upyo/core";
2
+
1
3
  //#region src/config.ts
2
4
  /**
3
5
  * Creates a resolved SES configuration with default values applied.
@@ -55,6 +57,16 @@ var SesHttpClient = class {
55
57
  signal
56
58
  });
57
59
  }
60
+ /**
61
+ * Makes an HTTP request to the SES API with retry logic.
62
+ *
63
+ * @param url The URL to make the request to.
64
+ * @param options Fetch options.
65
+ * @returns Promise that resolves to the parsed response.
66
+ * @throws {DOMException} If the caller aborts the request.
67
+ * @throws {SesApiError} If SES returns a client error or all retry attempts
68
+ * are exhausted.
69
+ */
58
70
  async makeRequest(url, options) {
59
71
  let lastError = null;
60
72
  for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
@@ -71,16 +83,30 @@ var SesHttpClient = class {
71
83
  } catch {
72
84
  errorData = { message: text || `HTTP ${response.status}` };
73
85
  }
74
- throw new SesApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.errors);
86
+ throw new SesApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.errors, parseRetryAfter(response.headers.get("Retry-After")), attempt + 1);
75
87
  } catch (error) {
76
88
  lastError = error instanceof Error ? error : new Error(String(error));
89
+ if (options.signal?.aborted) throw createAbortError(options.signal);
90
+ if (isAbortError$1(error)) throw error;
77
91
  if (error instanceof SesApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) throw error;
78
- if (attempt === this.config.retries) throw error;
92
+ if (attempt === this.config.retries) {
93
+ if (lastError instanceof SesApiError) throw lastError;
94
+ throw new SesApiError(lastError.message, void 0, void 0, void 0, attempt + 1);
95
+ }
79
96
  const delay = Math.pow(2, attempt) * 1e3;
80
- await new Promise((resolve) => setTimeout(resolve, delay));
97
+ await sleep(delay, options.signal);
81
98
  }
82
99
  throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
83
100
  }
101
+ /**
102
+ * Makes a signed fetch request to the SES API.
103
+ *
104
+ * @param url The URL to make the request to.
105
+ * @param options Fetch options.
106
+ * @returns Promise that resolves to the fetch response.
107
+ * @throws {Error} If the configured request timeout is reached.
108
+ * @throws {DOMException} If the caller aborts the request.
109
+ */
84
110
  async fetchWithAuth(url, options) {
85
111
  const credentials = await this.getCredentials();
86
112
  const headers = new Headers(options.headers);
@@ -91,19 +117,18 @@ var SesHttpClient = class {
91
117
  for (const [key, value] of Object.entries(this.config.headers)) signedHeaders.set(key, value);
92
118
  const controller = new AbortController();
93
119
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
94
- let signal = controller.signal;
95
- if (options.signal) {
96
- signal = options.signal;
97
- if (options.signal.aborted) controller.abort();
98
- else options.signal.addEventListener("abort", () => controller.abort());
99
- }
120
+ const combinedSignal = combineSignals(controller.signal, options.signal);
100
121
  try {
101
122
  return await globalThis.fetch(url, {
102
123
  ...options,
103
124
  headers: this.headersToRecord(signedHeaders),
104
- signal
125
+ signal: combinedSignal.signal
105
126
  });
127
+ } catch (error) {
128
+ if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`SES API request timed out after ${this.config.timeout} ms.`);
129
+ throw error;
106
130
  } finally {
131
+ combinedSignal.cleanup();
107
132
  clearTimeout(timeoutId);
108
133
  }
109
134
  }
@@ -189,14 +214,39 @@ var SesHttpClient = class {
189
214
  return record;
190
215
  }
191
216
  };
217
+ function isAbortError$1(error) {
218
+ return error instanceof Error && error.name === "AbortError";
219
+ }
220
+ function sleep(milliseconds, signal) {
221
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
222
+ return new Promise((resolve, reject) => {
223
+ function abort() {
224
+ clearTimeout(timeoutId);
225
+ signal?.removeEventListener("abort", abort);
226
+ reject(createAbortError(signal));
227
+ }
228
+ const timeoutId = setTimeout(() => {
229
+ signal?.removeEventListener("abort", abort);
230
+ resolve();
231
+ }, milliseconds);
232
+ signal?.addEventListener("abort", abort, { once: true });
233
+ });
234
+ }
235
+ function createAbortError(signal) {
236
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
237
+ }
192
238
  var SesApiError = class extends Error {
193
239
  statusCode;
194
240
  errors;
195
- constructor(message, statusCode, errors) {
241
+ retryAfterMilliseconds;
242
+ attempts;
243
+ constructor(message, statusCode, errors, retryAfterMilliseconds, attempts) {
196
244
  super(message);
197
245
  this.name = "SesApiError";
198
246
  this.statusCode = statusCode;
199
247
  this.errors = errors;
248
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
249
+ this.attempts = attempts;
200
250
  }
201
251
  };
202
252
 
@@ -310,6 +360,7 @@ function formatAddress(address) {
310
360
  * @since 0.2.0
311
361
  */
312
362
  var SesTransport = class {
363
+ id = "ses";
313
364
  /** Resolved configuration with defaults applied */
314
365
  config;
315
366
  /** HTTP client for SES API requests */
@@ -359,14 +410,13 @@ var SesTransport = class {
359
410
  const messageId = this.extractMessageId(response);
360
411
  return {
361
412
  successful: true,
362
- messageId
413
+ messageId,
414
+ provider: "ses"
363
415
  };
364
416
  } catch (error) {
417
+ if (isCallerAbort(error, options?.signal)) throw error;
365
418
  const errorMessage = error instanceof Error ? error.message : String(error);
366
- return {
367
- successful: false,
368
- errorMessages: [errorMessage]
369
- };
419
+ return createSesFailure(errorMessage, error);
370
420
  }
371
421
  }
372
422
  /**
@@ -429,6 +479,22 @@ var SesTransport = class {
429
479
  return `ses-${timestamp}-${random}`;
430
480
  }
431
481
  };
482
+ function createSesFailure(message, error) {
483
+ if (error instanceof SesApiError) return createFailedReceipt(message, {
484
+ provider: "ses",
485
+ statusCode: error.statusCode,
486
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
487
+ providerDetails: error.errors,
488
+ attempts: error.attempts
489
+ });
490
+ return createFailedReceipt(message, { provider: "ses" });
491
+ }
492
+ function isCallerAbort(error, signal) {
493
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
494
+ }
495
+ function isAbortError(error) {
496
+ return error instanceof Error && error.name === "AbortError";
497
+ }
432
498
 
433
499
  //#endregion
434
500
  export { SesTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/ses",
3
- "version": "0.5.0-dev.86",
3
+ "version": "0.5.0",
4
4
  "description": "Amazon SES transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -55,18 +55,13 @@
55
55
  },
56
56
  "sideEffects": false,
57
57
  "peerDependencies": {
58
- "@upyo/core": "0.5.0-dev.86+2a12a704"
58
+ "@upyo/core": "0.5.0"
59
59
  },
60
60
  "devDependencies": {
61
- "@dotenvx/dotenvx": "^1.47.3",
62
61
  "tsdown": "^0.12.7",
63
62
  "typescript": "5.8.3"
64
63
  },
65
64
  "scripts": {
66
- "build": "tsdown",
67
- "prepublish": "tsdown",
68
- "test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
69
- "test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
70
- "test:deno": "deno test --allow-env --allow-net --env-file=.env"
65
+ "prepublish": "mise run --no-deps :build"
71
66
  }
72
67
  }