diff --git a/astro.config.mjs b/astro.config.mjs index 778d936..56560db 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -14,7 +14,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 35af01f..2f1a161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,14 @@ "dependencies": { "@astrojs/rss": "^4.0.11", "@astrojs/tailwind": "^6.0.0", - "@astrojs/vercel": "^8.1.1", + "@astrojs/vercel": "^8.1.0", "@floating-ui/dom": "^1.6.13", "@types/node": "^22.13.9", "@vercel/analytics": "^1.5.0", "@vercel/kv": "^3.0.0", - "astro": "^5.4.2", + "astro": "^5.4.1", "feed": "^4.2.2", - "ioredis": "^5.6.0", + "ioredis": "^5.5.0", "tailwindcss": "^3.4.1" } }, @@ -122,14 +122,14 @@ } }, "node_modules/@astrojs/vercel": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.1.1.tgz", - "integrity": "sha512-4tTib9LkAtg7tqac4L+pnWEveUmYyWRXpHivaVV3lHKeQjB28KUZ+YXtR/VUG6/xpMduo+OCOOg0l1zOPq/e4w==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.1.0.tgz", + "integrity": "sha512-qaztQ0WCg8epe9+3PS7+eTbukNcwDtfjgN/QeWk0+wmmWHpEBlhzUA17Vmk5OINBX/Tf0bzXgydDWrAVG+/irQ==", "dependencies": { "@astrojs/internal-helpers": "0.6.0", - "@vercel/analytics": "^1.5.0", + "@vercel/analytics": "^1.4.1", "@vercel/edge": "^1.2.1", - "@vercel/nft": "^0.29.2", + "@vercel/nft": "^0.29.0", "@vercel/routing-utils": "^5.0.4", "esbuild": "^0.25.0", "tinyglobby": "^0.2.12" @@ -1486,6 +1486,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,21 +1534,10 @@ "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.2", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.2.tgz", - "integrity": "sha512-A/Si4mrTkQqJ6EXJKv5EYCDQ3NL6nJXxG8VGXePsaiQigsomHYQC9xSpX8qGk7AEZk4b1ssbYIqJ0ISQQ7bfcA==", + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.1.tgz", + "integrity": "sha512-6239JJM1V9b3OjvZIjbe+47/hGxMr44FEzlbTErrqzebCaoKAHK+yvahn3gdNacybDUbTxVF8Zuh0vqaeM8aKQ==", "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", @@ -1555,7 +1546,7 @@ "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", - "glob": "^10.4.5", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", @@ -1754,9 +1745,9 @@ } }, "node_modules/astro": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/astro/-/astro-5.4.2.tgz", - "integrity": "sha512-9Z3fAniIRJaK/o43OroZA1wHUIU+qHiOR9ovlVT/2XQaN25QRXScIsKWlFp0G/zrx5OuuoJ+QnaoHHW061u26A==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.4.1.tgz", + "integrity": "sha512-z9BiBz8pV7pZVn1evZv/9bJil3A9UZNyEjDFH3IjD87ioz95HHGLgbjRbbqF1EwSy8JOLKlMNyuQPqjrX8xqWw==", "dependencies": { "@astrojs/compiler": "^2.10.4", "@astrojs/internal-helpers": "0.6.0", @@ -1791,7 +1782,7 @@ "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", - "mrmime": "^2.0.1", + "mrmime": "^2.0.0", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", @@ -1803,19 +1794,19 @@ "shiki": "^1.29.2", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", - "tsconfck": "^3.1.5", + "tsconfck": "^3.1.4", "ultrahtml": "^1.5.3", "unist-util-visit": "^5.0.0", - "unstorage": "^1.15.0", + "unstorage": "^1.14.4", "vfile": "^6.0.3", "vite": "^6.2.0", - "vitefu": "^1.0.6", + "vitefu": "^1.0.5", "which-pm": "^3.0.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", - "yocto-spinner": "^0.2.1", - "zod": "^3.24.2", - "zod-to-json-schema": "^3.24.3", + "yocto-spinner": "^0.2.0", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.1", "zod-to-ts": "^1.2.0" }, "bin": { @@ -1857,20 +1848,6 @@ "crypto-js": "^4.2.0" } }, - "node_modules/astro/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/astro/node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1879,29 +1856,17 @@ "@types/estree": "^1.0.0" } }, - "node_modules/astro/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/astro/node_modules/unstorage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.15.0.tgz", - "integrity": "sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.14.4.tgz", + "integrity": "sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==", "dependencies": { "anymatch": "^3.1.3", - "chokidar": "^4.0.3", + "chokidar": "^3.6.0", "destr": "^2.0.3", - "h3": "^1.15.0", + "h3": "^1.13.0", "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.6", + "node-fetch-native": "^1.6.4", "ofetch": "^1.4.1", "ufo": "^1.5.4" }, @@ -1909,21 +1874,21 @@ "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", + "@azure/identity": "^4.5.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3", - "@deno/kv": ">=0.9.0", + "@deno/kv": ">=0.8.4", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", + "@vercel/blob": ">=0.27.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" + "uploadthing": "^7.4.1" }, "peerDependenciesMeta": { "@azure/app-configuration": { @@ -2091,11 +2056,12 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -2358,6 +2324,11 @@ "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "node_modules/consola": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", @@ -2393,9 +2364,9 @@ } }, "node_modules/crossws": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.4.tgz", - "integrity": "sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.3.tgz", + "integrity": "sha512-/71DJT3xJlqSnBr83uGJesmVHSzZEvgxHt/fIKxBAAngqMHmnBWQNxCphVxxJ2XL3xleu5+hJD6IQ3TglBedcw==", "dependencies": { "uncrypto": "^0.1.3" } @@ -2403,7 +2374,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", @@ -2816,6 +2789,11 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2854,19 +2832,20 @@ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2889,19 +2868,20 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/h3": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.1.tgz", - "integrity": "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.14.0.tgz", + "integrity": "sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==", "dependencies": { "cookie-es": "^1.2.2", - "crossws": "^0.3.3", + "crossws": "^0.3.2", "defu": "^6.1.4", "destr": "^2.0.3", "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.0", + "ohash": "^1.1.4", "radix3": "^1.1.2", "ufo": "^1.5.4", - "uncrypto": "^0.1.3" + "uncrypto": "^0.1.3", + "unenv": "^1.10.0" } }, "node_modules/hasown": { @@ -3122,10 +3102,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "node_modules/ioredis": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", - "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", + "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", @@ -4225,18 +4220,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "*" } }, "node_modules/minipass": { @@ -4274,9 +4277,9 @@ } }, "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "engines": { "node": ">=10" } @@ -4367,11 +4370,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-mock-http": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", - "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==" - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -4433,6 +4431,19 @@ "ufo": "^1.5.4" } }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oniguruma-to-es": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", @@ -4557,6 +4568,14 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4596,6 +4615,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5169,6 +5193,47 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.32.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", @@ -5515,6 +5580,47 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5668,9 +5774,9 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tsconfck": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", - "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", + "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -5736,6 +5842,18 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, + "node_modules/unenv": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", + "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", + "dependencies": { + "consola": "^3.2.3", + "defu": "^6.1.4", + "mime": "^3.0.0", + "node-fetch-native": "^1.6.4", + "pathe": "^1.1.2" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -5951,9 +6069,9 @@ } }, "node_modules/vite": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", - "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", @@ -6021,9 +6139,9 @@ } }, "node_modules/vitefu": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", - "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz", + "integrity": "sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==", "workspaces": [ "tests/deps/*", "tests/projects/*" @@ -6191,6 +6309,11 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "node_modules/xml-js": { "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", @@ -6246,9 +6369,9 @@ } }, "node_modules/yocto-spinner": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.1.tgz", - "integrity": "sha512-lHHxjh0bXaLgdJy3cNnVb/F9myx3CkhrvSOEVTkaUgNMXnYFa2xYPVhtGnqhh3jErY2gParBOHallCbc7NrlZQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.0.tgz", + "integrity": "sha512-Qu6WAqNLGleB687CCGcmgHIo8l+J19MX/32UrSMfbf/4L8gLoxjpOYoiHT1asiWyqvjRZbgvOhLlvne6E5Tbdw==", "dependencies": { "yoctocolors": "^2.1.1" }, @@ -6271,17 +6394,17 @@ } }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", - "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", + "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index a9fcfa5..de31832 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,14 @@ "dependencies": { "@astrojs/rss": "^4.0.11", "@astrojs/tailwind": "^6.0.0", - "@astrojs/vercel": "^8.1.1", + "@astrojs/vercel": "^8.1.0", "@floating-ui/dom": "^1.6.13", "@types/node": "^22.13.9", "@vercel/analytics": "^1.5.0", "@vercel/kv": "^3.0.0", - "astro": "^5.4.2", + "astro": "^5.4.1", "feed": "^4.2.2", - "ioredis": "^5.6.0", + "ioredis": "^5.5.0", "tailwindcss": "^3.4.1" } } diff --git a/src/lib/db.ts b/src/lib/db.ts index a903508..5fd885a 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -7,9 +7,16 @@ interface RedisNetworkError extends Error { code?: string; } -// Primary Redis configuration +// Primary Redis configuration - with more robust fallbacks and logging const PRIMARY_REDIS_URL = import.meta.env.PRIMARY_REDIS_URL || process.env.PRIMARY_REDIS_URL || process.env.REDIS_URL; +// Log Redis URL status (without exposing sensitive data) +console.log(`Redis URLs status: Primary=${Boolean(PRIMARY_REDIS_URL)}`); + +if (!PRIMARY_REDIS_URL) { + console.warn('WARNING: No Redis URL configured. Database functionality will not work properly.'); +} + // Multiple replicas support const REPLICA_URLS = [ import.meta.env.REPLICA_REDIS_URL_1 || process.env.REPLICA_REDIS_URL_1 || import.meta.env.REPLICA_REDIS_URL || process.env.REPLICA_REDIS_URL, @@ -17,6 +24,8 @@ const REPLICA_URLS = [ import.meta.env.REPLICA_REDIS_URL_3 || process.env.REPLICA_REDIS_URL_3, ].filter(Boolean) as string[]; +console.log(`Found ${REPLICA_URLS.length} Redis replica URLs`); + // Create a mapping of URLs to friendly names for logging const redisInstanceNames = new Map(); @@ -44,8 +53,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 +62,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,138 +103,60 @@ export async function getRedisClient(): Promise { console.log('Running in Vercel environment - using optimized Redis connection strategy'); } - // Try primary first - if (primaryClient?.status === 'ready') { + // Check if Redis is configured + if (!PRIMARY_REDIS_URL && REPLICA_URLS.length === 0) { + throw new Error('No Redis URLs configured. Please check your environment variables in the Vercel dashboard.'); + } + + try { + // Try primary first + if (!primaryClient) { + console.log('Creating new primary Redis client'); + if (!PRIMARY_REDIS_URL) { + throw new Error('Primary Redis URL is not set. Please check your environment variables.'); + } + 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')) { - console.log('Encountered ETIMEDOUT in Vercel environment. Retrying with longer timeout...'); - try { - // Try one more time with increased timeout - const tempClient = new Redis(PRIMARY_REDIS_URL!, { - connectTimeout: 20000, // Increased timeout for last-ditch effort - retryStrategy: () => null // No retries for this attempt - }); - - // Wait for ready or error - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - tempClient.disconnect(); - reject(new Error('Final connection attempt timed out')); - }, 20000); - - tempClient.once('ready', () => { - clearTimeout(timeout); - resolve(true); - }); - - tempClient.once('error', (err) => { - clearTimeout(timeout); - reject(err); - }); - }); - - // If we get here, connection was successful - primaryClient = tempClient; - return tempClient; - } catch (finalError) { - console.error('Final Redis connection attempt failed:', finalError); + if (IS_VERCEL && primaryError instanceof Error) { + const errorMsg = primaryError.message; + + if (errorMsg.includes('ETIMEDOUT')) { + console.log('Encountered ETIMEDOUT in Vercel environment. This is a common issue with Vercel serverless functions and Redis.'); + throw new Error('Redis connection timed out. Please ensure your Redis service allows connections from Vercel IP ranges. See Vercel documentation for IP ranges used by their serverless functions.'); + } else if (errorMsg.includes('ECONNREFUSED')) { + throw new Error('Redis connection refused. Please check if your Redis service is running and accessible from Vercel.'); + } else if (errorMsg.includes('ENOTFOUND')) { + throw new Error('Redis host not found. Please check your PRIMARY_REDIS_URL environment variable.'); } } - // Try connecting to any replica that might be available - for (const url of REPLICA_URLS) { - try { - const redisName = getRedisName(url); - console.log(`Trying to connect to ${redisName}...`); - const client = await createRedisClient(url); - - client - .on('ready', () => console.log(`${redisName} connection ready`)) - .on('error', (err: RedisNetworkError) => { - // Log more details for connection errors - if (err.code === 'ETIMEDOUT') { - console.error(`${redisName} 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(`${redisName} connection refused. Ensure the Redis server is running and the URL is correct.`); - } else if (err.code === 'ENOTFOUND') { - console.error(`${redisName} host not found. Check your Redis URL environment variables.`); - } else { - console.error(`${redisName} error:`, err); - } - }) - .on('reconnecting', () => console.log(`${redisName} reconnecting`)) - .on('end', () => console.log(`${redisName} connection closed`)); - - replicaClients.set(url, client); - return client; - } catch (replicaError) { - console.error(`Failed to connect to ${getRedisName(url)}:`, replicaError); - // Continue to next replica - } - } - - throw new Error('All Redis connections failed'); + throw primaryError; } } @@ -338,24 +277,57 @@ export async function warmChapterCache(slugs: string[], lang: string): Promise { + if (!feedId) throw new Error('Feed ID is required'); + if (!Array.isArray(slugs) || slugs.length === 0) throw new Error('Slugs array is required and cannot be empty'); + if (!lang) throw new Error('Language is required'); + const mapping = { slugs, lang, created_at: new Date().toISOString() }; + console.log(`Attempting to store feed mapping with ID: ${feedId}, language: ${lang}, and ${slugs.length} slugs`); + + // Check if Redis is configured + if (!PRIMARY_REDIS_URL && REPLICA_URLS.length === 0) { + console.error('No Redis URLs configured. Cannot store feed mapping.'); + throw new Error('Database connection not configured. Please check your environment variables.'); + } + try { await writeToAllRedis(async (redis) => { - await redis.set( - feedId, - JSON.stringify(mapping), - 'EX', - DEFAULT_CACHE_TTL - ); + console.log(`Writing feed mapping to Redis instance...`); + + try { + await redis.set( + feedId, + JSON.stringify(mapping), + 'EX', + DEFAULT_CACHE_TTL + ); + console.log(`Successfully stored feed mapping in Redis instance`); + } catch (redisError: any) { + console.error(`Redis set operation failed:`, redisError); + throw new Error(`Redis operation failed: ${redisError.message || 'Unknown Redis error'}`); + } }); - } catch (error) { + + console.log(`Feed mapping successfully stored with ID: ${feedId}`); + } catch (error: any) { console.error('Error storing feed mapping:', error); - throw new Error('Failed to store feed mapping'); + + // Check for common connection issues + const errorMsg = error.message || ''; + if (errorMsg.includes('ECONNREFUSED')) { + throw new Error('Unable to connect to the Redis database. Connection refused.'); + } else if (errorMsg.includes('ETIMEDOUT')) { + throw new Error('Connection to Redis database timed out. This may be due to network issues or firewall settings.'); + } else if (errorMsg.includes('ENOTFOUND')) { + throw new Error('Redis host not found. Please check your database URL configuration.'); + } else { + throw new Error(`Failed to store feed mapping: ${errorMsg}`); + } } } 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' + } + }); } };