@veams/status-quo-query 0.10.0 → 0.12.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 +212 -10
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- 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 +26 -15
- package/dist/query.js +40 -31
- package/dist/query.js.map +1 -1
- package/dist/react/hooks/index.d.ts +1 -0
- package/dist/react/hooks/index.js +2 -0
- package/dist/react/hooks/index.js.map +1 -0
- package/dist/react/hooks/use-query-handle.d.ts +2 -0
- package/dist/react/hooks/use-query-handle.js +71 -0
- package/dist/react/hooks/use-query-handle.js.map +1 -0
- package/dist/react/hooks/use-query-subscription.d.ts +1 -0
- package/dist/react/hooks/use-query-subscription.js +2 -0
- package/dist/react/hooks/use-query-subscription.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/eslint.config.mjs +13 -2
- package/jest.config.cjs +2 -2
- package/package.json +21 -10
- package/src/__tests__/provider.spec.ts +8 -0
- package/src/index.ts +0 -2
- package/src/provider.ts +6 -2
- package/src/query.ts +84 -68
- package/src/react/__tests__/use-query-handle.spec.tsx +104 -0
- package/src/react/hooks/index.ts +1 -0
- package/src/react/hooks/use-query-handle.ts +96 -0
- package/src/react/index.ts +1 -0
- package/tsconfig.eslint.json +2 -1
- package/tsconfig.json +5 -2
- package/dist/query-registry.d.ts +0 -9
- package/dist/query-registry.js +0 -28
- package/dist/query-registry.js.map +0 -1
- package/src/__tests__/query-registry.spec.ts +0 -101
- package/src/query-registry.ts +0 -52
package/README.md
CHANGED
|
@@ -8,6 +8,25 @@ TanStack Query service helpers with a small subscribable surface that fits natur
|
|
|
8
8
|
npm install @veams/status-quo-query @tanstack/query-core
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
React bindings are available through an optional peer dependency:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install react
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Mental Model
|
|
18
|
+
|
|
19
|
+
Status Quo Query deliberately keeps the public surface small:
|
|
20
|
+
|
|
21
|
+
- `QueryHandle<TData, TError>` is the read handle for one query.
|
|
22
|
+
- `MutationService<TData, TError, TVariables>` is the write handle for one mutation.
|
|
23
|
+
- snapshots are passive state objects returned from `getSnapshot()` and `subscribe(...)`.
|
|
24
|
+
- commands stay on the handle: `refetch()`, `invalidate()`, `mutate()`, `reset()`.
|
|
25
|
+
- `QueryManager` is the broader coordination layer for cross-query work.
|
|
26
|
+
- `@veams/status-quo-query/react` is optional and adds one React subscription hook over the same handle shape.
|
|
27
|
+
|
|
28
|
+
That keeps the package usable in service code, query handlers, state handlers, and React components without changing the core query or mutation API.
|
|
29
|
+
|
|
11
30
|
## Package Exports
|
|
12
31
|
|
|
13
32
|
Root exports:
|
|
@@ -16,6 +35,7 @@ Root exports:
|
|
|
16
35
|
- `setupQuery`
|
|
17
36
|
- `setupMutation`
|
|
18
37
|
- `isQueryLoading`
|
|
38
|
+
- `toQueryHandleData`
|
|
19
39
|
- `toQueryMetaState`
|
|
20
40
|
- `QueryFetchStatus`
|
|
21
41
|
- `QueryStatus`
|
|
@@ -27,12 +47,13 @@ Root exports:
|
|
|
27
47
|
- `CreateMutationWithDefaults`
|
|
28
48
|
- `CreateUntrackedQuery`
|
|
29
49
|
- `CreateUntrackedMutation`
|
|
30
|
-
- `
|
|
50
|
+
- `QueryHandle`
|
|
51
|
+
- `QueryHandleData`
|
|
31
52
|
- `MutationService`
|
|
32
|
-
- `
|
|
53
|
+
- `QueryHandleSnapshot`
|
|
33
54
|
- `MutationServiceSnapshot`
|
|
34
55
|
- `QueryDependencyTuple`
|
|
35
|
-
- `
|
|
56
|
+
- `QueryHandleOptions`
|
|
36
57
|
- `MutationServiceOptions`
|
|
37
58
|
- `TrackedMutationServiceOptions`
|
|
38
59
|
- `QueryInvalidateOptions`
|
|
@@ -49,6 +70,7 @@ Subpath exports:
|
|
|
49
70
|
- `@veams/status-quo-query/provider`
|
|
50
71
|
- `@veams/status-quo-query/query`
|
|
51
72
|
- `@veams/status-quo-query/mutation`
|
|
73
|
+
- `@veams/status-quo-query/react`
|
|
52
74
|
|
|
53
75
|
## Quickstart
|
|
54
76
|
|
|
@@ -109,6 +131,86 @@ await userQuery.refetch();
|
|
|
109
131
|
await userQuery.invalidate({ refetchType: 'none' });
|
|
110
132
|
```
|
|
111
133
|
|
|
134
|
+
## React Bindings
|
|
135
|
+
|
|
136
|
+
The React entrypoint exposes `useQueryHandle(...)` and keeps `react` optional unless you
|
|
137
|
+
import `@veams/status-quo-query/react`.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { useQueryHandle } from '@veams/status-quo-query/react';
|
|
141
|
+
import type { QueryHandle } from '@veams/status-quo-query';
|
|
142
|
+
|
|
143
|
+
function ProductName({ query }: { query: QueryHandle<{ name: string }, Error> }) {
|
|
144
|
+
const snapshot = useQueryHandle(query);
|
|
145
|
+
|
|
146
|
+
return <span>{snapshot.data?.name ?? 'loading'}</span>;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use the hook when a component should subscribe directly to a query handle and render from its latest snapshot. Keep mapping at the component level:
|
|
151
|
+
|
|
152
|
+
- read `data`, `status`, `fetchStatus`, and flags like `isPending` from the snapshot
|
|
153
|
+
- call `query.refetch()` or `query.invalidate()` on the handle itself
|
|
154
|
+
- derive view-specific values in the component instead of adding selector logic to the hook
|
|
155
|
+
|
|
156
|
+
## Status Quo Integration
|
|
157
|
+
|
|
158
|
+
The same query handle can also feed a `status-quo` handler through `bindSubscribable(...)`.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { NativeStateHandler } from '@veams/status-quo';
|
|
162
|
+
import {
|
|
163
|
+
toQueryMetaState,
|
|
164
|
+
type QueryMetaState,
|
|
165
|
+
type QueryHandle,
|
|
166
|
+
} from '@veams/status-quo-query';
|
|
167
|
+
|
|
168
|
+
type Product = {
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type ProductCardState = {
|
|
174
|
+
product: Product | undefined;
|
|
175
|
+
query: QueryMetaState;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type ProductCardActions = {
|
|
179
|
+
refresh: () => Promise<void>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export class ProductCardHandler extends NativeStateHandler<ProductCardState, ProductCardActions> {
|
|
183
|
+
constructor(private readonly productQuery: QueryHandle<Product, Error>) {
|
|
184
|
+
super({
|
|
185
|
+
initialState: {
|
|
186
|
+
product: productQuery.getSnapshot().data,
|
|
187
|
+
query: toQueryMetaState(productQuery.getSnapshot()),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
this.bindSubscribable(productQuery, (snapshot) => {
|
|
192
|
+
this.setState(
|
|
193
|
+
{
|
|
194
|
+
product: snapshot.data,
|
|
195
|
+
query: toQueryMetaState(snapshot),
|
|
196
|
+
},
|
|
197
|
+
'query:update'
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getActions(): ProductCardActions {
|
|
203
|
+
return {
|
|
204
|
+
refresh: async () => {
|
|
205
|
+
await this.productQuery.refetch();
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Use that approach when query state is only one input into a broader UI state model and the handler should remain the view-facing boundary.
|
|
213
|
+
|
|
112
214
|
## Why Tracked Invalidation
|
|
113
215
|
|
|
114
216
|
TanStack Query gives you flexible invalidation primitives, but the application still has to know which keys to invalidate after every mutation. Tracked invalidation moves that bookkeeping into the facade:
|
|
@@ -400,7 +502,7 @@ Use `dependsOn` when a query needs data from other queries before it can run.
|
|
|
400
502
|
|
|
401
503
|
`dependsOn` accepts a `QueryDependencyTuple`:
|
|
402
504
|
|
|
403
|
-
- an ordered list of source query
|
|
505
|
+
- an ordered list of source query handles
|
|
404
506
|
- a `deriveOptions(...)` callback that returns only `queryKey` and/or `enabled`
|
|
405
507
|
|
|
406
508
|
The watcher starts on the first `subscribe(...)` or `refetch()`, reads the current source snapshots immediately, and stops after the last unsubscribe. A downstream `refetch()` refetches all source services first, then refetches the derived query.
|
|
@@ -550,6 +652,7 @@ Returns `QueryManager` with:
|
|
|
550
652
|
- `cancelQueries(...)`
|
|
551
653
|
- `fetchQuery(...)`
|
|
552
654
|
- `getQueryData(...)`
|
|
655
|
+
- `getQueryState(...)`
|
|
553
656
|
- `invalidateQueries(...)`
|
|
554
657
|
- `refetchQueries(...)`
|
|
555
658
|
- `removeQueries(...)`
|
|
@@ -559,6 +662,95 @@ Returns `QueryManager` with:
|
|
|
559
662
|
|
|
560
663
|
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.
|
|
561
664
|
|
|
665
|
+
### How to write a service
|
|
666
|
+
|
|
667
|
+
Do not memoize `QueryHandle` instances in a package-level registry.
|
|
668
|
+
|
|
669
|
+
TanStack already deduplicates cached queries by `queryKey`. A `QueryHandle` is a handle over that cached state, closer to a TanStack `QueryObserver` than to the cached query entry itself. Creating a fresh handle per service method call is fine when the caller wants a live query handle.
|
|
670
|
+
|
|
671
|
+
Use this split in a query handler:
|
|
672
|
+
|
|
673
|
+
- return fresh query handles from methods that expose `refetch()`, `subscribe(...)`, or `invalidate()`
|
|
674
|
+
- read cache state directly from `QueryManager` in state-only methods
|
|
675
|
+
- add smaller data-only methods when callers do not need fetch metadata
|
|
676
|
+
|
|
677
|
+
Example:
|
|
678
|
+
|
|
679
|
+
```ts
|
|
680
|
+
import type {
|
|
681
|
+
QueryHandle,
|
|
682
|
+
QueryHandleData,
|
|
683
|
+
QueryHandleSnapshot,
|
|
684
|
+
} from '@veams/status-quo-query';
|
|
685
|
+
|
|
686
|
+
type Company = {
|
|
687
|
+
id: string;
|
|
688
|
+
name: string;
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// Shared key factories keep the live handle path and snapshot path aligned.
|
|
692
|
+
const companiesQueryKey = ['companies'] as const;
|
|
693
|
+
const companyByIdQueryKey = (companyId: string) => ['company', companyId] as const;
|
|
694
|
+
|
|
695
|
+
export interface CompanyQueryHandler {
|
|
696
|
+
getCompaniesQuery: () => QueryHandle<Company[], Error>;
|
|
697
|
+
getCompanyQueryById: (companyId: string) => QueryHandle<Company, Error>;
|
|
698
|
+
getCompanyStateById: (companyId: string) => QueryHandleSnapshot<Company, Error>;
|
|
699
|
+
getCompanyDataById: (companyId: string) => QueryHandleData<Company, Error>;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function createCompanyQueryHandler(): CompanyQueryHandler {
|
|
703
|
+
const manager = getQueryManager();
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
// Return a fresh query handle when callers need commands or subscriptions.
|
|
707
|
+
getCompaniesQuery() {
|
|
708
|
+
return manager.createUntrackedQuery(companiesQueryKey, fetchCompanies, {
|
|
709
|
+
staleTime: companyStaleTime,
|
|
710
|
+
});
|
|
711
|
+
},
|
|
712
|
+
// Parameterized query handles are cheap and map directly to the final query key.
|
|
713
|
+
getCompanyQueryById(companyId) {
|
|
714
|
+
const queryKey = companyByIdQueryKey(companyId);
|
|
715
|
+
|
|
716
|
+
return manager.createUntrackedQuery(queryKey, () => fetchCompanyById(companyId), {
|
|
717
|
+
staleTime: companyStaleTime,
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
// Snapshot-only reads should use the manager cache APIs instead of building another handle.
|
|
721
|
+
getCompanyStateById(companyId) {
|
|
722
|
+
const queryKey = companyByIdQueryKey(companyId);
|
|
723
|
+
const state = manager.getQueryState(queryKey);
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
data: manager.getQueryData(queryKey),
|
|
727
|
+
error: (state?.error as Error | null | undefined) ?? null,
|
|
728
|
+
fetchStatus: state?.fetchStatus ?? 'idle',
|
|
729
|
+
status: state?.status ?? 'pending',
|
|
730
|
+
isError: state?.status === 'error',
|
|
731
|
+
isFetching: state?.fetchStatus === 'fetching',
|
|
732
|
+
isPending: state?.status === 'pending',
|
|
733
|
+
isSuccess: state?.status === 'success',
|
|
734
|
+
};
|
|
735
|
+
},
|
|
736
|
+
// Data-only reads can stay even smaller when the caller does not need fetch meta state.
|
|
737
|
+
getCompanyDataById(companyId) {
|
|
738
|
+
const queryKey = companyByIdQueryKey(companyId);
|
|
739
|
+
const state = manager.getQueryState(queryKey);
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
data: manager.getQueryData(queryKey),
|
|
743
|
+
error: (state?.error as Error | null | undefined) ?? null,
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
In this example, `getQueryManager()` is your application-level accessor for the shared `QueryManager`.
|
|
751
|
+
|
|
752
|
+
This keeps the query handler focused on one feature area, supports parameterized query methods naturally, and offers both full state reads and smaller data-only reads without creating extra handle instances.
|
|
753
|
+
|
|
562
754
|
### Tracked Queries and Mutations
|
|
563
755
|
|
|
564
756
|
Tracked queries embed dependency metadata into the final query-key segment:
|
|
@@ -569,7 +761,7 @@ Tracked queries embed dependency metadata into the final query-key segment:
|
|
|
569
761
|
|
|
570
762
|
Only `deps` participates in automatic invalidation tracking. `view` is optional and is treated as normal query-key data.
|
|
571
763
|
|
|
572
|
-
`createQuery(queryKey, queryFn, options?)` returns the same `
|
|
764
|
+
`createQuery(queryKey, queryFn, options?)` returns the same `QueryHandle<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.
|
|
573
765
|
|
|
574
766
|
`createMutation(mutationFn, options?)` returns the same `MutationService<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
|
|
575
767
|
|
|
@@ -606,17 +798,17 @@ Reach for standalone `createMutation(...)` when:
|
|
|
606
798
|
|
|
607
799
|
Creates a `createUntrackedQuery` factory bound to a `QueryClient`.
|
|
608
800
|
|
|
609
|
-
`createUntrackedQuery(queryKey, queryFn, options?)` returns `
|
|
801
|
+
`createUntrackedQuery(queryKey, queryFn, options?)` returns `QueryHandle<TData, TError>`.
|
|
610
802
|
|
|
611
|
-
`
|
|
803
|
+
`QueryHandleOptions` is based on TanStack `QueryObserverOptions`, without `queryKey` and `queryFn` because those are provided directly to `createUntrackedQuery`.
|
|
612
804
|
|
|
613
805
|
It also adds:
|
|
614
806
|
|
|
615
807
|
- `dependsOn?: QueryDependencyTuple<[...sources]>`
|
|
616
808
|
|
|
617
|
-
`dependsOn` observes the listed source query
|
|
809
|
+
`dependsOn` observes the listed source query handles and lets the downstream query derive only `queryKey` and `enabled`. Source handles are activated while the downstream query is active, and downstream `refetch()` refetches the sources first. The public `QueryHandle` API does not change when this option is used.
|
|
618
810
|
|
|
619
|
-
`
|
|
811
|
+
`QueryHandle` methods:
|
|
620
812
|
|
|
621
813
|
- `getSnapshot()`
|
|
622
814
|
- `subscribe(listener)`
|
|
@@ -624,7 +816,7 @@ It also adds:
|
|
|
624
816
|
- `invalidate(options?)`
|
|
625
817
|
- `unsafe_getResult()`
|
|
626
818
|
|
|
627
|
-
`
|
|
819
|
+
`QueryHandleSnapshot<TData, TError>` fields:
|
|
628
820
|
|
|
629
821
|
- `data`
|
|
630
822
|
- `error`
|
|
@@ -635,6 +827,11 @@ It also adds:
|
|
|
635
827
|
- `isPending`
|
|
636
828
|
- `isSuccess`
|
|
637
829
|
|
|
830
|
+
`QueryHandleData<TData, TError>` fields:
|
|
831
|
+
|
|
832
|
+
- `data`
|
|
833
|
+
- `error`
|
|
834
|
+
|
|
638
835
|
`invalidate(options?)` invalidates the query by its exact key. `QueryInvalidateOptions` supports:
|
|
639
836
|
|
|
640
837
|
- `refetchType`
|
|
@@ -674,6 +871,11 @@ Creates a `createUntrackedMutation` factory bound to a `QueryClient`.
|
|
|
674
871
|
|
|
675
872
|
### Query Helpers
|
|
676
873
|
|
|
874
|
+
`toQueryHandleData(snapshot)` reduces a query snapshot to:
|
|
875
|
+
|
|
876
|
+
- `data`
|
|
877
|
+
- `error`
|
|
878
|
+
|
|
677
879
|
`toQueryMetaState(snapshot)` reduces a query snapshot to:
|
|
678
880
|
|
|
679
881
|
- `status`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export * from './mutation.js';
|
|
2
2
|
export * from './query.js';
|
|
3
3
|
export * from './provider.js';
|
|
4
|
-
export * from './query-registry.js';
|
|
5
4
|
export type { TrackedDependencyRecord, TrackedDependencyValue, TrackedInvalidateOn, TrackedMatchMode, TrackedQueryKey, TrackedQueryKeySegment, } from './tracking.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,4 @@ export * from './mutation.js';
|
|
|
4
4
|
export * from './query.js';
|
|
5
5
|
// Re-export all provider-related types and functions for cache management.
|
|
6
6
|
export * from './provider.js';
|
|
7
|
-
// Re-export query registry helpers for memoizing query services by key.
|
|
8
|
-
export * from './query-registry.js';
|
|
9
7
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,cAAc,eAAe,CAAC;AAC9B,mDAAmD;AACnD,cAAc,YAAY,CAAC;AAC3B,2EAA2E;AAC3E,cAAc,eAAe,CAAC
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,cAAc,eAAe,CAAC;AAC9B,mDAAmD;AACnD,cAAc,YAAY,CAAC;AAC3B,2EAA2E;AAC3E,cAAc,eAAe,CAAC"}
|
package/dist/provider.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface QueryManager {
|
|
|
29
29
|
cancelQueries: QueryClient['cancelQueries'];
|
|
30
30
|
fetchQuery: QueryClient['fetchQuery'];
|
|
31
31
|
getQueryData: QueryClient['getQueryData'];
|
|
32
|
+
getQueryState: QueryClient['getQueryState'];
|
|
32
33
|
invalidateQueries: QueryClient['invalidateQueries'];
|
|
33
34
|
refetchQueries: QueryClient['refetchQueries'];
|
|
34
35
|
removeQueries: QueryClient['removeQueries'];
|
package/dist/provider.js
CHANGED
|
@@ -48,6 +48,8 @@ export function setupQueryManager(queryClient) {
|
|
|
48
48
|
fetchQuery: queryClient.fetchQuery.bind(queryClient),
|
|
49
49
|
// Proxy for retrieving query data with this client context.
|
|
50
50
|
getQueryData: queryClient.getQueryData.bind(queryClient),
|
|
51
|
+
// Proxy for retrieving raw query state with this client context.
|
|
52
|
+
getQueryState: queryClient.getQueryState.bind(queryClient),
|
|
51
53
|
// Proxy for invalidating queries with this client context.
|
|
52
54
|
invalidateQueries: queryClient.invalidateQueries.bind(queryClient),
|
|
53
55
|
// Proxy for refetching 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;AAqEvB;;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,iEAAiE;QACjE,aAAa,EAAE,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1D,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
|
@@ -3,9 +3,9 @@ import { type TrackedDependencyRecord, type TrackingRegistry, type TrackedQueryK
|
|
|
3
3
|
export type QueryFetchStatus = FetchStatus;
|
|
4
4
|
export type QueryStatus = TanstackQueryStatus;
|
|
5
5
|
/**
|
|
6
|
-
* Represents a stable snapshot of
|
|
6
|
+
* Represents a stable snapshot of one query handle's state.
|
|
7
7
|
*/
|
|
8
|
-
export interface
|
|
8
|
+
export interface QueryHandleSnapshot<TData, TError> {
|
|
9
9
|
data: TData | undefined;
|
|
10
10
|
error: TError | null;
|
|
11
11
|
fetchStatus: QueryFetchStatus;
|
|
@@ -15,6 +15,13 @@ export interface QueryServiceSnapshot<TData, TError> {
|
|
|
15
15
|
isPending: boolean;
|
|
16
16
|
isSuccess: boolean;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Represents the lightweight data/error read model for one query handle.
|
|
20
|
+
*/
|
|
21
|
+
export interface QueryHandleData<TData, TError> {
|
|
22
|
+
data: TData | undefined;
|
|
23
|
+
error: TError | null;
|
|
24
|
+
}
|
|
18
25
|
/**
|
|
19
26
|
* Defines a subset of query state containing only the status and fetch status.
|
|
20
27
|
*/
|
|
@@ -23,12 +30,12 @@ export interface QueryMetaState {
|
|
|
23
30
|
status: QueryStatus;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
|
-
* Defines the public API for a query
|
|
33
|
+
* Defines the public API for a query handle.
|
|
27
34
|
*/
|
|
28
|
-
export interface
|
|
29
|
-
getSnapshot: () =>
|
|
30
|
-
subscribe: (listener: (snapshot:
|
|
31
|
-
refetch: (options?: RefetchOptions) => Promise<
|
|
35
|
+
export interface QueryHandle<TData, TError> {
|
|
36
|
+
getSnapshot: () => QueryHandleSnapshot<TData, TError>;
|
|
37
|
+
subscribe: (listener: (snapshot: QueryHandleSnapshot<TData, TError>) => void) => () => void;
|
|
38
|
+
refetch: (options?: RefetchOptions) => Promise<QueryHandleSnapshot<TData, TError>>;
|
|
32
39
|
invalidate: (options?: QueryInvalidateOptions) => Promise<void>;
|
|
33
40
|
unsafe_getResult: () => QueryObserverResult<TData, TError>;
|
|
34
41
|
}
|
|
@@ -43,38 +50,42 @@ type QueryDependencyDerivedOptions<TQueryKey extends QueryKey = QueryKey> = {
|
|
|
43
50
|
};
|
|
44
51
|
export type QueryDependencyTuple<TSources extends readonly unknown[], TQueryKey extends QueryKey = QueryKey> = readonly [
|
|
45
52
|
sources: {
|
|
46
|
-
readonly [K in keyof TSources]:
|
|
53
|
+
readonly [K in keyof TSources]: QueryHandle<TSources[K], Error>;
|
|
47
54
|
},
|
|
48
55
|
deriveOptions: (sourceSnapshots: {
|
|
49
|
-
readonly [K in keyof TSources]:
|
|
56
|
+
readonly [K in keyof TSources]: QueryHandleSnapshot<TSources[K], Error>;
|
|
50
57
|
}) => QueryDependencyDerivedOptions<TQueryKey>
|
|
51
58
|
];
|
|
52
59
|
/**
|
|
53
60
|
* Function signature for the untracked query factory.
|
|
54
61
|
*/
|
|
55
62
|
export interface CreateUntrackedQuery {
|
|
56
|
-
<TSources extends readonly unknown[] = [], TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?:
|
|
63
|
+
<TSources extends readonly unknown[] = [], TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>): QueryHandle<TData, TError>;
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
59
66
|
* Function signature for the default query factory that derives dependencies from the final
|
|
60
67
|
* query-key segment.
|
|
61
68
|
*
|
|
62
|
-
* The tracked query handle deliberately stays API-compatible with the normal query
|
|
69
|
+
* The tracked query handle deliberately stays API-compatible with the normal query handle.
|
|
63
70
|
* The only extra behavior is invisible: dependency registration and on-demand re-registration.
|
|
64
71
|
*/
|
|
65
72
|
export interface CreateQuery {
|
|
66
|
-
<TDeps extends TrackedDependencyRecord, TSources extends readonly unknown[] = [], TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends TrackedQueryKey<TDeps> = TrackedQueryKey<TDeps>>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?:
|
|
73
|
+
<TDeps extends TrackedDependencyRecord, TSources extends readonly unknown[] = [], TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends TrackedQueryKey<TDeps> = TrackedQueryKey<TDeps>>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>): QueryHandle<TData, TError>;
|
|
67
74
|
}
|
|
68
75
|
/**
|
|
69
|
-
* Configuration options for creating a query
|
|
76
|
+
* Configuration options for creating a query handle, excluding function and key.
|
|
70
77
|
*/
|
|
71
|
-
export type
|
|
78
|
+
export type QueryHandleOptions<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TSources extends readonly unknown[] = []> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>, 'queryFn' | 'queryKey'> & {
|
|
72
79
|
dependsOn?: QueryDependencyTuple<TSources, TQueryKey>;
|
|
73
80
|
};
|
|
74
81
|
/**
|
|
75
82
|
* Extracts and maps status and fetchStatus to our QueryMetaState interface.
|
|
76
83
|
*/
|
|
77
|
-
export declare function toQueryMetaState<TData, TError>(snapshot: Pick<
|
|
84
|
+
export declare function toQueryMetaState<TData, TError>(snapshot: Pick<QueryHandleSnapshot<TData, TError>, 'fetchStatus' | 'status'>): QueryMetaState;
|
|
85
|
+
/**
|
|
86
|
+
* Extracts only data and error from a query snapshot.
|
|
87
|
+
*/
|
|
88
|
+
export declare function toQueryHandleData<TData, TError>(snapshot: Pick<QueryHandleSnapshot<TData, TError>, 'data' | 'error'>): QueryHandleData<TData, TError>;
|
|
78
89
|
/**
|
|
79
90
|
* Helper function to check if the query is in its initial loading state.
|
|
80
91
|
*/
|
package/dist/query.js
CHANGED
|
@@ -12,6 +12,15 @@ export function toQueryMetaState(snapshot) {
|
|
|
12
12
|
status: snapshot.status,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Extracts only data and error from a query snapshot.
|
|
17
|
+
*/
|
|
18
|
+
export function toQueryHandleData(snapshot) {
|
|
19
|
+
return {
|
|
20
|
+
data: snapshot.data,
|
|
21
|
+
error: snapshot.error,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
15
24
|
/**
|
|
16
25
|
* Helper function to check if the query is in its initial loading state.
|
|
17
26
|
*/
|
|
@@ -23,14 +32,14 @@ export function isQueryLoading(query) {
|
|
|
23
32
|
* Prepares the query factory by binding it to a specific QueryClient instance.
|
|
24
33
|
*/
|
|
25
34
|
export function setupQuery(queryClient) {
|
|
26
|
-
// Returns the actual factory function for creating individual query
|
|
35
|
+
// Returns the actual factory function for creating individual query handles.
|
|
27
36
|
return function createQuery(queryKey, queryFn, options) {
|
|
28
|
-
const { dependsOn, runtimeOptions } =
|
|
29
|
-
const
|
|
37
|
+
const { dependsOn, runtimeOptions } = splitQueryHandleOptions(options);
|
|
38
|
+
const handle = createQueryHandle(queryClient, queryKey, queryFn, runtimeOptions);
|
|
30
39
|
if (!dependsOn) {
|
|
31
|
-
return
|
|
40
|
+
return handle.handle;
|
|
32
41
|
}
|
|
33
|
-
return bindQueryDependencies(
|
|
42
|
+
return bindQueryDependencies(handle, queryKey, dependsOn);
|
|
34
43
|
};
|
|
35
44
|
}
|
|
36
45
|
/**
|
|
@@ -45,22 +54,22 @@ export function setupQuery(queryClient) {
|
|
|
45
54
|
*/
|
|
46
55
|
export function setupTrackedQuery(queryClient, trackingRegistry) {
|
|
47
56
|
return function createQuery(queryKey, queryFn, options) {
|
|
48
|
-
const { dependsOn, runtimeOptions } =
|
|
49
|
-
// Reuse the same core query
|
|
50
|
-
const
|
|
57
|
+
const { dependsOn, runtimeOptions } = splitQueryHandleOptions(options);
|
|
58
|
+
// Reuse the same core query-handle implementation as the untracked API.
|
|
59
|
+
const handle = createQueryHandle(queryClient, queryKey, queryFn, runtimeOptions);
|
|
51
60
|
// We only need re-registration on the transition from zero to one subscribers.
|
|
52
61
|
let subscriberCount = 0;
|
|
53
62
|
// Register the current query hash immediately so future tracked mutations can find it.
|
|
54
|
-
trackingRegistry.register(
|
|
63
|
+
trackingRegistry.register(handle.observer.getCurrentQuery().queryHash, extractTrackedDependencies(handle.getCurrentQueryKey()));
|
|
55
64
|
const applyTrackedDerivedState = (derivedOptions) => {
|
|
56
|
-
const previousQueryHash =
|
|
57
|
-
|
|
58
|
-
const nextQueryHash =
|
|
65
|
+
const previousQueryHash = handle.observer.getCurrentQuery().queryHash;
|
|
66
|
+
handle.setDerivedState(derivedOptions);
|
|
67
|
+
const nextQueryHash = handle.observer.getCurrentQuery().queryHash;
|
|
59
68
|
if (nextQueryHash === previousQueryHash) {
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
62
71
|
trackingRegistry.unregister(previousQueryHash);
|
|
63
|
-
trackingRegistry.register(nextQueryHash, extractTrackedDependencies(
|
|
72
|
+
trackingRegistry.register(nextQueryHash, extractTrackedDependencies(handle.getCurrentQueryKey()));
|
|
64
73
|
};
|
|
65
74
|
const dependencyController = dependsOn
|
|
66
75
|
? createDependencyController(queryKey, applyTrackedDerivedState, dependsOn)
|
|
@@ -68,8 +77,8 @@ export function setupTrackedQuery(queryClient, trackingRegistry) {
|
|
|
68
77
|
const ensureRegistered = () => {
|
|
69
78
|
// Build resolves the current live TanStack query for the stored observer options. This is
|
|
70
79
|
// the same mechanism TanStack uses internally when a query gets recreated after GC.
|
|
71
|
-
const liveQuery = queryClient.getQueryCache().build(queryClient,
|
|
72
|
-
const liveDependencies = extractTrackedDependencies(
|
|
80
|
+
const liveQuery = queryClient.getQueryCache().build(queryClient, handle.getCurrentObserverOptions());
|
|
81
|
+
const liveDependencies = extractTrackedDependencies(handle.getCurrentQueryKey());
|
|
73
82
|
// Re-register only when TanStack has recreated the query and the registry has already
|
|
74
83
|
// cleaned up the previous hash. This keeps the edge-case handling cheap in the common case.
|
|
75
84
|
if (!trackingRegistry.has(liveQuery.queryHash)) {
|
|
@@ -77,12 +86,12 @@ export function setupTrackedQuery(queryClient, trackingRegistry) {
|
|
|
77
86
|
}
|
|
78
87
|
};
|
|
79
88
|
return {
|
|
80
|
-
...
|
|
89
|
+
...handle.handle,
|
|
81
90
|
refetch: async (refetchOptions) => {
|
|
82
91
|
await dependencyController?.evaluateForRefetch();
|
|
83
92
|
// Refetch is one of the two explicit reactivation paths agreed on in the design.
|
|
84
93
|
ensureRegistered();
|
|
85
|
-
return
|
|
94
|
+
return handle.handle.refetch(refetchOptions);
|
|
86
95
|
},
|
|
87
96
|
subscribe: (listener) => {
|
|
88
97
|
// The first active subscriber is the other reactivation path. Re-running registration
|
|
@@ -92,7 +101,7 @@ export function setupTrackedQuery(queryClient, trackingRegistry) {
|
|
|
92
101
|
ensureRegistered();
|
|
93
102
|
}
|
|
94
103
|
subscriberCount += 1;
|
|
95
|
-
const unsubscribe =
|
|
104
|
+
const unsubscribe = handle.handle.subscribe(listener);
|
|
96
105
|
return () => {
|
|
97
106
|
// Keep the counter bounded so accidental double-unsubscribe cannot push it negative.
|
|
98
107
|
subscriberCount = Math.max(0, subscriberCount - 1);
|
|
@@ -108,8 +117,8 @@ export function setupTrackedQuery(queryClient, trackingRegistry) {
|
|
|
108
117
|
/**
|
|
109
118
|
* Internal helper to transform a raw Tanstack query result into our public snapshot format.
|
|
110
119
|
*/
|
|
111
|
-
function
|
|
112
|
-
// Extract and return the relevant fields for the UI or other
|
|
120
|
+
function toQueryHandleSnapshot(result) {
|
|
121
|
+
// Extract and return the relevant fields for the UI or other handle consumers.
|
|
113
122
|
return {
|
|
114
123
|
data: result.data,
|
|
115
124
|
error: result.error,
|
|
@@ -121,7 +130,7 @@ function toQueryServiceSnapshot(result) {
|
|
|
121
130
|
isSuccess: result.isSuccess,
|
|
122
131
|
};
|
|
123
132
|
}
|
|
124
|
-
function
|
|
133
|
+
function createQueryHandle(queryClient, queryKey, queryFn, options) {
|
|
125
134
|
const baseQueryKey = queryKey;
|
|
126
135
|
const baseOptions = options;
|
|
127
136
|
let resolvedQueryKey = baseQueryKey;
|
|
@@ -141,12 +150,12 @@ function createQueryService(queryClient, queryKey, queryFn, options) {
|
|
|
141
150
|
getCurrentObserverOptions,
|
|
142
151
|
getCurrentQueryKey: () => resolvedQueryKey,
|
|
143
152
|
setDerivedState,
|
|
144
|
-
|
|
145
|
-
getSnapshot: () =>
|
|
153
|
+
handle: {
|
|
154
|
+
getSnapshot: () => toQueryHandleSnapshot(observer.getCurrentResult()),
|
|
146
155
|
subscribe: (listener) => observer.subscribe((result) => {
|
|
147
|
-
listener(
|
|
156
|
+
listener(toQueryHandleSnapshot(result));
|
|
148
157
|
}),
|
|
149
|
-
refetch: async (refetchOptions) =>
|
|
158
|
+
refetch: async (refetchOptions) => toQueryHandleSnapshot(await observer.refetch(refetchOptions)),
|
|
150
159
|
invalidate: (invalidateOptions) => queryClient.invalidateQueries({
|
|
151
160
|
exact: true,
|
|
152
161
|
queryKey: resolvedQueryKey,
|
|
@@ -158,21 +167,21 @@ function createQueryService(queryClient, queryKey, queryFn, options) {
|
|
|
158
167
|
},
|
|
159
168
|
};
|
|
160
169
|
}
|
|
161
|
-
function bindQueryDependencies(
|
|
162
|
-
const dependencyController = createDependencyController(queryKey,
|
|
170
|
+
function bindQueryDependencies(queryHandle, queryKey, dependsOn) {
|
|
171
|
+
const dependencyController = createDependencyController(queryKey, queryHandle.setDerivedState, dependsOn);
|
|
163
172
|
let subscriberCount = 0;
|
|
164
173
|
return {
|
|
165
|
-
...
|
|
174
|
+
...queryHandle.handle,
|
|
166
175
|
refetch: async (refetchOptions) => {
|
|
167
176
|
await dependencyController.evaluateForRefetch();
|
|
168
|
-
return
|
|
177
|
+
return queryHandle.handle.refetch(refetchOptions);
|
|
169
178
|
},
|
|
170
179
|
subscribe: (listener) => {
|
|
171
180
|
if (subscriberCount === 0) {
|
|
172
181
|
dependencyController.activate();
|
|
173
182
|
}
|
|
174
183
|
subscriberCount += 1;
|
|
175
|
-
const unsubscribe =
|
|
184
|
+
const unsubscribe = queryHandle.handle.subscribe(listener);
|
|
176
185
|
return () => {
|
|
177
186
|
subscriberCount = Math.max(0, subscriberCount - 1);
|
|
178
187
|
if (subscriberCount === 0) {
|
|
@@ -239,7 +248,7 @@ function createDependencyController(baseQueryKey, setDerivedState, dependsOn) {
|
|
|
239
248
|
},
|
|
240
249
|
};
|
|
241
250
|
}
|
|
242
|
-
function
|
|
251
|
+
function splitQueryHandleOptions(options) {
|
|
243
252
|
if (options === undefined) {
|
|
244
253
|
return {
|
|
245
254
|
dependsOn: undefined,
|