@veams/status-quo-query 0.12.0 → 0.13.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.
@@ -1,14 +1,14 @@
1
1
 
2
2
  
3
- > @veams/status-quo-query@0.3.0 build
3
+ > @veams/status-quo-query@0.12.0 build
4
4
  > npm-run-all compile
5
5
 
6
6
 
7
- > @veams/status-quo-query@0.3.0 compile
7
+ > @veams/status-quo-query@0.12.0 compile
8
8
  > npm-run-all bundle:ts
9
9
 
10
10
 
11
- > @veams/status-quo-query@0.3.0 bundle:ts
11
+ > @veams/status-quo-query@0.12.0 bundle:ts
12
12
  > tsc --project tsconfig.json
13
13
 
14
14
  ⠙⠙⠙
package/README.md CHANGED
@@ -19,11 +19,11 @@ npm install react
19
19
  Status Quo Query deliberately keeps the public surface small:
20
20
 
21
21
  - `QueryHandle<TData, TError>` is the read handle for one query.
22
- - `MutationService<TData, TError, TVariables>` is the write handle for one mutation.
22
+ - `MutationHandle<TData, TError, TVariables>` is the write handle for one mutation.
23
23
  - snapshots are passive state objects returned from `getSnapshot()` and `subscribe(...)`.
24
24
  - commands stay on the handle: `refetch()`, `invalidate()`, `mutate()`, `reset()`.
25
25
  - `QueryManager` is the broader coordination layer for cross-query work.
26
- - `@veams/status-quo-query/react` is optional and adds one React subscription hook over the same handle shape.
26
+ - `@veams/status-quo-query/react` is optional and adds React subscription hooks (`useQueryHandle`, `useMutationHandle`) over the same handle shape.
27
27
 
28
28
  That keeps the package usable in service code, query handlers, state handlers, and React components without changing the core query or mutation API.
29
29
 
@@ -49,13 +49,13 @@ Root exports:
49
49
  - `CreateUntrackedMutation`
50
50
  - `QueryHandle`
51
51
  - `QueryHandleData`
52
- - `MutationService`
52
+ - `MutationHandle`
53
53
  - `QueryHandleSnapshot`
54
- - `MutationServiceSnapshot`
54
+ - `MutationHandleSnapshot`
55
55
  - `QueryDependencyTuple`
56
56
  - `QueryHandleOptions`
57
- - `MutationServiceOptions`
58
- - `TrackedMutationServiceOptions`
57
+ - `MutationHandleOptions`
58
+ - `TrackedMutationHandleOptions`
59
59
  - `QueryInvalidateOptions`
60
60
  - `QueryMetaState`
61
61
  - `TrackedDependencyRecord`
@@ -70,7 +70,7 @@ Subpath exports:
70
70
  - `@veams/status-quo-query/provider`
71
71
  - `@veams/status-quo-query/query`
72
72
  - `@veams/status-quo-query/mutation`
73
- - `@veams/status-quo-query/react`
73
+ - `@veams/status-quo-query/react` (`useQueryHandle`, `useMutationHandle`)
74
74
 
75
75
  ## Quickstart
76
76
 
@@ -133,8 +133,9 @@ await userQuery.invalidate({ refetchType: 'none' });
133
133
 
134
134
  ## React Bindings
135
135
 
136
- The React entrypoint exposes `useQueryHandle(...)` and keeps `react` optional unless you
137
- import `@veams/status-quo-query/react`.
136
+ The React entrypoint exposes `useQueryHandle(...)` and `useMutationHandle(...)` and keeps `react` optional unless you import `@veams/status-quo-query/react`.
137
+
138
+ ### `useQueryHandle`
138
139
 
