@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/README.md
CHANGED
|
@@ -23,14 +23,25 @@ Root exports:
|
|
|
23
23
|
- `QueryManager`
|
|
24
24
|
- `CreateQuery`
|
|
25
25
|
- `CreateMutation`
|
|
26
|
+
- `CreateQueryAndMutation`
|
|
27
|
+
- `CreateMutationWithDefaults`
|
|
28
|
+
- `CreateUntrackedQuery`
|
|
29
|
+
- `CreateUntrackedMutation`
|
|
26
30
|
- `QueryService`
|
|
27
31
|
- `MutationService`
|
|
28
32
|
- `QueryServiceSnapshot`
|
|
29
33
|
- `MutationServiceSnapshot`
|
|
30
34
|
- `QueryServiceOptions`
|
|
31
35
|
- `MutationServiceOptions`
|
|
36
|
+
- `TrackedMutationServiceOptions`
|
|
32
37
|
- `QueryInvalidateOptions`
|
|
33
38
|
- `QueryMetaState`
|
|
39
|
+
- `TrackedDependencyRecord`
|
|
40
|
+
- `TrackedDependencyValue`
|
|
41
|
+
- `TrackedInvalidateOn`
|
|
42
|
+
- `TrackedMatchMode`
|
|
43
|
+
- `TrackedQueryKey`
|
|
44
|
+
- `TrackedQueryKeySegment`
|
|
34
45
|
|
|
35
46
|
Subpath exports:
|
|
36
47
|
|
|
@@ -42,26 +53,392 @@ Subpath exports:
|
|
|
42
53
|
|
|
43
54
|
```ts
|
|
44
55
|
import { QueryClient } from '@tanstack/query-core';
|
|
45
|
-
import {
|
|
46
|
-
setupQueryManager,
|
|
47
|
-
} from '@veams/status-quo-query';
|
|
56
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
48
57
|
|
|
49
58
|
const queryClient = new QueryClient();
|
|
50
59
|
const manager = setupQueryManager(queryClient);
|
|
60
|
+
const applicationId = 'app-1';
|
|
61
|
+
const productId = 'product-1';
|
|
51
62
|
|
|
52
|
-
const
|
|
63
|
+
const fetchProduct = async (currentApplicationId: string, currentProductId: string) => ({
|
|
64
|
+
applicationId: currentApplicationId,
|
|
65
|
+
name: 'Ada',
|
|
66
|
+
productId: currentProductId,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const saveProduct = async (variables: {
|
|
70
|
+
applicationId: string;
|
|
71
|
+
productId: string;
|
|
72
|
+
productName: string;
|
|
73
|
+
}) => ({
|
|
74
|
+
...variables,
|
|
75
|
+
saved: true as const,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const fetchUser = async (userId: number) => ({ id: userId, name: 'Ada' });
|
|
79
|
+
|
|
80
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
81
|
+
'applicationId',
|
|
82
|
+
'productId',
|
|
83
|
+
] as const);
|
|
84
|
+
|
|
85
|
+
const productQuery = createQuery(
|
|
86
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
87
|
+
() => fetchProduct('app-1', 'product-1'),
|
|
88
|
+
{
|
|
89
|
+
enabled: false,
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const updateProduct = createMutation(saveProduct, {
|
|
94
|
+
invalidateOn: 'success',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await productQuery.refetch();
|
|
98
|
+
await updateProduct.mutate({
|
|
99
|
+
applicationId: 'app-1',
|
|
100
|
+
productId: 'product-1',
|
|
101
|
+
productName: 'Ada',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const userQuery = manager.createUntrackedQuery(['user', 42], () => fetchUser(42), {
|
|
53
105
|
enabled: false,
|
|
54
106
|
});
|
|
55
107
|
await userQuery.refetch();
|
|
56
108
|
await userQuery.invalidate({ refetchType: 'none' });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Why Tracked Invalidation
|
|
112
|
+
|
|
113
|
+
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:
|
|
114
|
+
|
|
115
|
+
- queries declare their domain dependencies once in `queryKey[..., { deps, view }]`
|
|
116
|
+
- tracked mutations resolve the same dependency names from their variables
|
|
117
|
+
- the manager invalidates matching queries automatically
|
|
118
|
+
|
|
119
|
+
That changes the developer workflow from "remember which cache keys this mutation affects" to "describe which domain entities this query and mutation belong to".
|
|
120
|
+
|
|
121
|
+
Benefits:
|
|
122
|
+
|
|
123
|
+
- less manual cache invalidation code spread across features
|
|
124
|
+
- lower risk of stale UI because one dependent query was forgotten
|
|
125
|
+
- clearer separation between invalidation semantics in `deps` and UI variants in `view`
|
|
126
|
+
- typed paired helpers that remove repeated dependency mapping in the common case
|
|
127
|
+
|
|
128
|
+
## Examples
|
|
129
|
+
|
|
130
|
+
Tracked query keys use a final object segment:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
['products', { deps: { applicationId, productId }, view: { page, sort } }]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Rules:
|
|
137
|
+
|
|
138
|
+
- `deps` is required for tracked queries
|
|
139
|
+
- only `deps` is used for invalidation matching
|
|
140
|
+
- `view` is optional and recommended for pagination, sorting, filtering, and other cache variants
|
|
141
|
+
- tracked invalidation is manager-only because queries and mutations need one shared registry
|
|
142
|
+
|
|
143
|
+
### Paired Helper With Default Dependency Resolution
|
|
144
|
+
|
|
145
|
+
Use the paired helper when mutation variables already expose the dependency keys directly:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
149
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
150
|
+
|
|
151
|
+
const queryClient = new QueryClient();
|
|
152
|
+
const manager = setupQueryManager(queryClient);
|
|
153
|
+
const applicationId = 'app-1';
|
|
154
|
+
const productId = 'product-1';
|
|
155
|
+
|
|
156
|
+
const fetchProduct = async (currentApplicationId: string, currentProductId: string) => ({
|
|
157
|
+
applicationId: currentApplicationId,
|
|
158
|
+
name: 'Ada',
|
|
159
|
+
productId: currentProductId,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const saveProduct = async (variables: {
|
|
163
|
+
applicationId: string;
|
|
164
|
+
productId: string;
|
|
165
|
+
productName: string;
|
|
166
|
+
}) => variables;
|
|
167
|
+
|
|
168
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
169
|
+
'applicationId',
|
|
170
|
+
'productId',
|
|
171
|
+
] as const);
|
|
172
|
+
|
|
173
|
+
const productQuery = createQuery(
|
|
174
|
+
['product', { deps: { applicationId, productId }, view: { page: 1 } }],
|
|
175
|
+
() => fetchProduct(applicationId, productId),
|
|
176
|
+
{ enabled: false }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const saveProductMutation = createMutation(saveProduct);
|
|
180
|
+
|
|
181
|
+
await saveProductMutation.mutate({
|
|
182
|
+
applicationId,
|
|
183
|
+
productId,
|
|
184
|
+
productName: 'New title',
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
In this shape the mutation does not need `resolveDependencies`, because the paired helper already knows which dependency keys to read from the mutation variables.
|
|
189
|
+
|
|
190
|
+
### Default `intersection` Matching
|
|
191
|
+
|
|
192
|
+
`intersection` is the default. A mutation invalidates only queries that match all provided dependency pairs:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
196
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
197
|
+
|
|
198
|
+
const queryClient = new QueryClient();
|
|
199
|
+
const manager = setupQueryManager(queryClient);
|
|
200
|
+
|
|
201
|
+
const fetchProduct = async (applicationId: string, productId: string) => ({
|
|
202
|
+
applicationId,
|
|
203
|
+
name: 'Ada',
|
|
204
|
+
productId,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const saveProduct = async (variables: {
|
|
208
|
+
applicationId: string;
|
|
209
|
+
productId: string;
|
|
210
|
+
productName: string;
|
|
211
|
+
}) => variables;
|
|
212
|
+
|
|
213
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
214
|
+
'applicationId',
|
|
215
|
+
'productId',
|
|
216
|
+
] as const);
|
|
217
|
+
|
|
218
|
+
createQuery(
|
|
219
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
220
|
+
() => fetchProduct('app-1', 'product-1')
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
createQuery(
|
|
224
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-2' }, view: { page: 1 } }],
|
|
225
|
+
() => fetchProduct('app-1', 'product-2')
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const renameProduct = createMutation(saveProduct);
|
|
229
|
+
|
|
230
|
+
await renameProduct.mutate({
|
|
231
|
+
applicationId: 'app-1',
|
|
232
|
+
productId: 'product-1',
|
|
233
|
+
productName: 'Ada',
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Only the `app-1` / `product-1` query is invalidated.
|
|
238
|
+
|
|
239
|
+
### `union` Matching
|
|
240
|
+
|
|
241
|
+
Use `matchMode: 'union'` when a mutation should invalidate anything that matches any provided dependency pair:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
245
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
246
|
+
|
|
247
|
+
const queryClient = new QueryClient();
|
|
248
|
+
const manager = setupQueryManager(queryClient);
|
|
249
|
+
|
|
250
|
+
const fetchProduct = async (applicationId: string, productId: string) => ({
|
|
251
|
+
applicationId,
|
|
252
|
+
name: 'Ada',
|
|
253
|
+
productId,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const syncProductData = async (variables: {
|
|
257
|
+
applicationId: string;
|
|
258
|
+
productId: string;
|
|
259
|
+
}) => variables;
|
|
260
|
+
|
|
261
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
262
|
+
'applicationId',
|
|
263
|
+
'productId',
|
|
264
|
+
] as const);
|
|
265
|
+
|
|
266
|
+
createQuery(
|
|
267
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
268
|
+
() => fetchProduct('app-1', 'product-1')
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
createQuery(
|
|
272
|
+
['product', { deps: { applicationId: 'app-2', productId: 'product-1' }, view: { page: 1 } }],
|
|
273
|
+
() => fetchProduct('app-2', 'product-1')
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const syncProduct = createMutation(syncProductData, {
|
|
277
|
+
matchMode: 'union',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await syncProduct.mutate({
|
|
281
|
+
applicationId: 'app-1',
|
|
282
|
+
productId: 'product-1',
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
This invalidates tracked queries that match:
|
|
287
|
+
|
|
288
|
+
- `applicationId === 'app-1'`
|
|
289
|
+
- or `productId === 'product-1'`
|
|
290
|
+
|
|
291
|
+
Use it when a mutation affects a wider slice of cached state and exact intersection would be too narrow.
|
|
292
|
+
|
|
293
|
+
### Partial Dependency Invalidation
|
|
294
|
+
|
|
295
|
+
Tracked mutations may resolve only some dependency keys:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
299
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
300
|
+
|
|
301
|
+
const queryClient = new QueryClient();
|
|
302
|
+
const manager = setupQueryManager(queryClient);
|
|
303
|
+
|
|
304
|
+
const syncApplicationProducts = async (variables: { applicationId: string }) => variables;
|
|
305
|
+
|
|
306
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
307
|
+
'applicationId',
|
|
308
|
+
'productId',
|
|
309
|
+
] as const);
|
|
310
|
+
|
|
311
|
+
createQuery(
|
|
312
|
+
['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
|
|
313
|
+
async () => ({ applicationId: 'app-1', productId: 'product-1' })
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const refreshApplicationProducts = createMutation(syncApplicationProducts);
|
|
317
|
+
|
|
318
|
+
await refreshApplicationProducts.mutate({
|
|
319
|
+
applicationId: 'app-1',
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
This invalidates all tracked queries that match `applicationId === 'app-1'`, regardless of `productId`.
|
|
324
|
+
|
|
325
|
+
### Lifecycle Timing
|
|
326
|
+
|
|
327
|
+
Automatic invalidation runs on success by default. Change `invalidateOn` when the mutation workflow needs different timing:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
331
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
332
|
+
|
|
333
|
+
const queryClient = new QueryClient();
|
|
334
|
+
const manager = setupQueryManager(queryClient);
|
|
57
335
|
|
|
58
|
-
const
|
|
59
|
-
await updateUser.mutate({ id: 42 });
|
|
336
|
+
const removeProduct = async (variables: { applicationId: string }) => variables;
|
|
60
337
|
|
|
61
|
-
|
|
62
|
-
|
|
338
|
+
const [createQuery, createMutation] = manager.createQueryAndMutation([
|
|
339
|
+
'applicationId',
|
|
340
|
+
] as const);
|
|
341
|
+
|
|
342
|
+
createQuery(
|
|
343
|
+
['product-list', { deps: { applicationId: 'app-1' }, view: { page: 1 } }],
|
|
344
|
+
async () => [{ applicationId: 'app-1', productId: 'product-1' }]
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const cleanupMutation = createMutation(removeProduct, {
|
|
348
|
+
invalidateOn: 'settled',
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Supported values:
|
|
353
|
+
|
|
354
|
+
- `'success'` invalidates only after a successful mutation
|
|
355
|
+
- `'error'` invalidates only after a failed mutation
|
|
356
|
+
- `'settled'` invalidates after either outcome
|
|
357
|
+
|
|
358
|
+
### Custom Dependency Resolution
|
|
359
|
+
|
|
360
|
+
Use `resolveDependencies` when mutation variables do not expose the tracked keys directly:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
364
|
+
import { setupQueryManager } from '@veams/status-quo-query';
|
|
365
|
+
|
|
366
|
+
const queryClient = new QueryClient();
|
|
367
|
+
const manager = setupQueryManager(queryClient);
|
|
368
|
+
|
|
369
|
+
const saveProduct = async (variables: {
|
|
370
|
+
payload: { applicationId: string };
|
|
371
|
+
product: { id: string };
|
|
372
|
+
productName: string;
|
|
373
|
+
}) => variables;
|
|
374
|
+
|
|
375
|
+
const nestedMutation = manager.createMutation(saveProduct, {
|
|
376
|
+
resolveDependencies: (variables: {
|
|
377
|
+
payload: { applicationId: string };
|
|
378
|
+
product: { id: string };
|
|
379
|
+
}) => ({
|
|
380
|
+
applicationId: variables.payload.applicationId,
|
|
381
|
+
productId: variables.product.id,
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await nestedMutation.mutate({
|
|
386
|
+
payload: { applicationId: 'app-1' },
|
|
387
|
+
product: { id: 'product-1' },
|
|
388
|
+
});
|
|
63
389
|
```
|
|
64
390
|
|
|
391
|
+
Standalone tracked mutations need either:
|
|
392
|
+
|
|
393
|
+
- `dependencyKeys`
|
|
394
|
+
- or `resolveDependencies`
|
|
395
|
+
|
|
396
|
+
## FAQ
|
|
397
|
+
|
|
398
|
+
### Is `view` still part of the TanStack cache key?
|
|
399
|
+
|
|
400
|
+
Yes. TanStack uses the full query key for cache identity, so these are different cache entries:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
['products', { deps: { applicationId: 'app-1' }, view: { page: 1 } }]
|
|
404
|
+
['products', { deps: { applicationId: 'app-1' }, view: { page: 2 } }]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
That means `view` still matters for pagination, sorting, filtering, and any other cache variant you want TanStack to separate.
|
|
408
|
+
|
|
409
|
+
### Why does tracked invalidation ignore `view`?
|
|
410
|
+
|
|
411
|
+
Because `view` usually describes how the same domain data is presented, not what domain entity the query depends on.
|
|
412
|
+
|
|
413
|
+
If tracked invalidation matched on `view`, mutations would often miss related cache entries:
|
|
414
|
+
|
|
415
|
+
- renaming one product can change alphabetical sort order
|
|
416
|
+
- creating or deleting one product can shift pagination boundaries
|
|
417
|
+
- changing one record can affect multiple filtered views
|
|
418
|
+
|
|
419
|
+
Keeping `deps` for invalidation and `view` for cache partitioning makes that distinction explicit.
|
|
420
|
+
|
|
421
|
+
### What if I need to invalidate page 2 but not page 1?
|
|
422
|
+
|
|
423
|
+
That is a valid case, but it is intentionally not the default tracked behavior.
|
|
424
|
+
|
|
425
|
+
Use one of these options:
|
|
426
|
+
|
|
427
|
+
- call `query.invalidate()` on the exact query handle you want to refresh
|
|
428
|
+
- call `manager.invalidateQueries({ queryKey, exact: true })` with the specific key
|
|
429
|
+
- move `page` from `view` into `deps` only if page is truly part of the invalidation semantics in your domain
|
|
430
|
+
|
|
431
|
+
If page-specific invalidation is an exception, manual exact invalidation is usually the better choice.
|
|
432
|
+
|
|
433
|
+
### When is broad tracked invalidation the right tradeoff?
|
|
434
|
+
|
|
435
|
+
It is usually correct when one mutation can affect multiple views of the same domain slice:
|
|
436
|
+
|
|
437
|
+
- product creation can change counts and membership across multiple pages
|
|
438
|
+
- deletion can pull later items forward into earlier pages
|
|
439
|
+
- renaming can move items between sorted pages
|
|
440
|
+
- updates can move records in or out of filtered lists
|
|
441
|
+
|
|
65
442
|
## API
|
|
66
443
|
|
|
67
444
|
### `setupQueryManager(queryClient)`
|
|
@@ -72,6 +449,9 @@ Returns `QueryManager` with:
|
|
|
72
449
|
|
|
73
450
|
- `createQuery(queryKey, queryFn, options?)`
|
|
74
451
|
- `createMutation(mutationFn, options?)`
|
|
452
|
+
- `createQueryAndMutation(dependencyKeys)`
|
|
453
|
+
- `createUntrackedQuery(queryKey, queryFn, options?)`
|
|
454
|
+
- `createUntrackedMutation(mutationFn, options?)`
|
|
75
455
|
- `cancelQueries(...)`
|
|
76
456
|
- `getQueryData(...)`
|
|
77
457
|
- `invalidateQueries(...)`
|
|
@@ -83,13 +463,56 @@ Returns `QueryManager` with:
|
|
|
83
463
|
|
|
84
464
|
All manager methods forward directly to the corresponding `QueryClient` methods. `unsafe_getClient()` returns the raw TanStack client as an explicit escape hatch.
|
|
85
465
|
|
|
466
|
+
### Tracked Queries and Mutations
|
|
467
|
+
|
|
468
|
+
Tracked queries embed dependency metadata into the final query-key segment:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
['products', { deps: { applicationId, productId }, view: { page, sort } }]
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Only `deps` participates in automatic invalidation tracking. `view` is optional and is treated as normal query-key data.
|
|
475
|
+
|
|
476
|
+
`createQuery(queryKey, queryFn, options?)` returns the same `QueryService<TData, TError>` shape as `createUntrackedQuery(...)`, but it registers the query hash under every `deps` entry and re-registers on `refetch()` or the first `subscribe(...)` if TanStack has removed the cache entry in the meantime.
|
|
477
|
+
|
|
478
|
+
`createMutation(mutationFn, options?)` returns the same `MutationService<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
|
|
479
|
+
|
|
480
|
+
- `dependencyKeys?`
|
|
481
|
+
- `resolveDependencies?`
|
|
482
|
+
- `invalidateOn?` with `'success' | 'error' | 'settled'`
|
|
483
|
+
- `matchMode?` with `'intersection' | 'union'`
|
|
484
|
+
|
|
485
|
+
Standalone tracked mutations need either `dependencyKeys` or `resolveDependencies`.
|
|
486
|
+
|
|
487
|
+
`createQueryAndMutation(dependencyKeys)` captures dependency keys once and returns:
|
|
488
|
+
|
|
489
|
+
- the tracked `createQuery` factory
|
|
490
|
+
- a tracked `createMutation` factory whose default resolver reads `variables[dependencyKey]`
|
|
491
|
+
|
|
492
|
+
Use `resolveDependencies` when the mutation variables do not expose the tracked dependency fields directly.
|
|
493
|
+
|
|
494
|
+
### `createQueryAndMutation(dependencyKeys)`
|
|
495
|
+
|
|
496
|
+
Captures dependency names once and returns:
|
|
497
|
+
|
|
498
|
+
- the tracked query factory
|
|
499
|
+
- a tracked mutation factory whose default resolver reads `variables[dependencyKey]`
|
|
500
|
+
|
|
501
|
+
The tracked query factory still expects a query key with a final `{ deps, view? }` segment. The tracked mutation factory keeps the same `MutationService` shape as `createMutation(...)`, but no longer needs `dependencyKeys` repeated in each call.
|
|
502
|
+
|
|
503
|
+
Reach for standalone `createMutation(...)` when:
|
|
504
|
+
|
|
505
|
+
- query and mutation do not share one dependency-key list
|
|
506
|
+
- mutation variables need a custom `resolveDependencies(...)`
|
|
507
|
+
- you want one tracked mutation without pairing it to one tracked query workflow
|
|
508
|
+
|
|
86
509
|
### `setupQuery(queryClient)`
|
|
87
510
|
|
|
88
|
-
Creates a `
|
|
511
|
+
Creates a `createUntrackedQuery` factory bound to a `QueryClient`.
|
|
89
512
|
|
|
90
|
-
`
|
|
513
|
+
`createUntrackedQuery(queryKey, queryFn, options?)` returns `QueryService<TData, TError>`.
|
|
91
514
|
|
|
92
|
-
`QueryServiceOptions` is based on TanStack `QueryObserverOptions`, without `queryKey` and `queryFn` because those are provided directly to `
|
|
515
|
+
`QueryServiceOptions` is based on TanStack `QueryObserverOptions`, without `queryKey` and `queryFn` because those are provided directly to `createUntrackedQuery`.
|
|
93
516
|
|
|
94
517
|
`QueryService` methods:
|
|
95
518
|
|
|
@@ -120,11 +543,11 @@ Creates a `createQuery` factory bound to a `QueryClient`.
|
|
|
120
543
|
|
|
121
544
|
### `setupMutation(queryClient)`
|
|
122
545
|
|
|
123
|
-
Creates a `
|
|
546
|
+
Creates a `createUntrackedMutation` factory bound to a `QueryClient`.
|
|
124
547
|
|
|
125
|
-
`
|
|
548
|
+
`createUntrackedMutation(mutationFn, options?)` returns `MutationService<TData, TError, TVariables, TOnMutateResult>`.
|
|
126
549
|
|
|
127
|
-
`MutationServiceOptions` is based on TanStack `MutationObserverOptions`, without `mutationFn` because it is provided directly to `
|
|
550
|
+
`MutationServiceOptions` is based on TanStack `MutationObserverOptions`, without `mutationFn` because it is provided directly to `createUntrackedMutation`.
|
|
128
551
|
|
|
129
552
|
`MutationService` methods:
|
|
130
553
|
|
|
@@ -171,3 +594,4 @@ Creates a `createMutation` factory bound to a `QueryClient`.
|
|
|
171
594
|
- Commands live on the handle itself: `refetch`, `invalidate`, `mutate`, `reset`.
|
|
172
595
|
- Raw TanStack observer and client access is explicit through `unsafe_getResult()` and `unsafe_getClient()`.
|
|
173
596
|
- Manager-level operations live on `setupQueryManager()`, not on individual snapshots.
|
|
597
|
+
- Tracked invalidation is manager-only because the registry must be shared across query and mutation handles.
|
package/dist/index.d.ts
CHANGED
package/dist/mutation.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MutationFunction, type MutateOptions, type MutationObserverOptions, type MutationObserverResult, type MutationStatus as TanstackMutationStatus, type QueryClient } from '@tanstack/query-core';
|
|
2
|
+
import { type TrackedDependencyRecord, type TrackingRegistry, type TrackedInvalidateOn, type TrackedMatchMode } from './tracking';
|
|
2
3
|
export type MutationStatus = TanstackMutationStatus;
|
|
3
4
|
/**
|
|
4
5
|
* Represents a stable snapshot of the mutation service's state.
|
|
@@ -28,12 +29,39 @@ export interface MutationService<TData = unknown, TError = Error, TVariables = v
|
|
|
28
29
|
*/
|
|
29
30
|
export type MutationServiceOptions<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> = Omit<MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>, 'mutationFn'>;
|
|
30
31
|
/**
|
|
31
|
-
* Function signature for the mutation factory.
|
|
32
|
+
* Function signature for the untracked mutation factory.
|
|
32
33
|
*/
|
|
33
|
-
export interface
|
|
34
|
+
export interface CreateUntrackedMutation {
|
|
34
35
|
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: MutationServiceOptions<TData, TError, TVariables, TOnMutateResult>): MutationService<TData, TError, TVariables, TOnMutateResult>;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Additional options for tracked mutations that invalidate queries automatically.
|
|
39
|
+
*
|
|
40
|
+
* The tracked mutation still behaves like a normal mutation service from the outside. These
|
|
41
|
+
* options only describe how the facade should derive dependency values and when it should
|
|
42
|
+
* invalidate matching tracked queries after the mutation lifecycle settles.
|
|
43
|
+
*/
|
|
44
|
+
export interface TrackedMutationServiceOptions<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> extends MutationServiceOptions<TData, TError, TVariables, TOnMutateResult> {
|
|
45
|
+
dependencyKeys?: readonly (keyof TDeps & string)[];
|
|
46
|
+
resolveDependencies?: (variables: TVariables) => Partial<TDeps>;
|
|
47
|
+
invalidateOn?: TrackedInvalidateOn;
|
|
48
|
+
matchMode?: TrackedMatchMode;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Function signature for the default mutation factory with automatic invalidation.
|
|
52
|
+
*/
|
|
53
|
+
export interface CreateMutation {
|
|
54
|
+
<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: TrackedMutationServiceOptions<TDeps, TData, TError, TVariables, TOnMutateResult>): MutationService<TData, TError, TVariables, TOnMutateResult>;
|
|
55
|
+
}
|
|
36
56
|
/**
|
|
37
57
|
* Prepares the mutation factory by binding it to a specific QueryClient instance.
|
|
38
58
|
*/
|
|
39
|
-
export declare function setupMutation(queryClient: QueryClient):
|
|
59
|
+
export declare function setupMutation(queryClient: QueryClient): CreateUntrackedMutation;
|
|
60
|
+
/**
|
|
61
|
+
* Prepares the default mutation factory that coordinates invalidation through the shared registry.
|
|
62
|
+
*
|
|
63
|
+
* The implementation intentionally wraps the normal mutation service instead of re-implementing
|
|
64
|
+
* TanStack lifecycle behavior. TanStack still owns retries, callbacks, and state transitions;
|
|
65
|
+
* the facade only adds dependency resolution plus the follow-up invalidation pass.
|
|
66
|
+
*/
|
|
67
|
+
export declare function setupTrackedMutation(queryClient: QueryClient, trackingRegistry: TrackingRegistry, defaultDependencyKeys?: readonly string[]): CreateMutation;
|