@effector-tanstack-query/core 0.3.0 → 0.4.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.
@@ -0,0 +1,442 @@
1
+ import {
2
+ attach,
3
+ createEvent,
4
+ createStore,
5
+ sample,
6
+ scopeBind,
7
+ type Store,
8
+ } from 'effector'
9
+ import { QueryObserver, hashKey } from '@tanstack/query-core'
10
+ import type { QueryClient } from '@tanstack/query-core'
11
+ import { $queryClient as $globalQueryClient } from './queryClient'
12
+ import { sidConfig, warnMissingName } from './createBaseQuery'
13
+ import type {
14
+ CreateQueriesOptions,
15
+ QueriesResult,
16
+ QueryItemState,
17
+ } from './types'
18
+
19
+ interface ObserverEntry<TData, TError> {
20
+ observer: QueryObserver<TData, TError>
21
+ unsubscribe: (() => void) | null
22
+ }
23
+
24
+ const EMPTY_ITEMS: ReadonlyArray<QueryItemState<unknown, unknown, unknown>> = []
25
+
26
+ /**
27
+ * Reactive family of parallel queries indexed by a source store. See
28
+ * {@link CreateQueriesOptions} for the input shape and
29
+ * {@link QueriesResult} for the output API.
30
+ *
31
+ * Internals:
32
+ *
33
+ * - Each `source` element gets one `QueryObserver` (deduplicated by
34
+ * `hashKey(query(item).queryKey)`). Observers live in a per-scope
35
+ * `Map` held in a `serialize: 'ignore'` store (instances can't ride
36
+ * through `serialize(scope)`).
37
+ * - On every `source` update the family diffs prev/next, spawns
38
+ * observers for new items, disposes observers for removed items,
39
+ * and updates `$items`. Re-ordering without add/remove is cheap —
40
+ * observers stay, `$items` just re-projects.
41
+ * - Observer subscriptions are reference-counted via `mounted()` /
42
+ * `unmounted()`. First mount subscribes every observer; last
43
+ * unmount unsubscribes. Subsequent source changes while mounted
44
+ * auto-subscribe new observers.
45
+ * - SSR works via `prefetch`: triggers `qc.fetchQuery(...)` for every
46
+ * current source item in parallel, populates the QC cache, then
47
+ * mounts observers so `$items` snapshot carries the data through
48
+ * `serialize(scope)`.
49
+ */
50
+ export function createQueries<
51
+ TItem,
52
+ TQueryFnData = unknown,
53
+ TError = Error,
54
+ TData = TQueryFnData,
55
+ TQueryKey extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
56
+ >(
57
+ options: CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
58
+ ): QueriesResult<TItem, TData, TError>
59
+ export function createQueries<
60
+ TItem,
61
+ TQueryFnData = unknown,
62
+ TError = Error,
63
+ TData = TQueryFnData,
64
+ TQueryKey extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
65
+ >(
66
+ queryClient: QueryClient,
67
+ options: CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
68
+ ): QueriesResult<TItem, TData, TError>
69
+ export function createQueries<
70
+ TItem,
71
+ TQueryFnData = unknown,
72
+ TError = Error,
73
+ TData = TQueryFnData,
74
+ TQueryKey extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
75
+ >(
76
+ arg1:
77
+ | QueryClient
78
+ | CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
79
+ arg2?: CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
80
+ ): QueriesResult<TItem, TData, TError> {
81
+ const [explicitClient, options] = parseArgs<
82
+ TItem,
83
+ TQueryFnData,
84
+ TError,
85
+ TData,
86
+ TQueryKey
87
+ >(arg1, arg2)
88
+
89
+ const { name, source, query, ...sharedOptions } = options
90
+
91
+ if (!name) warnMissingName('createQueries')
92
+
93
+ const $queryClient: Store<QueryClient | null> = explicitClient
94
+ ? createStore(explicitClient as QueryClient | null, {
95
+ serialize: 'ignore',
96
+ })
97
+ : $globalQueryClient
98
+
99
+ // Per-scope observer storage. Map keyed by queryKey hash.
100
+ const $observers = createStore<Map<string, ObserverEntry<TData, TError>>>(
101
+ new Map(),
102
+ { serialize: 'ignore' },
103
+ )
104
+
105
+ // Per-scope ref-count: number of active consumers that called mounted().
106
+ // First mount subscribes observers, last unmount unsubscribes them.
107
+ const $refCount = createStore<number>(0, { serialize: 'ignore' })
108
+
109
+ // The serializable result snapshot.
110
+ const itemsUpdated = createEvent<
111
+ ReadonlyArray<QueryItemState<TItem, TData, TError>>
112
+ >()
113
+ const $items = createStore<
114
+ ReadonlyArray<QueryItemState<TItem, TData, TError>>
115
+ >(EMPTY_ITEMS as ReadonlyArray<QueryItemState<TItem, TData, TError>>, {
116
+ ...sidConfig(name, '$items'),
117
+ }).on(itemsUpdated, (_, items) => items)
118
+
119
+ const observersChanged = createEvent<
120
+ Map<string, ObserverEntry<TData, TError>>
121
+ >()
122
+ $observers.on(observersChanged, (_, next) => next)
123
+
124
+ const refCountChanged = createEvent<number>()
125
+ $refCount.on(refCountChanged, (_, next) => next)
126
+
127
+ const mounted = createEvent<void>()
128
+ const unmounted = createEvent<void>()
129
+ const refresh = createEvent<void>()
130
+ const refreshOne = createEvent<TItem>()
131
+ const prefetch = createEvent<void>()
132
+
133
+ // Snapshot helpers — pure, run from effects.
134
+ function itemsFromObservers(
135
+ src: ReadonlyArray<TItem>,
136
+ observers: Map<string, ObserverEntry<TData, TError>>,
137
+ ): ReadonlyArray<QueryItemState<TItem, TData, TError>> {
138
+ return src.map((item) => {
139
+ const opts = query(item)
140
+ const hash = hashKey(opts.queryKey as ReadonlyArray<unknown>)
141
+ const entry = observers.get(hash)
142
+ if (!entry) return defaultItemState(item)
143
+ const r = entry.observer.getCurrentResult()
144
+ return {
145
+ source: item,
146
+ data: r.data,
147
+ error: r.error,
148
+ status: r.status,
149
+ isPending: r.isPending,
150
+ isFetching: r.isFetching,
151
+ isSuccess: r.isSuccess,
152
+ isError: r.isError,
153
+ isPlaceholderData: r.isPlaceholderData,
154
+ fetchStatus: r.fetchStatus,
155
+ }
156
+ })
157
+ }
158
+
159
+ function buildObserverOptions(item: TItem) {
160
+ const itemOpts = query(item)
161
+ return {
162
+ ...sharedOptions,
163
+ ...itemOpts,
164
+ enabled: itemOpts.enabled ?? true,
165
+ } as ConstructorParameters<typeof QueryObserver<TQueryFnData, TError, TData>>[1]
166
+ }
167
+
168
+ // Diff source against current observers. Spawns / disposes observers
169
+ // to match the desired set; subscribes the ones that should be live
170
+ // given the current mount state. Idempotent — calling twice with the
171
+ // same input + state is a no-op.
172
+ function diffSource(
173
+ qc: QueryClient,
174
+ src: ReadonlyArray<TItem>,
175
+ prev: Map<string, ObserverEntry<TData, TError>>,
176
+ isMounted: boolean,
177
+ dispatchRecompute: () => void,
178
+ ): {
179
+ next: Map<string, ObserverEntry<TData, TError>>
180
+ items: ReadonlyArray<QueryItemState<TItem, TData, TError>>
181
+ } {
182
+ const next = new Map(prev)
183
+ const keep = new Set<string>()
184
+
185
+ for (const item of src) {
186
+ const opts = query(item)
187
+ const hash = hashKey(opts.queryKey as ReadonlyArray<unknown>)
188
+ keep.add(hash)
189
+ let entry = next.get(hash)
190
+ if (!entry) {
191
+ const obs = new QueryObserver<TQueryFnData, TError, TData>(
192
+ qc,
193
+ buildObserverOptions(item) as any,
194
+ )
195
+ entry = {
196
+ observer: obs as unknown as QueryObserver<TData, TError>,
197
+ unsubscribe: null,
198
+ }
199
+ next.set(hash, entry)
200
+ } else {
201
+ // Same queryKey but `query(item)` might have changed `enabled`
202
+ // (or other passthrough options). Keep observer options in sync.
203
+ entry.observer.setOptions({
204
+ ...entry.observer.options,
205
+ ...(buildObserverOptions(item) as any),
206
+ })
207
+ }
208
+ // Ensure subscription state matches mount state — covers both
209
+ // newly-spawned observers and pre-existing observers that were
210
+ // created while unmounted (`syncFx` ran on source change before
211
+ // anyone called `mounted()`).
212
+ if (isMounted && !entry.unsubscribe) {
213
+ entry.unsubscribe = entry.observer.subscribe(dispatchRecompute)
214
+ }
215
+ }
216
+
217
+ for (const [hash, entry] of next) {
218
+ if (!keep.has(hash)) {
219
+ entry.unsubscribe?.()
220
+ entry.observer.destroy()
221
+ next.delete(hash)
222
+ }
223
+ }
224
+
225
+ return { next, items: itemsFromObservers(src, next) }
226
+ }
227
+
228
+ // Triggered whenever an observer emits a result (subscribe callback)
229
+ // OR when source changes — re-projects $items.
230
+ const recomputeFx = attach({
231
+ source: { observers: $observers, currentSource: source },
232
+ effect: ({ observers, currentSource }) => {
233
+ return itemsFromObservers(currentSource, observers)
234
+ },
235
+ })
236
+ sample({ clock: recomputeFx.doneData, target: itemsUpdated })
237
+
238
+ // Source sync effect — applies diff, mutates observer subscriptions
239
+ // when mounted, then dispatches the items snapshot. When the scope
240
+ // has no QueryClient yet we still run, but the diff produces an
241
+ // empty observer set (no `new QueryObserver(qc, ...)` calls), so
242
+ // `$items` shows default-pending state and observers spawn the
243
+ // moment qc becomes available.
244
+ const syncFx = attach({
245
+ source: {
246
+ qc: $queryClient,
247
+ observers: $observers,
248
+ refCount: $refCount,
249
+ currentSource: source,
250
+ },
251
+ effect: ({ qc, observers, refCount, currentSource }) => {
252
+ if (!qc) {
253
+ return {
254
+ next: observers,
255
+ items: currentSource.map(defaultItemState),
256
+ }
257
+ }
258
+ const dispatchRecompute = scopeBind(recomputeFx, { safe: true })
259
+ return diffSource(
260
+ qc,
261
+ currentSource,
262
+ observers,
263
+ refCount > 0,
264
+ () => dispatchRecompute(),
265
+ )
266
+ },
267
+ })
268
+ sample({
269
+ clock: syncFx.doneData,
270
+ fn: (payload) => payload.next,
271
+ target: observersChanged,
272
+ })
273
+ sample({
274
+ clock: syncFx.doneData,
275
+ fn: (payload) => payload.items,
276
+ target: itemsUpdated,
277
+ })
278
+
279
+ sample({ clock: source, target: syncFx })
280
+ sample({ clock: $queryClient, target: syncFx })
281
+
282
+ // Mount lifecycle — increment refCount, then run syncFx so observers
283
+ // get spawned (if missing) and subscribed (because `refCount > 0`
284
+ // now). Same path covers the first-ever mount AND
285
+ // existing-but-unsubscribed observers (created while unmounted, e.g.
286
+ // on a source-change in a not-yet-mounted scope).
287
+ const mountFx = attach({
288
+ source: $refCount,
289
+ effect: (refCount) => refCount + 1,
290
+ })
291
+ sample({ clock: mounted, target: mountFx })
292
+ sample({ clock: mountFx.doneData, target: refCountChanged })
293
+ sample({ clock: mountFx.done, target: syncFx })
294
+
295
+ const unmountFx = attach({
296
+ source: { observers: $observers, refCount: $refCount },
297
+ effect: ({ observers, refCount }) => {
298
+ const nextRefCount = Math.max(0, refCount - 1)
299
+ if (nextRefCount === 0) {
300
+ for (const entry of observers.values()) {
301
+ entry.unsubscribe?.()
302
+ entry.unsubscribe = null
303
+ }
304
+ }
305
+ return nextRefCount
306
+ },
307
+ })
308
+ sample({ clock: unmounted, target: unmountFx })
309
+ sample({ clock: unmountFx.doneData, target: refCountChanged })
310
+
311
+ // Refresh — invalidate all observed queries; observer subscribers
312
+ // pick up the refetched data and refresh $items via recomputeFx.
313
+ const refreshFx = attach({
314
+ source: $observers,
315
+ effect: async (observers) => {
316
+ await Promise.all(
317
+ [...observers.values()].map((entry) =>
318
+ entry.observer.refetch().catch(() => undefined),
319
+ ),
320
+ )
321
+ },
322
+ })
323
+ sample({ clock: refresh, target: refreshFx })
324
+
325
+ const refreshOneFx = attach({
326
+ source: $observers,
327
+ effect: async (
328
+ observers,
329
+ item: TItem,
330
+ ) => {
331
+ const opts = query(item)
332
+ const hash = hashKey(opts.queryKey as ReadonlyArray<unknown>)
333
+ const entry = observers.get(hash)
334
+ if (!entry) return
335
+ await entry.observer.refetch().catch(() => undefined)
336
+ },
337
+ })
338
+ sample({ clock: refreshOne, target: refreshOneFx })
339
+
340
+ // Prefetch — for SSR. Walks source, calls qc.fetchQuery for each item
341
+ // in parallel, then re-runs syncFx so $items reflects the populated
342
+ // cache.
343
+ const prefetchFx = attach({
344
+ source: { qc: $queryClient, currentSource: source },
345
+ effect: async ({ qc, currentSource }) => {
346
+ if (!qc) return
347
+ await Promise.all(
348
+ currentSource.map((item) => {
349
+ const opts = query(item)
350
+ if (opts.enabled === false) return Promise.resolve()
351
+ return qc
352
+ .fetchQuery({
353
+ ...sharedOptions,
354
+ ...opts,
355
+ } as any)
356
+ .catch(() => undefined)
357
+ }),
358
+ )
359
+ },
360
+ })
361
+ sample({ clock: prefetch, target: prefetchFx })
362
+ // After prefetch resolves, re-sync to update $items from QC cache.
363
+ sample({ clock: prefetchFx.done, target: syncFx })
364
+
365
+ // Derived stores — convenience views.
366
+ const $data = $items.map((items) => items.map((it) => it.data))
367
+ const $isPending = $items.map((items) =>
368
+ items.length === 0 ? false : items.some((it) => it.isPending),
369
+ )
370
+ const $isSuccess = $items.map((items) =>
371
+ items.length === 0 ? true : items.every((it) => it.isSuccess),
372
+ )
373
+ const $isFetching = $items.map((items) => items.some((it) => it.isFetching))
374
+ const $isError = $items.map((items) => items.some((it) => it.isError))
375
+
376
+ const result: QueriesResult<TItem, TData, TError> = {
377
+ $items,
378
+ $data,
379
+ $isPending,
380
+ $isSuccess,
381
+ $isFetching,
382
+ $isError,
383
+ mounted,
384
+ unmounted,
385
+ refresh,
386
+ refreshOne,
387
+ prefetch,
388
+ $queryClient,
389
+ __family: true,
390
+ }
391
+
392
+ // Internal: callable from the suspense hook to reconstruct the
393
+ // QueryObserver options for one source item (with sharedOptions
394
+ // merged on top of `query(item)` and the default `enabled: true`).
395
+ // The hook then feeds this into `qc.fetchQuery` to obtain a
396
+ // deduped-by-queryHash inflight promise to throw at React.
397
+ Object.defineProperty(result, '__queryFor', {
398
+ enumerable: false,
399
+ value: buildObserverOptions,
400
+ })
401
+
402
+ return result
403
+
404
+ function defaultItemState(item: TItem): QueryItemState<TItem, TData, TError> {
405
+ return {
406
+ source: item,
407
+ data: undefined,
408
+ error: null,
409
+ status: 'pending',
410
+ isPending: true,
411
+ isFetching: false,
412
+ isSuccess: false,
413
+ isError: false,
414
+ isPlaceholderData: false,
415
+ fetchStatus: 'idle',
416
+ }
417
+ }
418
+ }
419
+
420
+ function parseArgs<TItem, TQueryFnData, TError, TData, TQueryKey extends ReadonlyArray<unknown>>(
421
+ arg1:
422
+ | QueryClient
423
+ | CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
424
+ arg2?: CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
425
+ ): [
426
+ QueryClient | null,
427
+ CreateQueriesOptions<TItem, TQueryFnData, TError, TData, TQueryKey>,
428
+ ] {
429
+ if (arg2 !== undefined) {
430
+ return [arg1 as QueryClient, arg2]
431
+ }
432
+ return [
433
+ null,
434
+ arg1 as CreateQueriesOptions<
435
+ TItem,
436
+ TQueryFnData,
437
+ TError,
438
+ TData,
439
+ TQueryKey
440
+ >,
441
+ ]
442
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createQuery } from './createQuery'
2
2
  export { createInfiniteQuery } from './createInfiniteQuery'
