@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.
Files changed (50) hide show
  1. package/README.md +83 -14
  2. package/dist/createInfiniteQuery.d.ts +23 -10
  3. package/dist/createInfiniteQuery.d.ts.map +1 -1
  4. package/dist/createInfiniteQuery.js +24 -3
  5. package/dist/createInfiniteQuery.js.map +1 -1
  6. package/dist/createMutation.d.ts.map +1 -1
  7. package/dist/createMutation.js +11 -1
  8. package/dist/createMutation.js.map +1 -1
  9. package/dist/createSuspenseInfiniteQuery.d.ts +29 -5
  10. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  11. package/dist/createSuspenseInfiniteQuery.js +31 -3
  12. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  13. package/dist/hooks/index.d.ts +18 -6
  14. package/dist/hooks/index.d.ts.map +1 -1
  15. package/dist/hooks/index.js +15 -6
  16. package/dist/hooks/index.js.map +1 -1
  17. package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
  18. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  19. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  20. package/dist/hooks/useActorMethod.d.ts +20 -24
  21. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  22. package/dist/hooks/useActorMethod.js +29 -18
  23. package/dist/hooks/useActorMethod.js.map +1 -1
  24. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
  25. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  26. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  27. package/package.json +8 -7
  28. package/src/createActorHooks.ts +146 -0
  29. package/src/createAuthHooks.ts +137 -0
  30. package/src/createInfiniteQuery.ts +522 -0
  31. package/src/createMutation.ts +173 -0
  32. package/src/createQuery.ts +197 -0
  33. package/src/createSuspenseInfiniteQuery.ts +556 -0
  34. package/src/createSuspenseQuery.ts +215 -0
  35. package/src/hooks/index.ts +93 -0
  36. package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
  37. package/src/hooks/useActorInfiniteQuery.ts +134 -0
  38. package/src/hooks/useActorMethod.test.tsx +798 -0
  39. package/src/hooks/useActorMethod.ts +397 -0
  40. package/src/hooks/useActorMutation.test.tsx +220 -0
  41. package/src/hooks/useActorMutation.ts +124 -0
  42. package/src/hooks/useActorQuery.test.tsx +287 -0
  43. package/src/hooks/useActorQuery.ts +110 -0
  44. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
  45. package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
  46. package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
  47. package/src/hooks/useActorSuspenseQuery.ts +112 -0
  48. package/src/index.ts +21 -0
  49. package/src/types.ts +435 -0
  50. 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
+ }