Skip to content

Commit 055a61b

Browse files
authored
Feat tag-cache-filter (#608)
* simple tag cache filter * changeset and lint * review fix
1 parent b0c14e1 commit 055a61b

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

.changeset/neat-hornets-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
Add a new `withFilter` tag cache to allow to filter the tags used
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { NextModeTagCache } from "@opennextjs/aws/types/overrides";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
import { softTagFilter, withFilter } from "./tag-cache-filter";
5+
6+
const mockedTagCache = {
7+
name: "mocked",
8+
mode: "nextMode",
9+
getPathsByTags: vi.fn(),
10+
hasBeenRevalidated: vi.fn(),
11+
writeTags: vi.fn(),
12+
} satisfies NextModeTagCache;
13+
14+
const filterFn = (tag: string) => tag.startsWith("valid_");
15+
16+
describe("withFilter", () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
it("should filter out tags based on writeTags", async () => {
22+
const tagCache = withFilter({
23+
tagCache: mockedTagCache,
24+
filterFn,
25+
});
26+
27+
const tags = ["valid_tag", "invalid_tag"];
28+
29+
await tagCache.writeTags(tags);
30+
expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
31+
});
32+
33+
it("should not call writeTags if no tags are valid", async () => {
34+
const tagCache = withFilter({
35+
tagCache: mockedTagCache,
36+
filterFn,
37+
});
38+
const tags = ["invalid_tag"];
39+
await tagCache.writeTags(tags);
40+
expect(mockedTagCache.writeTags).not.toHaveBeenCalled();
41+
});
42+
43+
it("should filter out tags based on hasBeenRevalidated", async () => {
44+
const tagCache = withFilter({
45+
tagCache: mockedTagCache,
46+
filterFn,
47+
});
48+
49+
const tags = ["valid_tag", "invalid_tag"];
50+
const lastModified = Date.now();
51+
52+
await tagCache.hasBeenRevalidated(tags, lastModified);
53+
expect(mockedTagCache.hasBeenRevalidated).toHaveBeenCalledWith(["valid_tag"], lastModified);
54+
});
55+
56+
it("should not call hasBeenRevalidated if no tags are valid", async () => {
57+
const tagCache = withFilter({
58+
tagCache: mockedTagCache,
59+
filterFn,
60+
});
61+
const tags = ["invalid_tag"];
62+
const lastModified = Date.now();
63+
await tagCache.hasBeenRevalidated(tags, lastModified);
64+
expect(mockedTagCache.hasBeenRevalidated).not.toHaveBeenCalled();
65+
});
66+
67+
it("should filter out tags based on getPathsByTags", async () => {
68+
const tagCache = withFilter({
69+
tagCache: mockedTagCache,
70+
filterFn,
71+
});
72+
73+
const tags = ["valid_tag", "invalid_tag"];
74+
75+
await tagCache.getPathsByTags?.(tags);
76+
expect(mockedTagCache.getPathsByTags).toHaveBeenCalledWith(["valid_tag"]);
77+
});
78+
79+
it("should not call getPathsByTags if no tags are valid", async () => {
80+
const tagCache = withFilter({
81+
tagCache: mockedTagCache,
82+
filterFn,
83+
});
84+
const tags = ["invalid_tag"];
85+
await tagCache.getPathsByTags?.(tags);
86+
expect(mockedTagCache.getPathsByTags).not.toHaveBeenCalled();
87+
});
88+
89+
it("should return the correct name", () => {
90+
const tagCache = withFilter({
91+
tagCache: mockedTagCache,
92+
filterFn,
93+
});
94+
95+
expect(tagCache.name).toBe("filtered-mocked");
96+
});
97+
98+
it("should not create a function if getPathsByTags is not defined", async () => {
99+
const tagCache = withFilter({
100+
tagCache: {
101+
...mockedTagCache,
102+
getPathsByTags: undefined,
103+
},
104+
filterFn,
105+
});
106+
107+
expect(tagCache.getPathsByTags).toBeUndefined();
108+
});
109+
110+
it("should filter soft tags", () => {
111+
const tagCache = withFilter({
112+
tagCache: mockedTagCache,
113+
filterFn: softTagFilter,
114+
});
115+
116+
tagCache.writeTags(["valid_tag", "_N_T_/", "_N_T_/test", "_N_T_/layout"]);
117+
expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
118+
});
119+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { NextModeTagCache } from "@opennextjs/aws/types/overrides";
2+
3+
interface WithFilterOptions {
4+
/**
5+
* The original tag cache.
6+
* Call to this will receive only the filtered tags.
7+
*/
8+
tagCache: NextModeTagCache;
9+
/**
10+
* The function to filter tags.
11+
* @param tag The tag to filter.
12+
* @returns true if the tag should be forwarded, false otherwise.
13+
*/
14+
filterFn: (tag: string) => boolean;
15+
}
16+
17+
/**
18+
* Creates a new tag cache that filters tags based on the provided filter function.
19+
* This is useful to remove tags that are not used by the app, this could reduce the number of requests to the underlying tag cache.
20+
*/
21+
export function withFilter({ tagCache, filterFn }: WithFilterOptions): NextModeTagCache {
22+
return {
23+
name: `filtered-${tagCache.name}`,
24+
mode: "nextMode",
25+
getPathsByTags: tagCache.getPathsByTags
26+
? async (tags) => {
27+
const filteredTags = tags.filter(filterFn);
28+
if (filteredTags.length === 0) {
29+
return [];
30+
}
31+
return tagCache.getPathsByTags!(filteredTags);
32+
}
33+
: undefined,
34+
hasBeenRevalidated: async (tags, lastModified) => {
35+
const filteredTags = tags.filter(filterFn);
36+
if (filteredTags.length === 0) {
37+
return false;
38+
}
39+
return tagCache.hasBeenRevalidated(filteredTags, lastModified);
40+
},
41+
writeTags: async (tags) => {
42+
const filteredTags = tags.filter(filterFn);
43+
if (filteredTags.length === 0) {
44+
return;
45+
}
46+
return tagCache.writeTags(filteredTags);
47+
},
48+
};
49+
}
50+
51+
/**
52+
* Filter function to exclude tags that start with "_N_T_".
53+
* This is used to filter out internal soft tags.
54+
* Can be used if `revalidatePath` is not used.
55+
*/
56+
export function softTagFilter(tag: string): boolean {
57+
return !tag.startsWith("_N_T_");
58+
}

0 commit comments

Comments
 (0)