diff --git a/public/cynthia1.webp b/public/cynthia1.webp deleted file mode 100644 index aa4e856..0000000 Binary files a/public/cynthia1.webp and /dev/null differ diff --git a/public/darling.webp b/public/darling.webp deleted file mode 100644 index 4a04723..0000000 Binary files a/public/darling.webp and /dev/null differ diff --git a/public/granny.webp b/public/granny.webp deleted file mode 100644 index 7c94251..0000000 Binary files a/public/granny.webp and /dev/null differ diff --git a/public/numi_lily.webp b/public/numi_lily.webp deleted file mode 100644 index 65019ba..0000000 Binary files a/public/numi_lily.webp and /dev/null differ diff --git a/src/config/backgrounds.ts b/src/config/backgrounds.ts index ea5bd65..e85074f 100644 --- a/src/config/backgrounds.ts +++ b/src/config/backgrounds.ts @@ -33,7 +33,7 @@ export const backgrounds: BackgroundImage[] = [ source: "https://danbooru.donmai.us/posts/6900673", socials: [ { - name: "Artist's Twitter", + name: "Twitter", link: "https://twitter.com/ayanagi0319", }, ], @@ -45,7 +45,7 @@ export const backgrounds: BackgroundImage[] = [ source: "https://danbooru.donmai.us/posts/8743208", socials: [ { - name: "Artist's Twitter", + name: "Twitter", link: "https://x.com/kamepan44231", }, ], @@ -57,7 +57,7 @@ export const backgrounds: BackgroundImage[] = [ source: "https://danbooru.donmai.us/posts/8074444", socials: [ { - name: "Artist's Twitter", + name: "Twitter", link: "https://x.com/Ten_0123_/", }, ], @@ -69,8 +69,8 @@ export const backgrounds: BackgroundImage[] = [ source: "https://www.pixiv.net/en/artworks/124222053", socials: [ { - name: "Artist's Twitter", - link: "https://x.com/greatodoggo", + name: "Twitter", + link: "https://twitter.com/greatodoggo", }, ], }, @@ -81,7 +81,7 @@ export const backgrounds: BackgroundImage[] = [ source: "https://safebooru.org/index.php?page=post&s=view&id=5418793", socials: [ { - name: "Artist's Twitter", + name: "Twitter", link: "https://x.com/soaphisoap", }, ], @@ -93,7 +93,7 @@ export const backgrounds: BackgroundImage[] = [ source: "https://safebooru.org/index.php?page=post&s=view&id=5531630", socials: [ { - name: "Artist's Twitter", + name: "Twitter", link: "https://x.com/charlieinoni", }, ], @@ -106,59 +106,11 @@ export const backgrounds: BackgroundImage[] = [ "https://i.pximg.net/img-original/img/2025/03/03/15/13/24/127822472_p0.png", socials: [ { - name: "Artist's Twitter", + name: "Twitter", link: "https://x.com/", }, ], }, - { - url: "/cynthia1.webp", - title: "Cynthia (Pokemon)", - artist: "???", - source: "https://safebooru.org/index.php?page=post&s=view&id=5547740", - socials: [ - { - name: "Artist's Twitter", - link: "https://x.com/", - }, - ], - }, - { - url: "/darling.webp", - title: "Zero Two", - artist: "AWZ", - source: "https://www.pixiv.net/en/artworks/123110068", - socials: [ - { - name: "Pixiv Profile", - link: "https://www.pixiv.net/en/users/67355371", - }, - ], - }, - { - url: "/granny.webp", - title: "Turbo Granny (Dandadan)", - artist: "MajimartBCN", - source: "https://twitter.com/MajimartBCN/status/1864741369383694841", - socials: [ - { - name: "Artist's Twitter", - link: "https://x.com/MajimartBCN", - }, - ], - }, - { - url: "/numi_lily.webp", - title: "Nihmune & Shylily", - artist: "Gryever_AW", - source: "https://x.com/gryever_aw/status/1890772686084686118", - socials: [ - { - name: "Artist's Twitter", - link: "https://x.com/Gryever_AW", - }, - ], - }, // Add more backgrounds here! // { // url: 'your-image-url', diff --git a/src/lib/db.ts b/src/lib/db.ts index 32cea62..e28ea3c 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -7,10 +7,6 @@ export const CHAPTER_CACHE_TTL = 60 * 60; // 1 hour const MAX_RETRIES = 3; const BASE_DELAY_MS = 50; const IS_VERCEL = process.env.VERCEL || false; -const HEALTH_CHECK_INTERVAL = 60 * 1000; // Check primary health every minute -const REPLICA_PROMOTION_THRESHOLD = 5; // After 5 failed primary health checks, promote a replica to primary -const SYNC_BUFFER_SIZE = 100; // Maximum number of operations to buffer before syncing -const SYNC_INTERVAL = 10 * 1000; // Sync replicas every 10 seconds // Redis URLs from environment variables const PRIMARY_REDIS_URL = @@ -22,35 +18,13 @@ const REPLICA_URLS = [ import.meta.env.REPLICA_REDIS_URL_1 || process.env.REPLICA_REDIS_URL_1 || import.meta.env.REPLICA_REDIS_URL, - import.meta.env.REPLICA_REDIS_URL_2 || process.env.REPLICA_REPLICA_URL_2 || process.env.REPLICA_REDIS_URL_2, + import.meta.env.REPLICA_REDIS_URL_2 || process.env.REPLICA_REDIS_URL_2, import.meta.env.REPLICA_REDIS_URL_3 || process.env.REPLICA_REDIS_URL_3, ].filter(Boolean) as string[]; -// Log available Redis instances -console.log(`Primary Redis URL: ${PRIMARY_REDIS_URL ? 'Available' : 'Not configured'}`); -console.log(`Replica URLs available: ${REPLICA_URLS.length}`); - -// Singleton clients with health status tracking +// Singleton clients let primaryClient: Redis | null = null; -let primaryHealthy: boolean = true; -let primaryFailedChecks = 0; let replicaClients: Map = new Map(); -let activeReplicaUrl: string | null = null; - -// Track which replica was used when primary was down -let lastUsedReplica: string | null = null; - -// Operation buffer for sync -interface RedisOperation { - key: string; - value: string; - ttl?: number; - type: 'set' | 'del'; - timestamp: number; -} - -// Buffer to store operations that need to be synced -const operationBuffer: RedisOperation[] = []; // Redis instance naming for logging const redisInstanceNames = new Map(); @@ -83,7 +57,6 @@ async function createRedisClient(url: string): Promise { throw new Error("Redis URL not provided"); } - console.log(`Creating Redis client for ${getRedisName(url)}: ${url.slice(0, 20)}...`); const redisName = getRedisName(url); const client = new Redis(url, { @@ -109,66 +82,14 @@ async function createRedisClient(url: string): Promise { // Setup event listeners client - .on("ready", () => { - console.log(`${redisName} ready`); - - // If primary recovers and we had used a replica, initiate sync from replica to primary - if (url === PRIMARY_REDIS_URL && lastUsedReplica) { - syncFromReplicaToPrimary(lastUsedReplica).catch(err => - console.error(`Failed to sync from replica to primary: ${err}`)); - } - }) - .on("error", (err) => { - console.error(`${redisName} error:`, err); - if (url === PRIMARY_REDIS_URL) { - primaryHealthy = false; - } - }) + .on("ready", () => console.log(`${redisName} ready`)) + .on("error", (err) => console.error(`${redisName} error:`, err)) .on("reconnecting", () => console.log(`${redisName} reconnecting`)) - .on("end", () => { - console.log(`${redisName} closed`); - if (url === PRIMARY_REDIS_URL) { - primaryHealthy = false; - } - }); + .on("end", () => console.log(`${redisName} closed`)); return client; } -/** - * Initialize all replicas to establish connections early - */ -async function initializeAllClients(): Promise { - try { - // Initialize primary - if (PRIMARY_REDIS_URL && (!primaryClient || primaryClient.status !== "ready")) { - primaryClient = await createRedisClient(PRIMARY_REDIS_URL); - primaryHealthy = await checkConnection(primaryClient, "Primary"); - console.log(`Primary Redis initialized, healthy: ${primaryHealthy}`); - } - - // Initialize all replicas - for (const url of REPLICA_URLS) { - if (!replicaClients.has(url)) { - try { - const client = await createRedisClient(url); - const isHealthy = await checkConnection(client, getRedisName(url)); - if (isHealthy) { - replicaClients.set(url, client); - console.log(`Replica ${getRedisName(url)} initialized successfully`); - } - } catch (error) { - console.error(`Failed to initialize replica ${getRedisName(url)}:`, error); - } - } - } - - console.log(`Initialized ${replicaClients.size} replica connections`); - } catch (error) { - console.error("Error during client initialization:", error); - } -} - /** * Check if a Redis client is connected and responsive */ @@ -189,257 +110,69 @@ async function checkConnection(client: Redis, name: string): Promise { /** * Get an active Redis client from the pool - * Prioritizes primary, falls back to replicas */ export async function getRedisClient(): Promise { - // Try primary client first if it's considered healthy - if (primaryHealthy && (primaryClient === null || primaryClient.status !== "ready")) { - try { - primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); - const isPrimaryConnected = await checkConnection(primaryClient, "Primary"); - primaryHealthy = isPrimaryConnected; - if (isPrimaryConnected) { - primaryFailedChecks = 0; - console.log("Initialized primary Redis connection"); - return primaryClient; - } - } catch (err) { - console.error("Failed to initialize primary Redis:", err); - primaryHealthy = false; - primaryFailedChecks++; - } - } else if (primaryHealthy && primaryClient && primaryClient.status === "ready") { + // Try primary client + if (!primaryClient || primaryClient.status !== "ready") { + primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); + } + + if (await checkConnection(primaryClient, "Primary")) { return primaryClient; } - // Primary is unhealthy, try to use the active replica if we have one - if (activeReplicaUrl && replicaClients.has(activeReplicaUrl)) { - const activeReplica = replicaClients.get(activeReplicaUrl)!; - if (activeReplica.status === "ready" && await checkConnection(activeReplica, getRedisName(activeReplicaUrl))) { - console.log(`Using active replica: ${getRedisName(activeReplicaUrl)}`); - lastUsedReplica = activeReplicaUrl; // Track that we used this replica - return activeReplica; - } - } - - // No active replica, try replicas in order + // Try replicas for (const url of REPLICA_URLS) { - try { - let client = replicaClients.get(url); - if (!client || client.status !== "ready") { - client = await createRedisClient(url); - replicaClients.set(url, client); - } - - if (await checkConnection(client, getRedisName(url))) { - console.log(`Switching to replica: ${getRedisName(url)}`); - activeReplicaUrl = url; - lastUsedReplica = url; // Track that we used this replica - - // If primary has been down for a while, promote this replica to be the new primary - if (primaryFailedChecks >= REPLICA_PROMOTION_THRESHOLD) { - console.log(`Promoting ${getRedisName(url)} to primary after ${primaryFailedChecks} failed checks`); - primaryClient = client; - primaryHealthy = true; - primaryFailedChecks = 0; - } - - return client; - } - } catch (err) { - console.error(`Failed to connect to replica ${getRedisName(url)}:`, err); + let client = replicaClients.get(url); + if (!client || client.status !== "ready") { + client = await createRedisClient(url); + replicaClients.set(url, client); + } + + if (await checkConnection(client, getRedisName(url))) { + return client; } } - // If all else fails, try one more time with primary - console.warn("All Redis connections failed, making last attempt with primary"); + // Fallback: Force new primary connection primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); return primaryClient; } /** - * Get all available replicas that are not the active client + * Write data to all available Redis instances */ -async function getAvailableReplicas(activeClientUrl: string): Promise { - const availableReplicas: Redis[] = []; - - // Initialize clients if not already done - await initializeAllClients(); - - // Check each replica - for (const [url, client] of replicaClients.entries()) { - if (url !== activeClientUrl && client.status === "ready") { - try { - const isHealthy = await checkConnection(client, getRedisName(url)); - if (isHealthy) { - availableReplicas.push(client); - } - } catch (error) { - console.error(`Error checking replica ${getRedisName(url)}:`, error); - } - } - } - - console.log(`Found ${availableReplicas.length} available replicas for replication`); - return availableReplicas; -} - -/** - * Immediately replicate a Redis operation to all available replicas - */ -async function replicateOperation( - activeClientUrl: string, - key: string, - value: string, - ttl?: number +async function writeToAllRedis( + operation: (redis: Redis) => Promise ): Promise { - try { - // Get available replicas that are not the active client - const replicas = await getAvailableReplicas(activeClientUrl); - - if (replicas.length === 0) { - console.log('No available replicas for replication'); - return; - } - - console.log(`Replicating operation for key ${key} to ${replicas.length} replicas`); - - // Replicate to each available replica - const replicationPromises = replicas.map(async (replica) => { - try { - if (ttl) { - await replica.set(key, value, 'EX', ttl); - } else { - await replica.set(key, value); - } - return true; - } catch (error) { - console.error(`Replication failed for key ${key}:`, error); - return false; + const clients = [ + primaryClient, + ...Array.from(replicaClients.values()), + ].filter(Boolean) as Redis[]; + + const errors: Error[] = []; + + for (const client of clients) { + try { + if (client.status === "ready") { + await operation(client); } - }); - - const results = await Promise.allSettled(replicationPromises); - const successful = results.filter(r => r.status === 'fulfilled' && r.value === true).length; - - console.log(`Successfully replicated to ${successful}/${replicas.length} replicas`); - } catch (error) { - console.error('Error during replication:', error); + } catch (err) { + errors.push(err as Error); + console.error( + `Write failed for ${getRedisName(client.options.host || "")}:`, + err + ); + } + } + + if (errors.length === clients.length && clients.length > 0) { + throw new Error("All Redis writes failed"); } } /** - * Synchronize data from a replica to the primary - * Used when primary recovers after being down - */ -async function syncFromReplicaToPrimary(replicaUrl: string): Promise { - if (!primaryClient || primaryClient.status !== "ready" || !replicaClients.has(replicaUrl)) { - console.log("Cannot sync: primary or replica not available"); - return; - } - - const replicaClient = replicaClients.get(replicaUrl)!; - console.log(`Starting sync from ${getRedisName(replicaUrl)} to Primary...`); - - try { - // Get all feed keys using scan to prevent blocking - const feedKeys: string[] = []; - let cursor = '0'; - - do { - const [nextCursor, keys] = await replicaClient.scan( - cursor, - 'MATCH', - '[0-9a-f]*', // Match feed IDs (hex format) - 'COUNT', - '100' - ); - - cursor = nextCursor; - feedKeys.push(...keys); - } while (cursor !== '0'); - - // Also try scanning for any key pattern that might be a feed ID - cursor = '0'; - const additionalKeys: string[] = []; - - do { - const [nextCursor, keys] = await replicaClient.scan( - cursor, - 'MATCH', - 'feed:*', // Try another pattern - 'COUNT', - '100' - ); - - cursor = nextCursor; - additionalKeys.push(...keys); - } while (cursor !== '0'); - - feedKeys.push(...additionalKeys); - - // Get chapter cache keys - const chapterKeys: string[] = []; - cursor = '0'; - - do { - const [nextCursor, keys] = await replicaClient.scan( - cursor, - 'MATCH', - 'chapters:*', - 'COUNT', - '100' - ); - - cursor = nextCursor; - chapterKeys.push(...keys); - } while (cursor !== '0'); - - console.log(`Found ${feedKeys.length} feed keys and ${chapterKeys.length} chapter keys to sync`); - - // Sync feed mappings - if (feedKeys.length > 0) { - const feedPipeline = primaryClient.pipeline(); - - for (const key of feedKeys) { - const value = await replicaClient.get(key); - const ttl = await replicaClient.ttl(key); - - if (value && ttl > 0) { - feedPipeline.set(key, value, 'EX', ttl); - } - } - - await feedPipeline.exec(); - console.log(`Synced ${feedKeys.length} feed mappings from replica to primary`); - } - - // Sync chapter cache - if (chapterKeys.length > 0) { - const chapterPipeline = primaryClient.pipeline(); - - for (const key of chapterKeys) { - const value = await replicaClient.get(key); - const ttl = await replicaClient.ttl(key); - - if (value && ttl > 0) { - chapterPipeline.set(key, value, 'EX', ttl); - } - } - - await chapterPipeline.exec(); - console.log(`Synced ${chapterKeys.length} chapter caches from replica to primary`); - } - - lastUsedReplica = null; // Reset after successful sync - console.log("Replica to primary sync completed successfully"); - } catch (error) { - console.error("Error during replica to primary sync:", error); - } -} - -/** - * Store a feed mapping in Redis with immediate replication + * Store a feed mapping in Redis */ export async function storeFeedMapping( feedId: string, @@ -452,54 +185,31 @@ export async function storeFeedMapping( created_at: new Date().toISOString() }; - const stringValue = JSON.stringify(mapping); - - try { - // First write to primary/active client - const redis = await getRedisClient(); // This already handles failover - const activeUrl = redis.options.host || ''; - - await redis.set(feedId, stringValue, "EX", DEFAULT_CACHE_TTL); - console.log(`Successfully stored mapping for feed ${feedId}`); - - // Immediately replicate to other instances - await replicateOperation(activeUrl, feedId, stringValue, DEFAULT_CACHE_TTL); - } catch (error) { - console.error(`Error storing feed mapping for ${feedId}:`, error); - throw error; - } + await writeToAllRedis(async (redis) => + redis.set(feedId, JSON.stringify(mapping), "EX", DEFAULT_CACHE_TTL) + ); } /** - * Retrieve a feed mapping from Redis with automatic failover + * Retrieve a feed mapping from Redis */ export async function getFeedMapping( feedId: string ): Promise { try { - const redis = await getRedisClient(); // This already handles failover + const redis = await getRedisClient(); const mapping = await redis.get(feedId); if (!mapping) return null; // Reset expiration on access - const activeUrl = redis.options.host || ''; await redis.expire(feedId, DEFAULT_CACHE_TTL); - // Also reset TTL on other replicas - replicateOperation(activeUrl, feedId, mapping, DEFAULT_CACHE_TTL) - .catch(err => console.error(`Failed to reset TTL on replicas for ${feedId}:`, err)); - - try { - const data = JSON.parse(mapping); - return { - slugs: data.slugs, - lang: data.lang - }; - } catch (e) { - console.error(`Error parsing feed mapping for ${feedId}:`, e); - return null; - } + const data = JSON.parse(mapping); + return { + slugs: data.slugs, + lang: data.lang + }; } catch (error) { console.error(`Error retrieving feed mapping for ${feedId}:`, error); return null; @@ -517,7 +227,7 @@ export async function warmChapterCache( ): Promise { try { console.log(`Warming chapter cache for ${slugs.length} comics in ${lang}`); - const redis = await getRedisClient(); // This already handles failover + const redis = await getRedisClient(); // Validate slugs const validSlugs = slugs.filter(slug => @@ -571,12 +281,5 @@ export async function warmChapterCache( } } -// Initialize connections eagerly to establish connections as soon as possible -if (!import.meta.env.SSR) { - initializeAllClients() - .then(() => console.log("Redis clients initialized")) - .catch(err => console.error("Failed to initialize Redis clients:", err)); -} - -// Initialize connections lazily as backup +// Initialize connections lazily console.log("Redis will connect on first use");