Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-walls-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": minor
---

feat: add an experimental KV based tag cache
1 change: 1 addition & 0 deletions examples/common/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const apps = [
"experimental",
// overrides
"d1-tag-next",
"kv-tag-next",
"memory-queue",
"r2-incremental-cache",
"static-assets-incremental-cache",
Expand Down
47 changes: 47 additions & 0 deletions examples/overrides/kv-tag-next/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
11 changes: 11 additions & 0 deletions examples/overrides/kv-tag-next/app/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use server";

import { revalidatePath, revalidateTag } from "next/cache";

export async function revalidateTagAction() {
revalidateTag("date");
}

export async function revalidatePathAction() {
revalidatePath("/");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { revalidateTagAction, revalidatePathAction } from "../action";

export default function RevalidationButtons() {
return (
<div>
<button
data-testid="revalidate-tag"
onClick={async () => {
await revalidateTagAction();
}}
>
Invalidate tag
</button>

<button
data-testid="revalidate-path"
onClick={async () => {
await revalidatePathAction();
}}
>
Invalidate Path
</button>
</div>
);
}
Binary file added examples/overrides/kv-tag-next/app/favicon.ico
Binary file not shown.
14 changes: 14 additions & 0 deletions examples/overrides/kv-tag-next/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
html,
body {
max-width: 100vw;
overflow-x: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}

footer {
padding: 1rem;
display: flex;
justify-content: end;
}
28 changes: 28 additions & 0 deletions examples/overrides/kv-tag-next/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Metadata } from "next";
import "./globals.css";

import { getCloudflareContext } from "@opennextjs/cloudflare";

export const metadata: Metadata = {
title: "SSG App",
description: "An app in which all the routes are SSG'd",
};

export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const cloudflareContext = await getCloudflareContext({
async: true,
});

return (
<html lang="en">
<body>
{children}
<footer data-testid="app-version">{cloudflareContext.env.APP_VERSION}</footer>
</body>
</html>
);
}
17 changes: 17 additions & 0 deletions examples/overrides/kv-tag-next/app/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.page {
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
flex: 1;
border: 3px solid gray;
margin: 1rem;
margin-block-end: 0;
}

.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
26 changes: 26 additions & 0 deletions examples/overrides/kv-tag-next/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { unstable_cache } from "next/cache";
import styles from "./page.module.css";
import RevalidationButtons from "./components/revalidationButtons";

const fetchedDateCb = unstable_cache(
async () => {
return Date.now();
},
["date"],
{ tags: ["date"] }
);

export default async function Home() {
const fetchedDate = await fetchedDateCb();
return (
<div className={styles.page}>
<main className={styles.main}>
<h1>Hello from a Statically generated page</h1>
<p data-testid="date-local">{Date.now()}</p>
<p data-testid="date-fetched">{fetchedDate}</p>

<RevalidationButtons />
</main>
</div>
);
}
47 changes: 47 additions & 0 deletions examples/overrides/kv-tag-next/e2e/base.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test, expect } from "@playwright/test";

test.describe("kv-tag-next", () => {
test("the index page should work", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Hello from a Statically generated page")).toBeVisible();
});

test("the index page should keep the same date on reload", async ({ page }) => {
await page.goto("/");
const date = await page.getByTestId("date-local").textContent();
expect(date).not.toBeNull();
await page.reload();
const newDate = await page.getByTestId("date-local").textContent();
expect(date).toEqual(newDate);
});

test("the index page should revalidate the date on click on revalidateTag", async ({ page }) => {
await page.goto("/");
const date = await page.getByTestId("date-fetched").textContent();
await page.getByTestId("revalidate-tag").click();
await page.waitForTimeout(100);
const newDate = await page.getByTestId("date-fetched").textContent();
expect(date).not.toEqual(newDate);
});

test("the index page should revalidate the date on click on revalidatePath", async ({ page }) => {
await page.goto("/");
const date = await page.getByTestId("date-fetched").textContent();
await page.getByTestId("revalidate-path").click();
await page.waitForTimeout(100);
const newDate = await page.getByTestId("date-fetched").textContent();
expect(date).not.toEqual(newDate);
});

test("the index page should keep the same date on reload after revalidation", async ({ page }) => {
await page.goto("/");
const initialDate = await page.getByTestId("date-fetched").textContent();
await page.getByTestId("revalidate-tag").click();
await page.waitForTimeout(100);
const date = await page.getByTestId("date-fetched").textContent();
expect(initialDate).not.toEqual(date);
await page.reload();
const newDate = await page.getByTestId("date-fetched").textContent();
expect(date).toEqual(newDate);
});
});
4 changes: 4 additions & 0 deletions examples/overrides/kv-tag-next/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { configurePlaywright } from "../../../common/config-e2e";

// Here we don't want to run the tests in parallel
export default configurePlaywright("kv-tag-next", { parallel: false });
11 changes: 11 additions & 0 deletions examples/overrides/kv-tag-next/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { NextConfig } from "next";
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";

initOpenNextCloudflareForDev();

const nextConfig: NextConfig = {
typescript: { ignoreBuildErrors: true },
eslint: { ignoreDuringBuilds: true },
};

export default nextConfig;
8 changes: 8 additions & 0 deletions examples/overrides/kv-tag-next/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
import kvNextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/kv-next-tag-cache";

export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
tagCache: kvNextTagCache,
});
29 changes: 29 additions & 0 deletions examples/overrides/kv-tag-next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "d1-tag-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"build:worker": "pnpm opennextjs-cloudflare build --skipWranglerConfigCheck",
"preview:worker": "pnpm opennextjs-cloudflare preview --config wrangler.e2e.jsonc",
"preview": "pnpm build:worker && pnpm preview:worker",
"e2e": "playwright test -c e2e/playwright.config.ts"
},
"dependencies": {
"react": "catalog:e2e",
"react-dom": "catalog:e2e",
"next": "catalog:e2e"
},
"devDependencies": {
"@opennextjs/cloudflare": "workspace:*",
"@playwright/test": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:e2e",
"@types/react-dom": "catalog:e2e",
"typescript": "catalog:",
"wrangler": "catalog:"
}
}
27 changes: 27 additions & 0 deletions examples/overrides/kv-tag-next/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
25 changes: 25 additions & 0 deletions examples/overrides/kv-tag-next/wrangler.e2e.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "ssg-app",
"compatibility_date": "2025-02-04",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS",
},
"vars": {
"APP_VERSION": "1.2.345",
},
"kv_namespaces": [
{
"binding": "NEXT_INC_CACHE_KV",
"id": "INC-CACHE",
"preview_id": "<BINDING_ID>",
},
{
"binding": "NEXT_TAG_CACHE_KV",
"id": "TAG-CACHE",
},
],
}
3 changes: 3 additions & 0 deletions packages/cloudflare/src/api/cloudflare-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ declare global {
// D1 db used for the tag cache
NEXT_TAG_CACHE_D1?: D1Database;

// KV used for the tag cache
NEXT_TAG_CACHE_KV?: KVNamespace;

// Durables object namespace to use for the sharded tag cache
NEXT_TAG_CACHE_DO_SHARDED?: DurableObjectNamespace<DOShardedTagCache>;
// Queue of failed tag write
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("D1NextModeTagCache", () => {
env: {
[BINDING_NAME]: mockDb,
},
} as ReturnType<typeof getCloudflareContext>);
} as unknown as ReturnType<typeof getCloudflareContext>);

// Reset global config
(globalThis as { openNextConfig?: { dangerous?: { disableTagCache?: boolean } } }).openNextConfig = {
Expand Down
Loading