Compare commits

...

4 commits

Author SHA1 Message Date
ld3zian
90f0e2417e huh 2025-03-11 14:09:52 -07:00
ld3zian
547880a5f5 forgot to export 2025-03-11 14:03:49 -07:00
ld3zian
f465484b1c bump deps 2025-03-11 14:01:47 -07:00
ld3zian
530866ae9d refactor code 2025-03-11 13:59:35 -07:00
6 changed files with 304 additions and 332 deletions

270
package-lock.json generated
View file

@ -10,12 +10,11 @@
"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.1", "@astrojs/vercel": "^8.1.2",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.9", "@types/node": "^22.13.10",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "astro": "^5.4.3",
"astro": "^5.4.2",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.6.0", "ioredis": "^5.6.0",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
@ -38,16 +37,16 @@
"integrity": "sha512-86B3QGagP99MvSNwuJGiYSBHnh8nLvm2Q1IFI15wIUJJsPeQTO3eb2uwBmrqRsXykeR/mBzH8XCgz5AAt1BJrQ==" "integrity": "sha512-86B3QGagP99MvSNwuJGiYSBHnh8nLvm2Q1IFI15wIUJJsPeQTO3eb2uwBmrqRsXykeR/mBzH8XCgz5AAt1BJrQ=="
}, },
"node_modules/@astrojs/internal-helpers": { "node_modules/@astrojs/internal-helpers": {
"version": "0.6.0", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.6.1.tgz",
"integrity": "sha512-XgHIJDQaGlFnTr0sDp1PiJrtqsWzbHP2qkTU+JpQ8SnBewKP2IKOe/wqCkl0CyfyRXRu3TSWu4t/cpYMVfuBNA==" "integrity": "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="
}, },
"node_modules/@astrojs/markdown-remark": { "node_modules/@astrojs/markdown-remark": {
"version": "6.2.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.2.0.tgz", "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.2.1.tgz",
"integrity": "sha512-LUDjgd9p1yG0qTFSocaj3GOLmZs8Hsw/pNtvqzvNY58Acebxvb/46vDO/e/wxYgsKgIfWS+p+ZI5SfOjoVrbCg==", "integrity": "sha512-qtQXfZXeG84XSH9bMgG2e/kZfA4J7U19PKjhmFDNsKX47nautSHC0DitvxaWgQFSED66k6hWKDHLq3VKHCy/rg==",
"dependencies": { "dependencies": {
"@astrojs/internal-helpers": "0.6.0", "@astrojs/internal-helpers": "0.6.1",
"@astrojs/prism": "3.2.0", "@astrojs/prism": "3.2.0",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3", "hast-util-from-html": "^2.0.3",
@ -57,7 +56,7 @@
"mdast-util-definitions": "^6.0.0", "mdast-util-definitions": "^6.0.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.1", "rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1", "remark-rehype": "^11.1.1",
"remark-smartypants": "^3.0.2", "remark-smartypants": "^3.0.2",
@ -122,11 +121,11 @@
} }
}, },
"node_modules/@astrojs/vercel": { "node_modules/@astrojs/vercel": {
"version": "8.1.1", "version": "8.1.2",
"resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.1.1.tgz", "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.1.2.tgz",
"integrity": "sha512-4tTib9LkAtg7tqac4L+pnWEveUmYyWRXpHivaVV3lHKeQjB28KUZ+YXtR/VUG6/xpMduo+OCOOg0l1zOPq/e4w==", "integrity": "sha512-Qps7oYfQVvXov/ucP5sjTqRAgqbY84ayf8zyz5lNqTy88As09KGaZJTW4qrxU2xO5GdoE3+AoMGDUkU2rVY+cg==",
"dependencies": { "dependencies": {
"@astrojs/internal-helpers": "0.6.0", "@astrojs/internal-helpers": "0.6.1",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/edge": "^1.2.1", "@vercel/edge": "^1.2.1",
"@vercel/nft": "^0.29.2", "@vercel/nft": "^0.29.2",
@ -1465,9 +1464,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.9", "version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }
@ -1486,6 +1485,8 @@
"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"
} }
@ -1532,17 +1533,6 @@
"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.2", "version": "0.29.2",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.2.tgz", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.2.tgz",
@ -1754,13 +1744,13 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "5.4.2", "version": "5.4.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.4.2.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-5.4.3.tgz",
"integrity": "sha512-9Z3fAniIRJaK/o43OroZA1wHUIU+qHiOR9ovlVT/2XQaN25QRXScIsKWlFp0G/zrx5OuuoJ+QnaoHHW061u26A==", "integrity": "sha512-GKkOJQCHLx6CrPoghGhj7824WDSvIuuc+HTVjfjMPdB9axp238iJLByREJNDaSdzMeR/lC13xvBiUnKvcYyEIA==",
"dependencies": { "dependencies": {
"@astrojs/compiler": "^2.10.4", "@astrojs/compiler": "^2.10.4",
"@astrojs/internal-helpers": "0.6.0", "@astrojs/internal-helpers": "0.6.1",
"@astrojs/markdown-remark": "6.2.0", "@astrojs/markdown-remark": "6.2.1",
"@astrojs/telemetry": "3.2.0", "@astrojs/telemetry": "3.2.0",
"@oslojs/encoding": "^1.1.0", "@oslojs/encoding": "^1.1.0",
"@rollup/pluginutils": "^5.1.4", "@rollup/pluginutils": "^5.1.4",
@ -1795,8 +1785,8 @@
"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",
"package-manager-detector": "^1.0.0",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"preferred-pm": "^4.1.1",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"rehype": "^13.0.2", "rehype": "^13.0.2",
"semver": "^7.7.1", "semver": "^7.7.1",
@ -1810,7 +1800,6 @@
"vfile": "^6.0.3", "vfile": "^6.0.3",
"vite": "^6.2.0", "vite": "^6.2.0",
"vitefu": "^1.0.6", "vitefu": "^1.0.6",
"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.1", "yocto-spinner": "^0.2.1",
@ -2403,7 +2392,9 @@
"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",
@ -2433,9 +2424,9 @@
} }
}, },
"node_modules/decode-named-character-reference": { "node_modules/decode-named-character-reference": {
"version": "1.0.2", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz",
"integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==",
"dependencies": { "dependencies": {
"character-entities": "^2.0.0" "character-entities": "^2.0.0"
}, },
@ -2626,18 +2617,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@ -2749,38 +2728,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/find-up-simple": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz",
"integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-yarn-workspace-root2": {
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz",
"integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==",
"dependencies": {
"micromatch": "^4.0.2",
"pkg-dir": "^4.2.0"
}
},
"node_modules/flattie": { "node_modules/flattie": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
@ -3343,59 +3290,6 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"node_modules/load-yaml-file": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz",
"integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==",
"dependencies": {
"graceful-fs": "^4.1.5",
"js-yaml": "^3.13.0",
"pify": "^4.0.1",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/load-yaml-file/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/load-yaml-file/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/load-yaml-file/node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"engines": {
"node": ">=6"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lodash.defaults": { "node_modules/lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@ -4457,31 +4351,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-locate/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-queue": { "node_modules/p-queue": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz",
@ -4508,19 +4377,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
}, },
"node_modules/package-manager-detector": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.0.0.tgz",
"integrity": "sha512-7elnH+9zMsRo7aS72w6MeRugTpdRvInmEB4Kmm9BVvPw/SLG8gXUGQ+4wF0Mys0RSWPz0B9nuBbDe8vFeA2sfg=="
},
"node_modules/parse-latin": { "node_modules/parse-latin": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
@ -4549,14 +4415,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"engines": {
"node": ">=8"
}
},
"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",
@ -4628,17 +4486,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dependencies": {
"find-up": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@ -4775,23 +4622,10 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
}, },
"node_modules/preferred-pm": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-4.1.1.tgz",
"integrity": "sha512-rU+ZAv1Ur9jAUZtGPebQVQPzdGhNzaEiQ7VL9+cjsAWPHFYOccNXPNiev1CCDSOg/2j7UujM7ojNhpkuILEVNQ==",
"dependencies": {
"find-up-simple": "^1.0.0",
"find-yarn-workspace-root2": "1.2.16",
"which-pm": "^3.0.1"
},
"engines": {
"node": ">=18.12"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.29.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -5370,11 +5204,6 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/standard-as-callback": { "node_modules/standard-as-callback": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
@ -5481,14 +5310,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"engines": {
"node": ">=4"
}
},
"node_modules/strnum": { "node_modules/strnum": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
@ -6074,17 +5895,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/which-pm": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/which-pm/-/which-pm-3.0.1.tgz",
"integrity": "sha512-v2JrMq0waAI4ju1xU5x3blsxBBMgdgZve580iYMN5frDaLGjbA24fok7wKCsya8KLVO19Ju4XDc5+zTZCJkQfg==",
"dependencies": {
"load-yaml-file": "^0.2.0"
},
"engines": {
"node": ">=18.12"
}
},
"node_modules/which-pm-runs": { "node_modules/which-pm-runs": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz",

View file

@ -13,12 +13,11 @@
"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.1", "@astrojs/vercel": "^8.1.2",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.9", "@types/node": "^22.13.10",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "astro": "^5.4.3",
"astro": "^5.4.2",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.6.0", "ioredis": "^5.6.0",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"

View file

@ -7,13 +7,28 @@ interface Props {
const { announcement } = Astro.props; const { announcement } = Astro.props;
const bgColorClass = { // Define colors based on announcement type
info: 'bg-[rgb(var(--accent))] text-[rgb(var(--accent-text))]', const typeStyles = {
warning: 'bg-yellow-500 text-white', info: {
success: 'bg-emerald-500 text-white' bgColor: 'bg-[rgb(var(--accent))]',
}[announcement.type]; textColor: 'text-[rgb(var(--accent-text))]',
icon: 'info-icon'
},
warning: {
bgColor: 'bg-yellow-500',
textColor: 'text-white',
icon: 'warning-icon'
},
success: {
bgColor: 'bg-emerald-500',
textColor: 'text-white',
icon: 'success-icon'
}
};
// Create a simple hash of the announcement content const { bgColor, textColor, icon } = typeStyles[announcement.type];
// Create a simple hash of the announcement content for localStorage key
const announcementHash = btoa(JSON.stringify({ const announcementHash = btoa(JSON.stringify({
message: announcement.message, message: announcement.message,
type: announcement.type, type: announcement.type,
@ -21,44 +36,79 @@ const announcementHash = btoa(JSON.stringify({
})); }));
--- ---
<div class={`announcement-banner hidden w-full ${bgColorClass} px-6 py-4 shadow-lg`}> <div
class={`announcement-banner hidden w-full ${bgColor} ${textColor} px-6 py-4 shadow-lg`}
role="alert"
aria-live="polite"
>
<div class="container mx-auto flex items-center justify-between"> <div class="container mx-auto flex items-center justify-between">
<div class="flex items-center gap-3 text-lg"> <div class="flex items-center gap-3 text-lg">
{/* Info Icon */}
{announcement.type === 'info' && ( {announcement.type === 'info' && (
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path
</svg> stroke-linecap="round"
)} stroke-linejoin="round"
{announcement.type === 'warning' && ( stroke-width="2"
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> />
</svg>
)}
{announcement.type === 'success' && (
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
)} )}
{/* Warning Icon */}
{announcement.type === 'warning' && (
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
)}
{/* Success Icon */}
{announcement.type === 'success' && (
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
{/* Message */}
<span class="font-medium">{announcement.message}</span> <span class="font-medium">{announcement.message}</span>
{/* Link if provided */}
{announcement.link && ( {announcement.link && (
<a <a
href={announcement.link.url} href={announcement.link.url}
class="underline hover:opacity-80 transition-opacity font-semibold ml-2" class="underline hover:opacity-80 transition-opacity font-semibold ml-2"
target="_blank"
rel="noopener noreferrer"
> >
{announcement.link.text} → {announcement.link.text} →
</a> </a>
)} )}
</div> </div>
{/* Dismiss button if announcement is dismissible */}
{announcement.dismissible && ( {announcement.dismissible && (
<button <button
class="dismiss-announcement p-2 hover:bg-black/10 rounded-full transition-colors" class="dismiss-announcement p-2 hover:bg-black/10 rounded-full transition-colors"
aria-label="Dismiss announcement" aria-label="Dismiss announcement"
title="Dismiss"
> >
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
)} )}
@ -66,11 +116,14 @@ const announcementHash = btoa(JSON.stringify({
</div> </div>
<script define:vars={{ dismissible: announcement.dismissible, announcementHash }}> <script define:vars={{ dismissible: announcement.dismissible, announcementHash }}>
// DOM elements
const banner = document.querySelector('.announcement-banner'); const banner = document.querySelector('.announcement-banner');
const dismissBtn = document.querySelector('.dismiss-announcement'); const dismissBtn = document.querySelector('.dismiss-announcement');
const storageKey = `announcement-dismissed-${announcementHash}`; const storageKey = `announcement-dismissed-${announcementHash}`;
// Check if announcement should be shown /**
* Check if the announcement should be shown based on localStorage
*/
function checkVisibility() { function checkVisibility() {
// Only check localStorage if the banner is dismissible // Only check localStorage if the banner is dismissible
const isDismissed = dismissible ? const isDismissed = dismissible ?
@ -82,20 +135,22 @@ const announcementHash = btoa(JSON.stringify({
} }
} }
// Run after DOM is loaded // Run after DOM is loaded to prevent flash of hidden content
document.addEventListener('DOMContentLoaded', checkVisibility); document.addEventListener('DOMContentLoaded', checkVisibility);
dismissBtn?.addEventListener('click', () => { // Add click handler for dismiss button if present
if (dismissible) { if (dismissBtn && dismissible) {
dismissBtn.addEventListener('click', () => {
banner?.classList.add('hidden'); banner?.classList.add('hidden');
localStorage.setItem(storageKey, 'true'); localStorage.setItem(storageKey, 'true');
} });
}); }
</script> </script>
<style> <style>
.announcement-banner { .announcement-banner {
animation: slideDown 0.5s ease-out; animation: slideDown 0.5s ease-out;
z-index: 50;
} }
@keyframes slideDown { @keyframes slideDown {

View file

@ -1,5 +1,5 @@
import type { Comic, Genre, ComicType } from './types'; import type { Comic, Genre, ComicType, ChapterDetail } from './types';
import { getRedisClient } from './db'; import { getRedisClient, CHAPTER_CACHE_TTL } from './db';
const languageMap = { const languageMap = {
"en": "English", "en": "English",
@ -139,20 +139,6 @@ interface ComicDetail {
}; };
} }
interface ChapterDetail {
id: string;
chap: string;
title: string;
updated_at: string;
md_comics: {
title: string;
slug: string;
md_covers?: {
b2key: string;
}[];
};
}
interface ChapterParams { interface ChapterParams {
limit?: number; limit?: number;
lang?: string; lang?: string;
@ -270,7 +256,6 @@ export async function getChaptersForSlugs(
): Promise<ChapterDetail[]> { ): Promise<ChapterDetail[]> {
console.log(`Fetching chapters for ${slugs.length} comics`); console.log(`Fetching chapters for ${slugs.length} comics`);
const redis = await getRedisClient(); const redis = await getRedisClient();
const CACHE_TTL = 60 * 60;
const uniqueSlugs = [...new Set(slugs)]; const uniqueSlugs = [...new Set(slugs)];
const pipeline = redis.pipeline(); const pipeline = redis.pipeline();
@ -293,7 +278,7 @@ export async function getChaptersForSlugs(
cacheResults.forEach(([err, result], index) => { cacheResults.forEach(([err, result], index) => {
if (!err && result) { if (!err && result) {
try { try {
const parsed = JSON.parse(result); const parsed = JSON.parse(result as string);
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
cachedChapters.push(...parsed); cachedChapters.push(...parsed);
} }
@ -351,7 +336,7 @@ export async function getChaptersForSlugs(
cacheKey, cacheKey,
JSON.stringify(chaps), JSON.stringify(chaps),
'EX', 'EX',
CACHE_TTL CHAPTER_CACHE_TTL
); );
}); });
@ -360,10 +345,18 @@ export async function getChaptersForSlugs(
// Combine and sort all chapters // Combine and sort all chapters
const allChapters = [...cachedChapters, ...liveChapters]; const allChapters = [...cachedChapters, ...liveChapters];
const deduplicatedChapters = deduplicateChapters(allChapters); const deduplicatedChapters = deduplicateChapters(allChapters);
return deduplicatedChapters.sort((a, b) => return deduplicatedChapters.sort((a, b) => {
parseFloat(b.chap) - parseFloat(a.chap) || // Ensure the created_at fields are valid dates
new Date(b.created_at).getTime() - new Date(a.created_at).getTime() const dateA = new Date(a.created_at).getTime();
); const dateB = new Date(b.created_at).getTime();
// First try to sort by chapter number
const chapDiff = parseFloat(b.chap) - parseFloat(a.chap);
if (!isNaN(chapDiff)) return chapDiff;
// Fall back to date if chapter numbers are equal or invalid
return dateB - dateA;
});
} }
// Add export to the validation function // Add export to the validation function

View file

@ -1,12 +1,19 @@
import Redis from "ioredis"; import Redis from "ioredis";
import type { FeedMapping } from "./types"; import type { FeedMapping } from "./types";
import { getChaptersForSlugs } from "./api";
// Redis URLs // Configuration constants
const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
export const CHAPTER_CACHE_TTL = 60 * 60; // 1 hour
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 50;
const IS_VERCEL = process.env.VERCEL || false;
// Redis URLs from environment variables
const PRIMARY_REDIS_URL = const PRIMARY_REDIS_URL =
import.meta.env.PRIMARY_REDIS_URL || import.meta.env.PRIMARY_REDIS_URL ||
process.env.PRIMARY_REDIS_URL || process.env.PRIMARY_REDIS_URL ||
process.env.REDIS_URL; process.env.REDIS_URL;
const REPLICA_URLS = [ const REPLICA_URLS = [
import.meta.env.REPLICA_REDIS_URL_1 || import.meta.env.REPLICA_REDIS_URL_1 ||
process.env.REPLICA_REDIS_URL_1 || process.env.REPLICA_REDIS_URL_1 ||
@ -15,31 +22,43 @@ 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[];
const IS_VERCEL = process.env.VERCEL || false;
const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
const MAX_RETRIES = 3; // Reduced retries to avoid Vercel timeout
const BASE_DELAY_MS = 50; // Faster retries for serverless
// Singleton clients // Singleton clients
let primaryClient: Redis | null = null; let primaryClient: Redis | null = null;
let replicaClients: Map<string, Redis> = new Map(); let replicaClients: Map<string, Redis> = new Map();
// Redis instance naming for logging // Redis instance naming for logging
const redisInstanceNames = new Map<string, string>(); const redisInstanceNames = new Map<string, string>();
/**
* Initialize Redis instance names for better logging
*/
function initializeRedisNames() { function initializeRedisNames() {
redisInstanceNames.set(PRIMARY_REDIS_URL!, "Primary"); if (PRIMARY_REDIS_URL) {
redisInstanceNames.set(PRIMARY_REDIS_URL, "Primary");
}
REPLICA_URLS.forEach((url, index) => REPLICA_URLS.forEach((url, index) =>
redisInstanceNames.set(url, `Replica ${index + 1}`) redisInstanceNames.set(url, `Replica ${index + 1}`)
); );
} }
initializeRedisNames(); initializeRedisNames();
/**
* Get the friendly name of a Redis instance
*/
const getRedisName = (url: string) => redisInstanceNames.get(url) || "Unknown"; const getRedisName = (url: string) => redisInstanceNames.get(url) || "Unknown";
// Create Redis client with Vercel-optimized settings /**
* Create a new Redis client with optimized settings
*/
async function createRedisClient(url: string): Promise<Redis> { async function createRedisClient(url: string): Promise<Redis> {
if (!url) throw new Error("Redis URL not set"); if (!url) {
throw new Error("Redis URL not provided");
}
const redisName = getRedisName(url); const redisName = getRedisName(url);
const client = new Redis(url, { const client = new Redis(url, {
retryStrategy: (times) => { retryStrategy: (times) => {
if (times >= MAX_RETRIES) return null; if (times >= MAX_RETRIES) return null;
@ -54,13 +73,14 @@ async function createRedisClient(url: string): Promise<Redis> {
enableReadyCheck: true, enableReadyCheck: true,
connectTimeout: IS_VERCEL ? 5000 : 10000, // 5s for Vercel, 10s local connectTimeout: IS_VERCEL ? 5000 : 10000, // 5s for Vercel, 10s local
keepAlive: IS_VERCEL ? 2000 : 10000, // 2s ping for Vercel keepAlive: IS_VERCEL ? 2000 : 10000, // 2s ping for Vercel
commandTimeout: 5000, // Align with Vercel’s Hobby tier timeout commandTimeout: 5000, // Align with Vercel's timeout
reconnectOnError: (err) => reconnectOnError: (err) =>
["READONLY", "ETIMEDOUT", "ECONNRESET"].some((e) => ["READONLY", "ETIMEDOUT", "ECONNRESET"].some((e) =>
err.message.includes(e) err.message.includes(e)
), ),
}); });
// Setup event listeners
client client
.on("ready", () => console.log(`${redisName} ready`)) .on("ready", () => console.log(`${redisName} ready`))
.on("error", (err) => console.error(`${redisName} error:`, err)) .on("error", (err) => console.error(`${redisName} error:`, err))
@ -70,28 +90,36 @@ async function createRedisClient(url: string): Promise<Redis> {
return client; return client;
} }
// Get or create a Redis client /**
async function getRedisClient(): Promise<Redis> { * Check if a Redis client is connected and responsive
const checkConnection = async (client: Redis, name: string) => { */
try { async function checkConnection(client: Redis, name: string): Promise<boolean> {
await Promise.race([ try {
client.ping(), await Promise.race([
new Promise((_, reject) => client.ping(),
setTimeout(() => reject(new Error("Ping timeout")), 2000) new Promise((_, reject) =>
), setTimeout(() => reject(new Error("Ping timeout")), 2000)
]); ),
return true; ]);
} catch (err) { return true;
console.warn(`${name} ping failed:`, err); } catch (err) {
return false; console.warn(`${name} ping failed:`, err);
} return false;
}; }
}
/**
* Get an active Redis client from the pool
*/
export async function getRedisClient(): Promise<Redis> {
// Try primary client // Try primary client
if (!primaryClient || primaryClient.status !== "ready") { if (!primaryClient || primaryClient.status !== "ready") {
primaryClient = await createRedisClient(PRIMARY_REDIS_URL!); primaryClient = await createRedisClient(PRIMARY_REDIS_URL!);
} }
if (await checkConnection(primaryClient, "Primary")) return primaryClient;
if (await checkConnection(primaryClient, "Primary")) {
return primaryClient;
}
// Try replicas // Try replicas
for (const url of REPLICA_URLS) { for (const url of REPLICA_URLS) {
@ -100,7 +128,10 @@ async function getRedisClient(): Promise<Redis> {
client = await createRedisClient(url); client = await createRedisClient(url);
replicaClients.set(url, client); replicaClients.set(url, client);
} }
if (await checkConnection(client, getRedisName(url))) return client;
if (await checkConnection(client, getRedisName(url))) {
return client;
}
} }
// Fallback: Force new primary connection // Fallback: Force new primary connection
@ -108,7 +139,9 @@ async function getRedisClient(): Promise<Redis> {
return primaryClient; return primaryClient;
} }
// Write to all Redis instances /**
* Write data to all available Redis instances
*/
async function writeToAllRedis( async function writeToAllRedis(
operation: (redis: Redis) => Promise<any> operation: (redis: Redis) => Promise<any>
): Promise<void> { ): Promise<void> {
@ -116,6 +149,7 @@ async function writeToAllRedis(
primaryClient, primaryClient,
...Array.from(replicaClients.values()), ...Array.from(replicaClients.values()),
].filter(Boolean) as Redis[]; ].filter(Boolean) as Redis[];
const errors: Error[] = []; const errors: Error[] = [];
for (const client of clients) { for (const client of clients) {
@ -132,33 +166,120 @@ async function writeToAllRedis(
} }
} }
if (errors.length === clients.length) { if (errors.length === clients.length && clients.length > 0) {
throw new Error("All Redis writes failed"); throw new Error("All Redis writes failed");
} }
} }
// Your existing functions (unchanged except for using getRedisClient/writeToAllRedis) /**
* Store a feed mapping in Redis
*/
export async function storeFeedMapping( export async function storeFeedMapping(
feedId: string, feedId: string,
slugs: string[], slugs: string[],
lang: string lang: string
): Promise<void> { ): Promise<void> {
const mapping = { slugs, lang, created_at: new Date().toISOString() }; const mapping = {
slugs,
lang,
created_at: new Date().toISOString()
};
await writeToAllRedis(async (redis) => await writeToAllRedis(async (redis) =>
redis.set(feedId, JSON.stringify(mapping), "EX", DEFAULT_CACHE_TTL) redis.set(feedId, JSON.stringify(mapping), "EX", DEFAULT_CACHE_TTL)
); );
} }
/**
* Retrieve a feed mapping from Redis
*/
export async function getFeedMapping( export async function getFeedMapping(
feedId: string feedId: string
): Promise<FeedMapping | null> { ): Promise<FeedMapping | null> {
const redis = await getRedisClient(); try {
const mapping = await redis.get(feedId); const redis = await getRedisClient();
if (!mapping) return null; const mapping = await redis.get(feedId);
await redis.expire(feedId, DEFAULT_CACHE_TTL);
const data = JSON.parse(mapping); if (!mapping) return null;
return { slugs: data.slugs, lang: data.lang };
// Reset expiration on access
await redis.expire(feedId, DEFAULT_CACHE_TTL);
const data = JSON.parse(mapping);
return {
slugs: data.slugs,
lang: data.lang
};
} catch (error) {
console.error(`Error retrieving feed mapping for ${feedId}:`, error);
return null;
}
} }
// Initialize connections lazily (no blocking startup) /**
* Pre-warm the chapter cache for better performance
* This function checks cache status and uses the getChaptersForSlugs function from api.ts
* to fetch and cache data in the background
*/
export async function warmChapterCache(
slugs: string[],
lang: string
): Promise<void> {
try {
console.log(`Warming chapter cache for ${slugs.length} comics in ${lang}`);
const redis = await getRedisClient();
// Validate slugs
const validSlugs = slugs.filter(slug =>
slug && typeof slug === 'string' && slug.match(/^[a-z0-9-]+$/));
if (validSlugs.length !== slugs.length) {
console.warn(`Filtered out ${slugs.length - validSlugs.length} invalid slugs`);
}
// Check which slugs are already in cache
const pipeline = redis.pipeline();
const cacheKeys = validSlugs.map(slug => `chapters:${slug}:${lang}`);
cacheKeys.forEach(key => {
pipeline.exists(key);
});
const results = await pipeline.exec();
const slugsToFetch: string[] = [];
results?.forEach(([err, exists], index) => {
if (err) {
console.error(`Error checking cache for ${cacheKeys[index]}:`, err);
slugsToFetch.push(validSlugs[index]);
} else if (!exists) {
slugsToFetch.push(validSlugs[index]);
}
});
if (slugsToFetch.length === 0) {
console.log('All chapters already in cache, no warming needed');
return;
}
console.log(`Cache miss for ${slugsToFetch.length} comics - starting background fetch`);
// To avoid circular dependencies, we'll import getChaptersForSlugs dynamically
const { getChaptersForSlugs } = await import('./api');
// Start fetching in the background without waiting for the result
getChaptersForSlugs(slugsToFetch, lang)
.then(chapters => {
console.log(`Successfully warmed cache with ${chapters.length} chapters`);
})
.catch(error => {
console.error('Error warming chapter cache:', error);
});
} catch (error) {
console.error('Error in warmChapterCache:', error);
}
}
// Initialize connections lazily
console.log("Redis will connect on first use"); console.log("Redis will connect on first use");

View file

@ -2,9 +2,6 @@ import type { APIRoute } from "astro";
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { storeFeedMapping } from '../../lib/db'; import { storeFeedMapping } from '../../lib/db';
// In-memory storage (replace with database in production)
const feedMappings = new Map<string, { slugs: string[], lang: string }>();
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
const { slugs, lang } = await request.json(); const { slugs, lang } = await request.json();
@ -17,7 +14,7 @@ export const POST: APIRoute = async ({ request }) => {
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);
// Store the mapping in Vercel KV // Store the mapping in Redis
await storeFeedMapping(hash, slugs, lang); await storeFeedMapping(hash, slugs, lang);
return new Response(JSON.stringify({ feedId: hash }), { return new Response(JSON.stringify({ feedId: hash }), {
@ -30,7 +27,4 @@ export const POST: APIRoute = async ({ request }) => {
console.error('Error creating feed:', error); console.error('Error creating feed:', error);
return new Response("Internal server error", { status: 500 }); return new Response("Internal server error", { status: 500 });
} }
}; };
// Export the mappings for use in other files
export { feedMappings };