@effect-app/vue 4.0.0-beta.182 → 4.0.0-beta.184

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/makeClient.ts CHANGED
@@ -7,16 +7,19 @@ import type { ExtractModuleName, RequestHandler, RequestHandlers, RequestHandler
7
7
  import type { InvalidationCallback } from "effect-app/client/makeClient"
8
8
  import type * as ExitResult from "effect/Exit"
9
9
  import { type Fiber } from "effect/Fiber"
10
+ import * as Stream from "effect/Stream"
10
11
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
11
12
  import { computed, type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
12
13
  import { type Commander, CommanderStatic, type Progress } from "./commander.js"
13
14
  import { type I18n } from "./intl.js"
14
15
  import { type CommanderResolved, makeUseCommand } from "./makeUseCommand.js"
15
- import { makeMutation, makeStreamMutation, type MutationOptionsBase, useMakeMutation } from "./mutate.js"
16
- import { type CustomUndefinedInitialQueryOptions, makeQuery } from "./query.js"
16
+ import { makeMutation, makeStreamMutation, makeStreamMutation2, type MutationOptionsBase, useMakeMutation } from "./mutate.js"
17
+ import { type CustomUndefinedInitialQueryOptions, makeQuery, makeStreamQuery } from "./query.js"
17
18
  import { makeRunPromise } from "./runtime.js"
18
19
  import { type Toast } from "./toast.js"
19
20
 
21
+ export type { Progress }
22
+
20
23
  const mapHandler = <A, E, R, I = void, A2 = A, E2 = E, R2 = R>(
21
24
  handler: Effect.Effect<A, E, R> | ((i: I) => Effect.Effect<A, E, R>),
22
25
  map: (self: Effect.Effect<A, E, R>, i: I) => Effect.Effect<A2, E2, R2>
@@ -292,11 +295,42 @@ export type StreamFnExtension<RT, Req> = Req extends
292
295
  ? Commander.CommanderFn<RT, Id, Id, undefined>
293
296
  : never
294
297
 
298
+ /**
299
+ * The `streamFn` builder for a stream-type request handler, using the stream-specific overloads.
300
+ */
301
+ export type StreamFnStreamExtension<RT, Req> = Req extends
302
+ RequestStreamHandlerWithInput<infer _I, infer _A, infer _E, infer _R, infer _Request, infer Id, infer _Final>
303
+ ? Commander.StreamGen<RT, Id, Id, undefined> & Commander.NonGenStream<RT, Id, Id, undefined>
304
+ : Req extends RequestStreamHandler<infer _A, infer _E, infer _R, infer _Request, infer Id, infer _Final>
305
+ ? Commander.StreamGen<RT, Id, Id, undefined> & Commander.NonGenStream<RT, Id, Id, undefined>
306
+ : never
307
+
308
+ /**
309
+ * `mutateStream2` factory — like `mutateStream` but wraps per-invocation invalidation scaffolding
310
+ * into the stream itself (via `Stream.unwrap`) for use with `streamFn` combinators.
311
+ */
312
+ export type StreamMutation2WithExtensions<RT, Req> = Req extends
313
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id, infer _Final> ?
314
+ & ((input: I) => Stream.Stream<A, E, R>)
315
+ & {
316
+ readonly id: Id
317
+ readonly wrapStream: Commander.StreamGen<RT, Id, Id, undefined> & Commander.NonGenStream<RT, Id, Id, undefined>
318
+ }
319
+ : Req extends RequestStreamHandler<infer A, infer E, infer R, infer _Request, infer Id, infer _Final> ?
320
+ & Stream.Stream<A, E, R>
321
+ & {
322
+ readonly id: Id
323
+ readonly wrapStream: Commander.StreamGen<RT, Id, Id, undefined> & Commander.NonGenStream<RT, Id, Id, undefined>
324
+ }
325
+ : never
326
+
295
327
  // we don't really care about the RT, as we are in charge of ensuring runtime safety anyway
296
328
  // eslint-disable-next-line unused-imports/no-unused-vars
297
329
  declare const useQuery_: QueryImpl<any>["useQuery"]
298
330
  // eslint-disable-next-line unused-imports/no-unused-vars
299
331
  declare const useSuspenseQuery_: QueryImpl<any>["useSuspenseQuery"]
332
+ // eslint-disable-next-line unused-imports/no-unused-vars
333
+ declare const useStreamQuery_: QueryImpl<any>["useStreamQuery"]
300
334
 
301
335
  export interface ProjectResult<RT, I, B, E, R, Request extends Req, Id extends string> {
302
336
  request: (i: I) => Effect.Effect<B, E, R>
@@ -387,6 +421,32 @@ export type Queries<RT, Req> = Req extends
387
421
  : never
388
422
  : never
389
423
 
424
+ export interface StreamQueriesWithInput<Request extends Req, Id extends string, I, A, E> {
425
+ /**
426
+ * Stream helper for stream requests.
427
+ * Runs as a tracked Vue Query and returns reactive state with accumulated chunks.
428
+ * Data is an array of all chunks received so far.
429
+ */
430
+ streamQuery: ReturnType<typeof useStreamQuery_<I, E, A, Request, Id>>
431
+ }
432
+ export interface StreamQueriesWithoutInput<Request extends Req, Id extends string, A, E> {
433
+ /**
434
+ * Stream helper for stream requests.
435
+ * Runs as a tracked Vue Query and returns reactive state with accumulated chunks.
436
+ * Data is an array of all chunks received so far.
437
+ */
438
+ streamQuery: ReturnType<typeof useStreamQuery_<E, A, Request, Id>>
439
+ }
440
+
441
+ export type StreamQueries<RT, HandlerReq> = HandlerReq extends
442
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id, infer _Final>
443
+ ? Exclude<R, RT> extends never ? StreamQueriesWithInput<Request, Id, I, A, E>
444
+ : { streamQuery: MissingDependencies<RT, R> & {} }
445
+ : HandlerReq extends RequestStreamHandler<infer A, infer E, infer R, infer Request, infer Id, infer _Final>
446
+ ? Exclude<R, RT> extends never ? StreamQueriesWithoutInput<Request, Id, A, E>
447
+ : { streamQuery: MissingDependencies<RT, R> & {} }
448
+ : never
449
+
390
450
  const _useMutation = makeMutation()
391
451
 
392
452
  const wrapWithSpan = (self: { id: string; handler: any }, mut: any) => {
@@ -445,6 +505,7 @@ export type ClientFrom<M extends RequestsAny> = RequestHandlers<never, never, M,
445
505
  export class QueryImpl<R> {
446
506
  constructor(readonly getRuntime: () => Context.Context<R>) {
447
507
  this.useQuery = makeQuery(this.getRuntime)
508
+ this.useStreamQuery = makeStreamQuery(this.getRuntime)
448
509
  }
449
510
  /**
450
511
  * Effect results are passed to the caller, including errors.
@@ -452,6 +513,12 @@ export class QueryImpl<R> {
452
513
  */
453
514
  readonly useQuery: ReturnType<typeof makeQuery<R>>
454
515
 
516
+ /**
517
+ * Stream results are accumulated as an array of chunks and returned as reactive state.
518
+ * @deprecated use client helpers instead (.streamQuery())
519
+ */
520
+ readonly useStreamQuery: ReturnType<typeof makeStreamQuery<R>>
521
+
455
522
  /**
456
523
  * The difference with useQuery is that this function will return a Promise you can await in the Setup,
457
524
  * which ensures that either there always is a latest value, or an error occurs on load.
@@ -649,9 +716,13 @@ export const makeClient = <RT_, RTHooks>(
649
716
  let sm: ReturnType<typeof makeStreamMutation>
650
717
  const useStreamMutation = () => sm ??= makeStreamMutation()
651
718
 
719
+ let sm2: ReturnType<typeof makeStreamMutation2>
720
+ const useStreamMutation2 = () => sm2 ??= makeStreamMutation2()
721
+
652
722
  const query = new QueryImpl(getBaseRt)
653
723
  const useQuery = query.useQuery
654
724
  const useSuspenseQuery = query.useSuspenseQuery
725
+ const useStreamQuery = query.useStreamQuery
655
726
 
656
727
  const mergeInvalidation = (
657
728
  a?: MutationOptionsBase["queryInvalidation"],
@@ -695,15 +766,19 @@ export const makeClient = <RT_, RTHooks>(
695
766
  ) => {
696
767
  const queries = Struct.keys(client).reduce(
697
768
  (acc, key) => {
698
- if (client[key].Request.type !== "query") {
699
- return acc
769
+ const requestType = client[key].Request.type
770
+ if (requestType === "query") {
771
+ ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(client[key] as any), {
772
+ id: client[key].id
773
+ })
774
+ ;(acc as any)[camelCase(key) + "SuspenseQuery"] = Object.assign(useSuspenseQuery(client[key] as any), {
775
+ id: client[key].id
776
+ })
777
+ } else if (requestType === "stream") {
778
+ ;(acc as any)[camelCase(key) + "StreamQuery"] = Object.assign(useStreamQuery(client[key] as any), {
779
+ id: client[key].id
780
+ })
700
781
  }
701
- ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(client[key] as any), {
702
- id: client[key].id
703
- })
704
- ;(acc as any)[camelCase(key) + "SuspenseQuery"] = Object.assign(useSuspenseQuery(client[key] as any), {
705
- id: client[key].id
706
- })
707
782
  return acc
708
783
  },
709
784
  {} as
@@ -725,6 +800,12 @@ export const makeClient = <RT_, RTHooks>(
725
800
  QueryHandler<typeof client[Key]>
726
801
  >["suspense"]
727
802
  }
803
+ & {
804
+ [
805
+ Key in keyof typeof client as StreamHandler<typeof client[Key]> extends never ? never
806
+ : `${ToCamel<string & Key>}StreamQuery`
807
+ ]: StreamQueries<RT, StreamHandler<typeof client[Key]>>["streamQuery"]
808
+ }
728
809
  )
729
810
  return queries
730
811
  }
@@ -961,9 +1042,25 @@ export const makeClient = <RT_, RTHooks>(
961
1042
  return {
962
1043
  ...client[key],
963
1044
  request: h_,
1045
+ streamQuery: useStreamQuery(client[key] as any),
964
1046
  mutateStream: streamMutFactory,
965
1047
  wrapStream: Command.wrapStream(streamMutFactory),
966
- fn: Command.fn(client[key].id)
1048
+ fn: Command.fn(client[key].id),
1049
+ streamFn: useCommand().streamFn(client[key].id as any) as any,
1050
+ mutateStream2: (() => {
1051
+ const sm2Act = useStreamMutation2()(client[key] as any, mergedInvalidation)
1052
+ const originalHandler = (client[key] as any).handler
1053
+ const sm2Handler = Stream.isStream(originalHandler)
1054
+ ? (_input: any, _ctx: any) => sm2Act
1055
+ : (input: any, _ctx: any) => (sm2Act as (i: any) => any)(input)
1056
+ return Object.assign(sm2Act, {
1057
+ id: client[key].id,
1058
+ wrapStream: (...combinators: any[]) => {
1059
+ const sfn = useCommand().streamFn(client[key].id as any) as any
1060
+ return sfn(sm2Handler, ...combinators)
1061
+ }
1062
+ })
1063
+ })()
967
1064
  }
968
1065
  })()
969
1066
  : {
@@ -1020,6 +1117,8 @@ export const makeClient = <RT_, RTHooks>(
1020
1117
  & QueryRequestWithExtensions<QueryHandler<typeof client[Key]>>
1021
1118
  & Queries<RT, QueryHandler<typeof client[Key]>>
1022
1119
  & QueryProjection<RT, QueryHandler<typeof client[Key]>>)
1120
+ & (StreamHandler<typeof client[Key]> extends never ? {}
1121
+ : StreamQueries<RT, StreamHandler<typeof client[Key]>>)
1023
1122
  & (CommandHandler<typeof client[Key]> extends never ? {}
1024
1123
  : CommandRequestWithExtensions<RT | RTHooks, CommandHandler<typeof client[Key]>>)
1025
1124
  & (CommandHandler<typeof client[Key]> extends never ? {}
@@ -1029,6 +1128,8 @@ export const makeClient = <RT_, RTHooks>(
1029
1128
  mutateStream: StreamMutationWithExtensions<StreamHandler<typeof client[Key]>>
1030
1129
  wrapStream: StreamCommandWithExtensions<RT | RTHooks, StreamHandler<typeof client[Key]>>
1031
1130
  fn: StreamFnExtension<RT | RTHooks, StreamHandler<typeof client[Key]>>
1131
+ streamFn: StreamFnStreamExtension<RT | RTHooks, StreamHandler<typeof client[Key]>>
1132
+ mutateStream2: StreamMutation2WithExtensions<RT | RTHooks, StreamHandler<typeof client[Key]>>
1032
1133
  })
1033
1134
  & { Input: typeof client[Key] extends RequestHandlerWithInput<infer I, any, any, any, any, any> ? I : never }
1034
1135
  }
@@ -1091,6 +1192,7 @@ export const makeClient = <RT_, RTHooks>(
1091
1192
  fn: (...args: [any]) => useCommand().fn(...args),
1092
1193
  wrap: (...args: [any]) => useCommand().wrap(...args),
1093
1194
  wrapStream: (...args: [any]) => useCommand().wrapStream(...args),
1195
+ streamFn: (...args: [any]) => useCommand().streamFn(...args),
1094
1196
  alt: (...args: [any]) => useCommand().alt(...args),
1095
1197
  alt2: (...args: [any]) => useCommand().alt2(...args)
1096
1198
  } as ReturnType<typeof useCommand>,
@@ -1121,7 +1223,7 @@ export type ToCamel<S extends string | number | symbol> = S extends string
1121
1223
  : Uncapitalize<S>
1122
1224
  : never
1123
1225
 
1124
- export interface CommandBase<I = void, A = void> {
1226
+ export interface CommandBase<I = void, A = void, RA = unknown, RE = unknown> {
1125
1227
  handle: (input: I) => A
1126
1228
  waiting: boolean
1127
1229
  blocked: boolean
@@ -1130,9 +1232,11 @@ export interface CommandBase<I = void, A = void> {
1130
1232
  label: string
1131
1233
  /** formatted progress info for current `running` state, when `progress` was supplied */
1132
1234
  progress?: Progress | undefined
1235
+ /** reactive result state, available on stream-backed commands */
1236
+ result?: AsyncResult.AsyncResult<RA, RE>
1133
1237
  }
1134
1238
 
1135
- export interface EffectCommand<I = void, A = unknown, E = unknown> extends CommandBase<I, Fiber<A, E>> {}
1239
+ export interface EffectCommand<I = void, A = unknown, E = unknown> extends CommandBase<I, Fiber<A, E>, A, E> {}
1136
1240
 
1137
1241
  export interface CommandFromRequest<I extends { readonly make: (...args: any[]) => any }, A = unknown, E = unknown>
1138
1242
  extends EffectCommand<RequestInputFromMake<I>, A, E>
@@ -5,7 +5,9 @@ type X<X> = X
5
5
 
6
6
  // helps retain JSDoc
7
7
  export interface CommanderResolved<RT, RTHooks>
8
- extends X<typeof CommanderStatic>, Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "wrapStream" | "alt" | "alt2">
8
+ extends
9
+ X<typeof CommanderStatic>,
10
+ Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "wrapStream" | "streamFn" | "alt" | "alt2">
9
11
  {
10
12
  }
11
13
 
package/src/mutate.ts CHANGED
@@ -414,17 +414,56 @@ export const useMakeMutation = () => {
414
414
  }
415
415
 
416
416
  /**
417
- * Like `makeMutation`, but for stream-type request handlers.
418
- * Returns a `[ref, execute]` tuple where `ref` is a reactive `AsyncResult` updated per
419
- * stream element. Queries are invalidated once when the stream finishes, regardless of
420
- * success or failure.
417
+ * Like `makeStreamMutation`, but returns an `Effect<Stream>` per invocation instead of a
418
+ * `[ref, execute]` tuple. The outer Effect sets up per-invocation invalidation scaffolding
419
+ * and returns a stream that triggers query invalidation via `Stream.ensuring` when it completes.
421
420
  *
422
- * When the request declares a `final` schema, `execute` resolves with the last emitted value
423
- * typed as `Final`; otherwise it resolves with the last emitted value typed as the success type.
424
- * Stream failures bubble through the execute effect's typed error channel `E`.
421
+ * Use this with `streamFn` / `Command.streamFn(id)(mutateStream2Handler, ...combinators)` so that
422
+ * the command manages its own reactive state internally. Unlike `makeStreamMutation`, no external
423
+ * reactive result ref is created.
425
424
  *
426
425
  * Must be called inside a Vue setup context (uses `useQueryClient` internally).
427
426
  */
427
+ export const makeStreamMutation2 = () => {
428
+ const queryClient = useQueryClient()
429
+
430
+ return (
431
+ self: {
432
+ id: string
433
+ options?: ClientForOptions
434
+ handler: Stream.Stream<any, any, any> | ((i: any) => Stream.Stream<any, any, any>)
435
+ },
436
+ mergedInvalidation?: MutationOptionsBase["queryInvalidation"]
437
+ ) => {
438
+ const invCache = buildInvalidateCache(queryClient, self, mergedInvalidation)
439
+
440
+ const makeInvocationEffect = (input: unknown, source: Stream.Stream<any, any, any>) =>
441
+ Effect.gen(function*() {
442
+ const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
443
+ const invKeys = makeInvalidationKeysService(keysRef, (key) => invCache(input, Exit.succeed(undefined), [key]))
444
+ const lastRef = yield* Ref.make<any>(undefined)
445
+ return source.pipe(
446
+ Stream.provideService(InvalidationKeysFromServer, invKeys),
447
+ Stream.tap((v) => Ref.set(lastRef, v)),
448
+ Stream.ensuring(
449
+ Effect.gen(function*() {
450
+ const lastValue = yield* Ref.get(lastRef)
451
+ const serverKeys = yield* Ref.get(keysRef)
452
+ yield* invCache(input, Exit.succeed(lastValue), serverKeys)
453
+ })
454
+ )
455
+ )
456
+ })
457
+
458
+ const handler = self.handler
459
+ const act = Stream.isStream(handler)
460
+ ? Stream.unwrap(makeInvocationEffect(undefined, handler))
461
+ : (i: any) => Stream.unwrap(makeInvocationEffect(i, (handler as (i: any) => Stream.Stream<any, any, any>)(i)))
462
+
463
+ return act
464
+ }
465
+ }
466
+
428
467
  export const makeStreamMutation = () => {
429
468
  const queryClient = useQueryClient()
430
469
 
package/src/query.ts CHANGED
@@ -2,11 +2,16 @@
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-call */
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-return */
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
5
- import { type DefaultError, type Enabled, 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 { Array, Cause, type Context, Effect, Option, S } from "effect-app"
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"
7
+ import { Array, Cause, type Context, Effect, Exit, Option, S } from "effect-app"
7
8
  import { makeQueryKey, type Req } from "effect-app/client"
8
- import type { RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
9
+ import type { RequestHandler, RequestHandlerWithInput, RequestStreamHandler, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
9
10
  import { CauseException, ServiceUnavailableError } from "effect-app/client/errors"
11
+ import * as Channel from "effect/Channel"
12
+ import * as Pull from "effect/Pull"
13
+ import * as Scope from "effect/Scope"
14
+ import type * as Stream from "effect/Stream"
10
15
  import { type Span } from "effect/Tracer"
11
16
  import { isHttpClientError } from "effect/unstable/http/HttpClientError"
12
17
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
@@ -75,6 +80,64 @@ export interface CustomDefinedPlaceholderQueryOptions<
75
80
  | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
76
81
  }
77
82
 
83
+ function swrToQuery<E, A>(r: {
84
+ error: CauseException<E> | undefined
85
+ data: A | undefined
86
+ isValidating: boolean
87
+ }): AsyncResult.AsyncResult<A, E> {
88
+ if (r.error !== undefined) {
89
+ return AsyncResult.failureWithPrevious(
90
+ r.error.originalCause,
91
+ {
92
+ previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
93
+ waiting: r.isValidating
94
+ }
95
+ )
96
+ }
97
+ if (r.data !== undefined) {
98
+ return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
99
+ }
100
+
101
+ return AsyncResult.initial(r.isValidating)
102
+ }
103
+
104
+ function streamToAsyncIterableWithCauseException<A, E, R>(
105
+ self: Stream.Stream<A, E, R>,
106
+ context: Context.Context<R>,
107
+ id: string
108
+ ): AsyncIterable<A> {
109
+ return {
110
+ [Symbol.asyncIterator]() {
111
+ const runPromise = Effect.runPromiseWith(context)
112
+ const runPromiseExit = Effect.runPromiseExitWith(context)
113
+ const scope = Scope.makeUnsafe()
114
+ let pull: any
115
+ let currentIter: Iterator<A> | undefined
116
+ return {
117
+ async next(): Promise<IteratorResult<A>> {
118
+ if (currentIter) {
119
+ const next = currentIter.next()
120
+ if (!next.done) return next
121
+ currentIter = undefined
122
+ }
123
+ pull ??= await runPromise(Channel.toPullScoped((self as any).channel, scope))
124
+ const exit = await runPromiseExit(pull)
125
+ if (Exit.isSuccess(exit)) {
126
+ currentIter = (exit.value as any)[Symbol.iterator]()
127
+ return currentIter!.next()
128
+ } else if (Pull.isDoneCause((exit as any).cause)) {
129
+ return { done: true, value: undefined }
130
+ }
131
+ throw new CauseException((exit as any).cause, id)
132
+ },
133
+ return(_) {
134
+ return runPromise(Effect.as(Scope.close(scope, Exit.void), { done: true, value: undefined }) as any)
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
78
141
  export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
79
142
  const useQuery_: {
80
143
  <I, A, E, Request extends Req, Name extends string>(
@@ -228,27 +291,6 @@ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
228
291
  ] as any
229
292
  }
230
293
 
231
- function swrToQuery<E, A>(r: {
232
- error: CauseException<E> | undefined
233
- data: A | undefined
234
- isValidating: boolean
235
- }): AsyncResult.AsyncResult<A, E> {
236
- if (r.error !== undefined) {
237
- return AsyncResult.failureWithPrevious(
238
- r.error.originalCause,
239
- {
240
- previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
241
- waiting: r.isValidating
242
- }
243
- )
244
- }
245
- if (r.data !== undefined) {
246
- return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
247
- }
248
-
249
- return AsyncResult.initial(r.isValidating)
250
- }
251
-
252
294
  const useQuery: {
253
295
  /**
254
296
  * Effect results are passed to the caller, including errors.
@@ -351,6 +393,88 @@ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
351
393
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
352
394
  export interface MakeQuery2<R> extends ReturnType<typeof makeQuery<R>> {}
353
395
 
396
+ type StreamQueryResult<A, E> = readonly [
397
+ ComputedRef<AsyncResult.AsyncResult<A[], E>>,
398
+ ComputedRef<A[] | undefined>,
399
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<A[], CauseException<E>>, never, never>,
400
+ UseQueryReturnType<any, any>
401
+ ]
402
+
403
+ export const makeStreamQuery = <R>(getRuntime: () => Context.Context<R>) => {
404
+ const streamQuery_: {
405
+ <E, A, Request extends Req, Name extends string>(
406
+ q: RequestStreamHandler<A, E, R, Request, Name>
407
+ ): () => StreamQueryResult<A, E>
408
+ <Arg, E, A, Request extends Req, Name extends string>(
409
+ q: RequestStreamHandlerWithInput<Arg, A, E, R, Request, Name>
410
+ ): (arg: Arg | WatchSource<Arg>) => StreamQueryResult<A, E>
411
+ } = (q: any) => (arg?: any) => {
412
+ const context = getRuntime()
413
+ const arr = arg
414
+ const req: { value: any } = !arg
415
+ ? undefined as any
416
+ : typeof arr === "function"
417
+ ? ({
418
+ get value() {
419
+ return arr()
420
+ }
421
+ })
422
+ : ref(arg)
423
+ const queryKey = makeQueryKey(q)
424
+ const handler = q.handler
425
+ const isWithInput = typeof handler === "function"
426
+
427
+ const r = useTanstackQuery<any[], CauseException<any>, any[]>(
428
+ {
429
+ throwOnError: false,
430
+ retry: (retryCount: number, error: unknown) => {
431
+ if (error instanceof CauseException) {
432
+ if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
433
+ return false
434
+ }
435
+ }
436
+ return retryCount < 5
437
+ },
438
+ queryKey: isWithInput ? [...queryKey, req] : queryKey,
439
+ queryFn: streamedQuery({
440
+ streamFn: () => {
441
+ const stream = isWithInput
442
+ ? handler(req.value)
443
+ : handler
444
+ return streamToAsyncIterableWithCauseException(stream, context, q.id)
445
+ }
446
+ })
447
+ }
448
+ )
449
+
450
+ const latestSuccess = shallowRef<any[]>()
451
+ const result = computed((): AsyncResult.AsyncResult<any[], any> =>
452
+ swrToQuery({
453
+ error: r.error.value ?? undefined,
454
+ data: r.data.value === undefined ? latestSuccess.value : r.data.value,
455
+ isValidating: r.isFetching.value
456
+ })
457
+ )
458
+ watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
459
+
460
+ return [
461
+ result,
462
+ computed(() => latestSuccess.value),
463
+ (options?: RefetchOptions) =>
464
+ Effect.currentSpan.pipe(
465
+ Effect.orElseSucceed(() => null),
466
+ Effect.flatMap((span) => Effect.promise(() => r.refetch({ ...options, updateMeta: { span } })))
467
+ ),
468
+ r
469
+ ] as any
470
+ }
471
+
472
+ return streamQuery_
473
+ }
474
+
475
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
476
+ export interface MakeStreamQuery2<R> extends ReturnType<typeof makeStreamQuery<R>> {}
477
+
354
478
  function orPrevious<E, A>(result: AsyncResult.AsyncResult<A, E>) {
355
479
  return AsyncResult.isFailure(result) && Option.isSome(result.previousSuccess)
356
480
  ? AsyncResult.success(result.previousSuccess.value, { waiting: result.waiting })