{"_id":"@0xdoublesharp/adaptive-cache","_rev":"8-f65d06d7cc289b13562616bc771e1265","name":"@0xdoublesharp/adaptive-cache","dist-tags":{"latest":"0.1.1"},"versions":{"0.0.1":{"name":"@0xdoublesharp/adaptive-cache","version":"0.0.1","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"_id":"@0xdoublesharp/adaptive-cache@0.0.1","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"dist":{"shasum":"f522c522f9a67a5857c7aac58fd0f08d4dae32f6","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.0.1.tgz","fileCount":12,"integrity":"sha512-rSaA0ea5VLTc1Jrk43+bvW4TQXv6H0/iQjlJn4vCKN58p7aJJoPW4tU1VKdb6z7q5iGbWazyJsxsIJRsrW60rg==","signatures":[{"sig":"MEUCIDgeCMOeGW+solHSWQflvQa3+63zgUiwurzlWRvA/U2UAiEA3KbdBUz/eYpqNx6alQlpM6CIuqv2Mt42/tZKXZddfZE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":148092},"main":"./dist/index.js","_from":"file:0xdoublesharp-adaptive-cache-0.0.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"dev":"tsup --watch","lint":"eslint . --ext .ts","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","typecheck":"tsc --noEmit","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"doublesharp","email":"jsilver@doublesharp.com"},"_resolved":"/private/var/folders/cp/46g8xl4s7j162v11pw1cb8940000gn/T/97732d7f2c4d5d06dc47c7864dbc5e15/0xdoublesharp-adaptive-cache-0.0.1.tgz","_integrity":"sha512-rSaA0ea5VLTc1Jrk43+bvW4TQXv6H0/iQjlJn4vCKN58p7aJJoPW4tU1VKdb6z7q5iGbWazyJsxsIJRsrW60rg==","_npmVersion":"11.6.2","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"22.19.0","dependencies":{"debug":"^4.4.3","ioredis":"^5.8.2"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","tsup":"^8.5.1","vitest":"^4.0.13","express":"^5.1.0","fastify":"^5.6.2","prettier":"^3.6.2","supertest":"^7.1.4","typescript":"^5.9.3","@types/node":"^22.19.0","@types/debug":"^4.1.12","@types/express":"^5.0.5","fastify-plugin":"^5.1.0","testcontainers":"^11.8.1","@types/supertest":"^6.0.3","@vitest/coverage-v8":"^4.0.13"},"peerDependencies":{"express":"^4.18.2","fastify":"^4.0.0 || ^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.0.1_1763937466018_0.8755472885815667","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@0xdoublesharp/adaptive-cache","version":"0.0.2","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"_id":"@0xdoublesharp/adaptive-cache@0.0.2","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"dist":{"shasum":"2456bd57efe1495aef5176eaabe1f72ce8bd9615","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.0.2.tgz","fileCount":12,"integrity":"sha512-yPgnDE+J9D5S7EtfDyjJMeP5jJyYdKfBhd+/Pri/wKQljduLBbXIz2lKvWJeApD6Dh5D3rW3LyJcqkeaUlnwiw==","signatures":[{"sig":"MEQCIAH70WG9imDNTho0TG9EitqOiYO8soeQfSLjKc1QOr50AiBQDQq+lY9cGZLpiSzMxKJpbeZ4+zMnYR+RQCKfa87QaA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":148340},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"98c7f112394a711e373f3ed5b58c65821c488294","scripts":{"dev":"tsup --watch","lint":"eslint . --ext .ts","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","typecheck":"tsc --noEmit","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"doublesharp","email":"jsilver@doublesharp.com"},"_npmVersion":"11.6.2","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"22.19.0","dependencies":{"debug":"^4.4.3","ioredis":"^5.8.2"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","tsup":"^8.5.1","vitest":"^4.0.13","express":"^5.1.0","fastify":"^5.6.2","prettier":"^3.6.2","supertest":"^7.1.4","typescript":"^5.9.3","@types/node":"^22.19.0","@types/debug":"^4.1.12","@types/express":"^5.0.5","fastify-plugin":"^5.1.0","testcontainers":"^11.8.1","@types/supertest":"^6.0.3","@vitest/coverage-v8":"^4.0.13"},"peerDependencies":{"express":"^4.18.2","fastify":"^4.0.0 || ^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.0.2_1763938155127_0.546055774744749","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@0xdoublesharp/adaptive-cache","version":"0.0.3","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"_id":"@0xdoublesharp/adaptive-cache@0.0.3","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"dist":{"shasum":"135e8e62fd5937e962126b421b0d2ab8b4426ea7","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.0.3.tgz","fileCount":12,"integrity":"sha512-p+aVANFpu20pgtNTUsT8A6OYdwzPDjugNX8x/uao7eXjYniQK6hLUn8LinTyW4fi5C3Zzi4hWwZNFxzQc1RFKw==","signatures":[{"sig":"MEQCIGfqTql41ypQv1lAQ6IZNCmnVQnQduEEgUwfjIG2U+9OAiAFrrfTPCut9pQ8a8CPV1A9FEA1EzpqEZa3PBm2I7adiw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":148340},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"9a60da6980fa9c6cf010c42555bf58281f02e850","scripts":{"dev":"tsup --watch","lint":"eslint . --ext .ts","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","typecheck":"tsc --noEmit","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"doublesharp","email":"jsilver@doublesharp.com"},"_npmVersion":"11.6.2","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"22.19.0","dependencies":{"debug":"^4.4.3","ioredis":"^5.8.2"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","tsup":"^8.5.1","vitest":"^4.0.13","express":"^5.1.0","fastify":"^5.6.2","prettier":"^3.6.2","supertest":"^7.1.4","typescript":"^5.9.3","@types/node":"^22.19.0","@types/debug":"^4.1.12","@types/express":"^5.0.5","fastify-plugin":"^5.1.0","testcontainers":"^11.8.1","@types/supertest":"^6.0.3","@vitest/coverage-v8":"^4.0.13"},"peerDependencies":{"express":"^4.18.2","fastify":"^4.0.0 || ^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.0.3_1763941437075_0.9415782998325049","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@0xdoublesharp/adaptive-cache","version":"0.0.4","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"_id":"@0xdoublesharp/adaptive-cache@0.0.4","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"dist":{"shasum":"c9f706afdbebf56c5943d4f9703d8de828b73db7","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.0.4.tgz","fileCount":12,"integrity":"sha512-WjxDqQrDFTtd+DMs9yUnJTcb7nQTFRAkP6g4EElL42rfTL/wBHtuFCd+U6vSVWQB0Ps460ImPCo/4JmoC4RMHg==","signatures":[{"sig":"MEUCIQC2aeLaXhQ8xyd+PB+BOw40h8+prWNm4I8VQ4pDjv/bjgIgRYoi/XAIAGnmitp1y7ClW9biLaSw1Hrghp9/kO+Wvq4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":147027},"main":"./dist/index.js","_from":"file:0xdoublesharp-adaptive-cache-0.0.4.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"dev":"tsup --watch","lint":"eslint . --ext .ts","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","typecheck":"tsc --noEmit","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"doublesharp","email":"jsilver@doublesharp.com"},"_resolved":"/private/var/folders/cp/46g8xl4s7j162v11pw1cb8940000gn/T/a1a6884142901e53fd2f9661bf3e5548/0xdoublesharp-adaptive-cache-0.0.4.tgz","_integrity":"sha512-WjxDqQrDFTtd+DMs9yUnJTcb7nQTFRAkP6g4EElL42rfTL/wBHtuFCd+U6vSVWQB0Ps460ImPCo/4JmoC4RMHg==","_npmVersion":"11.6.2","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"22.19.0","dependencies":{"debug":"^4.4.3","ioredis":"^5.8.2"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","tsup":"^8.5.1","vitest":"^4.0.13","express":"^5.1.0","fastify":"^5.6.2","prettier":"^3.6.2","supertest":"^7.1.4","typescript":"^5.9.3","@types/node":"^22.19.0","@types/debug":"^4.1.12","@types/express":"^5.0.5","fastify-plugin":"^5.1.0","testcontainers":"^11.8.1","@types/supertest":"^6.0.3","@vitest/coverage-v8":"^4.0.13"},"peerDependencies":{"express":"^4.18.2","fastify":"^4.0.0 || ^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.0.4_1763941711707_0.543124905749178","host":"s3://npm-registry-packages-npm-production"}},"0.0.5":{"name":"@0xdoublesharp/adaptive-cache","version":"0.0.5","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"author":{"name":"Justin Silver"},"license":"MIT","_id":"@0xdoublesharp/adaptive-cache@0.0.5","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"homepage":"https://github.com/doublesharp/adaptive-cache#readme","bugs":{"url":"https://github.com/doublesharp/adaptive-cache/issues"},"dist":{"shasum":"42a27dccc8dd98a556207cee4f160cd92ef321b3","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.0.5.tgz","fileCount":15,"integrity":"sha512-wrfpzL8lCU076wpfilmHmarpnTXakj7j7EB4ohjBjZ5nzaDXB582DltOzDln89OFwldNBvXa9pVcRB6eLqC34A==","signatures":[{"sig":"MEUCIFNbzs0LOx4c7t38VQCIr9NESBjVLQw+EdbQ/OmZO/8gAiEA0m+H7DllKTzyOm0d+pusA2cGxLpSOQIwqbyv874QT5w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@0xdoublesharp%2fadaptive-cache@0.0.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":296890},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"5b92262b16433f24dde19e7dc4526de03ef981e2","scripts":{"dev":"tsup --watch","lint":"eslint .","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","quality":"pnpm run lint && pnpm run typecheck && pnpm run test:coverage && pnpm run build","typecheck":"tsc --noEmit","bench:configs":"node --expose-gc scripts/benchmark-configs.mjs","test:coverage":"vitest run --coverage test/Backends.test.ts test/ClusteredLruBackend.test.ts test/LruMiddleware.test.ts test/SingletonIndex.test.ts test/utils.test.ts test/Logger.test.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:455fae00-8292-4170-9d2c-dae10341e171"}},"repository":{"url":"git+https://github.com/doublesharp/adaptive-cache.git","type":"git"},"_npmVersion":"11.11.0","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"24.14.1","dependencies":{"debug":"^4.4.3","ioredis":"^5.10.1"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.25.0","devDependencies":{"c8":"^11.0.0","tsup":"^8.5.1","eslint":"^10.3.0","vitest":"^4.1.6","express":"^5.2.1","fastify":"^5.8.5","prettier":"^3.8.3","lru-cache":"^11.3.6","supertest":"^7.2.2","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.7.0","@types/debug":"^4.1.13","@types/express":"^5.0.6","fastify-plugin":"^5.1.0","testcontainers":"^11.14.0","@types/supertest":"^7.2.0","@vitest/coverage-v8":"^4.1.6","eslint-config-prettier":"^10.1.8","@typescript-eslint/parser":"^8.59.3","@typescript-eslint/eslint-plugin":"^8.59.3","@0xdoublesharp/lru-cache-clustered":"^2.1.0"},"peerDependencies":{"express":"^5.2.1","fastify":"^5.8.5","lru-cache":"^11.0.0","@0xdoublesharp/lru-cache-clustered":"^2.1.0"},"peerDependenciesMeta":{"lru-cache":{"optional":true},"@0xdoublesharp/lru-cache-clustered":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.0.5_1778569125726_0.8683965267037475","host":"s3://npm-registry-packages-npm-production"}},"0.0.6":{"name":"@0xdoublesharp/adaptive-cache","version":"0.0.6","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"author":{"name":"Justin Silver"},"license":"MIT","_id":"@0xdoublesharp/adaptive-cache@0.0.6","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"homepage":"https://github.com/doublesharp/adaptive-cache#readme","bugs":{"url":"https://github.com/doublesharp/adaptive-cache/issues"},"dist":{"shasum":"157d48f73c625af569c3fca9f2e0ed5442ed9941","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.0.6.tgz","fileCount":15,"integrity":"sha512-uG+vov1BkiCDWDQCj+gci7UBbuDt5ogHD9V4KUkGjlrpN3rdnA3ag/7cgljVeKSKdLWqcLlSd36t0BQkiHsXeA==","signatures":[{"sig":"MEUCIEMqzlvcMsJRgtXjgeJ8P1PqhHirbeXULb/Wm8bZTjSUAiEAvvFoeYFNNyUErFmZ5CjBC/D/XQA8+9sqTuTCzjFazPs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@0xdoublesharp%2fadaptive-cache@0.0.6","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":306982},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"1adee3af617a149b9dfb1388b25398e4c9b07800","scripts":{"dev":"tsup --watch","lint":"eslint .","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","quality":"pnpm run lint && pnpm run typecheck && pnpm run test:coverage && pnpm run build","typecheck":"tsc --noEmit","bench:configs":"node --expose-gc scripts/benchmark-configs.mjs","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:455fae00-8292-4170-9d2c-dae10341e171"}},"repository":{"url":"git+https://github.com/doublesharp/adaptive-cache.git","type":"git"},"_npmVersion":"11.12.1","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"24.15.0","dependencies":{"debug":"^4.4.3","ioredis":"^5.10.1"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.25.0","devDependencies":{"c8":"^11.0.0","tsup":"^8.5.1","eslint":"^10.3.0","vitest":"^4.1.6","express":"^5.2.1","fastify":"^5.8.5","prettier":"^3.8.3","lru-cache":"^11.3.6","supertest":"^7.2.2","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.7.0","@types/debug":"^4.1.13","@types/express":"^5.0.6","testcontainers":"^11.14.0","@types/supertest":"^7.2.0","@vitest/coverage-v8":"^4.1.6","eslint-config-prettier":"^10.1.8","@typescript-eslint/parser":"^8.59.3","@typescript-eslint/eslint-plugin":"^8.59.3","@0xdoublesharp/lru-cache-clustered":"^2.1.0"},"peerDependencies":{"express":"^4.18.2 || ^5.0.0","fastify":"^4.0.0 || ^5.0.0","lru-cache":"^11.0.0","@0xdoublesharp/lru-cache-clustered":"^2.1.0"},"peerDependenciesMeta":{"express":{"optional":true},"fastify":{"optional":true},"lru-cache":{"optional":true},"@0xdoublesharp/lru-cache-clustered":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.0.6_1778837065925_0.7919762970928514","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@0xdoublesharp/adaptive-cache","version":"0.1.0","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"author":{"name":"Justin Silver"},"license":"MIT","_id":"@0xdoublesharp/adaptive-cache@0.1.0","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"homepage":"https://github.com/doublesharp/adaptive-cache#readme","bugs":{"url":"https://github.com/doublesharp/adaptive-cache/issues"},"dist":{"shasum":"f1d80d20a4289e46ce0311c09255a348f6376dca","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.1.0.tgz","fileCount":22,"integrity":"sha512-coKB+f3bJzcnFndwMjYy252OX7Vjj0MTPo7dLKZyXhmaeiRmmI69RR46YAQNmMvKWydjArTsDDnKlezqi15Pfw==","signatures":[{"sig":"MEYCIQD3G4SQlhqDVsyucX1cLv+d8wSSAdOafqUHzNuSIex+mgIhANBoDocAMd7rfPX/VFtptGr7raAoaM1DgjVadvs2bSzp","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDL/6KFkowSU4rf8hSaq8J8UoLleO9KsuU3zki7BUbdfwIgSc/wfm7YrpDpwY78Y3sUn6OU6NEBT6S0N+v3C/iTGXA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@0xdoublesharp%2fadaptive-cache@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":688286},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"37657ef95a8c356a2d68984775d8e6fd713dc31c","scripts":{"dev":"tsup --watch","lint":"eslint .","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","quality":"pnpm run lint && pnpm run typecheck && pnpm run test:coverage && pnpm run build && pnpm run test:types","typecheck":"tsc --noEmit","test:types":"tsc --ignoreConfig --noEmit --strict --target es2022 --module nodenext --moduleResolution nodenext --esModuleInterop --skipLibCheck test/types/consumer.ts","bench:configs":"node --expose-gc scripts/benchmark-configs.mjs","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:455fae00-8292-4170-9d2c-dae10341e171"}},"repository":{"url":"git+https://github.com/doublesharp/adaptive-cache.git","type":"git"},"_npmVersion":"11.19.0","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"_nodeVersion":"24.20.0","dependencies":{"debug":"^4.4.3","ioredis":"^6.0.0"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.25.0","devDependencies":{"tsup":"^8.5.1","eslint":"^10.9.1","vitest":"^5.0.0","express":"^5.2.1","fastify":"^5.12.1","prettier":"^3.9.6","lru-cache":"^11.5.2","supertest":"^7.2.2","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.4.1","@types/debug":"^4.1.13","@types/express":"^5.0.6","testcontainers":"^12.1.0","@types/supertest":"^7.2.1","@vitest/coverage-v8":"^5.0.0","eslint-config-prettier":"^10.1.8","@typescript-eslint/parser":"^8.69.0","@typescript-eslint/eslint-plugin":"^8.69.0","@0xdoublesharp/lru-cache-clustered":"^2.1.0"},"peerDependencies":{"express":"^4.18.2 || ^5.0.0","fastify":"^4.0.0 || ^5.0.0","lru-cache":"^11.0.0","@0xdoublesharp/lru-cache-clustered":"^2.1.0"},"peerDependenciesMeta":{"express":{"optional":true},"fastify":{"optional":true},"lru-cache":{"optional":true},"@0xdoublesharp/lru-cache-clustered":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/adaptive-cache_0.1.0_1789145804263_0.9251144710165617","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"_id":"@0xdoublesharp/adaptive-cache@0.1.1","bugs":{"url":"https://github.com/doublesharp/adaptive-cache/issues"},"dist":{"shasum":"c5e32f2ba7300403ccbf0ad5757a4019b8d636d8","tarball":"https://registry.npmjs.org/@0xdoublesharp/adaptive-cache/-/adaptive-cache-0.1.1.tgz","fileCount":26,"integrity":"sha512-eWxxBSQYhM+wSP6Lh5Gb13xpE6988Fs+xlFLKWkfDjXu+M7Sycz82saQV1mC4ryHErA3ug2Rfv55W5ajl0vLdw==","signatures":[{"sig":"MEUCIAIboj7roKVjfEUbeOQz2slbz7vhcFonA41rs265GrCCAiEAs95/5ZfmVfVj+9ygYFqoQQ/mbRsKxVu7Z/CoN6QmI+8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICUVjbii3Wx0vE+sk6HnJOgPv/q877YwWMgIM/vcTwmhAiEAzE51YO0QkM2+b9n0OvL0SoDdW4/B7W7ijQIEEdatyY8="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@0xdoublesharp%2fadaptive-cache@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":771677},"main":"./dist/index.js","name":"@0xdoublesharp/adaptive-cache","types":"./dist/index.d.ts","author":{"name":"Justin Silver"},"module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"a5f45fbf58c663bc716d1660473a3218bc197619","license":"MIT","scripts":{"dev":"tsup --watch","lint":"eslint .","test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","format":"prettier --write .","quality":"pnpm run lint && pnpm run typecheck && pnpm run test:coverage && pnpm run build && pnpm run test:types","bench:lru":"node --expose-gc scripts/benchmark-lru-upgrade.mjs","typecheck":"tsc --noEmit","test:types":"tsc --ignoreConfig --noEmit --strict --target es2022 --module nodenext --moduleResolution nodenext --esModuleInterop --skipLibCheck test/types/consumer.ts","bench:configs":"node --expose-gc scripts/benchmark-configs.mjs","test:coverage":"vitest run --coverage"},"version":"0.1.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:455fae00-8292-4170-9d2c-dae10341e171"}},"homepage":"https://github.com/doublesharp/adaptive-cache#readme","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"repository":{"url":"git+https://github.com/doublesharp/adaptive-cache.git","type":"git"},"_npmVersion":"11.19.0","description":"Adaptive caching module for Redis with Fastify and Express integration","directories":{},"maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"_nodeVersion":"24.20.0","dependencies":{"debug":"^4.4.3","ioredis":"^6.0.0"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.25.0","devDependencies":{"tsup":"^8.5.1","eslint":"^10.9.1","vitest":"^5.0.0","express":"^5.2.1","fastify":"^5.12.1","prettier":"^3.9.6","lru-cache":"^11.5.2","supertest":"^7.2.2","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.4.1","@types/debug":"^4.1.13","@types/express":"^5.0.6","testcontainers":"^12.1.0","@types/supertest":"^7.2.1","@vitest/coverage-v8":"^5.0.0","eslint-config-prettier":"^10.1.8","@typescript-eslint/parser":"^8.69.0","@typescript-eslint/eslint-plugin":"^8.69.0","@0xdoublesharp/lru-cache-clustered":"^2.1.1"},"peerDependencies":{"express":"^4.18.2 || ^5.0.0","fastify":"^4.0.0 || ^5.0.0","lru-cache":"^11.0.0","@0xdoublesharp/lru-cache-clustered":"^2.1.1"},"peerDependenciesMeta":{"express":{"optional":true},"fastify":{"optional":true},"lru-cache":{"optional":true},"@0xdoublesharp/lru-cache-clustered":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/adaptive-cache_0.1.1_1789149566864_0.7351020193295699"}}},"time":{"created":"2025-11-23T22:37:45.908Z","modified":"2026-09-11T17:59:27.334Z","0.0.1":"2025-11-23T22:37:46.215Z","0.0.2":"2025-11-23T22:49:15.352Z","0.0.3":"2025-11-23T23:43:57.327Z","0.0.4":"2025-11-23T23:48:31.923Z","0.0.5":"2026-05-12T06:58:45.872Z","0.0.6":"2026-05-15T09:24:26.097Z","0.1.0":"2026-09-11T16:56:44.353Z","0.1.1":"2026-09-11T17:59:26.948Z"},"bugs":{"url":"https://github.com/doublesharp/adaptive-cache/issues"},"author":{"name":"Justin Silver"},"license":"MIT","homepage":"https://github.com/doublesharp/adaptive-cache#readme","keywords":["adaptive","cache","redis","fastify","express","typescript","nodejs","lua","compression"],"repository":{"url":"git+https://github.com/doublesharp/adaptive-cache.git","type":"git"},"description":"Adaptive caching module for Redis with Fastify and Express integration","maintainers":[{"name":"doublesharp","email":"jsilver@doublesharp.com"}],"readme":"# @0xdoublesharp/adaptive-cache\n\n[![npm version](https://img.shields.io/npm/v/@0xdoublesharp/adaptive-cache.svg)](https://www.npmjs.com/package/@0xdoublesharp/adaptive-cache)\n[![npm downloads](https://img.shields.io/npm/dm/@0xdoublesharp/adaptive-cache.svg)](https://npm-stat.com/charts.html?package=%400xdoublesharp%2Fadaptive-cache)\n[![CI](https://github.com/doublesharp/adaptive-cache/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/doublesharp/adaptive-cache/actions?query=workflow%3ACI+branch%3Amain)\n[![coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fdoublesharp.github.io%2Fadaptive-cache%2Fcoverage-badge.json)](https://doublesharp.github.io/adaptive-cache/coverage/)\n[![license](https://img.shields.io/github/license/doublesharp/adaptive-cache.svg)](https://github.com/doublesharp/adaptive-cache/blob/main/LICENSE)\n\nServer-side response caching for APIs that need fast reads without hand-tuning every TTL.\n\n`@0xdoublesharp/adaptive-cache` caches Express, Fastify, and direct `AdaptiveCache` results. It hashes response bodies, tracks whether content changes, and automatically grows TTLs for stable data while resetting TTLs for volatile data.\n\nRedis remains the default authoritative backend through Lua scripts. To keep hot payloads in memory, add a bounded clustered LRU cache in front of Redis. L1 hits still validate invalidation state against Redis. For single-server or one-host Node cluster deployments, the clustered LRU backend can run without Redis.\n\nSee [loading, freshness, and cache policy](docs/cache-features.md) for `getOrSet`, stale refresh, negative caching, typed/bulk APIs, write fencing, selective compression, events, and HTTP identity controls.\n\nThe feature guide and examples below describe version 0.1.1. See the [0.1.1 release notes](CHANGELOG.md#011---2026-09-11) for batching and dependency fixes, and [0.1.0 release notes](CHANGELOG.md#010---2026-09-10) for new APIs and behavior since 0.0.6; see [release history and upgrade instructions](docs/releases.md) before deploying them.\n\nThis package does not generate HTTP `Cache-Control` headers. It honors storage restrictions and replays eligible cached headers. It caches server responses and adds diagnostic `X-Cache-*` headers unless disabled.\n\n## Use It For\n\n| Need                         | What the module does                                                                 |\n| ---------------------------- | ------------------------------------------------------------------------------------ |\n| API responses that stabilize | Increases TTL when response hashes stay the same.                                    |\n| API responses that change    | Resets TTL when content changes, so volatile data does not stay stale for long.      |\n| Cross-host correctness       | Uses Redis Lua as the atomic authority for payloads, metadata, tags, and locks.      |\n| Hot payloads in memory       | Keeps hot payloads in bounded L1 memory with a Redis validation round trip.          |\n| No-Redis deployments         | Provides a volatile `clustered-lru` backend for single-server or one-host clusters.  |\n| Expensive refreshes          | Coalesces `getOrSet` loaders and fences Redis writes after lease loss.               |\n| Targeted invalidation        | Invalidates keys, tags, or namespaces and rejects obsolete Redis writes.             |\n| Memory safety                | Sets L1 size budgets and skips entries that are too large for the configured budget. |\n\nUse `getOrSet` for expensive work that needs coalescing, stale refresh, or negative caching. Use ordinary middleware when a route only needs response caching. Stale background refresh requires an explicit loader.\n\n## Architecture\n\n![Adaptive Cache architecture](docs/adaptive-cache-architecture.svg)\n\n## Feature Matrix\n\n| Capability                     | `redis` | `l1-redis` | `clustered-lru` |\n| ------------------------------ | :-----: | :--------: | :-------------: |\n| Adaptive TTL growth/reset      |   Yes   |    Yes     |       Yes       |\n| Redis Lua atomic metadata      |   Yes   |    Yes     |       No        |\n| Bounded L1 hot reads           |   No    |    Yes     |       Yes       |\n| Cross-host cache state         |   Yes   |    Yes     |       No        |\n| Tag invalidation               |   Yes   |    Yes     |       Yes       |\n| Refresh locks                  |   Yes   |    Yes     |   Host-local    |\n| Works without Redis            |   No    |     No     |       Yes       |\n| Recommended production default |   Yes   |  Hot APIs  |   Single host   |\n\nAll built-in backends support typed/bulk APIs, stale windows, negative caching, selective compression, and events. Redis-backed modes provide atomic loader lease and write-token checks. Clustered LRU provides host-local coordination with weaker write/release guarantees.\n\n## Installation\n\nRequires Node.js 22 or newer.\n\nRedis-only usage:\n\n```bash\npnpm add @0xdoublesharp/adaptive-cache\n```\n\nExpress or Fastify users should also install the framework they use:\n\n```bash\npnpm add express\npnpm add fastify\n```\n\nLRU-backed modes require optional peer dependencies:\n\n```bash\npnpm add @0xdoublesharp/lru-cache-clustered@^2.1.1 lru-cache@^11\n```\n\nRedis-only users do not need the LRU packages at runtime.\n\n## Backend Modes\n\n| Backend         | Redis required | Best for                                | Behavior                                                           |\n| --------------- | -------------- | --------------------------------------- | ------------------------------------------------------------------ |\n| `redis`         | Yes            | Default production mode                 | Redis Lua stores data, metadata, tags, and locks.                  |\n| `l1-redis`      | Yes            | Production hot paths                    | Clustered LRU serves hot reads; Redis Lua remains authoritative.   |\n| `clustered-lru` | No             | Single-server or one-host Node clusters | Volatile bounded cache with adaptive TTL logic in process/cluster. |\n| custom backend  | Depends        | Advanced integrations                   | Provide the `AdaptiveCacheBackend` interface.                      |\n\n### `redis`\n\nThis is the default. It uses Redis Lua scripts for:\n\n- Fetching cached data and metadata.\n- Updating cached data and adaptive metadata atomically.\n- Refresh locks.\n- Tag invalidation.\n\n```typescript\nimport { adaptiveExpressCache } from '@0xdoublesharp/adaptive-cache'\n\napp.get('/api/summary', adaptiveExpressCache(), handler)\n```\n\n### `l1-redis`\n\nUse this when Redis exists but hot reads should be served from a bounded local/clustered LRU cache.\n\n```typescript\nimport { adaptiveExpressCache } from '@0xdoublesharp/adaptive-cache'\n\napp.get(\n  '/api/summary',\n  adaptiveExpressCache({\n    backend: 'l1-redis',\n    initialTTL: 5,\n    maxTTL: 900,\n    lru: {\n      namespace: 'api-cache',\n      maxSizeBytes: 64 * 1024 * 1024,\n      maxEntrySizeBytes: 6 * 1024 * 1024,\n    },\n  }),\n  handler,\n)\n```\n\nRead path:\n\n1. Read Redis invalidation barriers and check clustered LRU.\n2. On a valid L1 hit, return the payload without fetching or decoding it from Redis.\n3. On L1 miss, fetch from Redis Lua.\n4. On Redis hit, hydrate L1 with Redis remaining TTL and metadata.\n5. On Redis miss, continue to the handler.\n\nWrite path:\n\n1. Encode/compress once and hash the response.\n2. Store in L1 immediately with `initialTTL`.\n3. Run Redis Lua update asynchronously.\n4. When Redis returns, reconcile L1 with Redis' authoritative adaptive TTL and metadata.\n5. If Redis update fails, keep the short-lived L1 entry and log the failure.\n\nFor shutdown-sensitive workers, use `l1Redis.writeMode: 'await-redis'` to wait for Redis before `set()` resolves, or call `cache.flush()` before process shutdown to wait for pending async Redis writes.\n\nLoader writes, middleware writes, and asynchronous compression wait for Redis to accept their write token. Ordinary synchronous `set()` calls retain the optimistic write path above. Every L1 hit validates its token against Redis, including when a Pub/Sub message was missed. This adds a Redis round trip to hot reads in exchange for stronger invalidation guarantees.\n\nInvalidation:\n\n- Explicit clears drain pending writes on the shared Redis client, delete Redis data, drain active L1 hydrations, remove L1 entries, then publish an invalidation message.\n- Reads that overlap an invalidation cannot hydrate an older snapshot back into L1.\n- Tag invalidation removes matching L1 entries and Redis entries.\n- Redis update publishes L1 invalidation only when content changed.\n- Other server-local L1 caches subscribe to Redis invalidation messages and delete stale keys.\n- Namespace and write barriers prevent invalidated in-flight work from becoming a later cache hit.\n\nLocks:\n\n- `l1-redis` uses Redis Lua locks as the distributed lock authority.\n\n### `clustered-lru`\n\nUse this when Redis is not available and a volatile bounded backend is still useful.\n\n```typescript\nimport { adaptiveFastifyCache } from '@0xdoublesharp/adaptive-cache'\n\nawait fastify.register(\n  adaptiveFastifyCache({\n    backend: 'clustered-lru',\n    initialTTL: 5,\n    maxTTL: 300,\n    lru: {\n      namespace: 'api-cache',\n      maxSizeBytes: 64 * 1024 * 1024,\n    },\n  }),\n)\n```\n\nThis mode keeps adaptive metadata in separate bounded LRU entries for `metaTTL`, so unchanged data can increase its TTL after the payload expires. It is fast and memory-bounded, but it is not cross-host durable. Treat it as process/host-local cache state.\n\nLocks:\n\n- `clustered-lru` acquires locks atomically on the local Node cluster primary. Lock release is best-effort: the dependency has no atomic compare-and-delete operation, so release can race with an expired or force-replaced lock. Use a Redis-backed backend when strict refresh coordination is required.\n\n## LRU Memory Limits\n\n`l1-redis` and `clustered-lru` store an envelope in clustered LRU:\n\n- Encoded payload.\n- Response hash.\n- Adaptive metadata: `dataTTL`, `lastChanged`, `changeCount`.\n- Created/updated timestamps.\n- Expiration timestamp.\n- Tags.\n\nDefaults:\n\n| Option                  | Default               | Description                                          |\n| ----------------------- | --------------------- | ---------------------------------------------------- |\n| `lru.maxSizeBytes`      | `64 * 1024 * 1024`    | Total LRU size budget.                               |\n| `lru.maxEntrySizeBytes` | 10% of `maxSizeBytes` | Entries larger than this are skipped for L1 storage. |\n| `lru.timeout`           | package default       | Clustered LRU operation timeout.                     |\n| `lru.failsafe`          | `'reject'`            | Clustered LRU failure behavior.                      |\n| `lru.namespace`         | `'adaptive-cache'`    | Shared cache namespace.                              |\n\nEntry size is estimated as encoded payload bytes plus tag metadata and a small fixed metadata overhead. TTLs passed to clustered LRU are milliseconds; adaptive cache options use seconds.\n\n## LRU Cache Clustered v2.1 Local L1\n\nThe `lru.localL1` option is passed through to `@0xdoublesharp/lru-cache-clustered` v2.1.1+ for per-worker local L1 caching.\n\n```typescript\nadaptiveExpressCache({\n  backend: 'l1-redis',\n  lru: {\n    namespace: 'api-cache',\n    maxSizeBytes: 64 * 1024 * 1024,\n    localL1: {\n      enabled: true,\n      experimental: true,\n      ttl: 2_000,\n      maxSize: 4 * 1024 * 1024,\n      invalidation: 'broadcast',\n      methods: {\n        get: true,\n        has: true,\n        fetch: true,\n        memoize: true,\n      },\n    },\n  },\n})\n```\n\nYou can also pass `localL1: true` when using `@0xdoublesharp/lru-cache-clustered` v2.1.1 or newer.\n\n## Express Usage\n\n```typescript\nimport express from 'express'\nimport { adaptiveExpressCache } from '@0xdoublesharp/adaptive-cache'\n\nconst app = express()\n\napp.get(\n  '/api/items',\n  adaptiveExpressCache({\n    initialTTL: 10,\n    maxTTL: 3600,\n    ttlScaling: 1.5,\n    includeDebugHeaders: true,\n  }),\n  async (_req, res) => {\n    const items = await loadItems()\n    res.json(items)\n  },\n)\n```\n\n### Dynamic `maxTTL`\n\n`maxTTL` can be a function. Middleware resolves it after your handler returns.\n\n```typescript\napp.get(\n  '/api/items/:id',\n  adaptiveExpressCache({\n    initialTTL: 60,\n    maxTTL: (data) => (data.status === 'ended' ? 86400 : 300),\n  }),\n  async (req, res) => {\n    res.json(await loadItem(req.params.id))\n  },\n)\n```\n\n### Tags\n\nTags can be static or request-derived.\n\n```typescript\napp.get(\n  '/api/users/:id',\n  adaptiveExpressCache({\n    tags: (req) => [`user:${req.params.id}`, 'users'],\n  }),\n  async (req, res) => {\n    res.json(await loadUser(req.params.id))\n  },\n)\n```\n\nClear by tag:\n\n```typescript\nimport { getDefaultCache } from '@0xdoublesharp/adaptive-cache'\n\nawait getDefaultCache().invalidateTags(['users'])\n```\n\nClear one computed middleware key:\n\n```typescript\nimport { clearAdaptiveCache } from '@0xdoublesharp/adaptive-cache'\n\nawait clearAdaptiveCache('/api/users/123', {}, 'adaptive:')\n```\n\nFor non-default backends, pass backend options to `clearAdaptiveCache`:\n\n```typescript\nawait clearAdaptiveCache('/api/users/123', {}, 'adaptive:', {\n  backend: 'clustered-lru',\n  lru: { namespace: 'api-cache' },\n})\n```\n\n## Fastify Usage\n\n```typescript\nimport Fastify from 'fastify'\nimport { adaptiveFastifyCache } from '@0xdoublesharp/adaptive-cache'\n\nconst fastify = Fastify()\n\nawait fastify.register(\n  adaptiveFastifyCache({\n    backend: 'l1-redis',\n    initialTTL: 10,\n    maxTTL: 3600,\n    ttlScaling: 1.5,\n    includeDebugHeaders: true,\n    lru: {\n      namespace: 'fastify-api',\n      maxSizeBytes: 64 * 1024 * 1024,\n    },\n  }),\n)\n\nfastify.get('/api/summary', async () => {\n  return await loadSummary()\n})\n```\n\n## Direct API\n\nUse `AdaptiveCache` outside Express/Fastify.\n\n```typescript\nimport { AdaptiveCache } from '@0xdoublesharp/adaptive-cache'\n\nconst cache = new AdaptiveCache({\n  backend: 'l1-redis',\n  includeDebugHeaders: true,\n  lru: { namespace: 'jobs' },\n})\n\nawait cache.set('adaptive:job:summary', { total: 42 }, { tags: ['jobs'] })\n\nconst hit = await cache.get<{ total: number }>('adaptive:job:summary')\nif (hit) {\n  console.log(hit.data, hit.ttl, hit.metadata)\n}\n\nawait cache.flush()\nawait cache.close()\n```\n\nDirect API keys are complete keys. `keyPrefix` selects coordination and invalidation state but does not prepend text to a direct key. Middleware constructs prefixed keys automatically.\n\nPayloads are stored in a versioned envelope so direct API calls can round-trip JSON values, strings, buffers, and `undefined`. Middleware caches eligible successful GET responses with their status, selected headers, and original body. JSON-looking text and binary responses retain their representation on a hit. Other methods and Node/WHATWG streams bypass caching. HTTP 404 responses require an explicit `negativeTTL`. Existing body-only cache entries remain readable until they expire.\n\n### Coalesced loading and stale refresh\n\n```typescript\nconst cache = new AdaptiveCache({\n  keyPrefix: 'catalog:v2:',\n  initialTTL: 10,\n  maxTTL: 300,\n  staleWhileRevalidate: 30,\n  staleIfError: 120,\n  ttlJitter: 0.1,\n  compression: { minBytes: 1024, async: true, skipCompressed: true, onlyIfSmaller: true },\n})\n\nconst product = await cache.getOrSet('catalog:v2:product:42', () => loadProduct('42'), {\n  tags: ['products'],\n  negativeTTL: 5,\n})\nconst hits = await cache.getMany(['catalog:v2:product:42', 'catalog:v2:product:43'])\nawait cache.invalidateTags(['products'])\nawait cache.invalidateNamespace()\nawait cache.close()\n```\n\n`getMany()` uses batched payload and primary invalidation checks on clustered LRU. Results and events remain per key; the call is not a transaction. See the [batch-read report](docs/batched-reads-0.1.1.md) for measured gains and backend compatibility.\n\nStale windows are measured independently from freshness expiry. Plain `get()` returns fresh entries only; `get(key, { allowStale: true })` exposes retained stale data. Negative entries have no stale window. See the [feature guide](docs/cache-features.md) for force-refresh behavior, events, fingerprints, bulk operations, and custom backend limits.\n\n### HTTP identity and response policy\n\nRange and If-Range requests bypass caching, and partial `206` responses are not stored. Authorization and Cookie requests bypass caching unless explicitly enabled with a key or variation that isolates the caller. Responses with `Cache-Control: private` or `no-store`, `Set-Cookie`, `Vary: *`, or undeclared `Vary` headers are excluded. Declare `varyBy: ['Accept-Encoding']` when compression middleware adds that response header.\n\n```typescript\nconst products = adaptiveExpressCache({\n  keyPrefix: 'catalog:v2:',\n  key: (req) => `product:${req.params?.id}`,\n  varyBy: ['Accept-Language'],\n  loader: (req) => loadProduct(req.params?.id),\n  staleWhileRevalidate: 30,\n})\napp.get('/products/:id', products)\n\n// During shutdown, after accepting no further requests:\nawait products.close()\n```\n\nFastify accepts the same policy options and closes owned caches in its `onClose` hook. Both adapters accept `cache` to reuse a caller-owned instance. Keep that instance's namespace aligned with the middleware prefix. Use `products.clear(request)` to invalidate a response using the same custom key and header variations as the adapter. See [HTTP identity and ownership](docs/cache-features.md#http-identity-and-ownership).\n\n### Generic function caching\n\n`cacheResult` is a simple Redis helper for non-adaptive function results. It uses the default Redis client directly and does not use the adaptive backend layer.\n\n```typescript\nimport { cacheResult } from '@0xdoublesharp/adaptive-cache'\n\nconst data = await cacheResult('expensive-report', 60, async () => {\n  return await buildReport()\n})\n```\n\n### Refresh Locks\n\nUse refresh locks to prevent many workers from refreshing the same expensive resource at once.\n\n```typescript\nimport { releaseCacheRefreshLock, shouldRefreshCache } from '@0xdoublesharp/adaptive-cache'\n\nconst result = await shouldRefreshCache('report:last-update', 60)\n\nif (result[0] === 'UPDATE') {\n  const lockValue = result[1]\n  try {\n    await rebuildReport()\n  } finally {\n    await releaseCacheRefreshLock('report:last-update', lockValue)\n  }\n}\n```\n\n`lockExpirationSeconds` defaults to `60`. You can set a process-wide default:\n\n```typescript\nimport { setDefaultLockExpirationSeconds } from '@0xdoublesharp/adaptive-cache'\n\nsetDefaultLockExpirationSeconds(120)\n```\n\nOr pass a per-call value:\n\n```typescript\nawait shouldRefreshCache('report:last-update', 60, false, 120)\n```\n\n## Configuration\n\n| Option                  | Type                                                               | Default       | Description                                                                           |\n| ----------------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------- |\n| `initialTTL`            | `number`                                                           | `5`           | Starting cache duration in seconds.                                                   |\n| `maxTTL`                | `number \\| (data) => number \\| undefined`                          | `900`         | Maximum data TTL in seconds. Middleware can resolve it from response data.            |\n| `ttlScaling`            | `number`                                                           | `2`           | Factor used to grow TTL when content is unchanged. Growth is damped by `changeCount`. |\n| `redisPrefix`           | `string`                                                           | `'adaptive:'` | Prefix for Redis keys and invalidation channels.                                      |\n| `keyPrefix`             | `string`                                                           | unset         | Clearer alias for `redisPrefix`; applies to Redis and non-Redis backends.             |\n| `includeHeaders`        | `boolean`                                                          | `true`        | Add `X-Cache` and `X-Cache-TTL`.                                                      |\n| `includeDebugHeaders`   | `boolean`                                                          | `false`       | Add adaptive metadata headers.                                                        |\n| `forceRefresh`          | `boolean`                                                          | `false`       | Bypass existing cache reads and refresh after the handler response.                   |\n| `lockExpirationSeconds` | `number`                                                           | `60`          | Refresh lock expiration in seconds.                                                   |\n| `metaTTL`               | `number`                                                           | `604800`      | Metadata/tag TTL in positive integer seconds.                                         |\n| `compress`              | `boolean`                                                          | `true`        | Gzip payloads and store them as base64.                                               |\n| `logLevel`              | `'debug' \\| 'info' \\| 'warn' \\| 'error' \\| 'silent'`               | `'info'`      | Logger verbosity.                                                                     |\n| `backend`               | `'redis' \\| 'l1-redis' \\| 'clustered-lru' \\| AdaptiveCacheBackend` | `'redis'`     | Cache backend.                                                                        |\n| `lru`                   | `AdaptiveCacheLruOptions`                                          | unset         | LRU options for `l1-redis` and `clustered-lru`.                                       |\n| `l1Redis.writeMode`     | `'async' \\| 'await-redis'`                                         | `'async'`     | Whether `l1-redis` returns after L1 write or waits for Redis reconciliation.          |\n| `ignoreQueryParams`     | `string[]`                                                         | `['refresh']` | Query parameters excluded from middleware cache-key hashes.                           |\n\nAdditional options shared by the cache and adapters:\n\n| Option                         | Default | Purpose                                                                                        |\n| ------------------------------ | ------- | ---------------------------------------------------------------------------------------------- |\n| `staleWhileRevalidate`         | `0`     | Seconds after freshness expiry during which a loader can refresh in the background.            |\n| `staleIfError`                 | `0`     | Independent seconds after freshness expiry during which a failed loader may return stale data. |\n| `negativeTTL`                  | `0`     | Opt-in TTL for null/undefined loader results or normal-handler HTTP 404 responses.             |\n| `lockWaitTimeoutMs`            | `5000`  | Maximum loader lock wait in milliseconds.                                                      |\n| `ttlJitter`                    | `0`     | Fraction from 0 to 1 by which to randomly shorten freshness.                                   |\n| `fingerprint` / `ignoreFields` | unset   | Custom content fingerprint or dotted fields excluded from the hash.                            |\n| `compression`                  | unset   | `minBytes`, `async`, `skipCompressed`, and `onlyIfSmaller` selective codec settings.           |\n| `onEvent`                      | unset   | Observer for cache activity; also available through `cache.onEvent()`.                         |\n\nHTTP-only options are `cache`, `loader`, `key`, `varyBy`, `shouldCache`, `cacheAuthenticated`, `cacheCookies`, and request-derived `tags`. Defaults bypass authenticated/cookie requests and use path/query keys. See the [feature guide](docs/cache-features.md) for examples and per-call overrides.\n\n## Headers\n\n| Header                  | Meaning                                                                |\n| ----------------------- | ---------------------------------------------------------------------- |\n| `X-Cache: HIT`          | Cached response returned.                                              |\n| `X-Cache: MISS`         | No cache entry; handler ran.                                           |\n| `X-Cache: BYPASS`       | Cache read skipped because `forceRefresh` or `?refresh=true` was used. |\n| `X-Cache: RETRY`        | Cache read/decode failed; handler ran without failing the request.     |\n| `X-Cache-TTL`           | Remaining data TTL in seconds.                                         |\n| `X-Cache-Data-TTL`      | Debug: adaptive TTL assigned to the data version.                      |\n| `X-Cache-Last-Modified` | Debug: Unix timestamp when content last changed, or `unknown`.         |\n| `X-Cache-Refreshed`     | Debug: content change count.                                           |\n\n## Redis Keys and Lua Metadata\n\nGiven an adaptive key, this package uses:\n\n- `${key}data`: versioned Redis envelope containing encoded payload, freshness, namespace generation, tags, and tag epochs.\n- `${key}data:state`: auxiliary freshness and tag membership state.\n- `${key}meta`: adaptive metadata hash.\n- `${redisPrefix}tag:${tag}`: Redis set of keys for tag invalidation.\n- `${redisPrefix}l1:invalidate`: Redis Pub/Sub channel for L1 invalidations.\n- `${lastUpdateKey}-lock`: refresh lock key.\n- `${redisPrefix}__adaptive:namespace`, `${redisPrefix}__adaptive:barrier`, and `${redisPrefix}__adaptive:tag-epoch:${tag}`: invalidation control keys.\n- `__adaptive:legacy-barrier`: shared barrier for clears without namespace information.\n\nLua fetch returns:\n\n```text\n[data, remainingTTL, dataTTL, lastChanged, changeCount, hash, staleFlag, staleAgeMs]\n```\n\nLua update returns:\n\n```text\n['CACHED', dataTTL, lastChanged, changeCount, hash, changedFlag]\n```\n\nFetch preserves the original leading fields and appends stale state. The public hit reports `staleAge` in seconds. A rejected write returns `['INVALIDATED', 0]`. These internal Lua results do not imply wire-format compatibility with older readers; see the [upgrade notes](docs/releases.md#upgrading-from-006).\n\n## TTL policies\n\nThe default policy preserves the existing TTL growth/reset behavior. Opt into additive increase/multiplicative decrease with `ttlPolicy: { type: 'aimd' }`, or time-aware smoothing with `ttlPolicy: { type: 'ewma' }`. Both are available on all built-in backends. See [TTL policies](docs/ttl-policies.md) for parameters, formulas, and observation limits.\n\n## Default adaptive TTL formula\n\nOn changed content:\n\n- `changeCount += 1`\n- `dataTTL = min(initialTTL, maxTTL)`\n- `lastChanged = now`\n\nOn unchanged content:\n\n- If `dataTTL >= maxTTL`, keep `maxTTL`.\n- Otherwise compute a damped increase:\n\n```text\ndecayFactor = 1.0 - min(0.9, changeCount * 0.01)\nincreaseFactor = max(0, ttlScaling - 1)\nincrease = ceil(floor(dataTTL * increaseFactor) * decayFactor)\ndataTTL = max(initialTTL, min(dataTTL + increase, maxTTL))\n```\n\nAfter either path, clamp `dataTTL` to `maxTTL`. Optional jitter shortens freshness; stale retention extends physical storage without extending the fresh TTL.\n\n## Environment Variables\n\n| Variable                        | Description                                                       |\n| ------------------------------- | ----------------------------------------------------------------- |\n| `REDIS_TLS_URL`                 | Preferred Redis TLS connection string.                            |\n| `REDIS_URL`                     | Redis connection string. Used when `REDIS_TLS_URL` is not set.    |\n| `REDIS_HOST`                    | Redis host fallback. Defaults to `localhost`.                     |\n| `REDIS_PORT`                    | Redis port fallback. Defaults to `6379`.                          |\n| `REDIS_TLS_REJECT_UNAUTHORIZED` | Set to `false` to disable Redis TLS certificate verification.     |\n| `REDIS_INSECURE_TLS`            | Set to `true` to disable Redis TLS certificate verification.      |\n| `CACHE_TIME`                    | Default duration string for `cache()`, for example `\"5 seconds\"`. |\n\nTLS options are applied only for `REDIS_TLS_URL` or `rediss://` URLs. Certificate verification is enabled by default.\n\n## Custom Backends\n\nAdvanced users can provide a backend object instead of a backend name.\n\n```typescript\nimport type { AdaptiveCacheBackend } from '@0xdoublesharp/adaptive-cache'\n\nconst backend: AdaptiveCacheBackend = {\n  name: 'custom',\n  async fetch(input) {\n    return null\n  },\n  async update(input) {\n    return ['CACHED', input.initialTTL]\n  },\n  async clear(key, dataKey, metaKey) {},\n  async invalidateTags(tags, redisPrefix) {\n    return []\n  },\n  async shouldRefresh(lastUpdateKey, refreshThreshold, currentTime, force, lockExpirationSeconds, lockValue) {\n    return ['UPDATE', lockValue]\n  },\n  async releaseLock(lastUpdateKey, currentTime, lockValue) {\n    return ['UPDATED']\n  },\n  async flush() {},\n}\n```\n\nCustom backends can implement `captureWriteToken`, `invalidateNamespace`, and `onEvent`. `getOrSet` requires write-token support, and `supportsWriteFencing()` reports whether that method exists. It does not certify distributed atomicity: the backend must enforce tokens and leases during writes. A backend without namespace invalidation fails explicitly when that operation is requested.\n\n## Operational Notes\n\n- Redis is the cross-host authority in `redis` and `l1-redis` modes. Persistence depends on the Redis deployment configuration.\n- `clustered-lru` is volatile and local to the host/Node cluster.\n- L1 entries are intentionally short-lived until Redis Lua returns the authoritative adaptive TTL.\n- Oversized entries are skipped for L1 storage rather than risking unbounded server memory growth.\n- `l1-redis` invalidation uses Redis Pub/Sub, so processes must share the same Redis and prefix to invalidate each other.\n- `?refresh=true` bypasses reads but is ignored when computing the cache key, so refresh requests update the normal key.\n- Explicit `clear(key)` removes data, metadata, and auxiliary state and cleans recorded tag memberships. Namespace invalidation advances a generation without scanning payloads; old payloads expire normally.\n- Call `cache.flush()` before shutdown if you use default async `l1-redis` writes and need to wait for pending Redis reconciliation.\n- `redisPrefix` still works; prefer `keyPrefix` for new code when you want a backend-neutral name.\n\n## Development\n\nDependency resolution uses a seven-day minimum release age, configured as `minimumReleaseAge: 10080` in `pnpm-workspace.yaml`. Do not add release-age exclusions. Run `pnpm update --latest` to select eligible updates, then check peer compatibility and run the quality gate. The current toolchain keeps TypeScript at 6.0.3 because the installed ESLint parser declares support below 6.1.\n\nImplementation files follow responsibility boundaries: `AdaptiveCache.ts` owns the public API and lifecycle; `CacheLoader.ts` handles coalescing, freshness windows, and loader locks; `payload.ts` handles fingerprints and codecs. TTL policy validation and calculations have dedicated modules. HTTP adapters use shared policy/response helpers, and `backends/lruEnvelope.ts` contains LRU TTL and envelope calculations. Tests are grouped by backend, loader behavior, response policy, and lifecycle, with fixtures under `test/helpers`.\n\nKeep new behavior in the module that owns it. Split files when they accumulate distinct responsibilities, and preserve behavioral tests when moving code.\n\nEvery Vitest test must contain an assertion. Prefer stored state and real HTTP responses over log-only or mock-only checks.\n\n```bash\npnpm install\npnpm run lint\npnpm run typecheck\npnpm run test\npnpm run test:coverage\npnpm run quality\npnpm run build\npnpm run bench:configs\n```\n\n`pnpm run test` runs the full suite, including Redis/Testcontainers integration tests.\n\n`pnpm run test:coverage` runs the full suite, including Redis/Testcontainers integration tests, and currently verifies:\n\n```text\nStatements : 100%\nBranches   : 100%\nFunctions  : 100%\nLines      : 100%\n```\n\n`pnpm run quality` runs lint, typecheck, coverage, build, and built-declaration consumer checks.\n\n`pnpm run bench:lru` measures primary-process and real worker-process clustered LRU operations, with and without local L1. It also checks retained worker invalidation and delivered IPC requests under backpressure. See the [2.1.1 dependency evaluation](docs/lru-cache-clustered-2.1.1.md) for comparison results.\n\n`pnpm run bench:configs` compares `redis`, `l1-redis`, and `clustered-lru` behavior against a local Redis using unique benchmark key prefixes. It does not call `FLUSHALL`.\n\n## Releases and verification\n\n[CHANGELOG.md](CHANGELOG.md) records changes through version 0.1.1. [Release history and process](docs/releases.md) records the source revisions, migration steps, and release checklist. Keep release entries separate when preparing a version; do not replace the preceding entry.\n\nThe [2026-09-10 verification report](docs/features-verification-2026-09-10.md) records the feature checks and performance sample. The [earlier audit report](docs/review-2026-09-10.md) documents the preceding bug/test/dependency review and is a historical snapshot.\n\n## License\n\nMIT. See `package.json` for package metadata.\n","readmeFilename":"README.md"}