next-shared-cache
next-shared-cache copied to clipboard
Add Next.js 15 support
- [x] Old tests are passing
- [ ] New e2e tests for App router are passing
- [ ] New e2e tests for Pages router are passing
- [ ] New unit tests for testing backward compatibility
- [ ] All
@ts-expect-errorremoved
Summary by CodeRabbit
Release Notes
-
New Features
- Introduced a new API endpoint for cache revalidation, allowing users to revalidate data based on path or tag.
- Added a
CacheStateWatchercomponent to monitor and display cache states dynamically. - Implemented a
RestartButtoncomponent for restarting the application from the UI.
-
Improvements
- Enhanced global styles for better responsiveness and theming.
- Improved error handling and logging in cache operations for better debugging.
-
Tests
- Added comprehensive tests for cache revalidation mechanisms using Playwright.
⚠️ No Changeset found
Latest commit: 31d3d435fd2a1f8c605868621f50d9d5b906584d
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package "@repo/cache-testing-15-app" depends on the ignored package "@repo/eslint-config", but "@repo/cache-testing-15-app" is not being ignored. Please add "@repo/cache-testing-15-app" to the `ignore` option.
Walkthrough
The pull request introduces a series of changes across multiple files, primarily focusing on enhancing the caching mechanisms and configuration for a cache-testing application. Key modifications include updates to workflow commands, the addition of new configuration files, new components for handling caching and revalidation, and enhancements to existing cache handling logic. New environment variables and types are defined to improve type safety, and a comprehensive suite of tests is introduced to validate cache behaviors.
Changes
| File Path | Change Summary |
|---|---|
| .github/workflows/tests.yml | Updated e2e test command to specify working directory. |
| .vscode/extensions.json | Added three new recommended extensions for VSCode. |
| .vscode/settings.json | Added setting for local Turbo use and fixed JSON syntax. |
| apps/cache-testing-15-app/.env.example | Introduced new environment configuration file with essential variables for the application. |
| apps/cache-testing-15-app/.eslintrc.js | Added ESLint configuration for the application. |
| apps/cache-testing-15-app/cache-handler-local.mjs | Created a local cache handler integrating with existing CacheHandler framework. |
| apps/cache-testing-15-app/cache-handler-next-example.js | Introduced a CacheHandler class for managing in-memory cache. |
| apps/cache-testing-15-app/cache-handler-redis-stack.js | Implemented Redis caching with fallback to local LRU cache. |
| apps/cache-testing-15-app/cache-handler-redis-stack.mjs | Similar to above, with support for Redis and local caching. |
| apps/cache-testing-15-app/cache-handler-redis-strings.mjs | Added Redis cache handler with connection management and error handling. |
| apps/cache-testing-15-app/cluster.config.js | New configuration for application running in cluster mode. |
| apps/cache-testing-15-app/create-instances.sh | Script for setting up standalone application environment with directory management. |
| apps/cache-testing-15-app/next.config.mjs | New Next.js configuration file with various settings for the application. |
| apps/cache-testing-15-app/package.json | Introduced package.json with scripts and dependencies for the application. |
| apps/cache-testing-15-app/playwright.config.ts | New Playwright configuration for testing parameters. |
| apps/cache-testing-15-app/run-app-instances.ts | Script for managing application instances using PM2 and Fastify. |
| apps/cache-testing-15-app/src/app/api/revalidate-app/route.ts | New API endpoint for handling cache revalidation requests. |
| apps/cache-testing-15-app/src/app/app/no-params/dynamic-false/[slug]/page.tsx | New React component for handling dynamic routing without parameters. |
| apps/cache-testing-15-app/src/app/app/no-params/dynamic-true/[slug]/page.tsx | New component for dynamic routing with parameters. |
| apps/cache-testing-15-app/src/app/app/no-params/ssr/200/page.tsx | New SSR page for the specified route. |
| apps/cache-testing-15-app/src/app/app/ppr/page.tsx | New component utilizing partial prerendering with loading states. |
| apps/cache-testing-15-app/src/app/app/randomHex/[length]/page.tsx | New page for displaying random hexadecimal values based on length. |
| apps/cache-testing-15-app/src/app/app/static/route.ts | New static route returning a basic HTTP response. |
| apps/cache-testing-15-app/src/app/app/with-params/dynamic-false/[slug]/page.tsx | Component for handling dynamic routing with specific parameters. |
| apps/cache-testing-15-app/src/app/app/with-params/dynamic-true/[slug]/page.tsx | Dynamic page component for routing based on URL parameters. |
| apps/cache-testing-15-app/src/app/app/with-params/nesh-cache/[slug]/page.tsx | New component for handling dynamic routing with nesh cache. |
| apps/cache-testing-15-app/src/app/app/with-params/unstable-cache/[slug]/page.tsx | New component for unstable cache handling. |
| apps/cache-testing-15-app/src/app/layout.tsx | New root layout component for the application. |
| apps/cache-testing-15-app/src/components/cache-state-watcher.tsx | New component for monitoring cache state. |
| apps/cache-testing-15-app/src/components/pre-rendered-at.tsx | New component for displaying pre-rendered time. |
| apps/cache-testing-15-app/src/components/restart-button.tsx | New button component for restarting the application. |
| apps/cache-testing-15-app/src/components/revalidate-button.tsx | New button component for triggering revalidation processes. |
| apps/cache-testing-15-app/src/globals.css | New CSS file defining global styles and custom properties. |
| apps/cache-testing-15-app/src/instrumentation.ts | New function for registering initial cache based on environment settings. |
| apps/cache-testing-15-app/src/utils/common-app-page.tsx | New component for rendering common application pages. |
| apps/cache-testing-15-app/src/utils/create-get-data.ts | New utility function for fetching and caching data from an API. |
| apps/cache-testing-15-app/src/utils/format-time.ts | New utility function for formatting time. |
| apps/cache-testing-15-app/src/utils/normalize-slug.ts | New function for normalizing slugs based on predefined cases. |
| apps/cache-testing-15-app/src/utils/types.ts | New TypeScript type definitions for various response structures. |
| apps/cache-testing-15-app/tests/app.spec.ts | New test suite for validating cache revalidation mechanisms. |
| apps/cache-testing-15-app/tests/test-helpers.ts | New utility functions for cache revalidation in tests. |
| apps/cache-testing-15-app/tsconfig.json | New TypeScript configuration file for the application. |
| apps/cache-testing/package.json | Updated dependencies in package.json. |
| docs/cache-handler-docs/package.json | Updated dependencies in documentation package.json. |
| internal/next-common/package.json | Updated dependency version in next-common package.json. |
| internal/next-common/src/next-common.ts | Enhanced type definitions and imports related to caching. |
| internal/next-lru-cache/src/cache-types/next-cache-handler-value.ts | Improved object size calculation logic for different cache types. |
| packages/cache-handler/src/cache-handler.ts | Enhanced cache handling functionality with new types and properties. |
| packages/cache-handler/src/functions/nesh-cache.ts | Updated caching functions to include new properties and error handling. |
| packages/cache-handler/src/functions/nesh-classic-cache.ts | Modified cache handling logic to improve metadata handling. |
| packages/cache-handler/src/handlers/local-lru.ts | Improved type safety for implicit tag handling. |
| packages/cache-handler/src/helpers/is-implicit-tag.ts | Enhanced type safety for implicit tag checking. |
| packages/cache-handler/src/instrumentation/register-initial-cache.ts | Improved error handling and logging in cache registration logic. |
| packages/server/src/server.ts | Enhanced type safety and cache management in server route handlers. |
| package.json | Added scripts for building and starting the cache-testing application. |
Poem
In the burrows deep, where the cache does gleam,
A rabbit hops forth, with a joyful dream.
New scripts and handlers, oh what a delight,
With caching and testing, we'll hop through the night.
So raise up your carrots, let the code flow,
For changes are here, and we're ready to grow! 🐇✨
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?
🪧 Tips
Chat
There are 3 ways to chat with CodeRabbit:
- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
I pushed a fix in commit <commit_id>, please review it.Generate unit testing code for this file.Open a follow-up GitHub issue for this discussion.
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. Examples:@coderabbitai generate unit testing code for this file.@coderabbitai modularize this function.
- PR comments: Tag
@coderabbitaiin a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.@coderabbitai read src/utils.ts and generate unit testing code.@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.@coderabbitai help me debug CodeRabbit configuration file.
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.
CodeRabbit Commands (Invoked using PR comments)
@coderabbitai pauseto pause the reviews on a PR.@coderabbitai resumeto resume the paused reviews.@coderabbitai reviewto trigger an incremental review. This is useful when automatic reviews are disabled for the repository.@coderabbitai full reviewto do a full review from scratch and review all the files again.@coderabbitai summaryto regenerate the summary of the PR.@coderabbitai resolveresolve all the CodeRabbit review comments.@coderabbitai configurationto show the current CodeRabbit configuration for the repository.@coderabbitai helpto get help.
Other keywords and placeholders
- Add
@coderabbitai ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
CodeRabbit Configuration File (.coderabbit.yaml)
- You can programmatically configure CodeRabbit by adding a
.coderabbit.yamlfile to the root of your repository. - Please see the configuration documentation for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation:
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
Documentation and Community
- Visit our Documentation for detailed information on how to use CodeRabbit.
- Join our Discord Community to get help, request features, and share feedback.
- Follow us on X/Twitter for updates and announcements.
Can we include in this breaking change an export for
export type NextCacheImplicitTagId = '_N_T_'; for the type or a constant, available to custom driver creator ?
Can we include in this breaking change an export for
export type NextCacheImplicitTagId = '_N_T_';for the type or a constant, available to custom driver creator ?
I will do this in this PR
Hey @better-salmon - could you tell me what is the status of this PR? Is it ready to use for testing in non-production environments? Or do you still expect significant changes other than tests and typing?
Tested it quickly with cache-handler/redis-strings and next 15.0.3 stable
Got the error 500
⨯ TypeError: result.toUnchunkedString is not a function at sendRenderResult (/node_modules/next/dist/server/send-payload.js:66:54)
Happens only on the static pages
node: v18.18.2 production next build
https://github.com/vercel/next.js/blob/ef9d0969a050263403dd59573a09b345e75583a8/packages/next/src/server/send-payload.ts#L73
const payload = result.isDynamic ? null : result.toUnchunkedString();
result typeof "string" here, it seems
stupid fix fixes this problem, added it to cache-handler.mjs
/** fix for cache-handler and next 15 bug */
String.prototype.toUnchunkedString = function () {
return this;
};
the cache keys are simple strings as expected, probably the cache-handler/redis-strings should return RenderResult not string. Haven't dive into how it works...
Please publish this PR to npm so we can play with it easily. Had to publish my own package in npm with this branch, will remove it later
Hey y'all, any update on this? I'm assuming this resolves the following error
⨯ TypeError: Cannot read properties of undefined (reading 'isDynamic')
Is there any workaround in the meantime, or does this PR need to be merged?
Following up on this? Anything I can do to help, or are we just waiting on official approval?
Hello, any timeline for full next 15 support? (This PR merged @better-salmon @justinadkins
Based on what I have investigated, there is still a lot to do in this PR. Especially as it is supposed to be backward compatible. There has been really a lot of changes in next 15 in terms of cache. I think better-salomon will need more time to complete this. My approach was to create a simpler version of cache handler that has the minimum features I need. Works for me well, so far, thus I would suggest to bridge the gap in your own capacity and give better-salomon time to do this right
Based on what I have investigated, there is still a lot to do in this PR. Especially as it is supposed to be backward compatible. There has been really a lot of changes in next 15 in terms of cache. I think better-salomon will need more time to complete this. My approach was to create a simpler version of cache handler that has the minimum features I need. Works for me well, so far, thus I would suggest to bridge the gap in your own capacity and give better-salomon time to do this right
I believe it is not supposed to be backwards compatible? As per the official docs page This is the final release leading up to version 2.0.0. The upcoming version 2 will bring significant changes and will no longer support Next.js versions 13 and 14. While version 1 will still receive ongoing maintenance, it won't get any new features.
@astrodomas I'm not a contributor, just another dev looking for Next 15 support. I tinkered with this PR a bit though and have been running a patched version of this lib on a low traffic environment without any issues yet. The only changes I made to this PR can be seen here.
I also added this to the top of my cache-handler.mjs file based on previous feedback.
// @see https://github.com/caching-tools/next-shared-cache/pull/846#issuecomment-2481276786
// eslint-disable-next-line @typescript-eslint/no-explicit-any
String.prototype.toUnchunkedString = function () {
return this;
};
For anyone following along, I discovered an issue with my fork of this PR today and addressed it with this commit. It came to light when I noticed that an intercepting route wasn't working.
hi 👋
I see this PR has been stalled for a while. We're are currently implementing shared caching with next 15 and would love this update 👀
Same for us - we are waiting for this missing part to upgrade our apps to next15 - is there anything one can do to help with the development of this change? 😊
@better-salmon I'd be happy to help get this over the finish line as well. I've been running a forked and patched version of this PR for a little over a month now. I can make a PR against this branch if that would be helpful.
@justinadkins @better-salmon (if you still here) seems like the best solution for node.js + redis (strings) should look like this
/**
* @param {string} key
* @param {string[]} [tags]
* @returns {string}
*/
const generateCacheKey = (key, tags = []) => {
const tagsStr = tags.length > 0 ? `:t_${tags.join("t_")}` : "";
return `${key}${tagsStr}:${buildId}`;
};
// set
async set(key, data, ctx) {
const generatedKey = generateCacheKey(key, ctx.tags);
// ...
if (data.revalidate) {
await client.setEx(generatedKey, data.revalidate, value);
} else {
await client.set(generatedKey, value);
}
// ...
}
// get
async get(key, ctx) {
const generatedKey = generateCacheKey(key, ctx.tags);
// ...
if (data.revalidate) {
await client.setEx(generatedKey, data.revalidate, value);
} else {
await client.set(generatedKey, value);
}
// ...
}
/**
* @param {string} pattern
* @param {ReturnType<typeof createClient>} client
*/
export async function deleteKeysWithPattern(pattern, client) {
const script = `
local key = tostring(KEYS[1]);
local delsCount = 0;
local cursor = 0;
repeat
local result = redis.call('SCAN', cursor, 'MATCH', key)
for _, key in ipairs(result[2]) do
redis.call('UNLINK', key);
delsCount = delsCount + 1;
end
cursor = tonumber(result[1]);
until cursor == 0;
return delsCount;`;
try {
const delsCount = await client.eval(script, { keys: [pattern] });
} catch (error) {
console.error(`Error deleting keys matching pattern '${pattern}':`, error);
}
}
async revalidateTag(tags) {
// ...
for await (let tag of tags) {
await deleteKeysWithPattern(`*t_${tag}*`, client);
}
// ...
}
great read/write performance, no complexity with sets/hashes/etc, also AWS Elasticache supports EVAL
I wrote it today, tested and all works well
And do we really need to diff (kinds) APP_ROUTE, FETCH, etc?
Yes, because you have many different things you can cache: fetch, rendered html etc.
@justinadkins @better-salmon (if you still here) seems like the best solution for node.js + redis (strings) should look like this
/** * @param {string} key * @param {string[]} [tags] * @returns {string} */ const generateCacheKey = (key, tags = []) => { const tagsStr = tags.length > 0 ? `:t_${tags.join("t_")}` : ""; return `${key}${tagsStr}:${buildId}`; }; // set async set(key, data, ctx) { const generatedKey = generateCacheKey(key, ctx.tags); // ... if (data.revalidate) { await client.setEx(generatedKey, data.revalidate, value); } else { await client.set(generatedKey, value); } // ... } // get async get(key, ctx) { const generatedKey = generateCacheKey(key, ctx.tags); // ... if (data.revalidate) { await client.setEx(generatedKey, data.revalidate, value); } else { await client.set(generatedKey, value); } // ... } /** * @param {string} pattern * @param {ReturnType<typeof createClient>} client */ export async function deleteKeysWithPattern(pattern, client) { const script = ` local key = tostring(KEYS[1]); local delsCount = 0; local cursor = 0; repeat local result = redis.call('SCAN', cursor, 'MATCH', key) for _, key in ipairs(result[2]) do redis.call('UNLINK', key); delsCount = delsCount + 1; end cursor = tonumber(result[1]); until cursor == 0; return delsCount;`; try { const delsCount = await client.eval(script, { keys: [pattern] }); } catch (error) { console.error(`Error deleting keys matching pattern '${pattern}':`, error); } } async revalidateTag(tags) { // ... for await (let tag of tags) { await deleteKeysWithPattern(`*t_${tag}*`, client); } // ... }great read/write performance, no complexity with sets/hashes/etc, also AWS Elasticache supports EVAL
I wrote it today, tested and all works well
And do we really need to diff (kinds) APP_ROUTE, FETCH, etc?
@better-salmon I'd be happy to help get this over the finish line as well. I've been running a forked and patched version of this PR for a little over a month now. I can make a PR against this branch if that would be helpful.
@justinadkins Would you mind publishing the fork as a temporary measure?
Yes, because you have many different things you can cache: fetch, rendered html etc.
what is the difference? just store the value, is always string @KraKeN-47
@jdpnielsen I can get it published soon. Will report back here when I do.
@studentIvan I'd need to revisit the source code to see if the additional complexity in this repo is beneficial / necessary compared to your approach. I was just focused on getting this repo working when I was tinkering with it as opposed to home brewing a solution.
@justinadkins it seems like any fix can't fix the pipeTo problem. Because some keys (technically it's ?_rsc=xxx pages cache prefetched from desktop Link component) have rscData Buffer with too much weight now, like 550kb+. pipeTo fails because it has a data read/write timeout it seems (not sure from which library exactly) How are going to solve this problem?
First Idea I got is simple store big rscData values in LRU cache and check it first, before redis, but here we getting another problem - revalidation of these values.
Maybe next.js devs left this "bug" or "feature" for us, to move us to vercel...
@justinadkins it seems like any fix can't fix the pipeTo problem. Because some keys (technically it's ?_rsc=xxx pages cache prefetched from desktop Link component) have rscData Buffer with too much weight now, like 550kb+. pipeTo fails because it has a data read/write timeout it seems (not sure from which library exactly) How are going to solve this problem?
First Idea I got is simple store big rscData values in LRU cache and check it first, before redis, but here we getting another problem - revalidation of these values.
Maybe next.js devs left this "bug" or "feature" for us, to move us to vercel...
maybe compression is needed?
@justinadkins it seems like any fix can't fix the pipeTo problem. Because some keys (technically it's ?_rsc=xxx pages cache prefetched from desktop Link component) have rscData Buffer with too much weight now, like 550kb+. pipeTo fails because it has a data read/write timeout it seems (not sure from which library exactly) How are going to solve this problem?
First Idea I got is simple store big rscData values in LRU cache and check it first, before redis, but here we getting another problem - revalidation of these values.
Maybe next.js devs left this "bug" or "feature" for us, to move us to vercel...
How about relying on LRU for some cache values, but have the cache manifest in Redis (shared) so revalidation will apply to all instances.
@justinadkins it seems like any fix can't fix the pipeTo problem. Because some keys (technically it's ?_rsc=xxx pages cache prefetched from desktop Link component) have rscData Buffer with too much weight now, like 550kb+. pipeTo fails because it has a data read/write timeout it seems (not sure from which library exactly) How are going to solve this problem? First Idea I got is simple store big rscData values in LRU cache and check it first, before redis, but here we getting another problem - revalidation of these values. Maybe next.js devs left this "bug" or "feature" for us, to move us to vercel...
maybe compression is needed?
Yes can help but we can lost some time and cpu in this case, any idea how to implement it quickly? For now I've implemented my own cache handler with LRU + redis together
How about relying on LRU for some cache values, but have the cache manifest in Redis (shared) so revalidation will apply to all instances.
Didn't get you. Can you explain or add details pls?
How about relying on LRU for some cache values, but have the cache manifest in Redis (shared) so revalidation will apply to all instances.
Didn't get you. Can you explain or add details pls?
I don't know how best to solve the size issue for certain cache values in Redis. But I'm also looking how we can implement an hybrid approach. For example we have the following setup since upgrading to NextJS (and therefore having to abandon our Redis cache solution for now);
We are running Cloud Run instances with a Cloud Storage bucket volume mount on .next/cache/fetch-cache for example so we at least have a shared cache for all instances
Having this shared cache is great since when scaling up, the new instances can use the same shared data cache.
However, invalidation is still an issue because invalidation doesn't happen by deleting a file, but marking a certain tag as invalidated in the (in-memory) tagsManifest in https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts
If that tagsManifest could be shared as well (in Redis for example) at least you have a shared invalidation source.
At that point it wouldn't matter if you are getting your cache value from Redis, disk or in-memory; Invalidations will be shared anyway.
I found this repo while looking for alternatives:
- https://github.com/fortedigital/nextjs-cache-handler
@AyronK is your fork running OK with Next.js 15? I see you made couple of patches in there.
@taraspos yes, we're using it with Next 15 with a bit of workaround https://github.com/fortedigital/nextjs-cache-handler/blob/ce977bd5c45f6f117ec0dad4cc21263ec3b9fae0/packages/nextjs-cache-handler/src/handlers/buffer-string-decorator.ts. However I haven't seen this PR before, not sure how it's gonna work in the next version of this package.
Hey y'all, sorry for the delay in following up. I've published my patch here if you need something temporarily (use at your own discretion). It uses Buffer for routes and pages similarly to what is described above.
If you'd like to see the diff from this PR to what is published, you can find it here.