From dfa195ab2d45d6909025b7477beee83753820c2f Mon Sep 17 00:00:00 2001 From: Michel Smola Date: Fri, 25 Sep 2026 11:20:33 +0200 Subject: [PATCH 1/3] fix: force invalidation immediately on mutation or manual revalidation --- .changeset/heavy-crews-lose.md | 5 +++++ packages/toapi-client/src/cache.ts | 12 ++++++------ packages/toapi-client/src/create-fetch-client.ts | 9 ++++++--- packages/toapi-client/src/pub-sub.ts | 16 ++++++++++++++-- 4 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 .changeset/heavy-crews-lose.md diff --git a/.changeset/heavy-crews-lose.md b/.changeset/heavy-crews-lose.md new file mode 100644 index 00000000..cbd84327 --- /dev/null +++ b/.changeset/heavy-crews-lose.md @@ -0,0 +1,5 @@ +--- +"@toapi/client": patch +--- + +force invalidation immediately on mutation or manual revalidation diff --git a/packages/toapi-client/src/cache.ts b/packages/toapi-client/src/cache.ts index 112f00dc..7d774b94 100644 --- a/packages/toapi-client/src/cache.ts +++ b/packages/toapi-client/src/cache.ts @@ -70,7 +70,7 @@ export class Cache { const cached = this.storage.get(url); if (cached !== entry) return; this.storage.delete(url); - this.pubSub.publish(new Set([url])); + this.pubSub.publish(new Set([url]), false); }, Math.max(0, timeUntilRevalidation), ); @@ -82,12 +82,12 @@ export class Cache { return this.storage.get(url); } - public invalidateUrl(url: string) { + public invalidateUrl(url: string, force: boolean) { this.storage.delete(url); - return this.pubSub.publish(new Set([url])); + return this.pubSub.publish(new Set([url]), force); } - public invalidateTags(tags: string[]) { + public invalidateTags(tags: string[], force: boolean) { const urls = new Set(); for (const tag of tags) { const taggedUrls = this.tagIndex.get(tag); @@ -101,13 +101,13 @@ export class Cache { urls.add(url); } } - return this.pubSub.publish(urls); + return this.pubSub.publish(urls, force); } public invalidateAll() { const urls = new Set(this.storage.keys()); this.storage.clear(); this.tagIndex.clear(); - return this.pubSub.publish(urls); + return this.pubSub.publish(urls, false); } } diff --git a/packages/toapi-client/src/create-fetch-client.ts b/packages/toapi-client/src/create-fetch-client.ts index b8d7e99e..e10d58e2 100644 --- a/packages/toapi-client/src/create-fetch-client.ts +++ b/packages/toapi-client/src/create-fetch-client.ts @@ -49,7 +49,7 @@ export function createFetchClient< listenForInvalidations({ fetch, - onInvalidate: (tags) => cache.invalidateTags(tags), + onInvalidate: (tags) => cache.invalidateTags(tags, false), onConnect: () => cache.invalidateAll(), logger: options.logger, invalidationsUrl, @@ -69,7 +69,7 @@ export function createFetchClient< } async function revalidate(url: string) { - await cache.invalidateUrl(url); + await cache.invalidateUrl(url, true); } function mutate( @@ -102,7 +102,10 @@ export function createFetchClient< }); const revalidated = response.then((res) => - cache.invalidateTags(res.headers.get(TAGS_HEADER)?.split(" ") ?? []), + cache.invalidateTags( + res.headers.get(TAGS_HEADER)?.split(" ") ?? [], + true, + ), ); return Object.defineProperty(body, "revalidated", { diff --git a/packages/toapi-client/src/pub-sub.ts b/packages/toapi-client/src/pub-sub.ts index 22b04c3f..29334e48 100644 --- a/packages/toapi-client/src/pub-sub.ts +++ b/packages/toapi-client/src/pub-sub.ts @@ -21,7 +21,19 @@ export class PubSub { }; } - async publish(urls: Set) { + async publish(urls: Set, force: boolean) { + if (force) { + for (const url of urls) { + const timeout = this.debounceTimeouts.get(url); + clearTimeout(timeout); + this.debounceTimeouts.delete(url); + } + await Promise.all( + this.subscriptions.values().map((callback) => callback(urls)), + ); + return; + } + for (const url of urls) { if (this.debounceTimeouts.has(url)) { // debounced, mark as requested and ignore now @@ -39,7 +51,7 @@ export class PubSub { } } // Consume pending invalidations and unlock the entire batch before replay. - if (pendingUrls.size) return this.publish(pendingUrls); + if (pendingUrls.size) return this.publish(pendingUrls, false); }, this.minTTL); for (const url of urls) { From cc4204ff8ec25a35f055fb40970e520de573f14a Mon Sep 17 00:00:00 2001 From: Michel Smola Date: Fri, 25 Sep 2026 11:21:34 +0200 Subject: [PATCH 2/3] test: update test --- packages/toapi-client/src/create-fetch-client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toapi-client/src/create-fetch-client.test.ts b/packages/toapi-client/src/create-fetch-client.test.ts index 8d9e3e16..6e4d78a4 100644 --- a/packages/toapi-client/src/create-fetch-client.test.ts +++ b/packages/toapi-client/src/create-fetch-client.test.ts @@ -345,7 +345,7 @@ describe("createFetchClient", () => { expect(cb).toHaveBeenCalledTimes(2); await debounceClient.books.revalidate(); - expect(cb).toHaveBeenCalledTimes(2); + expect(cb).toHaveBeenCalledTimes(3); await vi.advanceTimersByTimeAsync(minTTL); expect(cb).toHaveBeenCalledTimes(3); From 4093d6d95a553e50a74891bbc405ca3950fc541e Mon Sep 17 00:00:00 2001 From: Michel Smola Date: Fri, 25 Sep 2026 11:24:03 +0200 Subject: [PATCH 3/3] test: update test naming --- packages/toapi-client/src/create-fetch-client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toapi-client/src/create-fetch-client.test.ts b/packages/toapi-client/src/create-fetch-client.test.ts index 6e4d78a4..9215be11 100644 --- a/packages/toapi-client/src/create-fetch-client.test.ts +++ b/packages/toapi-client/src/create-fetch-client.test.ts @@ -328,7 +328,7 @@ describe("createFetchClient", () => { expect(getPost).toHaveBeenCalledTimes(2); }); - test("debounce", async () => { + test("force immediate invalidation", async () => { vi.useFakeTimers(); const minTTL = 1000; const debounceClient = createFetchClient(