-
Notifications
You must be signed in to change notification settings - Fork 0
Pending tasks and inject queries type experiments #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: refactor/require-angular-v19
Are you sure you want to change the base?
Changes from all commits
fb7a280
ada7148
af76175
09cc4e2
2f7b2f3
3791f19
63664ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,24 @@ | ||
| import { TestBed } from '@angular/core/testing' | ||
| import { afterEach, beforeEach, describe, expectTypeOf, test, vi } from 'vitest' | ||
| import { provideZonelessChangeDetection } from '@angular/core' | ||
| import { sleep } from '@tanstack/query-test-utils' | ||
| import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' | ||
| import { describe, expectTypeOf, test } from 'vitest' | ||
| import { injectInfiniteQuery } from '..' | ||
| import type { InfiniteData } from '@tanstack/query-core' | ||
|
|
||
| describe('injectInfiniteQuery', () => { | ||
| let queryClient: QueryClient | ||
|
|
||
| beforeEach(() => { | ||
| queryClient = new QueryClient() | ||
| vi.useFakeTimers() | ||
| TestBed.configureTestingModule({ | ||
| providers: [ | ||
| provideZonelessChangeDetection(), | ||
| provideTanStackQuery(queryClient), | ||
| ], | ||
| }) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| test('should narrow type after isSuccess', () => { | ||
| const query = TestBed.runInInjectionContext(() => { | ||
| return injectInfiniteQuery(() => ({ | ||
| queryKey: ['infiniteQuery'], | ||
| queryFn: ({ pageParam }) => | ||
| sleep(0).then(() => 'data on page ' + pageParam), | ||
| initialPageParam: 0, | ||
| getNextPageParam: () => 12, | ||
| })) | ||
| }) | ||
| test('should narrow type with isSuccess, isError, isPending', () => { | ||
| const query = injectInfiniteQuery(() => ({ | ||
| queryKey: ['infiniteQuery'], | ||
| queryFn: () => Promise.resolve('data'), | ||
| initialPageParam: 1, | ||
| getNextPageParam: () => 12, | ||
| })) | ||
|
|
||
| if (query.isSuccess()) { | ||
| const data = query.data() | ||
| expectTypeOf(data).toEqualTypeOf<InfiniteData<string, unknown>>() | ||
| expectTypeOf(query.error()).toEqualTypeOf<null>() | ||
| expectTypeOf(query.data()).toEqualTypeOf<InfiniteData<string, unknown>>() | ||
| } else if (query.isError()) { | ||
| expectTypeOf(query.error()).toEqualTypeOf<Error>() | ||
| } else if (query.isPending()) { | ||
| expectTypeOf(query.data()).toEqualTypeOf<undefined>() | ||
| expectTypeOf(query.error()).toEqualTypeOf<null>() | ||
| } | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -355,38 +355,42 @@ describe('PendingTasks Integration', () => { | |||||
| } | ||||||
|
|
||||||
| test('should cleanup pending tasks when component with active query is destroyed', async () => { | ||||||
| const app = TestBed.inject(ApplicationRef) | ||||||
| const fixture = TestBed.createComponent(TestComponent) | ||||||
| fixture.detectChanges() | ||||||
|
|
||||||
| // Start the query | ||||||
| expect(fixture.componentInstance.query.status()).toBe('pending') | ||||||
| expect(fixture.isStable()).toBe(false) | ||||||
|
|
||||||
| // Destroy component while query is running | ||||||
| fixture.destroy() | ||||||
|
|
||||||
| // Angular should become stable even though component was destroyed | ||||||
| const stablePromise = app.whenStable() | ||||||
| const stablePromise = fixture.whenStable() | ||||||
| await vi.advanceTimersByTimeAsync(150) | ||||||
|
|
||||||
| await expect(stablePromise).resolves.toEqual(undefined) | ||||||
| await stablePromise | ||||||
| expect(fixture.isStable()).toBe(true) | ||||||
| }) | ||||||
|
|
||||||
| test('should cleanup pending tasks when component with active mutation is destroyed', async () => { | ||||||
| const app = TestBed.inject(ApplicationRef) | ||||||
| const fixture = TestBed.createComponent(TestComponent) | ||||||
| fixture.detectChanges() | ||||||
|
|
||||||
| fixture.componentInstance.mutation.mutate('test') | ||||||
| fixture.detectChanges() | ||||||
| expect(fixture.isStable()).toBe(false) | ||||||
|
|
||||||
| // Destroy component while mutation is running | ||||||
| fixture.destroy() | ||||||
| fixture.detectChanges() | ||||||
|
||||||
| fixture.detectChanges() | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,13 +125,16 @@ export function injectMutation< | |
| effect( | ||
| (onCleanup) => { | ||
| const observer = observerSignal() | ||
| let destroyed = false | ||
| let taskCleanupRef: (() => void) | null = null | ||
|
|
||
| untracked(() => { | ||
| const unsubscribe = ngZone.runOutsideAngular(() => | ||
| observer.subscribe( | ||
| notifyManager.batchCalls((state) => { | ||
| ngZone.run(() => { | ||
| if (destroyed) return | ||
|
|
||
| // Track pending task when mutation is pending | ||
| if (state.isPending && !taskCleanupRef) { | ||
| taskCleanupRef = pendingTasks.add() | ||
|
Comment on lines
+136
to
140
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some race condition caused that the subscription to be called after the component was destroyed, starting a pending task without a callback to stop it. |
||
|
|
@@ -158,6 +161,7 @@ export function injectMutation< | |
| ) | ||
| onCleanup(() => { | ||
| // Clean up any pending task on destroy | ||
| destroyed = true | ||
| if (taskCleanupRef) { | ||
| taskCleanupRef() | ||
| taskCleanupRef = null | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import type { | |
| QueryFunction, | ||
| QueryKey, | ||
| QueryObserverOptions, | ||
| QueryObserverResult, | ||
| ThrowOnError, | ||
| } from '@tanstack/query-core' | ||
| import type { | ||
|
|
@@ -138,6 +139,57 @@ type GetCreateQueryResult<T> = | |
| : // Fallback | ||
| CreateQueryResult | ||
|
|
||
| // For the combine callback - uses core QueryObserverResult (plain values, not signals) | ||
| type GetQueryObserverResultForCombine<T> = T extends { | ||
| queryFnData: any | ||
| error?: infer TError | ||
| data: infer TData | ||
| } | ||
| ? QueryObserverResult<TData, TError> | ||
| : T extends { queryFnData: infer TQueryFnData; error?: infer TError } | ||
| ? QueryObserverResult<TQueryFnData, TError> | ||
| : T extends { data: infer TData; error?: infer TError } | ||
| ? QueryObserverResult<TData, TError> | ||
| : T extends [any, infer TError, infer TData] | ||
| ? QueryObserverResult<TData, TError> | ||
| : T extends [infer TQueryFnData, infer TError] | ||
| ? QueryObserverResult<TQueryFnData, TError> | ||
| : T extends [infer TQueryFnData] | ||
| ? QueryObserverResult<TQueryFnData> | ||
| : T extends { | ||
| queryFn?: | ||
| | QueryFunction<infer TQueryFnData, any> | ||
| | SkipTokenForCreateQueries | ||
| select?: (data: any) => infer TData | ||
| throwOnError?: ThrowOnError<any, infer TError, any, any> | ||
| } | ||
| ? QueryObserverResult< | ||
| unknown extends TData ? TQueryFnData : TData, | ||
| unknown extends TError ? DefaultError : TError | ||
| > | ||
| : QueryObserverResult | ||
|
|
||
| /** | ||
| * CombineResults reducer recursively maps type param to core QueryObserverResult (for combine callback) | ||
| */ | ||
| type CombineResults< | ||
| T extends Array<any>, | ||
| TResults extends Array<any> = [], | ||
| TDepth extends ReadonlyArray<number> = [], | ||
| > = TDepth['length'] extends MAXIMUM_DEPTH | ||
| ? Array<QueryObserverResult> | ||
| : T extends [] | ||
| ? [] | ||
| : T extends [infer Head] | ||
| ? [...TResults, GetQueryObserverResultForCombine<Head>] | ||
| : T extends [infer Head, ...infer Tails] | ||
| ? CombineResults< | ||
| [...Tails], | ||
| [...TResults, GetQueryObserverResultForCombine<Head>], | ||
| [...TDepth, 1] | ||
| > | ||
| : { [K in keyof T]: GetQueryObserverResultForCombine<T[K]> } | ||
|
|
||
|
Comment on lines
+143
to
+192
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opus 4.5. It seems to work as expected, and it matches the structure of the type helpers. I'm not a type-wizard to understand it completely without help. |
||
| /** | ||
| * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param | ||
| */ | ||
|
|
@@ -210,7 +262,7 @@ export interface InjectQueriesOptions< | |
| | readonly [ | ||
| ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries<T[K]> }, | ||
| ] | ||
| combine?: (result: QueriesResults<T>) => TCombinedResult | ||
| combine?: (result: CombineResults<T>) => TCombinedResult | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem of awaiting
app.whenStable()directly is that it could depend on time advancing, so the promise needs to be awaited after advancing time.