Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions .changeset/rich-pumas-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": patch
---

Add a new option to bypass checking the tag cache from an incremental cache get
28 changes: 13 additions & 15 deletions packages/open-next/src/adapters/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,9 @@ export default class Cache {

const _tags = [...(tags ?? []), ...(softTags ?? [])];
const _lastModified = cachedEntry.lastModified ?? Date.now();
const _hasBeenRevalidated = await hasBeenRevalidated(
key,
_tags,
cachedEntry,
);
const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache
? false
: await hasBeenRevalidated(key, _tags, cachedEntry);

if (_hasBeenRevalidated) return null;

Expand All @@ -82,11 +80,13 @@ export default class Cache {
!tag.endsWith("page"),
);
if (path) {
const hasPathBeenUpdated = await hasBeenRevalidated(
path.replace("_N_T_/", ""),
[],
cachedEntry,
);
const hasPathBeenUpdated = cachedEntry.shouldBypassTagCache
? false
: await hasBeenRevalidated(
path.replace("_N_T_/", ""),
[],
cachedEntry,
);
if (hasPathBeenUpdated) {
// In case the path has been revalidated, we don't want to use the fetch cache
return null;
Expand Down Expand Up @@ -118,11 +118,9 @@ export default class Cache {
const meta = cacheData.meta;
const tags = getTagsFromValue(cacheData);
const _lastModified = cachedEntry.lastModified ?? Date.now();
const _hasBeenRevalidated = await hasBeenRevalidated(
key,
tags,
cachedEntry,
);
const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache
? false
: await hasBeenRevalidated(key, tags, cachedEntry);
if (_hasBeenRevalidated) return null;

const store = globalThis.__openNextAls.getStore();
Expand Down
21 changes: 12 additions & 9 deletions packages/open-next/src/adapters/composable-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,23 @@ export default {
globalThis.tagCache.mode === "nextMode" &&
result.value.tags.length > 0
) {
const hasBeenRevalidated = await globalThis.tagCache.hasBeenRevalidated(
result.value.tags,
result.lastModified,
);
const hasBeenRevalidated = result.shouldBypassTagCache
? false
: await globalThis.tagCache.hasBeenRevalidated(
result.value.tags,
result.lastModified,
);
if (hasBeenRevalidated) return undefined;
} else if (
globalThis.tagCache.mode === "original" ||
globalThis.tagCache.mode === undefined
) {
const hasBeenRevalidated =
(await globalThis.tagCache.getLastModified(
cacheKey,
result.lastModified,
)) === -1;
const hasBeenRevalidated = result.shouldBypassTagCache
? false
: (await globalThis.tagCache.getLastModified(
cacheKey,
result.lastModified,
)) === -1;
if (hasBeenRevalidated) return undefined;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/open-next/src/types/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ export type CachedFetchValue = {
export type WithLastModified<T> = {
lastModified?: number;
value?: T;
/**
* If set to true, we will not check the tag cache for this entry.
* `revalidateTag` and `revalidatePath` may not work as expected.
*/
shouldBypassTagCache?: boolean;
};

export type CacheEntryType = Extension;
Expand Down
197 changes: 197 additions & 0 deletions packages/tests-unit/tests/adapters/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ describe("CacheHandler", () => {
await cache.revalidateTag("tag");

expect(tagCache.writeTags).not.toHaveBeenCalled();
// Reset the config
globalThis.openNextConfig.dangerous.disableTagCache = false;
});

it("Should call tagCache.writeTags", async () => {
Expand Down Expand Up @@ -621,4 +623,199 @@ describe("CacheHandler", () => {
globalThis.tagCache.getPathsByTags = undefined;
});
});

describe("shouldBypassTagCache", () => {
describe("fetch cache", () => {
it("Should bypass tag cache validation when shouldBypassTagCache is true", async () => {
incrementalCache.get.mockResolvedValueOnce({
value: {
kind: "FETCH",
data: {
headers: {},
body: "{}",
url: "https://example.com",
status: 200,
},
},
lastModified: Date.now(),
shouldBypassTagCache: true,
});

const result = await cache.get("key", {
kind: "FETCH",
tags: ["tag1"],
});

expect(getFetchCacheSpy).toHaveBeenCalled();
expect(tagCache.getLastModified).not.toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).not.toHaveBeenCalled();
expect(result).not.toBeNull();
expect(result?.value).toEqual({
kind: "FETCH",
data: {
headers: {},
body: "{}",
url: "https://example.com",
status: 200,
},
});
});

it("Should not bypass tag cache validation when shouldBypassTagCache is false", async () => {
globalThis.tagCache.mode = "nextMode";
incrementalCache.get.mockResolvedValueOnce({
value: {
kind: "FETCH",
data: {
headers: {},
body: "{}",
url: "https://example.com",
status: 200,
},
},
lastModified: Date.now(),
shouldBypassTagCache: false,
});

const result = await cache.get("key", {
kind: "FETCH",
tags: ["tag1"],
});

expect(getFetchCacheSpy).toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).toHaveBeenCalled();
expect(result).not.toBeNull();
});

it("Should not bypass tag cache validation when shouldBypassTagCache is undefined", async () => {
globalThis.tagCache.mode = "nextMode";
tagCache.hasBeenRevalidated.mockResolvedValueOnce(false);
incrementalCache.get.mockResolvedValueOnce({
value: {
kind: "FETCH",
data: {
headers: {},
body: "{}",
url: "https://example.com",
status: 200,
},
},
lastModified: Date.now(),
// shouldBypassTagCache not set
});

const result = await cache.get("key", {
kind: "FETCH",
tags: ["tag1"],
});

expect(getFetchCacheSpy).toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).toHaveBeenCalled();
expect(result).not.toBeNull();
});

it("Should bypass path validation when shouldBypassTagCache is true for soft tags", async () => {
incrementalCache.get.mockResolvedValueOnce({
value: {
kind: "FETCH",
data: {
headers: {},
body: "{}",
url: "https://example.com",
status: 200,
},
},
lastModified: Date.now(),
shouldBypassTagCache: true,
});

const result = await cache.get("key", {
kind: "FETCH",
softTags: ["_N_T_/path"],
});

expect(getFetchCacheSpy).toHaveBeenCalled();
expect(tagCache.getLastModified).not.toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).not.toHaveBeenCalled();
expect(result).not.toBeNull();
});
});

describe("incremental cache", () => {
it("Should bypass tag cache validation when shouldBypassTagCache is true", async () => {
incrementalCache.get.mockResolvedValueOnce({
value: {
type: "route",
body: "{}",
},
lastModified: Date.now(),
shouldBypassTagCache: true,
});

const result = await cache.get("key", { kindHint: "app" });

expect(getIncrementalCache).toHaveBeenCalled();
expect(tagCache.getLastModified).not.toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).not.toHaveBeenCalled();
expect(result).not.toBeNull();
expect(result?.value?.kind).toEqual("ROUTE");
});

it("Should not bypass tag cache validation when shouldBypassTagCache is false", async () => {
globalThis.tagCache.mode = "nextMode";
incrementalCache.get.mockResolvedValueOnce({
value: {
type: "route",
body: "{}",
},
lastModified: Date.now(),
shouldBypassTagCache: false,
});

const result = await cache.get("key", { kindHint: "app" });

expect(getIncrementalCache).toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).toHaveBeenCalled();
expect(result).not.toBeNull();
});

it("Should return null when tag cache indicates revalidation and shouldBypassTagCache is false", async () => {
globalThis.tagCache.mode = "nextMode";
tagCache.hasBeenRevalidated.mockResolvedValueOnce(true);
incrementalCache.get.mockResolvedValueOnce({
value: {
type: "route",
body: "{}",
},
lastModified: Date.now(),
shouldBypassTagCache: false,
});

const result = await cache.get("key", { kindHint: "app" });

expect(getIncrementalCache).toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).toHaveBeenCalled();
expect(result).toBeNull();
});

it("Should return value when tag cache indicates revalidation but shouldBypassTagCache is true", async () => {
incrementalCache.get.mockResolvedValueOnce({
value: {
type: "route",
body: "{}",
},
lastModified: Date.now(),
shouldBypassTagCache: true,
});

const result = await cache.get("key", { kindHint: "app" });

expect(getIncrementalCache).toHaveBeenCalled();
expect(tagCache.getLastModified).not.toHaveBeenCalled();
expect(tagCache.hasBeenRevalidated).not.toHaveBeenCalled();
expect(result).not.toBeNull();
expect(result?.value?.kind).toEqual("ROUTE");
});
});
});
});