@veams/status-quo-query 0.7.1 → 0.8.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.
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?
@@ -453,6 +544,7 @@ Returns `QueryManager` with:
453
544
  - `createUntrackedQuery(queryKey, queryFn, options?)`
454
545
  - `createUntrackedMutation(mutationFn, options?)`
455
546
  - `cancelQueries(...)`
547
+ - `fetchQuery(...)`
456
548
  - `getQueryData(...)`
457
549
  - `invalidateQueries(...)`
458
550
  - `refetchQueries(...)`
@@ -461,7 +553,7 @@ Returns `QueryManager` with:
461
553
  - `setQueryData(...)`
462
554
  - `unsafe_getClient()`
463
555
 
464
- All manager methods forward directly to the corresponding `QueryClient` methods. `unsafe_getClient()` returns the raw TanStack client as an explicit escape hatch.
556
+ All manager methods forward directly to the corresponding `QueryClient` methods. `fetchQuery(...)` covers the common one-off read path without dropping to the raw client, while `unsafe_getClient()` remains the explicit escape hatch for unsupported TanStack APIs.
465
557
 
466
558
  ### Tracked Queries and Mutations
467
559
 
@@ -473,7 +565,7 @@ Tracked queries embed dependency metadata into the final query-key segment:
473
565
 
474
566
  Only `deps` participates in automatic invalidation tracking. `view` is optional and is treated as normal query-key data.
475
567
 
476
- `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.
477
569
 
478
570
  `createMutation(mutationFn, options?)` returns the same `MutationService<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
479
571
 
@@ -514,6 +606,12 @@ Creates a `createUntrackedQuery` factory bound to a `QueryClient`.
514
606
 
515
607
  `QueryServiceOptions` is based on TanStack `QueryObserverOptions`, without `queryKey` and `queryFn` because those are provided directly to `createUntrackedQuery`.
516
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
+
517
615
  `QueryService` methods:
518
616
 
519
617
  - `getSnapshot()`
