@ic-reactor/react 3.3.0 → 3.4.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.
Files changed (53) hide show
  1. package/README.md +117 -4
  2. package/dist/createActorHooks.d.ts +1 -1
  3. package/dist/createActorHooks.d.ts.map +1 -1
  4. package/dist/createActorHooks.js +10 -25
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createAuthHooks.d.ts +13 -5
  7. package/dist/createAuthHooks.d.ts.map +1 -1
  8. package/dist/createAuthHooks.js +51 -1
  9. package/dist/createAuthHooks.js.map +1 -1
  10. package/dist/createInfiniteQuery.d.ts.map +1 -1
  11. package/dist/createInfiniteQuery.js +2 -12
  12. package/dist/createInfiniteQuery.js.map +1 -1
  13. package/dist/createMutation.d.ts.map +1 -1
  14. package/dist/createMutation.js +39 -56
  15. package/dist/createMutation.js.map +1 -1
  16. package/dist/createQuery.d.ts.map +1 -1
  17. package/dist/createQuery.js +31 -39
  18. package/dist/createQuery.js.map +1 -1
  19. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  20. package/dist/createSuspenseInfiniteQuery.js +1 -14
  21. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  22. package/dist/createSuspenseQuery.d.ts.map +1 -1
  23. package/dist/createSuspenseQuery.js +30 -39
  24. package/dist/createSuspenseQuery.js.map +1 -1
  25. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  26. package/dist/hooks/useActorInfiniteQuery.js +4 -5
  27. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  28. package/dist/hooks/useActorMutation.d.ts +9 -2
  29. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  30. package/dist/hooks/useActorMutation.js +15 -15
  31. package/dist/hooks/useActorMutation.js.map +1 -1
  32. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  33. package/dist/hooks/useActorSuspenseInfiniteQuery.js +4 -5
  34. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  35. package/dist/types.d.ts +26 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils.d.ts +26 -0
  38. package/dist/utils.d.ts.map +1 -0
  39. package/dist/utils.js +41 -0
  40. package/dist/utils.js.map +1 -0
  41. package/package.json +17 -11
  42. package/src/createActorHooks.ts +32 -31
  43. package/src/createAuthHooks.ts +93 -6
  44. package/src/createInfiniteQuery.ts +2 -19
  45. package/src/createMutation.ts +52 -69
  46. package/src/createQuery.ts +41 -52
  47. package/src/createSuspenseInfiniteQuery.ts +1 -22
  48. package/src/createSuspenseQuery.ts +42 -56
  49. package/src/hooks/useActorInfiniteQuery.ts +4 -9
  50. package/src/hooks/useActorMutation.ts +40 -14
  51. package/src/hooks/useActorSuspenseInfiniteQuery.ts +4 -9
  52. package/src/types.ts +32 -0
  53. package/src/utils.ts +52 -0
@@ -30,6 +30,7 @@ import type {
30
30
  QueryFactoryConfig,
31
31
  NoInfer,
32
32
  } from "./types"
33
+ import { buildChainedSelect } from "./utils"
33
34
 
34
35
  // ============================================================================
35
36
  // Internal Implementation
@@ -56,37 +57,34 @@ const createQueryImpl = <
56
57
  ...rest
57
58
  } = config
58
59
 
59
- const params = {
60
- functionName,
61
- args,
62
- queryKey: customQueryKey,
63
- }
60
+ const params = { functionName, args, queryKey: customQueryKey }
64
61
 
65
- // Get query key from actor manager
66
- const getQueryKey = (): QueryKey => {
67
- return reactor.generateQueryKey(params)
68
- }
62
+ const getQueryKey = (): QueryKey => reactor.generateQueryKey(params)
69
63
 
