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.test.ts b/packages/toapi-client/src/create-fetch-client.test.ts index 8d9e3e16..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( @@ -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); 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) {