@veams/status-quo-query 0.4.0 → 0.6.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 +438 -14
- package/dist/index.d.ts +1 -0
- package/dist/mutation.d.ts +31 -3
- package/dist/mutation.js +86 -18
- package/dist/mutation.js.map +1 -1
- package/dist/provider.d.ts +22 -3
- package/dist/provider.js +35 -4
- package/dist/provider.js.map +1 -1
- package/dist/query.d.ts +25 -3
- package/dist/query.js +84 -23
- package/dist/query.js.map +1 -1
- package/dist/tracking.d.ts +85 -0
- package/dist/tracking.js +226 -0
- package/dist/tracking.js.map +1 -0
- package/package.json +15 -1
- package/src/__tests__/mutation.spec.ts +2 -2
- package/src/__tests__/tracked.spec.ts +276 -0
- package/src/index.ts +9 -0
- package/src/mutation.ts +199 -25
- package/src/provider.ts +110 -6
- package/src/query.ts +178 -33
- package/src/tracking.ts +384 -0
package/dist/tracking.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the internal registry that connects named dependency values to TanStack query hashes.
|
|
3
|
+
*
|
|
4
|
+
* We intentionally keep two indexes:
|
|
5
|
+
* - byDependencyName: fast forward lookup for invalidation
|
|
6
|
+
* - byQueryHash: fast reverse lookup for cleanup when TanStack removes a query
|
|
7
|
+
*
|
|
8
|
+
* The reverse index is what lets us react to TanStack `removed` events without scanning the
|
|
9
|
+
* entire registry and without leaking dead query hashes over time.
|
|
10
|
+
*/
|
|
11
|
+
export function createTrackingRegistry() {
|
|
12
|
+
// Forward index: dependency name -> serialized dependency value -> query hashes.
|
|
13
|
+
const byDependencyName = new Map();
|
|
14
|
+
// Reverse index: query hash -> dependency entries originally registered for that query.
|
|
15
|
+
const byQueryHash = new Map();
|
|
16
|
+
return {
|
|
17
|
+
has: (queryHash) => byQueryHash.has(queryHash),
|
|
18
|
+
match: (dependencies, mode) => matchTrackedQueryHashes(byDependencyName, dependencies, mode),
|
|
19
|
+
register: (queryHash, dependencies) => {
|
|
20
|
+
// Re-registration should replace stale mappings instead of accumulating duplicates.
|
|
21
|
+
if (byQueryHash.has(queryHash)) {
|
|
22
|
+
unregisterTrackedQueryHash(byDependencyName, byQueryHash, queryHash);
|
|
23
|
+
}
|
|
24
|
+
byQueryHash.set(queryHash, dependencies);
|
|
25
|
+
for (const dependency of dependencies) {
|
|
26
|
+
// One query can be addressed through several dependency dimensions at once.
|
|
27
|
+
const valueMap = getOrCreateMap(byDependencyName, dependency.name);
|
|
28
|
+
const queryHashes = getOrCreateSet(valueMap, dependency.key);
|
|
29
|
+
queryHashes.add(queryHash);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
unregister: (queryHash) => {
|
|
33
|
+
unregisterTrackedQueryHash(byDependencyName, byQueryHash, queryHash);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extracts tracked dependency entries from the final query-key segment.
|
|
39
|
+
*
|
|
40
|
+
* The tracked contract is intentionally strict:
|
|
41
|
+
* - the last query-key segment must be an object
|
|
42
|
+
* - `deps` must exist on that object
|
|
43
|
+
* - only `deps` participates in tracking
|
|
44
|
+
*
|
|
45
|
+
* Everything else in the key, including `view`, is still part of the TanStack cache key,
|
|
46
|
+
* but is ignored for invalidation matching.
|
|
47
|
+
*/
|
|
48
|
+
export function extractTrackedDependencies(queryKey) {
|
|
49
|
+
const finalSegment = queryKey.at(-1);
|
|
50
|
+
if (!isPlainObject(finalSegment)) {
|
|
51
|
+
throw new Error('Tracked queries require the final queryKey segment to be an object with a deps property.');
|
|
52
|
+
}
|
|
53
|
+
const dependencies = finalSegment.deps;
|
|
54
|
+
if (!isPlainObject(dependencies)) {
|
|
55
|
+
throw new Error('Tracked queries require queryKey[queryKey.length - 1].deps to be a plain object.');
|
|
56
|
+
}
|
|
57
|
+
return toTrackedDependencyEntries(dependencies, 'Tracked query dependencies');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Converts a dependency record into normalized registry entries.
|
|
61
|
+
*
|
|
62
|
+
* Normalization happens here so the registry only ever works with one internal shape,
|
|
63
|
+
* regardless of whether the dependencies came from a query key or a mutation resolver.
|
|
64
|
+
*/
|
|
65
|
+
export function toTrackedDependencyEntries(dependencies, contextLabel) {
|
|
66
|
+
return Object.entries(dependencies).map(([name, value]) => {
|
|
67
|
+
assertTrackedDependencyValue(name, value, contextLabel);
|
|
68
|
+
return {
|
|
69
|
+
key: serializeTrackedDependencyValue(value),
|
|
70
|
+
name,
|
|
71
|
+
value,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Default dependency resolver used by the paired helper.
|
|
77
|
+
*
|
|
78
|
+
* This is intentionally permissive with partial results. If a mutation variable object only
|
|
79
|
+
* exposes some dependency keys, we track only those keys and let the chosen match mode decide
|
|
80
|
+
* how broad the invalidation should be.
|
|
81
|
+
*/
|
|
82
|
+
export function pickTrackedDependencies(dependencyKeys, variables) {
|
|
83
|
+
if (!isRecordLike(variables)) {
|
|
84
|
+
throw new Error('Tracked mutations need object-like variables when using dependencyKeys without resolveDependencies.');
|
|
85
|
+
}
|
|
86
|
+
const resolved = {};
|
|
87
|
+
for (const dependencyKey of dependencyKeys) {
|
|
88
|
+
if (!(dependencyKey in variables)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const value = variables[dependencyKey];
|
|
92
|
+
assertTrackedDependencyValue(dependencyKey, value, 'Tracked mutation dependency resolution');
|
|
93
|
+
resolved[dependencyKey] = value;
|
|
94
|
+
}
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolves live TanStack queries from tracked query hashes.
|
|
99
|
+
*
|
|
100
|
+
* The registry may contain hashes that were valid when the invalidation pass started but have
|
|
101
|
+
* already disappeared from TanStack's cache. Filtering to live queries here keeps invalidation
|
|
102
|
+
* exact and avoids relying on stale registry state.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveTrackedQueries(queryClient, queryHashes) {
|
|
105
|
+
const queries = [];
|
|
106
|
+
for (const queryHash of queryHashes) {
|
|
107
|
+
const query = queryClient.getQueryCache().get(queryHash);
|
|
108
|
+
if (query) {
|
|
109
|
+
queries.push(query);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return queries;
|
|
113
|
+
}
|
|
114
|
+
function assertTrackedDependencyValue(name, value, contextLabel) {
|
|
115
|
+
if (value === null ||
|
|
116
|
+
value === undefined ||
|
|
117
|
+
typeof value === 'string' ||
|
|
118
|
+
typeof value === 'number' ||
|
|
119
|
+
typeof value === 'boolean') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`${contextLabel} only support primitive dependency values. "${name}" received ${typeof value}.`);
|
|
123
|
+
}
|
|
124
|
+
function getOrCreateMap(map, key) {
|
|
125
|
+
const existing = map.get(key);
|
|
126
|
+
if (existing) {
|
|
127
|
+
return existing;
|
|
128
|
+
}
|
|
129
|
+
const created = new Map();
|
|
130
|
+
map.set(key, created);
|
|
131
|
+
return created;
|
|
132
|
+
}
|
|
133
|
+
function getOrCreateSet(map, key) {
|
|
134
|
+
const existing = map.get(key);
|
|
135
|
+
if (existing) {
|
|
136
|
+
return existing;
|
|
137
|
+
}
|
|
138
|
+
const created = new Set();
|
|
139
|
+
map.set(key, created);
|
|
140
|
+
return created;
|
|
141
|
+
}
|
|
142
|
+
function isPlainObject(value) {
|
|
143
|
+
if (!isRecordLike(value)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return (Object.getPrototypeOf(value) === Object.prototype ||
|
|
147
|
+
Object.getPrototypeOf(value) === null);
|
|
148
|
+
}
|
|
149
|
+
function isRecordLike(value) {
|
|
150
|
+
return typeof value === 'object' && value !== null;
|
|
151
|
+
}
|
|
152
|
+
function matchTrackedQueryHashes(byDependencyName, dependencies, mode) {
|
|
153
|
+
// No dependency input means no automatic invalidation target.
|
|
154
|
+
if (dependencies.length === 0) {
|
|
155
|
+
return new Set();
|
|
156
|
+
}
|
|
157
|
+
if (mode === 'union') {
|
|
158
|
+
// Union broadens invalidation to any query that matches at least one dependency pair.
|
|
159
|
+
const matches = new Set();
|
|
160
|
+
for (const dependency of dependencies) {
|
|
161
|
+
for (const queryHash of getTrackedQueryHashes(byDependencyName, dependency)) {
|
|
162
|
+
matches.add(queryHash);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return matches;
|
|
166
|
+
}
|
|
167
|
+
// Intersection narrows invalidation to queries that match every provided dependency pair.
|
|
168
|
+
let matches;
|
|
169
|
+
for (const dependency of dependencies) {
|
|
170
|
+
const queryHashes = getTrackedQueryHashes(byDependencyName, dependency);
|
|
171
|
+
if (queryHashes.size === 0) {
|
|
172
|
+
return new Set();
|
|
173
|
+
}
|
|
174
|
+
if (!matches) {
|
|
175
|
+
matches = new Set(queryHashes);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const queryHash of [...matches]) {
|
|
179
|
+
if (!queryHashes.has(queryHash)) {
|
|
180
|
+
matches.delete(queryHash);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return matches ?? new Set();
|
|
185
|
+
}
|
|
186
|
+
function getTrackedQueryHashes(byDependencyName, dependency) {
|
|
187
|
+
return byDependencyName.get(dependency.name)?.get(dependency.key) ?? new Set();
|
|
188
|
+
}
|
|
189
|
+
function serializeTrackedDependencyValue(value) {
|
|
190
|
+
// The registry stores values as strings, so include the primitive type to avoid collisions
|
|
191
|
+
// such as `"1"` and `1` landing in the same bucket.
|
|
192
|
+
if (value === null) {
|
|
193
|
+
return 'null';
|
|
194
|
+
}
|
|
195
|
+
if (value === undefined) {
|
|
196
|
+
return 'undefined';
|
|
197
|
+
}
|
|
198
|
+
return `${typeof value}:${String(value)}`;
|
|
199
|
+
}
|
|
200
|
+
function unregisterTrackedQueryHash(byDependencyName, byQueryHash, queryHash) {
|
|
201
|
+
// If the query hash was never tracked or was already cleaned up, there is nothing to do.
|
|
202
|
+
const dependencies = byQueryHash.get(queryHash);
|
|
203
|
+
if (!dependencies) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const dependency of dependencies) {
|
|
207
|
+
const valueMap = byDependencyName.get(dependency.name);
|
|
208
|
+
if (!valueMap) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const queryHashes = valueMap.get(dependency.key);
|
|
212
|
+
if (!queryHashes) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
queryHashes.delete(queryHash);
|
|
216
|
+
// Prune empty buckets so the registry footprint follows the live TanStack cache.
|
|
217
|
+
if (queryHashes.size === 0) {
|
|
218
|
+
valueMap.delete(dependency.key);
|
|
219
|
+
}
|
|
220
|
+
if (valueMap.size === 0) {
|
|
221
|
+
byDependencyName.delete(dependency.name);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
byQueryHash.delete(queryHash);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=tracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracking.js","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veams/status-quo-query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -80,5 +80,19 @@
|
|
|
80
80
|
},
|
|
81
81
|
"bugs": {
|
|
82
82
|
"url": "https://github.com/Veams/veams/issues"
|
|
83
|
+
},
|
|
84
|
+
"release-it": {
|
|
85
|
+
"git": {
|
|
86
|
+
"tagName": "${npm.name}@${version}",
|
|
87
|
+
"pushRepo": "origin",
|
|
88
|
+
"commitMessage": "chore(status-quo-query): release ${npm.name}@${version}"
|
|
89
|
+
},
|
|
90
|
+
"github": {
|
|
91
|
+
"release": true,
|
|
92
|
+
"releaseName": "${npm.name}@${version}"
|
|
93
|
+
},
|
|
94
|
+
"npm": {
|
|
95
|
+
"publish": true
|
|
96
|
+
}
|
|
83
97
|
}
|
|
84
98
|
}
|
|
@@ -40,10 +40,10 @@ describe('Mutation Service', () => {
|
|
|
40
40
|
it('tracks failed mutations', async () => {
|
|
41
41
|
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: 0 } } });
|
|
42
42
|
const createMutation = setupMutation(queryClient);
|
|
43
|
-
const mutationFn = jest.fn().
|
|
43
|
+
const mutationFn = jest.fn((_variables: void) => Promise.reject(new Error('boom')));
|
|
44
44
|
const service = createMutation(mutationFn);
|
|
45
45
|
|
|
46
|
-
await expect(service.mutate()).rejects.toThrow('boom');
|
|
46
|
+
await expect(service.mutate(undefined)).rejects.toThrow('boom');
|
|
47
47
|
expect(service.getSnapshot().status).toBe('error');
|
|
48
48
|
});
|
|
49
49
|
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
2
|
+
|
|
3
|
+
import { setupQueryManager } from '../provider';
|
|
4
|
+
|
|
5
|
+
describe('Tracked Query Invalidation', () => {
|
|
6
|
+
it('registers tracked queries from deps and ignores view data during invalidation', async () => {
|
|
7
|
+
const queryClient = new QueryClient({
|
|
8
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
9
|
+
});
|
|
10
|
+
const manager = setupQueryManager(queryClient);
|
|
11
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
12
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
13
|
+
'applicationId',
|
|
14
|
+
'productId',
|
|
15
|
+
] as const);
|
|
16
|
+
|
|
17
|
+
createQuery(
|
|
18
|
+
[
|
|
19
|
+
'product',
|
|
20
|
+
{
|
|
21
|
+
deps: { applicationId: 'app-1', productId: 'product-1' },
|
|
22
|
+
view: { page: 2, sort: 'name' },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
jest.fn().mockResolvedValue('product'),
|
|
26
|
+
{ enabled: false }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const mutation = createMutation(jest.fn().mockResolvedValue({ ok: true as const }));
|
|
30
|
+
|
|
31
|
+
await mutation.mutate({
|
|
32
|
+
applicationId: 'app-1',
|
|
33
|
+
productId: 'product-1',
|
|
34
|
+
productName: 'Renamed',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledWith({
|
|
38
|
+
exact: true,
|
|
39
|
+
queryKey: [
|
|
40
|
+
'product',
|
|
41
|
+
{
|
|
42
|
+
deps: { applicationId: 'app-1', productId: 'product-1' },
|
|
43
|
+
view: { page: 2, sort: 'name' },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects tracked query keys without a deps object', () => {
|
|
50
|
+
const manager = setupQueryManager(new QueryClient());
|
|
51
|
+
|
|
52
|
+
expect(() =>
|
|
53
|
+
manager.createQuery(
|
|
54
|
+
['invalid', { view: { page: 1 } }] as never,
|
|
55
|
+
jest.fn().mockResolvedValue('nope')
|
|
56
|
+
)
|
|
57
|
+
).toThrow('Tracked queries require queryKey[queryKey.length - 1].deps to be a plain object.');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('supports partial dependency invalidation by matching only the provided keys', async () => {
|
|
61
|
+
const queryClient = new QueryClient({
|
|
62
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
63
|
+
});
|
|
64
|
+
const manager = setupQueryManager(queryClient);
|
|
65
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
66
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
67
|
+
'applicationId',
|
|
68
|
+
'productId',
|
|
69
|
+
] as const);
|
|
70
|
+
|
|
71
|
+
createQuery(
|
|
72
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
73
|
+
jest.fn().mockResolvedValue('page-1'),
|
|
74
|
+
{ enabled: false }
|
|
75
|
+
);
|
|
76
|
+
createQuery(
|
|
77
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-2' }, view: { page: 2 } }],
|
|
78
|
+
jest.fn().mockResolvedValue('page-2'),
|
|
79
|
+
{ enabled: false }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const mutation = createMutation(jest.fn().mockResolvedValue({ ok: true as const }));
|
|
83
|
+
|
|
84
|
+
await mutation.mutate({ applicationId: 'app-1', productName: 'Shared update' });
|
|
85
|
+
|
|
86
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('supports union matching and dedupes invalidation calls per query', async () => {
|
|
90
|
+
const queryClient = new QueryClient({
|
|
91
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
92
|
+
});
|
|
93
|
+
const manager = setupQueryManager(queryClient);
|
|
94
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
95
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
96
|
+
'applicationId',
|
|
97
|
+
'productId',
|
|
98
|
+
] as const);
|
|
99
|
+
|
|
100
|
+
createQuery(
|
|
101
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
102
|
+
jest.fn().mockResolvedValue('product-1'),
|
|
103
|
+
{ enabled: false }
|
|
104
|
+
);
|
|
105
|
+
createQuery(
|
|
106
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-2' }, view: { page: 2 } }],
|
|
107
|
+
jest.fn().mockResolvedValue('product-2'),
|
|
108
|
+
{ enabled: false }
|
|
109
|
+
);
|
|
110
|
+
createQuery(
|
|
111
|
+
['product', { deps: { applicationId: 'app-2', productId: 'product-1' }, view: { page: 3 } }],
|
|
112
|
+
jest.fn().mockResolvedValue('product-3'),
|
|
113
|
+
{ enabled: false }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const mutation = createMutation(jest.fn().mockResolvedValue({ ok: true as const }), {
|
|
117
|
+
matchMode: 'union',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await mutation.mutate({ applicationId: 'app-1', productId: 'product-1' });
|
|
121
|
+
|
|
122
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(3);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('supports custom dependency resolution for nested mutation variables', async () => {
|
|
126
|
+
const queryClient = new QueryClient({
|
|
127
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
128
|
+
});
|
|
129
|
+
const manager = setupQueryManager(queryClient);
|
|
130
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
131
|
+
|
|
132
|
+
manager.createQuery(
|
|
133
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
134
|
+
jest.fn().mockResolvedValue('product'),
|
|
135
|
+
{ enabled: false }
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const mutation = manager.createMutation(
|
|
139
|
+
jest.fn().mockResolvedValue({ ok: true as const }),
|
|
140
|
+
{
|
|
141
|
+
resolveDependencies: (variables: {
|
|
142
|
+
payload: { applicationId: string };
|
|
143
|
+
product: { id: string };
|
|
144
|
+
}) => ({
|
|
145
|
+
applicationId: variables.payload.applicationId,
|
|
146
|
+
productId: variables.product.id,
|
|
147
|
+
}),
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await mutation.mutate({
|
|
152
|
+
payload: { applicationId: 'app-1' },
|
|
153
|
+
product: { id: 'product-1' },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('supports error and settled invalidation timing', async () => {
|
|
160
|
+
const queryClient = new QueryClient({
|
|
161
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
162
|
+
});
|
|
163
|
+
const manager = setupQueryManager(queryClient);
|
|
164
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
165
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
166
|
+
'applicationId',
|
|
167
|
+
] as const);
|
|
168
|
+
|
|
169
|
+
createQuery(
|
|
170
|
+
['product', { deps: { applicationId: 'app-1' }, view: { page: 1 } }],
|
|
171
|
+
jest.fn().mockResolvedValue('product'),
|
|
172
|
+
{ enabled: false }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const invalidateOnError = createMutation(jest.fn().mockRejectedValue(new Error('boom')), {
|
|
176
|
+
invalidateOn: 'error',
|
|
177
|
+
});
|
|
178
|
+
const invalidateOnSettled = createMutation(
|
|
179
|
+
jest.fn().mockRejectedValue(new Error('boom again')),
|
|
180
|
+
{
|
|
181
|
+
invalidateOn: 'settled',
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await expect(invalidateOnError.mutate({ applicationId: 'app-1' })).rejects.toThrow('boom');
|
|
186
|
+
await expect(invalidateOnSettled.mutate({ applicationId: 'app-1' })).rejects.toThrow(
|
|
187
|
+
'boom again'
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(2);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('cleans removed query hashes out of dependency buckets', async () => {
|
|
194
|
+
const queryClient = new QueryClient({
|
|
195
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
196
|
+
});
|
|
197
|
+
const manager = setupQueryManager(queryClient);
|
|
198
|
+
const cacheGetSpy = jest.spyOn(queryClient.getQueryCache(), 'get');
|
|
199
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
200
|
+
'applicationId',
|
|
201
|
+
] as const);
|
|
202
|
+
|
|
203
|
+
const removedQueryKey = [
|
|
204
|
+
'product',
|
|
205
|
+
{ deps: { applicationId: 'app-1' }, view: { page: 1 } },
|
|
206
|
+
] as const;
|
|
207
|
+
|
|
208
|
+
createQuery(removedQueryKey, jest.fn().mockResolvedValue('page-1'), {
|
|
209
|
+
enabled: false,
|
|
210
|
+
});
|
|
211
|
+
queryClient.removeQueries({ exact: true, queryKey: removedQueryKey });
|
|
212
|
+
|
|
213
|
+
createQuery(
|
|
214
|
+
['product', { deps: { applicationId: 'app-1' }, view: { page: 2 } }],
|
|
215
|
+
jest.fn().mockResolvedValue('page-2'),
|
|
216
|
+
{ enabled: false }
|
|
217
|
+
);
|
|
218
|
+
cacheGetSpy.mockClear();
|
|
219
|
+
|
|
220
|
+
const mutation = createMutation(jest.fn().mockResolvedValue({ ok: true as const }));
|
|
221
|
+
|
|
222
|
+
await mutation.mutate({ applicationId: 'app-1' });
|
|
223
|
+
|
|
224
|
+
expect(cacheGetSpy).toHaveBeenCalledTimes(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('re-registers tracked queries on refetch after TanStack cache removal', async () => {
|
|
228
|
+
const queryClient = new QueryClient({
|
|
229
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
230
|
+
});
|
|
231
|
+
const manager = setupQueryManager(queryClient);
|
|
232
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
233
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
234
|
+
'applicationId',
|
|
235
|
+
] as const);
|
|
236
|
+
const queryKey = ['product', { deps: { applicationId: 'app-1' }, view: { page: 1 } }] as const;
|
|
237
|
+
const query = createQuery(queryKey, jest.fn().mockResolvedValue('product'), {
|
|
238
|
+
enabled: false,
|
|
239
|
+
});
|
|
240
|
+
const mutation = createMutation(jest.fn().mockResolvedValue({ ok: true as const }));
|
|
241
|
+
|
|
242
|
+
queryClient.removeQueries({ exact: true, queryKey });
|
|
243
|
+
|
|
244
|
+
await mutation.mutate({ applicationId: 'app-1' });
|
|
245
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(0);
|
|
246
|
+
|
|
247
|
+
await query.refetch();
|
|
248
|
+
await mutation.mutate({ applicationId: 'app-1' });
|
|
249
|
+
|
|
250
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('re-registers tracked queries on subscribe after TanStack cache removal', async () => {
|
|
254
|
+
const queryClient = new QueryClient({
|
|
255
|
+
defaultOptions: { mutations: { retry: 0 }, queries: { retry: 0 } },
|
|
256
|
+
});
|
|
257
|
+
const manager = setupQueryManager(queryClient);
|
|
258
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
259
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
260
|
+
'applicationId',
|
|
261
|
+
] as const);
|
|
262
|
+
const queryKey = ['product', { deps: { applicationId: 'app-1' }, view: { page: 1 } }] as const;
|
|
263
|
+
const query = createQuery(queryKey, jest.fn().mockResolvedValue('product'), {
|
|
264
|
+
enabled: false,
|
|
265
|
+
});
|
|
266
|
+
const mutation = createMutation(jest.fn().mockResolvedValue({ ok: true as const }));
|
|
267
|
+
|
|
268
|
+
queryClient.removeQueries({ exact: true, queryKey });
|
|
269
|
+
|
|
270
|
+
const unsubscribe = query.subscribe(() => undefined);
|
|
271
|
+
await mutation.mutate({ applicationId: 'app-1' });
|
|
272
|
+
unsubscribe();
|
|
273
|
+
|
|
274
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1);
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -4,3 +4,12 @@ export * from './mutation';
|
|
|
4
4
|
export * from './query';
|
|
5
5
|
// Re-export all provider-related types and functions for cache management.
|
|
6
6
|
export * from './provider';
|
|
7
|
+
// Re-export tracked dependency types used by the additive tracked facade.
|
|
8
|
+
export type {
|
|
9
|
+
TrackedDependencyRecord,
|
|
10
|
+
TrackedDependencyValue,
|
|
11
|
+
TrackedInvalidateOn,
|
|
12
|
+
TrackedMatchMode,
|
|
13
|
+
TrackedQueryKey,
|
|
14
|
+
TrackedQueryKeySegment,
|
|
15
|
+
} from './tracking';
|