70
- // Fetch function for loaders (cache-first)
64
+ // Apply config.select to raw data (shared by fetch, getCacheData, and the hook)
65
+ const applySelect = (raw: TData): TSelected =>
66
+ select ? select(raw) : (raw as unknown as TSelected)
67
+
68
+ /** Cache-first fetch for use in loaders / route preloading. */
71
69
  const fetch = async (): Promise<TSelected> => {
72
70
  const result = await reactor.fetchQuery(params)
73
- return select ? select(result) : (result as TSelected)
71
+ return applySelect(result)
72
+ }
73
+
74
+ /** Fire-and-forget prefetch — warms the cache without blocking. */
75
+ const prefetch = (): Promise<void> => {
76
+ const baseOptions = reactor.getQueryOptions(params)
77
+ return reactor.queryClient.prefetchQuery({
78
+ queryKey: baseOptions.queryKey,
79
+ queryFn: baseOptions.queryFn,
80
+ staleTime,
81
+ })
74
82
  }
75
83
 
76
- // Implementation
77
84
  const useQueryHook: UseQueryWithSelect<TData, TSelected, TError> = (
78
85
  options: any
79
86
  ): any => {
80
87
  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
88
  return useQuery(
91
89
  {
92
90
  queryKey: baseOptions.queryKey,
@@ -94,50 +92,46 @@ const createQueryImpl = <
94
92
  ...rest,
95
93
  ...options,
96
94
  queryFn: baseOptions.queryFn,
97
- select: chainedSelect,
95
+ select: buildChainedSelect(select, options?.select),
98
96
  },
99
97
  reactor.queryClient
100
98
  )
101
99
  }
102
100
 
103
- // Invalidate function
104
101
  const invalidate = async (): Promise<void> => {
105
- const queryKey = getQueryKey()
106
- await reactor.queryClient.invalidateQueries({ queryKey })
102
+ await reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() })
107
103
  }
108
104
 
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
- }
105
+ const getCacheData: QueryResult<TData, TSelected, TError>["getCacheData"] = (
106
+ selectFn?: (data: TSelected) => unknown
107
+ ): any => {
108
+ const raw = reactor.getQueryData(params)
109
+ if (raw === undefined) return undefined
110
+ const selected = applySelect(raw)
111
+ return selectFn ? selectFn(selected) : selected
112
+ }
126
113
 
127
- return selectedData
114
+ const setData: QueryResult<TData, TSelected, TError>["setData"] = (
115
+ updater
116
+ ) => {
117
+ return reactor.queryClient.setQueryData(getQueryKey(), updater as any) as
118
+ | TData
119
+ | undefined
128
120
  }
129
121
 
130
122
  return {
131
123
  fetch,
124
+ prefetch,
132
125
  useQuery: useQueryHook,
133
126
  invalidate,
134
127
  getQueryKey,
135
128
  getCacheData,
129
+ setData,
136
130
  }
137
131
  }
138
132
 
139
133
  // ============================================================================
140
- // Factory Function
134
+ // Public Factory Function
141
135
  // ============================================================================
142
136
 
143
137
  export function createQuery<
@@ -172,26 +166,21 @@ export function createQueryFactory<
172
166
  QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>>
173
167
  >()
174
168
 
175
- return (
176
- args: ReactorArgs<A, M, T>
177
- ): QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>> => {
169
+ return (args: ReactorArgs<A, M, T>) => {
178
170
  const key = reactor.generateQueryKey({
179
171
  functionName: config.functionName as M,
180
172
  args,
181
173
  })
182
174
  const cacheKey = JSON.stringify(key)
183
175
 
184
- if (cache.has(cacheKey)) {
185
- return cache.get(cacheKey)!
186
- }
176
+ const existing = cache.get(cacheKey)
177
+ if (existing) return existing
187
178
 
188
179
  const result = createQueryImpl<A, M, T, TSelected>(reactor, {
189
180
  ...config,
190
181
  args,
191
182
  })
192
-
193
183
  cache.set(cacheKey, result)
194
-
195
184
  return result
196
185
  }
197
186
  }
@@ -45,8 +45,7 @@ import {
45
45
  } from "@tanstack/react-query"
46
46
  import { CallConfig } from "@icp-sdk/core/agent"
47
47
  import { NoInfer } from "./types"
48
-
49
- const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
48
+ import { mergeFactoryQueryKey } from "./utils"
50
49
 
