@ic-reactor/react 3.0.3-beta.5 → 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,137 @@
|
|
|
1
|
+
import { useSyncExternalStore, useEffect, useRef, useMemo } from "react"
|
|
2
|
+
import { ClientManager, AgentState, AuthState } from "@ic-reactor/core"
|
|
3
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
4
|
+
import { Identity } from "@icp-sdk/core/agent"
|
|
5
|
+
import { AuthClientLoginOptions } from "@icp-sdk/auth/client"
|
|
6
|
+
|
|
7
|
+
export interface UseAuthReturn {
|
|
8
|
+
authenticate: () => Promise<Identity | undefined>
|
|
9
|
+
login: (options?: AuthClientLoginOptions) => Promise<void>
|
|
10
|
+
logout: (options?: { returnTo?: string }) => Promise<void>
|
|
11
|
+
isAuthenticated: boolean
|
|
12
|
+
isAuthenticating: boolean
|
|
13
|
+
principal: Principal | null
|
|
14
|
+
identity: Identity | null
|
|
15
|
+
error: Error | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateAuthHooksReturn {
|
|
19
|
+
useAgentState: () => AgentState
|
|
20
|
+
useUserPrincipal: () => Principal | null
|
|
21
|
+
useAuth: () => UseAuthReturn
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create authentication hooks for managing user sessions with Internet Identity.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const { useAuth, useUserPrincipal, useAgentState } = createAuthHooks(clientManager)
|
|
29
|
+
*
|
|
30
|
+
* function App() {
|
|
31
|
+
* const { login, logout, principal, isAuthenticated } = useAuth()
|
|
32
|
+
*
|
|
33
|
+
* return isAuthenticated
|
|
34
|
+
* ? <button onClick={logout}>Logout {principal?.toText()}</button>
|
|
35
|
+
* : <button onClick={login}>Login with II</button>
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export const createAuthHooks = (
|
|
39
|
+
clientManager: ClientManager
|
|
40
|
+
): CreateAuthHooksReturn => {
|
|
41
|
+
/**
|
|
42
|
+
* Subscribe to agent state changes.
|
|
43
|
+
* Returns the current agent state (agent, isInitialized, etc.)
|
|
44
|
+
*/
|
|
45
|
+
const useAgentState = (): AgentState =>
|
|
46
|
+
useSyncExternalStore(
|
|
47
|
+
(callback) => clientManager.subscribeAgentState(callback),
|
|
48
|
+
() => clientManager.agentState,
|
|
49
|
+
// Server snapshot - provide initial state for SSR
|
|
50
|
+
() => clientManager.agentState
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Subscribe to authentication state changes.
|
|
55
|
+
* Returns auth state (isAuthenticated, isAuthenticating, identity, error)
|
|
56
|
+
*/
|
|
57
|
+
const useAuthState = (): AuthState =>
|
|
58
|
+
useSyncExternalStore(
|
|
59
|
+
(callback) => clientManager.subscribeAuthState(callback),
|
|
60
|
+
() => clientManager.authState,
|
|
61
|
+
// Server snapshot - provide initial state for SSR
|
|
62
|
+
() => clientManager.authState
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Main authentication hook that provides login/logout methods and auth state.
|
|
67
|
+
* Automatically initializes the session on first use, restoring any previous session.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* function AuthButton() {
|
|
71
|
+
* const { login, logout, isAuthenticated, isAuthenticating } = useAuth()
|
|
72
|
+
*
|
|
73
|
+
* if (isAuthenticated) {
|
|
74
|
+
* return <button onClick={logout}>Logout</button>
|
|
75
|
+
* }
|
|
76
|
+
* return (
|
|
77
|
+
* <button onClick={login} disabled={isAuthenticating}>
|
|
78
|
+
* {isAuthenticating ? "Connecting..." : "Login"}
|
|
79
|
+
* </button>
|
|
80
|
+
* )
|
|
81
|
+
* }
|
|
82
|
+
*/
|
|
83
|
+
const useAuth = (): UseAuthReturn => {
|
|
84
|
+
const { login, logout, authenticate } = clientManager
|
|
85
|
+
const { isAuthenticated, isAuthenticating, identity, error } =
|
|
86
|
+
useAuthState()
|
|
87
|
+
|
|
88
|
+
// Track if we've already initialized to avoid duplicate calls
|
|
89
|
+
const initializedRef = useRef(false)
|
|
90
|
+
|
|
91
|
+
// Auto-initialize on first mount to restore previous session
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!initializedRef.current) {
|
|
94
|
+
initializedRef.current = true
|
|
95
|
+
clientManager.initialize()
|
|
96
|
+
}
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
99
|
+
const principal = useMemo(
|
|
100
|
+
() => (identity ? identity.getPrincipal() : null),
|
|
101
|
+
[identity]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
authenticate,
|
|
106
|
+
login,
|
|
107
|
+
logout,
|
|
108
|
+
isAuthenticated,
|
|
109
|
+
isAuthenticating,
|
|
110
|
+
principal,
|
|
111
|
+
identity,
|
|
112
|
+
error,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the current user's Principal.
|
|
118
|
+
* Returns null if not authenticated.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* function UserInfo() {
|
|
122
|
+
* const principal = useUserPrincipal()
|
|
123
|
+
* if (!principal) return null
|
|
124
|
+
* return <span>Logged in as: {principal.toText()}</span>
|
|
125
|
+
* }
|
|
126
|
+
*/
|
|
127
|
+
const useUserPrincipal = (): Principal | null => {
|
|
128
|
+
const { identity } = useAuthState()
|
|
129
|
+
return identity ? identity.getPrincipal() : null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
useAuth,
|
|
134
|
+
useAgentState,
|
|
135
|
+
useUserPrincipal,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infinite Query Factory - Generic wrapper for React Query paginated canister data
|
|
3
|
+
*
|
|
4
|
+
* Creates unified fetch/hook/invalidate functions for any paginated canister method.
|
|
5
|
+
* Works with any Reactor instance.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const postsQuery = createInfiniteQuery(reactor, {
|
|
9
|
+
* functionName: "get_posts",
|
|
10
|
+
* initialPageParam: 0,
|
|
11
|
+
* getArgs: (cursor) => [{ cursor, limit: 10 }],
|
|
12
|
+
* getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* // In component
|
|
16
|
+
* const { data, fetchNextPage, hasNextPage } = postsQuery.useInfiniteQuery()
|
|
17
|
+
*
|
|
18
|
+
* // Flatten all pages
|
|
19
|
+
* const allPosts = data?.pages.flatMap(page => page.posts)
|
|
20
|
+
*
|
|
21
|
+
* // Invalidate cache
|
|
22
|
+
* postsQuery.invalidate()
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
Reactor,
|
|
27
|
+
FunctionName,
|
|
28
|
+
ReactorArgs,
|
|
29
|
+
BaseActor,
|
|
30
|
+
TransformKey,
|
|
31
|
+
ReactorReturnOk,
|
|
32
|
+
ReactorReturnErr,
|
|
33
|
+
} from "@ic-reactor/core"
|
|
34
|
+
import {
|
|
35
|
+
QueryKey,
|
|
36
|
+
useInfiniteQuery,
|
|
37
|
+
InfiniteData,
|
|
38
|
+
UseInfiniteQueryResult,
|
|
39
|
+
UseInfiniteQueryOptions,
|
|
40
|
+
QueryFunctionContext,
|
|
41
|
+
FetchInfiniteQueryOptions,
|
|
42
|
+
InfiniteQueryObserverOptions,
|
|
43
|
+
} from "@tanstack/react-query"
|
|
44
|
+
import { CallConfig } from "@icp-sdk/core/agent"
|
|
45
|
+
import { NoInfer } from "./types"
|
|
46
|
+
|
|
47
|
+
const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
|
|
48
|
+
|
|
49
|
+
type InfiniteQueryFactoryFn<
|
|
50
|
+
A,
|
|
51
|
+
M extends FunctionName<A>,
|
|
52
|
+
T extends TransformKey,
|
|
53
|
+
TPageParam,
|
|
54
|
+
TSelected,
|
|
55
|
+
> = {
|
|
56
|
+
(
|
|
57
|
+
getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
|
|
58
|
+
): InfiniteQueryResult<
|
|
59
|
+
InfiniteQueryPageData<A, M, T>,
|
|
60
|
+
TPageParam,
|
|
61
|
+
TSelected,
|
|
62
|
+
InfiniteQueryError<A, M, T>
|
|
63
|
+
>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mergeFactoryQueryKey = (
|
|
67
|
+
baseQueryKey?: QueryKey,
|
|
68
|
+
keyArgs?: unknown
|
|
69
|
+
): QueryKey | undefined => {
|
|
70
|
+
const merged: unknown[] = []
|
|
71
|
+
|
|
72
|
+
if (baseQueryKey) {
|
|
73
|
+
merged.push(...baseQueryKey)
|
|
74
|
+
}
|
|
75
|
+
if (keyArgs !== undefined) {
|
|
76
|
+
merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return merged.length > 0 ? merged : undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Type Definitions
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/** The raw page data type returned by the query function */
|
|
87
|
+
export type InfiniteQueryPageData<
|
|
88
|
+
A = BaseActor,
|
|
89
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
90
|
+
T extends TransformKey = "candid",
|
|
91
|
+
> = ReactorReturnOk<A, M, T>
|
|
92
|
+
|
|
93
|
+
/** The error type for infinite queries */
|
|
94
|
+
export type InfiniteQueryError<
|
|
95
|
+
A = BaseActor,
|
|
96
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
97
|
+
T extends TransformKey = "candid",
|
|
98
|
+
> = ReactorReturnErr<A, M, T>
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Configuration Types
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Configuration for createActorInfiniteQuery.
|
|
106
|
+
* Extends InfiniteQueryObserverOptions to accept standard TanStack Query
|
|
107
|
+
* infinite-query options at the create level (e.g. refetchInterval,
|
|
108
|
+
* refetchOnMount, refetchOnWindowFocus, retry, gcTime, networkMode).
|
|
109
|
+
*
|
|
110
|
+
* @template A - The actor interface type
|
|
111
|
+
* @template M - The method name on the actor
|
|
112
|
+
* @template T - The transformation key (identity, display, etc.)
|
|
113
|
+
* @template TPageParam - The type of the page parameter
|
|
114
|
+
* @template TSelected - The type returned after select transformation
|
|
115
|
+
*/
|
|
116
|
+
export interface InfiniteQueryConfig<
|
|
117
|
+
A = BaseActor,
|
|
118
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
119
|
+
T extends TransformKey = "candid",
|
|
120
|
+
TPageParam = unknown,
|
|
121
|
+
TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
122
|
+
> extends Omit<
|
|
123
|
+
InfiniteQueryObserverOptions<
|
|
124
|
+
InfiniteQueryPageData<A, M, T>,
|
|
125
|
+
InfiniteQueryError<A, M, T>,
|
|
126
|
+
TSelected,
|
|
127
|
+
QueryKey,
|
|
128
|
+
TPageParam
|
|
129
|
+
>,
|
|
130
|
+
"queryKey" | "queryFn"
|
|
131
|
+
> {
|
|
132
|
+
/** The method to call on the canister */
|
|
133
|
+
functionName: M
|
|
134
|
+
/** Call configuration for the actor method */
|
|
135
|
+
callConfig?: CallConfig
|
|
136
|
+
/** Custom query key (optional, auto-generated if not provided) */
|
|
137
|
+
queryKey?: QueryKey
|
|
138
|
+
/** Initial page parameter */
|
|
139
|
+
initialPageParam: TPageParam
|
|
140
|
+
/** Function to get args from page parameter */
|
|
141
|
+
getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
|
|
142
|
+
/** Function to determine next page parameter */
|
|
143
|
+
getNextPageParam: (
|
|
144
|
+
lastPage: InfiniteQueryPageData<A, M, T>,
|
|
145
|
+
allPages: InfiniteQueryPageData<A, M, T>[],
|
|
146
|
+
lastPageParam: TPageParam,
|
|
147
|
+
allPageParams: TPageParam[]
|
|
148
|
+
) => TPageParam | undefined | null
|
|
149
|
+
/** Function to determine previous page parameter (for bi-directional) */
|
|
150
|
+
getPreviousPageParam?: (
|
|
151
|
+
firstPage: InfiniteQueryPageData<A, M, T>,
|
|
152
|
+
allPages: InfiniteQueryPageData<A, M, T>[],
|
|
153
|
+
firstPageParam: TPageParam,
|
|
154
|
+
allPageParams: TPageParam[]
|
|
155
|
+
) => TPageParam | undefined | null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Configuration for createActorInfiniteQueryFactory (without initialPageParam, getArgs determined at call time).
|
|
160
|
+
*/
|
|
161
|
+
export type InfiniteQueryFactoryConfig<
|
|
162
|
+
A = BaseActor,
|
|
163
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
164
|
+
T extends TransformKey = "candid",
|
|
165
|
+
TPageParam = unknown,
|
|
166
|
+
TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
167
|
+
> = Omit<InfiniteQueryConfig<A, M, T, TPageParam, TSelected>, "getArgs"> & {
|
|
168
|
+
/**
|
|
169
|
+
* Optional key-args derivation for factory calls.
|
|
170
|
+
* Receives the resolved args from `getArgs(initialPageParam)` and should return
|
|
171
|
+
* a stable serializable representation of the logical query identity
|
|
172
|
+
* (typically excluding pagination/cursor fields).
|
|
173
|
+
*/
|
|
174
|
+
getKeyArgs?: (args: ReactorArgs<A, M, T>) => unknown
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Hook Interface
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* useInfiniteQuery hook with chained select support.
|
|
183
|
+
*/
|
|
184
|
+
export interface UseInfiniteQueryWithSelect<
|
|
185
|
+
TPageData,
|
|
186
|
+
TPageParam,
|
|
187
|
+
TSelected = InfiniteData<TPageData, TPageParam>,
|
|
188
|
+
TError = Error,
|
|
189
|
+
> {
|
|
190
|
+
// Overload 1: Without select - returns TSelected
|
|
191
|
+
(
|
|
192
|
+
options?: Omit<
|
|
193
|
+
UseInfiniteQueryOptions<
|
|
194
|
+
TPageData,
|
|
195
|
+
TError,
|
|
196
|
+
TSelected,
|
|
197
|
+
QueryKey,
|
|
198
|
+
TPageParam
|
|
199
|
+
>,
|
|
200
|
+
| "select"
|
|
201
|
+
| "queryKey"
|
|
202
|
+
| "queryFn"
|
|
203
|
+
| "initialPageParam"
|
|
204
|
+
| "getNextPageParam"
|
|
205
|
+
| "getPreviousPageParam"
|
|
206
|
+
>
|
|
207
|
+
): UseInfiniteQueryResult<TSelected, TError>
|
|
208
|
+
|
|
209
|
+
// Overload 2: With select - chains on top and returns TFinal
|
|
210
|
+
<TFinal = TSelected>(
|
|
211
|
+
options: Omit<
|
|
212
|
+
UseInfiniteQueryOptions<TPageData, TError, TFinal, QueryKey, TPageParam>,
|
|
213
|
+
| "queryKey"
|
|
214
|
+
| "queryFn"
|
|
215
|
+
| "select"
|
|
216
|
+
| "initialPageParam"
|
|
217
|
+
| "getNextPageParam"
|
|
218
|
+
| "getPreviousPageParam"
|
|
219
|
+
> & {
|
|
220
|
+
select: (data: TSelected) => TFinal
|
|
221
|
+
}
|
|
222
|
+
): UseInfiniteQueryResult<TFinal, TError>
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Result Interface
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Result from createActorInfiniteQuery
|
|
231
|
+
*
|
|
232
|
+
* @template TPageData - The raw page data type
|
|
233
|
+
* @template TPageParam - The page parameter type
|
|
234
|
+
* @template TSelected - The type after select transformation
|
|
235
|
+
* @template TError - The error type
|
|
236
|
+
*/
|
|
237
|
+
export interface InfiniteQueryResult<
|
|
238
|
+
TPageData,
|
|
239
|
+
TPageParam,
|
|
240
|
+
TSelected = InfiniteData<TPageData, TPageParam>,
|
|
241
|
+
TError = Error,
|
|
242
|
+
> {
|
|
243
|
+
/** Fetch first page in loader (uses ensureQueryData for cache-first) */
|
|
244
|
+
fetch: () => Promise<TSelected>
|
|
245
|
+
|
|
246
|
+
/** React hook for components - supports pagination */
|
|
247
|
+
useInfiniteQuery: UseInfiniteQueryWithSelect<
|
|
248
|
+
TPageData,
|
|
249
|
+
TPageParam,
|
|
250
|
+
TSelected,
|
|
251
|
+
TError
|
|
252
|
+
>
|
|
253
|
+
|
|
254
|
+
/** Invalidate the cache (refetches if query is active) */
|
|
255
|
+
invalidate: () => Promise<void>
|
|
256
|
+
|
|
257
|
+
/** Get query key (for advanced React Query usage) */
|
|
258
|
+
getQueryKey: () => QueryKey
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read data directly from cache without fetching.
|
|
262
|
+
* Returns undefined if data is not in cache.
|
|
263
|
+
*/
|
|
264
|
+
getCacheData: {
|
|
265
|
+
(): TSelected | undefined
|
|
266
|
+
<TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Internal Implementation
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
const createInfiniteQueryImpl = <
|
|
275
|
+
A,
|
|
276
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
277
|
+
T extends TransformKey = "candid",
|
|
278
|
+
TPageParam = unknown,
|
|
279
|
+
TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
280
|
+
>(
|
|
281
|
+
reactor: Reactor<A, T>,
|
|
282
|
+
config: InfiniteQueryConfig<A, M, T, TPageParam, TSelected>
|
|
283
|
+
): InfiniteQueryResult<
|
|
284
|
+
InfiniteQueryPageData<A, M, T>,
|
|
285
|
+
TPageParam,
|
|
286
|
+
TSelected,
|
|
287
|
+
InfiniteQueryError<A, M, T>
|
|
288
|
+
> => {
|
|
289
|
+
type TPageData = InfiniteQueryPageData<A, M, T>
|
|
290
|
+
type TError = InfiniteQueryError<A, M, T>
|
|
291
|
+
type TInfiniteData = InfiniteData<TPageData, TPageParam>
|
|
292
|
+
|
|
293
|
+
const {
|
|
294
|
+
functionName,
|
|
295
|
+
callConfig,
|
|
296
|
+
queryKey: customQueryKey,
|
|
297
|
+
initialPageParam,
|
|
298
|
+
getArgs,
|
|
299
|
+
getNextPageParam,
|
|
300
|
+
getPreviousPageParam,
|
|
301
|
+
maxPages,
|
|
302
|
+
staleTime = 5 * 60 * 1000,
|
|
303
|
+
select,
|
|
304
|
+
...rest
|
|
305
|
+
} = config
|
|
306
|
+
|
|
307
|
+
// Get query key from actor manager
|
|
308
|
+
const getQueryKey = (): QueryKey => {
|
|
309
|
+
return reactor.generateQueryKey({
|
|
310
|
+
functionName,
|
|
311
|
+
queryKey: customQueryKey,
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Query function - accepts QueryFunctionContext
|
|
316
|
+
const queryFn = async (
|
|
317
|
+
context: QueryFunctionContext<QueryKey, TPageParam>
|
|
318
|
+
): Promise<TPageData> => {
|
|
319
|
+
// pageParam is typed as unknown in QueryFunctionContext, but we know its type
|
|
320
|
+
const pageParam = context.pageParam as TPageParam
|
|
321
|
+
const args = getArgs(pageParam)
|
|
322
|
+
const result = await reactor.callMethod({
|
|
323
|
+
functionName,
|
|
324
|
+
args,
|
|
325
|
+
callConfig,
|
|
326
|
+
})
|
|
327
|
+
return result
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Get infinite query options for fetchInfiniteQuery
|
|
331
|
+
const getInfiniteQueryOptions = (): FetchInfiniteQueryOptions<
|
|
332
|
+
TPageData,
|
|
333
|
+
TError,
|
|
334
|
+
TPageData,
|
|
335
|
+
QueryKey,
|
|
336
|
+
TPageParam
|
|
337
|
+
> => ({
|
|
338
|
+
queryKey: getQueryKey(),
|
|
339
|
+
queryFn,
|
|
340
|
+
initialPageParam,
|
|
341
|
+
getNextPageParam,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// Fetch function for loaders (cache-first, fetches first page)
|
|
345
|
+
const fetch = async (): Promise<TSelected> => {
|
|
346
|
+
// Check cache first
|
|
347
|
+
const cachedData = reactor.queryClient.getQueryData(getQueryKey()) as
|
|
348
|
+
| TInfiniteData
|
|
349
|
+
| undefined
|
|
350
|
+
|
|
351
|
+
if (cachedData !== undefined) {
|
|
352
|
+
return select ? select(cachedData) : (cachedData as TSelected)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fetch if not in cache
|
|
356
|
+
const result = await reactor.queryClient.fetchInfiniteQuery(
|
|
357
|
+
getInfiniteQueryOptions()
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
// Result is already InfiniteData format
|
|
361
|
+
return select ? select(result) : (result as unknown as TSelected)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Implementation
|
|
365
|
+
const useInfiniteQueryHook: UseInfiniteQueryWithSelect<
|
|
366
|
+
TPageData,
|
|
367
|
+
TPageParam,
|
|
368
|
+
TSelected,
|
|
369
|
+
TError
|
|
370
|
+
> = (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
|
+
}
|
|
379
|
+
|
|
380
|
+
return useInfiniteQuery(
|
|
381
|
+
{
|
|
382
|
+
queryKey: getQueryKey(),
|
|
383
|
+
queryFn,
|
|
384
|
+
initialPageParam,
|
|
385
|
+
getNextPageParam,
|
|
386
|
+
getPreviousPageParam,
|
|
387
|
+
maxPages,
|
|
388
|
+
staleTime,
|
|
389
|
+
...rest,
|
|
390
|
+
...options,
|
|
391
|
+
select: chainedSelect,
|
|
392
|
+
} as any,
|
|
393
|
+
reactor.queryClient
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Invalidate function
|
|
398
|
+
const invalidate = async (): Promise<void> => {
|
|
399
|
+
const queryKey = getQueryKey()
|
|
400
|
+
await reactor.queryClient.invalidateQueries({ queryKey })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Get data from cache without fetching
|
|
404
|
+
const getCacheData: any = (selectFn?: (data: TSelected) => any) => {
|
|
405
|
+
const queryKey = getQueryKey()
|
|
406
|
+
const cachedRawData = reactor.queryClient.getQueryData(
|
|
407
|
+
queryKey
|
|
408
|
+
) as TInfiniteData
|
|
409
|
+
|
|
410
|
+
if (cachedRawData === undefined) {
|
|
411
|
+
return undefined
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Apply config.select to raw cache data
|
|
415
|
+
const selectedData = (
|
|
416
|
+
select ? select(cachedRawData) : cachedRawData
|
|
417
|
+
) as TSelected
|
|
418
|
+
|
|
419
|
+
// Apply optional select parameter
|
|
420
|
+
if (selectFn) {
|
|
421
|
+
return selectFn(selectedData)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return selectedData
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
fetch,
|
|
429
|
+
useInfiniteQuery: useInfiniteQueryHook,
|
|
430
|
+
invalidate,
|
|
431
|
+
getQueryKey,
|
|
432
|
+
getCacheData,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ============================================================================
|
|
437
|
+
// Factory Function
|
|
438
|
+
// ============================================================================
|
|
439
|
+
|
|
440
|
+
export function createInfiniteQuery<
|
|
441
|
+
A,
|
|
442
|
+
T extends TransformKey,
|
|
443
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
444
|
+
TPageParam = unknown,
|
|
445
|
+
TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
446
|
+
>(
|
|
447
|
+
reactor: Reactor<A, T>,
|
|
448
|
+
config: InfiniteQueryConfig<NoInfer<A>, M, T, TPageParam, TSelected>
|
|
449
|
+
): InfiniteQueryResult<
|
|
450
|
+
InfiniteQueryPageData<A, M, T>,
|
|
451
|
+
TPageParam,
|
|
452
|
+
TSelected,
|
|
453
|
+
InfiniteQueryError<A, M, T>
|
|
454
|
+
> {
|
|
455
|
+
return createInfiniteQueryImpl(
|
|
456
|
+
reactor,
|
|
457
|
+
config as InfiniteQueryConfig<A, M, T, TPageParam, TSelected>
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Factory with Dynamic Args
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Create an infinite query factory that accepts getArgs at call time.
|
|
467
|
+
* Useful when pagination logic varies by context.
|
|
468
|
+
*
|
|
469
|
+
* @template A - The actor interface type
|
|
470
|
+
* @template M - The method name on the actor
|
|
471
|
+
* @template T - The transformation key (identity, display, etc.)
|
|
472
|
+
* @template TPageParam - The page parameter type
|
|
473
|
+
* @template TSelected - The type returned after select transformation
|
|
474
|
+
*
|
|
475
|
+
* @param reactor - The Reactor instance
|
|
476
|
+
* @param config - Infinite query configuration (without getArgs)
|
|
477
|
+
* @returns A function that accepts getArgs and returns an ActorInfiniteQueryResult
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* const getPostsQuery = createActorInfiniteQueryFactory(reactor, {
|
|
481
|
+
* functionName: "get_posts",
|
|
482
|
+
* initialPageParam: 0,
|
|
483
|
+
* getKeyArgs: (args) => {
|
|
484
|
+
* const [{ userId }] = args
|
|
485
|
+
* return [{ userId }]
|
|
486
|
+
* },
|
|
487
|
+
* getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
488
|
+
* })
|
|
489
|
+
*
|
|
490
|
+
* // Create query with specific args builder
|
|
491
|
+
* const userPostsQuery = getPostsQuery((cursor) => [{ userId, cursor, limit: 10 }])
|
|
492
|
+
* const { data, fetchNextPage } = userPostsQuery.useInfiniteQuery()
|
|
493
|
+
*/
|
|
494
|
+
|
|
495
|
+
export function createInfiniteQueryFactory<
|
|
496
|
+
A,
|
|
497
|
+
T extends TransformKey,
|
|
498
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
499
|
+
TPageParam = unknown,
|
|
500
|
+
TSelected = InfiniteData<InfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
501
|
+
>(
|
|
502
|
+
reactor: Reactor<A, T>,
|
|
503
|
+
config: InfiniteQueryFactoryConfig<NoInfer<A>, M, T, TPageParam, TSelected>
|
|
504
|
+
): InfiniteQueryFactoryFn<A, M, T, TPageParam, TSelected> {
|
|
505
|
+
const factory: InfiniteQueryFactoryFn<A, M, T, TPageParam, TSelected> = (
|
|
506
|
+
getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
|
|
507
|
+
) => {
|
|
508
|
+
const initialArgs = getArgs(config.initialPageParam)
|
|
509
|
+
const keyArgs = config.getKeyArgs?.(initialArgs) ?? initialArgs
|
|
510
|
+
const queryKey = mergeFactoryQueryKey(config.queryKey, keyArgs)
|
|
511
|
+
|
|
512
|
+
return createInfiniteQueryImpl<A, M, T, TPageParam, TSelected>(reactor, {
|
|
513
|
+
...(({ getKeyArgs: _getKeyArgs, ...rest }) => rest)(
|
|
514
|
+
config as InfiniteQueryFactoryConfig<A, M, T, TPageParam, TSelected>
|
|
515
|
+
),
|
|
516
|
+
queryKey,
|
|
517
|
+
getArgs,
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return factory
|
|
522
|
+
}
|