@upyo/ses 0.5.0-dev.158 → 0.5.0-dev.168

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
@@ -80,6 +80,16 @@ var SesHttpClient = class {
80
80
  signal
81
81
  });
82
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
+ */
83
93
  async makeRequest(url, options) {
84
94
  let lastError = null;
85
95
  for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
@@ -106,10 +116,19 @@ var SesHttpClient = class {
106
116
  throw new SesApiError(lastError.message, void 0, void 0, void 0, attempt + 1);
107
117
  }
108
118
  const delay = Math.pow(2, attempt) * 1e3;
109
- await new Promise((resolve) => setTimeout(resolve, delay));
119
+ await sleep(delay, options.signal);
110
120
  }
111
121
  throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
112
122
  }
123
+ /**
124
+ * Makes a signed fetch request to the SES API.
125
+ *
126
+ * @param url The URL to make the request to.
127
+ * @param options Fetch options.
128
+ * @returns Promise that resolves to the fetch response.
129
+ * @throws {Error} If the configured request timeout is reached.
130
+ * @throws {DOMException} If the caller aborts the request.
131
+ */
113
132
  async fetchWithAuth(url, options) {
114
133
  const credentials = await this.getCredentials();
115
134
  const headers = new Headers(options.headers);
@@ -120,17 +139,18 @@ var SesHttpClient = class {
120
139
  for (const [key, value] of Object.entries(this.config.headers)) signedHeaders.set(key, value);
121
140
  const controller = new AbortController();
122
141
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
123
- const signal = options.signal == null ? controller.signal : AbortSignal.any([controller.signal, options.signal]);
142
+ const combinedSignal = combineSignals(controller.signal, options.signal);
124
143
  try {
125
144
  return await globalThis.fetch(url, {
126
145
  ...options,
127
146
  headers: this.headersToRecord(signedHeaders),
128
- signal
147
+ signal: combinedSignal.signal
129
148
  });
130
149
  } catch (error) {
131
150
  if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`SES API request timed out after ${this.config.timeout} ms.`);
132
151
  throw error;
133
152
  } finally {
153
+ combinedSignal.cleanup();
134
154
  clearTimeout(timeoutId);
135
155
  }
136
156
  }
@@ -219,6 +239,57 @@ var SesHttpClient = class {
219
239
  function isAbortError$1(error) {
220
240
  return error instanceof Error && error.name === "AbortError";
221
241
  }
242
+ function sleep(milliseconds, signal) {
243
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
244
+ return new Promise((resolve, reject) => {
245
+ function abort() {
246
+ clearTimeout(timeoutId);
247
+ signal?.removeEventListener("abort", abort);
248
+ reject(createAbortError(signal));
249
+ }
250
+ const timeoutId = setTimeout(() => {
251
+ signal?.removeEventListener("abort", abort);
252
+ resolve();
253
+ }, milliseconds);
254
+ signal?.addEventListener("abort", abort, { once: true });
255
+ });
256
+ }
257
+ function createAbortError(signal) {
258
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
259
+ }
260
+ function combineSignals(timeoutSignal, externalSignal) {
261
+ if (externalSignal == null) return {
262
+ signal: timeoutSignal,
263
+ cleanup: () => {}
264
+ };
265
+ if (typeof AbortSignal.any === "function") return {
266
+ signal: AbortSignal.any([timeoutSignal, externalSignal]),
267
+ cleanup: () => {}
268
+ };
269
+ const controller = new AbortController();
270
+ const cleanup = () => {
271
+ timeoutSignal.removeEventListener("abort", abortFromTimeout);
272
+ externalSignal.removeEventListener("abort", abortFromExternal);
273
+ };
274
+ const abortFromTimeout = () => {
275
+ cleanup();
276
+ controller.abort(timeoutSignal.reason);
277
+ };
278
+ const abortFromExternal = () => {
279
+ cleanup();
280
+ controller.abort(externalSignal.reason);
281
+ };
282
+ if (timeoutSignal.aborted) controller.abort(timeoutSignal.reason);
283
+ else if (externalSignal.aborted) controller.abort(externalSignal.reason);
284
+ else {
285
+ timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
286
+ externalSignal.addEventListener("abort", abortFromExternal, { once: true });
287
+ }
288
+ return {
289
+ signal: controller.signal,
290
+ cleanup
291
+ };
292
+ }
222
293
  var SesApiError = class extends Error {
223
294
  statusCode;
224
295
  errors;
@@ -398,7 +469,7 @@ var SesTransport = class {
398
469
  provider: "ses"
399
470
  };
400
471
  } catch (error) {
401
- if (isAbortError(error) && options?.signal?.aborted) throw error;
472
+ if (isCallerAbort(error, options?.signal)) throw error;
402
473
  const errorMessage = error instanceof Error ? error.message : String(error);
403
474
  return createSesFailure(errorMessage, error);
404
475
  }
@@ -473,6 +544,9 @@ function createSesFailure(message, error) {
473
544
  });
474
545
  return (0, __upyo_core.createFailedReceipt)(message, { provider: "ses" });
475
546
  }
547
+ function isCallerAbort(error, signal) {
548
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
549
+ }
476
550
  function isAbortError(error) {
477
551
  return error instanceof Error && error.name === "AbortError";
478
552
  }
package/dist/index.js CHANGED
@@ -57,6 +57,16 @@ var SesHttpClient = class {
57
57
  signal
58
58
  });
