@ic-reactor/react 3.8.0 → 3.10.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 (77) hide show
  1. package/README.md +84 -10
  2. package/dist/auth/authentication-manager.d.ts.map +1 -1
  3. package/dist/auth/authentication-manager.js +146 -21
  4. package/dist/auth/authentication-manager.js.map +1 -1
  5. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  6. package/dist/auth/createIdentityAttributeHooks.js +47 -5
  7. package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
  8. package/dist/createInfiniteQuery.d.ts +11 -9
  9. package/dist/createInfiniteQuery.d.ts.map +1 -1
  10. package/dist/createInfiniteQuery.js +20 -17
  11. package/dist/createInfiniteQuery.js.map +1 -1
  12. package/dist/createMutation.d.ts.map +1 -1
  13. package/dist/createMutation.js +37 -3
  14. package/dist/createMutation.js.map +1 -1
  15. package/dist/createQuery.d.ts.map +1 -1
  16. package/dist/createQuery.js +7 -3
  17. package/dist/createQuery.js.map +1 -1
  18. package/dist/createSuspenseInfiniteQuery.d.ts +10 -9
  19. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  20. package/dist/createSuspenseInfiniteQuery.js +18 -16
  21. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  22. package/dist/createSuspenseQuery.d.ts.map +1 -1
  23. package/dist/createSuspenseQuery.js +7 -3
  24. package/dist/createSuspenseQuery.js.map +1 -1
  25. package/dist/defineReactor.d.ts.map +1 -1
  26. package/dist/defineReactor.js +17 -2
  27. package/dist/defineReactor.js.map +1 -1
  28. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  29. package/dist/hooks/createAuthHooks.js +13 -0
  30. package/dist/hooks/createAuthHooks.js.map +1 -1
  31. package/dist/hooks/index.d.ts.map +1 -1
  32. package/dist/hooks/index.js +6 -0
  33. package/dist/hooks/index.js.map +1 -1
  34. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  35. package/dist/hooks/useActorInfiniteQuery.js +21 -2
  36. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  37. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  38. package/dist/hooks/useActorMethod.js +76 -33
  39. package/dist/hooks/useActorMethod.js.map +1 -1
  40. package/dist/hooks/useActorMutation.d.ts +8 -1
  41. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  42. package/dist/hooks/useActorMutation.js +23 -7
  43. package/dist/hooks/useActorMutation.js.map +1 -1
  44. package/dist/hooks/useActorQuery.d.ts.map +1 -1
  45. package/dist/hooks/useActorQuery.js +12 -1
  46. package/dist/hooks/useActorQuery.js.map +1 -1
  47. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  48. package/dist/hooks/useActorSuspenseInfiniteQuery.js +21 -2
  49. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  50. package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
  51. package/dist/hooks/useActorSuspenseQuery.js +12 -1
  52. package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
  53. package/dist/types.d.ts +8 -2
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/utils.d.ts +27 -5
  56. package/dist/utils.d.ts.map +1 -1
  57. package/dist/utils.js +65 -9
  58. package/dist/utils.js.map +1 -1
  59. package/package.json +8 -2
  60. package/src/auth/authentication-manager.ts +162 -21
  61. package/src/auth/createIdentityAttributeHooks.ts +54 -5
  62. package/src/createInfiniteQuery.ts +48 -34
  63. package/src/createMutation.ts +48 -2
  64. package/src/createQuery.ts +17 -10
  65. package/src/createSuspenseInfiniteQuery.ts +44 -32
  66. package/src/createSuspenseQuery.ts +17 -10
  67. package/src/defineReactor.ts +24 -2
  68. package/src/hooks/createAuthHooks.ts +18 -0
  69. package/src/hooks/index.ts +6 -0
  70. package/src/hooks/useActorInfiniteQuery.ts +30 -3
  71. package/src/hooks/useActorMethod.ts +91 -34
  72. package/src/hooks/useActorMutation.ts +35 -12
  73. package/src/hooks/useActorQuery.ts +11 -1
  74. package/src/hooks/useActorSuspenseInfiniteQuery.ts +30 -3
  75. package/src/hooks/useActorSuspenseQuery.ts +11 -1
  76. package/src/types.ts +8 -2
  77. package/src/utils.ts +66 -10
