@tanstack/solid-query 6.0.0-rc.0 → 6.0.0-rc.1
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 +150 -157
- package/build/_tsup-dts-rollup.d.ts +150 -157
- package/build/dev.cjs +718 -559
- package/build/dev.d.cts +0 -22
- package/build/dev.d.ts +0 -22
- package/build/dev.js +721 -559
- package/build/index.cjs +718 -550
- package/build/index.d.cts +0 -22
- package/build/index.d.ts +0 -22
- package/build/index.js +721 -550
- package/package.json +6 -6
- package/src/QueryClient.ts +24 -45
- package/src/QueryClientProvider.tsx +77 -58
- package/src/cacheAggregate.ts +90 -0
- package/src/index.ts +1 -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 +304 -57
- package/src/useMutationState.ts +9 -22
- package/src/useQueries.ts +61 -110
- package/src/hydrationChannel.ts +0 -260
package/src/useInfiniteQuery.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
|
2
2
|
import { createMemo } from 'solid-js'
|
|
3
|
-
import {
|
|
3
|
+
import { useBaseQueryLayer } from './useBaseQuery'
|
|
4
4
|
import type {
|
|
5
5
|
DefaultError,
|
|
6
|
+
FetchNextPageOptions,
|
|
7
|
+
FetchPreviousPageOptions,
|
|
6
8
|
InfiniteData,
|
|
9
|
+
InfiniteQueryObserverResult,
|
|
7
10
|
QueryKey,
|
|
8
11
|
QueryObserver,
|
|
9
12
|
} from '@tanstack/query-core'
|
|
@@ -19,6 +22,58 @@ import type {
|
|
|
19
22
|
UndefinedInitialDataInfiniteOptions,
|
|
20
23
|
} from './infiniteQueryOptions'
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Page-boundary checks, mirrored from query-core's `infiniteQueryBehavior`
|
|
27
|
+
* (they are module-private there — not exported through the package index).
|
|
28
|
+
* Pure functions of options + cached data; keep byte-compatible with core.
|
|
29
|
+
*/
|
|
30
|
+
interface PageParamsOptions {
|
|
31
|
+
getNextPageParam: (
|
|
32
|
+
lastPage: unknown,
|
|
33
|
+
pages: Array<unknown>,
|
|
34
|
+
lastPageParam: unknown,
|
|
35
|
+
pageParams: Array<unknown>,
|
|
36
|
+
) => unknown
|
|
37
|
+
getPreviousPageParam?: (
|
|
38
|
+
firstPage: unknown,
|
|
39
|
+
pages: Array<unknown>,
|
|
40
|
+
firstPageParam: unknown,
|
|
41
|
+
pageParams: Array<unknown>,
|
|
42
|
+
) => unknown
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasNextPage(
|
|
46
|
+
options: PageParamsOptions,
|
|
47
|
+
data?: InfiniteData<unknown>,
|
|
48
|
+
): boolean {
|
|
49
|
+
if (!data || data.pages.length === 0) return false
|
|
50
|
+
const lastIndex = data.pages.length - 1
|
|
51
|
+
return (
|
|
52
|
+
options.getNextPageParam(
|
|
53
|
+
data.pages[lastIndex],
|
|
54
|
+
data.pages,
|
|
55
|
+
data.pageParams[lastIndex],
|
|
56
|
+
data.pageParams,
|
|
57
|
+
) != null
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasPreviousPage(
|
|
62
|
+
options: PageParamsOptions,
|
|
63
|
+
data?: InfiniteData<unknown>,
|
|
64
|
+
): boolean {
|
|
65
|
+
if (!data || data.pages.length === 0 || !options.getPreviousPageParam)
|
|
66
|
+
return false
|
|
67
|
+
return (
|
|
68
|
+
options.getPreviousPageParam(
|
|
69
|
+
data.pages[0],
|
|
70
|
+
data.pages,
|
|
71
|
+
data.pageParams[0],
|
|
72
|
+
data.pageParams,
|
|
73
|
+
) != null
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
22
77
|
export function useInfiniteQuery<
|
|
23
78
|
TQueryFnData,
|
|
24
79
|
TError = DefaultError,
|
|
@@ -68,9 +123,106 @@ export function useInfiniteQuery<
|
|
|
68
123
|
>,
|
|
69
124
|
queryClient?: Accessor<QueryClient>,
|
|
70
125
|
): UseInfiniteQueryResult<TData, TError> {
|
|
71
|
-
|
|
72
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Pagers ride the same entry as every other read: the base layer owns
|
|
128
|
+
* the data node, meta projection, and lifecycle observer (an
|
|
129
|
+
* `InfiniteQueryObserver`, so mount policy and refetches use the
|
|
130
|
+
* infinite behavior). This layer adds the page surface on top.
|
|
131
|
+
*/
|
|
132
|
+
const layer = useBaseQueryLayer(
|
|
133
|
+
// `_type` is how query-core stamps a cache entry as infinite
|
|
134
|
+
// (`Query.setOptions` → `#queryType`), which makes every fetch attach
|
|
135
|
+
// `infiniteQueryBehavior`. Core only sets it inside
|
|
136
|
+
// `InfiniteQueryObserver.setOptions`, but the adapter's pull path can
|
|
137
|
+
// build and fetch the entry first (reactive key change recomputes the
|
|
138
|
+
// data memo before the deferred observer effect runs) — without the
|
|
139
|
+
// stamp that fetch would run as a plain query and corrupt the entry.
|
|
140
|
+
createMemo(() => ({ ...options(), _type: 'infinite' }) as any),
|
|
73
141
|
InfiniteQueryObserver as typeof QueryObserver,
|
|
74
142
|
queryClient,
|
|
75
|
-
)
|
|
143
|
+
)
|
|
144
|
+
const observer = layer.observer as unknown as InfiniteQueryObserver<
|
|
145
|
+
TQueryFnData,
|
|
146
|
+
TError,
|
|
147
|
+
TData,
|
|
148
|
+
TQueryKey,
|
|
149
|
+
TPageParam
|
|
150
|
+
>
|
|
151
|
+
|
|
152
|
+
/** Committed fetch direction — set when a page fetch dispatches,
|
|
153
|
+
* version-tracked through the layer's query accessor. */
|
|
154
|
+
const direction = () =>
|
|
155
|
+
(layer.query().state.fetchMeta as any)?.fetchMore?.direction
|
|
156
|
+
|
|
157
|
+
const pageOptions = () =>
|
|
158
|
+
layer.defaultedOptions() as unknown as PageParamsOptions
|
|
159
|
+
const infiniteData = () =>
|
|
160
|
+
layer.query().state.data as InfiniteData<unknown> | undefined
|
|
161
|
+
|
|
162
|
+
const isFetchingNextPage = () =>
|
|
163
|
+
layer.isFetching() && direction() === 'forward'
|
|
164
|
+
const isFetchingPreviousPage = () =>
|
|
165
|
+
layer.isFetching() && direction() === 'backward'
|
|
166
|
+
const isError = () => layer.status() === 'error'
|
|
167
|
+
|
|
168
|
+
// The base result's getters transfer as-is; the infinite surface layers
|
|
169
|
+
// over them, and isRefetching/isRefetchError narrow to exclude page
|
|
170
|
+
// fetches (matching core's InfiniteQueryObserver result shape).
|
|
171
|
+
const result = Object.defineProperties(
|
|
172
|
+
{},
|
|
173
|
+
{
|
|
174
|
+
...Object.getOwnPropertyDescriptors(layer.result),
|
|
175
|
+
fetchNextPage: {
|
|
176
|
+
value: (fetchOptions?: FetchNextPageOptions) =>
|
|
177
|
+
observer.fetchNextPage(fetchOptions),
|
|
178
|
+
enumerable: true,
|
|
179
|
+
},
|
|
180
|
+
fetchPreviousPage: {
|
|
181
|
+
value: (fetchOptions?: FetchPreviousPageOptions) =>
|
|
182
|
+
observer.fetchPreviousPage(fetchOptions),
|
|
183
|
+
enumerable: true,
|
|
184
|
+
},
|
|
185
|
+
hasNextPage: {
|
|
186
|
+
get: () => hasNextPage(pageOptions(), infiniteData()),
|
|
187
|
+
enumerable: true,
|
|
188
|
+
},
|
|
189
|
+
hasPreviousPage: {
|
|
190
|
+
get: () => hasPreviousPage(pageOptions(), infiniteData()),
|
|
191
|
+
enumerable: true,
|
|
192
|
+
},
|
|
193
|
+
isFetchingNextPage: {
|
|
194
|
+
get: isFetchingNextPage,
|
|
195
|
+
enumerable: true,
|
|
196
|
+
},
|
|
197
|
+
isFetchingPreviousPage: {
|
|
198
|
+
get: isFetchingPreviousPage,
|
|
199
|
+
enumerable: true,
|
|
200
|
+
},
|
|
201
|
+
isFetchNextPageError: {
|
|
202
|
+
get: () => isError() && direction() === 'forward',
|
|
203
|
+
enumerable: true,
|
|
204
|
+
},
|
|
205
|
+
isFetchPreviousPageError: {
|
|
206
|
+
get: () => isError() && direction() === 'backward',
|
|
207
|
+
enumerable: true,
|
|
208
|
+
},
|
|
209
|
+
isRefetching: {
|
|
210
|
+
get: () =>
|
|
211
|
+
layer.isFetching() &&
|
|
212
|
+
layer.status() !== 'pending' &&
|
|
213
|
+
!isFetchingNextPage() &&
|
|
214
|
+
!isFetchingPreviousPage(),
|
|
215
|
+
enumerable: true,
|
|
216
|
+
},
|
|
217
|
+
isRefetchError: {
|
|
218
|
+
get: () => {
|
|
219
|
+
const errored = isError() && layer.query().state.dataUpdatedAt !== 0
|
|
220
|
+
return errored && direction() === undefined
|
|
221
|
+
},
|
|
222
|
+
enumerable: true,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
) as InfiniteQueryObserverResult<TData, TError>
|
|
226
|
+
|
|
227
|
+
return result as unknown as UseInfiniteQueryResult<TData, TError>
|
|
76
228
|
}
|
package/src/useIsFetching.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMemo
|
|
1
|
+
import { createMemo } from 'solid-js'
|
|
2
|
+
import { createCacheAggregate } from './cacheAggregate'
|
|
2
3
|
import { useQueryClient } from './QueryClientProvider'
|
|
3
4
|
import type { QueryFilters } from '@tanstack/query-core'
|
|
4
5
|
import type { QueryClient } from './QueryClient'
|
|
@@ -9,20 +10,9 @@ export function useIsFetching(
|
|
|
9
10
|
queryClient?: Accessor<QueryClient>,
|
|
10
11
|
): Accessor<number> {
|
|
11
12
|
const client = createMemo(() => useQueryClient(queryClient?.()))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ ownedWrite: true },
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
const unsubscribe = untrack(() =>
|
|
20
|
-
queryCache().subscribe(() => {
|
|
21
|
-
setFetches(client().isFetching(filters?.()))
|
|
22
|
-
}),
|
|
13
|
+
return createCacheAggregate(
|
|
14
|
+
(onEvent) => client().getQueryCache().subscribe(onEvent),
|
|
15
|
+
() => client().isFetching(filters?.()),
|
|
16
|
+
0,
|
|
23
17
|
)
|
|
24
|
-
|
|
25
|
-
onCleanup(unsubscribe)
|
|
26
|
-
|
|
27
|
-
return fetches
|
|
28
18
|
}
|
package/src/useIsMutating.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMemo
|
|
1
|
+
import { createMemo } from 'solid-js'
|
|
2
|
+
import { createCacheAggregate } from './cacheAggregate'
|
|
2
3
|
import { useQueryClient } from './QueryClientProvider'
|
|
3
4
|
import type { MutationFilters } from '@tanstack/query-core'
|
|
4
5
|
import type { QueryClient } from './QueryClient'
|
|
@@ -9,20 +10,9 @@ export function useIsMutating(
|
|
|
9
10
|
queryClient?: Accessor<QueryClient>,
|
|
10
11
|
): Accessor<number> {
|
|
11
12
|
const client = createMemo(() => useQueryClient(queryClient?.()))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ ownedWrite: true },
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
const unsubscribe = untrack(() =>
|
|
20
|
-
mutationCache().subscribe((_result) => {
|
|
21
|
-
setMutations(client().isMutating(filters?.()))
|
|
22
|
-
}),
|
|
13
|
+
return createCacheAggregate(
|
|
14
|
+
(onEvent) => client().getMutationCache().subscribe(onEvent),
|
|
15
|
+
() => client().isMutating(filters?.()),
|
|
16
|
+
0,
|
|
23
17
|
)
|
|
24
|
-
|
|
25
|
-
onCleanup(unsubscribe)
|
|
26
|
-
|
|
27
|
-
return mutations
|
|
28
18
|
}
|
package/src/useMutation.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { noop, shouldThrowError } from '@tanstack/query-core'
|
|
2
2
|
import {
|
|
3
|
+
action,
|
|
3
4
|
createMemo,
|
|
5
|
+
createOptimistic,
|
|
4
6
|
createRenderEffect,
|
|
5
|
-
|
|
7
|
+
createSignal,
|
|
6
8
|
onCleanup,
|
|
7
|
-
runWithOwner,
|
|
8
9
|
untrack,
|
|
9
10
|
} from 'solid-js'
|
|
10
11
|
import { useQueryClient } from './QueryClientProvider'
|
|
11
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
DefaultError,
|
|
14
|
+
Mutation,
|
|
15
|
+
MutationFunctionContext,
|
|
16
|
+
} from '@tanstack/query-core'
|
|
12
17
|
import type { QueryClient } from './QueryClient'
|
|
13
18
|
import type {
|
|
14
19
|
UseMutateFunction,
|
|
@@ -17,7 +22,74 @@ import type {
|
|
|
17
22
|
} from './types'
|
|
18
23
|
import type { Accessor } from 'solid-js'
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
const isServer = typeof window === 'undefined'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Durable mutation state: written post-`yield` inside the action's
|
|
29
|
+
* transaction, so it commits atomically with everything else the settle
|
|
30
|
+
* carries (invalidation-triggered refetches included).
|
|
31
|
+
*/
|
|
32
|
+
interface SettledState<TData, TError, TVariables> {
|
|
33
|
+
status: 'idle' | 'success' | 'error'
|
|
34
|
+
data: TData | undefined
|
|
35
|
+
error: TError | null
|
|
36
|
+
variables: TVariables | undefined
|
|
37
|
+
submittedAt: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const IDLE: SettledState<any, any, any> = {
|
|
41
|
+
status: 'idle',
|
|
42
|
+
data: undefined,
|
|
43
|
+
error: null,
|
|
44
|
+
variables: undefined,
|
|
45
|
+
submittedAt: 0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Drive the mutation generator without transaction semantics — the server
|
|
50
|
+
* has no interactive paint to keep atomic. Yielded thenable values are
|
|
51
|
+
* awaited and their results passed back in, matching what `action` does
|
|
52
|
+
* minus the transition bookkeeping.
|
|
53
|
+
*/
|
|
54
|
+
async function drain<TReturn>(
|
|
55
|
+
iterator: AsyncGenerator<unknown, TReturn, unknown>,
|
|
56
|
+
): Promise<TReturn> {
|
|
57
|
+
let input: unknown
|
|
58
|
+
for (;;) {
|
|
59
|
+
const result = await iterator.next(input)
|
|
60
|
+
if (result.done) return result.value
|
|
61
|
+
input = result.value != null ? await result.value : undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Mutations ride core `action`: each `mutate()` call is one transaction.
|
|
67
|
+
*
|
|
68
|
+
* - Transient flight state (`isPending`, in-flight `variables`) is a
|
|
69
|
+
* `createOptimistic` overlay written at the top of the action — visible
|
|
70
|
+
* immediately, dropped automatically when the transition settles. The
|
|
71
|
+
* engine's revert-on-settle IS the submission state machine.
|
|
72
|
+
* - `options.onMutate(variables)` runs inside the same window: it is the
|
|
73
|
+
* place to apply the caller's own optimistic overlays
|
|
74
|
+
* (`createOptimistic` / `createOptimisticStore` writes). No context
|
|
75
|
+
* object, no rollback plumbing — reverting is the engine's job.
|
|
76
|
+
* - The fetch itself goes through query-core:
|
|
77
|
+
* `mutationCache.build().execute()` supplies retry/backoff, offline
|
|
78
|
+
* pausing, scoped serial execution, and cache-level lifecycle callbacks.
|
|
79
|
+
* No `MutationObserver` — the adapter reads the Mutation's state
|
|
80
|
+
* directly where flight metadata (failureCount, isPaused) is needed.
|
|
81
|
+
* - Options-level callbacks are stripped from the built mutation and
|
|
82
|
+
* re-run *post-`yield`*, inside the transaction: an
|
|
83
|
+
* `onSuccess → invalidateQueries` chain issues version bumps whose
|
|
84
|
+
* refetches hold this very transition, so mutation success and fresh
|
|
85
|
+
* query data land in one atomic paint (and a single-flight payload has
|
|
86
|
+
* already primed the cache by the time `execute` resolves).
|
|
87
|
+
* - One `mutate`, returning a safe-to-ignore promise: rejection is also
|
|
88
|
+
* routed into reactive state, and a no-op catch keeps the ignored
|
|
89
|
+
* branch from surfacing as an unhandled rejection. The
|
|
90
|
+
* `mutate`/`mutateAsync` split existed for React's callback-vs-promise
|
|
91
|
+
* error routing; it has no space here.
|
|
92
|
+
*/
|
|
21
93
|
export function useMutation<
|
|
22
94
|
TData = unknown,
|
|
23
95
|
TError = DefaultError,
|
|
@@ -29,74 +101,249 @@ export function useMutation<
|
|
|
29
101
|
): UseMutationResult<TData, TError, TVariables, TOnMutateResult> {
|
|
30
102
|
const client = createMemo(() => useQueryClient(queryClient?.()))
|
|
31
103
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
options(),
|
|
37
|
-
),
|
|
38
|
-
)
|
|
104
|
+
/** Optimistic overlay: non-null exactly while a mutation is in flight. */
|
|
105
|
+
const [flight, setFlight] = createOptimistic<{
|
|
106
|
+
variables: TVariables
|
|
107
|
+
} | null>(null)
|
|
39
108
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
109
|
+
const [settled, setSettled] = createSignal<
|
|
110
|
+
SettledState<TData, TError, TVariables>
|
|
111
|
+
>(IDLE as SettledState<TData, TError, TVariables>)
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Flight metadata (retry failures, offline pause) lives on the Mutation
|
|
115
|
+
* instance and changes during the await gap; a version bump per cache
|
|
116
|
+
* event for the active mutation keeps reads current without cloning
|
|
117
|
+
* observer results.
|
|
118
|
+
*/
|
|
119
|
+
let activeMutation: Mutation<TData, TError, TVariables> | null = null
|
|
120
|
+
const [flightVersion, setFlightVersion] = createSignal(0)
|
|
121
|
+
if (!isServer) {
|
|
122
|
+
const unsubscribe = client()
|
|
123
|
+
.getMutationCache()
|
|
124
|
+
.subscribe((event) => {
|
|
125
|
+
if ('mutation' in event && event.mutation === activeMutation) {
|
|
126
|
+
setFlightVersion((v) => v + 1)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
onCleanup(unsubscribe)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function* run(
|
|
133
|
+
variables: TVariables,
|
|
134
|
+
): AsyncGenerator<unknown, TData, unknown> {
|
|
135
|
+
const opts = untrack(options)
|
|
136
|
+
const callbackContext: MutationFunctionContext = {
|
|
137
|
+
client: untrack(client),
|
|
138
|
+
meta: opts.meta,
|
|
139
|
+
mutationKey: opts.mutationKey,
|
|
140
|
+
}
|
|
141
|
+
// Server render is pure (rc.3 deprecates server setter calls): the
|
|
142
|
+
// drain path only needs the return value/throw, so all reactive
|
|
143
|
+
// bookkeeping — flight overlay and durable settle — is client-only.
|
|
144
|
+
if (!isServer) setFlight({ variables })
|
|
145
|
+
opts.onMutate?.(variables, callbackContext)
|
|
146
|
+
|
|
147
|
+
const mutation = client()
|
|
148
|
+
.getMutationCache()
|
|
149
|
+
.build(client(), {
|
|
150
|
+
...opts,
|
|
151
|
+
// Options-level callbacks re-run below, inside the transaction —
|
|
152
|
+
// executing them here (inside the await gap) would let their
|
|
153
|
+
// cache writes and invalidations escape the atomic settle.
|
|
154
|
+
onMutate: undefined,
|
|
155
|
+
onSuccess: undefined,
|
|
156
|
+
onError: undefined,
|
|
157
|
+
onSettled: undefined,
|
|
158
|
+
}) as unknown as Mutation<TData, TError, TVariables>
|
|
159
|
+
activeMutation = mutation
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Failure settle, shared by both paths. Callback routing mirrors
|
|
163
|
+
* query-core's `Mutation.execute` (which never sees these callbacks —
|
|
164
|
+
* they are stripped from the built mutation and re-run here, inside
|
|
165
|
+
* the transaction): `onError` and an error-shaped `onSettled` run,
|
|
166
|
+
* their own failures are reported as unhandled rejections but never
|
|
167
|
+
* displace `error`, then the durable error state commits and the
|
|
168
|
+
* mutate promise rejects with `error`.
|
|
169
|
+
*/
|
|
170
|
+
// Must be an async generator: async `yield` awaits its value in place,
|
|
171
|
+
// so a rejecting callback promise throws at the yield inside the local
|
|
172
|
+
// try/catch. A sync generator delegated from an async one gets its
|
|
173
|
+
// values unwrapped by the async-from-sync adapter *outside* the inner
|
|
174
|
+
// frame — the rejection would bypass these catches and kill the run.
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- async for yield-await semantics, not await expressions
|
|
176
|
+
async function* settleFailure(
|
|
177
|
+
error: TError,
|
|
178
|
+
): AsyncGenerator<unknown, never, unknown> {
|
|
179
|
+
try {
|
|
180
|
+
if (opts.onError)
|
|
181
|
+
yield opts.onError(error, variables, undefined, callbackContext)
|
|
182
|
+
} catch (callbackError) {
|
|
183
|
+
void Promise.reject(callbackError)
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
if (opts.onSettled)
|
|
187
|
+
yield opts.onSettled(
|
|
188
|
+
undefined,
|
|
189
|
+
error,
|
|
190
|
+
variables,
|
|
191
|
+
undefined,
|
|
192
|
+
callbackContext,
|
|
193
|
+
)
|
|
194
|
+
} catch (callbackError) {
|
|
195
|
+
void Promise.reject(callbackError)
|
|
196
|
+
}
|
|
197
|
+
if (!isServer)
|
|
198
|
+
setSettled({
|
|
199
|
+
status: 'error',
|
|
200
|
+
data: undefined,
|
|
201
|
+
error,
|
|
202
|
+
variables,
|
|
203
|
+
submittedAt: mutation.state.submittedAt,
|
|
204
|
+
})
|
|
205
|
+
activeMutation = null
|
|
206
|
+
throw error
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let result: TData
|
|
210
|
+
try {
|
|
211
|
+
result = await mutation.execute(variables)
|
|
212
|
+
} catch (error) {
|
|
213
|
+
yield // re-enter the transaction before writing
|
|
214
|
+
yield* settleFailure(error as TError)
|
|
215
|
+
// Unreachable — settleFailure always throws. Explicit for TS's
|
|
216
|
+
// definite-assignment analysis of `result` (yield* delegation is
|
|
217
|
+
// not narrowed as never-returning).
|
|
218
|
+
throw error
|
|
219
|
+
}
|
|
220
|
+
yield // re-enter the transaction before writing
|
|
221
|
+
try {
|
|
222
|
+
if (opts.onSuccess)
|
|
223
|
+
yield opts.onSuccess(
|
|
224
|
+
result,
|
|
225
|
+
variables,
|
|
226
|
+
undefined as TOnMutateResult,
|
|
227
|
+
callbackContext,
|
|
228
|
+
)
|
|
229
|
+
if (opts.onSettled)
|
|
230
|
+
yield opts.onSettled(
|
|
231
|
+
result,
|
|
232
|
+
null,
|
|
233
|
+
variables,
|
|
234
|
+
undefined,
|
|
235
|
+
callbackContext,
|
|
236
|
+
)
|
|
237
|
+
} catch (callbackError) {
|
|
238
|
+
// Mirror core: a success-path callback failure fails the mutation —
|
|
239
|
+
// even though the cache entry committed success, the hook settles
|
|
240
|
+
// to error with the callback error, and awaiting mutate rejects.
|
|
241
|
+
yield* settleFailure(callbackError as TError)
|
|
242
|
+
// Unreachable — settleFailure always throws (see above).
|
|
243
|
+
throw callbackError
|
|
244
|
+
}
|
|
245
|
+
if (!isServer)
|
|
246
|
+
setSettled({
|
|
247
|
+
status: 'success',
|
|
248
|
+
data: result,
|
|
249
|
+
error: null,
|
|
250
|
+
variables,
|
|
251
|
+
submittedAt: mutation.state.submittedAt,
|
|
252
|
+
})
|
|
253
|
+
activeMutation = null
|
|
254
|
+
return result
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const invoke: (variables: TVariables) => Promise<TData> = isServer
|
|
258
|
+
? (variables) => drain(run(variables))
|
|
259
|
+
: action(run)
|
|
47
260
|
|
|
48
261
|
const mutate: UseMutateFunction<
|
|
49
262
|
TData,
|
|
50
263
|
TError,
|
|
51
264
|
TVariables,
|
|
52
265
|
TOnMutateResult
|
|
53
|
-
> = (variables
|
|
54
|
-
|
|
266
|
+
> = (variables) => {
|
|
267
|
+
const promise = invoke(variables)
|
|
268
|
+
// Errors also land in reactive state; ignoring the promise must not
|
|
269
|
+
// surface an unhandled rejection. Awaiting it still rejects.
|
|
270
|
+
promise.catch(noop)
|
|
271
|
+
return promise
|
|
55
272
|
}
|
|
56
273
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const unsubscribe = observer.subscribe((result) => {
|
|
67
|
-
// observer.mutate may be invoked synchronously from an owned scope (e.g.
|
|
68
|
-
// during render or inside an effect), which triggers this subscriber
|
|
69
|
-
// synchronously. Store writes inside an owned scope throw in Solid v2, so
|
|
70
|
-
// escape the owner to allow the setState write — matching useBaseQuery.
|
|
71
|
-
runWithOwner(null, () => {
|
|
72
|
-
setState(() => ({
|
|
73
|
-
...result,
|
|
74
|
-
mutate,
|
|
75
|
-
mutateAsync: result.mutate,
|
|
76
|
-
}))
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
onCleanup(unsubscribe)
|
|
81
|
-
|
|
82
|
-
// Use createRenderEffect to throw errors when throwOnError is set.
|
|
83
|
-
// The throw must happen in the compute function (first arg), not the effect
|
|
84
|
-
// function, so that the error goes through notifyStatus and gets wrapped as
|
|
85
|
-
// a StatusError with a source — which is required for <Errored> boundaries
|
|
86
|
-
// to capture it via CollectionQueue.notify.
|
|
274
|
+
const status = () => (flight() !== null ? 'pending' : settled().status)
|
|
275
|
+
const isPending = () => flight() !== null
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Errors surface to `<Errored>` when `throwOnError` opts in. The throw
|
|
279
|
+
* must happen in the compute half so it routes through notifyStatus and
|
|
280
|
+
* boundary accounting.
|
|
281
|
+
*/
|
|
87
282
|
createRenderEffect(
|
|
88
283
|
() => {
|
|
89
|
-
const
|
|
90
|
-
const error = state.error
|
|
284
|
+
const state = settled()
|
|
91
285
|
if (
|
|
92
|
-
|
|
93
|
-
shouldThrowError(
|
|
286
|
+
state.status === 'error' &&
|
|
287
|
+
shouldThrowError(untrack(options).throwOnError, [state.error as TError])
|
|
94
288
|
) {
|
|
95
|
-
throw error
|
|
289
|
+
throw state.error
|
|
96
290
|
}
|
|
97
291
|
},
|
|
98
292
|
() => {},
|
|
99
293
|
)
|
|
100
294
|
|
|
101
|
-
|
|
295
|
+
const result = {
|
|
296
|
+
mutate,
|
|
297
|
+
get data() {
|
|
298
|
+
return settled().data
|
|
299
|
+
},
|
|
300
|
+
get error() {
|
|
301
|
+
return flight() !== null ? null : settled().error
|
|
302
|
+
},
|
|
303
|
+
get variables() {
|
|
304
|
+
return flight()?.variables ?? settled().variables
|
|
305
|
+
},
|
|
306
|
+
get status() {
|
|
307
|
+
return status()
|
|
308
|
+
},
|
|
309
|
+
get isPending() {
|
|
310
|
+
return isPending()
|
|
311
|
+
},
|
|
312
|
+
get isIdle() {
|
|
313
|
+
return status() === 'idle'
|
|
314
|
+
},
|
|
315
|
+
get isSuccess() {
|
|
316
|
+
return status() === 'success'
|
|
317
|
+
},
|
|
318
|
+
get isError() {
|
|
319
|
+
return status() === 'error'
|
|
320
|
+
},
|
|
321
|
+
get submittedAt() {
|
|
322
|
+
return flight() !== null
|
|
323
|
+
? (flightVersion(), activeMutation?.state.submittedAt ?? 0)
|
|
324
|
+
: settled().submittedAt
|
|
325
|
+
},
|
|
326
|
+
get failureCount() {
|
|
327
|
+
flightVersion()
|
|
328
|
+
return activeMutation?.state.failureCount ?? 0
|
|
329
|
+
},
|
|
330
|
+
get failureReason() {
|
|
331
|
+
flightVersion()
|
|
332
|
+
return (activeMutation?.state.failureReason ?? null) as TError | null
|
|
333
|
+
},
|
|
334
|
+
get isPaused() {
|
|
335
|
+
flightVersion()
|
|
336
|
+
return activeMutation?.state.isPaused ?? false
|
|
337
|
+
},
|
|
338
|
+
reset: () => {
|
|
339
|
+
setSettled(IDLE as SettledState<TData, TError, TVariables>)
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return result as unknown as UseMutationResult<
|
|
344
|
+
TData,
|
|
345
|
+
TError,
|
|
346
|
+
TVariables,
|
|
347
|
+
TOnMutateResult
|
|
348
|
+
>
|
|
102
349
|
}
|
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
|
}
|