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,121 @@
|
||||
// Pub/Sub abstraction with two implementations:
|
||||
// - memory: in-process EventEmitter (single instance only)
|
||||
// - redis: PUBLISH / SUBSCRIBE so a message published on one instance reaches
|
||||
// subscribers on every instance
|
||||
//
|
||||
// Messages are JSON-serialized. Selection happens once based on REDIS_URL.
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { getRedis, getSubscriber, isRedisEnabled } from '../redis.js';
|
||||
|
||||
export type PubSubHandler = (message: any) => void;
|
||||
|
||||
export interface PubSub {
|
||||
readonly backend: 'memory' | 'redis';
|
||||
publish(channel: string, message: any): Promise<void>;
|
||||
// Returns an unsubscribe function for this specific handler.
|
||||
subscribe(channel: string, handler: PubSubHandler): Promise<() => void>;
|
||||
}
|
||||
|
||||
// ==================== Memory implementation ====================
|
||||
|
||||
class MemoryPubSub implements PubSub {
|
||||
readonly backend = 'memory' as const;
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
constructor() {
|
||||
// SSE fan-out can attach many listeners to the same channel; lift the cap.
|
||||
this.emitter.setMaxListeners(0);
|
||||
}
|
||||
|
||||
async publish(channel: string, message: any): Promise<void> {
|
||||
this.emitter.emit(channel, message);
|
||||
}
|
||||
|
||||
async subscribe(channel: string, handler: PubSubHandler): Promise<() => void> {
|
||||
this.emitter.on(channel, handler);
|
||||
return () => this.emitter.off(channel, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Redis implementation ====================
|
||||
|
||||
class RedisPubSub implements PubSub {
|
||||
readonly backend = 'redis' as const;
|
||||
// Per-channel handler sets so a single Redis subscription fans out locally.
|
||||
private handlers = new Map<string, Set<PubSubHandler>>();
|
||||
private wired = false;
|
||||
|
||||
private ensureWired(): void {
|
||||
if (this.wired) return;
|
||||
const sub = getSubscriber();
|
||||
if (!sub) return;
|
||||
this.wired = true;
|
||||
sub.on('message', (channel: string, payload: string) => {
|
||||
const set = this.handlers.get(channel);
|
||||
if (!set || set.size === 0) return;
|
||||
let parsed: any = payload;
|
||||
try {
|
||||
parsed = JSON.parse(payload);
|
||||
} catch {
|
||||
// Leave as raw string if it was not JSON.
|
||||
}
|
||||
for (const handler of set) {
|
||||
try {
|
||||
handler(parsed);
|
||||
} catch (err: any) {
|
||||
console.error('[pubsub] handler error:', err?.message || err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async publish(channel: string, message: any): Promise<void> {
|
||||
const redis = getRedis();
|
||||
if (!redis) return;
|
||||
try {
|
||||
await redis.publish(channel, JSON.stringify(message));
|
||||
} catch (err: any) {
|
||||
console.error('[pubsub] publish error:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe(channel: string, handler: PubSubHandler): Promise<() => void> {
|
||||
this.ensureWired();
|
||||
const sub = getSubscriber();
|
||||
if (!sub) return () => undefined;
|
||||
|
||||
let set = this.handlers.get(channel);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this.handlers.set(channel, set);
|
||||
try {
|
||||
await sub.subscribe(channel);
|
||||
} catch (err: any) {
|
||||
console.error('[pubsub] subscribe error:', err?.message || err);
|
||||
}
|
||||
}
|
||||
set.add(handler);
|
||||
|
||||
return () => {
|
||||
const current = this.handlers.get(channel);
|
||||
if (!current) return;
|
||||
current.delete(handler);
|
||||
if (current.size === 0) {
|
||||
this.handlers.delete(channel);
|
||||
sub.unsubscribe(channel).catch(() => undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Selection ====================
|
||||
|
||||
let instance: PubSub | null = null;
|
||||
|
||||
export function getPubSub(): PubSub {
|
||||
if (!instance) {
|
||||
instance = isRedisEnabled() ? new RedisPubSub() : new MemoryPubSub();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
Reference in New Issue
Block a user