Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// Optional Redis connection manager.
|
||||
//
|
||||
// Redis is entirely optional. When REDIS_URL is unset the app runs exactly as
|
||||
// before with in-memory backends. When set, this module owns a single shared
|
||||
// command connection plus a dedicated subscriber connection (a connection in
|
||||
// subscribe mode cannot run normal commands), with auto-reconnect, capped
|
||||
// backoff, and a health flag that callers and the health endpoint can read.
|
||||
|
||||
import Redis from 'ioredis';
|
||||
|
||||
let client: Redis | null = null;
|
||||
let subscriber: Redis | null = null;
|
||||
let healthy = false;
|
||||
let initialized = false;
|
||||
|
||||
/** Whether Redis is configured via REDIS_URL. */
|
||||
export function isRedisEnabled(): boolean {
|
||||
return !!process.env.REDIS_URL;
|
||||
}
|
||||
|
||||
/** Whether the Redis connection is currently usable. */
|
||||
export function isRedisHealthy(): boolean {
|
||||
return isRedisEnabled() && healthy;
|
||||
}
|
||||
|
||||
function buildClient(label: string): Redis {
|
||||
const url = process.env.REDIS_URL as string;
|
||||
const instance = new Redis(url, {
|
||||
// Keep the process responsive: fail fast on a per-command basis and let the
|
||||
// callers degrade to their in-memory fallback rather than hanging.
|
||||
maxRetriesPerRequest: 1,
|
||||
enableOfflineQueue: false,
|
||||
lazyConnect: false,
|
||||
retryStrategy(times) {
|
||||
// Capped exponential backoff for reconnects: 200ms, 400ms ... max 5s.
|
||||
const delay = Math.min(times * 200, 5000);
|
||||
return delay;
|
||||
},
|
||||
});
|
||||
|
||||
instance.on('connect', () => {
|
||||
console.log(`[redis] (${label}) connecting`);
|
||||
});
|
||||
instance.on('ready', () => {
|
||||
healthy = true;
|
||||
console.log(`[redis] (${label}) ready`);
|
||||
});
|
||||
instance.on('error', (err) => {
|
||||
healthy = false;
|
||||
console.error(`[redis] (${label}) error:`, err?.message || err);
|
||||
});
|
||||
instance.on('reconnecting', () => {
|
||||
healthy = false;
|
||||
console.warn(`[redis] (${label}) reconnecting`);
|
||||
});
|
||||
instance.on('end', () => {
|
||||
healthy = false;
|
||||
console.warn(`[redis] (${label}) connection closed`);
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
function ensureInit(): void {
|
||||
if (initialized || !isRedisEnabled()) return;
|
||||
initialized = true;
|
||||
client = buildClient('commands');
|
||||
subscriber = buildClient('subscriber');
|
||||
}
|
||||
|
||||
/** Shared command connection, or null when Redis is not configured. */
|
||||
export function getRedis(): Redis | null {
|
||||
ensureInit();
|
||||
return client;
|
||||
}
|
||||
|
||||
/** Dedicated subscriber connection, or null when Redis is not configured. */
|
||||
export function getSubscriber(): Redis | null {
|
||||
ensureInit();
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
/** Close connections (used for graceful shutdown). */
|
||||
export async function closeRedis(): Promise<void> {
|
||||
const tasks: Promise<unknown>[] = [];
|
||||
if (client) tasks.push(client.quit().catch(() => undefined));
|
||||
if (subscriber) tasks.push(subscriber.quit().catch(() => undefined));
|
||||
await Promise.all(tasks);
|
||||
client = null;
|
||||
subscriber = null;
|
||||
initialized = false;
|
||||
healthy = false;
|
||||
}
|
||||
Reference in New Issue
Block a user