@ic-reactor/react 3.7.0 → 3.9.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 (107) hide show
  1. package/README.md +158 -9
  2. package/dist/auth/authentication-manager.d.ts +37 -5
  3. package/dist/auth/authentication-manager.d.ts.map +1 -1
  4. package/dist/auth/authentication-manager.js +242 -52
  5. package/dist/auth/authentication-manager.js.map +1 -1
  6. package/dist/auth/createIdentityAttributeHooks.d.ts +2 -2
  7. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  8. package/dist/auth/identity-attributes-manager.d.ts +12 -5
  9. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  10. package/dist/auth/identity-attributes-manager.js +42 -28
  11. package/dist/auth/identity-attributes-manager.js.map +1 -1
  12. package/dist/auth/identity-attributes.d.ts +1 -1
  13. package/dist/auth/identity-attributes.d.ts.map +1 -1
  14. package/dist/auth/identity-attributes.js +1 -1
  15. package/dist/auth/identity-attributes.js.map +1 -1
  16. package/dist/auth/index.d.ts +7 -7
  17. package/dist/auth/index.d.ts.map +1 -1
  18. package/dist/auth/index.js +7 -7
  19. package/dist/auth/index.js.map +1 -1
  20. package/dist/auth/types.d.ts +123 -16
  21. package/dist/auth/types.d.ts.map +1 -1
  22. package/dist/createActorHooks.d.ts +4 -4
  23. package/dist/createActorHooks.d.ts.map +1 -1
  24. package/dist/createActorHooks.js +6 -6
  25. package/dist/createActorHooks.js.map +1 -1
  26. package/dist/createInfiniteQuery.d.ts +12 -10
  27. package/dist/createInfiniteQuery.d.ts.map +1 -1
  28. package/dist/createInfiniteQuery.js +20 -17
  29. package/dist/createInfiniteQuery.js.map +1 -1
  30. package/dist/createMutation.d.ts +1 -1
  31. package/dist/createMutation.d.ts.map +1 -1
  32. package/dist/createMutation.js +22 -2
  33. package/dist/createMutation.js.map +1 -1
  34. package/dist/createQuery.d.ts +1 -1
  35. package/dist/createQuery.d.ts.map +1 -1
  36. package/dist/createQuery.js +6 -2
  37. package/dist/createQuery.js.map +1 -1
  38. package/dist/createSuspenseInfiniteQuery.d.ts +11 -10
  39. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  40. package/dist/createSuspenseInfiniteQuery.js +18 -16
  41. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  42. package/dist/createSuspenseQuery.d.ts +1 -1
  43. package/dist/createSuspenseQuery.d.ts.map +1 -1
  44. package/dist/createSuspenseQuery.js +6 -2
  45. package/dist/createSuspenseQuery.js.map +1 -1
  46. package/dist/defineReactor.d.ts +54 -4
  47. package/dist/defineReactor.d.ts.map +1 -1
  48. package/dist/defineReactor.js +77 -7
  49. package/dist/defineReactor.js.map +1 -1
  50. package/dist/hooks/createAuthHooks.d.ts +2 -2
  51. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  52. package/dist/hooks/createAuthHooks.js +7 -2
  53. package/dist/hooks/createAuthHooks.js.map +1 -1
  54. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  55. package/dist/hooks/useActorInfiniteQuery.js +16 -2
  56. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  57. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  58. package/dist/hooks/useActorMethod.js +66 -33
  59. package/dist/hooks/useActorMethod.js.map +1 -1
  60. package/dist/hooks/useActorMutation.d.ts +8 -1
  61. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  62. package/dist/hooks/useActorMutation.js +9 -1
  63. package/dist/hooks/useActorMutation.js.map +1 -1
  64. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  65. package/dist/hooks/useActorSuspenseInfiniteQuery.js +16 -2
  66. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  67. package/dist/index.d.ts +11 -11
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +11 -11
  70. package/dist/index.js.map +1 -1
  71. package/dist/types.d.ts +8 -2
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/utils.d.ts +14 -5
  74. package/dist/utils.d.ts.map +1 -1
  75. package/dist/utils.js +21 -10
  76. package/dist/utils.js.map +1 -1
  77. package/llms.txt +1 -1
  78. package/package.json +14 -9
  79. package/src/auth/authentication-manager.ts +267 -64
  80. package/src/auth/createIdentityAttributeHooks.ts +2 -2
  81. package/src/auth/identity-attributes-manager.ts +53 -44
  82. package/src/auth/identity-attributes.ts +2 -2
  83. package/src/auth/index.ts +7 -7
  84. package/src/auth/types.ts +134 -18
  85. package/src/createActorHooks.ts +9 -9
  86. package/src/createInfiniteQuery.ts +49 -35
  87. package/src/createMutation.ts +33 -3
  88. package/src/createQuery.ts +10 -3
  89. package/src/createSuspenseInfiniteQuery.ts +45 -33
  90. package/src/createSuspenseQuery.ts +10 -3
  91. package/src/defineReactor.ts +136 -14
  92. package/src/hooks/createAuthHooks.ts +8 -3
  93. package/src/hooks/useActorInfiniteQuery.ts +25 -3
  94. package/src/hooks/useActorMethod.ts +82 -34
  95. package/src/hooks/useActorMutation.ts +19 -4
  96. package/src/hooks/useActorSuspenseInfiniteQuery.ts +25 -3
  97. package/src/index.ts +11 -11
  98. package/src/types.ts +8 -2
  99. package/src/utils.ts +21 -10
  100. package/src/defineReactor.test.ts +0 -70
  101. package/src/hooks/useActorInfiniteQuery.test.tsx +0 -457
  102. package/src/hooks/useActorMethod.test.tsx +0 -798
  103. package/src/hooks/useActorMutation.test.tsx +0 -220
  104. package/src/hooks/useActorQuery.test.tsx +0 -287
  105. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +0 -472
  106. package/src/hooks/useActorSuspenseQuery.test.tsx +0 -254
  107. package/src/utils.test.ts +0 -14
