@veams/status-quo-query 0.7.2 → 0.8.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/README.md CHANGED
@@ -31,6 +31,7 @@ Root exports:
31
31
  - `MutationService`
32
32
  - `QueryServiceSnapshot`
33
33
  - `MutationServiceSnapshot`
34
+ - `QueryDependencyTuple`
34
35
  - `QueryServiceOptions`
35
36
  - `MutationServiceOptions`
36
37
  - `TrackedMutationServiceOptions`
@@ -393,6 +394,96 @@ Standalone tracked mutations need either:
393
394
  - `dependencyKeys`
394
395
  - or `resolveDependencies`
395
396
 
397
+ ### Reactive Query Dependencies
398
+
399
+ Use `dependsOn` when a query needs data from other queries before it can run.
400
+
401
+ `dependsOn` accepts a `QueryDependencyTuple`:
402
+
403
+ - an ordered list of source query keys to observe
404
+ - a `deriveOptions(...)` callback that returns only `queryKey` and/or `enabled`
405
+
406
+ The watcher starts on the first `subscribe(...)` or `refetch()`, reads the current cache immediately, and stops after the last unsubscribe.
407
+
408
+ Untracked example:
409
+
410
+ ```ts
411
+ import { QueryClient } from '@tanstack/query-core';
412
+ import { setupQuery, type QueryDependencyTuple } from '@veams/status-quo-query';
413
+
414
+ type User = { companyId: string };
415
+ type Config = { region: string; companyProfileEnabled: boolean };
416
+
417
+ const queryClient = new QueryClient();
418
+ const createQuery = setupQuery(queryClient);
419
+ const userKey = ['user', 42] as const;
420
+ const configKey = ['config', 'global'] as const;
421
+
422
+ const companyProfileQuery = createQuery(
423
+ ['company-profile', { companyId: undefined as string | undefined, region: undefined as string | undefined }],
424
+ ({ queryKey }) => fetchCompanyProfile(queryKey[1].companyId!, queryKey[1].region!),
425
+ {
426
+ enabled: false,
427
+ dependsOn: <QueryDependencyTuple<[User, Config]>>[
428
+ [userKey, configKey],
429
+ ([userSnapshot, configSnapshot]) => {
430
+ if (!userSnapshot.data?.companyId || !configSnapshot.data?.region) {
431
+ return { enabled: false };
432
+ }
433
+
434
+ return {
435
+ enabled: configSnapshot.data.companyProfileEnabled,
436
+ queryKey: [
437
+ 'company-profile',
438
+ {
439
+ companyId: userSnapshot.data.companyId,
440
+ region: configSnapshot.data.region,
441
+ },
442
+ ],
443
+ };
444
+ },
445
+ ],
446
+ }
447
+ );
448
+ ```
449
+
450
+ Tracked example:
451
+
452
+ ```ts
453
+ import { QueryClient } from '@tanstack/query-core';
454
+ import { setupQueryManager } from '@veams/status-quo-query';
455
+
456
+ const queryClient = new QueryClient();
457
+ const manager = setupQueryManager(queryClient);
458
+ const selectionKey = ['selection'] as const;
459
+
460
+ const productQuery = manager.createQuery(
461
+ ['product', { deps: { applicationId: 'pending' }, view: { page: 0 } }],
462
+ ({ queryKey }) => fetchProduct(queryKey[1].deps.applicationId),
463
+ {
464
+ enabled: false,
465
+ dependsOn: [
466
+ [selectionKey],
467
+ ([selectionSnapshot]) =>
468
+ selectionSnapshot.data?.applicationId
469
+ ? {
470
+ enabled: true,
471
+ queryKey: [
472
+ 'product',
473
+ {
474
+ deps: { applicationId: selectionSnapshot.data.applicationId },
475
+ view: { page: 1 },
476
+ },
477
+ ],
478
+ }
479
+ : { enabled: false },
480
+ ],
481
+ }
482
+ );
483
+ ```
484
+
485
+ For tracked queries, keep the placeholder key valid too. The initial key and every derived key must still end with `{ deps, view? }`.
486
+
396
487
  ## FAQ
397
488
 
398
489
  ### Is `view` still part of the TanStack cache key?
@@ -474,7 +565,7 @@ Tracked queries embed dependency metadata into the final query-key segment:
474
565
 
475
566
  Only `deps` participates in automatic invalidation tracking. `view` is optional and is treated as normal query-key data.
476
567
 
477
- `createQuery(queryKey, queryFn, options?)` returns the same `QueryService<TData, TError>` shape as `createUntrackedQuery(...)`, but it registers the query hash under every `deps` entry and re-registers on `refetch()` or the first `subscribe(...)` if TanStack has removed the cache entry in the meantime.
568
+ `createQuery(queryKey, queryFn, options?)` returns the same `QueryService<TData, TError>` shape as `createUntrackedQuery(...)`, but it registers the query hash under every `deps` entry, re-registers on `refetch()` or the first `subscribe(...)` if TanStack has removed the cache entry in the meantime, and keeps the registry in sync when `dependsOn` derives a new tracked key at runtime.
478
569
 
