@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/useBaseQuery.ts
CHANGED
|
@@ -1,138 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
// in solid-js/web package. I'll create a GitHub issue with them to see
|
|
3
|
-
// why that happens.
|
|
4
|
-
import { notifyManager, shouldThrowError } from '@tanstack/query-core'
|
|
1
|
+
import { hydrate, noop, shouldThrowError } from '@tanstack/query-core'
|
|
5
2
|
import {
|
|
3
|
+
createMemo,
|
|
4
|
+
createProjection,
|
|
6
5
|
createRenderEffect,
|
|
7
6
|
createSignal,
|
|
8
|
-
|
|
9
|
-
isPending,
|
|
7
|
+
isPending as isValuePending,
|
|
10
8
|
onCleanup,
|
|
11
|
-
|
|
12
|
-
refresh,
|
|
9
|
+
resolve,
|
|
13
10
|
runWithOwner,
|
|
14
|
-
|
|
11
|
+
sharedConfig,
|
|
15
12
|
untrack,
|
|
16
|
-
useContext,
|
|
17
13
|
} from 'solid-js'
|
|
18
|
-
import { useQueryClient } from './QueryClientProvider'
|
|
19
|
-
import { HydrationCoordinatorContext } from './hydrationChannel'
|
|
14
|
+
import { HYDRATION_KEY_PREFIX, useQueryClient } from './QueryClientProvider'
|
|
20
15
|
import { useIsRestoring } from './isRestoring'
|
|
21
|
-
import type { UseBaseQueryOptions } from './types'
|
|
16
|
+
import type { UseBaseQueryOptions, UseBaseQueryResult } from './types'
|
|
22
17
|
import type { Accessor } from 'solid-js'
|
|
23
18
|
import type { QueryClient } from './QueryClient'
|
|
24
19
|
import type {
|
|
20
|
+
DefaultedQueryObserverOptions,
|
|
25
21
|
Query,
|
|
26
22
|
QueryKey,
|
|
27
23
|
QueryObserver,
|
|
28
24
|
QueryObserverResult,
|
|
25
|
+
QueryState,
|
|
29
26
|
} from '@tanstack/query-core'
|
|
30
27
|
|
|
31
28
|
const isServer = typeof window === 'undefined'
|
|
32
29
|
|
|
33
30
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
31
|
+
* A read of a pending-idle query (disabled, or reset with nothing in flight)
|
|
32
|
+
* has no value and nothing to wait on. Parking the reader on a promise that
|
|
33
|
+
* never resolves suspends it into the nearest `<Loading>` boundary until the
|
|
34
|
+
* query actually starts fetching (enabling it, a refetch, a cache write) —
|
|
35
|
+
* at which point the version bump re-runs the compute and the superseded
|
|
36
|
+
* in-flight is ignored by the engine.
|
|
38
37
|
*/
|
|
39
|
-
|
|
40
|
-
obj: QueryObserverResult<TData, TError>,
|
|
41
|
-
): QueryObserverResult<TData, TError> {
|
|
42
|
-
if (!isServer) return obj
|
|
43
|
-
const out: Record<string, unknown> = {}
|
|
44
|
-
for (const k of Object.keys(obj)) {
|
|
45
|
-
if (k === 'refetch' || k === 'fetchNextPage' || k === 'fetchPreviousPage') {
|
|
46
|
-
out[k] = undefined
|
|
47
|
-
} else {
|
|
48
|
-
out[k] = (obj as any)[k]
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return out as unknown as QueryObserverResult<TData, TError>
|
|
52
|
-
}
|
|
38
|
+
const NEVER: Promise<never> = new Promise(noop)
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
result: QueryObserverResult<TData, TError>,
|
|
57
|
-
reconcileOption:
|
|
58
|
-
| string
|
|
59
|
-
| false
|
|
60
|
-
| ((oldData: TData | undefined, newData: TData) => TData),
|
|
61
|
-
queryHash?: string,
|
|
62
|
-
): QueryObserverResult<TData, TError> {
|
|
63
|
-
if (typeof reconcileOption === 'function') {
|
|
64
|
-
const newData = reconcileOption(store.data, result.data as TData)
|
|
65
|
-
return { ...result, data: newData } as typeof result
|
|
66
|
-
}
|
|
40
|
+
/** The scalar half of QueryState — `data` flows through the async node, not here. */
|
|
41
|
+
type MetaState = Omit<QueryState<unknown, unknown>, 'data' | 'fetchMeta'>
|
|
67
42
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// reconcile() in Solid 2.0 mutates in place and returns void.
|
|
90
|
-
// We apply it to store.data so the store's nested signals update.
|
|
91
|
-
// On first load (store.data is undefined), there's nothing to reconcile against,
|
|
92
|
-
// so we just return the data as-is.
|
|
93
|
-
if (store.data !== undefined && data !== undefined) {
|
|
94
|
-
reconcile(data, key)(store.data)
|
|
95
|
-
// Return result with the existing store.data reference (now reconciled in place)
|
|
96
|
-
return { ...result, data: store.data } as typeof result
|
|
43
|
+
/**
|
|
44
|
+
* A cache entry in Solid's hydration registry, serialized by the
|
|
45
|
+
* provider under `sq:<queryHash>` — see `serializeCacheOnServer`.
|
|
46
|
+
*/
|
|
47
|
+
interface HydratedEntry {
|
|
48
|
+
data: unknown
|
|
49
|
+
t: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function metaFrom(state: QueryState<any, any>): MetaState {
|
|
53
|
+
return {
|
|
54
|
+
dataUpdateCount: state.dataUpdateCount,
|
|
55
|
+
dataUpdatedAt: state.dataUpdatedAt,
|
|
56
|
+
error: state.error,
|
|
57
|
+
errorUpdateCount: state.errorUpdateCount,
|
|
58
|
+
errorUpdatedAt: state.errorUpdatedAt,
|
|
59
|
+
fetchFailureCount: state.fetchFailureCount,
|
|
60
|
+
fetchFailureReason: state.fetchFailureReason,
|
|
61
|
+
fetchStatus: state.fetchStatus,
|
|
62
|
+
isInvalidated: state.isInvalidated,
|
|
63
|
+
status: state.status,
|
|
97
64
|
}
|
|
98
|
-
return { ...result, data } as typeof result
|
|
99
65
|
}
|
|
100
66
|
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
* `refetch` (and the infinite-query pagers). They come back when the
|
|
105
|
-
* observer attaches on the client.
|
|
106
|
-
*
|
|
107
|
-
* The query's dehydrated cache state does not ride the observer result —
|
|
108
|
-
* it travels through the provider-owned dehydration channel (see
|
|
109
|
-
* `hydrationChannel.ts`).
|
|
110
|
-
*/
|
|
111
|
-
const hydratableObserverResult = <
|
|
67
|
+
/** Internal seam: everything the read layer builds, for hooks that extend
|
|
68
|
+
* the base result (`useInfiniteQuery` pagers ride the same entry). */
|
|
69
|
+
interface BaseQueryLayer<
|
|
112
70
|
TQueryFnData,
|
|
113
71
|
TError,
|
|
114
72
|
TData,
|
|
73
|
+
TQueryData,
|
|
115
74
|
TQueryKey extends QueryKey,
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// During SSR, functions cannot be serialized, so we need to remove them
|
|
125
|
-
// This is safe because we will add these functions back when the query is hydrated
|
|
126
|
-
refetch: undefined,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// If the query is an infinite query, we need to remove additional properties
|
|
130
|
-
if ('fetchNextPage' in result) {
|
|
131
|
-
obj.fetchNextPage = undefined
|
|
132
|
-
obj.fetchPreviousPage = undefined
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return obj
|
|
75
|
+
> {
|
|
76
|
+
result: UseBaseQueryResult<TData, TError>
|
|
77
|
+
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
78
|
+
/** Version-tracked: reading it subscribes to this query's cache events. */
|
|
79
|
+
query: () => Query<TQueryFnData, TError, TQueryData, TQueryKey>
|
|
80
|
+
defaultedOptions: Accessor<ReturnType<QueryClient['defaultQueryOptions']>>
|
|
81
|
+
isFetching: () => boolean
|
|
82
|
+
status: () => 'pending' | 'error' | 'success'
|
|
136
83
|
}
|
|
137
84
|
|
|
138
85
|
// Base Query Function that is used to create the query.
|
|
@@ -148,359 +95,674 @@ export function useBaseQuery<
|
|
|
148
95
|
>,
|
|
149
96
|
Observer: typeof QueryObserver,
|
|
150
97
|
queryClient?: Accessor<QueryClient>,
|
|
151
|
-
) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
98
|
+
): UseBaseQueryResult<TData, TError> {
|
|
99
|
+
return useBaseQueryLayer(options, Observer, queryClient).result
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function useBaseQueryLayer<
|
|
103
|
+
TQueryFnData,
|
|
104
|
+
TError,
|
|
105
|
+
TData,
|
|
106
|
+
TQueryData,
|
|
107
|
+
TQueryKey extends QueryKey,
|
|
108
|
+
>(
|
|
109
|
+
options: Accessor<
|
|
110
|
+
UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
111
|
+
>,
|
|
112
|
+
Observer: typeof QueryObserver,
|
|
113
|
+
queryClient?: Accessor<QueryClient>,
|
|
114
|
+
): BaseQueryLayer<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
|
|
115
|
+
const client = createMemo(() => useQueryClient(queryClient?.()))
|
|
160
116
|
const isRestoring = useIsRestoring()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The most recently computed hash, updated on every options compute —
|
|
120
|
+
* including holds, where the memo's committed face still serves the old
|
|
121
|
+
* options. The cache-event filter below must match both: on a key switch
|
|
122
|
+
* the new key's fetch settles *before* the hold commits, so filtering by
|
|
123
|
+
* the committed hash alone drops that settle event and nothing downstream
|
|
124
|
+
* (data recompute, meta projection) ever learns the fetch finished.
|
|
125
|
+
*/
|
|
126
|
+
let latestHash: string | undefined
|
|
127
|
+
let latestOptions:
|
|
128
|
+
| DefaultedQueryObserverOptions<
|
|
129
|
+
TQueryFnData,
|
|
130
|
+
TError,
|
|
131
|
+
TData,
|
|
132
|
+
TQueryData,
|
|
133
|
+
TQueryKey
|
|
134
|
+
>
|
|
135
|
+
| undefined
|
|
136
|
+
const defaultedOptions = createMemo(() => {
|
|
137
|
+
const defaulted = client().defaultQueryOptions(options())
|
|
138
|
+
defaulted._optimisticResults = isRestoring() ? 'isRestoring' : 'optimistic'
|
|
173
139
|
if (isServer) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Enable prefetch during render for SSR - required for createResource to work
|
|
177
|
-
// Without this, queries wait for effects which never run on the server
|
|
178
|
-
defaultOptions.experimental_prefetchInRender = true
|
|
140
|
+
defaulted.retry = false
|
|
141
|
+
defaulted.throwOnError = true
|
|
179
142
|
}
|
|
180
|
-
|
|
143
|
+
latestHash = defaulted.queryHash
|
|
144
|
+
latestOptions = defaulted
|
|
145
|
+
return defaulted
|
|
181
146
|
})
|
|
182
147
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
148
|
+
/**
|
|
149
|
+
* The observer is a lifecycle/policy engine only. Registering it pins the
|
|
150
|
+
* query against garbage collection, engages mount-fetch policy
|
|
151
|
+
* (`shouldFetchOnMount`), schedules `refetchInterval`, and gates
|
|
152
|
+
* focus/reconnect refetches — all of which live behind observer
|
|
153
|
+
* registration in query-core. Reactivity never touches its notifications
|
|
154
|
+
* or result objects: the listener is a noop, and the reactive layer below
|
|
155
|
+
* derives everything from cache events and fetch promises.
|
|
156
|
+
*
|
|
157
|
+
* `let`, not `const`: a reactive `queryClient` accessor can swap clients
|
|
158
|
+
* mid-life, and the observer must be rebuilt against the new client (see
|
|
159
|
+
* `syncClient` below).
|
|
160
|
+
*/
|
|
161
|
+
let observer = untrack(() => new Observer(client(), defaultedOptions()))
|
|
187
162
|
|
|
188
|
-
// Apply options in an effect to avoid store writes inside the memo.
|
|
189
|
-
// setOptions triggers updateResult → notify → subscription → setState,
|
|
190
|
-
// which must run in an effect context in Solid v2.
|
|
191
163
|
createRenderEffect(
|
|
192
|
-
() =>
|
|
193
|
-
(opts) =>
|
|
194
|
-
// observer.setOptions synchronously invokes subscribers which write to
|
|
195
|
-
// the store. In Solid v2, signal/store writes inside an owned scope
|
|
196
|
-
// (like this render effect) throw. Escape the owner so subscriber
|
|
197
|
-
// writes don't trip the guard.
|
|
198
|
-
runWithOwner(null, () => observer.setOptions(opts))
|
|
199
|
-
},
|
|
164
|
+
() => defaultedOptions(),
|
|
165
|
+
(opts) => observer.setOptions(opts),
|
|
200
166
|
)
|
|
201
167
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
168
|
+
/**
|
|
169
|
+
* `sharedConfig.hydrating` is Solid's own "this component is being
|
|
170
|
+
* hydrated" flag, captured at setup. (The server build's sharedConfig
|
|
171
|
+
* has no such field — undefined there, and unused.)
|
|
172
|
+
*/
|
|
173
|
+
const hydratedMount =
|
|
174
|
+
!isServer && (sharedConfig as { hydrating?: boolean }).hydrating === true
|
|
205
175
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
176
|
+
/**
|
|
177
|
+
* One version signal per hook, bumped by cache events for this query hash.
|
|
178
|
+
* This is the entire subscription model: any state transition on the query
|
|
179
|
+
* (fetch dispatch, settle, invalidation, `setQueryData`, removal) re-runs
|
|
180
|
+
* the derived reads below, which pull fresh state/promises from the cache.
|
|
181
|
+
*
|
|
182
|
+
* `ownedWrite`: a compute that reads through `query()` can itself create
|
|
183
|
+
* the cache entry (`cache.build` → synchronous 'added' event → this
|
|
184
|
+
* write), so the first bump can land inside whatever computation pulled
|
|
185
|
+
* the entry into existence. This is channel-owned state, not that
|
|
186
|
+
* computation's own — the same pattern as the router's per-key version
|
|
187
|
+
* signals.
|
|
188
|
+
*/
|
|
189
|
+
const [version, setVersion] = createSignal(0, { ownedWrite: true })
|
|
190
|
+
/**
|
|
191
|
+
* Whether this hook's serialized data entry has been applied to the
|
|
192
|
+
* cache (or was determined not to exist). Non-hydrated mounts are born
|
|
193
|
+
* primed. Gates the pull-model fetch: hydration's takeover recompute can
|
|
194
|
+
* run against a cache the priming hasn't reached yet, and fetching there
|
|
195
|
+
* would race data the stream already delivered. Created on both sides
|
|
196
|
+
* (unread on the server) for hydration id parity.
|
|
197
|
+
*/
|
|
198
|
+
const [primed, setPrimed] = createSignal(!hydratedMount, {
|
|
199
|
+
ownedWrite: true,
|
|
200
|
+
})
|
|
201
|
+
const onCacheEvent = (event: { query: { queryHash: string } }) => {
|
|
202
|
+
// Match the committed options hash OR the latest computed one (they
|
|
203
|
+
// diverge during a hold — see `latestHash`).
|
|
204
|
+
if (
|
|
205
|
+
event.query.queryHash === untrack(defaultedOptions).queryHash ||
|
|
206
|
+
event.query.queryHash === latestHash
|
|
207
|
+
) {
|
|
208
|
+
setVersion((v) => v + 1)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
unsubscribeIfQueued()
|
|
224
|
-
} else {
|
|
225
|
-
resolve(unwrappedResult)
|
|
226
|
-
unsubscribeIfQueued()
|
|
227
|
-
}
|
|
228
|
-
})()
|
|
229
|
-
})
|
|
212
|
+
let observerSub: (() => void) | null = null
|
|
213
|
+
let cacheSub: (() => void) | null = null
|
|
214
|
+
let disposed = false
|
|
215
|
+
/** Set once the mount flow decides the observer should be live — a client
|
|
216
|
+
* swap re-attaches the rebuilt observer iff its predecessor was attached. */
|
|
217
|
+
let shouldAttach = false
|
|
218
|
+
let activeClient = untrack(client)
|
|
219
|
+
|
|
220
|
+
const attach = () => {
|
|
221
|
+
if (!disposed && !observerSub && !untrack(isRestoring)) {
|
|
222
|
+
shouldAttach = true
|
|
223
|
+
observerSub = observer.subscribe(noop)
|
|
224
|
+
}
|
|
230
225
|
}
|
|
231
226
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Idempotent client re-pointing, called from the tracked `query()` read
|
|
229
|
+
* (which every derived compute goes through) rather than a dedicated
|
|
230
|
+
* watcher node — hydration id assignment is positional, so a client-only
|
|
231
|
+
* effect here would shift every downstream id. On a swap the version
|
|
232
|
+
* subscription moves to the new cache and the observer is rebuilt against
|
|
233
|
+
* the new client, so policy fetches and cache events both follow the
|
|
234
|
+
* client the hook is actually reading.
|
|
235
|
+
*/
|
|
236
|
+
const syncClient = (c: QueryClient) => {
|
|
237
|
+
if (isServer || c === activeClient) return
|
|
238
|
+
activeClient = c
|
|
239
|
+
cacheSub?.()
|
|
240
|
+
cacheSub = c.getQueryCache().subscribe(onCacheEvent)
|
|
241
|
+
observerSub?.()
|
|
242
|
+
observerSub = null
|
|
243
|
+
observer = new Observer(c, untrack(defaultedOptions))
|
|
244
|
+
if (shouldAttach && !disposed && !untrack(isRestoring)) {
|
|
245
|
+
observerSub = observer.subscribe(noop)
|
|
236
246
|
}
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Hydration priming — content-addressed, the Solid Router `query()`
|
|
251
|
+
* pattern. The provider serialized every query this request touched
|
|
252
|
+
* into Solid's hydration registry under `sq:<queryHash>` (raw pre-select
|
|
253
|
+
* data plus `dataUpdatedAt`); here the hook looks its hash up directly
|
|
254
|
+
* and reconstructs the cache entry through query-core `hydrate()`, so
|
|
255
|
+
* staleness policy stays honest. Content addressing means coverage is
|
|
256
|
+
* the CACHE's, not the rendered tree's: prefetched-never-rendered
|
|
257
|
+
* queries transfer, shared keys prime from whichever hook consumes the
|
|
258
|
+
* entry first, and — like the router — the registry can still be
|
|
259
|
+
* consulted past hydration end, so a component mounted long after (lazy route,
|
|
260
|
+
* post-hydration navigation) still adopts the server's payload instead
|
|
261
|
+
* of refetching. Entries are consumed one-shot (deleted on load): once
|
|
262
|
+
* the cache owns the data, a stale registry payload must never shadow
|
|
263
|
+
* it.
|
|
264
|
+
*
|
|
265
|
+
* Ordering matters twice over. The observer attaches only after priming
|
|
266
|
+
* resolves (on hydrated mounts via `primeAndAttach`; on later mounts by
|
|
267
|
+
* priming HERE, before the attach render effect below even exists —
|
|
268
|
+
* its effect half runs synchronously inside an active flush, and mount
|
|
269
|
+
* policy against a still-cold cache would refetch data the registry
|
|
270
|
+
* already holds). Settled entries prime synchronously at setup;
|
|
271
|
+
* streamed entries prime when their chunk lands, per query, so early
|
|
272
|
+
* components go live while later boundaries still stream. No entry
|
|
273
|
+
* (placeholder queries serialize nothing; SSR errors adopt at their
|
|
274
|
+
* boundary) attaches immediately and lets mount policy decide.
|
|
275
|
+
*/
|
|
276
|
+
const prime = (entry?: Partial<HydratedEntry> | null) => {
|
|
277
|
+
if (entry != null && typeof entry === 'object' && (entry.t ?? 0) > 0) {
|
|
278
|
+
const c = untrack(client)
|
|
279
|
+
const opts = untrack(defaultedOptions)
|
|
280
|
+
// Escape the setup/owned scope: hydrate() synchronously notifies
|
|
281
|
+
// cache subscribers, which may write into other hooks' stores.
|
|
282
|
+
runWithOwner(null, () =>
|
|
283
|
+
hydrate(c, {
|
|
284
|
+
mutations: [],
|
|
285
|
+
queries: [
|
|
286
|
+
{
|
|
287
|
+
queryKey: opts.queryKey,
|
|
288
|
+
queryHash: opts.queryHash,
|
|
289
|
+
dehydratedAt: entry.t,
|
|
290
|
+
...(opts.meta && { meta: opts.meta }),
|
|
291
|
+
...((opts as { _type?: string })._type && {
|
|
292
|
+
queryType: (opts as { _type?: string })._type,
|
|
293
|
+
}),
|
|
294
|
+
state: {
|
|
295
|
+
data: entry.data,
|
|
296
|
+
dataUpdatedAt: entry.t,
|
|
297
|
+
dataUpdateCount: 1,
|
|
298
|
+
error: null,
|
|
299
|
+
errorUpdateCount: 0,
|
|
300
|
+
errorUpdatedAt: 0,
|
|
301
|
+
fetchFailureCount: 0,
|
|
302
|
+
fetchFailureReason: null,
|
|
303
|
+
fetchMeta: null,
|
|
304
|
+
isInvalidated: false,
|
|
305
|
+
status: 'success',
|
|
306
|
+
fetchStatus: 'idle',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
} as Parameters<typeof hydrate>[1]),
|
|
311
|
+
)
|
|
312
|
+
}
|
|
260
313
|
}
|
|
261
314
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
315
|
+
if (!isServer) {
|
|
316
|
+
cacheSub = activeClient.getQueryCache().subscribe(onCacheEvent)
|
|
317
|
+
const sc = sharedConfig as unknown as {
|
|
318
|
+
has?: (id: string) => boolean
|
|
319
|
+
load?: (id: string) => any
|
|
320
|
+
}
|
|
321
|
+
const registryKey =
|
|
322
|
+
HYDRATION_KEY_PREFIX + untrack(defaultedOptions).queryHash
|
|
323
|
+
const primeAndAttach = (entry?: Partial<HydratedEntry> | null) => {
|
|
324
|
+
prime(entry)
|
|
325
|
+
setPrimed(true)
|
|
326
|
+
attach()
|
|
327
|
+
}
|
|
328
|
+
const settle = hydratedMount ? primeAndAttach : prime
|
|
329
|
+
if (sc.has?.(registryKey)) {
|
|
330
|
+
const entry = sc.load!(registryKey)
|
|
331
|
+
delete (globalThis as unknown as { _$HY: { r: Record<string, unknown> } })
|
|
332
|
+
._$HY.r[registryKey]
|
|
333
|
+
if (entry != null && typeof entry === 'object') {
|
|
334
|
+
// Settled serialization refs are stamped `s`/`v`; still-streaming
|
|
335
|
+
// ones are plain thenable refs that land with their chunk.
|
|
336
|
+
if (entry.s === 1) settle(entry.v)
|
|
337
|
+
else if (entry.s === 2) settle()
|
|
338
|
+
else if (typeof entry.then === 'function')
|
|
339
|
+
entry.then(settle, () => settle())
|
|
340
|
+
else settle(entry)
|
|
341
|
+
} else {
|
|
342
|
+
settle(entry)
|
|
343
|
+
}
|
|
344
|
+
} else if (hydratedMount) {
|
|
345
|
+
primeAndAttach()
|
|
346
|
+
}
|
|
347
|
+
if (!hydratedMount) {
|
|
348
|
+
createRenderEffect(
|
|
349
|
+
() => isRestoring(),
|
|
350
|
+
(restoring) => {
|
|
351
|
+
if (!restoring) attach()
|
|
352
|
+
},
|
|
273
353
|
)
|
|
354
|
+
}
|
|
355
|
+
onCleanup(() => {
|
|
356
|
+
disposed = true
|
|
357
|
+
observerSub?.()
|
|
358
|
+
observerSub = null
|
|
359
|
+
cacheSub?.()
|
|
360
|
+
cacheSub = null
|
|
274
361
|
})
|
|
275
362
|
}
|
|
276
363
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
364
|
+
const query = (): Query<TQueryFnData, TError, TQueryData, TQueryKey> => {
|
|
365
|
+
version()
|
|
366
|
+
const c = client()
|
|
367
|
+
syncClient(c)
|
|
368
|
+
return c.getQueryCache().build(c, defaultedOptions() as any) as any
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const isEnabled = () => {
|
|
372
|
+
const opts = defaultedOptions()
|
|
373
|
+
const enabled =
|
|
374
|
+
typeof opts.enabled === 'function'
|
|
375
|
+
? (opts.enabled as (q: any) => boolean)(untrack(query))
|
|
376
|
+
: opts.enabled
|
|
377
|
+
return enabled !== false
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const resolvePlaceholder = (
|
|
381
|
+
opts: ReturnType<typeof defaultedOptions>,
|
|
382
|
+
): TQueryData | undefined => {
|
|
383
|
+
const placeholder = opts.placeholderData
|
|
384
|
+
if (placeholder === undefined) return undefined
|
|
385
|
+
// The function form receives previous data/query in React Query as a
|
|
386
|
+
// `keepPreviousData` vehicle. Solid 2 holds the previous committed value
|
|
387
|
+
// natively while a new promise is pending, so previous-data plumbing is
|
|
388
|
+
// unnecessary here; a function placeholder computes from nothing.
|
|
389
|
+
return typeof placeholder === 'function'
|
|
390
|
+
? (placeholder as () => TQueryData | undefined)()
|
|
391
|
+
: placeholder
|
|
392
|
+
}
|
|
282
393
|
|
|
283
394
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* a mount refetch started from inside the replay would never settle).
|
|
291
|
-
* The replay is detected without touching any internals: a real
|
|
292
|
-
* `Promise` runs its executor synchronously, the hydration mock does
|
|
293
|
-
* not, so `executorRan` stays false exactly when this compute was
|
|
294
|
-
* replayed.
|
|
295
|
-
*
|
|
296
|
-
* The subscription is coordinated with the provider's dehydration
|
|
297
|
-
* channel: it attaches once this query's entry has been primed into the
|
|
298
|
-
* cache (or once the channel completes without one), so mount semantics
|
|
299
|
-
* see the hydrated cache state — a still-fresh query does not refetch, a
|
|
300
|
-
* stale one does, and cache writes that landed earlier are reconciled at
|
|
301
|
-
* attach. The wait is per-query, not global-hydration-end: a component
|
|
302
|
-
* hydrated from an early flush goes live while later boundaries are
|
|
303
|
-
* still streaming, so it is not deaf to cache writes, is seen by
|
|
304
|
-
* invalidateQueries' active-query refetch, and cannot be gc'ed while
|
|
305
|
-
* visible. Without a provider (manual `queryClient` option) it falls
|
|
306
|
-
* back to a plain microtask.
|
|
395
|
+
* The data compute. It returns either the settled `{ value }` root or a
|
|
396
|
+
* promise of it — the engine does the rest: pending reads suspend into
|
|
397
|
+
* `<Loading>` (holding the previous committed value on refetch),
|
|
398
|
+
* rejections surface to `<Errored>`, transitions hold commits until
|
|
399
|
+
* in-flight answers land, and on the server the settled value serializes
|
|
400
|
+
* for hydration adoption on the client.
|
|
307
401
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
402
|
+
// Stable promise identity across recomputes: mid-flight version bumps
|
|
403
|
+
// (fetch dispatch, observer updates) re-run the data compute while the
|
|
404
|
+
// same underlying fetch is pending. Chaining a fresh `.then` each time
|
|
405
|
+
// would hand the engine a new pending promise per recompute — the node
|
|
406
|
+
// keeps restarting its pending tracking and the settle can be missed
|
|
407
|
+
// (observed as `isPending` probes stuck true after the fetch lands).
|
|
408
|
+
let chained: {
|
|
409
|
+
base: Promise<unknown>
|
|
410
|
+
select: unknown
|
|
411
|
+
value: Promise<{ value: TData }>
|
|
412
|
+
} | null = null
|
|
413
|
+
const chainOnce = (
|
|
414
|
+
base: Promise<any>,
|
|
415
|
+
select: unknown,
|
|
416
|
+
wrap: (d: any) => { value: TData },
|
|
417
|
+
): Promise<{ value: TData }> => {
|
|
418
|
+
if (chained?.base !== base || chained.select !== select) {
|
|
419
|
+
chained = { base, select, value: base.then(wrap) }
|
|
320
420
|
}
|
|
421
|
+
return chained.value
|
|
321
422
|
}
|
|
322
423
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// reactively gets auto-disposed in Solid v2, which would cascade-dispose
|
|
334
|
-
// the component's onCleanup and unsubscribe the observer before fetch
|
|
335
|
-
// completion.
|
|
336
|
-
const [queryResource] = createSignal<ResourceData>(() => {
|
|
337
|
-
// Read trackedDefaultedOptions to ensure this memo re-runs when options change
|
|
338
|
-
const opts = trackedDefaultedOptions()
|
|
339
|
-
// Read isRestoring unconditionally so the memo re-runs when it changes
|
|
340
|
-
const restoring = isRestoring()
|
|
424
|
+
const computeData = (
|
|
425
|
+
prev?: TData,
|
|
426
|
+
): { value: TData } | Promise<{ value: TData }> => {
|
|
427
|
+
const opts = defaultedOptions()
|
|
428
|
+
const q = query()
|
|
429
|
+
const state = q.state
|
|
430
|
+
const select = opts.select as ((d: TQueryData) => TData) | undefined
|
|
431
|
+
const wrap = (d: any): { value: TData } => ({
|
|
432
|
+
value: select ? select(d as TQueryData) : (d as TData),
|
|
433
|
+
})
|
|
341
434
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// resolves synchronously) lets Solid's `processResult` set
|
|
348
|
-
// `comp.value` directly without queueing another async settle. If we
|
|
349
|
-
// returned a new Promise on every retry, Solid's streaming Loading
|
|
350
|
-
// boundary `while (ret.p.length)` loop in `createLoadingBoundary`
|
|
351
|
-
// would never terminate, because each retry adds a new pending
|
|
352
|
-
// Promise to the boundary's tracked set even when it resolves
|
|
353
|
-
// synchronously.
|
|
354
|
-
const cached = observer.getOptimisticResult(opts)
|
|
355
|
-
if (!cached.isLoading) {
|
|
356
|
-
observerResult = cached
|
|
357
|
-
runWithOwner(null, () => {
|
|
358
|
-
setStateWithReconciliation(cached)
|
|
359
|
-
})
|
|
360
|
-
return hydratableObserverResult(
|
|
361
|
-
observer.getCurrentQuery(),
|
|
362
|
-
cached,
|
|
363
|
-
) as ResourceData
|
|
364
|
-
}
|
|
435
|
+
// Placeholder: show immediately instead of suspending while the first
|
|
436
|
+
// fetch runs. When the fetch lands the version bump swaps in real data.
|
|
437
|
+
if (state.data === undefined && state.status === 'pending') {
|
|
438
|
+
const placeholder = resolvePlaceholder(opts)
|
|
439
|
+
if (placeholder !== undefined) return wrap(placeholder)
|
|
365
440
|
}
|
|
366
441
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
442
|
+
// A fetch in flight (including paused-offline with a retryer attached):
|
|
443
|
+
// hand the engine the promise. First load suspends; refetch holds the
|
|
444
|
+
// previous committed value and marks the node pending — which is what
|
|
445
|
+
// lets a mutation's invalidations hold its settle transition until
|
|
446
|
+
// fresh data lands.
|
|
447
|
+
//
|
|
448
|
+
// Exception: a FIRST compute that already has a value to show
|
|
449
|
+
// (initialData, hydrated or shared cache data with a mount refetch in
|
|
450
|
+
// flight) returns the value — suspending would have nothing committed
|
|
451
|
+
// to hold, blanking the UI on mount. The refetch stays observable via
|
|
452
|
+
// fetchStatus; the settle lands through the next version bump.
|
|
453
|
+
if (state.fetchStatus !== 'idle') {
|
|
454
|
+
if (state.data === undefined || prev !== undefined) {
|
|
455
|
+
const promise = q.promise
|
|
456
|
+
if (promise) return chainOnce(promise, select, wrap)
|
|
377
457
|
}
|
|
378
|
-
|
|
379
|
-
// free of store writes (updateResult triggers notify → setState).
|
|
380
|
-
const currentResult = observer.getOptimisticResult(opts)
|
|
381
|
-
observerResult = currentResult
|
|
382
|
-
|
|
383
|
-
// Store writes inside a memo's owned scope throw in Solid v2.
|
|
384
|
-
// Escape the owner so setState calls are allowed.
|
|
385
|
-
runWithOwner(null, () => {
|
|
386
|
-
if (
|
|
387
|
-
currentResult.isError &&
|
|
388
|
-
!currentResult.isFetching &&
|
|
389
|
-
!restoring &&
|
|
390
|
-
shouldThrowError(opts.throwOnError, [
|
|
391
|
-
currentResult.error,
|
|
392
|
-
observer.getCurrentQuery(),
|
|
393
|
-
])
|
|
394
|
-
) {
|
|
395
|
-
setStateWithReconciliation(currentResult)
|
|
396
|
-
reject(currentResult.error)
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
setStateWithReconciliation(currentResult)
|
|
400
|
-
})
|
|
458
|
+
}
|
|
401
459
|
|
|
460
|
+
if (state.status === 'error') {
|
|
461
|
+
// No stale value to serve means the read cannot produce TData — the
|
|
462
|
+
// rejection surfaces through the graph to the nearest <Errored>.
|
|
463
|
+
// With stale data present, throw only if throwOnError opts in.
|
|
402
464
|
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
currentResult.error,
|
|
408
|
-
observer.getCurrentQuery(),
|
|
465
|
+
state.data === undefined ||
|
|
466
|
+
shouldThrowError(opts.throwOnError as any, [
|
|
467
|
+
state.error as any,
|
|
468
|
+
q as any,
|
|
409
469
|
])
|
|
410
470
|
) {
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
if (!currentResult.isLoading) {
|
|
414
|
-
resolver = null
|
|
415
|
-
return resolve(
|
|
416
|
-
hydratableObserverResult(observer.getCurrentQuery(), currentResult),
|
|
417
|
-
)
|
|
471
|
+
throw state.error
|
|
418
472
|
}
|
|
419
|
-
}
|
|
473
|
+
}
|
|
420
474
|
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
475
|
+
if (state.data !== undefined) return wrap(state.data)
|
|
476
|
+
|
|
477
|
+
// Pending-idle: nothing in flight, nothing cached. If the query is
|
|
478
|
+
// enabled, the tracked read itself starts the fetch (the router
|
|
479
|
+
// `query()` model — reads pull the async). This is not just the server
|
|
480
|
+
// path: on the client it is what revives a query whose enabling change
|
|
481
|
+
// arrives while the subtree is parked under a suspended boundary —
|
|
482
|
+
// parked boundaries hold effects, so the observer's option-driven fetch
|
|
483
|
+
// can never fire there, but computes still re-run. `q.fetch` dedupes
|
|
484
|
+
// against any fetch the observer already started.
|
|
485
|
+
if (isEnabled()) {
|
|
486
|
+
/**
|
|
487
|
+
* Never start a fetch inside the hydration window. Adoption
|
|
488
|
+
* trace-runs this compute even on a hit (solid mocks `fetch` there,
|
|
489
|
+
* but a TanStack fetch is invisible to that mock) — a query fetching
|
|
490
|
+
* here would wedge on hydration's Promise mock inside its queryFn
|
|
491
|
+
* and then swallow every later legitimate refetch through retryer
|
|
492
|
+
* dedupe. During hydration the value comes from adoption or node
|
|
493
|
+
* priming; if a fetch is genuinely needed, the post-priming observer
|
|
494
|
+
* attach issues it outside the window.
|
|
495
|
+
*/
|
|
496
|
+
if (!isServer && (sharedConfig as { hydrating?: boolean }).hydrating) {
|
|
497
|
+
return NEVER
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Same discipline until this hook's serialized node entry has primed
|
|
501
|
+
* the cache (or was found absent): the takeover recompute at
|
|
502
|
+
* hydration end can land before a streamed entry does, and fetching
|
|
503
|
+
* against the still-empty cache would race data the stream already
|
|
504
|
+
* carries. The read is tracked — priming resolves through
|
|
505
|
+
* `setPrimed`, re-running this compute, and an entry-less query
|
|
506
|
+
* falls through to a normal fetch here.
|
|
507
|
+
*/
|
|
508
|
+
if (!primed()) {
|
|
509
|
+
return NEVER
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Same discipline while a persister is restoring: the restored value
|
|
513
|
+
* is about to land in the cache, so pulling a fetch now would race
|
|
514
|
+
* it. The read is tracked — `isRestoring` flipping false re-runs
|
|
515
|
+
* this compute (and the deferred observer attach engages mount
|
|
516
|
+
* policy), so the fetch fires then if the restore left a gap.
|
|
517
|
+
*/
|
|
518
|
+
if (isRestoring()) {
|
|
519
|
+
return NEVER
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Sync observer policy before pulling. Under a suspended boundary
|
|
523
|
+
* the options render effect is deferred, so on an enabled flip or
|
|
524
|
+
* key change this compute runs first and the effect only lands
|
|
525
|
+
* after the revival fetch settles — at which point a "changed"
|
|
526
|
+
* options diff against now-stale data would issue a redundant
|
|
527
|
+
* policy refetch. Syncing here means any policy fetch starts now
|
|
528
|
+
* (and `q.fetch` dedupes into it), and the deferred effect later
|
|
529
|
+
* sees the identical options object — a no-op diff.
|
|
530
|
+
*/
|
|
531
|
+
if (!isServer) observer.setOptions(opts as any)
|
|
532
|
+
return chainOnce(q.fetch(opts as any), select, wrap)
|
|
426
533
|
}
|
|
534
|
+
return NEVER
|
|
535
|
+
}
|
|
427
536
|
|
|
428
|
-
|
|
429
|
-
|
|
537
|
+
/**
|
|
538
|
+
* THE data node: one projection, identical on both sides — the router
|
|
539
|
+
* "feed `query()` into `createProjection`" shape. The derive asks the
|
|
540
|
+
* cache for the answer (`computeData`) and the engine owns everything
|
|
541
|
+
* else: the server runs the derive, waits, and serializes the settled
|
|
542
|
+
* store under this node's id (`deferStream` holds the flush); pending
|
|
543
|
+
* reads suspend into `<Loading>`; a hydrating client adopts the
|
|
544
|
+
* serialized value and re-derives once hydration hands over; and every
|
|
545
|
+
* landing — refetch, invalidation, placeholder upgrade — auto-reconciles
|
|
546
|
+
* into the existing proxy graph (keyed by `reconcile`, default `'id'`),
|
|
547
|
+
* so deep reads are fine-grained and item identity survives fetches.
|
|
548
|
+
*
|
|
549
|
+
* The root wrapper exists because store roots must be objects —
|
|
550
|
+
* primitive data rides the `value` leaf as a plain signal read. The
|
|
551
|
+
* draft's committed `value` doubles as the previous-data signal for the
|
|
552
|
+
* first-paint exception in `computeData`.
|
|
553
|
+
*
|
|
554
|
+
* Default ('server') hydration semantics: the serialized value owns the
|
|
555
|
+
* node for the whole hydration window — server truth holds the document
|
|
556
|
+
* while the stream is open — and any cache write that lands mid-stream
|
|
557
|
+
* arms the engine's hydration-end takeover (a latched node that
|
|
558
|
+
* recomputes has diverged), committing when hydration completes.
|
|
559
|
+
* Requires solid-js > 2.0.0-rc.3 (before the takeover fix, a mid-stream
|
|
560
|
+
* divergence was lost rather than deferred).
|
|
561
|
+
*/
|
|
562
|
+
const dataStore = createProjection<{ value: TData }>(
|
|
563
|
+
(draft) => computeData(draft.value as TData | undefined),
|
|
564
|
+
{} as { value: TData },
|
|
565
|
+
{
|
|
566
|
+
key: untrack(() => options().reconcile),
|
|
567
|
+
deferStream: untrack(() => options().deferStream),
|
|
568
|
+
},
|
|
569
|
+
)
|
|
570
|
+
const data = () => dataStore.value
|
|
430
571
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Scalar result metadata.
|
|
574
|
+
*
|
|
575
|
+
* Client: a projection reconciled per-field from query state on every
|
|
576
|
+
* cache event, so each field is its own fine-grained signal — a component
|
|
577
|
+
* reading only `isFetching` re-runs on fetchStatus flips, not on every
|
|
578
|
+
* state transition.
|
|
579
|
+
*
|
|
580
|
+
* Server: boundaries guarantee that only settled state serializes, and
|
|
581
|
+
* meta must honor the same contract (solid's own `isPending` suspends
|
|
582
|
+
* rather than ever serialize `true`). A projection cannot: its compute
|
|
583
|
+
* runs once at setup and never re-pulls, freezing an in-flight snapshot
|
|
584
|
+
* ('pending'/'fetching') into HTML that the hydrating client — reading
|
|
585
|
+
* the primed, settled cache — contradicts. So on the server every meta
|
|
586
|
+
* read pulls live query state, and while the entry is unsettled (pending
|
|
587
|
+
* with a fetch in flight, or one it can start) the read ties itself to the data
|
|
588
|
+
* node, which throws the pending read and holds the boundary until
|
|
589
|
+
* settle. A disabled pending query is served as-is: 'pending' IS its
|
|
590
|
+
* settled SSR truth, and the client hydrates the identical state.
|
|
591
|
+
*/
|
|
592
|
+
const serverMeta = (): MetaState => {
|
|
593
|
+
if (query().state.status === 'pending' && isEnabled()) data()
|
|
594
|
+
return metaFrom(query().state)
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Created on BOTH sides even though the server never reads it: hydration
|
|
598
|
+
* id assignment is positional, so every reactive node a hook creates on
|
|
599
|
+
* the client must have a server counterpart (and vice versa) or every id
|
|
600
|
+
* downstream shifts and hydration key-misses the whole subtree.
|
|
601
|
+
*/
|
|
602
|
+
const metaProjection = createProjection<MetaState>(
|
|
603
|
+
(draft) => {
|
|
604
|
+
Object.assign(draft, metaFrom(query().state))
|
|
605
|
+
},
|
|
606
|
+
untrack(() => metaFrom(query().state)),
|
|
607
|
+
)
|
|
608
|
+
const meta = isServer
|
|
609
|
+
? new Proxy({} as MetaState, {
|
|
610
|
+
get: (_, key) => serverMeta()[key as keyof MetaState],
|
|
611
|
+
})
|
|
612
|
+
: metaProjection
|
|
446
613
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// Always pass through symbols (needed for store internals, iteration, etc.)
|
|
462
|
-
if (typeof prop === 'symbol') {
|
|
463
|
-
return Reflect.get(target, prop, receiver)
|
|
614
|
+
/**
|
|
615
|
+
* Server: read live counts instead of a setup snapshot — "mount" resolves
|
|
616
|
+
* post-settle there, so `isFetchedAfterMount` serializes `false`, which is
|
|
617
|
+
* exactly what the hydrating client computes (its mount snapshot equals
|
|
618
|
+
* the hydrated counts).
|
|
619
|
+
*/
|
|
620
|
+
const mountedAt = isServer
|
|
621
|
+
? {
|
|
622
|
+
get dataUpdateCount() {
|
|
623
|
+
return untrack(query).state.dataUpdateCount
|
|
624
|
+
},
|
|
625
|
+
get errorUpdateCount() {
|
|
626
|
+
return untrack(query).state.errorUpdateCount
|
|
627
|
+
},
|
|
464
628
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// and SSR HTML reflects the initial loading state.
|
|
471
|
-
//
|
|
472
|
-
// Read the value from the *resolved resource* rather than `state`. When
|
|
473
|
-
// the boundary suspends and re-renders after the query settles, the
|
|
474
|
-
// `state` store is not synced (the server subscriber resolves the
|
|
475
|
-
// resource Promise but does not write the store), so reading `state`
|
|
476
|
-
// would render stale loading values. Reading the resolved resource keeps
|
|
477
|
-
// the streamed SSR HTML consistent with the serialized resource, which
|
|
478
|
-
// is what the client hydrates against.
|
|
479
|
-
if (isServer) {
|
|
480
|
-
const resolved = queryResource()
|
|
481
|
-
if (prop in resolved) {
|
|
482
|
-
return Reflect.get(resolved, prop)
|
|
629
|
+
: untrack(() => {
|
|
630
|
+
const state = query().state
|
|
631
|
+
return {
|
|
632
|
+
dataUpdateCount: state.dataUpdateCount,
|
|
633
|
+
errorUpdateCount: state.errorUpdateCount,
|
|
483
634
|
}
|
|
484
|
-
}
|
|
635
|
+
})
|
|
485
636
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
637
|
+
const hasPlaceholder = () =>
|
|
638
|
+
meta.status === 'pending' &&
|
|
639
|
+
untrack(() => resolvePlaceholder(defaultedOptions())) !== undefined
|
|
640
|
+
const status = () => (hasPlaceholder() ? 'success' : meta.status)
|
|
641
|
+
const isPending = () => status() === 'pending'
|
|
642
|
+
/**
|
|
643
|
+
* A refetch of settled data is a transition: the state write that flips
|
|
644
|
+
* `fetchStatus` to 'fetching' is held in the same batch as the pending
|
|
645
|
+
* data node, so the committed meta channel cannot show it mid-hold — by
|
|
646
|
+
* design, held updates commit atomically. The observable channel for
|
|
647
|
+
* "a new answer is in flight" during a hold is core's pending probe.
|
|
648
|
+
* The committed `fetchStatus` covers the first load (where the probe
|
|
649
|
+
* would see an uninitialized node) and untracked/imperative reads.
|
|
650
|
+
*/
|
|
651
|
+
const isFetching = () =>
|
|
652
|
+
meta.fetchStatus === 'fetching' || isValuePending(() => data())
|
|
653
|
+
const isError = () => status() === 'error'
|
|
654
|
+
const isStale = () => {
|
|
655
|
+
const opts = defaultedOptions()
|
|
656
|
+
const q = untrack(query)
|
|
657
|
+
const staleTime =
|
|
658
|
+
typeof opts.staleTime === 'function'
|
|
659
|
+
? (opts.staleTime as (q: any) => number | 'static')(q)
|
|
660
|
+
: opts.staleTime
|
|
661
|
+
version()
|
|
662
|
+
return q.isStaleByTime(staleTime)
|
|
663
|
+
}
|
|
490
664
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
)
|
|
500
|
-
|
|
665
|
+
const result = {
|
|
666
|
+
get data() {
|
|
667
|
+
return data()
|
|
668
|
+
},
|
|
669
|
+
get error() {
|
|
670
|
+
return meta.error as TError | null
|
|
671
|
+
},
|
|
672
|
+
get status() {
|
|
673
|
+
return status()
|
|
674
|
+
},
|
|
675
|
+
get fetchStatus() {
|
|
676
|
+
return meta.fetchStatus
|
|
677
|
+
},
|
|
678
|
+
get isPending() {
|
|
679
|
+
return isPending()
|
|
680
|
+
},
|
|
681
|
+
get isSuccess() {
|
|
682
|
+
return status() === 'success'
|
|
683
|
+
},
|
|
684
|
+
get isError() {
|
|
685
|
+
return isError()
|
|
686
|
+
},
|
|
687
|
+
get isLoading() {
|
|
688
|
+
return isPending() && isFetching()
|
|
689
|
+
},
|
|
690
|
+
get isFetching() {
|
|
691
|
+
return isFetching()
|
|
692
|
+
},
|
|
693
|
+
get isRefetching() {
|
|
694
|
+
return isFetching() && !isPending()
|
|
695
|
+
},
|
|
696
|
+
get isPaused() {
|
|
697
|
+
return meta.fetchStatus === 'paused'
|
|
698
|
+
},
|
|
699
|
+
get isEnabled() {
|
|
700
|
+
return isEnabled()
|
|
701
|
+
},
|
|
702
|
+
get isLoadingError() {
|
|
703
|
+
return isError() && meta.dataUpdatedAt === 0
|
|
704
|
+
},
|
|
705
|
+
get isRefetchError() {
|
|
706
|
+
return isError() && meta.dataUpdatedAt !== 0
|
|
707
|
+
},
|
|
708
|
+
get isPlaceholderData() {
|
|
709
|
+
return hasPlaceholder()
|
|
710
|
+
},
|
|
711
|
+
get isStale() {
|
|
712
|
+
return isStale()
|
|
713
|
+
},
|
|
714
|
+
get isFetched() {
|
|
715
|
+
return meta.dataUpdateCount > 0 || meta.errorUpdateCount > 0
|
|
716
|
+
},
|
|
717
|
+
get isFetchedAfterMount() {
|
|
718
|
+
return (
|
|
719
|
+
meta.dataUpdateCount > mountedAt.dataUpdateCount ||
|
|
720
|
+
meta.errorUpdateCount > mountedAt.errorUpdateCount
|
|
721
|
+
)
|
|
722
|
+
},
|
|
723
|
+
get dataUpdatedAt() {
|
|
724
|
+
return meta.dataUpdatedAt
|
|
725
|
+
},
|
|
726
|
+
get errorUpdatedAt() {
|
|
727
|
+
return meta.errorUpdatedAt
|
|
728
|
+
},
|
|
729
|
+
get failureCount() {
|
|
730
|
+
return meta.fetchFailureCount
|
|
731
|
+
},
|
|
732
|
+
get failureReason() {
|
|
733
|
+
return meta.fetchFailureReason as TError | null
|
|
734
|
+
},
|
|
735
|
+
get errorUpdateCount() {
|
|
736
|
+
return meta.errorUpdateCount
|
|
737
|
+
},
|
|
738
|
+
get promise() {
|
|
739
|
+
return resolve(() => data())
|
|
740
|
+
},
|
|
741
|
+
refetch: ((refetchOptions) => {
|
|
742
|
+
// A held transition defers the setOptions render effect, so after a
|
|
743
|
+
// key switch the observer can still carry the previous key's options.
|
|
744
|
+
// Sync to the latest computed options first: an imperative refetch
|
|
745
|
+
// targets what the UI is currently asking for, not what last
|
|
746
|
+
// committed.
|
|
747
|
+
if (!isServer && latestOptions) {
|
|
748
|
+
observer.setOptions(latestOptions as any)
|
|
501
749
|
}
|
|
750
|
+
return observer.refetch(refetchOptions)
|
|
751
|
+
}) as QueryObserverResult<TData, TError>['refetch'],
|
|
752
|
+
}
|
|
502
753
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
754
|
+
return {
|
|
755
|
+
result: result as unknown as UseBaseQueryResult<TData, TError>,
|
|
756
|
+
observer,
|
|
757
|
+
query,
|
|
758
|
+
defaultedOptions,
|
|
759
|
+
isFetching,
|
|
760
|
+
status,
|
|
761
|
+
} as unknown as BaseQueryLayer<
|
|
762
|
+
TQueryFnData,
|
|
763
|
+
TError,
|
|
764
|
+
TData,
|
|
765
|
+
TQueryData,
|
|
766
|
+
TQueryKey
|
|
767
|
+
>
|
|
506
768
|
}
|