orpc-fn 0.4.0 → 0.5.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.
- package/README.md +146 -4
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -1
- package/dist/create-fn.d.ts.map +1 -1
- package/dist/create-fn.js +0 -1
- package/dist/create-fn.js.map +1 -1
- package/dist/hono.d.ts +12 -1
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +15 -1
- package/dist/hono.js.map +1 -1
- package/dist/live/fn-live.d.ts +1 -6
- package/dist/live/fn-live.d.ts.map +1 -1
- package/dist/live/fn-live.js +2 -18
- package/dist/live/fn-live.js.map +1 -1
- package/dist/live/index.d.ts +3 -1
- package/dist/live/index.d.ts.map +1 -1
- package/dist/live/index.js +2 -0
- package/dist/live/index.js.map +1 -1
- package/dist/live/invalidation-stream.d.ts +52 -0
- package/dist/live/invalidation-stream.d.ts.map +1 -0
- package/dist/live/invalidation-stream.js +98 -0
- package/dist/live/invalidation-stream.js.map +1 -0
- package/dist/live/ioredis.d.ts +1 -0
- package/dist/live/ioredis.d.ts.map +1 -1
- package/dist/live/ioredis.js +1 -0
- package/dist/live/ioredis.js.map +1 -1
- package/dist/live/memory.d.ts +2 -1
- package/dist/live/memory.d.ts.map +1 -1
- package/dist/live/memory.js +6 -1
- package/dist/live/memory.js.map +1 -1
- package/dist/live/pub-sub.d.ts +5 -0
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +16 -3
- package/dist/live/pub-sub.js.map +1 -1
- package/dist/live/redis-bun.d.ts +3 -0
- package/dist/live/redis-bun.d.ts.map +1 -1
- package/dist/live/redis-bun.js +1 -0
- package/dist/live/redis-bun.js.map +1 -1
- package/dist/live/redis.d.ts +4 -1
- package/dist/live/redis.d.ts.map +1 -1
- package/dist/live/redis.js +14 -2
- package/dist/live/redis.js.map +1 -1
- package/dist/live/scoped-change-bus.d.ts +45 -0
- package/dist/live/scoped-change-bus.d.ts.map +1 -0
- package/dist/live/scoped-change-bus.js +38 -0
- package/dist/live/scoped-change-bus.js.map +1 -0
- package/dist/live/transport.d.ts +11 -0
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js.map +1 -1
- package/dist/procedure-cli.d.ts +25 -0
- package/dist/procedure-cli.d.ts.map +1 -0
- package/dist/procedure-cli.js +67 -0
- package/dist/procedure-cli.js.map +1 -0
- package/dist/react.d.ts +116 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +156 -0
- package/dist/react.js.map +1 -0
- package/dist/realtime.d.ts +114 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +316 -0
- package/dist/realtime.js.map +1 -0
- package/dist/vue/testing.d.ts +67 -0
- package/dist/vue/testing.d.ts.map +1 -0
- package/dist/vue/testing.js +225 -0
- package/dist/vue/testing.js.map +1 -0
- package/dist/vue.d.ts +211 -0
- package/dist/vue.d.ts.map +1 -0
- package/dist/vue.js +312 -0
- package/dist/vue.js.map +1 -0
- package/package.json +51 -3
- package/src/client.ts +57 -0
- package/src/create-fn.ts +0 -1
- package/src/hono.ts +22 -2
- package/src/live/fn-live.ts +3 -22
- package/src/live/index.ts +12 -1
- package/src/live/invalidation-stream.ts +142 -0
- package/src/live/ioredis.ts +2 -0
- package/src/live/memory.ts +11 -2
- package/src/live/pub-sub.ts +23 -2
- package/src/live/redis-bun.ts +3 -0
- package/src/live/redis.ts +33 -6
- package/src/live/scoped-change-bus.ts +98 -0
- package/src/live/transport.ts +12 -0
- package/src/procedure-cli.ts +99 -0
- package/src/react.ts +355 -0
- package/src/realtime.ts +416 -0
- package/src/vue/testing.ts +296 -0
- package/src/vue.ts +783 -0
package/src/react.ts
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {
|
|
2
|
+
focusManager,
|
|
3
|
+
hashKey,
|
|
4
|
+
keepPreviousData,
|
|
5
|
+
type QueryKey,
|
|
6
|
+
type UseQueryOptions,
|
|
7
|
+
type UseQueryResult,
|
|
8
|
+
useQuery,
|
|
9
|
+
useQueryClient
|
|
10
|
+
} from '@tanstack/react-query'
|
|
11
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
12
|
+
import {
|
|
13
|
+
createInvalidationCoordinator,
|
|
14
|
+
createStreamScope,
|
|
15
|
+
createSubscriptionRunner,
|
|
16
|
+
type InvalidationStreamOptions,
|
|
17
|
+
type LiveConnectionStatus,
|
|
18
|
+
type SubscriptionRunner,
|
|
19
|
+
seedQueryFn
|
|
20
|
+
} from './realtime.js'
|
|
21
|
+
|
|
22
|
+
export type { InvalidationDomain, LiveConnectionStatus } from './realtime.js'
|
|
23
|
+
|
|
24
|
+
/** An oRPC client procedure fits: `client.order.watch`. */
|
|
25
|
+
export type SubscribeFn<TInput, TEvent> = (
|
|
26
|
+
input: TInput,
|
|
27
|
+
options: { signal: AbortSignal }
|
|
28
|
+
) => Promise<AsyncIterable<TEvent>>
|
|
29
|
+
|
|
30
|
+
export type UseOrpcSubscriptionOptions<TInput, TEvent> = {
|
|
31
|
+
/** Run the stream (default: true). */
|
|
32
|
+
enabled?: boolean | undefined
|
|
33
|
+
onEvent?: (
|
|
34
|
+
event: TEvent,
|
|
35
|
+
meta: { connectionId: number; input: TInput }
|
|
36
|
+
) => void
|
|
37
|
+
/** Every successful connect; `connectionId > 1` is a reconnect. */
|
|
38
|
+
onConnect?: (connectionId: number) => void
|
|
39
|
+
onError?: ((error: Error) => void) | undefined
|
|
40
|
+
onClose?: () => void
|
|
41
|
+
/**
|
|
42
|
+
* Close the stream while the app is not focused (default: true). Focus is
|
|
43
|
+
* TanStack's `focusManager`: on React Native, wire it to `AppState` once.
|
|
44
|
+
*/
|
|
45
|
+
pauseWhenUnfocused?: boolean | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const subscribeFocus = (listener: () => void) =>
|
|
49
|
+
focusManager.subscribe(listener)
|
|
50
|
+
const getFocus = () => focusManager.isFocused()
|
|
51
|
+
|
|
52
|
+
/** Whether the app is focused, from TanStack's `focusManager`. */
|
|
53
|
+
function useIsFocused(): boolean {
|
|
54
|
+
return useSyncExternalStore(subscribeFocus, getFocus, getFocus)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* An oRPC event iterator with backoff reconnects that closes while the app is
|
|
59
|
+
* backgrounded. A changed input (compared as JSON) reconnects with a fresh
|
|
60
|
+
* retry budget.
|
|
61
|
+
*/
|
|
62
|
+
export function useOrpcSubscription<TInput, TEvent>(
|
|
63
|
+
input: TInput,
|
|
64
|
+
subscribe: SubscribeFn<TInput, TEvent>,
|
|
65
|
+
options: UseOrpcSubscriptionOptions<TInput, TEvent> = {}
|
|
66
|
+
): { status: LiveConnectionStatus } {
|
|
67
|
+
const { enabled = true, pauseWhenUnfocused = true } = options
|
|
68
|
+
const focused = useIsFocused()
|
|
69
|
+
const shouldRun = enabled && (!pauseWhenUnfocused || focused)
|
|
70
|
+
const [status, setStatus] = useState<LiveConnectionStatus>('idle')
|
|
71
|
+
|
|
72
|
+
const latest = useRef({ input, subscribe, options, shouldRun })
|
|
73
|
+
latest.current = { input, subscribe, options, shouldRun }
|
|
74
|
+
|
|
75
|
+
const runnerRef = useRef<SubscriptionRunner | null>(null)
|
|
76
|
+
if (!runnerRef.current) {
|
|
77
|
+
let openedInput = input
|
|
78
|
+
runnerRef.current = createSubscriptionRunner<TEvent>({
|
|
79
|
+
open: (signal) => {
|
|
80
|
+
openedInput = latest.current.input
|
|
81
|
+
return latest.current.subscribe(openedInput, { signal })
|
|
82
|
+
},
|
|
83
|
+
canConnect: () => latest.current.shouldRun,
|
|
84
|
+
shouldReconnect: () => latest.current.shouldRun,
|
|
85
|
+
onStatus: setStatus,
|
|
86
|
+
onEvent: (event, connectionId) =>
|
|
87
|
+
latest.current.options.onEvent?.(event, {
|
|
88
|
+
connectionId,
|
|
89
|
+
input: openedInput
|
|
90
|
+
}),
|
|
91
|
+
onConnect: (id) => latest.current.options.onConnect?.(id),
|
|
92
|
+
onError: (error) => latest.current.options.onError?.(error),
|
|
93
|
+
onClose: () => latest.current.options.onClose?.()
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
const runner = runnerRef.current
|
|
97
|
+
const serializedInput = JSON.stringify(input)
|
|
98
|
+
|
|
99
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: a changed input resubscribes
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!shouldRun) return
|
|
102
|
+
void runner.start()
|
|
103
|
+
return () => void runner.disconnect()
|
|
104
|
+
}, [runner, shouldRun, serializedInput])
|
|
105
|
+
|
|
106
|
+
return { status }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type ApplyEvent<TStreamEvent, TData> = (
|
|
110
|
+
event: TStreamEvent,
|
|
111
|
+
previous: TData | undefined
|
|
112
|
+
) => TData | undefined
|
|
113
|
+
|
|
114
|
+
type LiveQueryOptions<
|
|
115
|
+
TInput,
|
|
116
|
+
TRouteData,
|
|
117
|
+
TStreamEvent,
|
|
118
|
+
TData,
|
|
119
|
+
TSubscriptionInput
|
|
120
|
+
> = {
|
|
121
|
+
/** Run the query and its stream (default: true). */
|
|
122
|
+
enabled?: boolean | undefined
|
|
123
|
+
/** Input for the stream when it differs from the query's. */
|
|
124
|
+
subscriptionInput?: TSubscriptionInput | undefined
|
|
125
|
+
/**
|
|
126
|
+
* Convert the route output once at fetch time into a differently typed
|
|
127
|
+
* cache; `applyEvent` then reduces stream events into that cache.
|
|
128
|
+
*/
|
|
129
|
+
select?: (data: TRouteData) => TData
|
|
130
|
+
/** The raw route output of a seed, before `select` drops fields. */
|
|
131
|
+
onSeed?: (data: TRouteData, input: TInput) => void
|
|
132
|
+
/**
|
|
133
|
+
* Fold a stream event into the cached data. Required when the stream sends
|
|
134
|
+
* something other than the cached type (an `fnLive` patch route, or a
|
|
135
|
+
* `select`ed cache). Return `undefined` to skip the write.
|
|
136
|
+
*/
|
|
137
|
+
applyEvent?: ApplyEvent<TStreamEvent, NoInfer<TData>>
|
|
138
|
+
onEvent?: (event: TStreamEvent, meta: { connectionId: number }) => void
|
|
139
|
+
onError?: ((error: Error) => void) | undefined
|
|
140
|
+
/**
|
|
141
|
+
* Reconcile a fetched seed with what the stream already wrote. The HTTP
|
|
142
|
+
* request can resolve last, so a plain overwrite would drop events that
|
|
143
|
+
* arrived while it was in flight. Default: take the fetched value.
|
|
144
|
+
*/
|
|
145
|
+
mergeSeed?: (
|
|
146
|
+
fetched: NoInfer<TData>,
|
|
147
|
+
cached: NoInfer<TData> | undefined
|
|
148
|
+
) => NoInfer<TData>
|
|
149
|
+
/** Show the previous key's data while a new key loads (default: true). */
|
|
150
|
+
keepPreviousData?: boolean
|
|
151
|
+
/** Close the stream while the app is not focused (default: true). */
|
|
152
|
+
pauseWhenUnfocused?: boolean | undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type LiveQueryOptionsFn<TInput, TData, TError> = (
|
|
156
|
+
input: TInput
|
|
157
|
+
) => UseQueryOptions<TData, TError, TData, QueryKey>
|
|
158
|
+
|
|
159
|
+
type LiveQueryResult<TData, TError> = UseQueryResult<TData, TError> & {
|
|
160
|
+
liveStatus: LiveConnectionStatus
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The client half of `fnLive`: the HTTP query seeds the cache, and the live
|
|
165
|
+
* route's stream writes each snapshot into the same cache entry. Marked
|
|
166
|
+
* `meta.live`, so invalidation streams leave it alone.
|
|
167
|
+
*
|
|
168
|
+
* ```tsx
|
|
169
|
+
* const order = useLiveQuery({ id }, (input) => orpc.order.get.queryOptions({ input }), client.order.watch)
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* One signature, not overloads: TypeScript fixes an untyped callback's
|
|
173
|
+
* parameters at the first overload it tries, which made valid calls fail.
|
|
174
|
+
*/
|
|
175
|
+
export function useLiveQuery<
|
|
176
|
+
TInput,
|
|
177
|
+
TRouteData,
|
|
178
|
+
TStreamEvent,
|
|
179
|
+
TData = TRouteData,
|
|
180
|
+
TError = Error,
|
|
181
|
+
TSubscriptionInput = TInput
|
|
182
|
+
>(
|
|
183
|
+
input: TInput,
|
|
184
|
+
queryOptionsFn: LiveQueryOptionsFn<TInput, TRouteData, TError>,
|
|
185
|
+
subscribe: SubscribeFn<NoInfer<TSubscriptionInput>, TStreamEvent>,
|
|
186
|
+
...options: [TStreamEvent] extends [TData]
|
|
187
|
+
? [
|
|
188
|
+
options?: LiveQueryOptions<
|
|
189
|
+
TInput,
|
|
190
|
+
TRouteData,
|
|
191
|
+
TStreamEvent,
|
|
192
|
+
TData,
|
|
193
|
+
TSubscriptionInput
|
|
194
|
+
>
|
|
195
|
+
]
|
|
196
|
+
: [
|
|
197
|
+
options: LiveQueryOptions<
|
|
198
|
+
TInput,
|
|
199
|
+
TRouteData,
|
|
200
|
+
TStreamEvent,
|
|
201
|
+
TData,
|
|
202
|
+
TSubscriptionInput
|
|
203
|
+
> & { applyEvent: ApplyEvent<TStreamEvent, NoInfer<TData>> }
|
|
204
|
+
]
|
|
205
|
+
): LiveQueryResult<TData, TError>
|
|
206
|
+
export function useLiveQuery<
|
|
207
|
+
TInput,
|
|
208
|
+
TRouteData,
|
|
209
|
+
TStreamEvent,
|
|
210
|
+
TData,
|
|
211
|
+
TError,
|
|
212
|
+
TSubscriptionInput
|
|
213
|
+
>(
|
|
214
|
+
input: TInput,
|
|
215
|
+
queryOptionsFn: LiveQueryOptionsFn<TInput, TRouteData, TError>,
|
|
216
|
+
subscribe: SubscribeFn<TSubscriptionInput, TStreamEvent>,
|
|
217
|
+
options: LiveQueryOptions<
|
|
218
|
+
TInput,
|
|
219
|
+
TRouteData,
|
|
220
|
+
TStreamEvent,
|
|
221
|
+
TData,
|
|
222
|
+
TSubscriptionInput
|
|
223
|
+
> = {}
|
|
224
|
+
): LiveQueryResult<TData, TError> {
|
|
225
|
+
const queryClient = useQueryClient()
|
|
226
|
+
const focused = useIsFocused()
|
|
227
|
+
const [scope] = useState(createStreamScope)
|
|
228
|
+
|
|
229
|
+
// oRPC query options are plain objects; `queryFn` is read through one shape.
|
|
230
|
+
const raw = queryOptionsFn(input) as unknown as {
|
|
231
|
+
queryKey: QueryKey
|
|
232
|
+
queryFn?: (context: unknown) => Promise<TRouteData>
|
|
233
|
+
enabled?: UseQueryOptions['enabled']
|
|
234
|
+
meta?: Record<string, unknown>
|
|
235
|
+
} & Record<string, unknown>
|
|
236
|
+
const keyHash = hashKey(raw.queryKey)
|
|
237
|
+
const latest = useRef({ raw, keyHash, options })
|
|
238
|
+
latest.current = { raw, keyHash, options }
|
|
239
|
+
|
|
240
|
+
const { status } = useOrpcSubscription(
|
|
241
|
+
(options.subscriptionInput === undefined
|
|
242
|
+
? input
|
|
243
|
+
: options.subscriptionInput) as TSubscriptionInput,
|
|
244
|
+
subscribe,
|
|
245
|
+
{
|
|
246
|
+
enabled: options.enabled,
|
|
247
|
+
pauseWhenUnfocused: options.pauseWhenUnfocused,
|
|
248
|
+
onError: options.onError,
|
|
249
|
+
onConnect: (connectionId) =>
|
|
250
|
+
scope.open(connectionId, latest.current.keyHash),
|
|
251
|
+
onEvent: (event, { connectionId }) => {
|
|
252
|
+
const current = latest.current
|
|
253
|
+
// The consumer callback must not see an event for a scope no longer shown.
|
|
254
|
+
if (!scope.owns(connectionId, current.keyHash)) return
|
|
255
|
+
// Without `applyEvent` the stream event is the cache type (the first overload).
|
|
256
|
+
const applyEvent =
|
|
257
|
+
current.options.applyEvent ??
|
|
258
|
+
((streamEvent: TStreamEvent) => streamEvent as unknown as TData)
|
|
259
|
+
const key = current.raw.queryKey
|
|
260
|
+
const next = applyEvent(event, queryClient.getQueryData<TData>(key))
|
|
261
|
+
if (next !== undefined) queryClient.setQueryData(key, next)
|
|
262
|
+
current.options.onEvent?.(event, { connectionId })
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const wrapsSeed = options.select || options.mergeSeed
|
|
268
|
+
const queryFn =
|
|
269
|
+
raw.queryFn && wrapsSeed
|
|
270
|
+
? seedQueryFn(raw.queryFn, {
|
|
271
|
+
input,
|
|
272
|
+
readCache: () => queryClient.getQueryData<TData>(raw.queryKey),
|
|
273
|
+
select: options.select,
|
|
274
|
+
onSeed: options.onSeed,
|
|
275
|
+
mergeSeed: options.mergeSeed
|
|
276
|
+
})
|
|
277
|
+
: raw.queryFn
|
|
278
|
+
const runs = options.enabled !== false && focused
|
|
279
|
+
const query = useQuery({
|
|
280
|
+
...raw,
|
|
281
|
+
...(queryFn ? { queryFn } : {}),
|
|
282
|
+
enabled: runs ? (raw.enabled ?? true) : false,
|
|
283
|
+
...(options.keepPreviousData === false
|
|
284
|
+
? {}
|
|
285
|
+
: { placeholderData: keepPreviousData }),
|
|
286
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
287
|
+
meta: { ...raw.meta, live: true }
|
|
288
|
+
} as UseQueryOptions<TData, TError, TData, QueryKey>)
|
|
289
|
+
|
|
290
|
+
return Object.assign(query, { liveStatus: status })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* One app-wide stream of `{ domain }` change events that keeps list and count
|
|
295
|
+
* queries fresh: each event invalidates its domain's query keys (debounced),
|
|
296
|
+
* and a reconnect that proves healthy invalidates every domain to cover
|
|
297
|
+
* events lost while it was down. Mount once, in the signed-in shell.
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* useInvalidationStream({}, client.realtime.subscribeChanges, {
|
|
301
|
+
* enabled: signedIn,
|
|
302
|
+
* domains: { order: { debounceMs: 100, queryKeys: [orpc.order.key()] } }
|
|
303
|
+
* })
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export function useInvalidationStream<
|
|
307
|
+
TInput,
|
|
308
|
+
TEvent extends { domain: string }
|
|
309
|
+
>(
|
|
310
|
+
input: TInput,
|
|
311
|
+
subscribe: SubscribeFn<TInput, TEvent>,
|
|
312
|
+
options: InvalidationStreamOptions<TEvent> &
|
|
313
|
+
Pick<
|
|
314
|
+
UseOrpcSubscriptionOptions<TInput, TEvent>,
|
|
315
|
+
'enabled' | 'pauseWhenUnfocused'
|
|
316
|
+
>
|
|
317
|
+
): { status: LiveConnectionStatus } {
|
|
318
|
+
const queryClient = useQueryClient()
|
|
319
|
+
const latestOptions = useRef(options)
|
|
320
|
+
latestOptions.current = options
|
|
321
|
+
// `domains`, `refetchType` and `logger` are read on mount; `onEvent` stays current.
|
|
322
|
+
const [coordinator] = useState(() =>
|
|
323
|
+
createInvalidationCoordinator<TEvent>(queryClient, {
|
|
324
|
+
...options,
|
|
325
|
+
onEvent: (event) => latestOptions.current.onEvent?.(event)
|
|
326
|
+
})
|
|
327
|
+
)
|
|
328
|
+
useEffect(() => coordinator.dispose, [coordinator])
|
|
329
|
+
|
|
330
|
+
return useOrpcSubscription<TInput, TEvent>(input, subscribe, {
|
|
331
|
+
enabled: options.enabled,
|
|
332
|
+
pauseWhenUnfocused: options.pauseWhenUnfocused,
|
|
333
|
+
onEvent: coordinator.event,
|
|
334
|
+
onConnect: coordinator.connect,
|
|
335
|
+
onError: coordinator.lost,
|
|
336
|
+
onClose: coordinator.lost
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Clear the query cache when the signed-in owner of the cached data changes
|
|
342
|
+
* (another user, another organization), so the next one never sees it.
|
|
343
|
+
* `null` means signed out: signing in starts from the cache sign-out emptied.
|
|
344
|
+
* Pass the same owner as the persister's `buster` so a saved cache from
|
|
345
|
+
* someone else is discarded on the next launch too.
|
|
346
|
+
*/
|
|
347
|
+
export function useQueryCacheOwner(owner: string | null): void {
|
|
348
|
+
const queryClient = useQueryClient()
|
|
349
|
+
const previous = useRef(owner)
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
const before = previous.current
|
|
352
|
+
previous.current = owner
|
|
353
|
+
if (owner !== before && before !== null) queryClient.clear()
|
|
354
|
+
}, [owner, queryClient])
|
|
355
|
+
}
|