Compare commits

...

No commits in common. "600c3b0e1f6507e8be53a7151baf2444f316cc46" and "fd14c1a80ccc2f091ee2bcec668a88062b9a3f73" have entirely different histories.

5 changed files with 276 additions and 420 deletions

View file

@ -14,13 +14,7 @@ export default defineConfig({
'process.env.PRIMARY_REDIS_URL': JSON.stringify(process.env.PRIMARY_REDIS_URL || process.env.REDIS_URL), '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_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_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,
},
},
} }
}); });

385
package-lock.json generated
View file

@ -10,14 +10,14 @@
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.1.0", "@astrojs/vercel": "^8.1.1",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.9", "@types/node": "^22.13.9",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"astro": "^5.4.1", "astro": "^5.4.2",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.5.0", "ioredis": "^5.6.0",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
} }
}, },
@ -122,14 +122,14 @@
} }
}, },
"node_modules/@astrojs/vercel": { "node_modules/@astrojs/vercel": {
"version": "8.1.0", "version": "8.1.1",
"resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.1.0.tgz", "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.1.1.tgz",
"integrity": "sha512-qaztQ0WCg8epe9+3PS7+eTbukNcwDtfjgN/QeWk0+wmmWHpEBlhzUA17Vmk5OINBX/Tf0bzXgydDWrAVG+/irQ==", "integrity": "sha512-4tTib9LkAtg7tqac4L+pnWEveUmYyWRXpHivaVV3lHKeQjB28KUZ+YXtR/VUG6/xpMduo+OCOOg0l1zOPq/e4w==",
"dependencies": { "dependencies": {
"@astrojs/internal-helpers": "0.6.0", "@astrojs/internal-helpers": "0.6.0",
"@vercel/analytics": "^1.4.1", "@vercel/analytics": "^1.5.0",
"@vercel/edge": "^1.2.1", "@vercel/edge": "^1.2.1",
"@vercel/nft": "^0.29.0", "@vercel/nft": "^0.29.2",
"@vercel/routing-utils": "^5.0.4", "@vercel/routing-utils": "^5.0.4",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"tinyglobby": "^0.2.12" "tinyglobby": "^0.2.12"
@ -1486,8 +1486,6 @@
"version": "1.34.3", "version": "1.34.3",
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.34.3.tgz", "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.34.3.tgz",
"integrity": "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==", "integrity": "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"crypto-js": "^4.2.0" "crypto-js": "^4.2.0"
} }
@ -1534,10 +1532,21 @@
"resolved": "https://registry.npmjs.org/@vercel/edge/-/edge-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@vercel/edge/-/edge-1.2.1.tgz",
"integrity": "sha512-1++yncEyIAi68D3UEOlytYb1IUcIulMWdoSzX2h9LuSeeyR7JtaIgR8DcTQ6+DmYOQn+5MCh6LY+UmK6QBByNA==" "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": { "node_modules/@vercel/nft": {
"version": "0.29.1", "version": "0.29.2",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.1.tgz", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.2.tgz",
"integrity": "sha512-6239JJM1V9b3OjvZIjbe+47/hGxMr44FEzlbTErrqzebCaoKAHK+yvahn3gdNacybDUbTxVF8Zuh0vqaeM8aKQ==", "integrity": "sha512-A/Si4mrTkQqJ6EXJKv5EYCDQ3NL6nJXxG8VGXePsaiQigsomHYQC9xSpX8qGk7AEZk4b1ssbYIqJ0ISQQ7bfcA==",
"dependencies": { "dependencies": {
"@mapbox/node-pre-gyp": "^2.0.0", "@mapbox/node-pre-gyp": "^2.0.0",
"@rollup/pluginutils": "^5.1.3", "@rollup/pluginutils": "^5.1.3",
@ -1546,7 +1555,7 @@
"async-sema": "^3.1.1", "async-sema": "^3.1.1",
"bindings": "^1.4.0", "bindings": "^1.4.0",
"estree-walker": "2.0.2", "estree-walker": "2.0.2",
"glob": "^7.1.3", "glob": "^10.4.5",
"graceful-fs": "^4.2.9", "graceful-fs": "^4.2.9",
"node-gyp-build": "^4.2.2", "node-gyp-build": "^4.2.2",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
@ -1745,9 +1754,9 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "5.4.1", "version": "5.4.2",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.4.1.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-5.4.2.tgz",
"integrity": "sha512-z9BiBz8pV7pZVn1evZv/9bJil3A9UZNyEjDFH3IjD87ioz95HHGLgbjRbbqF1EwSy8JOLKlMNyuQPqjrX8xqWw==", "integrity": "sha512-9Z3fAniIRJaK/o43OroZA1wHUIU+qHiOR9ovlVT/2XQaN25QRXScIsKWlFp0G/zrx5OuuoJ+QnaoHHW061u26A==",
"dependencies": { "dependencies": {
"@astrojs/compiler": "^2.10.4", "@astrojs/compiler": "^2.10.4",
"@astrojs/internal-helpers": "0.6.0", "@astrojs/internal-helpers": "0.6.0",
@ -1782,7 +1791,7 @@
"kleur": "^4.1.5", "kleur": "^4.1.5",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"magicast": "^0.3.5", "magicast": "^0.3.5",
"mrmime": "^2.0.0", "mrmime": "^2.0.1",
"neotraverse": "^0.6.18", "neotraverse": "^0.6.18",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
@ -1794,19 +1803,19 @@
"shiki": "^1.29.2", "shiki": "^1.29.2",
"tinyexec": "^0.3.2", "tinyexec": "^0.3.2",
"tinyglobby": "^0.2.12", "tinyglobby": "^0.2.12",
"tsconfck": "^3.1.4", "tsconfck": "^3.1.5",
"ultrahtml": "^1.5.3", "ultrahtml": "^1.5.3",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"unstorage": "^1.14.4", "unstorage": "^1.15.0",
"vfile": "^6.0.3", "vfile": "^6.0.3",
"vite": "^6.2.0", "vite": "^6.2.0",
"vitefu": "^1.0.5", "vitefu": "^1.0.6",
"which-pm": "^3.0.1", "which-pm": "^3.0.1",
"xxhash-wasm": "^1.1.0", "xxhash-wasm": "^1.1.0",
"yargs-parser": "^21.1.1", "yargs-parser": "^21.1.1",
"yocto-spinner": "^0.2.0", "yocto-spinner": "^0.2.1",
"zod": "^3.24.1", "zod": "^3.24.2",
"zod-to-json-schema": "^3.24.1", "zod-to-json-schema": "^3.24.3",
"zod-to-ts": "^1.2.0" "zod-to-ts": "^1.2.0"
}, },
"bin": { "bin": {
@ -1848,6 +1857,20 @@
"crypto-js": "^4.2.0" "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": { "node_modules/astro/node_modules/estree-walker": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@ -1856,17 +1879,29 @@
"@types/estree": "^1.0.0" "@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": { "node_modules/astro/node_modules/unstorage": {
"version": "1.14.4", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.14.4.tgz", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.15.0.tgz",
"integrity": "sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==", "integrity": "sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg==",
"dependencies": { "dependencies": {
"anymatch": "^3.1.3", "anymatch": "^3.1.3",
"chokidar": "^3.6.0", "chokidar": "^4.0.3",
"destr": "^2.0.3", "destr": "^2.0.3",
"h3": "^1.13.0", "h3": "^1.15.0",
"lru-cache": "^10.4.3", "lru-cache": "^10.4.3",
"node-fetch-native": "^1.6.4", "node-fetch-native": "^1.6.6",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"ufo": "^1.5.4" "ufo": "^1.5.4"
}, },
@ -1874,21 +1909,21 @@
"@azure/app-configuration": "^1.8.0", "@azure/app-configuration": "^1.8.0",
"@azure/cosmos": "^4.2.0", "@azure/cosmos": "^4.2.0",
"@azure/data-tables": "^13.3.0", "@azure/data-tables": "^13.3.0",
"@azure/identity": "^4.5.0", "@azure/identity": "^4.6.0",
"@azure/keyvault-secrets": "^4.9.0", "@azure/keyvault-secrets": "^4.9.0",
"@azure/storage-blob": "^12.26.0", "@azure/storage-blob": "^12.26.0",
"@capacitor/preferences": "^6.0.3", "@capacitor/preferences": "^6.0.3",
"@deno/kv": ">=0.8.4", "@deno/kv": ">=0.9.0",
"@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0",
"@planetscale/database": "^1.19.0", "@planetscale/database": "^1.19.0",
"@upstash/redis": "^1.34.3", "@upstash/redis": "^1.34.3",
"@vercel/blob": ">=0.27.0", "@vercel/blob": ">=0.27.1",
"@vercel/kv": "^1.0.1", "@vercel/kv": "^1.0.1",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"db0": ">=0.2.1", "db0": ">=0.2.1",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
"uploadthing": "^7.4.1" "uploadthing": "^7.4.4"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@azure/app-configuration": { "@azure/app-configuration": {
@ -2056,12 +2091,11 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0"
"concat-map": "0.0.1"
} }
}, },
"node_modules/braces": { "node_modules/braces": {
@ -2324,11 +2358,6 @@
"resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz",
"integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==" "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": { "node_modules/consola": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz",
@ -2364,9 +2393,9 @@
} }
}, },
"node_modules/crossws": { "node_modules/crossws": {
"version": "0.3.3", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.3.tgz", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.4.tgz",
"integrity": "sha512-/71DJT3xJlqSnBr83uGJesmVHSzZEvgxHt/fIKxBAAngqMHmnBWQNxCphVxxJ2XL3xleu5+hJD6IQ3TglBedcw==", "integrity": "sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==",
"dependencies": { "dependencies": {
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
@ -2374,9 +2403,7 @@
"node_modules/crypto-js": { "node_modules/crypto-js": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "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": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
@ -2789,11 +2816,6 @@
"url": "https://github.com/sponsors/rawify" "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2832,20 +2854,19 @@
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "foreground-child": "^3.1.0",
"inflight": "^1.0.4", "jackspeak": "^3.1.2",
"inherits": "2", "minimatch": "^9.0.4",
"minimatch": "^3.1.1", "minipass": "^7.1.2",
"once": "^1.3.0", "package-json-from-dist": "^1.0.0",
"path-is-absolute": "^1.0.0" "path-scurry": "^1.11.1"
}, },
"engines": { "bin": {
"node": "*" "glob": "dist/esm/bin.mjs"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@ -2868,20 +2889,19 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
}, },
"node_modules/h3": { "node_modules/h3": {
"version": "1.14.0", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.14.0.tgz", "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.1.tgz",
"integrity": "sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==", "integrity": "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==",
"dependencies": { "dependencies": {
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
"crossws": "^0.3.2", "crossws": "^0.3.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
"iron-webcrypto": "^1.2.1", "iron-webcrypto": "^1.2.1",
"ohash": "^1.1.4", "node-mock-http": "^1.0.0",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3"
"unenv": "^1.10.0"
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
@ -3102,25 +3122,10 @@
"url": "https://github.com/sponsors/wooorm" "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": { "node_modules/ioredis": {
"version": "5.5.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz",
"integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==",
"dependencies": { "dependencies": {
"@ioredis/commands": "^1.1.1", "@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
@ -4220,26 +4225,18 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"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": ">=10.0.0"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^2.0.1"
}, },
"engines": { "engines": {
"node": "*" "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minipass": { "node_modules/minipass": {
@ -4277,9 +4274,9 @@
} }
}, },
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@ -4370,6 +4367,11 @@
"node-gyp-build-test": "build-test.js" "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": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@ -4431,19 +4433,6 @@
"ufo": "^1.5.4" "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": { "node_modules/oniguruma-to-es": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz",
@ -4568,14 +4557,6 @@
"node": ">=8" "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": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -4615,11 +4596,6 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -5193,47 +5169,6 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/rollup": {
"version": "4.32.1", "version": "4.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
@ -5580,47 +5515,6 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@ -5774,9 +5668,9 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
}, },
"node_modules/tsconfck": { "node_modules/tsconfck": {
"version": "3.1.4", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz",
"integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==",
"bin": { "bin": {
"tsconfck": "bin/tsconfck.js" "tsconfck": "bin/tsconfck.js"
}, },
@ -5842,18 +5736,6 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" "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": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@ -6069,9 +5951,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"postcss": "^8.5.3", "postcss": "^8.5.3",
@ -6139,9 +6021,9 @@
} }
}, },
"node_modules/vitefu": { "node_modules/vitefu": {
"version": "1.0.5", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
"integrity": "sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==", "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
"workspaces": [ "workspaces": [
"tests/deps/*", "tests/deps/*",
"tests/projects/*" "tests/projects/*"
@ -6309,11 +6191,6 @@
"node": ">=8" "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": { "node_modules/xml-js": {
"version": "1.6.11", "version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
@ -6369,9 +6246,9 @@
} }
}, },
"node_modules/yocto-spinner": { "node_modules/yocto-spinner": {
"version": "0.2.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.0.tgz", "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.1.tgz",
"integrity": "sha512-Qu6WAqNLGleB687CCGcmgHIo8l+J19MX/32UrSMfbf/4L8gLoxjpOYoiHT1asiWyqvjRZbgvOhLlvne6E5Tbdw==", "integrity": "sha512-lHHxjh0bXaLgdJy3cNnVb/F9myx3CkhrvSOEVTkaUgNMXnYFa2xYPVhtGnqhh3jErY2gParBOHallCbc7NrlZQ==",
"dependencies": { "dependencies": {
"yoctocolors": "^2.1.1" "yoctocolors": "^2.1.1"
}, },
@ -6394,17 +6271,17 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.24.1", "version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
}, },
"node_modules/zod-to-json-schema": { "node_modules/zod-to-json-schema": {
"version": "3.24.1", "version": "3.24.3",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==",
"peerDependencies": { "peerDependencies": {
"zod": "^3.24.1" "zod": "^3.24.1"
} }

View file

@ -13,14 +13,14 @@
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.1.0", "@astrojs/vercel": "^8.1.1",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.9", "@types/node": "^22.13.9",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"astro": "^5.4.1", "astro": "^5.4.2",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.5.0", "ioredis": "^5.6.0",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
} }
} }

View file

@ -7,16 +7,9 @@ interface RedisNetworkError extends Error {
code?: string; code?: string;
} }
// Primary Redis configuration - with more robust fallbacks and logging // Primary Redis configuration
const PRIMARY_REDIS_URL = import.meta.env.PRIMARY_REDIS_URL || process.env.PRIMARY_REDIS_URL || process.env.REDIS_URL; 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 // Multiple replicas support
const REPLICA_URLS = [ 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, import.meta.env.REPLICA_REDIS_URL_1 || process.env.REPLICA_REDIS_URL_1 || import.meta.env.REPLICA_REDIS_URL || process.env.REPLICA_REDIS_URL,
@ -24,8 +17,6 @@ const REPLICA_URLS = [
import.meta.env.REPLICA_REDIS_URL_3 || process.env.REPLICA_REDIS_URL_3, import.meta.env.REPLICA_REDIS_URL_3 || process.env.REPLICA_REDIS_URL_3,
].filter(Boolean) as string[]; ].filter(Boolean) as string[];
console.log(`Found ${REPLICA_URLS.length} Redis replica URLs`);
// Create a mapping of URLs to friendly names for logging // Create a mapping of URLs to friendly names for logging
const redisInstanceNames = new Map<string, string>(); const redisInstanceNames = new Map<string, string>();
@ -53,8 +44,8 @@ let replicaClients: Map<string, Redis> = new Map();
const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
const MAX_RETRIES = 5; const MAX_RETRIES = 5;
const BASE_DELAY_MS = 100; const BASE_DELAY_MS = 100;
const IS_VERCEL = process.env.VERCEL || import.meta.env.VERCEL || false; const IS_VERCEL = process.env.VERCEL || false;
const VERCEL_CONNECTION_TIMEOUT = 25000; // 25 seconds for Vercel const VERCEL_CONNECTION_TIMEOUT = 15000; // 15 seconds for Vercel
const LOCAL_CONNECTION_TIMEOUT = 5000; // 5 seconds for local const LOCAL_CONNECTION_TIMEOUT = 5000; // 5 seconds for local
// Improved connection handling with exponential backoff // Improved connection handling with exponential backoff
@ -62,34 +53,26 @@ async function createRedisClient(url: string): Promise<Redis> {
if (!url) throw new Error('Redis URL is not set'); if (!url) throw new Error('Redis URL is not set');
const redisName = getRedisName(url); const redisName = getRedisName(url);
console.log(`Connecting to ${redisName} Redis instance in ${IS_VERCEL ? 'Vercel' : 'local'} environment`);
// Parse URL to extract authentication info // Parse URL to extract authentication info
const options = { const options = {
retryStrategy: (times) => { retryStrategy: (times) => {
if (times >= MAX_RETRIES) { if (times >= MAX_RETRIES) return null;
console.error(`${redisName} max retries (${MAX_RETRIES}) reached, giving up`);
return null;
}
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, times), 5000); const delay = Math.min(BASE_DELAY_MS * Math.pow(2, times), 5000);
console.log(`${redisName} retry attempt ${times}, next try in ${delay}ms`); console.log(`${redisName} retry attempt ${times}, next try in ${delay}ms`);
return delay + Math.random() * 200; // Add jitter return delay + Math.random() * 200; // Add jitter
}, },
maxRetriesPerRequest: 3, // Limit retries for queued commands maxRetriesPerRequest: null, // Allow retries for queued commands
enableOfflineQueue: true, // Enable command queuing enableOfflineQueue: true, // Enable command queuing
enableReadyCheck: true, enableReadyCheck: true,
connectTimeout: IS_VERCEL ? VERCEL_CONNECTION_TIMEOUT : LOCAL_CONNECTION_TIMEOUT, connectTimeout: 15000, // Increase connection timeout to 15 seconds for Vercel's serverless environment
disconnectTimeout: 10000, // Increase disconnect timeout to 10 seconds disconnectTimeout: 10000, // Increase disconnect timeout to 10 seconds
keepAlive: 10000, // Send keepalive every 10 seconds keepAlive: 10000, // Send keepalive every 10 seconds
commandTimeout: 15000, // Command timeout commandTimeout: 10000, // Command timeout
retryMaxDelay: 5000, // Maximum delay between retries retryMaxDelay: 5000, // Maximum delay between retries
reconnectOnError: (err) => { reconnectOnError: (err) => {
const targetErrors = ['READONLY', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND']; const targetErrors = ['READONLY', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND'];
const shouldReconnect = targetErrors.some(e => err.message.includes(e)); return targetErrors.some(e => err.message.includes(e));
if (shouldReconnect) {
console.log(`Reconnecting due to error: ${err.message}`);
}
return shouldReconnect;
} }
}; };
@ -103,60 +86,138 @@ export async function getRedisClient(): Promise<Redis> {
console.log('Running in Vercel environment - using optimized Redis connection strategy'); console.log('Running in Vercel environment - using optimized Redis connection strategy');
} }
// Check if Redis is configured // Try primary first
if (!PRIMARY_REDIS_URL && REPLICA_URLS.length === 0) { if (primaryClient?.status === 'ready') {
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 { try {
// Add timeout to ping // Add timeout to ping
const pingPromise = primaryClient.ping(); const pingPromise = primaryClient.ping();
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Ping timeout')), 5000) setTimeout(() => reject(new Error('Ping timeout')), 3000)
); );
await Promise.race([pingPromise, timeoutPromise]); await Promise.race([pingPromise, timeoutPromise]);
return primaryClient; return primaryClient;
} catch (error) { } catch (error) {
console.error('Primary Redis client ping failed:', error); console.warn('Primary Redis connection failed, will try replicas:', error);
throw error; // Fall through to try replicas
} }
} catch (primaryError) { }
console.error('Error connecting to primary Redis:', primaryError);
// 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);
// Special handling for Vercel ETIMEDOUT errors // Special handling for Vercel ETIMEDOUT errors
if (IS_VERCEL && primaryError instanceof Error) { if (IS_VERCEL && error instanceof Error && error.message.includes('ETIMEDOUT')) {
const errorMsg = primaryError.message; console.log('Encountered ETIMEDOUT in Vercel environment. Retrying with longer timeout...');
try {
if (errorMsg.includes('ETIMEDOUT')) { // Try one more time with increased timeout
console.log('Encountered ETIMEDOUT in Vercel environment. This is a common issue with Vercel serverless functions and Redis.'); const tempClient = new Redis(PRIMARY_REDIS_URL!, {
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.'); connectTimeout: 20000, // Increased timeout for last-ditch effort
} else if (errorMsg.includes('ECONNREFUSED')) { retryStrategy: () => null // No retries for this attempt
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.'); // 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);
} }
} }
throw primaryError; // 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');
} }
} }
@ -277,57 +338,24 @@ export async function warmChapterCache(slugs: string[], lang: string): Promise<v
} }
export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise<void> { export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise<void> {
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 = { const mapping = {
slugs, slugs,
lang, lang,
created_at: new Date().toISOString() 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 { try {
await writeToAllRedis(async (redis) => { await writeToAllRedis(async (redis) => {
console.log(`Writing feed mapping to Redis instance...`); await redis.set(
feedId,
try { JSON.stringify(mapping),
await redis.set( 'EX',
feedId, DEFAULT_CACHE_TTL
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); 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}`);
}
} }
} }

View file

@ -7,52 +7,18 @@ const feedMappings = new Map<string, { slugs: string[], lang: string }>();
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
console.log("Handling create-feed API request"); const { slugs, lang } = await request.json();
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) { if (!Array.isArray(slugs) || slugs.length === 0) {
console.error("Invalid slugs array:", slugs);
return new Response("Invalid slugs array", { status: 400 }); 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 // Create a unique hash based on slugs and language
const dataString = `${slugs.sort().join(',')}:${lang}`; const dataString = `${slugs.sort().join(',')}:${lang}`;
const hash = createHash('md5').update(dataString).digest('hex').slice(0, 8); const hash = createHash('md5').update(dataString).digest('hex').slice(0, 8);
try { // Store the mapping in Vercel KV
// Store the mapping in Redis await storeFeedMapping(hash, slugs, lang);
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 }), { return new Response(JSON.stringify({ feedId: hash }), {
status: 200, status: 200,
@ -60,18 +26,9 @@ export const POST: APIRoute = async ({ request }) => {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
} catch (error: any) { } catch (error) {
console.error('Unexpected error creating feed:', error); console.error('Error creating feed:', error);
return new Response(JSON.stringify({ return new Response("Internal server error", { status: 500 });
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'
}
});
} }
}; };