@veams/status-quo-query 0.11.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 +122 -20
- 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 -1
- package/dist/react/hooks/index.js +1 -1
- package/dist/react/hooks/index.js.map +1 -1
- 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 -2
- package/dist/react/hooks/use-query-subscription.js +1 -72
- package/dist/react/hooks/use-query-subscription.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -8
- 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 -64
- package/src/react/__tests__/{query-subscription.spec.tsx → use-query-handle.spec.tsx} +7 -7
- package/src/react/hooks/index.ts +1 -1
- package/src/react/hooks/{use-query-subscription.ts → use-query-handle.ts} +19 -21
- package/src/react/index.ts +1 -1
- 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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Import the React hooks needed to memoize callbacks, hold mutable cache state,
|
|
2
|
+
// and connect an external store to React rendering.
|
|
3
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
// Subscribe a React component to a QueryHandle and return its latest snapshot.
|
|
5
|
+
export function useQueryHandle(
|
|
6
|
+
// Receive the external query handle instance to subscribe to.
|
|
7
|
+
queryHandle) {
|
|
8
|
+
// Count store notifications so we can tell when our cached snapshot is stale.
|
|
9
|
+
const snapshotVersionRef = useRef(0);
|
|
10
|
+
// Cache one client-side snapshot per observed version to keep getSnapshot stable.
|
|
11
|
+
const snapshotCacheRef = useRef(null);
|
|
12
|
+
// Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
|
|
13
|
+
const serverSnapshotCacheRef = useRef(null);
|
|
14
|
+
// Create the subscribe function expected by useSyncExternalStore.
|
|
15
|
+
const subscribe = useCallback(
|
|
16
|
+
// React passes a listener that must run whenever the external store changes.
|
|
17
|
+
(listener) =>
|
|
18
|
+
// Forward the subscription to the query handle.
|
|
19
|
+
queryHandle.subscribe(() => {
|
|
20
|
+
// Bump the version so later reads know the previous cache is outdated.
|
|
21
|
+
snapshotVersionRef.current += 1;
|
|
22
|
+
// Drop the cached client snapshot because the store just changed.
|
|
23
|
+
snapshotCacheRef.current = null;
|
|
24
|
+
// Drop the cached server snapshot for the same reason.
|
|
25
|
+
serverSnapshotCacheRef.current = null;
|
|
26
|
+
// Notify React that it should read a fresh snapshot.
|
|
27
|
+
listener();
|
|
28
|
+
}),
|
|
29
|
+
// Recreate the subscription function only when the handle instance changes.
|
|
30
|
+
[queryHandle]);
|
|
31
|
+
// Read the current client snapshot in a referentially stable way for React.
|
|
32
|
+
const getSnapshot = useCallback(() => {
|
|
33
|
+
// Read the latest store version number.
|
|
34
|
+
const version = snapshotVersionRef.current;
|
|
35
|
+
// Read the last cached client snapshot, if there is one.
|
|
36
|
+
const cachedSnapshot = snapshotCacheRef.current;
|
|
37
|
+
// Reuse the cached snapshot when it was produced for the current version.
|
|
38
|
+
if (cachedSnapshot && cachedSnapshot.version === version) {
|
|
39
|
+
// Return the cached snapshot so repeated reads in the same render stay stable.
|
|
40
|
+
return cachedSnapshot.snapshot;
|
|
41
|
+
}
|
|
42
|
+
// Ask the query handle for the latest snapshot because the cache is empty or stale.
|
|
43
|
+
const snapshot = queryHandle.getSnapshot();
|
|
44
|
+
// Store the new snapshot together with the version it belongs to.
|
|
45
|
+
snapshotCacheRef.current = {
|
|
46
|
+
snapshot,
|
|
47
|
+
version,
|
|
48
|
+
};
|
|
49
|
+
// Return the freshly read snapshot to React.
|
|
50
|
+
return snapshot;
|
|
51
|
+
}, [queryHandle]);
|
|
52
|
+
// Read the server snapshot used by React during SSR or hydration fallback paths.
|
|
53
|
+
const getServerSnapshot = useCallback(() => {
|
|
54
|
+
// Read the cached server snapshot, if one was stored earlier.
|
|
55
|
+
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
56
|
+
// Reuse the cached server snapshot to keep server reads stable.
|
|
57
|
+
if (cachedSnapshot) {
|
|
58
|
+
// Return the cached server snapshot directly.
|
|
59
|
+
return cachedSnapshot;
|
|
60
|
+
}
|
|
61
|
+
// Ask the query handle for a snapshot because no server cache exists yet.
|
|
62
|
+
const snapshot = queryHandle.getSnapshot();
|
|
63
|
+
// Cache that snapshot for the next server read.
|
|
64
|
+
serverSnapshotCacheRef.current = snapshot;
|
|
65
|
+
// Return the freshly read server snapshot.
|
|
66
|
+
return snapshot;
|
|
67
|
+
}, [queryHandle]);
|
|
68
|
+
// Let React subscribe to the external store and read snapshots through the callbacks above.
|
|
69
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=use-query-handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-query-handle.js","sourceRoot":"","sources":["../../../src/react/hooks/use-query-handle.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,oDAAoD;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAelE,+EAA+E;AAC/E,MAAM,UAAU,cAAc;AAC5B,8DAA8D;AAC9D,WAAuC;IAEvC,8EAA8E;IAC9E,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACrC,kFAAkF;IAClF,MAAM,gBAAgB,GAAG,MAAM,CAA2C,IAAI,CAAC,CAAC;IAChF,kFAAkF;IAClF,MAAM,sBAAsB,GAAG,MAAM,CAA4C,IAAI,CAAC,CAAC;IAEvF,kEAAkE;IAClE,MAAM,SAAS,GAAG,WAAW;IAC3B,6EAA6E;IAC7E,CAAC,QAAkB,EAAE,EAAE;IACrB,gDAAgD;IAChD,WAAW,CAAC,SAAS,CAAC,GAAG,EAAE;QACzB,uEAAuE;QACvE,kBAAkB,CAAC,OAAO,IAAI,CAAC,CAAC;QAChC,kEAAkE;QAClE,gBAAgB,CAAC,OAAO,GAAG,IAAI,CAAC;QAChC,uDAAuD;QACvD,sBAAsB,CAAC,OAAO,GAAG,IAAI,CAAC;QACtC,qDAAqD;QACrD,QAAQ,EAAE,CAAC;IACb,CAAC,CAAC;IACJ,4EAA4E;IAC5E,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,4EAA4E;IAC5E,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,wCAAwC;QACxC,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC;QAC3C,yDAAyD;QACzD,MAAM,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC;QAEhD,0EAA0E;QAC1E,IAAI,cAAc,IAAI,cAAc,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YACzD,+EAA+E;YAC/E,OAAO,cAAc,CAAC,QAAQ,CAAC;QACjC,CAAC;QAED,oFAAoF;QACpF,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;QAC3C,kEAAkE;QAClE,gBAAgB,CAAC,OAAO,GAAG;YACzB,QAAQ;YACR,OAAO;SACR,CAAC;QAEF,6CAA6C;QAC7C,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,iFAAiF;IACjF,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,EAAE;QACzC,8DAA8D;QAC9D,MAAM,cAAc,GAAG,sBAAsB,CAAC,OAAO,CAAC;QAEtD,gEAAgE;QAChE,IAAI,cAAc,EAAE,CAAC;YACnB,8CAA8C;YAC9C,OAAO,cAAc,CAAC;QACxB,CAAC;QAED,0EAA0E;QAC1E,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;QAC3C,gDAAgD;QAChD,sBAAsB,CAAC,OAAO,GAAG,QAAQ,CAAC;QAE1C,2CAA2C;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,4FAA4F;IAC5F,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function useQuerySubscription<TData, TError>(queryService: QueryService<TData, TError>): QueryServiceSnapshot<TData, TError>;
|
|
1
|
+
export { useQueryHandle, useQuerySubscription } from './use-query-handle.js';
|
|
@@ -1,73 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
// and connect an external store to React rendering.
|
|
3
|
-
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
4
|
-
// Subscribe a React component to a QueryService and return its latest snapshot.
|
|
5
|
-
export function useQuerySubscription(
|
|
6
|
-
// Receive the external query service instance to subscribe to.
|
|
7
|
-
queryService) {
|
|
8
|
-
// Count store notifications so we can tell when our cached snapshot is stale.
|
|
9
|
-
const snapshotVersionRef = useRef(0);
|
|
10
|
-
// Cache one client-side snapshot per observed version to keep getSnapshot stable.
|
|
11
|
-
const snapshotCacheRef = useRef(null);
|
|
12
|
-
// Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
|
|
13
|
-
const serverSnapshotCacheRef = useRef(null);
|
|
14
|
-
// Create the subscribe function expected by useSyncExternalStore.
|
|
15
|
-
const subscribe = useCallback(
|
|
16
|
-
// React passes a listener that must run whenever the external store changes.
|
|
17
|
-
(listener) =>
|
|
18
|
-
// Forward the subscription to the query service.
|
|
19
|
-
queryService.subscribe(() => {
|
|
20
|
-
// Bump the version so later reads know the previous cache is outdated.
|
|
21
|
-
snapshotVersionRef.current += 1;
|
|
22
|
-
// Drop the cached client snapshot because the store just changed.
|
|
23
|
-
snapshotCacheRef.current = null;
|
|
24
|
-
// Drop the cached server snapshot for the same reason.
|
|
25
|
-
serverSnapshotCacheRef.current = null;
|
|
26
|
-
// Notify React that it should read a fresh snapshot.
|
|
27
|
-
listener();
|
|
28
|
-
}),
|
|
29
|
-
// Recreate the subscription function only when the service instance changes.
|
|
30
|
-
[queryService]);
|
|
31
|
-
// Read the current client snapshot in a referentially stable way for React.
|
|
32
|
-
const getSnapshot = useCallback(() => {
|
|
33
|
-
// Read the latest store version number.
|
|
34
|
-
const version = snapshotVersionRef.current;
|
|
35
|
-
// Read the last cached client snapshot, if there is one.
|
|
36
|
-
const cachedSnapshot = snapshotCacheRef.current;
|
|
37
|
-
// Reuse the cached snapshot when it was produced for the current version.
|
|
38
|
-
if (cachedSnapshot && cachedSnapshot.version === version) {
|
|
39
|
-
// Return the cached snapshot so repeated reads in the same render stay stable.
|
|
40
|
-
return cachedSnapshot.snapshot;
|
|
41
|
-
}
|
|
42
|
-
// Ask the query service for the latest snapshot because the cache is empty or stale.
|
|
43
|
-
const snapshot = queryService.getSnapshot();
|
|
44
|
-
// Store the new snapshot together with the version it belongs to.
|
|
45
|
-
snapshotCacheRef.current = {
|
|
46
|
-
snapshot,
|
|
47
|
-
version,
|
|
48
|
-
};
|
|
49
|
-
// Return the freshly read snapshot to React.
|
|
50
|
-
return snapshot;
|
|
51
|
-
// Recreate this getter only when the service instance changes.
|
|
52
|
-
}, [queryService]);
|
|
53
|
-
// Read the server snapshot used by React during SSR or hydration fallback paths.
|
|
54
|
-
const getServerSnapshot = useCallback(() => {
|
|
55
|
-
// Read the cached server snapshot, if one was stored earlier.
|
|
56
|
-
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
57
|
-
// Reuse the cached server snapshot to keep server reads stable.
|
|
58
|
-
if (cachedSnapshot) {
|
|
59
|
-
// Return the cached server snapshot directly.
|
|
60
|
-
return cachedSnapshot;
|
|
61
|
-
}
|
|
62
|
-
// Ask the query service for a snapshot because no server cache exists yet.
|
|
63
|
-
const snapshot = queryService.getSnapshot();
|
|
64
|
-
// Cache that snapshot for the next server read.
|
|
65
|
-
serverSnapshotCacheRef.current = snapshot;
|
|
66
|
-
// Return the freshly read server snapshot.
|
|
67
|
-
return snapshot;
|
|
68
|
-
// Recreate this getter only when the service instance changes.
|
|
69
|
-
}, [queryService]);
|
|
70
|
-
// Let React subscribe to the external store and read snapshots through the callbacks above.
|
|
71
|
-
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
72
|
-
}
|
|
1
|
+
export { useQueryHandle, useQuerySubscription } from './use-query-handle.js';
|
|
73
2
|
//# sourceMappingURL=use-query-subscription.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-query-subscription.js","sourceRoot":"","sources":["../../../src/react/hooks/use-query-subscription.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"use-query-subscription.js","sourceRoot":"","sources":["../../../src/react/hooks/use-query-subscription.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useQueryHandle } from './hooks/index.js';
|
package/dist/react/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useQueryHandle } from './hooks/index.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veams/status-quo-query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "TanStack Query service layer for the VEAMS StatusQuo ecosystem.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -39,13 +39,6 @@
|
|
|
39
39
|
"default": "./dist/provider.js"
|
|
40
40
|
},
|
|
41
41
|
"require": "./dist/provider.js"
|
|
42
|
-
},
|
|
43
|
-
"./query-registry": {
|
|
44
|
-
"import": {
|
|
45
|
-
"types": "./dist/query-registry.d.ts",
|
|
46
|
-
"default": "./dist/query-registry.js"
|
|
47
|
-
},
|
|
48
|
-
"require": "./dist/query-registry.js"
|
|
49
42
|
}
|
|
50
43
|
},
|
|
51
44
|
"types": "dist/index.d.ts",
|
|
@@ -10,6 +10,7 @@ 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 getQueryStateSpy = jest.spyOn(queryClient, 'getQueryState');
|
|
13
14
|
const fetchUser = jest.fn().mockResolvedValue({ id: 7 });
|
|
14
15
|
const fetchQuerySpy = jest.spyOn(queryClient, 'fetchQuery');
|
|
15
16
|
const manager = setupQueryManager(queryClient);
|
|
@@ -17,6 +18,12 @@ describe('Query Manager API', () => {
|
|
|
17
18
|
manager.setQueryData<{ id: number }>(['user', 42], { id: 42 });
|
|
18
19
|
|
|
19
20
|
expect(manager.getQueryData<{ id: number }>(['user', 42])).toEqual({ id: 42 });
|
|
21
|
+
expect(manager.getQueryState(['user', 42])).toEqual(
|
|
22
|
+
expect.objectContaining({
|
|
23
|
+
data: { id: 42 },
|
|
24
|
+
status: 'success',
|
|
25
|
+
})
|
|
26
|
+
);
|
|
20
27
|
expect(manager.unsafe_getClient()).toBe(queryClient);
|
|
21
28
|
await expect(
|
|
22
29
|
manager.fetchQuery({
|
|
@@ -37,6 +44,7 @@ describe('Query Manager API', () => {
|
|
|
37
44
|
queryFn: fetchUser,
|
|
38
45
|
staleTime: 60_000,
|
|
39
46
|
});
|
|
47
|
+
expect(getQueryStateSpy).toHaveBeenCalledWith(['user', 42]);
|
|
40
48
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
41
49
|
expect(refetchQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
42
50
|
expect(cancelQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,6 @@ 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
|
// Re-export tracked dependency types used by the additive tracked facade.
|
|
10
8
|
export type {
|
|
11
9
|
TrackedDependencyRecord,
|
package/src/provider.ts
CHANGED
|
@@ -56,9 +56,9 @@ export interface CreateQueryAndMutation {
|
|
|
56
56
|
export interface QueryManager {
|
|
57
57
|
// Factory for creating a dependency-tracked mutation service within the context of this provider.
|
|
58
58
|
createMutation: CreateMutation;
|
|
59
|
-
// Factory for creating a dependency-tracked query
|
|
59
|
+
// Factory for creating a dependency-tracked query handle within the context of this provider.
|
|
60
60
|
createQuery: CreateQuery;
|
|
61
|
-
// Factory for creating an untracked query
|
|
61
|
+
// Factory for creating an untracked query handle within the context of this provider.
|
|
62
62
|
createUntrackedQuery: CreateUntrackedQuery;
|
|
63
63
|
// Factory for creating an untracked mutation service within the context of this provider.
|
|
64
64
|
createUntrackedMutation: CreateUntrackedMutation;
|
|
@@ -70,6 +70,8 @@ export interface QueryManager {
|
|
|
70
70
|
fetchQuery: QueryClient['fetchQuery'];
|
|
71
71
|
// Synchronously retrieves a snapshot of the current query data.
|
|
72
72
|
getQueryData: QueryClient['getQueryData'];
|
|
73
|
+
// Synchronously retrieves the raw TanStack query state for one query key.
|
|
74
|
+
getQueryState: QueryClient['getQueryState'];
|
|
73
75
|
// Marks queries as invalid to trigger a refetch if they are active.
|
|
74
76
|
invalidateQueries: QueryClient['invalidateQueries'];
|
|
75
77
|
// Forces a refetch of queries matching the specified filters.
|
|
@@ -155,6 +157,8 @@ export function setupQueryManager(queryClient: QueryClient): QueryManager {
|
|
|
155
157
|
fetchQuery: queryClient.fetchQuery.bind(queryClient),
|
|
156
158
|
// Proxy for retrieving query data with this client context.
|
|
157
159
|
getQueryData: queryClient.getQueryData.bind(queryClient),
|
|
160
|
+
// Proxy for retrieving raw query state with this client context.
|
|
161
|
+
getQueryState: queryClient.getQueryState.bind(queryClient),
|
|
158
162
|
// Proxy for invalidating queries with this client context.
|
|
159
163
|
invalidateQueries: queryClient.invalidateQueries.bind(queryClient),
|
|
160
164
|
// Proxy for refetching queries with this client context.
|
package/src/query.ts
CHANGED
|
@@ -36,9 +36,9 @@ export type QueryFetchStatus = FetchStatus;
|
|
|
36
36
|
export type QueryStatus = TanstackQueryStatus;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Represents a stable snapshot of
|
|
39
|
+
* Represents a stable snapshot of one query handle's state.
|
|
40
40
|
*/
|
|
41
|
-
export interface
|
|
41
|
+
export interface QueryHandleSnapshot<TData, TError> {
|
|
42
42
|
// The data retrieved from a successful query.
|
|
43
43
|
data: TData | undefined;
|
|
44
44
|
// The error object if the query failed.
|
|
@@ -57,6 +57,14 @@ export interface QueryServiceSnapshot<TData, TError> {
|
|
|
57
57
|
isSuccess: boolean;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Represents the lightweight data/error read model for one query handle.
|
|
62
|
+
*/
|
|
63
|
+
export interface QueryHandleData<TData, TError> {
|
|
64
|
+
data: TData | undefined;
|
|
65
|
+
error: TError | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
/**
|
|
61
69
|
* Defines a subset of query state containing only the status and fetch status.
|
|
62
70
|
*/
|
|
@@ -66,15 +74,15 @@ export interface QueryMetaState {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
69
|
-
* Defines the public API for a query
|
|
77
|
+
* Defines the public API for a query handle.
|
|
70
78
|
*/
|
|
71
|
-
export interface
|
|
79
|
+
export interface QueryHandle<TData, TError> {
|
|
72
80
|
// Returns the current state snapshot of the query.
|
|
73
|
-
getSnapshot: () =>
|
|
81
|
+
getSnapshot: () => QueryHandleSnapshot<TData, TError>;
|
|
74
82
|
// Subscribes a listener to state changes; returns an unsubscribe function.
|
|
75
|
-
subscribe: (listener: (snapshot:
|
|
83
|
+
subscribe: (listener: (snapshot: QueryHandleSnapshot<TData, TError>) => void) => () => void;
|
|
76
84
|
// Manually triggers a refetch of this query.
|
|
77
|
-
refetch: (options?: RefetchOptions) => Promise<
|
|
85
|
+
refetch: (options?: RefetchOptions) => Promise<QueryHandleSnapshot<TData, TError>>;
|
|
78
86
|
// Marks this specific query as invalid in the cache to trigger a refetch if active.
|
|
79
87
|
invalidate: (options?: QueryInvalidateOptions) => Promise<void>;
|
|
80
88
|
// Escape hatch: provides direct access to the underlying Tanstack Query observer result.
|
|
@@ -93,14 +101,14 @@ type QueryDependencyDerivedOptions<TQueryKey extends QueryKey = QueryKey> = {
|
|
|
93
101
|
queryKey?: TQueryKey;
|
|
94
102
|
};
|
|
95
103
|
|
|
96
|
-
type
|
|
104
|
+
type QueryHandleRuntimeOptions<
|
|
97
105
|
TQueryFnData = unknown,
|
|
98
106
|
TError = Error,
|
|
99
107
|
TData = TQueryFnData,
|
|
100
108
|
TQueryData = TQueryFnData,
|
|
101
109
|
TQueryKey extends QueryKey = QueryKey,
|
|
102
110
|
> = Omit<
|
|
103
|
-
|
|
111
|
+
QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
|
|
104
112
|
'dependsOn'
|
|
105
113
|
>;
|
|
106
114
|
|
|
@@ -108,9 +116,9 @@ export type QueryDependencyTuple<
|
|
|
108
116
|
TSources extends readonly unknown[],
|
|
109
117
|
TQueryKey extends QueryKey = QueryKey,
|
|
110
118
|
> = readonly [
|
|
111
|
-
sources: { readonly [K in keyof TSources]:
|
|
119
|
+
sources: { readonly [K in keyof TSources]: QueryHandle<TSources[K], Error> },
|
|
112
120
|
deriveOptions: (
|
|
113
|
-
sourceSnapshots: { readonly [K in keyof TSources]:
|
|
121
|
+
sourceSnapshots: { readonly [K in keyof TSources]: QueryHandleSnapshot<TSources[K], Error> }
|
|
114
122
|
) => QueryDependencyDerivedOptions<TQueryKey>,
|
|
115
123
|
];
|
|
116
124
|
|
|
@@ -131,15 +139,15 @@ export interface CreateUntrackedQuery {
|
|
|
131
139
|
// The asynchronous function that performs the data fetch.
|
|
132
140
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
133
141
|
// Optional configuration for behavior like staleness, retry, and refetching.
|
|
134
|
-
options?:
|
|
135
|
-
):
|
|
142
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
143
|
+
): QueryHandle<TData, TError>;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
/**
|
|
139
147
|
* Function signature for the default query factory that derives dependencies from the final
|
|
140
148
|
* query-key segment.
|
|
141
149
|
*
|
|
142
|
-
* The tracked query handle deliberately stays API-compatible with the normal query
|
|
150
|
+
* The tracked query handle deliberately stays API-compatible with the normal query handle.
|
|
143
151
|
* The only extra behavior is invisible: dependency registration and on-demand re-registration.
|
|
144
152
|
*/
|
|
145
153
|
export interface CreateQuery {
|
|
@@ -154,14 +162,14 @@ export interface CreateQuery {
|
|
|
154
162
|
>(
|
|
155
163
|
queryKey: TQueryKey,
|
|
156
164
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
157
|
-
options?:
|
|
158
|
-
):
|
|
165
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
166
|
+
): QueryHandle<TData, TError>;
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
/**
|
|
162
|
-
* Configuration options for creating a query
|
|
170
|
+
* Configuration options for creating a query handle, excluding function and key.
|
|
163
171
|
*/
|
|
164
|
-
export type
|
|
172
|
+
export type QueryHandleOptions<
|
|
165
173
|
TQueryFnData = unknown,
|
|
166
174
|
TError = Error,
|
|
167
175
|
TData = TQueryFnData,
|
|
@@ -179,7 +187,7 @@ export type QueryServiceOptions<
|
|
|
179
187
|
* Extracts and maps status and fetchStatus to our QueryMetaState interface.
|
|
180
188
|
*/
|
|
181
189
|
export function toQueryMetaState<TData, TError>(
|
|
182
|
-
snapshot: Pick<
|
|
190
|
+
snapshot: Pick<QueryHandleSnapshot<TData, TError>, 'fetchStatus' | 'status'>
|
|
183
191
|
): QueryMetaState {
|
|
184
192
|
// Return a simplified state object for UI or other services.
|
|
185
193
|
return {
|
|
@@ -188,6 +196,18 @@ export function toQueryMetaState<TData, TError>(
|
|
|
188
196
|
};
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Extracts only data and error from a query snapshot.
|
|
201
|
+
*/
|
|
202
|
+
export function toQueryHandleData<TData, TError>(
|
|
203
|
+
snapshot: Pick<QueryHandleSnapshot<TData, TError>, 'data' | 'error'>
|
|
204
|
+
): QueryHandleData<TData, TError> {
|
|
205
|
+
return {
|
|
206
|
+
data: snapshot.data,
|
|
207
|
+
error: snapshot.error,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/**
|
|
192
212
|
* Helper function to check if the query is in its initial loading state.
|
|
193
213
|
*/
|
|
@@ -200,7 +220,7 @@ export function isQueryLoading(query: QueryMetaState): boolean {
|
|
|
200
220
|
* Prepares the query factory by binding it to a specific QueryClient instance.
|
|
201
221
|
*/
|
|
202
222
|
export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
|
|
203
|
-
// Returns the actual factory function for creating individual query
|
|
223
|
+
// Returns the actual factory function for creating individual query handles.
|
|
204
224
|
return function createQuery<
|
|
205
225
|
TSources extends readonly unknown[] = [],
|
|
206
226
|
TQueryFnData = unknown,
|
|
@@ -211,16 +231,16 @@ export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
|
|
|
211
231
|
>(
|
|
212
232
|
queryKey: TQueryKey,
|
|
213
233
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
214
|
-
options?:
|
|
215
|
-
):
|
|
216
|
-
const { dependsOn, runtimeOptions } =
|
|
217
|
-
const
|
|
234
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
235
|
+
): QueryHandle<TData, TError> {
|
|
236
|
+
const { dependsOn, runtimeOptions } = splitQueryHandleOptions(options);
|
|
237
|
+
const handle = createQueryHandle(queryClient, queryKey, queryFn, runtimeOptions);
|
|
218
238
|
|
|
219
239
|
if (!dependsOn) {
|
|
220
|
-
return
|
|
240
|
+
return handle.handle;
|
|
221
241
|
}
|
|
222
242
|
|
|
223
|
-
return bindQueryDependencies(
|
|
243
|
+
return bindQueryDependencies(handle, queryKey, dependsOn);
|
|
224
244
|
};
|
|
225
245
|
}
|
|
226
246
|
|
|
@@ -249,33 +269,33 @@ export function setupTrackedQuery(
|
|
|
249
269
|
>(
|
|
250
270
|
queryKey: TQueryKey,
|
|
251
271
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
252
|
-
options?:
|
|
253
|
-
):
|
|
254
|
-
const { dependsOn, runtimeOptions } =
|
|
255
|
-
// Reuse the same core query
|
|
256
|
-
const
|
|
272
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
273
|
+
): QueryHandle<TData, TError> {
|
|
274
|
+
const { dependsOn, runtimeOptions } = splitQueryHandleOptions(options);
|
|
275
|
+
// Reuse the same core query-handle implementation as the untracked API.
|
|
276
|
+
const handle = createQueryHandle(queryClient, queryKey, queryFn, runtimeOptions);
|
|
257
277
|
// We only need re-registration on the transition from zero to one subscribers.
|
|
258
278
|
let subscriberCount = 0;
|
|
259
279
|
|
|
260
280
|
// Register the current query hash immediately so future tracked mutations can find it.
|
|
261
281
|
trackingRegistry.register(
|
|
262
|
-
|
|
263
|
-
extractTrackedDependencies(
|
|
282
|
+
handle.observer.getCurrentQuery().queryHash,
|
|
283
|
+
extractTrackedDependencies(handle.getCurrentQueryKey())
|
|
264
284
|
);
|
|
265
285
|
|
|
266
286
|
const applyTrackedDerivedState = (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => {
|
|
267
|
-
const previousQueryHash =
|
|
287
|
+
const previousQueryHash = handle.observer.getCurrentQuery().queryHash;
|
|
268
288
|
|
|
269
|
-
|
|
289
|
+
handle.setDerivedState(derivedOptions);
|
|
270
290
|
|
|
271
|
-
const nextQueryHash =
|
|
291
|
+
const nextQueryHash = handle.observer.getCurrentQuery().queryHash;
|
|
272
292
|
|
|
273
293
|
if (nextQueryHash === previousQueryHash) {
|
|
274
294
|
return;
|
|
275
295
|
}
|
|
276
296
|
|
|
277
297
|
trackingRegistry.unregister(previousQueryHash);
|
|
278
|
-
trackingRegistry.register(nextQueryHash, extractTrackedDependencies(
|
|
298
|
+
trackingRegistry.register(nextQueryHash, extractTrackedDependencies(handle.getCurrentQueryKey()));
|
|
279
299
|
};
|
|
280
300
|
|
|
281
301
|
const dependencyController = dependsOn
|
|
@@ -291,9 +311,9 @@ export function setupTrackedQuery(
|
|
|
291
311
|
// the same mechanism TanStack uses internally when a query gets recreated after GC.
|
|
292
312
|
const liveQuery = queryClient.getQueryCache().build(
|
|
293
313
|
queryClient,
|
|
294
|
-
|
|
314
|
+
handle.getCurrentObserverOptions()
|
|
295
315
|
);
|
|
296
|
-
const liveDependencies = extractTrackedDependencies(
|
|
316
|
+
const liveDependencies = extractTrackedDependencies(handle.getCurrentQueryKey());
|
|
297
317
|
|
|
298
318
|
// Re-register only when TanStack has recreated the query and the registry has already
|
|
299
319
|
// cleaned up the previous hash. This keeps the edge-case handling cheap in the common case.
|
|
@@ -303,12 +323,12 @@ export function setupTrackedQuery(
|
|
|
303
323
|
};
|
|
304
324
|
|
|
305
325
|
return {
|
|
306
|
-
...
|
|
326
|
+
...handle.handle,
|
|
307
327
|
refetch: async (refetchOptions) => {
|
|
308
328
|
await dependencyController?.evaluateForRefetch();
|
|
309
329
|
// Refetch is one of the two explicit reactivation paths agreed on in the design.
|
|
310
330
|
ensureRegistered();
|
|
311
|
-
return
|
|
331
|
+
return handle.handle.refetch(refetchOptions);
|
|
312
332
|
},
|
|
313
333
|
subscribe: (listener) => {
|
|
314
334
|
// The first active subscriber is the other reactivation path. Re-running registration
|
|
@@ -320,7 +340,7 @@ export function setupTrackedQuery(
|
|
|
320
340
|
|
|
321
341
|
subscriberCount += 1;
|
|
322
342
|
|
|
323
|
-
const unsubscribe =
|
|
343
|
+
const unsubscribe = handle.handle.subscribe(listener);
|
|
324
344
|
|
|
325
345
|
return () => {
|
|
326
346
|
// Keep the counter bounded so accidental double-unsubscribe cannot push it negative.
|
|
@@ -338,10 +358,10 @@ export function setupTrackedQuery(
|
|
|
338
358
|
/**
|
|
339
359
|
* Internal helper to transform a raw Tanstack query result into our public snapshot format.
|
|
340
360
|
*/
|
|
341
|
-
function
|
|
361
|
+
function toQueryHandleSnapshot<TData, TError>(
|
|
342
362
|
result: QueryObserverResult<TData, TError>
|
|
343
|
-
):
|
|
344
|
-
// Extract and return the relevant fields for the UI or other
|
|
363
|
+
): QueryHandleSnapshot<TData, TError> {
|
|
364
|
+
// Extract and return the relevant fields for the UI or other handle consumers.
|
|
345
365
|
return {
|
|
346
366
|
data: result.data,
|
|
347
367
|
error: result.error,
|
|
@@ -354,7 +374,7 @@ function toQueryServiceSnapshot<TData, TError>(
|
|
|
354
374
|
};
|
|
355
375
|
}
|
|
356
376
|
|
|
357
|
-
function
|
|
377
|
+
function createQueryHandle<
|
|
358
378
|
TQueryFnData = unknown,
|
|
359
379
|
TError = Error,
|
|
360
380
|
TData = TQueryFnData,
|
|
@@ -364,12 +384,12 @@ function createQueryService<
|
|
|
364
384
|
queryClient: QueryClient,
|
|
365
385
|
queryKey: TQueryKey,
|
|
366
386
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
367
|
-
options?:
|
|
387
|
+
options?: QueryHandleRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
368
388
|
): {
|
|
369
389
|
// Expose the observer internally so tracked queries can access the current query hash.
|
|
370
390
|
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
|
|
371
|
-
// Preserve the public query-
|
|
372
|
-
|
|
391
|
+
// Preserve the public query-handle shape for all callers.
|
|
392
|
+
handle: QueryHandle<TData, TError>;
|
|
373
393
|
getCurrentObserverOptions: () => QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
|
|
374
394
|
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
|
|
375
395
|
getCurrentQueryKey: () => TQueryKey;
|
|
@@ -401,14 +421,14 @@ function createQueryService<
|
|
|
401
421
|
getCurrentObserverOptions,
|
|
402
422
|
getCurrentQueryKey: () => resolvedQueryKey,
|
|
403
423
|
setDerivedState,
|
|
404
|
-
|
|
405
|
-
getSnapshot: () =>
|
|
424
|
+
handle: {
|
|
425
|
+
getSnapshot: () => toQueryHandleSnapshot(observer.getCurrentResult()),
|
|
406
426
|
subscribe: (listener) =>
|
|
407
427
|
observer.subscribe((result) => {
|
|
408
|
-
listener(
|
|
428
|
+
listener(toQueryHandleSnapshot(result));
|
|
409
429
|
}),
|
|
410
430
|
refetch: async (refetchOptions) =>
|
|
411
|
-
|
|
431
|
+
toQueryHandleSnapshot(await observer.refetch(refetchOptions)),
|
|
412
432
|
invalidate: (invalidateOptions) =>
|
|
413
433
|
queryClient.invalidateQueries(
|
|
414
434
|
{
|
|
@@ -433,24 +453,24 @@ function bindQueryDependencies<
|
|
|
433
453
|
TQueryData = TQueryFnData,
|
|
434
454
|
TQueryKey extends QueryKey = QueryKey,
|
|
435
455
|
>(
|
|
436
|
-
|
|
437
|
-
typeof
|
|
456
|
+
queryHandle: ReturnType<
|
|
457
|
+
typeof createQueryHandle<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
438
458
|
>,
|
|
439
459
|
queryKey: TQueryKey,
|
|
440
460
|
dependsOn: QueryDependencyTuple<TSources, TQueryKey>
|
|
441
|
-
):
|
|
461
|
+
): QueryHandle<TData, TError> {
|
|
442
462
|
const dependencyController = createDependencyController(
|
|
443
463
|
queryKey,
|
|
444
|
-
|
|
464
|
+
queryHandle.setDerivedState,
|
|
445
465
|
dependsOn
|
|
446
466
|
);
|
|
447
467
|
let subscriberCount = 0;
|
|
448
468
|
|
|
449
469
|
return {
|
|
450
|
-
...
|
|
470
|
+
...queryHandle.handle,
|
|
451
471
|
refetch: async (refetchOptions) => {
|
|
452
472
|
await dependencyController.evaluateForRefetch();
|
|
453
|
-
return
|
|
473
|
+
return queryHandle.handle.refetch(refetchOptions);
|
|
454
474
|
},
|
|
455
475
|
subscribe: (listener) => {
|
|
456
476
|
if (subscriberCount === 0) {
|
|
@@ -459,7 +479,7 @@ function bindQueryDependencies<
|
|
|
459
479
|
|
|
460
480
|
subscriberCount += 1;
|
|
461
481
|
|
|
462
|
-
const unsubscribe =
|
|
482
|
+
const unsubscribe = queryHandle.handle.subscribe(listener);
|
|
463
483
|
|
|
464
484
|
return () => {
|
|
465
485
|
subscriberCount = Math.max(0, subscriberCount - 1);
|
|
@@ -551,7 +571,7 @@ function createDependencyController<
|
|
|
551
571
|
};
|
|
552
572
|
}
|
|
553
573
|
|
|
554
|
-
function
|
|
574
|
+
function splitQueryHandleOptions<
|
|
555
575
|
TQueryFnData = unknown,
|
|
556
576
|
TError = Error,
|
|
557
577
|
TData = TQueryFnData,
|
|
@@ -559,10 +579,10 @@ function splitQueryServiceOptions<
|
|
|
559
579
|
TQueryKey extends QueryKey = QueryKey,
|
|
560
580
|
TSources extends readonly unknown[] = [],
|
|
561
581
|
>(
|
|
562
|
-
options?:
|
|
582
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
563
583
|
): {
|
|
564
584
|
dependsOn?: QueryDependencyTuple<TSources, TQueryKey>;
|
|
565
|
-
runtimeOptions:
|
|
585
|
+
runtimeOptions: QueryHandleRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> | undefined;
|
|
566
586
|
} {
|
|
567
587
|
if (options === undefined) {
|
|
568
588
|
return {
|
|
@@ -588,7 +608,7 @@ function toQueryOptions<
|
|
|
588
608
|
>(
|
|
589
609
|
queryKey: TQueryKey,
|
|
590
610
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
591
|
-
options?:
|
|
611
|
+
options?: QueryHandleRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
592
612
|
): QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
|
|
593
613
|
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
|
|
594
614
|
// Centralize option assembly so both normal queries and tracked queries build observers and
|