-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Expand file tree
/
Copy pathcreate-base-query.ts
More file actions
198 lines (175 loc) · 5.11 KB
/
Copy pathcreate-base-query.ts
File metadata and controls
198 lines (175 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import {
NgZone,
VERSION,
computed,
effect,
inject,
signal,
untracked,
} from '@angular/core'
import {
QueryClient,
notifyManager,
shouldThrowError,
} from '@tanstack/query-core'
import { signalProxy } from './signal-proxy'
import { injectIsRestoring } from './inject-is-restoring'
import { PENDING_TASKS } from './pending-tasks-compat'
import type { PendingTaskRef } from './pending-tasks-compat'
import type {
QueryKey,
QueryObserver,
QueryObserverResult,
} from '@tanstack/query-core'
import type { CreateBaseQueryOptions } from './types'
/**
* Base implementation for `injectQuery` and `injectInfiniteQuery`.
* @param optionsFn
* @param Observer
*/
export function createBaseQuery<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey extends QueryKey,
>(
optionsFn: () => CreateBaseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
Observer: typeof QueryObserver,
) {
const ngZone = inject(NgZone)
const pendingTasks = inject(PENDING_TASKS)
const queryClient = inject(QueryClient)
const isRestoring = injectIsRestoring()
/**
* Signal that has the default options from query client applied
* computed() is used so signals can be inserted into the options
* making it reactive. Wrapping options in a function ensures embedded expressions
* are preserved and can keep being applied after signal changes
*/
const defaultedOptionsSignal = computed(() => {
const defaultedOptions = queryClient.defaultQueryOptions(optionsFn())
defaultedOptions._optimisticResults = isRestoring()
? 'isRestoring'
: 'optimistic'
if (!isRestoring() && typeof defaultedOptions.queryFn === 'function') {
const originalQueryFn = defaultedOptions.queryFn
defaultedOptions.queryFn = (context) => {
const result = originalQueryFn(context)
if (result && typeof result.then === 'function') {
const pendingTaskRef = pendingTasks.add()
void result.then(
() => pendingTaskRef(),
() => pendingTaskRef(),
)
}
return result
}
}
return defaultedOptions
})
const observerSignal = (() => {
let instance: QueryObserver<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
> | null = null
return computed(() => {
return (instance ||= new Observer(queryClient, defaultedOptionsSignal()))
})
})()
const optimisticResultSignal = computed(() =>
observerSignal().getOptimisticResult(defaultedOptionsSignal()),
)
const resultFromSubscriberSignal = signal<QueryObserverResult<
TData,
TError
> | null>(null)
effect(
(onCleanup) => {
const observer = observerSignal()
const defaultedOptions = defaultedOptionsSignal()
untracked(() => {
observer.setOptions(defaultedOptions)
})
onCleanup(() => {
ngZone.run(() => resultFromSubscriberSignal.set(null))
})
},
{
// Set allowSignalWrites to support Angular < v19
// Set to undefined to avoid warning on newer versions
allowSignalWrites: VERSION.major < '19' || undefined,
},
)
effect((onCleanup) => {
// observer.trackResult is not used as this optimization is not needed for Angular
const observer = observerSignal()
let pendingTaskRef: PendingTaskRef | null = null
const updateState = (state: QueryObserverResult<TData, TError>) => {
ngZone.run(() => {
if (state.fetchStatus === 'fetching' && !pendingTaskRef) {
pendingTaskRef = pendingTasks.add()
}
if (state.fetchStatus === 'idle' && pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
if (
state.isError &&
!state.isFetching &&
shouldThrowError(observer.options.throwOnError, [
state.error,
observer.getCurrentQuery(),
])
) {
ngZone.onError.emit(state.error)
throw state.error
}
resultFromSubscriberSignal.set(state)
})
}
const unsubscribe = isRestoring()
? () => undefined
: untracked(() =>
ngZone.runOutsideAngular(() => {
const unsubscribeObserver = observer.subscribe(
notifyManager.batchCalls(updateState),
)
return unsubscribeObserver
}),
)
onCleanup(() => {
if (pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
unsubscribe()
})
})
return signalProxy(
computed(() => {
const subscriberResult = resultFromSubscriberSignal()
const optimisticResult = optimisticResultSignal()
const result = subscriberResult ?? optimisticResult
// Wrap methods to ensure observer has latest options before execution
const observer = observerSignal()
const originalRefetch = result.refetch
return {
...result,
refetch: ((...args: Parameters<typeof originalRefetch>) => {
observer.setOptions(defaultedOptionsSignal())
return originalRefetch(...args)
}) as typeof originalRefetch,
}
}),
)
}