139
140
  ```tsx
140
141
  import { useQueryHandle } from '@veams/status-quo-query/react';
@@ -153,6 +154,35 @@ Use the hook when a component should subscribe directly to a query handle and re
153
154
  - call `query.refetch()` or `query.invalidate()` on the handle itself
154
155
  - derive view-specific values in the component instead of adding selector logic to the hook
155
156
 
157
+ ### `useMutationHandle`
158
+
159
+ ```tsx
160
+ import { useMutationHandle } from '@veams/status-quo-query/react';
161
+ import type { MutationHandle } from '@veams/status-quo-query';
162
+
163
+ function SaveButton({
164
+ mutation,
165
+ payload,
166
+ }: {
167
+ mutation: MutationHandle<{ ok: boolean }, Error, { name: string }>;
168
+ payload: { name: string };
169
+ }) {
170
+ const snapshot = useMutationHandle(mutation);
171
+
172
+ return (
173
+ <button onClick={() => mutation.mutate(payload)} disabled={snapshot.isPending}>
174
+ {snapshot.isPending ? 'Saving…' : 'Save'}
175
+ </button>
176
+ );
177
+ }
178
+ ```
179
+
180
+ Use `useMutationHandle` when a component should react to mutation state (pending, success, error). Keep imperative calls on the handle itself:
181
+
182
+ - read `status`, `isPending`, `isError`, `isSuccess`, `data`, `error`, `variables` from the snapshot
183
+ - call `mutation.mutate(variables)` or `mutation.reset()` on the handle itself
184
+ - the hook does not trigger the mutation — it only subscribes to its state
185
+
156
186
  ## Status Quo Integration
157
187
 
158
188
  The same query handle can also feed a `status-quo` handler through `bindSubscribable(...)`.
@@ -763,7 +793,7 @@ Only `deps` participates in automatic invalidation tracking. `view` is optional
763
793
 
764
794
  `createQuery(queryKey, queryFn, options?)` returns the same `QueryHandle<TData, TError>` shape as `createUntrackedQuery(...)`, but it registers the query hash under every `deps` entry, re-registers on `refetch()` or the first `subscribe(...)` if TanStack has removed the cache entry in the meantime, and keeps the registry in sync when `dependsOn` derives a new tracked key at runtime.
765
795
 
766
- `createMutation(mutationFn, options?)` returns the same `MutationService<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
796
+ `createMutation(mutationFn, options?)` returns the same `MutationHandle<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
767
797
 
768
798
  - `dependencyKeys?`
769
799
  - `resolveDependencies?`
@@ -786,7 +816,7 @@ Captures dependency names once and returns:
786
816
  - the tracked query factory
787
817
  - a tracked mutation factory whose default resolver reads `variables[dependencyKey]`
788
818
 
789
- 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.
819
+ The tracked query factory still expects a query key with a final `{ deps, view? }` segment. The tracked mutation factory keeps the same `MutationHandle` shape as `createMutation(...)`, but no longer needs `dependencyKeys` repeated in each call.
790
820
 
791
821
  Reach for standalone `createMutation(...)` when:
792
822
 
@@ -844,11 +874,11 @@ It also adds:
844
874
 
845
875
  Creates a `createUntrackedMutation` factory bound to a `QueryClient`.
846
876
 
847
- `createUntrackedMutation(mutationFn, options?)` returns `MutationService<TData, TError, TVariables, TOnMutateResult>`.
877
+ `createUntrackedMutation(mutationFn, options?)` returns `MutationHandle<TData, TError, TVariables, TOnMutateResult>`.
848
878
 
849
- `MutationServiceOptions` is based on TanStack `MutationObserverOptions`, without `mutationFn` because it is provided directly to `createUntrackedMutation`.
879
+ `MutationHandleOptions` is based on TanStack `MutationObserverOptions`, without `mutationFn` because it is provided directly to `createUntrackedMutation`.
850
880
 
851
- `MutationService` methods:
881
+ `MutationHandle` methods:
852
882
 
853
883
  - `getSnapshot()`
854
884
  - `subscribe(listener)`
@@ -856,7 +886,7 @@ Creates a `createUntrackedMutation` factory bound to a `QueryClient`.
856
886
  - `reset()`
857
887
  - `unsafe_getResult()`
858
888
 
859
- `MutationServiceSnapshot<TData, TError, TVariables>` fields:
889
+ `MutationHandleSnapshot<TData, TError, TVariables>` fields:
860
890
 
861
891
  - `data`
862
892
  - `error`
@@ -4,7 +4,7 @@ export type MutationStatus = TanstackMutationStatus;
4
4
  /**
5
5
  * Represents a stable snapshot of the mutation service's state.
6
6
  */
7
- export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVariables = void> {
7
+ export interface MutationHandleSnapshot<TData = unknown, TError = Error, TVariables = void> {
8
8
  data: TData | undefined;
9
9
  error: TError | null;
10
10
  status: MutationStatus;
@@ -17,9 +17,9 @@ export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVaria
17
17
  /**
18
18
  * Defines the public API for a mutation service.
19
19
  */
20
- export interface MutationService<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> {
21
- getSnapshot: () => MutationServiceSnapshot<TData, TError, TVariables>;
22
- subscribe: (listener: (snapshot: MutationServiceSnapshot<TData, TError, TVariables>) => void) => () => void;
20
+ export interface MutationHandle<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> {
21
+ getSnapshot: () => MutationHandleSnapshot<TData, TError, TVariables>;
22
+ subscribe: (listener: (snapshot: MutationHandleSnapshot<TData, TError, TVariables>) => void) => () => void;
23
23
  mutate: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TOnMutateResult>) => Promise<TData>;
24
24
  reset: () => void;
25
25
  unsafe_getResult: () => MutationObserverResult<TData, TError, TVariables, TOnMutateResult>;
@@ -27,12 +27,12 @@ export interface MutationService<TData = unknown, TError = Error, TVariables = v
27
27
  /**
28
28
  * Configuration options for creating a mutation service, excluding the mutation function itself.
29
29
  */
30
- export type MutationServiceOptions<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> = Omit<MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>, 'mutationFn'>;
30
+ export type MutationHandleOptions<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> = Omit<MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>, 'mutationFn'>;
31
31
  /**
32
32
  * Function signature for the untracked mutation factory.
33
33
  */
34
34
  export interface CreateUntrackedMutation {
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
+ <TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
36
36
  }
37
37
  /**
38
38
  * Additional options for tracked mutations that invalidate queries automatically.
@@ -41,7 +41,7 @@ export interface CreateUntrackedMutation {
41
41
  * options only describe how the facade should derive dependency values and when it should
42
42
  * invalidate matching tracked queries after the mutation lifecycle settles.
43
43
  */
44
- export interface TrackedMutationServiceOptions<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> extends MutationServiceOptions<TData, TError, TVariables, TOnMutateResult> {
44
+ export interface TrackedMutationHandleOptions<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> extends MutationHandleOptions<TData, TError, TVariables, TOnMutateResult> {
45
45
  dependencyKeys?: readonly (keyof TDeps & string)[];
46
46
  resolveDependencies?: (variables: TVariables) => Partial<TDeps>;
47
47
  invalidateOn?: TrackedInvalidateOn;
@@ -51,7 +51,7 @@ export interface TrackedMutationServiceOptions<TDeps extends TrackedDependencyRe
51
51
  * Function signature for the default mutation factory with automatic invalidation.
52
52
  */
53
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>;
54
+ <TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: TrackedMutationHandleOptions<TDeps, TData, TError, TVariables, TOnMutateResult>): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
55
55
  }
56
56
  /**
57
57
  * Prepares the mutation factory by binding it to a specific QueryClient instance.
package/dist/mutation.js CHANGED
@@ -8,7 +8,7 @@ import { pickTrackedDependencies, resolveTrackedQueries, toTrackedDependencyEntr
8
8
  export function setupMutation(queryClient) {
9
9
  // Returns the actual factory function for creating individual mutation services.
10
10
  return function createMutation(mutationFn, options) {
11
- return createMutationService(queryClient, mutationFn, options);
11
+ return createMutationHandle(queryClient, mutationFn, options);
12
12
  };
13
13
  }
14
14
  /**
@@ -22,8 +22,8 @@ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDepen
22
22
  return function createMutation(mutationFn, options) {
23
23
  // Split tracked-only options from the underlying TanStack mutation observer options.
24
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);
25
+ // Reuse the normal mutation handle so snapshots and subscription behavior stay identical.
26
+ const handle = createMutationHandle(queryClient, mutationFn, mutationOptions);
27
27
  // The paired helper injects dependency keys here, while standalone tracked mutations can
28
28
  // still provide them directly or bypass them with a custom resolver.
29
29
  const resolvedDependencyKeys = (dependencyKeys ?? defaultDependencyKeys);
@@ -41,12 +41,12 @@ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDepen
41
41
  })));
42
42
  };
43
43
  return {
44
- ...service,
44
+ ...handle,
45
45
  mutate: async (variables, mutateOptions) => {
46
46
  try {
47
47
  // Let TanStack finish the mutation first so its own callbacks and state machine remain
48
48
  // authoritative. The facade only coordinates the follow-up invalidation.
49
- const result = await service.mutate(variables, mutateOptions);
49
+ const result = await handle.mutate(variables, mutateOptions);
50
50
  if (invalidateOn === 'success' || invalidateOn === 'settled') {
51
51
  await invalidateTrackedQueries(variables);
52
52
  }
@@ -71,7 +71,7 @@ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDepen
71
71
  /**
72
72
  * Internal helper to transform a raw Tanstack mutation result into our public snapshot format.
73
73
  */
74
- function toMutationServiceSnapshot(result) {
74
+ function toMutationHandleSnapshot(result) {
75
75
  // Extract and return the relevant fields for the UI or other services.
76
76
  return {
77
77
  data: result.data,
@@ -84,7 +84,7 @@ function toMutationServiceSnapshot(result) {
84
84
  isSuccess: result.isSuccess,
85
85
  };
86
86
  }
87
- function createMutationService(queryClient, mutationFn, options) {
87
+ function createMutationHandle(queryClient, mutationFn, options) {
88
88
  // Keep the original mutation implementation in one place so tracked and untracked mutations
89
89
  // always expose the same observer-backed runtime behavior.
90
90
  const observer = new MutationObserver(queryClient, {
@@ -92,9 +92,9 @@ function createMutationService(queryClient, mutationFn, options) {
92
92
  mutationFn,
93
93
  });
94
94
  return {
95
- getSnapshot: () => toMutationServiceSnapshot(observer.getCurrentResult()),
95
+ getSnapshot: () => toMutationHandleSnapshot(observer.getCurrentResult()),
96
96
  subscribe: (listener) => observer.subscribe((result) => {
97
- listener(toMutationServiceSnapshot(result));
97
+ listener(toMutationHandleSnapshot(result));
98
98
  }),
99
99
  mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
100
100
  reset: () => observer.reset(),
@@ -1 +1 @@
1
- {"version":3,"file":"mutation.js","sourceRoot":"","sources":["../src/mutation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mutation.js","sourceRoot":"","sources":["../src/mutation.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { type QueryClient, type MutationFunction } from '@tanstack/query-core';
2
- import { type CreateMutation, type CreateUntrackedMutation, type MutationService, type TrackedMutationServiceOptions } from './mutation.js';
2
+ import { type CreateMutation, type CreateUntrackedMutation, type MutationHandle, type TrackedMutationHandleOptions } from './mutation.js';
3
3
  import { type CreateQuery, type CreateUntrackedQuery } from './query.js';
4
4
  import { type TrackedDependencyValue } from './tracking.js';
5
5
  /**
@@ -9,7 +9,7 @@ import { type TrackedDependencyValue } from './tracking.js';
9
9
  * once at setup time and injects them automatically for each tracked mutation it creates.
10
10
  */
11
11
  export interface CreateMutationWithDefaults<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>;
12
+ <TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: Omit<TrackedMutationHandleOptions<Record<TDependencyKey, TrackedDependencyValue>, TData, TError, TVariables, TOnMutateResult>, 'dependencyKeys'>): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
13
13
  }
14
14
  /**
15
15
  * Paired tracked helper that captures dependency keys once for default mutation resolution.
@@ -1 +1,2 @@
1
1
  export { useQueryHandle } from './use-query-handle.js';
2
+ export { useMutationHandle } from './use-mutation-handle.js';
@@ -1,2 +1,3 @@
1
1
  export { useQueryHandle } from './use-query-handle.js';
2
+ export { useMutationHandle } from './use-mutation-handle.js';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { MutationHandle, MutationHandleSnapshot } from '../../mutation.js';
2
+ export declare function useMutationHandle<TData, TError = Error, TVariables = void>(mutationHandle: MutationHandle<TData, TError, TVariables>): MutationHandleSnapshot<TData, TError, TVariables>;
@@ -0,0 +1,71 @@
1
+ // Import the React hooks needed to memoize callbacks, hold mutable cache state,
2
+ // and connect an external store to React rendering.
3
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
4
+ // Subscribe a React component to a MutationHandle and return its latest snapshot.
5
+ export function useMutationHandle(
6
+ // Receive the external mutation handle instance to subscribe to.
7
+ mutationHandle) {
8
+ // Count store notifications so we can tell when our cached snapshot is stale.
9
+ const snapshotVersionRef = useRef(0);
10
+ // Cache one client-side snapshot per observed version to keep getSnapshot stable.
11
+ const snapshotCacheRef = useRef(null);
12
+ // Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
13
+ const serverSnapshotCacheRef = useRef(null);
14
+ // Create the subscribe function expected by useSyncExternalStore.
15
+ const subscribe = useCallback(
16
+ // React passes a listener that must run whenever the external store changes.
17
+ (listener) =>
18
+ // Forward the subscription to the mutation handle.
19
+ mutationHandle.subscribe(() => {
20
+ // Bump the version so later reads know the previous cache is outdated.
21
+ snapshotVersionRef.current += 1;
22
+ // Drop the cached client snapshot because the store just changed.
23
+ snapshotCacheRef.current = null;
24
+ // Drop the cached server snapshot for the same reason.
25
+ serverSnapshotCacheRef.current = null;
26
+ // Notify React that it should read a fresh snapshot.
27
+ listener();
28
+ }),
29
+ // Recreate the subscription function only when the handle instance changes.
30
+ [mutationHandle]);
31
+ // Read the current client snapshot in a referentially stable way for React.
32
+ const getSnapshot = useCallback(() => {
33
+ // Read the latest store version number.
34
+ const version = snapshotVersionRef.current;
35
+ // Read the last cached client snapshot, if there is one.
36
+ const cachedSnapshot = snapshotCacheRef.current;
37
+ // Reuse the cached snapshot when it was produced for the current version.
38
+ if (cachedSnapshot && cachedSnapshot.version === version) {
39
+ // Return the cached snapshot so repeated reads in the same render stay stable.
40
+ return cachedSnapshot.snapshot;
41
+ }
42
+ // Ask the mutation handle for the latest snapshot because the cache is empty or stale.
43
+ const snapshot = mutationHandle.getSnapshot();
44
+ // Store the new snapshot together with the version it belongs to.
45
+ snapshotCacheRef.current = {
46
+ snapshot,
47
+ version,
48
+ };
49
+ // Return the freshly read snapshot to React.
50
+ return snapshot;
51
+ }, [mutationHandle]);
52
+ // Read the server snapshot used by React during SSR or hydration fallback paths.
53
+ const getServerSnapshot = useCallback(() => {
54
+ // Read the cached server snapshot, if one was stored earlier.
55
+ const cachedSnapshot = serverSnapshotCacheRef.current;
56
+ // Reuse the cached server snapshot to keep server reads stable.
57
+ if (cachedSnapshot) {
58
+ // Return the cached server snapshot directly.
59
+ return cachedSnapshot;
60
+ }
61
+ // Ask the mutation handle for a snapshot because no server cache exists yet.
62
+ const snapshot = mutationHandle.getSnapshot();
63
+ // Cache that snapshot for the next server read.
64
+ serverSnapshotCacheRef.current = snapshot;
65
+ // Return the freshly read server snapshot.
66
+ return snapshot;
67
+ }, [mutationHandle]);
68
+ // Let React subscribe to the external store and read snapshots through the callbacks above.
69
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
70
+ }
71
+ //# sourceMappingURL=use-mutation-handle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-mutation-handle.js","sourceRoot":"","sources":["../../../src/react/hooks/use-mutation-handle.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,oDAAoD;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAelE,kFAAkF;AAClF,MAAM,UAAU,iBAAiB;AAC/B,iEAAiE;AACjE,cAAyD;IAEzD,8EAA8E;IAC9E,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACrC,kFAAkF;IAClF,MAAM,gBAAgB,GAAG,MAAM,CAAuD,IAAI,CAAC,CAAC;IAC5F,kFAAkF;IAClF,MAAM,sBAAsB,GAAG,MAAM,CACnC,IAAI,CACL,CAAC;IAEF,kEAAkE;IAClE,MAAM,SAAS,GAAG,WAAW;IAC3B,6EAA6E;IAC7E,CAAC,QAAkB,EAAE,EAAE;IACrB,mDAAmD;IACnD,cAAc,CAAC,SAAS,CAAC,GAAG,EAAE;QAC5B,uEAAuE;QACvE,kBAAkB,CAAC,OAAO,IAAI,CAAC,CAAC;QAChC,kEAAkE;QAClE,gBAAgB,CAAC,OAAO,GAAG,IAAI,CAAC;QAChC,uDAAuD;QACvD,sBAAsB,CAAC,OAAO,GAAG,IAAI,CAAC;QACtC,qDAAqD;QACrD,QAAQ,EAAE,CAAC;IACb,CAAC,CAAC;IACJ,4EAA4E;IAC5E,CAAC,cAAc,CAAC,CACjB,CAAC;IAEF,4EAA4E;IAC5E,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,wCAAwC;QACxC,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC;QAC3C,yDAAyD;QACzD,MAAM,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC;QAEhD,0EAA0E;QAC1E,IAAI,cAAc,IAAI,cAAc,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YACzD,+EAA+E;YAC/E,OAAO,cAAc,CAAC,QAAQ,CAAC;QACjC,CAAC;QAED,uFAAuF;QACvF,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,EAAE,CAAC;QAC9C,kEAAkE;QAClE,gBAAgB,CAAC,OAAO,GAAG;YACzB,QAAQ;YACR,OAAO;SACR,CAAC;QAEF,6CAA6C;QAC7C,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC;IAErB,iFAAiF;IACjF,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,EAAE;QACzC,8DAA8D;QAC9D,MAAM,cAAc,GAAG,sBAAsB,CAAC,OAAO,CAAC;QAEtD,gEAAgE;QAChE,IAAI,cAAc,EAAE,CAAC;YACnB,8CAA8C;YAC9C,OAAO,cAAc,CAAC;QACxB,CAAC;QAED,6EAA6E;QAC7E,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,EAAE,CAAC;QAC9C,gDAAgD;QAChD,sBAAsB,CAAC,OAAO,GAAG,QAAQ,CAAC;QAE1C,2CAA2C;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC;IAErB,4FAA4F;IAC5F,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC"}
@@ -1 +1 @@
1
- export { useQueryHandle } from './hooks/index.js';
1
+ export { useQueryHandle, useMutationHandle } from './hooks/index.js';
@@ -1,2 +1,2 @@
1
- export { useQueryHandle } from './hooks/index.js';
1
+ export { useQueryHandle, useMutationHandle } from './hooks/index.js';
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veams/status-quo-query",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",
package/src/mutation.ts CHANGED
@@ -32,7 +32,7 @@ export type MutationStatus = TanstackMutationStatus;
32
32
  /**
33
33
  * Represents a stable snapshot of the mutation service's state.
34
34
  */
35
- export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVariables = void> {
35
+ export interface MutationHandleSnapshot<TData = unknown, TError = Error, TVariables = void> {
36
36
  // The data returned from a successful mutation.
37
37
  data: TData | undefined;
38
38
  // The error object if the mutation failed.
@@ -54,17 +54,17 @@ export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVaria
54
54
  /**
55
55
  * Defines the public API for a mutation service.
56
56
  */
57
- export interface MutationService<
57
+ export interface MutationHandle<
58
58
  TData = unknown,
59
59
  TError = Error,
60
60
  TVariables = void,
61
61
  TOnMutateResult = unknown,
62
62
  > {
63
63
  // Returns the current state snapshot of the mutation.
64
- getSnapshot: () => MutationServiceSnapshot<TData, TError, TVariables>;
64
+ getSnapshot: () => MutationHandleSnapshot<TData, TError, TVariables>;
65
65
  // Subscribes a listener to state changes; returns an unsubscribe function.
66
66
  subscribe: (
67
- listener: (snapshot: MutationServiceSnapshot<TData, TError, TVariables>) => void
67
+ listener: (snapshot: MutationHandleSnapshot<TData, TError, TVariables>) => void
68
68
  ) => () => void;
69
69
  // Triggers the mutation with the given variables and optional lifecycle callbacks.
70
70
  mutate: (
@@ -80,7 +80,7 @@ export interface MutationService<
80
80
  /**
81
81
  * Configuration options for creating a mutation service, excluding the mutation function itself.
82
82
  */
83
- export type MutationServiceOptions<
83
+ export type MutationHandleOptions<
84
84
  TData = unknown,
85
85
  TError = Error,
86
86
  TVariables = void,
@@ -95,8 +95,8 @@ export interface CreateUntrackedMutation {
95
95
  // The asynchronous function that performs the mutation.
96
96
  mutationFn: MutationFunction<TData, TVariables>,
97
97
  // Optional configuration for behavior like retry or lifecycle hooks.
98
- options?: MutationServiceOptions<TData, TError, TVariables, TOnMutateResult>
99
- ): MutationService<TData, TError, TVariables, TOnMutateResult>;
98
+ options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>
99
+ ): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
100
100
  }
101
101
 
102
102
  /**
@@ -106,13 +106,13 @@ export interface CreateUntrackedMutation {
106
106
  * options only describe how the facade should derive dependency values and when it should
107
107
  * invalidate matching tracked queries after the mutation lifecycle settles.
108
108
  */
109
- export interface TrackedMutationServiceOptions<
109
+ export interface TrackedMutationHandleOptions<
110
110
  TDeps extends TrackedDependencyRecord = TrackedDependencyRecord,
111
111
  TData = unknown,
112
112
  TError = Error,
113
113
  TVariables = void,
114
114
  TOnMutateResult = unknown,
115
- > extends MutationServiceOptions<TData, TError, TVariables, TOnMutateResult> {
115
+ > extends MutationHandleOptions<TData, TError, TVariables, TOnMutateResult> {
116
116
  // Optional dependency keys used by the default variable reader.
117
117
  dependencyKeys?: readonly (keyof TDeps & string)[];
118
118
  // Optional custom resolver when mutation variables do not expose dependency fields directly.
@@ -135,8 +135,8 @@ export interface CreateMutation {
135
135
  TOnMutateResult = unknown,
136
136
  >(
137
137
  mutationFn: MutationFunction<TData, TVariables>,
138
- options?: TrackedMutationServiceOptions<TDeps, TData, TError, TVariables, TOnMutateResult>
139
- ): MutationService<TData, TError, TVariables, TOnMutateResult>;
138
+ options?: TrackedMutationHandleOptions<TDeps, TData, TError, TVariables, TOnMutateResult>
139
+ ): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
140
140
  }
141
141
 
142
142
  /**
@@ -151,9 +151,9 @@ export function setupMutation(queryClient: QueryClient): CreateUntrackedMutation
151
151
  TOnMutateResult = unknown,
152
152
  >(
153
153
  mutationFn: MutationFunction<TData, TVariables>,
154
- options?: MutationServiceOptions<TData, TError, TVariables, TOnMutateResult>
155
- ): MutationService<TData, TError, TVariables, TOnMutateResult> {
156
- return createMutationService(queryClient, mutationFn, options);
154
+ options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>
155
+ ): MutationHandle<TData, TError, TVariables, TOnMutateResult> {
156
+ return createMutationHandle(queryClient, mutationFn, options);
157
157
  };
158
158
  }
159
159
 
@@ -177,8 +177,8 @@ export function setupTrackedMutation(
177
177
  TOnMutateResult = unknown,
178
178
  >(
179
179
  mutationFn: MutationFunction<TData, TVariables>,
180
- options?: TrackedMutationServiceOptions<TDeps, TData, TError, TVariables, TOnMutateResult>
181
- ): MutationService<TData, TError, TVariables, TOnMutateResult> {
180
+ options?: TrackedMutationHandleOptions<TDeps, TData, TError, TVariables, TOnMutateResult>
181
+ ): MutationHandle<TData, TError, TVariables, TOnMutateResult> {
182
182
  // Split tracked-only options from the underlying TanStack mutation observer options.
183
183
  const {
184
184
  dependencyKeys,
@@ -187,8 +187,8 @@ export function setupTrackedMutation(
187
187
  resolveDependencies,
188
188
  ...mutationOptions
189
189
  } = options ?? {};
190
- // Reuse the normal mutation service so snapshots and subscription behavior stay identical.
191
- const service = createMutationService(queryClient, mutationFn, mutationOptions);
190
+ // Reuse the normal mutation handle so snapshots and subscription behavior stay identical.
191
+ const handle = createMutationHandle(queryClient, mutationFn, mutationOptions);
192
192
  // The paired helper injects dependency keys here, while standalone tracked mutations can
193
193
  // still provide them directly or bypass them with a custom resolver.
194
194
  const resolvedDependencyKeys = (dependencyKeys ?? defaultDependencyKeys);
@@ -220,12 +220,12 @@ export function setupTrackedMutation(
220
220
  };
221
221
 
222
222
  return {
223
- ...service,
223
+ ...handle,
224
224
  mutate: async (variables, mutateOptions) => {
225
225
  try {
226
226
  // Let TanStack finish the mutation first so its own callbacks and state machine remain
227
227
  // authoritative. The facade only coordinates the follow-up invalidation.
228
- const result = await service.mutate(variables, mutateOptions);
228
+ const result = await handle.mutate(variables, mutateOptions);
229
229
 
230
230
  if (invalidateOn === 'success' || invalidateOn === 'settled') {
231
231
  await invalidateTrackedQueries(variables);
@@ -252,9 +252,9 @@ export function setupTrackedMutation(
252
252
  /**
253
253
  * Internal helper to transform a raw Tanstack mutation result into our public snapshot format.
254
254
  */
255
- function toMutationServiceSnapshot<TData, TError, TVariables, TOnMutateResult>(
255
+ function toMutationHandleSnapshot<TData, TError, TVariables, TOnMutateResult>(
256
256
  result: MutationObserverResult<TData, TError, TVariables, TOnMutateResult>
257
- ): MutationServiceSnapshot<TData, TError, TVariables> {
257
+ ): MutationHandleSnapshot<TData, TError, TVariables> {
258
258
  // Extract and return the relevant fields for the UI or other services.
259
259
  return {
260
260
  data: result.data,
@@ -268,7 +268,7 @@ function toMutationServiceSnapshot<TData, TError, TVariables, TOnMutateResult>(
268
268
  };
269
269
  }
270
270
 
271
- function createMutationService<
271
+ function createMutationHandle<
272
272
  TData = unknown,
273
273
  TError = Error,
274
274
  TVariables = void,
@@ -276,8 +276,8 @@ function createMutationService<
276
276
  >(
277
277
  queryClient: QueryClient,
278
278
  mutationFn: MutationFunction<TData, TVariables>,
279
- options?: MutationServiceOptions<TData, TError, TVariables, TOnMutateResult>
280
- ): MutationService<TData, TError, TVariables, TOnMutateResult> {
279
+ options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>
280
+ ): MutationHandle<TData, TError, TVariables, TOnMutateResult> {
281
281
  // Keep the original mutation implementation in one place so tracked and untracked mutations
282
282
  // always expose the same observer-backed runtime behavior.
283
283
  const observer = new MutationObserver<TData, TError, TVariables, TOnMutateResult>(queryClient, {
@@ -286,10 +286,10 @@ function createMutationService<
286
286
  });
287
287
 
288
288
  return {
289
- getSnapshot: () => toMutationServiceSnapshot(observer.getCurrentResult()),
289
+ getSnapshot: () => toMutationHandleSnapshot(observer.getCurrentResult()),
290
290
  subscribe: (listener) =>
291
291
  observer.subscribe((result) => {
292
- listener(toMutationServiceSnapshot(result));
292
+ listener(toMutationHandleSnapshot(result));
293
293
  }),
294
294
  mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
295
295
  reset: () => observer.reset(),
package/src/provider.ts CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  import {
9
9
  type CreateMutation,
10
10
  type CreateUntrackedMutation,
11
- type MutationService,
12
- type TrackedMutationServiceOptions,
11
+ type MutationHandle,
12
+ type TrackedMutationHandleOptions,
13
13
  setupMutation,
14
14
  setupTrackedMutation,
15
15
  } from './mutation.js';
@@ -29,7 +29,7 @@ export interface CreateMutationWithDefaults<TDependencyKey extends string> {
29
29
  <TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(
30
30
  mutationFn: MutationFunction<TData, TVariables>,
31
31
  options?: Omit<
32
- TrackedMutationServiceOptions<
32
+ TrackedMutationHandleOptions<
33
33
  Record<TDependencyKey, TrackedDependencyValue>,
34
34
  TData,
35
35
  TError,
@@ -38,7 +38,7 @@ export interface CreateMutationWithDefaults<TDependencyKey extends string> {
38
38
  >,
39
39
  'dependencyKeys'
40
40
  >
41
- ): MutationService<TData, TError, TVariables, TOnMutateResult>;
41
+ ): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
42
42
  }
43
43
 
44
44
  /**
@@ -54,13 +54,13 @@ export interface CreateQueryAndMutation {
54
54
  * Defines the public API for the query manager facade.
55
55
  */
56
56
  export interface QueryManager {
57
- // Factory for creating a dependency-tracked mutation service within the context of this provider.
57
+ // Factory for creating a dependency-tracked mutation handle within the context of this provider.
58
58
  createMutation: CreateMutation;
59
59
  // Factory for creating a dependency-tracked query handle within the context of this provider.
60
60
  createQuery: CreateQuery;
61
61
  // Factory for creating an untracked query handle within the context of this provider.
62
62
  createUntrackedQuery: CreateUntrackedQuery;
63
- // Factory for creating an untracked mutation service within the context of this provider.
63
+ // Factory for creating an untracked mutation handle within the context of this provider.
64
64
  createUntrackedMutation: CreateUntrackedMutation;
65
65
  // Convenience helper that shares dependency keys between tracked queries and mutations.
66
66
  createQueryAndMutation: CreateQueryAndMutation;
@@ -126,7 +126,7 @@ export function setupQueryManager(queryClient: QueryClient): QueryManager {
126
126
  > = <TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(
127
127
  mutationFn: MutationFunction<TData, TVariables>,
128
128
  options?: Omit<
129
- TrackedMutationServiceOptions<
129
+ TrackedMutationHandleOptions<
130
130
  Record<TDependencyKeys[number], TrackedDependencyValue>,
131
131
  TData,
132
132
  TError,
@@ -0,0 +1,107 @@
1
+ import React, { act } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { QueryClient } from '@tanstack/query-core';
4
+
5
+ import { setupMutation } from '../../mutation.js';
6
+ import { useMutationHandle } from '../hooks/use-mutation-handle.js';
7
+
8
+ import type { MutationHandle, MutationHandleSnapshot } from '../../mutation.js';
9
+
10
+ declare global {
11
+ var IS_REACT_ACT_ENVIRONMENT: boolean;
12
+ }
13
+
14
+ describe('useMutationHandle', () => {
15
+ let container: HTMLDivElement;
16
+
17
+ beforeAll(() => {
18
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
19
+ });
20
+
21
+ beforeEach(() => {
22
+ container = document.createElement('div');
23
+ document.body.appendChild(container);
24
+ });
25
+
26
+ afterEach(() => {
27
+ container.remove();
28
+ });
29
+
30
+ it('renders the latest mutation snapshot', async () => {
31
+ const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: 0 } } });
32
+ const createMutation = setupMutation(queryClient);
33
+ const mutationFn = jest.fn((_payload: { name: string }) =>
34
+ Promise.resolve({ ok: true as const })
35
+ );
36
+ const handle = createMutation(mutationFn);
37
+ const statuses: string[] = [];
38
+
39
+ const Consumer = () => {
40
+ const snapshot = useMutationHandle(handle);
41
+ statuses.push(snapshot.status);
42
+
43
+ return <span>{snapshot.status}</span>;
44
+ };
45
+
46
+ const root = createRoot(container);
47
+
48
+ await act(async () => {
49
+ root.render(<Consumer />);
50
+ });
51
+
52
+ expect(container.textContent).toBe('idle');
53
+
54
+ await act(async () => {
55
+ await handle.mutate({ name: 'Ada' });
56
+ });
57
+
58
+ expect(container.textContent).toBe('success');
59
+ expect(statuses).toContain('idle');
60
+ expect(statuses).toContain('pending');
61
+ expect(statuses).toContain('success');
62
+
63
+ await act(async () => {
64
+ root.unmount();
65
+ });
66
+ });
67
+
68
+ it('cleans up the store subscription on unmount', async () => {
69
+ const snapshot: MutationHandleSnapshot<{ ok: true }, Error, void> = {
70
+ data: { ok: true },
71
+ error: null,
72
+ status: 'success',
73
+ variables: undefined,
74
+ isError: false,
75
+ isIdle: false,
76
+ isPending: false,
77
+ isSuccess: true,
78
+ };
79
+ const unsubscribe = jest.fn();
80
+ const handle: MutationHandle<{ ok: true }, Error, void> = {
81
+ getSnapshot: () => snapshot,
82
+ subscribe: jest.fn(() => unsubscribe),
83
+ mutate: jest.fn(async () => ({ ok: true as const })),
84
+ reset: jest.fn(),
85
+ unsafe_getResult: jest.fn(),
86
+ };
87
+
88
+ const root = createRoot(container);
89
+
90
+ const Consumer = () => {
91
+ useMutationHandle(handle);
92
+ return <span>ready</span>;
93
+ };
94
+
95
+ await act(async () => {
96
+ root.render(<Consumer />);
97
+ });
98
+
99
+ expect(handle.subscribe).toHaveBeenCalledTimes(1);
100
+
101
+ await act(async () => {
102
+ root.unmount();
103
+ });
104
+
105
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
106
+ });
107
+ });
@@ -1 +1,2 @@
1
1
  export { useQueryHandle } from './use-query-handle.js';
2
+ export { useMutationHandle } from './use-mutation-handle.js';
@@ -0,0 +1,98 @@
1
+ // Import the React hooks needed to memoize callbacks, hold mutable cache state,
2
+ // and connect an external store to React rendering.
3
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
4
+
5
+ // Import the mutation-handle contract and the stable snapshot shape the hook returns.
6
+ import type { MutationHandle, MutationHandleSnapshot } from '../../mutation.js';
7
+ // Describe the no-argument listener shape expected by useSyncExternalStore.
8
+ type Listener = () => void;
9
+
10
+ // Store one cached snapshot together with the store version it belongs to.
11
+ type SnapshotCacheEntry<TData, TError, TVariables> = {
12
+ // Hold the snapshot returned by the mutation handle for this version.
13
+ snapshot: MutationHandleSnapshot<TData, TError, TVariables>;
14
+ // Track which subscription version produced the cached snapshot.
15
+ version: number;
16
+ };
17
+
18
+ // Subscribe a React component to a MutationHandle and return its latest snapshot.
19
+ export function useMutationHandle<TData, TError = Error, TVariables = void>(
20
+ // Receive the external mutation handle instance to subscribe to.
21
+ mutationHandle: MutationHandle<TData, TError, TVariables>
22
+ ) {
23
+ // Count store notifications so we can tell when our cached snapshot is stale.
24
+ const snapshotVersionRef = useRef(0);
25
+ // Cache one client-side snapshot per observed version to keep getSnapshot stable.
26
+ const snapshotCacheRef = useRef<SnapshotCacheEntry<TData, TError, TVariables> | null>(null);
27
+ // Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
28
+ const serverSnapshotCacheRef = useRef<MutationHandleSnapshot<TData, TError, TVariables> | null>(
29
+ null
30
+ );
31
+
32
+ // Create the subscribe function expected by useSyncExternalStore.
33
+ const subscribe = useCallback(
34
+ // React passes a listener that must run whenever the external store changes.
35
+ (listener: Listener) =>
36
+ // Forward the subscription to the mutation handle.
37
+ mutationHandle.subscribe(() => {
38
+ // Bump the version so later reads know the previous cache is outdated.
39
+ snapshotVersionRef.current += 1;
40
+ // Drop the cached client snapshot because the store just changed.
41
+ snapshotCacheRef.current = null;
42
+ // Drop the cached server snapshot for the same reason.
43
+ serverSnapshotCacheRef.current = null;
44
+ // Notify React that it should read a fresh snapshot.
45
+ listener();
46
+ }),
47
+ // Recreate the subscription function only when the handle instance changes.
48
+ [mutationHandle]
49
+ );
50
+
51
+ // Read the current client snapshot in a referentially stable way for React.
52
+ const getSnapshot = useCallback(() => {
53
+ // Read the latest store version number.
54
+ const version = snapshotVersionRef.current;
55
+ // Read the last cached client snapshot, if there is one.
56
+ const cachedSnapshot = snapshotCacheRef.current;
57
+
58
+ // Reuse the cached snapshot when it was produced for the current version.
59
+ if (cachedSnapshot && cachedSnapshot.version === version) {
60
+ // Return the cached snapshot so repeated reads in the same render stay stable.
61
+ return cachedSnapshot.snapshot;
62
+ }
63
+
64
+ // Ask the mutation handle for the latest snapshot because the cache is empty or stale.
65
+ const snapshot = mutationHandle.getSnapshot();
66
+ // Store the new snapshot together with the version it belongs to.
67
+ snapshotCacheRef.current = {
68
+ snapshot,
69
+ version,
70
+ };
71
+
72
+ // Return the freshly read snapshot to React.
73
+ return snapshot;
74
+ }, [mutationHandle]);
75
+
76
+ // Read the server snapshot used by React during SSR or hydration fallback paths.
77
+ const getServerSnapshot = useCallback(() => {
78
+ // Read the cached server snapshot, if one was stored earlier.
79
+ const cachedSnapshot = serverSnapshotCacheRef.current;
80
+
81
+ // Reuse the cached server snapshot to keep server reads stable.
82
+ if (cachedSnapshot) {
83
+ // Return the cached server snapshot directly.
84
+ return cachedSnapshot;
85
+ }
86
+
87
+ // Ask the mutation handle for a snapshot because no server cache exists yet.
88
+ const snapshot = mutationHandle.getSnapshot();
89
+ // Cache that snapshot for the next server read.
90
+ serverSnapshotCacheRef.current = snapshot;
91
+
92
+ // Return the freshly read server snapshot.
93
+ return snapshot;
94
+ }, [mutationHandle]);
95
+
96
+ // Let React subscribe to the external store and read snapshots through the callbacks above.
97
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
98
+ }
@@ -1 +1 @@
1
- export { useQueryHandle } from './hooks/index.js';
1
+ export { useQueryHandle, useMutationHandle } from './hooks/index.js';