@ic-reactor/react 3.12.4 → 3.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.
Files changed (152) hide show
  1. package/README.md +410 -44
  2. package/dist/auth/auth-client-compat.d.ts +122 -0
  3. package/dist/auth/auth-client-compat.d.ts.map +1 -0
  4. package/dist/auth/auth-client-compat.js +162 -0
  5. package/dist/auth/auth-client-compat.js.map +1 -0
  6. package/dist/auth/authentication-manager.d.ts +287 -5
  7. package/dist/auth/authentication-manager.d.ts.map +1 -1
  8. package/dist/auth/authentication-manager.js +920 -150
  9. package/dist/auth/authentication-manager.js.map +1 -1
  10. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  11. package/dist/auth/createIdentityAttributeHooks.js +36 -20
  12. package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
  13. package/dist/auth/identity-attributes-manager.d.ts +2 -1
  14. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  15. package/dist/auth/identity-attributes-manager.js +90 -6
  16. package/dist/auth/identity-attributes-manager.js.map +1 -1
  17. package/dist/auth/identity-attributes.d.ts.map +1 -1
  18. package/dist/auth/identity-attributes.js +57 -0
  19. package/dist/auth/identity-attributes.js.map +1 -1
  20. package/dist/auth/local-ii-probe.d.ts +12 -1
  21. package/dist/auth/local-ii-probe.d.ts.map +1 -1
  22. package/dist/auth/local-ii-probe.js +22 -3
  23. package/dist/auth/local-ii-probe.js.map +1 -1
  24. package/dist/auth/types.d.ts +48 -5
  25. package/dist/auth/types.d.ts.map +1 -1
  26. package/dist/createActorHooks.d.ts +9 -20
  27. package/dist/createActorHooks.d.ts.map +1 -1
  28. package/dist/createActorHooks.js.map +1 -1
  29. package/dist/createInfiniteQuery.d.ts +51 -10
  30. package/dist/createInfiniteQuery.d.ts.map +1 -1
  31. package/dist/createInfiniteQuery.js +39 -15
  32. package/dist/createInfiniteQuery.js.map +1 -1
  33. package/dist/createMutation.d.ts +4 -1
  34. package/dist/createMutation.d.ts.map +1 -1
  35. package/dist/createMutation.js +121 -84
  36. package/dist/createMutation.js.map +1 -1
  37. package/dist/createQuery.d.ts +35 -2
  38. package/dist/createQuery.d.ts.map +1 -1
  39. package/dist/createQuery.js +104 -17
  40. package/dist/createQuery.js.map +1 -1
  41. package/dist/createReactorProvider.d.ts +158 -0
  42. package/dist/createReactorProvider.d.ts.map +1 -0
  43. package/dist/createReactorProvider.js +256 -0
  44. package/dist/createReactorProvider.js.map +1 -0
  45. package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
  46. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  47. package/dist/createSuspenseInfiniteQuery.js +59 -27
  48. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  49. package/dist/createSuspenseQuery.d.ts +23 -2
  50. package/dist/createSuspenseQuery.d.ts.map +1 -1
  51. package/dist/createSuspenseQuery.js +68 -21
  52. package/dist/createSuspenseQuery.js.map +1 -1
  53. package/dist/defineDisplayReactor.d.ts +43 -0
  54. package/dist/defineDisplayReactor.d.ts.map +1 -0
  55. package/dist/defineDisplayReactor.js +42 -0
  56. package/dist/defineDisplayReactor.js.map +1 -0
  57. package/dist/defineReactor.d.ts +46 -72
  58. package/dist/defineReactor.d.ts.map +1 -1
  59. package/dist/defineReactor.js +11 -176
  60. package/dist/defineReactor.js.map +1 -1
  61. package/dist/defineReactorShared.d.ts +84 -0
  62. package/dist/defineReactorShared.d.ts.map +1 -0
  63. package/dist/defineReactorShared.js +139 -0
  64. package/dist/defineReactorShared.js.map +1 -0
  65. package/dist/hooks/createAuthHooks.d.ts +9 -2
  66. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  67. package/dist/hooks/createAuthHooks.js +184 -24
  68. package/dist/hooks/createAuthHooks.js.map +1 -1
  69. package/dist/hooks/useActorInfiniteQuery.d.ts +36 -8
  70. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  71. package/dist/hooks/useActorInfiniteQuery.js +54 -21
  72. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  73. package/dist/hooks/useActorMethod.d.ts +37 -4
  74. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  75. package/dist/hooks/useActorMethod.js +201 -57
  76. package/dist/hooks/useActorMethod.js.map +1 -1
  77. package/dist/hooks/useActorMutation.d.ts +15 -12
  78. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  79. package/dist/hooks/useActorMutation.js +14 -13
  80. package/dist/hooks/useActorMutation.js.map +1 -1
  81. package/dist/hooks/useActorQuery.d.ts +17 -4
  82. package/dist/hooks/useActorQuery.d.ts.map +1 -1
  83. package/dist/hooks/useActorQuery.js +30 -9
  84. package/dist/hooks/useActorQuery.js.map +1 -1
  85. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +17 -5
  86. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  87. package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
  88. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  89. package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
  90. package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
  91. package/dist/hooks/useActorSuspenseQuery.js +20 -9
  92. package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
  93. package/dist/index.d.ts +4 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +6 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/ownedAuthentication.d.ts +52 -0
  98. package/dist/ownedAuthentication.d.ts.map +1 -0
  99. package/dist/ownedAuthentication.js +49 -0
  100. package/dist/ownedAuthentication.js.map +1 -0
  101. package/dist/server.d.ts +21 -0
  102. package/dist/server.d.ts.map +1 -0
  103. package/dist/server.js +23 -0
  104. package/dist/server.js.map +1 -0
  105. package/dist/testing.d.ts +19 -0
  106. package/dist/testing.d.ts.map +1 -0
  107. package/dist/testing.js +19 -0
  108. package/dist/testing.js.map +1 -0
  109. package/dist/types.d.ts +428 -21
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +1 -1
  112. package/dist/utils.d.ts +159 -3
  113. package/dist/utils.d.ts.map +1 -1
  114. package/dist/utils.js +301 -1
  115. package/dist/utils.js.map +1 -1
  116. package/dist/validation.d.ts +12 -7
  117. package/dist/validation.d.ts.map +1 -1
  118. package/dist/validation.js +34 -15
  119. package/dist/validation.js.map +1 -1
  120. package/llms.txt +259 -33
  121. package/package.json +17 -5
  122. package/src/auth/auth-client-compat.ts +273 -0
  123. package/src/auth/authentication-manager.ts +918 -96
  124. package/src/auth/createIdentityAttributeHooks.ts +47 -21
  125. package/src/auth/identity-attributes-manager.ts +100 -5
  126. package/src/auth/identity-attributes.ts +75 -0
  127. package/src/auth/local-ii-probe.ts +29 -3
  128. package/src/auth/types.ts +49 -6
  129. package/src/createActorHooks.ts +50 -42
  130. package/src/createInfiniteQuery.ts +120 -28
  131. package/src/createMutation.ts +213 -132
  132. package/src/createQuery.ts +164 -32
  133. package/src/createReactorProvider.ts +365 -0
  134. package/src/createSuspenseInfiniteQuery.ts +93 -43
  135. package/src/createSuspenseQuery.ts +102 -32
  136. package/src/defineDisplayReactor.ts +62 -0
  137. package/src/defineReactor.ts +81 -263
  138. package/src/defineReactorShared.ts +268 -0
  139. package/src/hooks/createAuthHooks.ts +210 -28
  140. package/src/hooks/useActorInfiniteQuery.ts +156 -55
  141. package/src/hooks/useActorMethod.ts +295 -92
  142. package/src/hooks/useActorMutation.ts +42 -30
  143. package/src/hooks/useActorQuery.ts +43 -10
  144. package/src/hooks/useActorSuspenseInfiniteQuery.ts +110 -54
  145. package/src/hooks/useActorSuspenseQuery.ts +30 -15
  146. package/src/index.ts +8 -0
  147. package/src/ownedAuthentication.ts +81 -0
  148. package/src/server.ts +23 -0
  149. package/src/testing.ts +18 -0
  150. package/src/types.ts +492 -22
  151. package/src/utils.ts +387 -3
  152. package/src/validation.ts +43 -19
