@effector-tanstack-query/core 0.3.0 → 0.5.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 (40) hide show
  1. package/dist/createBaseQuery.cjs +23 -0
  2. package/dist/createBaseQuery.cjs.map +1 -1
  3. package/dist/createBaseQuery.d.cts +23 -1
  4. package/dist/createBaseQuery.d.ts +23 -1
  5. package/dist/createBaseQuery.js +23 -0
  6. package/dist/createBaseQuery.js.map +1 -1
  7. package/dist/createCacheAction.cjs +60 -0
  8. package/dist/createCacheAction.cjs.map +1 -0
  9. package/dist/createCacheAction.d.cts +66 -0
  10. package/dist/createCacheAction.d.ts +66 -0
  11. package/dist/createCacheAction.js +56 -0
  12. package/dist/createCacheAction.js.map +1 -0
  13. package/dist/createInfiniteQuery.cjs +2 -1
  14. package/dist/createInfiniteQuery.cjs.map +1 -1
  15. package/dist/createInfiniteQuery.js +2 -1
  16. package/dist/createInfiniteQuery.js.map +1 -1
  17. package/dist/createQueries.cjs +262 -0
  18. package/dist/createQueries.cjs.map +1 -0
  19. package/dist/createQueries.d.cts +32 -0
  20. package/dist/createQueries.d.ts +32 -0
  21. package/dist/createQueries.js +260 -0
  22. package/dist/createQueries.js.map +1 -0
  23. package/dist/createQuery.cjs +2 -1
  24. package/dist/createQuery.cjs.map +1 -1
  25. package/dist/createQuery.js +2 -1
  26. package/dist/createQuery.js.map +1 -1
  27. package/dist/index.cjs +18 -0
  28. package/dist/index.d.cts +3 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -0
  31. package/dist/types.d.cts +140 -1
  32. package/dist/types.d.ts +140 -1
  33. package/package.json +4 -3
  34. package/src/createBaseQuery.ts +67 -1
  35. package/src/createCacheAction.ts +172 -0
  36. package/src/createInfiniteQuery.ts +1 -0
  37. package/src/createQueries.ts +442 -0
  38. package/src/createQuery.ts +1 -0
  39. package/src/index.ts +16 -0
  40. package/src/types.ts +175 -0
package/dist/types.d.cts CHANGED
@@ -118,6 +118,30 @@ interface QueryResult<TData, TError = Error> {
118
118
  * `$queryClient` and honors `fork({ values: [[$queryClient, qc]] })`.
119
119
  */
120
120
  $queryClient: Store<QueryClient | null>;
121
+ /**
122
+ * Lifecycle events for `sample`-driven reactions to fetch completion.
123
+ *
124
+ * - `success` fires with the (post-`select`) data on every newly-finished
125
+ * successful fetch — fresh fetch, refetch, reactive key change, or a
126
+ * cross-scope `setQueryData`.
127
+ * - `failure` fires with the error on every failed fetch.
128
+ *
129
+ * Neither fires for the baseline state observed on `mounted()` (e.g.
130
+ * SSR-hydrated cache data) — the events track *new* fetches, not the
131
+ * initial observation. Each fork scope tracks its own baseline.
132
+ *
133
+ * @example
134
+ * sample({ clock: userQuery.finished.success, target: loadSettings })
135
+ * sample({
136
+ * clock: userQuery.finished.failure,
137
+ * fn: (err) => `Failed: ${err.message}`,
138
+ * target: showToast,
139
+ * })
140
+ */
141
+ finished: {
142
+ success: Event<TData>;
143
+ failure: Event<TError>;
144
+ };
121
145
  }
122
146
  interface CreateInfiniteQueryOptions<TQueryFnData = unknown, TError = Error, TPageParam = unknown, TData = InfiniteData<TQueryFnData, TPageParam>, TQueryKey extends EffectorQueryKey = EffectorQueryKey> extends Omit<InfiniteQueryObserverOptions<TQueryFnData, TError, TData, ResolvedQueryKey<TQueryKey>, TPageParam>, 'queryKey' | 'enabled' | 'refetchInterval'> {
123
147
  queryKey: TQueryKey;
@@ -158,6 +182,11 @@ interface InfiniteQueryResult<TData, TError = Error, TPageParam = unknown> {
158
182
  $observer: Store<InfiniteQueryObserver<any, TError, TData, QueryKey, TPageParam> | null>;
159
183
  /** See {@link QueryResult.$queryClient}. */
160
184
  $queryClient: Store<QueryClient | null>;
185
+ /** See {@link QueryResult.finished}. */
186
+ finished: {
187
+ success: Event<TData>;
188
+ failure: Event<TError>;
189
+ };
161
190
  }
162
191
  type CreateMutationOptions<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> = MutationObserverOptions<TData, TError, TVariables, TOnMutateResult> & {
163
192
  /** See {@link CreateQueryOptions.name}. */
@@ -229,5 +258,115 @@ interface MutationResult<TData = unknown, TError = Error, TVariables = void> {
229
258
  }>;
230
259
  };
231
260
  }
