svelte-query: query subscriptions survive effect-root disposal after async Svelte update
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- frontend, testing-qa
Hướng nghiên cứu
The bug is in packages/svelte-query/src/createBaseQuery.svelte.ts, specifically the subscription cleanup logic after change #9810. Start by examining the test file tests/createQuery/observer-cleanup.svelte.test.ts to understand the failing scenarios. Run the provided test command to reproduce the issue. Focus on how the initial subscription is created and why its cleanup is not triggered when the effect root's disposer runs. The fix must preserve eager subscription behavior while ensuring proper teardown.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Describe the bug
After #9810, a query created inside $effect.root with an explicit QueryClient can remain subscribed after the root's disposer runs. This affects both createQuery and createInfiniteQuery when the client/restoration subscription has not changed.
The retained observer prevents normal garbage collection. Interval refetches continue after disposal, and a pending request that consumes its AbortSignal is not aborted. Normal component unmount still cleans up in the control test.
Your minimal, reproducible example
Test-only reproducer, pinned commit
A minimal client-side .svelte.ts example is:
import { flushSync } from 'svelte'
import { QueryClient, createQuery } from '@tanstack/svelte-query'
const client = new QueryClient()
const queryKey = ['root-cleanup']
const dispose = $effect.root(() => {
createQuery(
() => ({ queryKey, queryFn: () => Promise.resolve('data') }),
() => client,
)
})
flushSync()
const query = client.getQueryCache().find({ queryKey })!
console.log(query.getObserversCount()) // 1
dispose()
console.log(query.getObserversCount()) // expected 0, currently 1
client.clear()
Steps to reproduce
From a clean checkout of the linked reproduction branch, use Node 24.16.0 and pnpm 12.4.2, install dependencies with pnpm install --frozen-lockfile, then run:
NX_DAEMON=false NX_NO_CLOUD=true NX_ISOLATE_PLUGINS=false \
pnpm nx run @tanstack/svelte-query:test:lib --skip-nx-cache -- \
tests/createQuery/observer-cleanup.svelte.test.ts
The test-only commit is based on main 848b42129eef0dd7f55c99d2d3d1418bda4853eb. It deliberately fails: 10 failed, 2 passed.
| Check | Current result |
|---|---|
| Success, error, disabled state, for query and infinite query | Observer remains; cache entry survives gcTime |
| Disposal before the first effect runs, for both query types | Observer remains |
| Pending request consuming its signal | Signal is not aborted |
| Interval refetch after disposal | Mock query-function call count grows from 1 to 4 over 30ms of fake time |
| Normal component unmount | Passes |
| Disposal after switching query clients | Passes |
Expected behavior
Root disposal should unsubscribe its observers, allowing normal GC and cancellation behavior and stopping interval refetches.
How often does this bug happen?
Every time in the local reproduction. The tests use fake timers and local promises, without network requests.
Platform
macOS ARM64, Node 24.16.0, Vitest 4.1.2 with jsdom. Not reproduced in a real browser or an SSR application yet.
TanStack Query adapter and version
@tanstack/svelte-query 6.2.4, tested from the main checkout above; Svelte 5.55.1, TypeScript 5.9.3 in the Svelte package.
Additional context
With the same current dependencies, replacing only createBaseQuery.svelte.ts with its pre-#9810 version makes all 12 checks pass. This is a single-file comparison, not a claim that the complete historical checkout was tested. The existing Svelte suite passes all 214 tests on the initial audit main 8a5e385f67a192b308057c4873b2d550a731f42b; the relevant subscription code is unchanged on the newer base.
The initial subscription is created eagerly in createBaseQuery.svelte.ts. Its watchChanges callback supplies cleanup only after a later change, because the helper skips its first invocation. Outside component initialization, the onDestroy registration throws and is caught. This leaves the initial subscription without a working root teardown path.
This differs from #11542 and #11557: no reactive option update or query-function state write is needed. The repository's existing withEffectRoot helper also uses this non-component execution pattern, but does not assert observer disposal.
The scope should preserve #9810's eager async/SSR subscription behavior rather than revert it. A cleanup registered only once an effect executes would also need to account for the early-disposal case above. Is effect-root usage with an explicit client intended to remain supported? If so, would a lifecycle-only fix with these regression cases be in scope?
- Ngôn ngữ chính
- TypeScript
- Star
- 50.3k
- Fork
- 4.2k
- Merge trung bình
- 22 giờ 33 phút
- Pull request đã merge (30 ngày)
- 214
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của TanStack/query
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 40/100
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 78/100
Tất cả issue của TanStack/query
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
bcgov/bc-wallet-mobile#4761 · 1 bình luận ·
-
external-issue to-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
-
area-deployment area-integrations triage:bot-seen
Độ khó 2/5 Nửa ngày Mức phù hợp với người mới 86/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
-
refactor
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100