@effector-tanstack-query/core 0.1.0
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 +40 -0
- package/dist/createBaseQuery.cjs +208 -0
- package/dist/createBaseQuery.cjs.map +1 -0
- package/dist/createBaseQuery.d.cts +114 -0
- package/dist/createBaseQuery.d.ts +114 -0
- package/dist/createBaseQuery.js +204 -0
- package/dist/createBaseQuery.js.map +1 -0
- package/dist/createInfiniteQuery.cjs +193 -0
- package/dist/createInfiniteQuery.cjs.map +1 -0
- package/dist/createInfiniteQuery.d.cts +8 -0
- package/dist/createInfiniteQuery.d.ts +8 -0
- package/dist/createInfiniteQuery.js +191 -0
- package/dist/createInfiniteQuery.js.map +1 -0
- package/dist/createInvalidate.cjs +37 -0
- package/dist/createInvalidate.cjs.map +1 -0
- package/dist/createInvalidate.d.cts +50 -0
- package/dist/createInvalidate.d.ts +50 -0
- package/dist/createInvalidate.js +35 -0
- package/dist/createInvalidate.js.map +1 -0
- package/dist/createMutation.cjs +177 -0
- package/dist/createMutation.cjs.map +1 -0
- package/dist/createMutation.d.cts +7 -0
- package/dist/createMutation.d.ts +7 -0
- package/dist/createMutation.js +175 -0
- package/dist/createMutation.js.map +1 -0
- package/dist/createQuery.cjs +98 -0
- package/dist/createQuery.cjs.map +1 -0
- package/dist/createQuery.d.cts +8 -0
- package/dist/createQuery.d.ts +8 -0
- package/dist/createQuery.js +96 -0
- package/dist/createQuery.js.map +1 -0
- package/dist/index.cjs +36 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/queryClient.cjs +20 -0
- package/dist/queryClient.cjs.map +1 -0
- package/dist/queryClient.d.cts +15 -0
- package/dist/queryClient.d.ts +15 -0
- package/dist/queryClient.js +17 -0
- package/dist/queryClient.js.map +1 -0
- package/dist/resolve.cjs +37 -0
- package/dist/resolve.cjs.map +1 -0
- package/dist/resolve.d.cts +17 -0
- package/dist/resolve.d.ts +17 -0
- package/dist/resolve.js +33 -0
- package/dist/resolve.js.map +1 -0
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +209 -0
- package/dist/types.d.ts +209 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
- package/src/createBaseQuery.ts +428 -0
- package/src/createInfiniteQuery.ts +291 -0
- package/src/createInvalidate.ts +104 -0
- package/src/createMutation.ts +271 -0
- package/src/createQuery.ts +155 -0
- package/src/index.ts +17 -0
- package/src/queryClient.ts +23 -0
- package/src/resolve.ts +50 -0
- package/src/types.ts +270 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { attach, createEvent, createStore, sample, scopeBind } from 'effector'
|
|
2
|
+
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
|
3
|
+
import type {
|
|
4
|
+
InfiniteData,
|
|
5
|
+
QueryClient,
|
|
6
|
+
QueryKey,
|
|
7
|
+
} from '@tanstack/query-core'
|
|
8
|
+
import { createBaseQuery, sidConfig, warnMissingName } from './createBaseQuery'
|
|
9
|
+
import { resolveReactiveRefetchInterval } from './resolve'
|
|
10
|
+
import type {
|
|
11
|
+
CreateInfiniteQueryOptions,
|
|
12
|
+
InfiniteQueryResult,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
type Observer<TQueryFnData, TError, TData, TPageParam> = InfiniteQueryObserver<
|
|
16
|
+
TQueryFnData,
|
|
17
|
+
TError,
|
|
18
|
+
TData,
|
|
19
|
+
QueryKey,
|
|
20
|
+
TPageParam
|
|
21
|
+
>
|
|
22
|
+
|
|
23
|
+
type ObserverResult<TQueryFnData, TError, TData, TPageParam> = ReturnType<
|
|
24
|
+
Observer<TQueryFnData, TError, TData, TPageParam>['getCurrentResult']
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
export function createInfiniteQuery<
|
|
28
|
+
TQueryFnData = unknown,
|
|
29
|
+
TError = Error,
|
|
30
|
+
TPageParam = unknown,
|
|
31
|
+
TData = InfiniteData<TQueryFnData, TPageParam>,
|
|
32
|
+
>(
|
|
33
|
+
options: CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
34
|
+
): InfiniteQueryResult<TData, TError, TPageParam>
|
|
35
|
+
export function createInfiniteQuery<
|
|
36
|
+
TQueryFnData = unknown,
|
|
37
|
+
TError = Error,
|
|
38
|
+
TPageParam = unknown,
|
|
39
|
+
TData = InfiniteData<TQueryFnData, TPageParam>,
|
|
40
|
+
>(
|
|
41
|
+
queryClient: QueryClient,
|
|
42
|
+
options: CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
43
|
+
): InfiniteQueryResult<TData, TError, TPageParam>
|
|
44
|
+
export function createInfiniteQuery<
|
|
45
|
+
TQueryFnData = unknown,
|
|
46
|
+
TError = Error,
|
|
47
|
+
TPageParam = unknown,
|
|
48
|
+
TData = InfiniteData<TQueryFnData, TPageParam>,
|
|
49
|
+
>(
|
|
50
|
+
arg1:
|
|
51
|
+
| QueryClient
|
|
52
|
+
| CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
53
|
+
arg2?: CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
54
|
+
): InfiniteQueryResult<TData, TError, TPageParam> {
|
|
55
|
+
const [explicitClient, options] = parseInfiniteArgs<
|
|
56
|
+
TQueryFnData,
|
|
57
|
+
TError,
|
|
58
|
+
TPageParam,
|
|
59
|
+
TData
|
|
60
|
+
>(arg1, arg2)
|
|
61
|
+
const { queryKey, enabled, name, ...restOptions } = options
|
|
62
|
+
|
|
63
|
+
if (!name) warnMissingName('createInfiniteQuery')
|
|
64
|
+
|
|
65
|
+
const reactiveRefetchInterval = resolveReactiveRefetchInterval(
|
|
66
|
+
(restOptions as { refetchInterval?: unknown }).refetchInterval,
|
|
67
|
+
)
|
|
68
|
+
if (reactiveRefetchInterval) {
|
|
69
|
+
delete (restOptions as { refetchInterval?: unknown }).refetchInterval
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const base = createBaseQuery<
|
|
73
|
+
TData,
|
|
74
|
+
TError,
|
|
75
|
+
ObserverResult<TQueryFnData, TError, TData, TPageParam>,
|
|
76
|
+
Observer<TQueryFnData, TError, TData, TPageParam>,
|
|
77
|
+
{
|
|
78
|
+
$hasNextPage: ReturnType<typeof createStore<boolean>>
|
|
79
|
+
$hasPreviousPage: ReturnType<typeof createStore<boolean>>
|
|
80
|
+
$isFetchingNextPage: ReturnType<typeof createStore<boolean>>
|
|
81
|
+
$isFetchingPreviousPage: ReturnType<typeof createStore<boolean>>
|
|
82
|
+
$isFetchNextPageError: ReturnType<typeof createStore<boolean>>
|
|
83
|
+
$isFetchPreviousPageError: ReturnType<typeof createStore<boolean>>
|
|
84
|
+
fetchNextPage: ReturnType<typeof createEvent<void>>
|
|
85
|
+
fetchPreviousPage: ReturnType<typeof createEvent<void>>
|
|
86
|
+
}
|
|
87
|
+
>(
|
|
88
|
+
explicitClient,
|
|
89
|
+
{ queryKey, enabled, name, reactiveRefetchInterval },
|
|
90
|
+
{
|
|
91
|
+
createObserver: (qc, { queryKey: key, enabled: isEnabled }) =>
|
|
92
|
+
new InfiniteQueryObserver<
|
|
93
|
+
TQueryFnData,
|
|
94
|
+
TError,
|
|
95
|
+
TData,
|
|
96
|
+
QueryKey,
|
|
97
|
+
TPageParam
|
|
98
|
+
>(qc, {
|
|
99
|
+
...restOptions,
|
|
100
|
+
queryKey: key,
|
|
101
|
+
enabled: isEnabled,
|
|
102
|
+
} as any),
|
|
103
|
+
setupExtras: () => {
|
|
104
|
+
const hasNextPageUpdated = createEvent<boolean>()
|
|
105
|
+
const hasPreviousPageUpdated = createEvent<boolean>()
|
|
106
|
+
const isFetchingNextPageUpdated = createEvent<boolean>()
|
|
107
|
+
const isFetchingPreviousPageUpdated = createEvent<boolean>()
|
|
108
|
+
const isFetchNextPageErrorUpdated = createEvent<boolean>()
|
|
109
|
+
const isFetchPreviousPageErrorUpdated = createEvent<boolean>()
|
|
110
|
+
|
|
111
|
+
const $hasNextPage = createStore(false, {
|
|
112
|
+
...sidConfig(name, '$hasNextPage'),
|
|
113
|
+
}).on(hasNextPageUpdated, (_, v) => v)
|
|
114
|
+
const $hasPreviousPage = createStore(false, {
|
|
115
|
+
...sidConfig(name, '$hasPreviousPage'),
|
|
116
|
+
}).on(hasPreviousPageUpdated, (_, v) => v)
|
|
117
|
+
const $isFetchingNextPage = createStore(false, {
|
|
118
|
+
...sidConfig(name, '$isFetchingNextPage'),
|
|
119
|
+
}).on(isFetchingNextPageUpdated, (_, v) => v)
|
|
120
|
+
const $isFetchingPreviousPage = createStore(false, {
|
|
121
|
+
...sidConfig(name, '$isFetchingPreviousPage'),
|
|
122
|
+
}).on(isFetchingPreviousPageUpdated, (_, v) => v)
|
|
123
|
+
const $isFetchNextPageError = createStore(false, {
|
|
124
|
+
...sidConfig(name, '$isFetchNextPageError'),
|
|
125
|
+
}).on(isFetchNextPageErrorUpdated, (_, v) => v)
|
|
126
|
+
const $isFetchPreviousPageError = createStore(false, {
|
|
127
|
+
...sidConfig(name, '$isFetchPreviousPageError'),
|
|
128
|
+
}).on(isFetchPreviousPageErrorUpdated, (_, v) => v)
|
|
129
|
+
|
|
130
|
+
const fetchNextPage = createEvent<void>()
|
|
131
|
+
const fetchPreviousPage = createEvent<void>()
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
stores: {
|
|
135
|
+
$hasNextPage,
|
|
136
|
+
$hasPreviousPage,
|
|
137
|
+
$isFetchingNextPage,
|
|
138
|
+
$isFetchingPreviousPage,
|
|
139
|
+
$isFetchNextPageError,
|
|
140
|
+
$isFetchPreviousPageError,
|
|
141
|
+
fetchNextPage,
|
|
142
|
+
fetchPreviousPage,
|
|
143
|
+
},
|
|
144
|
+
// Wire fetchNextPage / fetchPreviousPage as scope-aware effects via
|
|
145
|
+
// attach over $observer — same pattern as the rest of createBaseQuery.
|
|
146
|
+
setupEffects: ({ $observer }) => {
|
|
147
|
+
const fetchNextPageFx = attach({
|
|
148
|
+
source: $observer,
|
|
149
|
+
effect: (observer) => {
|
|
150
|
+
if (!observer) return
|
|
151
|
+
observer.fetchNextPage()
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
sample({ clock: fetchNextPage, target: fetchNextPageFx })
|
|
155
|
+
|
|
156
|
+
const fetchPreviousPageFx = attach({
|
|
157
|
+
source: $observer,
|
|
158
|
+
effect: (observer) => {
|
|
159
|
+
if (!observer) return
|
|
160
|
+
observer.fetchPreviousPage()
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
sample({ clock: fetchPreviousPage, target: fetchPreviousPageFx })
|
|
164
|
+
},
|
|
165
|
+
bindDispatcher: () => {
|
|
166
|
+
const dispatchHasNextPage = scopeBind(hasNextPageUpdated, {
|
|
167
|
+
safe: true,
|
|
168
|
+
})
|
|
169
|
+
const dispatchHasPreviousPage = scopeBind(hasPreviousPageUpdated, {
|
|
170
|
+
safe: true,
|
|
171
|
+
})
|
|
172
|
+
const dispatchIsFetchingNextPage = scopeBind(
|
|
173
|
+
isFetchingNextPageUpdated,
|
|
174
|
+
{ safe: true },
|
|
175
|
+
)
|
|
176
|
+
const dispatchIsFetchingPreviousPage = scopeBind(
|
|
177
|
+
isFetchingPreviousPageUpdated,
|
|
178
|
+
{ safe: true },
|
|
179
|
+
)
|
|
180
|
+
const dispatchIsFetchNextPageError = scopeBind(
|
|
181
|
+
isFetchNextPageErrorUpdated,
|
|
182
|
+
{ safe: true },
|
|
183
|
+
)
|
|
184
|
+
const dispatchIsFetchPreviousPageError = scopeBind(
|
|
185
|
+
isFetchPreviousPageErrorUpdated,
|
|
186
|
+
{ safe: true },
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return (result) => {
|
|
190
|
+
dispatchHasNextPage(result.hasNextPage)
|
|
191
|
+
dispatchHasPreviousPage(result.hasPreviousPage)
|
|
192
|
+
dispatchIsFetchingNextPage(result.isFetchingNextPage)
|
|
193
|
+
dispatchIsFetchingPreviousPage(result.isFetchingPreviousPage)
|
|
194
|
+
dispatchIsFetchNextPageError(result.isFetchNextPageError)
|
|
195
|
+
dispatchIsFetchPreviousPageError(result.isFetchPreviousPageError)
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// See createQuery.prefetch — same contract, but uses fetchInfiniteQuery so
|
|
204
|
+
// the first page is fetched + cached on the server.
|
|
205
|
+
const prefetch = createEvent<void>()
|
|
206
|
+
const prefetchFx = attach({
|
|
207
|
+
source: {
|
|
208
|
+
qc: base.$queryClient,
|
|
209
|
+
key: base.$resolvedKey,
|
|
210
|
+
enabled: base.$enabled,
|
|
211
|
+
},
|
|
212
|
+
effect: ({ qc, key, enabled }) => {
|
|
213
|
+
if (!qc || !enabled) return
|
|
214
|
+
return qc.fetchInfiniteQuery({
|
|
215
|
+
...restOptions,
|
|
216
|
+
queryKey: key,
|
|
217
|
+
} as any)
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
sample({ clock: prefetch, target: prefetchFx })
|
|
221
|
+
|
|
222
|
+
const result: InfiniteQueryResult<TData, TError, TPageParam> = {
|
|
223
|
+
$data: base.$data,
|
|
224
|
+
$error: base.$error,
|
|
225
|
+
$status: base.$status,
|
|
226
|
+
$isPending: base.$isPending,
|
|
227
|
+
$isFetching: base.$isFetching,
|
|
228
|
+
$isSuccess: base.$isSuccess,
|
|
229
|
+
$isError: base.$isError,
|
|
230
|
+
$isPlaceholderData: base.$isPlaceholderData,
|
|
231
|
+
$fetchStatus: base.$fetchStatus,
|
|
232
|
+
$hasNextPage: base.$hasNextPage,
|
|
233
|
+
$hasPreviousPage: base.$hasPreviousPage,
|
|
234
|
+
$isFetchingNextPage: base.$isFetchingNextPage,
|
|
235
|
+
$isFetchingPreviousPage: base.$isFetchingPreviousPage,
|
|
236
|
+
$isFetchNextPageError: base.$isFetchNextPageError,
|
|
237
|
+
$isFetchPreviousPageError: base.$isFetchPreviousPageError,
|
|
238
|
+
$observer: base.$observer,
|
|
239
|
+
$queryClient: base.$queryClient,
|
|
240
|
+
fetchNextPage: base.fetchNextPage,
|
|
241
|
+
fetchPreviousPage: base.fetchPreviousPage,
|
|
242
|
+
refresh: base.refresh,
|
|
243
|
+
prefetch,
|
|
244
|
+
mounted: base.mounted,
|
|
245
|
+
unmounted: base.unmounted,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Object.defineProperty(result, '__createObserver', {
|
|
249
|
+
enumerable: false,
|
|
250
|
+
value: (qc: QueryClient, init: { queryKey: any; enabled: boolean }) =>
|
|
251
|
+
new InfiniteQueryObserver<
|
|
252
|
+
TQueryFnData,
|
|
253
|
+
TError,
|
|
254
|
+
TData,
|
|
255
|
+
QueryKey,
|
|
256
|
+
TPageParam
|
|
257
|
+
>(qc, {
|
|
258
|
+
...restOptions,
|
|
259
|
+
queryKey: init.queryKey,
|
|
260
|
+
enabled: init.enabled,
|
|
261
|
+
} as any),
|
|
262
|
+
})
|
|
263
|
+
Object.defineProperty(result, '__resolvedKey', {
|
|
264
|
+
enumerable: false,
|
|
265
|
+
value: base.$resolvedKey,
|
|
266
|
+
})
|
|
267
|
+
Object.defineProperty(result, '__enabled', {
|
|
268
|
+
enumerable: false,
|
|
269
|
+
value: base.$enabled,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseInfiniteArgs<TQueryFnData, TError, TPageParam, TData>(
|
|
276
|
+
arg1:
|
|
277
|
+
| QueryClient
|
|
278
|
+
| CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
279
|
+
arg2?: CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
280
|
+
): [
|
|
281
|
+
QueryClient | null,
|
|
282
|
+
CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
283
|
+
] {
|
|
284
|
+
if (arg2 !== undefined) {
|
|
285
|
+
return [arg1 as QueryClient, arg2]
|
|
286
|
+
}
|
|
287
|
+
return [
|
|
288
|
+
null,
|
|
289
|
+
arg1 as CreateInfiniteQueryOptions<TQueryFnData, TError, TPageParam, TData>,
|
|
290
|
+
]
|
|
291
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { attach, createEvent, createStore, sample } from 'effector'
|
|
2
|
+
import type { EventCallable, Store } from 'effector'
|
|
3
|
+
import type {
|
|
4
|
+
InvalidateQueryFilters,
|
|
5
|
+
QueryClient,
|
|
6
|
+
} from '@tanstack/query-core'
|
|
7
|
+
import { $queryClient } from './queryClient'
|
|
8
|
+
import { resolveKey } from './resolve'
|
|
9
|
+
import type { EffectorQueryKey } from './types'
|
|
10
|
+
|
|
11
|
+
export interface CreateInvalidateOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Key (or key prefix) to invalidate. Supports the same reactive shape as
|
|
14
|
+
* `createQuery.queryKey` — drop a `Store` anywhere in the array and the
|
|
15
|
+
* invalidate event will resolve it on every call, so dynamic keys "just
|
|
16
|
+
* work".
|
|
17
|
+
*/
|
|
18
|
+
queryKey: EffectorQueryKey
|
|
19
|
+
/**
|
|
20
|
+
* Forwarded to `queryClient.invalidateQueries({ exact })`. With `exact: true`
|
|
21
|
+
* only queries whose key *exactly* matches are invalidated; otherwise the
|
|
22
|
+
* key is treated as a prefix.
|
|
23
|
+
*/
|
|
24
|
+
exact?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Forwarded to `queryClient.invalidateQueries({ refetchType })`. Controls
|
|
27
|
+
* which observers refetch immediately after invalidation. Defaults to
|
|
28
|
+
* `'active'` (TanStack Query's own default).
|
|
29
|
+
*/
|
|
30
|
+
refetchType?: 'active' | 'inactive' | 'all' | 'none'
|
|
31
|
+
/**
|
|
32
|
+
* Forwarded to `queryClient.invalidateQueries({ type })`. Filters which
|
|
33
|
+
* queries match — `'active'`, `'inactive'`, `'all'`.
|
|
34
|
+
*/
|
|
35
|
+
type?: 'active' | 'inactive' | 'all'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds an effector event that invalidates a query (or a key prefix) on the
|
|
40
|
+
* resolved `QueryClient`. Useful when you want to invalidate a query
|
|
41
|
+
* declaratively from a `sample` (e.g. on a mutation's `finished.success`)
|
|
42
|
+
* without writing a one-off `attach` every time.
|
|
43
|
+
*
|
|
44
|
+
* @example Static key
|
|
45
|
+
* const invalidateFavorites = createInvalidate({ queryKey: ['favorites'] })
|
|
46
|
+
* sample({ clock: addFavorite.finished.success, target: invalidateFavorites })
|
|
47
|
+
*
|
|
48
|
+
* @example Reactive key — uses current $userId at invocation time
|
|
49
|
+
* const invalidateUser = createInvalidate({ queryKey: ['user', $userId] })
|
|
50
|
+
*
|
|
51
|
+
* @example Explicit client (same back-compat overload as the other factories)
|
|
52
|
+
* const invalidateFavorites = createInvalidate(queryClient, { queryKey: ['favorites'] })
|
|
53
|
+
*/
|
|
54
|
+
export function createInvalidate(
|
|
55
|
+
options: CreateInvalidateOptions,
|
|
56
|
+
): EventCallable<void>
|
|
57
|
+
export function createInvalidate(
|
|
58
|
+
queryClient: QueryClient,
|
|
59
|
+
options: CreateInvalidateOptions,
|
|
60
|
+
): EventCallable<void>
|
|
61
|
+
export function createInvalidate(
|
|
62
|
+
arg1: QueryClient | CreateInvalidateOptions,
|
|
63
|
+
arg2?: CreateInvalidateOptions,
|
|
64
|
+
): EventCallable<void> {
|
|
65
|
+
const [explicitClient, options] = parseArgs(arg1, arg2)
|
|
66
|
+
const { queryKey, exact, refetchType, type } = options
|
|
67
|
+
|
|
68
|
+
// Same locking semantics as the other factories: explicit client freezes
|
|
69
|
+
// the store, default flows through global $queryClient (and respects
|
|
70
|
+
// fork({ values: [[$queryClient, qc]] }) for per-scope isolation).
|
|
71
|
+
const $effectiveClient: Store<QueryClient | null> = explicitClient
|
|
72
|
+
? createStore(explicitClient as QueryClient | null, {
|
|
73
|
+
serialize: 'ignore',
|
|
74
|
+
})
|
|
75
|
+
: $queryClient
|
|
76
|
+
|
|
77
|
+
const $resolvedKey = resolveKey(queryKey)
|
|
78
|
+
|
|
79
|
+
const invalidate = createEvent<void>()
|
|
80
|
+
|
|
81
|
+
const invalidateFx = attach({
|
|
82
|
+
source: { qc: $effectiveClient, key: $resolvedKey },
|
|
83
|
+
effect: ({ qc, key }) => {
|
|
84
|
+
if (!qc) return
|
|
85
|
+
const filters: InvalidateQueryFilters = { queryKey: key }
|
|
86
|
+
if (exact !== undefined) filters.exact = exact
|
|
87
|
+
if (refetchType !== undefined) filters.refetchType = refetchType
|
|
88
|
+
if (type !== undefined) filters.type = type
|
|
89
|
+
return qc.invalidateQueries(filters)
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
sample({ clock: invalidate, target: invalidateFx })
|
|
94
|
+
|
|
95
|
+
return invalidate
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseArgs(
|
|
99
|
+
arg1: QueryClient | CreateInvalidateOptions,
|
|
100
|
+
arg2?: CreateInvalidateOptions,
|
|
101
|
+
): [QueryClient | null, CreateInvalidateOptions] {
|
|
102
|
+
if (arg2 !== undefined) return [arg1 as QueryClient, arg2]
|
|
103
|
+
return [null, arg1 as CreateInvalidateOptions]
|
|
104
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attach,
|
|
3
|
+
createEvent,
|
|
4
|
+
createStore,
|
|
5
|
+
sample,
|
|
6
|
+
scopeBind,
|
|
7
|
+
} from 'effector'
|
|
8
|
+
import type { Store } from 'effector'
|
|
9
|
+
import { MutationObserver } from '@tanstack/query-core'
|
|
10
|
+
import type { MutateOptions, QueryClient } from '@tanstack/query-core'
|
|
11
|
+
import { $queryClient } from './queryClient'
|
|
12
|
+
import { sidConfig, warnMissingName } from './createBaseQuery'
|
|
13
|
+
import type { CreateMutationOptions, MutationResult } from './types'
|
|
14
|
+
|
|
15
|
+
type MutationStatus = 'idle' | 'pending' | 'success' | 'error'
|
|
16
|
+
|
|
17
|
+
export function createMutation<
|
|
18
|
+
TData = unknown,
|
|
19
|
+
TError = Error,
|
|
20
|
+
TVariables = void,
|
|
21
|
+
TOnMutateResult = unknown,
|
|
22
|
+
>(
|
|
23
|
+
arg1:
|
|
24
|
+
| QueryClient
|
|
25
|
+
| CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
26
|
+
arg2?: CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
27
|
+
): MutationResult<TData, TError, TVariables> {
|
|
28
|
+
const [explicitClient, options] = parseMutationArgs<
|
|
29
|
+
TData,
|
|
30
|
+
TError,
|
|
31
|
+
TVariables,
|
|
32
|
+
TOnMutateResult
|
|
33
|
+
>(arg1, arg2)
|
|
34
|
+
|
|
35
|
+
const { name, ...observerOptions } = options
|
|
36
|
+
|
|
37
|
+
if (!name) warnMissingName('createMutation')
|
|
38
|
+
|
|
39
|
+
const $effectiveClient: Store<QueryClient | null> = explicitClient
|
|
40
|
+
? createStore(explicitClient as QueryClient | null, {
|
|
41
|
+
serialize: 'ignore',
|
|
42
|
+
})
|
|
43
|
+
: $queryClient
|
|
44
|
+
|
|
45
|
+
const dataUpdated = createEvent<TData | undefined>()
|
|
46
|
+
const errorUpdated = createEvent<TError | null>()
|
|
47
|
+
const statusUpdated = createEvent<MutationStatus>()
|
|
48
|
+
const variablesUpdated = createEvent<TVariables | undefined>()
|
|
49
|
+
const isPausedUpdated = createEvent<boolean>()
|
|
50
|
+
const finishedSuccess = createEvent<{ params: TVariables; result: TData }>()
|
|
51
|
+
const finishedFailure = createEvent<{ params: TVariables; error: TError }>()
|
|
52
|
+
|
|
53
|
+
const $data = createStore<TData | undefined>(undefined, {
|
|
54
|
+
skipVoid: false,
|
|
55
|
+
...sidConfig(name, '$data'),
|
|
56
|
+
}).on(dataUpdated, (_, v) => v)
|
|
57
|
+
const $error = createStore<TError | null>(null, {
|
|
58
|
+
skipVoid: false,
|
|
59
|
+
...sidConfig(name, '$error'),
|
|
60
|
+
}).on(errorUpdated, (_, v) => v)
|
|
61
|
+
const $status = createStore<MutationStatus>('idle', {
|
|
62
|
+
...sidConfig(name, '$status'),
|
|
63
|
+
}).on(statusUpdated, (_, v) => v)
|
|
64
|
+
const $variables = createStore<TVariables | undefined>(undefined, {
|
|
65
|
+
skipVoid: false,
|
|
66
|
+
...sidConfig(name, '$variables'),
|
|
67
|
+
}).on(variablesUpdated, (_, v) => v)
|
|
68
|
+
const $isPaused = createStore(false, {
|
|
69
|
+
...sidConfig(name, '$isPaused'),
|
|
70
|
+
}).on(isPausedUpdated, (_, v) => v)
|
|
71
|
+
|
|
72
|
+
// Derived stores via .map don't take sid — they recompute from $status on
|
|
73
|
+
// the client after fork({ values }).
|
|
74
|
+
const $isPending = $status.map((s) => s === 'pending')
|
|
75
|
+
const $isSuccess = $status.map((s) => s === 'success')
|
|
76
|
+
const $isError = $status.map((s) => s === 'error')
|
|
77
|
+
const $isIdle = $status.map((s) => s === 'idle')
|
|
78
|
+
|
|
79
|
+
// Per-scope observer — same pattern as createBaseQuery. Each fork scope
|
|
80
|
+
// gets its own MutationObserver bound to its scope's QueryClient.
|
|
81
|
+
type Observer = MutationObserver<TData, TError, TVariables, TOnMutateResult>
|
|
82
|
+
const $observer = createStore<Observer | null>(null, {
|
|
83
|
+
serialize: 'ignore',
|
|
84
|
+
})
|
|
85
|
+
const observerCreated = createEvent<Observer>()
|
|
86
|
+
$observer.on(observerCreated, (_, obs) => obs)
|
|
87
|
+
|
|
88
|
+
const observerSubscriptions = new WeakMap<Observer, () => void>()
|
|
89
|
+
|
|
90
|
+
const start = createEvent<void>()
|
|
91
|
+
const unmounted = createEvent<void>()
|
|
92
|
+
|
|
93
|
+
const startFx = attach({
|
|
94
|
+
source: { qc: $effectiveClient, observer: $observer },
|
|
95
|
+
effect: ({ qc, observer: existingObserver }) => {
|
|
96
|
+
if (!qc) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'[@tanstack/query-effector] No QueryClient is set for createMutation. Call setQueryClient(qc) before start, ' +
|
|
99
|
+
'pass it to fork({ values: [[$queryClient, qc]] }), or pass it explicitly to the factory.',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const observer =
|
|
104
|
+
existingObserver ??
|
|
105
|
+
new MutationObserver<TData, TError, TVariables, TOnMutateResult>(
|
|
106
|
+
qc,
|
|
107
|
+
observerOptions,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const dispatchData = scopeBind(dataUpdated, { safe: true })
|
|
111
|
+
const dispatchError = scopeBind(errorUpdated, { safe: true })
|
|
112
|
+
const dispatchStatus = scopeBind(statusUpdated, { safe: true })
|
|
113
|
+
const dispatchVariables = scopeBind(variablesUpdated, { safe: true })
|
|
114
|
+
const dispatchIsPaused = scopeBind(isPausedUpdated, { safe: true })
|
|
115
|
+
const dispatchFinishedSuccess = scopeBind(finishedSuccess, { safe: true })
|
|
116
|
+
const dispatchFinishedFailure = scopeBind(finishedFailure, { safe: true })
|
|
117
|
+
|
|
118
|
+
observerSubscriptions.get(observer)?.()
|
|
119
|
+
|
|
120
|
+
// Track status transitions to emit `finished.success` / `finished.failure`
|
|
121
|
+
// exactly once per pending → terminal transition. The observer holds at
|
|
122
|
+
// most one inflight mutation at a time, so a single previous-status flag
|
|
123
|
+
// is sufficient.
|
|
124
|
+
let prevStatus: MutationStatus = 'idle'
|
|
125
|
+
|
|
126
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
127
|
+
dispatchData(result.data)
|
|
128
|
+
dispatchError(result.error)
|
|
129
|
+
dispatchStatus(result.status)
|
|
130
|
+
dispatchVariables(result.variables)
|
|
131
|
+
dispatchIsPaused(result.isPaused)
|
|
132
|
+
|
|
133
|
+
if (prevStatus === 'pending') {
|
|
134
|
+
if (result.status === 'success') {
|
|
135
|
+
dispatchFinishedSuccess({
|
|
136
|
+
params: result.variables as TVariables,
|
|
137
|
+
result: result.data as TData,
|
|
138
|
+
})
|
|
139
|
+
} else if (result.status === 'error') {
|
|
140
|
+
dispatchFinishedFailure({
|
|
141
|
+
params: result.variables as TVariables,
|
|
142
|
+
error: result.error as TError,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
prevStatus = result.status
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
observerSubscriptions.set(observer, unsubscribe)
|
|
150
|
+
|
|
151
|
+
return observer
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
sample({ clock: start, target: startFx })
|
|
156
|
+
sample({ clock: startFx.doneData, target: observerCreated })
|
|
157
|
+
|
|
158
|
+
// Mutation observers (unlike query observers) live across mount cycles —
|
|
159
|
+
// their data state survives unmount and can be observed again on re-start.
|
|
160
|
+
// Match that by keeping `$observer` populated; we only drop the
|
|
161
|
+
// subscription so listeners stop receiving updates after unmount.
|
|
162
|
+
const unmountFx = attach({
|
|
163
|
+
source: $observer,
|
|
164
|
+
effect: (observer) => {
|
|
165
|
+
if (!observer) return
|
|
166
|
+
observerSubscriptions.get(observer)?.()
|
|
167
|
+
observerSubscriptions.delete(observer)
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
sample({ clock: unmounted, target: unmountFx })
|
|
171
|
+
|
|
172
|
+
const mutate = createEvent<TVariables>()
|
|
173
|
+
|
|
174
|
+
// Fire-and-forget: don't return the promise so allSettled doesn't block
|
|
175
|
+
// (would deadlock under fake timers since observer.mutate awaits the user's
|
|
176
|
+
// mutationFn). Errors are tracked via the observer subscription which
|
|
177
|
+
// updates $error/$status and emits `finished.failure`.
|
|
178
|
+
const mutateFx = attach({
|
|
179
|
+
source: $observer,
|
|
180
|
+
effect: (observer, variables: TVariables) => {
|
|
181
|
+
if (!observer) return
|
|
182
|
+
observer.mutate(variables).catch(() => {})
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
sample({ clock: mutate, target: mutateFx })
|
|
187
|
+
|
|
188
|
+
// Per-call callbacks variant: forwards onSuccess/onError/onSettled to
|
|
189
|
+
// observer.mutate(vars, opts). Use this for component-local reactions
|
|
190
|
+
// that don't fit module-level `sample({ clock: finished.success })` wiring.
|
|
191
|
+
const mutateWith = createEvent<{
|
|
192
|
+
variables: TVariables
|
|
193
|
+
onSuccess?: MutateOptions<TData, TError, TVariables>['onSuccess']
|
|
194
|
+
onError?: MutateOptions<TData, TError, TVariables>['onError']
|
|
195
|
+
onSettled?: MutateOptions<TData, TError, TVariables>['onSettled']
|
|
196
|
+
}>()
|
|
197
|
+
|
|
198
|
+
const mutateWithFx = attach({
|
|
199
|
+
source: $observer,
|
|
200
|
+
effect: (
|
|
201
|
+
observer,
|
|
202
|
+
{
|
|
203
|
+
variables,
|
|
204
|
+
...callbacks
|
|
205
|
+
}: {
|
|
206
|
+
variables: TVariables
|
|
207
|
+
onSuccess?: MutateOptions<TData, TError, TVariables>['onSuccess']
|
|
208
|
+
onError?: MutateOptions<TData, TError, TVariables>['onError']
|
|
209
|
+
onSettled?: MutateOptions<TData, TError, TVariables>['onSettled']
|
|
210
|
+
},
|
|
211
|
+
) => {
|
|
212
|
+
if (!observer) return
|
|
213
|
+
observer.mutate(variables, callbacks).catch(() => {})
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
sample({ clock: mutateWith, target: mutateWithFx })
|
|
218
|
+
|
|
219
|
+
const reset = createEvent<void>()
|
|
220
|
+
|
|
221
|
+
const resetFx = attach({
|
|
222
|
+
source: $observer,
|
|
223
|
+
effect: (observer) => {
|
|
224
|
+
if (!observer) return
|
|
225
|
+
observer.reset()
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
sample({ clock: reset, target: resetFx })
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
$data,
|
|
233
|
+
$error,
|
|
234
|
+
$status,
|
|
235
|
+
$variables,
|
|
236
|
+
$isPaused,
|
|
237
|
+
$isPending,
|
|
238
|
+
$isSuccess,
|
|
239
|
+
$isError,
|
|
240
|
+
$isIdle,
|
|
241
|
+
$observer,
|
|
242
|
+
$queryClient: $effectiveClient,
|
|
243
|
+
mutate,
|
|
244
|
+
mutateWith,
|
|
245
|
+
reset,
|
|
246
|
+
start,
|
|
247
|
+
unmounted,
|
|
248
|
+
finished: {
|
|
249
|
+
success: finishedSuccess,
|
|
250
|
+
failure: finishedFailure,
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseMutationArgs<TData, TError, TVariables, TOnMutateResult>(
|
|
256
|
+
arg1:
|
|
257
|
+
| QueryClient
|
|
258
|
+
| CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
259
|
+
arg2?: CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
260
|
+
): [
|
|
261
|
+
QueryClient | null,
|
|
262
|
+
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
263
|
+
] {
|
|
264
|
+
if (arg2 !== undefined) {
|
|
265
|
+
return [arg1 as QueryClient, arg2]
|
|
266
|
+
}
|
|
267
|
+
return [
|
|
268
|
+
null,
|
|
269
|
+
arg1 as CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
270
|
+
]
|
|
271
|
+
}
|