@upyo/plunk 0.5.0-dev.136 → 0.5.0-dev.154

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,6 +49,40 @@ function createPlunkConfig(config) {
25
49
  //#endregion
26
50
  //#region src/http-client.ts
27
51
  /**
52
+ * Error thrown when a Plunk API request fails.
53
+ *
54
+ * @since 0.5.0
55
+ */
56
+ var PlunkApiError = class extends Error {
57
+ /**
58
+ * HTTP status code returned by Plunk, if the request reached the API.
59
+ */
60
+ statusCode;
61
+ /**
62
+ * Retry delay from Plunk'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 Plunk API error.
71
+ *
72
+ * @param message Error message.
73
+ * @param statusCode HTTP status code returned by Plunk.
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) {
78
+ super(message);
79
+ this.name = "PlunkApiError";
80
+ this.statusCode = statusCode;
81
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
82
+ this.attempts = attempts;
83
+ }
84
+ };
85
+ /**
28
86
  * HTTP client wrapper for Plunk API requests.
29
87
  *
30
88
  * This class handles authentication, request formatting, error handling,
@@ -61,17 +119,16 @@ var PlunkHttpClient = class {
61
119
  return await this.parseResponse(response);
62
120
  } catch (error) {
63
121
  lastError = error instanceof Error ? error : new Error(String(error));
64
- if (error instanceof Error) {
65
- if (error.name === "AbortError") throw error;
66
- if (error.message.includes("status: 4")) throw this.createPlunkError(error.message, 400);
67
- }
122
+ if (error instanceof Error && error.name === "AbortError") throw error;
123
+ if (error instanceof PlunkApiError && error.statusCode !== void 0 && error.statusCode >= 400 && error.statusCode < 500) throw new PlunkApiError(error.message, error.statusCode, error.retryAfterMilliseconds, attempt + 1);
68
124
  if (attempt === this.config.retries) break;
69
125
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
70
126
  await this.sleep(delay);
71
127
  }
72
128
  }
73
129
  const errorMessage = lastError?.message ?? "Unknown error occurred";
74
- throw this.createPlunkError(errorMessage);
130
+ if (lastError instanceof PlunkApiError) throw new PlunkApiError(lastError.message, lastError.statusCode, lastError.retryAfterMilliseconds, this.config.retries + 1);
131
+ throw new PlunkApiError(errorMessage, void 0, void 0, this.config.retries + 1);
75
132
  }
76
133
  /**
77
134
  * Makes an HTTP request to the Plunk API.
@@ -87,13 +144,23 @@ var PlunkHttpClient = class {
87
144
  "Content-Type": "application/json",
88
145
  ...this.config.headers
89
146
  };
90
- const response = await fetch(url, {
91
- method: "POST",
92
- headers,
93
- body: JSON.stringify(emailData),
94
- signal,
95
- ...this.config.timeout > 0 && typeof globalThis.AbortSignal?.timeout === "function" ? { signal: AbortSignal.any([signal, AbortSignal.timeout(this.config.timeout)].filter(Boolean)) } : {}
96
- });
147
+ const timeoutController = new AbortController();
148
+ const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
149
+ const requestSignal = signal == null ? timeoutController.signal : AbortSignal.any([timeoutController.signal, signal]);
150
+ let response;
151
+ try {
152
+ response = await fetch(url, {
153
+ method: "POST",
154
+ headers,
155
+ body: JSON.stringify(emailData),
156
+ signal: requestSignal
157
+ });
158
+ } catch (error) {
159
+ if (isAbortError$1(error) && timeoutController.signal.aborted && !signal?.aborted) throw new Error(`Plunk API request timed out after ${this.config.timeout} ms.`);
160
+ throw error;
161
+ } finally {
162
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
163
+ }
97
164
  if (!response.ok) {
98
165
  let errorBody;
99
166
  try {
@@ -101,7 +168,7 @@ var PlunkHttpClient = class {
101
168
  } catch {
102
169
  errorBody = "Failed to read error response";
103
170
  }
104
- throw new Error(`HTTP ${response.status}: ${response.statusText}. ${errorBody}`);
171
+ throw new PlunkApiError(`HTTP ${response.status}: ${response.statusText}. ${truncateErrorBody(errorBody)}`, response.status, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")));
105
172
  }
106
173
  return response;
107
174
  }
@@ -124,19 +191,6 @@ var PlunkHttpClient = class {
124
191
  }
125
192
  }
126
193
  /**
127
- * Creates a PlunkError from an error message and optional status code.
128
- *
129
- * @param message - The error message
130
- * @param statusCode - Optional HTTP status code
131
- * @returns PlunkError instance
132
- */
133
- createPlunkError(message, statusCode) {
134
- return {
135
- message,
136
- statusCode
137
- };
138
- }
139
- /**
140
194
  * Sleeps for the specified number of milliseconds.
141
195
  *
142
196
  * @param ms - Milliseconds to sleep
@@ -146,6 +200,12 @@ var PlunkHttpClient = class {
146
200
  return new Promise((resolve) => setTimeout(resolve, ms));
147
201
  }
148
202
  };
203
+ function isAbortError$1(error) {
204
+ return error instanceof Error && error.name === "AbortError";
205
+ }
206
+ function truncateErrorBody(text) {
207
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
208
+ }
149
209
 
150
210
  //#endregion
151
211
  //#region src/message-converter.ts
@@ -277,6 +337,7 @@ function arrayBufferToBase64(buffer) {
277
337
  * ```
278
338
  */
279
339
  var PlunkTransport = class {
340
+ id = "plunk";
280
341
  /**
281
342
  * The resolved Plunk configuration used by this transport.
282
343
  */
@@ -333,14 +394,13 @@ var PlunkTransport = class {
333
394
  const messageId = this.extractMessageId(response, message);
334
395
  return {
335
396
  successful: true,
336
- messageId
397
+ messageId,
398
+ provider: "plunk"
337
399
  };
338
400
  } catch (error) {
401
+ if (isAbortError(error) && options?.signal?.aborted) throw error;
339
402
  const errorMessage = error instanceof Error ? error.message : String(error);
340
- return {
341
- successful: false,
342
- errorMessages: [errorMessage]
343
- };
403
+ return createPlunkFailure(errorMessage, error);
344
404
  }
345
405
  }
346
406
  /**
@@ -429,6 +489,18 @@ var PlunkTransport = class {
429
489
  return `plunk-${timestamp}-${recipientHash}-${random}`;
430
490
  }
431
491
  };
492
+ function createPlunkFailure(message, error) {
493
+ if (error instanceof PlunkApiError) return (0, __upyo_core.createFailedReceipt)(message, {
494
+ provider: "plunk",
495
+ statusCode: error.statusCode,
496
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
497
+ attempts: error.attempts
498
+ });
499
+ return (0, __upyo_core.createFailedReceipt)(message, { provider: "plunk" });
500
+ }
501
+ function isAbortError(error) {
502
+ return error instanceof Error && error.name === "AbortError";
503
+ }
432
504
 
433
505
  //#endregion
434
506
  exports.PlunkTransport = PlunkTransport;
package/dist/index.d.cts CHANGED
@@ -106,7 +106,8 @@ type ResolvedPlunkConfig = Required<PlunkConfig>;
106
106
  * }
107
107
  * ```
108
108
  */
109
- declare class PlunkTransport implements Transport {
109
+ declare class PlunkTransport implements Transport<"plunk"> {
110
+ readonly id = "plunk";
110
111
  /**
111
112
  * The resolved Plunk configuration used by this transport.
112
113
  */
@@ -151,7 +152,7 @@ declare class PlunkTransport implements Transport {
151
152
  * @returns A promise that resolves to a receipt indicating success or
152
153
  * failure.
153
154
  */
154
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
155
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"plunk">>;
155
156
  /**
156
157
  * Sends multiple email messages efficiently via Plunk API.
157
158
  *
@@ -204,7 +205,7 @@ declare class PlunkTransport implements Transport {
204
205
  * cancellation.
205
206
  * @returns An async iterable of receipts, one for each message.
206
207
  */
207
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
208
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"plunk">>;
208
209
  /**
209
210
  * Extracts or generates a message ID from the Plunk response.
210
211
  *
@@ -260,10 +261,9 @@ interface PlunkError {
260
261
  readonly details?: unknown;
261
262
  }
262
263
  /**
263
- * HTTP client wrapper for Plunk API requests.
264
+ * Error thrown when a Plunk API request fails.
264
265
  *
265
- * This class handles authentication, request formatting, error handling,
266
- * and retry logic for the Plunk HTTP API.
266
+ * @since 0.5.0
267
267
  */
268
268
  //#endregion
269
269
  export { PlunkConfig, PlunkError, PlunkResponse, PlunkTransport, ResolvedPlunkConfig };
package/dist/index.d.ts CHANGED
@@ -106,7 +106,8 @@ type ResolvedPlunkConfig = Required<PlunkConfig>;
106
106
  * }
107
107
  * ```
108
108
  */
109
- declare class PlunkTransport implements Transport {
109
+ declare class PlunkTransport implements Transport<"plunk"> {
110
+ readonly id = "plunk";
110
111
  /**
111
112
  * The resolved Plunk configuration used by this transport.
112
113
  */
@@ -151,7 +152,7 @@ declare class PlunkTransport implements Transport {
151
152
  * @returns A promise that resolves to a receipt indicating success or
152
153
  * failure.
153
154
  */
154
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
155
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"plunk">>;
155
156
  /**
156
157
  * Sends multiple email messages efficiently via Plunk API.
157
158
  *
@@ -204,7 +205,7 @@ declare class PlunkTransport implements Transport {
204
205
  * cancellation.
205
206
  * @returns An async iterable of receipts, one for each message.
206
207
  */
207
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
208
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"plunk">>;
208
209
  /**
209
210
  * Extracts or generates a message ID from the Plunk response.
210
211
  *
@@ -260,10 +261,9 @@ interface PlunkError {
260
261
  readonly details?: unknown;
261
262
  }
262
263
  /**
263
- * HTTP client wrapper for Plunk API requests.
264
+ * Error thrown when a Plunk API request fails.
264
265
  *
265
- * This class handles authentication, request formatting, error handling,
266
- * and retry logic for the Plunk HTTP API.
266
+ * @since 0.5.0
267
267
  */
268
268
  //#endregion
269
269
  export { PlunkConfig, PlunkError, PlunkResponse, PlunkTransport, ResolvedPlunkConfig };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createFailedReceipt, parseRetryAfter } from "@upyo/core";
2
+
1
3
  //#region src/config.ts
2
4
  /**
3
5
  * Creates a resolved Plunk configuration by applying default values to optional fields.
@@ -24,6 +26,40 @@ function createPlunkConfig(config) {
24
26
  //#endregion
25
27
  //#region src/http-client.ts
26
28
  /**
29
+ * Error thrown when a Plunk API request fails.
30
+ *
31
+ * @since 0.5.0
32
+ */
33
+ var PlunkApiError = class extends Error {
34
+ /**
35
+ * HTTP status code returned by Plunk, if the request reached the API.
36
+ */
37
+ statusCode;
38
+ /**
39
+ * Retry delay from Plunk'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 Plunk API error.
48
+ *
49
+ * @param message Error message.
50
+ * @param statusCode HTTP status code returned by Plunk.
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) {
55
+ super(message);
56
+ this.name = "PlunkApiError";
57
+ this.statusCode = statusCode;
58
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
59
+ this.attempts = attempts;
60
+ }
61
+ };
62
+ /**
27
63
  * HTTP client wrapper for Plunk API requests.
28
64
  *
29
65
  * This class handles authentication, request formatting, error handling,
@@ -60,17 +96,16 @@ var PlunkHttpClient = class {
60
96
  return await this.parseResponse(response);
61
97
  } catch (error) {
62
98
  lastError = error instanceof Error ? error : new Error(String(error));
63
- if (error instanceof Error) {
64
- if (error.name === "AbortError") throw error;
65
- if (error.message.includes("status: 4")) throw this.createPlunkError(error.message, 400);
66
- }
99
+ if (error instanceof Error && error.name === "AbortError") throw error;
100
+ if (error instanceof PlunkApiError && error.statusCode !== void 0 && error.statusCode >= 400 && error.statusCode < 500) throw new PlunkApiError(error.message, error.statusCode, error.retryAfterMilliseconds, attempt + 1);
67
101
  if (attempt === this.config.retries) break;
68
102
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
69
103
  await this.sleep(delay);
70
104
  }
71
105
  }
72
106
  const errorMessage = lastError?.message ?? "Unknown error occurred";
73
- throw this.createPlunkError(errorMessage);
107
+ if (lastError instanceof PlunkApiError) throw new PlunkApiError(lastError.message, lastError.statusCode, lastError.retryAfterMilliseconds, this.config.retries + 1);
108
+ throw new PlunkApiError(errorMessage, void 0, void 0, this.config.retries + 1);
74
109
  }
75
110
  /**
76
111
  * Makes an HTTP request to the Plunk API.
@@ -86,13 +121,23 @@ var PlunkHttpClient = class {
86
121
  "Content-Type": "application/json",
87
122
  ...this.config.headers
88
123
  };
89
- const response = await fetch(url, {
90
- method: "POST",
91
- headers,
92
- body: JSON.stringify(emailData),
93
- signal,
94
- ...this.config.timeout > 0 && typeof globalThis.AbortSignal?.timeout === "function" ? { signal: AbortSignal.any([signal, AbortSignal.timeout(this.config.timeout)].filter(Boolean)) } : {}
95
- });
124
+ const timeoutController = new AbortController();
125
+ const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
126
+ const requestSignal = signal == null ? timeoutController.signal : AbortSignal.any([timeoutController.signal, signal]);
127
+ let response;
128
+ try {
129
+ response = await fetch(url, {
130
+ method: "POST",
131
+ headers,
132
+ body: JSON.stringify(emailData),
133
+ signal: requestSignal
134
+ });
135
+ } catch (error) {
136
+ if (isAbortError$1(error) && timeoutController.signal.aborted && !signal?.aborted) throw new Error(`Plunk API request timed out after ${this.config.timeout} ms.`);
137
+ throw error;
138
+ } finally {
139
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
140
+ }
96
141
  if (!response.ok) {
97
142
  let errorBody;
98
143
  try {
@@ -100,7 +145,7 @@ var PlunkHttpClient = class {
100
145
  } catch {
101
146
  errorBody = "Failed to read error response";
102
147
  }
103
- throw new Error(`HTTP ${response.status}: ${response.statusText}. ${errorBody}`);
148
+ throw new PlunkApiError(`HTTP ${response.status}: ${response.statusText}. ${truncateErrorBody(errorBody)}`, response.status, parseRetryAfter(response.headers.get("Retry-After")));
104
149
  }
105
150
  return response;
106
151
  }
@@ -123,19 +168,6 @@ var PlunkHttpClient = class {
123
168
  }
124
169
  }
125
170
  /**
126
- * Creates a PlunkError from an error message and optional status code.
127
- *
128
- * @param message - The error message
129
- * @param statusCode - Optional HTTP status code
130
- * @returns PlunkError instance
131
- */
132
- createPlunkError(message, statusCode) {
133
- return {
134
- message,
135
- statusCode
136
- };
137
- }
138
- /**
139
171
  * Sleeps for the specified number of milliseconds.
140
172
  *
141
173
  * @param ms - Milliseconds to sleep
@@ -145,6 +177,12 @@ var PlunkHttpClient = class {
145
177
  return new Promise((resolve) => setTimeout(resolve, ms));
146
178
  }
147
179
  };
180
+ function isAbortError$1(error) {
181
+ return error instanceof Error && error.name === "AbortError";
182
+ }
183
+ function truncateErrorBody(text) {
184
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
185
+ }
148
186
 
149
187
  //#endregion
150
188
  //#region src/message-converter.ts
@@ -276,6 +314,7 @@ function arrayBufferToBase64(buffer) {
276
314
  * ```
277
315
  */
278
316
  var PlunkTransport = class {
317
+ id = "plunk";
279
318
  /**
280
319
  * The resolved Plunk configuration used by this transport.
281
320
  */
@@ -332,14 +371,13 @@ var PlunkTransport = class {
332
371
  const messageId = this.extractMessageId(response, message);
333
372
  return {
334
373
  successful: true,
335
- messageId
374
+ messageId,
375
+ provider: "plunk"
336
376
  };
337
377
  } catch (error) {
378
+ if (isAbortError(error) && options?.signal?.aborted) throw error;
338
379
  const errorMessage = error instanceof Error ? error.message : String(error);
339
- return {
340
- successful: false,
341
- errorMessages: [errorMessage]
342
- };
380
+ return createPlunkFailure(errorMessage, error);
343
381
  }
344
382
  }
345
383
  /**
@@ -428,6 +466,18 @@ var PlunkTransport = class {
428
466
  return `plunk-${timestamp}-${recipientHash}-${random}`;
429
467
  }
430
468
  };
469
+ function createPlunkFailure(message, error) {
470
+ if (error instanceof PlunkApiError) return createFailedReceipt(message, {
471
+ provider: "plunk",
472
+ statusCode: error.statusCode,
473
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
474
+ attempts: error.attempts
475
+ });
476
+ return createFailedReceipt(message, { provider: "plunk" });
477
+ }
478
+ function isAbortError(error) {
479
+ return error instanceof Error && error.name === "AbortError";
480
+ }
431
481
 
432
482
  //#endregion
433
483
  export { PlunkTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/plunk",
3
- "version": "0.5.0-dev.136",
3
+ "version": "0.5.0-dev.154",
4
4
  "description": "Plunk 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.136+adacf579"
56
+ "@upyo/core": "0.5.0-dev.154+2f72d353"
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
  }