@effect-app/vue 4.0.0-beta.272 → 4.0.0-beta.274

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,175 @@
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
+ }
27
42
 
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 :/
43
+ export interface QueryCacheUpdater {
44
+ readonly update: <I, A, E, R, Request extends Req, Name extends string>(
45
+ registry: ReturnType<typeof injectRegistry>,
46
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
47
+ input: I,
48
+ updater: (data: NoInfer<A>) => NoInfer<A>
49
+ ) => void
50
+ }
51
+
52
+ // retained generic aliases so the exported option-interface arity is unchanged for consumers
53
+ export type UseQueryReturnType<A = any, E = any> = QueryHandle<A, E>
54
+ export type UseQueryDefinedReturnType<A = any, E = any> = QueryHandle<A, E>
55
+ export type QueryObserverResult<A = any, _E = any> = AsyncResult.AsyncResult<A, any>
56
+ export type SuspenseQueryTuple<A, E> = readonly [
57
+ ComputedRef<AsyncResult.AsyncResult<A, E>>,
58
+ ComputedRef<A>,
59
+ (options?: RefetchOptions) => Effect.Effect<A, E, never>,
60
+ QueryHandle<A, E>
61
+ ]
62
+
63
+ export type SuspenseQueryView<A, E> =
64
+ & Omit<QueryView<A, E>, "data">
65
+ & {
66
+ readonly data: ComputedRef<A>
67
+ }
68
+ & SuspenseQueryTuple<A, E>
69
+
70
+ export type QueryAtomFamily<I, A, E> = (input: I) => Atom.Atom<AsyncResult.AsyncResult<A, E>>
71
+ export type StreamQueryAtomFamily<I, A, E> = (input: I) => Atom.Writable<Atom.PullResult<A, E>, void>
72
+
73
+ interface QueryFamilyDescriptor<I, A, E> {
74
+ readonly id: string
75
+ readonly handler: (i: I) => Effect.Effect<A, E, any>
76
+ readonly options?: ClientForOptions
77
+ readonly queryKeyProjectionHash?: string
78
+ }
79
+
80
+ interface StreamQueryFamilyDescriptor<I, A, E> {
81
+ readonly id: string
82
+ readonly handler: (i: I) => Stream.Stream<A, E, any>
83
+ readonly options?: ClientForOptions
84
+ readonly queryKeyProjectionHash?: string
85
+ }
86
+
87
+ const queryFamilyCacheKey = (
88
+ q: { readonly id: string; readonly options?: ClientForOptions; readonly queryKeyProjectionHash?: string }
89
+ ) => `${makeQueryKey(q).join("/")}:${q.queryKeyProjectionHash ?? ""}`
90
+
91
+ // One atom family per request shape, keyed by the stable query key + projection hash (not the
92
+ // handler object — `clientFor` returns a fresh proxy per call, so the object isn't shareable).
93
+ // Module-level + key-indexed => the family is process-global, so the same request+input read
94
+ // the same atom across components/pages => cross-page caching via the global registry.
95
+ const queryFamilyByKey = new Map<string, any>()
96
+ const getQueryFamily = <I, A, E>(
97
+ rt: AtomClientRuntime,
98
+ q: QueryFamilyDescriptor<I, A, E>
99
+ ): QueryAtomFamily<I, A, E> => {
100
+ const key = queryFamilyCacheKey(q)
101
+ let f = queryFamilyByKey.get(key)
102
+ if (!f) {
103
+ f = buildQueryFamily(rt, q)
104
+ queryFamilyByKey.set(key, f)
105
+ }
106
+ return f
107
+ }
108
+
109
+ const streamQueryFamilyByKey = new Map<string, any>()
110
+ const getStreamQueryFamily = <I, A, E>(
111
+ rt: AtomClientRuntime,
112
+ q: StreamQueryFamilyDescriptor<I, A, E>
113
+ ): StreamQueryAtomFamily<I, A, E> => {
114
+ const key = queryFamilyCacheKey(q)
115
+ let f = streamQueryFamilyByKey.get(key)
116
+ if (!f) {
117
+ f = buildStreamQueryFamily(rt, q)
118
+ streamQueryFamilyByKey.set(key, f)
119
+ }
120
+ return f
121
+ }
122
+
123
+ const makeAtomQueryCacheUpdater = (warnIfMissing: boolean): QueryCacheUpdater => ({
124
+ update: (registry, query, input) => {
125
+ const family = queryFamilyByKey.get(queryFamilyCacheKey(query))
126
+ if (!family) {
127
+ if (warnIfMissing) {
128
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
129
+ }
130
+ return
131
+ }
132
+ registry.refresh(family(input))
133
+ }
134
+ })
135
+
136
+ export const atomQueryCacheUpdater = makeAtomQueryCacheUpdater(true)
137
+ export const optionalAtomQueryCacheUpdater = makeAtomQueryCacheUpdater(false)
138
+
139
+ export const combineQueryCacheUpdaters = (first: QueryCacheUpdater, second: QueryCacheUpdater): QueryCacheUpdater => ({
140
+ update: (registry, query, input, updater) => {
141
+ first.update(registry, query, input, updater)
142
+ second.update(registry, query, input, updater)
143
+ }
144
+ })
145
+
146
+ let activeQueryCacheUpdater = atomQueryCacheUpdater
147
+
148
+ export const setQueryCacheUpdater = (updater: QueryCacheUpdater) => {
149
+ activeQueryCacheUpdater = updater
150
+ }
151
+
152
+ // Atom-engine query options (formerly reconstructed from @tanstack/vue-query types).
153
+ // The generic arity is kept so the exported interface signatures are unchanged for consumers.
31
154
  export interface CustomUseQueryOptions<
