Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
mauroaccornero committed Jul 20, 2024
0 parents commit fe02aee
Show file tree
Hide file tree
Showing 25 changed files with 862 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# 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
5 changes: 5 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/cache-handler-redis-example.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Next.js Redis Cluster Cache Integration Example

Run docker compose to start redis locally

```
docker-compose up -d
```

open redis-1 terminal and create the cluster

```
redis-cli --cluster create 172.38.0.11:6379 172.38.0.12:6379 172.38.0.13:6379 172.38.0.14:6379 172.38.0.15:6379 172.38.0.16:6379 --cluster-replicas 1
```

type "yes" to apply the configuration.

verify that the cluster was created

```
redis-cli -c
cluster nodes
```

install next.js dependencies for the project

```
npm i
```

build next.js (will not use custom redis cache handler during build)

```
npm run build
```

start next.js app

```
npm run start
```

navigate to the local homepage [http://localhost:3000/cet](http://localhost:3000/cet)

to remove logs, remove NEXT_PRIVATE_DEBUG_CACHE=1 from package.json

keep in mind that redis data will be stored in redis/node-X/data

to flush all the redis cluster use

```
redis-cli --cluster call --cluster-only-masters 172.38.0.11:6379 FLUSHALL
```
68 changes: 68 additions & 0 deletions app/[timezone]/page.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
50 changes: 50 additions & 0 deletions app/cache-state-watcher.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
102 changes: 102 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "./global.css";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
27 changes: 27 additions & 0 deletions app/revalidate-from.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Loading

0 comments on commit fe02aee

Please sign in to comment.