@ic-reactor/react 3.13.0 → 4.0.0-beta.2

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 (174) hide show
  1. package/README.md +237 -787
  2. package/dist/index.d.ts +260 -16
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +471 -23
  5. package/dist/index.js.map +1 -1
  6. package/llms.txt +82 -278
  7. package/package.json +11 -39
  8. package/src/index.tsx +612 -0
  9. package/dist/auth/auth-client-compat.d.ts +0 -122
  10. package/dist/auth/auth-client-compat.d.ts.map +0 -1
  11. package/dist/auth/auth-client-compat.js +0 -162
  12. package/dist/auth/auth-client-compat.js.map +0 -1
  13. package/dist/auth/authentication-manager.d.ts +0 -405
  14. package/dist/auth/authentication-manager.d.ts.map +0 -1
  15. package/dist/auth/authentication-manager.js +0 -1537
  16. package/dist/auth/authentication-manager.js.map +0 -1
  17. package/dist/auth/constants.d.ts +0 -24
  18. package/dist/auth/constants.d.ts.map +0 -1
  19. package/dist/auth/constants.js +0 -24
  20. package/dist/auth/constants.js.map +0 -1
  21. package/dist/auth/createIdentityAttributeHooks.d.ts +0 -14
  22. package/dist/auth/createIdentityAttributeHooks.d.ts.map +0 -1
  23. package/dist/auth/createIdentityAttributeHooks.js +0 -122
  24. package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
  25. package/dist/auth/identity-attributes-manager.d.ts +0 -27
  26. package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
  27. package/dist/auth/identity-attributes-manager.js +0 -191
  28. package/dist/auth/identity-attributes-manager.js.map +0 -1
  29. package/dist/auth/identity-attributes.d.ts +0 -19
  30. package/dist/auth/identity-attributes.d.ts.map +0 -1
  31. package/dist/auth/identity-attributes.js +0 -227
  32. package/dist/auth/identity-attributes.js.map +0 -1
  33. package/dist/auth/index.d.ts +0 -8
  34. package/dist/auth/index.d.ts.map +0 -1
  35. package/dist/auth/index.js +0 -8
  36. package/dist/auth/index.js.map +0 -1
  37. package/dist/auth/local-ii-probe.d.ts +0 -57
  38. package/dist/auth/local-ii-probe.d.ts.map +0 -1
  39. package/dist/auth/local-ii-probe.js +0 -121
  40. package/dist/auth/local-ii-probe.js.map +0 -1
  41. package/dist/auth/types.d.ts +0 -222
  42. package/dist/auth/types.d.ts.map +0 -1
  43. package/dist/auth/types.js +0 -2
  44. package/dist/auth/types.js.map +0 -1
  45. package/dist/createActorHooks.d.ts +0 -41
  46. package/dist/createActorHooks.d.ts.map +0 -1
  47. package/dist/createActorHooks.js +0 -17
  48. package/dist/createActorHooks.js.map +0 -1
  49. package/dist/createInfiniteQuery.d.ts +0 -185
  50. package/dist/createInfiniteQuery.d.ts.map +0 -1
  51. package/dist/createInfiniteQuery.js +0 -198
  52. package/dist/createInfiniteQuery.js.map +0 -1
  53. package/dist/createMutation.d.ts +0 -33
  54. package/dist/createMutation.d.ts.map +0 -1
  55. package/dist/createMutation.js +0 -199
  56. package/dist/createMutation.js.map +0 -1
  57. package/dist/createQuery.d.ts +0 -63
  58. package/dist/createQuery.d.ts.map +0 -1
  59. package/dist/createQuery.js +0 -204
  60. package/dist/createQuery.js.map +0 -1
  61. package/dist/createReactorProvider.d.ts +0 -158
  62. package/dist/createReactorProvider.d.ts.map +0 -1
  63. package/dist/createReactorProvider.js +0 -256
  64. package/dist/createReactorProvider.js.map +0 -1
  65. package/dist/createSuspenseInfiniteQuery.d.ts +0 -154
  66. package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
  67. package/dist/createSuspenseInfiniteQuery.js +0 -209
  68. package/dist/createSuspenseInfiniteQuery.js.map +0 -1
  69. package/dist/createSuspenseQuery.d.ts +0 -46
  70. package/dist/createSuspenseQuery.d.ts.map +0 -1
  71. package/dist/createSuspenseQuery.js +0 -158
  72. package/dist/createSuspenseQuery.js.map +0 -1
  73. package/dist/defineDisplayReactor.d.ts +0 -43
  74. package/dist/defineDisplayReactor.d.ts.map +0 -1
  75. package/dist/defineDisplayReactor.js +0 -42
  76. package/dist/defineDisplayReactor.js.map +0 -1
  77. package/dist/defineReactor.d.ts +0 -99
  78. package/dist/defineReactor.d.ts.map +0 -1
  79. package/dist/defineReactor.js +0 -15
  80. package/dist/defineReactor.js.map +0 -1
  81. package/dist/defineReactorShared.d.ts +0 -84
  82. package/dist/defineReactorShared.d.ts.map +0 -1
  83. package/dist/defineReactorShared.js +0 -139
  84. package/dist/defineReactorShared.js.map +0 -1
  85. package/dist/hooks/createAuthHooks.d.ts +0 -50
  86. package/dist/hooks/createAuthHooks.d.ts.map +0 -1
  87. package/dist/hooks/createAuthHooks.js +0 -291
  88. package/dist/hooks/createAuthHooks.js.map +0 -1
  89. package/dist/hooks/index.d.ts +0 -21
  90. package/dist/hooks/index.d.ts.map +0 -1
  91. package/dist/hooks/index.js +0 -24
  92. package/dist/hooks/index.js.map +0 -1
  93. package/dist/hooks/useActorInfiniteQuery.d.ts +0 -67
  94. package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
  95. package/dist/hooks/useActorInfiniteQuery.js +0 -89
  96. package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
  97. package/dist/hooks/useActorMethod.d.ts +0 -148
  98. package/dist/hooks/useActorMethod.d.ts.map +0 -1
  99. package/dist/hooks/useActorMethod.js +0 -394
  100. package/dist/hooks/useActorMethod.js.map +0 -1
  101. package/dist/hooks/useActorMutation.d.ts +0 -51
  102. package/dist/hooks/useActorMutation.d.ts.map +0 -1
  103. package/dist/hooks/useActorMutation.js +0 -70
  104. package/dist/hooks/useActorMutation.js.map +0 -1
  105. package/dist/hooks/useActorQuery.d.ts +0 -45
  106. package/dist/hooks/useActorQuery.d.ts.map +0 -1
  107. package/dist/hooks/useActorQuery.js +0 -67
  108. package/dist/hooks/useActorQuery.js.map +0 -1
  109. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -51
  110. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
  111. package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -76
  112. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +0 -1
  113. package/dist/hooks/useActorSuspenseQuery.d.ts +0 -32
  114. package/dist/hooks/useActorSuspenseQuery.d.ts.map +0 -1
  115. package/dist/hooks/useActorSuspenseQuery.js +0 -58
  116. package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
  117. package/dist/ownedAuthentication.d.ts +0 -52
  118. package/dist/ownedAuthentication.d.ts.map +0 -1
  119. package/dist/ownedAuthentication.js +0 -49
  120. package/dist/ownedAuthentication.js.map +0 -1
  121. package/dist/server.d.ts +0 -21
  122. package/dist/server.d.ts.map +0 -1
  123. package/dist/server.js +0 -23
  124. package/dist/server.js.map +0 -1
  125. package/dist/testing.d.ts +0 -19
  126. package/dist/testing.d.ts.map +0 -1
  127. package/dist/testing.js +0 -19
  128. package/dist/testing.js.map +0 -1
  129. package/dist/types.d.ts +0 -671
  130. package/dist/types.d.ts.map +0 -1
  131. package/dist/types.js +0 -5
  132. package/dist/types.js.map +0 -1
  133. package/dist/utils.d.ts +0 -207
  134. package/dist/utils.d.ts.map +0 -1
  135. package/dist/utils.js +0 -405
  136. package/dist/utils.js.map +0 -1
  137. package/dist/validation.d.ts +0 -136
  138. package/dist/validation.d.ts.map +0 -1
  139. package/dist/validation.js +0 -144
  140. package/dist/validation.js.map +0 -1
  141. package/src/auth/auth-client-compat.ts +0 -273
  142. package/src/auth/authentication-manager.ts +0 -1682
  143. package/src/auth/constants.ts +0 -32
  144. package/src/auth/createIdentityAttributeHooks.ts +0 -169
  145. package/src/auth/identity-attributes-manager.ts +0 -226
  146. package/src/auth/identity-attributes.ts +0 -345
  147. package/src/auth/index.ts +0 -7
  148. package/src/auth/local-ii-probe.ts +0 -173
  149. package/src/auth/types.ts +0 -243
  150. package/src/createActorHooks.ts +0 -208
  151. package/src/createInfiniteQuery.ts +0 -670
  152. package/src/createMutation.ts +0 -324
  153. package/src/createQuery.ts +0 -369
  154. package/src/createReactorProvider.ts +0 -365
  155. package/src/createSuspenseInfiniteQuery.ts +0 -651
  156. package/src/createSuspenseQuery.ts +0 -304
  157. package/src/defineDisplayReactor.ts +0 -62
  158. package/src/defineReactor.ts +0 -142
  159. package/src/defineReactorShared.ts +0 -268
  160. package/src/hooks/createAuthHooks.ts +0 -371
  161. package/src/hooks/index.ts +0 -103
  162. package/src/hooks/useActorInfiniteQuery.ts +0 -278
  163. package/src/hooks/useActorMethod.ts +0 -710
  164. package/src/hooks/useActorMutation.ts +0 -205
  165. package/src/hooks/useActorQuery.ts +0 -157
  166. package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -248
  167. package/src/hooks/useActorSuspenseQuery.ts +0 -147
  168. package/src/index.ts +0 -31
  169. package/src/ownedAuthentication.ts +0 -81
  170. package/src/server.ts +0 -23
  171. package/src/testing.ts +0 -18
  172. package/src/types.ts +0 -948
  173. package/src/utils.ts +0 -505
  174. package/src/validation.ts +0 -226