261
+ /**
262
+ * Per-item snapshot inside a `createQueries` family. Parallel to the
263
+ * factory's `source` array — one entry per item, in source order.
264
+ */
265
+ interface QueryItemState<TItem, TData, TError = Error> {
266
+ /** The source item this entry was derived from. */
267
+ source: TItem;
268
+ data: TData | undefined;
269
+ error: TError | null;
270
+ status: QueryStatus;
271
+ isPending: boolean;
272
+ isFetching: boolean;
273
+ isSuccess: boolean;
274
+ isError: boolean;
275
+ isPlaceholderData: boolean;
276
+ fetchStatus: FetchStatus;
277
+ }
278
+ /**
279
+ * Per-item query options produced by `createQueries({ query })`. A pure
280
+ * function of one source item — no effector stores, no closures over
281
+ * mutable state. Reactivity comes from the source store; whenever it
282
+ * updates, this callback re-runs to compute fresh options.
283
+ *
284
+ * `enabled` is a plain boolean (not a `Store`) for the same reason —
285
+ * it's derived from the item, so reactivity is already covered.
286
+ */
287
+ interface CreateQueriesItemOptions<TQueryFnData, TError, TData, TQueryKey extends ReadonlyArray<unknown>> extends Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'queryKey' | 'enabled'> {
288
+ queryKey: TQueryKey;
289
+ enabled?: boolean;
290
+ }
291
+ interface CreateQueriesOptions<TItem, TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends ReadonlyArray<unknown> = ReadonlyArray<unknown>> {
292
+ /**
293
+ * Stable name used to derive the SID of the result `$items` store so
294
+ * `serialize(scope)` round-trips it for SSR. Without a name, the
295
+ * QueryClient cache still hydrates via `dehydrate`/`hydrate`, but the
296
+ * `$items` snapshot is silently dropped from `serialize(scope)`.
297
+ */
298
+ name?: string;
299
+ /**
300
+ * Reactive list of items. Each item becomes one parallel query in the
301
+ * family. Adding / removing items updates the family (spawn / dispose
302
+ * observer). Order is preserved in `$items`.
303
+ *
304
+ * Duplicates in `source` deduplicate observers (same `queryKey` hash)
305
+ * but produce separate `$items` entries — one per occurrence.
306
+ */
307
+ source: Store<ReadonlyArray<TItem>>;
308
+ /**
309
+ * Per-item options builder. MUST be pure — same item in, same options
310
+ * out. Reactivity is driven by the source store; this callback fires
311
+ * synchronously when source updates.
312
+ */
313
+ query: (item: TItem) => CreateQueriesItemOptions<TQueryFnData, TError, TData, TQueryKey>;
314
+ /** Shared QueryObserver defaults applied on top of `query(item)`. */
315
+ staleTime?: number;
316
+ gcTime?: number;
317
+ retry?: QueryObserverOptions<TQueryFnData, TError, TData>['retry'];
318
+ retryDelay?: QueryObserverOptions<TQueryFnData, TError, TData>['retryDelay'];
319
+ refetchOnMount?: QueryObserverOptions<TQueryFnData, TError, TData>['refetchOnMount'];
320
+ refetchOnReconnect?: QueryObserverOptions<TQueryFnData, TError, TData>['refetchOnReconnect'];
321
+ refetchOnWindowFocus?: QueryObserverOptions<TQueryFnData, TError, TData>['refetchOnWindowFocus'];
322
+ networkMode?: QueryObserverOptions<TQueryFnData, TError, TData>['networkMode'];
323
+ }
324
+ /**
325
+ * Result of `createQueries(...)`. A reactive "family" of parallel
326
+ * queries indexed by a source store. Composes naturally with
327
+ * `useUnit($items)` for non-Suspense usage and with
328
+ * `useQueries(family)` / `useSuspenseQueries(family)` for React-style
329
+ * consumption.
330
+ */
331
+ interface QueriesResult<TItem, TData = unknown, TError = Error> {
332
+ /** Per-item snapshots, parallel to `source`. */
333
+ $items: Store<ReadonlyArray<QueryItemState<TItem, TData, TError>>>;
334
+ /** Just the `data` field of `$items` — shortcut for common UIs. */
335
+ $data: Store<ReadonlyArray<TData | undefined>>;
336
+ /** `true` while **any** query in the family is pending. */
337
+ $isPending: Store<boolean>;
338
+ /** `true` only when **every** query in the family has succeeded. */
339
+ $isSuccess: Store<boolean>;
340
+ /** `true` if **any** query in the family is currently fetching. */
341
+ $isFetching: Store<boolean>;
342
+ /** `true` if **any** query in the family errored. */
343
+ $isError: Store<boolean>;
344
+ /**
345
+ * Triggers a parallel `fetchQuery` for every current source item.
346
+ * SSR-friendly — `await allSettled(family.prefetch, { scope })`
347
+ * returns after every queryFn has resolved (or failed).
348
+ */
349
+ prefetch: EventCallable<void>;
350
+ /**
351
+ * Increment the per-scope refcount and ensure every observer is
352
+ * subscribed to the QueryClient. Call from `useEffect` (or its
353
+ * effector equivalent). The matching `unmounted()` decrements; when
354
+ * the count hits zero, observers unsubscribe.
355
+ */
356
+ mounted: EventCallable<void>;
357
+ unmounted: EventCallable<void>;
358
+ /** Invalidates every query in the family — re-fetches in background. */
359
+ refresh: EventCallable<void>;
360
+ /** Invalidates one specific item's query. */
361
+ refreshOne: EventCallable<TItem>;
362
+ /** See {@link QueryResult.$queryClient}. */
363
+ $queryClient: Store<QueryClient | null>;
364
+ /**
365
+ * Discriminator that lets `useQueries` / `useSuspenseQueries` and
366
+ * other helpers distinguish a family from a tuple of factories at
367
+ * runtime. Not part of the public API.
368
+ */
369
+ readonly __family: true;
370
+ }
232
371
 
