@ic-reactor/react 3.3.0 → 3.4.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.
Files changed (53) hide show
  1. package/README.md +117 -4
  2. package/dist/createActorHooks.d.ts +1 -1
  3. package/dist/createActorHooks.d.ts.map +1 -1
  4. package/dist/createActorHooks.js +10 -25
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createAuthHooks.d.ts +13 -5
  7. package/dist/createAuthHooks.d.ts.map +1 -1
  8. package/dist/createAuthHooks.js +51 -1
  9. package/dist/createAuthHooks.js.map +1 -1
  10. package/dist/createInfiniteQuery.d.ts.map +1 -1
  11. package/dist/createInfiniteQuery.js +2 -12
  12. package/dist/createInfiniteQuery.js.map +1 -1
  13. package/dist/createMutation.d.ts.map +1 -1
  14. package/dist/createMutation.js +39 -56
  15. package/dist/createMutation.js.map +1 -1
  16. package/dist/createQuery.d.ts.map +1 -1
  17. package/dist/createQuery.js +31 -39
  18. package/dist/createQuery.js.map +1 -1
  19. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  20. package/dist/createSuspenseInfiniteQuery.js +1 -14
  21. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  22. package/dist/createSuspenseQuery.d.ts.map +1 -1
  23. package/dist/createSuspenseQuery.js +30 -39
  24. package/dist/createSuspenseQuery.js.map +1 -1
  25. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  26. package/dist/hooks/useActorInfiniteQuery.js +4 -5
  27. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  28. package/dist/hooks/useActorMutation.d.ts +9 -2
  29. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  30. package/dist/hooks/useActorMutation.js +15 -15
  31. package/dist/hooks/useActorMutation.js.map +1 -1
  32. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  33. package/dist/hooks/useActorSuspenseInfiniteQuery.js +4 -5
  34. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  35. package/dist/types.d.ts +26 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils.d.ts +26 -0
  38. package/dist/utils.d.ts.map +1 -0
  39. package/dist/utils.js +41 -0
  40. package/dist/utils.js.map +1 -0
  41. package/package.json +17 -11
  42. package/src/createActorHooks.ts +32 -31
  43. package/src/createAuthHooks.ts +93 -6
  44. package/src/createInfiniteQuery.ts +2 -19
  45. package/src/createMutation.ts +52 -69
  46. package/src/createQuery.ts +41 -52
  47. package/src/createSuspenseInfiniteQuery.ts +1 -22
  48. package/src/createSuspenseQuery.ts +42 -56
  49. package/src/hooks/useActorInfiniteQuery.ts +4 -9
  50. package/src/hooks/useActorMutation.ts +40 -14
  51. package/src/hooks/useActorSuspenseInfiniteQuery.ts +4 -9
  52. package/src/types.ts +32 -0
  53. package/src/utils.ts +52 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared internal utilities for query and mutation factories.
