Skip to content

Commit df5b19f

Browse files
author
Nicolas Dorseuil
committed
compute cache key according to persistentDataCache
1 parent f569a34 commit df5b19f

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: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type {
33
IncrementalCacheContext,
44
IncrementalCacheValue,
55
} from "types/cache";
6-
import { getTagsFromValue, hasBeenRevalidated } from "utils/cache";
6+
import {
7+
createCacheKey,
8+
getTagsFromValue,
9+
hasBeenRevalidated,
10+
} from "utils/cache";
711
import { isBinaryContentType } from "../utils/binary";
812
import { debug, error, warn } from "./logger";
913

@@ -31,7 +35,7 @@ function isFetchCache(
3135
// 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
3236
export default class Cache {
3337
public async get(
34-
key: string,
38+
baseKey: string,
3539
// fetchCache is for next 13.5 and above, kindHint is for next 14 and above and boolean is for earlier versions
3640
options?:
3741
| boolean
@@ -49,7 +53,9 @@ export default class Cache {
4953

5054
const softTags = typeof options === "object" ? options.softTags : [];
5155
const tags = typeof options === "object" ? options.tags : [];
52-
return isFetchCache(options)
56+
const isDataCache = isFetchCache(options);
57+
const key = createCacheKey(baseKey, isDataCache);
58+
return isDataCache
5359
? this.getFetchCache(key, softTags, tags)
5460
: this.getIncrementalCache(key);
5561
}
@@ -191,20 +197,22 @@ export default class Cache {
191197
}
192198

193199
async set(
194-
key: string,
200+
baseKey: string,
195201
data?: IncrementalCacheValue,
196202
ctx?: IncrementalCacheContext,
197203
): Promise<void> {
198204
if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) {
199205
return;
200206
}
207+
const key = createCacheKey(baseKey, data?.kind === "FETCH");
201208
// This one might not even be necessary anymore
202209
// Better be safe than sorry
203210
const detachedPromise = globalThis.__openNextAls
204211
.getStore()
205212
?.pendingPromiseRunner.withResolvers<void>();
206213
try {
207214
if (data === null || data === undefined) {
215+
// only case where we delete the cache is for ISR/SSG cache
208216
await globalThis.incrementalCache.delete(key);
209217
} else {
210218
const revalidate = this.extractRevalidateForSet(ctx);

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,33 @@ import type { ComposableCacheEntry, ComposableCacheHandler } from "types/cache";
22
import { fromReadableStream, toReadableStream } from "utils/stream";
33
import { debug } from "./logger";
44

5+
/**
6+
* Get the cache key for a composable entry.
7+
* 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.
8+
* @param key The composable cache key
9+
* @returns The composable cache key.
10+
*/
11+
function getComposableCacheKey(key: string): string {
12+
try {
13+
const shouldPrependBuildId =
14+
globalThis.openNextConfig.dangerous?.persistentDataCache !== true;
15+
if (shouldPrependBuildId) {
16+
return key;
17+
}
18+
const [_buildId, ...rest] = JSON.parse(key);
19+
return JSON.stringify([...rest]);
20+
} catch (e) {
21+
debug("Error while parsing composable cache key", e);
22+
// If we fail to parse the key, we just return it as is
23+
// This is not ideal, but we don't want to crash the application
24+
return key;
25+
}
26+
}
27+
528
export default {
6-
async get(cacheKey: string) {
29+
async get(key: string) {
730
try {
31+
const cacheKey = getComposableCacheKey(key);
832
const result = await globalThis.incrementalCache.get(
933
cacheKey,
1034
"composable",
@@ -47,7 +71,8 @@ export default {
4771
}
4872
},
4973

50-
async set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>) {
74+
async set(key: string, pendingEntry: Promise<ComposableCacheEntry>) {
75+
const cacheKey = getComposableCacheKey(key);
5176
const entry = await pendingEntry;
5277
const valueToStore = await fromReadableStream(entry.value);
5378
await globalThis.incrementalCache.set(

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ export interface DangerousOptions {
7777
headersAndCookiesPriority?: (
7878
event: InternalEvent,
7979
) => "middleware" | "handler";
80+
81+
/**
82+
* Persist data cache between deployments.
83+
* Next.js claims that the data cache is persistent (not true for `use cache` and it depends on how you build/deploy otherwise).
84+
* By default, every entry will be prepended with the BUILD_ID, when enabled it will not.
85+
* This means that the data cache will be persistent between deployments.
86+
* 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)
87+
* @default false
88+
*/
89+
persistentDataCache?: boolean;
8090
}
8191

8292
export type BaseOverride = {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,19 @@ export function getTagsFromValue(value?: CacheValue<"cache">) {
3939
return [];
4040
}
4141
}
42+
43+
export function createCacheKey(key: string, isDataCache: boolean): string {
44+
// We always prepend the build ID to the cache key for ISR/SSG cache entry
45+
// For data cache, we only prepend the build ID if the persistentDataCache is not enabled
46+
const shouldPrependBuildId =
47+
globalThis.openNextConfig.dangerous?.persistentDataCache !== true ||
48+
!isDataCache;
49+
if (shouldPrependBuildId) {
50+
// If we don't have a build ID, we just return the key as is
51+
if (!process.env.NEXT_BUILD_ID) {
52+
return key;
53+
}
54+
return `${process.env.NEXT_BUILD_ID}/${key}`;
55+
}
56+
return key;
57+
}

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)