51
50
  type SuspenseInfiniteFactoryCallOptions = {
52
51
  queryKey?: QueryKey
@@ -78,26 +77,6 @@ type SuspenseInfiniteQueryFactoryFn<
78
77
  >
79
78
  }
80
79
 
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
80
  // ============================================================================
102
81
  // Type Definitions
103
82
  // ============================================================================
@@ -35,6 +35,7 @@ import type {
35
35
  SuspenseQueryFactoryConfig,
36
36
  NoInfer,
37
37
  } from "./types"
38
+ import { buildChainedSelect } from "./utils"
38
39
 
39
40
  // ============================================================================
40
41
  // Internal Implementation
@@ -65,39 +66,35 @@ const createSuspenseQueryImpl = <
65
66
  ...rest
66
67
  } = config
67
68
 
68
- const params = {
69
- functionName,
70
- args,
71
- queryKey: customQueryKey,
72
- }
69
+ const params = { functionName, args, queryKey: customQueryKey }
73
70
 
74
- // Get query key from actor manager
75
- const getQueryKey = () => {
76
- return reactor.generateQueryKey(params)
77
- }
71
+ const getQueryKey = () => reactor.generateQueryKey(params)
72
+
73
+ const applySelect = (raw: TData): TSelected =>
74
+ select ? select(raw) : (raw as unknown as TSelected)
78
75
 
79
- // Fetch function for loaders (cache-first)
76
+ /** Cache-first fetch for use in loaders / route preloading. */
80
77
  const fetch = async (): Promise<TSelected> => {
81
78
  const result = await reactor.fetchQuery(params)
82
- return select ? select(result) : (result as TSelected)
79
+ return applySelect(result)
80
+ }
81
+
82
+ /** Fire-and-forget prefetch — warms the cache without blocking. */
83
+ const prefetch = (): Promise<void> => {
84
+ const baseOptions = reactor.getQueryOptions(params)
85
+ return reactor.queryClient.prefetchQuery({
86
+ queryKey: baseOptions.queryKey,
87
+ queryFn: baseOptions.queryFn,
88
+ staleTime,
89
+ })
83
90
  }
84
91
 
85
- // Implementation
86
92
  const useSuspenseQueryHook: UseSuspenseQueryWithSelect<
87
93
  TData,
88
94
  TSelected,
89
95
  TError
90
96
  > = (options: any): any => {
91
97
  const baseOptions = reactor.getQueryOptions(params)
92
-
93
- // Chain the selects: raw -> config.select -> options.select
94
- const chainedSelect = (rawData: TData) => {
95
- const firstPass = config.select ? config.select(rawData) : rawData
96
- if (options?.select) {
97
- return options.select(firstPass)
98
- }
99
- return firstPass
100
- }
101
98
  return useSuspenseQuery(
102
99
  {
103
100
  queryKey: baseOptions.queryKey,
@@ -105,50 +102,48 @@ const createSuspenseQueryImpl = <
105
102
  ...rest,
106
103
  ...options,
107
104
  queryFn: baseOptions.queryFn,
108
- select: chainedSelect,
105
+ select: buildChainedSelect(select, options?.select),
109
106
  },
110
107
  reactor.queryClient
111
108
  )
112
109
  }
113
110
 
114
- // Invalidate function
115
111
  const invalidate = async (): Promise<void> => {
116
- const queryKey = getQueryKey()
117
- await reactor.queryClient.invalidateQueries({ queryKey })
112
+ await reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() })
118
113
  }
119
114
 
