Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/friendly-experts-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

fix deduplication for memory queue and add some log
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>
);
}
36 changes: 36 additions & 0 deletions examples/overrides/memory-queue/e2e/base.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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, request }) => {
// We need to make sure the page is loaded and is a HIT
// If it is STALE, the next hit may have an updated date and thus fail the test
let cacheHeaders = "";
do {
const req = await request.get("/");
cacheHeaders = req.headers()["x-nextjs-cache"];
await page.waitForTimeout(500);
} while (cacheHeaders !== "HIT");

await page.goto("/");
const firstDate = await page.getByTestId("date-local").textContent();

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

await page.waitForTimeout(5000);

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": "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/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