@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.
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,556 @@
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
+ const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
50
+
51
+ type SuspenseInfiniteFactoryCallOptions = {
52
+ queryKey?: QueryKey
53
+ }
54
+
55
+ type SuspenseInfiniteQueryFactoryFn<
56
+ A,
57
+ M extends FunctionName<A>,
58
+ T extends TransformKey,
59
+ TPageParam,
60
+ TSelected,
61
+ > = {
62
+ (
63
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
64
+ ): SuspenseInfiniteQueryResult<
65
+ SuspenseInfiniteQueryPageData<A, M, T>,
66
+ TPageParam,
67
+ TSelected,
68
+ SuspenseInfiniteQueryError<A, M, T>
69
+ >
70
+ (
71
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>,
72
+ options?: SuspenseInfiniteFactoryCallOptions
73
+ ): SuspenseInfiniteQueryResult<
74
+ SuspenseInfiniteQueryPageData<A, M, T>,
75
+ TPageParam,
76
+ TSelected,
77
+ SuspenseInfiniteQueryError<A, M, T>
78
+ >
79
+ }
80
+
81
+ const mergeFactoryQueryKey = (
82
+ baseQueryKey?: QueryKey,
83
+ callQueryKey?: QueryKey,
84
+ keyArgs?: unknown
85
+ ): QueryKey | undefined => {
86
+ const merged: unknown[] = []
87
+
88
+ if (baseQueryKey) {
89
+ merged.push(...baseQueryKey)
90
+ }
91
+ if (callQueryKey) {
92
+ merged.push(...callQueryKey)
93
+ }
94
+ if (keyArgs !== undefined) {
95
+ merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
96
+ }
97
+
98
+ return merged.length > 0 ? merged : undefined
99
+ }
100
+
101
+ // ============================================================================
102
+ // Type Definitions
103
+ // ============================================================================
104
+
105
+ /** The raw page data type returned by the query function */
106
+ export type SuspenseInfiniteQueryPageData<
107
+ A = BaseActor,
108
+ M extends FunctionName<A> = FunctionName<A>,
109
+ T extends TransformKey = "candid",
110
+ > = ReactorReturnOk<A, M, T>
111
+
112
+ /** The error type for infinite queries */
113
+ export type SuspenseInfiniteQueryError<
114
+ A = BaseActor,
115
+ M extends FunctionName<A> = FunctionName<A>,
116
+ T extends TransformKey = "candid",
117
+ > = ReactorReturnErr<A, M, T>
118
+
119
+ // ============================================================================
120
+ // Configuration Types
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Configuration for createActorSuspenseInfiniteQuery.
125
+ * Extends InfiniteQueryObserverOptions to accept all React Query options at the create level.
126
+ *
127
+ * @template A - The actor interface type
128
+ * @template M - The method name on the actor
129
+ * @template T - The transformation key (identity, display, etc.)
130
+ * @template TPageParam - The type of the page parameter
131
+ * @template TSelected - The type returned after select transformation
132
+ */
133
+ export interface SuspenseInfiniteQueryConfig<
134
+ A = BaseActor,
135
+ M extends FunctionName<A> = FunctionName<A>,
136
+ T extends TransformKey = "candid",
137
+ TPageParam = unknown,
138
+ TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
139
+ > extends Omit<
140
+ InfiniteQueryObserverOptions<
141
+ SuspenseInfiniteQueryPageData<A, M, T>,
142
+ SuspenseInfiniteQueryError<A, M, T>,
143
+ TSelected,
144
+ QueryKey,
145
+ TPageParam
146
+ >,
147
+ "queryKey" | "queryFn"
148
+ > {
149
+ /** The method to call on the canister */
150
+ functionName: M
151
+ /** Call configuration for the actor method */
152
+ callConfig?: CallConfig
153
+ /** Custom query key (optional, auto-generated if not provided) */
154
+ queryKey?: QueryKey
155
+ /** Function to get args from page parameter */
156
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>
157
+ }
158
+
159
+ /**
160
+ * Configuration for createActorSuspenseInfiniteQueryFactory (without getArgs; provided at call time).
161
+ */
162
+ export type SuspenseInfiniteQueryFactoryConfig<
163
+ A = BaseActor,
164
+ M extends FunctionName<A> = FunctionName<A>,
165
+ T extends TransformKey = "candid",
166
+ TPageParam = unknown,
167
+ TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
168
+ > = Omit<
169
+ SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>,
170
+ "getArgs"
171
+ > & {
172
+ /**
173
+ * Optional key-args derivation for factory calls.
174
+ * Receives the resolved args from `getArgs(initialPageParam)` and should return
175
+ * a stable serializable representation of the logical query identity
176
+ * (typically excluding pagination/cursor fields).
177
+ */
178
+ getKeyArgs?: (args: ReactorArgs<A, M, T>) => unknown
179
+ }
180
+
181
+ // ============================================================================
182
+ // Hook Interface
183
+ // ============================================================================
184
+
185
+ /**
186
+ * useSuspenseInfiniteQuery hook with chained select support.
187
+ */
188
+ export interface UseSuspenseInfiniteQueryWithSelect<
189
+ TPageData,
190
+ TPageParam,
191
+ TSelected = InfiniteData<TPageData, TPageParam>,
192
+ TError = Error,
193
+ > {
194
+ // Overload 1: Without select - returns TSelected
195
+ (
196
+ options?: Omit<
197
+ UseSuspenseInfiniteQueryOptions<
198
+ TPageData,
199
+ TError,
200
+ TSelected,
201
+ QueryKey,
202
+ TPageParam
203
+ >,
204
+ | "select"
205
+ | "queryKey"
206
+ | "queryFn"
207
+ | "initialPageParam"
208
+ | "getNextPageParam"
209
+ | "getPreviousPageParam"
210
+ >
211
+ ): UseSuspenseInfiniteQueryResult<TSelected, TError>
212
+
213
+ // Overload 2: With select - chains on top and returns TFinal
214
+ <TFinal = TSelected>(
215
+ options: Omit<
216
+ UseSuspenseInfiniteQueryOptions<
217
+ TPageData,
218
+ TError,
219
+ TFinal,
220
+ QueryKey,
221
+ TPageParam
222
+ >,
223
+ | "queryKey"
224
+ | "queryFn"
225
+ | "select"
226
+ | "initialPageParam"
227
+ | "getNextPageParam"
228
+ | "getPreviousPageParam"
229
+ > & {
230
+ select: (data: TSelected) => TFinal
231
+ }
232
+ ): UseSuspenseInfiniteQueryResult<TFinal, TError>
233
+ }
234
+
235
+ // ============================================================================
236
+ // Result Interface
237
+ // ============================================================================
238
+
239
+ /**
240
+ * Result from createActorSuspenseInfiniteQuery
241
+ *
242
+ * @template TPageData - The raw page data type
243
+ * @template TPageParam - The page parameter type
244
+ * @template TSelected - The type after select transformation
245
+ * @template TError - The error type
246
+ */
247
+ export interface SuspenseInfiniteQueryResult<
248
+ TPageData,
249
+ TPageParam,
250
+ TSelected = InfiniteData<TPageData, TPageParam>,
251
+ TError = Error,
252
+ > {
253
+ /** Fetch first page in loader (uses ensureInfiniteQueryData for cache-first) */
254
+ fetch: () => Promise<TSelected>
255
+
256
+ /** React hook for components - supports pagination */
257
+ useSuspenseInfiniteQuery: UseSuspenseInfiniteQueryWithSelect<
258
+ TPageData,
259
+ TPageParam,
260
+ TSelected,
261
+ TError
262
+ >
263
+
264
+ /** Invalidate the cache (refetches if query is active) */
265
+ invalidate: () => Promise<void>
266
+
267
+ /** Get query key (for advanced React Query usage) */
268
+ getQueryKey: () => QueryKey
269
+
270
+ /**
271
+ * Read data directly from cache without fetching.
272
+ * Returns undefined if data is not in cache.
273
+ */
274
+ getCacheData: {
275
+ (): TSelected | undefined
276
+ <TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
277
+ }
278
+ }
279
+
280
+ // ============================================================================
281
+ // Internal Implementation
282
+ // ============================================================================
283
+
284
+ const createSuspenseInfiniteQueryImpl = <
285
+ A,
286
+ M extends FunctionName<A> = FunctionName<A>,
287
+ T extends TransformKey = "candid",
288
+ TPageParam = unknown,
289
+ TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
290
+ >(
291
+ reactor: Reactor<A, T>,
292
+ config: SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>
293
+ ): SuspenseInfiniteQueryResult<
294
+ SuspenseInfiniteQueryPageData<A, M, T>,
295
+ TPageParam,
296
+ TSelected,
297
+ SuspenseInfiniteQueryError<A, M, T>
298
+ > => {
299
+ type TPageData = SuspenseInfiniteQueryPageData<A, M, T>
300
+ type TError = SuspenseInfiniteQueryError<A, M, T>
301
+ type TInfiniteData = InfiniteData<TPageData, TPageParam>
302
+
303
+ const {
304
+ functionName,
305
+ callConfig,
306
+ queryKey: customQueryKey,
307
+ initialPageParam,
308
+ getArgs,
309
+ getNextPageParam,
310
+ getPreviousPageParam,
311
+ maxPages,
312
+ staleTime = 5 * 60 * 1000,
313
+ select,
314
+ ...rest
315
+ } = config
316
+
317
+ // Get query key from actor manager
318
+ const getQueryKey = (): QueryKey => {
319
+ return reactor.generateQueryKey({
320
+ functionName,
321
+ queryKey: customQueryKey,
322
+ })
323
+ }
324
+
325
+ // Query function - accepts QueryFunctionContext
326
+ const queryFn = async (
327
+ context: QueryFunctionContext<QueryKey, TPageParam>
328
+ ): Promise<TPageData> => {
329
+ // pageParam is typed as unknown in QueryFunctionContext, but we know its type
330
+ const pageParam = context.pageParam as TPageParam
331
+ const args = getArgs(pageParam)
332
+ const result = await reactor.callMethod({
333
+ functionName,
334
+ args,
335
+ callConfig,
336
+ })
337
+ return result
338
+ }
339
+
340
+ // Get infinite query options for fetchInfiniteQuery
341
+ const getInfiniteQueryOptions = (): FetchInfiniteQueryOptions<
342
+ TPageData,
343
+ TError,
344
+ TPageData,
345
+ QueryKey,
346
+ TPageParam
347
+ > => ({
348
+ queryKey: getQueryKey(),
349
+ queryFn,
350
+ initialPageParam,
351
+ getNextPageParam,
352
+ staleTime,
353
+ })
354
+
355
+ // Fetch function for loaders (cache-first, fetches first page)
356
+ const fetch = async (): Promise<TSelected> => {
357
+ // Use ensureInfiniteQueryData to get cached data or fetch if stale
358
+ const result = await reactor.queryClient.ensureInfiniteQueryData(
359
+ getInfiniteQueryOptions()
360
+ )
361
+
362
+ // Result is already InfiniteData format
363
+ return select ? select(result) : (result as unknown as TSelected)
364
+ }
365
+
366
+ // Implementation
367
+ const useSuspenseInfiniteQueryHook: UseSuspenseInfiniteQueryWithSelect<
368
+ TPageData,
369
+ TPageParam,
370
+ TSelected,
371
+ TError
372
+ > = (options: any): any => {
373
+ // Chain the selects: raw -> config.select -> options.select
374
+ const chainedSelect = (rawData: TInfiniteData) => {
375
+ const firstPass = select ? select(rawData) : rawData
376
+ if (options?.select) {
377
+ return options.select(firstPass)
378
+ }
379
+ return firstPass
380
+ }
381
+
382
+ return useSuspenseInfiniteQuery(
383
+ {
384
+ queryKey: getQueryKey(),
385
+ queryFn,
386
+ initialPageParam,
387
+ getNextPageParam,
388
+ getPreviousPageParam,
389
+ maxPages,
390
+ staleTime,
391
+ ...rest,
392
+ ...options,
393
+ select: chainedSelect,
394
+ },
395
+ reactor.queryClient
396
+ )
397
+ }
398
+
399
+ // Invalidate function
400
+ const invalidate = async (): Promise<void> => {
401
+ const queryKey = getQueryKey()
402
+ await reactor.queryClient.invalidateQueries({ queryKey })
403
+ }
404
+
405
+ // Get data from cache without fetching
406
+ const getCacheData = (selectFn?: (data: TSelected) => any) => {
407
+ const queryKey = getQueryKey()
408
+ const cachedRawData = reactor.queryClient.getQueryData(
409
+ queryKey
410
+ ) as TInfiniteData
411
+
412
+ if (cachedRawData === undefined) {
413
+ return undefined
414
+ }
415
+
416
+ // Apply config.select to raw cache data
417
+ const selectedData = (
418
+ select ? select(cachedRawData) : cachedRawData
419
+ ) as TSelected
420
+
421
+ // Apply optional select parameter
422
+ if (selectFn) {
423
+ return selectFn(selectedData)
424
+ }
425
+
426
+ return selectedData
427
+ }
428
+
429
+ return {
430
+ fetch,
431
+ useSuspenseInfiniteQuery: useSuspenseInfiniteQueryHook,
432
+ invalidate,
433
+ getQueryKey,
434
+ getCacheData,
435
+ }
436
+ }
437
+
438
+ // ============================================================================
439
+ // Factory Function
440
+ // ============================================================================
441
+
442
+ export function createSuspenseInfiniteQuery<
443
+ A,
444
+ T extends TransformKey,
445
+ M extends FunctionName<A> = FunctionName<A>,
446
+ TPageParam = unknown,
447
+ TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
448
+ >(
449
+ reactor: Reactor<A, T>,
450
+ config: SuspenseInfiniteQueryConfig<NoInfer<A>, M, T, TPageParam, TSelected>
451
+ ): SuspenseInfiniteQueryResult<
452
+ SuspenseInfiniteQueryPageData<A, M, T>,
453
+ TPageParam,
454
+ TSelected,
455
+ SuspenseInfiniteQueryError<A, M, T>
456
+ > {
457
+ return createSuspenseInfiniteQueryImpl(
458
+ reactor,
459
+ config as SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>
460
+ )
461
+ }
462
+
463
+ // ============================================================================
464
+ // Factory with Dynamic Args
465
+ // ============================================================================
466
+
467
+ /**
468
+ * Create a suspense infinite query factory that accepts getArgs at call time.
469
+ * Useful when pagination logic varies by context.
470
+ *
471
+ * @template A - The actor interface type
472
+ * @template M - The method name on the actor
473
+ * @template T - The transformation key (identity, display, etc.)
474
+ * @template TPageParam - The page parameter type
475
+ * @template TSelected - The type returned after select transformation
476
+ *
477
+ * @param reactor - The Reactor instance
478
+ * @param config - Suspense infinite query configuration (without getArgs)
479
+ * @returns A function that accepts getArgs and returns an SuspenseActorInfiniteQueryResult
480
+ *
481
+ * @example
482
+ * const getPostsQuery = createActorSuspenseInfiniteQueryFactory(reactor, {
483
+ * functionName: "get_posts",
484
+ * initialPageParam: 0,
485
+ * getKeyArgs: (args) => {
486
+ * const [{ userId }] = args
487
+ * return [{ userId }]
488
+ * },
489
+ * getNextPageParam: (lastPage) => lastPage.nextCursor,
490
+ * })
491
+ *
492
+ * // Create query with specific args builder
493
+ * const userPostsQuery = getPostsQuery((cursor) => [{ userId, cursor, limit: 10 }])
494
+ * const { data, fetchNextPage } = userPostsQuery.useSuspenseInfiniteQuery()
495
+ *
496
+ * // Optional: append a manual query-key suffix in addition to auto key args
497
+ * const scopedPostsQuery = getPostsQuery(
498
+ * (cursor) => [{ userId, cursor, limit: 10 }],
499
+ * { queryKey: ["v2"] }
500
+ * )
501
+ */
502
+
503
+ export function createSuspenseInfiniteQueryFactory<
504
+ A,
505
+ T extends TransformKey,
506
+ M extends FunctionName<A> = FunctionName<A>,
507
+ TPageParam = unknown,
508
+ TSelected = InfiniteData<SuspenseInfiniteQueryPageData<A, M, T>, TPageParam>,
509
+ >(
510
+ reactor: Reactor<A, T>,
511
+ config: SuspenseInfiniteQueryFactoryConfig<
512
+ NoInfer<A>,
513
+ M,
514
+ T,
515
+ TPageParam,
516
+ TSelected
517
+ >
518
+ ): SuspenseInfiniteQueryFactoryFn<A, M, T, TPageParam, TSelected> {
519
+ const factory: SuspenseInfiniteQueryFactoryFn<
520
+ A,
521
+ M,
522
+ T,
523
+ TPageParam,
524
+ TSelected
525
+ > = (
526
+ getArgs: (pageParam: TPageParam) => ReactorArgs<A, M, T>,
527
+ options?: SuspenseInfiniteFactoryCallOptions
528
+ ) => {
529
+ const initialArgs = getArgs(config.initialPageParam)
530
+ const keyArgs = config.getKeyArgs?.(initialArgs) ?? initialArgs
531
+ const queryKey = mergeFactoryQueryKey(
532
+ config.queryKey,
533
+ options?.queryKey,
534
+ keyArgs
535
+ )
536
+
537
+ return createSuspenseInfiniteQueryImpl<A, M, T, TPageParam, TSelected>(
538
+ reactor,
539
+ {
540
+ ...(({ getKeyArgs: _getKeyArgs, ...rest }) => rest)(
541
+ config as SuspenseInfiniteQueryFactoryConfig<
542
+ A,
543
+ M,
544
+ T,
545
+ TPageParam,
546
+ TSelected
547
+ >
548
+ ),
549
+ queryKey,
550
+ getArgs,
551
+ } as SuspenseInfiniteQueryConfig<A, M, T, TPageParam, TSelected>
552
+ )
553
+ }
554
+
555
+ return factory
556
+ }