@effector-tanstack-query/core 0.4.0 → 1.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/dist/createBaseQuery.cjs +23 -0
- package/dist/createBaseQuery.cjs.map +1 -1
- package/dist/createBaseQuery.d.cts +23 -1
- package/dist/createBaseQuery.d.ts +23 -1
- package/dist/createBaseQuery.js +23 -0
- package/dist/createBaseQuery.js.map +1 -1
- package/dist/createCacheAction.cjs +60 -0
- package/dist/createCacheAction.cjs.map +1 -0
- package/dist/createCacheAction.d.cts +66 -0
- package/dist/createCacheAction.d.ts +66 -0
- package/dist/createCacheAction.js +56 -0
- package/dist/createCacheAction.js.map +1 -0
- package/dist/createInfiniteQuery.cjs +2 -1
- package/dist/createInfiniteQuery.cjs.map +1 -1
- package/dist/createInfiniteQuery.js +2 -1
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createQuery.cjs +2 -1
- package/dist/createQuery.cjs.map +1 -1
- package/dist/createQuery.js +2 -1
- package/dist/createQuery.js.map +1 -1
- package/dist/index.cjs +13 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.cts +29 -0
- package/dist/types.d.ts +29 -0
- package/package.json +9 -5
- package/src/createBaseQuery.ts +67 -1
- package/src/createCacheAction.ts +172 -0
- package/src/createInfiniteQuery.ts +1 -0
- package/src/createQuery.ts +1 -0
- package/src/index.ts +11 -0
- package/src/types.ts +29 -0
package/src/createBaseQuery.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
sample,
|
|
7
7
|
scopeBind,
|
|
8
8
|
} from 'effector'
|
|
9
|
-
import type { EventCallable, Store } from 'effector'
|
|
9
|
+
import type { Event, EventCallable, Store } from 'effector'
|
|
10
10
|
import type {
|
|
11
11
|
FetchStatus,
|
|
12
12
|
QueryClient,
|
|
@@ -40,6 +40,17 @@ export interface BaseObserverResult<TData, TError> {
|
|
|
40
40
|
isFetching: boolean
|
|
41
41
|
fetchStatus: FetchStatus
|
|
42
42
|
isPlaceholderData: boolean
|
|
43
|
+
/**
|
|
44
|
+
* Timestamp (ms) of the last successful data resolution. Monotonically
|
|
45
|
+
* increases per successful fetch — used to detect newly-finished fetches
|
|
46
|
+
* for the `finished.success` lifecycle event.
|
|
47
|
+
*/
|
|
48
|
+
dataUpdatedAt: number
|
|
49
|
+
/**
|
|
50
|
+
* Timestamp (ms) of the last error. Increments per failed fetch — used to
|
|
51
|
+
* detect newly-finished failures for the `finished.failure` lifecycle event.
|
|
52
|
+
*/
|
|
53
|
+
errorUpdatedAt: number
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export interface BaseQueryStores<TData, TError, TObserver> {
|
|
@@ -71,6 +82,17 @@ export interface BaseQueryStores<TData, TError, TObserver> {
|
|
|
71
82
|
refresh: EventCallable<void>
|
|
72
83
|
mounted: EventCallable<void>
|
|
73
84
|
unmounted: EventCallable<void>
|
|
85
|
+
/**
|
|
86
|
+
* Lifecycle events for `sample`-driven reactions to fetch completion.
|
|
87
|
+
* `success` fires with the (post-`select`) data on every newly-finished
|
|
88
|
+
* successful fetch; `failure` fires with the error on every failed fetch.
|
|
89
|
+
* Neither fires for the baseline state observed on mount (e.g. hydrated
|
|
90
|
+
* cache) — they track *new* fetches, not initial observability.
|
|
91
|
+
*/
|
|
92
|
+
finished: {
|
|
93
|
+
success: Event<TData>
|
|
94
|
+
failure: Event<TError>
|
|
95
|
+
}
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
export interface BaseQueryOptions {
|
|
@@ -186,6 +208,11 @@ export function createBaseQuery<
|
|
|
186
208
|
const fetchStatusUpdated = createEvent<FetchStatus>()
|
|
187
209
|
const isPlaceholderDataUpdated = createEvent<boolean>()
|
|
188
210
|
|
|
211
|
+
// Lifecycle events. Created once at factory time; dispatched per-scope via
|
|
212
|
+
// scopeBind inside the mount effect so `allSettled` / fork isolation work.
|
|
213
|
+
const finishedSuccess = createEvent<TData>()
|
|
214
|
+
const finishedFailure = createEvent<TError>()
|
|
215
|
+
|
|
189
216
|
const $data = createStore<TData | undefined>(undefined, {
|
|
190
217
|
skipVoid: false,
|
|
191
218
|
...sidConfig(name, '$data'),
|
|
@@ -268,8 +295,19 @@ export function createBaseQuery<
|
|
|
268
295
|
const dispatchIsPlaceholderData = scopeBind(isPlaceholderDataUpdated, {
|
|
269
296
|
safe: true,
|
|
270
297
|
})
|
|
298
|
+
const dispatchFinishedSuccess = scopeBind(finishedSuccess, { safe: true })
|
|
299
|
+
const dispatchFinishedFailure = scopeBind(finishedFailure, { safe: true })
|
|
271
300
|
const dispatchExtras = extras?.bindDispatcher()
|
|
272
301
|
|
|
302
|
+
// Per-mount, per-scope baseline for lifecycle events. The first
|
|
303
|
+
// notification (the immediate getCurrentResult() emit below, or the
|
|
304
|
+
// observer's first callback) establishes the baseline without firing —
|
|
305
|
+
// so hydrated cache data on mount doesn't dispatch `finished.success`.
|
|
306
|
+
// Subsequent increments of dataUpdatedAt / errorUpdatedAt are genuine
|
|
307
|
+
// new fetches and do fire.
|
|
308
|
+
let lastDataUpdatedAt = -1
|
|
309
|
+
let lastErrorUpdatedAt = -1
|
|
310
|
+
|
|
273
311
|
observerSubscriptions.get(observer)?.()
|
|
274
312
|
observer.setOptions({
|
|
275
313
|
...observer.options,
|
|
@@ -289,6 +327,30 @@ export function createBaseQuery<
|
|
|
289
327
|
dispatchFetchStatus(result.fetchStatus)
|
|
290
328
|
dispatchIsPlaceholderData(result.isPlaceholderData)
|
|
291
329
|
dispatchExtras?.(result)
|
|
330
|
+
|
|
331
|
+
if (lastDataUpdatedAt === -1) {
|
|
332
|
+
// Baseline — record current timestamps without emitting.
|
|
333
|
+
lastDataUpdatedAt = result.dataUpdatedAt
|
|
334
|
+
lastErrorUpdatedAt = result.errorUpdatedAt
|
|
335
|
+
} else {
|
|
336
|
+
// A newly-resolved successful fetch. Guard against placeholderData,
|
|
337
|
+
// which carries status 'success' but never advances dataUpdatedAt.
|
|
338
|
+
if (
|
|
339
|
+
result.dataUpdatedAt > lastDataUpdatedAt &&
|
|
340
|
+
result.status === 'success' &&
|
|
341
|
+
!result.isPlaceholderData
|
|
342
|
+
) {
|
|
343
|
+
lastDataUpdatedAt = result.dataUpdatedAt
|
|
344
|
+
dispatchFinishedSuccess(result.data as TData)
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
result.errorUpdatedAt > lastErrorUpdatedAt &&
|
|
348
|
+
result.status === 'error'
|
|
349
|
+
) {
|
|
350
|
+
lastErrorUpdatedAt = result.errorUpdatedAt
|
|
351
|
+
dispatchFinishedFailure(result.error as TError)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
292
354
|
}
|
|
293
355
|
|
|
294
356
|
const unsubscribe = observer.subscribe(dispatch)
|
|
@@ -423,6 +485,10 @@ export function createBaseQuery<
|
|
|
423
485
|
refresh,
|
|
424
486
|
mounted,
|
|
425
487
|
unmounted,
|
|
488
|
+
finished: {
|
|
489
|
+
success: finishedSuccess,
|
|
490
|
+
failure: finishedFailure,
|
|
491
|
+
},
|
|
426
492
|
...(extras?.stores ?? ({} as TExtraStores)),
|
|
427
493
|
}
|
|
428
494
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { attach, createEvent, createStore, sample } from 'effector'
|
|
2
|
+
import type { EventCallable, Store } from 'effector'
|
|
3
|
+
import type { QueryClient, QueryFilters, QueryKey } from '@tanstack/query-core'
|
|
4
|
+
import { $queryClient } from './queryClient'
|
|
5
|
+
import { resolveKey } from './resolve'
|
|
6
|
+
import type { EffectorQueryKey } from './types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options shared by `createCancel` / `createRemove` / `createReset`.
|
|
10
|
+
*
|
|
11
|
+
* Structurally a `QueryFilters` (from `@tanstack/query-core`) with the
|
|
12
|
+
* `queryKey` field widened to the reactive `EffectorQueryKey` shape — drop a
|
|
13
|
+
* `Store` anywhere in the array and the action resolves it on every call.
|
|
14
|
+
*
|
|
15
|
+
* `queryKey` is **optional**: omit it to target *all* queries, mirroring
|
|
16
|
+
* `queryClient.cancelQueries()` / `removeQueries()` / `resetQueries()` with no
|
|
17
|
+
* filters.
|
|
18
|
+
*/
|
|
19
|
+
export interface CacheActionOptions extends Omit<QueryFilters, 'queryKey'> {
|
|
20
|
+
/**
|
|
21
|
+
* Key (or key prefix) to act on. Supports the same reactive shape as
|
|
22
|
+
* `createQuery.queryKey`. Omit to match every query in the cache.
|
|
23
|
+
*/
|
|
24
|
+
queryKey?: EffectorQueryKey
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Distinct named aliases so each factory documents its own option type. */
|
|
28
|
+
export type CreateCancelOptions = CacheActionOptions
|
|
29
|
+
export type CreateRemoveOptions = CacheActionOptions
|
|
30
|
+
export type CreateResetOptions = CacheActionOptions
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The QueryClient method a given action drives. Receives the resolved filters
|
|
34
|
+
* (with `queryKey` already merged in when present). May be sync (`removeQueries`
|
|
35
|
+
* returns `void`) or async (`cancelQueries` / `resetQueries` return a Promise).
|
|
36
|
+
*/
|
|
37
|
+
type CacheActionRunner = (qc: QueryClient, filters: QueryFilters) => unknown
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shared builder. Returns a `void` event that, when fired, runs `action`
|
|
41
|
+
* against the resolved `QueryClient` with the resolved filters. Mirrors
|
|
42
|
+
* `createInvalidate`'s locking + reactive-key + per-scope semantics exactly.
|
|
43
|
+
*/
|
|
44
|
+
function buildCacheAction(
|
|
45
|
+
action: CacheActionRunner,
|
|
46
|
+
explicitClient: QueryClient | null,
|
|
47
|
+
options: CacheActionOptions,
|
|
48
|
+
): EventCallable<void> {
|
|
49
|
+
const { queryKey, ...restFilters } = options
|
|
50
|
+
|
|
51
|
+
// Same locking semantics as the other factories: explicit client freezes the
|
|
52
|
+
// store, default flows through global $queryClient (and respects
|
|
53
|
+
// fork({ values: [[$queryClient, qc]] }) for per-scope isolation).
|
|
54
|
+
const $effectiveClient: Store<QueryClient | null> = explicitClient
|
|
55
|
+
? createStore(explicitClient as QueryClient | null, {
|
|
56
|
+
serialize: 'ignore',
|
|
57
|
+
})
|
|
58
|
+
: $queryClient
|
|
59
|
+
|
|
60
|
+
// No queryKey → a store of `undefined`, so the effect omits the field and the
|
|
61
|
+
// action matches all queries.
|
|
62
|
+
const $resolvedKey: Store<QueryKey | undefined> = queryKey
|
|
63
|
+
? resolveKey(queryKey)
|
|
64
|
+
: createStore<QueryKey | undefined>(undefined, { skipVoid: false })
|
|
65
|
+
|
|
66
|
+
const run = createEvent<void>()
|
|
67
|
+
|
|
68
|
+
const runFx = attach({
|
|
69
|
+
source: { qc: $effectiveClient, key: $resolvedKey },
|
|
70
|
+
effect: ({ qc, key }) => {
|
|
71
|
+
if (!qc) return
|
|
72
|
+
const filters: QueryFilters = { ...restFilters }
|
|
73
|
+
if (key !== undefined) filters.queryKey = key
|
|
74
|
+
return action(qc, filters)
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
sample({ clock: run, target: runFx })
|
|
79
|
+
|
|
80
|
+
return run
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseArgs(
|
|
84
|
+
arg1: QueryClient | CacheActionOptions,
|
|
85
|
+
arg2?: CacheActionOptions,
|
|
86
|
+
): [QueryClient | null, CacheActionOptions] {
|
|
87
|
+
if (arg2 !== undefined) return [arg1 as QueryClient, arg2]
|
|
88
|
+
return [null, arg1 as CacheActionOptions]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds a `sample`-friendly event that **cancels** in-flight queries on the
|
|
93
|
+
* resolved `QueryClient` (without removing cached data). Wraps
|
|
94
|
+
* `queryClient.cancelQueries` — async, so `await allSettled(cancel, { scope })`
|
|
95
|
+
* waits for cancellation to settle.
|
|
96
|
+
*
|
|
97
|
+
* @example Cancel a user's queries on logout
|
|
98
|
+
* const cancelUserQueries = createCancel({ queryKey: ['user', $userId] })
|
|
99
|
+
* sample({ clock: logoutClicked, target: cancelUserQueries })
|
|
100
|
+
*
|
|
101
|
+
* @example Explicit client (same back-compat overload as createInvalidate)
|
|
102
|
+
* const cancelAll = createCancel(queryClient, {})
|
|
103
|
+
*/
|
|
104
|
+
export function createCancel(options: CacheActionOptions): EventCallable<void>
|
|
105
|
+
export function createCancel(
|
|
106
|
+
queryClient: QueryClient,
|
|
107
|
+
options: CacheActionOptions,
|
|
108
|
+
): EventCallable<void>
|
|
109
|
+
export function createCancel(
|
|
110
|
+
arg1: QueryClient | CacheActionOptions,
|
|
111
|
+
arg2?: CacheActionOptions,
|
|
112
|
+
): EventCallable<void> {
|
|
113
|
+
const [explicitClient, options] = parseArgs(arg1, arg2)
|
|
114
|
+
return buildCacheAction(
|
|
115
|
+
(qc, filters) => qc.cancelQueries(filters),
|
|
116
|
+
explicitClient,
|
|
117
|
+
options,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds a `sample`-friendly event that **removes** matching entries from the
|
|
123
|
+
* cache entirely. Wraps `queryClient.removeQueries` — synchronous (no Promise).
|
|
124
|
+
* The next mount of a removed query starts from a pending state.
|
|
125
|
+
*
|
|
126
|
+
* @example Drop stale cache on navigation
|
|
127
|
+
* const removeStale = createRemove({ queryKey: ['stale-cache'] })
|
|
128
|
+
* sample({ clock: navigated, target: removeStale })
|
|
129
|
+
*/
|
|
130
|
+
export function createRemove(options: CacheActionOptions): EventCallable<void>
|
|
131
|
+
export function createRemove(
|
|
132
|
+
queryClient: QueryClient,
|
|
133
|
+
options: CacheActionOptions,
|
|
134
|
+
): EventCallable<void>
|
|
135
|
+
export function createRemove(
|
|
136
|
+
arg1: QueryClient | CacheActionOptions,
|
|
137
|
+
arg2?: CacheActionOptions,
|
|
138
|
+
): EventCallable<void> {
|
|
139
|
+
const [explicitClient, options] = parseArgs(arg1, arg2)
|
|
140
|
+
return buildCacheAction(
|
|
141
|
+
(qc, filters) => qc.removeQueries(filters),
|
|
142
|
+
explicitClient,
|
|
143
|
+
options,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Builds a `sample`-friendly event that **resets** matching queries back to
|
|
149
|
+
* their initial state and refetches any active observers. Wraps
|
|
150
|
+
* `queryClient.resetQueries` — async, so `await allSettled(reset, { scope })`
|
|
151
|
+
* waits for the refetch to settle.
|
|
152
|
+
*
|
|
153
|
+
* @example Reset search results when the filters are cleared
|
|
154
|
+
* const resetSearch = createReset({ queryKey: ['search'] })
|
|
155
|
+
* sample({ clock: filtersCleared, target: resetSearch })
|
|
156
|
+
*/
|
|
157
|
+
export function createReset(options: CacheActionOptions): EventCallable<void>
|
|
158
|
+
export function createReset(
|
|
159
|
+
queryClient: QueryClient,
|
|
160
|
+
options: CacheActionOptions,
|
|
161
|
+
): EventCallable<void>
|
|
162
|
+
export function createReset(
|
|
163
|
+
arg1: QueryClient | CacheActionOptions,
|
|
164
|
+
arg2?: CacheActionOptions,
|
|
165
|
+
): EventCallable<void> {
|
|
166
|
+
const [explicitClient, options] = parseArgs(arg1, arg2)
|
|
167
|
+
return buildCacheAction(
|
|
168
|
+
(qc, filters) => qc.resetQueries(filters),
|
|
169
|
+
explicitClient,
|
|
170
|
+
options,
|
|
171
|
+
)
|
|
172
|
+
}
|
package/src/createQuery.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,6 +4,17 @@ export { createQueries } from './createQueries'
|
|
|
4
4
|
export { createMutation } from './createMutation'
|
|
5
5
|
export { createInvalidate } from './createInvalidate'
|
|
6
6
|
export type { CreateInvalidateOptions } from './createInvalidate'
|
|
7
|
+
export {
|
|
8
|
+
createCancel,
|
|
9
|
+
createRemove,
|
|
10
|
+
createReset,
|
|
11
|
+
} from './createCacheAction'
|
|
12
|
+
export type {
|
|
13
|
+
CacheActionOptions,
|
|
14
|
+
CreateCancelOptions,
|
|
15
|
+
CreateRemoveOptions,
|
|
16
|
+
CreateResetOptions,
|
|
17
|
+
} from './createCacheAction'
|
|
7
18
|
export { $queryClient, setQueryClient } from './queryClient'
|
|
8
19
|
export { prefetchQueries } from './prefetchQueries'
|
|
9
20
|
export type {
|
package/src/types.ts
CHANGED
|
@@ -160,6 +160,30 @@ export interface QueryResult<TData, TError = Error> {
|
|
|
160
160
|
* `$queryClient` and honors `fork({ values: [[$queryClient, qc]] })`.
|
|
161
161
|
*/
|
|
162
162
|
$queryClient: Store<QueryClient | null>
|
|
163
|
+
/**
|
|
164
|
+
* Lifecycle events for `sample`-driven reactions to fetch completion.
|
|
165
|
+
*
|
|
166
|
+
* - `success` fires with the (post-`select`) data on every newly-finished
|
|
167
|
+
* successful fetch — fresh fetch, refetch, reactive key change, or a
|
|
168
|
+
* cross-scope `setQueryData`.
|
|
169
|
+
* - `failure` fires with the error on every failed fetch.
|
|
170
|
+
*
|
|
171
|
+
* Neither fires for the baseline state observed on `mounted()` (e.g.
|
|
172
|
+
* SSR-hydrated cache data) — the events track *new* fetches, not the
|
|
173
|
+
* initial observation. Each fork scope tracks its own baseline.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* sample({ clock: userQuery.finished.success, target: loadSettings })
|
|
177
|
+
* sample({
|
|
178
|
+
* clock: userQuery.finished.failure,
|
|
179
|
+
* fn: (err) => `Failed: ${err.message}`,
|
|
180
|
+
* target: showToast,
|
|
181
|
+
* })
|
|
182
|
+
*/
|
|
183
|
+
finished: {
|
|
184
|
+
success: Event<TData>
|
|
185
|
+
failure: Event<TError>
|
|
186
|
+
}
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
export interface CreateInfiniteQueryOptions<
|
|
@@ -231,6 +255,11 @@ export interface InfiniteQueryResult<
|
|
|
231
255
|
>
|
|
232
256
|
/** See {@link QueryResult.$queryClient}. */
|
|
233
257
|
$queryClient: Store<QueryClient | null>
|
|
258
|
+
/** See {@link QueryResult.finished}. */
|
|
259
|
+
finished: {
|
|
260
|
+
success: Event<TData>
|
|
261
|
+
failure: Event<TError>
|
|
262
|
+
}
|
|
234
263
|
}
|
|
235
264
|
|
|
236
265
|
export type CreateMutationOptions<
|