120
- // Get data from cache without fetching
121
- const getCacheData: any = (selectFn?: (data: TData) => any) => {
122
- const cachedRawData = reactor.getQueryData(params)
123
-
124
- if (cachedRawData === undefined) {
125
- return undefined
126
- }
127
-
128
- // Apply config.select to raw cache data
129
- const selectedData = (
130
- config.select ? config.select(cachedRawData) : cachedRawData
131
- ) as TData
132
-
133
- // Apply optional select parameter
134
- if (selectFn) {
135
- return selectFn(selectedData)
136
- }
115
+ const getCacheData: SuspenseQueryResult<
116
+ TData,
117
+ TSelected,
118
+ TError
119
+ >["getCacheData"] = (selectFn?: (data: TSelected) => unknown): any => {
120
+ const raw = reactor.getQueryData(params)
121
+ if (raw === undefined) return undefined
122
+ const selected = applySelect(raw)
123
+ return selectFn ? selectFn(selected) : selected
124
+ }
137
125
 
138
- return selectedData
126
+ const setData: SuspenseQueryResult<TData, TSelected, TError>["setData"] = (
127
+ updater
128
+ ) => {
129
+ return reactor.queryClient.setQueryData(getQueryKey(), updater as any) as
130
+ | TData
131
+ | undefined
139
132
  }
140
133
 
141
134
  return {
142
135
  fetch,
136
+ prefetch,
143
137
  useSuspenseQuery: useSuspenseQueryHook,
144
138
  invalidate,
145
139
  getQueryKey,
146
140
  getCacheData,
141
+ setData,
147
142
  }
148
143
  }
149
144
 
150
145
  // ============================================================================
151
- // Factory Function
146
+ // Public Factory Function
152
147
  // ============================================================================
153
148
 
154
149
  export function createSuspenseQuery<
@@ -186,30 +181,21 @@ export function createSuspenseQueryFactory<
186
181
  SuspenseQueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>>
187
182
  >()
188
183
 
189
- return (
190
- args: ReactorArgs<A, M, T>
191
- ): SuspenseQueryResult<
192
- QueryFnData<A, M, T>,
193
- TSelected,
194
- QueryError<A, M, T>
195
- > => {
184
+ return (args: ReactorArgs<A, M, T>) => {
196
185
  const key = reactor.generateQueryKey({
197
186
  functionName: config.functionName as M,
198
187
  args,
199
188
  })
200
189
  const cacheKey = JSON.stringify(key)
201
190
 
202
- if (cache.has(cacheKey)) {
203
- return cache.get(cacheKey)!
204
- }
191
+ const existing = cache.get(cacheKey)
192
+ if (existing) return existing
205
193
 
206
194
  const result = createSuspenseQueryImpl<A, M, T, TSelected>(reactor, {
207
195
  ...(config as SuspenseQueryFactoryConfig<A, M, T, TSelected>),
208
196
  args,
209
197
  })
210
-
211
198
  cache.set(cacheKey, result)
212
-
213
199
  return result
214
200
  }
215
201
  }
@@ -104,16 +104,11 @@ export const useActorInfiniteQuery = <
104
104
  T,
105
105
  TPageParam
106
106
  >): UseActorInfiniteQueryResult<A, M, T, TPageParam> => {
107
- // Memoize queryKey to prevent unnecessary re-calculations
107
+ // Always pass queryKey through generateQueryKey so it is merged with the
108
+ // reactor/function identity. Using the custom key verbatim would cause cache
109
+ // collisions if two different actors or methods share the same key string.
108
110
  const baseQueryKey = useMemo(
109
- () =>
110
- queryKey ??
111
- reactor.generateQueryKey(
112
- {
113
- functionName,
114
- },
115
- callConfig
116
- ),
111
+ () => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
117
112
  [queryKey, reactor, functionName, callConfig]
118
113
  )
119
114
 
@@ -12,6 +12,11 @@ import {
12
12
  FunctionName,
13
13
  TransformKey,
14
14
  ReactorReturnErr,
15
+ isCanisterError,
16
+ CanisterError,
17
+ ErrResult,
18
+ ActorMethodReturnType,
19
+ TransformReturnRegistry,
15
20
  } from "@ic-reactor/core"
16
21
  import { CallConfig } from "@icp-sdk/core/agent"
17
22
 
@@ -31,6 +36,17 @@ export interface UseActorMutationParameters<
31
36
  functionName: M
32
37
  callConfig?: CallConfig
33
38
  invalidateQueries?: QueryKey[]
39
+ /**
40
+ * Callback for canister-level business logic errors.
41
+ * Called when the canister returns a Result { Err: E } variant.
42
+ * Separate from `onError`, which fires for all errors including network failures.
43
+ */
44
+ onCanisterError?: (
45
+ error: CanisterError<
46
+ TransformReturnRegistry<ErrResult<ActorMethodReturnType<A[M]>>>[T]
47
+ >,
48
+ variables: ReactorArgs<A, M, T>
49
+ ) => void
34
50
  }
