Skip to content

Commit

Permalink
Cache loading and update optimization (#756)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibobo authored May 9, 2024
1 parent fa02080 commit 2d55e8f
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-points-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/next-on-pages': minor
---

Optimized cache tags manifest loading; cache updates do not block responses
30 changes: 23 additions & 7 deletions packages/next-on-pages/templates/_worker.js/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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.
*
Expand Down Expand Up @@ -58,15 +60,29 @@ export async function handleSuspenseCacheRequest(request: Request) {
});
}
case 'POST': {
// 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) ?? [];
// 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();
}

await cache.set(cacheKey, body);

return new Response(null, { status: 200 });
}
default:
Expand Down
44 changes: 38 additions & 6 deletions packages/next-on-pages/templates/cache/adaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export class CacheAdaptor {
public tagsManifest: TagsManifest | undefined;
/** The key used for the tags manifest in the cache. */
public tagsManifestKey = 'tags-manifest';
/** Promise that resolves when tags manifest is loaded */
public tagsManifestPromise: Promise<void> | undefined;

/**
* @param ctx The incremental cache context from Next.js. NOTE: This is not currently utilised in NOP.
Expand Down Expand Up @@ -52,7 +54,7 @@ export class CacheAdaptor {
};

// Update the cache entry.
await this.update(key, JSON.stringify(newEntry));
const updateOp = this.update(key, JSON.stringify(newEntry));

switch (newEntry.value?.kind) {
case 'FETCH': {
Expand All @@ -70,6 +72,9 @@ export class CacheAdaptor {
);
}
}

// Make sure the cache has been updated before returning
await updateOp;
}

/**
Expand All @@ -84,7 +89,12 @@ export class CacheAdaptor {
{ softTags }: { softTags?: string[] },
): Promise<CacheHandlerValue | null> {
// Get entry from the cache.
const entry = await this.retrieve(key);
const entryPromise = this.retrieve(key);

// Start loading the tags manifest.
const tagsManifestLoad = this.loadTagsManifest();

const entry = await entryPromise;
if (!entry) return null;

let data: CacheHandlerValue;
Expand All @@ -97,8 +107,8 @@ export class CacheAdaptor {

switch (data.value?.kind) {
case 'FETCH': {
// Load the tags manifest.
await this.loadTagsManifest();
// Await for the tags manifest to end loading.
await tagsManifestLoad;

// Check if the cache entry is stale or fresh based on the tags.
const tags = getTagsFromEntry(data);
Expand Down Expand Up @@ -140,8 +150,29 @@ export class CacheAdaptor {

/**
* Loads the tags manifest from the suspense cache.
*
* @param force Whether to force a reload of the tags manifest.
*/
public async loadTagsManifest(force = false): Promise<void> {
// Load tags manifest if missing or refresh if forced.
const shouldLoad = force || !this.tagsManifest;

if (!shouldLoad) {
return;
}

// If the tags manifest is not already being loaded, kickstart the retrieval.
if (!this.tagsManifestPromise) {
this.tagsManifestPromise = this.loadTagsManifestInternal();
}

await this.tagsManifestPromise;
}

/**
* Internal method to load the tags manifest from the suspense cache.
*/
public async loadTagsManifest(): Promise<void> {
private async loadTagsManifestInternal(): Promise<void> {
try {
const rawManifest = await this.retrieve(this.tagsManifestKey);
if (rawManifest) {
Expand All @@ -152,6 +183,7 @@ export class CacheAdaptor {
}

this.tagsManifest ??= { version: 1, items: {} };
this.tagsManifestPromise = undefined;
}

/**
Expand All @@ -174,7 +206,7 @@ export class CacheAdaptor {
tags: string[],
{ cacheKey, revalidatedAt }: { cacheKey?: string; revalidatedAt?: number },
): Promise<void> {
await this.loadTagsManifest();
await this.loadTagsManifest(true);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const tagsManifest = this.tagsManifest!;
Expand Down

0 comments on commit 2d55e8f

Please sign in to comment.