@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/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 { type DefaultError, type Enabled, experimental_streamedQuery as streamedQuery, type InitialDataFunction, type NonUndefinedGuard, type PlaceholderDataFunction, type QueryKey, type QueryObserverOptions, type QueryObserverResult, type RefetchOptions, useQuery as useTanstackQuery, useQueryClient, type UseQueryDefinedReturnType, type UseQueryReturnType } from "@tanstack/vue-query"
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, ServiceUnavailableError } from "effect-app/client/errors"
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 S from "effect-app/Schema"
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 Pull from "effect/Pull"
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 { computed, type ComputedRef, type MaybeRefOrGetter, ref, shallowRef, watch, type WatchSource } from "vue"
25
- import { reportRuntimeError } from "./lib.ts"
26
- import { makeRunPromise } from "./runtime.ts"
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
- // we must use interface extends, or we get the dreaded typescript error of isn't portable blabla @tanstack/vue-query/build/modern/types.js
29
- // but because how they are dealing with some extends clause, we loose all properties except initialData
30
- // so we actually reconstruct the interfaces here from the ground up :/
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
- > extends
38
- Omit<
39
- QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
40
- "queryKey" | "queryFn" | "initialData" | "enabled" | "placeholderData"
41
- >
42
- {
43
- enabled?: MaybeRefOrGetter<boolean | undefined> | (() => Enabled<TQueryFnData, TError, TQueryData, TQueryKey>)
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?: undefined | InitialDataFunction<NonUndefinedGuard<TQueryFnData>> | NonUndefinedGuard<TQueryFnData>
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: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>)
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?: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>) | undefined
84
- placeholderData:
85
- | NonFunctionGuard<TQueryData>
86
- | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
87
- }
88
-
89
- function swrToQuery<E, A>(r: {
90
- error: CauseException<E> | undefined
91
- data: A | undefined
92
- isValidating: boolean
93
- }): AsyncResult.AsyncResult<A, E> {
94
- if (r.error !== undefined) {
95
- return AsyncResult.failureWithPrevious(
96
- r.error.originalCause,
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
- previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
99
- waiting: r.isValidating
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 AsyncResult.initial(r.isValidating)
300
+ return Effect.runPromise(eff)
108
301
  }
109
302
 
110
- function streamToAsyncIterableWithCauseException<A, E, R>(
111
- self: Stream.Stream<A, E, R>,
112
- context: Context.Context<R>,
113
- id: string
114
- ): AsyncIterable<A> {
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
- [Symbol.asyncIterator]() {
117
- const runPromise = Effect.runPromiseWith(context)
118
- const runPromiseExit = Effect.runPromiseExitWith(context)
119
- const scope = Scope.makeUnsafe()
120
- let pull: any
121
- let currentIter: Iterator<A> | undefined
122
- return {
123
- async next(): Promise<IteratorResult<A>> {
124
- if (currentIter) {
125
- const next = currentIter.next()
126
- if (!next.done) return next
127
- currentIter = undefined
128
- }
129
- pull ??= await runPromise(Channel.toPullScoped((self as any).channel, scope))
130
- const exit = await runPromiseExit(pull)
131
- if (Exit.isSuccess(exit)) {
132
- currentIter = (exit.value as any)[Symbol.iterator]()
133
- return currentIter!.next()
134
- } else if (Pull.isDoneCause((exit as any).cause)) {
135
- return { done: true, value: undefined }
136
- }
137
- throw new CauseException((exit as any).cause, id)
138
- },
139
- return(_) {
140
- return runPromise(Effect.as(Scope.close(scope, Exit.void), { done: true, value: undefined }) as any)
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 makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
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
- // we wrap into CauseException because we want to keep the full cause of the failure.
202
- const runPromise = makeRunPromise(getRuntime())
203
- const arr = arg
204
-
205
- let req: { value: I } | undefined
206
- let callerOptions: any = options
207
-
208
- if (options?.mode === "optional") {
209
- const getOption: () => Option.Option<I> = typeof arr === "function"
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
- computed(() => latestSuccess.value),
285
- // one thing to keep in mind is that span will be disconnected as Context does not pass from outside.
286
- // TODO: consider how we should handle the Result here which is `QueryObserverResult<A, E>`
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>(getRuntime: () => Context.Context<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) => (arg?: any) => {
368
- const context = getRuntime()
369
- const arr = arg
370
- const req: { value: any } | undefined = !arg
371
- ? undefined
372
- : typeof arr === "function"
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 queryClient = useQueryClient()
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, updater: any) => {
486
- const key = [...makeQueryKey(query), input]
487
- const data = queryClient.getQueryData(key)
488
- if (data) {
489
- queryClient.setQueryData(key, updater)
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
  }