@ic-reactor/react 3.12.5 → 3.13.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 (152) hide show
  1. package/README.md +410 -44
  2. package/dist/auth/auth-client-compat.d.ts +122 -0
  3. package/dist/auth/auth-client-compat.d.ts.map +1 -0
  4. package/dist/auth/auth-client-compat.js +162 -0
  5. package/dist/auth/auth-client-compat.js.map +1 -0
  6. package/dist/auth/authentication-manager.d.ts +287 -5
  7. package/dist/auth/authentication-manager.d.ts.map +1 -1
  8. package/dist/auth/authentication-manager.js +920 -150
  9. package/dist/auth/authentication-manager.js.map +1 -1
  10. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  11. package/dist/auth/createIdentityAttributeHooks.js +36 -20
  12. package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
  13. package/dist/auth/identity-attributes-manager.d.ts +2 -1
  14. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  15. package/dist/auth/identity-attributes-manager.js +90 -6
  16. package/dist/auth/identity-attributes-manager.js.map +1 -1
  17. package/dist/auth/identity-attributes.d.ts.map +1 -1
  18. package/dist/auth/identity-attributes.js +57 -0
  19. package/dist/auth/identity-attributes.js.map +1 -1
  20. package/dist/auth/local-ii-probe.d.ts +12 -1
  21. package/dist/auth/local-ii-probe.d.ts.map +1 -1
  22. package/dist/auth/local-ii-probe.js +22 -3
  23. package/dist/auth/local-ii-probe.js.map +1 -1
  24. package/dist/auth/types.d.ts +48 -5
  25. package/dist/auth/types.d.ts.map +1 -1
  26. package/dist/createActorHooks.d.ts +7 -18
  27. package/dist/createActorHooks.d.ts.map +1 -1
  28. package/dist/createActorHooks.js.map +1 -1
  29. package/dist/createInfiniteQuery.d.ts +51 -10
  30. package/dist/createInfiniteQuery.d.ts.map +1 -1
  31. package/dist/createInfiniteQuery.js +39 -15
  32. package/dist/createInfiniteQuery.js.map +1 -1
  33. package/dist/createMutation.d.ts +3 -0
  34. package/dist/createMutation.d.ts.map +1 -1
  35. package/dist/createMutation.js +87 -80
  36. package/dist/createMutation.js.map +1 -1
  37. package/dist/createQuery.d.ts +35 -2
  38. package/dist/createQuery.d.ts.map +1 -1
  39. package/dist/createQuery.js +104 -17
  40. package/dist/createQuery.js.map +1 -1
  41. package/dist/createReactorProvider.d.ts +158 -0
  42. package/dist/createReactorProvider.d.ts.map +1 -0
  43. package/dist/createReactorProvider.js +256 -0
  44. package/dist/createReactorProvider.js.map +1 -0
  45. package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
  46. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  47. package/dist/createSuspenseInfiniteQuery.js +59 -27
  48. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  49. package/dist/createSuspenseQuery.d.ts +23 -2
  50. package/dist/createSuspenseQuery.d.ts.map +1 -1
  51. package/dist/createSuspenseQuery.js +68 -21
  52. package/dist/createSuspenseQuery.js.map +1 -1
  53. package/dist/defineDisplayReactor.d.ts +43 -0
  54. package/dist/defineDisplayReactor.d.ts.map +1 -0
  55. package/dist/defineDisplayReactor.js +42 -0
  56. package/dist/defineDisplayReactor.js.map +1 -0
  57. package/dist/defineReactor.d.ts +46 -72
  58. package/dist/defineReactor.d.ts.map +1 -1
  59. package/dist/defineReactor.js +11 -176
  60. package/dist/defineReactor.js.map +1 -1
  61. package/dist/defineReactorShared.d.ts +84 -0
  62. package/dist/defineReactorShared.d.ts.map +1 -0
  63. package/dist/defineReactorShared.js +139 -0
  64. package/dist/defineReactorShared.js.map +1 -0
  65. package/dist/hooks/createAuthHooks.d.ts +7 -0
  66. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  67. package/dist/hooks/createAuthHooks.js +180 -20
  68. package/dist/hooks/createAuthHooks.js.map +1 -1
  69. package/dist/hooks/useActorInfiniteQuery.d.ts +34 -6
  70. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  71. package/dist/hooks/useActorInfiniteQuery.js +54 -21
  72. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  73. package/dist/hooks/useActorMethod.d.ts +37 -4
  74. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  75. package/dist/hooks/useActorMethod.js +201 -57
  76. package/dist/hooks/useActorMethod.js.map +1 -1
  77. package/dist/hooks/useActorMutation.d.ts +10 -7
  78. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  79. package/dist/hooks/useActorMutation.js +14 -13
  80. package/dist/hooks/useActorMutation.js.map +1 -1
  81. package/dist/hooks/useActorQuery.d.ts +17 -4
  82. package/dist/hooks/useActorQuery.d.ts.map +1 -1
  83. package/dist/hooks/useActorQuery.js +30 -9
  84. package/dist/hooks/useActorQuery.js.map +1 -1
  85. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +15 -3
  86. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  87. package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
  88. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  89. package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
  90. package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
  91. package/dist/hooks/useActorSuspenseQuery.js +20 -9
  92. package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
  93. package/dist/index.d.ts +4 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +6 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/ownedAuthentication.d.ts +52 -0
  98. package/dist/ownedAuthentication.d.ts.map +1 -0
  99. package/dist/ownedAuthentication.js +49 -0
  100. package/dist/ownedAuthentication.js.map +1 -0
  101. package/dist/server.d.ts +21 -0
  102. package/dist/server.d.ts.map +1 -0
  103. package/dist/server.js +23 -0
  104. package/dist/server.js.map +1 -0
  105. package/dist/testing.d.ts +19 -0
  106. package/dist/testing.d.ts.map +1 -0
  107. package/dist/testing.js +19 -0
  108. package/dist/testing.js.map +1 -0
  109. package/dist/types.d.ts +416 -15
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +1 -1
  112. package/dist/utils.d.ts +159 -3
  113. package/dist/utils.d.ts.map +1 -1
  114. package/dist/utils.js +301 -1
  115. package/dist/utils.js.map +1 -1
  116. package/dist/validation.d.ts +12 -7
  117. package/dist/validation.d.ts.map +1 -1
  118. package/dist/validation.js +34 -15
  119. package/dist/validation.js.map +1 -1
  120. package/llms.txt +259 -33
  121. package/package.json +17 -5
  122. package/src/auth/auth-client-compat.ts +273 -0
  123. package/src/auth/authentication-manager.ts +918 -96
  124. package/src/auth/createIdentityAttributeHooks.ts +47 -21
  125. package/src/auth/identity-attributes-manager.ts +100 -5
  126. package/src/auth/identity-attributes.ts +75 -0
  127. package/src/auth/local-ii-probe.ts +29 -3
  128. package/src/auth/types.ts +49 -6
  129. package/src/createActorHooks.ts +20 -32
  130. package/src/createInfiniteQuery.ts +120 -28
  131. package/src/createMutation.ts +161 -178
  132. package/src/createQuery.ts +164 -32
  133. package/src/createReactorProvider.ts +365 -0
  134. package/src/createSuspenseInfiniteQuery.ts +93 -43
  135. package/src/createSuspenseQuery.ts +102 -32
  136. package/src/defineDisplayReactor.ts +62 -0
  137. package/src/defineReactor.ts +81 -263
  138. package/src/defineReactorShared.ts +268 -0
  139. package/src/hooks/createAuthHooks.ts +206 -24
  140. package/src/hooks/useActorInfiniteQuery.ts +122 -49
  141. package/src/hooks/useActorMethod.ts +295 -92
  142. package/src/hooks/useActorMutation.ts +23 -23
  143. package/src/hooks/useActorQuery.ts +43 -10
  144. package/src/hooks/useActorSuspenseInfiniteQuery.ts +93 -50
  145. package/src/hooks/useActorSuspenseQuery.ts +30 -15
  146. package/src/index.ts +8 -0
  147. package/src/ownedAuthentication.ts +81 -0
  148. package/src/server.ts +23 -0
  149. package/src/testing.ts +18 -0
  150. package/src/types.ts +463 -14
  151. package/src/utils.ts +387 -3
  152. package/src/validation.ts +43 -19
