@effect-app/vue 4.0.0-beta.272 → 4.0.0-beta.273
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/CHANGELOG.md +9 -0
- package/dist/atomQuery.d.ts +90 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +275 -0
- package/dist/internal/tanstackQuery.d.ts +8 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +145 -0
- package/dist/makeClient.d.ts +75 -13
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +203 -78
- package/dist/mutate.d.ts +20 -12
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +65 -64
- package/dist/query.d.ts +114 -12
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +275 -179
- package/docs/atom-query-api-redesign.md +191 -0
- package/package.json +2 -2
- package/src/atomQuery.ts +361 -0
- package/src/internal/tanstackQuery.ts +221 -0
- package/src/makeClient.ts +382 -91
- package/src/mutate.ts +101 -110
- package/src/query.ts +564 -243
- package/test/dist/stubs.d.ts +169 -2
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +11 -6
- package/test/makeClient.test.ts +110 -0
- package/test/stubs.ts +10 -5
- package/tsconfig.json.bak +72 -14
package/src/query.ts
CHANGED
|
@@ -3,44 +3,137 @@
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
4
4
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
5
5
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
6
|
-
import {
|
|
6
|
+
import { injectRegistry, useAtomValue } from "@effect/atom-vue"
|
|
7
7
|
import * as Array from "effect-app/Array"
|
|
8
8
|
import { makeQueryKey, type Req } from "effect-app/client"
|
|
9
|
-
import type { RequestHandlerWithInput, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
|
|
10
|
-
import { CauseException
|
|
9
|
+
import type { ClientForOptions, RequestHandlerWithInput, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
|
|
10
|
+
import { type CauseException } from "effect-app/client/errors"
|
|
11
11
|
import type * as Context from "effect-app/Context"
|
|
12
12
|
import * as Effect from "effect-app/Effect"
|
|
13
13
|
import * as Option from "effect-app/Option"
|
|
14
|
-
import * as
|
|
15
|
-
import * as Cause from "effect/Cause"
|
|
16
|
-
import * as Channel from "effect/Channel"
|
|
14
|
+
import type * as Cause from "effect/Cause"
|
|
17
15
|
import * as Exit from "effect/Exit"
|
|
18
|
-
import * as
|
|
19
|
-
import * as Scope from "effect/Scope"
|
|
20
|
-
import type * as Stream from "effect/Stream"
|
|
21
|
-
import { type Span } from "effect/Tracer"
|
|
22
|
-
import { isHttpClientError } from "effect/unstable/http/HttpClientError"
|
|
16
|
+
import * as Stream from "effect/Stream"
|
|
23
17
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
18
|
+
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
19
|
+
import { computed, type ComputedRef, type MaybeRefOrGetter, onBeforeUnmount, onMounted, ref, toValue, type WatchSource } from "vue"
|
|
20
|
+
import { type AtomClientRuntime, type AtomQueryOptions, awaitAtomResult, buildQueryFamily, buildStreamQueryFamily, disabledQueryAtom, isStaleResult, staleTimeMsOf, withQueryOptions } from "./atomQuery.ts"
|
|
21
|
+
|
|
22
|
+
// --- minimal local types (replacing the former @tanstack/vue-query type imports) ---
|
|
23
|
+
type DefaultError = Error
|
|
24
|
+
type QueryKey = ReadonlyArray<unknown>
|
|
25
|
+
/** Options accepted by `refetch()` — kept for source compatibility; the atom path ignores them. */
|
|
26
|
+
export interface RefetchOptions {
|
|
27
|
+
readonly cancelRefetch?: boolean
|
|
28
|
+
readonly throwOnError?: boolean
|
|
29
|
+
}
|
|
30
|
+
/** The 4th tuple element: private atom/registry handle for client helpers. */
|
|
31
|
+
export interface QueryHandle<A = unknown, E = unknown> {
|
|
32
|
+
readonly awaitResult: () => Effect.Effect<A, E, never>
|
|
33
|
+
readonly refetch: () => Effect.Effect<A, E, never>
|
|
34
|
+
readonly refresh: () => void
|
|
35
|
+
readonly registry: ReturnType<typeof injectRegistry>
|
|
36
|
+
readonly atom: ComputedRef<Atom.Atom<AsyncResult.AsyncResult<A, E>>>
|
|
37
|
+
}
|
|
38
|
+
export interface QueryView<A, E> extends QueryHandle<A, E> {
|
|
39
|
+
readonly result: ComputedRef<AsyncResult.AsyncResult<A, E>>
|
|
40
|
+
readonly data: ComputedRef<A | undefined>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// retained generic aliases so the exported option-interface arity is unchanged for consumers
|
|
44
|
+
export type UseQueryReturnType<A = any, E = any> = QueryHandle<A, E>
|
|
45
|
+
export type UseQueryDefinedReturnType<A = any, E = any> = QueryHandle<A, E>
|
|
46
|
+
export type QueryObserverResult<A = any, _E = any> = AsyncResult.AsyncResult<A, any>
|
|
47
|
+
export type SuspenseQueryTuple<A, E> = readonly [
|
|
48
|
+
ComputedRef<AsyncResult.AsyncResult<A, E>>,
|
|
49
|
+
ComputedRef<A>,
|
|
50
|
+
(options?: RefetchOptions) => Effect.Effect<A, E, never>,
|
|
51
|
+
QueryHandle<A, E>
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
export type SuspenseQueryView<A, E> =
|
|
55
|
+
& Omit<QueryView<A, E>, "data">
|
|
56
|
+
& {
|
|
57
|
+
readonly data: ComputedRef<A>
|
|
58
|
+
}
|
|
59
|
+
& SuspenseQueryTuple<A, E>
|
|
60
|
+
|
|
61
|
+
export type QueryAtomFamily<I, A, E> = (input: I) => Atom.Atom<AsyncResult.AsyncResult<A, E>>
|
|
62
|
+
export type StreamQueryAtomFamily<I, A, E> = (input: I) => Atom.Writable<Atom.PullResult<A, E>, void>
|
|
63
|
+
|
|
64
|
+
interface QueryFamilyDescriptor<I, A, E> {
|
|
65
|
+
readonly id: string
|
|
66
|
+
readonly handler: (i: I) => Effect.Effect<A, E, any>
|
|
67
|
+
readonly options?: ClientForOptions
|
|
68
|
+
readonly queryKeyProjectionHash?: string
|
|
69
|
+
}
|
|
27
70
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
interface StreamQueryFamilyDescriptor<I, A, E> {
|
|
72
|
+
readonly id: string
|
|
73
|
+
readonly handler: (i: I) => Stream.Stream<A, E, any>
|
|
74
|
+
readonly options?: ClientForOptions
|
|
75
|
+
readonly queryKeyProjectionHash?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const queryFamilyCacheKey = (
|
|
79
|
+
q: { readonly id: string; readonly options?: ClientForOptions; readonly queryKeyProjectionHash?: string }
|
|
80
|
+
) => `${makeQueryKey(q).join("/")}:${q.queryKeyProjectionHash ?? ""}`
|
|
81
|
+
|
|
82
|
+
// One atom family per request shape, keyed by the stable query key + projection hash (not the
|
|
83
|
+
// handler object — `clientFor` returns a fresh proxy per call, so the object isn't shareable).
|
|
84
|
+
// Module-level + key-indexed => the family is process-global, so the same request+input read
|
|
85
|
+
// the same atom across components/pages => cross-page caching via the global registry.
|
|
86
|
+
const queryFamilyByKey = new Map<string, any>()
|
|
87
|
+
const getQueryFamily = <I, A, E>(
|
|
88
|
+
rt: AtomClientRuntime,
|
|
89
|
+
q: QueryFamilyDescriptor<I, A, E>
|
|
90
|
+
): QueryAtomFamily<I, A, E> => {
|
|
91
|
+
const key = queryFamilyCacheKey(q)
|
|
92
|
+
let f = queryFamilyByKey.get(key)
|
|
93
|
+
if (!f) {
|
|
94
|
+
f = buildQueryFamily(rt, q)
|
|
95
|
+
queryFamilyByKey.set(key, f)
|
|
96
|
+
}
|
|
97
|
+
return f
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const streamQueryFamilyByKey = new Map<string, any>()
|
|
101
|
+
const getStreamQueryFamily = <I, A, E>(
|
|
102
|
+
rt: AtomClientRuntime,
|
|
103
|
+
q: StreamQueryFamilyDescriptor<I, A, E>
|
|
104
|
+
): StreamQueryAtomFamily<I, A, E> => {
|
|
105
|
+
const key = queryFamilyCacheKey(q)
|
|
106
|
+
let f = streamQueryFamilyByKey.get(key)
|
|
107
|
+
if (!f) {
|
|
108
|
+
f = buildStreamQueryFamily(rt, q)
|
|
109
|
+
streamQueryFamilyByKey.set(key, f)
|
|
110
|
+
}
|
|
111
|
+
return f
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Atom-engine query options (formerly reconstructed from @tanstack/vue-query types).
|
|
115
|
+
// The generic arity is kept so the exported interface signatures are unchanged for consumers.
|
|
31
116
|
export interface CustomUseQueryOptions<
|
|
32
117
|
TQueryFnData = unknown,
|
|
33
118
|
TError = DefaultError,
|
|
34
119
|
TData = TQueryFnData,
|
|
35
120
|
TQueryData = TQueryFnData,
|
|
36
121
|
TQueryKey extends QueryKey = QueryKey
|
|
37
|
-
>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
122
|
+
> {
|
|
123
|
+
readonly enabled?: MaybeRefOrGetter<boolean | undefined>
|
|
124
|
+
/** stale threshold in ms (or a Duration input) */
|
|
125
|
+
readonly staleTime?: number
|
|
126
|
+
/** garbage-collect after idle, ms (or "infinity") */
|
|
127
|
+
readonly gcTime?: number | "infinity"
|
|
128
|
+
readonly refetchOnWindowFocus?: boolean
|
|
129
|
+
readonly structuralSharing?: boolean
|
|
130
|
+
/** poll: re-fetch every N ms (tanstack refetchInterval) */
|
|
131
|
+
readonly refetchInterval?: number
|
|
132
|
+
readonly select?: (data: TQueryFnData) => TData
|
|
133
|
+
/** accepted for source compatibility; not used by the atom engine */
|
|
134
|
+
readonly retry?: boolean | number
|
|
135
|
+
readonly meta?: Record<string, unknown>
|
|
136
|
+
readonly _phantom?: [TQueryData, TQueryKey, TError]
|
|
44
137
|
}
|
|
45
138
|
|
|
46
139
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -53,11 +146,8 @@ export interface CustomUndefinedInitialQueryOptions<
|
|
|
53
146
|
TQueryData = TQueryFnData,
|
|
54
147
|
TQueryKey extends QueryKey = QueryKey
|
|
55
148
|
> extends CustomUseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
|
|
56
|
-
initialData?:
|
|
57
|
-
placeholderData?:
|
|
58
|
-
| undefined
|
|
59
|
-
| NonFunctionGuard<TQueryData>
|
|
60
|
-
| PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
|
|
149
|
+
readonly initialData?: TQueryFnData | (() => TQueryFnData) | undefined
|
|
150
|
+
readonly placeholderData?: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData) | undefined
|
|
61
151
|
}
|
|
62
152
|
export interface CustomDefinedInitialQueryOptions<
|
|
63
153
|
TQueryFnData = unknown,
|
|
@@ -66,11 +156,8 @@ export interface CustomDefinedInitialQueryOptions<
|
|
|
66
156
|
TQueryData = TQueryFnData,
|
|
67
157
|
TQueryKey extends QueryKey = QueryKey
|
|
68
158
|
> extends CustomUseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
|
|
69
|
-
initialData:
|
|
70
|
-
placeholderData?:
|
|
71
|
-
| undefined
|
|
72
|
-
| NonFunctionGuard<TQueryData>
|
|
73
|
-
| PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
|
|
159
|
+
readonly initialData: TQueryFnData | (() => TQueryFnData)
|
|
160
|
+
readonly placeholderData?: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData) | undefined
|
|
74
161
|
}
|
|
75
162
|
|
|
76
163
|
export interface CustomDefinedPlaceholderQueryOptions<
|
|
@@ -80,71 +167,373 @@ export interface CustomDefinedPlaceholderQueryOptions<
|
|
|
80
167
|
TQueryData = TQueryFnData,
|
|
81
168
|
TQueryKey extends QueryKey = QueryKey
|
|
82
169
|
> extends CustomUseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
|
|
83
|
-
initialData?:
|
|
84
|
-
placeholderData:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
170
|
+
readonly initialData?: TQueryFnData | (() => TQueryFnData) | undefined
|
|
171
|
+
readonly placeholderData: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface AtomQueryNewOptions<TQueryFnData = unknown, TData = TQueryFnData> {
|
|
175
|
+
readonly enabled?: MaybeRefOrGetter<boolean | undefined>
|
|
176
|
+
readonly staleTime?: number
|
|
177
|
+
readonly idleTTL?: number | "infinity"
|
|
178
|
+
readonly gcTime?: number | "infinity"
|
|
179
|
+
readonly revalidateOnFocus?: boolean
|
|
180
|
+
readonly refetchOnWindowFocus?: boolean
|
|
181
|
+
readonly structuralSharing?: boolean
|
|
182
|
+
readonly refreshEvery?: number
|
|
183
|
+
readonly refetchInterval?: number
|
|
184
|
+
readonly select?: (data: TQueryFnData) => TData
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface AtomStreamQueryOptions {
|
|
188
|
+
readonly staleTime?: number
|
|
189
|
+
readonly idleTTL?: number | "infinity"
|
|
190
|
+
readonly gcTime?: number | "infinity"
|
|
191
|
+
readonly revalidateOnFocus?: boolean
|
|
192
|
+
readonly refetchOnWindowFocus?: boolean
|
|
193
|
+
readonly refreshEvery?: number
|
|
194
|
+
readonly refetchInterval?: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const normalizeQueryOptions = (options?: {
|
|
198
|
+
readonly staleTime?: number
|
|
199
|
+
readonly gcTime?: number | "infinity"
|
|
200
|
+
readonly idleTTL?: number | "infinity"
|
|
201
|
+
readonly refetchOnWindowFocus?: boolean
|
|
202
|
+
readonly revalidateOnFocus?: boolean
|
|
203
|
+
readonly structuralSharing?: boolean
|
|
204
|
+
readonly refetchInterval?: number
|
|
205
|
+
readonly refreshEvery?: number
|
|
206
|
+
}): AtomQueryOptions => {
|
|
207
|
+
const out: {
|
|
208
|
+
staleTime?: number
|
|
209
|
+
gcTime?: number | "infinity"
|
|
210
|
+
revalidateOnFocus?: boolean
|
|
211
|
+
structuralSharing?: boolean
|
|
212
|
+
refetchInterval?: number
|
|
213
|
+
} = {}
|
|
214
|
+
if (options?.staleTime !== undefined) out.staleTime = options.staleTime
|
|
215
|
+
const gcTime = options?.idleTTL ?? options?.gcTime
|
|
216
|
+
if (gcTime !== undefined) out.gcTime = gcTime
|
|
217
|
+
const revalidateOnFocus = options?.revalidateOnFocus ?? options?.refetchOnWindowFocus
|
|
218
|
+
if (revalidateOnFocus !== undefined) out.revalidateOnFocus = revalidateOnFocus
|
|
219
|
+
if (options?.structuralSharing !== undefined) out.structuralSharing = options.structuralSharing
|
|
220
|
+
const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
|
|
221
|
+
if (refetchInterval !== undefined) out.refetchInterval = refetchInterval
|
|
222
|
+
return out
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const useAtomQuery = <A, E>(
|
|
226
|
+
atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
|
|
227
|
+
): QueryView<A, E> => {
|
|
228
|
+
const registry = injectRegistry()
|
|
229
|
+
const atomRef = computed(atom)
|
|
230
|
+
const atomResult = useAtomValue(() => atomRef.value)
|
|
231
|
+
const result = computed(() => atomResult.value)
|
|
232
|
+
const refresh = () => registry.refresh(atomRef.value)
|
|
233
|
+
const awaitResult = () => awaitAtomResult(registry, atomRef.value)
|
|
234
|
+
const refetch = () =>
|
|
235
|
+
Effect.gen(function*() {
|
|
236
|
+
refresh()
|
|
237
|
+
return yield* awaitResult()
|
|
238
|
+
})
|
|
239
|
+
const data = computed(() => Option.getOrUndefined(AsyncResult.value(result.value)))
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
result,
|
|
243
|
+
data,
|
|
244
|
+
awaitResult,
|
|
245
|
+
refetch,
|
|
246
|
+
refresh,
|
|
247
|
+
registry,
|
|
248
|
+
atom: atomRef
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const useAtomSuspense = <A, E>(
|
|
253
|
+
atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
|
|
254
|
+
): Promise<SuspenseQueryView<A, E>> => {
|
|
255
|
+
const view = useAtomQuery(atom)
|
|
256
|
+
const data = computed<A>(() => {
|
|
257
|
+
const latest = view.data.value
|
|
258
|
+
if (latest === undefined) {
|
|
259
|
+
throw new Error("Internal Error: atom suspense resolved without a latest value")
|
|
260
|
+
}
|
|
261
|
+
return latest
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const isMounted = ref(true)
|
|
265
|
+
onBeforeUnmount(() => {
|
|
266
|
+
isMounted.value = false
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const eff = Effect.gen(function*() {
|
|
270
|
+
const exit = yield* view.awaitResult().pipe(Effect.exit)
|
|
271
|
+
if (!isMounted.value) {
|
|
272
|
+
return yield* Effect.interrupt
|
|
273
|
+
}
|
|
274
|
+
if (Exit.isFailure(exit)) {
|
|
275
|
+
return yield* Exit.failCause(exit.cause)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fetch = (_options?: RefetchOptions) => view.refetch()
|
|
279
|
+
const handle = {
|
|
280
|
+
awaitResult: view.awaitResult,
|
|
281
|
+
refetch: view.refetch,
|
|
282
|
+
refresh: view.refresh,
|
|
283
|
+
registry: view.registry,
|
|
284
|
+
atom: view.atom
|
|
285
|
+
}
|
|
286
|
+
return Object.assign(
|
|
287
|
+
[
|
|
288
|
+
view.result,
|
|
289
|
+
data,
|
|
290
|
+
fetch,
|
|
291
|
+
handle
|
|
292
|
+
] as const,
|
|
97
293
|
{
|
|
98
|
-
|
|
99
|
-
|
|
294
|
+
...view,
|
|
295
|
+
data
|
|
100
296
|
}
|
|
101
297
|
)
|
|
102
|
-
}
|
|
103
|
-
if (r.data !== undefined) {
|
|
104
|
-
return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
|
|
105
|
-
}
|
|
298
|
+
})
|
|
106
299
|
|
|
107
|
-
return
|
|
300
|
+
return Effect.runPromise(eff)
|
|
108
301
|
}
|
|
109
302
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
303
|
+
export type StreamQueryPullValue<A> = {
|
|
304
|
+
readonly done: boolean
|
|
305
|
+
readonly items: ReadonlyArray<A>
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface StreamQueryView<A, E> {
|
|
309
|
+
readonly result: ComputedRef<Atom.PullResult<A, E>>
|
|
310
|
+
readonly items: ComputedRef<ReadonlyArray<A>>
|
|
311
|
+
readonly latest: ComputedRef<A | undefined>
|
|
312
|
+
readonly done: ComputedRef<boolean>
|
|
313
|
+
readonly awaitResult: () => Effect.Effect<StreamQueryPullValue<A>, E | Cause.NoSuchElementError, never>
|
|
314
|
+
readonly pull: () => void
|
|
315
|
+
readonly pullAndAwait: () => Effect.Effect<StreamQueryPullValue<A>, E | Cause.NoSuchElementError, never>
|
|
316
|
+
readonly refresh: () => void
|
|
317
|
+
readonly registry: ReturnType<typeof injectRegistry>
|
|
318
|
+
readonly atom: ComputedRef<Atom.Writable<Atom.PullResult<A, E>, void>>
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export const useAtomStreamQuery = <A, E>(
|
|
322
|
+
atom: () => Atom.Writable<Atom.PullResult<A, E>, void>
|
|
323
|
+
): StreamQueryView<A, E> => {
|
|
324
|
+
const registry = injectRegistry()
|
|
325
|
+
const atomRef = computed(atom)
|
|
326
|
+
const atomResult = useAtomValue(() => atomRef.value)
|
|
327
|
+
const result = computed(() => atomResult.value)
|
|
328
|
+
const pull = () => registry.set(atomRef.value, void 0)
|
|
329
|
+
const refresh = () => registry.refresh(atomRef.value)
|
|
330
|
+
const awaitResult = () => awaitAtomResult(registry, atomRef.value)
|
|
331
|
+
const pullAndAwait = () =>
|
|
332
|
+
Effect.gen(function*() {
|
|
333
|
+
pull()
|
|
334
|
+
return yield* awaitResult()
|
|
335
|
+
})
|
|
336
|
+
const items = computed(() =>
|
|
337
|
+
Option.getOrElse(
|
|
338
|
+
Option.map(AsyncResult.value(result.value), (_) => _.items),
|
|
339
|
+
() => []
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
const latest = computed(() => items.value.at(-1))
|
|
343
|
+
const done = computed(() =>
|
|
344
|
+
Option.getOrElse(
|
|
345
|
+
Option.map(AsyncResult.value(result.value), (_) => _.done),
|
|
346
|
+
() => false
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
115
350
|
return {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
351
|
+
result,
|
|
352
|
+
items,
|
|
353
|
+
latest,
|
|
354
|
+
done,
|
|
355
|
+
awaitResult,
|
|
356
|
+
pull,
|
|
357
|
+
pullAndAwait,
|
|
358
|
+
refresh,
|
|
359
|
+
registry,
|
|
360
|
+
atom: atomRef
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const optionValue = <I>(
|
|
365
|
+
arr: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
366
|
+
options?: { readonly mode?: "optional"; readonly enabled?: MaybeRefOrGetter<boolean | undefined> }
|
|
367
|
+
): readonly [{ readonly value: I }, ComputedRef<boolean>] => {
|
|
368
|
+
if (options?.mode === "optional") {
|
|
369
|
+
const getOption: () => Option.Option<I> = typeof arr === "function"
|
|
370
|
+
? arr as () => Option.Option<I>
|
|
371
|
+
: () => (arr as { value: Option.Option<I> }).value
|
|
372
|
+
return [
|
|
373
|
+
{
|
|
374
|
+
get value() {
|
|
375
|
+
return Option.getOrUndefined(getOption()) as I
|
|
141
376
|
}
|
|
377
|
+
},
|
|
378
|
+
computed(() => Option.isSome(getOption()))
|
|
379
|
+
] as const
|
|
380
|
+
}
|
|
381
|
+
const req = !arr
|
|
382
|
+
? ({ value: undefined as I })
|
|
383
|
+
: typeof arr === "function"
|
|
384
|
+
? ({
|
|
385
|
+
get value() {
|
|
386
|
+
return (arr as any)()
|
|
142
387
|
}
|
|
143
|
-
}
|
|
388
|
+
})
|
|
389
|
+
: (ref(arr) as any)
|
|
390
|
+
const enabled = options?.enabled
|
|
391
|
+
return [req, computed(() => enabled === undefined ? true : !!toValue(enabled))] as const
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const observedAtom = <A, E>(
|
|
395
|
+
atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
|
|
396
|
+
options?: {
|
|
397
|
+
readonly staleTime?: number
|
|
398
|
+
readonly gcTime?: number | "infinity"
|
|
399
|
+
readonly idleTTL?: number | "infinity"
|
|
400
|
+
readonly refetchOnWindowFocus?: boolean
|
|
401
|
+
readonly revalidateOnFocus?: boolean
|
|
402
|
+
readonly structuralSharing?: boolean
|
|
403
|
+
readonly refetchInterval?: number
|
|
404
|
+
readonly refreshEvery?: number
|
|
405
|
+
}
|
|
406
|
+
): Atom.Atom<AsyncResult.AsyncResult<A, E>> => withQueryOptions(atom, normalizeQueryOptions(options))
|
|
407
|
+
|
|
408
|
+
const observedStreamAtom = <A, E>(
|
|
409
|
+
atom: Atom.Writable<Atom.PullResult<A, E>, void>,
|
|
410
|
+
options?: AtomStreamQueryOptions
|
|
411
|
+
): Atom.Writable<Atom.PullResult<A, E>, void> => {
|
|
412
|
+
let next = atom
|
|
413
|
+
const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
|
|
414
|
+
if (refetchInterval !== undefined) next = Atom.withRefresh(refetchInterval)(next)
|
|
415
|
+
const gcTime = options?.idleTTL ?? options?.gcTime
|
|
416
|
+
if (gcTime === "infinity") return Atom.keepAlive(next)
|
|
417
|
+
if (gcTime !== undefined) return Atom.setIdleTTL(next, gcTime)
|
|
418
|
+
return next
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const queryAtomFor = <I, A, E, TData>(
|
|
422
|
+
rt: AtomClientRuntime,
|
|
423
|
+
q: QueryFamilyDescriptor<I, A, E>,
|
|
424
|
+
arg: I,
|
|
425
|
+
options?: Omit<AtomQueryNewOptions<A, TData>, "select"> | Omit<CustomUseQueryOptions<A, E, TData>, "select">
|
|
426
|
+
): Atom.Atom<AsyncResult.AsyncResult<A, E>> => {
|
|
427
|
+
const family = getQueryFamily(rt, q)
|
|
428
|
+
return observedAtom(family(arg), options)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const makeQueryView = <I, A, E, TData>(
|
|
432
|
+
getAtomRt: () => AtomClientRuntime,
|
|
433
|
+
q: QueryFamilyDescriptor<I, A, E>,
|
|
434
|
+
arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
435
|
+
options?: (AtomQueryNewOptions<A, TData> | CustomUseQueryOptions<A, E, TData>) & {
|
|
436
|
+
readonly mode?: "optional"
|
|
144
437
|
}
|
|
438
|
+
): QueryView<TData, E> => {
|
|
439
|
+
const atomRt = getAtomRt()
|
|
440
|
+
const registry = injectRegistry()
|
|
441
|
+
const [req, enabledRef] = optionValue<I>(arg, options)
|
|
442
|
+
const family = getQueryFamily(atomRt, q)
|
|
443
|
+
const atomRef = computed(() => enabledRef.value ? observedAtom(family(req.value), options) : disabledQueryAtom)
|
|
444
|
+
const rawResult = useAtomValue(() => atomRef.value) as ComputedRef<AsyncResult.AsyncResult<A, E>>
|
|
445
|
+
const select = options?.select
|
|
446
|
+
const result = (select
|
|
447
|
+
? computed(() => AsyncResult.map(rawResult.value, select))
|
|
448
|
+
: rawResult) as ComputedRef<AsyncResult.AsyncResult<TData, E>>
|
|
449
|
+
const refresh = () => registry.refresh(atomRef.value)
|
|
450
|
+
const awaitResult = () =>
|
|
451
|
+
select
|
|
452
|
+
? awaitAtomResult(registry, atomRef.value).pipe(Effect.map(select))
|
|
453
|
+
: awaitAtomResult(registry, atomRef.value)
|
|
454
|
+
const refetch = () =>
|
|
455
|
+
Effect.gen(function*() {
|
|
456
|
+
refresh()
|
|
457
|
+
return yield* awaitResult()
|
|
458
|
+
})
|
|
459
|
+
const staleMs = staleTimeMsOf(normalizeQueryOptions(options))
|
|
460
|
+
onMounted(() => {
|
|
461
|
+
if (!enabledRef.value) return
|
|
462
|
+
if (isStaleResult(registry.get(atomRef.value), staleMs)) refresh()
|
|
463
|
+
})
|
|
464
|
+
const data = computed(() => Option.getOrUndefined(AsyncResult.value(result.value)))
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
result,
|
|
468
|
+
data,
|
|
469
|
+
awaitResult,
|
|
470
|
+
refetch,
|
|
471
|
+
refresh,
|
|
472
|
+
registry,
|
|
473
|
+
atom: atomRef
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const makeQueryFamily = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
478
|
+
const useQueryFamily: {
|
|
479
|
+
<I, E, A, Request extends Req, Name extends string>(
|
|
480
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
481
|
+
): QueryAtomFamily<I, A, E>
|
|
482
|
+
} = <I, E, A, Request extends Req, Name extends string>(
|
|
483
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
484
|
+
) => getQueryFamily(getAtomRt(), q)
|
|
485
|
+
|
|
486
|
+
return useQueryFamily
|
|
145
487
|
}
|
|
146
488
|
|
|
147
|
-
export const
|
|
489
|
+
export const makeQueryAtom = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
490
|
+
const useQueryAtom: {
|
|
491
|
+
<I, E, A, Request extends Req, Name extends string>(
|
|
492
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
493
|
+
): {
|
|
494
|
+
<TData = A>(
|
|
495
|
+
arg: I,
|
|
496
|
+
options?: Omit<AtomQueryNewOptions<A, TData>, "select">
|
|
497
|
+
): Atom.Atom<AsyncResult.AsyncResult<A, E>>
|
|
498
|
+
}
|
|
499
|
+
} = <I, E, A, Request extends Req, Name extends string>(
|
|
500
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
501
|
+
) =>
|
|
502
|
+
<TData = A>(
|
|
503
|
+
arg: I,
|
|
504
|
+
options?: Omit<AtomQueryNewOptions<A, TData>, "select">
|
|
505
|
+
) => queryAtomFor(getAtomRt(), q, arg, options)
|
|
506
|
+
|
|
507
|
+
return useQueryAtom
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export const makeQueryNew = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
511
|
+
const useQueryNew: {
|
|
512
|
+
<I, E, A, Request extends Req, Name extends string>(
|
|
513
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
514
|
+
): {
|
|
515
|
+
<TData = A>(
|
|
516
|
+
arg: WatchSource<Option.Option<I>>,
|
|
517
|
+
options: Omit<AtomQueryNewOptions<A, TData>, "enabled"> & { mode: "optional" }
|
|
518
|
+
): QueryView<TData, E>
|
|
519
|
+
|
|
520
|
+
<TData = A>(
|
|
521
|
+
arg: I | WatchSource<I> | undefined,
|
|
522
|
+
options?: AtomQueryNewOptions<A, TData>
|
|
523
|
+
): QueryView<TData, E>
|
|
524
|
+
}
|
|
525
|
+
} = <I, E, A, Request extends Req, Name extends string>(
|
|
526
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
527
|
+
) =>
|
|
528
|
+
<TData = A>(
|
|
529
|
+
arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
530
|
+
options?: AtomQueryNewOptions<A, TData> & { readonly mode?: "optional" }
|
|
531
|
+
) => makeQueryView<I, A, E, TData>(getAtomRt, q, arg, options)
|
|
532
|
+
|
|
533
|
+
return useQueryNew
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export const makeQuery = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
148
537
|
const useQuery_: {
|
|
149
538
|
<I, A, E, Request extends Req, Name extends string>(
|
|
150
539
|
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
@@ -194,103 +583,24 @@ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
|
|
|
194
583
|
) =>
|
|
195
584
|
<TData = A>(
|
|
196
585
|
arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
197
|
-
// todo QueryKey type would be [string, ...string[]], but with I it would be [string, ...string[], I]
|
|
198
586
|
options?: any
|
|
199
|
-
// TODO
|
|
200
587
|
) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
? arr as () => Option.Option<I>
|
|
211
|
-
: () => (arr as { value: Option.Option<I> }).value
|
|
212
|
-
req = {
|
|
213
|
-
get value() {
|
|
214
|
-
// getOrUndefined returns undefined when None, but queryFn is only called when enabled (Some)
|
|
215
|
-
return Option.getOrUndefined(getOption()) as I
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const { mode: _mode, enabled: _enabled, ...rest } = options ?? {}
|
|
219
|
-
callerOptions = { ...rest, enabled: computed(() => Option.isSome(getOption())) }
|
|
220
|
-
} else {
|
|
221
|
-
req = !arg
|
|
222
|
-
? undefined
|
|
223
|
-
: typeof arr === "function"
|
|
224
|
-
? ({
|
|
225
|
-
get value() {
|
|
226
|
-
return (arr as any)()
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
: ref(arg) as any
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const queryKey = makeQueryKey(q)
|
|
233
|
-
const projectionHash = (q as { queryKeyProjectionHash?: string }).queryKeyProjectionHash
|
|
234
|
-
|
|
235
|
-
const defaultOptions = {
|
|
236
|
-
// we do not want to throw errors, because we turn the success and error responses into a Result type
|
|
237
|
-
// why don't we turn the error/success response into a Result type before returning to tanstack query? because we want to leverage tanstack query's retry and caching mechanism, which relies on throwing errors to trigger retries, and we don't want to interfere with that by catching the errors too early.
|
|
238
|
-
// but if we allow tanstack query to throw, it will trigger the error boundary in Vue - via a "watcher callback" error - which we currently report and log, which is not what we want.
|
|
239
|
-
// TODO: we might want to rethink the strategy of how to handle errors that happen after the initial load.
|
|
240
|
-
// For suspense, the initial load is captured by the suspense boundary.
|
|
241
|
-
// For subsequent loads (or non suspense use) we currently are required to use the QueryResult component to conditionally render error/loading/etc.
|
|
242
|
-
throwOnError: false
|
|
588
|
+
const view = makeQueryView<I, A, E, TData>(getAtomRt, q, arg, options)
|
|
589
|
+
|
|
590
|
+
// 4th element is internal-only; the public `.suspense()` Promise boundary lives in makeClient.
|
|
591
|
+
const handle = {
|
|
592
|
+
awaitResult: view.awaitResult,
|
|
593
|
+
refetch: view.refetch,
|
|
594
|
+
refresh: view.refresh,
|
|
595
|
+
registry: view.registry,
|
|
596
|
+
atom: view.atom
|
|
243
597
|
}
|
|
244
598
|
|
|
245
|
-
const r = useTanstackQuery<A, CauseException<E>, TData>({
|
|
246
|
-
...defaultOptions,
|
|
247
|
-
...callerOptions,
|
|
248
|
-
retry: (retryCount, error) => {
|
|
249
|
-
if (error instanceof CauseException) {
|
|
250
|
-
if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
|
|
251
|
-
return false
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return retryCount < 5
|
|
256
|
-
},
|
|
257
|
-
queryKey: projectionHash === undefined ? [...queryKey, req] : [...queryKey, req, projectionHash],
|
|
258
|
-
queryFn: ({ meta, signal }) =>
|
|
259
|
-
runPromise(
|
|
260
|
-
q
|
|
261
|
-
.handler(req?.value as I)
|
|
262
|
-
.pipe(
|
|
263
|
-
Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
|
|
264
|
-
Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
|
|
265
|
-
meta?.["span"] ? Effect.withParentSpan(meta["span"] as Span) : (_) => _
|
|
266
|
-
),
|
|
267
|
-
{ signal }
|
|
268
|
-
)
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
const latestSuccess = shallowRef<TData>()
|
|
272
|
-
const result = computed((): AsyncResult.AsyncResult<TData, E> =>
|
|
273
|
-
swrToQuery({
|
|
274
|
-
error: r.error.value ?? undefined,
|
|
275
|
-
data: r.data.value === undefined ? latestSuccess.value : r.data.value, // we fall back to existing data, as tanstack query might loose it when the key changes
|
|
276
|
-
isValidating: r.isFetching.value
|
|
277
|
-
})
|
|
278
|
-
)
|
|
279
|
-
// not using `computed` here as we have a circular dependency
|
|
280
|
-
watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
|
|
281
|
-
|
|
282
599
|
return [
|
|
283
|
-
result,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// and always ends up in the success channel, even when error..
|
|
288
|
-
(options?: RefetchOptions) =>
|
|
289
|
-
Effect.currentSpan.pipe(
|
|
290
|
-
Effect.orElseSucceed(() => null),
|
|
291
|
-
Effect.flatMap((span) => Effect.promise(() => r.refetch({ ...options, updateMeta: { span } })))
|
|
292
|
-
),
|
|
293
|
-
r
|
|
600
|
+
view.result,
|
|
601
|
+
view.data,
|
|
602
|
+
(_options?: RefetchOptions) => view.refetch(),
|
|
603
|
+
handle
|
|
294
604
|
] as any
|
|
295
605
|
}
|
|
296
606
|
|
|
@@ -352,6 +662,55 @@ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
|
|
|
352
662
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
353
663
|
export interface MakeQuery2<R> extends ReturnType<typeof makeQuery<R>> {}
|
|
354
664
|
|
|
665
|
+
const streamQueryAtomFor = <I, A, E>(
|
|
666
|
+
rt: AtomClientRuntime,
|
|
667
|
+
q: StreamQueryFamilyDescriptor<I, A, E>,
|
|
668
|
+
arg: I,
|
|
669
|
+
options?: AtomStreamQueryOptions
|
|
670
|
+
): Atom.Writable<Atom.PullResult<A, E>, void> => {
|
|
671
|
+
const family = getStreamQueryFamily(rt, q)
|
|
672
|
+
return observedStreamAtom(family(arg), options)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export const makeStreamQueryFamily = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
676
|
+
const useStreamQueryFamily: {
|
|
677
|
+
<I, E, A, Request extends Req, Name extends string>(
|
|
678
|
+
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
679
|
+
): StreamQueryAtomFamily<I, A, E>
|
|
680
|
+
} = <I, E, A, Request extends Req, Name extends string>(
|
|
681
|
+
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
682
|
+
) => getStreamQueryFamily(getAtomRt(), q)
|
|
683
|
+
|
|
684
|
+
return useStreamQueryFamily
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export const makeStreamQueryAtom = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
688
|
+
const useStreamQueryAtom: {
|
|
689
|
+
<I, E, A, Request extends Req, Name extends string>(
|
|
690
|
+
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
691
|
+
): (arg: I, options?: AtomStreamQueryOptions) => Atom.Writable<Atom.PullResult<A, E>, void>
|
|
692
|
+
} = <I, E, A, Request extends Req, Name extends string>(
|
|
693
|
+
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
694
|
+
) =>
|
|
695
|
+
(arg: I, options?: AtomStreamQueryOptions) => streamQueryAtomFor(getAtomRt(), q, arg, options)
|
|
696
|
+
|
|
697
|
+
return useStreamQueryAtom
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export const makeStreamQueryNew = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
|
|
701
|
+
const useStreamQueryNew: {
|
|
702
|
+
<I, E, A, Request extends Req, Name extends string>(
|
|
703
|
+
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
704
|
+
): (arg: MaybeRefOrGetter<I>, options?: AtomStreamQueryOptions) => StreamQueryView<A, E>
|
|
705
|
+
} = <I, E, A, Request extends Req, Name extends string>(
|
|
706
|
+
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
707
|
+
) =>
|
|
708
|
+
(arg: MaybeRefOrGetter<I>, options?: AtomStreamQueryOptions) =>
|
|
709
|
+
useAtomStreamQuery(() => streamQueryAtomFor(getAtomRt(), q, toValue(arg), options))
|
|
710
|
+
|
|
711
|
+
return useStreamQueryNew
|
|
712
|
+
}
|
|
713
|
+
|
|
355
714
|
type StreamQueryResult<A, E> = readonly [
|
|
356
715
|
ComputedRef<AsyncResult.AsyncResult<A[], E>>,
|
|
357
716
|
ComputedRef<A[] | undefined>,
|
|
@@ -359,66 +718,26 @@ type StreamQueryResult<A, E> = readonly [
|
|
|
359
718
|
UseQueryReturnType<any, any>
|
|
360
719
|
]
|
|
361
720
|
|
|
362
|
-
export const makeStreamQuery = <R>(
|
|
721
|
+
export const makeStreamQuery = <R>(
|
|
722
|
+
getRuntime: () => Context.Context<R>,
|
|
723
|
+
getAtomRt: () => AtomClientRuntime
|
|
724
|
+
) => {
|
|
725
|
+
const query = makeQuery(getRuntime, getAtomRt)
|
|
726
|
+
// A stream query is an ordinary atom query over an effect that collects the whole stream
|
|
727
|
+
// into an array (`Stream.runCollect`). It reuses all the atom machinery (family cache, swr,
|
|
728
|
+
// invalidation, structural sharing). Note: unlike the old tanstack `streamedQuery`, the result
|
|
729
|
+
// appears once the stream completes, not incrementally (stream queries are not used today).
|
|
363
730
|
const streamQuery_: {
|
|
364
731
|
<I, E, A, Request extends Req, Name extends string>(
|
|
365
732
|
q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
|
|
366
733
|
): (arg: I | WatchSource<I>) => StreamQueryResult<A, E>
|
|
367
|
-
} = (q: any) =>
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
get value() {
|
|
375
|
-
return arr()
|
|
376
|
-
}
|
|
377
|
-
})
|
|
378
|
-
: ref(arg)
|
|
379
|
-
const queryKey = makeQueryKey(q)
|
|
380
|
-
|
|
381
|
-
const r = useTanstackQuery<any[], CauseException<any>, any[]>(
|
|
382
|
-
{
|
|
383
|
-
throwOnError: false,
|
|
384
|
-
retry: (retryCount: number, error: unknown) => {
|
|
385
|
-
if (error instanceof CauseException) {
|
|
386
|
-
if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
|
|
387
|
-
return false
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return retryCount < 5
|
|
391
|
-
},
|
|
392
|
-
queryKey: [...queryKey, req],
|
|
393
|
-
queryFn: streamedQuery({
|
|
394
|
-
streamFn: () => {
|
|
395
|
-
const stream = q.handler(req?.value)
|
|
396
|
-
return streamToAsyncIterableWithCauseException(stream, context, q.id)
|
|
397
|
-
}
|
|
398
|
-
})
|
|
399
|
-
}
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
const latestSuccess = shallowRef<any[]>()
|
|
403
|
-
const result = computed((): AsyncResult.AsyncResult<any[], any> =>
|
|
404
|
-
swrToQuery({
|
|
405
|
-
error: r.error.value ?? undefined,
|
|
406
|
-
data: r.data.value === undefined ? latestSuccess.value : r.data.value,
|
|
407
|
-
isValidating: r.isFetching.value
|
|
408
|
-
})
|
|
409
|
-
)
|
|
410
|
-
watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
|
|
411
|
-
|
|
412
|
-
return [
|
|
413
|
-
result,
|
|
414
|
-
computed(() => latestSuccess.value),
|
|
415
|
-
(options?: RefetchOptions) =>
|
|
416
|
-
Effect.currentSpan.pipe(
|
|
417
|
-
Effect.orElseSucceed(() => null),
|
|
418
|
-
Effect.flatMap((span) => Effect.promise(() => r.refetch({ ...options, updateMeta: { span } })))
|
|
419
|
-
),
|
|
420
|
-
r
|
|
421
|
-
] as any
|
|
734
|
+
} = (q: any) => {
|
|
735
|
+
const hook = query({
|
|
736
|
+
id: q.id,
|
|
737
|
+
options: q.options,
|
|
738
|
+
handler: (i: any) => Stream.runCollect(q.handler(i)).pipe(Effect.map((chunk) => [...chunk]))
|
|
739
|
+
} as any)
|
|
740
|
+
return (arg?: any) => hook(arg) as any
|
|
422
741
|
}
|
|
423
742
|
|
|
424
743
|
return streamQuery_
|
|
@@ -474,22 +793,24 @@ export function composeQueries<
|
|
|
474
793
|
}
|
|
475
794
|
|
|
476
795
|
export const useUpdateQuery = () => {
|
|
477
|
-
const
|
|
796
|
+
const registry = injectRegistry()
|
|
478
797
|
|
|
798
|
+
// NOTE: query atoms are derived (read-only) here, so unlike tanstack's `setQueryData` we can't
|
|
799
|
+
// optimistically patch the cache in place — this refetches the query (the `updater` is ignored).
|
|
800
|
+
// A first-class optimistic-update layer is planned for the atom-native redesign.
|
|
479
801
|
const f: {
|
|
480
802
|
<I, A>(
|
|
481
803
|
query: RequestHandlerWithInput<I, A, any, any, any, any>,
|
|
482
804
|
input: I,
|
|
483
805
|
updater: (data: NoInfer<A>) => NoInfer<A>
|
|
484
806
|
): void
|
|
485
|
-
} = (query: any, input: any,
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
} else {
|
|
491
|
-
console.warn(`Query data for key ${key} not found`, key)
|
|
807
|
+
} = (query: any, input: any, _updater: any) => {
|
|
808
|
+
const family = queryFamilyByKey.get(queryFamilyCacheKey(query))
|
|
809
|
+
if (!family) {
|
|
810
|
+
console.warn(`Query ${query.id} has not been used yet; nothing to update`)
|
|
811
|
+
return
|
|
492
812
|
}
|
|
813
|
+
registry.refresh(family(input))
|
|
493
814
|
}
|
|
494
815
|
return f
|
|
495
816
|
}
|