|
| 1 | +import { tryResolveSync } from './thenable' |
1 | 2 | import type {
|
2 | 3 | DefaultError,
|
3 | 4 | MutationKey,
|
@@ -46,6 +47,10 @@ interface DehydratedQuery {
|
46 | 47 | state: QueryState
|
47 | 48 | promise?: Promise<unknown>
|
48 | 49 | meta?: QueryMeta
|
| 50 | + // This is only optional because older versions of Query might have dehydrated |
| 51 | + // without it which we need to handle for backwards compatibility. |
| 52 | + // This should be changed to required in the future. |
| 53 | + dehydratedAt?: number |
49 | 54 | }
|
50 | 55 |
|
51 | 56 | export interface DehydratedState {
|
@@ -74,6 +79,7 @@ function dehydrateQuery(
|
74 | 79 | shouldRedactErrors: (error: unknown) => boolean,
|
75 | 80 | ): DehydratedQuery {
|
76 | 81 | return {
|
| 82 | + dehydratedAt: Date.now(), |
77 | 83 | state: {
|
78 | 84 | ...query.state,
|
79 | 85 | ...(query.state.data !== undefined && {
|
@@ -189,52 +195,73 @@ export function hydrate(
|
189 | 195 | )
|
190 | 196 | })
|
191 | 197 |
|
192 |
| - queries.forEach(({ queryKey, state, queryHash, meta, promise }) => { |
193 |
| - let query = queryCache.get(queryHash) |
194 |
| - |
195 |
| - const data = |
196 |
| - state.data === undefined ? state.data : deserializeData(state.data) |
197 |
| - |
198 |
| - // Do not hydrate if an existing query exists with newer data |
199 |
| - if (query) { |
200 |
| - if (query.state.dataUpdatedAt < state.dataUpdatedAt) { |
201 |
| - // omit fetchStatus from dehydrated state |
202 |
| - // so that query stays in its current fetchStatus |
203 |
| - const { fetchStatus: _ignored, ...serializedState } = state |
204 |
| - query.setState({ |
205 |
| - ...serializedState, |
206 |
| - data, |
| 198 | + queries.forEach( |
| 199 | + ({ queryKey, state, queryHash, meta, promise, dehydratedAt }) => { |
| 200 | + const syncData = promise ? tryResolveSync(promise) : undefined |
| 201 | + const rawData = state.data === undefined ? syncData?.data : state.data |
| 202 | + const data = rawData === undefined ? rawData : deserializeData(rawData) |
| 203 | + |
| 204 | + let query = queryCache.get(queryHash) |
| 205 | + const existingQueryIsPending = query?.state.status === 'pending' |
| 206 | + |
| 207 | + // Do not hydrate if an existing query exists with newer data |
| 208 | + if (query) { |
| 209 | + const hasNewerSyncData = |
| 210 | + syncData && |
| 211 | + // We only need this undefined check to handle older dehydration |
| 212 | + // payloads that might not have dehydratedAt |
| 213 | + dehydratedAt !== undefined && |
| 214 | + dehydratedAt > query.state.dataUpdatedAt |
| 215 | + if ( |
| 216 | + state.dataUpdatedAt > query.state.dataUpdatedAt || |
| 217 | + hasNewerSyncData |
| 218 | + ) { |
| 219 | + // omit fetchStatus from dehydrated state |
| 220 | + // so that query stays in its current fetchStatus |
| 221 | + const { fetchStatus: _ignored, ...serializedState } = state |
| 222 | + query.setState({ |
| 223 | + ...serializedState, |
| 224 | + data, |
| 225 | + }) |
| 226 | + } |
| 227 | + } else { |
| 228 | + // Restore query |
| 229 | + query = queryCache.build( |
| 230 | + client, |
| 231 | + { |
| 232 | + ...client.getDefaultOptions().hydrate?.queries, |
| 233 | + ...options?.defaultOptions?.queries, |
| 234 | + queryKey, |
| 235 | + queryHash, |
| 236 | + meta, |
| 237 | + }, |
| 238 | + // Reset fetch status to idle to avoid |
| 239 | + // query being stuck in fetching state upon hydration |
| 240 | + { |
| 241 | + ...state, |
| 242 | + data, |
| 243 | + fetchStatus: 'idle', |
| 244 | + status: data !== undefined ? 'success' : state.status, |
| 245 | + }, |
| 246 | + ) |
| 247 | + } |
| 248 | + |
| 249 | + if ( |
| 250 | + promise && |
| 251 | + !existingQueryIsPending && |
| 252 | + // Only hydrate promise if no synchronous data was available to hydrate |
| 253 | + !data && |
| 254 | + // Only hydrate if dehydration is newer than any existing data |
| 255 | + dehydratedAt !== undefined && |
| 256 | + dehydratedAt > query.state.dataUpdatedAt |
| 257 | + ) { |
| 258 | + // this doesn't actually fetch - it just creates a retryer |
| 259 | + // which will re-use the passed `initialPromise` |
| 260 | + void query.fetch(undefined, { |
| 261 | + // RSC transformed promises are not thenable |
| 262 | + initialPromise: Promise.resolve(promise).then(deserializeData), |
207 | 263 | })
|
208 | 264 | }
|
209 |
| - } else { |
210 |
| - // Restore query |
211 |
| - query = queryCache.build( |
212 |
| - client, |
213 |
| - { |
214 |
| - ...client.getDefaultOptions().hydrate?.queries, |
215 |
| - ...options?.defaultOptions?.queries, |
216 |
| - queryKey, |
217 |
| - queryHash, |
218 |
| - meta, |
219 |
| - }, |
220 |
| - // Reset fetch status to idle to avoid |
221 |
| - // query being stuck in fetching state upon hydration |
222 |
| - { |
223 |
| - ...state, |
224 |
| - data, |
225 |
| - fetchStatus: 'idle', |
226 |
| - }, |
227 |
| - ) |
228 |
| - } |
229 |
| - |
230 |
| - if (promise) { |
231 |
| - // Note: `Promise.resolve` required cause |
232 |
| - // RSC transformed promises are not thenable |
233 |
| - const initialPromise = Promise.resolve(promise).then(deserializeData) |
234 |
| - |
235 |
| - // this doesn't actually fetch - it just creates a retryer |
236 |
| - // which will re-use the passed `initialPromise` |
237 |
| - void query.fetch(undefined, { initialPromise }) |
238 |
| - } |
239 |
| - }) |
| 265 | + }, |
| 266 | + ) |
240 | 267 | }
|
0 commit comments