Skip to content

Commit 02159d3

Browse files
author
Nicolas Dorseuil
committed
compute cache key according to persistentDataCache
1 parent 60848d9 commit 02159d3

File tree

6 files changed

+132
-7
lines changed

6 files changed

+132
-7
lines changed

packages/open-next/src/adapters/cache.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import type {
33
IncrementalCacheContext,
44
IncrementalCacheValue,
55
} from "types/cache";
6-
import { getTagsFromValue, hasBeenRevalidated, writeTags } from "utils/cache";
6+
import {
7+
createCacheKey,
8+
getTagsFromValue,
9+
hasBeenRevalidated,
10+
writeTags,
11+
} from "utils/cache";
712
import { isBinaryContentType } from "../utils/binary";
813
import { debug, error, warn } from "./logger";
914

@@ -31,7 +36,7 @@ function isFetchCache(
3136
// We need to use globalThis client here as this class can be defined at load time in next 12 but client is not available at load time
3237
export default class Cache {
3338
public async get(
34-
key: string,
39+
baseKey: string,
3540
// fetchCache is for next 13.5 and above, kindHint is for next 14 and above and boolean is for earlier versions
3641
options?:
3742
| boolean
@@ -49,7 +54,9 @@ export default class Cache {
4954

5055
const softTags = typeof options === "object" ? options.softTags : [];
5156
const tags = typeof options === "object" ? options.tags : [];
52-
return isFetchCache(options)
57+
const isDataCache = isFetchCache(options);
58+
const key = createCacheKey(baseKey, isDataCache);
59+
return isDataCache
5360
? this.getFetchCache(key, softTags, tags)
5461
: this.getIncrementalCache(key);
5562
}
@@ -191,20 +198,22 @@ export default class Cache {
191198
}
192199

193200
async set(
194-
key: string,
201+
baseKey: string,
195202
data?: IncrementalCacheValue,
196203
ctx?: IncrementalCacheContext,
197204
): Promise<void> {
198205
if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) {
199206
return;
200207
}
208+
const key = createCacheKey(baseKey, data?.kind === "FETCH");
201209
// This one might not even be necessary anymore
202210
// Better be safe than sorry
203211
const detachedPromise = globalThis.__openNextAls
204212
.getStore()
205213
?.pendingPromiseRunner.withResolvers<void>();
206214
try {
207215
if (data === null || data === undefined) {
216+
// only case where we delete the cache is for ISR/SSG cache
208217
await globalThis.incrementalCache.delete(key);
209218
} else {
210219
const revalidate = this.extractRevalidateForSet(ctx);

packages/open-next/src/adapters/composable-cache.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,33 @@ import { fromReadableStream, toReadableStream } from "utils/stream";
44
import { debug } from "./logger";
55

66
const pendingWritePromiseMap = new Map<string, Promise<ComposableCacheEntry>>();
7+
/**
8+
* Get the cache key for a composable entry.
9+
* Composable cache keys are a special cases as they are a stringified version of a tuple composed of a representation of the BUILD_ID and the actual key.
10+
* @param key The composable cache key
11+
* @returns The composable cache key.
12+
*/
13+
function getComposableCacheKey(key: string): string {
14+
try {
15+
const shouldPrependBuildId =
16+
globalThis.openNextConfig.dangerous?.persistentDataCache !== true;
17+
if (shouldPrependBuildId) {
18+
return key;
19+
}
20+
const [_buildId, ...rest] = JSON.parse(key);
21+
return JSON.stringify([...rest]);
22+
} catch (e) {
23+
debug("Error while parsing composable cache key", e);
24+
// If we fail to parse the key, we just return it as is
25+
// This is not ideal, but we don't want to crash the application
26+
return key;
27+
}
28+
}
729

830
export default {
9-
async get(cacheKey: string) {
31+
async get(key: string) {
1032
try {
33+
const cacheKey = getComposableCacheKey(key);
1134
// We first check if we have a pending write for this cache key
1235
// If we do, we return the pending promise instead of fetching the cache
1336
if (pendingWritePromiseMap.has(cacheKey)) {
@@ -55,7 +78,8 @@ export default {
5578
}
5679
},
5780

58-
async set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>) {
81+
async set(key: string, pendingEntry: Promise<ComposableCacheEntry>) {
82+
const cacheKey = getComposableCacheKey(key);
5983
pendingWritePromiseMap.set(cacheKey, pendingEntry);
6084
const entry = await pendingEntry.finally(() => {
6185
pendingWritePromiseMap.delete(cacheKey);

packages/open-next/src/types/open-next.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ export interface DangerousOptions {
7878
headersAndCookiesPriority?: (
7979
event: InternalEvent,
8080
) => "middleware" | "handler";
81+
82+
/**
83+
* Persist data cache between deployments.
84+
* Next.js claims that the data cache is persistent (not true for `use cache` and it depends on how you build/deploy otherwise).
85+
* By default, every entry will be prepended with the BUILD_ID, when enabled it will not.
86+
* This means that the data cache will be persistent between deployments.
87+
* This is useful in a lot of cases, but be aware that it could cause issues, especially with `use cache` or `unstable_cache` (Some external change may not be reflected in the key, leading to stale data)
88+
* @default false
89+
*/
90+
persistentDataCache?: boolean;
8191
}
8292

8393
export type BaseOverride = {

packages/open-next/src/utils/cache.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,19 @@ export async function writeTags(
8080
// Here we know that we have the correct type
8181
await globalThis.tagCache.writeTags(tagsToWrite as any);
8282
}
83+
84+
export function createCacheKey(key: string, isDataCache: boolean): string {
85+
// We always prepend the build ID to the cache key for ISR/SSG cache entry
86+
// For data cache, we only prepend the build ID if the persistentDataCache is not enabled
87+
const shouldPrependBuildId =
88+
globalThis.openNextConfig.dangerous?.persistentDataCache !== true ||
89+
!isDataCache;
90+
if (shouldPrependBuildId) {
91+
// If we don't have a build ID, we just return the key as is
92+
if (!process.env.NEXT_BUILD_ID) {
93+
return key;
94+
}
95+
return `${process.env.NEXT_BUILD_ID}/${key}`;
96+
}
97+
return key;
98+
}

packages/tests-unit/tests/adapters/cache.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { vi } from "vitest";
44

55
declare global {
66
var openNextConfig: {
7-
dangerous: { disableIncrementalCache?: boolean; disableTagCache?: boolean };
7+
dangerous: {
8+
disableIncrementalCache?: boolean;
9+
disableTagCache?: boolean;
10+
persistentDataCache?: boolean;
11+
};
812
};
913
var isNextAfter15: boolean;
1014
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
2+
import { createCacheKey } from "@opennextjs/aws/utils/cache.js";
3+
4+
describe("createCacheKey", () => {
5+
const originalEnv = process.env;
6+
const originalGlobalThis = globalThis as any;
7+
8+
beforeEach(() => {
9+
vi.resetModules();
10+
process.env = { ...originalEnv };
11+
12+
// Mock globalThis.openNextConfig
13+
if (!globalThis.openNextConfig) {
14+
globalThis.openNextConfig = {
15+
dangerous: {},
16+
};
17+
}
18+
});
19+
20+
afterEach(() => {
21+
process.env = originalEnv;
22+
globalThis.openNextConfig = originalGlobalThis.openNextConfig;
23+
});
24+
25+
test("prepends build ID for non-data cache entries", () => {
26+
process.env.NEXT_BUILD_ID = "test-build-id";
27+
const key = "test-key";
28+
29+
const result = createCacheKey(key, false);
30+
31+
expect(result).toBe("test-build-id/test-key");
32+
});
33+
34+
test("prepends build ID for data cache when persistentDataCache is not enabled", () => {
35+
process.env.NEXT_BUILD_ID = "test-build-id";
36+
globalThis.openNextConfig.dangerous.persistentDataCache = false;
37+
const key = "test-key";
38+
39+
const result = createCacheKey(key, true);
40+
41+
expect(result).toBe("test-build-id/test-key");
42+
});
43+
44+
test("does not prepend build ID for data cache when persistentDataCache is enabled", () => {
45+
process.env.NEXT_BUILD_ID = "test-build-id";
46+
globalThis.openNextConfig.dangerous.persistentDataCache = true;
47+
const key = "test-key";
48+
49+
const result = createCacheKey(key, true);
50+
51+
expect(result).toBe("test-key");
52+
});
53+
54+
test("handles missing build ID", () => {
55+
process.env.NEXT_BUILD_ID = undefined;
56+
const key = "test-key";
57+
58+
const result = createCacheKey(key, false);
59+
60+
expect(result).toBe("test-key");
61+
});
62+
});

0 commit comments

Comments
 (0)