@ic-reactor/react 3.0.0-beta.7 → 3.0.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 +11 -10
- package/dist/createActorHooks.d.ts +2 -0
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +2 -0
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +4 -0
- package/dist/createMutation.js.map +1 -1
- package/dist/hooks/index.d.ts +18 -5
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -5
- 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 +105 -0
- package/dist/hooks/useActorMethod.d.ts.map +1 -0
- package/dist/hooks/useActorMethod.js +192 -0
- package/dist/hooks/useActorMethod.js.map +1 -0
- 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 +9 -8
- package/src/createActorHooks.ts +146 -0
- package/src/createAuthHooks.ts +137 -0
- package/src/createInfiniteQuery.ts +471 -0
- package/src/createMutation.ts +163 -0
- package/src/createQuery.ts +197 -0
- package/src/createSuspenseInfiniteQuery.ts +478 -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,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
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suspense Infinite Query Factory - Generic wrapper for React Query suspense-based paginated canister data
|
|
3
|
+
*
|
|
4
|
+
* Creates unified fetch/hook/invalidate functions for any paginated canister method.
|
|
5
|
+
* Works with any Reactor instance.
|
|
6
|
+
*
|
|
7
|
+
* Uses `useSuspenseInfiniteQuery` which:
|
|
8
|
+
* - Requires wrapping in <Suspense> boundary
|
|
9
|
+
* - Data is always defined (no undefined checks)
|
|
10
|
+
* - Does NOT support `enabled` or `placeholderData` options
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const postsQuery = createSuspenseInfiniteQuery(reactor, {
|
|
14
|
+
* functionName: "get_posts",
|
|
15
|
+
* initialPageParam: 0,
|
|
16
|
+
* getArgs: (cursor) => [{ cursor, limit: 10 }],
|
|
17
|
+
* getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // In component (wrap in Suspense)
|
|
21
|
+
* const { data, fetchNextPage, hasNextPage } = postsQuery.useSuspenseInfiniteQuery()
|
|
22
|
+
*
|
|
23
|
+
* // Flatten all pages
|
|
24
|
+
* const allPosts = data.pages.flatMap(page => page.posts)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
Reactor,
|
|
29
|
+
FunctionName,
|
|
30
|
+
ReactorArgs,
|
|
31
|
+
BaseActor,
|
|
32
|
+
TransformKey,
|
|
33
|
+
ReactorReturnOk,
|
|
34
|
+
ReactorReturnErr,
|
|
35
|
+
} from "@ic-reactor/core"
|
|
36
|
+
import {
|
|
37
|
+
QueryKey,
|
|
38
|
+
useSuspenseInfiniteQuery,
|
|
39
|
+
InfiniteData,
|
|
40
|
+
UseSuspenseInfiniteQueryResult,
|
|
41
|
+
UseSuspenseInfiniteQueryOptions,
|
|
42
|
+
QueryFunctionContext,
|
|
43
|
+
FetchInfiniteQueryOptions,
|
|
44
|
+
InfiniteQueryObserverOptions,
|
|
45
|
+
} from "@tanstack/react-query"
|
|
46
|
+
import { CallConfig } from "@icp-sdk/core/agent"
|
|
47
|
+
import { NoInfer } from "./types"
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Type Definitions
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/** The raw page data type returned by the query function */
|
|
54
|
+
export type SuspenseInfiniteQueryPageData<
|
|
55
|
+
A = BaseActor,
|
|
56
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
57
|
+
T extends TransformKey = "candid",
|
|
58
|
+
> = ReactorReturnOk<A, M, T>
|
|
59
|
+
|
|
60
|
+
/** The error type for infinite queries */
|
|
61
|
+
export type SuspenseInfiniteQueryError<
|
|
62
|
+
A = BaseActor,
|
|
63
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
64
|
+
T extends TransformKey = "candid",
|
|
65
|
+
> = ReactorReturnErr<A, M, T>
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Configuration Types
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Configuration for createActorSuspenseInfiniteQuery.
|
|
73
|
+
* Extends InfiniteQueryObserverOptions to accept all React Query options at the create level.
|
|
74
|
+
*
|
|
75
|
+
* @template A - The actor interface type
|
|
76
|
+
* @template M - The method name on the actor
|
|
77
|
+
* @template T - The transformation key (identity, display, etc.)
|
|
78
|
+
* @template TPageParam - The type of the page parameter
|
|
79
|
+
* @template TSelected - The type returned after select transformation
|
|
80
|
+
*/
|
|
81
|
+
export interface SuspenseInfiniteQueryConfig<
|
|
82
|
+
A = BaseActor,
|
|
83
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
84
|
+
T extends TransformKey = "candid",
|
|
85
|
+
TPageParam = unknown,
|
|
86
|
+
TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
87
|
+
> extends Omit<
|
|
88
|
+
InfiniteQueryObserverOptions<
|
|
89
|
+
SuspenseInfiniteQueryPageData<A, M, T>,
|
|
90
|
+
SuspenseInfiniteQueryError<A, M, T>,
|
|
91
|
+
TSelected,
|
|
92
|
+
QueryKey,
|
|
93
|
+
TPageParam
|
|
94
|
+
>,
|
|
95
|
+
"queryKey" | "queryFn"
|
|
96
|
+
> {
|
|
97
|
+
/** The method to call on the canister */
|
|
98
|
+
functionName: M
|
|
99
|
+
/** Call configuration for the actor method */
|
|
100
|
+
callConfig?: CallConfig
|
|
101
|
+
/** Custom query key (optional, auto-generated if not provided) */
|
|
102
|
+
queryKey?: QueryKey
|
|
103
|
+
/** Function to get args from page parameter */
|
|
104
|
+
getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Configuration for createActorSuspenseInfiniteQueryFactory (without initialPageParam, getArgs determined at call time).
|
|
109
|
+
*/
|
|
110
|
+
export type SuspenseInfiniteQueryFactoryConfig<
|
|
111
|
+
A = BaseActor,
|
|
112
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
113
|
+
T extends TransformKey = "candid",
|
|
114
|
+
TPageParam = unknown,
|
|
115
|
+
TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
116
|
+
> = Omit<SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>, "getArgs">
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Hook Interface
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* useSuspenseInfiniteQuery hook with chained select support.
|
|
124
|
+
*/
|
|
125
|
+
export interface UseSuspenseInfiniteQueryWithSelect<
|
|
126
|
+
TPageData,
|
|
127
|
+
TPageParam,
|
|
128
|
+
TSelected = InfiniteData<TPageData, TPageParam>,
|
|
129
|
+
TError = Error,
|
|
130
|
+
> {
|
|
131
|
+
// Overload 1: Without select - returns TSelected
|
|
132
|
+
(
|
|
133
|
+
options?: Omit<
|
|
134
|
+
UseSuspenseInfiniteQueryOptions<
|
|
135
|
+
TPageData,
|
|
136
|
+
TError,
|
|
137
|
+
TSelected,
|
|
138
|
+
QueryKey,
|
|
139
|
+
TPageParam
|
|
140
|
+
>,
|
|
141
|
+
| "select"
|
|
142
|
+
| "queryKey"
|
|
143
|
+
| "queryFn"
|
|
144
|
+
| "initialPageParam"
|
|
145
|
+
| "getNextPageParam"
|
|
146
|
+
| "getPreviousPageParam"
|
|
147
|
+
>
|
|
148
|
+
): UseSuspenseInfiniteQueryResult<TSelected, TError>
|
|
149
|
+
|
|
150
|
+
// Overload 2: With select - chains on top and returns TFinal
|
|
151
|
+
<TFinal = TSelected>(
|
|
152
|
+
options: Omit<
|
|
153
|
+
UseSuspenseInfiniteQueryOptions<
|
|
154
|
+
TPageData,
|
|
155
|
+
TError,
|
|
156
|
+
TFinal,
|
|
157
|
+
QueryKey,
|
|
158
|
+
TPageParam
|
|
159
|
+
>,
|
|
160
|
+
| "queryKey"
|
|
161
|
+
| "queryFn"
|
|
162
|
+
| "select"
|
|
163
|
+
| "initialPageParam"
|
|
164
|
+
| "getNextPageParam"
|
|
165
|
+
| "getPreviousPageParam"
|
|
166
|
+
> & {
|
|
167
|
+
select: (data: TSelected) => TFinal
|
|
168
|
+
}
|
|
169
|
+
): UseSuspenseInfiniteQueryResult<TFinal, TError>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Result Interface
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Result from createActorSuspenseInfiniteQuery
|
|
178
|
+
*
|
|
179
|
+
* @template TPageData - The raw page data type
|
|
180
|
+
* @template TPageParam - The page parameter type
|
|
181
|
+
* @template TSelected - The type after select transformation
|
|
182
|
+
* @template TError - The error type
|
|
183
|
+
*/
|
|
184
|
+
export interface SuspenseInfiniteQueryResult<
|
|
185
|
+
TPageData,
|
|
186
|
+
TPageParam,
|
|
187
|
+
TSelected = InfiniteData<TPageData, TPageParam>,
|
|
188
|
+
TError = Error,
|
|
189
|
+
> {
|
|
190
|
+
/** Fetch first page in loader (uses ensureInfiniteQueryData for cache-first) */
|
|
191
|
+
fetch: () => Promise<TSelected>
|
|
192
|
+
|
|
193
|
+
/** React hook for components - supports pagination */
|
|
194
|
+
useSuspenseInfiniteQuery: UseSuspenseInfiniteQueryWithSelect<
|
|
195
|
+
TPageData,
|
|
196
|
+
TPageParam,
|
|
197
|
+
TSelected,
|
|
198
|
+
TError
|
|
199
|
+
>
|
|
200
|
+
|
|
201
|
+
/** Invalidate the cache (refetches if query is active) */
|
|
202
|
+
invalidate: () => Promise<void>
|
|
203
|
+
|
|
204
|
+
/** Get query key (for advanced React Query usage) */
|
|
205
|
+
getQueryKey: () => QueryKey
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read data directly from cache without fetching.
|
|
209
|
+
* Returns undefined if data is not in cache.
|
|
210
|
+
*/
|
|
211
|
+
getCacheData: {
|
|
212
|
+
(): TSelected | undefined
|
|
213
|
+
<TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Internal Implementation
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
const createSuspenseInfiniteQueryImpl = <
|
|
222
|
+
A,
|
|
223
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
224
|
+
T extends TransformKey = "candid",
|
|
225
|
+
TPageParam = unknown,
|
|
226
|
+
TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
227
|
+
>(
|
|
228
|
+
reactor: Reactor<A, T>,
|
|
229
|
+
config: SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>
|
|
230
|
+
): SuspenseInfiniteQueryResult<
|
|
231
|
+
SuspenseInfiniteQueryPageData<A, M, T>,
|
|
232
|
+
TPageParam,
|
|
233
|
+
TSelected,
|
|
234
|
+
SuspenseInfiniteQueryError<A, M, T>
|
|
235
|
+
> => {
|
|
236
|
+
type TPageData = SuspenseInfiniteQueryPageData<A, M, T>
|
|
237
|
+
type TError = SuspenseInfiniteQueryError<A, M, T>
|
|
238
|
+
type TInfiniteData = InfiniteData<TPageData, TPageParam>
|
|
239
|
+
|
|
240
|
+
const {
|
|
241
|
+
functionName,
|
|
242
|
+
callConfig,
|
|
243
|
+
queryKey: customQueryKey,
|
|
244
|
+
initialPageParam,
|
|
245
|
+
getArgs,
|
|
246
|
+
getNextPageParam,
|
|
247
|
+
getPreviousPageParam,
|
|
248
|
+
maxPages,
|
|
249
|
+
staleTime = 5 * 60 * 1000,
|
|
250
|
+
select,
|
|
251
|
+
...rest
|
|
252
|
+
} = config
|
|
253
|
+
|
|
254
|
+
// Get query key from actor manager
|
|
255
|
+
const getQueryKey = (): QueryKey => {
|
|
256
|
+
return reactor.generateQueryKey({
|
|
257
|
+
functionName,
|
|
258
|
+
queryKey: customQueryKey,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Query function - accepts QueryFunctionContext
|
|
263
|
+
const queryFn = async (
|
|
264
|
+
context: QueryFunctionContext<QueryKey, TPageParam>
|
|
265
|
+
): Promise<TPageData> => {
|
|
266
|
+
// pageParam is typed as unknown in QueryFunctionContext, but we know its type
|
|
267
|
+
const pageParam = context.pageParam as TPageParam
|
|
268
|
+
const args = getArgs(pageParam)
|
|
269
|
+
const result = await reactor.callMethod({
|
|
270
|
+
functionName,
|
|
271
|
+
args,
|
|
272
|
+
callConfig,
|
|
273
|
+
})
|
|
274
|
+
return result
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Get infinite query options for fetchInfiniteQuery
|
|
278
|
+
const getInfiniteQueryOptions = (): FetchInfiniteQueryOptions<
|
|
279
|
+
TPageData,
|
|
280
|
+
TError,
|
|
281
|
+
TPageData,
|
|
282
|
+
QueryKey,
|
|
283
|
+
TPageParam
|
|
284
|
+
> => ({
|
|
285
|
+
queryKey: getQueryKey(),
|
|
286
|
+
queryFn,
|
|
287
|
+
initialPageParam,
|
|
288
|
+
getNextPageParam,
|
|
289
|
+
staleTime,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// Fetch function for loaders (cache-first, fetches first page)
|
|
293
|
+
const fetch = async (): Promise<TSelected> => {
|
|
294
|
+
// Use ensureInfiniteQueryData to get cached data or fetch if stale
|
|
295
|
+
const result = await reactor.queryClient.ensureInfiniteQueryData(
|
|
296
|
+
getInfiniteQueryOptions()
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
// Result is already InfiniteData format
|
|
300
|
+
return select ? select(result) : (result as unknown as TSelected)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Implementation
|
|
304
|
+
const useSuspenseInfiniteQueryHook: UseSuspenseInfiniteQueryWithSelect<
|
|
305
|
+
TPageData,
|
|
306
|
+
TPageParam,
|
|
307
|
+
TSelected,
|
|
308
|
+
TError
|
|
309
|
+
> = (options: any): any => {
|
|
310
|
+
// Chain the selects: raw -> config.select -> options.select
|
|
311
|
+
const chainedSelect = (rawData: TInfiniteData) => {
|
|
312
|
+
const firstPass = select ? select(rawData) : rawData
|
|
313
|
+
if (options?.select) {
|
|
314
|
+
return options.select(firstPass)
|
|
315
|
+
}
|
|
316
|
+
return firstPass
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return useSuspenseInfiniteQuery(
|
|
320
|
+
{
|
|
321
|
+
queryKey: getQueryKey(),
|
|
322
|
+
queryFn,
|
|
323
|
+
initialPageParam,
|
|
324
|
+
getNextPageParam,
|
|
325
|
+
getPreviousPageParam,
|
|
326
|
+
maxPages,
|
|
327
|
+
staleTime,
|
|
328
|
+
...rest,
|
|
329
|
+
...options,
|
|
330
|
+
select: chainedSelect,
|
|
331
|
+
},
|
|
332
|
+
reactor.queryClient
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Invalidate function
|
|
337
|
+
const invalidate = async (): Promise<void> => {
|
|
338
|
+
const queryKey = getQueryKey()
|
|
339
|
+
await reactor.queryClient.invalidateQueries({ queryKey })
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Get data from cache without fetching
|
|
343
|
+
const getCacheData = (selectFn?: (data: TSelected) => any) => {
|
|
344
|
+
const queryKey = getQueryKey()
|
|
345
|
+
const cachedRawData = reactor.queryClient.getQueryData(
|
|
346
|
+
queryKey
|
|
347
|
+
) as TInfiniteData
|
|
348
|
+
|
|
349
|
+
if (cachedRawData === undefined) {
|
|
350
|
+
return undefined
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Apply config.select to raw cache data
|
|
354
|
+
const selectedData = (
|
|
355
|
+
select ? select(cachedRawData) : cachedRawData
|
|
356
|
+
) as TSelected
|
|
357
|
+
|
|
358
|
+
// Apply optional select parameter
|
|
359
|
+
if (selectFn) {
|
|
360
|
+
return selectFn(selectedData)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return selectedData
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
fetch,
|
|
368
|
+
useSuspenseInfiniteQuery: useSuspenseInfiniteQueryHook,
|
|
369
|
+
invalidate,
|
|
370
|
+
getQueryKey,
|
|
371
|
+
getCacheData,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// Factory Function
|
|
377
|
+
// ============================================================================
|
|
378
|
+
|
|
379
|
+
export function createSuspenseInfiniteQuery<
|
|
380
|
+
A,
|
|
381
|
+
T extends TransformKey,
|
|
382
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
383
|
+
TPageParam = unknown,
|
|
384
|
+
TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
385
|
+
>(
|
|
386
|
+
reactor: Reactor<A, T>,
|
|
387
|
+
config: SuspenseInfiniteQueryConfig<NoInfer<A>, M, T, TPageParam, TSelected>
|
|
388
|
+
): SuspenseInfiniteQueryResult<
|
|
389
|
+
SuspenseInfiniteQueryPageData<A, M, T>,
|
|
390
|
+
TPageParam,
|
|
391
|
+
TSelected,
|
|
392
|
+
SuspenseInfiniteQueryError<A, M, T>
|
|
393
|
+
> {
|
|
394
|
+
return createSuspenseInfiniteQueryImpl(
|
|
395
|
+
reactor,
|
|
396
|
+
config as SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// Factory with Dynamic Args
|
|
402
|
+
// ============================================================================
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Create a suspense infinite query factory that accepts getArgs at call time.
|
|
406
|
+
* Useful when pagination logic varies by context.
|
|
407
|
+
*
|
|
408
|
+
* @template A - The actor interface type
|
|
409
|
+
* @template M - The method name on the actor
|
|
410
|
+
* @template T - The transformation key (identity, display, etc.)
|
|
411
|
+
* @template TPageParam - The page parameter type
|
|
412
|
+
* @template TSelected - The type returned after select transformation
|
|
413
|
+
*
|
|
414
|
+
* @param reactor - The Reactor instance
|
|
415
|
+
* @param config - Suspense infinite query configuration (without getArgs)
|
|
416
|
+
* @returns A function that accepts getArgs and returns an SuspenseActorInfiniteQueryResult
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* const getPostsQuery = createActorSuspenseInfiniteQueryFactory(reactor, {
|
|
420
|
+
* functionName: "get_posts",
|
|
421
|
+
* initialPageParam: 0,
|
|
422
|
+
* getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
423
|
+
* })
|
|
424
|
+
*
|
|
425
|
+
* // Create query with specific args builder
|
|
426
|
+
* const userPostsQuery = getPostsQuery((cursor) => [{ userId, cursor, limit: 10 }])
|
|
427
|
+
* const { data, fetchNextPage } = userPostsQuery.useSuspenseInfiniteQuery()
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
export function createSuspenseInfiniteQueryFactory<
|
|
431
|
+
A,
|
|
432
|
+
T extends TransformKey,
|
|
433
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
434
|
+
TPageParam = unknown,
|
|
435
|
+
TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
|
|
436
|
+
>(
|
|
437
|
+
reactor: Reactor<A, T>,
|
|
438
|
+
config: SuspenseInfiniteQueryFactoryConfig<
|
|
439
|
+
NoInfer<A>,
|
|
440
|
+
M,
|
|
441
|
+
T,
|
|
442
|
+
TPageParam,
|
|
443
|
+
TSelected
|
|
444
|
+
>
|
|
445
|
+
): (
|
|
446
|
+
getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>,
|
|
447
|
+
options?: { queryKey?: QueryKey }
|
|
448
|
+
) => SuspenseInfiniteQueryResult<
|
|
449
|
+
SuspenseInfiniteQueryPageData<A, M, T>,
|
|
450
|
+
TPageParam,
|
|
451
|
+
TSelected,
|
|
452
|
+
SuspenseInfiniteQueryError<A, M, T>
|
|
453
|
+
> {
|
|
454
|
+
return (
|
|
455
|
+
getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>,
|
|
456
|
+
options?: { queryKey?: QueryKey }
|
|
457
|
+
): SuspenseInfiniteQueryResult<
|
|
458
|
+
SuspenseInfiniteQueryPageData<A, M, T>,
|
|
459
|
+
TPageParam,
|
|
460
|
+
TSelected,
|
|
461
|
+
SuspenseInfiniteQueryError<A, M, T>
|
|
462
|
+
> => {
|
|
463
|
+
return createSuspenseInfiniteQueryImpl<A, M, T, TPageParam, TSelected>(
|
|
464
|
+
reactor,
|
|
465
|
+
{
|
|
466
|
+
...(config as SuspenseInfiniteQueryFactoryConfig<
|
|
467
|
+
A,
|
|
468
|
+
M,
|
|
469
|
+
T,
|
|
470
|
+
TPageParam,
|
|
471
|
+
TSelected
|
|
472
|
+
>),
|
|
473
|
+
...options,
|
|
474
|
+
getArgs,
|
|
475
|
+
} as SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
}
|