3
+ */
4
+ import type { QueryKey } from "@tanstack/react-query";
5
+ /**
6
+ * Internal query-key segment used to distinguish per-call factory args
7
+ * from the base query key. Not part of the public API.
8
+ */
9
+ export declare const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args";
10
+ /**
11
+ * Merge a base query key, optional per-call query key, and optional key-args
12
+ * into a single query key array.
13
+ *
14
+ * Used by createInfiniteQueryFactory and createSuspenseInfiniteQueryFactory to
15
+ * ensure each unique set of factory args produces a distinct cache entry.
16
+ */
17
+ export declare function mergeFactoryQueryKey(baseQueryKey?: QueryKey, callQueryKey?: QueryKey, keyArgs?: unknown): QueryKey | undefined;
18
+ /**
19
+ * Build a chained select function that first applies the config-level select
20
+ * (if any) and then the hook-level select (if any).
21
+ *
22
+ * This enables `createQuery` / `createSuspenseQuery` to support two-level
23
+ * select chaining without duplicating the logic.
24
+ */
25
+ export declare function buildChainedSelect<TData, TSelected, TFinal = TSelected>(configSelect: ((data: TData) => TSelected) | undefined, hookSelect: ((data: TSelected) => TFinal) | undefined): (rawData: TData) => TSelected | TFinal;
26
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAErD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,kCAAkC,CAAA;AAEzE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,CAAC,EAAE,QAAQ,EACvB,YAAY,CAAC,EAAE,QAAQ,EACvB,OAAO,CAAC,EAAE,OAAO,GAChB,QAAQ,GAAG,SAAS,CAStB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EACrE,YAAY,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS,CAAC,GAAG,SAAS,EACtD,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,SAAS,GACpD,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,GAAG,MAAM,CAOxC"}
package/dist/utils.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared internal utilities for query and mutation factories.
3
+ */
4
+ /**
5
+ * Internal query-key segment used to distinguish per-call factory args
6
+ * from the base query key. Not part of the public API.
7
+ */
8
+ export const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args";
9
+ /**
10
+ * Merge a base query key, optional per-call query key, and optional key-args
11
+ * into a single query key array.
12
+ *
13
+ * Used by createInfiniteQueryFactory and createSuspenseInfiniteQueryFactory to
14
+ * ensure each unique set of factory args produces a distinct cache entry.
15
+ */
16
+ export function mergeFactoryQueryKey(baseQueryKey, callQueryKey, keyArgs) {
17
+ const merged = [];
18
+ if (baseQueryKey)
19
+ merged.push(...baseQueryKey);
20
+ if (callQueryKey)
21
+ merged.push(...callQueryKey);
22
+ if (keyArgs !== undefined)
23
+ merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs });
24
+ return merged.length > 0 ? merged : undefined;
25
+ }
26
+ /**
27
+ * Build a chained select function that first applies the config-level select
28
+ * (if any) and then the hook-level select (if any).
29
+ *
30
+ * This enables `createQuery` / `createSuspenseQuery` to support two-level
31
+ * select chaining without duplicating the logic.
32
+ */
33
+ export function buildChainedSelect(configSelect, hookSelect) {
34
+ return (rawData) => {
35
+ const firstPass = configSelect
36
+ ? configSelect(rawData)
37
+ : rawData;
38
+ return hookSelect ? hookSelect(firstPass) : firstPass;
39
+ };
40
+ }
41
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,+BAA+B,CAAA;AAEzE;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,YAAuB,EACvB,YAAuB,EACvB,OAAiB;IAEjB,MAAM,MAAM,GAAc,EAAE,CAAA;IAE5B,IAAI,YAAY;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAA;IAC9C,IAAI,YAAY;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAA;IAC9C,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,0BAA0B,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;IAExD,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,YAAsD,EACtD,UAAqD;IAErD,OAAO,CAAC,OAAc,EAAE,EAAE;QACxB,MAAM,SAAS,GAAG,YAAY;YAC5B,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC;YACvB,CAAC,CAAE,OAAgC,CAAA;QACrC,OAAO,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACvD,CAAC,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/react",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "IC Reactor React Library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -42,29 +42,35 @@
42
42
  "author": "Behrad Deylami",
43
43
  "license": "MIT",
44
44
  "dependencies": {
45
- "@ic-reactor/core": "^3.3.0"
45
+ "@ic-reactor/core": "^3.4.0"
46
46
  },
47
47
  "peerDependencies": {
48
+ "@icp-sdk/auth": "^6.0.0",
48
49
  "@tanstack/react-query": "^5.0.0",
49
50
  "react": ">=18.0.0",
50
51
  "react-dom": ">=18.0.0"
51
52
  },
53
+ "peerDependenciesMeta": {
54
+ "@icp-sdk/auth": {
55
+ "optional": true
56
+ }
57
+ },
52
58
  "devDependencies": {
53
- "@icp-sdk/auth": "^5.0.0",
54
- "@icp-sdk/core": "^5.2.1",
55
- "@size-limit/preset-small-lib": "^12.0.1",
56
- "@tanstack/react-query": "^5.96.2",
59
+ "@icp-sdk/auth": "^6.0.0",
60
+ "@icp-sdk/core": "^5.3.1",
61
+ "@size-limit/preset-small-lib": "^12.1.0",
62
+ "@tanstack/react-query": "^5.100.5",
57
63
  "@testing-library/dom": "^10.4.1",
58
64
  "@testing-library/jest-dom": "^6.9.1",
59
65
  "@testing-library/react": "^16.3.2",
60
66
  "@types/react": "^19.2.14",
61
67
  "@types/react-dom": "^19.2.3",
62
68
  "fake-indexeddb": "^6.2.5",
63
- "jsdom": "^29.0.1",
64
- "react": "^19.2.4",
65
- "react-dom": "^19.2.4",
66
- "size-limit": "^12.0.1",
67
- "vitest": "^4.1.2"
69
+ "jsdom": "^29.1.0",
70
+ "react": "^19.2.5",
71
+ "react-dom": "^19.2.5",
72
+ "size-limit": "^12.1.0",
73
+ "vitest": "^4.1.5"
68
74
  },
