{"_id":"@aortl/next-cache","_rev":"5-751bfc2a230824c12c383a45605d9fce","name":"@aortl/next-cache","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@aortl/next-cache","version":"0.1.0","keywords":["cache","cache-handler","next","nextjs","redis","self-hosted","use-cache"],"author":{"url":"https://ao.dk","name":"Brødrene A&O Johansen A/S"},"license":"MIT","_id":"@aortl/next-cache@0.1.0","maintainers":[{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"}],"contributors":[{"name":"Nickolaj Jepsen","email":"nij@ao.dk"}],"dist":{"shasum":"810120a5176817ebbace1e8509da3ebb8d519ff9","tarball":"https://registry.npmjs.org/@aortl/next-cache/-/next-cache-0.1.0.tgz","fileCount":9,"integrity":"sha512-3Ypu7V6BYFQ27aGpLwRMntXqq3p0qAX3isDj3rR9w2ME05exop5usceSNp+qiSmvW/UjfyFRPBfq2l8U9EK4Tg==","signatures":[{"sig":"MEYCIQDrrEJZ9jpYFfJExxBeaELepio776pwYDE66LVkOesx7wIhANy8jwtwlxbpix0EFUCkqoYjdrOjtqLr6nn9QoLIBjYW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137524},"main":"./dist/index.cjs","type":"module","_from":"file:aortl-next-cache-0.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"oxlint","test":"vitest run","build":"tsup","format":"oxfmt","lint:fix":"oxlint --fix","test:all":"pnpm -F @aortl/next-cache test && pnpm test:e2e","test:e2e":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F @aortl/next-cache-e2e test","test:unit":"vitest run --project=unit","typecheck":"tsc --noEmit","test:watch":"vitest","example:dev":"pnpm -F @aortl/next-cache build && pnpm -F example-basic dev","format:check":"oxfmt --check","example:start":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F example-basic start","test:integration":"vitest run --project=integration"},"_npmUser":{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"},"_resolved":"/tmp/5e5378d5efbd6d0be64bf32ae722f03d/aortl-next-cache-0.1.0.tgz","_integrity":"sha512-3Ypu7V6BYFQ27aGpLwRMntXqq3p0qAX3isDj3rR9w2ME05exop5usceSNp+qiSmvW/UjfyFRPBfq2l8U9EK4Tg==","_npmVersion":"10.9.7","description":"Redis cache handlers and programmatic cache API for self-hosted Next.js 16","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","oxfmt":"^0.47.0","oxlint":"^1.62.0","vitest":"^2.1.0","ioredis":"^5.4.1","typescript":"^5.6.0","@types/node":"^22.7.0","ioredis-mock":"^8.9.0","testcontainers":"^10.13.0","@types/ioredis-mock":"^8.2.5","@testcontainers/redis":"^10.13.0"},"peerDependencies":{"ioredis":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/next-cache_0.1.0_1777634618141_0.09400217065912475","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@aortl/next-cache","version":"0.1.3","keywords":["cache","cache-handler","next","nextjs","redis","self-hosted","use-cache"],"author":{"url":"https://ao.dk","name":"Brødrene A&O Johansen A/S"},"license":"MIT","_id":"@aortl/next-cache@0.1.3","maintainers":[{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"}],"contributors":[{"name":"Nickolaj Jepsen","email":"nij@ao.dk"}],"homepage":"https://github.com/Digital-Udvikling/next-cache#readme","bugs":{"url":"https://github.com/Digital-Udvikling/next-cache/issues"},"dist":{"shasum":"d064b71e3cd2da54dabfec4867c7c6c0f9fc1644","tarball":"https://registry.npmjs.org/@aortl/next-cache/-/next-cache-0.1.3.tgz","fileCount":9,"integrity":"sha512-LGJBAvP5qAK+VS0nwNZLef5VIWW0XNJQ6lJtP2MuBMqAX26eWZRbZYUsj7+jwgADyaIxbae1218hjMRL9vplXg==","signatures":[{"sig":"MEUCIQDya/qow0YWzC/IVGduOCGT0vPXK7Q4w/8zdB/3Lt9amgIgTNWXijmlLhTPbf2yu/ZhMu7xLQLqMo3qZVFpIUS1zBM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aortl%2fnext-cache@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":138654},"main":"./dist/index.cjs","pnpm":{"onlyBuiltDependencies":["esbuild","sharp","unrs-resolver"]},"type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"9cfce593fae54325723a783c475350d2db893b5b","scripts":{"lint":"oxlint","test":"vitest run","build":"tsup","format":"oxfmt","lint:fix":"oxlint --fix","test:all":"pnpm -F @aortl/next-cache test && pnpm test:e2e","test:e2e":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F @aortl/next-cache-e2e test","test:unit":"vitest run --project=unit","typecheck":"tsc --noEmit","test:watch":"vitest","example:dev":"pnpm -F @aortl/next-cache build && pnpm -F example-basic dev","format:check":"oxfmt --check","example:start":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F example-basic start","test:integration":"vitest run --project=integration"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:a79381e2-543d-43f9-a5d2-0ea11c1d4469"}},"repository":{"url":"git+https://github.com/Digital-Udvikling/next-cache.git","type":"git"},"_npmVersion":"11.11.0","description":"Redis cache handlers and programmatic cache API for self-hosted Next.js 16","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","oxfmt":"^0.47.0","oxlint":"^1.62.0","vitest":"^2.1.0","ioredis":"^5.4.1","typescript":"^5.6.0","@types/node":"^22.7.0","ioredis-mock":"^8.9.0","testcontainers":"^10.13.0","@types/ioredis-mock":"^8.2.5","@testcontainers/redis":"^10.13.0"},"peerDependencies":{"ioredis":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/next-cache_0.1.3_1777636634185_0.025727402647476527","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aortl/next-cache","version":"0.2.0","keywords":["cache","cache-handler","next","nextjs","redis","self-hosted","use-cache"],"author":{"url":"https://ao.dk","name":"Brødrene A&O Johansen A/S"},"license":"MIT","_id":"@aortl/next-cache@0.2.0","maintainers":[{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"}],"contributors":[{"name":"Nickolaj Jepsen","email":"nij@ao.dk"}],"homepage":"https://github.com/Digital-Udvikling/next-cache#readme","bugs":{"url":"https://github.com/Digital-Udvikling/next-cache/issues"},"dist":{"shasum":"b8651e0f73bd8e557e65d50f8bf80f749b61767a","tarball":"https://registry.npmjs.org/@aortl/next-cache/-/next-cache-0.2.0.tgz","fileCount":9,"integrity":"sha512-lbgqeN+8olPYCEX2mEN2oIYCkzVbHoqwBFPXAcaoJgKA7hSyGhFFMkg/Q3VgqxJg8cHH7VoH1FAbL4yu8ok7PQ==","signatures":[{"sig":"MEUCIQCI4OSHw+ZnB9jQELWL9Q4q9pYUNrk8btub8+56kRRtfgIgGFilgJ3YZCYVZJLpUhjCbipuO6+e66Hsyo+LAiqSXsU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aortl%2fnext-cache@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":167210},"main":"./dist/index.cjs","pnpm":{"onlyBuiltDependencies":["esbuild","sharp","unrs-resolver"]},"type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"c81c855105054270901ca1ace9b867e6e85bb43c","scripts":{"lint":"oxlint","test":"vitest run","build":"tsup","format":"oxfmt","lint:fix":"oxlint --fix","test:all":"pnpm -F @aortl/next-cache test && pnpm test:e2e","test:e2e":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F @aortl/next-cache-e2e test","test:unit":"vitest run --project=unit","typecheck":"tsc --noEmit","test:watch":"vitest","example:dev":"pnpm -F @aortl/next-cache build && pnpm -F example-basic dev","format:check":"oxfmt --check","example:start":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F example-basic start","test:integration":"vitest run --project=integration"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:a79381e2-543d-43f9-a5d2-0ea11c1d4469"}},"repository":{"url":"git+https://github.com/Digital-Udvikling/next-cache.git","type":"git"},"_npmVersion":"11.16.0","description":"Redis cache handlers and programmatic cache API for self-hosted Next.js 16","directories":{},"_nodeVersion":"24.18.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","oxfmt":"^0.47.0","oxlint":"^1.62.0","vitest":"^2.1.0","ioredis":"^5.4.1","typescript":"^5.6.0","@types/node":"^22.7.0","ioredis-mock":"^8.9.0","testcontainers":"^10.13.0","@types/ioredis-mock":"^8.2.5","@testcontainers/redis":"^10.13.0"},"peerDependencies":{"next":">=16.1.0","ioredis":"^5.0.0"},"peerDependenciesMeta":{"next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/next-cache_0.2.0_1784793970996_0.290339968170368","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@aortl/next-cache","version":"0.3.0","keywords":["cache","cache-handler","next","nextjs","redis","self-hosted","use-cache"],"author":{"url":"https://ao.dk","name":"Brødrene A&O Johansen A/S"},"license":"MIT","_id":"@aortl/next-cache@0.3.0","maintainers":[{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"}],"contributors":[{"name":"Nickolaj Jepsen","email":"nij@ao.dk"}],"homepage":"https://github.com/Digital-Udvikling/next-cache#readme","bugs":{"url":"https://github.com/Digital-Udvikling/next-cache/issues"},"dist":{"shasum":"af9ffbf8ac095acaa6961c1a80d882bd094cc7a2","tarball":"https://registry.npmjs.org/@aortl/next-cache/-/next-cache-0.3.0.tgz","fileCount":9,"integrity":"sha512-A75NkGjfiCVyU9Pts0/LUAhF2YEkXLtTh6e4dIGbgWe02HU3pu/GaqcNziDnyvyQMnXCLJM3hCrDndH0ruSRKg==","signatures":[{"sig":"MEUCIQDRmG2jHohBVylcaz2xzxYzFL+HHvbftbYQ+TRKXmJOpgIgIi0ERdIxfgnSVBCz/Rgv+4UD9rnrxKOlWUfBWq8yp3Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aortl%2fnext-cache@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":173772},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"b43e1c8208ed17a180efbcc157bc39524a2ae32f","scripts":{"lint":"oxlint","test":"vitest run","build":"tsup","format":"oxfmt","lint:fix":"oxlint --fix","test:all":"pnpm -F @aortl/next-cache test && pnpm test:e2e","test:e2e":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F @aortl/next-cache-e2e test","test:unit":"vitest run --project=unit","typecheck":"tsc --noEmit","test:watch":"vitest","example:dev":"pnpm -F @aortl/next-cache build && pnpm -F example-basic dev","format:check":"oxfmt --check","example:start":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F example-basic start","test:integration":"vitest run --project=integration"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:a79381e2-543d-43f9-a5d2-0ea11c1d4469"}},"repository":{"url":"git+https://github.com/Digital-Udvikling/next-cache.git","type":"git"},"_npmVersion":"11.17.0","description":"Redis cache handlers and programmatic cache API for self-hosted Next.js 16","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","oxfmt":"^0.47.0","oxlint":"^1.62.0","vitest":"^2.1.0","ioredis":"^5.4.1","typescript":"^5.6.0","@types/node":"^22.7.0","ioredis-mock":"^8.9.0","testcontainers":"^10.13.0","@types/ioredis-mock":"^8.2.5","@testcontainers/redis":"^10.13.0"},"peerDependencies":{"next":">=16.1.0","ioredis":"^5.0.0"},"peerDependenciesMeta":{"next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/next-cache_0.3.0_1787735255439_0.648321621487858","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@aortl/next-cache","version":"0.3.1","description":"Redis cache handlers and programmatic cache API for self-hosted Next.js 16","keywords":["cache","cache-handler","next","nextjs","redis","self-hosted","use-cache"],"homepage":"https://github.com/Digital-Udvikling/next-cache#readme","bugs":{"url":"https://github.com/Digital-Udvikling/next-cache/issues"},"license":"MIT","author":{"name":"Brødrene A&O Johansen A/S","url":"https://ao.dk"},"contributors":[{"name":"Nickolaj Jepsen","email":"nij@ao.dk"}],"repository":{"type":"git","url":"git+https://github.com/Digital-Udvikling/next-cache.git"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","lint":"oxlint","lint:fix":"oxlint --fix","format":"oxfmt","format:check":"oxfmt --check","test":"vitest run","test:unit":"vitest run --project=unit","test:integration":"vitest run --project=integration","test:watch":"vitest","test:e2e":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F @aortl/next-cache-e2e test","test:all":"pnpm -F @aortl/next-cache test && pnpm test:e2e","example:dev":"pnpm -F @aortl/next-cache build && pnpm -F example-basic dev","example:start":"pnpm -F @aortl/next-cache build && pnpm -F example-basic build && pnpm -F example-basic start"},"devDependencies":{"@testcontainers/redis":"^10.13.0","@types/ioredis-mock":"^8.2.5","@types/node":"^22.7.0","ioredis":"^5.4.1","ioredis-mock":"^8.9.0","oxfmt":"^0.47.0","oxlint":"^1.62.0","testcontainers":"^10.13.0","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.1.0"},"peerDependencies":{"ioredis":"^5.0.0","next":">=16.1.0"},"peerDependenciesMeta":{"next":{"optional":true}},"engines":{"node":">=20"},"gitHead":"2bfdd0b36dc0f1906cd0caf63555e7873c609e08","_id":"@aortl/next-cache@0.3.1","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-UHb4cHbhJrZI8A3tge90oVK6GWveAhogq4j0qycQlmRfO6ni6HDN4GDokZJRJBnqS9Q/BuevLBvYfDjeBKpEtw==","shasum":"17bf0eb6c86daa192830de7afa65e64c342a9f67","tarball":"https://registry.npmjs.org/@aortl/next-cache/-/next-cache-0.3.1.tgz","fileCount":9,"unpackedSize":189525,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aortl%2fnext-cache@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCWKpu7aZZtDDlCLkfd+FHVrOXoxkFWd24dfqziIyDfYQIgHvDd9FFX8YOy8MuDpzBFsRULHjUyJxv8GG0YffaafRI="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:a79381e2-543d-43f9-a5d2-0ea11c1d4469"}},"directories":{},"maintainers":[{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/next-cache_0.3.1_1788355974301_0.5554535335049271"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-01T11:23:38.057Z","modified":"2026-09-02T13:32:54.887Z","0.1.0":"2026-05-01T11:23:38.290Z","0.1.3":"2026-05-01T11:57:14.327Z","0.2.0":"2026-07-23T08:06:11.132Z","0.3.0":"2026-08-26T09:07:35.603Z","0.3.1":"2026-09-02T13:32:54.431Z"},"bugs":{"url":"https://github.com/Digital-Udvikling/next-cache/issues"},"author":{"name":"Brødrene A&O Johansen A/S","url":"https://ao.dk"},"license":"MIT","homepage":"https://github.com/Digital-Udvikling/next-cache#readme","keywords":["cache","cache-handler","next","nextjs","redis","self-hosted","use-cache"],"repository":{"type":"git","url":"git+https://github.com/Digital-Udvikling/next-cache.git"},"description":"Redis cache handlers and programmatic cache API for self-hosted Next.js 16","contributors":[{"name":"Nickolaj Jepsen","email":"nij@ao.dk"}],"maintainers":[{"name":"nickolaj1177","email":"nickolaj1177@gmail.com"}],"readme":"# @aortl/next-cache\n\nRedis cache handlers and a programmatic cache API for self-hosted Next.js 16.\n\n- **`default` handler** for `'use cache'` — keeps cached output in-memory for fast reads, but coordinates tag invalidation through Redis (pub/sub) so `revalidateTag()` propagates across instances.\n- **`remote` handler** for `'use cache: remote'` — stores entries in Redis, shared across every instance of your app.\n- **`incremental` handler** for Next's ISR/page cache (`cacheHandler`, singular) — extends the built-in FileSystemCache so saved static pages are invalidated on _every_ instance when `revalidateTag()` runs on any of them.\n- **Programmatic cache** (`createCache`) — `get` / `set` / `delete` / `getOrSet` / `invalidateTag` over Redis, sharing the same tag manifest as the cache handlers.\n- TypeScript, ESM + CJS, peer-deps `ioredis` (≥ 5) and, for the incremental handler only, `next` (≥ 16.1).\n\n## Installation\n\n```bash\npnpm add @aortl/next-cache ioredis\n# or: npm i @aortl/next-cache ioredis\n# or: yarn add @aortl/next-cache ioredis\n```\n\n## Setup\n\n`@aortl/next-cache` needs Cache Components enabled and two cache handler files referenced from `next.config.ts`. The handler files are tiny shims around the factories.\n\n```ts\n// next.config.ts\nimport type { NextConfig } from \"next\";\n\nconst config: NextConfig = {\n  cacheComponents: true,\n  cacheHandlers: {\n    default: require.resolve(\"./cache-handlers/default.js\"),\n    remote: require.resolve(\"./cache-handlers/remote.js\"),\n  },\n  // Optional: coordinate the ISR/page cache too (see \"The incremental handler\").\n  cacheHandler: require.resolve(\"./cache-handlers/incremental.js\"),\n};\n\nexport default config;\n```\n\n```js\n// cache-handlers/default.js\nmodule.exports = require(\"@aortl/next-cache\").createDefaultHandler();\n```\n\n```js\n// cache-handlers/remote.js\nmodule.exports = require(\"@aortl/next-cache\").createRemoteHandler();\n```\n\n```js\n// cache-handlers/incremental.js\nmodule.exports = require(\"@aortl/next-cache\").createIncrementalHandler();\n```\n\nSet `REDIS_URL` in your environment and you're done. With no options, the factories pull `REDIS_URL` (defaulting to `redis://localhost:6379`) and use sensible defaults.\n\n### How it works\n\n- **`'use cache'`** entries live in each instance's in-memory LRU. Reads stay local — no Redis round-trip for cache hits. When `revalidateTag()` runs on any instance, that instance writes the new tag timestamp to a Redis hash and publishes on a channel; every other instance picks the change up over pub/sub and discards the affected entries on their next read.\n- **`'use cache: remote'`** entries are JSON-encoded and stored in Redis with a TTL matching `expire`. Any instance can serve the cache. Entries past their `revalidate` time are still served (stale-while-revalidate) — Next.js refreshes them in the background — and are only dropped once past `expire`. Dynamic entries (`expire: 0`) are never written. Tag invalidation uses the same Redis-backed manifest.\n- The programmatic `createCache` API uses the same manifest, so `cache.invalidateTag(\"foo\")` also flushes any `'use cache'` / `'use cache: remote'` entries tagged `foo`.\n- The **incremental** handler keeps page storage on the local filesystem but checks every read against the shared manifest, and publishes its own `revalidateTag()` calls to it.\n\n### The incremental handler\n\nNext's `cacheHandler` (singular) is a separate system from `cacheHandlers` (plural): it stores finished responses — the saved fully-static pages (HTML + RSC payload) that Cache Components produce after the first visit to a URL not listed in `generateStaticParams`, plus the fetch cache. The built-in FileSystemCache tracks tag invalidations in an in-process map with no propagation, so behind a load balancer `revalidateTag()` only drops the saved pages on the instance that executed it; every other instance keeps serving its saved copy until the route's `revalidate` window elapses — even when the `'use cache'` data underneath was correctly invalidated everywhere (a saved-page hit skips rendering entirely, so the invalidated data is never consulted).\n\n`createIncrementalHandler()` returns a class extending FileSystemCache. Storage stays on each instance's filesystem (build output remains readable, no page payloads in Redis), but reads are additionally checked against the shared tag manifest: a tag expired after a page was saved turns the read into a miss, forcing a re-render that picks up fresh data.\n\nCaveats:\n\n- It extends `next/dist/server/lib/incremental-cache/file-system-cache`, an **internal** Next API — pin your Next version and re-verify on upgrades. The factory fails loudly if the module shape changes.\n- `next` must be installed alongside this package (declared as an optional peer dependency).\n- Only invalidation is coordinated. Each instance still warms its saved pages independently, and a Redis outage degrades to the built-in per-instance behavior rather than failing requests.\n- Redis outages are survivable at any point: handler calls fail fast while Redis is unreachable (no offline queue) and resume once ioredis reconnects; a failed initial sync is retried on the next call, and the subscriber re-reads the tag manifest on reconnect so invalidations published meanwhile are not missed. Connection errors are `console.warn`ed at most once a minute.\n- During `next build` the unmodified FileSystemCache is returned (no Redis in the build environment needed), following `disableDuringBuild`.\n\n## Programmatic cache API\n\n```ts\nimport { createCache } from \"@aortl/next-cache\";\n\nconst cache = createCache(); // reads REDIS_URL\n\nawait cache.set(\"user:42\", { name: \"Ada\" }, { ttl: 60, tags: [\"user:42\"] });\n\nconst user = await cache.get<{ name: string }>(\"user:42\");\n\nconst product = await cache.getOrSet(\"product:7\", async () => fetchProductFromDb(7), {\n  ttl: 300,\n  tags: [\"product:7\"],\n});\n\n// invalidate every entry (including any 'use cache' / 'use cache: remote' entries)\n// with the matching tag.\nawait cache.invalidateTag(\"product:7\");\n\nawait cache.close(); // tear down Redis connection on shutdown\n```\n\nThe cache stores values as JSON, so values must be JSON-serializable (`Date` objects become strings; `undefined` is dropped from objects; class instances lose their prototype). If you need richer serialization, encode the value yourself before `set`.\n\n## Configuration\n\nBoth factories and `createCache` accept the same options:\n\n```ts\nimport { createDefaultHandler, createCache } from \"@aortl/next-cache\";\n\ncreateDefaultHandler({\n  redis: process.env.REDIS_URL, // string | RedisOptions | Redis instance\n  keyPrefix: \"myapp:\", // default: \"next-cache:\"\n  pubsub: { enabled: true, channel: \"tags\" },\n  defaultHandler: { maxItems: 5000, maxBytes: 100 * 1024 * 1024 },\n  disableDuringBuild: true, // skip Redis during `next build`\n  debug: true, // log via console.debug\n});\n```\n\n| Option                    | Type                              | Default                                              | Notes                                                                      |\n| ------------------------- | --------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |\n| `redis`                   | `string \\| RedisOptions \\| Redis` | `process.env.REDIS_URL ?? \"redis://localhost:6379\"`  | URL string, ioredis options, or a pre-built `Redis` instance               |\n| `keyPrefix`               | `string`                          | `process.env.NEXT_CACHE_PREFIX ?? \"next-cache:\"`     | Prefix for every key + pub/sub channel                                     |\n| `pubsub.enabled`          | `boolean`                         | `true`                                               | Disable to fall back to per-request `HGETALL` polling                      |\n| `pubsub.channel`          | `string`                          | `\"tags\"`                                             | Suffix for the pub/sub channel (full channel: `<prefix>__pubsub:<suffix>`) |\n| `defaultHandler.maxItems` | `number`                          | `1000`                                               | LRU item cap for the default handler                                       |\n| `defaultHandler.maxBytes` | `number`                          | `52_428_800` (50 MiB)                                | LRU byte cap for the default handler                                       |\n| `disableDuringBuild`      | `boolean`                         | `true`                                               | Returns a no-op handler when `NEXT_PHASE === \"phase-production-build\"`     |\n| `debug`                   | `boolean`                         | `process.env.NEXT_PRIVATE_DEBUG_CACHE !== undefined` | Verbose logging                                                            |\n\n### Sharing a runtime\n\nBy default, repeated calls to `createDefaultHandler` / `createRemoteHandler` / `createCache` (without explicit options) share a single process-wide runtime — one Redis connection, one tag manifest. To get an isolated runtime (e.g. multiple Redis backends in one process), build a runtime explicitly:\n\n```ts\nimport {\n  createRuntime,\n  buildDefaultHandler,\n  buildRemoteHandler,\n  attachCache,\n} from \"@aortl/next-cache\";\n\nconst runtime = createRuntime({ redis: \"redis://other-host:6379\" });\nconst defaultHandler = buildDefaultHandler(runtime);\nconst remoteHandler = buildRemoteHandler(runtime);\nconst cache = attachCache(runtime);\n// runtime.close() shuts everything down.\n```\n\n## Local development\n\nA bundled `docker-compose.yml` brings up a Redis 7 instance on `localhost:6379`:\n\n```bash\ndocker compose up -d\n```\n\n## Example app\n\nA minimal Next.js 16 app exercising both directives + the programmatic API lives in [`examples/basic`](./examples/basic). It declares cache handler shims under `cache-handlers/`, wires them in `next.config.ts`, and exposes:\n\n- `/cached` — page rendered through `'use cache'`\n- `/cached-remote` — page rendered through `'use cache: remote'`\n- `POST /api/invalidate?tag=X` — calls `revalidateTag(tag, { expire: 0 })`\n- `GET/POST/DELETE /api/kv?key=X` — programmatic cache surface\n- `POST /api/kv/invalidate?tag=X` — programmatic `cache.invalidateTag`\n\nRun it locally:\n\n```bash\ndocker compose up -d                      # bring up Redis\npnpm example:dev                          # Next.js dev mode\n# or:\npnpm example:start                        # production build + start\n```\n\n## Testing\n\nThis is a pnpm workspace; scripts live on the root package and need the `-w` (workspace-root) flag to run from the repo root:\n\n- **Unit** (`pnpm -w test:unit`) — runs against `ioredis-mock`, no daemon needed.\n- **Integration** (`pnpm -w test:integration`) — uses [Testcontainers](https://node.testcontainers.org/) to spin up a real Redis container per test file. Requires Docker.\n- **End-to-end** (`pnpm -w test:e2e`) — Playwright-driven tests that build the example app, start two `next start` instances backed by a Testcontainers Redis, and exercise `'use cache'`, `'use cache: remote'`, cross-instance pub/sub invalidation, and the programmatic API. See [`e2e/`](./e2e). On NixOS-style systems, the suite auto-detects a system `chromium`/`google-chrome` binary because Playwright's bundled headless-shell isn't dynamically linked to the FHS layout.\n- `pnpm -w test` runs unit + integration. `pnpm -w test:all` adds e2e.\n- `pnpm -w typecheck` runs `tsc --noEmit`.\n- `pnpm -w build` produces ESM + CJS + `.d.ts` in `dist/`.\n\n### A note on `revalidateTag` semantics\n\nNext.js 16's `revalidateTag(tag, profile)` passes the cacheLife profile's `expire` to the cache handler's `updateTags(tags, { expire })`. Per Next.js's `areTagsExpired` semantics (`expired <= now AND expired > entry.timestamp`), the invalidation only takes effect once we cross the `expired` timestamp. Profile names like `\"default\"` or `\"max\"` carry an `expire` of ~136 years — so they _schedule_ an invalidation rather than _fire_ one. To invalidate immediately from a route handler, pass an explicit `{ expire: 0 }`. From a Server Action, use `updateTag(tag)` (which Next.js routes to immediate invalidation internally).\n\n## Releasing\n\nA GitHub Actions workflow at [`.github/workflows/release.yml`](./.github/workflows/release.yml) publishes `@aortl/next-cache` to npm when you push a `v<version>` tag. Auth uses **npm Trusted Publishing via OIDC** — there's no long-lived `NPM_TOKEN` secret, and every published version carries a [provenance attestation](https://docs.npmjs.com/generating-provenance-statements) linking back to the exact GitHub Actions run.\n\nTo cut a release, use `pnpm version` to bump `package.json`, commit, and tag in one step, then push branch + tag together:\n\n```bash\npnpm version patch          # 0.1.0 → 0.1.1: bumps package.json, commits, tags v0.1.1\n                            # use `minor` or `major` for non-patch bumps\ngit push --follow-tags      # push HEAD + the new tag — triggers the workflow\n```\n\nThe workflow runs typecheck → unit + integration tests → build → `npm publish --access public --provenance`. It refuses to publish if the tag's version doesn't match `package.json`.\n\nTrusted publisher configuration on npm (one-time, on the package's Settings → Trusted Publishers page):\n\n| Field             | Value               |\n| ----------------- | ------------------- |\n| Repository owner  | `Digital-Udvikling` |\n| Repository        | `next-cache`        |\n| Workflow filename | `release.yml`       |\n| Environment       | _(empty)_           |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}