35
51
 
36
52
  export type UseActorMutationConfig<
@@ -57,6 +73,7 @@ export type UseActorMutationResult<
57
73
  * reactor,
58
74
  * functionName: "transfer",
59
75
  * onSuccess: () => console.log("Success!"),
76
+ * onCanisterError: (err) => console.error("Canister Err:", err.code),
60
77
  * })
61
78
  */
62
79
  export const useActorMutation = <
@@ -68,22 +85,17 @@ export const useActorMutation = <
68
85
  functionName,
69
86
  invalidateQueries,
70
87
  onSuccess,
88
+ onError,
89
+ onCanisterError,
71
90
  callConfig,
72
91
  ...options
73
92
  }: UseActorMutationParameters<A, M, T>): UseActorMutationResult<A, M, T> => {
74
- // Memoize mutationFn to avoid creating new function on every render
75
93
  const mutationFn = useCallback(
76
- async (args: ReactorArgs<A, M, T>) => {
77
- return reactor.callMethod({
78
- functionName,
79
- callConfig,
80
- args,
81
- })
82
- },
94
+ async (args: ReactorArgs<A, M, T>) =>
95
+ reactor.callMethod({ functionName, callConfig, args }),
83
96
  [reactor, functionName, callConfig]
84
97
  )
85
98
 
86
- // Memoize onSuccess handler
87
99
  const handleSuccess = useCallback(
88
100
  async (
89
101
  ...params: Parameters<
@@ -103,21 +115,35 @@ export const useActorMutation = <
103
115
  )
104
116
  )
105
117
  }
106
- if (onSuccess) {
107
- await onSuccess(...params)
108
- }
118
+ await onSuccess?.(...params)
109
119
  },
110
120
  [reactor, invalidateQueries, onSuccess]
111
121
  )
112
122
 
113
- // Memoize mutation options
123
+ const handleError = useCallback(
124
+ (
125
+ error: ReactorReturnErr<A, M, T>,
126
+ variables: ReactorArgs<A, M, T>,
127
+ context: unknown,
128
+ mutation: unknown
129
+ ) => {
130
+ if (isCanisterError(error)) {
131
+ onCanisterError?.(error as any, variables)
132
+ }
133
+ onError?.(error, variables, context as any, mutation as any)
134
+ },
135
+ [onCanisterError, onError]
136
+ )
137
+
114
138
  const mutationOptions = useMemo(
115
139
  () => ({
116
140
  ...options,
117
141
  mutationFn,
118
142
  onSuccess: handleSuccess,
143
+ onError: handleError,
119
144
  }),
120
- [options, mutationFn, handleSuccess]
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ [mutationFn, handleSuccess, handleError]
121
147
  )
122
148
 
123
149
  return useMutation(mutationOptions, reactor.queryClient)
@@ -107,16 +107,11 @@ export const useActorSuspenseInfiniteQuery = <
107
107
  T,
108
108
  TPageParam