@@ -34,6 +34,8 @@ import { useMemo } from "react"
34
34
  import {
35
35
  QueryKey,
36
36
  useQuery,
37
+ skipToken,
38
+ type SkipToken,
37
39
  type UseQueryOptions,
38
40
  type Updater,
39
41
  } from "@tanstack/react-query"
@@ -41,12 +43,24 @@ import type {
41
43
  QueryFnData,
42
44
  QueryError,
43
45
  QueryConfig,
46
+ SkippableQueryConfig,
44
47
  UseQueryWithSelect,
45
48
  QueryResult,
46
49
  QueryFactoryConfig,
50
+ SkippableQueryFactoryFn,
51
+ SkippedQuery,
47
52
  NoInfer,
48
53
  } from "./types.js"
49
- import { buildChainedSelect, createBoundedCache } from "./utils.js"
54
+ import {
55
+ buildChainedSelect,
56
+ createBoundedCache,
57
+ pickFetchOptions,
58
+ queryCacheControls,
59
+ retryOption,
60
+ skippedQueryKey,
61
+ useMountQueryClient,
62
+ withQueryFactoryMethods,
63
+ } from "./utils.js"
50
64
 
51
65
  // ============================================================================
52
66
  // Internal Implementation
@@ -59,7 +73,9 @@ const createQueryImpl = <
59
73
  Selected = QueryFnData<Service, Method, Transform>,