32
155
  TQueryFnData = unknown,
33
156
  TError = DefaultError,
34
157
  TData = TQueryFnData,
35
158
  TQueryData = TQueryFnData,
36
159
  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>)
160
+ > {
161
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined>
162
+ /** stale threshold in ms (or a Duration input) */
163
+ readonly staleTime?: number
164
+ /** garbage-collect after idle, ms (or "infinity") */
165
+ readonly gcTime?: number | "infinity"
166
+ readonly refetchOnWindowFocus?: boolean
167
+ readonly structuralSharing?: boolean
168
+ /** poll: re-fetch every N ms (tanstack refetchInterval) */
169
+ readonly refetchInterval?: number
170
+ readonly select?: (data: TQueryFnData) => TData
171
+ /** accepted for source compatibility; not used by the atom engine */
172
+ readonly retry?: boolean | number
173
+ readonly meta?: Record<string, unknown>
174
+ readonly _phantom?: [TQueryData, TQueryKey, TError]
44
175
  }
45
176
 
46
177
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -53,11 +184,8 @@ export interface CustomUndefinedInitialQueryOptions<
53
184
  TQueryData = TQueryFnData,
54
185
  TQueryKey extends QueryKey = QueryKey
55
186
  > 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>
187
+ readonly initialData?: TQueryFnData | (() => TQueryFnData) | undefined
188
+ readonly placeholderData?: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData) | undefined
61
189
  }
62
190
  export interface CustomDefinedInitialQueryOptions<
63
191
  TQueryFnData = unknown,
@@ -66,11 +194,8 @@ export interface CustomDefinedInitialQueryOptions<
66
194
  TQueryData = TQueryFnData,
67
195
  TQueryKey extends QueryKey = QueryKey
68
196
  > 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>
197
+ readonly initialData: TQueryFnData | (() => TQueryFnData)
198
+ readonly placeholderData?: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData) | undefined
74
199
  }
75
200
 
76
201
  export interface CustomDefinedPlaceholderQueryOptions<
@@ -80,71 +205,373 @@ export interface CustomDefinedPlaceholderQueryOptions<
80
205
  TQueryData = TQueryFnData,
81
206
  TQueryKey extends QueryKey = QueryKey
82
207
  > 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,