109
109
  >): UseActorSuspenseInfiniteQueryResult<A, M, T, TPageParam> => {
110
- // Memoize queryKey to prevent unnecessary re-calculations
110
+ // Always pass queryKey through generateQueryKey so it is merged with the
111
+ // reactor/function identity. Using the custom key verbatim would cause cache
112
+ // collisions if two different actors or methods share the same key string.
111
113
  const baseQueryKey = useMemo(
112
- () =>
113
- queryKey ??
114
- reactor.generateQueryKey(
115
- {
116
- functionName,
117
- },
118
- callConfig
119
- ),
114
+ () => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
120
115
  [queryKey, reactor, functionName, callConfig]
121
116
  )
122
117
 
package/src/types.ts CHANGED
@@ -231,6 +231,18 @@ export interface BaseQueryResult<
231
231
  /** Fetch data in loader (uses ensureQueryData for cache-first) */
232
232
  fetch: () => Promise<TSelected>
233
233
 
234
+ /**
235
+ * Eagerly prefetch data into the cache without blocking.
236
+ * Useful for preloading data before navigating to a route.
237
+ *
238
+ * Unlike `fetch()`, this returns a void promise so it can be fire-and-forget.
239
+ *
240
+ * @example
241
+ * // In a route hover handler
242
+ * button.addEventListener("mouseenter", () => userQuery.prefetch())
243
+ */
244
+ prefetch: () => Promise<void>
245
+
234
246
  /** Invalidate the cache (refetches if query is active) */
235
247
  invalidate: () => Promise<void>
236
248
 
@@ -256,6 +268,26 @@ export interface BaseQueryResult<
256
268
  (): TSelected | undefined
257
269
  <TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
258
270
  }
271
+
272
+ /**
273
+ * Write raw data directly into the cache (useful for optimistic updates).
274
+ * Accepts a new value or an updater function that receives the current cached raw data.
275
+ *
276
+ * Note: The value is stored as raw (pre-select) data. Any active `select`
277
+ * transformations are automatically re-applied by React Query on the next render.
278
+ *
279
+ * @example
280
+ * // Optimistic update before a mutation
281
+ * userQuery.setData({ id: "1", name: "Alice" })
282
+ *
283
+ * // Functional update
284
+ * counterQuery.setData((prev) => (prev ?? 0) + 1)
285
+ */
286
+ setData: (
287
+ updater:
288
+ | TQueryFnData
289
+ | ((old: TQueryFnData | undefined) => TQueryFnData | undefined)
290
+ ) => TQueryFnData | undefined
259
291
  }
260
292
 
261
293
  /**
package/src/utils.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared internal utilities for query and mutation factories.
3
+ */
4
+
5
+ import type { QueryKey } from "@tanstack/react-query"
6
+
7
+ /**
8
+ * Internal query-key segment used to distinguish per-call factory args
9
+ * from the base query key. Not part of the public API.
10
+ */
11
+ export const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
12
+
13
+ /**
14
+ * Merge a base query key, optional per-call query key, and optional key-args
15
+ * into a single query key array.
16
+ *
17
+ * Used by createInfiniteQueryFactory and createSuspenseInfiniteQueryFactory to
18
+ * ensure each unique set of factory args produces a distinct cache entry.
19
+ */
20
+ export function mergeFactoryQueryKey(
21
+ baseQueryKey?: QueryKey,
22
+ callQueryKey?: QueryKey,
23
+ keyArgs?: unknown
24
+ ): QueryKey | undefined {
25
+ const merged: unknown[] = []
26
+
27
+ if (baseQueryKey) merged.push(...baseQueryKey)
28
+ if (callQueryKey) merged.push(...callQueryKey)
29
+ if (keyArgs !== undefined)
30
+ merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
31
+
32
+ return merged.length > 0 ? merged : undefined
33
+ }
34
+
35
+ /**
36
+ * Build a chained select function that first applies the config-level select
37
+ * (if any) and then the hook-level select (if any).
38
+ *
39
+ * This enables `createQuery` / `createSuspenseQuery` to support two-level
40
+ * select chaining without duplicating the logic.
41
+ */
42
+ export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
43
+ configSelect: ((data: TData) => TSelected) | undefined,
44
+ hookSelect: ((data: TSelected) => TFinal) | undefined
45
+ ): (rawData: TData) => TSelected | TFinal {
46
+ return (rawData: TData) => {
47
+ const firstPass = configSelect
48
+ ? configSelect(rawData)
49
+ : (rawData as unknown as TSelected)
50
+ return hookSelect ? hookSelect(firstPass) : firstPass
51
+ }
52
+ }