@upyo/plunk 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,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,
@@ -60,18 +118,18 @@ var PlunkHttpClient = class {
60
118
  const response = await this.makeRequest(url, emailData, signal);
61
119
  return await this.parseResponse(response);
62
120
  } catch (error) {
121
+ if (isCallerAbort$1(error, signal)) throw error;
63
122
  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
- }
123
+ if (error instanceof Error && error.name === "AbortError") throw error;
124
+ 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
125
  if (attempt === this.config.retries) break;
69
126
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
70
127
  await this.sleep(delay);
71
128
  }
72
129
  }
73
130
  const errorMessage = lastError?.message ?? "Unknown error occurred";
74
- throw this.createPlunkError(errorMessage);
131
+ if (lastError instanceof PlunkApiError) throw new PlunkApiError(lastError.message, lastError.statusCode, lastError.retryAfterMilliseconds, this.config.retries + 1);
132
+ throw new PlunkApiError(errorMessage, void 0, void 0, this.config.retries + 1);
75
133
  }
76
134
  /**
77
135
  * Makes an HTTP request to the Plunk API.
@@ -87,13 +145,24 @@ var PlunkHttpClient = class {
87
145
  "Content-Type": "application/json",
88
146
  ...this.config.headers
89
147
  };
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
- });
148
+ const timeoutController = new AbortController();
149
+ const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
150
+ const combinedSignal = (0, __upyo_core.combineSignals)(timeoutController.signal, signal);
151
+ let response;
152
+ try {
153
+ response = await fetch(url, {
154
+ method: "POST",
155
+ headers,
156
+ body: JSON.stringify(emailData),
157
+ signal: combinedSignal.signal
158
+ });
159
+ } catch (error) {
160
+ if (isAbortError$1(error) && timeoutController.signal.aborted && !signal?.aborted) throw new Error(`Plunk API request timed out after ${this.config.timeout} ms.`);
161
+ throw error;
162
+ } finally {
163
+ combinedSignal.cleanup();
164
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
165
+ }
97
166
  if (!response.ok) {
98
167
  let errorBody;
99
168
  try {
@@ -101,7 +170,7 @@ var PlunkHttpClient = class {
101
170
  } catch {
102
171
  errorBody = "Failed to read error response";
103
172
  }
104
- throw new Error(`HTTP ${response.status}: ${response.statusText}. ${errorBody}`);
173
+ throw new PlunkApiError(`HTTP ${response.status}: ${response.statusText}. ${truncateErrorBody(errorBody)}`, response.status, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")));
105
174
  }
106
175
  return response;
107
176
  }
@@ -124,19 +193,6 @@ var PlunkHttpClient = class {
124
193
  }
125
194
  }
126
195
  /**
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
196
  * Sleeps for the specified number of milliseconds.
141
197
  *
142
198
  * @param ms - Milliseconds to sleep
@@ -146,6 +202,15 @@ var PlunkHttpClient = class {
146
202
  return new Promise((resolve) => setTimeout(resolve, ms));
147
203
  }
148
204
  };
205
+ function isAbortError$1(error) {
206
+ return error instanceof Error && error.name === "AbortError";
207
+ }
208
+ function isCallerAbort$1(error, signal) {
209
+ return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
210
+ }
211
+ function truncateErrorBody(text) {
212
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
213
+ }
149
214
 
150
215
  //#endregion
151
216
  //#region src/message-converter.ts
@@ -277,6 +342,7 @@ function arrayBufferToBase64(buffer) {
277
342
  * ```
278
343
  */
279
344
  var PlunkTransport = class {
345
+ id = "plunk";
280
346
  /**
281
347
  * The resolved Plunk configuration used by this transport.
282
348
  */
@@ -333,14 +399,13 @@ var PlunkTransport = class {
333
399
  const messageId = this.extractMessageId(response, message);
334
400
  return {
335
401
  successful: true,
336
- messageId
402
+ messageId,
403
+ provider: "plunk"
337
404
  };
338
405
  } catch (error) {
406
+ if (isCallerAbort(error, options?.signal)) throw error;
339
407
  const errorMessage = error instanceof Error ? error.message : String(error);
340
- return {
341
- successful: false,
342
- errorMessages: [errorMessage]
343
- };
408
+ return createPlunkFailure(errorMessage, error);
344
409
  }
345
410
  }
