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

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/commander.ts CHANGED
@@ -6,7 +6,7 @@ import { SupportedErrors } from "effect-app/client"
6
6
  import { OperationFailure, OperationSuccess } from "effect-app/Operations"
7
7
  import { isGeneratorFunction, wrapEffect } from "effect-app/utils"
8
8
  import { type Refinement } from "effect/Predicate"
9
- import { type AsyncResult } from "effect/unstable/reactivity/AsyncResult"
9
+ import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
10
10
  import { type FormatXMLElementFn, type PrimitiveType } from "intl-messageformat"
11
11
  import { computed, type ComputedRef, reactive, ref, toRaw } from "vue"
12
12
  import { Confirm } from "./confirm.js"
@@ -14,7 +14,53 @@ import { I18n } from "./intl.js"
14
14
  import { WithToast } from "./withToast.js"
15
15
 
16
16
  type IntlRecord = Record<string, PrimitiveType | FormatXMLElementFn<string, string>>
17
- type FnOptions<Id extends string, I18nCustomKey extends string, State extends IntlRecord | undefined> = {
17
+
18
+ /**
19
+ * Progress information surfaced by a stream command. Either a plain text label
20
+ * or a `{ text, percentage }` pair when concrete progress is known.
21
+ */
22
+ export type Progress = string | { readonly text: string; readonly percentage: number }
23
+
24
+ /**
25
+ * Options accepted when calling a stream mutation factory.
26
+ * Supplying `progress` causes the resulting command to expose `running`
27
+ * (the live AsyncResult ref) and `progress` (formatted loading info).
28
+ * When omitted, neither is exposed on the command.
29
+ */
30
+ export type StreamMutationCallOptions<A, E> = {
31
+ progress?: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined
32
+ }
33
+
34
+ /**
35
+ * The result of invoking a `mutateStream` factory: the `execute` function (or
36
+ * `Effect`, when the request takes no input) carries `id`, plus `running` and
37
+ * `progress` when the factory was called with a `progress` formatter. Pass
38
+ * directly to `Command.fn` / `Command.wrap` / `Command.wrapStream`, or invoke
39
+ * to run the stream.
40
+ */
41
+ type StreamMutationCallable<Id extends string, Arg, A, E, R> =
42
+ & (((arg: Arg) => Effect.Effect<any, E, R>) | Effect.Effect<any, E, R>)
43
+ & {
44
+ readonly id: Id
45
+ readonly _streamCallable: true
46
+ readonly running?: ComputedRef<AsyncResult.AsyncResult<A, E>>
47
+ readonly progress?: ComputedRef<Progress | undefined>
48
+ }
49
+
50
+ type StreamMutationFactory<Id extends string, Arg, A, E, R> =
51
+ & ((options?: StreamMutationCallOptions<A, E>) => StreamMutationCallable<Id, Arg, A, E, R>)
52
+ & { readonly id: Id; readonly _streamFactory: true }
53
+
54
+ const isStreamFactory = (x: unknown): x is StreamMutationFactory<string, any, any, any, any> =>
55
+ typeof x === "function" && (x as any)._streamFactory === true
56
+
57
+ const isStreamCallable = (x: unknown): x is StreamMutationCallable<string, any, any, any, any> =>
58
+ x !== null && x !== undefined && (x as any)._streamCallable === true
59
+ type FnOptions<
60
+ Id extends string,
61
+ I18nCustomKey extends string,
62
+ State extends IntlRecord | undefined
63
+ > = {
18
64
  i18nCustomKey?: I18nCustomKey
19
65
  /**
20
66
  * passed to the i18n formatMessage calls so you can use it in translation messagee
@@ -139,7 +185,19 @@ export declare namespace Commander {
139
185
  /** reactive */
140
186
  label: string
141
187
  /** reactive */
142
- result: AsyncResult<A, E>
188
+ result: AsyncResult.AsyncResult<A, E>
189
+ /**
190
+ * reactive – set when the command wraps a stream (`wrapStream` / `wrap` with `mutateStream`)
191
+ * or when the `progress` option is provided to `fn`.
192
+ * Reflects the live AsyncResult of the underlying stream.
193
+ */
194
+ running: AsyncResult.AsyncResult<any, any> | undefined
195
+ /**
196
+ * reactive – formatted progress info computed from `running` via the
197
+ * `progress` option. Useful as the loading state on a `CommandButton`.
198
+ * Undefined when no `progress` formatter was supplied.
199
+ */
200
+ progress: Progress | undefined
143
201
  /** reactive */
144
202
  waiting: boolean
145
203
  /** reactive */
@@ -1983,7 +2041,11 @@ const unregisterWait = (id: string) => {
1983
2041
  }
1984
2042
  }
1985
2043
 
1986
- const getStateValues = <const Id extends string, const I18nKey extends string, State extends IntlRecord | undefined>(
2044
+ const getStateValues = <
2045
+ const Id extends string,
2046
+ const I18nKey extends string,
2047
+ State extends IntlRecord | undefined
2048
+ >(
1987
2049
  options?: FnOptions<Id, I18nKey, State>
1988
2050
  ): ComputedRef<State> => {
1989
2051
  const state_ = options?.state
@@ -2035,11 +2097,17 @@ export class CommanderImpl<RT, RTHooks> {
2035
2097
  readonly makeCommand = <
2036
2098
  const Id extends string,
2037
2099
  const State extends IntlRecord | undefined,
2038
- const I18nKey extends string = Id
2100
+ const I18nKey extends string = Id,
2101
+ RunningA = unknown,
2102
+ RunningE = unknown
2039
2103
  >(
2040
2104
  id_: Id | { id: Id },
2041
2105
  options?: FnOptions<Id, I18nKey, State>,
2042
- errorDef?: Error
2106
+ errorDef?: Error,
2107
+ streamMeta?: {
2108
+ running?: ComputedRef<AsyncResult.AsyncResult<RunningA, RunningE>> | undefined
2109
+ progress?: ComputedRef<Progress | undefined> | undefined
2110
+ }
2043
2111
  ) => {
2044
2112
  const id = typeof id_ === "string" ? id_ : id_.id
2045
2113
  const state = getStateValues(options)
@@ -2224,6 +2292,12 @@ export class CommanderImpl<RT, RTHooks> {
2224
2292
 
2225
2293
  /** reactive */
2226
2294
  result,
2295
+ /** reactive – live AsyncResult of the underlying stream, exposed only when
2296
+ * the stream factory was called with a `progress` formatter */
2297
+ running: streamMeta?.running,
2298
+ /** reactive – formatted progress info for current `running` state, when `progress`
2299
+ * formatter was supplied to the stream factory */
2300
+ progress: streamMeta?.progress,
2227
2301
  /** reactive */
2228
2302
  waiting,
2229
2303
  /** reactive */
@@ -2325,14 +2399,41 @@ export class CommanderImpl<RT, RTHooks> {
2325
2399
  fn = <
2326
2400
  const Id extends string,
2327
2401
  const State extends IntlRecord = IntlRecord,
2328
- const I18nKey extends string = Id
2402
+ const I18nKey extends string = Id,
2403
+ RunningA = unknown,
2404
+ RunningE = unknown
2329
2405
  >(
2330
- id: Id | { id: Id },
2406
+ id:
2407
+ | Id
2408
+ | { id: Id }
2409
+ | StreamMutationCallable<Id, any, RunningA, RunningE, any>
2410
+ | StreamMutationFactory<Id, any, RunningA, RunningE, any>,
2331
2411
  options?: FnOptions<Id, I18nKey, State>
2332
2412
  ): Commander.Gen<RT | RTHooks, Id, I18nKey, State> & Commander.NonGen<RT | RTHooks, Id, I18nKey, State> & {
2333
2413
  state: Context.Service<`Commander.Command.${Id}.state`, State>
2334
- } =>
2335
- Object.assign(
2414
+ } => {
2415
+ // Resolve id and (optionally) per-build stream metadata.
2416
+ const resolvedId: Id = typeof id === "string" ? id : (id as { id: Id }).id
2417
+ const factory = isStreamFactory(id)
2418
+ const callable = !factory && isStreamCallable(id)
2419
+ const resolveStreamMeta = ():
2420
+ | {
2421
+ running?: ComputedRef<AsyncResult.AsyncResult<RunningA, RunningE>> | undefined
2422
+ progress?: ComputedRef<Progress | undefined> | undefined
2423
+ }
2424
+ | undefined =>
2425
+ {
2426
+ if (factory) {
2427
+ const c = id()
2428
+ return { running: c.running, progress: c.progress }
2429
+ }
2430
+ if (callable) {
2431
+ const c = id as StreamMutationCallable<Id, any, RunningA, RunningE, any>
2432
+ return { running: c.running, progress: c.progress }
2433
+ }
2434
+ return undefined
2435
+ }
2436
+ return Object.assign(
2336
2437
  (
2337
2438
  fn: any,
2338
2439
  ...combinators: any[]
@@ -2343,7 +2444,9 @@ export class CommanderImpl<RT, RTHooks> {
2343
2444
  const errorDef = new Error()
2344
2445
  Error.stackTraceLimit = limit
2345
2446
 
2346
- return this.makeCommand(id, options, errorDef)(
2447
+ const streamMeta = resolveStreamMeta()
2448
+
2449
+ return this.makeCommand(resolvedId, options, errorDef, streamMeta)(
2347
2450
  Effect.fnUntraced(
2348
2451
  // fnUntraced only supports generators as first arg, so we convert to generator if needed
2349
2452
  isGeneratorFunction(fn) ? fn : function*(...args) {
@@ -2353,13 +2456,14 @@ export class CommanderImpl<RT, RTHooks> {
2353
2456
  ) as any
2354
2457
  )
2355
2458
  },
2356
- makeBaseInfo(typeof id === "string" ? id : id.id, options),
2459
+ makeBaseInfo(resolvedId, options),
2357
2460
  {
2358
2461
  state: Context.Service<`Commander.Command.${Id}.state`, State>(
2359
- `Commander.Command.${typeof id === "string" ? id : id.id}.state`
2462
+ `Commander.Command.${resolvedId}.state`
2360
2463
  )
2361
2464
  }
2362
2465
  )
2466
+ }
2363
2467
 
2364
2468
  /** @deprecated */
2365
2469
  alt2: <
@@ -2464,10 +2568,28 @@ export class CommanderImpl<RT, RTHooks> {
2464
2568
  >(
2465
2569
  mutation:
2466
2570
  | { mutate: (arg: Arg) => Effect.Effect<A, E, R>; id: Id }
2467
- | ((arg: Arg) => Effect.Effect<A, E, R>) & { id: Id },
2571
+ | ((arg: Arg) => Effect.Effect<A, E, R>) & { id: Id }
2572
+ | StreamMutationFactory<Id, Arg, A, E, R>
2573
+ | {
2574
+ id: Id
2575
+ mutateStream:
2576
+ | StreamMutationFactory<Id, Arg, A, E, R>
2577
+ | StreamMutationCallable<Id, Arg, A, E, R>
2578
+ }
2579
+ | StreamMutationCallable<Id, Arg, A, E, R>,
2468
2580
  options?: FnOptions<Id, I18nKey, State>
2469
- ): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> =>
2470
- Object.assign(
2581
+ ): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> => {
2582
+ if (mutation !== null && typeof mutation === "object" && "mutateStream" in mutation) {
2583
+ return this.wrapStream(mutation as any, options) as any
2584
+ }
2585
+ if (isStreamCallable(mutation) || isStreamFactory(mutation)) {
2586
+ return this.wrapStream(mutation as any, options) as any
2587
+ }
2588
+ // At this point mutation is either { mutate, id } or (fn & { id })
2589
+ const callMutation = mutation as
2590
+ | { mutate: (arg: Arg) => Effect.Effect<A, E, R>; id: Id }
2591
+ | (((arg: Arg) => Effect.Effect<A, E, R>) & { id: Id })
2592
+ return Object.assign(
2471
2593
  (
2472
2594
  ...combinators: any[]
2473
2595
  ): any => {
@@ -2476,9 +2598,11 @@ export class CommanderImpl<RT, RTHooks> {
2476
2598
  Error.stackTraceLimit = 2
2477
2599
  const errorDef = new Error()
2478
2600
  Error.stackTraceLimit = limit
2479
- const mutate = "mutate" in mutation ? mutation.mutate : mutation
2601
+ const mutate = "mutate" in callMutation
2602
+ ? callMutation.mutate
2603
+ : callMutation
2480
2604
 
2481
- return this.makeCommand(mutation.id, options, errorDef)(
2605
+ return this.makeCommand(callMutation.id, options, errorDef)(
2482
2606
  Effect.fnUntraced(
2483
2607
  // fnUntraced only supports generators as first arg, so we convert to generator if needed
2484
2608
  isGeneratorFunction(mutate) ? mutate : function*(arg: Arg) {
@@ -2488,13 +2612,107 @@ export class CommanderImpl<RT, RTHooks> {
2488
2612
  ) as any
2489
2613
  )
2490
2614
  },
2491
- makeBaseInfo(mutation.id, options),
2615
+ makeBaseInfo(callMutation.id, options),
2492
2616
  {
2493
2617
  state: Context.Service<`Commander.Command.${Id}.state`, State>(
2494
- `Commander.Command.${mutation.id}.state`
2618
+ `Commander.Command.${callMutation.id}.state`
2495
2619
  )
2496
2620
  }
2497
2621
  )
2622
+ }
2623
+
2624
+ /**
2625
+ * Define a Command from a stream-type mutation (`mutateStream` factory).
2626
+ * The stream's reactive `AsyncResult` ref is exposed as `running` for independent progress tracking.
2627
+ * The command's own `result` reflects the execution outcome of the `execute` function.
2628
+ * Supports the same combinator pipeline as `wrap` (e.g. `withDefaultToast`).
2629
+ *
2630
+ * Each invocation of the resulting wrap call produces a fresh `[ref, execute]` pair
2631
+ * (the `mutateStream` factory is called once per build), so independent commands
2632
+ * don't share progress state.
2633
+ *
2634
+ * Accepts either:
2635
+ * - An object with `id` and `mutateStream` factory (e.g. a client entry)
2636
+ * - The `mutateStream` factory directly (callable, with `id`)
2637
+ * - An already-called factory result (`[resultRef, execute] & { id }`) — shared ref across builds
2638
+ *
2639
+ * @example
2640
+ * ```ts
2641
+ * // Via client entry (recommended):
2642
+ * const exportCmd = Command.wrapStream(client.myExport)()
2643
+ *
2644
+ * // Via factory directly:
2645
+ * const exportCmd = Command.wrapStream(client.myExport.mutateStream)()
2646
+ *
2647
+ * // Via already-called factory (shared ref):
2648
+ * const stream = client.myExport.mutateStream()
2649
+ * const exportCmd = Command.wrapStream(stream)()
2650
+ * ```
2651
+ */
2652
+ wrapStream = <
2653
+ const Id extends string,
2654
+ Arg,
2655
+ A,
2656
+ E,
2657
+ R,
2658
+ const State extends IntlRecord = IntlRecord,
2659
+ const I18nKey extends string = Id
2660
+ >(
2661
+ mutation:
2662
+ | {
2663
+ id: Id
2664
+ mutateStream:
2665
+ | StreamMutationFactory<Id, Arg, A, E, R>
2666
+ | StreamMutationCallable<Id, Arg, A, E, R>
2667
+ }
2668
+ | StreamMutationFactory<Id, Arg, A, E, R>
2669
+ | StreamMutationCallable<Id, Arg, A, E, R>,
2670
+ options?: FnOptions<Id, I18nKey, State>
2671
+ ): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> => {
2672
+ const id = mutation.id
2673
+ // Resolve `source` to the factory or already-invoked callable.
2674
+ const source: StreamMutationFactory<Id, Arg, A, E, R> | StreamMutationCallable<Id, Arg, A, E, R> =
2675
+ mutation !== null && typeof mutation === "object" && "mutateStream" in mutation
2676
+ ? (mutation.mutateStream as any)
2677
+ : (mutation as any)
2678
+ const resolveCallable = (): StreamMutationCallable<Id, Arg, A, E, R> =>
2679
+ (isStreamFactory(source)
2680
+ ? (source as StreamMutationFactory<Id, Arg, A, E, R>)()
2681
+ : source) as StreamMutationCallable<Id, Arg, A, E, R>
2682
+ return Object.assign(
2683
+ (...combinators: any[]): any => {
2684
+ // we capture the definition stack here, so we can append it to later stack traces
2685
+ const limit = Error.stackTraceLimit
2686
+ Error.stackTraceLimit = 2
2687
+ const errorDef = new Error()
2688
+ Error.stackTraceLimit = limit
2689
+
2690
+ // Fresh per build: invoke the factory once per command instance so each
2691
+ // wrap call gets its own state + execute pair. `running`/`progress`
2692
+ // are only surfaced when the factory was called with a `progress` formatter.
2693
+ const callable = resolveCallable()
2694
+ const mutate: (_arg: Arg) => Effect.Effect<any, E, R> = Effect.isEffect(callable)
2695
+ ? (_arg: Arg) => callable
2696
+ : callable as (arg: Arg) => Effect.Effect<any, E, R>
2697
+ const streamMeta = { running: callable.running, progress: callable.progress }
2698
+
2699
+ return this.makeCommand(id, options, errorDef, streamMeta)(
2700
+ Effect.fnUntraced(
2701
+ isGeneratorFunction(mutate) ? mutate : function*(arg: Arg) {
2702
+ return yield* mutate(arg)
2703
+ },
2704
+ ...combinators as [any]
2705
+ ) as any
2706
+ )
2707
+ },
2708
+ makeBaseInfo(id, options),
2709
+ {
2710
+ state: Context.Service<`Commander.Command.${Id}.state`, State>(
2711
+ `Commander.Command.${id}.state`
2712
+ )
2713
+ }
2714
+ )
2715
+ }
2498
2716
  }
2499
2717
 
2500
2718
  // @effect-diagnostics-next-line missingEffectServiceDependency:off
package/src/makeClient.ts CHANGED
@@ -8,8 +8,8 @@ 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
10
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
11
- import { type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
12
- import { type Commander, CommanderStatic } from "./commander.js"
11
+ import { computed, type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
12
+ import { type Commander, CommanderStatic, type Progress } from "./commander.js"
13
13
  import { type I18n } from "./intl.js"
14
14
  import { type CommanderResolved, makeUseCommand } from "./makeUseCommand.js"
15
15
  import { makeMutation, makeStreamMutation, type MutationOptionsBase, useMakeMutation } from "./mutate.js"
@@ -225,16 +225,71 @@ export type MutationWithExtensions<RT, Req> = Req extends
225
225
  : never
226
226
 
227
227
  /**
228
- * The `mutateStream` tuple for a stream-type request handler:
229
- * `[resultRef, execute]` where `execute` updates the ref live with each emitted value.
230
- * When the request declares a `final` schema, `execute` resolves with the last emitted value
231
- * typed as `Final`; otherwise it resolves with `void`.
228
+ * Options for invoking a `mutateStream` factory. Supplying `progress` produces
229
+ * a tuple-with-id that carries `running` (the live AsyncResult ref) and
230
+ * `progress` (a `ComputedRef<Progress | undefined>` formatted from each value),
231
+ * which `Command.fn` / `Command.wrapStream` surface as the command's `running`
232
+ * and `progress`. When omitted, the resulting command exposes neither.
233
+ */
234
+ export type MutateStreamCallOptions<A, E> = {
235
+ progress?: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined
236
+ }
237
+
238
+ /**
239
+ * The `mutateStream` factory for a stream-type request handler. Always invoke
240
+ * (optionally with `{ progress }`) to get a fresh callable `execute` — each call
241
+ * produces a new state + execute pair so independent invocations don't share
242
+ * state. The callable updates its underlying ref live with each emitted value
243
+ * and carries `id`, plus `running` and `progress` when the factory was called
244
+ * with a `progress` formatter. When the request declares a `final` schema,
245
+ * the callable resolves with the last emitted value typed as `Final`; otherwise
246
+ * it resolves with the success type. The factory itself carries the request
247
+ * `id` so it can be passed to `Command.fn` / `Command.wrapStream` directly.
232
248
  */
233
249
  export type StreamMutationWithExtensions<Req> = Req extends
234
- RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer _Id, infer Final>
235
- ? readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (input: I) => Effect.Effect<Final, never, R>]
236
- : Req extends RequestStreamHandler<infer A, infer E, infer R, infer _Request, infer _Id, infer Final>
237
- ? readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, Effect.Effect<Final, never, R>]
250
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id, infer Final> ?
251
+ & ((options?: MutateStreamCallOptions<A, E>) =>
252
+ & ((input: I) => Effect.Effect<Final, E, R>)
253
+ & {
254
+ readonly id: Id
255
+ readonly _streamCallable: true
256
+ readonly running?: ComputedRef<AsyncResult.AsyncResult<A, E>>
257
+ readonly progress?: ComputedRef<Progress | undefined>
258
+ })
259
+ & { readonly id: Id; readonly _streamFactory: true }
260
+ : Req extends RequestStreamHandler<infer A, infer E, infer R, infer _Request, infer Id, infer Final> ?
261
+ & ((options?: MutateStreamCallOptions<A, E>) =>
262
+ & Effect.Effect<Final, E, R>
263
+ & {
264
+ readonly id: Id
265
+ readonly _streamCallable: true
266
+ readonly running?: ComputedRef<AsyncResult.AsyncResult<A, E>>
267
+ readonly progress?: ComputedRef<Progress | undefined>
268
+ })
269
+ & { readonly id: Id; readonly _streamFactory: true }
270
+ : never
271
+
272
+ /**
273
+ * The pre-built `wrapStream` CommanderWrap for a stream-type request handler.
274
+ * The command's `result` and `running` are the live stream ref.
275
+ * Callable like `wrap`: `client.myExport.wrapStream()` returns the CommandOut.
276
+ */
277
+ export type StreamCommandWithExtensions<RT, Req> = Req extends
278
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id, infer _Final>
279
+ ? Commander.CommanderWrap<RT, Id, Id, undefined, I, A, E, R>
280
+ : Req extends RequestStreamHandler<infer A, infer E, infer R, infer _Request, infer Id, infer _Final>
281
+ ? Commander.CommanderWrap<RT, Id, Id, undefined, void, A, E, R>
282
+ : never
283
+
284
+ /**
285
+ * The `fn` builder for a stream-type request handler — identical to calling
286
+ * `Command.fn(id)` where `id` comes from the request.
287
+ */
288
+ export type StreamFnExtension<RT, Req> = Req extends
289
+ RequestStreamHandlerWithInput<infer _I, infer _A, infer _E, infer _R, infer _Request, infer Id, infer _Final>
290
+ ? Commander.CommanderFn<RT, Id, Id, undefined>
291
+ : Req extends RequestStreamHandler<infer _A, infer _E, infer _R, infer _Request, infer Id, infer _Final>
292
+ ? Commander.CommanderFn<RT, Id, Id, undefined>
238
293
  : never
239
294
 
240
295
  // we don't really care about the RT, as we are in charge of ensuring runtime safety anyway
@@ -767,6 +822,7 @@ export const makeClient = <RT_, RTHooks>(
767
822
  queryInvalidation?: (client: ClientFrom<M>) => QueryInvalidation<M>,
768
823
  invalidationResources?: InvalidationResourcesFor<M>
769
824
  ) => {
825
+ const Command = useCommand()
770
826
  const streamMutation = useStreamMutation()
771
827
  const invalidation = queryInvalidation?.(client)
772
828
  const queryResources = makeQueryResources(invalidationResources)
@@ -786,14 +842,36 @@ export const makeClient = <RT_, RTHooks>(
786
842
  })))
787
843
  : undefined
788
844
  const mergedInvalidation = mergeInvalidation(fromRequest, invalidation?.[key])
789
- ;(acc as any)[camelCase(key) + "Stream"] = streamMutation(client[key] as any, mergedInvalidation)
845
+ const smFactory = Object.assign(
846
+ (opts?: { progress?: (result: AsyncResult.AsyncResult<any, any>) => Progress | undefined }) => {
847
+ const [resultRef, execute] = streamMutation(client[key] as any, mergedInvalidation)
848
+ const extras: {
849
+ id: string
850
+ _streamCallable: true
851
+ running?: ComputedRef<AsyncResult.AsyncResult<any, any>>
852
+ progress?: ComputedRef<Progress | undefined>
853
+ } = { id: client[key].id, _streamCallable: true }
854
+ if (opts?.progress) {
855
+ const fmt = opts.progress
856
+ extras.running = resultRef
857
+ extras.progress = computed(() => fmt(resultRef.value))
858
+ }
859
+ return Object.assign(execute, extras)
860
+ },
861
+ { id: client[key].id, _streamFactory: true as const }
862
+ )
863
+ ;(acc as any)[camelCase(key) + "Stream"] = Object.assign(smFactory, {
864
+ fn: Command.fn(client[key].id)
865
+ })
790
866
  return acc
791
867
  },
792
868
  {} as {
793
869
  [
794
870
  Key in keyof typeof client as StreamHandler<typeof client[Key]> extends never ? never
795
871
  : `${ToCamel<string & Key>}Stream`
796
- ]: StreamMutationWithExtensions<StreamHandler<typeof client[Key]>>
872
+ ]:
873
+ & StreamMutationWithExtensions<StreamHandler<typeof client[Key]>>
874
+ & { fn: StreamFnExtension<RT | RTHooks, StreamHandler<typeof client[Key]>> }
797
875
  }
798
876
  )
799
877
  return streams
@@ -862,10 +940,30 @@ export const makeClient = <RT_, RTHooks>(
862
940
  })))
863
941
  : undefined
864
942
  const mergedInvalidation = mergeInvalidation(fromRequest, invalidation?.[key])
943
+ const streamMutFactory = Object.assign(
944
+ (opts?: { progress?: (result: AsyncResult.AsyncResult<any, any>) => Progress | undefined }) => {
945
+ const [resultRef, execute] = streamMutation(client[key] as any, mergedInvalidation)
946
+ const extras: {
947
+ id: string
948
+ _streamCallable: true
949
+ running?: ComputedRef<AsyncResult.AsyncResult<any, any>>
950
+ progress?: ComputedRef<Progress | undefined>
951
+ } = { id: client[key].id, _streamCallable: true }
952
+ if (opts?.progress) {
953
+ const fmt = opts.progress
954
+ extras.running = resultRef
955
+ extras.progress = computed(() => fmt(resultRef.value))
956
+ }
957
+ return Object.assign(execute, extras)
958
+ },
959
+ { id: client[key].id, _streamFactory: true as const }
960
+ )
865
961
  return {
866
962
  ...client[key],
867
963
  request: h_,
868
- mutateStream: streamMutation(client[key] as any, mergedInvalidation)
964
+ mutateStream: streamMutFactory,
965
+ wrapStream: Command.wrapStream(streamMutFactory),
966
+ fn: Command.fn(client[key].id)
869
967
  }
870
968
  })()
871
969
  : {
@@ -927,7 +1025,11 @@ export const makeClient = <RT_, RTHooks>(
927
1025
  & (CommandHandler<typeof client[Key]> extends never ? {}
928
1026
  : { mutate: MutationWithExtensions<RT | RTHooks, CommandHandler<typeof client[Key]>> })
929
1027
  & (StreamHandler<typeof client[Key]> extends never ? {}
930
- : { mutateStream: StreamMutationWithExtensions<StreamHandler<typeof client[Key]>> })
1028
+ : {
1029
+ mutateStream: StreamMutationWithExtensions<StreamHandler<typeof client[Key]>>
1030
+ wrapStream: StreamCommandWithExtensions<RT | RTHooks, StreamHandler<typeof client[Key]>>
1031
+ fn: StreamFnExtension<RT | RTHooks, StreamHandler<typeof client[Key]>>
1032
+ })
931
1033
  & { Input: typeof client[Key] extends RequestHandlerWithInput<infer I, any, any, any, any, any> ? I : never }
932
1034
  }
933
1035
  )
@@ -988,6 +1090,7 @@ export const makeClient = <RT_, RTHooks>(
988
1090
  // delay initialisation until first use...
989
1091
  fn: (...args: [any]) => useCommand().fn(...args),
990
1092
  wrap: (...args: [any]) => useCommand().wrap(...args),
1093
+ wrapStream: (...args: [any]) => useCommand().wrapStream(...args),
991
1094
  alt: (...args: [any]) => useCommand().alt(...args),
992
1095
  alt2: (...args: [any]) => useCommand().alt2(...args)
993
1096
  } as ReturnType<typeof useCommand>,
@@ -1025,6 +1128,8 @@ export interface CommandBase<I = void, A = void> {
1025
1128
  allowed: boolean
1026
1129
  action: string
1027
1130
  label: string
1131
+ /** formatted progress info for current `running` state, when `progress` was supplied */
1132
+ progress?: Progress | undefined
1028
1133
  }
1029
1134
 
1030
1135
  export interface EffectCommand<I = void, A = unknown, E = unknown> extends CommandBase<I, Fiber<A, E>> {}
@@ -5,7 +5,7 @@ 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" | "alt" | "alt2">
8
+ extends X<typeof CommanderStatic>, Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "wrapStream" | "alt" | "alt2">
9
9
  {
10
10
  }
11
11
 
package/src/mutate.ts CHANGED
@@ -420,7 +420,8 @@ export const useMakeMutation = () => {
420
420
  * success or failure.
421
421
  *
422
422
  * When the request declares a `final` schema, `execute` resolves with the last emitted value
423
- * typed as `Final`; otherwise it resolves with `void`.
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`.
424
425
  *
425
426
  * Must be called inside a Vue setup context (uses `useQueryClient` internally).
426
427
  */
@@ -437,7 +438,7 @@ export const makeStreamMutation = () => {
437
438
  ) => {
438
439
  const state = shallowRef<AsyncResult.AsyncResult<any, any>>(AsyncResult.initial())
439
440
 
440
- const runStream = (stream: Stream.Stream<any, any, any>, input?: unknown): Effect.Effect<any, never, any> => {
441
+ const runStream = (stream: Stream.Stream<any, any, any>, input?: unknown): Effect.Effect<any, any, any> => {
441
442
  const invCache = buildInvalidateCache(queryClient, self, mergedInvalidation)
442
443
  const keysRef = Ref.makeUnsafe<ReadonlyArray<InvalidationKey>>([])
443
444
  // V3: pass onAdded so each mid-stream metadata chunk triggers query
@@ -476,11 +477,14 @@ export const makeStreamMutation = () => {
476
477
  const lastValue = AsyncResult.isSuccess(current) ? current.value : undefined
477
478
  const invExit = exit._tag === "Success" ? Exit.succeed(lastValue) : exit
478
479
  const serverKeys = Ref.getUnsafe(keysRef)
479
- // Note: when the stream fails, `lastValue` is undefined. The failure is
480
- // communicated via the reactive `state` ref (AsyncResult.failure). The
481
- // execute effect always resolves successfully; callers should inspect the
482
- // ref to distinguish success from failure.
483
- return invCache(input, invExit, serverKeys).pipe(Effect.as(lastValue))
480
+ // Stream failures bubble through the execute effect's typed error
481
+ // channel. The reactive `state` ref still mirrors the failure as
482
+ // `AsyncResult.failure` for live progress UI.
483
+ return invCache(input, invExit, serverKeys).pipe(
484
+ Effect.flatMap(() =>
485
+ exit._tag === "Success" ? Effect.succeed(lastValue) : Effect.failCause(exit.cause)
486
+ )
487
+ )
484
488
  })
485
489
  )
486
490
  )