233
- export type { CreateInfiniteQueryOptions, CreateMutationOptions, CreateQueryOptions, EffectorQueryKey, InfiniteQueryResult, MutationResult, MutationStatus, QueryResult, ResolveQueryKeyElement, ResolvedQueryKey, StoreOrValue };
372
+ export type { CreateInfiniteQueryOptions, CreateMutationOptions, CreateQueriesItemOptions, CreateQueriesOptions, CreateQueryOptions, EffectorQueryKey, InfiniteQueryResult, MutationResult, MutationStatus, QueriesResult, QueryItemState, QueryResult, ResolveQueryKeyElement, ResolvedQueryKey, StoreOrValue };
package/dist/types.d.ts CHANGED
@@ -118,6 +118,30 @@ interface QueryResult<TData, TError = Error> {
118
118
  * `$queryClient` and honors `fork({ values: [[$queryClient, qc]] })`.
119
119
  */
120
120
  $queryClient: Store<QueryClient | null>;
121
+ /**
122
+ * Lifecycle events for `sample`-driven reactions to fetch completion.
123
+ *
124
+ * - `success` fires with the (post-`select`) data on every newly-finished
125
+ * successful fetch — fresh fetch, refetch, reactive key change, or a
126
+ * cross-scope `setQueryData`.
127
+ * - `failure` fires with the error on every failed fetch.
128
+ *
129
+ * Neither fires for the baseline state observed on `mounted()` (e.g.
130
+ * SSR-hydrated cache data) — the events track *new* fetches, not the
131
+ * initial observation. Each fork scope tracks its own baseline.
132
+ *
133
+ * @example
134
+ * sample({ clock: userQuery.finished.success, target: loadSettings })
135
+ * sample({
136
+ * clock: userQuery.finished.failure,
137
+ * fn: (err) => `Failed: ${err.message}`,
138
+ * target: showToast,
139
+ * })
140
+ */
141
+ finished: {
142
+ success: Event<TData>;
143
+ failure: Event<TError>;
144
+ };
121
145
  }