60
74
  >(
61
75
  reactor: Reactor<Service, Transform>,
62
- config: QueryConfig<Service, Method, Transform, Selected>
76
+ // `args` is `skipToken` only for a factory's skipped query, which exposes
77
+ // nothing but `useQuery`; see createQueryFactory.
78
+ config: SkippableQueryConfig<Service, Method, Transform, Selected>
63
79
  ): QueryResult<
64
80
  QueryFnData<Service, Method, Transform>,
65
81
  Selected,
@@ -71,34 +87,80 @@ const createQueryImpl = <
71
87
  const {
72
88
  functionName,
73
89
  args,
90
+ callConfig,
74
91
  staleTime = 5 * 60 * 1000,
75
92
  select,
76
93
  queryKey: customQueryKey,
77
94
  ...rest
78
95
  } = config
79
96
 
80
- const params = { functionName, args, queryKey: customQueryKey }
97
+ const skipped = args === skipToken
98
+
99
+ // `callConfig` goes wherever the hooks send it: to the call and into the
100
+ // key, so a query of another canister or agent has an entry of its own.
101
+ const params = {
102
+ functionName,
103
+ args: skipped ? undefined : args,
104
+ queryKey: customQueryKey,
105
+ callConfig,
106
+ }
107
+
108
+ // What the hook observes: the call, or for a query still waiting for its
109
+ // args, an entry of its own under the method's key, with nothing to run;
110
+ // see skippedQueryKey.
111
+ const queryOptions = () =>
112
+ skipped
113
+ ? {
114
+ queryKey: skippedQueryKey(
115
+ reactor.generateQueryKey({ functionName }, callConfig),
116
+ "query"
117
+ ),
118
+ // Kept as the unique symbol, which an object literal widens.
119
+ queryFn: skipToken as SkipToken,
120
+ retry: undefined,
121
+ }
122
+ : reactor.getQueryOptions(params)
81
123
 
82
- const getQueryKey = (): QueryKey => reactor.generateQueryKey(params)
124
+ const getQueryKey = (): QueryKey =>
125
+ reactor.generateQueryKey(params, callConfig)
83
126
 
84
127
  // Apply config.select to raw data (shared by fetch, getCacheData, and the hook)
85
128
  const applySelect = (raw: TData): Selected =>
86
129
  select ? select(raw) : (raw as unknown as Selected)
87
130
 
131
+ // How the query function runs, shared with the hook; see pickFetchOptions.
132
+ const fetchOptions = pickFetchOptions(rest)
133
+
88
134
  /** Cache-first fetch for use in loaders / route preloading. */
89
135
  const fetch = async (): Promise<Selected> => {
90
- const result = await reactor.fetchQuery(params)
136
+ // Through the reactor rather than straight to the QueryClient: overriding
137
+ // `fetchQuery` in a Reactor subclass is a documented way to add logic to
138
+ // every factory fetch.
139
+ const result = await reactor.fetchQuery(params, fetchOptions)
91
140
  return applySelect(result)
92
141
  }
93
142
 
94
143
  /** Fire-and-forget prefetch — warms the cache without blocking. */
95
144
  const prefetch = (): Promise<void> => {
96
145
  const baseOptions = reactor.getQueryOptions(params)
97
- return reactor.queryClient.prefetchQuery({
98
- queryKey: baseOptions.queryKey,
99
- queryFn: baseOptions.queryFn,
100
- staleTime,
101
- })
146
+ // A sign-in or sign-out cancels a prefetch in flight, and TanStack
147
+ // resolves it all the same: an entry nothing observes was left empty, and
148
+ // one a mounted query shows still held the previous principal's data
149
+ // until that query's refetch landed. It now runs again for the principal
150
+ // signed in, as `fetch()` does. Like `prefetchQuery` it never rejects, so
151
+ // the CallError for a principal that keeps switching is dropped too.
152
+ return reactor.clientManager
153
+ .fetchAcrossIdentitySwitch(() =>
154
+ reactor.queryClient.prefetchQuery({
155
+ ...fetchOptions,
156
+ // An update method's default `retry`; see `Reactor.getQueryRetry`.
157
+ ...retryOption(fetchOptions.retry, baseOptions.retry),
158
+ queryKey: baseOptions.queryKey,
159
+ queryFn: baseOptions.queryFn,
160
+ staleTime,
161
+ })
162
+ )
163
+ .catch(() => undefined)
102
164
  }