package/src/auth/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- export * from "./authentication-manager"
2
- export * from "./identity-attributes-manager"
3
- export * from "./identity-attributes"
4
- export * from "./constants"
5
- export * from "./types"
6
- export * from "../hooks/createAuthHooks"
7
- export * from "./createIdentityAttributeHooks"
1
+ export * from "./authentication-manager.js"
2
+ export * from "./identity-attributes-manager.js"
3
+ export * from "./identity-attributes.js"
4
+ export * from "./constants.js"
5
+ export * from "./types.js"
6
+ export * from "../hooks/createAuthHooks.js"
7
+ export * from "./createIdentityAttributeHooks.js"
package/src/auth/types.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Identity } from "@icp-sdk/core/agent"
1
+ import type { Identity, SignIdentity } from "@icp-sdk/core/agent"
2
+ import type { PartialIdentity } from "@icp-sdk/core/identity"
2
3
  import type { Principal } from "@icp-sdk/core/principal"
3
4
 
4
5
  export interface SignedIdentityAttributes {
@@ -6,9 +7,21 @@ export interface SignedIdentityAttributes {
6
7
  signature: Uint8Array
7
8
  }
8
9
 
10
+ /**
11
+ * The 32-byte nonce issued by the relying-party canister.
12
+ *
13
+ * A function (or promise) lets the Internet Identity window open while the
14
+ * nonce is still being fetched, so the popup is opened inside the user gesture
15
+ * instead of after it. `@icp-sdk/auth` v8 requires the function form; v7
16
+ * accepts the promise form. IC Reactor normalizes whichever you pass to the
17
+ * shape the installed client expects.
18
+ */
19
+ export type IdentityAttributeNonce =
20
+ Uint8Array | Promise<Uint8Array> | (() => Uint8Array | Promise<Uint8Array>)
21
+
9
22
  export interface IdentityAttributeRequest {
10
23
  keys: string[]
11
- nonce: Uint8Array
24
+ nonce: IdentityAttributeNonce
12
25
  }
13
26
 
14
27
  export interface IdentityAttributeValues {
@@ -26,15 +39,102 @@ export interface IdentityAttributeResult {
26
39
  completedAt: string
27
40
  }
28
41
 
42
+ /**
43
+ * The providers that Internet Identity supports for one-click sign-in. Only
44
+ * these three reach the underlying auth client.
45
+ */
29
46
  type IdentityAttributeOpenIdProviderAlias = "google" | "apple" | "microsoft"
30
47
 
48
+ /**
49
+ * An OpenID provider, either as a well-known alias or as a raw issuer URL.
50
+ *
51
+ * The two forms are **not** interchangeable, and what each one does depends on
52
+ * where it is passed:
53
+ *
54
+ * - An alias (`"google" | "apple" | "microsoft"`) is forwarded to the auth
55
+ * client, which adds an `openid` search param to the identity provider URL so
56
+ * the user signs in through that provider directly.
57
+ * - Any other string is treated as a raw issuer URL and is **never** forwarded
58
+ * to the auth client, so it never affects how the user signs in. It is only
59
+ * meaningful when passed to {@link IdentityAttributesManager.requestOpenId}
60
+ * (or `identityAttributeKeys`), which scopes the requested keys to
61
+ * `openid:<issuer>:<key>`. Passed anywhere else it has no effect at all.
62
+ */
31
63
  export type IdentityAttributeOpenIdProvider =
32
64
  IdentityAttributeOpenIdProviderAlias | (string & {})
33
65
 
66
+ /** Session key algorithm used for each sign-in. */
67
+ export type AuthClientKeyType = "ECDSA" | "Ed25519"
68
+
69
+ /** Structural shape of an `@icp-sdk/auth` `AuthClientStorage`. */
70
+ export interface AuthClientStorageLike {
71
+ get(key: string): Promise<string | CryptoKeyPair | null>
72
+ set(key: string, value: string | CryptoKeyPair): Promise<void>
73
+ remove(key: string): Promise<void>
74
+ }
75
+
76
+ /** Idle-session configuration forwarded to the underlying AuthClient. */
77
+ export interface AuthClientIdleOptions {
78
+ /** Called once the user has been idle for `idleTimeout` ms. */
79
+ onIdle?: () => unknown
80
+ /** Idle timeout in ms. @default 600_000 (10 minutes) */
81
+ idleTimeout?: number
82
+ /** Treat scroll events as activity. @default false */
83
+ captureScroll?: boolean
84
+ /** Scroll debounce in ms. @default 100 */
85
+ scrollDebounce?: number
86
+ /** Disable idle detection entirely. @default false */
87
+ disableIdle?: boolean
88
+ /**
89
+ * Disable the built-in idle callback, which signs the user out and reloads
90
+ * the page. @default false
91
+ */
92
+ disableDefaultIdleCallback?: boolean
93
+ }
94
+
34
95
  export interface AuthenticationClientOptions {
96
+ /** Identity provider URL. @default "https://id.ai/authorize" */
35
97
  identityProvider?: string | URL
98
+ /** `window.open` features string for the identity provider popup. */
36
99
  windowOpenerFeatures?: string
100
+ /**
101
+ * Provider for one-click sign-in.
102
+ *
103
+ * Only the `"google" | "apple" | "microsoft"` aliases reach the auth client.
104
+ * Any other value is dropped here and has **no effect** — raw issuer URLs are
105
+ * only meaningful on `requestOpenId`, where they scope the requested keys.
106
+ * @see {@link IdentityAttributeOpenIdProvider}
107
+ */
37
108
  openIdProvider?: IdentityAttributeOpenIdProvider
109
+ /**
110
+ * Derivation origin for the identity provider.
111
+ *
112
+ * Required when the app is served from more than one origin (e.g. a custom
113
+ * domain plus `<canisterId>.icp0.io`) and every origin must resolve to the
114
+ * same principal.
115
+ * @see https://github.com/dfinity/internet-identity/blob/main/docs/internet-identity-spec.adoc
116
+ */
117
+ derivationOrigin?: string | URL
118
+ /** Persistent storage backend. @default IndexedDB */
119
+ storage?: AuthClientStorageLike
120
+ /**
121
+ * Session key algorithm. Use `"Ed25519"` when the storage backend cannot
122
+ * hold a `CryptoKey`. @default "ECDSA"
123
+ */
124
+ keyType?: AuthClientKeyType
125
+ /**
126
+ * Idle timeout configuration. Pass `{ disableIdle: true }` to opt out of the
127
+ * default behaviour, which signs the user out and reloads after 10 minutes.
128
+ */
129
+ idleOptions?: AuthClientIdleOptions
130
+ /** An existing identity to authenticate via delegation. */
131
+ identity?: SignIdentity | PartialIdentity
132
+ /**
133
+ * How the client talks to the identity provider. `"redirect"` requires
134
+ * `@icp-sdk/auth` v8 or later and is ignored by older versions.
135
+ * @default "window"
136
+ */
137
+ transport?: "window" | "redirect"
38
138
  }
39
139
 
40
140
  export interface AuthClientSignInOptions {
@@ -48,36 +148,52 @@ export interface AuthenticationSignInOptions
48
148
  onError?: (error?: string) => void | Promise<void>
49
149
  }
50
150
 
51
- export interface RequestIdentityAttributesParameters {
151
+ export interface RequestIdentityAttributesParameters extends AuthenticationClientOptions {
52
152
  keys: string[]
53
- nonce: Uint8Array
54
- identityProvider?: string | URL
55
- openIdProvider?: IdentityAttributeOpenIdProvider
56
- windowOpenerFeatures?: string
153
+ nonce: IdentityAttributeNonce
154
+ /**
155
+ * Sign in as part of the same identity-provider interaction.
156
+ * @default true
157
+ */
57
158
  signIn?: boolean
58
159
  maxTimeToLive?: bigint
59
160
  targets?: Principal[]
60
161
  }
61
162
 
62
- export interface RequestOpenIdIdentityAttributesParameters {
63
- nonce: Uint8Array
163
+ export interface RequestOpenIdIdentityAttributesParameters extends Omit<
164
+ RequestIdentityAttributesParameters,
165
+ "openIdProvider"
166
+ > {
167
+ /**
168
+ * Provider the requested `keys` are scoped to, producing
169
+ * `openid:<issuer>:<key>`.
170
+ *
171
+ * Passing one of the `"google" | "apple" | "microsoft"` aliases additionally
172
+ * routes sign-in through that provider, so the user grants attribute access
173
+ * in the same step. A raw issuer URL only scopes the keys.
174
+ * @see {@link IdentityAttributeOpenIdProvider}
175
+ */
64
176
  openIdProvider: IdentityAttributeOpenIdProvider
65
- keys: string[]
66
- identityProvider?: string | URL
67
- windowOpenerFeatures?: string
68
- signIn?: boolean
69
- maxTimeToLive?: bigint
70
- targets?: Principal[]
71
177
  }
72
178
 
179
+ /**
180
+ * Structural subset of `@icp-sdk/auth`'s `AuthClient` that IC Reactor relies
181
+ * on. Declared locally so `@icp-sdk/auth` stays an optional peer dependency.
182
+ */
73
183
  export interface AuthClientLike {
74
184
  getIdentity(): Promise<Identity> | Identity
75
185
  isAuthenticated(): Promise<boolean> | boolean
76
186
  signIn(options?: AuthClientSignInOptions): Promise<Identity>
77
187
  signOut(options?: { returnTo?: string }): Promise<void>
78
- requestAttributes(
79
- params: IdentityAttributeRequest
80
- ): Promise<SignedIdentityAttributes>
188
+ requestAttributes(params: {
189
+ keys: string[]
190
+ // Widened across `@icp-sdk/auth` v7 (value or promise) and v8 (thunk).
191
+ // Method-shorthand parameters are bivariant, so both real signatures are
192
+ // assignable to this one.
193
+ nonce: Uint8Array | Promise<Uint8Array> | (() => Promise<Uint8Array>)
194
+ }): Promise<SignedIdentityAttributes>
195
+ /** Present from `@icp-sdk/auth` v8; used to detect the nonce contract. */
196
+ memoize?<T>(produce: () => T | Promise<T>): Promise<T>
81
197
  }
82
198
 
83
199
  export interface AuthState {
@@ -34,18 +34,18 @@ import {
34
34
  UseMutationResult,
35
35
  InfiniteData,
36
36
  } from "@tanstack/react-query"
37
- import { useActorQuery } from "./hooks/useActorQuery"
38
- import { useActorSuspenseQuery } from "./hooks/useActorSuspenseQuery"
39
- import { useActorInfiniteQuery } from "./hooks/useActorInfiniteQuery"
40
- import { useActorSuspenseInfiniteQuery } from "./hooks/useActorSuspenseInfiniteQuery"
41
- import { useActorMutation } from "./hooks/useActorMutation"
37
+ import { useActorQuery } from "./hooks/useActorQuery.js"
38
+ import { useActorSuspenseQuery } from "./hooks/useActorSuspenseQuery.js"
39
+ import { useActorInfiniteQuery } from "./hooks/useActorInfiniteQuery.js"
40
+ import { useActorSuspenseInfiniteQuery } from "./hooks/useActorSuspenseInfiniteQuery.js"
41
+ import { useActorMutation } from "./hooks/useActorMutation.js"
42
42
  import {
43
43
  useActorMethod,
44
44
  UseActorMethodParameters,
45
- } from "./hooks/useActorMethod"
46
- import { InfiniteQueryConfig } from "./createInfiniteQuery"
47
- import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery"
48
- import { QueryConfig, SuspenseQueryConfig, MutationConfig } from "./types"
45
+ } from "./hooks/useActorMethod.js"
46
+ import { InfiniteQueryConfig } from "./createInfiniteQuery.js"
47
+ import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery.js"
48
+ import { QueryConfig, SuspenseQueryConfig, MutationConfig } from "./types.js"
49
49
 
50
50
  export type ActorHooks<Service, Transform extends TransformKey> = {
51
51
  useActorQuery: {
@@ -22,6 +22,7 @@
22
22
  * postsQuery.invalidate()
23
23
  */
24
24
 
25
+ import { useMemo } from "react"
25
26
  import type {
26
27
  Reactor,
27
28
  FunctionName,
@@ -43,8 +44,12 @@ import {
43
44
  InfiniteQueryObserverOptions,
44
45
  } from "@tanstack/react-query"
45
46
  import { CallConfig } from "@icp-sdk/core/agent"
46
- import { NoInfer } from "./types"
47
- import { mergeFactoryQueryKey, normalizeQueryData } from "./utils"
47
+ import { NoInfer } from "./types.js"
48
+ import {
49
+ buildChainedSelect,
50
+ mergeFactoryQueryKey,
51
+ normalizeQueryData,
52
+ } from "./utils.js"
48
53
 
49
54
  type InfiniteQueryFactoryFn<
50
55
  Service,
@@ -126,6 +131,16 @@ export interface InfiniteQueryConfig<
126
131
  initialPageParam: TPageParam
127
132
  /** Function to get args from page parameter */
128
133
  getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>
134
+ /**
135
+ * Narrows what the cache key derives from the call arguments.
136
+ *
137
+ * By default the key is scoped by `getArgs(initialPageParam)`, so two
138
+ * infinite queries on the same method with different arguments stay in
139
+ * separate cache entries. Supply this when those args embed the cursor and
140
+ * only part of them identifies the query — return the stable, serializable
141
+ * portion (typically everything except the pagination field).
142
+ */
143
+ getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
129
144
  /** Function to determine next page parameter */
130
145
  getNextPageParam: (
131
146
  lastPage: InfiniteQueryPageData<Service, Method, Transform>,
@@ -155,17 +170,12 @@ export type InfiniteQueryFactoryConfig<
155
170
  TPageParam
156
171
  >,
157
172
  > = Omit<
173
+ // `getArgs` is supplied per factory call. `getKeyArgs` is inherited from
174
+ // InfiniteQueryConfig, which now applies it for every infinite query rather
175
+ // than only for factory-created ones.
158
176
  InfiniteQueryConfig<Service, Method, Transform, TPageParam, Selected>,
159
177
  "getArgs"
160
- > & {
161
- /**
162
- * Optional key-args derivation for factory calls.
163
- * Receives the resolved args from `getArgs(initialPageParam)` and should return
164
- * a stable serializable representation of the logical query identity
165
- * (typically excluding pagination/cursor fields).
166
- */
167
- getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
168
- }
178
+ >
169
179
 
170
180
  // ============================================================================
171
181
  // Hook Interface
@@ -292,6 +302,7 @@ const createInfiniteQueryImpl = <
292
302
  queryKey: customQueryKey,
293
303
  initialPageParam,
294
304
  getArgs,
305
+ getKeyArgs,
295
306
  getNextPageParam,
296
307
  getPreviousPageParam,
297
308
  maxPages,
@@ -300,12 +311,22 @@ const createInfiniteQueryImpl = <
300
311
  ...rest
301
312
  } = config
302
313
 
303
- // Get query key from actor manager
314
+ // Fold the call arguments into the key. Without them every infinite query on
315
+ // a method shares one cache entry, so two lists with different `getArgs` (a
316
+ // different page size, a different filter) serve each other's pages — the
317
+ // arguments live in a closure, not in the config, so nothing else
318
+ // distinguishes them.
319
+ //
320
+ // The first page's args stand for the query's identity; `getKeyArgs` lets a
321
+ // caller drop the cursor field when it is part of those args.
304
322
  const getQueryKey = (): QueryKey => {
323
+ const initialArgs = getArgs(initialPageParam)
324
+ const keyArgs = getKeyArgs?.(initialArgs) ?? initialArgs
325
+
305
326
  return reactor.generateQueryKey(
306
327
  {
307
328
  functionName,
308
- queryKey: customQueryKey,
329
+ queryKey: mergeFactoryQueryKey(customQueryKey, undefined, keyArgs),
309
330
  },
310
331
  callConfig
311
332
  )
@@ -368,14 +389,12 @@ const createInfiniteQueryImpl = <
368
389
  Selected,
369
390
  TError
370
391
  > = (options: any): any => {
371
- // Chain the selects: raw -> config.select -> options.select
372
- const chainedSelect = (rawData: TInfiniteData) => {
373
- const firstPass = select ? select(rawData) : rawData
374
- if (options?.select) {
375
- return options.select(firstPass)
376
- }
377
- return firstPass
378
- }
392
+ // Memoized and identity-preserving; see buildChainedSelect for why the
393
+ // function identity matters to the observer's select-result cache.
394
+ const chainedSelect = useMemo(
395
+ () => buildChainedSelect(select as never, options?.select),
396
+ [options?.select]
397
+ )
379
398
 
380
399
  return useInfiniteQuery(
381
400
  {
@@ -535,10 +554,8 @@ export function createInfiniteQueryFactory<
535
554
  > = (
536
555
  getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>
537
556
  ) => {
538
- const initialArgs = getArgs(config.initialPageParam)
539
- const keyArgs = config.getKeyArgs?.(initialArgs) ?? initialArgs
540
- const queryKey = mergeFactoryQueryKey(config.queryKey, undefined, keyArgs)
541
-
557
+ // `getKeyArgs` and the args-derived key segment are applied by the impl,
558
+ // which now does it for every infinite query rather than only for factories.
542
559
  return createInfiniteQueryImpl<
543
560
  Service,
544
561
  Method,
@@ -546,16 +563,13 @@ export function createInfiniteQueryFactory<
546
563
  TPageParam,
547
564
  Selected
548
565
  >(reactor, {
549
- ...(({ getKeyArgs: _getKeyArgs, ...rest }) => rest)(
550
- config as InfiniteQueryFactoryConfig<
551
- Service,
552
- Method,
553
- Transform,
554
- TPageParam,
555
- Selected
556
- >
557
- ),
558
- queryKey,
566
+ ...(config as InfiniteQueryFactoryConfig<
567
+ Service,
568
+ Method,
569
+ Transform,
570
+ TPageParam,
571
+ Selected
572
+ >),
559
573
  getArgs,
560
574
  })
561
575
  }
@@ -39,7 +39,7 @@ import type {
39
39
  MutationResult,
40
40
  MutationHookOptions,
41
41
  NoInfer,
42
- } from "./types"
42
+ } from "./types.js"
43
43
 
44
44
  // ============================================================================
45
45
  // Internal helpers
@@ -91,16 +91,46 @@ const createMutationImpl = <
91
91
 
92
92
  /**
93
93
  * Imperative execution for non-React usage.
94
- * Calls the canister method and invalidates factory-level queries.
94
+ *
95
+ * Runs the same factory-level chain the hook does — invalidation, then
96
+ * `onSuccess`, or `onCanisterError`/`onError` on failure — so a mutation
97
+ * object behaves the same through both call paths. Only hook-level callbacks
98
+ * are absent, because there is no hook here to supply them.
99
+ *
100
+ * The error is rethrown after the callbacks run, so `await execute(...)`
101
+ * still rejects for the caller.
102
+ *
95
103
  * Use this in route loaders, scripts, or server-side code.
96
104
  */
97
105
  const execute = async (
98
106
  args: ReactorArgs<Service, Method, Transform>
99
107
  ): Promise<ReactorReturnOk<Service, Method, Transform>> => {
100
- const result = await callFn(args)
108
+ let result: ReactorReturnOk<Service, Method, Transform>
109
+ try {
110
+ result = await callFn(args)
111
+ } catch (error) {
112
+ if (isCanisterError(error)) {
113
+ factoryOnCanisterError?.(error, args)
114
+ }
115
+ // No mutation context or instance exists on the imperative path.
116
+ factoryOnError?.(
117
+ error as Parameters<NonNullable<typeof factoryOnError>>[0],
118
+ args,
119
+ undefined as never,
120
+ undefined as never
121
+ )
122
+ throw error
123
+ }
124
+
101
125
  if (factoryInvalidateQueries) {
102
126
  await invalidateAll(reactor.queryClient, factoryInvalidateQueries)
103
127
  }
128
+ await factoryOnSuccess?.(
129
+ result,
130
+ args,
131
+ undefined as never,
132
+ undefined as never
133
+ )
104
134
  return result
105
135
  }
106
136
 
@@ -30,6 +30,7 @@ import type {
30
30
  ReactorArgs,
31
31
  TransformKey,
32
32
  } from "@ic-reactor/core"
33
+ import { useMemo } from "react"
33
34
  import {
34
35
  QueryKey,
35
36
  useQuery,
@@ -44,8 +45,8 @@ import type {
44
45
  QueryResult,
45
46
  QueryFactoryConfig,
46
47
  NoInfer,
47
- } from "./types"
48
- import { buildChainedSelect } from "./utils"
48
+ } from "./types.js"
49
+ import { buildChainedSelect } from "./utils.js"
49
50
 
50
51
  // ============================================================================
51
52
  // Internal Implementation
@@ -110,6 +111,12 @@ const createQueryImpl = <
110
111
 
111
112
  const useQueryHook = ((options?: UseQueryHookOptions) => {
112
113
  const baseOptions = reactor.getQueryOptions(params)
114
+ // Memoized so the observer's select-result cache can hit; see
115
+ // buildChainedSelect. `select` comes from the factory config and is stable.
116
+ const chainedSelect = useMemo(
117
+ () => buildChainedSelect(select, options?.select),
118
+ [options?.select]
119
+ )
113
120
  return useQuery(
114
121
  {
115
122
  queryKey: baseOptions.queryKey,
@@ -117,7 +124,7 @@ const createQueryImpl = <
117
124
  ...rest,
118
125
  ...options,
119
126
  queryFn: baseOptions.queryFn,
120
- select: buildChainedSelect(select, options?.select),
127
+ select: chainedSelect,
121
128
  },
122
129
  reactor.queryClient
123
130
  )
@@ -24,6 +24,7 @@
24
24
  * const allPosts = data.pages.flatMap(page => page.posts)
25
25
  */
26
26
 
27
+ import { useMemo } from "react"
27
28
  import type {
28
29
  Reactor,
29
30
  FunctionName,
@@ -45,8 +46,12 @@ import {
45
46
  InfiniteQueryObserverOptions,
46
47
  } from "@tanstack/react-query"
47
48
  import { CallConfig } from "@icp-sdk/core/agent"
48
- import { NoInfer } from "./types"
49
- import { mergeFactoryQueryKey, normalizeQueryData } from "./utils"
49
+ import { NoInfer } from "./types.js"
50
+ import {
51
+ buildChainedSelect,
52
+ mergeFactoryQueryKey,
53
+ normalizeQueryData,
54
+ } from "./utils.js"
50
55
 
51
56
  type SuspenseInfiniteFactoryCallOptions = {
52
57
  queryKey?: QueryKey
@@ -137,6 +142,15 @@ export interface SuspenseInfiniteQueryConfig<
137
142
  queryKey?: QueryKey
138
143
  /** Function to get args from page parameter */
139
144
  getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>
145
+ /**
146
+ * Narrows what the cache key derives from the call arguments.
147
+ *
148
+ * By default the key is scoped by `getArgs(initialPageParam)`, so two
149
+ * infinite queries on the same method with different arguments stay in
150
+ * separate cache entries. Supply this when those args embed the cursor and
151
+ * only part of them identifies the query.
152
+ */
153
+ getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
140
154
  }
141
155
 
142
156
  /**
@@ -152,17 +166,11 @@ export type SuspenseInfiniteQueryFactoryConfig<
152
166
  TPageParam
153
167
  >,
154
168
  > = Omit<
169
+ // `getArgs` is supplied per factory call. `getKeyArgs` is inherited from
170
+ // SuspenseInfiniteQueryConfig, which now applies it for every infinite query.
155
171
  SuspenseInfiniteQueryConfig<Service, Method, Transform, TPageParam, Selected>,
156
172
  "getArgs"
157
- > & {
158
- /**
159
- * Optional key-args derivation for factory calls.
160
- * Receives the resolved args from `getArgs(initialPageParam)` and should return
161
- * a stable serializable representation of the logical query identity
162
- * (typically excluding pagination/cursor fields).
163
- */
164
- getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
165
- }
173
+ >
166
174
 
167
175
  // ============================================================================
168
176
  // Hook Interface
@@ -301,6 +309,7 @@ const createSuspenseInfiniteQueryImpl = <
301
309
  queryKey: customQueryKey,
302
310
  initialPageParam,
303
311
  getArgs,
312
+ getKeyArgs,
304
313
  getNextPageParam,
305
314
  getPreviousPageParam,
306
315
  maxPages,
@@ -309,12 +318,18 @@ const createSuspenseInfiniteQueryImpl = <
309
318
  ...rest
310
319
  } = config
311
320
 
312
- // Get query key from actor manager
321
+ // Fold the call arguments into the key — see the matching comment in
322
+ // createInfiniteQuery. Without them every infinite query on a method shares
323
+ // one cache entry, because the arguments live in a `getArgs` closure rather
324
+ // than in the config.
313
325
  const getQueryKey = (): QueryKey => {
326
+ const initialArgs = getArgs(initialPageParam)
327
+ const keyArgs = getKeyArgs?.(initialArgs) ?? initialArgs
328
+
314
329
  return reactor.generateQueryKey(
315
330
  {
316
331
  functionName,
317
- queryKey: customQueryKey,
332
+ queryKey: mergeFactoryQueryKey(customQueryKey, undefined, keyArgs),
318
333
  },
319
334
  callConfig
320
335
  )
@@ -370,14 +385,12 @@ const createSuspenseInfiniteQueryImpl = <
370
385
  Selected,
371
386
  TError
372
387
  > = (options: any): any => {
373
- // Chain the selects: raw -> config.select -> options.select
374
- const chainedSelect = (rawData: TInfiniteData) => {
375
- const firstPass = select ? select(rawData) : rawData
376
- if (options?.select) {
377
- return options.select(firstPass)
378
- }
379
- return firstPass
380
- }
388
+ // Memoized and identity-preserving; see buildChainedSelect for why the
389
+ // function identity matters to the observer's select-result cache.
390
+ const chainedSelect = useMemo(
391
+ () => buildChainedSelect(select as never, options?.select),
392
+ [options?.select]
393
+ )
381
394
 
382
395
  return useSuspenseInfiniteQuery(
383
396
  {
@@ -550,12 +563,13 @@ export function createSuspenseInfiniteQueryFactory<
550
563
  getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>,
551
564
  options?: SuspenseInfiniteFactoryCallOptions
552
565
  ) => {
553
- const initialArgs = getArgs(config.initialPageParam)
554
- const keyArgs = config.getKeyArgs?.(initialArgs) ?? initialArgs
566
+ // Only the caller-supplied segments are merged here; the args-derived
567
+ // segment is added by the impl, which does it for every infinite query.
568
+ // Applying it in both places would append the args twice.
555
569
  const queryKey = mergeFactoryQueryKey(
556
570
  config.queryKey,
557
571
  options?.queryKey,
558
- keyArgs
572
+ undefined
559
573
  )
560
574
 
561
575
  return createSuspenseInfiniteQueryImpl<
@@ -565,15 +579,13 @@ export function createSuspenseInfiniteQueryFactory<
565
579
  TPageParam,
566
580
  Selected
567
581
  >(reactor, {
568
- ...(({ getKeyArgs: _getKeyArgs, ...rest }) => rest)(
569
- config as SuspenseInfiniteQueryFactoryConfig<
570
- Service,
571
- Method,
572
- Transform,
573
- TPageParam,
574
- Selected
575
- >
576
- ),
582
+ ...(config as SuspenseInfiniteQueryFactoryConfig<
583
+ Service,
584
+ Method,
585
+ Transform,
586
+ TPageParam,
587
+ Selected
588
+ >),
577
589
  queryKey,
578
590
  getArgs,
579
591
  } as SuspenseInfiniteQueryConfig<