346
411
  /**
@@ -429,6 +494,21 @@ var PlunkTransport = class {
429
494
  return `plunk-${timestamp}-${recipientHash}-${random}`;
430
495
  }
431
496
  };
497
+ function createPlunkFailure(message, error) {
498
+ if (error instanceof PlunkApiError) return (0, __upyo_core.createFailedReceipt)(message, {
499
+ provider: "plunk",
500
+ statusCode: error.statusCode,
501
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
502
+ attempts: error.attempts
503
+ });
504
+ return (0, __upyo_core.createFailedReceipt)(message, { provider: "plunk" });
505
+ }
506
+ function isAbortError(error) {
507
+ return error instanceof Error && error.name === "AbortError";
508
+ }
509
+ function isCallerAbort(error, signal) {
510
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
511
+ }
432
512
 
433
513
  //#endregion
434
514
  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 { combineSignals, 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,
@@ -59,18 +95,18 @@ var PlunkHttpClient = class {
59
95
  const response = await this.makeRequest(url, emailData, signal);
60
96
  return await this.parseResponse(response);
61
97
  } catch (error) {
98
+ if (isCallerAbort$1(error, signal)) throw error;
62
99
  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
- }
100
+ if (error instanceof Error && error.name === "AbortError") throw error;
101
+ 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
102
  if (attempt === this.config.retries) break;
68
103
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
69
104
  await this.sleep(delay);
70
105
  }
71
106
  }
72
107
  const errorMessage = lastError?.message ?? "Unknown error occurred";
73
- throw this.createPlunkError(errorMessage);
108
+ if (lastError instanceof PlunkApiError) throw new PlunkApiError(lastError.message, lastError.statusCode, lastError.retryAfterMilliseconds, this.config.retries + 1);
109
+ throw new PlunkApiError(errorMessage, void 0, void 0, this.config.retries + 1);
74
110
  }
75
111
  /**
76
112
  * Makes an HTTP request to the Plunk API.
@@ -86,13 +122,24 @@ var PlunkHttpClient = class {
86
122
  "Content-Type": "application/json",
87
123
  ...this.config.headers
88
124
  };
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
- });
125
+ const timeoutController = new AbortController();
126
+ const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
127
+ const combinedSignal = combineSignals(timeoutController.signal, signal);
128
+ let response;
129
+ try {
130
+ response = await fetch(url, {
131
+ method: "POST",
132
+ headers,
133
+ body: JSON.stringify(emailData),
134
+ signal: combinedSignal.signal
135
+ });
136
+ } catch (error) {
137
+ if (isAbortError$1(error) && timeoutController.signal.aborted && !signal?.aborted) throw new Error(`Plunk API request timed out after ${this.config.timeout} ms.`);
138
+ throw error;
139
+ } finally {
140
+ combinedSignal.cleanup();
141
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
142
+ }
96
143
  if (!response.ok) {
97
144
  let errorBody;
98
145
  try {
@@ -100,7 +147,7 @@ var PlunkHttpClient = class {
100
147
  } catch {
101
148
  errorBody = "Failed to read error response";
102
149
  }
103
- throw new Error(`HTTP ${response.status}: ${response.statusText}. ${errorBody}`);
150
+ throw new PlunkApiError(`HTTP ${response.status}: ${response.statusText}. ${truncateErrorBody(errorBody)}`, response.status, parseRetryAfter(response.headers.get("Retry-After")));
104
151
  }
105
152
  return response;
106
153
  }
@@ -123,19 +170,6 @@ var PlunkHttpClient = class {
123
170
  }
124
171
  }
125
172
  /**
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
173
  * Sleeps for the specified number of milliseconds.
140
174
  *
141
175
  * @param ms - Milliseconds to sleep
@@ -145,6 +179,15 @@ var PlunkHttpClient = class {
145
179
  return new Promise((resolve) => setTimeout(resolve, ms));
146
180
  }
147
181
  };
182
+ function isAbortError$1(error) {
183
+ return error instanceof Error && error.name === "AbortError";
184
+ }
185
+ function isCallerAbort$1(error, signal) {
186
+ return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
187
+ }
188
+ function truncateErrorBody(text) {
189
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
190
+ }
148
191
 
149
192
  //#endregion
150
193
  //#region src/message-converter.ts
@@ -276,6 +319,7 @@ function arrayBufferToBase64(buffer) {
276
319
  * ```
277
320
  */
278
321
  var PlunkTransport = class {
322
+ id = "plunk";
279
323
  /**
280
324
  * The resolved Plunk configuration used by this transport.
281
325
  */
@@ -332,14 +376,13 @@ var PlunkTransport = class {
332
376
  const messageId = this.extractMessageId(response, message);
333
377
  return {
334
378
  successful: true,
335
- messageId
379
+ messageId,
380
+ provider: "plunk"
336
381
  };
337
382
  } catch (error) {
383
+ if (isCallerAbort(error, options?.signal)) throw error;
338
384
  const errorMessage = error instanceof Error ? error.message : String(error);
339
- return {
340
- successful: false,
341
- errorMessages: [errorMessage]
342
- };
385
+ return createPlunkFailure(errorMessage, error);
343
386
  }
344
387
  }
345
388
  /**
@@ -428,6 +471,21 @@ var PlunkTransport = class {
428
471
  return `plunk-${timestamp}-${recipientHash}-${random}`;
429
472
  }
430
473
  };
474
+ function createPlunkFailure(message, error) {
475
+ if (error instanceof PlunkApiError) return createFailedReceipt(message, {
476
+ provider: "plunk",
477
+ statusCode: error.statusCode,
478
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
479
+ attempts: error.attempts
480
+ });
481
+ return createFailedReceipt(message, { provider: "plunk" });
482
+ }
483
+ function isAbortError(error) {
484
+ return error instanceof Error && error.name === "AbortError";
485
+ }
486
+ function isCallerAbort(error, signal) {
487
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
488
+ }
431
489
 
432
490
  //#endregion
433
491
  export { PlunkTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/plunk",
3
- "version": "0.5.0-dev.86",
3
+ "version": "0.5.0",
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.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
  }