103
165
 
104
166
  // The hook publicly exposes the overloaded UseQueryWithSelect signature.
@@ -110,7 +172,8 @@ const createQueryImpl = <
110
172
  > & { select?: (data: Selected) => unknown }
111
173
 
112
174
  const useQueryHook = ((options?: UseQueryHookOptions) => {
113
- const baseOptions = reactor.getQueryOptions(params)
175
+ useMountQueryClient(reactor.queryClient)
176
+ const baseOptions = queryOptions()
114
177
  // Memoized so the observer's select-result cache can hit; see
115
178
  // buildChainedSelect. `select` comes from the factory config and is stable.
116
179
  const chainedSelect = useMemo(
@@ -125,6 +188,9 @@ const createQueryImpl = <
125
188
  ...options,
126
189
  queryFn: baseOptions.queryFn,
127
190
  select: chainedSelect,
191
+ // The hook's `retry`, else the config's, else an update method's
192
+ // default; see `Reactor.getQueryRetry`.
193
+ ...retryOption(options?.retry ?? rest.retry, baseOptions.retry),
128
194
  },
129
195
  reactor.queryClient
130
196
  )
@@ -137,7 +203,7 @@ const createQueryImpl = <
137
203
  const getCacheData = ((
138
204
  selectFn?: (data: Selected) => unknown
139
205
  ): Selected | unknown => {
140
- const raw = reactor.getQueryData(params)
206
+ const raw = reactor.getQueryData(params, callConfig)
141
207
  if (raw === undefined) return undefined
142
208
  const selected = applySelect(raw)
143
209
  return selectFn ? selectFn(selected) : selected
@@ -160,6 +226,7 @@ const createQueryImpl = <
160
226
  getQueryKey,
161
227
  getCacheData,
162
228
  setData,
229
+ ...queryCacheControls<TData>(reactor, getQueryKey),
163
230
  }
164
231
  }
165
232
 
@@ -190,6 +257,39 @@ export function createQuery<
190
257
  // Convenience: Create query with dynamic args
191
258
  // ============================================================================
192
259
 
260
+ /**
261
+ * Create a query factory: a function that takes the method's args and returns
262
+ * the query object for them, the same object for the same args.
263
+ *
264
+ * The function also has `getQueryKey()`, the key prefix every query it
265
+ * returns shares, and `invalidate()`, which invalidates all of them whatever
266
+ * their args. Pass the function itself to a mutation's `invalidateQueries` to
267
+ * refresh every instance after the mutation.
268
+ *
269
+ * It also takes TanStack Query's `skipToken` in place of args, for a
270
+ * component whose args are not known yet, and returns a query with only
271
+ * `useQuery()`, which waits without fetching; see
272
+ * {@link SkippableQueryFactoryFn}.
273
+ *
274
+ * @example
275
+ * const getBalance = createQueryFactory(ledger, {
276
+ * functionName: "icrc1_balance_of",
277
+ * })
278
+ *
279
+ * // In a component
280
+ * const { data } = getBalance([{ owner, subaccount: [] }]).useQuery()
281
+ *
282
+ * // In a component whose owner may not be known yet
283
+ * const { data: maybe } = getBalance(
284
+ * owner ? [{ owner, subaccount: [] }] : skipToken
285
+ * ).useQuery()
286
+ *
287
+ * // Refetch every account's balance after a transfer
288
+ * const transfer = createMutation(ledger, {
289
+ * functionName: "icrc1_transfer",
290
+ * invalidateQueries: [getBalance],
291
+ * })
292
+ */
193
293
  export function createQueryFactory<
194
294
  Service,
195
295
  Transform extends TransformKey,
@@ -198,27 +298,49 @@ export function createQueryFactory<
198
298
  >(
199
299
  reactor: Reactor<Service, Transform>,
200
300
  config: QueryFactoryConfig<NoInfer<Service>, Method, Transform, Selected>
201
- ): (
202
- args: ReactorArgs<Service, Method, Transform>
203
- ) => QueryResult<
204
- QueryFnData<Service, Method, Transform>,
205
- Selected,
206
- QueryError<Service, Method, Transform>
301
+ ): SkippableQueryFactoryFn<
302
+ ReactorArgs<Service, Method, Transform>,
303
+ QueryResult<
304
+ QueryFnData<Service, Method, Transform>,
305
+ Selected,
306
+ QueryError<Service, Method, Transform>
307
+ >
207
308
  > {
208
- const cache =
209
- createBoundedCache<
210
- QueryResult<
211
- QueryFnData<Service, Method, Transform>,
212
- Selected,
213
- QueryError<Service, Method, Transform>
214
- >
215
- >()
216
-
217
- return (args: ReactorArgs<Service, Method, Transform>) => {
218
- const key = reactor.generateQueryKey({
219
- functionName: config.functionName as Method,
220
- args,
221
- })
309
+ type Query = QueryResult<
310
+ QueryFnData<Service, Method, Transform>,
311
+ Selected,
312
+ QueryError<Service, Method, Transform>
313
+ >
314
+
315
+ const cache = createBoundedCache<Query>()
316
+
317
+ // One skipped query per factory, built on first use. Only its hook is
318
+ // handed out: until the args are known there is no call to make and no
319
+ // entry of their own to read or write.
320
+ let skippedQuery: SkippedQuery<Query> | undefined
321
+
322
+ function factory(args: ReactorArgs<Service, Method, Transform>): Query
323
+ function factory(args: SkipToken): SkippedQuery<Query>
324
+ function factory(
325
+ args: ReactorArgs<Service, Method, Transform> | SkipToken
326
+ ): Query | SkippedQuery<Query>
327
+ function factory(
328
+ args: ReactorArgs<Service, Method, Transform> | SkipToken
329
+ ): Query | SkippedQuery<Query> {
330
+ if (args === skipToken) {
331
+ skippedQuery ??= {
332
+ useQuery: createQueryImpl<Service, Method, Transform, Selected>(
333
+ reactor,
334
+ { ...config, args: skipToken }
335
+ ).useQuery,
336
+ }
337
+ return skippedQuery
338
+ }
339
+
340
+ const key = reactor.generateQueryKey(
341
+ { functionName: config.functionName as Method, args },
342
+ config.callConfig
343
+ )
222
344
  const cacheKey = JSON.stringify(key)
223
345
 
224
346
  const existing = cache.get(cacheKey)
@@ -234,4 +356,14 @@ export function createQueryFactory<
234
356
  cache.set(cacheKey, result)
235
357
  return result
236
358
  }
359
+
360
+ // The method's own prefix, at the canister and agent the config's
361
+ // `callConfig` names. A config `queryKey` follows the args segment in every
362
+ // instance's key, so it cannot narrow the prefix.
363
+ return withQueryFactoryMethods(factory, reactor, () =>
364
+ reactor.generateQueryKey(
365
+ { functionName: config.functionName as Method },
366
+ config.callConfig
367
+ )
368
+ )
237
369
  }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Reactor Provider Factory - builds an app's reactors once per mounted tree.
3
+ *
4
+ * A server renders every request through the same module graph, so a reactor
5
+ * built at module scope is one cache shared by every visitor: query keys carry
6
+ * no caller principal, and one request's caller-scoped result can be served to
7
+ * the next. The setup belongs inside the render tree instead, built in a
8
+ * `useState` initializer and handed down through context. Apps wrote that
9
+ * provider by hand (examples/nextjs had one of 93 lines) and then forwarded
10
+ * each hook out of the context with an `as any` cast, because a hook that is
11
+ * generic over the method name has no parameter tuple a typed wrapper could
12
+ * name.
13
+ *
14
+ * `createReactorProvider` is that provider. `useReactor()` returns what the
15
+ * factory built, typed as the factory's return, so every hook on it keeps its
16
+ * own generic signature and needs no forwarder.
17
+ */
18
+ import {
19
+ createContext,
20
+ createElement,
21
+ useContext,
22
+ useEffect,
23
+ useState,
24
+ } from "react"
25
+ import type { ReactElement, ReactNode } from "react"
26
+ import { QueryClientProvider } from "@tanstack/react-query"
27
+ import type { QueryClient } from "@tanstack/react-query"
28
+ import type { AuthenticationManager } from "./auth/authentication-manager.js"
29
+ import {
30
+ authenticationOf,
31
+ collectAuthentication,
32
+ } from "./ownedAuthentication.js"
33
+
34
+ /**
35
+ * Props of the provider {@link createReactorProvider} returns: `children`, and
36
+ * the props its factory takes.
37
+ */
38
+ export type ReactorProviderProps<TProps extends object = {}> = TProps & {
39
+ children?: ReactNode
40
+ }
41
+
42
+ /** Options for {@link createReactorProvider}. */
43
+ export interface CreateReactorProviderOptions {
44
+ /**
45
+ * Render a `QueryClientProvider` for the QueryClient the built value uses,
46
+ * so that `useQueryClient()`, React Query Devtools and `HydrationBoundary`
47
+ * below the provider read the cache the reactor hooks fill. The reactor
48
+ * hooks do not need it: each binds to its reactor's own QueryClient.
49
+ *
50
+ * Rendered only when the value holds exactly one QueryClient, found on the
51
+ * value and on its own properties: a `defineReactor` result, a reactor or a
52
+ * `ClientManager` brings its manager's, and a `QueryClient` counts as
53
+ * itself. Below the provider it takes the place of an outer
54
+ * `QueryClientProvider`, so a plain `useQuery` there caches in the
55
+ * reactors' QueryClient too. Pass `false` to leave that context to a
56
+ * provider of your own.
57
+ *
58
+ * @default true
59
+ */
60
+ queryClientProvider?: boolean
61
+ }
62
+
63
+ /** What {@link createReactorProvider} returns. */
64
+ export interface CreateReactorProviderReturn<
65
+ TValue extends object,
66
+ TProps extends object = {},
67
+ > {
68
+ /**
69
+ * Builds the value once per mount and provides it to `useReactor`.
70
+ *
71
+ * Its props, other than `children`, are passed to the factory when it
72
+ * mounts and are not read again. To build a new value, give the provider a
73
+ * new `key`: React then unmounts the old tree, which releases the old value,
74
+ * and mounts a new one.
75
+ */
76
+ ReactorProvider: (props: ReactorProviderProps<TProps>) => ReactElement
77
+ /**
78
+ * The value the nearest `ReactorProvider` built, or one of its properties
79
+ * when given that property's name. A hook: call it in a component or a
80
+ * custom hook below the provider. It throws when there is none.
81
+ */
82
+ useReactor: {
83
+ <TKey extends keyof TValue>(key: TKey): TValue[TKey]
84
+ // Last, so that `ReturnType<typeof useReactor>` is the whole value.
85
+ (): TValue
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Creates a provider that builds an app's reactors once per mounted tree, and
91
+ * a `useReactor()` hook that returns what it built, fully typed.
92
+ *
93
+ * This is the setup a server-rendered app needs. The factory runs in a
94
+ * `useState` initializer, once per mounted tree, and a server render is a tree
95
+ * of its own: each request builds its own `ClientManager`, `QueryClient`,
96
+ * reactors and `AuthenticationManager`, so no cache or identity is shared
97
+ * between visitors. The first render needs no effect, so the server's HTML is
98
+ * complete, and the browser builds a fresh value from the same props to
99
+ * hydrate it. In a client-only app it works the same, one value per mount.
100
+ *
101
+ * The factory returns anything: a `defineReactor` or `defineDisplayReactor`
102
+ * result, a record of them, reactors and managers built by hand, query and
103
+ * mutation objects from `createQuery` or `createMutation`. `useReactor()`
104
+ * returns it with the factory's own return type, so its hooks keep their
105
+ * generic signatures. `useReactor("todo")` returns one property of it.
106
+ *
107
+ * When the tree unmounts, the provider disposes each `AuthenticationManager`
108
+ * built for the value, releasing the Internet Identity client it built: those
109
+ * constructed while the factory ran, and the one a `defineReactor` result in
110
+ * the value, or among its own properties, builds on first use (a result that
111
+ * never used authentication builds none). A manager built elsewhere and
112
+ * handed in, such as an app-wide one passed as `authentication` or as a
113
+ * prop, is left to whoever built it. `dispose()` only forgets the client, and
114
+ * the next sign-in builds a new one, so this is safe under StrictMode, which
115
+ * runs the cleanup and then the effect again on the same value.
116
+ *
117
+ * It also renders a `QueryClientProvider` for the value's QueryClient; see
118
+ * {@link CreateReactorProviderOptions.queryClientProvider}.
119
+ *
120
+ * A suspense hook below the provider may suspend its first render: React
121
+ * renders the same element again when the data arrives, and the provider
122
+ * reuses the value that first render built, whether the Suspense boundary
123
+ * sits above the provider or, while hydrating, there is none. A provider that
124
+ * a transition mounts (`startTransition`, a client-side navigation) can be
125
+ * rendered from a new element on each retry and build a new value each time,
126
+ * so give its suspending components a `<Suspense>` boundary inside it.
127
+ *
128
+ * Call `createReactorProvider` at module scope: it builds nothing itself,
129
+ * only the context, and every mounted provider builds its own value. In the
130
+ * Next.js App Router that module needs `"use client"`, like any module with
131
+ * hooks; a server component renders the provider from there.
132
+ *
133
+ * @param factory - Builds the value. It receives the provider's props other
134
+ * than `children`, and runs once per mounted provider.
135
+ * @param options - See {@link CreateReactorProviderOptions}.
136
+ *
137
+ * @example One canister, with Internet Identity
138
+ * ```tsx
139
+ * "use client"
140
+ * import { createReactorProvider, defineReactor } from "@ic-reactor/react"
141
+ * import { canisterId, idlFactory, type _SERVICE } from "./declarations/todo"
142
+ *
143
+ * export const { ReactorProvider, useReactor } = createReactorProvider(() =>
144
+ * defineReactor<_SERVICE>({ name: "todo", idlFactory, canisterId })
145
+ * )
146
+ *
147
+ * function Todos() {
148
+ * const { useActorQuery, useAuth } = useReactor()
149
+ * const { isAuthenticated } = useAuth()
150
+ * const { data } = useActorQuery({ functionName: "getAllTodos" })
151
+ * return <p>{isAuthenticated ? data?.length : "Sign in"}</p>
152
+ * }
153
+ *
154
+ * // app/layout.tsx (a server component)
155
+ * // <ReactorProvider>{children}</ReactorProvider>
156
+ * ```
157
+ *
158
+ * @example Several canisters sharing one agent and one sign-in
159
+ * ```tsx
160
+ * export const { ReactorProvider, useReactor } = createReactorProvider(() => {
161
+ * const backend = defineReactor<Backend>({
162
+ * name: "backend",
163
+ * idlFactory: backendIdl,
164
+ * canisterId: backendId,
165
+ * })
166
+ * const ledger = defineDisplayReactor<Ledger>({
167
+ * name: "ledger",
168
+ * idlFactory: ledgerIdl,
169
+ * canisterId: ledgerId,
170
+ * authentication: backend.authentication,
171
+ * })
172
+ * return { backend, ledger }
173
+ * })
174
+ *
175
+ * function Balance() {
176
+ * const { data } = useReactor("ledger").useActorQuery({
177
+ * functionName: "icrc1_total_supply",
178
+ * })
179
+ * return <span>{data}</span>
180
+ * }
181
+ * ```
182
+ *
183
+ * @example Props for the factory, and a new key to build again
184
+ * ```tsx
185
+ * export const { ReactorProvider: LedgerProvider, useReactor: useLedger } =
186
+ * createReactorProvider(({ canisterId }: { canisterId: string }) =>
187
+ * defineDisplayReactor<Ledger>({ name: "ledger", idlFactory, canisterId })
188
+ * )
189
+ *
190
+ * // Read once per mount: the key makes a new canister a new tree.
191
+ * <LedgerProvider key={canisterId} canisterId={canisterId}>
192
+ * <TokenPage />
193
+ * </LedgerProvider>
194
+ * ```
195
+ */
196
+ export function createReactorProvider<
197
+ TValue extends object,
198
+ // `| undefined` so that a factory whose props are optional, as in
199
+ // `({ host = "..." }: { host?: string } = {})`, still types them.
200
+ TProps extends object | undefined = {},
201
+ >(
202
+ factory: (props: TProps) => TValue,
203
+ options: CreateReactorProviderOptions = {}
204
+ ): CreateReactorProviderReturn<TValue, NonNullable<TProps>> {
205
+ const { queryClientProvider = true } = options
206
+ const ReactorContext = createContext<TValue | null>(null)
207
+
208
+ // What a browser render built and has not committed yet, by the props
209
+ // object that render received. React throws away the state of a tree that
210
+ // suspends before it first commits, and renders it again, from the same
211
+ // element, once the promise settles. A value built afresh for that render
212
+ // came with an empty QueryClient, so a suspense query below, with no
213
+ // Suspense boundary between it and this provider, fetched again, suspended
214
+ // again and rebuilt the value in an endless loop of canister calls. The
215
+ // render that follows reuses the value instead, and so does StrictMode's
216
+ // second call of the initializer, which would otherwise build a value only
217
+ // to drop it. A server keeps nothing here: it never commits, and a
218
+ // module-scope element rendered for each request would hand one request's
219
+ // value to the next.
220
+ const uncommitted = new WeakMap<object, Built<TValue>>()
221
+
222
+ function ReactorProvider(
223
+ providerProps: ReactorProviderProps<NonNullable<TProps>>
224
+ ): ReactElement {
225
+ // Once per mounted tree. A server renders each request as a tree of its
226
+ // own, so each request builds its own value and nothing it caches is seen
227
+ // by another. The props are read here only; a new `key` builds again.
228
+ const [built] = useState(() => {
229
+ const pending = uncommitted.get(providerProps)
230
+ if (pending) return pending
231
+ const { children: _children, ...props } = providerProps
232
+ const { value, built: authentication } = collectAuthentication(() =>
233
+ factory(props as unknown as TProps)
234
+ )
235
+ const next: Built<TValue> = {
236
+ value,
237
+ authentication,
238
+ queryClient: queryClientProvider ? soleQueryClient(value) : undefined,
239
+ }
240
+ if (!isServer()) {
241
+ next.pendingProps = providerProps
242
+ uncommitted.set(providerProps, next)
243
+ }
244
+ return next
245
+ })
246
+
247
+ // Releases the Internet Identity clients of the managers built for this
248
+ // value once this tree unmounts. A v10 client listens to the page until
249
+ // it is disposed, so each remount would otherwise leave one behind.
250
+ // dispose() only forgets the client: StrictMode runs this cleanup and then
251
+ // the effect again on the same value, and the next sign-in builds a new
252
+ // one. A server runs no effects, and its managers build no client.
253
+ useEffect(() => {
254
+ // Committed: the value is this mount's, and a later mount of the same
255
+ // element builds its own.
256
+ if (built.pendingProps) {
257
+ uncommitted.delete(built.pendingProps)
258
+ built.pendingProps = undefined
259
+ }
260
+ return () => disposeAuthentication(built)
261
+ }, [built])
262
+
263
+ const provided = createElement(
264
+ ReactorContext.Provider,
265
+ { value: built.value },
266
+ providerProps.children
267
+ )
268
+ return built.queryClient
269
+ ? createElement(
270
+ QueryClientProvider,
271
+ { client: built.queryClient },
272
+ provided
273
+ )
274
+ : provided
275
+ }
276
+
277
+ function useReactor<TKey extends keyof TValue>(key: TKey): TValue[TKey]
278
+ function useReactor(): TValue
279
+ function useReactor(key?: keyof TValue) {
280
+ const value = useContext(ReactorContext)
281
+ if (value === null) {
282
+ throw new Error(
283
+ "[ic-reactor] useReactor() was called outside its <ReactorProvider>. " +
284
+ "Render the component below the ReactorProvider returned by the " +
285
+ "same createReactorProvider() call: each call has a context of its own."
286
+ )
287
+ }
288
+ return key === undefined ? value : value[key]
289
+ }
290
+
291
+ return { ReactorProvider, useReactor }
292
+ }
293
+
294
+ /** What a provider built, and until it commits, the props it built it from. */
295
+ interface Built<TValue> {
296
+ value: TValue
297
+ /** The managers constructed while the factory ran. */
298
+ authentication: AuthenticationManager[]
299
+ queryClient: QueryClient | undefined
300
+ pendingProps?: object
301
+ }
302
+
303
+ /**
304
+ * Whether this render runs on a server, as TanStack Query decides it: Deno
305
+ * defines `window` without being a browser.
306
+ */
307
+ const isServer = () => typeof window === "undefined" || "Deno" in globalThis
308
+
309
+ /**
310
+ * The value and each of its own data properties: where a built value keeps
311
+ * what it built. Accessors are skipped rather than read, because reading
312
+ * one can build what it returns: a `defineReactor` result's `authentication`
313
+ * builds the manager on first read.
314
+ */
315
+ function partsOf(value: object): object[] {
316
+ const parts = [value]
317
+ for (const descriptor of Object.values(
318
+ Object.getOwnPropertyDescriptors(value)
319
+ )) {
320
+ const part: unknown = descriptor.value
321
+ if (typeof part === "object" && part !== null) parts.push(part)
322
+ }
323
+ return parts
324
+ }
325
+
326
+ function isQueryClient(candidate: unknown): candidate is QueryClient {
327
+ const client = candidate as Partial<QueryClient> | null | undefined
328
+ return (
329
+ typeof client?.getQueryCache === "function" &&
330
+ typeof client.mount === "function"
331
+ )
332
+ }
333
+
334
+ /**
335
+ * The QueryClient the value uses, when it uses exactly one. A reactor, a
336
+ * `ClientManager` and a `defineReactor` result each bring theirs as
337
+ * `queryClient`.
338
+ */
339
+ function soleQueryClient(value: object): QueryClient | undefined {
340
+ const clients = new Set<QueryClient>()
341
+ for (const part of partsOf(value)) {
342
+ const client = isQueryClient(part)
343
+ ? part
344
+ : (part as { queryClient?: unknown }).queryClient
345
+ if (isQueryClient(client)) clients.add(client)
346
+ }
347
+ if (clients.size !== 1) return undefined
348
+ const [client] = clients
349
+ return client
350
+ }
351
+
352
+ /**
353
+ * Disposes each `AuthenticationManager` built for the value, once each: those
354
+ * constructed while the factory ran, and the one each `defineReactor` result
355
+ * in the value or among its own properties has built since. A manager built
356
+ * elsewhere and handed in is left to whoever built it.
357
+ */
358
+ function disposeAuthentication({ value, authentication }: Built<object>): void {
359
+ const managers = new Set(authentication)
360
+ for (const part of partsOf(value)) {
361
+ const owned = authenticationOf(part)
362
+ if (owned) managers.add(owned)
363
+ }
364
+ for (const manager of managers) manager.dispose()
365
+ }