@ic-reactor/react 3.13.0 → 4.0.0-beta.2

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 (174) hide show
  1. package/README.md +237 -787
  2. package/dist/index.d.ts +260 -16
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +471 -23
  5. package/dist/index.js.map +1 -1
  6. package/llms.txt +82 -278
  7. package/package.json +11 -39
  8. package/src/index.tsx +612 -0
  9. package/dist/auth/auth-client-compat.d.ts +0 -122
  10. package/dist/auth/auth-client-compat.d.ts.map +0 -1
  11. package/dist/auth/auth-client-compat.js +0 -162
  12. package/dist/auth/auth-client-compat.js.map +0 -1
  13. package/dist/auth/authentication-manager.d.ts +0 -405
  14. package/dist/auth/authentication-manager.d.ts.map +0 -1
  15. package/dist/auth/authentication-manager.js +0 -1537
  16. package/dist/auth/authentication-manager.js.map +0 -1
  17. package/dist/auth/constants.d.ts +0 -24
  18. package/dist/auth/constants.d.ts.map +0 -1
  19. package/dist/auth/constants.js +0 -24
  20. package/dist/auth/constants.js.map +0 -1
  21. package/dist/auth/createIdentityAttributeHooks.d.ts +0 -14
  22. package/dist/auth/createIdentityAttributeHooks.d.ts.map +0 -1
  23. package/dist/auth/createIdentityAttributeHooks.js +0 -122
  24. package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
  25. package/dist/auth/identity-attributes-manager.d.ts +0 -27
  26. package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
  27. package/dist/auth/identity-attributes-manager.js +0 -191
  28. package/dist/auth/identity-attributes-manager.js.map +0 -1
  29. package/dist/auth/identity-attributes.d.ts +0 -19
  30. package/dist/auth/identity-attributes.d.ts.map +0 -1
  31. package/dist/auth/identity-attributes.js +0 -227
  32. package/dist/auth/identity-attributes.js.map +0 -1
  33. package/dist/auth/index.d.ts +0 -8
  34. package/dist/auth/index.d.ts.map +0 -1
  35. package/dist/auth/index.js +0 -8
  36. package/dist/auth/index.js.map +0 -1
  37. package/dist/auth/local-ii-probe.d.ts +0 -57
  38. package/dist/auth/local-ii-probe.d.ts.map +0 -1
  39. package/dist/auth/local-ii-probe.js +0 -121
  40. package/dist/auth/local-ii-probe.js.map +0 -1
  41. package/dist/auth/types.d.ts +0 -222
  42. package/dist/auth/types.d.ts.map +0 -1
  43. package/dist/auth/types.js +0 -2
  44. package/dist/auth/types.js.map +0 -1
  45. package/dist/createActorHooks.d.ts +0 -41
  46. package/dist/createActorHooks.d.ts.map +0 -1
  47. package/dist/createActorHooks.js +0 -17
  48. package/dist/createActorHooks.js.map +0 -1
  49. package/dist/createInfiniteQuery.d.ts +0 -185
  50. package/dist/createInfiniteQuery.d.ts.map +0 -1
  51. package/dist/createInfiniteQuery.js +0 -198
  52. package/dist/createInfiniteQuery.js.map +0 -1
  53. package/dist/createMutation.d.ts +0 -33
  54. package/dist/createMutation.d.ts.map +0 -1
  55. package/dist/createMutation.js +0 -199
  56. package/dist/createMutation.js.map +0 -1
  57. package/dist/createQuery.d.ts +0 -63
  58. package/dist/createQuery.d.ts.map +0 -1
  59. package/dist/createQuery.js +0 -204
  60. package/dist/createQuery.js.map +0 -1
  61. package/dist/createReactorProvider.d.ts +0 -158
  62. package/dist/createReactorProvider.d.ts.map +0 -1
  63. package/dist/createReactorProvider.js +0 -256
  64. package/dist/createReactorProvider.js.map +0 -1
  65. package/dist/createSuspenseInfiniteQuery.d.ts +0 -154
  66. package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
  67. package/dist/createSuspenseInfiniteQuery.js +0 -209
  68. package/dist/createSuspenseInfiniteQuery.js.map +0 -1
  69. package/dist/createSuspenseQuery.d.ts +0 -46
  70. package/dist/createSuspenseQuery.d.ts.map +0 -1
  71. package/dist/createSuspenseQuery.js +0 -158
  72. package/dist/createSuspenseQuery.js.map +0 -1
  73. package/dist/defineDisplayReactor.d.ts +0 -43
  74. package/dist/defineDisplayReactor.d.ts.map +0 -1
  75. package/dist/defineDisplayReactor.js +0 -42
  76. package/dist/defineDisplayReactor.js.map +0 -1
  77. package/dist/defineReactor.d.ts +0 -99
  78. package/dist/defineReactor.d.ts.map +0 -1
  79. package/dist/defineReactor.js +0 -15
  80. package/dist/defineReactor.js.map +0 -1
  81. package/dist/defineReactorShared.d.ts +0 -84
  82. package/dist/defineReactorShared.d.ts.map +0 -1
  83. package/dist/defineReactorShared.js +0 -139
  84. package/dist/defineReactorShared.js.map +0 -1
  85. package/dist/hooks/createAuthHooks.d.ts +0 -50
  86. package/dist/hooks/createAuthHooks.d.ts.map +0 -1
  87. package/dist/hooks/createAuthHooks.js +0 -291
  88. package/dist/hooks/createAuthHooks.js.map +0 -1
  89. package/dist/hooks/index.d.ts +0 -21
  90. package/dist/hooks/index.d.ts.map +0 -1
  91. package/dist/hooks/index.js +0 -24
  92. package/dist/hooks/index.js.map +0 -1
  93. package/dist/hooks/useActorInfiniteQuery.d.ts +0 -67
  94. package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
  95. package/dist/hooks/useActorInfiniteQuery.js +0 -89
  96. package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
  97. package/dist/hooks/useActorMethod.d.ts +0 -148
  98. package/dist/hooks/useActorMethod.d.ts.map +0 -1
  99. package/dist/hooks/useActorMethod.js +0 -394
  100. package/dist/hooks/useActorMethod.js.map +0 -1
  101. package/dist/hooks/useActorMutation.d.ts +0 -51
  102. package/dist/hooks/useActorMutation.d.ts.map +0 -1
  103. package/dist/hooks/useActorMutation.js +0 -70
  104. package/dist/hooks/useActorMutation.js.map +0 -1
  105. package/dist/hooks/useActorQuery.d.ts +0 -45
  106. package/dist/hooks/useActorQuery.d.ts.map +0 -1
  107. package/dist/hooks/useActorQuery.js +0 -67
  108. package/dist/hooks/useActorQuery.js.map +0 -1
  109. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -51
  110. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
  111. package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -76
  112. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +0 -1
  113. package/dist/hooks/useActorSuspenseQuery.d.ts +0 -32
  114. package/dist/hooks/useActorSuspenseQuery.d.ts.map +0 -1
  115. package/dist/hooks/useActorSuspenseQuery.js +0 -58
  116. package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
  117. package/dist/ownedAuthentication.d.ts +0 -52
  118. package/dist/ownedAuthentication.d.ts.map +0 -1
  119. package/dist/ownedAuthentication.js +0 -49
  120. package/dist/ownedAuthentication.js.map +0 -1
  121. package/dist/server.d.ts +0 -21
  122. package/dist/server.d.ts.map +0 -1
  123. package/dist/server.js +0 -23
  124. package/dist/server.js.map +0 -1
  125. package/dist/testing.d.ts +0 -19
  126. package/dist/testing.d.ts.map +0 -1
  127. package/dist/testing.js +0 -19
  128. package/dist/testing.js.map +0 -1
  129. package/dist/types.d.ts +0 -671
  130. package/dist/types.d.ts.map +0 -1
  131. package/dist/types.js +0 -5
  132. package/dist/types.js.map +0 -1
  133. package/dist/utils.d.ts +0 -207
  134. package/dist/utils.d.ts.map +0 -1
  135. package/dist/utils.js +0 -405
  136. package/dist/utils.js.map +0 -1
  137. package/dist/validation.d.ts +0 -136
  138. package/dist/validation.d.ts.map +0 -1
  139. package/dist/validation.js +0 -144
  140. package/dist/validation.js.map +0 -1
  141. package/src/auth/auth-client-compat.ts +0 -273
  142. package/src/auth/authentication-manager.ts +0 -1682
  143. package/src/auth/constants.ts +0 -32
  144. package/src/auth/createIdentityAttributeHooks.ts +0 -169
  145. package/src/auth/identity-attributes-manager.ts +0 -226
  146. package/src/auth/identity-attributes.ts +0 -345
  147. package/src/auth/index.ts +0 -7
  148. package/src/auth/local-ii-probe.ts +0 -173
  149. package/src/auth/types.ts +0 -243
  150. package/src/createActorHooks.ts +0 -208
  151. package/src/createInfiniteQuery.ts +0 -670
  152. package/src/createMutation.ts +0 -324
  153. package/src/createQuery.ts +0 -369
  154. package/src/createReactorProvider.ts +0 -365
  155. package/src/createSuspenseInfiniteQuery.ts +0 -651
  156. package/src/createSuspenseQuery.ts +0 -304
  157. package/src/defineDisplayReactor.ts +0 -62
  158. package/src/defineReactor.ts +0 -142
  159. package/src/defineReactorShared.ts +0 -268
  160. package/src/hooks/createAuthHooks.ts +0 -371
  161. package/src/hooks/index.ts +0 -103
  162. package/src/hooks/useActorInfiniteQuery.ts +0 -278
  163. package/src/hooks/useActorMethod.ts +0 -710
  164. package/src/hooks/useActorMutation.ts +0 -205
  165. package/src/hooks/useActorQuery.ts +0 -157
  166. package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -248
  167. package/src/hooks/useActorSuspenseQuery.ts +0 -147
  168. package/src/index.ts +0 -31
  169. package/src/ownedAuthentication.ts +0 -81
  170. package/src/server.ts +0 -23
  171. package/src/testing.ts +0 -18
  172. package/src/types.ts +0 -948
  173. package/src/utils.ts +0 -505
  174. package/src/validation.ts +0 -226
