UNPKG

waitlist-mailer

Version:

Modern, modular TypeScript library for managing waitlists with pluggable storage and mail providers. Supports MongoDB, SQL databases, and custom adapters with zero required dependencies for basic usage.

350 lines (348 loc) 10.4 kB
// lib/core/WaitlistManager.ts import { EventEmitter } from "events"; import validator from "validator"; var WaitlistManager = class extends EventEmitter { /** * Create a new WaitlistManager instance. * @param config - Configuration object with storage, optional mailer, etc. */ constructor(config) { super(); this.initialized = false; this.storage = config.storage; this.mailer = config.mailer; this.companyName = config.companyName || "Our Platform"; if (config.autoConnect !== false) { this.connect().catch((error) => { console.error("Failed to auto-connect:", error); this.emit("error", { context: "autoConnect", error }); }); } } /** * Initialize connections to storage and mail providers. * @returns Promise that resolves when initialization is complete */ async connect() { try { if (this.storage.connect) { await this.storage.connect(); this.emit("storage:connected"); } if (this.mailer?.verify) { const verified = await this.mailer.verify(); if (verified) { this.emit("mailer:verified"); } else { console.warn("Mail provider verification failed"); this.emit("mailer:verification-failed"); } } this.initialized = true; this.emit("initialized"); } catch (error) { this.emit("error", { context: "connect", error }); throw error; } } /** * Wait for the manager to be fully initialized. * @returns Promise that resolves when initialized */ async waitForInitialization() { if (this.initialized) return; return new Promise((resolve) => this.once("initialized", resolve)); } /** * Check if the manager is initialized. * @returns true if initialized */ isInitialized() { return this.initialized; } /** * Add an email to the waitlist. * * Validates the email and checks for duplicates before adding. * Optionally sends a confirmation email. * * @param email - The email address to add * @param metadata - Optional custom metadata * @returns Result object with success status and message * * @example * ```typescript * const result = await manager.join('user@example.com', { * name: 'Alice', * referralSource: 'ProductHunt' * }); * * if (result.success) { * console.log('User added:', result.email); * } * ``` */ async join(email, metadata) { try { if (!validator.isEmail(email)) { this.emit("join:validation-error", { email, reason: "invalid-format" }); return { success: false, message: "Invalid email format", email }; } const normalizedEmail = email.toLowerCase(); const exists = await this.storage.exists(normalizedEmail); if (exists) { this.emit("join:duplicate", { email: normalizedEmail, metadata }); return { success: false, message: "Email already in waitlist", email: normalizedEmail }; } await this.storage.add(normalizedEmail, metadata); this.emit("join:success", { email: normalizedEmail, metadata }); if (this.mailer) { try { const context = { email: normalizedEmail, companyName: this.companyName, ...metadata }; const sent = await this.mailer.sendConfirmation(normalizedEmail, context); if (sent) { this.emit("mailer:sent", { email: normalizedEmail }); } else { this.emit("mailer:failed", { email: normalizedEmail }); } } catch (error) { console.error("Mail sending error:", error); this.emit("mailer:error", { email: normalizedEmail, error }); } } return { success: true, message: "Successfully joined the waitlist", email: normalizedEmail }; } catch (error) { this.emit("join:error", { email, error }); return { success: false, message: "Failed to add email to waitlist", email }; } } /** * Remove an email from the waitlist. * @param email - The email address to remove * @returns true if the email was found and removed */ async leave(email) { try { const normalizedEmail = email.toLowerCase(); const removed = await this.storage.remove(normalizedEmail); if (removed) { this.emit("leave:success", { email: normalizedEmail }); } else { this.emit("leave:not-found", { email: normalizedEmail }); } return removed; } catch (error) { this.emit("leave:error", { email, error }); return false; } } /** * Check if an email is in the waitlist. * @param email - The email to check * @returns true if the email exists */ async has(email) { return this.storage.exists(email.toLowerCase()); } /** * Get all waitlist entries. * @returns Array of WaitlistEntry objects */ async getAll() { return this.storage.getAll(); } /** * Get count of waitlist entries. * @returns Number of entries */ async count() { return this.storage.count(); } /** * Clear all entries from the waitlist. * @returns true if successful */ async clear() { try { await this.storage.clear(); this.emit("clear:success"); return true; } catch (error) { this.emit("clear:error", { error }); return false; } } /** * Send a custom email to an address. * Useful for sending emails to users who aren't on the waitlist. * * @param email - Recipient email * @param context - Email context with template variables * @returns true if sent successfully */ async sendEmail(email, context) { if (!this.mailer) { console.warn("No mail provider configured"); return false; } try { const normalizedEmail = email.toLowerCase(); const fullContext = { companyName: this.companyName, ...context, email: normalizedEmail }; return await this.mailer.sendConfirmation(normalizedEmail, fullContext); } catch (error) { this.emit("email:error", { email, error }); return false; } } /** * Send emails to all waitlist members. * Processes emails concurrently with configurable concurrency limit. * * @param contextBuilder - Function to build context for each email * @param concurrency - Maximum concurrent emails to send (1 = sequential, Infinity = all at once) * Default: 5 for balance between speed and resource usage * @returns Object with statistics * * @example * ```typescript * // Send with default concurrency (5 at a time) * const result = await manager.sendBulkEmails((email) => ({ * subject: 'Welcome!' * })); * * // Send slowly (1 at a time) * const result = await manager.sendBulkEmails( * (email) => ({ subject: 'Welcome!' }), * 1 * ); * * // Send fast (all at once - use with caution) * const result = await manager.sendBulkEmails( * (email) => ({ subject: 'Welcome!' }), * Infinity * ); * ``` */ async sendBulkEmails(contextBuilder, concurrency = 5) { if (!this.mailer) { console.warn("No mail provider configured"); return { sent: 0, failed: 0, total: 0 }; } const entries = await this.storage.getAll(); if (entries.length === 0) { this.emit("bulk:complete", { sent: 0, failed: 0, total: 0 }); return { sent: 0, failed: 0, total: 0 }; } let sent = 0; let failed = 0; if (concurrency === 1) { for (const entry of entries) { const success = await this.sendSingleBulkEmail(entry, contextBuilder); if (success) sent++; else failed++; } } else if (concurrency === Infinity) { const promises = entries.map( (entry) => this.sendSingleBulkEmail(entry, contextBuilder) ); const results = await Promise.all(promises); sent = results.filter((s) => s).length; failed = results.filter((s) => !s).length; } else { const queue = []; for (const entry of entries) { const promise = this.sendSingleBulkEmail(entry, contextBuilder); queue.push(promise); if (queue.length >= concurrency) { const result2 = await Promise.race(queue); queue.splice(queue.indexOf(promise), 1); if (result2) sent++; else failed++; } } const results = await Promise.all(queue); sent += results.filter((s) => s).length; failed += results.filter((s) => !s).length; } const result = { sent, failed, total: entries.length }; this.emit("bulk:complete", result); return result; } /** * Internal method to send a single bulk email. * @private */ async sendSingleBulkEmail(entry, contextBuilder) { try { const context = { companyName: this.companyName, ...contextBuilder(entry.email, entry.metadata), email: entry.email }; const result = await this.mailer.sendConfirmation(entry.email, context); if (result) { this.emit("bulk:email-sent", { email: entry.email }); } else { this.emit("bulk:email-failed", { email: entry.email }); } return result; } catch (error) { this.emit("bulk:email-error", { email: entry.email, error }); return false; } } /** * Get entries matching a search pattern. * @param pattern - Pattern to search for (partial email match) * @returns Matching entries */ async search(pattern) { const entries = await this.storage.getAll(); const lowerPattern = pattern.toLowerCase(); return entries.filter((entry) => entry.email.toLowerCase().includes(lowerPattern)); } /** * Disconnect from storage and mail providers. * Should be called before shutting down the application. */ async disconnect() { try { if (this.storage.disconnect) { await this.storage.disconnect(); this.emit("storage:disconnected"); } this.initialized = false; this.emit("disconnected"); } catch (error) { this.emit("error", { context: "disconnect", error }); throw error; } } }; export { WaitlistManager };