@ic-reactor/react 3.0.0-beta.8 → 3.0.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 (46) hide show
  1. package/README.md +11 -10
  2. package/dist/createActorHooks.d.ts +2 -0
  3. package/dist/createActorHooks.d.ts.map +1 -1
  4. package/dist/createActorHooks.js +2 -0
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createMutation.d.ts.map +1 -1
  7. package/dist/createMutation.js +4 -0
  8. package/dist/createMutation.js.map +1 -1
  9. package/dist/hooks/index.d.ts +18 -5
  10. package/dist/hooks/index.d.ts.map +1 -1
  11. package/dist/hooks/index.js +15 -5
  12. package/dist/hooks/index.js.map +1 -1
  13. package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
  14. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  15. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  16. package/dist/hooks/useActorMethod.d.ts +105 -0
  17. package/dist/hooks/useActorMethod.d.ts.map +1 -0
  18. package/dist/hooks/useActorMethod.js +192 -0
  19. package/dist/hooks/useActorMethod.js.map +1 -0
  20. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
  21. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  22. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  23. package/package.json +7 -6
  24. package/src/createActorHooks.ts +146 -0
  25. package/src/createAuthHooks.ts +137 -0
  26. package/src/createInfiniteQuery.ts +471 -0
  27. package/src/createMutation.ts +163 -0
  28. package/src/createQuery.ts +197 -0
  29. package/src/createSuspenseInfiniteQuery.ts +478 -0
  30. package/src/createSuspenseQuery.ts +215 -0
  31. package/src/hooks/index.ts +93 -0
  32. package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
  33. package/src/hooks/useActorInfiniteQuery.ts +134 -0
  34. package/src/hooks/useActorMethod.test.tsx +798 -0
  35. package/src/hooks/useActorMethod.ts +397 -0
  36. package/src/hooks/useActorMutation.test.tsx +220 -0
  37. package/src/hooks/useActorMutation.ts +124 -0
  38. package/src/hooks/useActorQuery.test.tsx +287 -0
  39. package/src/hooks/useActorQuery.ts +110 -0
  40. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
  41. package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
  42. package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
  43. package/src/hooks/useActorSuspenseQuery.ts +112 -0
  44. package/src/index.ts +21 -0
  45. package/src/types.ts +435 -0
  46. package/src/validation.ts +202 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Infinite Query Factory - Generic wrapper for React Query paginated canister data
