@cloudflare/next-on-pages
Version:
`@cloudflare/next-on-pages` is a CLI tool that you can use to build and develop [Next.js](https://nextjs.org/) applications so that they can run on the [Cloudflare Pages](https://pages.cloudflare.com/) platform (and integrate with Cloudflare's various oth
129 lines (109 loc) • 3.97 kB
text/typescript
import type { CacheAdaptor, IncrementalCacheValue } from '../../cache';
import { SUSPENSE_CACHE_URL } from '../../cache';
import { doImport } from './doImport';
// https://github.com/vercel/next.js/blob/48a566bc/packages/next/src/server/lib/incremental-cache/fetch-cache.ts#L19
const CACHE_TAGS_HEADER = 'x-vercel-cache-tags';
// https://github.com/vercel/next.js/blob/ba23d986/packages/next/src/lib/constants.ts#L18
const NEXT_CACHE_SOFT_TAGS_HEADER = 'x-next-cache-soft-tags';
const REQUEST_CONTEXT_KEY = Symbol.for('__cloudflare-request-context__');
/**
* Handles an internal request to the suspense cache.
*
* @param request Incoming request to handle.
* @returns Response to the request, or null if the request is not for the suspense cache.
*/
export async function handleSuspenseCacheRequest(request: Request) {
const baseUrl = `https://${SUSPENSE_CACHE_URL}/v1/suspense-cache/`;
if (!request.url.startsWith(baseUrl)) return null;
try {
const url = new URL(request.url);
const cache = await getSuspenseCacheAdaptor();
if (url.pathname === '/v1/suspense-cache/revalidate') {
// Update the revalidated timestamp for the tags in the tags manifest.
const tags = url.searchParams.get('tags')?.split(',') ?? [];
for (const tag of tags) {
await cache.revalidateTag(tag);
}
return new Response(null, { status: 200 });
}
// Extract the cache key from the URL.
const cacheKey = url.pathname.replace('/v1/suspense-cache/', '');
if (!cacheKey.length) {
return new Response('Invalid cache key', { status: 400 });
}
switch (request.method) {
case 'GET': {
const softTags = getTagsFromHeader(
request,
NEXT_CACHE_SOFT_TAGS_HEADER,
);
// Retrieve the value from the cache.
const data = await cache.get(cacheKey, { softTags });
if (!data) return new Response(null, { status: 404 });
return new Response(JSON.stringify(data.value), {
status: 200,
headers: {
'Content-Type': 'application/json',
'x-vercel-cache-state': 'fresh',
age: `${(Date.now() - (data.lastModified ?? Date.now())) / 1000}`,
},
});
}
case 'POST': {
// Retrieve request context.
const reqCtx = (globalThis as unknown as Record<symbol, unknown>)[
REQUEST_CONTEXT_KEY
] as { ctx: ExecutionContext };
const update = async () => {
// Update the value in the cache.
const body = await request.json<IncrementalCacheValue>();
// Falling back to the cache tags header for Next.js 13.5+
if (body.data.tags === undefined) {
body.tags ??= getTagsFromHeader(request, CACHE_TAGS_HEADER) ?? [];
}
await cache.set(cacheKey, body);
};
if (reqCtx) {
// Avoid waiting for the cache to update before responding, if possible.
reqCtx.ctx.waitUntil(update());
} else {
await update();
}
return new Response(null, { status: 200 });
}
default:
return new Response(null, { status: 405 });
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return new Response('Error handling cache request', { status: 500 });
}
}
/**
* Gets the cache adaptor to use for the suspense cache.
*
* @returns Adaptor for the suspense cache.
*/
export async function getSuspenseCacheAdaptor(): Promise<CacheAdaptor> {
if (process.env.__NEXT_ON_PAGES__KV_SUSPENSE_CACHE) {
return getInternalCacheAdaptor('kv');
}
return getInternalCacheAdaptor('cache-api');
}
/**
* Gets an internal cache adaptor.
*
* @param type The type of adaptor to get.
* @returns A new instance of the adaptor.
*/
async function getInternalCacheAdaptor(
type: 'kv' | 'cache-api',
): Promise<CacheAdaptor> {
const moduleName = `./__next-on-pages-dist__/cache/${type}.js`;
const adaptor = await doImport(moduleName);
return new adaptor.default();
}
function getTagsFromHeader(req: Request, key: string): string[] | undefined {
return req.headers.get(key)?.split(',')?.filter(Boolean);
}