208
+ readonly initialData?: TQueryFnData | (() => TQueryFnData) | undefined
209
+ readonly placeholderData: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData)
210
+ }
211
+
212
+ export interface AtomQueryNewOptions<TQueryFnData = unknown, TData = TQueryFnData> {
213
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined>
214
+ readonly staleTime?: number
215
+ readonly idleTTL?: number | "infinity"
216
+ readonly gcTime?: number | "infinity"
217
+ readonly revalidateOnFocus?: boolean
218
+ readonly refetchOnWindowFocus?: boolean
219
+ readonly structuralSharing?: boolean
220
+ readonly refreshEvery?: number
221
+ readonly refetchInterval?: number
222
+ readonly select?: (data: TQueryFnData) => TData
223
+ }
224
+
225
+ export interface AtomStreamQueryOptions {
226
+ readonly staleTime?: number
227
+ readonly idleTTL?: number | "infinity"
228
+ readonly gcTime?: number | "infinity"
229
+ readonly revalidateOnFocus?: boolean
230
+ readonly refetchOnWindowFocus?: boolean
231
+ readonly refreshEvery?: number
232
+ readonly refetchInterval?: number
233
+ }
234
+
235
+ const normalizeQueryOptions = (options?: {
236
+ readonly staleTime?: number
237
+ readonly gcTime?: number | "infinity"
238
+ readonly idleTTL?: number | "infinity"
239
+ readonly refetchOnWindowFocus?: boolean
240
+ readonly revalidateOnFocus?: boolean
241
+ readonly structuralSharing?: boolean
242
+ readonly refetchInterval?: number
243
+ readonly refreshEvery?: number
244
+ }): AtomQueryOptions => {
245
+ const out: {
246
+ staleTime?: number
247
+ gcTime?: number | "infinity"
248
+ revalidateOnFocus?: boolean
249
+ structuralSharing?: boolean
250
+ refetchInterval?: number
251
+ } = {}
252
+ if (options?.staleTime !== undefined) out.staleTime = options.staleTime
253
+ const gcTime = options?.idleTTL ?? options?.gcTime
254
+ if (gcTime !== undefined) out.gcTime = gcTime
255
+ const revalidateOnFocus = options?.revalidateOnFocus ?? options?.refetchOnWindowFocus
256
+ if (revalidateOnFocus !== undefined) out.revalidateOnFocus = revalidateOnFocus
257
+ if (options?.structuralSharing !== undefined) out.structuralSharing = options.structuralSharing
258
+ const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
259
+ if (refetchInterval !== undefined) out.refetchInterval = refetchInterval
260
+ return out
261
+ }
262
+
263
+ export const useAtomQuery = <A, E>(
264
+ atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
265
+ ): QueryView<A, E> => {
266
+ const registry = injectRegistry()
267
+ const atomRef = computed(atom)
268
+ const atomResult = useAtomValue(() => atomRef.value)
269
+ const result = computed(() => atomResult.value)
270
+ const refresh = () => registry.refresh(atomRef.value)
271
+ const awaitResult = () => awaitAtomResult(registry, atomRef.value)
272
+ const refetch = () =>
273
+ Effect.gen(function*() {
274
+ refresh()
275
+ return yield* awaitResult()
276
+ })
277
+ const data = computed(() => Option.getOrUndefined(AsyncResult.value(result.value)))
278
+
279
+ return {
280
+ result,
281
+ data,
282
+ awaitResult,
283
+ refetch,
284
+ refresh,
285
+ registry,
286
+ atom: atomRef
287
+ }
288
+ }
289
+
290
+ export const useAtomSuspense = <A, E>(
291
+ atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
292
+ ): Promise<SuspenseQueryView<A, E>> => {
293
+ const view = useAtomQuery(atom)
294
+ const data = computed<A>(() => {
295
+ const latest = view.data.value
296
+ if (latest === undefined) {
297
+ throw new Error("Internal Error: atom suspense resolved without a latest value")
298
+ }
299
+ return latest
300
+ })
301
+
302
+ const isMounted = ref(true)
303
+ onBeforeUnmount(() => {
304
+ isMounted.value = false
305
+ })
306
+
307
+ const eff = Effect.gen(function*() {
308
+ const exit = yield* view.awaitResult().pipe(Effect.exit)
309
+ if (!isMounted.value) {
310
+ return yield* Effect.interrupt
311
+ }
312
+ if (Exit.isFailure(exit)) {
313
+ return yield* Exit.failCause(exit.cause)
314
+ }
315
+
316
+ const fetch = (_options?: RefetchOptions) => view.refetch()
317
+ const handle = {
318
+ awaitResult: view.awaitResult,
319
+ refetch: view.refetch,
320
+ refresh: view.refresh,
321
+ registry: view.registry,
322
+ atom: view.atom
323
+ }
324
+ return Object.assign(
325
+ [
326
+ view.result,
327
+ data,
328
+ fetch,
329
+ handle
330
+ ] as const,
97
331
  {
98
- previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
99
- waiting: r.isValidating
332
+ ...view,
333
+ data
100
334
  }
101
335
  )
102
- }
103
- if (r.data !== undefined) {
104
- return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
105
- }
336
+ })
337
+
338
+ return Effect.runPromise(eff)
339
+ }
106
340
 