package/src/utils.ts DELETED
@@ -1,505 +0,0 @@
1
- /**
2
- * Shared internal utilities for the query and mutation hooks and factories.
3
- */
4
-
5
- import { useEffect } from "react"
6
- import type { QueryClient, QueryKey } from "@tanstack/react-query"
7
- import type { CallConfig } from "@icp-sdk/core/agent"
8
- import type {
9
- ClientManager,
10
- FunctionName,
11
- Reactor,
12
- ReactorArgs,
13
- ReactorQueryData,
14
- TransformKey,
15
- } from "@ic-reactor/core"
16
- import { generateKey } from "@ic-reactor/core"
17
- import type {
18
- InvalidationTarget,
19
- OptimisticRollback,
20
- QueryCacheControls,
21
- QueryFactoryMethods,
22
- QueryKeySource,
23
- } from "./types.js"
24
-
25
- /**
26
- * Keep `queryClient` mounted while the calling component is.
27
- *
28
- * `QueryClient.mount()` is what subscribes a client to TanStack's focus and
29
- * online managers. Those subscriptions refetch stale queries on window focus
30
- * and on reconnect, resume a fetch that started offline or a retry that paused
31
- * while the tab was hidden, and resume mutations sent offline.
32
- * `QueryClientProvider` normally calls it, but every hook here binds to its
33
- * reactor's own client rather than the context one, and the setup guide calls
34
- * the provider optional. Without one, nothing mounted the client: a query that
35
- * started offline stayed `paused` after the connection came back, a mutation
36
- * sent offline stayed pending, and `refetchOnWindowFocus` and
37
- * `refetchOnReconnect` never fired.
38
- *
39
- * `mount()` and `unmount()` are reference-counted, so this composes with a
40
- * provider mounting the same client and with any number of hooks. It runs in
41
- * an effect, like the provider's, so a server render never subscribes.
42
- *
43
- * A reactor stand-in without a `queryClient` (a test double, say) makes the
44
- * TanStack hooks fall back to the context client, which its provider mounts,
45
- * so there is nothing to do for one.
46
- */
47
- export function useMountQueryClient(
48
- queryClient: QueryClient | undefined
49
- ): void {
50
- useEffect(() => {
51
- if (!queryClient) return
52
- queryClient.mount()
53
- return () => queryClient.unmount()
54
- }, [queryClient])
55
- }
56
-
57
- const isThenable = (value: unknown): value is PromiseLike<unknown> =>
58
- typeof (value as { then?: unknown } | null | undefined)?.then === "function"
59
-
60
- /**
61
- * Keep `queryClient` mounted while a suspense hook waits on the promise it
62
- * threw. Call it from a `catch` around the TanStack suspense hook, then
63
- * rethrow. The `try` only lets the hook see the promise: it is rethrown
64
- * untouched, so React discards the render as before and hook order is kept.
65
- *
66
- * A suspense hook throws its fetch promise before its component commits, so
67
- * the effect in {@link useMountQueryClient} cannot run until that promise
68
- * settles. A fetch that started offline, or whose retry is waiting for a hidden
69
- * tab to come back, settles only once a mounted client hears the connection or
70
- * the focus return. Without a provider nothing had mounted the client, so the
71
- * Suspense fallback stayed up after reconnecting.
72
- *
73
- * The mount taken here is released when the promise settles, whether or not
74
- * anything ever commits, so it stays balanced: each suspended render, a
75
- * StrictMode double render or a retry included, takes and releases its own
76
- * reference. Once the component commits, its effect holds the client like any
77
- * other hook's. A tree discarded while suspended lets go once its fetch
78
- * finishes, which it does on reconnect, as it would under a mounted provider.
79
- * A server render never subscribes.
80
- */
81
- export function mountWhileSuspended(
82
- queryClient: QueryClient | undefined,
83
- thrown: unknown
84
- ): void {
85
- if (!queryClient || typeof window === "undefined" || !isThenable(thrown)) {
86
- return
87
- }
88
- queryClient.mount()
89
- const release = () => queryClient.unmount()
90
- void thrown.then(release, release)
91
- }
92
-
93
- /**
94
- * Internal query-key segment used to distinguish per-call factory args
95
- * from the base query key. Not part of the public API.
96
- */
97
- export const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
98
-
99
- /**
100
- * Internal query-key segment that ends the key of a query waiting on
101
- * `skipToken`. Not part of the public API.
102
- */
103
- export const SKIPPED_QUERY_KEY = "__ic_reactor_skipped"
104
-
105
- /**
106
- * The key of a query whose args are `skipToken`: `prefix`, the method's key
107
- * that every key its args give extends, and then a segment no call's key has.
108
- *
109
- * It used to be the bare prefix, which is also the key of a query of the same
110
- * method made without args, as a method with no parameters is called. The
111
- * skipped query then showed that query's data though its own args were not
112
- * known. And TanStack Query refetches an entry with the options of whichever
113
- * of its observers rendered last, so an invalidation, including the sweep a
114
- * sign-in or sign-out runs, could find `skipToken` in place of the query
115
- * function: the refetch failed with "Missing queryFn" and the other query
116
- * kept the previous caller's answer.
117
- *
118
- * `kind` keeps a waiting list apart from a waiting query of the same method,
119
- * since TanStack Query does not share an entry between `useQuery` and
120
- * `useInfiniteQuery`.
121
- */
122
- export function skippedQueryKey(
123
- prefix: QueryKey,
124
- kind: "query" | "infinite"
125
- ): QueryKey {
126
- return [...prefix, { [SKIPPED_QUERY_KEY]: kind }]
127
- }
128
-
129
- /**
130
- * The call config an infinite query's function fetches with: the caller's,
131
- * aimed at the canister its query key names unless the caller named one.
132
- *
133
- * The key is built from the reactor's canister when the query is set up, but
134
- * the query function reached `callMethod`, which reads `reactor.canisterId`
135
- * again whenever it runs. After a `setCanisterId`, a retry or a refetch by an
136
- * observer that had not re-rendered then fetched the new canister's pages and
137
- * cached them under the old canister's key. `generateQueryKey` always roots a
138
- * key at the canister it resolved, so the key says where its pages come from.
139
- * `Reactor.getQueryOptions` pins its query function the same way.
140
- */
141
- export function callConfigForKey(
142
- queryKey: QueryKey,
143
- callConfig: CallConfig | undefined
144
- ): CallConfig | undefined {
145
- const keyedCanister = queryKey[0]
146
- if (callConfig?.canisterId || typeof keyedCanister !== "string") {
147
- return callConfig
148
- }
149
- return { ...callConfig, canisterId: keyedCanister }
150
- }
151
-
152
- /** Convert a direct reactor result into a value TanStack Query can cache. */
153
- export const normalizeQueryData = <T>(value: T): ReactorQueryData<T> =>
154
- (value === undefined ? null : value) as ReactorQueryData<T>
155
-
156
- /** Config options that decide how a query's function runs. */
157
- const FETCH_OPTION_KEYS = [
158
- "networkMode",
159
- "retry",
160
- "retryDelay",
161
- "meta",
162
- ] as const
163
-
164
- type FetchOptionKey = (typeof FETCH_OPTION_KEYS)[number]
165
-
166
- /**
167
- * The part of a factory config that `fetch()` and `prefetch()` pass on.
168
- *
169
- * A factory's hook hands its whole config to TanStack Query, but the
170
- * imperative path used to pass only the key and query function. So a config's
171
- * `networkMode: "always"` let the hook fetch while `fetch()` stayed paused
172
- * offline, its `retry` retried in the hook and not in a loader, and its `meta`
173
- * never reached the QueryCache callbacks for a `fetch()` failure. These four
174
- * say how the query function runs, so they now apply to both paths.
175
- *
176
- * Options that decide what the cache keeps (`gcTime`, `initialData`) or how an
177
- * observer renders (`select`, `placeholderData`, `enabled`, …) stay with the
178
- * hook. Unset options are left out rather than passed as `undefined`, which
179
- * would override the QueryClient's own defaults.
180
- */
181
- export function pickFetchOptions<Config extends object>(
182
- config: Config
183
- ): Partial<Pick<Config, Extract<keyof Config, FetchOptionKey>>> {
184
- const picked: Partial<Record<FetchOptionKey, unknown>> = {}
185
- for (const key of FETCH_OPTION_KEYS) {
186
- const value = (config as Partial<Record<FetchOptionKey, unknown>>)[key]
187
- if (value !== undefined) picked[key] = value
188
- }
189
- return picked as Partial<Pick<Config, Extract<keyof Config, FetchOptionKey>>>
190
- }
191
-
192
- /**
193
- * The `retry` entry of a query's options: the query's own `retry` when it sets
194
- * one, otherwise the reactor's default for its method, and no entry when
195
- * neither is set, so the QueryClient's defaults apply.
196
- *
197
- * The default is `Reactor.getQueryRetry`'s: `undefined` for a query method,
198
- * and for an update method a retry of only the failures that prove the
199
- * canister never ran the call, since each attempt runs the update again.
200
- * Spread this after the caller's options. An update method's default then also
201
- * replaces a `retry: undefined` spread in from them, which would otherwise
202
- * select TanStack Query's own three retries.
203
- */
204
- export function retryOption<TRetry, TDefault>(
205
- ownRetry: TRetry | undefined,
206
- defaultRetry: TDefault | undefined
207
- ): { retry?: TRetry | TDefault } {
208
- const retry = ownRetry ?? defaultRetry
209
- return retry === undefined ? {} : { retry }
210
- }
211
-
212
- /**
213
- * Merge a base query key, optional per-call query key, and optional key-args
214
- * into a single query key array.
215
- *
216
- * Used by createInfiniteQueryFactory and createSuspenseInfiniteQueryFactory to
217
- * ensure each unique set of factory args produces a distinct cache entry.
218
- */
219
- export function mergeFactoryQueryKey(
220
- baseQueryKey?: QueryKey,
221
- callQueryKey?: QueryKey,
222
- keyArgs?: unknown
223
- ): QueryKey | undefined {
224
- const merged: unknown[] = []
225
-
226
- if (baseQueryKey) merged.push(...baseQueryKey)
227
- if (callQueryKey) merged.push(...callQueryKey)
228
- if (keyArgs !== undefined) {
229
- // Serialize keyArgs through generateKey so BigInt values (and other
230
- // non-JSON-serializable Candid types such as Principal) are safely
231
- // converted to strings before React Query hashes the key.
232
- const safeKeyArgs = generateKey(
233
- Array.isArray(keyArgs) ? keyArgs : [keyArgs]
234
- )
235
- merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: safeKeyArgs })
236
- }
237
-
238
- return merged.length > 0 ? merged : undefined
239
- }
240
-
241
- /**
242
- * Build a chained select that applies the config-level select (if any) and then
243
- * the hook-level select (if any).
244
- *
245
- * Returns the caller's own function untouched when only one of the two is
246
- * present, and `undefined` when neither is — allocating a wrapper only for the
247
- * genuinely chained case. Identity matters here: `QueryObserver` memoizes a
248
- * select result on `options.select === previousSelectFn`, so a wrapper rebuilt
249
- * on every render defeats that check. The select then re-runs each render, and
250
- * for a select returning a non-plain value (a `Map`, a `Date`, a `Principal`)
251
- * `replaceEqualDeep` cannot structurally share the result either, so `data`
252
- * gets a fresh reference every render and a `useEffect([data])` that sets state
253
- * becomes an unbounded loop.
254
- *
255
- * Callers should still memoize the result, since the chained case allocates.
256
- */
257
- export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
258
- configSelect: ((data: TData) => TSelected) | undefined,
259
- hookSelect: ((data: TSelected) => TFinal) | undefined
260
- ): ((rawData: TData) => TSelected | TFinal) | undefined {
261
- if (!configSelect) {
262
- // `hookSelect` receives the raw data when there is no config-level select,
263
- // matching the previous behaviour.
264
- return hookSelect as unknown as
265
- ((rawData: TData) => TSelected | TFinal) | undefined
266
- }
267
- if (!hookSelect) return configSelect
268
- return (rawData: TData) => hookSelect(configSelect(rawData))
269
- }
270
-
271
- /**
272
- * How many memoized query objects an args-late factory keeps.
273
- *
274
- * The memo exists so `getBalance(sameArgs)` returns the same object twice; it
275
- * was unbounded, so an open-ended argument space — a per-principal balance in a
276
- * long-lived SPA, a search-as-you-type filter, a cursor-keyed list — grew it
277
- * forever (measured at ~2.9 kB per entry, 55.8 MB for 20k). A few hundred
278
- * covers any realistic working set while capping the worst case.
279
- */
280
- const FACTORY_CACHE_LIMIT = 256
281
-
282
- /**
283
- * Smallest useful LRU: a `Map` already iterates in insertion order, so
284
- * re-inserting on read is enough to track recency.
285
- *
286
- * Eviction is safe — it costs memoization, never correctness. A query object is
287
- * a closure over the reactor and config, so one rebuilt after eviction behaves
288
- * identically; only its reference identity differs.
289
- */
290
- export function createBoundedCache<V>(limit: number = FACTORY_CACHE_LIMIT) {
291
- const entries = new Map<string, V>()
292
-
293
- return {
294
- get(key: string): V | undefined {
295
- const value = entries.get(key)
296
- if (value === undefined) return undefined
297
- // Touch: move to the most-recent end.
298
- entries.delete(key)
299
- entries.set(key, value)
300
- return value
301
- },
302
- set(key: string, value: V): void {
303
- if (entries.has(key)) entries.delete(key)
304
- else if (entries.size >= limit) {
305
- const oldest = entries.keys().next().value
306
- if (oldest !== undefined) entries.delete(oldest)
307
- }
308
- entries.set(key, value)
309
- },
310
- get size(): number {
311
- return entries.size
312
- },
313
- }
314
- }
315
-
316
- const isQueryKeySource = (value: object): value is QueryKeySource =>
317
- typeof (value as Partial<QueryKeySource>).getQueryKey === "function"
318
-
319
- /**
320
- * Invalidate one `invalidateQueries` entry; see {@link invalidateTargets}.
321
- */
322
- function invalidateTarget<Service, Transform extends TransformKey>(
323
- reactor: Reactor<Service, Transform>,
324
- target: InvalidationTarget<Service, Transform> | null,
325
- canisterId: CallConfig["canisterId"]
326
- ): Promise<void> {
327
- // React Query reads `{ queryKey: undefined }` as "match everything", so an
328
- // absent entry would invalidate every query in the client, the app's
329
- // unrelated non-canister ones included. `null` only comes from untyped code
330
- // and would do the same.
331
- if (target == null) return Promise.resolve()
332
- if (Array.isArray(target)) {
333
- return reactor.queryClient.invalidateQueries({ queryKey: target })
334
- }
335
- if (isQueryKeySource(target)) {
336
- // A query object or factory invalidates on its own reactor's client, which
337
- // is not this reactor's when each reactor has a QueryClient of its own.
338
- return target.invalidate
339
- ? target.invalidate()
340
- : reactor.queryClient.invalidateQueries({
341
- queryKey: target.getQueryKey(),
342
- })
343
- }
344
- // A `{ functionName, args? }` descriptor. Its key is built now rather than
345
- // when the mutation was set up, so it follows a `setCanisterId`.
346
- const { functionName, args } = target as {
347
- functionName: FunctionName<Service>
348
- args?: ReactorArgs<Service, FunctionName<Service>, Transform>
349
- }
350
- // `args: []`, all that a method without parameters takes, names the same
351
- // queries as no args. Keyed as given, it adds an args segment that a query
352
- // made without args lacks, and would match none of those.
353
- const hasArgs = (args as readonly unknown[] | undefined)?.length
354
- return reactor.queryClient.invalidateQueries({
355
- // At the canister the mutation was sent to, whose queries it changed.
356
- // Only the canister is taken from its `callConfig`: an agent or effective
357
- // target segment would narrow the prefix to the queries sent the same
358
- // way, while the canister's state changed for every caller.
359
- queryKey: reactor.generateQueryKey(
360
- { functionName, args: hasArgs ? args : undefined },
361
- canisterId ? { canisterId } : undefined
362
- ),
363
- })
364
- }
365
-
366
- /**
367
- * Invalidate every entry of a mutation's `invalidateQueries` in parallel, on
368
- * behalf of `reactor`, the mutation's own. It resolves once every active
369
- * query they matched has refetched; a refetch that fails does not reject it,
370
- * so it cannot turn an update that already ran into a failure.
371
- *
372
- * An entry is a query key, a query object or query factory (anything with a
373
- * `getQueryKey()`), or a `{ functionName, args? }` descriptor, which is keyed
374
- * by `reactor.generateQueryKey` at the canister the mutation was sent to:
375
- * the one its `callConfig.canisterId` names, else the reactor's. `undefined`
376
- * entries are skipped.
377
- */
378
- export async function invalidateTargets<
379
- Service,
380
- Transform extends TransformKey,
381
- >(
382
- reactor: Reactor<Service, Transform>,
383
- targets: readonly InvalidationTarget<Service, Transform>[] | undefined,
384
- callConfig?: CallConfig
385
- ): Promise<void> {
386
- if (!targets || targets.length === 0) return
387
- await Promise.all(
388
- targets.map((target) =>
389
- invalidateTarget(reactor, target, callConfig?.canisterId)
390
- )
391
- )
392
- }
393
-
394
- /**
395
- * Give an args-late query factory function its {@link QueryFactoryMethods}.
396
- *
397
- * `getQueryKey` builds the prefix every query of the factory shares, and is
398
- * called each time so that it follows a `setCanisterId`.
399
- */
400
- export function withQueryFactoryMethods<Factory extends object>(
401
- factory: Factory,
402
- reactor: { readonly queryClient: QueryClient },
403
- getQueryKey: () => QueryKey
404
- ): Factory & QueryFactoryMethods {
405
- const methods: QueryFactoryMethods = {
406
- getQueryKey,
407
- invalidate: () =>
408
- reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() }),
409
- }
410
- return Object.assign(factory, methods)
411
- }
412
-
413
- /** The rollback of an optimistic update that wrote nothing. */
414
- const NOTHING_TO_ROLL_BACK: OptimisticRollback = { rollback: () => {} }
415
-
416
- /** What {@link queryCacheControls} reads from a reactor. */
417
- interface CacheOwner {
418
- readonly queryClient: QueryClient
419
- readonly clientManager: Pick<ClientManager, "identity">
420
- }
421
-
422
- /**
423
- * The principal whose answers the reactor's cache holds: the one installed on
424
- * the manager's agent, `undefined` before one is.
425
- *
426
- * Query keys carry no principal. `ClientManager.updateAgent` sweeps the cache
427
- * instead when another principal signs in, removing inactive entries and
428
- * refetching active ones, so a value read from the cache is this principal's
429
- * only while it stays installed.
430
- */
431
- const cachedPrincipal = (reactor: CacheOwner): string | undefined =>
432
- reactor.clientManager.identity?.getPrincipal().toText()
433
-
434
- /**
435
- * The {@link QueryCacheControls} of a query object: `cancel`, `reset` and
436
- * `optimisticUpdate` on its own entry of the reactor's QueryClient.
437
- *
438
- * They match the key exactly. A query object's key is also the prefix of
439
- * other entries (a query without args prefixes every args instance of its
440
- * method), and cancelling or resetting those would reach queries this object
441
- * does not own. `getQueryKey` is called each time, so the controls follow a
442
- * `setCanisterId`.
443
- */
444
- export function queryCacheControls<TQueryFnData>(
445
- reactor: CacheOwner,
446
- getQueryKey: () => QueryKey
447
- ): QueryCacheControls<TQueryFnData> {
448
- return {
449
- cancel: () =>
450
- reactor.queryClient.cancelQueries({
451
- queryKey: getQueryKey(),
452
- exact: true,
453
- }),
454
-
455
- reset: () =>
456
- reactor.queryClient.resetQueries({
457
- queryKey: getQueryKey(),
458
- exact: true,
459
- }),
460
-
461
- optimisticUpdate: async (updater) => {
462
- const { queryClient } = reactor
463
- const queryKey = getQueryKey()
464
- // Nothing cached means nothing on screen to update. Cancelling the
465
- // entry's first fetch would also leave it pending with no data.
466
- if (queryClient.getQueryData(queryKey) === undefined) {
467
- return NOTHING_TO_ROLL_BACK
468
- }
469
- const principal = cachedPrincipal(reactor)
470
- // A fetch in flight would otherwise land after the write below with the
471
- // canister's answer from before the mutation. Cancelling reverts the
472
- // entry to what it held before that fetch, so it is read afterwards.
473
- await queryClient.cancelQueries({ queryKey, exact: true })
474
- // A sign-in or sign-out while that ran left the previous principal's
475
- // value in the entry until the sweep's refetch lands. An update built
476
- // on it would show that value to the principal signed in now.
477
- if (cachedPrincipal(reactor) !== principal) return NOTHING_TO_ROLL_BACK
478
- const snapshot = queryClient.getQueryState<TQueryFnData>(queryKey)
479
- if (snapshot?.data === undefined) return NOTHING_TO_ROLL_BACK
480
- const { data: previous, dataUpdatedAt, isInvalidated } = snapshot
481
- queryClient.setQueryData<TQueryFnData>(queryKey, updater(previous))
482
- return {
483
- rollback: () => {
484
- // After a switch to another principal, `previous` is the previous
485
- // principal's value, which the sweep has removed or is refetching.
486
- // Written back, it would be served to the one signed in now.
487
- if (cachedPrincipal(reactor) !== principal) return
488
- // With its own timestamp: written back as new, a value from before
489
- // the mutation would look freshly fetched and skip the refetches
490
- // its age calls for.
491
- queryClient.setQueryData<TQueryFnData>(queryKey, previous, {
492
- updatedAt: dataUpdatedAt,
493
- })
494
- // The write clears the invalidated mark too, and the fetch the
495
- // update cancelled was often the refetch an invalidation started.
496
- // Marked again, the value reads as outdated as it was, and a
497
- // mounted query refetches it.
498
- if (isInvalidated) {
499
- void queryClient.invalidateQueries({ queryKey, exact: true })
500
- }
501
- },
502
- }
503
- },
504
- }
505
- }