@@ -1,324 +0,0 @@
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
- * Use this when one mutation should support both React hooks and imperative
7
- * execution outside React.
8
- *
9
- * @example
10
- * const transferMutation = createMutation(reactor, {
11
- * functionName: "transfer",
12
- * // Refetched before onSuccess runs: a query factory covers every args
13
- * // instance, a `{ functionName }` every query of that method
14
- * invalidateQueries: [getBalance, { functionName: "get_history" }],
15
- * onSuccess: () => console.log("Success!"),
16
- * })
17
- *
18
- * // In component
19
- * const { mutate, isPending } = transferMutation.useMutation()
20
- *
21
- * @example
22
- * const transferMutation = createMutation(reactor, {
23
- * functionName: "transfer",
24
- * onCanisterError: (err) => console.error(err.code),
25
- * })
26
- *
27
- * // Outside React (loader/service/script)
28
- * await transferMutation.execute([{ to: "aaaaa-aa", amount: "1000" }])
29
- */
30
-
31
- import {
32
- useMutation,
33
- type MutationFunctionContext,
34
- type UseMutationOptions,
35
- } from "@tanstack/react-query"
36
- import type {
37
- Reactor,
38
- FunctionName,
39
- ReactorArgs,
40
- TransformKey,
41
- ReactorReturnOk,
42
- ReactorReturnErr,
43
- } from "@ic-reactor/core"
44
- import { isCanisterError } from "@ic-reactor/core"
45
- import type {
46
- MutationConfig,
47
- MutationResult,
48
- MutationHookOptions,
49
- NoInfer,
50
- } from "./types.js"
51
- import { invalidateTargets, useMountQueryClient } from "./utils.js"
52
-
53
- // ============================================================================
54
- // Internal Implementation
55
- // ============================================================================
56
-
57
- const createMutationImpl = <
58
- Service,
59
- Method extends FunctionName<Service> = FunctionName<Service>,
60
- Transform extends TransformKey = "candid",
61
- TOnMutateResult = unknown,
62
- >(
63
- reactor: Reactor<Service, Transform>,
64
- config: MutationConfig<
65
- Service,
66
- Method,
67
- Transform,
68
- TOnMutateResult | undefined
69
- >
70
- ): MutationResult<Service, Method, Transform> => {
71
- type TData = ReactorReturnOk<Service, Method, Transform>
72
- type TError = ReactorReturnErr<Service, Method, Transform>
73
- type TVariables = ReactorArgs<Service, Method, Transform>
74
-
75
- const {
76
- functionName,
77
- callConfig,
78
- invalidateQueries: factoryInvalidateQueries,
79
- onSuccess: factoryOnSuccess,
80
- onCanisterError: factoryOnCanisterError,
81
- onError: factoryOnError,
82
- onMutate: factoryOnMutate,
83
- onSettled: factoryOnSettled,
84
- ...factoryOptions
85
- } = config
86
-
87
- /**
88
- * Raw call without any invalidation logic.
89
- * Used as mutationFn so that onSuccess handles all post-mutation work
90
- * and there is no double-invalidation.
91
- */
92
- const callFn = (args: TVariables): Promise<TData> =>
93
- reactor.callMethod({ functionName, args, callConfig })
94
-
95
- /**
96
- * The factory's own `onMutate` result for each call.
97
- *
98
- * TanStack Query stores one `onMutate` result per mutation and hands it to
99
- * every callback. That slot holds the hook's result, which the mutation also
100
- * exposes as `context`, so this map holds the factory's. Each key is the
101
- * context object TanStack Query creates for a call and passes to every
102
- * callback of that call.
103
- */
104
- const factoryOnMutateResults = new WeakMap<
105
- MutationFunctionContext,
106
- TOnMutateResult | undefined
107
- >()
108
-
109
- /**
110
- * The `onMutate` result a factory callback receives. A factory without its
111
- * own `onMutate` gets the hook's, as it always has.
112
- */
113
- const factoryOnMutateResult = (
114
- onMutateResult: unknown,
115
- context: MutationFunctionContext | undefined
116
- ) =>
117
- (factoryOnMutate && context
118
- ? factoryOnMutateResults.get(context)
119
- : onMutateResult) as TOnMutateResult | undefined
120
-
121
- /**
122
- * The options a mutation of this factory runs with, on both call paths: the
123
- * factory's config, the hook's options on top when there is a hook, and the
124
- * callbacks of both levels chained, factory first. `useMutation()` hands
125
- * them to TanStack Query's `useMutation`, and `execute()` builds a mutation
126
- * from them in the QueryClient's MutationCache.
127
- */
128
- const mutationOptions = <THookOnMutateResult = unknown>(
129
- options?: MutationHookOptions<
130
- Service,
131
- Method,
132
- Transform,
133
- THookOnMutateResult
134
- >
135
- ): UseMutationOptions<TData, TError, TVariables, THookOnMutateResult> => {
136
- const {
137
- invalidateQueries: hookInvalidateQueries,
138
- onCanisterError: hookOnCanisterError,
139
- ...restOptions
140
- } = options ?? {}
141
-
142
- return {
143
- mutationKey: reactor.getQueryOptions({ functionName }).queryKey,
144
- ...factoryOptions,
145
- ...restOptions,
146
- // Use callFn (not execute) to avoid double-invalidation:
147
- // factoryInvalidateQueries are handled in onSuccess below.
148
- mutationFn: callFn,
149
- onSuccess: async (data, variables, onMutateResult, context) => {
150
- // 1. Factory-level invalidation
151
- await invalidateTargets(reactor, factoryInvalidateQueries, callConfig)
152
- // 2. Hook-level invalidation
153
- await invalidateTargets(reactor, hookInvalidateQueries, callConfig)
154
- // 3. Factory onSuccess
155
- await factoryOnSuccess?.(
156
- data,
157
- variables,
158
- factoryOnMutateResult(onMutateResult, context),
159
- context
160
- )
161
- // 4. Hook onSuccess
162
- await restOptions.onSuccess?.(data, variables, onMutateResult, context)
163
- },
164
- onError: async (error, variables, onMutateResult, context) => {
165
- if (isCanisterError(error)) {
166
- factoryOnCanisterError?.(error, variables)
167
- hookOnCanisterError?.(error, variables)
168
- }
169
- // Awaited in order, like `onSuccess`. TanStack Query holds
170
- // `onSettled` and the settled state until this promise resolves, so
171
- // an async `onError` must be part of it.
172
- await factoryOnError?.(
173
- error,
174
- variables,
175
- factoryOnMutateResult(onMutateResult, context),
176
- context
177
- )
178
- await restOptions.onError?.(error, variables, onMutateResult, context)
179
- },
180
- // `onMutate` and `onSettled` are composed like `onSuccess`/`onError`
181
- // above. They used to arrive through the `...restOptions` spread, so a
182
- // hook-level one silently replaced the factory's — factory teardown,
183
- // telemetry or logging simply vanished the moment any call site passed
184
- // its own, with no warning and no type error. Chaining is what a
185
- // reader who has seen `onSuccess` chain already expects.
186
- onMutate: async (variables, context) => {
187
- if (factoryOnMutate) {
188
- const result = await factoryOnMutate(variables, context)
189
- if (context) factoryOnMutateResults.set(context, result)
190
- }
191
- // Without a hook-level `onMutate` this is `undefined`, and
192
- // THookOnMutateResult is then `unknown`.
193
- return (await restOptions.onMutate?.(
194
- variables,
195
- context
196
- )) as THookOnMutateResult
197
- },
198
- onSettled: async (data, error, variables, onMutateResult, context) => {
199
- await factoryOnSettled?.(
200
- data,
201
- error,
202
- variables,
203
- factoryOnMutateResult(onMutateResult, context),
204
- context
205
- )
206
- await restOptions.onSettled?.(
207
- data,
208
- error,
209
- variables,
210
- onMutateResult,
211
- context
212
- )
213
- },
214
- }
215
- }
216
-
217
- /**
218
- * Imperative execution for non-React usage.
219
- *
220
- * Builds the mutation in the QueryClient's MutationCache and runs it, as
221
- * TanStack Query's `useMutation` does, with the options the hook path uses
222
- * minus the hook's own. It used to call the canister and the factory's
223
- * callbacks itself, so the MutationCache never saw it: its global
224
- * `onError`, `onSuccess` and `onSettled` did not run, `useIsMutating` did
225
- * not count it, and neither the factory's `retry` or `networkMode` nor the
226
- * same options in the QueryClient's mutation defaults applied to the call.
227
- * The factory's `onMutate` and `onSettled` did not run either.
228
- *
229
- * Now the factory's chain runs as it does through `useMutation()`:
230
- * `onMutate`, then the factory's invalidation and `onSuccess`, or
231
- * `onCanisterError` and `onError` on failure, then `onSettled`. Each factory
232
- * callback gets the result of the factory's `onMutate` for this call. Only
233
- * hook-level callbacks are absent, because there is no hook here to supply
234
- * them.
235
- *
236
- * The QueryClient's mutation defaults apply as well, so a `mutations.retry`
237
- * there re-sends a failed call here too. Each retry of an update method is
238
- * a new call the canister runs; set `retry` on the factory (`false`, or
239
- * `reactorUpdateRetry` to retry only a SysTransient rejection) to decide it
240
- * per method.
241
- *
242
- * It resolves with the method's result. On failure it rejects with the
243
- * call's error once the callbacks have run, so `await execute(...)` still
244
- * rejects for the caller.
245
- *
246
- * Use this in route loaders, scripts, or server-side code.
247
- */
248
- const execute = async (args: TVariables): Promise<TData> => {
249
- const { queryClient } = reactor
250
- const options = mutationOptions()
251
- const { onError } = options
252
- // What the factory's `onError` or `onCanisterError` throws, if either
253
- // does. execute() has always rejected with it, and a TanStack Query
254
- // mutation at the 5.90.2 peer floor does too. Later releases reject with
255
- // the call's error and report the thrown one as an unhandled rejection,
256
- // which ends a Node script, so it is caught here and rethrown below.
257
- let thrownByOnError: { error: unknown } | undefined
258
- const mutation = queryClient
259
- .getMutationCache()
260
- .build<TData, TError, TVariables, unknown>(queryClient, {
261
- ...options,
262
- onError: async (error, variables, onMutateResult, context) => {
263
- try {
264
- await onError?.(error, variables, onMutateResult, context)
265
- } catch (thrown) {
266
- thrownByOnError = { error: thrown }
267
- }
268
- },
269
- })
270
- try {
271
- return await mutation.execute(args)
272
- } catch (error) {
273
- throw thrownByOnError ? thrownByOnError.error : error
274
- }
275
- }
276
-
277
- // Hook implementation
278
- const useMutationHook = <THookOnMutateResult = unknown>(
279
- options?: MutationHookOptions<
280
- Service,
281
- Method,
282
- Transform,
283
- THookOnMutateResult
284
- >
285
- ) => {
286
- useMountQueryClient(reactor.queryClient)
287
- return useMutation(mutationOptions(options), reactor.queryClient)
288
- }
289
-
290
- return { useMutation: useMutationHook, execute }
291
- }
292
-
293
- // ============================================================================
294
- // Public Factory Function
295
- // ============================================================================
296
-
297
- export function createMutation<
298
- Service,
299
- Transform extends TransformKey,
300
- Method extends FunctionName<Service> = FunctionName<Service>,
301
- TOnMutateResult = unknown,
302
- >(
303
- reactor: Reactor<Service, Transform>,
304
- // A factory `onError` or `onSettled` gets no `onMutate` result when
305
- // `onMutate` throws, so the result can also be `undefined`. `onSuccess`
306
- // shares the type argument. It kept the `undefined` from when `execute()`
307
- // ran no `onMutate`, although it now always gets a result.
308
- config: MutationConfig<
309
- NoInfer<Service>,
310
- Method,
311
- Transform,
312
- TOnMutateResult | undefined
313
- >
314
- ): MutationResult<Service, Method, Transform> {
315
- return createMutationImpl(
316
- reactor,
317
- config as MutationConfig<
318
- Service,
319
- Method,
320
- Transform,
321
- TOnMutateResult | undefined
322
- >
323
- )
324
- }
@@ -1,369 +0,0 @@
1
- /**
2
- * Query Factory - Generic wrapper for React Query-based canister data
3
- *
4
- * Creates unified fetch/hook/invalidate functions for any canister method.
5
- * Works with any Reactor instance.
6
- * Use this when the same read operation must work both inside React components
7
- * and outside React (loaders, actions, services, tests).
8
- *
9
- * @example
10
- * const userQuery = createQuery(todoManager, {
11
- * functionName: "get_user",
12
- * select: (result) => result.user,
13
- * })
14
- *
15
- * // In component
16
- * const { data: user } = userQuery.useQuery()
17
- *
18
- * @example
19
- * const userQuery = createQuery(todoManager, { functionName: "get_user", args: ["alice"] })
20
- *
21
- * // Outside React (loader/service/script)
22
- * await userQuery.fetch()
23
- * const cached = userQuery.getCacheData()
24
- * await userQuery.invalidate()
25
- */
26
-
27
- import type {
28
- Reactor,
29
- FunctionName,
30
- ReactorArgs,
31
- TransformKey,
32
- } from "@ic-reactor/core"
33
- import { useMemo } from "react"
34
- import {
35
- QueryKey,
36
- useQuery,
37
- skipToken,
38
- type SkipToken,
39
- type UseQueryOptions,
40
- type Updater,
41
- } from "@tanstack/react-query"
42
- import type {
43
- QueryFnData,
44
- QueryError,
45
- QueryConfig,
46
- SkippableQueryConfig,
47
- UseQueryWithSelect,
48
- QueryResult,
49
- QueryFactoryConfig,
50
- SkippableQueryFactoryFn,
51
- SkippedQuery,
52
- NoInfer,
53
- } from "./types.js"
54
- import {
55
- buildChainedSelect,
56
- createBoundedCache,
57
- pickFetchOptions,
58
- queryCacheControls,
59
- retryOption,
60
- skippedQueryKey,
61
- useMountQueryClient,
62
- withQueryFactoryMethods,
63
- } from "./utils.js"
64
-
65
- // ============================================================================
66
- // Internal Implementation
67
- // ============================================================================
68
-
69
- const createQueryImpl = <
70
- Service,
71
- Method extends FunctionName<Service> = FunctionName<Service>,
72
- Transform extends TransformKey = "candid",
73
- Selected = QueryFnData<Service, Method, Transform>,
74
- >(
75
- reactor: Reactor<Service, Transform>,
76
- // `args` is `skipToken` only for a factory's skipped query, which exposes
77
- // nothing but `useQuery`; see createQueryFactory.
78
- config: SkippableQueryConfig<Service, Method, Transform, Selected>
79
- ): QueryResult<
80
- QueryFnData<Service, Method, Transform>,
81
- Selected,
82
- QueryError<Service, Method, Transform>
83
- > => {
84
- type TData = QueryFnData<Service, Method, Transform>
85
- type TError = QueryError<Service, Method, Transform>
86
-
87
- const {
88
- functionName,
89
- args,
90
- callConfig,
91
- staleTime = 5 * 60 * 1000,
92
- select,
93
- queryKey: customQueryKey,
94
- ...rest
95
- } = config
96
-
97
- const skipped = args === skipToken
98
-
99
- // `callConfig` goes wherever the hooks send it: to the call and into the
100
- // key, so a query of another canister or agent has an entry of its own.
101
- const params = {
102
- functionName,
103
- args: skipped ? undefined : args,
104
- queryKey: customQueryKey,
105
- callConfig,
106
- }
107
-
108
- // What the hook observes: the call, or for a query still waiting for its
109
- // args, an entry of its own under the method's key, with nothing to run;
110
- // see skippedQueryKey.
111
- const queryOptions = () =>
112
- skipped
113
- ? {
114
- queryKey: skippedQueryKey(
115
- reactor.generateQueryKey({ functionName }, callConfig),
116
- "query"
117
- ),
118
- // Kept as the unique symbol, which an object literal widens.
119
- queryFn: skipToken as SkipToken,
120
- retry: undefined,
121
- }
122
- : reactor.getQueryOptions(params)
123
-
124
- const getQueryKey = (): QueryKey =>
125
- reactor.generateQueryKey(params, callConfig)
126
-
127
- // Apply config.select to raw data (shared by fetch, getCacheData, and the hook)
128
- const applySelect = (raw: TData): Selected =>
129
- select ? select(raw) : (raw as unknown as Selected)
130
-
131
- // How the query function runs, shared with the hook; see pickFetchOptions.
132
- const fetchOptions = pickFetchOptions(rest)
133
-
134
- /** Cache-first fetch for use in loaders / route preloading. */
135
- const fetch = async (): Promise<Selected> => {
136
- // Through the reactor rather than straight to the QueryClient: overriding
137
- // `fetchQuery` in a Reactor subclass is a documented way to add logic to
138
- // every factory fetch.
139
- const result = await reactor.fetchQuery(params, fetchOptions)
140
- return applySelect(result)
141
- }
142
-
143
- /** Fire-and-forget prefetch — warms the cache without blocking. */
144
- const prefetch = (): Promise<void> => {
145
- const baseOptions = reactor.getQueryOptions(params)
146
- // A sign-in or sign-out cancels a prefetch in flight, and TanStack
147
- // resolves it all the same: an entry nothing observes was left empty, and
148
- // one a mounted query shows still held the previous principal's data
149
- // until that query's refetch landed. It now runs again for the principal
150
- // signed in, as `fetch()` does. Like `prefetchQuery` it never rejects, so
151
- // the CallError for a principal that keeps switching is dropped too.
152
- return reactor.clientManager
153
- .fetchAcrossIdentitySwitch(() =>
154
- reactor.queryClient.prefetchQuery({
155
- ...fetchOptions,
156
- // An update method's default `retry`; see `Reactor.getQueryRetry`.
157
- ...retryOption(fetchOptions.retry, baseOptions.retry),
158
- queryKey: baseOptions.queryKey,
159
- queryFn: baseOptions.queryFn,
160
- staleTime,
161
- })
162
- )
163
- .catch(() => undefined)
164
- }
165
-
166
- // The hook publicly exposes the overloaded UseQueryWithSelect signature.
167
- // Internally it takes a single broad options object, so we type the
168
- // implementation explicitly and cast once to the public overloaded type.
169
- type UseQueryHookOptions = Omit<
170
- UseQueryOptions<TData, TError, unknown>,
171
- "queryKey" | "queryFn"
172
- > & { select?: (data: Selected) => unknown }
173
-
174
- const useQueryHook = ((options?: UseQueryHookOptions) => {
175
- useMountQueryClient(reactor.queryClient)
176
- const baseOptions = queryOptions()
177
- // Memoized so the observer's select-result cache can hit; see
178
- // buildChainedSelect. `select` comes from the factory config and is stable.
179
- const chainedSelect = useMemo(
180
- () => buildChainedSelect(select, options?.select),
181
- [options?.select]
182
- )
183
- return useQuery(
184
- {
185
- queryKey: baseOptions.queryKey,
186
- staleTime,
187
- ...rest,
188
- ...options,
189
- queryFn: baseOptions.queryFn,
190
- select: chainedSelect,
191
- // The hook's `retry`, else the config's, else an update method's
192
- // default; see `Reactor.getQueryRetry`.
193
- ...retryOption(options?.retry ?? rest.retry, baseOptions.retry),
194
- },
195
- reactor.queryClient
196
- )
197
- }) as UseQueryWithSelect<TData, Selected, TError>
198
-
199
- const invalidate = async (): Promise<void> => {
200
- await reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() })
201
- }
202
-
203
- const getCacheData = ((
204
- selectFn?: (data: Selected) => unknown
205
- ): Selected | unknown => {
206
- const raw = reactor.getQueryData(params, callConfig)
207
- if (raw === undefined) return undefined
208
- const selected = applySelect(raw)
209
- return selectFn ? selectFn(selected) : selected
210
- }) as QueryResult<TData, Selected, TError>["getCacheData"]
211
-
212
- const setData: QueryResult<TData, Selected, TError>["setData"] = (
213
- updater
214
- ) => {
215
- return reactor.queryClient.setQueryData(
216
- getQueryKey(),
217
- updater as Updater<TData | undefined, TData | undefined>
218
- ) as TData | undefined
219
- }
220
-
221
- return {
222
- fetch,
223
- prefetch,
224
- useQuery: useQueryHook,
225
- invalidate,
226
- getQueryKey,
227
- getCacheData,
228
- setData,
229
- ...queryCacheControls<TData>(reactor, getQueryKey),
230
- }
231
- }
232
-
233
- // ============================================================================
234
- // Public Factory Function
235
- // ============================================================================
236
-
237
- export function createQuery<
238
- Service,
239
- Transform extends TransformKey,
240
- Method extends FunctionName<Service> = FunctionName<Service>,
241
- Selected = QueryFnData<Service, Method, Transform>,
242
- >(
243
- reactor: Reactor<Service, Transform>,
244
- config: QueryConfig<NoInfer<Service>, Method, Transform, Selected>
245
- ): QueryResult<
246
- QueryFnData<Service, Method, Transform>,
247
- Selected,
248
- QueryError<Service, Method, Transform>
249
- > {
250
- return createQueryImpl(
251
- reactor,
252
- config as QueryConfig<Service, Method, Transform, Selected>
253
- )
254
- }
255
-
256
- // ============================================================================
257
- // Convenience: Create query with dynamic args
258
- // ============================================================================
259
-
260
- /**
261
- * Create a query factory: a function that takes the method's args and returns
262
- * the query object for them, the same object for the same args.
263
- *
264
- * The function also has `getQueryKey()`, the key prefix every query it
265
- * returns shares, and `invalidate()`, which invalidates all of them whatever
266
- * their args. Pass the function itself to a mutation's `invalidateQueries` to
267
- * refresh every instance after the mutation.
268
- *
269
- * It also takes TanStack Query's `skipToken` in place of args, for a
270
- * component whose args are not known yet, and returns a query with only
271
- * `useQuery()`, which waits without fetching; see
272
- * {@link SkippableQueryFactoryFn}.
273
- *
274
- * @example
275
- * const getBalance = createQueryFactory(ledger, {
276
- * functionName: "icrc1_balance_of",
277
- * })
278
- *
279
- * // In a component
280
- * const { data } = getBalance([{ owner, subaccount: [] }]).useQuery()
281
- *
282
- * // In a component whose owner may not be known yet
283
- * const { data: maybe } = getBalance(
284
- * owner ? [{ owner, subaccount: [] }] : skipToken
285
- * ).useQuery()
286
- *
287
- * // Refetch every account's balance after a transfer
288
- * const transfer = createMutation(ledger, {
289
- * functionName: "icrc1_transfer",
290
- * invalidateQueries: [getBalance],
291
- * })
292
- */
293
- export function createQueryFactory<
294
- Service,
295
- Transform extends TransformKey,
296
- Method extends FunctionName<Service> = FunctionName<Service>,
297
- Selected = QueryFnData<Service, Method, Transform>,
298
- >(
299
- reactor: Reactor<Service, Transform>,
300
- config: QueryFactoryConfig<NoInfer<Service>, Method, Transform, Selected>
301
- ): SkippableQueryFactoryFn<
302
- ReactorArgs<Service, Method, Transform>,
303
- QueryResult<
304
- QueryFnData<Service, Method, Transform>,
305
- Selected,
306
- QueryError<Service, Method, Transform>
307
- >
308
- > {
309
- type Query = QueryResult<
310
- QueryFnData<Service, Method, Transform>,
311
- Selected,
312
- QueryError<Service, Method, Transform>
313
- >
314
-
315
- const cache = createBoundedCache<Query>()
316
-
317
- // One skipped query per factory, built on first use. Only its hook is
318
- // handed out: until the args are known there is no call to make and no
319
- // entry of their own to read or write.
320
- let skippedQuery: SkippedQuery<Query> | undefined
321
-
322
- function factory(args: ReactorArgs<Service, Method, Transform>): Query
323
- function factory(args: SkipToken): SkippedQuery<Query>
324
- function factory(
325
- args: ReactorArgs<Service, Method, Transform> | SkipToken
326
- ): Query | SkippedQuery<Query>
327
- function factory(
328
- args: ReactorArgs<Service, Method, Transform> | SkipToken
329
- ): Query | SkippedQuery<Query> {
330
- if (args === skipToken) {
331
- skippedQuery ??= {
332
- useQuery: createQueryImpl<Service, Method, Transform, Selected>(
333
- reactor,
334
- { ...config, args: skipToken }
335
- ).useQuery,
336
- }
337
- return skippedQuery
338
- }
339
-
340
- const key = reactor.generateQueryKey(
341
- { functionName: config.functionName as Method, args },
342
- config.callConfig
343
- )
344
- const cacheKey = JSON.stringify(key)
345
-
346
- const existing = cache.get(cacheKey)
347
- if (existing) return existing
348
-
349
- const result = createQueryImpl<Service, Method, Transform, Selected>(
350
- reactor,
351
- {
352
- ...config,
353
- args,
354
- }
355
- )
356
- cache.set(cacheKey, result)
357
- return result
358
- }
359
-
360
- // The method's own prefix, at the canister and agent the config's
361
- // `callConfig` names. A config `queryKey` follows the args segment in every
362
- // instance's key, so it cannot narrow the prefix.
363
- return withQueryFactoryMethods(factory, reactor, () =>
364
- reactor.generateQueryKey(
365
- { functionName: config.functionName as Method },
366
- config.callConfig
367
- )
368
- )
369
- }