@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/solid-query",
3
- "version": "6.0.0-rc.0",
3
+ "version": "6.0.0-rc.2",
4
4
  "description": "Primitives for managing, caching and syncing asynchronous and remote data in Solid",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -46,22 +46,23 @@
46
46
  "!src/__tests__"
47
47
  ],
48
48
  "dependencies": {
49
- "@tanstack/query-core": "5.101.0"
49
+ "@tanstack/query-core": "5.101.4"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@babel/core": "^7.28.0",
53
53
  "@babel/preset-typescript": "^7.18.6",
54
54
  "@solidjs/testing-library": "^0.8.10",
55
55
  "@solidjs/vite-plugin": "^3.0.0-next.27",
56
- "@solidjs/web": "^2.0.0-rc.0",
57
- "babel-preset-solid": "^2.0.0-rc.0",
56
+ "@solidjs/web": "^2.0.0-rc.6",
57
+ "babel-preset-solid": "^2.0.0-rc.2",
58
58
  "npm-run-all2": "^5.0.0",
59
- "solid-js": "^2.0.0-rc.0",
59
+ "solid-js": "^2.0.0-rc.6",
60
60
  "tsup-preset-solid": "^2.2.0",
61
61
  "@tanstack/query-test-utils": "0.0.0"
62
62
  },
63
63
  "peerDependencies": {
64
- "solid-js": ">=2.0.0-rc.0 <3.0.0"
64
+ "@solidjs/web": ">=2.0.0-rc.6 <3.0.0",
65
+ "solid-js": ">=2.0.0-rc.6 <3.0.0"
65
66
  },
