@upyo/resend 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
  /**
@@ -25,14 +49,37 @@ function createResendConfig(config) {
25
49
  //#endregion
26
50
  //#region src/http-client.ts
27
51
  /**
28
- * Resend API error class for handling API-specific errors.
52
+ * Error thrown when a Resend API request fails.
53
+ *
54
+ * @since 0.5.0
29
55
  */
30
56
  var ResendApiError = class extends Error {
57
+ /**
58
+ * HTTP status code returned by Resend, if the request reached the API.
59
+ */
31
60
  statusCode;
32
- constructor(message, statusCode) {
61
+ /**
62
+ * Retry delay from Resend's `Retry-After` response header.
63
+ */
64
+ retryAfterMilliseconds;
65
+ /**
66
+ * Number of attempts made before this error was produced.
67
+ */
68
+ attempts;
69
+ /**
70
+ * Creates a Resend API error.
71
+ *
72
+ * @param message Error message.
73
+ * @param statusCode HTTP status code returned by Resend.
74
+ * @param retryAfterMilliseconds Retry delay from the response.
75
+ * @param attempts Number of attempts made before this error.
76
+ */
77
+ constructor(message, statusCode, retryAfterMilliseconds, attempts) {
33
78
  super(message);
34
79
  this.name = "ResendApiError";
35
80
  this.statusCode = statusCode;
81
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
82
+ this.attempts = attempts;
36
83
  }
37
84
  };
38
85
  /**
@@ -102,7 +149,10 @@ var ResendHttpClient = class {
102
149
  const errorBody = JSON.parse(text);
103
150
  errorMessage = errorBody.message;
104
151
  } catch {}
105
- throw new ResendApiError(errorMessage || text || `HTTP ${response.status}`, response.status);
152
+ const parsedErrorMessage = errorMessage === "" ? void 0 : errorMessage;
153
+ const responseMessage = truncateErrorBody(text);
154
+ const fallbackMessage = responseMessage === "" ? void 0 : responseMessage;
155
+ throw new ResendApiError(parsedErrorMessage ?? fallbackMessage ?? `HTTP ${response.status}`, response.status, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")), attempt + 1);
106
156
  }
107
157
  try {
108
158
  return JSON.parse(text);
@@ -110,10 +160,14 @@ var ResendHttpClient = class {
110
160
  throw new Error(`Invalid JSON response from Resend API: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
111
161
  }
112
162
  } catch (error) {
163
+ if (isCallerAbort$1(error, options.signal)) throw error;
113
164
  lastError = error instanceof Error ? error : new Error(String(error));
114
- if (error instanceof ResendApiError && error.statusCode >= 400 && error.statusCode < 500) throw error;
165
+ if (error instanceof ResendApiError && error.statusCode !== void 0 && error.statusCode >= 400 && error.statusCode < 500) throw error;
115
166
  if (error instanceof Error && error.name === "AbortError") throw error;
116
- if (attempt === this.config.retries) throw lastError;
167
+ if (attempt === this.config.retries) {
168
+ if (lastError instanceof ResendApiError) throw lastError;
169
+ throw new ResendApiError(lastError.message, void 0, void 0, attempt + 1);
170
+ }
117
171
  const backoffMs = Math.min(1e3 * Math.pow(2, attempt), 1e4);
118
172
  await new Promise((resolve) => setTimeout(resolve, backoffMs));
119
173
  }
@@ -132,26 +186,32 @@ var ResendHttpClient = class {
132
186
  for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
133
187
  const controller = new AbortController();
134
188
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
135
- let signal;
136
- if (options.signal) {
137
- const combinedController = new AbortController();
138
- const onAbort = () => combinedController.abort();
139
- options.signal.addEventListener("abort", onAbort, { once: true });
140
- controller.signal.addEventListener("abort", onAbort, { once: true });
141
- signal = combinedController.signal;
142
- } else signal = controller.signal;
189
+ const combinedSignal = (0, __upyo_core.combineSignals)(controller.signal, options.signal);
143
190
  try {
144
191
  const response = await fetch(url, {
145
192
  ...options,
146
193
  headers,
147
- signal
194
+ signal: combinedSignal.signal
148
195
  });
149
196
  return response;
197
+ } catch (error) {
198
+ if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`Resend API request timed out after ${this.config.timeout} ms.`);
199
+ throw error;
150
200
  } finally {
201
+ combinedSignal.cleanup();
151
202
  clearTimeout(timeoutId);
152
203
  }
153
204
  }
154
205
  };
206
+ function isAbortError$1(error) {
207
+ return error instanceof Error && error.name === "AbortError";
208
+ }
209
+ function isCallerAbort$1(error, signal) {
210
+ return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
211
+ }
212
+ function truncateErrorBody(text) {
213
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
214
+ }
155
215
 
156
216
  //#endregion
157
217
  //#region src/message-converter.ts
@@ -339,6 +399,7 @@ function generateIdempotencyKey() {
339
399
  * ```
340
400
  */
341
401
  var ResendTransport = class {
402
+ id = "resend";
342
403
  /**
343
404
  * The resolved Resend configuration used by this transport.
344
405
  */
@@ -396,14 +457,13 @@ var ResendTransport = class {
396
457
  const response = await this.httpClient.sendMessage(emailData, options?.signal, idempotencyKey);
397
458
  return {
398
459
  successful: true,
399
- messageId: response.id
460
+ messageId: response.id,
461
+ provider: "resend"
400
462
  };
401
463
  } catch (error) {
464
+ if (isCallerAbort(error, options?.signal)) throw error;
402
465
  const errorMessage = error instanceof Error ? error.message : String(error);
403
- return {
404
- successful: false,
405
- errorMessages: [errorMessage]
406
- };
466
+ return createResendFailure(errorMessage, error);
407
467
  }
408
468
  }
409
469
  /**
@@ -512,14 +572,13 @@ var ResendTransport = class {
512
572
  const response = await this.httpClient.sendBatch(batchData, options?.signal, idempotencyKey);
513
573
  for (const result of response.data) yield {
514
574
  successful: true,
515
- messageId: result.id
575
+ messageId: result.id,
576
+ provider: "resend"
516
577
  };
517
578
  } catch (error) {
579
+ if (isCallerAbort(error, options?.signal)) throw error;
518
580
  const errorMessage = error instanceof Error ? error.message : String(error);
519
- for (let i = 0; i < messages.length; i++) yield {
520
- successful: false,
521
- errorMessages: [errorMessage]
522
- };
581
+ for (let i = 0; i < messages.length; i++) yield createResendFailure(errorMessage, error);
523
582
  }
524
583
  }
525
584
  /**
@@ -549,6 +608,21 @@ var ResendTransport = class {
549
608
  return chunks;
550
609
  }
551
610
  };
611
+ function createResendFailure(message, error) {
612
+ if (error instanceof ResendApiError) return (0, __upyo_core.createFailedReceipt)(message, {
613
+ provider: "resend",
614
+ statusCode: error.statusCode,
615
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
616
+ attempts: error.attempts
617
+ });
618
+ return (0, __upyo_core.createFailedReceipt)(message, { provider: "resend" });
619
+ }
620
+ function isAbortError(error) {
621
+ return error instanceof Error && error.name === "AbortError";
622
+ }
623
+ function isCallerAbort(error, signal) {
624
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
625
+ }
552
626
 
553
627
  //#endregion
554
628
  exports.ResendTransport = ResendTransport;
package/dist/index.d.cts CHANGED
@@ -99,7 +99,8 @@ type ResolvedResendConfig = Required<ResendConfig>;
99
99
  * }
100
100
  * ```
101
101
  */
102
- declare class ResendTransport implements Transport {
102
+ declare class ResendTransport implements Transport<"resend"> {
103
+ readonly id = "resend";
103
104
  /**
104
105
  * The resolved Resend configuration used by this transport.
105
106
  */
@@ -145,7 +146,7 @@ declare class ResendTransport implements Transport {
145
146
  * @returns A promise that resolves to a receipt indicating success or
146
147
  * failure.
147
148
  */
148
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
149
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"resend">>;
149
150
  /**
150
151
  * Sends multiple email messages efficiently via Resend API.
151
152
  *
@@ -200,7 +201,7 @@ declare class ResendTransport implements Transport {
200
201
  * cancellation.
201
202
  * @returns An async iterable of receipts, one for each message.
202
203
  */
203
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
204
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"resend">>;
204
205
  /**
205
206
  * Optimized batch sending that chooses the best strategy based on message features.
206
207
  *
@@ -274,11 +275,32 @@ interface ResendError {
274
275
  name?: string;
275
276
  }
276
277
  /**
277
- * Resend API error class for handling API-specific errors.
278
+ * Error thrown when a Resend API request fails.
279
+ *
280
+ * @since 0.5.0
278
281
  */
279
282
  declare class ResendApiError extends Error {
280
- readonly statusCode: number;
281
- constructor(message: string, statusCode: number);
283
+ /**
284
+ * HTTP status code returned by Resend, if the request reached the API.
285
+ */
286
+ readonly statusCode?: number;
287
+ /**
288
+ * Retry delay from Resend's `Retry-After` response header.
289
+ */
290
+ readonly retryAfterMilliseconds?: number;
291
+ /**
292
+ * Number of attempts made before this error was produced.
293
+ */
294
+ readonly attempts?: number;
295
+ /**
296
+ * Creates a Resend API error.
297
+ *
298
+ * @param message Error message.
299
+ * @param statusCode HTTP status code returned by Resend.
300
+ * @param retryAfterMilliseconds Retry delay from the response.
301
+ * @param attempts Number of attempts made before this error.
302
+ */
303
+ constructor(message: string, statusCode?: number, retryAfterMilliseconds?: number, attempts?: number);
282
304
  }
283
305
  /**
284
306
  * HTTP client wrapper for Resend API requests.
package/dist/index.d.ts CHANGED
@@ -99,7 +99,8 @@ type ResolvedResendConfig = Required<ResendConfig>;
99
99
  * }
100
100
  * ```
101
101
  */
102
- declare class ResendTransport implements Transport {
102
+ declare class ResendTransport implements Transport<"resend"> {
103
+ readonly id = "resend";
103
104
  /**
104
105
  * The resolved Resend configuration used by this transport.
105
106
  */
@@ -145,7 +146,7 @@ declare class ResendTransport implements Transport {
145
146
  * @returns A promise that resolves to a receipt indicating success or
146
147
  * failure.
147
148
  */
148
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
149
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"resend">>;
149
150
  /**
150
151
  * Sends multiple email messages efficiently via Resend API.
151
152
  *
@@ -200,7 +201,7 @@ declare class ResendTransport implements Transport {
200
201
  * cancellation.
201
202
  * @returns An async iterable of receipts, one for each message.
202
203
  */
203
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
204
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"resend">>;
204
205
  /**
205
206
  * Optimized batch sending that chooses the best strategy based on message features.
206
207
  *
@@ -274,11 +275,32 @@ interface ResendError {
274
275
  name?: string;
275
276
  }
276
277
  /**
277
- * Resend API error class for handling API-specific errors.
278
+ * Error thrown when a Resend API request fails.
279
+ *
280
+ * @since 0.5.0
278
281
  */
279
282
  declare class ResendApiError extends Error {
280
- readonly statusCode: number;
281
- constructor(message: string, statusCode: number);
283
+ /**
284
+ * HTTP status code returned by Resend, if the request reached the API.
285
+ */
286
+ readonly statusCode?: number;
287
+ /**
288
+ * Retry delay from Resend's `Retry-After` response header.
289
+ */
290
+ readonly retryAfterMilliseconds?: number;
291
+ /**
292
+ * Number of attempts made before this error was produced.
293
+ */
294
+ readonly attempts?: number;
295
+ /**
296
+ * Creates a Resend API error.
297
+ *
298
+ * @param message Error message.
299
+ * @param statusCode HTTP status code returned by Resend.
300
+ * @param retryAfterMilliseconds Retry delay from the response.
301
+ * @param attempts Number of attempts made before this error.
302
+ */
303
+ constructor(message: string, statusCode?: number, retryAfterMilliseconds?: number, attempts?: number);
282
304
  }
283
305
  /**
284
306
  * HTTP client wrapper for Resend API requests.
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 Resend configuration by applying default values to optional fields.
@@ -24,14 +26,37 @@ function createResendConfig(config) {
24
26
  //#endregion
25
27
  //#region src/http-client.ts
26
28
  /**
27
- * Resend API error class for handling API-specific errors.
29
+ * Error thrown when a Resend API request fails.
30
+ *
31
+ * @since 0.5.0
28
32
  */
29
33
  var ResendApiError = class extends Error {
34
+ /**
35
+ * HTTP status code returned by Resend, if the request reached the API.
36
+ */
30
37
  statusCode;
31
- constructor(message, statusCode) {
38
+ /**
39
+ * Retry delay from Resend's `Retry-After` response header.
40
+ */
41
+ retryAfterMilliseconds;
42
+ /**
43
+ * Number of attempts made before this error was produced.
44
+ */
45
+ attempts;
46
+ /**
47
+ * Creates a Resend API error.
48
+ *
49
+ * @param message Error message.
50
+ * @param statusCode HTTP status code returned by Resend.
51
+ * @param retryAfterMilliseconds Retry delay from the response.
52
+ * @param attempts Number of attempts made before this error.
53
+ */
54
+ constructor(message, statusCode, retryAfterMilliseconds, attempts) {
32
55
  super(message);
33
56
  this.name = "ResendApiError";
34
57
  this.statusCode = statusCode;
58
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
59
+ this.attempts = attempts;
35
60
  }
36
61
  };
37
62
  /**
@@ -101,7 +126,10 @@ var ResendHttpClient = class {
101
126
  const errorBody = JSON.parse(text);
102
127
  errorMessage = errorBody.message;
103
128
  } catch {}
104
- throw new ResendApiError(errorMessage || text || `HTTP ${response.status}`, response.status);
129
+ const parsedErrorMessage = errorMessage === "" ? void 0 : errorMessage;
130
+ const responseMessage = truncateErrorBody(text);
131
+ const fallbackMessage = responseMessage === "" ? void 0 : responseMessage;
132
+ throw new ResendApiError(parsedErrorMessage ?? fallbackMessage ?? `HTTP ${response.status}`, response.status, parseRetryAfter(response.headers.get("Retry-After")), attempt + 1);
105
133
  }
106
134
  try {
107
135
  return JSON.parse(text);
@@ -109,10 +137,14 @@ var ResendHttpClient = class {
109
137
  throw new Error(`Invalid JSON response from Resend API: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
110
138
  }
111
139
  } catch (error) {
140
+ if (isCallerAbort$1(error, options.signal)) throw error;
112
141
  lastError = error instanceof Error ? error : new Error(String(error));
113
- if (error instanceof ResendApiError && error.statusCode >= 400 && error.statusCode < 500) throw error;
142
+ if (error instanceof ResendApiError && error.statusCode !== void 0 && error.statusCode >= 400 && error.statusCode < 500) throw error;
114
143
  if (error instanceof Error && error.name === "AbortError") throw error;
115
- if (attempt === this.config.retries) throw lastError;
144
+ if (attempt === this.config.retries) {
145
+ if (lastError instanceof ResendApiError) throw lastError;
146
+ throw new ResendApiError(lastError.message, void 0, void 0, attempt + 1);
147
+ }
116
148
  const backoffMs = Math.min(1e3 * Math.pow(2, attempt), 1e4);
117
149
  await new Promise((resolve) => setTimeout(resolve, backoffMs));
118
150
  }
@@ -131,26 +163,32 @@ var ResendHttpClient = class {
131
163
  for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
132
164
  const controller = new AbortController();
133
165
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
134
- let signal;
135
- if (options.signal) {
136
- const combinedController = new AbortController();
137
- const onAbort = () => combinedController.abort();
138
- options.signal.addEventListener("abort", onAbort, { once: true });
139
- controller.signal.addEventListener("abort", onAbort, { once: true });
140
- signal = combinedController.signal;
141
- } else signal = controller.signal;
166
+ const combinedSignal = combineSignals(controller.signal, options.signal);
142
167
  try {
143
168
  const response = await fetch(url, {
144
169
  ...options,
145
170
  headers,
146
- signal
171
+ signal: combinedSignal.signal
147
172
  });
148
173
  return response;
174
+ } catch (error) {
175
+ if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`Resend API request timed out after ${this.config.timeout} ms.`);
176
+ throw error;
149
177
  } finally {
178
+ combinedSignal.cleanup();
150
179
  clearTimeout(timeoutId);
151
180
  }
152
181
  }
153
182
  };
183
+ function isAbortError$1(error) {
184
+ return error instanceof Error && error.name === "AbortError";
185
+ }
186
+ function isCallerAbort$1(error, signal) {
187
+ return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
188
+ }
189
+ function truncateErrorBody(text) {
190
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
191
+ }
154
192
 
155
193
  //#endregion
156
194
  //#region src/message-converter.ts
@@ -338,6 +376,7 @@ function generateIdempotencyKey() {
338
376
  * ```
339
377
  */
340
378
  var ResendTransport = class {
379
+ id = "resend";
341
380
  /**
342
381
  * The resolved Resend configuration used by this transport.
343
382
  */
@@ -395,14 +434,13 @@ var ResendTransport = class {
395
434
  const response = await this.httpClient.sendMessage(emailData, options?.signal, idempotencyKey);
396
435
  return {
397
436
  successful: true,
398
- messageId: response.id
437
+ messageId: response.id,
438
+ provider: "resend"
399
439
  };
400
440
  } catch (error) {
441
+ if (isCallerAbort(error, options?.signal)) throw error;
401
442
  const errorMessage = error instanceof Error ? error.message : String(error);
402
- return {
403
- successful: false,
404
- errorMessages: [errorMessage]
405
- };
443
+ return createResendFailure(errorMessage, error);
406
444
  }
407
445
  }
408
446
  /**
@@ -511,14 +549,13 @@ var ResendTransport = class {
511
549
  const response = await this.httpClient.sendBatch(batchData, options?.signal, idempotencyKey);
512
550
  for (const result of response.data) yield {
513
551
  successful: true,
514
- messageId: result.id
552
+ messageId: result.id,
553
+ provider: "resend"
515
554
  };
516
555
  } catch (error) {
556
+ if (isCallerAbort(error, options?.signal)) throw error;
517
557
  const errorMessage = error instanceof Error ? error.message : String(error);
518
- for (let i = 0; i < messages.length; i++) yield {
519
- successful: false,
520
- errorMessages: [errorMessage]
521
- };
558
+ for (let i = 0; i < messages.length; i++) yield createResendFailure(errorMessage, error);
522
559
  }
523
560
  }
524
561
  /**
@@ -548,6 +585,21 @@ var ResendTransport = class {
548
585
  return chunks;
549
586
  }
550
587
  };
588
+ function createResendFailure(message, error) {
589
+ if (error instanceof ResendApiError) return createFailedReceipt(message, {
590
+ provider: "resend",
591
+ statusCode: error.statusCode,
592
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
593
+ attempts: error.attempts
594
+ });
595
+ return createFailedReceipt(message, { provider: "resend" });
596
+ }
597
+ function isAbortError(error) {
598
+ return error instanceof Error && error.name === "AbortError";
599
+ }
600
+ function isCallerAbort(error, signal) {
601
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
602
+ }
551
603
 
552
604
  //#endregion
553
605
  export { ResendTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/resend",
3
- "version": "0.5.0-dev.86",
3
+ "version": "0.5.0",
4
4
  "description": "Resend transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -53,18 +53,13 @@
53
53
  },
54
54
  "sideEffects": false,
55
55
  "peerDependencies": {
56
- "@upyo/core": "0.5.0-dev.86+2a12a704"
56
+ "@upyo/core": "0.5.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@dotenvx/dotenvx": "^1.47.3",
60
59
  "tsdown": "^0.12.7",
61
60
  "typescript": "5.8.3"
62
61
  },
63
62
  "scripts": {
64
- "build": "tsdown",
65
- "prepublish": "tsdown",
66
- "test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
67
- "test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
68
- "test:deno": "deno test --allow-env --allow-net --env-file=.env"
63
+ "prepublish": "mise run --no-deps :build"
69
64
  }
70
65
  }