From 2d55e8ffb6a7bc779cb962582e76bc5f6fa3005f Mon Sep 17 00:00:00 2001 From: iBobo Date: Thu, 9 May 2024 16:45:09 +0200 Subject: [PATCH] Cache loading and update optimization (#756) --- .changeset/fast-points-count.md | 5 +++ .../templates/_worker.js/utils/cache.ts | 30 ++++++++++--- .../next-on-pages/templates/cache/adaptor.ts | 44 ++++++++++++++++--- 3 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 .changeset/fast-points-count.md diff --git a/.changeset/fast-points-count.md b/.changeset/fast-points-count.md new file mode 100644 index 000000000..c72596a7d --- /dev/null +++ b/.changeset/fast-points-count.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/next-on-pages': minor +--- + +Optimized cache tags manifest loading; cache updates do not block responses diff --git a/packages/next-on-pages/templates/_worker.js/utils/cache.ts b/packages/next-on-pages/templates/_worker.js/utils/cache.ts index 5030fa70d..40b8192f1 100644 --- a/packages/next-on-pages/templates/_worker.js/utils/cache.ts +++ b/packages/next-on-pages/templates/_worker.js/utils/cache.ts @@ -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. * @@ -58,15 +60,29 @@ export async function handleSuspenseCacheRequest(request: Request) { }); } case 'POST': { - // Update the value in the cache. - const body = await request.json(); - // 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)[ + REQUEST_CONTEXT_KEY + ] as { ctx: ExecutionContext }; + + const update = async () => { + // Update the value in the cache. + const body = await request.json(); + // 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: diff --git a/packages/next-on-pages/templates/cache/adaptor.ts b/packages/next-on-pages/templates/cache/adaptor.ts index 608877882..56c116991 100644 --- a/packages/next-on-pages/templates/cache/adaptor.ts +++ b/packages/next-on-pages/templates/cache/adaptor.ts @@ -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 | undefined; /** * @param ctx The incremental cache context from Next.js. NOTE: This is not currently utilised in NOP. @@ -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': { @@ -70,6 +72,9 @@ export class CacheAdaptor { ); } } + + // Make sure the cache has been updated before returning + await updateOp; } /** @@ -84,7 +89,12 @@ export class CacheAdaptor { { softTags }: { softTags?: string[] }, ): Promise { // 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; @@ -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); @@ -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 { + // 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 { + private async loadTagsManifestInternal(): Promise { try { const rawManifest = await this.retrieve(this.tagsManifestKey); if (rawManifest) { @@ -152,6 +183,7 @@ export class CacheAdaptor { } this.tagsManifest ??= { version: 1, items: {} }; + this.tagsManifestPromise = undefined; } /** @@ -174,7 +206,7 @@ export class CacheAdaptor { tags: string[], { cacheKey, revalidatedAt }: { cacheKey?: string; revalidatedAt?: number }, ): Promise { - await this.loadTagsManifest(); + await this.loadTagsManifest(true); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const tagsManifest = this.tagsManifest!;