479
570
  `createMutation(mutationFn, options?)` returns the same `MutationService<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
480
571
 
@@ -515,6 +606,12 @@ Creates a `createUntrackedQuery` factory bound to a `QueryClient`.
515
606
 
516
607
  `QueryServiceOptions` is based on TanStack `QueryObserverOptions`, without `queryKey` and `queryFn` because those are provided directly to `createUntrackedQuery`.
517
608
 
609
+ It also adds:
610
+
611
+ - `dependsOn?: QueryDependencyTuple<[...sources]>`
612
+
613
+ `dependsOn` observes the listed source keys through TanStack `QueriesObserver` and lets the downstream query derive only `queryKey` and `enabled`. The public `QueryService` API does not change when this option is used.
614
+
518
615
  `QueryService` methods:
519
616
 
520
617
  - `getSnapshot()`
package/dist/query.d.ts CHANGED
@@ -37,11 +37,23 @@ export interface QueryService<TData, TError> {
37
37
  */
38
38
  export interface QueryInvalidateOptions extends Pick<InvalidateOptions, 'cancelRefetch' | 'throwOnError'>, Pick<InvalidateQueryFilters, 'refetchType'> {
39
39
  }
40
+ type QueryDependencyDerivedOptions<TQueryKey extends QueryKey = QueryKey> = {
41
+ enabled?: boolean;
42
+ queryKey?: TQueryKey;
43
+ };
44
+ export type QueryDependencyTuple<TSources extends readonly unknown[], TQueryKey extends QueryKey = QueryKey> = readonly [
45
+ sourceKeys: {
46
+ readonly [K in keyof TSources]: QueryKey;
47
+ },
48
+ deriveOptions: (sourceSnapshots: {
49
+ readonly [K in keyof TSources]: QueryServiceSnapshot<TSources[K], Error>;
50
+ }) => QueryDependencyDerivedOptions<TQueryKey>
51
+ ];
40
52
  /**
41
53
  * Function signature for the untracked query factory.
42
54
  */
43
55
  export interface CreateUntrackedQuery {
44
- <TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>): QueryService<TData, TError>;
56
+ <TSources extends readonly unknown[] = [], TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>): QueryService<TData, TError>;
45
57
  }
46
58
  /**
47
59
  * Function signature for the default query factory that derives dependencies from the final
@@ -51,12 +63,14 @@ export interface CreateUntrackedQuery {
51
63
  * The only extra behavior is invisible: dependency registration and on-demand re-registration.
52
64
  */
53
65
  export interface CreateQuery {
54
- <TDeps extends TrackedDependencyRecord, TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends TrackedQueryKey<TDeps> = TrackedQueryKey<TDeps>>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>): QueryService<TData, TError>;
66
+ <TDeps extends TrackedDependencyRecord, TSources extends readonly unknown[] = [], TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends TrackedQueryKey<TDeps> = TrackedQueryKey<TDeps>>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>): QueryService<TData, TError>;
55
67
  }
56
68
  /**
57
69
  * Configuration options for creating a query service, excluding function and key.
58
70
  */
59
- export type QueryServiceOptions<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>, 'queryFn' | 'queryKey'>;
71
+ export type QueryServiceOptions<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TSources extends readonly unknown[] = []> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>, 'queryFn' | 'queryKey'> & {
72
+ dependsOn?: QueryDependencyTuple<TSources, TQueryKey>;
73
+ };
60
74
  /**
61
75
  * Extracts and maps status and fetchStatus to our QueryMetaState interface.
62
76
  */
@@ -80,3 +94,4 @@ export declare function setupQuery(queryClient: QueryClient): CreateUntrackedQue
80
94
  * query handles call `ensureRegistered()` before they become active again.
81
95
  */
82
96
  export declare function setupTrackedQuery(queryClient: QueryClient, trackingRegistry: TrackingRegistry): CreateQuery;
97
+ export {};
package/dist/query.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  // Import QueryObserver to monitor and manage individual queries.
3
- QueryObserver, } from '@tanstack/query-core';
3
+ QueryObserver, QueriesObserver, } from '@tanstack/query-core';
4
4
  import { extractTrackedDependencies, } from './tracking.js';
5
5
  /**
6
6
  * Extracts and maps status and fetchStatus to our QueryMetaState interface.
@@ -25,7 +25,12 @@ export function isQueryLoading(query) {
25
25
  export function setupQuery(queryClient) {
26
26
  // Returns the actual factory function for creating individual query services.
27
27
  return function createQuery(queryKey, queryFn, options) {
28
- return createQueryService(queryClient, queryKey, queryFn, options).service;
28
+ const { dependsOn, runtimeOptions } = splitQueryServiceOptions(options);
29
+ const service = createQueryService(queryClient, queryKey, queryFn, runtimeOptions);
30
+ if (!dependsOn) {
31
+ return service.service;
32
+ }
33
+ return bindQueryDependencies(queryClient, service, queryKey, dependsOn);
29
34
  };
30
35
  }
31
36
  /**
@@ -40,27 +45,41 @@ export function setupQuery(queryClient) {
40
45
  */
41
46
  export function setupTrackedQuery(queryClient, trackingRegistry) {
42
47
  return function createQuery(queryKey, queryFn, options) {
43
- // Validate and normalize the dependency entries once when the handle is created.
44
- const dependencies = extractTrackedDependencies(queryKey);
48
+ const { dependsOn, runtimeOptions } = splitQueryServiceOptions(options);
45
49
  // Reuse the same core query service implementation as the untracked API.
46
- const service = createQueryService(queryClient, queryKey, queryFn, options);
50
+ const service = createQueryService(queryClient, queryKey, queryFn, runtimeOptions);
47
51
  // We only need re-registration on the transition from zero to one subscribers.
48
52
  let subscriberCount = 0;
49
53
  // Register the current query hash immediately so future tracked mutations can find it.
50
- trackingRegistry.register(service.observer.getCurrentQuery().queryHash, dependencies);
54
+ trackingRegistry.register(service.observer.getCurrentQuery().queryHash, extractTrackedDependencies(service.getCurrentQueryKey()));
55
+ const applyTrackedDerivedState = (derivedOptions) => {
56
+ const previousQueryHash = service.observer.getCurrentQuery().queryHash;
57
+ service.setDerivedState(derivedOptions);
58
+ const nextQueryHash = service.observer.getCurrentQuery().queryHash;
59
+ if (nextQueryHash === previousQueryHash) {
60
+ return;
61
+ }
62
+ trackingRegistry.unregister(previousQueryHash);
63
+ trackingRegistry.register(nextQueryHash, extractTrackedDependencies(service.getCurrentQueryKey()));
64
+ };
65
+ const dependencyController = dependsOn
66
+ ? createDependencyController(queryClient, queryKey, applyTrackedDerivedState, dependsOn)
67
+ : undefined;
51
68
  const ensureRegistered = () => {
52
69
  // Build resolves the current live TanStack query for the stored observer options. This is
53
70
  // the same mechanism TanStack uses internally when a query gets recreated after GC.
54
- const liveQuery = queryClient.getQueryCache().build(queryClient, toQueryOptions(queryKey, queryFn, options));
71
+ const liveQuery = queryClient.getQueryCache().build(queryClient, service.getCurrentObserverOptions());
72
+ const liveDependencies = extractTrackedDependencies(service.getCurrentQueryKey());
55
73
  // Re-register only when TanStack has recreated the query and the registry has already
56
74
  // cleaned up the previous hash. This keeps the edge-case handling cheap in the common case.
57
75
  if (!trackingRegistry.has(liveQuery.queryHash)) {
58
- trackingRegistry.register(liveQuery.queryHash, dependencies);
76
+ trackingRegistry.register(liveQuery.queryHash, liveDependencies);
59
77
  }
60
78
  };
61
79
  return {
62
80
  ...service.service,
63
81
  refetch: async (refetchOptions) => {
82
+ dependencyController?.evaluateOnce();
64
83
  // Refetch is one of the two explicit reactivation paths agreed on in the design.
65
84
  ensureRegistered();
66
85
  return service.service.refetch(refetchOptions);
@@ -69,6 +88,7 @@ export function setupTrackedQuery(queryClient, trackingRegistry) {
69
88
  // The first active subscriber is the other reactivation path. Re-running registration
70
89
  // here makes a previously removed query visible to tracked invalidation again.
71
90
  if (subscriberCount === 0) {
91
+ dependencyController?.activate();
72
92
  ensureRegistered();
73
93
  }
74
94
  subscriberCount += 1;
@@ -76,6 +96,9 @@ export function setupTrackedQuery(queryClient, trackingRegistry) {
76
96
  return () => {
77
97
  // Keep the counter bounded so accidental double-unsubscribe cannot push it negative.
78
98
  subscriberCount = Math.max(0, subscriberCount - 1);
99
+ if (subscriberCount === 0) {
100
+ dependencyController?.deactivate();
101
+ }
79
102
  unsubscribe();
80
103
  };
81
104
  },
@@ -99,9 +122,25 @@ function toQueryServiceSnapshot(result) {
99
122
  };
100
123
  }
101
124
  function createQueryService(queryClient, queryKey, queryFn, options) {
102
- const observer = new QueryObserver(queryClient, toQueryOptions(queryKey, queryFn, options));
125
+ const baseQueryKey = queryKey;
126
+ const baseOptions = options;
127
+ let resolvedQueryKey = baseQueryKey;
128
+ let resolvedOptions = baseOptions;
129
+ const observer = new QueryObserver(queryClient, toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions));
130
+ const setDerivedState = (derivedOptions) => {
131
+ resolvedQueryKey = derivedOptions.queryKey ?? baseQueryKey;
132
+ resolvedOptions = {
133
+ ...baseOptions,
134
+ ...(derivedOptions.enabled === undefined ? {} : { enabled: derivedOptions.enabled }),
135
+ };
136
+ observer.setOptions(toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions));
137
+ };
138
+ const getCurrentObserverOptions = () => toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions);
103
139
  return {
104
140
  observer,
141
+ getCurrentObserverOptions,
142
+ getCurrentQueryKey: () => resolvedQueryKey,
143
+ setDerivedState,
105
144
  service: {
106
145
  getSnapshot: () => toQueryServiceSnapshot(observer.getCurrentResult()),
107
146
  subscribe: (listener) => observer.subscribe((result) => {
@@ -110,7 +149,7 @@ function createQueryService(queryClient, queryKey, queryFn, options) {
110
149
  refetch: async (refetchOptions) => toQueryServiceSnapshot(await observer.refetch(refetchOptions)),
111
150
  invalidate: (invalidateOptions) => queryClient.invalidateQueries({
112
151
  exact: true,
113
- queryKey,
152
+ queryKey: resolvedQueryKey,
114
153
  ...(invalidateOptions?.refetchType === undefined
115
154
  ? {}
116
155
  : { refetchType: invalidateOptions.refetchType }),
@@ -119,6 +158,85 @@ function createQueryService(queryClient, queryKey, queryFn, options) {
119
158
  },
120
159
  };
121
160
  }
161
+ function bindQueryDependencies(queryClient, queryService, queryKey, dependsOn) {
162
+ const dependencyController = createDependencyController(queryClient, queryKey, queryService.setDerivedState, dependsOn);
163
+ let subscriberCount = 0;
164
+ return {
165
+ ...queryService.service,
166
+ refetch: async (refetchOptions) => {
167
+ dependencyController.evaluateOnce();
168
+ return queryService.service.refetch(refetchOptions);
169
+ },
170
+ subscribe: (listener) => {
171
+ if (subscriberCount === 0) {
172
+ dependencyController.activate();
173
+ }
174
+ subscriberCount += 1;
175
+ const unsubscribe = queryService.service.subscribe(listener);
176
+ return () => {
177
+ subscriberCount = Math.max(0, subscriberCount - 1);
178
+ if (subscriberCount === 0) {
179
+ dependencyController.deactivate();
180
+ }
181
+ unsubscribe();
182
+ };
183
+ },
184
+ };
185
+ }
186
+ function createDependencyController(queryClient, baseQueryKey, setDerivedState, dependsOn) {
187
+ const [sourceKeys, deriveOptions] = dependsOn;
188
+ let queriesObserver;
189
+ let unsubscribe;
190
+ const evaluateBinding = (results) => {
191
+ const snapshots = results.map((result) => toQueryServiceSnapshot(result));
192
+ const derivedOptions = deriveOptions(snapshots);
193
+ setDerivedState({
194
+ queryKey: derivedOptions.queryKey ?? baseQueryKey,
195
+ ...(derivedOptions.enabled === undefined ? {} : { enabled: derivedOptions.enabled }),
196
+ });
197
+ };
198
+ const createObserver = () => new QueriesObserver(queryClient, sourceKeys.map((sourceKey) => ({
199
+ enabled: false,
200
+ queryKey: sourceKey,
201
+ })));
202
+ return {
203
+ activate: () => {
204
+ if (queriesObserver) {
205
+ return;
206
+ }
207
+ queriesObserver = createObserver();
208
+ evaluateBinding(queriesObserver.getCurrentResult());
209
+ unsubscribe = queriesObserver.subscribe(evaluateBinding);
210
+ },
211
+ deactivate: () => {
212
+ unsubscribe?.();
213
+ unsubscribe = undefined;
214
+ queriesObserver = undefined;
215
+ },
216
+ evaluateOnce: () => {
217
+ if (queriesObserver) {
218
+ evaluateBinding(queriesObserver.getCurrentResult());
219
+ return;
220
+ }
221
+ const transientObserver = createObserver();
222
+ evaluateBinding(transientObserver.getCurrentResult());
223
+ transientObserver.destroy();
224
+ },
225
+ };
226
+ }
227
+ function splitQueryServiceOptions(options) {
228
+ if (options === undefined) {
229
+ return {
230
+ dependsOn: undefined,
231
+ runtimeOptions: undefined,
232
+ };
233
+ }
234
+ const { dependsOn, ...runtimeOptions } = options;
235
+ return {
236
+ dependsOn,
237
+ runtimeOptions,
238
+ };
239
+ }
122
240
  function toQueryOptions(queryKey, queryFn, options) {
123
241
  // Centralize option assembly so both normal queries and tracked queries build observers and
124
242
  // live TanStack query instances from exactly the same inputs.
package/dist/query.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veams/status-quo-query",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "TanStack Query service layer for the VEAMS StatusQuo ecosystem.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -3,6 +3,11 @@ import { QueryClient } from '@tanstack/query-core';
3
3
  import { isQueryLoading, setupQuery, toQueryMetaState } from '../query';
4
4
 
5
5
  describe('Query Service', () => {
6
+ async function flushTasks() {
7
+ await Promise.resolve();
8
+ await Promise.resolve();
9
+ }
10
+
6
11
  it('returns data after refetch', async () => {
7
12
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
8
13
  const createQuery = setupQuery(queryClient);
@@ -58,6 +63,235 @@ describe('Query Service', () => {
58
63
  );
59
64
  });
60
65
 
66
+ it('derives query activation and key updates from upstream cache state', async () => {
67
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
68
+ const createQuery = setupQuery(queryClient);
69
+ const userKey = ['user', 42] as const;
70
+ const configKey = ['config', 'global'] as const;
71
+ const queryFn = jest
72
+ .fn()
73
+ .mockImplementation(({ queryKey }) => `${queryKey[1].companyId}:${queryKey[1].region}`);
74
+
75
+ const service = createQuery(
76
+ ['crefo', { companyId: undefined as string | undefined, region: undefined as string | undefined }],
77
+ queryFn,
78
+ {
79
+ enabled: false,
80
+ dependsOn: [
81
+ [userKey, configKey],
82
+ ([userSnapshot, configSnapshot]) => {
83
+ if (!userSnapshot.data?.companyId || !configSnapshot.data?.region) {
84
+ return { enabled: false };
85
+ }
86
+
87
+ return {
88
+ enabled: true,
89
+ queryKey: [
90
+ 'crefo',
91
+ {
92
+ companyId: userSnapshot.data.companyId,
93
+ region: configSnapshot.data.region,
94
+ },
95
+ ],
96
+ };
97
+ },
98
+ ],
99
+ }
100
+ );
101
+
102
+ const snapshots: string[] = [];
103
+ const unsubscribe = service.subscribe((snapshot) => {
104
+ snapshots.push(snapshot.status);
105
+ });
106
+
107
+ queryClient.setQueryData(userKey, { companyId: 'company-1' });
108
+ queryClient.setQueryData(configKey, { region: 'eu' });
109
+ await flushTasks();
110
+
111
+ expect(queryFn).toHaveBeenCalledTimes(1);
112
+ expect(queryFn).toHaveBeenLastCalledWith(
113
+ expect.objectContaining({
114
+ queryKey: ['crefo', { companyId: 'company-1', region: 'eu' }],
115
+ })
116
+ );
117
+ expect(service.getSnapshot().data).toBe('company-1:eu');
118
+ expect(snapshots).toContain('success');
119
+
120
+ unsubscribe();
121
+ });
122
+
123
+ it('uses warm upstream cache data when dependsOn is evaluated on first refetch', async () => {
124
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
125
+ const createQuery = setupQuery(queryClient);
126
+ const selectionKey = ['selection'] as const;
127
+ const queryFn = jest.fn().mockImplementation(async ({ queryKey }) => queryKey[1].id);
128
+
129
+ queryClient.setQueryData(selectionKey, { id: 'item-2' });
130
+
131
+ const service = createQuery(
132
+ ['details', { id: undefined as string | undefined }],
133
+ queryFn,
134
+ {
135
+ enabled: false,
136
+ dependsOn: [
137
+ [selectionKey],
138
+ ([selectionSnapshot]) =>
139
+ selectionSnapshot.data?.id
140
+ ? {
141
+ enabled: true,
142
+ queryKey: ['details', { id: selectionSnapshot.data.id }],
143
+ }
144
+ : { enabled: false },
145
+ ],
146
+ }
147
+ );
148
+
149
+ const result = await service.refetch();
150
+
151
+ expect(result.data).toBe('item-2');
152
+ expect(queryFn).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ queryKey: ['details', { id: 'item-2' }],
155
+ })
156
+ );
157
+ });
158
+
159
+ it('preserves source snapshot tuple order in dependsOn', async () => {
160
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
161
+ const createQuery = setupQuery(queryClient);
162
+ const firstKey = ['source', 'first'] as const;
163
+ const secondKey = ['source', 'second'] as const;
164
+ const observedOrders: Array<[string | undefined, string | undefined]> = [];
165
+
166
+ queryClient.setQueryData(firstKey, { label: 'first' });
167
+ queryClient.setQueryData(secondKey, { label: 'second' });
168
+
169
+ const service = createQuery(
170
+ ['ordered', { left: undefined as string | undefined, right: undefined as string | undefined }],
171
+ jest.fn().mockResolvedValue('ok'),
172
+ {
173
+ enabled: false,
174
+ dependsOn: [
175
+ [firstKey, secondKey],
176
+ ([firstSnapshot, secondSnapshot]) => {
177
+ observedOrders.push([firstSnapshot.data?.label, secondSnapshot.data?.label]);
178
+
179
+ return {
180
+ enabled: false,
181
+ queryKey: [
182
+ 'ordered',
183
+ {
184
+ left: firstSnapshot.data?.label,
185
+ right: secondSnapshot.data?.label,
186
+ },
187
+ ],
188
+ };
189
+ },
190
+ ],
191
+ }
192
+ );
193
+
194
+ await service.refetch();
195
+
196
+ expect(observedOrders).toContainEqual(['first', 'second']);
197
+ });
198
+
199
+ it('invalidates the current derived query key after dependsOn updates it', async () => {
200
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
201
+ const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
202
+ const createQuery = setupQuery(queryClient);
203
+ const selectionKey = ['selection'] as const;
204
+
205
+ queryClient.setQueryData(selectionKey, { id: 'item-3' });
206
+
207
+ const service = createQuery(
208
+ ['details', { id: undefined as string | undefined }],
209
+ jest.fn().mockResolvedValue('item-3'),
210
+ {
211
+ enabled: false,
212
+ dependsOn: [
213
+ [selectionKey],
214
+ ([selectionSnapshot]) =>
215
+ selectionSnapshot.data?.id
216
+ ? {
217
+ enabled: true,
218
+ queryKey: ['details', { id: selectionSnapshot.data.id }],
219
+ }
220
+ : { enabled: false },
221
+ ],
222
+ }
223
+ );
224
+
225
+ await service.refetch();
226
+ await service.invalidate({ refetchType: 'none' });
227
+
228
+ expect(invalidateQueriesSpy).toHaveBeenCalledWith(
229
+ {
230
+ exact: true,
231
+ queryKey: ['details', { id: 'item-3' }],
232
+ refetchType: 'none',
233
+ },
234
+ undefined
235
+ );
236
+ });
237
+
238
+ it('stops reacting to upstream cache updates after the last unsubscribe', async () => {
239
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
240
+ const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
241
+ const createQuery = setupQuery(queryClient);
242
+ const selectionKey = ['selection'] as const;
243
+ const queryFn = jest.fn().mockImplementation(async ({ queryKey }) => queryKey[1].id);
244
+
245
+ queryClient.setQueryData(selectionKey, { id: 'item-1' });
246
+
247
+ const service = createQuery(
248
+ ['details', { id: undefined as string | undefined }],
249
+ queryFn,
250
+ {
251
+ enabled: false,
252
+ dependsOn: [
253
+ [selectionKey],
254
+ ([selectionSnapshot]) =>
255
+ selectionSnapshot.data?.id
256
+ ? {
257
+ enabled: true,
258
+ queryKey: ['details', { id: selectionSnapshot.data.id }],
259
+ }
260
+ : { enabled: false },
261
+ ],
262
+ }
263
+ );
264
+
265
+ const unsubscribe = service.subscribe(() => undefined);
266
+ await flushTasks();
267
+ unsubscribe();
268
+
269
+ queryClient.setQueryData(selectionKey, { id: 'item-2' });
270
+ await flushTasks();
271
+
272
+ expect(queryFn).toHaveBeenCalledTimes(1);
273
+
274
+ await service.invalidate({ refetchType: 'none' });
275
+
276
+ expect(invalidateQueriesSpy).toHaveBeenCalledWith(
277
+ {
278
+ exact: true,
279
+ queryKey: ['details', { id: 'item-1' }],
280
+ refetchType: 'none',
281
+ },
282
+ undefined
283
+ );
284
+
285
+ await service.refetch();
286
+
287
+ expect(queryFn).toHaveBeenCalledTimes(2);
288
+ expect(queryFn).toHaveBeenLastCalledWith(
289
+ expect.objectContaining({
290
+ queryKey: ['details', { id: 'item-2' }],
291
+ })
292
+ );
293
+ });
294
+
61
295
  it('derives loading state from query metadata', () => {
62
296
  expect(
63
297
  isQueryLoading(
@@ -3,6 +3,11 @@ import { QueryClient } from '@tanstack/query-core';
3
3
  import { setupQueryManager } from '../provider';
4
4
 
5
5
  describe('Tracked Query Invalidation', () => {
6
+ async function flushTasks() {
7
+ await Promise.resolve();
8
+ await Promise.resolve();
9
+ }
10
+
6
11
  it('registers tracked queries from deps and ignores view data during invalidation', async () => {
7
12
  const queryClient = new QueryClient({
8
13
  defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
@@ -273,4 +278,126 @@ describe('Tracked Query Invalidation', () => {
273
278
 
274
279
  expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1);
275
280
  });
281
+
282
+ it('moves tracked registrations to the derived key when dependsOn updates the query', async () => {
283
+ const queryClient = new QueryClient({
284
+ defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
285
+ });
286
+ const manager = setupQueryManager(queryClient);
287
+ const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
288
+ const selectionKey = ['selection'] as const;
289
+
290
+ queryClient.setQueryData(selectionKey, { applicationId: 'app-1' });
291
+
292
+ const query = manager.createQuery(
293
+ ['product', { deps: { applicationId: 'pending' }, view: { page: 0 } }],
294
+ jest.fn().mockResolvedValue('product'),
295
+ {
296
+ enabled: false,
297
+ dependsOn: [
298
+ [selectionKey],
299
+ ([selectionSnapshot]) =>
300
+ selectionSnapshot.data?.applicationId
301
+ ? {
302
+ enabled: true,
303
+ queryKey: [
304
+ 'product',
305
+ {
306
+ deps: { applicationId: selectionSnapshot.data.applicationId },
307
+ view: { page: 1 },
308
+ },
309
+ ],
310
+ }
311
+ : { enabled: false },
312
+ ],
313
+ }
314
+ );
315
+
316
+ const mutation = manager.createMutation(jest.fn().mockResolvedValue({ ok: true as const }), {
317
+ dependencyKeys: ['applicationId'],
318
+ });
319
+
320
+ await query.refetch();
321
+ await mutation.mutate({ applicationId: 'pending' });
322
+
323
+ expect(invalidateQueriesSpy).toHaveBeenCalledTimes(0);
324
+
325
+ await mutation.mutate({ applicationId: 'app-1' });
326
+
327
+ expect(invalidateQueriesSpy).toHaveBeenCalledWith({
328
+ exact: true,
329
+ queryKey: ['product', { deps: { applicationId: 'app-1' }, view: { page: 1 } }],
330
+ });
331
+ });
332
+
333
+ it('re-registers the latest derived tracked key after cache removal and a later refetch', async () => {
334
+ const queryClient = new QueryClient({
335
+ defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
336
+ });
337
+ const manager = setupQueryManager(queryClient);
338
+ const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
339
+ const selectionKey = ['selection'] as const;
340
+ const initialDerivedKey = [
341
+ 'product',
342
+ { deps: { applicationId: 'app-1' }, view: { page: 1 } },
343
+ ] as const;
344
+ const latestDerivedKey = [
345
+ 'product',
346
+ { deps: { applicationId: 'app-2' }, view: { page: 1 } },
347
+ ] as const;
348
+
349
+ queryClient.setQueryData(selectionKey, { applicationId: 'app-1' });
350
+
351
+ const query = manager.createQuery(
352
+ ['product', { deps: { applicationId: 'pending' }, view: { page: 0 } }],
353
+ jest.fn().mockResolvedValue('product'),
354
+ {
355
+ enabled: false,
356
+ dependsOn: [
357
+ [selectionKey],
358
+ ([selectionSnapshot]) =>
359
+ selectionSnapshot.data?.applicationId
360
+ ? {
361
+ enabled: true,
362
+ queryKey: [
363
+ 'product',
364
+ {
365
+ deps: { applicationId: selectionSnapshot.data.applicationId },
366
+ view: { page: 1 },
367
+ },
368
+ ],
369
+ }
370
+ : { enabled: false },
371
+ ],
372
+ }
373
+ );
374
+
375
+ const mutation = manager.createMutation(jest.fn().mockResolvedValue({ ok: true as const }), {
376
+ dependencyKeys: ['applicationId'],
377
+ });
378
+
379
+ await query.refetch();
380
+ queryClient.removeQueries({ exact: true, queryKey: initialDerivedKey });
381
+ queryClient.setQueryData(selectionKey, { applicationId: 'app-2' });
382
+ await flushTasks();
383
+
384
+ invalidateQueriesSpy.mockClear();
385
+
386
+ await mutation.mutate({ applicationId: 'app-2' });
387
+ expect(invalidateQueriesSpy).toHaveBeenCalledTimes(0);
388
+
389
+ await query.refetch();
390
+ await mutation.mutate({ applicationId: 'app-2' });
391
+
392
+ expect(invalidateQueriesSpy).toHaveBeenCalledWith({
393
+ exact: true,
394
+ queryKey: latestDerivedKey,
395
+ });
396
+
397
+ invalidateQueriesSpy.mockClear();
398
+
399
+ await mutation.mutate({ applicationId: 'pending' });
400
+
401
+ expect(invalidateQueriesSpy).toHaveBeenCalledTimes(0);
402
+ });
276
403
  });
package/src/query.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  type QueryKey,
14
14
  // Import QueryObserver to monitor and manage individual queries.
15
15
  QueryObserver,
16
+ QueriesObserver,
16
17
  type QueryOptions,
17
18
  // Import configuration options for the query observer.
18
19
  type QueryObserverOptions,
@@ -88,11 +89,38 @@ export interface QueryInvalidateOptions
88
89
  extends Pick<InvalidateOptions, 'cancelRefetch' | 'throwOnError'>,
89
90
  Pick<InvalidateQueryFilters, 'refetchType'> {}
90
91
 
92
+ type QueryDependencyDerivedOptions<TQueryKey extends QueryKey = QueryKey> = {
93
+ enabled?: boolean;
94
+ queryKey?: TQueryKey;
95
+ };
96
+
97
+ type QueryServiceRuntimeOptions<
98
+ TQueryFnData = unknown,
99
+ TError = Error,
100
+ TData = TQueryFnData,
101
+ TQueryData = TQueryFnData,
102
+ TQueryKey extends QueryKey = QueryKey,
103
+ > = Omit<
104
+ QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
105
+ 'dependsOn'
106
+ >;
107
+
108
+ export type QueryDependencyTuple<
109
+ TSources extends readonly unknown[],
110
+ TQueryKey extends QueryKey = QueryKey,
111
+ > = readonly [
112
+ sourceKeys: { readonly [K in keyof TSources]: QueryKey },
113
+ deriveOptions: (
114
+ sourceSnapshots: { readonly [K in keyof TSources]: QueryServiceSnapshot<TSources[K], Error> }
115
+ ) => QueryDependencyDerivedOptions<TQueryKey>,
116
+ ];
117
+
91
118
  /**
92
119
  * Function signature for the untracked query factory.
93
120
  */
94
121
  export interface CreateUntrackedQuery {
95
122
  <
123
+ TSources extends readonly unknown[] = [],
96
124
  TQueryFnData = unknown,
97
125
  TError = Error,
98
126
  TData = TQueryFnData,
@@ -104,7 +132,7 @@ export interface CreateUntrackedQuery {
104
132
  // The asynchronous function that performs the data fetch.
105
133
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
106
134
  // Optional configuration for behavior like staleness, retry, and refetching.
107
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
135
+ options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
108
136
  ): QueryService<TData, TError>;
109
137
  }
110
138
 
@@ -118,6 +146,7 @@ export interface CreateUntrackedQuery {
118
146
  export interface CreateQuery {
119
147
  <
120
148
  TDeps extends TrackedDependencyRecord,
149
+ TSources extends readonly unknown[] = [],
121
150
  TQueryFnData = unknown,
122
151
  TError = Error,
123
152
  TData = TQueryFnData,
@@ -126,7 +155,7 @@ export interface CreateQuery {
126
155
  >(
127
156
  queryKey: TQueryKey,
128
157
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
129
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
158
+ options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
130
159
  ): QueryService<TData, TError>;
131
160
  }
132
161
 
@@ -139,10 +168,13 @@ export type QueryServiceOptions<
139
168
  TData = TQueryFnData,
140
169
  TQueryData = TQueryFnData,
141
170
  TQueryKey extends QueryKey = QueryKey,
171
+ TSources extends readonly unknown[] = [],
142
172
  > = Omit<
143
173
  QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
144
174
  'queryFn' | 'queryKey'
145
- >;
175
+ > & {
176
+ dependsOn?: QueryDependencyTuple<TSources, TQueryKey>;
177
+ };
146
178
 
147
179
  /**
148
180
  * Extracts and maps status and fetchStatus to our QueryMetaState interface.
@@ -171,6 +203,7 @@ export function isQueryLoading(query: QueryMetaState): boolean {
171
203
  export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
172
204
  // Returns the actual factory function for creating individual query services.
173
205
  return function createQuery<
206
+ TSources extends readonly unknown[] = [],
174
207
  TQueryFnData = unknown,
175
208
  TError = Error,
176
209
  TData = TQueryFnData,
@@ -179,9 +212,16 @@ export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
179
212
  >(
180
213
  queryKey: TQueryKey,
181
214
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
182
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
215
+ options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
183
216
  ): QueryService<TData, TError> {
184
- return createQueryService(queryClient, queryKey, queryFn, options).service;
217
+ const { dependsOn, runtimeOptions } = splitQueryServiceOptions(options);
218
+ const service = createQueryService(queryClient, queryKey, queryFn, runtimeOptions);
219
+
220
+ if (!dependsOn) {
221
+ return service.service;
222
+ }
223
+
224
+ return bindQueryDependencies(queryClient, service, queryKey, dependsOn);
185
225
  };
186
226
  }
187
227
 
@@ -201,6 +241,7 @@ export function setupTrackedQuery(
201
241
  ): CreateQuery {
202
242
  return function createQuery<
203
243
  TDeps extends TrackedDependencyRecord,
244
+ TSources extends readonly unknown[] = [],
204
245
  TQueryFnData = unknown,
205
246
  TError = Error,
206
247
  TData = TQueryFnData,
@@ -209,36 +250,64 @@ export function setupTrackedQuery(
209
250
  >(
210
251
  queryKey: TQueryKey,
211
252
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
212
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
253
+ options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
213
254
  ): QueryService<TData, TError> {
214
- // Validate and normalize the dependency entries once when the handle is created.
215
- const dependencies = extractTrackedDependencies(queryKey);
255
+ const { dependsOn, runtimeOptions } = splitQueryServiceOptions(options);
216
256
  // Reuse the same core query service implementation as the untracked API.
217
- const service = createQueryService(queryClient, queryKey, queryFn, options);
257
+ const service = createQueryService(queryClient, queryKey, queryFn, runtimeOptions);
218
258
  // We only need re-registration on the transition from zero to one subscribers.
219
259
  let subscriberCount = 0;
220
260
 
221
261
  // Register the current query hash immediately so future tracked mutations can find it.
222
- trackingRegistry.register(service.observer.getCurrentQuery().queryHash, dependencies);
262
+ trackingRegistry.register(
263
+ service.observer.getCurrentQuery().queryHash,
264
+ extractTrackedDependencies(service.getCurrentQueryKey())
265
+ );
266
+
267
+ const applyTrackedDerivedState = (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => {
268
+ const previousQueryHash = service.observer.getCurrentQuery().queryHash;
269
+
270
+ service.setDerivedState(derivedOptions);
271
+
272
+ const nextQueryHash = service.observer.getCurrentQuery().queryHash;
273
+
274
+ if (nextQueryHash === previousQueryHash) {
275
+ return;
276
+ }
277
+
278
+ trackingRegistry.unregister(previousQueryHash);
279
+ trackingRegistry.register(nextQueryHash, extractTrackedDependencies(service.getCurrentQueryKey()));
280
+ };
281
+
282
+ const dependencyController = dependsOn
283
+ ? createDependencyController(
284
+ queryClient,
285
+ queryKey,
286
+ applyTrackedDerivedState,
287
+ dependsOn
288
+ )
289
+ : undefined;
223
290
 
224
291
  const ensureRegistered = () => {
225
292
  // Build resolves the current live TanStack query for the stored observer options. This is
226
293
  // the same mechanism TanStack uses internally when a query gets recreated after GC.
227
294
  const liveQuery = queryClient.getQueryCache().build(
228
295
  queryClient,
229
- toQueryOptions(queryKey, queryFn, options)
296
+ service.getCurrentObserverOptions()
230
297
  );
298
+ const liveDependencies = extractTrackedDependencies(service.getCurrentQueryKey());
231
299
 
232
300
  // Re-register only when TanStack has recreated the query and the registry has already
233
301
  // cleaned up the previous hash. This keeps the edge-case handling cheap in the common case.
234
302
  if (!trackingRegistry.has(liveQuery.queryHash)) {
235
- trackingRegistry.register(liveQuery.queryHash, dependencies);
303
+ trackingRegistry.register(liveQuery.queryHash, liveDependencies);
236
304
  }
237
305
  };
238
306
 
239
307
  return {
240
308
  ...service.service,
241
309
  refetch: async (refetchOptions) => {
310
+ dependencyController?.evaluateOnce();
242
311
  // Refetch is one of the two explicit reactivation paths agreed on in the design.
243
312
  ensureRegistered();
244
313
  return service.service.refetch(refetchOptions);
@@ -247,6 +316,7 @@ export function setupTrackedQuery(
247
316
  // The first active subscriber is the other reactivation path. Re-running registration
248
317
  // here makes a previously removed query visible to tracked invalidation again.
249
318
  if (subscriberCount === 0) {
319
+ dependencyController?.activate();
250
320
  ensureRegistered();
251
321
  }
252
322
 
@@ -257,6 +327,9 @@ export function setupTrackedQuery(
257
327
  return () => {
258
328
  // Keep the counter bounded so accidental double-unsubscribe cannot push it negative.
259
329
  subscriberCount = Math.max(0, subscriberCount - 1);
330
+ if (subscriberCount === 0) {
331
+ dependencyController?.deactivate();
332
+ }
260
333
  unsubscribe();
261
334
  };
262
335
  },
@@ -293,20 +366,43 @@ function createQueryService<
293
366
  queryClient: QueryClient,
294
367
  queryKey: TQueryKey,
295
368
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
296
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
369
+ options?: QueryServiceRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
297
370
  ): {
298
371
  // Expose the observer internally so tracked queries can access the current query hash.
299
372
  observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
300
373
  // Preserve the public query-service shape for all callers.
301
374
  service: QueryService<TData, TError>;
375
+ getCurrentObserverOptions: () => QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
376
+ QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
377
+ getCurrentQueryKey: () => TQueryKey;
378
+ setDerivedState: (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => void;
302
379
  } {
380
+ const baseQueryKey = queryKey;
381
+ const baseOptions = options;
382
+ let resolvedQueryKey = baseQueryKey;
383
+ let resolvedOptions = baseOptions;
384
+
303
385
  const observer = new QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
304
386
  queryClient,
305
- toQueryOptions(queryKey, queryFn, options)
387
+ toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions)
306
388
  );
307
389
 
390
+ const setDerivedState = (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => {
391
+ resolvedQueryKey = derivedOptions.queryKey ?? baseQueryKey;
392
+ resolvedOptions = {
393
+ ...baseOptions,
394
+ ...(derivedOptions.enabled === undefined ? {} : { enabled: derivedOptions.enabled }),
395
+ };
396
+ observer.setOptions(toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions));
397
+ };
398
+
399
+ const getCurrentObserverOptions = () => toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions);
400
+
308
401
  return {
309
402
  observer,
403
+ getCurrentObserverOptions,
404
+ getCurrentQueryKey: () => resolvedQueryKey,
405
+ setDerivedState,
310
406
  service: {
311
407
  getSnapshot: () => toQueryServiceSnapshot(observer.getCurrentResult()),
312
408
  subscribe: (listener) =>
@@ -319,7 +415,7 @@ function createQueryService<
319
415
  queryClient.invalidateQueries(
320
416
  {
321
417
  exact: true,
322
- queryKey,
418
+ queryKey: resolvedQueryKey,
323
419
  ...(invalidateOptions?.refetchType === undefined
324
420
  ? {}
325
421
  : { refetchType: invalidateOptions.refetchType }),
@@ -331,6 +427,149 @@ function createQueryService<
331
427
  };
332
428
  }
333
429
 
430
+ function bindQueryDependencies<
431
+ TSources extends readonly unknown[] = [],
432
+ TQueryFnData = unknown,
433
+ TError = Error,
434
+ TData = TQueryFnData,
435
+ TQueryData = TQueryFnData,
436
+ TQueryKey extends QueryKey = QueryKey,
437
+ >(
438
+ queryClient: QueryClient,
439
+ queryService: ReturnType<
440
+ typeof createQueryService<TQueryFnData, TError, TData, TQueryData, TQueryKey>
441
+ >,
442
+ queryKey: TQueryKey,
443
+ dependsOn: QueryDependencyTuple<TSources, TQueryKey>
444
+ ): QueryService<TData, TError> {
445
+ const dependencyController = createDependencyController(
446
+ queryClient,
447
+ queryKey,
448
+ queryService.setDerivedState,
449
+ dependsOn
450
+ );
451
+ let subscriberCount = 0;
452
+
453
+ return {
454
+ ...queryService.service,
455
+ refetch: async (refetchOptions) => {
456
+ dependencyController.evaluateOnce();
457
+ return queryService.service.refetch(refetchOptions);
458
+ },
459
+ subscribe: (listener) => {
460
+ if (subscriberCount === 0) {
461
+ dependencyController.activate();
462
+ }
463
+
464
+ subscriberCount += 1;
465
+
466
+ const unsubscribe = queryService.service.subscribe(listener);
467
+
468
+ return () => {
469
+ subscriberCount = Math.max(0, subscriberCount - 1);
470
+ if (subscriberCount === 0) {
471
+ dependencyController.deactivate();
472
+ }
473
+ unsubscribe();
474
+ };
475
+ },
476
+ };
477
+ }
478
+
479
+ function createDependencyController<
480
+ TSources extends readonly unknown[] = [],
481
+ TQueryFnData = unknown,
482
+ TError = Error,
483
+ TData = TQueryFnData,
484
+ TQueryData = TQueryFnData,
485
+ TQueryKey extends QueryKey = QueryKey,
486
+ >(
487
+ queryClient: QueryClient,
488
+ baseQueryKey: TQueryKey,
489
+ setDerivedState: (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => void,
490
+ dependsOn: QueryDependencyTuple<TSources, TQueryKey>
491
+ ) {
492
+ const [sourceKeys, deriveOptions] = dependsOn;
493
+ let queriesObserver: QueriesObserver | undefined;
494
+ let unsubscribe: (() => void) | undefined;
495
+
496
+ const evaluateBinding = (results: QueryObserverResult[]) => {
497
+ const snapshots = results.map((result) =>
498
+ toQueryServiceSnapshot(result)
499
+ ) as Parameters<typeof deriveOptions>[0];
500
+ const derivedOptions = deriveOptions(snapshots);
501
+
502
+ setDerivedState({
503
+ queryKey: derivedOptions.queryKey ?? baseQueryKey,
504
+ ...(derivedOptions.enabled === undefined ? {} : { enabled: derivedOptions.enabled }),
505
+ });
506
+ };
507
+
508
+ const createObserver = () =>
509
+ new QueriesObserver(
510
+ queryClient,
511
+ sourceKeys.map((sourceKey) => ({
512
+ enabled: false,
513
+ queryKey: sourceKey,
514
+ }))
515
+ );
516
+
517
+ return {
518
+ activate: () => {
519
+ if (queriesObserver) {
520
+ return;
521
+ }
522
+
523
+ queriesObserver = createObserver();
524
+ evaluateBinding(queriesObserver.getCurrentResult());
525
+ unsubscribe = queriesObserver.subscribe(evaluateBinding);
526
+ },
527
+ deactivate: () => {
528
+ unsubscribe?.();
529
+ unsubscribe = undefined;
530
+ queriesObserver = undefined;
531
+ },
532
+ evaluateOnce: () => {
533
+ if (queriesObserver) {
534
+ evaluateBinding(queriesObserver.getCurrentResult());
535
+ return;
536
+ }
537
+
538
+ const transientObserver = createObserver();
539
+ evaluateBinding(transientObserver.getCurrentResult());
540
+ transientObserver.destroy();
541
+ },
542
+ };
543
+ }
544
+
545
+ function splitQueryServiceOptions<
546
+ TQueryFnData = unknown,
547
+ TError = Error,
548
+ TData = TQueryFnData,
549
+ TQueryData = TQueryFnData,
550
+ TQueryKey extends QueryKey = QueryKey,
551
+ TSources extends readonly unknown[] = [],
552
+ >(
553
+ options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
554
+ ): {
555
+ dependsOn?: QueryDependencyTuple<TSources, TQueryKey>;
556
+ runtimeOptions: QueryServiceRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> | undefined;
557
+ } {
558
+ if (options === undefined) {
559
+ return {
560
+ dependsOn: undefined,
561
+ runtimeOptions: undefined,
562
+ };
563
+ }
564
+
565
+ const { dependsOn, ...runtimeOptions } = options;
566
+
567
+ return {
568
+ dependsOn,
569
+ runtimeOptions,
570
+ };
571
+ }
572
+
334
573
  function toQueryOptions<
335
574
  TQueryFnData = unknown,
336
575
  TError = Error,
@@ -340,7 +579,7 @@ function toQueryOptions<
340
579
  >(
341
580
  queryKey: TQueryKey,
342
581
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
343
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
582
+ options?: QueryServiceRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
344
583
  ): QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
345
584
  QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
346
585
  // Centralize option assembly so both normal queries and tracked queries build observers and