@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 +100 -2
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +2 -0
- package/dist/provider.js.map +1 -1
- package/dist/query.d.ts +16 -1
- package/dist/query.js +128 -10
- package/dist/query.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/provider.spec.ts +14 -0
- package/src/__tests__/query.spec.ts +234 -0
- package/src/__tests__/tracked.spec.ts +127 -0
- package/src/provider.ts +4 -0
- package/src/query.ts +243 -12
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. `
|
|
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
|
|
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()`
|
package/dist/provider.d.ts
CHANGED
|
@@ -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.
|
package/dist/provider.js.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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":"AAAA,OAAO;AAaL,iEAAiE;AACjE,aAAa,
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO;AAaL,iEAAiE;AACjE,aAAa,EACb,eAAe,GAUhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAIL,0BAA0B,GAC3B,MAAM,eAAe,CAAC;AA+IvB;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA6E;IAE7E,6DAA6D;IAC7D,OAAO;QACL,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;KACxB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAqB;IAClD,mEAAmE;IACnE,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,KAAK,UAAU,CAAC;AACxE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,WAAwB;IACjD,8EAA8E;IAC9E,OAAO,SAAS,WAAW,CAOzB,QAAmB,EACnB,OAA+C,EAC/C,OAAiF;QAEjF,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,kBAAkB,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAEnF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,OAAO,CAAC,OAAO,CAAC;QACzB,CAAC;QAED,OAAO,qBAAqB,CAAC,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC1E,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,WAAwB,EACxB,gBAAkC;IAElC,OAAO,SAAS,WAAW,CAQzB,QAAmB,EACnB,OAA+C,EAC/C,OAAiF;QAEjF,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACxE,yEAAyE;QACzE,MAAM,OAAO,GAAG,kBAAkB,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QACnF,+EAA+E;QAC/E,IAAI,eAAe,GAAG,CAAC,CAAC;QAExB,uFAAuF;QACvF,gBAAgB,CAAC,QAAQ,CACvB,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,SAAS,EAC5C,0BAA0B,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CACzD,CAAC;QAEF,MAAM,wBAAwB,GAAG,CAAC,cAAwD,EAAE,EAAE;YAC5F,MAAM,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,SAAS,CAAC;YAEvE,OAAO,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;YAExC,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,SAAS,CAAC;YAEnE,IAAI,aAAa,KAAK,iBAAiB,EAAE,CAAC;gBACxC,OAAO;YACT,CAAC;YAED,gBAAgB,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;YAC/C,gBAAgB,CAAC,QAAQ,CAAC,aAAa,EAAE,0BAA0B,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC;QACrG,CAAC,CAAC;QAEF,MAAM,oBAAoB,GAAG,SAAS;YACpC,CAAC,CAAC,0BAA0B,CACxB,WAAW,EACX,QAAQ,EACR,wBAAwB,EACxB,SAAS,CACV;YACH,CAAC,CAAC,SAAS,CAAC;QAEd,MAAM,gBAAgB,GAAG,GAAG,EAAE;YAC5B,0FAA0F;YAC1F,oFAAoF;YACpF,MAAM,SAAS,GAAG,WAAW,CAAC,aAAa,EAAE,CAAC,KAAK,CACjD,WAAW,EACX,OAAO,CAAC,yBAAyB,EAAE,CACpC,CAAC;YACF,MAAM,gBAAgB,GAAG,0BAA0B,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;YAElF,sFAAsF;YACtF,4FAA4F;YAC5F,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC/C,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;YACnE,CAAC;QACH,CAAC,CAAC;QAEF,OAAO;YACL,GAAG,OAAO,CAAC,OAAO;YAClB,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;gBAChC,oBAAoB,EAAE,YAAY,EAAE,CAAC;gBACrC,iFAAiF;gBACjF,gBAAgB,EAAE,CAAC;gBACnB,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YACjD,CAAC;YACD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;gBACtB,sFAAsF;gBACtF,+EAA+E;gBAC/E,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;oBAC1B,oBAAoB,EAAE,QAAQ,EAAE,CAAC;oBACjC,gBAAgB,EAAE,CAAC;gBACrB,CAAC;gBAED,eAAe,IAAI,CAAC,CAAC;gBAErB,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;gBAExD,OAAO,GAAG,EAAE;oBACV,qFAAqF;oBACrF,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,CAAC,CAAC;oBACnD,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;wBAC1B,oBAAoB,EAAE,UAAU,EAAE,CAAC;oBACrC,CAAC;oBACD,WAAW,EAAE,CAAC;gBAChB,CAAC,CAAC;YACJ,CAAC;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,sBAAsB,CAC7B,MAA0C;IAE1C,uEAAuE;IACvE,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAOzB,WAAwB,EACxB,QAAmB,EACnB,OAA+C,EAC/C,OAAwF;IAWxF,MAAM,YAAY,GAAG,QAAQ,CAAC;IAC9B,MAAM,WAAW,GAAG,OAAO,CAAC;IAC5B,IAAI,gBAAgB,GAAG,YAAY,CAAC;IACpC,IAAI,eAAe,GAAG,WAAW,CAAC;IAElC,MAAM,QAAQ,GAAG,IAAI,aAAa,CAChC,WAAW,EACX,cAAc,CAAC,gBAAgB,EAAE,OAAO,EAAE,eAAe,CAAC,CAC3D,CAAC;IAEF,MAAM,eAAe,GAAG,CAAC,cAAwD,EAAE,EAAE;QACnF,gBAAgB,GAAG,cAAc,CAAC,QAAQ,IAAI,YAAY,CAAC;QAC3D,eAAe,GAAG;YAChB,GAAG,WAAW;YACd,GAAG,CAAC,cAAc,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,EAAE,CAAC;SACrF,CAAC;QACF,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,gBAAgB,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC;IAClF,CAAC,CAAC;IAEF,MAAM,yBAAyB,GAAG,GAAG,EAAE,CAAC,cAAc,CAAC,gBAAgB,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC;IAEnG,OAAO;QACL,QAAQ;QACR,yBAAyB;QACzB,kBAAkB,EAAE,GAAG,EAAE,CAAC,gBAAgB;QAC1C,eAAe;QACf,OAAO,EAAE;YACP,WAAW,EAAE,GAAG,EAAE,CAAC,sBAAsB,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YACtE,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CACtB,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;gBAC5B,QAAQ,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;YAC3C,CAAC,CAAC;YACJ,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,CAChC,sBAAsB,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YAChE,UAAU,EAAE,CAAC,iBAAiB,EAAE,EAAE,CAChC,WAAW,CAAC,iBAAiB,CAC3B;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,gBAAgB;gBAC1B,GAAG,CAAC,iBAAiB,EAAE,WAAW,KAAK,SAAS;oBAC9C,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,EAAE,WAAW,EAAE,iBAAiB,CAAC,WAAW,EAAE,CAAC;aACpD,EACD,mBAAmB,CAAC,iBAAiB,CAAC,CACvC;YACH,gBAAgB,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,gBAAgB,EAAE;SACpD;KACF,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAO5B,WAAwB,EACxB,YAEC,EACD,QAAmB,EACnB,SAAiD;IAEjD,MAAM,oBAAoB,GAAG,0BAA0B,CACrD,WAAW,EACX,QAAQ,EACR,YAAY,CAAC,eAAe,EAC5B,SAAS,CACV,CAAC;IACF,IAAI,eAAe,GAAG,CAAC,CAAC;IAExB,OAAO;QACL,GAAG,YAAY,CAAC,OAAO;QACvB,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;YAChC,oBAAoB,CAAC,YAAY,EAAE,CAAC;YACpC,OAAO,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;YACtB,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;gBAC1B,oBAAoB,CAAC,QAAQ,EAAE,CAAC;YAClC,CAAC;YAED,eAAe,IAAI,CAAC,CAAC;YAErB,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YAE7D,OAAO,GAAG,EAAE;gBACV,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,CAAC,CAAC;gBACnD,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;oBAC1B,oBAAoB,CAAC,UAAU,EAAE,CAAC;gBACpC,CAAC;gBACD,WAAW,EAAE,CAAC;YAChB,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,0BAA0B,CAOjC,WAAwB,EACxB,YAAuB,EACvB,eAAmF,EACnF,SAAiD;IAEjD,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,SAAS,CAAC;IAC9C,IAAI,eAA4C,CAAC;IACjD,IAAI,WAAqC,CAAC;IAE1C,MAAM,eAAe,GAAG,CAAC,OAA8B,EAAE,EAAE;QACzD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CACvC,sBAAsB,CAAC,MAAM,CAAC,CACQ,CAAC;QACzC,MAAM,cAAc,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;QAEhD,eAAe,CAAC;YACd,QAAQ,EAAE,cAAc,CAAC,QAAQ,IAAI,YAAY;YACjD,GAAG,CAAC,cAAc,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,EAAE,CAAC;SACrF,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,GAAG,EAAE,CAC1B,IAAI,eAAe,CACjB,WAAW,EACX,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC7B,OAAO,EAAE,KAAK;QACd,QAAQ,EAAE,SAAS;KACpB,CAAC,CAAC,CACJ,CAAC;IAEJ,OAAO;QACL,QAAQ,EAAE,GAAG,EAAE;YACb,IAAI,eAAe,EAAE,CAAC;gBACpB,OAAO;YACT,CAAC;YAED,eAAe,GAAG,cAAc,EAAE,CAAC;YACnC,eAAe,CAAC,eAAe,CAAC,gBAAgB,EAAE,CAAC,CAAC;YACpD,WAAW,GAAG,eAAe,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QAC3D,CAAC;QACD,UAAU,EAAE,GAAG,EAAE;YACf,WAAW,EAAE,EAAE,CAAC;YAChB,WAAW,GAAG,SAAS,CAAC;YACxB,eAAe,GAAG,SAAS,CAAC;QAC9B,CAAC;QACD,YAAY,EAAE,GAAG,EAAE;YACjB,IAAI,eAAe,EAAE,CAAC;gBACpB,eAAe,CAAC,eAAe,CAAC,gBAAgB,EAAE,CAAC,CAAC;gBACpD,OAAO;YACT,CAAC;YAED,MAAM,iBAAiB,GAAG,cAAc,EAAE,CAAC;YAC3C,eAAe,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,CAAC,CAAC;YACtD,iBAAiB,CAAC,OAAO,EAAE,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,wBAAwB,CAO/B,OAAiF;IAKjF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;YACL,SAAS,EAAE,SAAS;YACpB,cAAc,EAAE,SAAS;SAC1B,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;IAEjD,OAAO;QACL,SAAS;QACT,cAAc;KACf,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAOrB,QAAmB,EACnB,OAA+C,EAC/C,OAAwF;IAGxF,4FAA4F;IAC5F,8DAA8D;IAC9D,OAAO;QACL,GAAG,OAAO;QACV,OAAO;QACP,QAAQ;KACT,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,OAAgC;IAC3D,gDAAgD;IAChD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,6EAA6E;IAC7E,MAAM,iBAAiB,GAAsB;QAC3C,GAAG,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;QACxF,GAAG,CAAC,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;KACtF,CAAC;IAEF,sEAAsE;IACtE,OAAO,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC;AACnF,CAAC"}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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?:
|
|
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(
|
|
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?:
|
|
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
|