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,111 @@
|
||||
// Distributed lock abstraction with two implementations:
|
||||
// - memory: per-process key set with TTL (only meaningful within one instance)
|
||||
// - redis: SET key token NX PX ttl, released with a compare-and-delete Lua
|
||||
// script so only the holder can release it
|
||||
//
|
||||
// Use acquire/release for long-lived ownership (e.g. a background poller) and
|
||||
// withLock for a one-shot critical section. Selection is based on REDIS_URL.
|
||||
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getRedis, isRedisEnabled } from '../redis.js';
|
||||
|
||||
export interface Lock {
|
||||
readonly backend: 'memory' | 'redis';
|
||||
// Returns a token when the lock was acquired, or null when already held.
|
||||
acquire(key: string, ttlMs: number): Promise<string | null>;
|
||||
release(key: string, token: string): Promise<void>;
|
||||
// Runs fn while holding the lock; returns fn's result, or null if not acquired.
|
||||
withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null>;
|
||||
}
|
||||
|
||||
// ==================== Memory implementation ====================
|
||||
|
||||
class MemoryLock implements Lock {
|
||||
readonly backend = 'memory' as const;
|
||||
private held = new Map<string, { token: string; expiresAt: number }>();
|
||||
|
||||
async acquire(key: string, ttlMs: number): Promise<string | null> {
|
||||
const existing = this.held.get(key);
|
||||
const now = Date.now();
|
||||
if (existing && existing.expiresAt > now) {
|
||||
return null;
|
||||
}
|
||||
const token = randomUUID();
|
||||
this.held.set(key, { token, expiresAt: now + ttlMs });
|
||||
return token;
|
||||
}
|
||||
|
||||
async release(key: string, token: string): Promise<void> {
|
||||
const existing = this.held.get(key);
|
||||
if (existing && existing.token === token) {
|
||||
this.held.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
async withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null> {
|
||||
const token = await this.acquire(key, ttlMs);
|
||||
if (!token) return null;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await this.release(key, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Redis implementation ====================
|
||||
|
||||
const RELEASE_SCRIPT =
|
||||
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end';
|
||||
|
||||
class RedisLock implements Lock {
|
||||
readonly backend = 'redis' as const;
|
||||
|
||||
async acquire(key: string, ttlMs: number): Promise<string | null> {
|
||||
const redis = getRedis();
|
||||
if (!redis) {
|
||||
// Redis configured but unavailable: do not block critical sections.
|
||||
return randomUUID();
|
||||
}
|
||||
const token = randomUUID();
|
||||
try {
|
||||
const result = await redis.set(`lock:${key}`, token, 'PX', ttlMs, 'NX');
|
||||
return result === 'OK' ? token : null;
|
||||
} catch (err: any) {
|
||||
console.error('[lock] redis acquire error, proceeding without lock:', err?.message || err);
|
||||
// Fail open so a Redis outage does not deadlock startup or jobs.
|
||||
return randomUUID();
|
||||
}
|
||||
}
|
||||
|
||||
async release(key: string, token: string): Promise<void> {
|
||||
const redis = getRedis();
|
||||
if (!redis) return;
|
||||
try {
|
||||
await redis.eval(RELEASE_SCRIPT, 1, `lock:${key}`, token);
|
||||
} catch (err: any) {
|
||||
console.error('[lock] redis release error:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
async withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null> {
|
||||
const token = await this.acquire(key, ttlMs);
|
||||
if (!token) return null;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await this.release(key, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Selection ====================
|
||||
|
||||
let instance: Lock | null = null;
|
||||
|
||||
export function getLock(): Lock {
|
||||
if (!instance) {
|
||||
instance = isRedisEnabled() ? new RedisLock() : new MemoryLock();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
Reference in New Issue
Block a user