@ic-reactor/react 3.8.0 → 3.10.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 +84 -10
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +146 -21
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.js +47 -5
- package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +11 -9
- 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.map +1 -1
- package/dist/createMutation.js +37 -3
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +7 -3
- package/dist/createQuery.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +10 -9
- 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.map +1 -1
- package/dist/createSuspenseQuery.js +7 -3
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +17 -2
- package/dist/defineReactor.js.map +1 -1
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +13 -0
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +21 -2
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +76 -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 +23 -7
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorQuery.d.ts.map +1 -1
- package/dist/hooks/useActorQuery.js +12 -1
- package/dist/hooks/useActorQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +21 -2
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.js +12 -1
- package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +27 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +65 -9
- package/dist/utils.js.map +1 -1
- package/package.json +8 -2
- package/src/auth/authentication-manager.ts +162 -21
- package/src/auth/createIdentityAttributeHooks.ts +54 -5
- package/src/createInfiniteQuery.ts +48 -34
- package/src/createMutation.ts +48 -2
- package/src/createQuery.ts +17 -10
- package/src/createSuspenseInfiniteQuery.ts +44 -32
- package/src/createSuspenseQuery.ts +17 -10
- package/src/defineReactor.ts +24 -2
- package/src/hooks/createAuthHooks.ts +18 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useActorInfiniteQuery.ts +30 -3
- package/src/hooks/useActorMethod.ts +91 -34
- package/src/hooks/useActorMutation.ts +35 -12
- package/src/hooks/useActorQuery.ts +11 -1
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +30 -3
- package/src/hooks/useActorSuspenseQuery.ts +11 -1
- package/src/types.ts +8 -2
- package/src/utils.ts +66 -10
package/src/createMutation.ts
CHANGED
|
@@ -76,6 +76,8 @@ const createMutationImpl = <
|
|
|
76
76
|
onSuccess: factoryOnSuccess,
|
|
77
77
|
onCanisterError: factoryOnCanisterError,
|
|
78
78
|
onError: factoryOnError,
|
|
79
|
+
onMutate: factoryOnMutate,
|
|
80
|
+
onSettled: factoryOnSettled,
|
|
79
81
|
...factoryOptions
|
|
80
82
|
} = config
|
|
81
83
|
|
|
@@ -91,16 +93,46 @@ const createMutationImpl = <
|
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
95
|
* Imperative execution for non-React usage.
|
|
94
|
-
*
|
|
96
|
+
*
|
|
97
|
+
* Runs the same factory-level chain the hook does — invalidation, then
|
|
98
|
+
* `onSuccess`, or `onCanisterError`/`onError` on failure — so a mutation
|
|
99
|
+
* object behaves the same through both call paths. Only hook-level callbacks
|
|
100
|
+
* are absent, because there is no hook here to supply them.
|
|
101
|
+
*
|
|
102
|
+
* The error is rethrown after the callbacks run, so `await execute(...)`
|
|
103
|
+
* still rejects for the caller.
|
|
104
|
+
*
|
|
95
105
|
* Use this in route loaders, scripts, or server-side code.
|
|
96
106
|
*/
|
|
97
107
|
const execute = async (
|
|
98
108
|
args: ReactorArgs<Service, Method, Transform>
|
|
99
109
|
): Promise<ReactorReturnOk<Service, Method, Transform>> => {
|
|
100
|
-
|
|
110
|
+
let result: ReactorReturnOk<Service, Method, Transform>
|
|
111
|
+
try {
|
|
112
|
+
result = await callFn(args)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isCanisterError(error)) {
|
|
115
|
+
factoryOnCanisterError?.(error, args)
|
|
116
|
+
}
|
|
117
|
+
// No mutation context or instance exists on the imperative path.
|
|
118
|
+
factoryOnError?.(
|
|
119
|
+
error as Parameters<NonNullable<typeof factoryOnError>>[0],
|
|
120
|
+
args,
|
|
121
|
+
undefined as never,
|
|
122
|
+
undefined as never
|
|
123
|
+
)
|
|
124
|
+
throw error
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
if (factoryInvalidateQueries) {
|
|
102
128
|
await invalidateAll(reactor.queryClient, factoryInvalidateQueries)
|
|
103
129
|
}
|
|
130
|
+
await factoryOnSuccess?.(
|
|
131
|
+
result,
|
|
132
|
+
args,
|
|
133
|
+
undefined as never,
|
|
134
|
+
undefined as never
|
|
135
|
+
)
|
|
104
136
|
return result
|
|
105
137
|
}
|
|
106
138
|
|
|
@@ -145,6 +177,20 @@ const createMutationImpl = <
|
|
|
145
177
|
factoryOnError?.(error, variables, context, mutation)
|
|
146
178
|
restOptions.onError?.(error, variables, context, mutation)
|
|
147
179
|
},
|
|
180
|
+
// `onMutate` and `onSettled` are composed like `onSuccess`/`onError`
|
|
181
|
+
// above. They used to arrive through the `...restOptions` spread, so a
|
|
182
|
+
// hook-level one silently replaced the factory's — factory teardown,
|
|
183
|
+
// telemetry or logging simply vanished the moment any call site passed
|
|
184
|
+
// its own, with no warning and no type error. Chaining is what a
|
|
185
|
+
// reader who has seen `onSuccess` chain already expects.
|
|
186
|
+
onMutate: async (...params) => {
|
|
187
|
+
await factoryOnMutate?.(...params)
|
|
188
|
+
return await restOptions.onMutate?.(...params)
|
|
189
|
+
},
|
|
190
|
+
onSettled: async (...params) => {
|
|
191
|
+
await factoryOnSettled?.(...params)
|
|
192
|
+
await restOptions.onSettled?.(...params)
|
|
193
|
+
},
|
|
148
194
|
},
|
|
149
195
|
reactor.queryClient
|
|
150
196
|
)
|
package/src/createQuery.ts
CHANGED
|
@@ -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,
|
|
@@ -45,7 +46,7 @@ import type {
|
|
|
45
46
|
QueryFactoryConfig,
|
|
46
47
|
NoInfer,
|
|
47
48
|
} from "./types.js"
|
|
48
|
-
import { buildChainedSelect } from "./utils.js"
|
|
49
|
+
import { buildChainedSelect, createBoundedCache } 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:
|
|
127
|
+
select: chainedSelect,
|
|
121
128
|
},
|
|
122
129
|
reactor.queryClient
|
|
123
130
|
)
|
|
@@ -198,14 +205,14 @@ export function createQueryFactory<
|
|
|
198
205
|
Selected,
|
|
199
206
|
QueryError<Service, Method, Transform>
|
|
200
207
|
> {
|
|
201
|
-
const cache =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
const cache =
|
|
209
|
+
createBoundedCache<
|
|
210
|
+
QueryResult<
|
|
211
|
+
QueryFnData<Service, Method, Transform>,
|
|
212
|
+
Selected,
|
|
213
|
+
QueryError<Service, Method, Transform>
|
|
214
|
+
>
|
|
215
|
+
>()
|
|
209
216
|
|
|
210
217
|
return (args: ReactorArgs<Service, Method, Transform>) => {
|
|
211
218
|
const key = reactor.generateQueryKey({
|
|
@@ -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,
|
|
@@ -46,7 +47,11 @@ import {
|
|
|
46
47
|
} from "@tanstack/react-query"
|
|
47
48
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
48
49
|
import { NoInfer } from "./types.js"
|
|
49
|
-
import {
|
|
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
|
-
//
|
|
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
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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<
|
|
@@ -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,
|
|
@@ -35,7 +36,7 @@ import type {
|
|
|
35
36
|
SuspenseQueryFactoryConfig,
|
|
36
37
|
NoInfer,
|
|
37
38
|
} from "./types.js"
|
|
38
|
-
import { buildChainedSelect } from "./utils.js"
|
|
39
|
+
import { buildChainedSelect, createBoundedCache } 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
|
)
|
|
@@ -188,14 +195,14 @@ export function createSuspenseQueryFactory<
|
|
|
188
195
|
Selected,
|
|
189
196
|
QueryError<Service, Method, Transform>
|
|
190
197
|
> {
|
|
191
|
-
const cache =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
const cache =
|
|
199
|
+
createBoundedCache<
|
|
200
|
+
SuspenseQueryResult<
|
|
201
|
+
QueryFnData<Service, Method, Transform>,
|
|
202
|
+
Selected,
|
|
203
|
+
QueryError<Service, Method, Transform>
|
|
204
|
+
>
|
|
205
|
+
>()
|
|
199
206
|
|
|
200
207
|
return (args: ReactorArgs<Service, Method, Transform>) => {
|
|
201
208
|
const key = reactor.generateQueryKey({
|
package/src/defineReactor.ts
CHANGED
|
@@ -54,7 +54,12 @@
|
|
|
54
54
|
* }
|
|
55
55
|
* ```
|
|
56
56
|
*/
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
ClientManager,
|
|
59
|
+
Reactor,
|
|
60
|
+
DisplayReactor,
|
|
61
|
+
reactorRetry,
|
|
62
|
+
} from "@ic-reactor/core"
|
|
58
63
|
import type {
|
|
59
64
|
BaseActor,
|
|
60
65
|
TransformKey,
|
|
@@ -142,6 +147,23 @@ export type DefineReactorResult<
|
|
|
142
147
|
useIdentityAttributes: () => UseIdentityAttributesReturn
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The QueryClient this module creates when the caller does not supply one.
|
|
152
|
+
*
|
|
153
|
+
* React Query retries every failure three times by default, which for canister
|
|
154
|
+
* calls means four attempts and several seconds of backoff on outcomes that
|
|
155
|
+
* cannot change — a canister `Err`, a validation failure, a Candid encode
|
|
156
|
+
* error that never reached the network. `reactorRetry` keeps the same three
|
|
157
|
+
* attempts for transport failures and stops immediately on the rest.
|
|
158
|
+
*
|
|
159
|
+
* A caller-supplied `queryClient` is left exactly as given; opt in there with
|
|
160
|
+
* `defaultOptions: { queries: { retry: reactorRetry } }`.
|
|
161
|
+
*/
|
|
162
|
+
const createDefaultQueryClient = () =>
|
|
163
|
+
new QueryClient({
|
|
164
|
+
defaultOptions: { queries: { retry: reactorRetry } },
|
|
165
|
+
})
|
|
166
|
+
|
|
145
167
|
export function defineReactor<Service = BaseActor>(
|
|
146
168
|
params: DefineDisplayReactorParameters<Service>
|
|
147
169
|
): DefineReactorResult<Service, "display", DisplayReactor<Service>>
|
|
@@ -197,7 +219,7 @@ export function defineReactor<Service = BaseActor>(
|
|
|
197
219
|
providedClientManager ??
|
|
198
220
|
providedAuthentication?.clientManager ??
|
|
199
221
|
new ClientManager({
|
|
200
|
-
queryClient: providedQueryClient ??
|
|
222
|
+
queryClient: providedQueryClient ?? createDefaultQueryClient(),
|
|
201
223
|
agentOptions,
|
|
202
224
|
})
|
|
203
225
|
|
|
@@ -39,6 +39,24 @@ export interface CreateAuthHooksReturn {
|
|
|
39
39
|
export const createAuthHooks = (
|
|
40
40
|
authentication: AuthenticationManager
|
|
41
41
|
): CreateAuthHooksReturn => {
|
|
42
|
+
// Passing a ClientManager here is the natural mistake — it is what
|
|
43
|
+
// `AuthenticationManager` is built from, and the two are adjacent in every
|
|
44
|
+
// setup snippet. TypeScript rejects it, but a JS caller got no error until
|
|
45
|
+
// render, where it surfaced as "Cannot destructure property 'isAuthenticated'
|
|
46
|
+
// of 'useAuthState(...)' as it is undefined" — which names neither the cause
|
|
47
|
+
// nor this function.
|
|
48
|
+
if (
|
|
49
|
+
!authentication ||
|
|
50
|
+
typeof authentication.subscribeAuthState !== "function"
|
|
51
|
+
) {
|
|
52
|
+
throw new TypeError(
|
|
53
|
+
"[ic-reactor] createAuthHooks() expects an AuthenticationManager, not a " +
|
|
54
|
+
"ClientManager. Build one first: " +
|
|
55
|
+
"`new AuthenticationManager({ clientManager })`, or take it from " +
|
|
56
|
+
"`defineReactor(...).authentication`."
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
const { clientManager } = authentication
|
|
43
61
|
/**
|
|
44
62
|
* Subscribe to agent state changes.
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Hooks
|
|
3
|
+
//
|
|
4
|
+
// These are the RAW hooks: they take the reactor as a property. They are
|
|
5
|
+
// therefore exported as `useReactor*` only — the `useActor*` names belong to
|
|
6
|
+
// the bound hooks `createActorHooks` returns, which take no reactor. Aliasing
|
|
7
|
+
// the raw implementations under the bound names would hand a caller following
|
|
8
|
+
// the documented `useActorQuery({ functionName })` shape an undefined reactor.
|
|
3
9
|
// ============================================================================
|
|
4
10
|
|
|
5
11
|
// useActorQuery / useReactorQuery
|
|
@@ -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.js"
|
|
19
|
+
import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Parameters for useActorInfiniteQuery hook.
|
|
@@ -119,8 +119,35 @@ 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
|
+
// `canisterId` is mutable reactor state that `setCanisterId` can change,
|
|
142
|
+
// while `reactor` itself stays the same object — so it has to be a
|
|
143
|
+
// dependency in its own right or the key stays pinned to the old canister
|
|
144
|
+
// while the queryFn already calls the new one.
|
|
145
|
+
reactor.canisterId?.toString(),
|
|
146
|
+
functionName,
|
|
147
|
+
callConfig,
|
|
148
|
+
getArgs,
|
|
149
|
+
options.initialPageParam,
|
|
150
|
+
]
|
|
124
151
|
)
|
|
125
152
|
|
|
126
153
|
// 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,39 @@ 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
|
+
// `canisterId` is mutable reactor state that `setCanisterId` can change,
|
|
212
|
+
// while `reactor` itself stays the same object — so it has to be a
|
|
213
|
+
// dependency in its own right or the key stays pinned to the old canister.
|
|
214
|
+
[
|
|
215
|
+
reactor,
|
|
216
|
+
reactor.canisterId?.toString(),
|
|
217
|
+
functionName,
|
|
218
|
+
callConfig,
|
|
219
|
+
customQueryKey,
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const queryKey = useMemo(() => buildQueryKey(args), [buildQueryKey, args])
|
|
202
224
|
|
|
203
225
|
// ============================================================================
|
|
204
226
|
// Query Implementation
|
|
@@ -210,26 +232,51 @@ export function useActorMethod<
|
|
|
210
232
|
>(
|
|
211
233
|
{
|
|
212
234
|
queryKey,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
throw error
|
|
225
|
-
}
|
|
226
|
-
},
|
|
235
|
+
// Callbacks deliberately do NOT live here: a queryFn runs once per fetch
|
|
236
|
+
// attempt, so `onError` fired on every retry (four times with the default
|
|
237
|
+
// QueryClient) and `onSuccess` never fired when data came from the cache
|
|
238
|
+
// or a deduped sibling. They are dispatched from the settled observer
|
|
239
|
+
// result below instead.
|
|
240
|
+
queryFn: () =>
|
|
241
|
+
reactor.callMethod({
|
|
242
|
+
functionName,
|
|
243
|
+
args,
|
|
244
|
+
callConfig,
|
|
245
|
+
}),
|
|
227
246
|
enabled: isQuery && enabled,
|
|
228
247
|
...queryOptions,
|
|
229
248
|
},
|
|
230
249
|
reactor.queryClient
|
|
231
250
|
)
|
|
232
251
|
|
|
252
|
+
// Dispatch the query callbacks from the settled observer result, once per
|
|
253
|
+
// distinct outcome. TanStack v5 removed `onSuccess`/`onError` from useQuery
|
|
254
|
+
// for exactly this reason, so the settle timestamps are what identify a new
|
|
255
|
+
// result: they also advance for a cache hit on a fresh mount, which is the
|
|
256
|
+
// case that previously never notified at all.
|
|
257
|
+
const notifiedSuccessAt = useRef<number | undefined>(undefined)
|
|
258
|
+
const notifiedErrorAt = useRef<number | undefined>(undefined)
|
|
259
|
+
|
|
260
|
+
const { status, data, error, dataUpdatedAt, errorUpdatedAt } = queryResult
|
|
261
|
+
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (!isQuery) return
|
|
264
|
+
if (status === "success" && dataUpdatedAt !== notifiedSuccessAt.current) {
|
|
265
|
+
notifiedSuccessAt.current = dataUpdatedAt
|
|
266
|
+
onSuccessRef.current?.(data)
|
|
267
|
+
}
|
|
268
|
+
}, [isQuery, status, data, dataUpdatedAt])
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (!isQuery) return
|
|
272
|
+
if (status === "error" && errorUpdatedAt !== notifiedErrorAt.current) {
|
|
273
|
+
notifiedErrorAt.current = errorUpdatedAt
|
|
274
|
+
onErrorRef.current?.(
|
|
275
|
+
error as ReactorReturnErr<Service, Method, Transform>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
}, [isQuery, status, error, errorUpdatedAt])
|
|
279
|
+
|
|
233
280
|
// ============================================================================
|
|
234
281
|
// Mutation Implementation
|
|
235
282
|
// ============================================================================
|
|
@@ -250,7 +297,7 @@ export function useActorMethod<
|
|
|
250
297
|
return result
|
|
251
298
|
},
|
|
252
299
|
onSuccess: (data) => {
|
|
253
|
-
|
|
300
|
+
onSuccessRef.current?.(data)
|
|
254
301
|
// Invalidate specified queries after successful mutation
|
|
255
302
|
if (invalidateQueries && invalidateQueries.length > 0) {
|
|
256
303
|
invalidateQueries.forEach((key) => {
|
|
@@ -259,7 +306,7 @@ export function useActorMethod<
|
|
|
259
306
|
}
|
|
260
307
|
},
|
|
261
308
|
onError: (error) => {
|
|
262
|
-
|
|
309
|
+
onErrorRef.current?.(error)
|
|
263
310
|
},
|
|
264
311
|
},
|
|
265
312
|
reactor.queryClient
|
|
@@ -276,9 +323,14 @@ export function useActorMethod<
|
|
|
276
323
|
if (isQuery) {
|
|
277
324
|
// For queries, refetch with new args if provided
|
|
278
325
|
if (callArgs !== undefined) {
|
|
326
|
+
// Key on the args actually being called. Reusing the hook's
|
|
327
|
+
// mount-time key would store this result under the previous args'
|
|
328
|
+
// entry — poisoning it for every other reader — and let fetchQuery
|
|
329
|
+
// dedupe onto an in-flight request for the old args, returning that
|
|
330
|
+
// response as though it answered this one.
|
|
279
331
|
try {
|
|
280
332
|
const result = await reactor.queryClient.fetchQuery({
|
|
281
|
-
queryKey,
|
|
333
|
+
queryKey: buildQueryKey(callArgs),
|
|
282
334
|
queryFn: () =>
|
|
283
335
|
reactor.callMethod({
|
|
284
336
|
functionName,
|
|
@@ -287,10 +339,17 @@ export function useActorMethod<
|
|
|
287
339
|
}),
|
|
288
340
|
staleTime: 0,
|
|
289
341
|
})
|
|
290
|
-
|
|
342
|
+
// Dispatched here rather than by the observer effect: this result
|
|
343
|
+
// lands under the called args' key, which the mounted observer (bound
|
|
344
|
+
// to the hook's own args) does not watch. That separation is also why
|
|
345
|
+
// this can no longer double-fire the way it did when both wrote to
|
|
346
|
+
// the same key.
|
|
347
|
+
onSuccessRef.current?.(result)
|
|
291
348
|
return result
|
|
292
349
|
} catch (error) {
|
|
293
|
-
|
|
350
|
+
onErrorRef.current?.(
|
|
351
|
+
error as ReactorReturnErr<Service, Method, Transform>
|
|
352
|
+
)
|
|
294
353
|
return undefined
|
|
295
354
|
}
|
|
296
355
|
}
|
|
@@ -309,11 +368,9 @@ export function useActorMethod<
|
|
|
309
368
|
reactor,
|
|
310
369
|
functionName,
|
|
311
370
|
callConfig,
|
|
312
|
-
|
|
371
|
+
buildQueryKey,
|
|
313
372
|
queryResult,
|
|
314
373
|
mutationResult,
|
|
315
|
-
onSuccess,
|
|
316
|
-
onError,
|
|
317
374
|
]
|
|
318
375
|
)
|
|
319
376
|
|