@@ -1,4 +1,4 @@
1
- import { useMemo, useCallback } from "react"
1
+ import { useCallback } from "react"
2
2
  import {
3
3
  useMutation,
4
4
  UseMutationOptions,
@@ -35,7 +35,14 @@ export interface UseActorMutationParameters<
35
35
  reactor: Reactor<Service, Transform>
36
36
  functionName: Method
37
37
  callConfig?: CallConfig
38
- invalidateQueries?: QueryKey[]
38
+ /**
39
+ * Queries to invalidate upon successful mutation.
40
+ *
41
+ * `undefined` entries are skipped, so the common
42
+ * `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
43
+ * is absent.
44
+ */
45
+ invalidateQueries?: (QueryKey | undefined)[]
39
46
  /**
40
47
  * Callback for canister-level business logic errors.
41
48
  * Called when the canister returns a Result { Err: E } variant.
@@ -126,10 +133,18 @@ export const useActorMutation = <
126
133
  >
127
134
  ) => {
128
135
  if (invalidateQueries) {
136
+ // Skip undefined entries. React Query reads `{ queryKey: undefined }`
137
+ // as "match everything", so a single undefined — which the natural
138
+ // `invalidateQueries: [maybeQuery?.getQueryKey()]` idiom produces
139
+ // whenever the optional query object is absent — would invalidate every
140
+ // query in the client, including an app's unrelated non-canister ones.
141
+ // `createMutation.invalidateAll` already filters the same way.
129
142
  await Promise.all(
130
- invalidateQueries.map((queryKey) =>
131
- reactor.queryClient.invalidateQueries({ queryKey })
132
- )
143
+ invalidateQueries
144
+ .filter((queryKey) => queryKey !== undefined)
145
+ .map((queryKey) =>
146
+ reactor.queryClient.invalidateQueries({ queryKey })
147
+ )
133
148
  )
134
149
  }
135
150
  await onSuccess?.(...params)
@@ -152,16 +167,24 @@ export const useActorMutation = <
152
167
  [onCanisterError, onError]
153
168
  )
154
169
 
155
- const mutationOptions = useMemo(
156
- () => ({
170
+ // Not memoized. The deps could only ever list the values destructured above,
171
+ // never the `options` rest bucket, so everything passed straight through —
172
+ // `onMutate`, `onSettled`, `retry`, `meta`, `gcTime` — was frozen at the first
173
+ // render: `useMutation` calls `observer.setOptions` from an effect keyed on
174
+ // the options identity, so a later render's closures never reached it. A
175
+ // component passing only `onSettled` would report the recipient selected at
176
+ // mount rather than at submit.
177
+ //
178
+ // `createMutation`'s own hook builds its options inline for the same reason.
179
+ // A fresh object per render costs nothing here: `setOptions` diffs the values
180
+ // rather than the reference.
181
+ return useMutation(
182
+ {
157
183
  ...options,
158
184
  mutationFn,
159
185
  onSuccess: handleSuccess,
160
186
  onError: handleError,
161
- }),
162
- // eslint-disable-next-line react-hooks/exhaustive-deps
163
- [mutationFn, handleSuccess, handleError]
187
+ },
188
+ reactor.queryClient
164
189
  )
165
-
166
- return useMutation(mutationOptions, reactor.queryClient)
167
190
  }
@@ -100,7 +100,17 @@ export const useActorQuery = <
100
100
  args,
101
101
  queryKey: defaultQueryKey,
102
102
  }),
103
- [reactor, callConfig, functionName, args, defaultQueryKey]
103
+ // `canisterId` is mutable reactor state that `setCanisterId` can change,
104
+ // while `reactor` itself stays the same object — so it has to be a
105
+ // dependency in its own right or the key stays pinned to the old canister.
106
+ [
107
+ reactor,
108
+ reactor.canisterId?.toString(),
109
+ callConfig,
110
+ functionName,
111
+ args,
112
+ defaultQueryKey,
113
+ ]
104
114
  )
105
115
 
106
116
  return useQuery(
@@ -16,7 +16,7 @@ import {
16
16
  ReactorReturnErr,
17
17
  } from "@ic-reactor/core"
18
18
  import { CallConfig } from "@icp-sdk/core/agent"
19
- import { normalizeQueryData } from "../utils.js"
19
+ import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
20
20
 
21
21
  /**
22
22
  * Parameters for useActorSuspenseInfiniteQuery hook.
@@ -129,8 +129,35 @@ export const useActorSuspenseInfiniteQuery = <
129
129
  // reactor/function identity. Using the custom key verbatim would cause cache
130
130
  // collisions if two different actors or methods share the same key string.
131
131
  const baseQueryKey = useMemo(
132
- () => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
133
- [queryKey, reactor, functionName, callConfig]
132
+ () =>
133
+ reactor.generateQueryKey(
134
+ {
135
+ functionName,
136
+ // Fold the call arguments into the key. They live in the `getArgs`
137
+ // closure rather than in the config, so without this two hooks on the
138
+ // same method with different arguments share one cache entry and
139
+ // serve each other's pages.
140
+ queryKey: mergeFactoryQueryKey(
141
+ queryKey,
142
+ undefined,
143
+ getArgs(options.initialPageParam)
144
+ ),
145
+ },
146
+ callConfig
147
+ ),
148
+ [
149
+ queryKey,
150
+ reactor,
151
+ // `canisterId` is mutable reactor state that `setCanisterId` can change,
152
+ // while `reactor` itself stays the same object — so it has to be a
153
+ // dependency in its own right or the key stays pinned to the old canister
154
+ // while the queryFn already calls the new one.
155
+ reactor.canisterId?.toString(),
156
+ functionName,
157
+ callConfig,
158
+ getArgs,
159
+ options.initialPageParam,
160
+ ]
134
161
  )
135
162
 
136
163
  // Memoize queryFn to prevent recreation on every render
@@ -104,7 +104,17 @@ export const useActorSuspenseQuery = <
104
104
  args,
105
105
  queryKey: defaultQueryKey,
106
106
  }),
107
- [reactor, callConfig, functionName, args, defaultQueryKey]
107
+ // `canisterId` is mutable reactor state that `setCanisterId` can change,
108
+ // while `reactor` itself stays the same object — so it has to be a
109
+ // dependency in its own right or the key stays pinned to the old canister.
110
+ [
111
+ reactor,
112
+ reactor.canisterId?.toString(),
113
+ callConfig,
114
+ functionName,
115
+ args,
116
+ defaultQueryKey,
117
+ ]
108
118
  )
109
119
 
110
120
  return useSuspenseQuery(
package/src/types.ts CHANGED
@@ -356,8 +356,14 @@ export interface MutationConfig<
356
356
  functionName: Method
357
357
  /** Call configuration for the actor method */
358
358
  callConfig?: CallConfig
359
- /** Queries to invalidate upon successful mutation */
360
- invalidateQueries?: QueryKey[]
359
+ /**
360
+ * Queries to invalidate upon successful mutation.
361
+ *
362
+ * `undefined` entries are skipped, so the common
363
+ * `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
364
+ * is absent.
365
+ */
366
+ invalidateQueries?: (QueryKey | undefined)[]
361
367
  /**
362
368
  * Callback for canister-level business logic errors.
363
369
  * Called when the canister returns a Result { Err: E } variant.
package/src/utils.ts CHANGED
@@ -46,20 +46,76 @@ export function mergeFactoryQueryKey(
46
46
  }
47
47
 
48
48
  /**
49
- * Build a chained select function that first applies the config-level select
50
- * (if any) and then the hook-level select (if any).
49
+ * Build a chained select that applies the config-level select (if any) and then
50
+ * the hook-level select (if any).
51
51
  *
52
- * This enables `createQuery` / `createSuspenseQuery` to support two-level
53
- * select chaining without duplicating the logic.
52
+ * Returns the caller's own function untouched when only one of the two is
53
+ * present, and `undefined` when neither is — allocating a wrapper only for the
54
+ * genuinely chained case. Identity matters here: `QueryObserver` memoizes a
55
+ * select result on `options.select === previousSelectFn`, so a wrapper rebuilt
56
+ * on every render defeats that check. The select then re-runs each render, and
57
+ * for a select returning a non-plain value (a `Map`, a `Date`, a `Principal`)
58
+ * `replaceEqualDeep` cannot structurally share the result either, so `data`
59
+ * gets a fresh reference every render and a `useEffect([data])` that sets state
60
+ * becomes an unbounded loop.
61
+ *
62
+ * Callers should still memoize the result, since the chained case allocates.
54
63
  */
55
64
  export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
56
65
  configSelect: ((data: TData) => TSelected) | undefined,
57
66
  hookSelect: ((data: TSelected) => TFinal) | undefined
58
- ): (rawData: TData) => TSelected | TFinal {
59
- return (rawData: TData) => {
60
- const firstPass = configSelect
61
- ? configSelect(rawData)
62
- : (rawData as unknown as TSelected)
63
- return hookSelect ? hookSelect(firstPass) : firstPass
67
+ ): ((rawData: TData) => TSelected | TFinal) | undefined {
68
+ if (!configSelect) {
69
+ // `hookSelect` receives the raw data when there is no config-level select,
70
+ // matching the previous behaviour.
71
+ return hookSelect as unknown as
72
+ ((rawData: TData) => TSelected | TFinal) | undefined
73
+ }
74
+ if (!hookSelect) return configSelect
75
+ return (rawData: TData) => hookSelect(configSelect(rawData))
76
+ }
77
+
78
+ /**
79
+ * How many memoized query objects an args-late factory keeps.
80
+ *
81
+ * The memo exists so `getBalance(sameArgs)` returns the same object twice; it
82
+ * was unbounded, so an open-ended argument space — a per-principal balance in a
83
+ * long-lived SPA, a search-as-you-type filter, a cursor-keyed list — grew it
84
+ * forever (measured at ~2.9 kB per entry, 55.8 MB for 20k). A few hundred
85
+ * covers any realistic working set while capping the worst case.
86
+ */
87
+ const FACTORY_CACHE_LIMIT = 256
88
+
89
+ /**
90
+ * Smallest useful LRU: a `Map` already iterates in insertion order, so
91
+ * re-inserting on read is enough to track recency.
92
+ *
93
+ * Eviction is safe — it costs memoization, never correctness. A query object is
94
+ * a closure over the reactor and config, so one rebuilt after eviction behaves
95
+ * identically; only its reference identity differs.
96
+ */
97
+ export function createBoundedCache<V>(limit: number = FACTORY_CACHE_LIMIT) {
98
+ const entries = new Map<string, V>()
99
+
100
+ return {
101
+ get(key: string): V | undefined {
102
+ const value = entries.get(key)
103
+ if (value === undefined) return undefined
104
+ // Touch: move to the most-recent end.
105
+ entries.delete(key)
106
+ entries.set(key, value)
107
+ return value
108
+ },
109
+ set(key: string, value: V): void {
110
+ if (entries.has(key)) entries.delete(key)
111
+ else if (entries.size >= limit) {
112
+ const oldest = entries.keys().next().value
113
+ if (oldest !== undefined) entries.delete(oldest)
114
+ }
115
+ entries.set(key, value)
116
+ },
117
+ get size(): number {
118
+ return entries.size
119
+ },
64
120
  }
65
121
  }