107
- return AsyncResult.initial(r.isValidating)
341
+ export type StreamQueryPullValue<A> = {
342
+ readonly done: boolean
343
+ readonly items: ReadonlyArray<A>
108
344
  }
109
345
 
110
- function streamToAsyncIterableWithCauseException<A, E, R>(
111
- self: Stream.Stream<A, E, R>,
112
- context: Context.Context<R>,
113
- id: string
114
- ): AsyncIterable<A> {
346
+ export interface StreamQueryView<A, E> {
347
+ readonly result: ComputedRef<Atom.PullResult<A, E>>
348
+ readonly items: ComputedRef<ReadonlyArray<A>>
349
+ readonly latest: ComputedRef<A | undefined>
350
+ readonly done: ComputedRef<boolean>
351
+ readonly awaitResult: () => Effect.Effect<StreamQueryPullValue<A>, E | Cause.NoSuchElementError, never>
352
+ readonly pull: () => void
353
+ readonly pullAndAwait: () => Effect.Effect<StreamQueryPullValue<A>, E | Cause.NoSuchElementError, never>
354
+ readonly refresh: () => void
355
+ readonly registry: ReturnType<typeof injectRegistry>
356
+ readonly atom: ComputedRef<Atom.Writable<Atom.PullResult<A, E>, void>>
357
+ }
358
+
359
+ export const useAtomStreamQuery = <A, E>(
360
+ atom: () => Atom.Writable<Atom.PullResult<A, E>, void>
361
+ ): StreamQueryView<A, E> => {
362
+ const registry = injectRegistry()
363
+ const atomRef = computed(atom)
364
+ const atomResult = useAtomValue(() => atomRef.value)
365
+ const result = computed(() => atomResult.value)
366
+ const pull = () => registry.set(atomRef.value, void 0)
367
+ const refresh = () => registry.refresh(atomRef.value)
368
+ const awaitResult = () => awaitAtomResult(registry, atomRef.value)
369
+ const pullAndAwait = () =>
370
+ Effect.gen(function*() {
371
+ pull()
372
+ return yield* awaitResult()
373
+ })
374
+ const items = computed(() =>
375
+ Option.getOrElse(
376
+ Option.map(AsyncResult.value(result.value), (_) => _.items),
377
+ () => []
378
+ )
379
+ )
380
+ const latest = computed(() => items.value.at(-1))
381
+ const done = computed(() =>
382
+ Option.getOrElse(
383
+ Option.map(AsyncResult.value(result.value), (_) => _.done),
384
+ () => false
385
+ )
386
+ )
387
+
115
388
  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)
389
+ result,
390
+ items,
391
+ latest,
392
+ done,
393
+ awaitResult,
394
+ pull,
395
+ pullAndAwait,
396
+ refresh,
397
+ registry,
398
+ atom: atomRef
399
+ }
400
+ }
401
+
402
+ const optionValue = <I>(
403
+ arr: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
404
+ options?: { readonly mode?: "optional"; readonly enabled?: MaybeRefOrGetter<boolean | undefined> }
405
+ ): readonly [{ readonly value: I }, ComputedRef<boolean>] => {
406
+ if (options?.mode === "optional") {
407
+ const getOption: () => Option.Option<I> = typeof arr === "function"
408
+ ? arr as () => Option.Option<I>
409
+ : () => (arr as { value: Option.Option<I> }).value
410
+ return [
411
+ {
412
+ get value() {
413
+ return Option.getOrUndefined(getOption()) as I
141
414
  }
415
+ },
416
+ computed(() => Option.isSome(getOption()))
417
+ ] as const
418
+ }
419
+ const req = !arr
420
+ ? ({ value: undefined as I })
421
+ : typeof arr === "function"
422
+ ? ({
423
+ get value() {
424
+ return (arr as any)()
142
425
  }
143
- }
426
+ })
427
+ : (ref(arr) as any)
428
+ const enabled = options?.enabled
429
+ return [req, computed(() => enabled === undefined ? true : !!toValue(enabled))] as const
430
+ }
431
+
432
+ const observedAtom = <A, E>(
433
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
434
+ options?: {
435
+ readonly staleTime?: number
436
+ readonly gcTime?: number | "infinity"
437
+ readonly idleTTL?: number | "infinity"
438
+ readonly refetchOnWindowFocus?: boolean
439
+ readonly revalidateOnFocus?: boolean
440
+ readonly structuralSharing?: boolean
441
+ readonly refetchInterval?: number
442
+ readonly refreshEvery?: number
443
+ }
444
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>> => withQueryOptions(atom, normalizeQueryOptions(options))
445
+
446
+ const observedStreamAtom = <A, E>(
447
+ atom: Atom.Writable<Atom.PullResult<A, E>, void>,
448
+ options?: AtomStreamQueryOptions
449
+ ): Atom.Writable<Atom.PullResult<A, E>, void> => {
450
+ let next = atom
451
+ const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
452
+ if (refetchInterval !== undefined) next = Atom.withRefresh(refetchInterval)(next)
453
+ const gcTime = options?.idleTTL ?? options?.gcTime
454
+ if (gcTime === "infinity") return Atom.keepAlive(next)
455
+ if (gcTime !== undefined) return Atom.setIdleTTL(next, gcTime)
456
+ return next
457
+ }
458
+
459
+ const queryAtomFor = <I, A, E, TData>(
460
+ rt: AtomClientRuntime,
461
+ q: QueryFamilyDescriptor<I, A, E>,
462
+ arg: I,
463
+ options?: Omit<AtomQueryNewOptions<A, TData>, "select"> | Omit<CustomUseQueryOptions<A, E, TData>, "select">
464
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>> => {
465
+ const family = getQueryFamily(rt, q)
466
+ return observedAtom(family(arg), options)
467
+ }
468
+
469
+ const makeQueryView = <I, A, E, TData>(
470
+ getAtomRt: () => AtomClientRuntime,
471
+ q: QueryFamilyDescriptor<I, A, E>,
472
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
473
+ options?: (AtomQueryNewOptions<A, TData> | CustomUseQueryOptions<A, E, TData>) & {
474
+ readonly mode?: "optional"
475
+ }
476
+ ): QueryView<TData, E> => {
477
+ const atomRt = getAtomRt()
478
+ const registry = injectRegistry()
479
+ const [req, enabledRef] = optionValue<I>(arg, options)
480
+ const family = getQueryFamily(atomRt, q)
481
+ const atomRef = computed(() => enabledRef.value ? observedAtom(family(req.value), options) : disabledQueryAtom)
482
+ const rawResult = useAtomValue(() => atomRef.value) as ComputedRef<AsyncResult.AsyncResult<A, E>>
483
+ const select = options?.select
484
+ const result = (select
485
+ ? computed(() => AsyncResult.map(rawResult.value, select))
486
+ : rawResult) as ComputedRef<AsyncResult.AsyncResult<TData, E>>
487
+ const refresh = () => registry.refresh(atomRef.value)
488
+ const awaitResult = () =>
489
+ select
490
+ ? awaitAtomResult(registry, atomRef.value).pipe(Effect.map(select))
491
+ : awaitAtomResult(registry, atomRef.value)
492
+ const refetch = () =>
493
+ Effect.gen(function*() {
494
+ refresh()
495
+ return yield* awaitResult()
496
+ })
497
+ const staleMs = staleTimeMsOf(normalizeQueryOptions(options))
498
+ onMounted(() => {
499
+ if (!enabledRef.value) return
500
+ if (isStaleResult(registry.get(atomRef.value), staleMs)) refresh()
501
+ })
502
+ const data = computed(() => Option.getOrUndefined(AsyncResult.value(result.value)))
503
+
504
+ return {
505
+ result,
506
+ data,
507
+ awaitResult,
508
+ refetch,
509
+ refresh,
510
+ registry,
511
+ atom: atomRef
144
512
  }
145
513
  }
146
514
 
147
- export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
515
+ export const makeQueryFamily = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
516
+ const useQueryFamily: {
517
+ <I, E, A, Request extends Req, Name extends string>(
518
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
519
+ ): QueryAtomFamily<I, A, E>
520
+ } = <I, E, A, Request extends Req, Name extends string>(
521
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
522
+ ) => getQueryFamily(getAtomRt(), q)
523
+
524
+ return useQueryFamily
525
+ }
526
+
527
+ export const makeQueryAtom = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
528
+ const useQueryAtom: {
529
+ <I, E, A, Request extends Req, Name extends string>(
530
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
531
+ ): {
532
+ <TData = A>(
533
+ arg: I,
534
+ options?: Omit<AtomQueryNewOptions<A, TData>, "select">
535
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>>
536
+ }
537
+ } = <I, E, A, Request extends Req, Name extends string>(
538
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
539
+ ) =>
540
+ <TData = A>(
541
+ arg: I,
542
+ options?: Omit<AtomQueryNewOptions<A, TData>, "select">
543
+ ) => queryAtomFor(getAtomRt(), q, arg, options)
544
+
545
+ return useQueryAtom
546
+ }
547
+
548
+ export const makeQueryNew = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
549
+ const useQueryNew: {
550
+ <I, E, A, Request extends Req, Name extends string>(
551
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
552
+ ): {
553
+ <TData = A>(
554
+ arg: WatchSource<Option.Option<I>>,
555
+ options: Omit<AtomQueryNewOptions<A, TData>, "enabled"> & { mode: "optional" }
556
+ ): QueryView<TData, E>
557
+
558
+ <TData = A>(
559
+ arg: I | WatchSource<I> | undefined,
560
+ options?: AtomQueryNewOptions<A, TData>
561
+ ): QueryView<TData, E>
562
+ }
563
+ } = <I, E, A, Request extends Req, Name extends string>(
564
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
565
+ ) =>
566
+ <TData = A>(
567
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
568
+ options?: AtomQueryNewOptions<A, TData> & { readonly mode?: "optional" }
569
+ ) => makeQueryView<I, A, E, TData>(getAtomRt, q, arg, options)
570
+
571
+ return useQueryNew
572
+ }
573
+
574
+ export const makeQuery = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
148
575
  const useQuery_: {
149
576
  <I, A, E, Request extends Req, Name extends string>(
150
577
  q: RequestHandlerWithInput<I, A, E, R, Request, Name>
@@ -194,103 +621,24 @@ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
194
621
  ) =>
195
622
  <TData = A>(
196
623
  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
624
  options?: any
199
- // TODO
200
625
  ) => {
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
626
+ const view = makeQueryView<I, A, E, TData>(getAtomRt, q, arg, options)
627
+
628
+ // 4th element is internal-only; the public `.suspense()` Promise boundary lives in makeClient.
629
+ const handle = {
630
+ awaitResult: view.awaitResult,
631
+ refetch: view.refetch,
632
+ refresh: view.refresh,
633
+ registry: view.registry,
634
+ atom: view.atom
230
635
  }
231
636
 
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
243
- }
244
-
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
637
  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
638
+ view.result,
639
+ view.data,
640
+ (_options?: RefetchOptions) => view.refetch(),
641
+ handle
294
642
  ] as any