66
67
  "scripts": {
67
68
  "clean": "premove ./build ./coverage ./dist-ts",
@@ -9,63 +9,42 @@ import type {
9
9
  QueryKey,
10
10
  } from '@tanstack/query-core'
11
11
 
12
- export interface QueryObserverOptions<
12
+ /**
13
+ * Core observer options pass through unchanged. The old adapter omitted
14
+ * `structuralSharing` and replaced it with a store-level `reconcile`
15
+ * option; with reads derived directly from cache state, core's
16
+ * cache-level structural sharing is exactly what keeps the data memo
17
+ * referentially stable, so it is exposed again and `reconcile` is gone.
18
+ */
19
+ export type QueryObserverOptions<
13
20
  TQueryFnData = unknown,
14
21
  TError = DefaultError,
15
22
  TData = TQueryFnData,
16
23
  TQueryData = TQueryFnData,
17
24
  TQueryKey extends QueryKey = QueryKey,
18
25
  TPageParam = never,
19
- > extends OmitKeyof<
20
- QueryCoreObserverOptions<
21
- TQueryFnData,
22
- TError,
23
- TData,
24
- TQueryData,
25
- TQueryKey,
26
- TPageParam
27
- >,
28
- 'structuralSharing'
29
- > {
30
- /**
31
- * Set this to a reconciliation key to enable reconciliation between query results.
32
- * Set this to `false` to disable reconciliation between query results.
33
- * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom reconciliation logic.
34
- * Defaults reconciliation to false.
35
- */
36
- reconcile?:
37
- | string
38
- | false
39
- | ((oldData: TData | undefined, newData: TData) => TData)
40
- }
26
+ > = QueryCoreObserverOptions<
27
+ TQueryFnData,
28
+ TError,
29
+ TData,
30
+ TQueryData,
31
+ TQueryKey,
32
+ TPageParam
33
+ >
41
34
 
42
- export interface InfiniteQueryObserverOptions<
35
+ export type InfiniteQueryObserverOptions<
43
36
  TQueryFnData = unknown,
44
37
  TError = DefaultError,
45
38
  TData = TQueryFnData,
46
39
  TQueryKey extends QueryKey = QueryKey,
47
40
  TPageParam = unknown,
48
- > extends OmitKeyof<
49
- QueryCoreInfiniteQueryObserverOptions<
50
- TQueryFnData,
51
- TError,
52
- TData,
53
- TQueryKey,
54
- TPageParam
55
- >,
56
- 'structuralSharing'
57
- > {
58
- /**
59
- * Set this to a reconciliation key to enable reconciliation between query results.
60
- * Set this to `false` to disable reconciliation between query results.
61
- * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom reconciliation logic.
62
- * Defaults reconciliation to false.
63
- */
64
- reconcile?:
65
- | string
66
- | false
67
- | ((oldData: TData | undefined, newData: TData) => TData)
68
- }
41
+ > = QueryCoreInfiniteQueryObserverOptions<
42
+ TQueryFnData,
43
+ TError,
44
+ TData,
45
+ TQueryKey,
46
+ TPageParam
47
+ >
69
48
 
70
49
  export interface DefaultOptions<
71
50
  TError = DefaultError,
@@ -1,21 +1,43 @@
1
- import {
2
- createContext,
3
- createRenderEffect,
4
- createSignal,
5
- onCleanup,
6
- useContext,
7
- } from 'solid-js'
8
- import {
9
- HydrationCoordinatorContext,
10
- createHydrationCoordinator,
11
- createServerDehydrationChannel,
12
- } from './hydrationChannel'
13
- import type { DehydrationChannelYield } from './hydrationChannel'
1
+ import { createContext, onCleanup, sharedConfig, useContext } from 'solid-js'
2
+ import { hydrate } from '@tanstack/query-core'
3
+ import { subscribeFlightData } from '@solidjs/web/server-functions'
4
+ import type { DehydratedState, Query } from '@tanstack/query-core'
14
5
  import type { QueryClient } from './QueryClient'
15
6
  import type { JSX } from '@solidjs/web'
16
7
 
17
8
  const isServer = typeof window === 'undefined'
18
9
 
10
+ /**
11
+ * Namespace prefix for cache entries in Solid's hydration registry — the
12
+ * registry is shared with positional node ids and other libraries'
13
+ * content-addressed keys (Solid Router uses its own cache keys), so query
14
+ * hashes get their own prefix.
15
+ */
16
+ export const HYDRATION_KEY_PREFIX = 'sq:'
17
+
18
+ /**
19
+ * The query cache's single-flight source id. Mutation responses can fold
20
+ * fresh data for multiple caches at once (Solid's multi-source
21
+ * single-flight protocol); this is the slice the query cache claims — the
22
+ * provider subscribes its consumer under it, and server collectors
23
+ * register under it to produce the data:
24
+ *
25
+ * ```ts
26
+ * import { registerFlightDataSource } from '@solidjs/web/server-functions/server'
27
+ * import { FLIGHT_DATA_SOURCE, dehydrate } from '@tanstack/solid-query'
28
+ *
29
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, async (event, outcome) => {
30
+ * // rebuild the data for outcome.targetUrl into a QueryClient, then
31
+ * return dehydrate(queryClient)
32
+ * })
33
+ * ```
34
+ *
35
+ * The slice's payload is a `DehydratedState`; the provider consumes it
36
+ * with `hydrate()`, so every mounted query on those keys updates before
37
+ * the mutation's promise resolves — no follow-up refetches.
38
+ */
39
+ export const FLIGHT_DATA_SOURCE = 'sq'
40
+
19
41
  export const QueryClientContext = createContext<(() => QueryClient) | null>(
20
42
  null,
21
43
  )
@@ -38,59 +60,115 @@ export type QueryClientProviderProps = {
38
60
  children?: JSX.Element
39
61
  }
40
62
 
63
+ /**
64
+ * Server side of hydration: serialize every query the request touches into
65
+ * Solid's hydration registry, content-addressed by query hash (the Solid
66
+ * Router `query()` pattern). Serialization happens at fetch-DISPATCH time —
67
+ * synchronously, while the request's serialization context is live — by
68
+ * handing seroval the fetch promise; it streams the `{ data, t }` payload
69
+ * whenever the fetch settles. Settled entries (initialData, setQueryData,
70
+ * loader prefetches that already landed) serialize their value directly.
71
+ *
72
+ * Content addressing is what makes this cover MORE than rendered
73
+ * components: a query prefetched in a loader and never read by any
74
+ * component still transfers, and any client hook (hydrating or mounted
75
+ * long after) finds it by hash. One entry per hash regardless of how many
76
+ * hooks read it; the payload object is the same reference the data nodes
77
+ * serialize, so seroval's cross-reference dedupe emits it once.
78
+ */
79
+ function serializeCacheOnServer(client: QueryClient): void {
80
+ const ctx = (
81
+ sharedConfig as unknown as {
82
+ context?: {
83
+ async?: boolean
84
+ noHydrate?: boolean
85
+ serialize: (key: string, value: unknown) => void
86
+ }
87
+ }
88
+ ).context
89
+ if (!ctx || !ctx.async || ctx.noHydrate) return
90
+
91
+ const cache = client.getQueryCache()
92
+ // The standard dehydrate filter gates the wire here too, so apps keep
93
+ // sensitive or oversized queries out of the HTML with the same option
94
+ // they'd pass any other transport.
95
+ const shouldDehydrateQuery =
96
+ client.getDefaultOptions().dehydrate?.shouldDehydrateQuery
97
+ const seen = new Set<string>()
98
+ const serializeQuery = (query: Query<any, any, any, any>) => {
99
+ if (seen.has(query.queryHash)) return
100
+ // Consulted per cache event until it passes, so a filter that rejects
101
+ // pending queries (e.g. the core default) still admits the settled
102
+ // value if it lands while the request's serialization context is live.
103
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return
104
+ const state = query.state
105
+ if (state.status === 'success') {
106
+ seen.add(query.queryHash)
107
+ ctx.serialize(HYDRATION_KEY_PREFIX + query.queryHash, {
108
+ data: state.data,
109
+ t: state.dataUpdatedAt,
110
+ })
111
+ } else if (state.fetchStatus !== 'idle') {
112
+ // Serialize the PROMISE now, while the context is live — the settle
113
+ // event fires from IO, where no request context exists anymore.
114
+ const promise = query.promise as Promise<unknown> | undefined
115
+ if (!promise) return
116
+ seen.add(query.queryHash)
117
+ ctx.serialize(
118
+ HYDRATION_KEY_PREFIX + query.queryHash,
119
+ promise.then(() => ({
120
+ data: query.state.data,
121
+ t: query.state.dataUpdatedAt,
122
+ })),
123
+ )
124
+ }
125
+ }
126
+
127
+ for (const query of cache.getAll()) serializeQuery(query)
128
+ onCleanup(cache.subscribe((event) => serializeQuery(event.query)))
129
+ }
130
+
131
+ /**
132
+ * Provides the QueryClient and manages its mount lifecycle. On the server
133
+ * it also registers the cache serializer above; on the client, hooks prime
134
+ * the cache from their hash-keyed registry entries themselves (see
135
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
136
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
137
+ * `DehydratedState` produced by a server collector registered under the
138
+ * same id) hydrate this client before the mutation's promise resolves.
139
+ * Subscribing is inert when no server collector exists — the server just
140
+ * folds nothing — so it is unconditional. One consumer per source: with
141
+ * nested providers, the innermost mounted one owns the slice.
142
+ */
41
143
  export const QueryClientProvider = (
42
144
  props: QueryClientProviderProps,
43
145
  ): JSX.Element => {
44
146
  props.client.mount()
45
147
  onCleanup(() => props.client.unmount())
46
-
47
- // Library-owned serialization channel for SSR dehydration.
48
- //
49
- // Server: the computation's value IS the channel's async iterable, so
50
- // Solid serializes it through its normal per-computation signal path:
51
- // the server runtime tees the iterator into the hydration serializer
52
- // (`ctx.serialize(id, tapped)` in solid-js' `processResult`) and
53
- // seroval streams each cumulative dehydrated-cache snapshot to the
54
- // client as a chunk riding the SSR stream, with the entry objects
55
- // inside deduplicated by reference against everything else in the
56
- // payload. Nothing reads the signal during SSR, so it never suspends
57
- // anything.
58
- //
59
- // Client, hydrating: Solid replays the serialized iterable through the
60
- // per-computation signal path (`hydrateSignalFromAsyncIterable`).
61
- // Yields that were still buffered when hydration began are conflated
62
- // to the LATEST yield (`normalizeIterator`) — lossless here because
63
- // every yield is a cumulative snapshot — and live yields after that
64
- // apply one at a time. Requires a solid-js build with the buffered
65
- // async-iterable replay conflation fix (> 2.0.0-beta.32): before it,
66
- // the replay dropped every buffered yield after the first, including
67
- // the terminal `done` snapshot. The render effect below hands each
68
- // signal value to the coordinator, which primes the QueryClient via
69
- // query-core hydrate() (newer-wins) and unblocks `useBaseQuery`
70
- // subscribers waiting on their query's entry.
71
- //
72
- // Client, fresh mount: the compute returns undefined and the effect
73
- // never fires.
74
- const [channelValue] = createSignal<DehydrationChannelYield | undefined>(
75
- () => (isServer ? createServerDehydrationChannel(props.client) : undefined),
76
- )
77
- const coordinator = isServer
78
- ? null
79
- : createHydrationCoordinator(() => props.client)
80
- createRenderEffect(
81
- () => (isServer ? undefined : channelValue()),
82
- (value) => {
83
- if (value && coordinator) {
84
- coordinator.applyYield(value)
85
- }
86
- },
87
- )
148
+ if (isServer) {
149
+ serializeCacheOnServer(props.client)
150
+ // Render disposal ends the request: abort what's still in flight and
151
+ // drop the cache so user-configured finite gcTime timers can't pin
152
+ // the per-request client (and whatever its queries closed over) until
153
+ // they fire. Serialized promises are already in seroval's hands, so
154
+ // clearing here can't affect the streamed payload.
155
+ onCleanup(() => {
156
+ props.client.cancelQueries().catch(() => undefined)
157
+ props.client.clear()
158
+ })
159
+ } else {
160
+ // Client-only: the server's consumer registry is module state shared
161
+ // across requests — registering there would leak between them.
162
+ onCleanup(
163
+ subscribeFlightData<DehydratedState>(FLIGHT_DATA_SOURCE, (data) => {
164
+ hydrate(props.client, data)
165
+ }),
166
+ )
167
+ }
88
168
 
89
169
  return (
90
170
  <QueryClientContext value={() => props.client}>
91
- <HydrationCoordinatorContext value={coordinator}>
92
- {props.children}
93
- </HydrationCoordinatorContext>
171
+ {props.children}
94
172
  </QueryClientContext>
95
173
  )
96
174
  }
@@ -0,0 +1,90 @@
1
+ import {
2
+ createEffect,
3
+ createOptimistic,
4
+ createSignal,
5
+ onCleanup,
6
+ sharedConfig,
7
+ untrack,
8
+ } from 'solid-js'
9
+ import type { Accessor } from 'solid-js'
10
+
11
+ const isServer = typeof window === 'undefined'
12
+
13
+ /**
14
+ * Shared reactive core for the cross-cache aggregate hooks — `useIsFetching`,
15
+ * `useIsMutating`, `useMutationState` — which all reduce to "a value derived
16
+ * from a whole cache, refreshed on its events". Two invariants live here so
17
+ * each hook doesn't restate them:
18
+ *
19
+ * Dual write. Cache events can fire inside an action transaction
20
+ * (`useMutation` rides core `action`; an onSuccess invalidation dispatches
21
+ * refetches mid-action), where a plain signal write is held until settle —
22
+ * the in-flight value would be invisible. An optimistic override alone fails
23
+ * the other way: outside a transaction, overrides drop at batch end. So a
24
+ * durable signal carries committed state and an optimistic node tracks it as
25
+ * its base; in-transaction values surface through the override, and the held
26
+ * durable write becomes the base when the settle lands.
27
+ *
28
+ * Hydration. A cross-cache aggregate is not tied to a single async source,
29
+ * so it cannot ride the boundary contract per-query meta uses (suspend until
30
+ * settled). Its contract is fixed by construction instead: a hydrating
31
+ * client can only ever observe the empty value at claim time — its own
32
+ * fetches are held inside the hydration window, channel-primed entries are
33
+ * settled, and mutations do not transfer across SSR. So the server
34
+ * serializes the empty value, and a hydrated mount latches there until its
35
+ * window closes: the subscription and first sync run from an effect half,
36
+ * deferred past hydration — the earliest globally-safe moment for a
37
+ * cross-cache read to go live (on a streamed page, an already-hydrated
38
+ * region's refetch can be in flight while this region is still claiming).
39
+ *
40
+ * Every node here is created on the server too, where it is never read or
41
+ * written: hydration id assignment is positional, so both sides must create
42
+ * the same reactive nodes or every id downstream shifts and the subtree
43
+ * key-misses.
44
+ */
45
+ export function createCacheAggregate<T>(
46
+ subscribe: (onEvent: () => void) => () => void,
47
+ read: (prev: T) => T,
48
+ empty: T,
49
+ ): Accessor<T> {
50
+ const hydratedMount =
51
+ !isServer && (sharedConfig as { hydrating?: boolean }).hydrating === true
52
+
53
+ // Cast: createSignal's value overload excludes functions (a function
54
+ // argument means a compute); aggregate values are counts and arrays.
55
+ const [durable, setDurable] = createSignal<T>(
56
+ (hydratedMount || isServer ? empty : untrack(() => read(empty))) as Exclude<
57
+ T,
58
+ Function
59
+ >,
60
+ { ownedWrite: true },
61
+ )
62
+ const [value, setValue] = createOptimistic(() => durable(), {
63
+ ownedWrite: true,
64
+ })
65
+
66
+ const sync = () => {
67
+ const next = untrack(() => read(untrack(durable)))
68
+ setDurable(() => next)
69
+ setValue(() => next)
70
+ }
71
+ let unsubscribe: (() => void) | null = null
72
+ const attach = () => {
73
+ unsubscribe ??= untrack(() => subscribe(sync))
74
+ }
75
+ onCleanup(() => unsubscribe?.())
76
+
77
+ createEffect(
78
+ () => undefined,
79
+ () => {
80
+ if (hydratedMount) {
81
+ attach()
82
+ sync()
83
+ }
84
+ },
85
+ )
86
+
87
+ if (isServer) return () => empty
88
+ if (!hydratedMount) attach()
89
+ return value
90
+ }
@@ -0,0 +1,42 @@
1
+ import { dehydrate } from '@tanstack/query-core'
2
+ import type { DehydrateOptions, DehydratedState } from '@tanstack/query-core'
3
+ import type { QueryClient } from './QueryClient'
4
+
5
+ /**
6
+ * Waits for every fetch the client has in flight to settle, then
7
+ * dehydrates. This is the extraction half of a single-flight collector:
8
+ * after route data functions run for the mutation's target URL, loaders
9
+ * commonly kick off prefetches without awaiting them — plain
10
+ * `dehydrate()` would snapshot those mid-fetch and ship nothing.
11
+ *
12
+ * ```ts
13
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, (event, outcome) =>
14
+ * loadFlightTarget({
15
+ * router,
16
+ * event,
17
+ * outcome,
18
+ * collect: () => dehydrateSettled(queryClient),
19
+ * }),
20
+ * )
21
+ * ```
22
+ *
23
+ * Settling is chased to quiescence — awaiting one batch of fetches can
24
+ * dispatch more (dependent queries keyed off a first result) — so an
25
+ * unconditionally self-refetching query would keep this pending; that's
26
+ * an app bug mirrored, not guarded.
27
+ */
28
+ export async function dehydrateSettled(
29
+ client: QueryClient,
30
+ options?: DehydrateOptions,
31
+ ): Promise<DehydratedState> {
32
+ const cache = client.getQueryCache()
33
+ for (;;) {
34
+ const pending = cache
35
+ .getAll()
36
+ .filter((query) => query.state.fetchStatus !== 'idle')
37
+ .map((query) => query.promise)
38
+ if (pending.length === 0) break
39
+ await Promise.allSettled(pending)
40
+ }
41
+ return dehydrate(client, options)
42
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,5 @@
1
1
  /* istanbul ignore file */
2
2
 
3
- import { useQuery } from './useQuery'
4
- import { useInfiniteQuery } from './useInfiniteQuery'
5
- import { useMutation } from './useMutation'
6
- import { useQueries } from './useQueries'
7
-
8
3
  // Re-export core
9
4
  export * from '@tanstack/query-core'
10
5
 
@@ -23,27 +18,11 @@ export type {
23
18
  UseBaseQueryResult,
24
19
  UseInfiniteQueryOptions,
25
20
  UseInfiniteQueryResult,
26
- UseMutateAsyncFunction,
27
21
  UseMutateFunction,
28
22
  UseMutationOptions,
29
23
  UseMutationResult,
30
24
  UseQueryOptions,
31
25
  UseQueryResult,
32
- // Aliases (create* and use* are both supported)
33
- UseBaseQueryOptions as CreateBaseQueryOptions,
34
- UseBaseQueryResult as CreateBaseQueryResult,
35
- UseInfiniteQueryOptions as CreateInfiniteQueryOptions,
36
- UseInfiniteQueryResult as CreateInfiniteQueryResult,
37
- UseMutateAsyncFunction as CreateMutateAsyncFunction,
38
- UseMutateFunction as CreateMutateFunction,
39
- UseMutationOptions as CreateMutationOptions,
40
- UseMutationResult as CreateMutationResult,
41
- UseBaseMutationResult as CreateBaseMutationResult,
42
- UseQueryOptions as CreateQueryOptions,
43
- UseQueryResult as CreateQueryResult,
44
- DefinedUseBaseQueryResult as DefinedCreateBaseQueryResult,
45
- DefinedUseInfiniteQueryResult as DefinedCreateInfiniteQueryResult,
46
- DefinedUseQueryResult as DefinedCreateQueryResult,
47
26
  } from './types'
48
27
 
49
28
  export { QueryClient } from './QueryClient'
@@ -54,22 +33,21 @@ export type {
54
33
  InfiniteQueryObserverOptions,
55
34
  } from './QueryClient'
56
35
  export { useQuery } from './useQuery'
57
- export const createQuery = useQuery
58
36
  export { queryOptions } from './queryOptions'
59
37
  export type {
60
38
  DefinedInitialDataOptions,
61
39
  UndefinedInitialDataOptions,
62
40
  } from './queryOptions'
63
41
  export {
42
+ FLIGHT_DATA_SOURCE,
64
43
  QueryClientContext,
65
44
  QueryClientProvider,
66
45
  useQueryClient,
67
46
  } from './QueryClientProvider'
68
47
  export type { QueryClientProviderProps } from './QueryClientProvider'
48
+ export { dehydrateSettled } from './dehydrateSettled'
69
49
  export { useIsFetching } from './useIsFetching'
70
- export { useIsFetching as createIsFetching } from './useIsFetching'
71
- export { useInfiniteQuery }
72
- export const createInfiniteQuery = useInfiniteQuery
50
+ export { useInfiniteQuery } from './useInfiniteQuery'
73
51
  export { infiniteQueryOptions } from './infiniteQueryOptions'
74
52
  export type {
75
53
  DefinedInitialDataInfiniteOptions,
@@ -77,11 +55,7 @@ export type {
77
55
  } from './infiniteQueryOptions'
78
56
  export { useMutation } from './useMutation'
79
57
  export { mutationOptions } from './mutationOptions'
80
- export const createMutation = useMutation
81
58
  export { useIsMutating } from './useIsMutating'
82
- export { useIsMutating as createIsMutating } from './useIsMutating'
83
59
  export { useMutationState } from './useMutationState'
84
- export { useMutationState as createMutationState } from './useMutationState'
85
60
  export { useQueries } from './useQueries'
86
- export const createQueries = useQueries
87
61
  export { useIsRestoring, IsRestoringContext } from './isRestoring'