@tanstack/solid-query 6.0.0-rc.0 → 6.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/build/_tsup-dts-rollup.d.cts +203 -152
- package/build/_tsup-dts-rollup.d.ts +203 -152
- package/build/dev.cjs +755 -559
- package/build/dev.d.cts +2 -22
- package/build/dev.d.ts +2 -22
- package/build/dev.js +757 -559
- package/build/index.cjs +755 -550
- package/build/index.d.cts +2 -22
- package/build/index.d.ts +2 -22
- package/build/index.js +757 -550
- package/package.json +7 -6
- package/src/QueryClient.ts +24 -45
- package/src/QueryClientProvider.tsx +136 -58
- package/src/cacheAggregate.ts +90 -0
- package/src/dehydrateSettled.ts +42 -0
- package/src/index.ts +3 -29
- package/src/types.ts +62 -44
- package/src/useBaseQuery.ts +674 -412
- package/src/useInfiniteQuery.ts +156 -4
- package/src/useIsFetching.ts +6 -16
- package/src/useIsMutating.ts +6 -16
- package/src/useMutation.ts +317 -56
- package/src/useMutationState.ts +9 -22
- package/src/useQueries.ts +61 -110
- package/src/hydrationChannel.ts +0 -260
package/src/useMutationState.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createMemo
|
|
1
|
+
import { createMemo } from 'solid-js'
|
|
2
2
|
import { replaceEqualDeep } from '@tanstack/query-core'
|
|
3
|
+
import { createCacheAggregate } from './cacheAggregate'
|
|
3
4
|
import { useQueryClient } from './QueryClientProvider'
|
|
4
5
|
import type {
|
|
5
6
|
Mutation,
|
|
@@ -32,26 +33,12 @@ export function useMutationState<TResult = MutationState>(
|
|
|
32
33
|
queryClient?: Accessor<QueryClient>,
|
|
33
34
|
): Accessor<Array<TResult>> {
|
|
34
35
|
const client = createMemo(() => useQueryClient(queryClient?.()))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const unsubscribe = untrack(() =>
|
|
43
|
-
mutationCache().subscribe(() => {
|
|
44
|
-
setResult((prev) => {
|
|
45
|
-
const nextResult = replaceEqualDeep(
|
|
46
|
-
prev,
|
|
47
|
-
getResult(mutationCache(), options()),
|
|
48
|
-
)
|
|
49
|
-
return prev === nextResult ? prev : nextResult
|
|
50
|
-
})
|
|
51
|
-
}),
|
|
36
|
+
return createCacheAggregate<Array<TResult>>(
|
|
37
|
+
(onEvent) => client().getMutationCache().subscribe(onEvent),
|
|
38
|
+
// Structural sharing against the previous committed value keeps
|
|
39
|
+
// unchanged mutation states referentially stable across syncs.
|
|
40
|
+
(prev) =>
|
|
41
|
+
replaceEqualDeep(prev, getResult(client().getMutationCache(), options())),
|
|
42
|
+
[],
|
|
52
43
|
)
|
|
53
|
-
|
|
54
|
-
onCleanup(unsubscribe)
|
|
55
|
-
|
|
56
|
-
return result
|
|
57
44
|
}
|
package/src/useQueries.ts
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
createMemo,
|
|
5
|
-
createStore,
|
|
6
|
-
merge,
|
|
7
|
-
onCleanup,
|
|
8
|
-
reconcile,
|
|
9
|
-
runWithOwner,
|
|
10
|
-
untrack,
|
|
11
|
-
} from 'solid-js'
|
|
12
|
-
import { useQueryClient } from './QueryClientProvider'
|
|
13
|
-
import { useIsRestoring } from './isRestoring'
|
|
1
|
+
import { QueryObserver } from '@tanstack/query-core'
|
|
2
|
+
import { $TRACK, createMemo, repeat } from 'solid-js'
|
|
3
|
+
import { useBaseQuery } from './useBaseQuery'
|
|
14
4
|
import type { QueryOptions, UseQueryResult } from './types'
|
|
15
5
|
import type { Accessor } from 'solid-js'
|
|
16
6
|
import type { QueryClient } from './QueryClient'
|
|
17
7
|
import type {
|
|
18
8
|
DefaultError,
|
|
19
9
|
OmitKeyof,
|
|
20
|
-
QueriesObserverOptions,
|
|
21
10
|
QueriesPlaceholderDataFunction,
|
|
22
11
|
QueryFunction,
|
|
23
12
|
QueryKey,
|
|
24
|
-
QueryObserverOptions,
|
|
25
|
-
QueryObserverResult,
|
|
26
13
|
ThrowOnError,
|
|
27
14
|
} from '@tanstack/query-core'
|
|
28
15
|
|
|
@@ -35,14 +22,14 @@ type UseQueryOptionsForUseQueries<
|
|
|
35
22
|
TQueryKey extends QueryKey = QueryKey,
|
|
36
23
|
> = OmitKeyof<
|
|
37
24
|
QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
|
38
|
-
'placeholderData'
|
|
25
|
+
'placeholderData'
|
|
39
26
|
> & {
|
|
40
27
|
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
|
|
41
28
|
/**
|
|
42
|
-
* @deprecated The `suspense` option has been
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
29
|
+
* @deprecated The `suspense` option has been removed. Suspense is the
|
|
30
|
+
* model: each result's `data` is an async read that suspends into the
|
|
31
|
+
* nearest `<Loading>` boundary while its first fetch is in flight, same
|
|
32
|
+
* as `useQuery`. Setting `suspense` is a no-op.
|
|
46
33
|
*/
|
|
47
34
|
suspense?: boolean
|
|
48
35
|
}
|
|
@@ -140,7 +127,7 @@ type QueriesOptions<
|
|
|
140
127
|
>
|
|
141
128
|
: ReadonlyArray<unknown> extends T
|
|
142
129
|
? T
|
|
143
|
-
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/
|
|
130
|
+
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type!
|
|
144
131
|
// use this to infer the param types in the case of Array.map() argument
|
|
145
132
|
T extends Array<
|
|
146
133
|
UseQueryOptionsForUseQueries<
|
|
@@ -184,7 +171,7 @@ type QueriesResults<
|
|
|
184
171
|
|
|
185
172
|
export function useQueries<
|
|
186
173
|
T extends Array<any>,
|
|
187
|
-
TCombinedResult
|
|
174
|
+
TCombinedResult = QueriesResults<T>,
|
|
188
175
|
>(
|
|
189
176
|
queriesOptions: Accessor<{
|
|
190
177
|
queries:
|
|
@@ -194,95 +181,59 @@ export function useQueries<
|
|
|
194
181
|
}>,
|
|
195
182
|
queryClient?: Accessor<QueryClient>,
|
|
196
183
|
): TCombinedResult {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
),
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
// Get initial optimistic result
|
|
224
|
-
const [, getCombinedResult] = untrack(() =>
|
|
225
|
-
observer.getOptimisticResult(
|
|
226
|
-
defaultedQueries(),
|
|
227
|
-
(queriesOptions() as QueriesObserverOptions<TCombinedResult>).combine,
|
|
228
|
-
),
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
const initialResult = getCombinedResult()
|
|
232
|
-
|
|
233
|
-
// Store the combined result in a reactive store
|
|
234
|
-
const [state, setState] = createStore<Array<QueryObserverResult>>(
|
|
235
|
-
(Array.isArray(initialResult)
|
|
236
|
-
? initialResult
|
|
237
|
-
: [initialResult]) as Array<QueryObserverResult>,
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
// Subscribe to the observer for updates reactively.
|
|
241
|
-
// When isRestoring is true (persist client is restoring), we defer
|
|
242
|
-
// subscription until restoring completes.
|
|
243
|
-
let unsubscribe: () => void = noop
|
|
244
|
-
createEffect(
|
|
245
|
-
() => {
|
|
246
|
-
if (!isRestoring()) {
|
|
247
|
-
unsubscribe = observer.subscribe((result) => {
|
|
248
|
-
runWithOwner(null, () => {
|
|
249
|
-
setState(
|
|
250
|
-
reconcile(
|
|
251
|
-
[...result] as Array<QueryObserverResult>,
|
|
252
|
-
// Use a key function that returns undefined so reconcile
|
|
253
|
-
// uses positional matching and recursively updates nested properties
|
|
254
|
-
() => undefined,
|
|
255
|
-
),
|
|
256
|
-
)
|
|
257
|
-
})
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
() => {},
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
onCleanup(() => {
|
|
265
|
-
unsubscribe()
|
|
184
|
+
/**
|
|
185
|
+
* One read layer per position, count-keyed: each row owns a full
|
|
186
|
+
* `useBaseQuery` (observer lifecycle, version signal, async data node)
|
|
187
|
+
* whose options stay reactive through the per-index accessor — option
|
|
188
|
+
* changes flow into the existing row without tearing it down, while
|
|
189
|
+
* length changes create/dispose tail rows. Every result has identical
|
|
190
|
+
* semantics to `useQuery`: reads suspend, settled data is non-nullable.
|
|
191
|
+
*/
|
|
192
|
+
const length = createMemo(() => queriesOptions().queries.length)
|
|
193
|
+
const results = repeat(length, (index) => {
|
|
194
|
+
// When the array shrinks, cache events can reach this row's
|
|
195
|
+
// subscriptions before disposal runs; the row keeps answering with its
|
|
196
|
+
// last options rather than reading past the end of the shorter array.
|
|
197
|
+
let lastOptions: any
|
|
198
|
+
return useBaseQuery(
|
|
199
|
+
() => {
|
|
200
|
+
const next = queriesOptions().queries[index]
|
|
201
|
+
if (next !== undefined) lastOptions = next
|
|
202
|
+
return lastOptions
|
|
203
|
+
},
|
|
204
|
+
QueryObserver,
|
|
205
|
+
queryClient,
|
|
206
|
+
)
|
|
266
207
|
})
|
|
267
208
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
observer.setQueries(
|
|
273
|
-
queries,
|
|
274
|
-
queriesOptions().combine
|
|
275
|
-
? ({
|
|
276
|
-
combine: queriesOptions().combine,
|
|
277
|
-
} as QueriesObserverOptions<TCombinedResult>)
|
|
278
|
-
: undefined,
|
|
279
|
-
)
|
|
280
|
-
})
|
|
281
|
-
return queries
|
|
209
|
+
const combined = createMemo(() => {
|
|
210
|
+
const combine = queriesOptions().combine
|
|
211
|
+
const list = results() as unknown as QueriesResults<T>
|
|
212
|
+
return combine ? combine(list) : (list as unknown as TCombinedResult)
|
|
282
213
|
})
|
|
283
214
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return
|
|
215
|
+
// A stable facade over the combined result. Property reads pull through
|
|
216
|
+
// the memo so consumers track exactly what they touch; $TRACK hands list
|
|
217
|
+
// helpers (<For>, mapArray) the underlying array for diffing.
|
|
218
|
+
return new Proxy([] as unknown as TCombinedResult & object, {
|
|
219
|
+
get(_, prop) {
|
|
220
|
+
if (prop === $TRACK) return combined()
|
|
221
|
+
return Reflect.get(combined() as object, prop)
|
|
222
|
+
},
|
|
223
|
+
has(_, prop) {
|
|
224
|
+
if (prop === $TRACK) return true
|
|
225
|
+
return Reflect.has(combined() as object, prop)
|
|
226
|
+
},
|
|
227
|
+
ownKeys() {
|
|
228
|
+
return Reflect.ownKeys(combined() as object)
|
|
229
|
+
},
|
|
230
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
231
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(
|
|
232
|
+
combined() as object,
|
|
233
|
+
prop,
|
|
234
|
+
)
|
|
235
|
+
if (descriptor) descriptor.configurable = true
|
|
236
|
+
return descriptor
|
|
237
|
+
},
|
|
238
|
+
}) as TCombinedResult
|
|
288
239
|
}
|
package/src/hydrationChannel.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { hydrate } from '@tanstack/query-core'
|
|
2
|
-
import { createContext, runWithOwner } from 'solid-js'
|
|
3
|
-
import type { DehydratedState, QueryState } from '@tanstack/query-core'
|
|
4
|
-
import type { QueryClient } from './QueryClient'
|
|
5
|
-
|
|
6
|
-
type DehydratedQueryEntry = DehydratedState['queries'][number]
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A single message on the dehydration channel. `entries` is *cumulative* —
|
|
10
|
-
* every yield carries all entries settled so far. Two reasons:
|
|
11
|
-
*
|
|
12
|
-
* - It is what makes Solid's signal-path hydration replay lossless.
|
|
13
|
-
* Yields still buffered when hydration begins are conflated to the
|
|
14
|
-
* LATEST one (`normalizeIterator` drains synchronously available
|
|
15
|
-
* results, keeps the last data yield, and delivers the stream's done
|
|
16
|
-
* result on a subsequent pull), so each yield must be self-contained:
|
|
17
|
-
* the latest cumulative snapshot alone carries everything the dropped
|
|
18
|
-
* intermediates did. Requires the solid-js build with that conflation
|
|
19
|
-
* behavior (> 2.0.0-beta.32); earlier betas pinned the replay at the
|
|
20
|
-
* FIRST buffered yield, dropping every later entry and the `done`
|
|
21
|
-
* marker.
|
|
22
|
-
* - Entry objects keep their identity across yields, so seroval's
|
|
23
|
-
* cross-reference serialization emits each entry once and later yields
|
|
24
|
-
* only reference it — the cumulative shape costs bytes proportional to
|
|
25
|
-
* the number of entries, not its square.
|
|
26
|
-
*
|
|
27
|
-
* `done: true` marks the final yield. The client uses it to release
|
|
28
|
-
* subscribers still waiting for entries that will never arrive (e.g.
|
|
29
|
-
* queries that errored during SSR and were not dehydrated).
|
|
30
|
-
*/
|
|
31
|
-
export interface DehydrationChannelYield {
|
|
32
|
-
entries: Array<DehydratedQueryEntry>
|
|
33
|
-
done: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Server side of the library-owned serialization channel.
|
|
38
|
-
*
|
|
39
|
-
* Returns an AsyncIterable that yields a cumulative snapshot of the
|
|
40
|
-
* dehydrated query cache (success entries, per query-core `dehydrate()`
|
|
41
|
-
* shapes) every time a query settles during SSR. `QueryClientProvider`
|
|
42
|
-
* holds it as a signal value, so Solid serializes it through the normal
|
|
43
|
-
* per-computation path: the server runtime tees the iterator into the
|
|
44
|
-
* hydration serializer (`ctx.serialize(id, tapped)` in solid-js'
|
|
45
|
-
* `processResult`) and seroval streams each yield to the client as a
|
|
46
|
-
* script chunk riding the SSR stream.
|
|
47
|
-
*
|
|
48
|
-
* The iterable must terminate for the SSR stream to complete: the
|
|
49
|
-
* hydration serializer's `flush()` only fires its `onDone` once all
|
|
50
|
-
* pending streams have closed, and the render root is disposed *after*
|
|
51
|
-
* that, so neither `onCleanup` nor the serializer itself can close the
|
|
52
|
-
* channel. Instead the channel closes itself on cache quiescence: after
|
|
53
|
-
* every cache event (and once at creation) it schedules a timer-task
|
|
54
|
-
* check; if no query is fetching by then, no further settle can occur —
|
|
55
|
-
* suspense retry passes that start waterfall fetches are scheduled on
|
|
56
|
-
* microtasks, so they have begun before the check runs — and the channel
|
|
57
|
-
* emits its final cumulative snapshot with `done: true` and completes.
|
|
58
|
-
*
|
|
59
|
-
* Single-consumer by design: solid-js creates exactly one iterator from
|
|
60
|
-
* the value and shares it between the memo and the serializer tap.
|
|
61
|
-
*/
|
|
62
|
-
export function createServerDehydrationChannel(
|
|
63
|
-
client: QueryClient,
|
|
64
|
-
): AsyncIterable<DehydrationChannelYield> {
|
|
65
|
-
const cache = client.getQueryCache()
|
|
66
|
-
// Entry objects are reused across yields while the query's state object
|
|
67
|
-
// is unchanged, both so seroval can deduplicate them by reference and
|
|
68
|
-
// so the client can cheaply skip already-applied entries.
|
|
69
|
-
const entryCache = new Map<
|
|
70
|
-
string,
|
|
71
|
-
{ state: QueryState<unknown, unknown>; entry: DehydratedQueryEntry }
|
|
72
|
-
>()
|
|
73
|
-
|
|
74
|
-
const snapshot = (): Array<DehydratedQueryEntry> => {
|
|
75
|
-
const entries: Array<DehydratedQueryEntry> = []
|
|
76
|
-
for (const query of cache.getAll()) {
|
|
77
|
-
// Mirrors query-core's defaultShouldDehydrateQuery.
|
|
78
|
-
if (query.state.status !== 'success') continue
|
|
79
|
-
let cached = entryCache.get(query.queryHash)
|
|
80
|
-
if (!cached || cached.state !== query.state) {
|
|
81
|
-
cached = {
|
|
82
|
-
state: query.state,
|
|
83
|
-
entry: {
|
|
84
|
-
dehydratedAt: Date.now(),
|
|
85
|
-
state: query.state,
|
|
86
|
-
queryKey: query.queryKey,
|
|
87
|
-
queryHash: query.queryHash,
|
|
88
|
-
...(query.meta && { meta: query.meta }),
|
|
89
|
-
...(query.queryType && { queryType: query.queryType }),
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
entryCache.set(query.queryHash, cached)
|
|
93
|
-
}
|
|
94
|
-
entries.push(cached.entry)
|
|
95
|
-
}
|
|
96
|
-
return entries
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let closed = false
|
|
100
|
-
let pull: ((result: IteratorResult<DehydrationChannelYield>) => void) | null =
|
|
101
|
-
null
|
|
102
|
-
const buffered: Array<DehydrationChannelYield> = []
|
|
103
|
-
|
|
104
|
-
const emit = (value: DehydrationChannelYield) => {
|
|
105
|
-
if (closed) return
|
|
106
|
-
if (value.done) closed = true
|
|
107
|
-
if (pull) {
|
|
108
|
-
const resolve = pull
|
|
109
|
-
pull = null
|
|
110
|
-
resolve({ done: false, value })
|
|
111
|
-
} else {
|
|
112
|
-
buffered.push(value)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let closeTimer: ReturnType<typeof setTimeout> | null = null
|
|
117
|
-
const scheduleCloseCheck = () => {
|
|
118
|
-
if (closed || closeTimer !== null) return
|
|
119
|
-
closeTimer = setTimeout(() => {
|
|
120
|
-
closeTimer = null
|
|
121
|
-
if (closed) return
|
|
122
|
-
if (client.isFetching() === 0) {
|
|
123
|
-
unsubscribe()
|
|
124
|
-
emit({ entries: snapshot(), done: true })
|
|
125
|
-
}
|
|
126
|
-
}, 0)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const unsubscribe = cache.subscribe((event) => {
|
|
130
|
-
if (closed) return
|
|
131
|
-
if (event.type === 'updated' && event.action.type === 'success') {
|
|
132
|
-
emit({ entries: snapshot(), done: false })
|
|
133
|
-
}
|
|
134
|
-
scheduleCloseCheck()
|
|
135
|
-
})
|
|
136
|
-
scheduleCloseCheck()
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
[Symbol.asyncIterator]() {
|
|
140
|
-
return {
|
|
141
|
-
next() {
|
|
142
|
-
if (buffered.length > 0) {
|
|
143
|
-
return Promise.resolve({ done: false, value: buffered.shift()! })
|
|
144
|
-
}
|
|
145
|
-
if (closed) {
|
|
146
|
-
return Promise.resolve({
|
|
147
|
-
done: true as const,
|
|
148
|
-
value: undefined,
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
return new Promise<IteratorResult<DehydrationChannelYield>>(
|
|
152
|
-
(resolve) => {
|
|
153
|
-
pull = resolve
|
|
154
|
-
},
|
|
155
|
-
)
|
|
156
|
-
},
|
|
157
|
-
return(value?: unknown) {
|
|
158
|
-
if (!closed) {
|
|
159
|
-
closed = true
|
|
160
|
-
unsubscribe()
|
|
161
|
-
if (closeTimer !== null) {
|
|
162
|
-
clearTimeout(closeTimer)
|
|
163
|
-
closeTimer = null
|
|
164
|
-
}
|
|
165
|
-
const resolve = pull
|
|
166
|
-
pull = null
|
|
167
|
-
resolve?.({ done: true, value: undefined })
|
|
168
|
-
}
|
|
169
|
-
return Promise.resolve({
|
|
170
|
-
done: true as const,
|
|
171
|
-
value: value as DehydrationChannelYield,
|
|
172
|
-
})
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
interface HydrationCoordinator {
|
|
180
|
-
/**
|
|
181
|
-
* Prime the QueryClient from a channel yield. Entries already applied
|
|
182
|
-
* (same queryHash and dataUpdatedAt) are skipped; the rest go through
|
|
183
|
-
* query-core `hydrate()`, which keeps whichever data is newer.
|
|
184
|
-
*/
|
|
185
|
-
applyYield: (value: DehydrationChannelYield) => void
|
|
186
|
-
/**
|
|
187
|
-
* Invoke `callback` (on a microtask) once the entry for `queryHash` has
|
|
188
|
-
* been applied — or immediately-on-a-microtask if it already was, or
|
|
189
|
-
* when the channel completes without one (SSR-errored queries are not
|
|
190
|
-
* dehydrated, so their components must not wait forever).
|
|
191
|
-
*/
|
|
192
|
-
whenQueryPrimed: (queryHash: string, callback: () => void) => void
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Client side of the channel. Created by `QueryClientProvider` on the
|
|
197
|
-
* client and handed to `useBaseQuery` via context so hydrated components
|
|
198
|
-
* can attach their observers as soon as their query's entry has been
|
|
199
|
-
* primed — per query, not at global hydration end, which keeps
|
|
200
|
-
* early-hydrated components live while other boundaries still stream.
|
|
201
|
-
*/
|
|
202
|
-
export function createHydrationCoordinator(
|
|
203
|
-
client: () => QueryClient,
|
|
204
|
-
): HydrationCoordinator {
|
|
205
|
-
// queryHash -> dataUpdatedAt of the applied entry
|
|
206
|
-
const applied = new Map<string, number>()
|
|
207
|
-
const waiters = new Map<string, Array<() => void>>()
|
|
208
|
-
let channelDone = false
|
|
209
|
-
|
|
210
|
-
const fireWaiters = (queryHash: string) => {
|
|
211
|
-
const callbacks = waiters.get(queryHash)
|
|
212
|
-
if (!callbacks) return
|
|
213
|
-
waiters.delete(queryHash)
|
|
214
|
-
for (const callback of callbacks) queueMicrotask(callback)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
applyYield(value) {
|
|
219
|
-
const fresh = value.entries.filter(
|
|
220
|
-
(entry) => applied.get(entry.queryHash) !== entry.state.dataUpdatedAt,
|
|
221
|
-
)
|
|
222
|
-
if (fresh.length > 0) {
|
|
223
|
-
// hydrate() synchronously notifies cache subscribers which may
|
|
224
|
-
// write to stores/signals; escape the owned scope (this runs
|
|
225
|
-
// inside the provider's render effect) so those writes are
|
|
226
|
-
// allowed.
|
|
227
|
-
runWithOwner(null, () =>
|
|
228
|
-
hydrate(client(), { queries: fresh, mutations: [] }),
|
|
229
|
-
)
|
|
230
|
-
for (const entry of fresh) {
|
|
231
|
-
applied.set(entry.queryHash, entry.state.dataUpdatedAt)
|
|
232
|
-
fireWaiters(entry.queryHash)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (value.done && !channelDone) {
|
|
236
|
-
channelDone = true
|
|
237
|
-
const remaining = [...waiters.values()]
|
|
238
|
-
waiters.clear()
|
|
239
|
-
for (const callbacks of remaining) {
|
|
240
|
-
for (const callback of callbacks) queueMicrotask(callback)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
whenQueryPrimed(queryHash, callback) {
|
|
245
|
-
if (channelDone || applied.has(queryHash)) {
|
|
246
|
-
queueMicrotask(callback)
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
let list = waiters.get(queryHash)
|
|
250
|
-
if (!list) {
|
|
251
|
-
list = []
|
|
252
|
-
waiters.set(queryHash, list)
|
|
253
|
-
}
|
|
254
|
-
list.push(callback)
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export const HydrationCoordinatorContext =
|
|
260
|
-
createContext<HydrationCoordinator | null>(null)
|