···11+import { configurePlaywright } from "../../../common/config-e2e";
22+33+// Here we don't want to run the tests in parallel
44+export default configurePlaywright("kv-tag-next", { parallel: false });
···3333 // D1 db used for the tag cache
3434 NEXT_TAG_CACHE_D1?: D1Database;
35353636+ // KV used for the tag cache
3737+ NEXT_TAG_CACHE_KV?: KVNamespace;
3838+3639 // Durables object namespace to use for the sharded tag cache
3740 NEXT_TAG_CACHE_DO_SHARDED?: DurableObjectNamespace<DOShardedTagCache>;
3841 // Queue of failed tag write
···11+import { error } from "@opennextjs/aws/adapters/logger.js";
22+import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
33+44+import { getCloudflareContext } from "../../cloudflare-context.js";
55+import { FALLBACK_BUILD_ID, purgeCacheByTags } from "../internal.js";
66+77+export const NAME = "kv-next-mode-tag-cache";
88+99+export const BINDING_NAME = "NEXT_TAG_CACHE_KV";
1010+1111+/**
1212+ * Tag Cache based on a KV namespace
1313+ *
1414+ * Warning:
1515+ * This implementation is considered experimental for now.
1616+ * KV is eventually consistent and can take up to 60s to reflect the last write.
1717+ * This means that:
1818+ * - revalidations can take up to 60s to apply
1919+ * - when a page depends on multiple tags they can be inconsistent for up to 60s.
2020+ * It also means that cached data could be outdated for one tag when other tags
2121+ * are revalidated resulting in the page being generated based on outdated data.
2222+ */
2323+export class KVNextModeTagCache implements NextModeTagCache {
2424+ readonly mode = "nextMode" as const;
2525+ readonly name = NAME;
2626+2727+ async getLastRevalidated(tags: string[]): Promise<number> {
2828+ const kv = this.getKv();
2929+ if (!kv) {
3030+ return 0;
3131+ }
3232+3333+ try {
3434+ const keys = tags.map((tag) => this.getCacheKey(tag));
3535+ // Use the `json` type to get back numbers/null
3636+ const result: Map<string, number | null> = await kv.get(keys, { type: "json" });
3737+3838+ const revalidations = [...result.values()].filter((v) => v != null);
3939+4040+ return revalidations.length === 0 ? 0 : Math.max(...revalidations);
4141+ } catch (e) {
4242+ // By default we don't want to crash here, so we return false
4343+ // We still log the error though so we can debug it
4444+ error(e);
4545+ return 0;
4646+ }
4747+ }
4848+4949+ async hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean> {
5050+ return (await this.getLastRevalidated(tags)) > (lastModified ?? Date.now());
5151+ }
5252+5353+ async writeTags(tags: string[]): Promise<void> {
5454+ const kv = this.getKv();
5555+ if (!kv || tags.length === 0) {
5656+ return Promise.resolve();
5757+ }
5858+5959+ const timeMs = String(Date.now());
6060+6161+ await Promise.all(
6262+ tags.map(async (tag) => {
6363+ await kv.put(this.getCacheKey(tag), timeMs);
6464+ })
6565+ );
6666+6767+ // TODO: See https://github.com/opennextjs/opennextjs-aws/issues/986
6868+ await purgeCacheByTags(tags);
6969+ }
7070+7171+ /**
7272+ * Returns the KV namespace when it exists and tag cache is not disabled.
7373+ *
7474+ * @returns KV namespace or undefined
7575+ */
7676+ private getKv(): KVNamespace | undefined {
7777+ const kv = getCloudflareContext().env[BINDING_NAME];
7878+7979+ if (!kv) {
8080+ error(`No KV binding ${BINDING_NAME} found`);
8181+ return undefined;
8282+ }
8383+8484+ const isDisabled = Boolean(globalThis.openNextConfig.dangerous?.disableTagCache);
8585+8686+ return isDisabled ? undefined : kv;
8787+ }
8888+8989+ protected getCacheKey(key: string) {
9090+ return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
9191+ }
9292+9393+ protected getBuildId() {
9494+ return process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
9595+ }
9696+}
9797+9898+export default new KVNextModeTagCache();