3
+ *
4
+ * Creates unified fetch/hook/invalidate functions for any paginated canister method.
5
+ * Works with any Reactor instance.
6
+ *
7
+ * @example
8
+ * const postsQuery = createInfiniteQuery(reactor, {
9
+ * functionName: "get_posts",
10
+ * initialPageParam: 0,
11
+ * getArgs: (cursor) => [{ cursor, limit: 10 }],
12
+ * getNextPageParam: (lastPage) => lastPage.nextCursor,
13
+ * })
14
+ *
15
+ * // In component
16
+ * const { data, fetchNextPage, hasNextPage } = postsQuery.useInfiniteQuery()
17
+ *
18
+ * // Flatten all pages
19
+ * const allPosts = data?.pages.flatMap(page => page.posts)
20
+ *
21
+ * // Invalidate cache
22
+ * postsQuery.invalidate()
23
+ */
24
+
25
+ import type {
26
+ Reactor,
27
+ FunctionName,
28
+ ReactorArgs,
29
+ BaseActor,
30
+ TransformKey,
31
+ ReactorReturnOk,
32
+ ReactorReturnErr,
33
+ } from "@ic-reactor/core"
34
+ import {
35
+ QueryKey,
36
+ useInfiniteQuery,
37
+ InfiniteData,
38
+ UseInfiniteQueryResult,
39
+ UseInfiniteQueryOptions,
40
+ QueryFunctionContext,
41
+ FetchInfiniteQueryOptions,
42
+ } from "@tanstack/react-query"
43
+ import { CallConfig } from "@icp-sdk/core/agent"
44
+ import { NoInfer } from "./types"
45
+
46
+ // ============================================================================
47
+ // Type Definitions
48
+ // ============================================================================
49
+
50
+ /** The raw page data type returned by the query function */
51
+ export type InfiniteQueryPageData<
52
+ A = BaseActor,
53
+ M extends FunctionName<A> = FunctionName<A>,
54
+ T extends TransformKey = "candid",
55
+ > = ReactorReturnOk<A, M, T>
56
+
57
+ /** The error type for infinite queries */
58
+ export type InfiniteQueryError<
59
+ A = BaseActor,
60
+ M extends FunctionName<A> = FunctionName<A>,
61
+ T extends TransformKey = "candid",
62
+ > = ReactorReturnErr<A, M, T>
63
+
64
+ // ============================================================================
65
+ // Configuration Types
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Configuration for createActorInfiniteQuery.
70
+ *
71
+ * @template A - The actor interface type
72
+ * @template M - The method name on the actor
73
+ * @template T - The transformation key (identity, display, etc.)
74
+ * @template TPageParam - The type of the page parameter
75
+ * @template TSelected - The type returned after select transformation
76
+ */
77
+ export interface InfiniteQueryConfig<
78
+ A = BaseActor,
79
+ M extends FunctionName<A> = FunctionName<A>,
80
+ T extends TransformKey = "candid",
81
+ TPageParam = unknown,
82
+ TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
83
+ > {
84
+ /** The method to call on the canister */
85
+ functionName: M
86
+ /** Call configuration for the actor method */
87
+ callConfig?: CallConfig
88
+ /** Custom query key (optional, auto-generated if not provided) */
89
+ queryKey?: QueryKey
90
+ /** Initial page parameter */
91
+ initialPageParam: TPageParam
92
+ /** Function to get args from page parameter */
93
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
94
+ /** Function to determine next page parameter */
95
+ getNextPageParam: (
96
+ lastPage: InfiniteQueryPageData<A, M, T>,
97
+ allPages: InfiniteQueryPageData<A, M, T>[],
98
+ lastPageParam: TPageParam,
99
+ allPageParams: TPageParam[]
100
+ ) => TPageParam | undefined | null
101
+ /** Function to determine previous page parameter (for bi-directional) */
102
+ getPreviousPageParam?: (
103
+ firstPage: InfiniteQueryPageData<A, M, T>,
104
+ allPages: InfiniteQueryPageData<A, M, T>[],
105
+ firstPageParam: TPageParam,
106
+ allPageParams: TPageParam[]
107
+ ) => TPageParam | undefined | null
108
+ /** Maximum number of pages to keep in cache */
109
+ maxPages?: number
110
+ /** How long data stays fresh before becoming stale (default: 5 min) */
111
+ staleTime?: number
112
+ /** Transform the raw InfiniteData result before returning */
113
+ select?: (
114
+ data: InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>
115
+ ) => TSelected
116
+ }
117
+
118
+ /**
119
+ * Configuration for createActorInfiniteQueryFactory (without initialPageParam, getArgs determined at call time).
120
+ */
121
+ export type InfiniteQueryFactoryConfig<
122
+ A = BaseActor,
123
+ M extends FunctionName<A> = FunctionName<A>,
124
+ T extends TransformKey = "candid",
125
+ TPageParam = unknown,
126
+ TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
127
+ > = Omit<InfiniteQueryConfig<A, M, T, TPageParam, TSelected>, "getArgs">
128
+
129
+ // ============================================================================
130
+ // Hook Interface
131
+ // ============================================================================
132
+
133
+ /**
134
+ * useInfiniteQuery hook with chained select support.
135
+ */
136
+ export interface UseInfiniteQueryWithSelect<
137
+ TPageData,
138
+ TPageParam,
139
+ TSelected = InfiniteData<TPageData, TPageParam>,
140
+ TError = Error,
141
+ > {
142
+ // Overload 1: Without select - returns TSelected
143
+ (
144
+ options?: Omit<
145
+ UseInfiniteQueryOptions<
146
+ TPageData,
147
+ TError,
148
+ TSelected,
149
+ QueryKey,
150
+ TPageParam
151
+ >,
152
+ | "select"
153
+ | "queryKey"
154
+ | "queryFn"
155
+ | "initialPageParam"
156
+ | "getNextPageParam"
157
+ | "getPreviousPageParam"
158
+ >
159
+ ): UseInfiniteQueryResult<TSelected, TError>
160
+
161
+ // Overload 2: With select - chains on top and returns TFinal
162
+ <TFinal = TSelected>(
163
+ options: Omit<
164
+ UseInfiniteQueryOptions<TPageData, TError, TFinal, QueryKey, TPageParam>,
165
+ | "queryKey"
166
+ | "queryFn"
167
+ | "select"
168
+ | "initialPageParam"
169
+ | "getNextPageParam"
170
+ | "getPreviousPageParam"
171
+ > & {
172
+ select: (data: TSelected) => TFinal
173
+ }
174
+ ): UseInfiniteQueryResult<TFinal, TError>
175
+ }
176
+
177
+ // ============================================================================
178
+ // Result Interface
179
+ // ============================================================================
180
+
181
+ /**
182
+ * Result from createActorInfiniteQuery
183
+ *
184
+ * @template TPageData - The raw page data type
185
+ * @template TPageParam - The page parameter type
186
+ * @template TSelected - The type after select transformation
187
+ * @template TError - The error type
188
+ */
189
+ export interface InfiniteQueryResult<
190
+ TPageData,
191
+ TPageParam,
192
+ TSelected = InfiniteData<TPageData, TPageParam>,
193
+ TError = Error,
194
+ > {
195
+ /** Fetch first page in loader (uses ensureQueryData for cache-first) */
196
+ fetch: () => Promise<TSelected>
197
+
198
+ /** React hook for components - supports pagination */
199
+ useInfiniteQuery: UseInfiniteQueryWithSelect<
200
+ TPageData,
201
+ TPageParam,
202
+ TSelected,
203
+ TError
204
+ >
205
+
206
+ /** Invalidate the cache (refetches if query is active) */
207
+ invalidate: () => Promise<void>
208
+
209
+ /** Get query key (for advanced React Query usage) */
210
+ getQueryKey: () => QueryKey
211
+
212
+ /**
213
+ * Read data directly from cache without fetching.
214
+ * Returns undefined if data is not in cache.
215
+ */
216
+ getCacheData: {
217
+ (): TSelected | undefined
218
+ <TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
219
+ }
220
+ }
221
+
222
+ // ============================================================================
223
+ // Internal Implementation
224
+ // ============================================================================
225
+
226
+ const createInfiniteQueryImpl = <
227
+ A,
228
+ M extends FunctionName<A> = FunctionName<A>,
229
+ T extends TransformKey = "candid",
230
+ TPageParam = unknown,
231
+ TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
232
+ >(
233
+ reactor: Reactor<A, T>,
234
+ config: InfiniteQueryConfig<A, M, T, TPageParam, TSelected>
235
+ ): InfiniteQueryResult<
236
+ InfiniteQueryPageData<A, M, T>,
237
+ TPageParam,
238
+ TSelected,
239
+ InfiniteQueryError<A, M, T>
240
+ > => {
241
+ type TPageData = InfiniteQueryPageData<A, M, T>
242
+ type TError = InfiniteQueryError<A, M, T>
243
+ type TInfiniteData = InfiniteData<TPageData, TPageParam>
244
+
245
+ const {
246
+ functionName,
247
+ callConfig,
248
+ queryKey: customQueryKey,
249
+ initialPageParam,
250
+ getArgs,
251
+ getNextPageParam,
252
+ getPreviousPageParam,
253
+ maxPages,
254
+ staleTime = 5 * 60 * 1000,
255
+ select,
256
+ } = config
257
+
258
+ // Get query key from actor manager
259
+ const getQueryKey = (): QueryKey => {
260
+ return reactor.generateQueryKey({
261
+ functionName,
262
+ queryKey: customQueryKey,
263
+ })
264
+ }
265
+
266
+ // Query function - accepts QueryFunctionContext
267
+ const queryFn = async (
268
+ context: QueryFunctionContext<QueryKey, TPageParam>
269
+ ): Promise<TPageData> => {
270
+ // pageParam is typed as unknown in QueryFunctionContext, but we know its type
271
+ const pageParam = context.pageParam as TPageParam
272
+ const args = getArgs(pageParam)
273
+ const result = await reactor.callMethod({
274
+ functionName,
275
+ args,
276
+ callConfig,
277
+ })
278
+ return result
279
+ }
280
+
281
+ // Get infinite query options for fetchInfiniteQuery
282
+ const getInfiniteQueryOptions = (): FetchInfiniteQueryOptions<
283
+ TPageData,
284
+ TError,
285
+ TPageData,
286
+ QueryKey,
287
+ TPageParam
288
+ > => ({
289
+ queryKey: getQueryKey(),
290
+ queryFn,
291
+ initialPageParam,
292
+ getNextPageParam,
293
+ })
294
+
295
+ // Fetch function for loaders (cache-first, fetches first page)
296
+ const fetch = async (): Promise<TSelected> => {
297
+ // Check cache first
298
+ const cachedData = reactor.queryClient.getQueryData(getQueryKey()) as
299
+ | TInfiniteData
300
+ | undefined
301
+
302
+ if (cachedData !== undefined) {
303
+ return select ? select(cachedData) : (cachedData as TSelected)
304
+ }
305
+
306
+ // Fetch if not in cache
307
+ const result = await reactor.queryClient.fetchInfiniteQuery(
308
+ getInfiniteQueryOptions()
309
+ )
310
+
311
+ // Result is already InfiniteData format
312
+ return select ? select(result) : (result as unknown as TSelected)
313
+ }
314
+
315
+ // Implementation
316
+ const useInfiniteQueryHook: UseInfiniteQueryWithSelect<
317
+ TPageData,
318
+ TPageParam,
319
+ TSelected,
320
+ TError
321
+ > = (options: any): any => {
322
+ // Chain the selects: raw -> config.select -> options.select
323
+ const chainedSelect = (rawData: TInfiniteData) => {
324
+ const firstPass = select ? select(rawData) : rawData
325
+ if (options?.select) {
326
+ return options.select(firstPass)
327
+ }
328
+ return firstPass
329
+ }
330
+
331
+ return useInfiniteQuery(
332
+ {
333
+ queryKey: getQueryKey(),
334
+ queryFn,
335
+ initialPageParam,
336
+ getNextPageParam,
337
+ getPreviousPageParam,
338
+ maxPages,
339
+ staleTime,
340
+ ...options,
341
+ select: chainedSelect,
342
+ } as any,
343
+ reactor.queryClient
344
+ )
345
+ }
346
+
347
+ // Invalidate function
348
+ const invalidate = async (): Promise<void> => {
349
+ const queryKey = getQueryKey()
350
+ await reactor.queryClient.invalidateQueries({ queryKey })
351
+ }
352
+
353
+ // Get data from cache without fetching
354
+ const getCacheData: any = (selectFn?: (data: TSelected) => any) => {
355
+ const queryKey = getQueryKey()
356
+ const cachedRawData = reactor.queryClient.getQueryData(
357
+ queryKey
358
+ ) as TInfiniteData
359
+
360
+ if (cachedRawData === undefined) {
361
+ return undefined
362
+ }
363
+
364
+ // Apply config.select to raw cache data
365
+ const selectedData = (
366
+ select ? select(cachedRawData) : cachedRawData
367
+ ) as TSelected
368
+
369
+ // Apply optional select parameter
370
+ if (selectFn) {
371
+ return selectFn(selectedData)
372
+ }
373
+
374
+ return selectedData
375
+ }
376
+
377
+ return {
378
+ fetch,
379
+ useInfiniteQuery: useInfiniteQueryHook,
380
+ invalidate,
381
+ getQueryKey,
382
+ getCacheData,
383
+ }
384
+ }
385
+
386
+ // ============================================================================
387
+ // Factory Function
388
+ // ============================================================================
389
+
390
+ export function createInfiniteQuery<
391
+ A,
392
+ T extends TransformKey,
393
+ M extends FunctionName<A> = FunctionName<A>,
394
+ TPageParam = unknown,
395
+ TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
396
+ >(
397
+ reactor: Reactor<A, T>,
398
+ config: InfiniteQueryConfig<NoInfer<A>, M, T, TPageParam, TSelected>
399
+ ): InfiniteQueryResult<
400
+ InfiniteQueryPageData<A, M, T>,
401
+ TPageParam,
402
+ TSelected,
403
+ InfiniteQueryError<A, M, T>
404
+ > {
405
+ return createInfiniteQueryImpl(
406
+ reactor,
407
+ config as InfiniteQueryConfig<A, M, T, TPageParam, TSelected>
408
+ )
409
+ }
410
+
411
+ // ============================================================================
412
+ // Factory with Dynamic Args
413
+ // ============================================================================
414
+
415
+ /**
416
+ * Create an infinite query factory that accepts getArgs at call time.
417
+ * Useful when pagination logic varies by context.
418
+ *
419
+ * @template A - The actor interface type
420
+ * @template M - The method name on the actor
421
+ * @template T - The transformation key (identity, display, etc.)
422
+ * @template TPageParam - The page parameter type
423
+ * @template TSelected - The type returned after select transformation
424
+ *
425
+ * @param reactor - The Reactor instance
426
+ * @param config - Infinite query configuration (without getArgs)
427
+ * @returns A function that accepts getArgs and returns an ActorInfiniteQueryResult
428
+ *
429
+ * @example
430
+ * const getPostsQuery = createActorInfiniteQueryFactory(reactor, {
431
+ * functionName: "get_posts",
432
+ * initialPageParam: 0,
433
+ * getNextPageParam: (lastPage) => lastPage.nextCursor,
434
+ * })
435
+ *
436
+ * // Create query with specific args builder
437
+ * const userPostsQuery = getPostsQuery((cursor) => [{ userId, cursor, limit: 10 }])
438
+ * const { data, fetchNextPage } = userPostsQuery.useInfiniteQuery()
439
+ */
440
+
441
+ export function createInfiniteQueryFactory<
442
+ A,
443
+ T extends TransformKey,
444
+ M extends FunctionName<A> = FunctionName<A>,
445
+ TPageParam = unknown,
446
+ TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
447
+ >(
448
+ reactor: Reactor<A, T>,
449
+ config: InfiniteQueryFactoryConfig<NoInfer<A>, M, T, TPageParam, TSelected>
450
+ ): (
451
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
452
+ ) => InfiniteQueryResult<
453
+ InfiniteQueryPageData<A, M, T>,
454
+ TPageParam,
455
+ TSelected,
456
+ InfiniteQueryError<A, M, T>
457
+ > {
458
+ return (
459
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
460
+ ): InfiniteQueryResult<
461
+ InfiniteQueryPageData<A, M, T>,
462
+ TPageParam,
463
+ TSelected,
464
+ InfiniteQueryError<A, M, T>
465
+ > => {
466
+ return createInfiniteQueryImpl<A, M, T, TPageParam, TSelected>(reactor, {
467
+ ...(config as InfiniteQueryFactoryConfig<A, M, T, TPageParam, TSelected>),
468
+ getArgs,
469
+ })
470
+ }
471
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Mutation Factory - Generic wrapper for mutating canister data
3
+ *
4
+ * Creates unified mutation hooks for any canister method.
5
+ * Works with any Reactor instance.
6
+ *
7
+ * @example
8
+ * const transferMutation = createMutation(reactor, {
9
+ * functionName: "transfer",
10
+ * onSuccess: () => console.log("Success!"),
11
+ * })
12
+ *
13
+ * // In component
14
+ * const { mutate, isPending } = transferMutation.useMutation()
15
+ */
16
+
17
+ import { useMutation } from "@tanstack/react-query"
18
+ import type {
19
+ Reactor,
20
+ FunctionName,
21
+ ReactorArgs,
22
+ TransformKey,
23
+ ReactorReturnOk,
24
+ } from "@ic-reactor/core"
25
+ import { isCanisterError } from "@ic-reactor/core"
26
+ import type {
27
+ MutationConfig,
28
+ MutationResult,
29
+ MutationHookOptions,
30
+ NoInfer,
31
+ } from "./types"
32
+
33
+ // ============================================================================
34
+ // Internal Implementation
35
+ // ============================================================================
36
+
37
+ const createMutationImpl = <
38
+ A,
39
+ M extends FunctionName<A> = FunctionName<A>,
40
+ T extends TransformKey = "candid",
41
+ >(
42
+ reactor: Reactor<A, T>,
43
+ config: MutationConfig<A, M, T>
44
+ ): MutationResult<A, M, T> => {
45
+ const {
46
+ functionName,
47
+ callConfig,
48
+ invalidateQueries: factoryInvalidateQueries,
49
+ onSuccess: factoryOnSuccess,
50
+ onCanisterError: factoryOnCanisterError,
51
+ onError: factoryOnError,
52
+ ...factoryOptions
53
+ } = config
54
+
55
+ // Direct execution function
56
+ const execute = async (
57
+ args: ReactorArgs<A, M, T>
58
+ ): Promise<ReactorReturnOk<A, M, T>> => {
59
+ return reactor.callMethod({
60
+ functionName,
61
+ args,
62
+ callConfig,
63
+ })
64
+ }
65
+
66
+ // Hook implementation
67
+ const useMutationHook = (options?: MutationHookOptions<A, M, T>) => {
68
+ const baseOptions = reactor.getQueryOptions({
69
+ functionName,
70
+ })
71
+ // Extract our custom options
72
+ const {
73
+ invalidateQueries: hookInvalidateQueries,
74
+ onCanisterError: hookOnCanisterError,
75
+ ...restOptions
76
+ } = options || {}
77
+
78
+ return useMutation(
79
+ {
80
+ mutationKey: baseOptions.queryKey,
81
+ ...factoryOptions,
82
+ ...restOptions,
83
+ mutationFn: execute,
84
+ onSuccess: async (...args) => {
85
+ // 1. Handle factory-level invalidateQueries
86
+ if (factoryInvalidateQueries) {
87
+ await Promise.all(
88
+ factoryInvalidateQueries.map((queryKey) => {
89
+ return reactor.queryClient.invalidateQueries({ queryKey })
90
+ })
91
+ )
92
+ }
93
+
94
+ // 2. Handle hook-level invalidateQueries
95
+ if (hookInvalidateQueries) {
96
+ await Promise.all(
97
+ hookInvalidateQueries.map((queryKey) => {
98
+ if (queryKey) {
99
+ return reactor.queryClient.invalidateQueries({ queryKey })
100
+ }
101
+ return Promise.resolve()
102
+ })
103
+ )
104
+ }
105
+
106
+ // 3. Call factory onSuccess
107
+ if (factoryOnSuccess) {
108
+ await factoryOnSuccess(...args)
109
+ }
110
+
111
+ // 4. Call hook-local onSuccess
112
+ if (restOptions?.onSuccess) {
113
+ await restOptions.onSuccess(...args)
114
+ }
115
+ },
116
+ onError: (error, variables, context, mutation) => {
117
+ // Check if this is a CanisterError (from Result { Err: E })
118
+ if (isCanisterError(error)) {
119
+ // 1. Call factory-level onCanisterError
120
+ if (factoryOnCanisterError) {
121
+ factoryOnCanisterError(error, variables)
122
+ }
123
+ // 2. Call hook-level onCanisterError
124
+ if (hookOnCanisterError) {
125
+ hookOnCanisterError(error, variables)
126
+ }
127
+ }
128
+
129
+ // 3. Call factory-level onError (for all errors)
130
+ if (factoryOnError) {
131
+ factoryOnError(error, variables, context, mutation)
132
+ }
133
+
134
+ // 4. Call hook-level onError (for all errors)
135
+ if (restOptions?.onError) {
136
+ restOptions.onError(error, variables, context, mutation)
137
+ }
138
+ },
139
+ },
140
+ reactor.queryClient
141
+ )
142
+ }
143
+
144
+ return {
145
+ useMutation: useMutationHook,
146
+ execute,
147
+ }
148
+ }
149
+
150
+ // ============================================================================
151
+ // Factory Function
152
+ // ============================================================================
153
+
154
+ export function createMutation<
155
+ A,
156
+ T extends TransformKey,
157
+ M extends FunctionName<A> = FunctionName<A>,
158
+ >(
159
+ reactor: Reactor<A, T>,
160
+ config: MutationConfig<NoInfer<A>, M, T>
161
+ ): MutationResult<A, M, T> {
162
+ return createMutationImpl(reactor, config as MutationConfig<A, M, T>)
163
+ }