@@ -3,7 +3,6 @@ import {
3
3
  useMutation,
4
4
  UseMutationOptions,
5
5
  UseMutationResult,
6
- QueryKey,
7
6
  } from "@tanstack/react-query"
8
7
  import {
9
8
  Reactor,
@@ -19,16 +18,20 @@ import {
19
18
  TransformReturnRegistry,
20
19
  } from "@ic-reactor/core"
21
20
  import { CallConfig } from "@icp-sdk/core/agent"
21
+ import { invalidateTargets, useMountQueryClient } from "../utils.js"
22
+ import type { InvalidationTarget } from "../types.js"
22
23
 
23
24
  export interface UseActorMutationParameters<
24
25
  Service,
25
26
  Method extends FunctionName<Service>,
26
27
  Transform extends TransformKey = "candid",
28
+ TOnMutateResult = unknown,
27
29
  > extends Omit<
28
30
  UseMutationOptions<
29
31
  ReactorReturnOk<Service, Method, Transform>,
30
32
  ReactorReturnErr<Service, Method, Transform>,
31
- ReactorArgs<Service, Method, Transform>
33
+ ReactorArgs<Service, Method, Transform>,
34
+ TOnMutateResult
32
35
  >,
33
36
  "mutationFn"
34
37
  > {
@@ -36,13 +39,15 @@ export interface UseActorMutationParameters<
36
39
  functionName: Method
37
40
  callConfig?: CallConfig
38
41
  /**
39
- * Queries to invalidate upon successful mutation.
42
+ * Queries to invalidate upon successful mutation, awaited before
43
+ * `onSuccess` runs.
40
44
  *
41
- * `undefined` entries are skipped, so the common
42
- * `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
43
- * is absent.
45
+ * Each entry is a query key, a query object or query factory, or a
46
+ * `{ functionName, args? }` method of `reactor`; see
47
+ * {@link InvalidationTarget}. `undefined` entries are skipped, so
48
+ * `[maybeQuery]` is safe when the optional query object is absent.
44
49
  */
45
- invalidateQueries?: (QueryKey | undefined)[]
50
+ invalidateQueries?: InvalidationTarget<Service, Transform>[]
46
51
  /**
47
52
  * Callback for canister-level business logic errors.
48
53
  * Called when the canister returns a Result { Err: E } variant.
@@ -51,7 +56,8 @@ export interface UseActorMutationParameters<
51
56
  onCanisterError?: (
52
57
  error: CanisterError<
53
58
  TransformReturnRegistry<
54
- ErrResult<ActorMethodReturnType<Service[Method]>>
59
+ ErrResult<ActorMethodReturnType<Service[Method]>>,
60
+ Service
55
61
  >[Transform]
56
62
  >,
57
63
  variables: ReactorArgs<Service, Method, Transform>
@@ -62,16 +68,22 @@ export type UseActorMutationConfig<
62
68
  Service,
63
69
  Method extends FunctionName<Service>,
64
70
  Transform extends TransformKey = "candid",
65
- > = Omit<UseActorMutationParameters<Service, Method, Transform>, "reactor">
71
+ TOnMutateResult = unknown,
72
+ > = Omit<
73
+ UseActorMutationParameters<Service, Method, Transform, TOnMutateResult>,
74
+ "reactor"
75
+ >
66
76
 
67
77
  export type UseActorMutationResult<
68
78
  Service,
69
79
  Method extends FunctionName<Service>,
70
80
  Transform extends TransformKey = "candid",
81
+ TOnMutateResult = unknown,
71
82
  > = UseMutationResult<
72
83
  ReactorReturnOk<Service, Method, Transform>,
73
84
  ReactorReturnErr<Service, Method, Transform>,
74
- ReactorArgs<Service, Method, Transform>
85
+ ReactorArgs<Service, Method, Transform>,
86
+ TOnMutateResult
75
87
  >
76
88
 
77
89
  /**
@@ -80,7 +92,7 @@ export type UseActorMutationResult<
80
92
  * For non-React usage, prefer `createMutation(...).execute(args)`.
81
93
  *
82
94
  * @example
83
- * const { mutate, isPending } = useActorMutation({
95
+ * const { mutate, isPending } = useReactorMutation({
84
96
  * reactor,
85
97
  * functionName: "transfer",
86
98
  * onSuccess: () => console.log("Success!"),
@@ -100,6 +112,7 @@ export const useActorMutation = <
100
112
  Service,
101
113
  Method extends FunctionName<Service>,
102
114
  Transform extends TransformKey = "candid",
115
+ TOnMutateResult = unknown,
103
116
  >({
104
117
  reactor,
105
118
  functionName,
@@ -112,8 +125,11 @@ export const useActorMutation = <
112
125
  }: UseActorMutationParameters<
113
126
  Service,
114
127
  Method,
115
- Transform
116
- >): UseActorMutationResult<Service, Method, Transform> => {
128
+ Transform,
129
+ TOnMutateResult
130
+ >): UseActorMutationResult<Service, Method, Transform, TOnMutateResult> => {
131
+ useMountQueryClient(reactor.queryClient)
132
+
117
133
  const mutationFn = useCallback(
118
134
  async (args: ReactorArgs<Service, Method, Transform>) =>
119
135
  reactor.callMethod({ functionName, callConfig, args }),
@@ -127,29 +143,19 @@ export const useActorMutation = <
127
143
  UseMutationOptions<
128
144
  ReactorReturnOk<Service, Method, Transform>,
129
145
  ReactorReturnErr<Service, Method, Transform>,
130
- ReactorArgs<Service, Method, Transform>
146
+ ReactorArgs<Service, Method, Transform>,
147
+ TOnMutateResult
131
148
  >["onSuccess"]
132
149
  >
133
150
  >
134
151
  ) => {
135
- if (invalidateQueries) {
136
- // Skip undefined entries. React Query reads `{ queryKey: undefined }`
137
- // as "match everything", so a single undefined — which the natural
138
- // `invalidateQueries: [maybeQuery?.getQueryKey()]` idiom produces
139
- // whenever the optional query object is absent — would invalidate every
140
- // query in the client, including an app's unrelated non-canister ones.
141
- // `createMutation.invalidateAll` already filters the same way.
142
- await Promise.all(
143
- invalidateQueries
144
- .filter((queryKey) => queryKey !== undefined)
145
- .map((queryKey) =>
146
- reactor.queryClient.invalidateQueries({ queryKey })
147
- )
148
- )
149
- }
152
+ // Undefined entries are skipped: React Query reads
153
+ // `{ queryKey: undefined }` as "match everything", and the natural
154
+ // `[maybeQuery]` idiom produces one whenever the query is absent.
155
+ await invalidateTargets(reactor, invalidateQueries, callConfig)
150
156
  await onSuccess?.(...params)
151
157
  },
152
- [reactor, invalidateQueries, onSuccess]
158
+ [reactor, invalidateQueries, onSuccess, callConfig]
153
159
  )
154
160
 
155
161
  const handleError = useCallback(
@@ -183,6 +189,12 @@ export const useActorMutation = <
183
189
  // rather than the reference.
184
190
  return useMutation(
185
191
  {
192
+ // Keyed like `createMutation`'s hook and `useActorMethod`'s mutations,
193
+ // so `useIsMutating({ mutationKey })`, `useMutationState` and defaults
194
+ // registered with `setMutationDefaults` can find this one too. An
195
+ // explicit `mutationKey` in the options still wins.
196
+ mutationKey: reactor.getQueryOptions({ functionName, callConfig })
197
+ .queryKey,
186
198
  ...options,
187
199
  mutationFn,
188
200
  onSuccess: handleSuccess,
@@ -2,8 +2,10 @@ import { useMemo } from "react"
2
2
  import {
3
3
  QueryKey,
4
4
  useQuery,
5
+ skipToken,
5
6
  QueryObserverOptions,
6
7
  UseQueryResult,
8
+ type SkipToken,
7
9
  } from "@tanstack/react-query"
8
10
  import {
9
11
  FunctionName,
@@ -15,6 +17,7 @@ import {
15
17
  ReactorReturnErr,
16
18
  } from "@ic-reactor/core"
17
19
  import { CallConfig } from "@icp-sdk/core/agent"
20
+ import { retryOption, skippedQueryKey, useMountQueryClient } from "../utils.js"
18
21
 
19
22
  export interface UseActorQueryParameters<
20
23
  Service,
@@ -33,7 +36,13 @@ export interface UseActorQueryParameters<
33
36
  > {
34
37
  reactor: Reactor<Service, Transform>
35
38
  functionName: Method
36
- args?: ReactorArgs<Service, Method, Transform>
39
+ /**
40
+ * The method's arguments, or TanStack Query's `skipToken` while they are
41
+ * not known: the query then waits without fetching, in an entry of its own
42
+ * under its method's key, which no call's key shares, so it shows no data
43
+ * until the arguments arrive.
44
+ */
45
+ args?: ReactorArgs<Service, Method, Transform> | SkipToken
37
46
  callConfig?: CallConfig
38
47
  queryKey?: QueryKey
39
48
  }
@@ -59,19 +68,26 @@ export type UseActorQueryResult<
59
68
  * Hook for executing query calls on a canister.
60
69
  *
61
70
  * @example
62
- * const { data, isLoading } = useActorQuery({
71
+ * const { data, isLoading } = useReactorQuery({
63
72
  * reactor,
64
73
  * functionName: "getUser",
65
74
  * args: ["user-123"],
66
75
  * })
67
76
  *
68
77
  * // With select transformation
69
- * const { data } = useActorQuery({
78
+ * const { data } = useReactorQuery({
70
79
  * reactor,
71
80
  * functionName: "getUser",
72
81
  * args: ["user-123"],
73
82
  * select: (user) => user.name,
74
83
  * })
84
+ *
85
+ * // Wait for the args: no fetch until userId is known
86
+ * const { data } = useReactorQuery({
87
+ * reactor,
88
+ * functionName: "getUser",
89
+ * args: userId ? [userId] : skipToken,
90
+ * })
75
91
  */
76
92
  export const useActorQuery = <
77
93
  Service,
@@ -91,15 +107,31 @@ export const useActorQuery = <
91
107
  Transform,
92
108
  Selected
93
109
  >): UseActorQueryResult<Service, Method, Transform, Selected> => {
110
+ useMountQueryClient(reactor.queryClient)
111
+
94
112
  // Memoize query options to prevent unnecessary re-computations
95
- const { queryKey, queryFn } = useMemo(
113
+ // For an update method, the options also carry its default `retry`; see
114
+ // `Reactor.getQueryRetry`.
115
+ const { queryKey, queryFn, retry } = useMemo(
96
116
  () =>
97
- reactor.getQueryOptions<Method>({
98
- callConfig,
99
- functionName,
100
- args,
101
- queryKey: defaultQueryKey,
102
- }),
117
+ args === skipToken
118
+ ? // Waiting for its args: an entry of its own under the method's key,
119
+ // with nothing to run until then; see skippedQueryKey.
120
+ {
121
+ queryKey: skippedQueryKey(
122
+ reactor.generateQueryKey({ functionName }, callConfig),
123
+ "query"
124
+ ),
125
+ // Kept as the unique symbol, which an object literal widens.
126
+ queryFn: skipToken as SkipToken,
127
+ retry: undefined,
128
+ }
129
+ : reactor.getQueryOptions<Method>({
130
+ callConfig,
131
+ functionName,
132
+ args,
133
+ queryKey: defaultQueryKey,
134
+ }),
103
135
  // `canisterId` is mutable reactor state that `setCanisterId` can change,
104
136
  // while `reactor` itself stays the same object — so it has to be a
105
137
  // dependency in its own right or the key stays pinned to the old canister.
@@ -118,6 +150,7 @@ export const useActorQuery = <
118
150
  queryFn,
119
151
  ...options,
120
152
  queryKey,
153
+ ...retryOption(options.retry, retry),
121
154
  },
122
155
  reactor.queryClient
123
156
  )
@@ -16,7 +16,14 @@ import {
16
16
  ReactorReturnErr,
17
17
  } from "@ic-reactor/core"
18
18
  import { CallConfig } from "@icp-sdk/core/agent"
19
- import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
19
+ import {
20
+ callConfigForKey,
21
+ mergeFactoryQueryKey,
22
+ mountWhileSuspended,
23
+ normalizeQueryData,
24
+ retryOption,
25
+ useMountQueryClient,
26
+ } from "../utils.js"
20
27
 
21
28
  /**
22
29
  * Parameters for useActorSuspenseInfiniteQuery hook.
@@ -47,6 +54,18 @@ export interface UseActorSuspenseInfiniteQueryParameters<
47
54
  functionName: Method
48
55
  /** Function to get args from page parameter */
49
56
  getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>
57
+ /**
58
+ * Narrows what the cache key derives from the call arguments.
59
+ *
60
+ * By default the key is scoped by `getArgs(initialPageParam)`, so two
61
+ * infinite queries on the same method with different arguments stay in
62
+ * separate cache entries. Supply this when those args embed the cursor and
63
+ * only part of them identifies the query — return the stable, serializable
64
+ * portion (typically everything except the pagination field). It is required
65
+ * when `initialPageParam` changes between renders, as `Date.now()` does:
66
+ * without it every render keys a new query.
67
+ */
68
+ getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
50
69
  /** Agent call configuration (effectiveCanisterId, etc.) */
51
70
  callConfig?: CallConfig
52
71
  /** Custom query key (auto-generated if not provided) */
@@ -67,12 +86,17 @@ export type UseActorSuspenseInfiniteQueryConfig<
67
86
  Method extends FunctionName<Service>,
68
87
  Transform extends TransformKey = "candid",
69
88
  TPageParam = unknown,
89
+ Selected = InfiniteData<
90
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
91
+ TPageParam
92
+ >,
70
93
  > = Omit<
71
94
  UseActorSuspenseInfiniteQueryParameters<
72
95
  Service,
73
96
  Method,
74
97
  Transform,
75
- TPageParam
98
+ TPageParam,
99
+ Selected
76
100
  >,
77
101
  "reactor"
78
102
  >
@@ -82,11 +106,12 @@ export type UseActorSuspenseInfiniteQueryResult<
82
106
  Method extends FunctionName<Service>,
83
107
  Transform extends TransformKey = "candid",
84
108
  TPageParam = unknown,
85
- > = UseSuspenseInfiniteQueryResult<
86
- InfiniteData<
109
+ Selected = InfiniteData<
87
110
  ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
88
111
  TPageParam
89
112
  >,
113
+ > = UseSuspenseInfiniteQueryResult<
114
+ Selected,
90
115
  ReactorReturnErr<Service, Method, Transform>
91
116
  >
92
117
 
@@ -94,10 +119,10 @@ export type UseActorSuspenseInfiniteQueryResult<
94
119
  * Hook for executing suspense-enabled infinite/paginated query calls on a canister.
95
120
  *
96
121
  * @example
97
- * const { data, fetchNextPage, hasNextPage } = useActorSuspenseInfiniteQuery({
122
+ * const { data, fetchNextPage, hasNextPage } = useReactorSuspenseInfiniteQuery({
98
123
  * reactor,
99
124
  * functionName: "getItems",
100
- * getArgs: (pageParam) => [{ offset: pageParam, limit: 10 }],
125
+ * getArgs: (pageParam) => [{ offset: pageParam, limit: 10 }] as const,
101
126
  * initialPageParam: 0,
102
127
  * getNextPageParam: (lastPage) => lastPage.nextOffset,
103
128
  * })
@@ -107,10 +132,15 @@ export const useActorSuspenseInfiniteQuery = <
107
132
  Method extends FunctionName<Service>,
108
133
  Transform extends TransformKey = "candid",
109
134
  TPageParam = unknown,
135
+ Selected = InfiniteData<
136
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
137
+ TPageParam
138
+ >,
110
139
  >({
111
140
  reactor,
112
141
  functionName,
113
142
  getArgs,
143
+ getKeyArgs,
114
144
  callConfig,
115
145
  queryKey,
116
146
  ...options
@@ -118,56 +148,68 @@ export const useActorSuspenseInfiniteQuery = <
118
148
  Service,
119
149
  Method,
120
150
  Transform,
121
- TPageParam
151
+ TPageParam,
152
+ Selected
122
153
  >): UseActorSuspenseInfiniteQueryResult<
123
154
  Service,
124
155
  Method,
125
156
  Transform,
126
- TPageParam
157
+ TPageParam,
158
+ Selected
127
159
  > => {
160
+ useMountQueryClient(reactor.queryClient)
161
+
128
162
  // Always pass queryKey through generateQueryKey so it is merged with the
129
163
  // reactor/function identity. Using the custom key verbatim would cause cache
130
164
  // collisions if two different actors or methods share the same key string.
131
- const baseQueryKey = useMemo(
132
- () =>
133
- reactor.generateQueryKey(
134
- {
135
- functionName,
136
- // Fold the call arguments into the key. They live in the `getArgs`
137
- // closure rather than in the config, so without this two hooks on the
138
- // same method with different arguments share one cache entry and
139
- // serve each other's pages.
140
- queryKey: mergeFactoryQueryKey(
141
- queryKey,
142
- undefined,
143
- getArgs(options.initialPageParam)
144
- ),
145
- },
146
- callConfig
147
- ),
148
- [
149
- queryKey,
150
- reactor,
151
- // `canisterId` is mutable reactor state that `setCanisterId` can change,
152
- // while `reactor` itself stays the same object — so it has to be a
153
- // dependency in its own right or the key stays pinned to the old canister
154
- // while the queryFn already calls the new one.
155
- reactor.canisterId?.toString(),
156
- functionName,
157
- callConfig,
158
- getArgs,
159
- options.initialPageParam,
160
- ]
161
- )
165
+ const baseQueryKey = useMemo(() => {
166
+ // Fold the call arguments into the key. They live in the `getArgs`
167
+ // closure rather than in the config, so without this two hooks on the
168
+ // same method with different arguments share one cache entry and serve
169
+ // each other's pages. `getKeyArgs` narrows them exactly as it does for the
170
+ // factories, which the bound hook's config type always accepted: it used
171
+ // to be ignored here, so the cursor stayed in the key, and an
172
+ // `initialPageParam` that changes every render (`Date.now()`) keyed a new
173
+ // query on every render — an endless loop of first-page fetches.
174
+ const initialArgs = getArgs(options.initialPageParam)
175
+ const keyArgs = getKeyArgs?.(initialArgs) ?? initialArgs
176
+
177
+ return reactor.generateQueryKey(
178
+ {
179
+ functionName,
180
+ queryKey: mergeFactoryQueryKey(queryKey, undefined, keyArgs),
181
+ },
182
+ callConfig
183
+ )
184
+ }, [
185
+ queryKey,
186
+ reactor,
187
+ // `canisterId` is mutable reactor state that `setCanisterId` can change,
188
+ // while `reactor` itself stays the same object — so it has to be a
189
+ // dependency in its own right or the key stays pinned to the old canister
190
+ // while the queryFn already calls the new one.
191
+ reactor.canisterId?.toString(),
192
+ functionName,
193
+ callConfig,
194
+ getArgs,
195
+ getKeyArgs,
196
+ options.initialPageParam,
197
+ ])
162
198
 
163
199
  // Memoize queryFn to prevent recreation on every render
164
200
  const queryFn = useCallback(
165
- async ({ pageParam }: { pageParam: TPageParam }) => {
201
+ async ({
202
+ pageParam,
203
+ queryKey: fetchedKey,
204
+ }: {
205
+ pageParam: TPageParam
206
+ queryKey: QueryKey
207
+ }) => {
166
208
  const args = getArgs(pageParam)
167
209
  const result = await reactor.callMethod({
168
210
  functionName,
169
211
  args,
170
- callConfig,
212
+ callConfig: callConfigForKey(fetchedKey, callConfig),
171
213
  })
172
214
  return normalizeQueryData<ReactorReturnOk<Service, Method, Transform>>(
173
215
  result as ReactorReturnOk<Service, Method, Transform>
@@ -176,17 +218,31 @@ export const useActorSuspenseInfiniteQuery = <
176
218
  [reactor, functionName, getArgs, callConfig]
177
219
  )
178
220
 
179
- return useSuspenseInfiniteQuery(
180
- {
181
- queryKey: baseQueryKey,
182
- queryFn,
183
- ...options,
184
- } as any,
185
- reactor.queryClient
186
- ) as UseActorSuspenseInfiniteQueryResult<
187
- Service,
188
- Method,
189
- Transform,
190
- TPageParam
191
- >
221
+ // The method's default `retry`: for an update method, only failures that
222
+ // prove the canister never ran the call; see `Reactor.getQueryRetry`.
223
+ const defaultRetry = useMemo(
224
+ () => reactor.getQueryRetry(functionName, baseQueryKey),
225
+ [reactor, functionName, baseQueryKey]
226
+ )
227
+
228
+ try {
229
+ return useSuspenseInfiniteQuery(
230
+ {
231
+ queryKey: baseQueryKey,
232
+ queryFn,
233
+ ...options,
234
+ ...retryOption(options.retry, defaultRetry),
235
+ } as any,
236
+ reactor.queryClient
237
+ ) as UseActorSuspenseInfiniteQueryResult<
238
+ Service,
239
+ Method,
240
+ Transform,
241
+ TPageParam,
242
+ Selected
243
+ >
244
+ } catch (thrown) {
245
+ mountWhileSuspended(reactor.queryClient, thrown)
246
+ throw thrown
247
+ }
192
248
  }
@@ -16,6 +16,11 @@ import {
16
16
  ReactorReturnErr,
17
17
  } from "@ic-reactor/core"
18
18
  import { CallConfig } from "@icp-sdk/core/agent"
19
+ import {
20
+ mountWhileSuspended,
21
+ retryOption,
22
+ useMountQueryClient,
23
+ } from "../utils.js"
19
24
 
20
25
  export interface UseActorSuspenseQueryParameters<
21
26
  Service,
@@ -63,14 +68,14 @@ export type UseActorSuspenseQueryResult<
63
68
  * Hook for executing suspense-enabled query calls on a canister.
64
69
  *
65
70
  * @example
66
- * const { data } = useActorSuspenseQuery({
71
+ * const { data } = useReactorSuspenseQuery({
67
72
  * reactor,
68
73
  * functionName: "getUser",
69
74
  * args: ["user-123"],
70
75
  * })
71
76
  *
72
77
  * // With select transformation
73
- * const { data } = useActorSuspenseQuery({
78
+ * const { data } = useReactorSuspenseQuery({
74
79
  * reactor,
75
80
  * functionName: "getUser",
76
81
  * args: ["user-123"],
@@ -95,8 +100,12 @@ export const useActorSuspenseQuery = <
95
100
  Transform,
96
101
  Selected
97
102
  >): UseActorSuspenseQueryResult<Service, Method, Transform, Selected> => {
103
+ useMountQueryClient(reactor.queryClient)
104
+
98
105
  // Memoize query options to prevent unnecessary re-computations
99
- const { queryKey, queryFn } = useMemo(
106
+ // For an update method, the options also carry its default `retry`; see
107
+ // `Reactor.getQueryRetry`.
108
+ const { queryKey, queryFn, retry } = useMemo(
100
109
  () =>
101
110
  reactor.getQueryOptions<Method>({
102
111
  callConfig,
@@ -117,16 +126,22 @@ export const useActorSuspenseQuery = <
117
126
  ]
118
127
  )
119
128
 
120
- return useSuspenseQuery(
121
- {
122
- // Suspense queries don't support skipToken, so cast the queryFn
123
- queryFn: queryFn as QueryFunction<
124
- ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
125
- QueryKey
126
- >,
127
- ...options,
128
- queryKey,
129
- },
130
- reactor.queryClient
131
- )
129
+ try {
130
+ return useSuspenseQuery(
131
+ {
132
+ // Suspense queries don't support skipToken, so cast the queryFn
133
+ queryFn: queryFn as QueryFunction<
134
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
135
+ QueryKey
136
+ >,
137
+ ...options,
138
+ queryKey,
139
+ ...retryOption(options.retry, retry),
140
+ },
141
+ reactor.queryClient
142
+ )
143
+ } catch (thrown) {
144
+ mountWhileSuspended(reactor.queryClient, thrown)
145
+ throw thrown
146
+ }
132
147
  }
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@ export * from "./validation.js"
11
11
  // React-specific exports
12
12
  export * from "./createActorHooks.js"
13
13
  export * from "./defineReactor.js"
14
+ export * from "./defineDisplayReactor.js"
15
+ export * from "./createReactorProvider.js"
14
16
 
15
17
  export * from "./createQuery.js"
16
18
  export * from "./createSuspenseQuery.js"
@@ -21,3 +23,9 @@ export * from "./createMutation.js"
21
23
  export * from "./auth/index.js"
22
24
 
23
25
  export * from "./types.js"
26
+
27
+ // TanStack Query's marker for a query whose args are not known yet, which the
28
+ // non-suspense query hooks and createQueryFactory take in place of args. It is
29
+ // the same symbol as @tanstack/react-query's, so either import works.
30
+ export { skipToken } from "@tanstack/react-query"
31
+ export type { SkipToken } from "@tanstack/react-query"