295
643
  }
296
644
 
@@ -352,6 +700,55 @@ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
352
700
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
353
701
  export interface MakeQuery2<R> extends ReturnType<typeof makeQuery<R>> {}
354
702
 
703
+ const streamQueryAtomFor = <I, A, E>(
704
+ rt: AtomClientRuntime,
705
+ q: StreamQueryFamilyDescriptor<I, A, E>,
706
+ arg: I,
707
+ options?: AtomStreamQueryOptions
708
+ ): Atom.Writable<Atom.PullResult<A, E>, void> => {
709
+ const family = getStreamQueryFamily(rt, q)
710
+ return observedStreamAtom(family(arg), options)
711
+ }
712
+
713
+ export const makeStreamQueryFamily = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
714
+ const useStreamQueryFamily: {
715
+ <I, E, A, Request extends Req, Name extends string>(
716
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
717
+ ): StreamQueryAtomFamily<I, A, E>
718
+ } = <I, E, A, Request extends Req, Name extends string>(
719
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
720
+ ) => getStreamQueryFamily(getAtomRt(), q)
721
+
722
+ return useStreamQueryFamily
723
+ }
724
+
725
+ export const makeStreamQueryAtom = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
726
+ const useStreamQueryAtom: {
727
+ <I, E, A, Request extends Req, Name extends string>(
728
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
729
+ ): (arg: I, options?: AtomStreamQueryOptions) => Atom.Writable<Atom.PullResult<A, E>, void>
730
+ } = <I, E, A, Request extends Req, Name extends string>(
731
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
732
+ ) =>
733
+ (arg: I, options?: AtomStreamQueryOptions) => streamQueryAtomFor(getAtomRt(), q, arg, options)
734
+
735
+ return useStreamQueryAtom
736
+ }
737
+
738
+ export const makeStreamQueryNew = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
739
+ const useStreamQueryNew: {
740
+ <I, E, A, Request extends Req, Name extends string>(
741
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
742
+ ): (arg: MaybeRefOrGetter<I>, options?: AtomStreamQueryOptions) => StreamQueryView<A, E>
743
+ } = <I, E, A, Request extends Req, Name extends string>(
744
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
745
+ ) =>
746
+ (arg: MaybeRefOrGetter<I>, options?: AtomStreamQueryOptions) =>
747
+ useAtomStreamQuery(() => streamQueryAtomFor(getAtomRt(), q, toValue(arg), options))
748
+
749
+ return useStreamQueryNew
750
+ }
751
+
355
752
  type StreamQueryResult<A, E> = readonly [
356
753
  ComputedRef<AsyncResult.AsyncResult<A[], E>>,
357
754
  ComputedRef<A[] | undefined>,
@@ -359,66 +756,26 @@ type StreamQueryResult<A, E> = readonly [
359
756
  UseQueryReturnType<any, any>
360
757
  ]
361
758
 
362
- export const makeStreamQuery = <R>(getRuntime: () => Context.Context<R>) => {
759
+ export const makeStreamQuery = <R>(
760
+ getRuntime: () => Context.Context<R>,
761
+ getAtomRt: () => AtomClientRuntime
762
+ ) => {
763
+ const query = makeQuery(getRuntime, getAtomRt)
764
+ // A stream query is an ordinary atom query over an effect that collects the whole stream
765
+ // into an array (`Stream.runCollect`). It reuses all the atom machinery (family cache, swr,
766
+ // invalidation, structural sharing). Note: unlike the old tanstack `streamedQuery`, the result
767
+ // appears once the stream completes, not incrementally (stream queries are not used today).
363
768
  const streamQuery_: {
364
769
  <I, E, A, Request extends Req, Name extends string>(
365
770
  q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
366
771
  ): (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
772
+ } = (q: any) => {
773
+ const hook = query({
774
+ id: q.id,
775
+ options: q.options,
776
+ handler: (i: any) => Stream.runCollect(q.handler(i)).pipe(Effect.map((chunk) => [...chunk]))
777
+ } as any)
778
+ return (arg?: any) => hook(arg) as any
422
779
  }
423
780
 
424
781
  return streamQuery_
@@ -474,22 +831,14 @@ export function composeQueries<
474
831
  }
475
832
 
476
833
  export const useUpdateQuery = () => {
477
- const queryClient = useQueryClient()
834
+ const registry = injectRegistry()
478
835
 
479
836
  const f: {
480
- <I, A>(
481
- query: RequestHandlerWithInput<I, A, any, any, any, any>,
837
+ <I, A, E, R, Request extends Req, Name extends string>(
838
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
482
839
  input: I,
483
840
  updater: (data: NoInfer<A>) => NoInfer<A>
484
841
  ): 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)
492
- }
493
- }
842
+ } = (query, input, updater) => activeQueryCacheUpdater.update(registry, query, input, updater)
494
843
  return f
495
844
  }