122
146
  interface CreateInfiniteQueryOptions<TQueryFnData = unknown, TError = Error, TPageParam = unknown, TData = InfiniteData<TQueryFnData, TPageParam>, TQueryKey extends EffectorQueryKey = EffectorQueryKey> extends Omit<InfiniteQueryObserverOptions<TQueryFnData, TError, TData, ResolvedQueryKey<TQueryKey>, TPageParam>, 'queryKey' | 'enabled' | 'refetchInterval'> {
123
147
  queryKey: TQueryKey;
@@ -158,6 +182,11 @@ interface InfiniteQueryResult<TData, TError = Error, TPageParam = unknown> {
158
182
  $observer: Store<InfiniteQueryObserver<any, TError, TData, QueryKey, TPageParam> | null>;
159
183
  /** See {@link QueryResult.$queryClient}. */
160
184
  $queryClient: Store<QueryClient | null>;
185
+ /** See {@link QueryResult.finished}. */
186
+ finished: {
187
+ success: Event<TData>;
188
+ failure: Event<TError>;
189
+ };
161
190
  }
162
191
  type CreateMutationOptions<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> = MutationObserverOptions<TData, TError, TVariables, TOnMutateResult> & {
163
192
  /** See {@link CreateQueryOptions.name}. */
@@ -229,5 +258,115 @@ interface MutationResult<TData = unknown, TError = Error, TVariables = void> {
229
258
  }>;
230
259
  };
231
260
  }
261
+ /**
262
+ * Per-item snapshot inside a `createQueries` family. Parallel to the
263
+ * factory's `source` array — one entry per item, in source order.
264
+ */
265
+ interface QueryItemState<TItem, TData, TError = Error> {
266
+ /** The source item this entry was derived from. */
267
+ source: TItem;
268
+ data: TData | undefined;
269
+ error: TError | null;
270
+ status: QueryStatus;
271
+ isPending: boolean;
272
+ isFetching: boolean;
273
+ isSuccess: boolean;
274
+ isError: boolean;
275
+ isPlaceholderData: boolean;
276
+ fetchStatus: FetchStatus;
277
+ }
278
+ /**
279
+ * Per-item query options produced by `createQueries({ query })`. A pure
280
+ * function of one source item — no effector stores, no closures over
281
+ * mutable state. Reactivity comes from the source store; whenever it
282
+ * updates, this callback re-runs to compute fresh options.
283
+ *
284
+ * `enabled` is a plain boolean (not a `Store`) for the same reason —
285
+ * it's derived from the item, so reactivity is already covered.
286
+ */
287
+ interface CreateQueriesItemOptions<TQueryFnData, TError, TData, TQueryKey extends ReadonlyArray<unknown>> extends Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'queryKey' | 'enabled'> {
288
+ queryKey: TQueryKey;
289
+ enabled?: boolean;
290
+ }
291
+ interface CreateQueriesOptions<TItem, TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends ReadonlyArray<unknown> = ReadonlyArray<unknown>> {
292
+ /**
293
+ * Stable name used to derive the SID of the result `$items` store so
294
+ * `serialize(scope)` round-trips it for SSR. Without a name, the
295
+ * QueryClient cache still hydrates via `dehydrate`/`hydrate`, but the
296
+ * `$items` snapshot is silently dropped from `serialize(scope)`.
297
+ */
298
+ name?: string;
299
+ /**
300
+ * Reactive list of items. Each item becomes one parallel query in the
301
+ * family. Adding / removing items updates the family (spawn / dispose
302
+ * observer). Order is preserved in `$items`.
303
+ *
304
+ * Duplicates in `source` deduplicate observers (same `queryKey` hash)
305
+ * but produce separate `$items` entries — one per occurrence.
306
+ */
307
+ source: Store<ReadonlyArray<TItem>>;
308
+ /**
309
+ * Per-item options builder. MUST be pure — same item in, same options
310
+ * out. Reactivity is driven by the source store; this callback fires
311
+ * synchronously when source updates.
312
+ */
313
+ query: (item: TItem) => CreateQueriesItemOptions<TQueryFnData, TError, TData, TQueryKey>;
314
+ /** Shared QueryObserver defaults applied on top of `query(item)`. */
315
+ staleTime?: number;
316
+ gcTime?: number;
317
+ retry?: QueryObserverOptions<TQueryFnData, TError, TData>['retry'];
318
+ retryDelay?: QueryObserverOptions<TQueryFnData, TError, TData>['retryDelay'];
319
+ refetchOnMount?: QueryObserverOptions<TQueryFnData, TError, TData>['refetchOnMount'];
320
+ refetchOnReconnect?: QueryObserverOptions<TQueryFnData, TError, TData>['refetchOnReconnect'];
321
+ refetchOnWindowFocus?: QueryObserverOptions<TQueryFnData, TError, TData>['refetchOnWindowFocus'];
322
+ networkMode?: QueryObserverOptions<TQueryFnData, TError, TData>['networkMode'];
323
+ }
324
+ /**
325
+ * Result of `createQueries(...)`. A reactive "family" of parallel
326
+ * queries indexed by a source store. Composes naturally with
327
+ * `useUnit($items)` for non-Suspense usage and with
328
+ * `useQueries(family)` / `useSuspenseQueries(family)` for React-style
329
+ * consumption.
330
+ */
331
+ interface QueriesResult<TItem, TData = unknown, TError = Error> {
332
+ /** Per-item snapshots, parallel to `source`. */
333
+ $items: Store<ReadonlyArray<QueryItemState<TItem, TData, TError>>>;
334
+ /** Just the `data` field of `$items` — shortcut for common UIs. */
335
+ $data: Store<ReadonlyArray<TData | undefined>>;
336
+ /** `true` while **any** query in the family is pending. */
337
+ $isPending: Store<boolean>;
338
+ /** `true` only when **every** query in the family has succeeded. */
339
+ $isSuccess: Store<boolean>;
340
+ /** `true` if **any** query in the family is currently fetching. */
341
+ $isFetching: Store<boolean>;
342
+ /** `true` if **any** query in the family errored. */
343
+ $isError: Store<boolean>;
344
+ /**
345
+ * Triggers a parallel `fetchQuery` for every current source item.
346
+ * SSR-friendly — `await allSettled(family.prefetch, { scope })`
347
+ * returns after every queryFn has resolved (or failed).
348
+ */
349
+ prefetch: EventCallable<void>;
350
+ /**
351
+ * Increment the per-scope refcount and ensure every observer is
352
+ * subscribed to the QueryClient. Call from `useEffect` (or its
353
+ * effector equivalent). The matching `unmounted()` decrements; when
354
+ * the count hits zero, observers unsubscribe.
355
+ */
356
+ mounted: EventCallable<void>;
357
+ unmounted: EventCallable<void>;
358
+ /** Invalidates every query in the family — re-fetches in background. */
359
+ refresh: EventCallable<void>;
360
+ /** Invalidates one specific item's query. */
361
+ refreshOne: EventCallable<TItem>;
362
+ /** See {@link QueryResult.$queryClient}. */
363
+ $queryClient: Store<QueryClient | null>;
364
+ /**
365
+ * Discriminator that lets `useQueries` / `useSuspenseQueries` and
366
+ * other helpers distinguish a family from a tuple of factories at
367
+ * runtime. Not part of the public API.
368
+ */
369
+ readonly __family: true;
370
+ }
232
371
 