@@ -27,6 +27,7 @@ export interface QueryManager {
27
27
  createUntrackedMutation: CreateUntrackedMutation;
28
28
  createQueryAndMutation: CreateQueryAndMutation;
29
29
  cancelQueries: QueryClient['cancelQueries'];
30
+ fetchQuery: QueryClient['fetchQuery'];
30
31
  getQueryData: QueryClient['getQueryData'];
31
32
  invalidateQueries: QueryClient['invalidateQueries'];
32
33
  refetchQueries: QueryClient['refetchQueries'];
package/dist/provider.js CHANGED
@@ -44,6 +44,8 @@ export function setupQueryManager(queryClient) {
44
44
  },
45
45
  // Proxy for canceling queries with this client context.
46
46
  cancelQueries: queryClient.cancelQueries.bind(queryClient),
47
+ // Proxy for fetching one query with this client context.
48
+ fetchQuery: queryClient.fetchQuery.bind(queryClient),
47
49
  // Proxy for retrieving query data with this client context.
48
50
  getQueryData: queryClient.getQueryData.bind(queryClient),
49
51
  // Proxy for invalidating queries with this client context.
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAMA,qEAAqE;AACrE,OAAO,EAKL,aAAa,EACb,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAA+C,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACxG,OAAO,EACL,sBAAsB,GAEvB,MAAM,eAAe,CAAC;AAiEvB;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAwB;IACxD,sFAAsF;IACtF,sFAAsF;IACtF,MAAM,gBAAgB,GAAG,sBAAsB,EAAE,CAAC;IAClD,MAAM,YAAY,GAAG,iBAAiB,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IACtE,MAAM,eAAe,GAAG,oBAAoB,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IAC5E,MAAM,qBAAqB,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACtD,MAAM,wBAAwB,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;IAE5D,WAAW,CAAC,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,6FAA6F;YAC7F,4FAA4F;YAC5F,gFAAgF;YAChF,gBAAgB,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACrD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,2DAA2D;IAC3D,OAAO;QACL,6CAA6C;QAC7C,cAAc,EAAE,eAAe;QAC/B,0CAA0C;QAC1C,WAAW,EAAE,YAAY;QACzB,oDAAoD;QACpD,oBAAoB,EAAE,qBAAqB;QAC3C,uDAAuD;QACvD,uBAAuB,EAAE,wBAAwB;QACjD,8DAA8D;QAC9D,sBAAsB,EAAE,CACtB,cAA+B,EAC/B,EAAE;YACF,MAAM,0BAA0B,GAE5B,CACF,UAA+C,EAC/C,OASC,EACD,EAAE;YACF,uFAAuF;YACvF,mFAAmF;YACnF,eAAe,CAMb,UAAU,EAAE;gBACZ,GAAG,OAAO;gBACV,cAAc;aACf,CAAC,CAAC;YAEL,OAAO,CAAC,YAAY,EAAE,0BAA0B,CAAU,CAAC;QAC7D,CAAC;QACD,wDAAwD;QACxD,aAAa,EAAE,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1D,4DAA4D;QAC5D,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;QACxD,2DAA2D;QAC3D,iBAAiB,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;QAClE,yDAAyD;QACzD,cAAc,EAAE,WAAW,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC;QAC5D,uDAAuD;QACvD,aAAa,EAAE,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1D,wDAAwD;QACxD,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;QACxD,yDAAyD;QACzD,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;QACxD,mDAAmD;QACnD,gBAAgB,EAAE,GAAG,EAAE,CAAC,WAAW;KACpC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAMA,qEAAqE;AACrE,OAAO,EAKL,aAAa,EACb,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAA+C,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACxG,OAAO,EACL,sBAAsB,GAEvB,MAAM,eAAe,CAAC;AAmEvB;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAwB;IACxD,sFAAsF;IACtF,sFAAsF;IACtF,MAAM,gBAAgB,GAAG,sBAAsB,EAAE,CAAC;IAClD,MAAM,YAAY,GAAG,iBAAiB,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IACtE,MAAM,eAAe,GAAG,oBAAoB,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IAC5E,MAAM,qBAAqB,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACtD,MAAM,wBAAwB,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;IAE5D,WAAW,CAAC,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,6FAA6F;YAC7F,4FAA4F;YAC5F,gFAAgF;YAChF,gBAAgB,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACrD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,2DAA2D;IAC3D,OAAO;QACL,6CAA6C;QAC7C,cAAc,EAAE,eAAe;QAC/B,0CAA0C;QAC1C,WAAW,EAAE,YAAY;QACzB,oDAAoD;QACpD,oBAAoB,EAAE,qBAAqB;QAC3C,uDAAuD;QACvD,uBAAuB,EAAE,wBAAwB;QACjD,8DAA8D;QAC9D,sBAAsB,EAAE,CACtB,cAA+B,EAC/B,EAAE;YACF,MAAM,0BAA0B,GAE5B,CACF,UAA+C,EAC/C,OASC,EACD,EAAE;YACF,uFAAuF;YACvF,mFAAmF;YACnF,eAAe,CAMb,UAAU,EAAE;gBACZ,GAAG,OAAO;gBACV,cAAc;aACf,CAAC,CAAC;YAEL,OAAO,CAAC,YAAY,EAAE,0BAA0B,CAAU,CAAC;QAC7D,CAAC;QACD,wDAAwD;QACxD,aAAa,EAAE,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1D,yDAAyD;QACzD,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC;QACpD,4DAA4D;QAC5D,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;QACxD,2DAA2D;QAC3D,iBAAiB,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;QAClE,yDAAyD;QACzD,cAAc,EAAE,WAAW,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC;QAC5D,uDAAuD;QACvD,aAAa,EAAE,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1D,wDAAwD;QACxD,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;QACxD,yDAAyD;QACzD,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;QACxD,mDAAmD;QACnD,gBAAgB,EAAE,GAAG,EAAE,CAAC,WAAW;KACpC,CAAC;AACJ,CAAC"}
package/dist/query.d.ts CHANGED
@@ -37,6 +37,18 @@ 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
  */
@@ -56,7 +68,9 @@ export interface CreateQuery {
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> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>, 'queryFn' | 'queryKey'> & {
72
+ dependsOn?: QueryDependencyTuple<any[], 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.1",
3
+ "version": "0.8.0",
4
4
  "description": "TanStack Query service layer for the VEAMS StatusQuo ecosystem.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -10,12 +10,21 @@ describe('Query Manager API', () => {
10
10
  const cancelQueriesSpy = jest.spyOn(queryClient, 'cancelQueries');
11
11
  const resetQueriesSpy = jest.spyOn(queryClient, 'resetQueries');
12
12
  const removeQueriesSpy = jest.spyOn(queryClient, 'removeQueries');
13
+ const fetchUser = jest.fn().mockResolvedValue({ id: 7 });
14
+ const fetchQuerySpy = jest.spyOn(queryClient, 'fetchQuery');
13
15
  const manager = setupQueryManager(queryClient);
14
16
 
15
17
  manager.setQueryData<{ id: number }>(['user', 42], { id: 42 });
16
18
 
17
19
  expect(manager.getQueryData<{ id: number }>(['user', 42])).toEqual({ id: 42 });
18
20
  expect(manager.unsafe_getClient()).toBe(queryClient);
21
+ await expect(
22
+ manager.fetchQuery({
23
+ queryKey: ['user', 7],
24
+ queryFn: fetchUser,
25
+ staleTime: 60_000,
26
+ })
27
+ ).resolves.toEqual({ id: 7 });
19
28
 
20
29
  await manager.invalidateQueries({ queryKey: ['user'] });
21
30
  await manager.refetchQueries({ queryKey: ['user'] });
@@ -23,6 +32,11 @@ describe('Query Manager API', () => {
23
32
  await manager.resetQueries({ queryKey: ['user'] });
24
33
  manager.removeQueries({ queryKey: ['user'] });
25
34
 
35
+ expect(fetchQuerySpy).toHaveBeenCalledWith({
36
+ queryKey: ['user', 7],
37
+ queryFn: fetchUser,
38
+ staleTime: 60_000,
39
+ });
26
40
  expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
27
41
  expect(refetchQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
28
42
  expect(cancelQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
@@ -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/provider.ts CHANGED
@@ -66,6 +66,8 @@ export interface QueryManager {
66
66
  createQueryAndMutation: CreateQueryAndMutation;
67
67
  // Cancels active queries for the specified filters.
68
68
  cancelQueries: QueryClient['cancelQueries'];
69
+ // Fetches one query through the shared QueryClient and returns the resolved data.
70
+ fetchQuery: QueryClient['fetchQuery'];
69
71
  // Synchronously retrieves a snapshot of the current query data.
70
72
  getQueryData: QueryClient['getQueryData'];
71
73
  // Marks queries as invalid to trigger a refetch if they are active.
@@ -149,6 +151,8 @@ export function setupQueryManager(queryClient: QueryClient): QueryManager {
149
151
  },
150
152
  // Proxy for canceling queries with this client context.
151
153
  cancelQueries: queryClient.cancelQueries.bind(queryClient),
154
+ // Proxy for fetching one query with this client context.
155
+ fetchQuery: queryClient.fetchQuery.bind(queryClient),
152
156
  // Proxy for retrieving query data with this client context.
153
157
  getQueryData: queryClient.getQueryData.bind(queryClient),
154
158
  // Proxy for invalidating queries with this client context.
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,6 +89,32 @@ 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
  */
@@ -142,7 +169,9 @@ export type QueryServiceOptions<
142
169
  > = Omit<
143
170
  QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
144
171
  'queryFn' | 'queryKey'
145
- >;
172
+ > & {
173
+ dependsOn?: QueryDependencyTuple<any[], TQueryKey>;
174
+ };
146
175
 
147
176
  /**
148
177
  * Extracts and maps status and fetchStatus to our QueryMetaState interface.
@@ -181,7 +210,14 @@ export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
181
210
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
182
211
  options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
183
212
  ): QueryService<TData, TError> {
184
- return createQueryService(queryClient, queryKey, queryFn, options).service;
213
+ const { dependsOn, runtimeOptions } = splitQueryServiceOptions(options);
214
+ const service = createQueryService(queryClient, queryKey, queryFn, runtimeOptions);
215
+
216
+ if (!dependsOn) {
217
+ return service.service;
218
+ }
219
+
220
+ return bindQueryDependencies(queryClient, service, queryKey, dependsOn);
185
221
  };
186
222
  }
187
223
 
@@ -211,34 +247,62 @@ export function setupTrackedQuery(
211
247
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
212
248
  options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
213
249
  ): QueryService<TData, TError> {
214
- // Validate and normalize the dependency entries once when the handle is created.
215
- const dependencies = extractTrackedDependencies(queryKey);
250
+ const { dependsOn, runtimeOptions } = splitQueryServiceOptions(options);
216
251
  // Reuse the same core query service implementation as the untracked API.
217
- const service = createQueryService(queryClient, queryKey, queryFn, options);
252
+ const service = createQueryService(queryClient, queryKey, queryFn, runtimeOptions);
218
253
  // We only need re-registration on the transition from zero to one subscribers.
219
254
  let subscriberCount = 0;
220
255
 
221
256
  // Register the current query hash immediately so future tracked mutations can find it.
222
- trackingRegistry.register(service.observer.getCurrentQuery().queryHash, dependencies);
257
+ trackingRegistry.register(
258
+ service.observer.getCurrentQuery().queryHash,
259
+ extractTrackedDependencies(service.getCurrentQueryKey())
260
+ );
261
+
262
+ const applyTrackedDerivedState = (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => {
263
+ const previousQueryHash = service.observer.getCurrentQuery().queryHash;
264
+
265
+ service.setDerivedState(derivedOptions);
266
+
267
+ const nextQueryHash = service.observer.getCurrentQuery().queryHash;
268
+
269
+ if (nextQueryHash === previousQueryHash) {
270
+ return;
271
+ }
272
+
273
+ trackingRegistry.unregister(previousQueryHash);
274
+ trackingRegistry.register(nextQueryHash, extractTrackedDependencies(service.getCurrentQueryKey()));
275
+ };
276
+
277
+ const dependencyController = dependsOn
278
+ ? createDependencyController(
279
+ queryClient,
280
+ queryKey,
281
+ applyTrackedDerivedState,
282
+ dependsOn
283
+ )
284
+ : undefined;
223
285
 
224
286
  const ensureRegistered = () => {
225
287
  // Build resolves the current live TanStack query for the stored observer options. This is
226
288
  // the same mechanism TanStack uses internally when a query gets recreated after GC.
227
289
  const liveQuery = queryClient.getQueryCache().build(
228
290
  queryClient,
229
- toQueryOptions(queryKey, queryFn, options)
291
+ service.getCurrentObserverOptions()
230
292
  );
293
+ const liveDependencies = extractTrackedDependencies(service.getCurrentQueryKey());
231
294
 
232
295
  // Re-register only when TanStack has recreated the query and the registry has already
233
296
  // cleaned up the previous hash. This keeps the edge-case handling cheap in the common case.
234
297
  if (!trackingRegistry.has(liveQuery.queryHash)) {
235
- trackingRegistry.register(liveQuery.queryHash, dependencies);
298
+ trackingRegistry.register(liveQuery.queryHash, liveDependencies);
236
299
  }
237
300
  };
238
301
 
239
302
  return {
240
303
  ...service.service,
241
304
  refetch: async (refetchOptions) => {
305
+ dependencyController?.evaluateOnce();
242
306
  // Refetch is one of the two explicit reactivation paths agreed on in the design.
243
307
  ensureRegistered();
244
308
  return service.service.refetch(refetchOptions);
@@ -247,6 +311,7 @@ export function setupTrackedQuery(
247
311
  // The first active subscriber is the other reactivation path. Re-running registration
248
312
  // here makes a previously removed query visible to tracked invalidation again.
249
313
  if (subscriberCount === 0) {
314
+ dependencyController?.activate();
250
315
  ensureRegistered();
251
316
  }
252
317
 
@@ -257,6 +322,9 @@ export function setupTrackedQuery(
257
322
  return () => {
258
323
  // Keep the counter bounded so accidental double-unsubscribe cannot push it negative.
259
324
  subscriberCount = Math.max(0, subscriberCount - 1);
325
+ if (subscriberCount === 0) {
326
+ dependencyController?.deactivate();
327
+ }
260
328
  unsubscribe();
261
329
  };
262
330
  },
@@ -293,20 +361,43 @@ function createQueryService<
293
361
  queryClient: QueryClient,
294
362
  queryKey: TQueryKey,
295
363
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
296
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
364
+ options?: QueryServiceRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
297
365
  ): {
298
366
  // Expose the observer internally so tracked queries can access the current query hash.
299
367
  observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
300
368
  // Preserve the public query-service shape for all callers.
301
369
  service: QueryService<TData, TError>;
370
+ getCurrentObserverOptions: () => QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
371
+ QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
372
+ getCurrentQueryKey: () => TQueryKey;
373
+ setDerivedState: (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => void;
302
374
  } {
375
+ const baseQueryKey = queryKey;
376
+ const baseOptions = options;
377
+ let resolvedQueryKey = baseQueryKey;
378
+ let resolvedOptions = baseOptions;
379
+
303
380
  const observer = new QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
304
381
  queryClient,
305
- toQueryOptions(queryKey, queryFn, options)
382
+ toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions)
306
383
  );
307
384
 
385
+ const setDerivedState = (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => {
386
+ resolvedQueryKey = derivedOptions.queryKey ?? baseQueryKey;
387
+ resolvedOptions = {
388
+ ...baseOptions,
389
+ ...(derivedOptions.enabled === undefined ? {} : { enabled: derivedOptions.enabled }),
390
+ };
391
+ observer.setOptions(toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions));
392
+ };
393
+
394
+ const getCurrentObserverOptions = () => toQueryOptions(resolvedQueryKey, queryFn, resolvedOptions);
395
+
308
396
  return {
309
397
  observer,
398
+ getCurrentObserverOptions,
399
+ getCurrentQueryKey: () => resolvedQueryKey,
400
+ setDerivedState,
310
401
  service: {
311
402
  getSnapshot: () => toQueryServiceSnapshot(observer.getCurrentResult()),
312
403
  subscribe: (listener) =>
@@ -319,7 +410,7 @@ function createQueryService<
319
410
  queryClient.invalidateQueries(
320
411
  {
321
412
  exact: true,
322
- queryKey,
413
+ queryKey: resolvedQueryKey,
323
414
  ...(invalidateOptions?.refetchType === undefined
324
415
  ? {}
325
416
  : { refetchType: invalidateOptions.refetchType }),
@@ -331,6 +422,146 @@ function createQueryService<
331
422
  };
332
423
  }
333
424
 
425
+ function bindQueryDependencies<
426
+ TQueryFnData = unknown,
427
+ TError = Error,
428
+ TData = TQueryFnData,
429
+ TQueryData = TQueryFnData,
430
+ TQueryKey extends QueryKey = QueryKey,
431
+ >(
432
+ queryClient: QueryClient,
433
+ queryService: ReturnType<
434
+ typeof createQueryService<TQueryFnData, TError, TData, TQueryData, TQueryKey>
435
+ >,
436
+ queryKey: TQueryKey,
437
+ dependsOn: QueryDependencyTuple<any[], TQueryKey>
438
+ ): QueryService<TData, TError> {
439
+ const dependencyController = createDependencyController(
440
+ queryClient,
441
+ queryKey,
442
+ queryService.setDerivedState,
443
+ dependsOn
444
+ );
445
+ let subscriberCount = 0;
446
+
447
+ return {
448
+ ...queryService.service,
449
+ refetch: async (refetchOptions) => {
450
+ dependencyController.evaluateOnce();
451
+ return queryService.service.refetch(refetchOptions);
452
+ },
453
+ subscribe: (listener) => {
454
+ if (subscriberCount === 0) {
455
+ dependencyController.activate();
456
+ }
457
+
458
+ subscriberCount += 1;
459
+
460
+ const unsubscribe = queryService.service.subscribe(listener);
461
+
462
+ return () => {
463
+ subscriberCount = Math.max(0, subscriberCount - 1);
464
+ if (subscriberCount === 0) {
465
+ dependencyController.deactivate();
466
+ }
467
+ unsubscribe();
468
+ };
469
+ },
470
+ };
471
+ }
472
+
473
+ function createDependencyController<
474
+ TQueryFnData = unknown,
475
+ TError = Error,
476
+ TData = TQueryFnData,
477
+ TQueryData = TQueryFnData,
478
+ TQueryKey extends QueryKey = QueryKey,
479
+ >(
480
+ queryClient: QueryClient,
481
+ baseQueryKey: TQueryKey,
482
+ setDerivedState: (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => void,
483
+ dependsOn: QueryDependencyTuple<any[], TQueryKey>
484
+ ) {
485
+ const [sourceKeys, deriveOptions] = dependsOn;
486
+ let queriesObserver: QueriesObserver | undefined;
487
+ let unsubscribe: (() => void) | undefined;
488
+
489
+ const evaluateBinding = (results: QueryObserverResult[]) => {
490
+ const snapshots = results.map((result) =>
491
+ toQueryServiceSnapshot(result)
492
+ ) as Parameters<typeof deriveOptions>[0];
493
+ const derivedOptions = deriveOptions(snapshots);
494
+
495
+ setDerivedState({
496
+ queryKey: derivedOptions.queryKey ?? baseQueryKey,
497
+ ...(derivedOptions.enabled === undefined ? {} : { enabled: derivedOptions.enabled }),
498
+ });
499
+ };
500
+
501
+ const createObserver = () =>
502
+ new QueriesObserver(
503
+ queryClient,
504
+ sourceKeys.map((sourceKey) => ({
505
+ enabled: false,
506
+ queryKey: sourceKey,
507
+ }))
508
+ );
509
+
510
+ return {
511
+ activate: () => {
512
+ if (queriesObserver) {
513
+ return;
514
+ }
515
+
516
+ queriesObserver = createObserver();
517
+ evaluateBinding(queriesObserver.getCurrentResult());
518
+ unsubscribe = queriesObserver.subscribe(evaluateBinding);
519
+ },
520
+ deactivate: () => {
521
+ unsubscribe?.();
522
+ unsubscribe = undefined;
523
+ queriesObserver = undefined;
524
+ },
525
+ evaluateOnce: () => {
526
+ if (queriesObserver) {
527
+ evaluateBinding(queriesObserver.getCurrentResult());
528
+ return;
529
+ }
530
+
531
+ const transientObserver = createObserver();
532
+ evaluateBinding(transientObserver.getCurrentResult());
533
+ transientObserver.destroy();
534
+ },
535
+ };
536
+ }
537
+
538
+ function splitQueryServiceOptions<
539
+ TQueryFnData = unknown,
540
+ TError = Error,
541
+ TData = TQueryFnData,
542
+ TQueryData = TQueryFnData,
543
+ TQueryKey extends QueryKey = QueryKey,
544
+ >(
545
+ options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
546
+ ): {
547
+ dependsOn?: QueryDependencyTuple<any[], TQueryKey>;
548
+ runtimeOptions: QueryServiceRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> | undefined;
549
+ } {
550
+ if (options === undefined) {
551
+ return {
552
+ dependsOn: undefined,
553
+ runtimeOptions: undefined,
554
+ };
555
+ }
556
+
557
+ const { dependsOn, ...runtimeOptions } = options;
558
+
559
+ return {
560
+ dependsOn,
561
+ runtimeOptions,
562
+ };
563
+ }
564
+
334
565
  function toQueryOptions<
335
566
  TQueryFnData = unknown,
336
567
  TError = Error,
@@ -340,7 +571,7 @@ function toQueryOptions<
340
571
  >(
341
572
  queryKey: TQueryKey,
342
573
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
343
- options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
574
+ options?: QueryServiceRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
344
575
  ): QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
345
576
  QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
346
577
  // Centralize option assembly so both normal queries and tracked queries build observers and