@veams/status-quo-query 0.4.0 → 0.5.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 CHANGED
@@ -23,14 +23,25 @@ Root exports:
23
23
  - `QueryManager`
24
24
  - `CreateQuery`
25
25
  - `CreateMutation`
26
+ - `CreateTrackedQuery`
27
+ - `CreateTrackedMutation`
28
+ - `CreateTrackedQueryAndMutation`
29
+ - `CreateTrackedMutationWithDefaults`
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,334 @@ 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';
62
+
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 [createTrackedQuery, createTrackedMutation] = manager.createTrackedQueryAndMutation([
79
+ 'applicationId',
80
+ 'productId',
81
+ ] as const);
82
+
83
+ const productQuery = createTrackedQuery(
84
+ ['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
85
+ () => fetchProduct('app-1', 'product-1'),
86
+ {
87
+ enabled: false,
88
+ }
89
+ );
90
+
91
+ const updateProduct = createTrackedMutation(saveProduct, {
92
+ invalidateOn: 'success',
93
+ });
94
+
95
+ await productQuery.refetch();
96
+ await updateProduct.mutate({
97
+ applicationId: 'app-1',
98
+ productId: 'product-1',
99
+ productName: 'Ada',
100
+ });
51
101
 
52
102
  const userQuery = manager.createQuery(['user', 42], () => fetchUser(42), {
53
103
  enabled: false,
54
104
  });
55
105
  await userQuery.refetch();
56
106
  await userQuery.invalidate({ refetchType: 'none' });
107
+ ```
108
+
109
+ ## Why Tracked Invalidation
110
+
111
+ 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:
112
+
113
+ - queries declare their domain dependencies once in `queryKey[..., { deps, view }]`
114
+ - tracked mutations resolve the same dependency names from their variables
115
+ - the manager invalidates matching queries automatically
116
+
117
+ That changes the developer workflow from "remember which cache keys this mutation affects" to "describe which domain entities this query and mutation belong to".
118
+
119
+ Benefits:
120
+
121
+ - less manual cache invalidation code spread across features
122
+ - lower risk of stale UI because one dependent query was forgotten
123
+ - clearer separation between invalidation semantics in `deps` and UI variants in `view`
124
+ - typed paired helpers that remove repeated dependency mapping in the common case
125
+
126
+ ## Examples
127
+
128
+ Tracked query keys use a final object segment:
57
129
 
58
- const updateUser = manager.createMutation((payload: UpdateUserPayload) => saveUser(payload));
59
- await updateUser.mutate({ id: 42 });
130
+ ```ts
131
+ ['products', { deps: { applicationId, productId }, view: { page, sort } }]
132
+ ```
133
+
134
+ Rules:
135
+
136
+ - `deps` is required for tracked queries
137
+ - only `deps` is used for invalidation matching
138
+ - `view` is optional and recommended for pagination, sorting, filtering, and other cache variants
139
+ - tracked invalidation is manager-only because queries and mutations need one shared registry
140
+
141
+ ### Paired Helper With Default Dependency Resolution
142
+
143
+ Use the paired helper when mutation variables already expose the dependency keys directly:
144
+
145
+ ```ts
146
+ import { QueryClient } from '@tanstack/query-core';
147
+ import { setupQueryManager } from '@veams/status-quo-query';
60
148
 
61
- await manager.invalidateQueries({ queryKey: ['user'] });
62
- manager.setQueryData(['user', 42], (current) => current);
149
+ const queryClient = new QueryClient();
150
+ const manager = setupQueryManager(queryClient);
151
+ const applicationId = 'app-1';
152
+ const productId = 'product-1';
153
+
154
+ const fetchProduct = async (currentApplicationId: string, currentProductId: string) => ({
155
+ applicationId: currentApplicationId,
156
+ name: 'Ada',
157
+ productId: currentProductId,
158
+ });
159
+
160
+ const saveProduct = async (variables: {
161
+ applicationId: string;
162
+ productId: string;
163
+ productName: string;
164
+ }) => variables;
165
+
166
+ const [createTrackedQuery, createTrackedMutation] = manager.createTrackedQueryAndMutation([
167
+ 'applicationId',
168
+ 'productId',
169
+ ] as const);
170
+
171
+ const productQuery = createTrackedQuery(
172
+ ['product', { deps: { applicationId, productId }, view: { page: 1 } }],
173
+ () => fetchProduct(applicationId, productId),
174
+ { enabled: false }
175
+ );
176
+
177
+ const saveProductMutation = createTrackedMutation(saveProduct);
178
+
179
+ await saveProductMutation.mutate({
180
+ applicationId,
181
+ productId,
182
+ productName: 'New title',
183
+ });
63
184
  ```
64
185
 
186
+ In this shape the mutation does not need `resolveDependencies`, because the paired helper already knows which dependency keys to read from the mutation variables.
187
+
188
+ ### Default `intersection` Matching
189
+
190
+ `intersection` is the default. A mutation invalidates only queries that match all provided dependency pairs:
191
+
192
+ ```ts
193
+ import { QueryClient } from '@tanstack/query-core';
194
+ import { setupQueryManager } from '@veams/status-quo-query';
195
+
196
+ const queryClient = new QueryClient();
197
+ const manager = setupQueryManager(queryClient);
198
+
199
+ const fetchProduct = async (applicationId: string, productId: string) => ({
200
+ applicationId,
201
+ name: 'Ada',
202
+ productId,
203
+ });
204
+
205
+ const saveProduct = async (variables: {
206
+ applicationId: string;
207
+ productId: string;
208
+ productName: string;
209
+ }) => variables;
210
+
211
+ const [createTrackedQuery, createTrackedMutation] = manager.createTrackedQueryAndMutation([
212
+ 'applicationId',
213
+ 'productId',
214
+ ] as const);
215
+
216
+ createTrackedQuery(
217
+ ['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
218
+ () => fetchProduct('app-1', 'product-1')
219
+ );
220
+
221
+ createTrackedQuery(
222
+ ['product', { deps: { applicationId: 'app-1', productId: 'product-2' }, view: { page: 1 } }],
223
+ () => fetchProduct('app-1', 'product-2')
224
+ );
225
+
226
+ const renameProduct = createTrackedMutation(saveProduct);
227
+
228
+ await renameProduct.mutate({
229
+ applicationId: 'app-1',
230
+ productId: 'product-1',
231
+ productName: 'Ada',
232
+ });
233
+ ```
234
+
235
+ Only the `app-1` / `product-1` query is invalidated.
236
+
237
+ ### `union` Matching
238
+
239
+ Use `matchMode: 'union'` when a mutation should invalidate anything that matches any provided dependency pair:
240
+
241
+ ```ts
242
+ import { QueryClient } from '@tanstack/query-core';
243
+ import { setupQueryManager } from '@veams/status-quo-query';
244
+
245
+ const queryClient = new QueryClient();
246
+ const manager = setupQueryManager(queryClient);
247
+
248
+ const fetchProduct = async (applicationId: string, productId: string) => ({
249
+ applicationId,
250
+ name: 'Ada',
251
+ productId,
252
+ });
253
+
254
+ const syncProductData = async (variables: {
255
+ applicationId: string;
256
+ productId: string;
257
+ }) => variables;
258
+
259
+ const [createTrackedQuery, createTrackedMutation] = manager.createTrackedQueryAndMutation([
260
+ 'applicationId',
261
+ 'productId',
262
+ ] as const);
263
+
264
+ createTrackedQuery(
265
+ ['product', { deps: { applicationId: 'app-1', productId: 'product-1' }, view: { page: 1 } }],
266
+ () => fetchProduct('app-1', 'product-1')
267
+ );
268
+
269
+ createTrackedQuery(
270
+ ['product', { deps: { applicationId: 'app-2', productId: 'product-1' }, view: { page: 1 } }],
271
+ () => fetchProduct('app-2', 'product-1')
272
+ );
273
+
274
+ const syncProduct = createTrackedMutation(syncProductData, {
275
+ matchMode: 'union',
276
+ });
277
+
278
+ await syncProduct.mutate({
279
+ applicationId: 'app-1',
280
+ productId: 'product-1',
281
+ });
282
+ ```
283
+
284
+ This invalidates tracked queries that match:
285
+
286
+ - `applicationId === 'app-1'`
287
+ - or `productId === 'product-1'`
288
+
289
+ Use it when a mutation affects a wider slice of cached state and exact intersection would be too narrow.
290
+
291
+ ### Partial Dependency Invalidation
292
+
293
+ Tracked mutations may resolve only some dependency keys:
294
+
295
+ ```ts
296
+ import { QueryClient } from '@tanstack/query-core';
297
+ import { setupQueryManager } from '@veams/status-quo-query';
298
+
299
+ const queryClient = new QueryClient();
300
+ const manager = setupQueryManager(queryClient);
301
+
302
+ const syncApplicationProducts = async (variables: { applicationId: string }) => variables;
303
+
304
+ const [createTrackedQuery, createTrackedMutation] = manager.createTrackedQueryAndMutation([
305
+ 'applicationId',
306
+ 'productId',
307
+ ] as const);
308
+
309
+ const refreshApplicationProducts = createTrackedMutation(syncApplicationProducts);
310
+
311
+ await refreshApplicationProducts.mutate({
312
+ applicationId: 'app-1',
313
+ });
314
+ ```
315
+
316
+ This invalidates all tracked queries that match `applicationId === 'app-1'`, regardless of `productId`.
317
+
318
+ ### Lifecycle Timing
319
+
320
+ Automatic invalidation runs on success by default. Change `invalidateOn` when the mutation workflow needs different timing:
321
+
322
+ ```ts
323
+ import { QueryClient } from '@tanstack/query-core';
324
+ import { setupQueryManager } from '@veams/status-quo-query';
325
+
326
+ const queryClient = new QueryClient();
327
+ const manager = setupQueryManager(queryClient);
328
+
329
+ const removeProduct = async (variables: { applicationId: string }) => variables;
330
+
331
+ const [createTrackedQuery, createTrackedMutation] = manager.createTrackedQueryAndMutation([
332
+ 'applicationId',
333
+ ] as const);
334
+
335
+ const cleanupMutation = createTrackedMutation(removeProduct, {
336
+ invalidateOn: 'settled',
337
+ });
338
+ ```
339
+
340
+ Supported values:
341
+
342
+ - `'success'` invalidates only after a successful mutation
343
+ - `'error'` invalidates only after a failed mutation
344
+ - `'settled'` invalidates after either outcome
345
+
346
+ ### Custom Dependency Resolution
347
+
348
+ Use `resolveDependencies` when mutation variables do not expose the tracked keys directly:
349
+
350
+ ```ts
351
+ import { QueryClient } from '@tanstack/query-core';
352
+ import { setupQueryManager } from '@veams/status-quo-query';
353
+
354
+ const queryClient = new QueryClient();
355
+ const manager = setupQueryManager(queryClient);
356
+
357
+ const saveProduct = async (variables: {
358
+ payload: { applicationId: string };
359
+ product: { id: string };
360
+ productName: string;
361
+ }) => variables;
362
+
363
+ const nestedMutation = manager.createTrackedMutation(saveProduct, {
364
+ resolveDependencies: (variables: {
365
+ payload: { applicationId: string };
366
+ product: { id: string };
367
+ }) => ({
368
+ applicationId: variables.payload.applicationId,
369
+ productId: variables.product.id,
370
+ }),
371
+ });
372
+
373
+ await nestedMutation.mutate({
374
+ payload: { applicationId: 'app-1' },
375
+ product: { id: 'product-1' },
376
+ });
377
+ ```
378
+
379
+ Standalone tracked mutations need either:
380
+
381
+ - `dependencyKeys`
382
+ - or `resolveDependencies`
383
+
65
384
  ## API
66
385
 
67
386
  ### `setupQueryManager(queryClient)`
@@ -72,6 +391,9 @@ Returns `QueryManager` with:
72
391
 
73
392
  - `createQuery(queryKey, queryFn, options?)`
74
393
  - `createMutation(mutationFn, options?)`
394
+ - `createTrackedQuery(queryKey, queryFn, options?)`
395
+ - `createTrackedMutation(mutationFn, options?)`
396
+ - `createTrackedQueryAndMutation(dependencyKeys)`
75
397
  - `cancelQueries(...)`
76
398
  - `getQueryData(...)`
77
399
  - `invalidateQueries(...)`
@@ -83,6 +405,49 @@ Returns `QueryManager` with:
83
405
 
84
406
  All manager methods forward directly to the corresponding `QueryClient` methods. `unsafe_getClient()` returns the raw TanStack client as an explicit escape hatch.
85
407
 
408
+ ### Tracked Queries and Mutations
409
+
410
+ Tracked queries embed dependency metadata into the final query-key segment:
411
+
412
+ ```ts
413
+ ['products', { deps: { applicationId, productId }, view: { page, sort } }]
414
+ ```
415
+
416
+ Only `deps` participates in automatic invalidation tracking. `view` is optional and is treated as normal query-key data.
417
+
418
+ `createTrackedQuery(queryKey, queryFn, options?)` returns the same `QueryService<TData, TError>` shape as `createQuery(...)`, 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.
419
+
420
+ `createTrackedMutation(mutationFn, options?)` returns the same `MutationService<TData, TError, TVariables, TOnMutateResult>` shape as `createMutation(...)`, but adds:
421
+
422
+ - `dependencyKeys?`
423
+ - `resolveDependencies?`
424
+ - `invalidateOn?` with `'success' | 'error' | 'settled'`
425
+ - `matchMode?` with `'intersection' | 'union'`
426
+
427
+ Standalone tracked mutations need either `dependencyKeys` or `resolveDependencies`.
428
+
429
+ `createTrackedQueryAndMutation(dependencyKeys)` captures dependency keys once and returns:
430
+
431
+ - the tracked query factory
432
+ - a tracked mutation factory whose default resolver reads `variables[dependencyKey]`
433
+
434
+ Use `resolveDependencies` when the mutation variables do not expose the tracked dependency fields directly.
435
+
436
+ ### `createTrackedQueryAndMutation(dependencyKeys)`
437
+
438
+ Captures dependency names once and returns:
439
+
440
+ - the tracked query factory
441
+ - a tracked mutation factory whose default resolver reads `variables[dependencyKey]`
442
+
443
+ 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.
444
+
445
+ Reach for standalone `createTrackedMutation(...)` when:
446
+
447
+ - query and mutation do not share one dependency-key list
448
+ - mutation variables need a custom `resolveDependencies(...)`
449
+ - you want one tracked mutation without pairing it to one tracked query workflow
450
+
86
451
  ### `setupQuery(queryClient)`
87
452
 
88
453
  Creates a `createQuery` factory bound to a `QueryClient`.
@@ -171,3 +536,4 @@ Creates a `createMutation` factory bound to a `QueryClient`.
171
536
  - Commands live on the handle itself: `refetch`, `invalidate`, `mutate`, `reset`.
172
537
  - Raw TanStack observer and client access is explicit through `unsafe_getResult()` and `unsafe_getClient()`.
173
538
  - Manager-level operations live on `setupQueryManager()`, not on individual snapshots.
539
+ - Tracked invalidation is manager-only because the registry must be shared across query and mutation handles.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './mutation';
2
2
  export * from './query';
3
3
  export * from './provider';
4
+ export type { TrackedDependencyRecord, TrackedDependencyValue, TrackedInvalidateOn, TrackedMatchMode, TrackedQueryKey, TrackedQueryKeySegment, } from './tracking';
@@ -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.
@@ -33,7 +34,34 @@ export type MutationServiceOptions<TData = unknown, TError = Error, TVariables =
33
34
  export interface CreateMutation {
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 tracked mutation factories.
52
+ */
53
+ export interface CreateTrackedMutation {
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
59
  export declare function setupMutation(queryClient: QueryClient): CreateMutation;
60
+ /**
61
+ * Prepares a tracked 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[]): CreateTrackedMutation;
package/dist/mutation.js CHANGED
@@ -1,31 +1,70 @@
1
1
  import {
2
2
  // Import MutationObserver to observe and manage mutations.
3
3
  MutationObserver, } from '@tanstack/query-core';
4
+ import { pickTrackedDependencies, resolveTrackedQueries, toTrackedDependencyEntries, } from './tracking';
4
5
  /**
5
6
  * Prepares the mutation factory by binding it to a specific QueryClient instance.
6
7
  */
7
8
  export function setupMutation(queryClient) {
8
9
  // Returns the actual factory function for creating individual mutation services.
9
10
  return function createMutation(mutationFn, options) {
10
- // Create a new MutationObserver instance to manage this specific mutation's lifecycle.
11
- const observer = new MutationObserver(queryClient, {
12
- ...options,
13
- mutationFn,
14
- });
15
- // Return the implementation of the MutationService interface.
11
+ return createMutationService(queryClient, mutationFn, options);
12
+ };
13
+ }
14
+ /**
15
+ * Prepares a tracked mutation factory that coordinates invalidation through the shared registry.
16
+ *
17
+ * The implementation intentionally wraps the normal mutation service instead of re-implementing
18
+ * TanStack lifecycle behavior. TanStack still owns retries, callbacks, and state transitions;
19
+ * the facade only adds dependency resolution plus the follow-up invalidation pass.
20
+ */
21
+ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDependencyKeys) {
22
+ return function createTrackedMutation(mutationFn, options) {
23
+ // Split tracked-only options from the underlying TanStack mutation observer options.
24
+ const { dependencyKeys, invalidateOn = 'success', matchMode = 'intersection', resolveDependencies, ...mutationOptions } = options ?? {};
25
+ // Reuse the normal mutation service so snapshots and subscription behavior stay identical.
26
+ const service = createMutationService(queryClient, mutationFn, mutationOptions);
27
+ // The paired helper injects dependency keys here, while standalone tracked mutations can
28
+ // still provide them directly or bypass them with a custom resolver.
29
+ const resolvedDependencyKeys = (dependencyKeys ?? defaultDependencyKeys);
30
+ const invalidateTrackedQueries = async (variables) => {
31
+ // Resolve the mutation variables into the same named dependency shape that tracked queries
32
+ // registered under when they were created.
33
+ const dependencies = resolveTrackedMutationDependencies(variables, resolvedDependencyKeys, resolveDependencies);
34
+ // Ask the registry for matching query hashes using the selected invalidation breadth.
35
+ const queryHashes = trackingRegistry.match(toTrackedDependencyEntries(dependencies, 'Tracked mutation dependency resolution'), matchMode);
36
+ // Filter the registry result down to currently live TanStack queries before invalidating.
37
+ const queries = resolveTrackedQueries(queryClient, queryHashes);
38
+ await Promise.all(queries.map((query) => queryClient.invalidateQueries({
39
+ exact: true,
40
+ queryKey: query.queryKey,
41
+ })));
42
+ };
16
43
  return {
17
- // Map the current observer state to our service's snapshot format.
18
- getSnapshot: () => toMutationServiceSnapshot(observer.getCurrentResult()),
19
- // Subscribe to observer changes and notify the listener with updated snapshots.
20
- subscribe: (listener) => observer.subscribe((result) => {
21
- listener(toMutationServiceSnapshot(result));
22
- }),
23
- // Proxy the mutate call to the underlying observer.
24
- mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
25
- // Reset the underlying observer state.
26
- reset: () => observer.reset(),
27
- // Provide direct access to the raw observer result when needed.
28
- unsafe_getResult: () => observer.getCurrentResult(),
44
+ ...service,
45
+ mutate: async (variables, mutateOptions) => {
46
+ try {
47
+ // Let TanStack finish the mutation first so its own callbacks and state machine remain
48
+ // authoritative. The facade only coordinates the follow-up invalidation.
49
+ const result = await service.mutate(variables, mutateOptions);
50
+ if (invalidateOn === 'success' || invalidateOn === 'settled') {
51
+ await invalidateTrackedQueries(variables);
52
+ }
53
+ return result;
54
+ }
55
+ catch (error) {
56
+ if (invalidateOn === 'error' || invalidateOn === 'settled') {
57
+ try {
58
+ await invalidateTrackedQueries(variables);
59
+ }
60
+ catch {
61
+ // Preserve the original mutation failure as the primary rejection. If invalidation
62
+ // also fails here, the caller still sees the mutation error that triggered this path.
63
+ }
64
+ }
65
+ throw error;
66
+ }
67
+ },
29
68
  };
30
69
  };
31
70
  }
@@ -45,4 +84,33 @@ function toMutationServiceSnapshot(result) {
45
84
  isSuccess: result.isSuccess,
46
85
  };
47
86
  }
87
+ function createMutationService(queryClient, mutationFn, options) {
88
+ // Keep the original mutation implementation in one place so tracked and untracked mutations
89
+ // always expose the same observer-backed runtime behavior.
90
+ const observer = new MutationObserver(queryClient, {
91
+ ...options,
92
+ mutationFn,
93
+ });
94
+ return {
95
+ getSnapshot: () => toMutationServiceSnapshot(observer.getCurrentResult()),
96
+ subscribe: (listener) => observer.subscribe((result) => {
97
+ listener(toMutationServiceSnapshot(result));
98
+ }),
99
+ mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
100
+ reset: () => observer.reset(),
101
+ unsafe_getResult: () => observer.getCurrentResult(),
102
+ };
103
+ }
104
+ function resolveTrackedMutationDependencies(variables, dependencyKeys, resolveDependencies) {
105
+ // A custom resolver takes precedence because it can adapt nested payloads or renamed fields.
106
+ if (resolveDependencies) {
107
+ return resolveDependencies(variables);
108
+ }
109
+ // Without a custom resolver, tracked mutations need known dependency keys so the default
110
+ // variable picker knows which fields are invalidation-relevant.
111
+ if (!dependencyKeys || dependencyKeys.length === 0) {
112
+ throw new Error('Tracked mutations require resolveDependencies or dependencyKeys to derive invalidation targets.');
113
+ }
114
+ return pickTrackedDependencies(dependencyKeys, variables);
115
+ }
48
116
  //# sourceMappingURL=mutation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mutation.js","sourceRoot":"","sources":["../src/mutation.ts"],"names":[],"mappings":"AAAA,OAAO;AACL,2DAA2D;AAC3D,gBAAgB,GAajB,MAAM,sBAAsB,CAAC;AA2E9B;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,WAAwB;IACpD,iFAAiF;IACjF,OAAO,SAAS,cAAc,CAM5B,UAA+C,EAC/C,OAA4E;QAE5E,uFAAuF;QACvF,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CACnC,WAAW,EACX;YACE,GAAG,OAAO;YACV,UAAU;SACX,CACF,CAAC;QAEF,8DAA8D;QAC9D,OAAO;YACL,mEAAmE;YACnE,WAAW,EAAE,GAAG,EAAE,CAAC,yBAAyB,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YACzE,gFAAgF;YAChF,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CACtB,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;gBAC5B,QAAQ,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC,CAAC;YAC9C,CAAC,CAAC;YACJ,oDAAoD;YACpD,MAAM,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,CAAC;YAC/E,uCAAuC;YACvC,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC7B,gEAAgE;YAChE,gBAAgB,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,gBAAgB,EAAE;SACpD,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,yBAAyB,CAChC,MAA0E;IAE1E,uEAAuE;IACvE,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"mutation.js","sourceRoot":"","sources":["../src/mutation.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,31 @@
1
- import { type QueryClient } from '@tanstack/query-core';
2
- import { type CreateMutation } from './mutation';
3
- import { type CreateQuery } from './query';
1
+ import { type QueryClient, type MutationFunction } from '@tanstack/query-core';
2
+ import { type CreateMutation, type CreateTrackedMutation, type MutationService, type TrackedMutationServiceOptions } from './mutation';
3
+ import { type CreateQuery, type CreateTrackedQuery } from './query';
4
+ import { type TrackedDependencyValue } from './tracking';
5
+ /**
6
+ * Mutation factory returned by the paired tracked helper.
7
+ *
8
+ * This omits `dependencyKeys` on purpose because the paired helper already captured those keys
9
+ * once at setup time and injects them automatically for each tracked mutation it creates.
10
+ */
11
+ export interface CreateTrackedMutationWithDefaults<TDependencyKey extends string> {
12
+ <TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: Omit<TrackedMutationServiceOptions<Record<TDependencyKey, TrackedDependencyValue>, TData, TError, TVariables, TOnMutateResult>, 'dependencyKeys'>): MutationService<TData, TError, TVariables, TOnMutateResult>;
13
+ }
14
+ /**
15
+ * Paired tracked helper that captures dependency keys once for default mutation resolution.
16
+ */
17
+ export interface CreateTrackedQueryAndMutation {
18
+ <const TDependencyKeys extends readonly string[]>(dependencyKeys: TDependencyKeys): readonly [CreateTrackedQuery, CreateTrackedMutationWithDefaults<TDependencyKeys[number]>];
19
+ }
4
20
  /**
5
21
  * Defines the public API for the query manager facade.
6
22
  */
7
23
  export interface QueryManager {
8
24
  createMutation: CreateMutation;
9
25
  createQuery: CreateQuery;
26
+ createTrackedQuery: CreateTrackedQuery;
27
+ createTrackedMutation: CreateTrackedMutation;
28
+ createTrackedQueryAndMutation: CreateTrackedQueryAndMutation;
10
29
  cancelQueries: QueryClient['cancelQueries'];
11
30
  getQueryData: QueryClient['getQueryData'];
12
31
  invalidateQueries: QueryClient['invalidateQueries'];