@ic-reactor/react 3.0.3-beta.4 → 3.0.3
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 +83 -14
- package/dist/createInfiniteQuery.d.ts +23 -10
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +24 -3
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +11 -1
- package/dist/createMutation.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +29 -5
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +31 -3
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/hooks/index.d.ts +18 -6
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -6
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +20 -24
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +29 -18
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/package.json +8 -7
- package/src/createActorHooks.ts +146 -0
- package/src/createAuthHooks.ts +137 -0
- package/src/createInfiniteQuery.ts +522 -0
- package/src/createMutation.ts +173 -0
- package/src/createQuery.ts +197 -0
- package/src/createSuspenseInfiniteQuery.ts +556 -0
- package/src/createSuspenseQuery.ts +215 -0
- package/src/hooks/index.ts +93 -0
- package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
- package/src/hooks/useActorInfiniteQuery.ts +134 -0
- package/src/hooks/useActorMethod.test.tsx +798 -0
- package/src/hooks/useActorMethod.ts +397 -0
- package/src/hooks/useActorMutation.test.tsx +220 -0
- package/src/hooks/useActorMutation.ts +124 -0
- package/src/hooks/useActorQuery.test.tsx +287 -0
- package/src/hooks/useActorQuery.ts +110 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
- package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
- package/src/hooks/useActorSuspenseQuery.ts +112 -0
- package/src/index.ts +21 -0
- package/src/types.ts +435 -0
- package/src/validation.ts +202 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation Factory - Generic wrapper for mutating canister data
|
|
3
|
+
*
|
|
4
|
+
* Creates unified mutation hooks for any canister method.
|
|
5
|
+
* Works with any Reactor instance.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const transferMutation = createMutation(reactor, {
|
|
9
|
+
* functionName: "transfer",
|
|
10
|
+
* onSuccess: () => console.log("Success!"),
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* // In component
|
|
14
|
+
* const { mutate, isPending } = transferMutation.useMutation()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useMutation } from "@tanstack/react-query"
|
|
18
|
+
import type {
|
|
19
|
+
Reactor,
|
|
20
|
+
FunctionName,
|
|
21
|
+
ReactorArgs,
|
|
22
|
+
TransformKey,
|
|
23
|
+
ReactorReturnOk,
|
|
24
|
+
} from "@ic-reactor/core"
|
|
25
|
+
import { isCanisterError } from "@ic-reactor/core"
|
|
26
|
+
import type {
|
|
27
|
+
MutationConfig,
|
|
28
|
+
MutationResult,
|
|
29
|
+
MutationHookOptions,
|
|
30
|
+
NoInfer,
|
|
31
|
+
} from "./types"
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Internal Implementation
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
const createMutationImpl = <
|
|
38
|
+
A,
|
|
39
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
40
|
+
T extends TransformKey = "candid",
|
|
41
|
+
>(
|
|
42
|
+
reactor: Reactor<A, T>,
|
|
43
|
+
config: MutationConfig<A, M, T>
|
|
44
|
+
): MutationResult<A, M, T> => {
|
|
45
|
+
const {
|
|
46
|
+
functionName,
|
|
47
|
+
callConfig,
|
|
48
|
+
invalidateQueries: factoryInvalidateQueries,
|
|
49
|
+
onSuccess: factoryOnSuccess,
|
|
50
|
+
onCanisterError: factoryOnCanisterError,
|
|
51
|
+
onError: factoryOnError,
|
|
52
|
+
...factoryOptions
|
|
53
|
+
} = config
|
|
54
|
+
|
|
55
|
+
// Direct execution function
|
|
56
|
+
const execute = async (
|
|
57
|
+
args: ReactorArgs<A, M, T>
|
|
58
|
+
): Promise<ReactorReturnOk<A, M, T>> => {
|
|
59
|
+
const result = await reactor.callMethod({
|
|
60
|
+
functionName,
|
|
61
|
+
args,
|
|
62
|
+
callConfig,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (factoryInvalidateQueries) {
|
|
66
|
+
await Promise.all(
|
|
67
|
+
factoryInvalidateQueries.map((queryKey) => {
|
|
68
|
+
return reactor.queryClient.invalidateQueries({ queryKey })
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Hook implementation
|
|
77
|
+
const useMutationHook = (options?: MutationHookOptions<A, M, T>) => {
|
|
78
|
+
const baseOptions = reactor.getQueryOptions({
|
|
79
|
+
functionName,
|
|
80
|
+
})
|
|
81
|
+
// Extract our custom options
|
|
82
|
+
const {
|
|
83
|
+
invalidateQueries: hookInvalidateQueries,
|
|
84
|
+
onCanisterError: hookOnCanisterError,
|
|
85
|
+
...restOptions
|
|
86
|
+
} = options || {}
|
|
87
|
+
|
|
88
|
+
return useMutation(
|
|
89
|
+
{
|
|
90
|
+
mutationKey: baseOptions.queryKey,
|
|
91
|
+
...factoryOptions,
|
|
92
|
+
...restOptions,
|
|
93
|
+
mutationFn: execute,
|
|
94
|
+
onSuccess: async (...args) => {
|
|
95
|
+
// 1. Handle factory-level invalidateQueries
|
|
96
|
+
if (factoryInvalidateQueries) {
|
|
97
|
+
await Promise.all(
|
|
98
|
+
factoryInvalidateQueries.map((queryKey) => {
|
|
99
|
+
return reactor.queryClient.invalidateQueries({ queryKey })
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Handle hook-level invalidateQueries
|
|
105
|
+
if (hookInvalidateQueries) {
|
|
106
|
+
await Promise.all(
|
|
107
|
+
hookInvalidateQueries.map((queryKey) => {
|
|
108
|
+
if (queryKey) {
|
|
109
|
+
return reactor.queryClient.invalidateQueries({ queryKey })
|
|
110
|
+
}
|
|
111
|
+
return Promise.resolve()
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. Call factory onSuccess
|
|
117
|
+
if (factoryOnSuccess) {
|
|
118
|
+
await factoryOnSuccess(...args)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4. Call hook-local onSuccess
|
|
122
|
+
if (restOptions?.onSuccess) {
|
|
123
|
+
await restOptions.onSuccess(...args)
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
onError: (error, variables, context, mutation) => {
|
|
127
|
+
// Check if this is a CanisterError (from Result { Err: E })
|
|
128
|
+
if (isCanisterError(error)) {
|
|
129
|
+
// 1. Call factory-level onCanisterError
|
|
130
|
+
if (factoryOnCanisterError) {
|
|
131
|
+
factoryOnCanisterError(error, variables)
|
|
132
|
+
}
|
|
133
|
+
// 2. Call hook-level onCanisterError
|
|
134
|
+
if (hookOnCanisterError) {
|
|
135
|
+
hookOnCanisterError(error, variables)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Call factory-level onError (for all errors)
|
|
140
|
+
if (factoryOnError) {
|
|
141
|
+
factoryOnError(error, variables, context, mutation)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 4. Call hook-level onError (for all errors)
|
|
145
|
+
if (restOptions?.onError) {
|
|
146
|
+
restOptions.onError(error, variables, context, mutation)
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
reactor.queryClient
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
useMutation: useMutationHook,
|
|
156
|
+
execute,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Factory Function
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
export function createMutation<
|
|
165
|
+
A,
|
|
166
|
+
T extends TransformKey,
|
|
167
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
168
|
+
>(
|
|
169
|
+
reactor: Reactor<A, T>,
|
|
170
|
+
config: MutationConfig<NoInfer<A>, M, T>
|
|
171
|
+
): MutationResult<A, M, T> {
|
|
172
|
+
return createMutationImpl(reactor, config as MutationConfig<A, M, T>)
|
|
173
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Factory - Generic wrapper for React Query-based canister data
|
|
3
|
+
*
|
|
4
|
+
* Creates unified fetch/hook/invalidate functions for any canister method.
|
|
5
|
+
* Works with any Reactor instance.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const userQuery = createQuery(todoManager, {
|
|
9
|
+
* functionName: "get_user",
|
|
10
|
+
* select: (result) => result.user,
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* // In component
|
|
14
|
+
* const { data: user } = userQuery.useQuery()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
Reactor,
|
|
19
|
+
FunctionName,
|
|
20
|
+
ReactorArgs,
|
|
21
|
+
TransformKey,
|
|
22
|
+
} from "@ic-reactor/core"
|
|
23
|
+
import { QueryKey, useQuery } from "@tanstack/react-query"
|
|
24
|
+
import type {
|
|
25
|
+
QueryFnData,
|
|
26
|
+
QueryError,
|
|
27
|
+
QueryConfig,
|
|
28
|
+
UseQueryWithSelect,
|
|
29
|
+
QueryResult,
|
|
30
|
+
QueryFactoryConfig,
|
|
31
|
+
NoInfer,
|
|
32
|
+
} from "./types"
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Internal Implementation
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
const createQueryImpl = <
|
|
39
|
+
A,
|
|
40
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
41
|
+
T extends TransformKey = "candid",
|
|
42
|
+
TSelected = QueryFnData<A, M, T>,
|
|
43
|
+
>(
|
|
44
|
+
reactor: Reactor<A, T>,
|
|
45
|
+
config: QueryConfig<A, M, T, TSelected>
|
|
46
|
+
): QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>> => {
|
|
47
|
+
type TData = QueryFnData<A, M, T>
|
|
48
|
+
type TError = QueryError<A, M, T>
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
functionName,
|
|
52
|
+
args,
|
|
53
|
+
staleTime = 5 * 60 * 1000,
|
|
54
|
+
select,
|
|
55
|
+
queryKey: customQueryKey,
|
|
56
|
+
...rest
|
|
57
|
+
} = config
|
|
58
|
+
|
|
59
|
+
const params = {
|
|
60
|
+
functionName,
|
|
61
|
+
args,
|
|
62
|
+
queryKey: customQueryKey,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get query key from actor manager
|
|
66
|
+
const getQueryKey = (): QueryKey => {
|
|
67
|
+
return reactor.generateQueryKey(params)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fetch function for loaders (cache-first)
|
|
71
|
+
const fetch = async (): Promise<TSelected> => {
|
|
72
|
+
const result = await reactor.fetchQuery(params)
|
|
73
|
+
return select ? select(result) : (result as TSelected)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Implementation
|
|
77
|
+
const useQueryHook: UseQueryWithSelect<TData, TSelected, TError> = (
|
|
78
|
+
options: any
|
|
79
|
+
): any => {
|
|
80
|
+
const baseOptions = reactor.getQueryOptions(params)
|
|
81
|
+
|
|
82
|
+
// Chain the selects: raw -> config.select -> options.select
|
|
83
|
+
const chainedSelect = (rawData: TData) => {
|
|
84
|
+
const firstPass = config.select ? config.select(rawData) : rawData
|
|
85
|
+
if (options?.select) {
|
|
86
|
+
return options.select(firstPass)
|
|
87
|
+
}
|
|
88
|
+
return firstPass
|
|
89
|
+
}
|
|
90
|
+
return useQuery(
|
|
91
|
+
{
|
|
92
|
+
queryKey: baseOptions.queryKey,
|
|
93
|
+
staleTime,
|
|
94
|
+
...rest,
|
|
95
|
+
...options,
|
|
96
|
+
queryFn: baseOptions.queryFn,
|
|
97
|
+
select: chainedSelect,
|
|
98
|
+
},
|
|
99
|
+
reactor.queryClient
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Invalidate function
|
|
104
|
+
const invalidate = async (): Promise<void> => {
|
|
105
|
+
const queryKey = getQueryKey()
|
|
106
|
+
await reactor.queryClient.invalidateQueries({ queryKey })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get data from cache without fetching
|
|
110
|
+
const getCacheData: any = (selectFn?: (data: TData) => any) => {
|
|
111
|
+
const cachedRawData = reactor.getQueryData(params)
|
|
112
|
+
|
|
113
|
+
if (cachedRawData === undefined) {
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Apply config.select to raw cache data
|
|
118
|
+
const selectedData = (
|
|
119
|
+
config.select ? config.select(cachedRawData) : cachedRawData
|
|
120
|
+
) as TData
|
|
121
|
+
|
|
122
|
+
// Apply optional select parameter
|
|
123
|
+
if (selectFn) {
|
|
124
|
+
return selectFn(selectedData)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return selectedData
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
fetch,
|
|
132
|
+
useQuery: useQueryHook,
|
|
133
|
+
invalidate,
|
|
134
|
+
getQueryKey,
|
|
135
|
+
getCacheData,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Factory Function
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
export function createQuery<
|
|
144
|
+
A,
|
|
145
|
+
T extends TransformKey,
|
|
146
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
147
|
+
TSelected = QueryFnData<A, M, T>,
|
|
148
|
+
>(
|
|
149
|
+
reactor: Reactor<A, T>,
|
|
150
|
+
config: QueryConfig<NoInfer<A>, M, T, TSelected>
|
|
151
|
+
): QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>> {
|
|
152
|
+
return createQueryImpl(reactor, config as QueryConfig<A, M, T, TSelected>)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Convenience: Create query with dynamic args
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
export function createQueryFactory<
|
|
160
|
+
A,
|
|
161
|
+
T extends TransformKey,
|
|
162
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
163
|
+
TSelected = QueryFnData<A, M, T>,
|
|
164
|
+
>(
|
|
165
|
+
reactor: Reactor<A, T>,
|
|
166
|
+
config: QueryFactoryConfig<NoInfer<A>, M, T, TSelected>
|
|
167
|
+
): (
|
|
168
|
+
args: ReactorArgs<A, M, T>
|
|
169
|
+
) => QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>> {
|
|
170
|
+
const cache = new Map<
|
|
171
|
+
string,
|
|
172
|
+
QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>>
|
|
173
|
+
>()
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
args: ReactorArgs<A, M, T>
|
|
177
|
+
): QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>> => {
|
|
178
|
+
const key = reactor.generateQueryKey({
|
|
179
|
+
functionName: config.functionName as M,
|
|
180
|
+
args,
|
|
181
|
+
})
|
|
182
|
+
const cacheKey = JSON.stringify(key)
|
|
183
|
+
|
|
184
|
+
if (cache.has(cacheKey)) {
|
|
185
|
+
return cache.get(cacheKey)!
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = createQueryImpl<A, M, T, TSelected>(reactor, {
|
|
189
|
+
...config,
|
|
190
|
+
args,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
cache.set(cacheKey, result)
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
}
|