@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
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared type definitions for query factories (createActorQuery, createActorSuspenseQuery, etc.)
2
+ * Shared type definitions for query factories (createQuery, createSuspenseQuery, etc.)
3
3
  */
4
4
 
5
5
  import type {
@@ -25,6 +25,7 @@ import {
25
25
  UseSuspenseQueryResult,
26
26
  UseMutationOptions,
27
27
  UseMutationResult,
28
+ SkipToken,
28
29
  } from "@tanstack/react-query"
29
30
 
30
31
  // ============================================================================
@@ -83,9 +84,38 @@ export interface BaseQueryConfig<
83
84
  functionName: Method
84
85
  /** Arguments to pass to the method (if any) */
85
86
  args?: ReactorArgs<Service, Method, Transform>
87
+ /**
88
+ * Call configuration for the method, as `Reactor.callMethod` takes it: a
89
+ * `canisterId` sends the query to another canister of the same interface,
90
+ * an `agent` sends it through another agent, `effectiveCanisterId` routes
91
+ * it. The query key carries what it sets, as the hooks' keys do, so the
92
+ * answer is cached apart from the reactor's own canister and agent, and
93
+ * `getQueryKey()`, `invalidate()` and the other cache controls act on that
94
+ * entry.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // The same ledger interface, another token's canister
99
+ * const ckbtcSymbol = createQuery(ledger, {
100
+ * functionName: "icrc1_symbol",
101
+ * callConfig: { canisterId: "mxzaz-hqaaa-aaaar-qaada-cai" },
102
+ * })
103
+ * ```
104
+ */
105
+ callConfig?: CallConfig
86
106
  /** The query key to use for this query */
87
107
  queryKey?: QueryKey
88
- /** How long data stays fresh before refetching (default: 5 min) */
108
+ /**
109
+ * How long data stays fresh before refetching, in milliseconds.
110
+ *
111
+ * `createQuery`, `createSuspenseQuery` and their factories default to 5
112
+ * minutes. The bound `useActorQuery` and `useActorSuspenseQuery` hooks (from
113
+ * `createActorHooks` or `defineReactor`) set no default and leave it to
114
+ * TanStack Query, which reads the QueryClient's
115
+ * `defaultOptions.queries.staleTime`. With that unset too, `useActorQuery`
116
+ * uses 0 and `useActorSuspenseQuery` uses 1 second, TanStack Query's
117
+ * fallback for suspense queries.
118
+ */
89
119
  staleTime?: number
90
120
  /** Transform the raw result before returning */
91
121
  select?: (data: QueryFnData<Service, Method, Transform>) => Selected
@@ -102,6 +132,49 @@ export type QueryConfig<
102
132
  Selected = QueryFnData<Service, Method, Transform>,
103
133
  > = BaseQueryConfig<Service, Method, Transform, Selected>
104
134
 
135
+ /**
136
+ * Configuration for the non-suspense query hook of `createActorHooks` and
137
+ * `defineReactor` (`useActorQuery`): a {@link QueryConfig} whose `args` may
138
+ * also be TanStack Query's `skipToken`, for a query whose arguments are not
139
+ * known yet.
140
+ *
141
+ * A skipped query does not fetch. It has an entry of its own under its
142
+ * method's key (at the canister and agent `callConfig` names), which no
143
+ * call's key shares, so it shows no data until the arguments arrive, not
144
+ * even that of a call made without arguments. Once `args` holds arguments,
145
+ * the query is keyed and fetched as usual. Its `refetch()` has nothing to
146
+ * run: TanStack Query answers it with a "Missing queryFn" error, so offer a
147
+ * refresh only once the arguments exist.
148
+ *
149
+ * The suspense hooks do not take `skipToken`: TanStack Query has no way to
150
+ * suspend on a query that cannot run.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * import { skipToken } from "@ic-reactor/react"
155
+ *
156
+ * function Balance({ owner }: { owner?: string }) {
157
+ * // No `!`, no placeholder account, no `enabled`
158
+ * const { data } = useActorQuery({
159
+ * functionName: "icrc1_balance_of",
160
+ * args: owner ? [{ owner }] : skipToken,
161
+ * })
162
+ * }
163
+ * ```
164
+ */
165
+ export interface SkippableQueryConfig<
166
+ Service = BaseActor,
167
+ Method extends FunctionName<Service> = FunctionName<Service>,
168
+ Transform extends TransformKey = "candid",
169
+ Selected = QueryFnData<Service, Method, Transform>,
170
+ > extends Omit<QueryConfig<Service, Method, Transform, Selected>, "args"> {
171
+ /**
172
+ * Arguments to pass to the method, or `skipToken` while they are not
173
+ * known: the query then waits without fetching.
174
+ */
175
+ args?: ReactorArgs<Service, Method, Transform> | SkipToken
176
+ }
177
+
105
178
  /**
106
179
  * Configuration for createSuspenseQuery (useSuspenseQuery).
107
180
  * Alias for BaseQueryConfig for clarity.
@@ -213,6 +286,120 @@ export interface UseSuspenseQueryWithSelect<
213
286
  ): UseSuspenseQueryResult<TFinal, TError>
214
287
  }
215
288
 
289
+ // ============================================================================
290
+ // Cache Controls
291
+ // ============================================================================
292
+
293
+ /**
294
+ * What `optimisticUpdate()` resolves with: the way back to the value the
295
+ * cache held before the update.
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const update = await postQuery.optimisticUpdate((post) => ({
300
+ * ...post,
301
+ * likes: post.likes + 1n,
302
+ * }))
303
+ * // The call failed: show the post as it was
304
+ * update.rollback()
305
+ * ```
306
+ */
307
+ export interface OptimisticRollback {
308
+ /**
309
+ * Write back the value the cache held before the update, with the time it
310
+ * was fetched, so it is as fresh or as stale as it was. A value that was
311
+ * invalidated is invalidated again, so a mounted query refetches it, as
312
+ * the refetch the update cancelled would have.
313
+ *
314
+ * It does nothing when the update wrote nothing, or when another principal
315
+ * has signed in or out since: the value was the previous principal's, and
316
+ * the sign-in has already removed or refetched it. It restores that value
317
+ * even if a fetch or another update has written since; invalidate the
318
+ * query afterwards when the canister's current value matters.
319
+ */
320
+ rollback: () => void
321
+ }
322
+
323
+ /**
324
+ * The operations every query object has on its own cache entry, on the
325
+ * reactor's QueryClient. They act on that one entry: other args of the same
326
+ * method, and other queries under the same key prefix, are left alone.
327
+ *
328
+ * @template TQueryFnData - The raw (pre-`select`) data the entry holds
329
+ */
330
+ export interface QueryCacheControls<TQueryFnData> {
331
+ /**
332
+ * Cancel this query's fetch in flight, if there is one. The entry keeps
333
+ * the value it held before that fetch started, and a later refetch runs as
334
+ * usual.
335
+ *
336
+ * @example
337
+ * ```typescript
338
+ * // Before writing to the cache, so an older answer cannot land on top
339
+ * await postQuery.cancel()
340
+ * postQuery.setData(draft)
341
+ * ```
342
+ */
343
+ cancel: () => Promise<void>
344
+
345
+ /**
346
+ * Reset this query's entry to its initial state, as TanStack Query's
347
+ * `resetQueries` does: its data is cleared, or goes back to `initialData`
348
+ * when one was given. A mounted hook then fetches it again, and a suspense
349
+ * hook suspends until it has. It resolves once that fetch settles.
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * // A reload button that shows the Suspense fallback again
354
+ * <button onClick={() => void statsQuery.reset()}>Reload</button>
355
+ * ```
356
+ */
357
+ reset: () => Promise<void>
358
+
359
+ /**
360
+ * Replace this query's cached value for the duration of a mutation, and
361
+ * get back a rollback for when it fails.
362
+ *
363
+ * It cancels the query's fetch in flight, so an answer from before the
364
+ * mutation cannot overwrite the new value, then writes what `updater`
365
+ * returns for the cached value. `updater` gets and returns the raw,
366
+ * pre-`select` data. When nothing is cached yet it is not called, nothing
367
+ * is cancelled or written, and `rollback()` does nothing: there is no
368
+ * value on screen to update, and the query's own fetch will bring one. The
369
+ * same goes when another principal signs in or out while it cancels, since
370
+ * the cached value is then the previous principal's.
371
+ *
372
+ * The fetch it cancels may be a refetch an invalidation or a sign-in
373
+ * started, so refetch the query once the mutation settles, with
374
+ * `invalidate()` in `onSettled` or the query in `invalidateQueries`.
375
+ *
376
+ * Return it from `onMutate`, so the rollback reaches `onError`.
377
+ *
378
+ * @param updater - The new value, from the cached one. Do not mutate the
379
+ * cached value in place; return a new one.
380
+ *
381
+ * @example
382
+ * ```typescript
383
+ * const getPost = createQueryFactory(backend, { functionName: "getPost" })
384
+ * const likePost = createMutation(backend, { functionName: "likePost" })
385
+ *
386
+ * const { mutate } = likePost.useMutation({
387
+ * onMutate: ([postId]) =>
388
+ * getPost([postId]).optimisticUpdate((post) => ({
389
+ * ...post,
390
+ * likes: post.likes + 1n,
391
+ * })),
392
+ * onError: (_error, _args, update) => update?.rollback(),
393
+ * // Refetch either way: a call that failed in transit may still have run
394
+ * onSettled: (_data, _error, [postId]) => getPost([postId]).invalidate(),
395
+ * })
396
+ * ```
397
+ */
398
+ optimisticUpdate: (
399
+ updater: (old: TQueryFnData) => TQueryFnData
400
+ ) => Promise<OptimisticRollback>
401
+ }
402
+
216
403
  // ============================================================================
217
404
  // Result Interfaces
218
405
  // ============================================================================
@@ -228,7 +415,7 @@ export interface BaseQueryResult<
228
415
  TQueryFnData,
229
416
  TSelected = TQueryFnData,
230
417
  _TError = Error,
231
- > {
418
+ > extends QueryCacheControls<TQueryFnData> {
232
419
  /** Fetch data in loader (uses ensureQueryData for cache-first) */
233
420
  fetch: () => Promise<TSelected>
234
421
 
@@ -237,6 +424,12 @@ export interface BaseQueryResult<
237
424
  * Useful for preloading data before navigating to a route.
238
425
  *
239
426
  * Unlike `fetch()`, this returns a void promise so it can be fire-and-forget.
427
+ * It never rejects: after a failed fetch the cached data is left as it was.
428
+ *
429
+ * A sign-in or sign-out while it is in flight cancels the fetch, so the
430
+ * previous principal's answer is never cached, and it runs again for the
431
+ * principal signed in, as `fetch()` does. When that run succeeds, the cache
432
+ * holds that principal's answer by the time the promise resolves.
240
433
  *
241
434
  * @example
242
435
  * // In a route hover handler
@@ -333,22 +526,267 @@ export interface SuspenseQueryResult<
333
526
  useSuspenseQuery: UseSuspenseQueryWithSelect<TQueryFnData, TSelected, TError>
334
527
  }
335
528
 
529
+ // ============================================================================
530
+ // Query Factory Functions
531
+ // ============================================================================
532
+
533
+ /**
534
+ * The members every args-late query factory function carries
535
+ * (`createQueryFactory`, `createSuspenseQueryFactory`,
536
+ * `createInfiniteQueryFactory`, `createSuspenseInfiniteQueryFactory`).
537
+ *
538
+ * A factory makes one query per set of args, so there was no key to name all
539
+ * of them: invalidating a list after a mutation meant keeping the args of each
540
+ * instance around. These address every query the factory returns at once.
541
+ */
542
+ export interface QueryFactoryMethods {
543
+ /**
544
+ * The key prefix every query of this factory shares, whatever its args: the
545
+ * canister (the config's `callConfig.canisterId`, else the reactor's) and
546
+ * the method, plus the reactor's transform segment and any agent or
547
+ * effective-target segment the config's `callConfig` adds, and for an
548
+ * infinite factory its config `queryKey`. TanStack Query matches keys by
549
+ * prefix, so the prefix covers every args instance and every infinite page
550
+ * set. Queries of the same method made elsewhere share it too.
551
+ *
552
+ * @example
553
+ * ```typescript
554
+ * const getBalance = createQueryFactory(ledger, {
555
+ * functionName: "icrc1_balance_of",
556
+ * })
557
+ *
558
+ * // Every cached balance, whatever the account
559
+ * ledger.queryClient.getQueriesData({ queryKey: getBalance.getQueryKey() })
560
+ * ```
561
+ */
562
+ getQueryKey: () => QueryKey
563
+ /**
564
+ * Invalidate every query of this factory, whatever its args, on the
565
+ * reactor's QueryClient. It resolves once the active ones have refetched; a
566
+ * refetch that fails does not reject it.
567
+ *
568
+ * @example
569
+ * ```typescript
570
+ * // After a transfer, refresh every balance on screen
571
+ * await getBalance.invalidate()
572
+ * ```
573
+ */
574
+ invalidate: () => Promise<void>
575
+ }
576
+
577
+ /**
578
+ * The function `createQueryFactory` and `createSuspenseQueryFactory` return:
579
+ * called with args it returns the query object for them, the same object for
580
+ * the same args, and it also carries {@link QueryFactoryMethods}.
581
+ *
582
+ * @template TArgs - The method's arguments
583
+ * @template TQuery - The query object it returns
584
+ *
585
+ * @example
586
+ * ```typescript
587
+ * const getPost = createQueryFactory(backend, { functionName: "get_post" })
588
+ *
589
+ * // One post's query object
590
+ * const { data } = getPost([postId]).useQuery()
591
+ *
592
+ * // Every post's, whatever its args
593
+ * await getPost.invalidate()
594
+ * ```
595
+ */
596
+ export interface QueryFactoryFn<TArgs, TQuery> extends QueryFactoryMethods {
597
+ (args: TArgs): TQuery
598
+ }
599
+
600
+ /**
601
+ * What a query factory returns for `skipToken`: the query's `useQuery` hook
602
+ * alone, which renders a query that waits without fetching.
603
+ *
604
+ * The imperative members are left out because there is no call to make or
605
+ * entry to read until the args are known: narrow to the args first to reach
606
+ * `fetch()`, `invalidate()` or the cache controls.
607
+ *
608
+ * @template TQuery - The query object the factory returns for args
609
+ *
610
+ * @example
611
+ * ```typescript
612
+ * const getBalance = createQueryFactory(ledger, {
613
+ * functionName: "icrc1_balance_of",
614
+ * })
615
+ *
616
+ * // A SkippedQuery: only useQuery(), which does not fetch
617
+ * const { data } = getBalance(skipToken).useQuery()
618
+ * ```
619
+ */
620
+ export type SkippedQuery<TQuery extends { useQuery: unknown }> = Pick<
621
+ TQuery,
622
+ "useQuery"
623
+ >
624
+
625
+ /**
626
+ * The function `createQueryFactory` returns: a {@link QueryFactoryFn} that
627
+ * also takes TanStack Query's `skipToken` in place of args, for a component
628
+ * whose args are not known yet. For `skipToken` it returns a
629
+ * {@link SkippedQuery}, whose `useQuery()` waits without fetching, in an
630
+ * entry of its own under the factory's `getQueryKey()` prefix. Given
631
+ * `args ? [args] : skipToken`, it returns either, and `useQuery()` can be
632
+ * called on the result directly.
633
+ *
634
+ * `createSuspenseQueryFactory` returns a plain {@link QueryFactoryFn}: a
635
+ * suspense query cannot wait on `skipToken`.
636
+ *
637
+ * @template TArgs - The method's arguments
638
+ * @template TQuery - The query object it returns for args
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * const getBalance = createQueryFactory(ledger, {
643
+ * functionName: "icrc1_balance_of",
644
+ * })
645
+ *
646
+ * function Balance({ owner }: { owner?: string }) {
647
+ * const { data } = getBalance(owner ? [{ owner }] : skipToken).useQuery()
648
+ * }
649
+ * ```
650
+ */
651
+ export interface SkippableQueryFactoryFn<
652
+ TArgs,
653
+ TQuery extends { useQuery: unknown },
654
+ > extends QueryFactoryMethods {
655
+ // Args first: a factory called with args must resolve to the full query
656
+ // object, not to the union the args-or-skipToken signature returns.
657
+ (args: TArgs): TQuery
658
+ (args: SkipToken): SkippedQuery<TQuery>
659
+ (args: TArgs | SkipToken): TQuery | SkippedQuery<TQuery>
660
+ // And args last as well: TypeScript reads an overloaded function's last
661
+ // signature for `ReturnType`, `Parameters` and inference, so
662
+ // `ReturnType<typeof getBalance>` stays the full query object, and a
663
+ // factory passed where a `QueryFactoryFn<A, Q>` is inferred still gives
664
+ // its args and query, as they did before `skipToken`. With the union
665
+ // signature last, all three widened to include the skipped query.
666
+ (args: TArgs): TQuery
667
+ }
668
+
669
+ // ============================================================================
670
+ // Invalidation Targets
671
+ // ============================================================================
672
+
673
+ /**
674
+ * Anything that knows the key of its queries: a query object from
675
+ * `createQuery`, `createSuspenseQuery`, `createInfiniteQuery` or
676
+ * `createSuspenseInfiniteQuery` (factory instances included), or a query
677
+ * factory function, whose key covers every query it returns.
678
+ *
679
+ * @example
680
+ * ```typescript
681
+ * const postsQuery = createQuery(backend, { functionName: "get_posts" })
682
+ * const getPost = createQueryFactory(backend, { functionName: "get_post" })
683
+ *
684
+ * // Both are key sources, so both go straight into invalidateQueries
685
+ * createMutation(backend, {
686
+ * functionName: "create_post",
687
+ * invalidateQueries: [postsQuery, getPost],
688
+ * })
689
+ * ```
690
+ */
691
+ export interface QueryKeySource {
692
+ /** The key, or key prefix, of the queries it names. */
693
+ getQueryKey: () => QueryKey
694
+ /**
695
+ * Invalidate those queries on the QueryClient they are cached in.
696
+ * `invalidateQueries` calls it when it is there, so a query of another
697
+ * reactor with a QueryClient of its own is invalidated in that client. The
698
+ * key goes to the mutation's reactor's QueryClient otherwise.
699
+ */
700
+ invalidate?: () => Promise<void>
701
+ }
702
+
703
+ /**
704
+ * A method of the mutation's own reactor, and optionally one set of its
705
+ * arguments: the same shape `Reactor.invalidateQueries` takes. Its key is
706
+ * built by the reactor's `generateQueryKey` when the mutation succeeds, so it
707
+ * follows a `setCanisterId` and carries the reactor's transform segment. It
708
+ * is rooted at the canister the mutation was sent to: the reactor's, or the
709
+ * one the mutation's `callConfig.canisterId` names.
710
+ *
711
+ * Without `args` it names every query of the method, whatever its args,
712
+ * infinite queries included. With `args` it names the queries made with those
713
+ * args by `createQuery`, a query factory or the hooks, and `args: []` names a
714
+ * method without parameters as no `args` does. An infinite query keys its
715
+ * page set by its first page's args in another form, which `args` does not
716
+ * match, and a query of another canister than the mutation's is keyed apart:
717
+ * name either by its query object or key instead.
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * invalidateQueries: [
722
+ * { functionName: "get_posts" },
723
+ * { functionName: "get_post", args: [postId] },
724
+ * ]
725
+ * ```
726
+ */
727
+ export type QueryDescriptor<
728
+ Service = BaseActor,
729
+ Transform extends TransformKey = "candid",
730
+ > = {
731
+ [Method in FunctionName<Service>]: {
732
+ /** The method whose queries to name */
733
+ functionName: Method
734
+ /** The arguments of the one query to name; omit for every query of the method */
735
+ args?: ReactorArgs<Service, Method, Transform>
736
+ }
737
+ }[FunctionName<Service>]
738
+
739
+ /**
740
+ * One entry of `invalidateQueries`: which queries a successful mutation
741
+ * invalidates.
742
+ *
743
+ * - a query key, as `generateQueryKey` or `getQueryKey()` builds it;
744
+ * - a query object or query factory ({@link QueryKeySource});
745
+ * - a method of the mutation's own reactor, with or without args
746
+ * ({@link QueryDescriptor});
747
+ * - `undefined`, which is skipped, so `[maybeQuery]` and
748
+ * `[maybeQuery?.getQueryKey()]` are safe when the query is absent.
749
+ *
750
+ * TanStack Query matches each key by prefix.
751
+ *
752
+ * @example
753
+ * ```typescript
754
+ * createMutation(backend, {
755
+ * functionName: "create_post",
756
+ * invalidateQueries: [
757
+ * postsQuery, // a query object
758
+ * getPost, // a query factory: every post, whatever its args
759
+ * { functionName: "get_posts_count" }, // a method of this reactor
760
+ * ],
761
+ * })
762
+ * ```
763
+ */
764
+ export type InvalidationTarget<
765
+ Service = BaseActor,
766
+ Transform extends TransformKey = "candid",
767
+ > = QueryKey | QueryKeySource | QueryDescriptor<Service, Transform> | undefined
768
+
336
769
  // ============================================================================
337
770
  // Actor Mutation Types
338
771
  // ============================================================================
339
772
 
340
773
  /**
341
774
  * Configuration for createMutation and useActorMutation.
775
+ *
776
+ * @template TOnMutateResult - The value `onMutate` returns, which `onSuccess`,
777
+ * `onError` and `onSettled` receive. TypeScript infers it from `onMutate`.
342
778
  */
343
779
  export interface MutationConfig<
344
780
  Service = BaseActor,
345
781
  Method extends FunctionName<Service> = FunctionName<Service>,
346
782
  Transform extends TransformKey = "candid",
783
+ TOnMutateResult = unknown,
347
784
  > extends Omit<
348
785
  UseMutationOptions<
349
786
  ReactorReturnOk<Service, Method, Transform>,
350
787
  ReactorReturnErr<Service, Method, Transform>,
351
- ReactorArgs<Service, Method, Transform>
788
+ ReactorArgs<Service, Method, Transform>,
789
+ TOnMutateResult
352
790
  >,
353
791
  "mutationFn"
354
792
  > {
@@ -357,13 +795,21 @@ export interface MutationConfig<
357
795
  /** Call configuration for the actor method */
358
796
  callConfig?: CallConfig
359
797
  /**
360
- * Queries to invalidate upon successful mutation.
798
+ * Queries to invalidate upon successful mutation, before `onSuccess` runs.
799
+ * The mutation stays pending until the invalidated queries in use have
800
+ * refetched, so `onSuccess` reads the refetched data.
361
801
  *
362
- * `undefined` entries are skipped, so the common
363
- * `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
364
- * is absent.
802
+ * Each entry is a query key, a query object or query factory, or a
803
+ * `{ functionName, args? }` method of this mutation's reactor; see
804
+ * {@link InvalidationTarget}. `undefined` entries are skipped, so
805
+ * `[maybeQuery]` is safe when the optional query object is absent.
806
+ *
807
+ * @example
808
+ * ```typescript
809
+ * invalidateQueries: [getPosts, { functionName: "get_posts_count" }]
810
+ * ```
365
811
  */
366
- invalidateQueries?: (QueryKey | undefined)[]
812
+ invalidateQueries?: InvalidationTarget<Service, Transform>[]
367
813
  /**
368
814
  * Callback for canister-level business logic errors.
369
815
  * Called when the canister returns a Result { Err: E } variant.
@@ -387,7 +833,12 @@ export interface MutationConfig<
387
833
  * ```
388
834
  */
389
835
  onCanisterError?: (
390
- error: CanisterError<unknown>,
836
+ error: CanisterError<
837
+ TransformReturnRegistry<
838
+ ErrResult<ActorMethodReturnType<Service[Method]>>,
839
+ Service
840
+ >[Transform]
841
+ >,
391
842
  variables: ReactorArgs<Service, Method, Transform>
392
843
  ) => void
393
844
  }
@@ -399,35 +850,47 @@ export type MutationFactoryConfig<
399
850
  Service = BaseActor,
400
851
  Method extends FunctionName<Service> = FunctionName<Service>,
401
852
  Transform extends TransformKey = "candid",
402
- > = Omit<MutationConfig<Service, Method, Transform>, "onSuccess">
853
+ TOnMutateResult = unknown,
854
+ > = Omit<
855
+ MutationConfig<Service, Method, Transform, TOnMutateResult>,
856
+ "onSuccess"
857
+ >
403
858
 
404
859
  /**
405
860
  * Options for useMutation hook.
406
861
  * Extends React Query's UseMutationOptions with invalidateQueries support.
862
+ *
863
+ * @template TOnMutateResult - The value `onMutate` returns, which `onSuccess`,
864
+ * `onError` and `onSettled` receive. TypeScript infers it from `onMutate`.
407
865
  */
408
866
  export interface MutationHookOptions<
409
867
  Service = BaseActor,
410
868
  Method extends FunctionName<Service> = FunctionName<Service>,
411
869
  Transform extends TransformKey = "candid",
870
+ TOnMutateResult = unknown,
412
871
  > extends Omit<
413
872
  UseMutationOptions<
414
873
  ReactorReturnOk<Service, Method, Transform>,
415
874
  ReactorReturnErr<Service, Method, Transform>,
416
- ReactorArgs<Service, Method, Transform>
875
+ ReactorArgs<Service, Method, Transform>,
876
+ TOnMutateResult
417
877
  >,
418
878
  "mutationFn"
419
879
  > {
420
880
  /**
421
- * Query keys to invalidate upon successful mutation.
422
- * Use query.getQueryKey() to get the key from a query result.
881
+ * Queries to invalidate upon successful mutation, after the factory's own
882
+ * `invalidateQueries` and before `onSuccess`. Takes the same entries:
883
+ * a query key, a query object or query factory, or a
884
+ * `{ functionName, args? }` method of the mutation's reactor; see
885
+ * {@link InvalidationTarget}.
423
886
  *
424
887
  * @example
425
- * const balanceQuery = getIcpBalance(account)
888
+ * const balanceQuery = getIcpBalance([account])
426
889
  * useMutation({
427
- * invalidateQueries: [balanceQuery.getQueryKey()],
890
+ * invalidateQueries: [balanceQuery],
428
891
  * })
429
892
  */
430
- invalidateQueries?: (QueryKey | undefined)[]
893
+ invalidateQueries?: InvalidationTarget<Service, Transform>[]
431
894
  /**
432
895
  * Callback for canister-level business logic errors.
433
896
  * Called when the canister returns a Result { Err: E } variant.
@@ -438,7 +901,8 @@ export interface MutationHookOptions<
438
901
  onCanisterError?: (
439
902
  error: CanisterError<
440
903
  TransformReturnRegistry<
441
- ErrResult<ActorMethodReturnType<Service[Method]>>
904
+ ErrResult<ActorMethodReturnType<Service[Method]>>,
905
+ Service
442
906
  >[Transform]
443
907
  >,
444
908
  variables: ReactorArgs<Service, Method, Transform>
@@ -463,15 +927,21 @@ export interface MutationResult<
463
927
  * invalidateQueries: [userBalanceQuery], // Auto-invalidate after success!
464
928
  * })
465
929
  */
466
- useMutation: (
467
- options?: MutationHookOptions<Service, Method, Transform>
930
+ useMutation: <TOnMutateResult = unknown>(
931
+ options?: MutationHookOptions<Service, Method, Transform, TOnMutateResult>
468
932
  ) => UseMutationResult<
469
933
  ReactorReturnOk<Service, Method, Transform>,
470
934
  ReactorReturnErr<Service, Method, Transform>,
471
- ReactorArgs<Service, Method, Transform>
935
+ ReactorArgs<Service, Method, Transform>,
936
+ TOnMutateResult
472
937
  >
473
938
 
474
- /** Execute the update call directly (outside of React) */
939
+ /**
940
+ * Execute the update call outside React. It runs in the QueryClient's
941
+ * MutationCache with the factory's options and callbacks, as `useMutation()`
942
+ * does without hook options. It resolves with the method's result and
943
+ * rejects with the call's error.
944
+ */
475
945
  execute: (
476
946
  args: ReactorArgs<Service, Method, Transform>
477
947
  ) => Promise<ReactorReturnOk<Service, Method, Transform>>