From d68aca5047ecd4a4a109714bb2a08611b67e99c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 18:26:11 +0000 Subject: [PATCH 1/3] fix(vue): DataClientPlugin garbage collects by default createDataClient defaults gcPolicy to new GCPolicy(), matching React's DataProvider, instead of falling back to ImmortalGCPolicy. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XaxNACdGzPiBhn9kgoP9n7 --- .../references/DataClientPlugin.md | 4 +- .../references/DataClientPlugin.md | 4 +- .changeset/vue-default-gcpolicy.md | 14 ++++++ docs/core/api/DataClientPlugin.vue.md | 4 +- .../integration-garbage-collection.web.ts | 46 ++++++++++++++++++- .../vue/src/providers/createDataClient.ts | 7 ++- website/blog/2026-10-03-v0.19-batch-set.md | 30 ++++++++++++ 7 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 .changeset/vue-default-gcpolicy.md diff --git a/.agents/skills/data-client-setup/references/DataClientPlugin.md b/.agents/skills/data-client-setup/references/DataClientPlugin.md index 740f3ec855f1..dc56592955e0 100644 --- a/.agents/skills/data-client-setup/references/DataClientPlugin.md +++ b/.agents/skills/data-client-setup/references/DataClientPlugin.md @@ -136,8 +136,8 @@ ctrl.doSomething(); ### gcPolicy?: GCInterface {#gcPolicy} -Removes data from the store once no component uses it and it has gone stale. By default, nothing -is ever removed. +Removes data from the store once no component uses it and it has gone stale. Defaults to +`new GCPolicy()`; pass one to change how often it sweeps or how long unused data is kept. ```ts title="main.ts" import { DataClientPlugin, GCPolicy } from '@data-client/vue'; diff --git a/.agents/skills/data-client-vue/references/DataClientPlugin.md b/.agents/skills/data-client-vue/references/DataClientPlugin.md index 2fe9c1909427..fd97f0763115 100644 --- a/.agents/skills/data-client-vue/references/DataClientPlugin.md +++ b/.agents/skills/data-client-vue/references/DataClientPlugin.md @@ -136,8 +136,8 @@ ctrl.doSomething(); ### gcPolicy?: GCInterface {#gcPolicy} -Removes data from the store once no component uses it and it has gone stale. By default, nothing -is ever removed. +Removes data from the store once no component uses it and it has gone stale. Defaults to +`new GCPolicy()`; pass one to change how often it sweeps or how long unused data is kept. ```ts title="main.ts" import { DataClientPlugin, GCPolicy } from '@data-client/vue'; diff --git a/.changeset/vue-default-gcpolicy.md b/.changeset/vue-default-gcpolicy.md new file mode 100644 index 000000000000..14ab51ef93c4 --- /dev/null +++ b/.changeset/vue-default-gcpolicy.md @@ -0,0 +1,14 @@ +--- +'@data-client/vue': patch +--- + +Fix `DataClientPlugin` never garbage collecting by default + +Without a `gcPolicy` option, data was kept in the store forever. It now defaults to `new GCPolicy()`, matching +`DataProvider` in `@data-client/react`: data no component uses is removed once it is stale. + +```ts +app.use(DataClientPlugin); +// Before: unused data stays in the store forever +// After: unused data is removed after it goes stale (swept every 5 minutes) +``` diff --git a/docs/core/api/DataClientPlugin.vue.md b/docs/core/api/DataClientPlugin.vue.md index bde3e4a57415..a9d8a98ac951 100644 --- a/docs/core/api/DataClientPlugin.vue.md +++ b/docs/core/api/DataClientPlugin.vue.md @@ -112,8 +112,8 @@ ctrl.doSomething(); ### gcPolicy?: GCInterface {#gcPolicy} -Removes data from the store once no component uses it and it has gone stale. By default, nothing -is ever removed. +Removes data from the store once no component uses it and it has gone stale. Defaults to +`new GCPolicy()`; pass one to change how often it sweeps or how long unused data is kept. ```ts title="main.ts" import { DataClientPlugin, GCPolicy } from '@data-client/vue'; diff --git a/packages/vue/src/__tests__/integration-garbage-collection.web.ts b/packages/vue/src/__tests__/integration-garbage-collection.web.ts index 64f76564566d..f5878b989d91 100644 --- a/packages/vue/src/__tests__/integration-garbage-collection.web.ts +++ b/packages/vue/src/__tests__/integration-garbage-collection.web.ts @@ -1,5 +1,5 @@ import { GCPolicy } from '@data-client/core'; -import { defineComponent, h } from 'vue'; +import { defineComponent, h, reactive } from 'vue'; import { Article, ArticleResource } from '../../../../__tests__/new'; import useFetch from '../consumers/useFetch'; @@ -16,6 +16,7 @@ describe('Integration Garbage Collection Web (Vue)', () => { }); afterEach(() => { infoSpy.mockRestore(); + jest.useRealTimers(); }); it('should initialize with GCPolicy', () => { @@ -191,4 +192,47 @@ describe('Integration Garbage Collection Web (Vue)', () => { cleanup2(); }); + + it('removes unused stale data without passing a gcPolicy', async () => { + jest.useFakeTimers(); + const ArticleDetail = defineComponent({ + name: 'ArticleDetail', + setup() { + const article = useQuery(Article, { id: 1 }); + return () => h('div', article.value?.title ?? 'missing'); + }, + }); + const props = reactive({ show: true }); + const TestComp = defineComponent({ + name: 'TestComp', + props: ['show'], + setup(props) { + return () => (props.show ? h(ArticleDetail) : h('div', 'blank')); + }, + }); + + const { wrapper, controller, cleanup } = mountDataClient(TestComp, { + props, + initialFixtures: [ + { + endpoint: ArticleResource.get, + args: [{ id: 1 }], + response: { id: 1, title: 'Test Article', content: 'Content' }, + }, + ], + }); + await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('Test Article'); + + props.show = false; + await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('blank'); + + // default GCPolicy sweeps every 5 minutes; data must also be stale + jest.advanceTimersByTime(60 * 1000 * 5); + await Promise.resolve(); + expect(controller.getState().entities.Article?.['1']).toBeUndefined(); + + cleanup(); + }); }); diff --git a/packages/vue/src/providers/createDataClient.ts b/packages/vue/src/providers/createDataClient.ts index aed012497a6a..2202da449c04 100644 --- a/packages/vue/src/providers/createDataClient.ts +++ b/packages/vue/src/providers/createDataClient.ts @@ -4,6 +4,7 @@ import { applyManager, initManager, createReducer, + GCPolicy, } from '@data-client/core'; import type { State, Manager, GCInterface } from '@data-client/core'; import { provide, shallowRef, type ShallowRef, type App } from 'vue'; @@ -36,7 +37,11 @@ export interface ProvidedDataClient { export function createDataClient( options: ProvideOptions = {}, ): ProvidedDataClient { - const { Controller = DataController, gcPolicy, app } = options; + const { + Controller = DataController, + gcPolicy = new GCPolicy(), + app, + } = options; // stable singletons for this provider scope const controller = new Controller({ gcPolicy }); diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index f313ea7ae8dd..b87d089927cc 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -30,6 +30,7 @@ endpoint code about 2x faster, and fixes a round of TypeScript and Vue issues. - Vue [useSuspense()](/vue/api/useSuspense) [shows stale data on mount](/blog/2026/10/03/v0.19-batch-set#vue-stale-while-revalidate) and refetches in the background, like React, instead of showing the `` fallback until the refetch finishes ([#4169](https://github.com/reactive/data-client/pull/4169)) - Vue [useDLE()](/vue/api/useDLE) and [useCache()](/vue/api/useCache) keep expired `invalidIfStale` data through unrelated store updates instead of getting stuck loading ([#4142](https://github.com/reactive/data-client/pull/4142)) - Vue `DataClientPlugin` installs on Vue versions before 3.5 instead of throwing `app.onUnmount is not a function` ([#4146](https://github.com/reactive/data-client/pull/4146)) +- Vue [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) garbage collects unused stale data by default, matching React's [DataProvider](/docs/api/DataProvider); [how it decides what to remove](/blog/2026/10/03/v0.19-batch-set#vue-gc) ([#4148](https://github.com/reactive/data-client/pull/4148)) - Vue composables accept [getter arguments](/blog/2026/10/03/v0.19-batch-set#vue-getter-args) like `() => ({ id: props.id })`; they were typed to allow them but passed the function itself to the endpoint ([#4115](https://github.com/reactive/data-client/pull/4115)) - Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) - Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#4140](https://github.com/reactive/data-client/pull/4140)) @@ -419,6 +420,35 @@ This applies to every composable that takes endpoint arguments: [useSuspense()]( [useLive()](/vue/api/useLive), [useCache()](/vue/api/useCache), [useDLE()](/vue/api/useDLE), [useFetch()](/vue/api/useFetch), [useQuery()](/vue/api/useQuery) and [useSubscription()](/vue/api/useSubscription). +### Vue garbage collects by default {#vue-gc} + +Without a `gcPolicy`, Vue's [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) kept every response in the store +for the life of the app, so long-running pages kept growing. It now defaults to `new GCPolicy()`, the same as React's +[DataProvider](/docs/api/DataProvider) ([#4148](https://github.com/reactive/data-client/pull/4148)). + +Data is removed only when no mounted component reads it _and_ twice its freshness lifetime has passed since it was +fetched (at least two minutes). A sweep checks every five minutes. Anything a component reads again before then is +back in use. + +
+ +
+ +```mermaid +flowchart LR + inuse("In use") -- "last reader unmounts" --> unused("Unused") + unused -- "2x lifetime passes" --> old("Unused and old") + old -- "next sweep" --> removed("Removed") +``` + +
+ +
+ +A component that mounts after removal treats the data as never fetched: [useSuspense()](/vue/api/useSuspense) fetches +it again, while [useCache()](/vue/api/useCache) returns `undefined`. To keep unused data longer or sweep less often, +pass your own [gcPolicy](/vue/api/DataClientPlugin#gcPolicy). + ### Vue stale-while-revalidate {#vue-stale-while-revalidate} When a component mounts with data that is [stale](/docs/concepts/expiry-policy#stale) but still valid, Vue From c9781785d324689cf40855264ef8b5b9c11fa851 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 18:44:52 +0000 Subject: [PATCH 2/3] docs(changeset): describe Vue GC default as behavior, not code comments Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XaxNACdGzPiBhn9kgoP9n7 --- .changeset/vue-default-gcpolicy.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.changeset/vue-default-gcpolicy.md b/.changeset/vue-default-gcpolicy.md index 14ab51ef93c4..f3eebf2c6bf0 100644 --- a/.changeset/vue-default-gcpolicy.md +++ b/.changeset/vue-default-gcpolicy.md @@ -4,11 +4,9 @@ Fix `DataClientPlugin` never garbage collecting by default -Without a `gcPolicy` option, data was kept in the store forever. It now defaults to `new GCPolicy()`, matching -`DataProvider` in `@data-client/react`: data no component uses is removed once it is stale. +Without a `gcPolicy` option, every response stayed in the store for the life of the app, so long-running +pages kept growing. It now defaults to `new GCPolicy()`, matching `DataProvider` in `@data-client/react`. -```ts -app.use(DataClientPlugin); -// Before: unused data stays in the store forever -// After: unused data is removed after it goes stale (swept every 5 minutes) -``` +Data that no mounted component uses is removed once it is stale (checked every 5 minutes). A component that +mounts after that refetches it with `useSuspense()`, while `useCache()` returns `undefined` until something fetches +it again. Pass your own `gcPolicy` to keep unused data longer or sweep less often. From d4f35eb64958833152d1beea594f3ffb9b77bb29 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 18:45:35 +0000 Subject: [PATCH 3/3] docs: frame Vue GC default as dropping the gcPolicy option Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XaxNACdGzPiBhn9kgoP9n7 --- .changeset/vue-default-gcpolicy.md | 25 ++++++++++++++++------ website/blog/2026-10-03-v0.19-batch-set.md | 24 +++++++++++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.changeset/vue-default-gcpolicy.md b/.changeset/vue-default-gcpolicy.md index f3eebf2c6bf0..0bf139ffba2a 100644 --- a/.changeset/vue-default-gcpolicy.md +++ b/.changeset/vue-default-gcpolicy.md @@ -2,11 +2,24 @@ '@data-client/vue': patch --- -Fix `DataClientPlugin` never garbage collecting by default +`DataClientPlugin` garbage collects by default -Without a `gcPolicy` option, every response stayed in the store for the life of the app, so long-running -pages kept growing. It now defaults to `new GCPolicy()`, matching `DataProvider` in `@data-client/react`. +Previously, unused data was only removed from the store if you passed a `gcPolicy` yourself. It now defaults +to `new GCPolicy()`, matching `DataProvider` in `@data-client/react`, so you can drop the option unless you +customize it. -Data that no mounted component uses is removed once it is stale (checked every 5 minutes). A component that -mounts after that refetches it with `useSuspense()`, while `useCache()` returns `undefined` until something fetches -it again. Pass your own `gcPolicy` to keep unused data longer or sweep less often. +Before + +```ts +import { DataClientPlugin, GCPolicy } from '@data-client/vue'; + +app.use(DataClientPlugin, { gcPolicy: new GCPolicy() }); +``` + +After + +```ts +import { DataClientPlugin } from '@data-client/vue'; + +app.use(DataClientPlugin); +``` diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index 6d9e58c5ffca..46ecdb184028 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -30,7 +30,7 @@ endpoint code about 2x faster, and fixes a round of TypeScript and Vue issues. - Vue [useSuspense()](/vue/api/useSuspense) [shows stale data on mount](/blog/2026/10/03/v0.19-batch-set#vue-stale-while-revalidate) and refetches in the background, like React, instead of showing the `` fallback until the refetch finishes ([#4169](https://github.com/reactive/data-client/pull/4169)) - Vue [useDLE()](/vue/api/useDLE) and [useCache()](/vue/api/useCache) keep expired `invalidIfStale` data through unrelated store updates instead of getting stuck loading ([#4142](https://github.com/reactive/data-client/pull/4142)) - Vue `DataClientPlugin` installs on Vue versions before 3.5 instead of throwing `app.onUnmount is not a function` ([#4146](https://github.com/reactive/data-client/pull/4146)) -- Vue [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) garbage collects unused stale data by default, matching React's [DataProvider](/docs/api/DataProvider); [how it decides what to remove](/blog/2026/10/03/v0.19-batch-set#vue-gc) ([#4148](https://github.com/reactive/data-client/pull/4148)) +- Vue [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) garbage collects by default, matching React's [DataProvider](/docs/api/DataProvider), so you no longer need to pass `gcPolicy`; [how it decides what to remove](/blog/2026/10/03/v0.19-batch-set#vue-gc) ([#4148](https://github.com/reactive/data-client/pull/4148)) - Vue composables accept [getter arguments](/blog/2026/10/03/v0.19-batch-set#vue-getter-args) like `() => ({ id: props.id })`; they were typed to allow them but passed the function itself to the endpoint ([#4115](https://github.com/reactive/data-client/pull/4115)) - Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) - Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#4140](https://github.com/reactive/data-client/pull/4140)) @@ -423,9 +423,25 @@ This applies to every composable that takes endpoint arguments: [useSuspense()]( ### Vue garbage collects by default {#vue-gc} -Without a `gcPolicy`, Vue's [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) kept every response in the store -for the life of the app, so long-running pages kept growing. It now defaults to `new GCPolicy()`, the same as React's -[DataProvider](/docs/api/DataProvider) ([#4148](https://github.com/reactive/data-client/pull/4148)). +Vue's [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) only removed unused data if you passed a `gcPolicy` +yourself. It now defaults to `new GCPolicy()`, the same as React's [DataProvider](/docs/api/DataProvider), so you can +drop the option unless you customize it ([#4148](https://github.com/reactive/data-client/pull/4148)). + + + +```ts title="Before" +import { DataClientPlugin, GCPolicy } from '@data-client/vue'; + +app.use(DataClientPlugin, { gcPolicy: new GCPolicy() }); +``` + +```ts title="After" +import { DataClientPlugin } from '@data-client/vue'; + +app.use(DataClientPlugin); +``` + + Data is removed only when no mounted component reads it _and_ twice its freshness lifetime has passed since it was fetched (at least two minutes). A sweep checks every five minutes. Anything a component reads again before then is