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
JavaScript
// 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
};