From fc2e1726051e67e50189b345b6861ff09ece0459 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 28 Apr 2025 14:44:14 +0200 Subject: [PATCH 1/3] simple tag cache filter --- .../tag-cache/tag-cache-filter.spec.ts | 123 ++++++++++++++++++ .../overrides/tag-cache/tag-cache-filter.ts | 59 +++++++++ 2 files changed, 182 insertions(+) create mode 100644 packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts create mode 100644 packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts new file mode 100644 index 00000000..defab14e --- /dev/null +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts @@ -0,0 +1,123 @@ +import { NextModeTagCache } from "@opennextjs/aws/types/overrides"; +import {beforeEach, describe, expect, it, vi } from "vitest"; + +import { softTagFilter,withFilter } from "./tag-cache-filter"; + +const mockedTagCache = { + name: "mocked", + mode: "nextMode", + getPathsByTags: vi.fn(), + hasBeenRevalidated: vi.fn(), + writeTags: vi.fn(), +} satisfies NextModeTagCache; + +const filterFn = (tag: string) => tag.startsWith("valid_"); + +describe("withFilter", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should filter out tags based on writeTags", async () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + + const tags = ["valid_tag", "invalid_tag"]; + + await tagCache.writeTags(tags); + expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]); + }) + + it('should not call writeTags if no tags are valid', async () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + const tags = ["invalid_tag"]; + await tagCache.writeTags(tags); + expect(mockedTagCache.writeTags).not.toHaveBeenCalled(); + }) + + it("should filter out tags based on hasBeenRevalidated", async () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + + const tags = ["valid_tag", "invalid_tag"]; + const lastModified = Date.now(); + + await tagCache.hasBeenRevalidated(tags, lastModified); + expect(mockedTagCache.hasBeenRevalidated).toHaveBeenCalledWith(["valid_tag"], lastModified); + } + ) + + it('should not call hasBeenRevalidated if no tags are valid', async () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + const tags = ["invalid_tag"]; + const lastModified = Date.now(); + await tagCache.hasBeenRevalidated(tags, lastModified); + expect(mockedTagCache.hasBeenRevalidated).not.toHaveBeenCalled(); + }) + + it("should filter out tags based on getPathsByTags", async () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + + const tags = ["valid_tag", "invalid_tag"]; + + await tagCache.getPathsByTags?.(tags); + expect(mockedTagCache.getPathsByTags).toHaveBeenCalledWith(["valid_tag"]); + } + ) + + it('should not call getPathsByTags if no tags are valid', async () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + const tags = ["invalid_tag"]; + await tagCache.getPathsByTags?.(tags); + expect(mockedTagCache.getPathsByTags).not.toHaveBeenCalled(); + }) + + it('should return the correct name', () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn, + }); + + expect(tagCache.name).toBe('filtered-mocked'); + } + ) + + it("should not create a function if getPathsByTags is not defined", async () => { + const tagCache = withFilter({ + originalTagCache: { + ...mockedTagCache, + getPathsByTags: undefined, + }, + filterFn, + }); + + expect(tagCache.getPathsByTags).toBeUndefined(); + } + ) + + it("should properly filter soft tags", () => { + const tagCache = withFilter({ + originalTagCache: mockedTagCache, + filterFn: softTagFilter, + }); + + tagCache.writeTags(["valid_tag", "_N_T_/", "_N_T_/test", "_N_T_/layout"]); + expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]); + }) +}); \ No newline at end of file diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts new file mode 100644 index 00000000..c7f4250a --- /dev/null +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts @@ -0,0 +1,59 @@ +import { NextModeTagCache } from "@opennextjs/aws/types/overrides"; + +interface WithFilterOptions { + /** + * The original tag cache. + * Call to this will receive only the filtered tags. + */ + originalTagCache: NextModeTagCache; + /** + * The function to filter tags. + * @param tag The tag to filter. + * @returns true if the tag should be forwarde, false otherwise. + */ + filterFn: (tag: string) => boolean; +} + +/** + * Creates a new tag cache that filters tags based on the provided filter function. + * This is usefult to remove tags that are not used by the app, this could reduce the number of request to the underlying tag cache. + */ +export function withFilter({ + originalTagCache, + filterFn, +}: WithFilterOptions): NextModeTagCache { + return { + name: `filtered-${originalTagCache.name}`, + mode: "nextMode", + getPathsByTags: originalTagCache.getPathsByTags ? async (tags) => { + const filteredTags = tags.filter(filterFn); + if (filteredTags.length === 0) { + return []; + } + return originalTagCache.getPathsByTags!(filteredTags) + } : undefined, + hasBeenRevalidated: async (tags, lastModified) => { + const filteredTags = tags.filter(filterFn); + if (filteredTags.length === 0) { + return false; + } + return originalTagCache.hasBeenRevalidated(filteredTags, lastModified); + }, + writeTags: async (tags) => { + const filteredTags = tags.filter(filterFn); + if (filteredTags.length === 0) { + return; + } + return originalTagCache.writeTags(filteredTags); + }, + }; +} + +/** + * Filter function to exclude tags that start with "_N_T_". + * This is used to filter out internal soft tags. + * Can be used if `revalidatePath` is not used. + */ +export function softTagFilter(tag: string): boolean { + return !tag.startsWith("_N_T_"); +} \ No newline at end of file From 4ef4d2de4e72020520ed3a8ee33525d1b36ced94 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 28 Apr 2025 16:08:21 +0200 Subject: [PATCH 2/3] changeset and lint --- .changeset/neat-hornets-call.md | 5 +++ .../tag-cache/tag-cache-filter.spec.ts | 38 +++++++++---------- .../overrides/tag-cache/tag-cache-filter.ts | 25 ++++++------ 3 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 .changeset/neat-hornets-call.md diff --git a/.changeset/neat-hornets-call.md b/.changeset/neat-hornets-call.md new file mode 100644 index 00000000..b20026ab --- /dev/null +++ b/.changeset/neat-hornets-call.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +Add a new `withFilter` tag cache to allow to filter the tag cache used diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts index defab14e..c22f062f 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts @@ -1,7 +1,7 @@ import { NextModeTagCache } from "@opennextjs/aws/types/overrides"; -import {beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import { softTagFilter,withFilter } from "./tag-cache-filter"; +import { softTagFilter, withFilter } from "./tag-cache-filter"; const mockedTagCache = { name: "mocked", @@ -28,9 +28,9 @@ describe("withFilter", () => { await tagCache.writeTags(tags); expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]); - }) + }); - it('should not call writeTags if no tags are valid', async () => { + it("should not call writeTags if no tags are valid", async () => { const tagCache = withFilter({ originalTagCache: mockedTagCache, filterFn, @@ -38,7 +38,7 @@ describe("withFilter", () => { const tags = ["invalid_tag"]; await tagCache.writeTags(tags); expect(mockedTagCache.writeTags).not.toHaveBeenCalled(); - }) + }); it("should filter out tags based on hasBeenRevalidated", async () => { const tagCache = withFilter({ @@ -51,10 +51,9 @@ describe("withFilter", () => { await tagCache.hasBeenRevalidated(tags, lastModified); expect(mockedTagCache.hasBeenRevalidated).toHaveBeenCalledWith(["valid_tag"], lastModified); - } - ) + }); - it('should not call hasBeenRevalidated if no tags are valid', async () => { + it("should not call hasBeenRevalidated if no tags are valid", async () => { const tagCache = withFilter({ originalTagCache: mockedTagCache, filterFn, @@ -63,7 +62,7 @@ describe("withFilter", () => { const lastModified = Date.now(); await tagCache.hasBeenRevalidated(tags, lastModified); expect(mockedTagCache.hasBeenRevalidated).not.toHaveBeenCalled(); - }) + }); it("should filter out tags based on getPathsByTags", async () => { const tagCache = withFilter({ @@ -75,10 +74,9 @@ describe("withFilter", () => { await tagCache.getPathsByTags?.(tags); expect(mockedTagCache.getPathsByTags).toHaveBeenCalledWith(["valid_tag"]); - } - ) + }); - it('should not call getPathsByTags if no tags are valid', async () => { + it("should not call getPathsByTags if no tags are valid", async () => { const tagCache = withFilter({ originalTagCache: mockedTagCache, filterFn, @@ -86,17 +84,16 @@ describe("withFilter", () => { const tags = ["invalid_tag"]; await tagCache.getPathsByTags?.(tags); expect(mockedTagCache.getPathsByTags).not.toHaveBeenCalled(); - }) + }); - it('should return the correct name', () => { + it("should return the correct name", () => { const tagCache = withFilter({ originalTagCache: mockedTagCache, filterFn, }); - expect(tagCache.name).toBe('filtered-mocked'); - } - ) + expect(tagCache.name).toBe("filtered-mocked"); + }); it("should not create a function if getPathsByTags is not defined", async () => { const tagCache = withFilter({ @@ -108,8 +105,7 @@ describe("withFilter", () => { }); expect(tagCache.getPathsByTags).toBeUndefined(); - } - ) + }); it("should properly filter soft tags", () => { const tagCache = withFilter({ @@ -119,5 +115,5 @@ describe("withFilter", () => { tagCache.writeTags(["valid_tag", "_N_T_/", "_N_T_/test", "_N_T_/layout"]); expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]); - }) -}); \ No newline at end of file + }); +}); diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts index c7f4250a..9faefdfd 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts @@ -18,20 +18,19 @@ interface WithFilterOptions { * Creates a new tag cache that filters tags based on the provided filter function. * This is usefult to remove tags that are not used by the app, this could reduce the number of request to the underlying tag cache. */ -export function withFilter({ - originalTagCache, - filterFn, -}: WithFilterOptions): NextModeTagCache { +export function withFilter({ originalTagCache, filterFn }: WithFilterOptions): NextModeTagCache { return { name: `filtered-${originalTagCache.name}`, mode: "nextMode", - getPathsByTags: originalTagCache.getPathsByTags ? async (tags) => { - const filteredTags = tags.filter(filterFn); - if (filteredTags.length === 0) { - return []; - } - return originalTagCache.getPathsByTags!(filteredTags) - } : undefined, + getPathsByTags: originalTagCache.getPathsByTags + ? async (tags) => { + const filteredTags = tags.filter(filterFn); + if (filteredTags.length === 0) { + return []; + } + return originalTagCache.getPathsByTags!(filteredTags); + } + : undefined, hasBeenRevalidated: async (tags, lastModified) => { const filteredTags = tags.filter(filterFn); if (filteredTags.length === 0) { @@ -45,7 +44,7 @@ export function withFilter({ return; } return originalTagCache.writeTags(filteredTags); - }, + }, }; } @@ -56,4 +55,4 @@ export function withFilter({ */ export function softTagFilter(tag: string): boolean { return !tag.startsWith("_N_T_"); -} \ No newline at end of file +} From 58af07bec8e5a04fb6cd35a915899e0db3c70e3a Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 28 Apr 2025 16:52:06 +0200 Subject: [PATCH 3/3] review fix --- .changeset/neat-hornets-call.md | 2 +- .../tag-cache/tag-cache-filter.spec.ts | 20 +++++++++---------- .../overrides/tag-cache/tag-cache-filter.ts | 18 ++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.changeset/neat-hornets-call.md b/.changeset/neat-hornets-call.md index b20026ab..540bc7ae 100644 --- a/.changeset/neat-hornets-call.md +++ b/.changeset/neat-hornets-call.md @@ -2,4 +2,4 @@ "@opennextjs/cloudflare": patch --- -Add a new `withFilter` tag cache to allow to filter the tag cache used +Add a new `withFilter` tag cache to allow to filter the tags used diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts index c22f062f..4dec2868 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts @@ -20,7 +20,7 @@ describe("withFilter", () => { it("should filter out tags based on writeTags", async () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); @@ -32,7 +32,7 @@ describe("withFilter", () => { it("should not call writeTags if no tags are valid", async () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); const tags = ["invalid_tag"]; @@ -42,7 +42,7 @@ describe("withFilter", () => { it("should filter out tags based on hasBeenRevalidated", async () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); @@ -55,7 +55,7 @@ describe("withFilter", () => { it("should not call hasBeenRevalidated if no tags are valid", async () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); const tags = ["invalid_tag"]; @@ -66,7 +66,7 @@ describe("withFilter", () => { it("should filter out tags based on getPathsByTags", async () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); @@ -78,7 +78,7 @@ describe("withFilter", () => { it("should not call getPathsByTags if no tags are valid", async () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); const tags = ["invalid_tag"]; @@ -88,7 +88,7 @@ describe("withFilter", () => { it("should return the correct name", () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn, }); @@ -97,7 +97,7 @@ describe("withFilter", () => { it("should not create a function if getPathsByTags is not defined", async () => { const tagCache = withFilter({ - originalTagCache: { + tagCache: { ...mockedTagCache, getPathsByTags: undefined, }, @@ -107,9 +107,9 @@ describe("withFilter", () => { expect(tagCache.getPathsByTags).toBeUndefined(); }); - it("should properly filter soft tags", () => { + it("should filter soft tags", () => { const tagCache = withFilter({ - originalTagCache: mockedTagCache, + tagCache: mockedTagCache, filterFn: softTagFilter, }); diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts index 9faefdfd..f9a0acf8 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts @@ -5,30 +5,30 @@ interface WithFilterOptions { * The original tag cache. * Call to this will receive only the filtered tags. */ - originalTagCache: NextModeTagCache; + tagCache: NextModeTagCache; /** * The function to filter tags. * @param tag The tag to filter. - * @returns true if the tag should be forwarde, false otherwise. + * @returns true if the tag should be forwarded, false otherwise. */ filterFn: (tag: string) => boolean; } /** * Creates a new tag cache that filters tags based on the provided filter function. - * This is usefult to remove tags that are not used by the app, this could reduce the number of request to the underlying tag cache. + * 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. */ -export function withFilter({ originalTagCache, filterFn }: WithFilterOptions): NextModeTagCache { +export function withFilter({ tagCache, filterFn }: WithFilterOptions): NextModeTagCache { return { - name: `filtered-${originalTagCache.name}`, + name: `filtered-${tagCache.name}`, mode: "nextMode", - getPathsByTags: originalTagCache.getPathsByTags + getPathsByTags: tagCache.getPathsByTags ? async (tags) => { const filteredTags = tags.filter(filterFn); if (filteredTags.length === 0) { return []; } - return originalTagCache.getPathsByTags!(filteredTags); + return tagCache.getPathsByTags!(filteredTags); } : undefined, hasBeenRevalidated: async (tags, lastModified) => { @@ -36,14 +36,14 @@ export function withFilter({ originalTagCache, filterFn }: WithFilterOptions): N if (filteredTags.length === 0) { return false; } - return originalTagCache.hasBeenRevalidated(filteredTags, lastModified); + return tagCache.hasBeenRevalidated(filteredTags, lastModified); }, writeTags: async (tags) => { const filteredTags = tags.filter(filterFn); if (filteredTags.length === 0) { return; } - return originalTagCache.writeTags(filteredTags); + return tagCache.writeTags(filteredTags); }, }; }