@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.
- package/README.md +158 -9
- package/dist/auth/authentication-manager.d.ts +37 -5
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +242 -52
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts +2 -2
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.d.ts +12 -5
- package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.js +42 -28
- package/dist/auth/identity-attributes-manager.js.map +1 -1
- package/dist/auth/identity-attributes.d.ts +1 -1
- package/dist/auth/identity-attributes.d.ts.map +1 -1
- package/dist/auth/identity-attributes.js +1 -1
- package/dist/auth/identity-attributes.js.map +1 -1
- package/dist/auth/index.d.ts +7 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +7 -7
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/types.d.ts +123 -16
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/createActorHooks.d.ts +4 -4
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +6 -6
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +12 -10
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +20 -17
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +22 -2
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts +1 -1
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +6 -2
- package/dist/createQuery.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +11 -10
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +18 -16
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts +1 -1
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +6 -2
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineReactor.d.ts +54 -4
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +77 -7
- package/dist/defineReactor.js.map +1 -1
- package/dist/hooks/createAuthHooks.d.ts +2 -2
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +7 -2
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +16 -2
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +66 -33
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +8 -1
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +9 -1
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +16 -2
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +14 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +21 -10
- package/dist/utils.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +14 -9
- package/src/auth/authentication-manager.ts +267 -64
- package/src/auth/createIdentityAttributeHooks.ts +2 -2
- package/src/auth/identity-attributes-manager.ts +53 -44
- package/src/auth/identity-attributes.ts +2 -2
- package/src/auth/index.ts +7 -7
- package/src/auth/types.ts +134 -18
- package/src/createActorHooks.ts +9 -9
- package/src/createInfiniteQuery.ts +49 -35
- package/src/createMutation.ts +33 -3
- package/src/createQuery.ts +10 -3
- package/src/createSuspenseInfiniteQuery.ts +45 -33
- package/src/createSuspenseQuery.ts +10 -3
- package/src/defineReactor.ts +136 -14
- package/src/hooks/createAuthHooks.ts +8 -3
- package/src/hooks/useActorInfiniteQuery.ts +25 -3
- package/src/hooks/useActorMethod.ts +82 -34
- package/src/hooks/useActorMutation.ts +19 -4
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +25 -3
- package/src/index.ts +11 -11
- package/src/types.ts +8 -2
- package/src/utils.ts +21 -10
- package/src/defineReactor.test.ts +0 -70
- package/src/hooks/useActorInfiniteQuery.test.tsx +0 -457
- package/src/hooks/useActorMethod.test.tsx +0 -798
- package/src/hooks/useActorMutation.test.tsx +0 -220
- package/src/hooks/useActorQuery.test.tsx +0 -287
- package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +0 -472
- package/src/hooks/useActorSuspenseQuery.test.tsx +0 -254
- package/src/utils.test.ts +0 -14
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
ReactorArgs,
|
|
26
26
|
TransformKey,
|
|
27
27
|
} from "@ic-reactor/core"
|
|
28
|
+
import { useMemo } from "react"
|
|
28
29
|
import { useSuspenseQuery } from "@tanstack/react-query"
|
|
29
30
|
import type {
|
|
30
31
|
QueryFnData,
|
|
@@ -34,8 +35,8 @@ import type {
|
|
|
34
35
|
SuspenseQueryResult,
|
|
35
36
|
SuspenseQueryFactoryConfig,
|
|
36
37
|
NoInfer,
|
|
37
|
-
} from "./types"
|
|
38
|
-
import { buildChainedSelect } from "./utils"
|
|
38
|
+
} from "./types.js"
|
|
39
|
+
import { buildChainedSelect } from "./utils.js"
|
|
39
40
|
|
|
40
41
|
// ============================================================================
|
|
41
42
|
// Internal Implementation
|
|
@@ -95,6 +96,12 @@ const createSuspenseQueryImpl = <
|
|
|
95
96
|
TError
|
|
96
97
|
> = (options: any): any => {
|
|
97
98
|
const baseOptions = reactor.getQueryOptions(params)
|
|
99
|
+
// Memoized so the observer's select-result cache can hit; see
|
|
100
|
+
// buildChainedSelect. `select` comes from the factory config and is stable.
|
|
101
|
+
const chainedSelect = useMemo(
|
|
102
|
+
() => buildChainedSelect(select, options?.select),
|
|
103
|
+
[options?.select]
|
|
104
|
+
)
|
|
98
105
|
return useSuspenseQuery(
|
|
99
106
|
{
|
|
100
107
|
queryKey: baseOptions.queryKey,
|
|
@@ -102,7 +109,7 @@ const createSuspenseQueryImpl = <
|
|
|
102
109
|
...rest,
|
|
103
110
|
...options,
|
|
104
111
|
queryFn: baseOptions.queryFn,
|
|
105
|
-
select:
|
|
112
|
+
select: chainedSelect,
|
|
106
113
|
},
|
|
107
114
|
reactor.queryClient
|
|
108
115
|
)
|
package/src/defineReactor.ts
CHANGED
|
@@ -33,8 +33,26 @@
|
|
|
33
33
|
* name: "index",
|
|
34
34
|
* idlFactory: indexIdl,
|
|
35
35
|
* clientManager: ledger.clientManager,
|
|
36
|
+
* authentication: ledger.authentication, // one Internet Identity session
|
|
36
37
|
* })
|
|
37
38
|
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example Internet Identity is wired up out of the box
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const { useAuth, useIdentityAttributes } = defineReactor<_SERVICE>({
|
|
43
|
+
* name: "backend",
|
|
44
|
+
* idlFactory,
|
|
45
|
+
* // Needed when the app is served from more than one origin.
|
|
46
|
+
* auth: { derivationOrigin: "https://app.example.com" },
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* function LoginButton() {
|
|
50
|
+
* const { login, logout, isAuthenticated, principal } = useAuth()
|
|
51
|
+
* return isAuthenticated
|
|
52
|
+
* ? <button onClick={() => logout()}>{principal?.toText()}</button>
|
|
53
|
+
* : <button onClick={() => login()}>Sign in</button>
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
38
56
|
*/
|
|
39
57
|
import { ClientManager, Reactor, DisplayReactor } from "@ic-reactor/core"
|
|
40
58
|
import type {
|
|
@@ -45,7 +63,14 @@ import type {
|
|
|
45
63
|
DisplayReactorParameters,
|
|
46
64
|
} from "@ic-reactor/core"
|
|
47
65
|
import { QueryClient } from "@tanstack/react-query"
|
|
48
|
-
import { createActorHooks, ActorHooks } from "./createActorHooks"
|
|
66
|
+
import { createActorHooks, ActorHooks } from "./createActorHooks.js"
|
|
67
|
+
import { AuthenticationManager } from "./auth/authentication-manager.js"
|
|
68
|
+
import { IdentityAttributesManager } from "./auth/identity-attributes-manager.js"
|
|
69
|
+
import { createIdentityAttributeHooks } from "./auth/createIdentityAttributeHooks.js"
|
|
70
|
+
import type { UseIdentityAttributesReturn } from "./auth/createIdentityAttributeHooks.js"
|
|
71
|
+
import { createAuthHooks } from "./hooks/createAuthHooks.js"
|
|
72
|
+
import type { CreateAuthHooksReturn } from "./hooks/createAuthHooks.js"
|
|
73
|
+
import type { AuthenticationManagerParameters } from "./auth/authentication-manager.js"
|
|
49
74
|
|
|
50
75
|
/** Options shared by both the standard and display variants of defineReactor. */
|
|
51
76
|
export interface DefineReactorSharedParameters
|
|
@@ -55,13 +80,35 @@ export interface DefineReactorSharedParameters
|
|
|
55
80
|
/**
|
|
56
81
|
* Reuse an existing ClientManager (e.g. to share one agent across canisters).
|
|
57
82
|
* When omitted, a ClientManager is created from the agent options below.
|
|
83
|
+
*
|
|
84
|
+
* A supplied or adopted manager brings its own agent and QueryClient, so
|
|
85
|
+
* `agentOptions` and `queryClient` apply only when this call creates one.
|
|
58
86
|
*/
|
|
59
87
|
clientManager?: ClientManager
|
|
60
88
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
89
|
+
* QueryClient for a ClientManager created by this call.
|
|
90
|
+
*
|
|
91
|
+
* Ignored when `clientManager` or `authentication` is supplied — queries run
|
|
92
|
+
* against that manager's own QueryClient, which is what is returned.
|
|
63
93
|
*/
|
|
64
94
|
queryClient?: QueryClient
|
|
95
|
+
/**
|
|
96
|
+
* Reuse an existing AuthenticationManager, so several reactors share one
|
|
97
|
+
* Internet Identity session. When omitted, one is created for this reactor.
|
|
98
|
+
*
|
|
99
|
+
* Its `clientManager` is adopted for this reactor, so sign-in updates the
|
|
100
|
+
* same agent the reactor calls through. Supplying a different `clientManager`
|
|
101
|
+
* alongside it is rejected.
|
|
102
|
+
*/
|
|
103
|
+
authentication?: AuthenticationManager
|
|
104
|
+
/**
|
|
105
|
+
* Internet Identity options forwarded to the AuthenticationManager
|
|
106
|
+
* (`identityProvider`, `derivationOrigin`, `idleOptions`, `storage`, …).
|
|
107
|
+
*
|
|
108
|
+
* Mutually exclusive with `authentication`: a manager built elsewhere is
|
|
109
|
+
* already configured, so these could not be applied to it.
|
|
110
|
+
*/
|
|
111
|
+
auth?: Omit<AuthenticationManagerParameters, "clientManager">
|
|
65
112
|
}
|
|
66
113
|
|
|
67
114
|
/** Parameters for a standard (raw Candid types) reactor. */
|
|
@@ -83,11 +130,17 @@ export type DefineReactorResult<
|
|
|
83
130
|
Service,
|
|
84
131
|
Transform extends TransformKey,
|
|
85
132
|
R extends Reactor<Service, Transform>,
|
|
86
|
-
> = ActorHooks<Service, Transform> &
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
133
|
+
> = ActorHooks<Service, Transform> &
|
|
134
|
+
CreateAuthHooksReturn & {
|
|
135
|
+
reactor: R
|
|
136
|
+
clientManager: ClientManager
|
|
137
|
+
queryClient: QueryClient
|
|
138
|
+
/** Internet Identity session manager backing `useAuth`. */
|
|
139
|
+
authentication: AuthenticationManager
|
|
140
|
+
/** Signed identity attribute requests backing `useIdentityAttributes`. */
|
|
141
|
+
identityAttributes: IdentityAttributesManager
|
|
142
|
+
useIdentityAttributes: () => UseIdentityAttributesReturn
|
|
143
|
+
}
|
|
91
144
|
|
|
92
145
|
export function defineReactor<Service = BaseActor>(
|
|
93
146
|
params: DefineDisplayReactorParameters<Service>
|
|
@@ -104,6 +157,8 @@ export function defineReactor<Service = BaseActor>(
|
|
|
104
157
|
const {
|
|
105
158
|
clientManager: providedClientManager,
|
|
106
159
|
queryClient: providedQueryClient,
|
|
160
|
+
authentication: providedAuthentication,
|
|
161
|
+
auth,
|
|
107
162
|
display,
|
|
108
163
|
agentOptions,
|
|
109
164
|
name,
|
|
@@ -112,18 +167,44 @@ export function defineReactor<Service = BaseActor>(
|
|
|
112
167
|
pollingOptions,
|
|
113
168
|
} = params as DefineDisplayReactorParameters<Service>
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
170
|
+
// A shared AuthenticationManager updates the identity on its own
|
|
171
|
+
// ClientManager. Giving this reactor a different one would leave its calls
|
|
172
|
+
// anonymous after sign-in, so adopt the manager's rather than building a new
|
|
173
|
+
// one, and refuse an explicit mismatch instead of splitting them silently.
|
|
174
|
+
if (
|
|
175
|
+
providedClientManager &&
|
|
176
|
+
providedAuthentication &&
|
|
177
|
+
providedAuthentication.clientManager !== providedClientManager
|
|
178
|
+
) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`[ic-reactor] defineReactor("${name}") received an \`authentication\` manager bound to a different \`clientManager\`. ` +
|
|
181
|
+
`Sign-in would update the authentication manager's agent while this reactor calls through another one, ` +
|
|
182
|
+
`leaving its calls anonymous. Pass \`clientManager: authentication.clientManager\`, or omit \`clientManager\` to adopt it.`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// `auth` configures a manager this call would build; an existing one is
|
|
187
|
+
// already constructed, so these options could only be dropped on the floor.
|
|
188
|
+
if (providedAuthentication && auth) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`[ic-reactor] defineReactor("${name}") received both \`authentication\` and \`auth\`. ` +
|
|
191
|
+
`The supplied manager is already configured, so \`auth\` (${Object.keys(auth).join(", ")}) would be ignored. ` +
|
|
192
|
+
`Pass those options where that AuthenticationManager is created, or drop \`authentication\` to build one here.`
|
|
193
|
+
)
|
|
194
|
+
}
|
|
119
195
|
|
|
120
196
|
const clientManager =
|
|
121
197
|
providedClientManager ??
|
|
198
|
+
providedAuthentication?.clientManager ??
|
|
122
199
|
new ClientManager({
|
|
123
|
-
queryClient,
|
|
200
|
+
queryClient: providedQueryClient ?? new QueryClient(),
|
|
124
201
|
agentOptions,
|
|
125
202
|
})
|
|
126
203
|
|
|
204
|
+
// Always report the QueryClient actually in use: when a ClientManager is
|
|
205
|
+
// supplied or adopted, its own QueryClient is the one queries run against.
|
|
206
|
+
const queryClient = clientManager.queryClient
|
|
207
|
+
|
|
127
208
|
const reactorConfig = {
|
|
128
209
|
clientManager,
|
|
129
210
|
name,
|
|
@@ -146,5 +227,46 @@ export function defineReactor<Service = BaseActor>(
|
|
|
146
227
|
|
|
147
228
|
const hooks = createActorHooks(reactor)
|
|
148
229
|
|
|
149
|
-
|
|
230
|
+
// Auth is built on first use. `AuthenticationManager` dynamically imports the
|
|
231
|
+
// optional `@icp-sdk/auth` peer as soon as it is constructed, and reactors
|
|
232
|
+
// that never touch authentication should not pay for that.
|
|
233
|
+
let authenticationInstance: AuthenticationManager | undefined
|
|
234
|
+
const getAuthentication = () =>
|
|
235
|
+
(authenticationInstance ??=
|
|
236
|
+
providedAuthentication ??
|
|
237
|
+
new AuthenticationManager({ ...auth, clientManager }))
|
|
238
|
+
|
|
239
|
+
let identityAttributesInstance: IdentityAttributesManager | undefined
|
|
240
|
+
const getIdentityAttributes = () =>
|
|
241
|
+
(identityAttributesInstance ??= new IdentityAttributesManager(
|
|
242
|
+
getAuthentication()
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
let authHooks: CreateAuthHooksReturn | undefined
|
|
246
|
+
const getAuthHooks = () =>
|
|
247
|
+
(authHooks ??= createAuthHooks(getAuthentication()))
|
|
248
|
+
|
|
249
|
+
let attributeHooks:
|
|
250
|
+
ReturnType<typeof createIdentityAttributeHooks> | undefined
|
|
251
|
+
const getAttributeHooks = () =>
|
|
252
|
+
(attributeHooks ??= createIdentityAttributeHooks(getIdentityAttributes()))
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
...hooks,
|
|
256
|
+
reactor,
|
|
257
|
+
clientManager,
|
|
258
|
+
queryClient,
|
|
259
|
+
// Stable wrappers: the hook call order inside them never changes, so the
|
|
260
|
+
// rules of hooks still hold.
|
|
261
|
+
useAuth: () => getAuthHooks().useAuth(),
|
|
262
|
+
useAgentState: () => getAuthHooks().useAgentState(),
|
|
263
|
+
useUserPrincipal: () => getAuthHooks().useUserPrincipal(),
|
|
264
|
+
useIdentityAttributes: () => getAttributeHooks().useIdentityAttributes(),
|
|
265
|
+
get authentication() {
|
|
266
|
+
return getAuthentication()
|
|
267
|
+
},
|
|
268
|
+
get identityAttributes() {
|
|
269
|
+
return getIdentityAttributes()
|
|
270
|
+
},
|
|
271
|
+
}
|
|
150
272
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useSyncExternalStore, useEffect, useRef, useMemo } from "react"
|
|
2
|
-
import type { AuthenticationManager } from "../auth/authentication-manager"
|
|
3
|
-
import type { AuthState, AuthenticationSignInOptions } from "../auth/types"
|
|
2
|
+
import type { AuthenticationManager } from "../auth/authentication-manager.js"
|
|
3
|
+
import type { AuthState, AuthenticationSignInOptions } from "../auth/types.js"
|
|
4
4
|
import type { AgentState } from "@ic-reactor/core"
|
|
5
5
|
import type { Principal } from "@icp-sdk/core/principal"
|
|
6
6
|
import type { Identity } from "@icp-sdk/core/agent"
|
|
@@ -90,7 +90,9 @@ export const createAuthHooks = (
|
|
|
90
90
|
// Track if we've already initialized to avoid duplicate calls
|
|
91
91
|
const initializedRef = useRef(false)
|
|
92
92
|
|
|
93
|
-
// Auto-initialize on first mount to restore previous session
|
|
93
|
+
// Auto-initialize on first mount to restore previous session.
|
|
94
|
+
// `prepareClient` also warms up the AuthClient so a later `login()` can
|
|
95
|
+
// open the identity provider window inside the click handler.
|
|
94
96
|
useEffect(() => {
|
|
95
97
|
if (!initializedRef.current) {
|
|
96
98
|
initializedRef.current = true
|
|
@@ -99,6 +101,9 @@ export const createAuthHooks = (
|
|
|
99
101
|
.catch(() => undefined)
|
|
100
102
|
.then(() => clientManager.initialize())
|
|
101
103
|
.then(() => authentication.authenticate())
|
|
104
|
+
// Failures are already reflected in authState/agentState; without
|
|
105
|
+
// this the rejection escapes as an unhandled promise rejection.
|
|
106
|
+
.catch(() => undefined)
|
|
102
107
|
}
|
|
103
108
|
}, [])
|
|
104
109
|
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
ReactorReturnErr,
|
|
17
17
|
} from "@ic-reactor/core"
|
|
18
18
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
19
|
-
import { normalizeQueryData } from "../utils"
|
|
19
|
+
import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Parameters for useActorInfiniteQuery hook.
|
|
@@ -119,8 +119,30 @@ export const useActorInfiniteQuery = <
|
|
|
119
119
|
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
120
120
|
// collisions if two different actors or methods share the same key string.
|
|
121
121
|
const baseQueryKey = useMemo(
|
|
122
|
-
() =>
|
|
123
|
-
|
|
122
|
+
() =>
|
|
123
|
+
reactor.generateQueryKey(
|
|
124
|
+
{
|
|
125
|
+
functionName,
|
|
126
|
+
// Fold the call arguments into the key. They live in the `getArgs`
|
|
127
|
+
// closure rather than in the config, so without this two hooks on the
|
|
128
|
+
// same method with different arguments share one cache entry and
|
|
129
|
+
// serve each other's pages.
|
|
130
|
+
queryKey: mergeFactoryQueryKey(
|
|
131
|
+
queryKey,
|
|
132
|
+
undefined,
|
|
133
|
+
getArgs(options.initialPageParam)
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
callConfig
|
|
137
|
+
),
|
|
138
|
+
[
|
|
139
|
+
queryKey,
|
|
140
|
+
reactor,
|
|
141
|
+
functionName,
|
|
142
|
+
callConfig,
|
|
143
|
+
getArgs,
|
|
144
|
+
options.initialPageParam,
|
|
145
|
+
]
|
|
124
146
|
)
|
|
125
147
|
|
|
126
148
|
// Memoize queryFn to prevent recreation on every render
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react"
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react"
|
|
2
2
|
import {
|
|
3
3
|
useQuery,
|
|
4
4
|
useMutation,
|
|
@@ -188,17 +188,30 @@ export function useActorMethod<
|
|
|
188
188
|
|
|
189
189
|
const functionType: FunctionType = isQuery ? "query" : "update"
|
|
190
190
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
// Latest callbacks, read at dispatch time: this keeps a rerendered closure
|
|
192
|
+
// from being ignored, and keeps callback identity out of the effect deps.
|
|
193
|
+
const onSuccessRef = useRef(onSuccess)
|
|
194
|
+
const onErrorRef = useRef(onError)
|
|
195
|
+
onSuccessRef.current = onSuccess
|
|
196
|
+
onErrorRef.current = onError
|
|
197
|
+
|
|
198
|
+
// Build the key for a given set of call arguments.
|
|
199
|
+
//
|
|
200
|
+
// A custom `queryKey` is appended to the canister/function prefix rather than
|
|
201
|
+
// replacing it, matching `useActorQuery`. Used verbatim it would carry no
|
|
202
|
+
// canister id, so the entry would be invisible to the canister-scoped
|
|
203
|
+
// invalidation `ClientManager.updateAgent` runs on identity change and would
|
|
204
|
+
// keep serving the previous principal's data after sign-in or sign-out.
|
|
205
|
+
const buildQueryKey = useCallback(
|
|
206
|
+
(keyArgs: ReactorArgs<Service, Method, Transform> | undefined): QueryKey =>
|
|
207
|
+
reactor.generateQueryKey(
|
|
208
|
+
{ functionName, args: keyArgs, queryKey: customQueryKey },
|
|
209
|
+
callConfig
|
|
210
|
+
),
|
|
211
|
+
[reactor, functionName, callConfig, customQueryKey]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const queryKey = useMemo(() => buildQueryKey(args), [buildQueryKey, args])
|
|
202
215
|
|
|
203
216
|
// ============================================================================
|
|
204
217
|
// Query Implementation
|
|
@@ -210,26 +223,51 @@ export function useActorMethod<
|
|
|
210
223
|
>(
|
|
211
224
|
{
|
|
212
225
|
queryKey,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
throw error
|
|
225
|
-
}
|
|
226
|
-
},
|
|
226
|
+
// Callbacks deliberately do NOT live here: a queryFn runs once per fetch
|
|
227
|
+
// attempt, so `onError` fired on every retry (four times with the default
|
|
228
|
+
// QueryClient) and `onSuccess` never fired when data came from the cache
|
|
229
|
+
// or a deduped sibling. They are dispatched from the settled observer
|
|
230
|
+
// result below instead.
|
|
231
|
+
queryFn: () =>
|
|
232
|
+
reactor.callMethod({
|
|
233
|
+
functionName,
|
|
234
|
+
args,
|
|
235
|
+
callConfig,
|
|
236
|
+
}),
|
|
227
237
|
enabled: isQuery && enabled,
|
|
228
238
|
...queryOptions,
|
|
229
239
|
},
|
|
230
240
|
reactor.queryClient
|
|
231
241
|
)
|
|
232
242
|
|
|
243
|
+
// Dispatch the query callbacks from the settled observer result, once per
|
|
244
|
+
// distinct outcome. TanStack v5 removed `onSuccess`/`onError` from useQuery
|
|
245
|
+
// for exactly this reason, so the settle timestamps are what identify a new
|
|
246
|
+
// result: they also advance for a cache hit on a fresh mount, which is the
|
|
247
|
+
// case that previously never notified at all.
|
|
248
|
+
const notifiedSuccessAt = useRef<number | undefined>(undefined)
|
|
249
|
+
const notifiedErrorAt = useRef<number | undefined>(undefined)
|
|
250
|
+
|
|
251
|
+
const { status, data, error, dataUpdatedAt, errorUpdatedAt } = queryResult
|
|
252
|
+
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!isQuery) return
|
|
255
|
+
if (status === "success" && dataUpdatedAt !== notifiedSuccessAt.current) {
|
|
256
|
+
notifiedSuccessAt.current = dataUpdatedAt
|
|
257
|
+
onSuccessRef.current?.(data)
|
|
258
|
+
}
|
|
259
|
+
}, [isQuery, status, data, dataUpdatedAt])
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!isQuery) return
|
|
263
|
+
if (status === "error" && errorUpdatedAt !== notifiedErrorAt.current) {
|
|
264
|
+
notifiedErrorAt.current = errorUpdatedAt
|
|
265
|
+
onErrorRef.current?.(
|
|
266
|
+
error as ReactorReturnErr<Service, Method, Transform>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}, [isQuery, status, error, errorUpdatedAt])
|
|
270
|
+
|
|
233
271
|
// ============================================================================
|
|
234
272
|
// Mutation Implementation
|
|
235
273
|
// ============================================================================
|
|
@@ -250,7 +288,7 @@ export function useActorMethod<
|
|
|
250
288
|
return result
|
|
251
289
|
},
|
|
252
290
|
onSuccess: (data) => {
|
|
253
|
-
|
|
291
|
+
onSuccessRef.current?.(data)
|
|
254
292
|
// Invalidate specified queries after successful mutation
|
|
255
293
|
if (invalidateQueries && invalidateQueries.length > 0) {
|
|
256
294
|
invalidateQueries.forEach((key) => {
|
|
@@ -259,7 +297,7 @@ export function useActorMethod<
|
|
|
259
297
|
}
|
|
260
298
|
},
|
|
261
299
|
onError: (error) => {
|
|
262
|
-
|
|
300
|
+
onErrorRef.current?.(error)
|
|
263
301
|
},
|
|
264
302
|
},
|
|
265
303
|
reactor.queryClient
|
|
@@ -276,9 +314,14 @@ export function useActorMethod<
|
|
|
276
314
|
if (isQuery) {
|
|
277
315
|
// For queries, refetch with new args if provided
|
|
278
316
|
if (callArgs !== undefined) {
|
|
317
|
+
// Key on the args actually being called. Reusing the hook's
|
|
318
|
+
// mount-time key would store this result under the previous args'
|
|
319
|
+
// entry — poisoning it for every other reader — and let fetchQuery
|
|
320
|
+
// dedupe onto an in-flight request for the old args, returning that
|
|
321
|
+
// response as though it answered this one.
|
|
279
322
|
try {
|
|
280
323
|
const result = await reactor.queryClient.fetchQuery({
|
|
281
|
-
queryKey,
|
|
324
|
+
queryKey: buildQueryKey(callArgs),
|
|
282
325
|
queryFn: () =>
|
|
283
326
|
reactor.callMethod({
|
|
284
327
|
functionName,
|
|
@@ -287,10 +330,17 @@ export function useActorMethod<
|
|
|
287
330
|
}),
|
|
288
331
|
staleTime: 0,
|
|
289
332
|
})
|
|
290
|
-
|
|
333
|
+
// Dispatched here rather than by the observer effect: this result
|
|
334
|
+
// lands under the called args' key, which the mounted observer (bound
|
|
335
|
+
// to the hook's own args) does not watch. That separation is also why
|
|
336
|
+
// this can no longer double-fire the way it did when both wrote to
|
|
337
|
+
// the same key.
|
|
338
|
+
onSuccessRef.current?.(result)
|
|
291
339
|
return result
|
|
292
340
|
} catch (error) {
|
|
293
|
-
|
|
341
|
+
onErrorRef.current?.(
|
|
342
|
+
error as ReactorReturnErr<Service, Method, Transform>
|
|
343
|
+
)
|
|
294
344
|
return undefined
|
|
295
345
|
}
|
|
296
346
|
}
|
|
@@ -309,11 +359,9 @@ export function useActorMethod<
|
|
|
309
359
|
reactor,
|
|
310
360
|
functionName,
|
|
311
361
|
callConfig,
|
|
312
|
-
|
|
362
|
+
buildQueryKey,
|
|
313
363
|
queryResult,
|
|
314
364
|
mutationResult,
|
|
315
|
-
onSuccess,
|
|
316
|
-
onError,
|
|
317
365
|
]
|
|
318
366
|
)
|
|
319
367
|
|
|
@@ -35,7 +35,14 @@ export interface UseActorMutationParameters<
|
|
|
35
35
|
reactor: Reactor<Service, Transform>
|
|
36
36
|
functionName: Method
|
|
37
37
|
callConfig?: CallConfig
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Queries to invalidate upon successful mutation.
|
|
40
|
+
*
|
|
41
|
+
* `undefined` entries are skipped, so the common
|
|
42
|
+
* `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
|
|
43
|
+
* is absent.
|
|
44
|
+
*/
|
|
45
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
39
46
|
/**
|
|
40
47
|
* Callback for canister-level business logic errors.
|
|
41
48
|
* Called when the canister returns a Result { Err: E } variant.
|
|
@@ -126,10 +133,18 @@ export const useActorMutation = <
|
|
|
126
133
|
>
|
|
127
134
|
) => {
|
|
128
135
|
if (invalidateQueries) {
|
|
136
|
+
// Skip undefined entries. React Query reads `{ queryKey: undefined }`
|
|
137
|
+
// as "match everything", so a single undefined — which the natural
|
|
138
|
+
// `invalidateQueries: [maybeQuery?.getQueryKey()]` idiom produces
|
|
139
|
+
// whenever the optional query object is absent — would invalidate every
|
|
140
|
+
// query in the client, including an app's unrelated non-canister ones.
|
|
141
|
+
// `createMutation.invalidateAll` already filters the same way.
|
|
129
142
|
await Promise.all(
|
|
130
|
-
invalidateQueries
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
invalidateQueries
|
|
144
|
+
.filter((queryKey) => queryKey !== undefined)
|
|
145
|
+
.map((queryKey) =>
|
|
146
|
+
reactor.queryClient.invalidateQueries({ queryKey })
|
|
147
|
+
)
|
|
133
148
|
)
|
|
134
149
|
}
|
|
135
150
|
await onSuccess?.(...params)
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
ReactorReturnErr,
|
|
17
17
|
} from "@ic-reactor/core"
|
|
18
18
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
19
|
-
import { normalizeQueryData } from "../utils"
|
|
19
|
+
import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Parameters for useActorSuspenseInfiniteQuery hook.
|
|
@@ -129,8 +129,30 @@ export const useActorSuspenseInfiniteQuery = <
|
|
|
129
129
|
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
130
130
|
// collisions if two different actors or methods share the same key string.
|
|
131
131
|
const baseQueryKey = useMemo(
|
|
132
|
-
() =>
|
|
133
|
-
|
|
132
|
+
() =>
|
|
133
|
+
reactor.generateQueryKey(
|
|
134
|
+
{
|
|
135
|
+
functionName,
|
|
136
|
+
// Fold the call arguments into the key. They live in the `getArgs`
|
|
137
|
+
// closure rather than in the config, so without this two hooks on the
|
|
138
|
+
// same method with different arguments share one cache entry and
|
|
139
|
+
// serve each other's pages.
|
|
140
|
+
queryKey: mergeFactoryQueryKey(
|
|
141
|
+
queryKey,
|
|
142
|
+
undefined,
|
|
143
|
+
getArgs(options.initialPageParam)
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
callConfig
|
|
147
|
+
),
|
|
148
|
+
[
|
|
149
|
+
queryKey,
|
|
150
|
+
reactor,
|
|
151
|
+
functionName,
|
|
152
|
+
callConfig,
|
|
153
|
+
getArgs,
|
|
154
|
+
options.initialPageParam,
|
|
155
|
+
]
|
|
134
156
|
)
|
|
135
157
|
|
|
136
158
|
// Memoize queryFn to prevent recreation on every render
|
package/src/index.ts
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
export * from "@ic-reactor/core"
|
|
4
4
|
|
|
5
5
|
// Re-export hooks
|
|
6
|
-
export * from "./hooks"
|
|
6
|
+
export * from "./hooks/index.js"
|
|
7
7
|
|
|
8
8
|
// Validation utilities for React
|
|
9
|
-
export * from "./validation"
|
|
9
|
+
export * from "./validation.js"
|
|
10
10
|
|
|
11
11
|
// React-specific exports
|
|
12
|
-
export * from "./createActorHooks"
|
|
13
|
-
export * from "./defineReactor"
|
|
12
|
+
export * from "./createActorHooks.js"
|
|
13
|
+
export * from "./defineReactor.js"
|
|
14
14
|
|
|
15
|
-
export * from "./createQuery"
|
|
16
|
-
export * from "./createSuspenseQuery"
|
|
17
|
-
export * from "./createInfiniteQuery"
|
|
18
|
-
export * from "./createSuspenseInfiniteQuery"
|
|
19
|
-
export * from "./createMutation"
|
|
15
|
+
export * from "./createQuery.js"
|
|
16
|
+
export * from "./createSuspenseQuery.js"
|
|
17
|
+
export * from "./createInfiniteQuery.js"
|
|
18
|
+
export * from "./createSuspenseInfiniteQuery.js"
|
|
19
|
+
export * from "./createMutation.js"
|
|
20
20
|
|
|
21
|
-
export * from "./auth"
|
|
21
|
+
export * from "./auth/index.js"
|
|
22
22
|
|
|
23
|
-
export * from "./types"
|
|
23
|
+
export * from "./types.js"
|
package/src/types.ts
CHANGED
|
@@ -356,8 +356,14 @@ export interface MutationConfig<
|
|
|
356
356
|
functionName: Method
|
|
357
357
|
/** Call configuration for the actor method */
|
|
358
358
|
callConfig?: CallConfig
|
|
359
|
-
/**
|
|
360
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Queries to invalidate upon successful mutation.
|
|
361
|
+
*
|
|
362
|
+
* `undefined` entries are skipped, so the common
|
|
363
|
+
* `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
|
|
364
|
+
* is absent.
|
|
365
|
+
*/
|
|
366
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
361
367
|
/**
|
|
362
368
|
* Callback for canister-level business logic errors.
|
|
363
369
|
* Called when the canister returns a Result { Err: E } variant.
|