diff --git a/astro.config.mjs b/astro.config.mjs index 778d936..65ec26b 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,7 +4,11 @@ import tailwind from "@astrojs/tailwind"; export default defineConfig({ output: "server", - adapter: vercel(), + adapter: vercel({ + analytics: true, + maxDuration: 60, + includeFiles: ['.env'], + }), image: { remotePatterns: [{ protocol: "https" }], }, @@ -14,7 +18,13 @@ export default defineConfig({ 'process.env.PRIMARY_REDIS_URL': JSON.stringify(process.env.PRIMARY_REDIS_URL || process.env.REDIS_URL), 'process.env.REPLICA_REDIS_URL_1': JSON.stringify(process.env.REPLICA_REDIS_URL_1 || process.env.REPLICA_REDIS_URL), 'process.env.REPLICA_REDIS_URL_2': JSON.stringify(process.env.REPLICA_REDIS_URL_2), - 'process.env.REPLICA_REDIS_URL_3': JSON.stringify(process.env.REPLICA_REDIS_URL_3) - } + 'process.env.REPLICA_REDIS_URL_3': JSON.stringify(process.env.REPLICA_REDIS_URL_3), + 'process.env.VERCEL': JSON.stringify(process.env.VERCEL || false) + }, + server: { + watch: { + usePolling: true, + }, + }, } }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 853b281..b1d678c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@floating-ui/dom": "^1.6.13", "@types/node": "^22.13.9", "@vercel/analytics": "^1.5.0", - "@vercel/kv": "^3.0.0", "astro": "^5.4.1", "feed": "^4.2.2", "ioredis": "^5.5.0", @@ -1486,6 +1485,8 @@ "version": "1.34.3", "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.34.3.tgz", "integrity": "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==", + "optional": true, + "peer": true, "dependencies": { "crypto-js": "^4.2.0" } @@ -1532,17 +1533,6 @@ "resolved": "https://registry.npmjs.org/@vercel/edge/-/edge-1.2.1.tgz", "integrity": "sha512-1++yncEyIAi68D3UEOlytYb1IUcIulMWdoSzX2h9LuSeeyR7JtaIgR8DcTQ6+DmYOQn+5MCh6LY+UmK6QBByNA==" }, - "node_modules/@vercel/kv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-3.0.0.tgz", - "integrity": "sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==", - "dependencies": { - "@upstash/redis": "^1.34.0" - }, - "engines": { - "node": ">=14.6" - } - }, "node_modules/@vercel/nft": { "version": "0.29.1", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.1.tgz", @@ -2383,7 +2373,9 @@ "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "optional": true, + "peer": true }, "node_modules/cssesc": { "version": "3.0.0", diff --git a/package.json b/package.json index de31832..0f7c050 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@floating-ui/dom": "^1.6.13", "@types/node": "^22.13.9", "@vercel/analytics": "^1.5.0", - "@vercel/kv": "^3.0.0", "astro": "^5.4.1", "feed": "^4.2.2", "ioredis": "^5.5.0", diff --git a/src/lib/db.ts b/src/lib/db.ts index a903508..6a8bfba 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -44,8 +44,8 @@ let replicaClients: Map = new Map(); const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days const MAX_RETRIES = 5; const BASE_DELAY_MS = 100; -const IS_VERCEL = process.env.VERCEL || false; -const VERCEL_CONNECTION_TIMEOUT = 15000; // 15 seconds for Vercel +const IS_VERCEL = process.env.VERCEL || import.meta.env.VERCEL || false; +const VERCEL_CONNECTION_TIMEOUT = 25000; // 25 seconds for Vercel const LOCAL_CONNECTION_TIMEOUT = 5000; // 5 seconds for local // Improved connection handling with exponential backoff @@ -53,26 +53,34 @@ async function createRedisClient(url: string): Promise { if (!url) throw new Error('Redis URL is not set'); const redisName = getRedisName(url); + console.log(`Connecting to ${redisName} Redis instance in ${IS_VERCEL ? 'Vercel' : 'local'} environment`); // Parse URL to extract authentication info const options = { retryStrategy: (times) => { - if (times >= MAX_RETRIES) return null; + if (times >= MAX_RETRIES) { + console.error(`${redisName} max retries (${MAX_RETRIES}) reached, giving up`); + return null; + } const delay = Math.min(BASE_DELAY_MS * Math.pow(2, times), 5000); console.log(`${redisName} retry attempt ${times}, next try in ${delay}ms`); return delay + Math.random() * 200; // Add jitter }, - maxRetriesPerRequest: null, // Allow retries for queued commands - enableOfflineQueue: true, // Enable command queuing + maxRetriesPerRequest: 3, // Limit retries for queued commands + enableOfflineQueue: true, // Enable command queuing enableReadyCheck: true, - connectTimeout: 15000, // Increase connection timeout to 15 seconds for Vercel's serverless environment + connectTimeout: IS_VERCEL ? VERCEL_CONNECTION_TIMEOUT : LOCAL_CONNECTION_TIMEOUT, disconnectTimeout: 10000, // Increase disconnect timeout to 10 seconds keepAlive: 10000, // Send keepalive every 10 seconds - commandTimeout: 10000, // Command timeout + commandTimeout: 15000, // Command timeout retryMaxDelay: 5000, // Maximum delay between retries reconnectOnError: (err) => { const targetErrors = ['READONLY', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND']; - return targetErrors.some(e => err.message.includes(e)); + const shouldReconnect = targetErrors.some(e => err.message.includes(e)); + if (shouldReconnect) { + console.log(`Reconnecting due to error: ${err.message}`); + } + return shouldReconnect; } }; @@ -86,71 +94,39 @@ export async function getRedisClient(): Promise { console.log('Running in Vercel environment - using optimized Redis connection strategy'); } - // Try primary first - if (primaryClient?.status === 'ready') { + try { + // Try primary first + if (!primaryClient) { + console.log('Creating new primary Redis client'); + primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); + } else if (primaryClient.status !== 'ready') { + console.log('Primary Redis client not ready, reconnecting'); + try { + await primaryClient.disconnect(); + } catch (e) { + console.error('Error disconnecting primary client:', e); + } + primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); + } + try { // Add timeout to ping const pingPromise = primaryClient.ping(); const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Ping timeout')), 3000) + setTimeout(() => reject(new Error('Ping timeout')), 5000) ); await Promise.race([pingPromise, timeoutPromise]); return primaryClient; } catch (error) { - console.warn('Primary Redis connection failed, will try replicas:', error); - // Fall through to try replicas + console.error('Primary Redis client ping failed:', error); + throw error; } - } - - // Try replicas in sequence - for (const [url, client] of replicaClients.entries()) { - if (client.status === 'ready') { - try { - // Add timeout to ping - const pingPromise = client.ping(); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Ping timeout')), 3000) - ); - - await Promise.race([pingPromise, timeoutPromise]); - console.log(`Using ${getRedisName(url)}`); - return client; - } catch (error) { - console.warn(`${getRedisName(url)} connection failed:`, error); - // Continue to next replica - } - } - } - - // Reconnect to primary - try { - console.log('Initializing new primary Redis connection...'); - primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); - - primaryClient - .on('ready', () => console.log('Primary Redis connection ready')) - .on('error', (err: RedisNetworkError) => { - // Log more details for connection errors - if (err.code === 'ETIMEDOUT') { - console.error('Primary Redis connection timeout. This is common in serverless environments like Vercel. Check your network connectivity and Redis server availability.'); - } else if (err.code === 'ECONNREFUSED') { - console.error('Primary Redis connection refused. Ensure the Redis server is running and the URL is correct.'); - } else if (err.code === 'ENOTFOUND') { - console.error('Primary Redis host not found. Check your PRIMARY_REDIS_URL environment variable.'); - } else { - console.error('Primary Redis error:', err); - } - }) - .on('reconnecting', () => console.log('Primary Redis reconnecting')) - .on('end', () => console.log('Primary Redis connection closed')); - - return primaryClient; - } catch (error) { - console.error('Failed to connect to primary Redis:', error); + } catch (primaryError) { + console.error('Error connecting to primary Redis:', primaryError); // Special handling for Vercel ETIMEDOUT errors - if (IS_VERCEL && error instanceof Error && error.message.includes('ETIMEDOUT')) { + if (IS_VERCEL && primaryError instanceof Error && primaryError.message.includes('ETIMEDOUT')) { console.log('Encountered ETIMEDOUT in Vercel environment. Retrying with longer timeout...'); try { // Try one more time with increased timeout diff --git a/src/pages/api/create-feed.ts b/src/pages/api/create-feed.ts index 68db19d..2eeb00c 100644 --- a/src/pages/api/create-feed.ts +++ b/src/pages/api/create-feed.ts @@ -7,18 +7,52 @@ const feedMappings = new Map(); export const POST: APIRoute = async ({ request }) => { try { - const { slugs, lang } = await request.json(); + console.log("Handling create-feed API request"); + + if (!request.body) { + console.error("Request has no body"); + return new Response("Request body is required", { status: 400 }); + } + + let body; + try { + body = await request.json(); + } catch (parseError) { + console.error("Failed to parse request JSON:", parseError); + return new Response("Invalid JSON in request body", { status: 400 }); + } + + const { slugs, lang } = body; if (!Array.isArray(slugs) || slugs.length === 0) { + console.error("Invalid slugs array:", slugs); return new Response("Invalid slugs array", { status: 400 }); } + if (!lang) { + console.error("Language not specified"); + return new Response("Language is required", { status: 400 }); + } + + console.log(`Creating feed for ${slugs.length} slugs in language: ${lang}`); + // Create a unique hash based on slugs and language const dataString = `${slugs.sort().join(',')}:${lang}`; const hash = createHash('md5').update(dataString).digest('hex').slice(0, 8); - // Store the mapping in Vercel KV - await storeFeedMapping(hash, slugs, lang); + try { + // Store the mapping in Redis + await storeFeedMapping(hash, slugs, lang); + console.log(`Successfully created feed with ID: ${hash}`); + } catch (dbError: any) { + console.error("Database error while storing feed mapping:", dbError); + return new Response(`Database error: ${dbError.message || "Unknown error"}`, { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }); + } return new Response(JSON.stringify({ feedId: hash }), { status: 200, @@ -26,9 +60,18 @@ export const POST: APIRoute = async ({ request }) => { 'Content-Type': 'application/json' } }); - } catch (error) { - console.error('Error creating feed:', error); - return new Response("Internal server error", { status: 500 }); + } catch (error: any) { + console.error('Unexpected error creating feed:', error); + return new Response(JSON.stringify({ + error: "Internal server error", + message: error.message || "Unknown error", + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }); } };