233
- export type { CreateInfiniteQueryOptions, CreateMutationOptions, CreateQueryOptions, EffectorQueryKey, InfiniteQueryResult, MutationResult, MutationStatus, QueryResult, ResolveQueryKeyElement, ResolvedQueryKey, StoreOrValue };
372
+ export type { CreateInfiniteQueryOptions, CreateMutationOptions, CreateQueriesItemOptions, CreateQueriesOptions, CreateQueryOptions, EffectorQueryKey, InfiniteQueryResult, MutationResult, MutationStatus, QueriesResult, QueryItemState, QueryResult, ResolveQueryKeyElement, ResolvedQueryKey, StoreOrValue };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@effector-tanstack-query/core",
3
- "version": "0.3.0",
4
- "description": "Effector bindings for TanStack Query — core factories (createQuery, createMutation, createInfiniteQuery)",
3
+ "version": "0.5.0",
4
+ "description": "Effector bindings for TanStack Query — core factories (createQuery, createMutation, createInfiniteQuery, createQueries) plus invalidate / cancel / remove / reset cache actions",
5
5
  "license": "MIT",
6
6
  "author": "Ilya Agarkov <ilya.al.ag@gmail.com>",
7
7
  "repository": {
@@ -43,10 +43,11 @@
43
43
  "!src/__tests__"
44
44
  ],
45
45
  "sideEffects": false,
46
- "dependencies": {
46
+ "devDependencies": {
47
47
  "@tanstack/query-core": "^5.100.10"
48
48
  },
49
49
  "peerDependencies": {
50
+ "@tanstack/query-core": "^5.0.0",
50
51
  "effector": ">=23.0.0"
51
52
  },
52
53
  "scripts": {
@@ -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
+ }
@@ -272,6 +272,7 @@ export function createInfiniteQuery<
272
272
  prefetch,
273
273
  mounted: base.mounted,
274
274
  unmounted: base.unmounted,
275
+ finished: base.finished,
275
276
  }
276
277
 
277
278
  Object.defineProperty(result, '__createObserver', {