first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

40
examples/cache-handler-redis/.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,59 @@
# Next.js Redis Cache Integration Example
This example is tailored for self-hosted setups and demonstrates how to use Redis as a shared cache. It is built on the principles of the `@neshca/cache-handler` package, which replaces the default Next.js cache handler and adds advanced caching features.
Check out this [repository](https://github.com/ezeparziale/nextjs-k8s) that contains a comprehensive setup for Kubernetes.
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example cache-handler-redis cache-handler-redis-app
```
```bash
yarn create next-app --example cache-handler-redis cache-handler-redis-app
```
```bash
pnpm create next-app --example cache-handler-redis cache-handler-redis-app
```
Once you have installed the dependencies, you can begin running the example Redis Stack server by using the following command:
```bash
docker compose up -d
```
Then, build and start the Next.js app as usual.
To see the cache logs use NEXT_PRIVATE_DEBUG_CACHE=1 [troubleshooting](https://caching-tools.github.io/next-shared-cache/troubleshooting)
## Documentation
For detailed information on configuration and usage, please refer to our comprehensive [Documentation ↗](https://caching-tools.github.io/next-shared-cache).
## Key Features and Considerations
- **Handlers:** The `@neshca/cache-handler` package includes [Handlers](https://caching-tools.github.io/next-shared-cache/handlers/redis-stack) for seamless integration with Redis.
- **Create Your Own Handlers:** Take a look at [Custom Redis Handler](https://caching-tools.github.io/next-shared-cache/usage/creating-a-custom-handler) and use it as a basis to create your own handler.
- **Redis Server Setup:** Ensure your Redis server is running and properly configured before starting your Next.js application.
- **Configure Redis Credentials:** Update the `cache-handler-redis*` files with your Redis credentials. Connection details can be found [here](https://redis.io/docs/connect/clients/nodejs/).
- **Building Without Redis:** To build the app without connecting to Redis use conditions inside `onCreation` callback. Check the [documentation](https://caching-tools.github.io/next-shared-cache/configuration/opt-out-cache-on-build) for more details.
## Development and Production Considerations
- The provided `compose.yaml` is intended for local development. For production deployment, refer to the official [Redis installation](https://redis.io/docs/install/) and [management](https://redis.io/docs/management/) guidelines.
- **Clearing Redis Cache:** To clear the Redis cache, use RedisInsight Workbench or the following CLI command:
```bash
docker exec -it redis-stack redis-cli
127.0.0.1:6379> flushall
OK
```

View File

@@ -0,0 +1,68 @@
import { notFound } from "next/navigation";
import { CacheStateWatcher } from "../cache-state-watcher";
import { Suspense } from "react";
import { RevalidateFrom } from "../revalidate-from";
import Link from "next/link";
type TimeData = {
unixtime: number;
datetime: string;
timezone: string;
};
const timeZones = ["cet", "gmt"];
export const revalidate = 500;
export async function generateStaticParams() {
return timeZones.map((timezone) => ({ timezone }));
}
export default async function Page({ params: { timezone } }) {
const data = await fetch(
`https://worldtimeapi.org/api/timezone/${timezone}`,
{
next: { tags: ["time-data"] },
},
);
if (!data.ok) {
notFound();
}
const timeData: TimeData = await data.json();
return (
<>
<header className="header">
{timeZones.map((timeZone) => (
<Link key={timeZone} className="link" href={`/${timeZone}`}>
{timeZone.toUpperCase()} Time
</Link>
))}
</header>
<main className="widget">
<div className="pre-rendered-at">
{timeData.timezone} Time {timeData.datetime}
</div>
<Suspense fallback={null}>
<CacheStateWatcher
revalidateAfter={revalidate * 1000}
time={timeData.unixtime * 1000}
/>
</Suspense>
<RevalidateFrom />
</main>
<footer className="footer">
<Link
href={process.env.NEXT_PUBLIC_REDIS_INSIGHT_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
View RedisInsight &#x21AA;
</Link>
</footer>
</>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import { ReactNode, useEffect, useState } from "react";
type CacheStateWatcherProps = { time: number; revalidateAfter: number };
export function CacheStateWatcher({
time,
revalidateAfter,
}: CacheStateWatcherProps): ReactNode {
const [cacheState, setCacheState] = useState("");
const [countDown, setCountDown] = useState("");
useEffect(() => {
let id = -1;
function check(): void {
const now = Date.now();
setCountDown(
Math.max(0, (time + revalidateAfter - now) / 1000).toFixed(3),
);
if (now > time + revalidateAfter) {
setCacheState("stale");
return;
}
setCacheState("fresh");
id = requestAnimationFrame(check);
}
id = requestAnimationFrame(check);
return () => {
cancelAnimationFrame(id);
};
}, [revalidateAfter, time]);
return (
<>
<div className={`cache-state ${cacheState}`}>
Cache state: {cacheState}
</div>
<div className="stale-after">Stale in: {countDown}</div>
</>
);
}

View File

@@ -0,0 +1,102 @@
*,
*:before,
*:after {
box-sizing: border-box;
}
body,
html {
margin: 0;
padding: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: #333;
line-height: 1.6;
background-color: #f4f4f4;
}
.widget {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin: 20px auto;
padding: 20px;
max-width: 600px;
text-align: center;
}
.pre-rendered-at,
.cache-state,
.stale-after {
font-size: 0.9em;
color: #666;
margin: 5px 0;
}
.cache-state.fresh {
color: #4caf50;
}
.cache-state.stale {
color: #f44336;
}
.revalidate-from {
margin-top: 20px;
}
.revalidate-from-button {
background-color: #008cba;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.revalidate-from-button:hover {
background-color: #005f73;
}
.revalidate-from-button:active {
transform: translateY(2px);
}
.revalidate-from-button[aria-disabled="true"] {
background-color: #ccc;
cursor: not-allowed;
}
.footer,
.header {
padding: 10px;
position: relative;
place-items: center;
grid-auto-flow: column;
bottom: 0;
grid-gap: 20px;
width: 100%;
display: grid;
justify-content: center;
}
.link {
color: #09f;
text-decoration: none;
transition: color 0.3s ease;
}
.link:hover {
color: #07c;
}
@media (max-width: 768px) {
.widget {
width: 90%;
margin: 20px auto;
}
.footer {
padding: 20px;
}
}

View File

@@ -0,0 +1,13 @@
import "./global.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useFormStatus } from "react-dom";
import revalidate from "./server-actions";
function RevalidateButton() {
const { pending } = useFormStatus();
return (
<button
className="revalidate-from-button"
type="submit"
disabled={pending}
aria-disabled={pending}
>
Revalidate
</button>
);
}
export function RevalidateFrom() {
return (
<form className="revalidate-from" action={revalidate}>
<RevalidateButton />
</form>
);
}

View File

@@ -0,0 +1,7 @@
"use server";
import { revalidateTag } from "next/cache";
export default async function revalidate() {
revalidateTag("time-data");
}

View File

@@ -0,0 +1,80 @@
const { CacheHandler } = require("@neshca/cache-handler");
const createRedisHandler = require("@neshca/cache-handler/redis-stack").default;
const createLruHandler = require("@neshca/cache-handler/local-lru").default;
const { createClient } = require("redis");
const { PHASE_PRODUCTION_BUILD } = require("next/constants");
/* from https://caching-tools.github.io/next-shared-cache/redis */
CacheHandler.onCreation(async () => {
let client;
// use redis client during build could cause issue https://github.com/caching-tools/next-shared-cache/issues/284#issuecomment-1919145094
if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
try {
// Create a Redis client.
client = createClient({
url: process.env.REDIS_URL ?? "redis://localhost:6379",
});
// Redis won't work without error handling.
// NB do not throw exceptions in the redis error listener,
// because it will prevent reconnection after a socket exception.
client.on("error", (e) => {
if (typeof process.env.NEXT_PRIVATE_DEBUG_CACHE !== "undefined") {
console.warn("Redis error", e);
}
});
} catch (error) {
console.warn("Failed to create Redis client:", error);
}
}
if (client) {
try {
console.info("Connecting Redis client...");
// Wait for the client to connect.
// Caveat: This will block the server from starting until the client is connected.
// And there is no timeout. Make your own timeout if needed.
await client.connect();
console.info("Redis client connected.");
} catch (error) {
console.warn("Failed to connect Redis client:", error);
console.warn("Disconnecting the Redis client...");
// Try to disconnect the client to stop it from reconnecting.
client
.disconnect()
.then(() => {
console.info("Redis client disconnected.");
})
.catch(() => {
console.warn(
"Failed to quit the Redis client after failing to connect.",
);
});
}
}
/** @type {import("@neshca/cache-handler").Handler | null} */
let redisHandler = null;
if (client?.isReady) {
// Create the `redis-stack` Handler if the client is available and connected.
redisHandler = await createRedisHandler({
client,
keyPrefix: "prefix:",
timeoutMs: 1000,
});
}
// Fallback to LRU handler if Redis client is not available.
// The application will still work, but the cache will be in memory only and not shared.
const LRUHandler = createLruHandler();
console.warn(
"Falling back to LRU handler because Redis client is not available.",
);
return {
handlers: [redisHandler, LRUHandler],
};
});
module.exports = CacheHandler;

View File

@@ -0,0 +1,7 @@
services:
redis-stack:
image: redis/redis-stack:latest
container_name: cache-handler-redis
ports:
- "6379:6379"
- "8001:8001"

View File

@@ -0,0 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
cacheHandler:
process.env.NODE_ENV === "production"
? require.resolve("./cache-handler.js")
: undefined,
env: {
NEXT_PUBLIC_REDIS_INSIGHT_URL:
process.env.REDIS_INSIGHT_URL ?? "http://localhost:8001",
},
};
module.exports = nextConfig;

View File

@@ -0,0 +1,21 @@
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@neshca/cache-handler": "^1.3.2",
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"redis": "latest"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5.3.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}