Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/vinext/src/shims/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,21 @@ const _UNSTABLE_CACHE_ALS_KEY = Symbol.for("vinext.unstableCache.als");
const _unstableCacheAls = (
(_g[_UNSTABLE_CACHE_ALS_KEY] ??= new AsyncLocalStorage<boolean>()) as AsyncLocalStorage<boolean>
);
const UNSTABLE_CACHE_UNDEFINED_SENTINEL = "__vinext_unstable_cache_undefined__";

function serializeUnstableCacheResult(value: unknown): string {
return value === undefined
? UNSTABLE_CACHE_UNDEFINED_SENTINEL
: JSON.stringify(value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: JSON.stringify also returns undefined (the JS value) for top-level function and Symbol values. Those are equally un-cacheable with the current code path. Not something that needs fixing in this PR — just noting for awareness that the same class of bug exists for those types. In practice nobody caches functions, so this is fine.

}

function deserializeUnstableCacheResult(body: string): unknown {
if (body === UNSTABLE_CACHE_UNDEFINED_SENTINEL) {
return undefined;
}

return JSON.parse(body);
}

/**
* Check if the current execution context is inside an unstable_cache() callback.
Expand Down Expand Up @@ -625,7 +640,7 @@ export function unstable_cache<T extends (...args: any[]) => Promise<any>>(
});
if (existing?.value && existing.value.kind === "FETCH" && existing.cacheState !== "stale") {
try {
return JSON.parse(existing.value.data.body);
return deserializeUnstableCacheResult(existing.value.data.body);
} catch {
// Corrupted entry, fall through to re-fetch
}
Expand All @@ -641,7 +656,7 @@ export function unstable_cache<T extends (...args: any[]) => Promise<any>>(
kind: "FETCH",
data: {
headers: {},
body: JSON.stringify(result),
body: serializeUnstableCacheResult(result),
url: cacheKey,
},
tags,
Expand Down
21 changes: 21 additions & 0 deletions tests/shims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,27 @@ describe("next/cache shim", () => {
setCacheHandler(new MemoryCacheHandler());
});

it("unstable_cache caches undefined results", async () => {
const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
await import("../packages/vinext/src/shims/cache.js");

setCacheHandler(new MemoryCacheHandler());

let callCount = 0;
const cached = unstable_cache(async () => {
callCount++;
return undefined;
}, ["undefined-result-test"]);

await expect(cached()).resolves.toBeUndefined();
expect(callCount).toBe(1);

await expect(cached()).resolves.toBeUndefined();
expect(callCount).toBe(1);

setCacheHandler(new MemoryCacheHandler());
});

it("revalidateTag invalidates cached entries", async () => {
const {
unstable_cache,
Expand Down
Loading