59
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
+ */
60
70
  async makeRequest(url, options) {
61
71
  let lastError = null;
62
72
  for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
@@ -83,10 +93,19 @@ var SesHttpClient = class {
83
93
  throw new SesApiError(lastError.message, void 0, void 0, void 0, attempt + 1);
84
94
  }
85
95
  const delay = Math.pow(2, attempt) * 1e3;
86
- await new Promise((resolve) => setTimeout(resolve, delay));
96
+ await sleep(delay, options.signal);
87
97
  }
88
98
  throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
89
99
  }
100
+ /**
101
+ * Makes a signed fetch request to the SES API.
102
+ *
103
+ * @param url The URL to make the request to.
104
+ * @param options Fetch options.
105
+ * @returns Promise that resolves to the fetch response.
106
+ * @throws {Error} If the configured request timeout is reached.
107
+ * @throws {DOMException} If the caller aborts the request.
108
+ */
90
109
  async fetchWithAuth(url, options) {
91
110
  const credentials = await this.getCredentials();
92
111
  const headers = new Headers(options.headers);
@@ -97,17 +116,18 @@ var SesHttpClient = class {
97
116
  for (const [key, value] of Object.entries(this.config.headers)) signedHeaders.set(key, value);
98
117
  const controller = new AbortController();
99
118
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
100
- const signal = options.signal == null ? controller.signal : AbortSignal.any([controller.signal, options.signal]);
119
+ const combinedSignal = combineSignals(controller.signal, options.signal);
101
120
  try {
102
121
  return await globalThis.fetch(url, {
103
122
  ...options,
104
123
  headers: this.headersToRecord(signedHeaders),
105
- signal
124
+ signal: combinedSignal.signal
106
125
  });
107
126
  } catch (error) {
108
127
  if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`SES API request timed out after ${this.config.timeout} ms.`);
109
128
  throw error;
110
129
  } finally {
130
+ combinedSignal.cleanup();
111
131
  clearTimeout(timeoutId);
112
132
  }
113
133
  }
@@ -196,6 +216,57 @@ var SesHttpClient = class {
196
216
  function isAbortError$1(error) {
197
217
  return error instanceof Error && error.name === "AbortError";
198
218
  }
219
+ function sleep(milliseconds, signal) {
220
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
221
+ return new Promise((resolve, reject) => {
222
+ function abort() {
223
+ clearTimeout(timeoutId);
224
+ signal?.removeEventListener("abort", abort);
225
+ reject(createAbortError(signal));
226
+ }
227
+ const timeoutId = setTimeout(() => {
228
+ signal?.removeEventListener("abort", abort);
229
+ resolve();
230
+ }, milliseconds);
231
+ signal?.addEventListener("abort", abort, { once: true });
232
+ });
233
+ }
234
+ function createAbortError(signal) {
235
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
236
+ }
237
+ function combineSignals(timeoutSignal, externalSignal) {
238
+ if (externalSignal == null) return {
239
+ signal: timeoutSignal,
240
+ cleanup: () => {}
241
+ };
242
+ if (typeof AbortSignal.any === "function") return {
243
+ signal: AbortSignal.any([timeoutSignal, externalSignal]),
244
+ cleanup: () => {}
245
+ };
246
+ const controller = new AbortController();
247
+ const cleanup = () => {
248
+ timeoutSignal.removeEventListener("abort", abortFromTimeout);
249
+ externalSignal.removeEventListener("abort", abortFromExternal);
250
+ };
251
+ const abortFromTimeout = () => {
252
+ cleanup();
253
+ controller.abort(timeoutSignal.reason);
254
+ };
255
+ const abortFromExternal = () => {
256
+ cleanup();
257
+ controller.abort(externalSignal.reason);
258
+ };
259
+ if (timeoutSignal.aborted) controller.abort(timeoutSignal.reason);
260
+ else if (externalSignal.aborted) controller.abort(externalSignal.reason);
261
+ else {
262
+ timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
263
+ externalSignal.addEventListener("abort", abortFromExternal, { once: true });
264
+ }
265
+ return {
266
+ signal: controller.signal,
267
+ cleanup
268
+ };
269
+ }
199
270
  var SesApiError = class extends Error {
200
271
  statusCode;
201
272
  errors;
@@ -375,7 +446,7 @@ var SesTransport = class {
375
446
  provider: "ses"
376
447
  };
377
448
  } catch (error) {
378
- if (isAbortError(error) && options?.signal?.aborted) throw error;
449
+ if (isCallerAbort(error, options?.signal)) throw error;
379
450
  const errorMessage = error instanceof Error ? error.message : String(error);
380
451
  return createSesFailure(errorMessage, error);
381
452
  }
@@ -450,6 +521,9 @@ function createSesFailure(message, error) {
450
521
  });
451
522
  return createFailedReceipt(message, { provider: "ses" });
452
523
  }
524
+ function isCallerAbort(error, signal) {
525
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
526
+ }
453
527
  function isAbortError(error) {
454
528
  return error instanceof Error && error.name === "AbortError";
455
529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/ses",
3
- "version": "0.5.0-dev.158",
3
+ "version": "0.5.0-dev.168",
4
4
  "description": "Amazon SES transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "sideEffects": false,
57
57
  "peerDependencies": {
58
- "@upyo/core": "0.5.0-dev.158+468b767c"
58
+ "@upyo/core": "0.5.0-dev.168+1e808a3a"
59
59
  },
60
60
  "devDependencies": {
61
61
  "tsdown": "^0.12.7",