@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.
Files changed (65) hide show
  1. package/README.md +40 -0
  2. package/dist/createBaseQuery.cjs +208 -0
  3. package/dist/createBaseQuery.cjs.map +1 -0
  4. package/dist/createBaseQuery.d.cts +114 -0
  5. package/dist/createBaseQuery.d.ts +114 -0
  6. package/dist/createBaseQuery.js +204 -0
  7. package/dist/createBaseQuery.js.map +1 -0
  8. package/dist/createInfiniteQuery.cjs +193 -0
  9. package/dist/createInfiniteQuery.cjs.map +1 -0
  10. package/dist/createInfiniteQuery.d.cts +8 -0
  11. package/dist/createInfiniteQuery.d.ts +8 -0
  12. package/dist/createInfiniteQuery.js +191 -0
  13. package/dist/createInfiniteQuery.js.map +1 -0
  14. package/dist/createInvalidate.cjs +37 -0
  15. package/dist/createInvalidate.cjs.map +1 -0
  16. package/dist/createInvalidate.d.cts +50 -0
  17. package/dist/createInvalidate.d.ts +50 -0
  18. package/dist/createInvalidate.js +35 -0
  19. package/dist/createInvalidate.js.map +1 -0
  20. package/dist/createMutation.cjs +177 -0
  21. package/dist/createMutation.cjs.map +1 -0
  22. package/dist/createMutation.d.cts +7 -0
  23. package/dist/createMutation.d.ts +7 -0
  24. package/dist/createMutation.js +175 -0
  25. package/dist/createMutation.js.map +1 -0
  26. package/dist/createQuery.cjs +98 -0
  27. package/dist/createQuery.cjs.map +1 -0
  28. package/dist/createQuery.d.cts +8 -0
  29. package/dist/createQuery.d.ts +8 -0
  30. package/dist/createQuery.js +96 -0
  31. package/dist/createQuery.js.map +1 -0
  32. package/dist/index.cjs +36 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d.cts +8 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.js +7 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/queryClient.cjs +20 -0
  39. package/dist/queryClient.cjs.map +1 -0
  40. package/dist/queryClient.d.cts +15 -0
  41. package/dist/queryClient.d.ts +15 -0
  42. package/dist/queryClient.js +17 -0
  43. package/dist/queryClient.js.map +1 -0
  44. package/dist/resolve.cjs +37 -0
  45. package/dist/resolve.cjs.map +1 -0
  46. package/dist/resolve.d.cts +17 -0
  47. package/dist/resolve.d.ts +17 -0
  48. package/dist/resolve.js +33 -0
  49. package/dist/resolve.js.map +1 -0
  50. package/dist/types.cjs +4 -0
  51. package/dist/types.cjs.map +1 -0
  52. package/dist/types.d.cts +209 -0
  53. package/dist/types.d.ts +209 -0
  54. package/dist/types.js +3 -0
  55. package/dist/types.js.map +1 -0
  56. package/package.json +60 -0
  57. package/src/createBaseQuery.ts +428 -0
  58. package/src/createInfiniteQuery.ts +291 -0
  59. package/src/createInvalidate.ts +104 -0
  60. package/src/createMutation.ts +271 -0
  61. package/src/createQuery.ts +155 -0
  62. package/src/index.ts +17 -0
  63. package/src/queryClient.ts +23 -0
  64. package/src/resolve.ts +50 -0
  65. 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
+ }