@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
|
@@ -3,15 +3,15 @@ import { createRoot } from 'react-dom/client';
|
|
|
3
3
|
import { QueryClient } from '@tanstack/query-core';
|
|
4
4
|
|
|
5
5
|
import { setupQuery } from '../../query.js';
|
|
6
|
-
import {
|
|
6
|
+
import { useQueryHandle } from '../hooks/use-query-handle.js';
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type { QueryHandle, QueryHandleSnapshot } from '../../query.js';
|
|
9
9
|
|
|
10
10
|
declare global {
|
|
11
11
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
describe('
|
|
14
|
+
describe('useQueryHandle', () => {
|
|
15
15
|
let container: HTMLDivElement;
|
|
16
16
|
|
|
17
17
|
beforeAll(() => {
|
|
@@ -36,7 +36,7 @@ describe('useQuerySubscription', () => {
|
|
|
36
36
|
const renderStates: Array<string | undefined> = [];
|
|
37
37
|
|
|
38
38
|
const Consumer = () => {
|
|
39
|
-
const snapshot =
|
|
39
|
+
const snapshot = useQueryHandle(query);
|
|
40
40
|
renderStates.push(snapshot.data?.name);
|
|
41
41
|
|
|
42
42
|
return <span>{snapshot.data?.name ?? 'pending'}</span>;
|
|
@@ -63,7 +63,7 @@ describe('useQuerySubscription', () => {
|
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
it('cleans up the store subscription on unmount', async () => {
|
|
66
|
-
const snapshot:
|
|
66
|
+
const snapshot: QueryHandleSnapshot<{ name: string }, Error> = {
|
|
67
67
|
data: { name: 'Ada' },
|
|
68
68
|
error: null,
|
|
69
69
|
fetchStatus: 'idle',
|
|
@@ -74,7 +74,7 @@ describe('useQuerySubscription', () => {
|
|
|
74
74
|
isSuccess: true,
|
|
75
75
|
};
|
|
76
76
|
const unsubscribe = jest.fn();
|
|
77
|
-
const query:
|
|
77
|
+
const query: QueryHandle<{ name: string }, Error> = {
|
|
78
78
|
getSnapshot: () => snapshot,
|
|
79
79
|
subscribe: jest.fn(() => unsubscribe),
|
|
80
80
|
refetch: jest.fn(async () => snapshot),
|
|
@@ -85,7 +85,7 @@ describe('useQuerySubscription', () => {
|
|
|
85
85
|
const root = createRoot(container);
|
|
86
86
|
|
|
87
87
|
const Consumer = () => {
|
|
88
|
-
|
|
88
|
+
useQueryHandle(query);
|
|
89
89
|
return <span>ready</span>;
|
|
90
90
|
};
|
|
91
91
|
|
package/src/react/hooks/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useQueryHandle } from './use-query-handle.js';
|
|
@@ -2,37 +2,37 @@
|
|
|
2
2
|
// and connect an external store to React rendering.
|
|
3
3
|
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
4
4
|
|
|
5
|
-
// Import the query
|
|
6
|
-
import type {
|
|
5
|
+
// Import the query-handle contract and the stable snapshot shape the hook returns.
|
|
6
|
+
import type { QueryHandle, QueryHandleSnapshot } from '../../query.js';
|
|
7
7
|
// Describe the no-argument listener shape expected by useSyncExternalStore.
|
|
8
8
|
type Listener = () => void;
|
|
9
9
|
|
|
10
10
|
// Store one cached snapshot together with the store version it belongs to.
|
|
11
11
|
type SnapshotCacheEntry<TData, TError> = {
|
|
12
|
-
// Hold the snapshot returned by the query
|
|
13
|
-
snapshot:
|
|
12
|
+
// Hold the snapshot returned by the query handle for this version.
|
|
13
|
+
snapshot: QueryHandleSnapshot<TData, TError>;
|
|
14
14
|
// Track which subscription version produced the cached snapshot.
|
|
15
15
|
version: number;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
// Subscribe a React component to a
|
|
19
|
-
export function
|
|
20
|
-
// Receive the external query
|
|
21
|
-
|
|
18
|
+
// Subscribe a React component to a QueryHandle and return its latest snapshot.
|
|
19
|
+
export function useQueryHandle<TData, TError>(
|
|
20
|
+
// Receive the external query handle instance to subscribe to.
|
|
21
|
+
queryHandle: QueryHandle<TData, TError>
|
|
22
22
|
) {
|
|
23
23
|
// Count store notifications so we can tell when our cached snapshot is stale.
|
|
24
24
|
const snapshotVersionRef = useRef(0);
|
|
25
25
|
// Cache one client-side snapshot per observed version to keep getSnapshot stable.
|
|
26
26
|
const snapshotCacheRef = useRef<SnapshotCacheEntry<TData, TError> | null>(null);
|
|
27
27
|
// Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
|
|
28
|
-
const serverSnapshotCacheRef = useRef<
|
|
28
|
+
const serverSnapshotCacheRef = useRef<QueryHandleSnapshot<TData, TError> | null>(null);
|
|
29
29
|
|
|
30
30
|
// Create the subscribe function expected by useSyncExternalStore.
|
|
31
31
|
const subscribe = useCallback(
|
|
32
32
|
// React passes a listener that must run whenever the external store changes.
|
|
33
33
|
(listener: Listener) =>
|
|
34
|
-
// Forward the subscription to the query
|
|
35
|
-
|
|
34
|
+
// Forward the subscription to the query handle.
|
|
35
|
+
queryHandle.subscribe(() => {
|
|
36
36
|
// Bump the version so later reads know the previous cache is outdated.
|
|
37
37
|
snapshotVersionRef.current += 1;
|
|
38
38
|
// Drop the cached client snapshot because the store just changed.
|
|
@@ -42,8 +42,8 @@ export function useQuerySubscription<TData, TError>(
|
|
|
42
42
|
// Notify React that it should read a fresh snapshot.
|
|
43
43
|
listener();
|
|
44
44
|
}),
|
|
45
|
-
// Recreate the subscription function only when the
|
|
46
|
-
[
|
|
45
|
+
// Recreate the subscription function only when the handle instance changes.
|
|
46
|
+
[queryHandle]
|
|
47
47
|
);
|
|
48
48
|
|
|
49
49
|
// Read the current client snapshot in a referentially stable way for React.
|
|
@@ -59,8 +59,8 @@ export function useQuerySubscription<TData, TError>(
|
|
|
59
59
|
return cachedSnapshot.snapshot;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Ask the query
|
|
63
|
-
const snapshot =
|
|
62
|
+
// Ask the query handle for the latest snapshot because the cache is empty or stale.
|
|
63
|
+
const snapshot = queryHandle.getSnapshot();
|
|
64
64
|
// Store the new snapshot together with the version it belongs to.
|
|
65
65
|
snapshotCacheRef.current = {
|
|
66
66
|
snapshot,
|
|
@@ -69,8 +69,7 @@ export function useQuerySubscription<TData, TError>(
|
|
|
69
69
|
|
|
70
70
|
// Return the freshly read snapshot to React.
|
|
71
71
|
return snapshot;
|
|
72
|
-
|
|
73
|
-
}, [queryService]);
|
|
72
|
+
}, [queryHandle]);
|
|
74
73
|
|
|
75
74
|
// Read the server snapshot used by React during SSR or hydration fallback paths.
|
|
76
75
|
const getServerSnapshot = useCallback(() => {
|
|
@@ -83,15 +82,14 @@ export function useQuerySubscription<TData, TError>(
|
|
|
83
82
|
return cachedSnapshot;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
// Ask the query
|
|
87
|
-
const snapshot =
|
|
85
|
+
// Ask the query handle for a snapshot because no server cache exists yet.
|
|
86
|
+
const snapshot = queryHandle.getSnapshot();
|
|
88
87
|
// Cache that snapshot for the next server read.
|
|
89
88
|
serverSnapshotCacheRef.current = snapshot;
|
|
90
89
|
|
|
91
90
|
// Return the freshly read server snapshot.
|
|
92
91
|
return snapshot;
|
|
93
|
-
|
|
94
|
-
}, [queryService]);
|
|
92
|
+
}, [queryHandle]);
|
|
95
93
|
|
|
96
94
|
// Let React subscribe to the external store and read snapshots through the callbacks above.
|
|
97
95
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
package/src/react/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useQueryHandle } from './hooks/index.js';
|
package/dist/query-registry.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { QueryService } from './query.js';
|
|
2
|
-
export interface QueryRegistry<TParams, TKey extends readonly unknown[]> {
|
|
3
|
-
clear: () => void;
|
|
4
|
-
getKey: (params: TParams) => TKey;
|
|
5
|
-
name: string;
|
|
6
|
-
resolve: <TData, TError = Error>(params: TParams, create: (queryKey: TKey) => QueryService<TData, TError>) => QueryService<TData, TError>;
|
|
7
|
-
}
|
|
8
|
-
export declare function createQueryRegistry<TParams, TKey extends readonly unknown[]>(name: string, createKey: (params: TParams) => TKey): QueryRegistry<TParams, TKey>;
|
|
9
|
-
export declare function serializeQueryKey(queryKey: readonly unknown[]): string;
|
package/dist/query-registry.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { hashKey } from '@tanstack/query-core';
|
|
2
|
-
export function createQueryRegistry(name, createKey) {
|
|
3
|
-
const entries = new Map();
|
|
4
|
-
return {
|
|
5
|
-
name,
|
|
6
|
-
clear() {
|
|
7
|
-
entries.clear();
|
|
8
|
-
},
|
|
9
|
-
getKey(params) {
|
|
10
|
-
return createKey(params);
|
|
11
|
-
},
|
|
12
|
-
resolve(params, create) {
|
|
13
|
-
const queryKey = createKey(params);
|
|
14
|
-
const cacheKey = serializeQueryKey(queryKey);
|
|
15
|
-
const existingEntry = entries.get(cacheKey);
|
|
16
|
-
if (existingEntry) {
|
|
17
|
-
return existingEntry;
|
|
18
|
-
}
|
|
19
|
-
const nextEntry = create(queryKey);
|
|
20
|
-
entries.set(cacheKey, nextEntry);
|
|
21
|
-
return nextEntry;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
export function serializeQueryKey(queryKey) {
|
|
26
|
-
return hashKey(queryKey);
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=query-registry.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"query-registry.js","sourceRoot":"","sources":["../src/query-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAc/C,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,SAAoC;IAEpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0C,CAAC;IAElE,OAAO;QACL,IAAI;QACJ,KAAK;YACH,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;QACD,MAAM,CAAC,MAAM;YACX,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,CACL,MAAe,EACf,MAAuD;YAEvD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;YAC7C,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAA4C,CAAC;YAEvF,IAAI,aAAa,EAAE,CAAC;gBAClB,OAAO,aAAa,CAAC;YACvB,CAAC;YAED,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YAEnC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAA2C,CAAC,CAAC;YAEnE,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAA4B;IAC5D,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import type { QueryService } from '../query';
|
|
2
|
-
import { createQueryRegistry, serializeQueryKey } from '../query-registry';
|
|
3
|
-
|
|
4
|
-
describe('createQueryRegistry', () => {
|
|
5
|
-
it('reuses the same entry for identical params', () => {
|
|
6
|
-
const registry = createQueryRegistry('branches', (params: { branchId: string }) => [
|
|
7
|
-
'branch',
|
|
8
|
-
{
|
|
9
|
-
deps: {
|
|
10
|
-
branchId: params.branchId,
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
] as const);
|
|
14
|
-
const createEntry = jest.fn((queryKey) => ({ queryKey }));
|
|
15
|
-
|
|
16
|
-
const firstEntry = registry.resolve({ branchId: 'branch-1' }, createEntry as never);
|
|
17
|
-
const secondEntry = registry.resolve({ branchId: 'branch-1' }, createEntry as never);
|
|
18
|
-
|
|
19
|
-
expect(firstEntry).toBe(secondEntry);
|
|
20
|
-
expect(createEntry).toHaveBeenCalledTimes(1);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('creates separate entries for different params', () => {
|
|
24
|
-
const registry = createQueryRegistry('branches', (params: { branchId: string }) => [
|
|
25
|
-
'branch',
|
|
26
|
-
{
|
|
27
|
-
deps: {
|
|
28
|
-
branchId: params.branchId,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
] as const);
|
|
32
|
-
const createEntry = jest.fn((queryKey) => ({ queryKey }));
|
|
33
|
-
|
|
34
|
-
const firstEntry = registry.resolve({ branchId: 'branch-1' }, createEntry as never);
|
|
35
|
-
const secondEntry = registry.resolve({ branchId: 'branch-2' }, createEntry as never);
|
|
36
|
-
|
|
37
|
-
expect(firstEntry).not.toBe(secondEntry);
|
|
38
|
-
expect(createEntry).toHaveBeenCalledTimes(2);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('exposes the generated query key', () => {
|
|
42
|
-
const registry = createQueryRegistry('branches', (params: { branchId: string }) => [
|
|
43
|
-
'branch',
|
|
44
|
-
{
|
|
45
|
-
deps: {
|
|
46
|
-
branchId: params.branchId,
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
] as const);
|
|
50
|
-
|
|
51
|
-
expect(registry.getKey({ branchId: 'branch-1' })).toEqual([
|
|
52
|
-
'branch',
|
|
53
|
-
{
|
|
54
|
-
deps: {
|
|
55
|
-
branchId: 'branch-1',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('uses TanStack-compatible stable hashing for object keys', () => {
|
|
62
|
-
const firstKey = ['branch', { deps: { branchId: 'branch-1', companyId: 'company-1' } }] as const;
|
|
63
|
-
const secondKey = ['branch', { deps: { companyId: 'company-1', branchId: 'branch-1' } }] as const;
|
|
64
|
-
|
|
65
|
-
expect(serializeQueryKey(firstKey)).toBe(serializeQueryKey(secondKey));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('infers the query service type from the creator callback', () => {
|
|
69
|
-
const registry = createQueryRegistry('branches', (params: { branchId: string }) => [
|
|
70
|
-
'branch',
|
|
71
|
-
{
|
|
72
|
-
deps: {
|
|
73
|
-
branchId: params.branchId,
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
] as const);
|
|
77
|
-
|
|
78
|
-
const query = registry.resolve({ branchId: 'branch-1' }, () => {
|
|
79
|
-
return {
|
|
80
|
-
getSnapshot: () => ({
|
|
81
|
-
data: { id: 'branch-1' },
|
|
82
|
-
error: null,
|
|
83
|
-
fetchStatus: 'idle',
|
|
84
|
-
isError: false,
|
|
85
|
-
isFetching: false,
|
|
86
|
-
isPending: false,
|
|
87
|
-
isSuccess: true,
|
|
88
|
-
status: 'success',
|
|
89
|
-
}),
|
|
90
|
-
invalidate: jest.fn(),
|
|
91
|
-
refetch: jest.fn(),
|
|
92
|
-
subscribe: jest.fn(),
|
|
93
|
-
unsafe_getResult: jest.fn(),
|
|
94
|
-
} as unknown as QueryService<{ id: string }, Error>;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const typedQuery: QueryService<{ id: string }, Error> = query;
|
|
98
|
-
|
|
99
|
-
expect(typedQuery).toBe(query);
|
|
100
|
-
});
|
|
101
|
-
});
|
package/src/query-registry.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { hashKey } from '@tanstack/query-core';
|
|
2
|
-
|
|
3
|
-
import type { QueryService } from './query.js';
|
|
4
|
-
|
|
5
|
-
export interface QueryRegistry<TParams, TKey extends readonly unknown[]> {
|
|
6
|
-
clear: () => void;
|
|
7
|
-
getKey: (params: TParams) => TKey;
|
|
8
|
-
name: string;
|
|
9
|
-
resolve: <TData, TError = Error>(
|
|
10
|
-
params: TParams,
|
|
11
|
-
create: (queryKey: TKey) => QueryService<TData, TError>
|
|
12
|
-
) => QueryService<TData, TError>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function createQueryRegistry<TParams, TKey extends readonly unknown[]>(
|
|
16
|
-
name: string,
|
|
17
|
-
createKey: (params: TParams) => TKey
|
|
18
|
-
): QueryRegistry<TParams, TKey> {
|
|
19
|
-
const entries = new Map<string, QueryService<unknown, unknown>>();
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
name,
|
|
23
|
-
clear() {
|
|
24
|
-
entries.clear();
|
|
25
|
-
},
|
|
26
|
-
getKey(params) {
|
|
27
|
-
return createKey(params);
|
|
28
|
-
},
|
|
29
|
-
resolve<TData, TError = Error>(
|
|
30
|
-
params: TParams,
|
|
31
|
-
create: (queryKey: TKey) => QueryService<TData, TError>
|
|
32
|
-
) {
|
|
33
|
-
const queryKey = createKey(params);
|
|
34
|
-
const cacheKey = serializeQueryKey(queryKey);
|
|
35
|
-
const existingEntry = entries.get(cacheKey) as QueryService<TData, TError> | undefined;
|
|
36
|
-
|
|
37
|
-
if (existingEntry) {
|
|
38
|
-
return existingEntry;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const nextEntry = create(queryKey);
|
|
42
|
-
|
|
43
|
-
entries.set(cacheKey, nextEntry as QueryService<unknown, unknown>);
|
|
44
|
-
|
|
45
|
-
return nextEntry;
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function serializeQueryKey(queryKey: readonly unknown[]): string {
|
|
51
|
-
return hashKey(queryKey);
|
|
52
|
-
}
|