This release is 4 versions behind 0.4.0 — the latest version of @upyo/plunk. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
Works with
•JSR Score100%•This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




Downloads1/wk
•Published2 months ago (0.3.1)
Plunk transport for Upyo email library
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234import type { ResolvedPlunkConfig } from "./config.ts"; /** * Response from Plunk API for sending messages. */ export interface PlunkResponse { /** * Indicates whether the API call was successful. */ readonly success: boolean; /** * Array of sent email details. */ readonly emails: readonly { readonly contact: { readonly id: string; readonly email: string; }; readonly email: string; }[]; /** * Timestamp of the send operation. */ readonly timestamp: string; } /** * Error response from Plunk API. */ export interface PlunkError { /** * Error message from Plunk. */ readonly message: string; /** * HTTP status code. */ readonly statusCode?: number; /** * Additional error details from Plunk API. */ readonly details?: unknown; } /** * HTTP client wrapper for Plunk API requests. * * This class handles authentication, request formatting, error handling, * and retry logic for the Plunk HTTP API. */ export class PlunkHttpClient { private config: ResolvedPlunkConfig; /** * Creates a new Plunk HTTP client instance. * * @param config - Resolved Plunk configuration */ constructor(config: ResolvedPlunkConfig) { this.config = config; } /** * Sends a message via the Plunk API. * * This method makes a POST request to the `/v1/send` endpoint with proper * authentication, retry logic, and error handling. * * @param emailData - The email data in Plunk API format * @param signal - Optional AbortSignal for request cancellation * @returns Promise that resolves to Plunk API response * @throws PlunkError if the request fails after all retries */ async sendMessage( emailData: Record<string, unknown>, signal?: AbortSignal, ): Promise<PlunkResponse> { const url = `${this.config.baseUrl}/v1/send`; let lastError: Error | null = null; for (let attempt = 0; attempt <= this.config.retries; attempt++) { signal?.throwIfAborted(); try { const response = await this.makeRequest(url, emailData, signal); return await this.parseResponse(response); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Don't retry on client errors (4xx) or AbortError if (error instanceof Error) { if (error.name === "AbortError") { throw error; } if (error.message.includes("status: 4")) { throw this.createPlunkError(error.message, 400); } } // If this was the last attempt, throw the error if (attempt === this.config.retries) { break; } // Wait before retrying (exponential backoff) const delay = Math.min(1000 * Math.pow(2, attempt), 10000); await this.sleep(delay); } } // All retries failed const errorMessage = lastError?.message ?? "Unknown error occurred"; throw this.createPlunkError(errorMessage); } /** * Makes an HTTP request to the Plunk API. * * @param url - The request URL * @param emailData - The email data to send * @param signal - Optional AbortSignal for cancellation * @returns Promise that resolves to the Response object */ private async makeRequest( url: string, emailData: Record<string, unknown>, signal?: AbortSignal, ): Promise<Response> { const headers: Record<string, string> = { "Authorization": `Bearer ${this.config.apiKey}`, "Content-Type": "application/json", ...this.config.headers, }; const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(emailData), signal, // Add timeout if supported by the runtime ...(this.config.timeout > 0 && typeof globalThis.AbortSignal?.timeout === "function" ? { signal: AbortSignal.any([ signal, AbortSignal.timeout(this.config.timeout), ].filter(Boolean) as AbortSignal[]), } : {}), }); if (!response.ok) { let errorBody: string; try { errorBody = await response.text(); } catch { errorBody = "Failed to read error response"; } throw new Error( `HTTP ${response.status}: ${response.statusText}. ${errorBody}`, ); } return response; } /** * Parses the response from the Plunk API. * * @param response - The Response object from fetch * @returns Promise that resolves to parsed PlunkResponse */ private async parseResponse(response: Response): Promise<PlunkResponse> { try { const data = await response.json(); // Validate response structure if (typeof data !== "object" || data === null) { throw new Error("Invalid response format: expected object"); } if (typeof data.success !== "boolean") { throw new Error("Invalid response format: missing success field"); } if (!data.success) { throw new Error( data.message ?? "Send operation failed without error details", ); } return data as PlunkResponse; } catch (error) { if (error instanceof SyntaxError) { throw new Error("Invalid JSON response from Plunk API"); } throw error; } } /** * Creates a PlunkError from an error message and optional status code. * * @param message - The error message * @param statusCode - Optional HTTP status code * @returns PlunkError instance */ private createPlunkError( message: string, statusCode?: number, ): PlunkError { return { message, statusCode, }; } /** * Sleeps for the specified number of milliseconds. * * @param ms - Milliseconds to sleep * @returns Promise that resolves after the delay */ private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } }