diff --git a/.gitignore b/.gitignore index d9ae61c..fe4eed1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist coverage .DS_Store .idea +debug \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a348792..5ea3bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,185 +1,208 @@ -## [1.7.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.7.0...v1.7.1) (2025-06-10) +# [1.8.0-beta.6](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.8.0-beta.5...v1.8.0-beta.6) (2025-06-13) + + +### Features + +* remove general timeoutMs and replace it with getTimeoutMs (for faster non-blocking rendering) ([02deb64](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/02deb649fce40085495c6fec5e8750cba42d2428)) + +# [1.8.0-beta.5](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.8.0-beta.4...v1.8.0-beta.5) (2025-06-12) ### Bug Fixes -* revalidate times for fetch entries from context is outdated ([4538f9f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/4538f9f8bf58b97727e83f2026929db11be470ec)) -* revalidate times from data parameter for fetch entries ([e48f0d1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/e48f0d19b81b65ae2564453fd75251372080407e)) -* revalidate times from data parameter for fetch entries ([3c38335](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3c3833561863ec4f07252ba7b9b30b42518b7485)) +* scan+hscan logging ([56c82c3](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/56c82c3dd62257e8686fedc25dea45a0b7fec18e)) -# [1.7.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.6.0...v1.7.0) (2025-05-26) +# [1.8.0-beta.4](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.8.0-beta.3...v1.8.0-beta.4) (2025-06-12) + + +### Bug Fixes + +* improve logs ([92508b6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/92508b6697c87cad4a8720fb5380f41fe9ec2257)) + +# [1.8.0-beta.3](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.8.0-beta.2...v1.8.0-beta.3) (2025-06-12) ### Features -* add TLS/SSL support with Redis client configuration options ([9f80dd6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9f80dd6b53659ea488af6948a9c952ce0cda7490)) +* add redis commands debug logging ([4c8a0d7](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/4c8a0d7f82573bbaab63bdd380063244c641735f)) -# [1.6.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.5.1...v1.6.0) (2025-05-26) +# [1.8.0-beta.2](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.8.0-beta.1...v1.8.0-beta.2) (2025-06-11) ### Bug Fixes -* config ([d326ef4](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d326ef46298a96535720f78d562a20b27e7b3c8f)) -* config ([311d492](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/311d492c41d8c05b89d675753d79413632fc0019)) -* process.env.redisUrl ([a57fd6a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/a57fd6a6b641015726edf11a00fa5c020dea0cca)) -* readme ([2901ab0](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2901ab0e0d646839e1779107aa9bea80deacf6c8)) -* readme test startup ([e291666](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/e291666ed7b9d77c0f3baa68c3638bbad264a868)) -* rename redis_url to redisUrl to align with other config option naming convention + fix: readme ([2e3f20d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2e3f20dce31a678b781913d230581461b3d67a07)) +* remove connect timeout ([4dceda1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/4dceda15a0ae8ca3c5fcdddf861263ef7ca237ce)) + +# [1.8.0-beta.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.7.1...v1.8.0-beta.1) (2025-06-10) ### Features -* disable keyspace config check ([0b8878d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/0b8878d321808d00140664f51fc1d2d904cc4664)) +* improve error handling ([dd591da](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/dd591daab9539e6ba96da1d1c493a1e771ba272d)) -## [1.5.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.5.0...v1.5.1) (2025-05-23) +## [1.7.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.7.0...v1.7.1) (2025-06-10) + +### Bug Fixes + +- revalidate times for fetch entries from context is outdated ([4538f9f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/4538f9f8bf58b97727e83f2026929db11be470ec)) +- revalidate times from data parameter for fetch entries ([e48f0d1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/e48f0d19b81b65ae2564453fd75251372080407e)) +- revalidate times from data parameter for fetch entries ([3c38335](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3c3833561863ec4f07252ba7b9b30b42518b7485)) + +# [1.7.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.6.0...v1.7.0) (2025-05-26) + +### Features +- add TLS/SSL support with Redis client configuration options ([9f80dd6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9f80dd6b53659ea488af6948a9c952ce0cda7490)) + +# [1.6.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.5.1...v1.6.0) (2025-05-26) ### Bug Fixes -* Update package.json ([63e6ffd](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/63e6ffdc21032cfb1c5ecb507276fd832ee8252a)) +- config ([d326ef4](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d326ef46298a96535720f78d562a20b27e7b3c8f)) +- config ([311d492](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/311d492c41d8c05b89d675753d79413632fc0019)) +- process.env.redisUrl ([a57fd6a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/a57fd6a6b641015726edf11a00fa5c020dea0cca)) +- readme ([2901ab0](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2901ab0e0d646839e1779107aa9bea80deacf6c8)) +- readme test startup ([e291666](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/e291666ed7b9d77c0f3baa68c3638bbad264a868)) +- rename redis_url to redisUrl to align with other config option naming convention + fix: readme ([2e3f20d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2e3f20dce31a678b781913d230581461b3d67a07)) -# [1.5.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.4.0...v1.5.0) (2025-05-16) +### Features + +- disable keyspace config check ([0b8878d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/0b8878d321808d00140664f51fc1d2d904cc4664)) +## [1.5.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.5.0...v1.5.1) (2025-05-23) ### Bug Fixes -* improving log fitting the new redis url option ([59b7e59](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/59b7e5921c724ceff58917d712e310b18a23b464)) +- Update package.json ([63e6ffd](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/63e6ffdc21032cfb1c5ecb507276fd832ee8252a)) +# [1.5.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.4.0...v1.5.0) (2025-05-16) + +### Bug Fixes + +- improving log fitting the new redis url option ([59b7e59](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/59b7e5921c724ceff58917d712e310b18a23b464)) ### Features -* add redis_url param ([cea59a1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/cea59a10f0f2e1b73b8005683ea65f22ba95edc7)) +- add redis_url param ([cea59a1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/cea59a10f0f2e1b73b8005683ea65f22ba95edc7)) # [1.4.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.3.0...v1.4.0) (2025-05-09) - ### Bug Fixes -* add tests ([225fc49](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/225fc49eff26d631c4b8d50a15ef2c864213f36b)) -* comment ([321ddec](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/321ddec4ee9b6a6e2cf1f78062d05d2cc7b45e4c)) -* remove retry ([ca4ff6c](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/ca4ff6c4072e675da42dbcfe36d3ff422dc54f12)) -* remove retry ([3a1cf7d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3a1cf7dc5fd315ff3c84460052af20b8e40014ec)) -* remove retry ([207413b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/207413b81cb51143e04dff9a2310a185d6bba568)) -* tests and ci ([b0841ad](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/b0841ad9bc2c4b3ab9edde7047259cd45fbd5f02)) - +- add tests ([225fc49](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/225fc49eff26d631c4b8d50a15ef2c864213f36b)) +- comment ([321ddec](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/321ddec4ee9b6a6e2cf1f78062d05d2cc7b45e4c)) +- remove retry ([ca4ff6c](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/ca4ff6c4072e675da42dbcfe36d3ff422dc54f12)) +- remove retry ([3a1cf7d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3a1cf7dc5fd315ff3c84460052af20b8e40014ec)) +- remove retry ([207413b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/207413b81cb51143e04dff9a2310a185d6bba568)) +- tests and ci ([b0841ad](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/b0841ad9bc2c4b3ab9edde7047259cd45fbd5f02)) ### Features -* add support for nextjs 15.3.2 ([fcd8bb5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/fcd8bb5469bbefcd88397d3ef86ce3eb0fee7c02)) +- add support for nextjs 15.3.2 ([fcd8bb5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/fcd8bb5469bbefcd88397d3ef86ce3eb0fee7c02)) # [1.3.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.2.1...v1.3.0) (2025-05-08) - ### Bug Fixes -* encoding of data in cache entry ([9f0e747](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9f0e747b224294905427ba3060185f68fee16f0f)) -* escape characte ([dbd9141](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/dbd91413b777b2a4554597f3ca2b7f1de1163a80)) -* readme ([31dcaa4](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/31dcaa40ac19ea42a7f4d6ca33e4642704ad1f6b)) -* readme ([dfb69c8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/dfb69c861ddaa4a94411d419854e1c2e1e99152e)) -* test ([13b2cb8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b2cb8020ef7457473b208675bf3fce4161f492)) -* tests ([02357e1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/02357e1a7a29d997c95dce42399fef26530467d4)) -* tests ([a5587dc](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/a5587dc3701a92069da945f548991aeb0d999285)) -* tests + re-add old implementation ([cb6d36d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/cb6d36d46008d422b4396fd580d35fa97848edb7)) -* tests improvement ([73a3594](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/73a35946cfcd5e4811c2c09201bb02c9cdea5298)) -* tests, make them independent from each other ([35885e3](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/35885e38a58ac0aa976c86dd964549e2d3426a79)) - +- encoding of data in cache entry ([9f0e747](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9f0e747b224294905427ba3060185f68fee16f0f)) +- escape characte ([dbd9141](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/dbd91413b777b2a4554597f3ca2b7f1de1163a80)) +- readme ([31dcaa4](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/31dcaa40ac19ea42a7f4d6ca33e4642704ad1f6b)) +- readme ([dfb69c8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/dfb69c861ddaa4a94411d419854e1c2e1e99152e)) +- test ([13b2cb8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b2cb8020ef7457473b208675bf3fce4161f492)) +- tests ([02357e1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/02357e1a7a29d997c95dce42399fef26530467d4)) +- tests ([a5587dc](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/a5587dc3701a92069da945f548991aeb0d999285)) +- tests + re-add old implementation ([cb6d36d](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/cb6d36d46008d422b4396fd580d35fa97848edb7)) +- tests improvement ([73a3594](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/73a35946cfcd5e4811c2c09201bb02c9cdea5298)) +- tests, make them independent from each other ([35885e3](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/35885e38a58ac0aa976c86dd964549e2d3426a79)) ### Features -* add files ([d8a2474](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d8a24747a6a2a12e2709d017dd36c7b80b2ad49f)) -* add more comments ([fb2f105](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/fb2f10588566fad042d2e8da4999cf7bf591555d)) -* add new invalidation logic for fetch requests + new tests ([9d0d1d2](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9d0d1d2eafb785dbe91b172358a19494c623cc68)) -* add new tests ([836b882](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/836b88249365ea8745ca839a7dc3a3a4a77732e6)) -* add new tests ([d0e6833](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d0e68335ce13827f74ed1be5b115f7351beebd47)) -* adding a test page ([d4c8a8c](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d4c8a8caaba7f4e5003606860babd9e50c6ed99a)) -* adding comfortable cache testing tool on homepage ([340d5cb](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/340d5cbc929ea8dcdbef2a949e1656d014d1f3bb)) -* adding nextjs unit tests ([368ec75](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/368ec755a8532a52526e3364cc02f64b7d4245fa)) -* changed ([8501a4e](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/8501a4ee963975f32661789e31281785cf01519a)) -* changed hook ([92f9d3c](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/92f9d3c4b8d9afd6bbbb9cc3a69138823f4d4f87)) -* changed hook ([6b32f0a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/6b32f0abcda935046e61242ba633728bcf23a460)) -* extend and fix caching + chore: update readme ([7ef38a8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/7ef38a8ed2a20957cc78ffa213be3cb334736b19)) -* extend tests ([db9bb85](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/db9bb85e398a7d6e1ba522b990de7e5e241b54b6)) -* improve flaky ms sync delay ([6655884](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/665588474a09f1eb4f4683d2e107ec8b5b36b39a)) -* improve readme ([926493a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/926493a198d15e32e5e4b5c619375df85c646f1c)) -* improve tests ([3fcfadf](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3fcfadf49389fe8a7218417fb79ec74445e76cb7)) -* remove unnecessary revalidatedTagsMap ([7b5c313](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/7b5c313e91b5157f113085be75dceba8013c0e81)) -* test is running ([6091471](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/6091471a12f954e3da4bd94db0929b0bd2cfd701)) -* tests ([bd6904e](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/bd6904e94a43cfac183a5ec86cbbea0f4e40a816)) -* tests 2 ([974c952](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/974c952587a9d330934f49b26db84c037b3695f5)) -* update CI with pnpm ([c1b0b33](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/c1b0b33417b416bd7755b6b2f43d0b9bea5c7690)) -* update CI with pnpm ([d850377](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d85037706876dcb036bb6f952a48937c3e96cba3)) -* update CI with pnpm 10 ([c559936](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/c5599361f15c4c82fa999e16a48fcf78e53355a6)) -* update CI with pnpm 11 ([f83708f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/f83708f76c3924b8b80dd612965dc49a95d7e18c)) -* update CI with pnpm 2 ([235f0fc](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/235f0fc8cb35dbe532091d8f545791b8dd05b6be)) -* update CI with pnpm 2 ([cb9624e](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/cb9624e900f555bc07c734ff152c4f4a93000e54)) -* update CI with pnpm 3 ([72c25ce](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/72c25cefa2aea6767d5a71b5470955a18f9036b9)) -* update CI with pnpm 4 ([fff355a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/fff355a8e047d766121d255881d891fa7c5a754e)) -* update CI with pnpm 4 ([facd58b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/facd58bdefd530d70fdbb2f7d48c9962ca2195e5)) -* update CI with pnpm 5 ([03d9d90](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/03d9d906e852927496a58504dd9c7448e31878a1)) -* update CI with pnpm 6 ([b5e7066](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/b5e7066ec840edfa7d6df6c6ab978c61b356c8b5)) -* update CI with pnpm 8 ([20c3a5b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/20c3a5be9d465537240cce6ac5346554667cda68)) -* update CI with pnpm 9 ([5160fd3](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/5160fd3e1a010a5b52b99d0e2e0de3b9035d1e88)) -* update docs and tests ([7a46c21](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/7a46c21346d878df88283071551f06a34d71eb9a)) +- add files ([d8a2474](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d8a24747a6a2a12e2709d017dd36c7b80b2ad49f)) +- add more comments ([fb2f105](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/fb2f10588566fad042d2e8da4999cf7bf591555d)) +- add new invalidation logic for fetch requests + new tests ([9d0d1d2](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9d0d1d2eafb785dbe91b172358a19494c623cc68)) +- add new tests ([836b882](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/836b88249365ea8745ca839a7dc3a3a4a77732e6)) +- add new tests ([d0e6833](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d0e68335ce13827f74ed1be5b115f7351beebd47)) +- adding a test page ([d4c8a8c](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d4c8a8caaba7f4e5003606860babd9e50c6ed99a)) +- adding comfortable cache testing tool on homepage ([340d5cb](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/340d5cbc929ea8dcdbef2a949e1656d014d1f3bb)) +- adding nextjs unit tests ([368ec75](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/368ec755a8532a52526e3364cc02f64b7d4245fa)) +- changed ([8501a4e](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/8501a4ee963975f32661789e31281785cf01519a)) +- changed hook ([92f9d3c](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/92f9d3c4b8d9afd6bbbb9cc3a69138823f4d4f87)) +- changed hook ([6b32f0a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/6b32f0abcda935046e61242ba633728bcf23a460)) +- extend and fix caching + chore: update readme ([7ef38a8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/7ef38a8ed2a20957cc78ffa213be3cb334736b19)) +- extend tests ([db9bb85](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/db9bb85e398a7d6e1ba522b990de7e5e241b54b6)) +- improve flaky ms sync delay ([6655884](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/665588474a09f1eb4f4683d2e107ec8b5b36b39a)) +- improve readme ([926493a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/926493a198d15e32e5e4b5c619375df85c646f1c)) +- improve tests ([3fcfadf](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3fcfadf49389fe8a7218417fb79ec74445e76cb7)) +- remove unnecessary revalidatedTagsMap ([7b5c313](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/7b5c313e91b5157f113085be75dceba8013c0e81)) +- test is running ([6091471](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/6091471a12f954e3da4bd94db0929b0bd2cfd701)) +- tests ([bd6904e](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/bd6904e94a43cfac183a5ec86cbbea0f4e40a816)) +- tests 2 ([974c952](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/974c952587a9d330934f49b26db84c037b3695f5)) +- update CI with pnpm ([c1b0b33](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/c1b0b33417b416bd7755b6b2f43d0b9bea5c7690)) +- update CI with pnpm ([d850377](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/d85037706876dcb036bb6f952a48937c3e96cba3)) +- update CI with pnpm 10 ([c559936](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/c5599361f15c4c82fa999e16a48fcf78e53355a6)) +- update CI with pnpm 11 ([f83708f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/f83708f76c3924b8b80dd612965dc49a95d7e18c)) +- update CI with pnpm 2 ([235f0fc](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/235f0fc8cb35dbe532091d8f545791b8dd05b6be)) +- update CI with pnpm 2 ([cb9624e](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/cb9624e900f555bc07c734ff152c4f4a93000e54)) +- update CI with pnpm 3 ([72c25ce](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/72c25cefa2aea6767d5a71b5470955a18f9036b9)) +- update CI with pnpm 4 ([fff355a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/fff355a8e047d766121d255881d891fa7c5a754e)) +- update CI with pnpm 4 ([facd58b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/facd58bdefd530d70fdbb2f7d48c9962ca2195e5)) +- update CI with pnpm 5 ([03d9d90](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/03d9d906e852927496a58504dd9c7448e31878a1)) +- update CI with pnpm 6 ([b5e7066](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/b5e7066ec840edfa7d6df6c6ab978c61b356c8b5)) +- update CI with pnpm 8 ([20c3a5b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/20c3a5be9d465537240cce6ac5346554667cda68)) +- update CI with pnpm 9 ([5160fd3](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/5160fd3e1a010a5b52b99d0e2e0de3b9035d1e88)) +- update docs and tests ([7a46c21](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/7a46c21346d878df88283071551f06a34d71eb9a)) ## [1.2.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.2.0...v1.2.1) (2025-03-28) - ### Bug Fixes -* imports ([41f0cf9](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/41f0cf97506c134f8dfb37fab746cc9c066c515f)) +- imports ([41f0cf9](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/41f0cf97506c134f8dfb37fab746cc9c066c515f)) # [1.2.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.1.3...v1.2.0) (2025-03-28) - ### Features -* add tsup ([ccf122a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/ccf122a243fade016b6b2d544acec4098222becd)) -* add tsup ([2dffad6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2dffad68401bc273cf81a0a0d06446d34b574a5e)) +- add tsup ([ccf122a](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/ccf122a243fade016b6b2d544acec4098222becd)) +- add tsup ([2dffad6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2dffad68401bc273cf81a0a0d06446d34b574a5e)) ## [1.1.3](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.1.2...v1.1.3) (2025-03-28) - ### Bug Fixes -* Update README.md ([0a79274](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/0a79274e363856f07b1dce62ec74b54ad92a946e)) +- Update README.md ([0a79274](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/0a79274e363856f07b1dce62ec74b54ad92a946e)) ## [1.1.2](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.1.1...v1.1.2) (2025-03-28) - ### Bug Fixes -* Update package.json ([3775c36](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3775c36f3c110686856f8644315ca6e02a3c483f)) +- Update package.json ([3775c36](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/3775c36f3c110686856f8644315ca6e02a3c483f)) ## [1.1.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.1.0...v1.1.1) (2025-03-28) - ### Bug Fixes -* Update package.json ([bf17b41](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/bf17b4186c8b7d94be83c61b5d4f8622ac7cf7f0)) +- Update package.json ([bf17b41](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/bf17b4186c8b7d94be83c61b5d4f8622ac7cf7f0)) # [1.1.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.0.0...v1.1.0) (2025-03-28) - ### Features -* Update README.md ([10b474b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/10b474b456803be924bf4170b6cda662827202c4)) +- Update README.md ([10b474b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/10b474b456803be924bf4170b6cda662827202c4)) # 1.0.0 (2025-03-28) - ### Bug Fixes -* double sync on key expiration ([14afef6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/14afef6b08e3399a2aa7d6cf42a4b9b7b5ea5d33)) -* lint errors ([2b9b138](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2b9b138759f5754577205b58a998cc034b3b0db5)) -* rEADME ([9e4fab1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9e4fab163002c34e8077285064c24ee05ba92bac)) - +- double sync on key expiration ([14afef6](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/14afef6b08e3399a2aa7d6cf42a4b9b7b5ea5d33)) +- lint errors ([2b9b138](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/2b9b138759f5754577205b58a998cc034b3b0db5)) +- rEADME ([9e4fab1](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/9e4fab163002c34e8077285064c24ee05ba92bac)) ### Features -* add handler code ([72251f5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/72251f58446ec6fb3819ea0bdd67fc012e8a5c38)) -* add handler code ([f674f26](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/f674f262f292e47fd228a827590e8dc10391e5cb)) -* add handler code ([24c497f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/24c497f1d67898e64528105c61a90b00f55ba02a)) -* improve readme and remove logs ([79408fb](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/79408fbd488db11fcc7472b690f1fff237816da8)) -* improve readme and remove logs ([de7a6aa](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/de7a6aa735d6295299d3a5d41d0fd00d64ac6f89)) -* rename package and extend readme ([e16bcdc](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/e16bcdc6329ee913e1794f2bce05e1e88a08d91b)) -* update to next 15 types + feat: add delete sync to deduplication cache ([832c28f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/832c28f1fe0831b87790c2d60e33b314be0adf58)) +- add handler code ([72251f5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/72251f58446ec6fb3819ea0bdd67fc012e8a5c38)) +- add handler code ([f674f26](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/f674f262f292e47fd228a827590e8dc10391e5cb)) +- add handler code ([24c497f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/24c497f1d67898e64528105c61a90b00f55ba02a)) +- improve readme and remove logs ([79408fb](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/79408fbd488db11fcc7472b690f1fff237816da8)) +- improve readme and remove logs ([de7a6aa](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/de7a6aa735d6295299d3a5d41d0fd00d64ac6f89)) +- rename package and extend readme ([e16bcdc](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/e16bcdc6329ee913e1794f2bce05e1e88a08d91b)) +- update to next 15 types + feat: add delete sync to deduplication cache ([832c28f](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/832c28f1fe0831b87790c2d60e33b314be0adf58)) diff --git a/README.md b/README.md index ad2210b..6c62036 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,10 @@ Furthermore there exists the DEBUG_CACHE_HANDLER environment variable to enable There exists also the SKIP_KEYSPACE_CONFIG_CHECK environment variable to skip the check for the keyspace configuration. This is useful if you are using redis in a cloud environment that forbids access to config commands. If you set SKIP_KEYSPACE_CONFIG_CHECK=true the check will be skipped and the keyspace configuration will be assumed to be correct (e.g. notify-keyspace-events Exe). +KILL_CONTAINER_ON_ERROR_THRESHOLD: Optional environment variable that defines how many Redis client errors should occur before the process exits with code 1. This is useful in container environments like Kubernetes where you want the container to restart if Redis connectivity issues persist. Set to 0 (default) to disable this feature. For example, setting KILL_CONTAINER_ON_ERROR_THRESHOLD=10 will exit the process after 10 Redis client errors, allowing the container orchestrator to restart the container. + +REDIS_COMMAND_TIMEOUT_MS: Optional environment variable that sets the timeout in milliseconds for Redis get command. If not set, defaults to 500ms. The value is parsed as an integer, and if parsing fails, falls back to the 500ms default. + ### Option A: minimum implementation with default options extend `next.config.js` with: @@ -119,21 +123,22 @@ A working example of above can be found in the `test/integration/next-app-custom ## Available Options -| Option | Description | Default Value | -| ---------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| redisUrl | Redis connection url | `process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379'` | -| database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | `process.env.VERCEL_ENV === 'production' ? 0 : 1` | -| keyPrefix | Prefix added to all Redis keys | `process.env.VERCEL_URL \|\| 'UNDEFINED_URL_'` | -| sharedTagsKey | Key used to store shared tags hash map in Redis | `'__sharedTags__'` | -| timeoutMs | Timeout in milliseconds for Redis operations | `5000` | -| revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | `250` | -| avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | `3600000` (1 hour) | -| redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | `true` | -| inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | `10000` | -| defaultStaleAge | Default stale age in seconds for cached items | `1209600` (14 days) | -| estimateExpireAge | Function to calculate expire age (redis TTL value) from stale age | Production: `staleAge * 2`
Other: `staleAge * 1.2` | -| socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `undefined` | -| clientOptions | Additional Redis client options (e.g., username, password) | `undefined` | +| Option | Description | Default Value | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| redisUrl | Redis connection url | `process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379'` | +| database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | `process.env.VERCEL_ENV === 'production' ? 0 : 1` | +| keyPrefix | Prefix added to all Redis keys | `process.env.VERCEL_URL \|\| 'UNDEFINED_URL_'` | +| sharedTagsKey | Key used to store shared tags hash map in Redis | `'__sharedTags__'` | +| getTimeoutMs | Timeout in milliseconds for time critical Redis operations. If Redis get is not fulfilled within this time, returns null to avoid blocking site rendering. | `process.env.REDIS_COMMAND_TIMEOUT_MS ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500) : 500` | +| revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | `250` | +| avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | `3600000` (1 hour) | +| redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | `true` | +| inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | `10000` | +| defaultStaleAge | Default stale age in seconds for cached items | `1209600` (14 days) | +| estimateExpireAge | Function to calculate expire age (redis TTL value) from stale age | Production: `staleAge * 2`
Other: `staleAge * 1.2` | +| socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `{ connectTimeout: timeoutMs }` | +| clientOptions | Additional Redis client options (e.g., username, password) | `undefined` | +| killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | `Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0` | ## TLS Configuration diff --git a/docker/redis.conf b/docker/redis.conf new file mode 100644 index 0000000..81abdee --- /dev/null +++ b/docker/redis.conf @@ -0,0 +1 @@ +notify-keyspace-events Exe \ No newline at end of file diff --git a/package.json b/package.json index 62fd1c4..584cc55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@trieb.work/nextjs-turbo-redis-cache", - "version": "1.7.1", + "version": "1.8.0-beta.6", "repository": { "type": "git", "url": "https://github.com/trieb-work/nextjs-turbo-redis-cache.git" diff --git a/src/RedisStringsHandler.ts b/src/RedisStringsHandler.ts index ac0c310..771a8ff 100644 --- a/src/RedisStringsHandler.ts +++ b/src/RedisStringsHandler.ts @@ -13,6 +13,36 @@ export type CacheEntry = { tags: string[]; }; +export function redisErrorHandler>( + debugInfo: string, + redisCommandResult: T, +): T { + const beforeTimestamp = performance.now(); + return redisCommandResult.catch((error) => { + console.error( + 'Redis command error', + (performance.now() - beforeTimestamp).toFixed(2), + 'ms', + debugInfo, + error, + ); + throw error; + }) as T; +} + +// This is a test to check if the event loop is lagging. If it lags, increase CPU of container +setInterval(() => { + const start = performance.now(); + setImmediate(() => { + const duration = performance.now() - start; + if (duration > 100) { + console.warn( + `RedisStringsHandler detected an event loop lag of: ${duration.toFixed(2)}ms`, + ); + } + }); +}, 10); + export type CreateRedisStringsHandlerOptions = { /** Redis redisUrl to use. * @default process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST @@ -28,10 +58,12 @@ export type CreateRedisStringsHandlerOptions = { * @default process.env.VERCEL_URL || 'UNDEFINED_URL_' */ keyPrefix?: string; - /** Timeout in milliseconds for Redis operations - * @default 5000 + /** Timeout in milliseconds for time critical Redis operations (during cache get, which blocks site rendering). + * If redis get is not fulfilled within this time, the cache handler will return null so site rendering will + * not be blocked further and site can fallback to re-render/re-fetch the content. + * @default 500 */ - timeoutMs?: number; + getTimeoutMs?: number; /** Number of entries to query in one batch during full sync of shared tags hash map * @default 250 */ @@ -60,6 +92,10 @@ export type CreateRedisStringsHandlerOptions = { * @default Production: staleAge * 2, Other: staleAge * 1.2 */ estimateExpireAge?: (staleAge: number) => number; + /** Kill container on Redis client error if error threshold is reached + * @default 0 (0 means no error threshold) + */ + killContainerOnErrorThreshold?: number; /** Additional Redis client socket options * @example { tls: true, rejectUnauthorized: false } */ @@ -78,12 +114,7 @@ const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_'; // This helps track when specific tags were last invalidated const REVALIDATED_TAGS_KEY = '__revalidated_tags__'; -export function getTimeoutRedisCommandOptions( - timeoutMs: number, -): CommandOptions { - return commandOptions({ signal: AbortSignal.timeout(timeoutMs) }); -} - +let killContainerOnErrorCount: number = 0; export default class RedisStringsHandler { private client: Client; private sharedTagsMap: SyncedMap; @@ -91,18 +122,19 @@ export default class RedisStringsHandler { private inMemoryDeduplicationCache: SyncedMap< Promise> >; + private getTimeoutMs: number; private redisGet: Client['get']; private redisDeduplicationHandler: DeduplicatedRequestHandler< Client['get'], string | Buffer | null >; private deduplicatedRedisGet: (key: string) => Client['get']; - private timeoutMs: number; private keyPrefix: string; private redisGetDeduplication: boolean; private inMemoryCachingTime: number; private defaultStaleAge: number; private estimateExpireAge: (staleAge: number) => number; + private killContainerOnErrorThreshold: number; constructor({ redisUrl = process.env.REDIS_URL @@ -113,7 +145,9 @@ export default class RedisStringsHandler { database = process.env.VERCEL_ENV === 'production' ? 0 : 1, keyPrefix = process.env.VERCEL_URL || 'UNDEFINED_URL_', sharedTagsKey = '__sharedTags__', - timeoutMs = 5_000, + getTimeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS + ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500) + : 500, revalidateTagQuerySize = 250, avgResyncIntervalMs = 60 * 60 * 1_000, redisGetDeduplication = true, @@ -121,111 +155,184 @@ export default class RedisStringsHandler { defaultStaleAge = 60 * 60 * 24 * 14, estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === 'production' ? staleAge * 2 : staleAge * 1.2, + killContainerOnErrorThreshold = process.env + .KILL_CONTAINER_ON_ERROR_THRESHOLD + ? (Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0) + : 0, socketOptions, clientOptions, }: CreateRedisStringsHandlerOptions) { - this.keyPrefix = keyPrefix; - this.timeoutMs = timeoutMs; - this.redisGetDeduplication = redisGetDeduplication; - this.inMemoryCachingTime = inMemoryCachingTime; - this.defaultStaleAge = defaultStaleAge; - this.estimateExpireAge = estimateExpireAge; - try { - // Create Redis client with properly typed configuration - this.client = createClient({ - url: redisUrl, - ...(database !== 0 ? { database } : {}), - ...(socketOptions ? { socket: socketOptions } : {}), - ...(clientOptions || {}), + this.keyPrefix = keyPrefix; + this.redisGetDeduplication = redisGetDeduplication; + this.inMemoryCachingTime = inMemoryCachingTime; + this.defaultStaleAge = defaultStaleAge; + this.estimateExpireAge = estimateExpireAge; + this.killContainerOnErrorThreshold = killContainerOnErrorThreshold; + this.getTimeoutMs = getTimeoutMs; + + try { + // Create Redis client with properly typed configuration + this.client = createClient({ + url: redisUrl, + pingInterval: 10_000, // Useful with Redis deployments that do not use TCP Keep-Alive. Restarts the connection if it is idle for too long. + ...(database !== 0 ? { database } : {}), + ...(socketOptions ? { socket: { ...socketOptions } } : {}), + ...(clientOptions || {}), + }); + + this.client.on('error', (error) => { + console.error( + 'Redis client error', + error, + killContainerOnErrorCount++, + ); + setTimeout( + () => + this.client.connect().catch((error) => { + console.error( + 'Failed to reconnect Redis client after connection loss:', + error, + ); + }), + 1000, + ); + if ( + this.killContainerOnErrorThreshold > 0 && + killContainerOnErrorCount >= this.killContainerOnErrorThreshold + ) { + console.error( + 'Redis client error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)', + error, + killContainerOnErrorCount++, + ); + this.client.disconnect(); + this.client.quit(); + setTimeout(() => { + process.exit(1); + }, 500); + } + }); + + this.client + .connect() + .then(() => { + console.info('Redis client connected.'); + }) + .catch(() => { + this.client.connect().catch((error) => { + console.error('Failed to connect Redis client:', error); + this.client.disconnect(); + throw error; + }); + }); + } catch (error: unknown) { + console.error('Failed to initialize Redis client'); + throw error; + } + + const filterKeys = (key: string): boolean => + key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey; + + this.sharedTagsMap = new SyncedMap({ + client: this.client, + keyPrefix, + redisKey: sharedTagsKey, + database, + querySize: revalidateTagQuerySize, + filterKeys, + resyncIntervalMs: + avgResyncIntervalMs - + avgResyncIntervalMs / 10 + + Math.random() * (avgResyncIntervalMs / 10), }); - this.client.on('error', (error) => { - console.error('Redis client error', error); + this.revalidatedTagsMap = new SyncedMap({ + client: this.client, + keyPrefix, + redisKey: REVALIDATED_TAGS_KEY, + database, + querySize: revalidateTagQuerySize, + filterKeys, + resyncIntervalMs: + avgResyncIntervalMs + + avgResyncIntervalMs / 10 + + Math.random() * (avgResyncIntervalMs / 10), }); - this.client - .connect() - .then(() => { - console.info('Redis client connected.'); - }) - .catch(() => { - this.client.connect().catch((error) => { - console.error('Failed to connect Redis client:', error); - this.client.disconnect(); - throw error; - }); - }); - } catch (error: unknown) { - console.error('Failed to initialize Redis client'); + this.inMemoryDeduplicationCache = new SyncedMap({ + client: this.client, + keyPrefix, + redisKey: 'inMemoryDeduplicationCache', + database, + querySize: revalidateTagQuerySize, + filterKeys, + customizedSync: { + withoutRedisHashmap: true, + withoutSetSync: true, + }, + }); + + const redisGet: Client['get'] = this.client.get.bind(this.client); + this.redisDeduplicationHandler = new DeduplicatedRequestHandler( + redisGet, + inMemoryCachingTime, + this.inMemoryDeduplicationCache, + ); + this.redisGet = redisGet; + this.deduplicatedRedisGet = + this.redisDeduplicationHandler.deduplicatedFunction; + } catch (error) { + console.error( + 'RedisStringsHandler constructor error', + error, + killContainerOnErrorCount++, + ); + if ( + killContainerOnErrorThreshold > 0 && + killContainerOnErrorCount >= killContainerOnErrorThreshold + ) { + console.error( + 'RedisStringsHandler constructor error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)', + error, + killContainerOnErrorCount++, + ); + process.exit(1); + } throw error; } - - const filterKeys = (key: string): boolean => - key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey; - - this.sharedTagsMap = new SyncedMap({ - client: this.client, - keyPrefix, - redisKey: sharedTagsKey, - database, - timeoutMs, - querySize: revalidateTagQuerySize, - filterKeys, - resyncIntervalMs: - avgResyncIntervalMs - - avgResyncIntervalMs / 10 + - Math.random() * (avgResyncIntervalMs / 10), - }); - - this.revalidatedTagsMap = new SyncedMap({ - client: this.client, - keyPrefix, - redisKey: REVALIDATED_TAGS_KEY, - database, - timeoutMs, - querySize: revalidateTagQuerySize, - filterKeys, - resyncIntervalMs: - avgResyncIntervalMs + - avgResyncIntervalMs / 10 + - Math.random() * (avgResyncIntervalMs / 10), - }); - - this.inMemoryDeduplicationCache = new SyncedMap({ - client: this.client, - keyPrefix, - redisKey: 'inMemoryDeduplicationCache', - database, - timeoutMs, - querySize: revalidateTagQuerySize, - filterKeys, - customizedSync: { - withoutRedisHashmap: true, - withoutSetSync: true, - }, - }); - - const redisGet: Client['get'] = this.client.get.bind(this.client); - this.redisDeduplicationHandler = new DeduplicatedRequestHandler( - redisGet, - inMemoryCachingTime, - this.inMemoryDeduplicationCache, - ); - this.redisGet = redisGet; - this.deduplicatedRedisGet = - this.redisDeduplicationHandler.deduplicatedFunction; } resetRequestCache(): void {} + private clientReadyCalls = 0; + private async assertClientIsReady(): Promise { - await Promise.all([ - this.sharedTagsMap.waitUntilReady(), - this.revalidatedTagsMap.waitUntilReady(), + if (this.clientReadyCalls > 10) { + throw new Error( + 'assertClientIsReady called more than 10 times without being ready.', + ); + } + await Promise.race([ + Promise.all([ + this.sharedTagsMap.waitUntilReady(), + this.revalidatedTagsMap.waitUntilReady(), + ]), + new Promise((_, reject) => + setTimeout(() => { + reject( + new Error( + 'assertClientIsReady: Timeout waiting for Redis maps to be ready', + ), + ); + }, 30_000), + ), ]); + this.clientReadyCalls = 0; if (!this.client.isReady) { - throw new Error('Redis client is not ready yet or connection is lost.'); + throw new Error( + 'assertClientIsReady: Redis client is not ready yet or connection is lost.', + ); } } @@ -247,139 +354,181 @@ export default class RedisStringsHandler { isFallback: boolean; }, ): Promise { - if ( - ctx.kind !== 'APP_ROUTE' && - ctx.kind !== 'APP_PAGE' && - ctx.kind !== 'FETCH' - ) { - console.warn( - 'RedisStringsHandler.get() called with', - key, - ctx, - ' this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ', - (ctx as { kind: string })?.kind, - ); - } - - debug('green', 'RedisStringsHandler.get() called with', key, ctx); - await this.assertClientIsReady(); - - const clientGet = this.redisGetDeduplication - ? this.deduplicatedRedisGet(key) - : this.redisGet; - const serializedCacheEntry = await clientGet( - getTimeoutRedisCommandOptions(this.timeoutMs), - this.keyPrefix + key, - ); - - debug( - 'green', - 'RedisStringsHandler.get() finished with result (serializedCacheEntry)', - serializedCacheEntry?.substring(0, 200), - ); - - if (!serializedCacheEntry) { - return null; - } + try { + if ( + ctx.kind !== 'APP_ROUTE' && + ctx.kind !== 'APP_PAGE' && + ctx.kind !== 'FETCH' + ) { + console.warn( + 'RedisStringsHandler.get() called with', + key, + ctx, + ' this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ', + (ctx as { kind: string })?.kind, + ); + } - const cacheEntry: CacheEntry | null = JSON.parse( - serializedCacheEntry, - bufferReviver, - ); + debug('green', 'RedisStringsHandler.get() called with', key, ctx); + await this.assertClientIsReady(); + + const clientGet = this.redisGetDeduplication + ? this.deduplicatedRedisGet(key) + : this.redisGet; + const serializedCacheEntry = await redisErrorHandler( + 'RedisStringsHandler.get(), operation: get' + + (this.redisGetDeduplication ? 'deduplicated' : '') + + ' ' + + this.getTimeoutMs + + 'ms ' + + this.keyPrefix + + ' ' + + key, + clientGet( + commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }), + this.keyPrefix + key, + ), + ); - debug( - 'green', - 'RedisStringsHandler.get() finished with result (cacheEntry)', - JSON.stringify(cacheEntry).substring(0, 200), - ); + debug( + 'green', + 'RedisStringsHandler.get() finished with result (serializedCacheEntry)', + serializedCacheEntry?.substring(0, 200), + ); - if (!cacheEntry) { - return null; - } + if (!serializedCacheEntry) { + return null; + } - if (!cacheEntry?.tags) { - console.warn( - 'RedisStringsHandler.get() called with', - key, - ctx, - 'cacheEntry is mall formed (missing tags)', - ); - } - if (!cacheEntry?.value) { - console.warn( - 'RedisStringsHandler.get() called with', - key, - ctx, - 'cacheEntry is mall formed (missing value)', + const cacheEntry: CacheEntry | null = JSON.parse( + serializedCacheEntry, + bufferReviver, ); - } - if (!cacheEntry?.lastModified) { - console.warn( - 'RedisStringsHandler.get() called with', - key, - ctx, - 'cacheEntry is mall formed (missing lastModified)', + + debug( + 'green', + 'RedisStringsHandler.get() finished with result (cacheEntry)', + JSON.stringify(cacheEntry).substring(0, 200), ); - } - if (ctx.kind === 'FETCH') { - const combinedTags = new Set([ - ...(ctx?.softTags || []), - ...(ctx?.tags || []), - ]); + if (!cacheEntry) { + return null; + } - if (combinedTags.size === 0) { - return cacheEntry; + if (!cacheEntry?.tags) { + console.warn( + 'RedisStringsHandler.get() called with', + key, + ctx, + 'cacheEntry is mall formed (missing tags)', + ); + } + if (!cacheEntry?.value) { + console.warn( + 'RedisStringsHandler.get() called with', + key, + ctx, + 'cacheEntry is mall formed (missing value)', + ); + } + if (!cacheEntry?.lastModified) { + console.warn( + 'RedisStringsHandler.get() called with', + key, + ctx, + 'cacheEntry is mall formed (missing lastModified)', + ); } - // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route). See revalidateTag() for more information - // - // This code checks if any of the cache tags associated with this entry (normally the internal tag of the parent page/api route containing the fetch request) - // have been revalidated since the entry was last modified. If any tag was revalidated more recently than the entry's - // lastModified timestamp, then the cached content is considered stale (therefore return null) and should be removed. - for (const tag of combinedTags) { - // Get the last revalidation time for this tag from our revalidatedTagsMap - const revalidationTime = this.revalidatedTagsMap.get(tag); - - // If we have a revalidation time for this tag and it's more recent than when - // this cache entry was last modified, the entry is stale - if (revalidationTime && revalidationTime > cacheEntry.lastModified) { - const redisKey = this.keyPrefix + key; - - // We don't await this cleanup since it can happen asynchronously in the background. - // The cache entry is already considered invalid at this point. - this.client - .unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey) - .catch((err) => { - // If the first unlink fails, only log the error - // Never implement a retry here as the cache entry will be updated directly after this get request - console.error( - 'Error occurred while unlinking stale data. Error was:', - err, - ); - }) - .finally(async () => { - // Clean up our tag tracking maps after the Redis key is removed - await this.sharedTagsMap.delete(key); - await this.revalidatedTagsMap.delete(tag); - }); + if (ctx.kind === 'FETCH') { + const combinedTags = new Set([ + ...(ctx?.softTags || []), + ...(ctx?.tags || []), + ]); - debug( - 'green', - 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.', - tag, - redisKey, - revalidationTime, - cacheEntry, - ); + if (combinedTags.size === 0) { + return cacheEntry; + } - // Return null to indicate no valid cache entry was found - return null; + // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route). See revalidateTag() for more information + // + // This code checks if any of the cache tags associated with this entry (normally the internal tag of the parent page/api route containing the fetch request) + // have been revalidated since the entry was last modified. If any tag was revalidated more recently than the entry's + // lastModified timestamp, then the cached content is considered stale (therefore return null) and should be removed. + for (const tag of combinedTags) { + // Get the last revalidation time for this tag from our revalidatedTagsMap + const revalidationTime = this.revalidatedTagsMap.get(tag); + + // If we have a revalidation time for this tag and it's more recent than when + // this cache entry was last modified, the entry is stale + if (revalidationTime && revalidationTime > cacheEntry.lastModified) { + const redisKey = this.keyPrefix + key; + + // We don't await this cleanup since it can happen asynchronously in the background. + // The cache entry is already considered invalid at this point. + this.client + .unlink(redisKey) + .catch((err) => { + // Log error but don't retry deletion since the cache entry will likely be + // updated immediately after via set(). A retry could dangerously execute + // after the new value is set. + console.error( + 'Error occurred while unlinking stale data. Error was:', + err, + ); + }) + .finally(async () => { + // Clean up our tag tracking maps after the Redis key is removed + await this.sharedTagsMap.delete(key); + await this.revalidatedTagsMap.delete(tag); + }); + + debug( + 'green', + 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.', + tag, + redisKey, + revalidationTime, + cacheEntry, + ); + + // Return null to indicate no valid cache entry was found + return null; + } } } - } - return cacheEntry; + return cacheEntry; + } catch (error) { + // This catch block is necessary to handle any errors that may occur during: + // 1. Redis operations (get, unlink) + // 2. JSON parsing of cache entries + // 3. Tag validation and cleanup + // If any error occurs, we return null to indicate no valid cache entry was found, + // allowing the application to regenerate the content rather than crash + console.error( + 'RedisStringsHandler.get() Error occurred while getting cache entry. Returning null so site can continue to serve content while cache is disabled. The original error was:', + error, + killContainerOnErrorCount++, + ); + + if ( + this.killContainerOnErrorThreshold > 0 && + killContainerOnErrorCount >= this.killContainerOnErrorThreshold + ) { + console.error( + 'RedisStringsHandler get() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)', + error, + killContainerOnErrorCount, + ); + this.client.disconnect(); + this.client.quit(); + setTimeout(() => { + process.exit(1); + }, 500); + } + return null; + } } public async set( key: string, @@ -425,182 +574,236 @@ export default class RedisStringsHandler { cacheControl?: { revalidate: 5; expire: undefined }; // Version 15.0.3 }, ) { - if ( - data.kind !== 'APP_ROUTE' && - data.kind !== 'APP_PAGE' && - data.kind !== 'FETCH' - ) { - console.warn( - 'RedisStringsHandler.set() called with', - key, - ctx, - data, - ' this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ', - (data as { kind: string })?.kind, - ); - } + try { + if ( + data.kind !== 'APP_ROUTE' && + data.kind !== 'APP_PAGE' && + data.kind !== 'FETCH' + ) { + console.warn( + 'RedisStringsHandler.set() called with', + key, + ctx, + data, + ' this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ', + (data as { kind: string })?.kind, + ); + } - await this.assertClientIsReady(); + await this.assertClientIsReady(); - if (data.kind === 'APP_PAGE' || data.kind === 'APP_ROUTE') { - const tags = data.headers['x-next-cache-tags']?.split(','); - ctx.tags = [...(ctx.tags || []), ...(tags || [])]; - } + if (data.kind === 'APP_PAGE' || data.kind === 'APP_ROUTE') { + const tags = data.headers['x-next-cache-tags']?.split(','); + ctx.tags = [...(ctx.tags || []), ...(tags || [])]; + } + + // Constructing and serializing the value for storing it in redis + const cacheEntry: CacheEntry = { + lastModified: Date.now(), + tags: ctx?.tags || [], + value: data, + }; + const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer); + + // pre seed data into deduplicated get client. This will reduce redis load by not requesting + // the same value from redis which was just set. + if (this.redisGetDeduplication) { + this.redisDeduplicationHandler.seedRequestReturn( + key, + serializedCacheEntry, + ); + } - // Constructing and serializing the value for storing it in redis - const cacheEntry: CacheEntry = { - lastModified: Date.now(), - tags: ctx?.tags || [], - value: data, - }; - const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer); - - // pre seed data into deduplicated get client. This will reduce redis load by not requesting - // the same value from redis which was just set. - if (this.redisGetDeduplication) { - this.redisDeduplicationHandler.seedRequestReturn( + // TODO: implement expiration based on cacheControl.expire argument, -> probably relevant for cacheLife and "use cache" etc.: https://nextjs.org/docs/app/api-reference/functions/cacheLife + // Constructing the expire time for the cache entry + const revalidate = + // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used + (data.kind === 'FETCH' && data.revalidate) || + ctx.revalidate || + ctx.cacheControl?.revalidate || + (data as { revalidate?: number | false })?.revalidate; + const expireAt = + revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 + ? this.estimateExpireAge(revalidate) + : this.estimateExpireAge(this.defaultStaleAge); + + // Setting the cache entry in redis + const setOperation: Promise = redisErrorHandler( + 'RedisStringsHandler.set(), operation: set ' + + this.keyPrefix + + ' ' + + key, + this.client.set(this.keyPrefix + key, serializedCacheEntry, { + EX: expireAt, + }), + ); + + debug( + 'blue', + 'RedisStringsHandler.set() will set the following serializedCacheEntry', + this.keyPrefix, key, - serializedCacheEntry, + data, + ctx, + serializedCacheEntry?.substring(0, 200), + expireAt, ); - } - // TODO: implement expiration based on cacheControl.expire argument, -> probably relevant for cacheLife and "use cache" etc.: https://nextjs.org/docs/app/api-reference/functions/cacheLife - // Constructing the expire time for the cache entry - const revalidate = - // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used - (data.kind === 'FETCH' && data.revalidate) || - ctx.revalidate || - ctx.cacheControl?.revalidate; - const expireAt = - revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 - ? this.estimateExpireAge(revalidate) - : this.estimateExpireAge(this.defaultStaleAge); - - // Setting the cache entry in redis - const options = getTimeoutRedisCommandOptions(this.timeoutMs); - const setOperation: Promise = this.client.set( - options, - this.keyPrefix + key, - serializedCacheEntry, - { - EX: expireAt, - }, - ); + // Setting the tags for the cache entry in the sharedTagsMap (locally stored hashmap synced via redis) + let setTagsOperation: Promise | undefined; + if (ctx.tags && ctx.tags.length > 0) { + const currentTags = this.sharedTagsMap.get(key); + const currentIsSameAsNew = + currentTags?.length === ctx.tags.length && + currentTags.every((v) => ctx.tags!.includes(v)) && + ctx.tags.every((v) => currentTags.includes(v)); + + if (!currentIsSameAsNew) { + setTagsOperation = this.sharedTagsMap.set( + key, + structuredClone(ctx.tags) as string[], + ); + } + } - debug( - 'blue', - 'RedisStringsHandler.set() will set the following serializedCacheEntry', - this.keyPrefix, - key, - data, - ctx, - serializedCacheEntry?.substring(0, 200), - expireAt, - ); + debug( + 'blue', + 'RedisStringsHandler.set() will set the following sharedTagsMap', + key, + ctx.tags as string[], + ); - // Setting the tags for the cache entry in the sharedTagsMap (locally stored hashmap synced via redis) - let setTagsOperation: Promise | undefined; - if (ctx.tags && ctx.tags.length > 0) { - const currentTags = this.sharedTagsMap.get(key); - const currentIsSameAsNew = - currentTags?.length === ctx.tags.length && - currentTags.every((v) => ctx.tags!.includes(v)) && - ctx.tags.every((v) => currentTags.includes(v)); - - if (!currentIsSameAsNew) { - setTagsOperation = this.sharedTagsMap.set( - key, - structuredClone(ctx.tags) as string[], + await Promise.all([setOperation, setTagsOperation]); + } catch (error) { + console.error( + 'RedisStringsHandler.set() Error occurred while setting cache entry. The original error was:', + error, + killContainerOnErrorCount++, + ); + if ( + this.killContainerOnErrorThreshold > 0 && + killContainerOnErrorCount >= this.killContainerOnErrorThreshold + ) { + console.error( + 'RedisStringsHandler set() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)', + error, + killContainerOnErrorCount, ); + this.client.disconnect(); + this.client.quit(); + setTimeout(() => { + process.exit(1); + }, 500); } + throw error; } - - debug( - 'blue', - 'RedisStringsHandler.set() will set the following sharedTagsMap', - key, - ctx.tags as string[], - ); - - await Promise.all([setOperation, setTagsOperation]); } // eslint-disable-next-line @typescript-eslint/no-explicit-any public async revalidateTag(tagOrTags: string | string[], ...rest: any[]) { - debug( - 'red', - 'RedisStringsHandler.revalidateTag() called with', - tagOrTags, - rest, - ); - const tags = new Set([tagOrTags || []].flat()); - await this.assertClientIsReady(); - - // find all keys that are related to this tag - const keysToDelete: Set = new Set(); - - for (const tag of tags) { - // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route) - // - // Invalidation logic for fetch requests that are related to a invalidated page. - // revalidateTag is called for the page tag (_N_T_...) and the fetch request needs to be invalidated as well - // unfortunately this is not possible since the revalidateTag is not called with any data that would allow us to find the cache entry of the fetch request - // in case of a fetch request get method call, the get method of the cache handler is called with some information about the pages/routes the fetch request is inside - // therefore we only mark the page/route as stale here (with help of the revalidatedTagsMap) - // and delete the cache entry of the fetch request on the next request to the get function - if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) { - const now = Date.now(); - debug( - 'red', - 'RedisStringsHandler.revalidateTag() set revalidation time for tag', - tag, - 'to', - now, - ); - await this.revalidatedTagsMap.set(tag, now); + try { + debug( + 'red', + 'RedisStringsHandler.revalidateTag() called with', + tagOrTags, + rest, + ); + const tags = new Set([tagOrTags || []].flat()); + await this.assertClientIsReady(); + + // find all keys that are related to this tag + const keysToDelete: Set = new Set(); + + for (const tag of tags) { + // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route) + // + // Invalidation logic for fetch requests that are related to a invalidated page. + // revalidateTag is called for the page tag (_N_T_...) and the fetch request needs to be invalidated as well + // unfortunately this is not possible since the revalidateTag is not called with any data that would allow us to find the cache entry of the fetch request + // in case of a fetch request get method call, the get method of the cache handler is called with some information about the pages/routes the fetch request is inside + // therefore we only mark the page/route as stale here (with help of the revalidatedTagsMap) + // and delete the cache entry of the fetch request on the next request to the get function + if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) { + const now = Date.now(); + debug( + 'red', + 'RedisStringsHandler.revalidateTag() set revalidation time for tag', + tag, + 'to', + now, + ); + await this.revalidatedTagsMap.set(tag, now); + } } - } - // Scan the whole sharedTagsMap for keys that are dependent on any of the revalidated tags - for (const [key, sharedTags] of this.sharedTagsMap.entries()) { - if (sharedTags.some((tag) => tags.has(tag))) { - keysToDelete.add(key); + // Scan the whole sharedTagsMap for keys that are dependent on any of the revalidated tags + for (const [key, sharedTags] of this.sharedTagsMap.entries()) { + if (sharedTags.some((tag) => tags.has(tag))) { + keysToDelete.add(key); + } } - } - debug( - 'red', - 'RedisStringsHandler.revalidateTag() found', - keysToDelete, - 'keys to delete', - ); + debug( + 'red', + 'RedisStringsHandler.revalidateTag() found', + keysToDelete, + 'keys to delete', + ); - // exit early if no keys are related to this tag - if (keysToDelete.size === 0) { - return; - } + // exit early if no keys are related to this tag + if (keysToDelete.size === 0) { + return; + } - // prepare deletion of all keys in redis that are related to this tag - const redisKeys = Array.from(keysToDelete); - const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key); - const options = getTimeoutRedisCommandOptions(this.timeoutMs); - const deleteKeysOperation = this.client.unlink(options, fullRedisKeys); + // prepare deletion of all keys in redis that are related to this tag + const redisKeys = Array.from(keysToDelete); + const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key); + const deleteKeysOperation = redisErrorHandler( + 'RedisStringsHandler.revalidateTag(), operation: unlink ' + + this.keyPrefix + + ' ' + + fullRedisKeys, + this.client.unlink(fullRedisKeys), + ); - // also delete entries from in-memory deduplication cache if they get revalidated - if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) { - for (const key of keysToDelete) { - this.inMemoryDeduplicationCache.delete(key); + // also delete entries from in-memory deduplication cache if they get revalidated + if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) { + for (const key of keysToDelete) { + this.inMemoryDeduplicationCache.delete(key); + } } - } - // prepare deletion of entries from shared tags map if they get revalidated so that the map will not grow indefinitely - const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys); + // prepare deletion of entries from shared tags map if they get revalidated so that the map will not grow indefinitely + const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys); - // execute keys and tag maps deletion - await Promise.all([deleteKeysOperation, deleteTagsOperation]); - debug( - 'red', - 'RedisStringsHandler.revalidateTag() finished delete operations', - ); + // execute keys and tag maps deletion + await Promise.all([deleteKeysOperation, deleteTagsOperation]); + debug( + 'red', + 'RedisStringsHandler.revalidateTag() finished delete operations', + ); + } catch (error) { + console.error( + 'RedisStringsHandler.revalidateTag() Error occurred while revalidating tags. The original error was:', + error, + killContainerOnErrorCount++, + ); + if ( + this.killContainerOnErrorThreshold > 0 && + killContainerOnErrorCount >= this.killContainerOnErrorThreshold + ) { + console.error( + 'RedisStringsHandler revalidateTag() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)', + error, + killContainerOnErrorCount, + ); + this.client.disconnect(); + this.client.quit(); + setTimeout(() => { + process.exit(1); + }, 500); + } + throw error; + } } } diff --git a/src/SyncedMap.ts b/src/SyncedMap.ts index 8770165..e89ac8c 100644 --- a/src/SyncedMap.ts +++ b/src/SyncedMap.ts @@ -1,5 +1,5 @@ // SyncedMap.ts -import { Client, getTimeoutRedisCommandOptions } from './RedisStringsHandler'; +import { Client, redisErrorHandler } from './RedisStringsHandler'; import { debugVerbose, debug } from './utils/debug'; type CustomizedSync = { @@ -12,7 +12,6 @@ type SyncedMapOptions = { keyPrefix: string; redisKey: string; // Redis Hash key database: number; - timeoutMs: number; querySize: number; filterKeys: (key: string) => boolean; resyncIntervalMs?: number; @@ -35,7 +34,6 @@ export class SyncedMap { private syncChannel: string; private redisKey: string; private database: number; - private timeoutMs: number; private querySize: number; private filterKeys: (key: string) => boolean; private resyncIntervalMs?: number; @@ -50,7 +48,6 @@ export class SyncedMap { this.redisKey = options.redisKey; this.syncChannel = `${options.keyPrefix}${SYNC_CHANNEL_SUFFIX}${options.redisKey}`; this.database = options.database; - this.timeoutMs = options.timeoutMs; this.querySize = options.querySize; this.filterKeys = options.filterKeys; this.resyncIntervalMs = options.resyncIntervalMs; @@ -85,11 +82,22 @@ export class SyncedMap { try { do { - const remoteItems = await this.client.hScan( - getTimeoutRedisCommandOptions(this.timeoutMs), - this.keyPrefix + this.redisKey, - cursor, - hScanOptions, + const remoteItems = await redisErrorHandler( + 'SyncedMap.initialSync(), operation: hScan ' + + this.syncChannel + + ' ' + + this.keyPrefix + + ' ' + + this.redisKey + + ' ' + + cursor + + ' ' + + this.querySize, + this.client.hScan( + this.keyPrefix + this.redisKey, + cursor, + hScanOptions, + ), ); for (const { field, value } of remoteItems.tuples) { if (this.filterKeys(field)) { @@ -114,10 +122,10 @@ export class SyncedMap { let remoteKeys: string[] = []; try { do { - const remoteKeysPortion = await this.client.scan( - getTimeoutRedisCommandOptions(this.timeoutMs), - cursor, - scanOptions, + const remoteKeysPortion = await redisErrorHandler( + 'SyncedMap.cleanupKeysNotInRedis(), operation: scan ' + + this.keyPrefix, + this.client.scan(cursor, scanOptions), ); remoteKeys = remoteKeys.concat(remoteKeysPortion.keys); cursor = remoteKeysPortion.cursor; @@ -202,7 +210,11 @@ export class SyncedMap { try { await this.subscriberClient.connect().catch(async () => { - await this.subscriberClient.connect(); + console.error('Failed to connect subscriber client. Retrying...'); + await this.subscriberClient.connect().catch((error) => { + console.error('Failed to connect subscriber client.', error); + throw error; + }); }); // Check if keyspace event configuration is set correctly @@ -214,7 +226,9 @@ export class SyncedMap { )?.['notify-keyspace-events']; if (!keyspaceEventConfig.includes('E')) { throw new Error( - "Keyspace event configuration has to include 'E' for Keyevent events, published with __keyevent@__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`", + 'Keyspace event configuration is set to "' + + keyspaceEventConfig + + "\" but has to include 'E' for Keyevent events, published with __keyevent@__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`", ); } if ( @@ -225,7 +239,9 @@ export class SyncedMap { ) ) { throw new Error( - "Keyspace event configuration has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`", + 'Keyspace event configuration is set to "' + + keyspaceEventConfig + + "\" but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`", ); } } @@ -294,13 +310,19 @@ export class SyncedMap { return; } if (!this.customizedSync?.withoutRedisHashmap) { - const options = getTimeoutRedisCommandOptions(this.timeoutMs); operations.push( - this.client.hSet( - options, - this.keyPrefix + this.redisKey, - key as unknown as string, - JSON.stringify(value), + redisErrorHandler( + 'SyncedMap.set(), operation: hSet ' + + this.syncChannel + + ' ' + + this.keyPrefix + + ' ' + + key, + this.client.hSet( + this.keyPrefix + this.redisKey, + key as unknown as string, + JSON.stringify(value), + ), ), ); } @@ -311,14 +333,19 @@ export class SyncedMap { value, }; operations.push( - this.client.publish(this.syncChannel, JSON.stringify(insertMessage)), + redisErrorHandler( + 'SyncedMap.set(), operation: publish ' + + this.syncChannel + + ' ' + + this.keyPrefix + + ' ' + + key, + this.client.publish(this.syncChannel, JSON.stringify(insertMessage)), + ), ); await Promise.all(operations); } - // /api/revalidated-fetch - // true - public async delete( keys: string[] | string, withoutSyncMessage = false, @@ -338,9 +365,18 @@ export class SyncedMap { } if (!this.customizedSync?.withoutRedisHashmap) { - const options = getTimeoutRedisCommandOptions(this.timeoutMs); operations.push( - this.client.hDel(options, this.keyPrefix + this.redisKey, keysArray), + redisErrorHandler( + 'SyncedMap.delete(), operation: hDel ' + + this.syncChannel + + ' ' + + this.keyPrefix + + ' ' + + this.redisKey + + ' ' + + keysArray, + this.client.hDel(this.keyPrefix + this.redisKey, keysArray), + ), ); } @@ -350,7 +386,18 @@ export class SyncedMap { keys: keysArray, }; operations.push( - this.client.publish(this.syncChannel, JSON.stringify(deletionMessage)), + redisErrorHandler( + 'SyncedMap.delete(), operation: publish ' + + this.syncChannel + + ' ' + + this.keyPrefix + + ' ' + + keysArray, + this.client.publish( + this.syncChannel, + JSON.stringify(deletionMessage), + ), + ), ); }