3
+ export { createQueries } from './createQueries'
3
4
  export { createMutation } from './createMutation'
4
5
  export { createInvalidate } from './createInvalidate'
5
6
  export type { CreateInvalidateOptions } from './createInvalidate'
@@ -12,11 +13,15 @@ export type {
12
13
  export type {
13
14
  CreateInfiniteQueryOptions,
14
15
  CreateMutationOptions,
16
+ CreateQueriesItemOptions,
17
+ CreateQueriesOptions,
15
18
  CreateQueryOptions,
16
19
  EffectorQueryKey,
17
20
  InfiniteQueryResult,
18
21
  MutationResult,
19
22
  MutationStatus,
23
+ QueriesResult,
24
+ QueryItemState,
20
25
  QueryResult,
21
26
  StoreOrValue,
22
27
  } from './types'
package/src/types.ts CHANGED
@@ -308,3 +308,149 @@ export interface MutationResult<
308
308
  failure: Event<{ params: TVariables; error: TError }>
309
309
  }
310
310
  }
311
+
312
+ /**
313
+ * Per-item snapshot inside a `createQueries` family. Parallel to the
314
+ * factory's `source` array — one entry per item, in source order.
315
+ */
316
+ export interface QueryItemState<TItem, TData, TError = Error> {
317
+ /** The source item this entry was derived from. */
318
+ source: TItem
319
+ data: TData | undefined
320
+ error: TError | null
321
+ status: QueryStatus
322
+ isPending: boolean
323
+ isFetching: boolean
324
+ isSuccess: boolean
325
+ isError: boolean
326
+ isPlaceholderData: boolean
327
+ fetchStatus: FetchStatus
328
+ }
329
+
330
+ /**
331
+ * Per-item query options produced by `createQueries({ query })`. A pure
332
+ * function of one source item — no effector stores, no closures over
333
+ * mutable state. Reactivity comes from the source store; whenever it
334
+ * updates, this callback re-runs to compute fresh options.
335
+ *
336
+ * `enabled` is a plain boolean (not a `Store`) for the same reason —
337
+ * it's derived from the item, so reactivity is already covered.
338
+ */
339
+ export interface CreateQueriesItemOptions<
340
+ TQueryFnData,
341
+ TError,
342
+ TData,
343
+ TQueryKey extends ReadonlyArray<unknown>,
344
+ > extends Omit<
345
+ QueryObserverOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
346
+ 'queryKey' | 'enabled'
347
+ > {
348
+ queryKey: TQueryKey
349
+ enabled?: boolean
350
+ }
351
+
352
+ export interface CreateQueriesOptions<
353
+ TItem,
354
+ TQueryFnData = unknown,
355
+ TError = Error,
356
+ TData = TQueryFnData,
357
+ TQueryKey extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
358
+ > {
359
+ /**
360
+ * Stable name used to derive the SID of the result `$items` store so
361
+ * `serialize(scope)` round-trips it for SSR. Without a name, the
362
+ * QueryClient cache still hydrates via `dehydrate`/`hydrate`, but the
363
+ * `$items` snapshot is silently dropped from `serialize(scope)`.
364
+ */
365
+ name?: string
366
+ /**
367
+ * Reactive list of items. Each item becomes one parallel query in the
368
+ * family. Adding / removing items updates the family (spawn / dispose
369
+ * observer). Order is preserved in `$items`.
370
+ *
371
+ * Duplicates in `source` deduplicate observers (same `queryKey` hash)
372
+ * but produce separate `$items` entries — one per occurrence.
373
+ */
374
+ source: Store<ReadonlyArray<TItem>>
375
+ /**
376
+ * Per-item options builder. MUST be pure — same item in, same options
377
+ * out. Reactivity is driven by the source store; this callback fires
378
+ * synchronously when source updates.
379
+ */
380
+ query: (
381
+ item: TItem,
382
+ ) => CreateQueriesItemOptions<TQueryFnData, TError, TData, TQueryKey>
383
+ /** Shared QueryObserver defaults applied on top of `query(item)`. */
384
+ staleTime?: number
385
+ gcTime?: number
386
+ retry?: QueryObserverOptions<TQueryFnData, TError, TData>['retry']
387
+ retryDelay?: QueryObserverOptions<TQueryFnData, TError, TData>['retryDelay']
388
+ refetchOnMount?: QueryObserverOptions<
389
+ TQueryFnData,
390
+ TError,
391
+ TData
392
+ >['refetchOnMount']
393
+ refetchOnReconnect?: QueryObserverOptions<
394
+ TQueryFnData,
395
+ TError,
396
+ TData
397
+ >['refetchOnReconnect']
398
+ refetchOnWindowFocus?: QueryObserverOptions<
399
+ TQueryFnData,
400
+ TError,
401
+ TData
402
+ >['refetchOnWindowFocus']
403
+ networkMode?: QueryObserverOptions<
404
+ TQueryFnData,
405
+ TError,
406
+ TData
407
+ >['networkMode']
408
+ }
409
+
410
+ /**
411
+ * Result of `createQueries(...)`. A reactive "family" of parallel
412
+ * queries indexed by a source store. Composes naturally with
413
+ * `useUnit($items)` for non-Suspense usage and with
414
+ * `useQueries(family)` / `useSuspenseQueries(family)` for React-style
415
+ * consumption.
416
+ */
417
+ export interface QueriesResult<TItem, TData = unknown, TError = Error> {
418
+ /** Per-item snapshots, parallel to `source`. */
419
+ $items: Store<ReadonlyArray<QueryItemState<TItem, TData, TError>>>
420
+ /** Just the `data` field of `$items` — shortcut for common UIs. */
421
+ $data: Store<ReadonlyArray<TData | undefined>>
422
+ /** `true` while **any** query in the family is pending. */
423
+ $isPending: Store<boolean>
424
+ /** `true` only when **every** query in the family has succeeded. */
425
+ $isSuccess: Store<boolean>
426
+ /** `true` if **any** query in the family is currently fetching. */
427
+ $isFetching: Store<boolean>
428
+ /** `true` if **any** query in the family errored. */
429
+ $isError: Store<boolean>
430
+ /**
431
+ * Triggers a parallel `fetchQuery` for every current source item.
432
+ * SSR-friendly — `await allSettled(family.prefetch, { scope })`
433
+ * returns after every queryFn has resolved (or failed).
434
+ */
435
+ prefetch: EventCallable<void>
436
+ /**
437
+ * Increment the per-scope refcount and ensure every observer is
438
+ * subscribed to the QueryClient. Call from `useEffect` (or its
439
+ * effector equivalent). The matching `unmounted()` decrements; when
440
+ * the count hits zero, observers unsubscribe.
441
+ */
442
+ mounted: EventCallable<void>
443
+ unmounted: EventCallable<void>
444
+ /** Invalidates every query in the family — re-fetches in background. */
445
+ refresh: EventCallable<void>
446
+ /** Invalidates one specific item's query. */
447
+ refreshOne: EventCallable<TItem>
448
+ /** See {@link QueryResult.$queryClient}. */
449
+ $queryClient: Store<QueryClient | null>
450
+ /**
451
+ * Discriminator that lets `useQueries` / `useSuspenseQueries` and
452
+ * other helpers distinguish a family from a tuple of factories at
453
+ * runtime. Not part of the public API.
454
+ */
455
+ readonly __family: true
456
+ }