69
75
  "size-limit": [
70
76
  {
@@ -33,18 +33,17 @@ import {
33
33
  UseMutationResult,
34
34
  InfiniteData,
35
35
  } from "@tanstack/react-query"
36
- import { createQuery } from "./createQuery"
37
- import { createSuspenseQuery } from "./createSuspenseQuery"
38
- import { createInfiniteQuery, InfiniteQueryConfig } from "./createInfiniteQuery"
39
- import {
40
- createSuspenseInfiniteQuery,
41
- SuspenseInfiniteQueryConfig,
42
- } from "./createSuspenseInfiniteQuery"
43
- import { createMutation } from "./createMutation"
36
+ import { useActorQuery } from "./hooks/useActorQuery"
37
+ import { useActorSuspenseQuery } from "./hooks/useActorSuspenseQuery"
38
+ import { useActorInfiniteQuery } from "./hooks/useActorInfiniteQuery"
39
+ import { useActorSuspenseInfiniteQuery } from "./hooks/useActorSuspenseInfiniteQuery"
40
+ import { useActorMutation } from "./hooks/useActorMutation"
44
41
  import {
45
42
  useActorMethod,
46
43
  UseActorMethodParameters,
47
44
  } from "./hooks/useActorMethod"
45
+ import { InfiniteQueryConfig } from "./createInfiniteQuery"
46
+ import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery"
48
47
  import { QueryConfig, SuspenseQueryConfig, MutationConfig } from "./types"
49
48
 
50
49
  export type ActorHooks<A, T extends TransformKey> = {
@@ -108,33 +107,35 @@ export function createActorHooks<A, T extends TransformKey>(
108
107
  reactor: Reactor<A, T>
109
108
  ): ActorHooks<A, T> {
110
109
  return {
111
- useActorQuery: ((config: any) => {
112
- const { select, ...options } = config
113
- return createQuery(reactor, config).useQuery(options)
114
- }) as ActorHooks<A, T>["useActorQuery"],
110
+ useActorQuery: ((config: any) =>
111
+ useActorQuery({ ...config, reactor })) as ActorHooks<
112
+ A,
113
+ T
114
+ >["useActorQuery"],
115
115
 
116
- useActorSuspenseQuery: ((config: any) => {
117
- const { select, ...options } = config
118
- return createSuspenseQuery(reactor, config).useSuspenseQuery(options)
119
- }) as ActorHooks<A, T>["useActorSuspenseQuery"],
116
+ useActorSuspenseQuery: ((config: any) =>
117
+ useActorSuspenseQuery({ ...config, reactor })) as ActorHooks<
118
+ A,
119
+ T
120
+ >["useActorSuspenseQuery"],
120
121
 
121
- useActorInfiniteQuery: ((config) => {
122
- const { select, ...options } = config
123
- return createInfiniteQuery(reactor, config).useInfiniteQuery(options)
124
- }) as ActorHooks<A, T>["useActorInfiniteQuery"],
122
+ useActorInfiniteQuery: ((config: any) =>
123
+ useActorInfiniteQuery({ ...config, reactor })) as ActorHooks<
124
+ A,
125
+ T
126
+ >["useActorInfiniteQuery"],
125
127
 
126
- useActorSuspenseInfiniteQuery: ((config) => {
127
- const { select, ...options } = config
128
- return createSuspenseInfiniteQuery(
129
- reactor,
130
- config
131
- ).useSuspenseInfiniteQuery(options)
132
- }) as ActorHooks<A, T>["useActorSuspenseInfiniteQuery"],
128
+ useActorSuspenseInfiniteQuery: ((config: any) =>
129
+ useActorSuspenseInfiniteQuery({ ...config, reactor })) as ActorHooks<
130
+ A,
131
+ T
132
+ >["useActorSuspenseInfiniteQuery"],
133
133
 
134
- useActorMutation: ((config) => {
135
- const { onSuccess, invalidateQueries, ...options } = config
136
- return createMutation(reactor, config).useMutation(options)
137
- }) as ActorHooks<A, T>["useActorMutation"],
134
+ useActorMutation: ((config: any) =>
135
+ useActorMutation({ ...config, reactor })) as ActorHooks<
136
+ A,
137
+ T
138
+ >["useActorMutation"],
138
139
 
139
140
  useActorMethod: (config) =>
140
141
  useActorMethod({ ...config, reactor } as UseActorMethodParameters<
@@ -1,12 +1,26 @@
1
- import { useSyncExternalStore, useEffect, useRef, useMemo } from "react"
2
- import { ClientManager, AgentState, AuthState } from "@ic-reactor/core"
3
- import { Principal } from "@icp-sdk/core/principal"
4
- import { Identity } from "@icp-sdk/core/agent"
5
- import { AuthClientLoginOptions } from "@icp-sdk/auth/client"
1
+ import {
2
+ useSyncExternalStore,
3
+ useEffect,
4
+ useRef,
5
+ useMemo,
6
+ useState,
7
+ useCallback,
8
+ } from "react"
9
+ import {
10
+ ClientManager,
11
+ AgentState,
12
+ AuthState,
13
+ ClientManagerSignInOptions,
14
+ IdentityAttributeResult,
15
+ RequestIdentityAttributesParameters,
16
+ RequestOpenIdIdentityAttributesParameters,
17
+ } from "@ic-reactor/core"
18
+ import type { Principal } from "@icp-sdk/core/principal"
19
+ import type { Identity } from "@icp-sdk/core/agent"
6
20
 
7
21
  export interface UseAuthReturn {
8
22
  authenticate: () => Promise<Identity | undefined>
9
- login: (options?: AuthClientLoginOptions) => Promise<void>
23
+ login: (options?: ClientManagerSignInOptions) => Promise<void>
10
24
  logout: (options?: { returnTo?: string }) => Promise<void>
11
25
  isAuthenticated: boolean
12
26
  isAuthenticating: boolean
@@ -19,6 +33,20 @@ export interface CreateAuthHooksReturn {
19
33
  useAgentState: () => AgentState
20
34
  useUserPrincipal: () => Principal | null
21
35
  useAuth: () => UseAuthReturn
36
+ useIdentityAttributes: () => UseIdentityAttributesReturn
37
+ }
38
+
39
+ export interface UseIdentityAttributesReturn {
40
+ requestAttributes: (
41
+ params: RequestIdentityAttributesParameters
42
+ ) => Promise<IdentityAttributeResult>
43
+ requestOpenIdAttributes: (
44
+ params: RequestOpenIdIdentityAttributesParameters
45
+ ) => Promise<IdentityAttributeResult>
46
+ attributes: IdentityAttributeResult | null
47
+ isRequestingAttributes: boolean
48
+ attributeError: Error | null
49
+ clearAttributes: () => void
22
50
  }
23
51
 
24
52
  /**
@@ -129,9 +157,68 @@ export const createAuthHooks = (
129
157
  return identity ? identity.getPrincipal() : null
130
158
  }
131
159
 
160
+ const useIdentityAttributes = (): UseIdentityAttributesReturn => {
161
+ const [attributes, setAttributes] =
162
+ useState<IdentityAttributeResult | null>(null)
163
+ const [isRequestingAttributes, setIsRequestingAttributes] = useState(false)
164
+ const [attributeError, setAttributeError] = useState<Error | null>(null)
165
+
166
+ const requestAttributes = useCallback(
167
+ async (params: RequestIdentityAttributesParameters) => {
168
+ setIsRequestingAttributes(true)
169
+ setAttributeError(null)
170
+ try {
171
+ const result = await clientManager.requestIdentityAttributes(params)
172
+ setAttributes(result)
173
+ return result
174
+ } catch (error) {
175
+ setAttributeError(error as Error)
176
+ throw error
177
+ } finally {
178
+ setIsRequestingAttributes(false)
179
+ }
180
+ },
181
+ []
182
+ )
183
+
184
+ const requestOpenIdAttributes = useCallback(
185
+ async (params: RequestOpenIdIdentityAttributesParameters) => {
186
+ setIsRequestingAttributes(true)
187
+ setAttributeError(null)
188
+ try {
189
+ const result =
190
+ await clientManager.requestOpenIdIdentityAttributes(params)
191
+ setAttributes(result)
192
+ return result
193
+ } catch (error) {
194
+ setAttributeError(error as Error)
195
+ throw error
196
+ } finally {
197
+ setIsRequestingAttributes(false)
198
+ }
199
+ },
200
+ []
201
+ )
202
+
203
+ const clearAttributes = useCallback(() => {
204
+ setAttributes(null)
205
+ setAttributeError(null)
206
+ }, [])
207
+
208
+ return {
209
+ requestAttributes,
210
+ requestOpenIdAttributes,
211
+ attributes,
212
+ isRequestingAttributes,
213
+ attributeError,
214
+ clearAttributes,
215
+ }
216
+ }
217
+
132
218
  return {
133
219
  useAuth,
134
220
  useAgentState,
135
221
  useUserPrincipal,
222
+ useIdentityAttributes,
136
223
  }
137
224
  }
@@ -43,8 +43,7 @@ import {
43
43
  } from "@tanstack/react-query"
44
44
  import { CallConfig } from "@icp-sdk/core/agent"
45
45
  import { NoInfer } from "./types"
46
-
47
- const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
46
+ import { mergeFactoryQueryKey } from "./utils"
48
47
 
49
48
  type InfiniteQueryFactoryFn<
50
49
  A,
@@ -63,22 +62,6 @@ type InfiniteQueryFactoryFn<
63
62
  >
64
63
  }
65
64
 
66
- const mergeFactoryQueryKey = (
67
- baseQueryKey?: QueryKey,
68
- keyArgs?: unknown
69
- ): QueryKey | undefined => {
70
- const merged: unknown[] = []
71
-
72
- if (baseQueryKey) {
73
- merged.push(...baseQueryKey)
74
- }
75
- if (keyArgs !== undefined) {
76
- merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
77
- }
78
-
79
- return merged.length > 0 ? merged : undefined
80
- }
81
-
82
65
  // ============================================================================
83
66
  // Type Definitions
84
67
  // ============================================================================
@@ -510,7 +493,7 @@ export function createInfiniteQueryFactory<
510
493
  ) => {
511
494
  const initialArgs = getArgs(config.initialPageParam)
512
495
  const keyArgs = config.getKeyArgs?.(initialArgs) ?? initialArgs
513
- const queryKey = mergeFactoryQueryKey(config.queryKey, keyArgs)
496
+ const queryKey = mergeFactoryQueryKey(config.queryKey, undefined, keyArgs)
514
497
 
515
498
  return createInfiniteQueryImpl<A, M, T, TPageParam, TSelected>(reactor, {
516
499
  ...(({ getKeyArgs: _getKeyArgs, ...rest }) => rest)(
@@ -30,6 +30,22 @@ import type {
30
30
  NoInfer,
31
31
  } from "./types"
32
32
 
33
+ // ============================================================================
34
+ // Internal helpers
35
+ // ============================================================================
36
+
37
+ /** Invalidate a list of query keys in parallel, filtering out undefineds. */
38
+ async function invalidateAll(
39
+ queryClient: Reactor<any, any>["queryClient"],
40
+ keys: (import("@tanstack/react-query").QueryKey | undefined)[]
41
+ ): Promise<void> {
42
+ await Promise.all(
43
+ keys.map((queryKey) =>
44
+ queryKey ? queryClient.invalidateQueries({ queryKey }) : Promise.resolve()
45
+ )
46
+ )
47
+ }
48
+
33
49
  // ============================================================================
34
50
  // Internal Implementation
35
51
  // ============================================================================
@@ -52,113 +68,80 @@ const createMutationImpl = <
52
68
  ...factoryOptions
53
69
  } = config
54
70
 
55
- // Direct execution function
71
+ /**
72
+ * Raw call without any invalidation logic.
73
+ * Used as mutationFn so that onSuccess handles all post-mutation work
74
+ * and there is no double-invalidation.
75
+ */
76
+ const callFn = (
77
+ args: ReactorArgs<A, M, T>
78
+ ): Promise<ReactorReturnOk<A, M, T>> =>
79
+ reactor.callMethod({ functionName, args, callConfig })
80
+
81
+ /**
82
+ * Imperative execution for non-React usage.
83
+ * Calls the canister method and invalidates factory-level queries.
84
+ * Use this in route loaders, scripts, or server-side code.
85
+ */
56
86
  const execute = async (
57
87
  args: ReactorArgs<A, M, T>
58
88
  ): Promise<ReactorReturnOk<A, M, T>> => {
59
- const result = await reactor.callMethod({
60
- functionName,
61
- args,
62
- callConfig,
63
- })
64
-
89
+ const result = await callFn(args)
65
90
  if (factoryInvalidateQueries) {
66
- await Promise.all(
67
- factoryInvalidateQueries.map((queryKey) => {
68
- return reactor.queryClient.invalidateQueries({ queryKey })
69
- })
70
- )
91
+ await invalidateAll(reactor.queryClient, factoryInvalidateQueries)
71
92
  }
72
-
73
93
  return result
74
94
  }
75
95
 
76
96
  // Hook implementation
77
97
  const useMutationHook = (options?: MutationHookOptions<A, M, T>) => {
78
- const baseOptions = reactor.getQueryOptions({
79
- functionName,
80
- })
81
- // Extract our custom options
98
+ const baseOptions = reactor.getQueryOptions({ functionName })
82
99
  const {
83
100
  invalidateQueries: hookInvalidateQueries,
84
101
  onCanisterError: hookOnCanisterError,
85
102
  ...restOptions
86
- } = options || {}
103
+ } = options ?? {}
87
104
 
88
105
  return useMutation(
89
106
  {
90
107
  mutationKey: baseOptions.queryKey,
91
108
  ...factoryOptions,
92
109
  ...restOptions,
93
- mutationFn: execute,
110
+ // Use callFn (not execute) to avoid double-invalidation:
111
+ // factoryInvalidateQueries are handled in onSuccess below.
112
+ mutationFn: callFn,
94
113
  onSuccess: async (...args) => {
95
- // 1. Handle factory-level invalidateQueries
114
+ // 1. Factory-level invalidation
96
115
  if (factoryInvalidateQueries) {
97
- await Promise.all(
98
- factoryInvalidateQueries.map((queryKey) => {
99
- return reactor.queryClient.invalidateQueries({ queryKey })
100
- })
101
- )
116
+ await invalidateAll(reactor.queryClient, factoryInvalidateQueries)
102
117
  }
103
-
104
- // 2. Handle hook-level invalidateQueries
118
+ // 2. Hook-level invalidation
105
119
  if (hookInvalidateQueries) {
106
- await Promise.all(
107
- hookInvalidateQueries.map((queryKey) => {
108
- if (queryKey) {
109
- return reactor.queryClient.invalidateQueries({ queryKey })
110
- }
111
- return Promise.resolve()
112
- })
113
- )
114
- }
115
-
116
- // 3. Call factory onSuccess
117
- if (factoryOnSuccess) {
118
- await factoryOnSuccess(...args)
119
- }
120
-
121
- // 4. Call hook-local onSuccess
122
- if (restOptions?.onSuccess) {
123
- await restOptions.onSuccess(...args)
120
+ await invalidateAll(reactor.queryClient, hookInvalidateQueries)
124
121
  }
122
+ // 3. Factory onSuccess
123
+ await factoryOnSuccess?.(...args)
124
+ // 4. Hook onSuccess
125
+ await restOptions.onSuccess?.(...args)
125
126
  },
126
127
  onError: (error, variables, context, mutation) => {
127
- // Check if this is a CanisterError (from Result { Err: E })
128
128
  if (isCanisterError(error)) {
129
- // 1. Call factory-level onCanisterError
130
- if (factoryOnCanisterError) {
131
- factoryOnCanisterError(error, variables)
132
- }
133
- // 2. Call hook-level onCanisterError
134
- if (hookOnCanisterError) {
135
- hookOnCanisterError(error, variables)
136
- }
137
- }
138
-
139
- // 3. Call factory-level onError (for all errors)
140
- if (factoryOnError) {
141
- factoryOnError(error, variables, context, mutation)
142
- }
143
-
144
- // 4. Call hook-level onError (for all errors)
145
- if (restOptions?.onError) {
146
- restOptions.onError(error, variables, context, mutation)
129
+ factoryOnCanisterError?.(error, variables)
130
+ hookOnCanisterError?.(error, variables)
147
131
  }
132
+ factoryOnError?.(error, variables, context, mutation)
133
+ restOptions.onError?.(error, variables, context, mutation)
148
134
  },
149
135
  },
150
136
  reactor.queryClient
151
137
  )
152
138
  }
153
139
 
154
- return {
155
- useMutation: useMutationHook,
156
- execute,
157
- }
140
+ return { useMutation: useMutationHook, execute }
158
141
  }
159
142
 
160
143
  // ============================================================================
161
- // Factory Function
144
+ // Public Factory Function
162
145
  // ============================================================================
163
146
 
164
147
  export function createMutation<