Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
1 change: 1 addition & 0 deletions examples/common/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const apps = [
"pages-router",
// overrides
"d1-tag-next",
"memory-queue",
// bugs
"gh-119",
"gh-219",
Expand Down
47 changes: 47 additions & 0 deletions examples/overrides/memory-queue/.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/
Binary file added examples/overrides/memory-queue/app/favicon.ico
Binary file not shown.
14 changes: 14 additions & 0 deletions examples/overrides/memory-queue/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;
}
25 changes: 25 additions & 0 deletions examples/overrides/memory-queue/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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}</body>
</html>
);
}
17 changes: 17 additions & 0 deletions examples/overrides/memory-queue/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;
}
16 changes: 16 additions & 0 deletions examples/overrides/memory-queue/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styles from "./page.module.css";

export const revalidate = 5;

export default async function Home() {
// We purposefully wait for 2 seconds to allow deduplication to occur
await new Promise((resolve) => setTimeout(resolve, 2000));
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>
</main>
</div>
);
}
26 changes: 26 additions & 0 deletions examples/overrides/memory-queue/e2e/base.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from "@playwright/test";

test.describe("memory-queue", () => {
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 revalidate", async ({ page }) => {
await page.goto("/");
const firstDate = await page.getByTestId("date-local").textContent();
await page.waitForTimeout(5000);
await page.reload();

let newDate = await page.getByTestId("date-local").textContent();
expect(newDate).toBe(firstDate);

do {
await page.reload();
newDate = await page.getByTestId("date-local").textContent();
await page.waitForTimeout(1000);
} while (newDate === firstDate);

expect(newDate).not.toBe(firstDate);
});
});
8 changes: 8 additions & 0 deletions examples/overrides/memory-queue/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { configurePlaywright } from "../../../common/config-e2e";

// Here we don't want to run the tests in parallel
export default configurePlaywright("memory-queue", {
isCI: !!process.env.CI,
parallel: false,
multipleBrowsers: false,
});
11 changes: 11 additions & 0 deletions examples/overrides/memory-queue/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/memory-queue/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/kv-cache";
import memoryQueue from "@opennextjs/cloudflare/memory-queue";

export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
queue: memoryQueue,
});
28 changes: 28 additions & 0 deletions examples/overrides/memory-queue/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "memory-queue",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"build:worker": "opennextjs-cloudflare",
"preview": "pnpm build:worker && pnpm wrangler dev",
"e2e": "playwright test -c e2e/playwright.config.ts"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "catalog:e2e"
},
"devDependencies": {
"@opennextjs/cloudflare": "workspace:*",
"@playwright/test": "catalog:",
"@types/node": "catalog:",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "catalog:",
"wrangler": "catalog:"
}
}
27 changes: 27 additions & 0 deletions examples/overrides/memory-queue/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"]
}
23 changes: 23 additions & 0 deletions examples/overrides/memory-queue/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "memory-queue",
"compatibility_date": "2025-02-04",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"kv_namespaces": [
{
"binding": "NEXT_CACHE_WORKERS_KV",
"id": "<BINDING_ID>"
}
],
"services": [
{
"binding": "NEXT_CACHE_REVALIDATION_WORKER",
"service": "memory-queue"
}
]
}
12 changes: 6 additions & 6 deletions packages/cloudflare/src/api/memory-queue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("MemoryQueue", () => {
const firstRequest = cache.send({
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
MessageGroupId: generateMessageGroupId("/test"),
MessageDeduplicationId: "",
MessageDeduplicationId: "/test",
});
vi.advanceTimersByTime(DEFAULT_REVALIDATION_TIMEOUT_MS);
await firstRequest;
Expand All @@ -40,7 +40,7 @@ describe("MemoryQueue", () => {
const secondRequest = cache.send({
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
MessageGroupId: generateMessageGroupId("/test"),
MessageDeduplicationId: "",
MessageDeduplicationId: "/test",
});
vi.advanceTimersByTime(1);
await secondRequest;
Expand All @@ -51,7 +51,7 @@ describe("MemoryQueue", () => {
const firstRequest = cache.send({
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
MessageGroupId: generateMessageGroupId("/test"),
MessageDeduplicationId: "",
MessageDeduplicationId: "/test",
});
vi.advanceTimersByTime(1);
await firstRequest;
Expand All @@ -60,7 +60,7 @@ describe("MemoryQueue", () => {
const secondRequest = cache.send({
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
MessageGroupId: generateMessageGroupId("/other"),
MessageDeduplicationId: "",
MessageDeduplicationId: "/other",
});
vi.advanceTimersByTime(1);
await secondRequest;
Expand All @@ -72,12 +72,12 @@ describe("MemoryQueue", () => {
cache.send({
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
MessageGroupId: generateMessageGroupId("/test"),
MessageDeduplicationId: "",
MessageDeduplicationId: "/test",
}),
cache.send({
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
MessageGroupId: generateMessageGroupId("/test"),
MessageDeduplicationId: "",
MessageDeduplicationId: "/test",
}),
];
vi.advanceTimersByTime(1);
Expand Down
29 changes: 16 additions & 13 deletions packages/cloudflare/src/api/memory-queue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import logger from "@opennextjs/aws/logger.js";
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
import type { Queue, QueueMessage } from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";

Expand All @@ -16,40 +16,43 @@ export const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;
export class MemoryQueue implements Queue {
readonly name = "memory-queue";

revalidatedPaths = new Map<string, ReturnType<typeof setTimeout>>();
revalidatedPaths = new Set<string>();

constructor(private opts = { revalidationTimeoutMs: DEFAULT_REVALIDATION_TIMEOUT_MS }) {}

async send({ MessageBody: { host, url }, MessageGroupId }: QueueMessage): Promise<void> {
async send({ MessageBody: { host, url }, MessageDeduplicationId }: QueueMessage): Promise<void> {
const service = getCloudflareContext().env.NEXT_CACHE_REVALIDATION_WORKER;
if (!service) throw new IgnorableError("No service binding for cache revalidation worker");

if (this.revalidatedPaths.has(MessageGroupId)) return;
if (this.revalidatedPaths.has(MessageDeduplicationId)) return;

this.revalidatedPaths.set(
MessageGroupId,
// force remove to allow new revalidations incase something went wrong
setTimeout(() => this.revalidatedPaths.delete(MessageGroupId), this.opts.revalidationTimeoutMs)
);
this.revalidatedPaths.add(MessageDeduplicationId);

try {
const protocol = host.includes("localhost") ? "http" : "https";

// TODO: Drop the import - https://github.com/opennextjs/opennextjs-cloudflare/issues/361
// @ts-ignore
const manifest = await import("./.next/prerender-manifest.json");
await service.fetch(`${protocol}://${host}${url}`, {
const response = await service.fetch(`${protocol}://${host}${url}`, {
method: "HEAD",
headers: {
"x-prerender-revalidate": manifest.preview.previewModeId,
"x-isr": "1",
},
// We want to timeout the revalidation to avoid hanging the queue
signal: AbortSignal.timeout(this.opts.revalidationTimeoutMs),
});

// Here we want at least to log when the revalidation was not successful
if (response.status !== 200 || response.headers.get("x-nextjs-cache") !== "REVALIDATED") {
error(`Revalidation failed for ${url} with status ${response.status}`);
}
debug(`Revalidation successful for ${url}`);
} catch (e) {
logger.error(e);
error(e);
} finally {
clearTimeout(this.revalidatedPaths.get(MessageGroupId));
this.revalidatedPaths.delete(MessageGroupId);
this.revalidatedPaths.delete(MessageDeduplicationId);
}
}
}
Expand Down
Loading