@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/commander.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { asResult, deepToRaw, type MissingDependencies, reportRuntimeError } from "@effect-app/vue"
2
+ import { asResult, asStreamResult, deepToRaw, type MissingDependencies, reportRuntimeError } from "@effect-app/vue"
3
3
  import { reportMessage } from "@effect-app/vue/errorReporter"
4
4
  import { Cause, Context, Effect, type Exit, type Fiber, flow, Layer, Match, MutableHashMap, Option, Predicate, S } from "effect-app"
5
5
  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 * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
9
+ import * as Stream from "effect/Stream"
10
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
10
11
  import { type FormatXMLElementFn, type PrimitiveType } from "intl-messageformat"
11
12
  import { computed, type ComputedRef, reactive, ref, toRaw } from "vue"
12
13
  import { Confirm } from "./confirm.js"
13
14
  import { I18n } from "./intl.js"
15
+ import { CurrentToastId, Toast } from "./toast.js"
14
16
  import { WithToast } from "./withToast.js"
15
17
 
16
18
  type IntlRecord = Record<string, PrimitiveType | FormatXMLElementFn<string, string>>
@@ -125,6 +127,35 @@ export class CommandContext extends Context.Service<CommandContext, {
125
127
  "CommandContext"
126
128
  ) {}
127
129
 
130
+ /**
131
+ * Service available inside `streamFn` stream handlers that lets you imperatively push
132
+ * progress updates to the command's reactive `progress` ref.
133
+ *
134
+ * Use `Command.mapProgress(fn)` or `Command.updateProgress(progress)` to interact with this service.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * // Using mapProgress (recommended) — applied as a stream pipe operator:
139
+ * const exportCmd = Command.streamFn("exportData")(
140
+ * function*(arg, ctx) {
141
+ * return makeExportStream(arg.id).pipe(
142
+ * Command.mapProgress((r) =>
143
+ * AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
144
+ * ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
145
+ * : undefined
146
+ * )
147
+ * )
148
+ * }
149
+ * )
150
+ * // exportCmd.progress is updated for every OperationProgress event
151
+ * ```
152
+ */
153
+ export class CommandProgress extends Context.Reference<{
154
+ readonly update: (progress: Progress | undefined) => Effect.Effect<void>
155
+ }>("Commander.CommandProgress", {
156
+ defaultValue: () => ({ update: (_progress: Progress | undefined): Effect.Effect<void> => Effect.void })
157
+ }) {}
158
+
128
159
  export type EmitWithCallback<A, Event extends string> = (event: Event, value: A, onDone: () => void) => void
129
160
 
130
161
  /**
@@ -1716,6 +1747,132 @@ export declare namespace Commander {
1716
1747
  ) => Eff
1717
1748
  ): CommandOutHelper<Arg, Eff, Id, I18nKey, State>
1718
1749
  }
1750
+
1751
+ /**
1752
+ * Type for `streamFn` — generator overload where the body yields Effects and returns a `Stream`.
1753
+ * `waiting` stays `true` while the stream is running, and updates the `result` ref per emitted value.
1754
+ */
1755
+ export type StreamGen<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
1756
+ <
1757
+ Eff extends Effect.Yieldable<any, any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
1758
+ SA,
1759
+ SE,
1760
+ SR,
1761
+ Arg = void
1762
+ >(
1763
+ body: (
1764
+ arg: Arg,
1765
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1766
+ ) => Generator<Eff, Stream.Stream<SA, SE, SR>, never>
1767
+ ): CommandOut<
1768
+ Arg,
1769
+ SA,
1770
+ | SE
1771
+ | ([Eff] extends [never] ? never
1772
+ : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
1773
+ : never),
1774
+ | SR
1775
+ | ([Eff] extends [never] ? never
1776
+ : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
1777
+ : never),
1778
+ Id,
1779
+ I18nKey,
1780
+ State
1781
+ >
1782
+ <
1783
+ Eff extends Effect.Yieldable<any, any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
1784
+ SA,
1785
+ SE,
1786
+ SR,
1787
+ B,
1788
+ Arg = void
1789
+ >(
1790
+ body: (
1791
+ arg: Arg,
1792
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1793
+ ) => Generator<Eff, Stream.Stream<SA, SE, SR>, never>,
1794
+ a: (
1795
+ _: Effect.Effect<
1796
+ Stream.Stream<SA, SE, SR>,
1797
+ ([Eff] extends [never] ? never
1798
+ : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
1799
+ : never),
1800
+ ([Eff] extends [never] ? never
1801
+ : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
1802
+ : never)
1803
+ >,
1804
+ arg: ArgForCombinator<Arg>,
1805
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1806
+ ) => B
1807
+ ): B extends Stream.Stream<infer SA2, infer SE2, infer SR2> ? CommandOut<Arg, SA2, SE2, SR2, Id, I18nKey, State>
1808
+ : B extends Effect.Effect<Stream.Stream<infer SA2, infer SE2, infer SR2>, infer EE2, infer ER2>
1809
+ ? CommandOut<Arg, SA2, SE2 | EE2, SR2 | ER2, Id, I18nKey, State>
1810
+ : never
1811
+ }
1812
+
1813
+ /**
1814
+ * Type for `streamFn` — non-generator overload accepting a function that returns a `Stream` directly,
1815
+ * or an `Effect` that resolves to a `Stream`.
1816
+ */
1817
+ export type NonGenStream<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
1818
+ <
1819
+ SA,
1820
+ SE,
1821
+ SR extends RT | CommandContext | `Commander.Command.${Id}.state`,
1822
+ Arg = void
1823
+ >(
1824
+ body: (arg: Arg, ctx: CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>
1825
+ ): CommandOut<Arg, SA, SE, SR, Id, I18nKey, State>
1826
+ <
1827
+ SA,
1828
+ SE,
1829
+ SR,
1830
+ A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
1831
+ Arg = void
1832
+ >(
1833
+ body: (arg: Arg, ctx: CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>,
1834
+ a: (
1835
+ _: Stream.Stream<SA, SE, SR>,
1836
+ arg: ArgForCombinator<Arg>,
1837
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1838
+ ) => A
1839
+ ): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
1840
+ <
1841
+ SA,
1842
+ SE,
1843
+ SR,
1844
+ EE,
1845
+ ER extends RT | CommandContext | `Commander.Command.${Id}.state`,
1846
+ Arg = void
1847
+ >(
1848
+ body: (
1849
+ arg: Arg,
1850
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1851
+ ) => Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>
1852
+ ): CommandOut<Arg, SA, SE | EE, SR | ER, Id, I18nKey, State>
1853
+ <
1854
+ SA,
1855
+ SE,
1856
+ SR,
1857
+ EE,
1858
+ ER,
1859
+ B,
1860
+ Arg = void
1861
+ >(
1862
+ body: (
1863
+ arg: Arg,
1864
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1865
+ ) => Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>,
1866
+ a: (
1867
+ _: Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>,
1868
+ arg: ArgForCombinator<Arg>,
1869
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1870
+ ) => B
1871
+ ): B extends Stream.Stream<infer SA2, infer SE2, infer SR2> ? CommandOut<Arg, SA2, SE2, SR2, Id, I18nKey, State>
1872
+ : B extends Effect.Effect<Stream.Stream<infer SA2, infer SE2, infer SR2>, infer EE2, infer ER2>
1873
+ ? CommandOut<Arg, SA2, SE2 | EE2, SR2 | ER2, Id, I18nKey, State>
1874
+ : never
1875
+ }
1719
1876
  }
1720
1877
 
1721
1878
  type ErrorRenderer<E, Args extends readonly any[]> = (e: E, action: string, ...args: Args) => string | undefined
@@ -1838,6 +1995,66 @@ export const CommanderStatic = {
1838
1995
  ) =>
1839
1996
  (self: In, arg: Arg, arg2: Arg2) => cb(arg, arg2)(self),
1840
1997
 
1998
+ /**
1999
+ * Stream pipe operator that maps each emitted value to a `Progress` entry and updates the
2000
+ * command's reactive `progress` ref via the `CommandProgress` service.
2001
+ *
2002
+ * The mapper receives an `AsyncResult<A, E>` (each emitted value wrapped as
2003
+ * `AsyncResult.success(value, { waiting: true })`), matching the same shape used by
2004
+ * `CommandButton`'s `:progress-map` prop.
2005
+ *
2006
+ * Designed to be used inside a `streamFn` handler (either directly with `.pipe()`, or as
2007
+ * a combinator argument):
2008
+ *
2009
+ * @example
2010
+ * ```ts
2011
+ * // Inside the handler body:
2012
+ * Command.streamFn("exportData")(function*(arg, ctx) {
2013
+ * return makeExportStream(arg.id).pipe(
2014
+ * Command.mapProgress((r) =>
2015
+ * AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
2016
+ * ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
2017
+ * : undefined
2018
+ * )
2019
+ * )
2020
+ * })
2021
+ *
2022
+ * // Or as a stream combinator argument:
2023
+ * Command.streamFn("exportData")(
2024
+ * function*(arg, ctx) { return makeExportStream(arg.id) },
2025
+ * (s) => s.pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress" ? { text: `${r.value.completed}/${r.value.total}` } : undefined))
2026
+ * )
2027
+ * ```
2028
+ */
2029
+ mapProgress:
2030
+ <A, E>(fn: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined) =>
2031
+ <R>(stream: Stream.Stream<A, E, R>): Stream.Stream<A, E, R> =>
2032
+ stream.pipe(
2033
+ Stream.tap((v) => {
2034
+ const p = fn(AsyncResult.success(v, { waiting: true }))
2035
+ return p !== undefined ? CommandProgress.use((s) => s.update(p)) : Effect.void
2036
+ })
2037
+ ),
2038
+
2039
+ /**
2040
+ * Imperatively push a progress update from inside a `streamFn` handler.
2041
+ * Requires `CommandProgress` to be in context — provided automatically for all `streamFn` streams.
2042
+ *
2043
+ * @example
2044
+ * ```ts
2045
+ * // In a streamFn handler:
2046
+ * stream.pipe(
2047
+ * Stream.tap((event) =>
2048
+ * event._tag === "OperationProgress"
2049
+ * ? Command.updateProgress({ text: `${event.completed}/${event.total}`, percentage: event.completed / event.total * 100 })
2050
+ * : Effect.void
2051
+ * )
2052
+ * )
2053
+ * ```
2054
+ */
2055
+ updateProgress: (progress: Progress | undefined): Effect.Effect<void> =>
2056
+ CommandProgress.use((s) => s.update(progress)),
2057
+
1841
2058
  /** Version of @see confirmOrInterrupt that automatically includes the action name in the default messages */
1842
2059
  confirmOrInterrupt: Effect.fnUntraced(function*(
1843
2060
  message: string | undefined = undefined
@@ -1981,6 +2198,167 @@ export const CommanderStatic = {
1981
2198
  )
1982
2199
  }),
1983
2200
 
2201
+ /**
2202
+ * Stream-aware version of `withDefaultToast`. Use this as a combinator inside `streamFn`
2203
+ * (or anywhere a `Stream` needs toast lifecycle handling) instead of `withDefaultToast`.
2204
+ *
2205
+ * Unlike `withDefaultToast` (which only wraps the initial `Effect`), this combinator:
2206
+ * - Shows the "waiting" toast **before** the stream starts
2207
+ * - Shows the "success" toast only **after** the stream drains fully without error
2208
+ * - Shows the "failure" toast if the stream errors or fails
2209
+ *
2210
+ * Accepts either a `Stream<A, E, R>` or an `Effect<Stream<A, E, R>, EE, ER>` as input,
2211
+ * so it works in both the `NonGenStream` and `StreamGen` overloads of `streamFn`.
2212
+ *
2213
+ * @example
2214
+ * ```ts
2215
+ * Command.streamFn("exportData")(
2216
+ * function*(arg, ctx) { return makeExportStream(arg.id) },
2217
+ * Command.withDefaultToastStream()
2218
+ * )
2219
+ * ```
2220
+ */
2221
+ withDefaultToastStream: <A, E, R, Args extends Array<unknown>>(
2222
+ options?: {
2223
+ stableToastId?:
2224
+ | undefined
2225
+ | true
2226
+ | string
2227
+ | ((id: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => true | string | undefined)
2228
+ errorRenderer?: (e: E, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | undefined
2229
+ showSpanInfo?: false
2230
+ onWaiting?:
2231
+ | null
2232
+ | undefined
2233
+ | string
2234
+ | ((id: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | null | undefined)
2235
+ onSuccess?:
2236
+ | null
2237
+ | undefined
2238
+ | string
2239
+ | ((a: A, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | null | undefined)
2240
+ }
2241
+ ) =>
2242
+ (
2243
+ self: Stream.Stream<A, E, R> | Effect.Effect<Stream.Stream<A, E, R>, any, any>,
2244
+ ...args: Args
2245
+ ): Stream.Stream<A, E, R | I18n | Toast | CommandContext> => {
2246
+ const rawStream: Stream.Stream<A, E, R> = Stream.isStream(self)
2247
+ ? self
2248
+ : Stream.unwrap(self)
2249
+
2250
+ return Stream.unwrap(Effect.gen(function*() {
2251
+ const cc = yield* CommandContext
2252
+ const { intl } = yield* I18n
2253
+ const toast = yield* Toast
2254
+
2255
+ const customWaiting = cc.namespaced("waiting")
2256
+ const hasCustomWaiting = !!intl.messages[customWaiting]
2257
+ const customSuccess = cc.namespaced("success")
2258
+ const hasCustomSuccess = !!intl.messages[customSuccess]
2259
+ const customFailure = cc.namespaced("failure")
2260
+ const hasCustomFailure = !!intl.messages[customFailure]
2261
+
2262
+ const stableToastId: string | undefined = options?.stableToastId
2263
+ ? typeof options.stableToastId === "string"
2264
+ ? options.stableToastId
2265
+ : typeof options.stableToastId === "boolean"
2266
+ ? cc.id
2267
+ : typeof options.stableToastId === "function"
2268
+ ? (() => {
2269
+ const r = (options.stableToastId as (...a: any[]) => true | string | undefined)(cc.id, ...args)
2270
+ if (typeof r === "string") return r
2271
+ if (r === true) return cc.id
2272
+ return undefined
2273
+ })()
2274
+ : undefined
2275
+ : undefined
2276
+
2277
+ const baseTimeout = 3_000
2278
+
2279
+ const waitingMsg: string | null = options?.onWaiting === null
2280
+ ? null
2281
+ : typeof options?.onWaiting === "string"
2282
+ ? options.onWaiting
2283
+ : typeof options?.onWaiting === "function"
2284
+ ? (options.onWaiting as (...a: any[]) => string | null | undefined)(cc.id, ...args) ?? null
2285
+ : hasCustomWaiting
2286
+ ? intl.formatMessage({ id: customWaiting }, cc.state)
2287
+ : intl.formatMessage({ id: "handle.waiting" }, { action: cc.action })
2288
+
2289
+ const toastId: string | number | undefined = waitingMsg === null
2290
+ ? stableToastId
2291
+ : yield* toast.info(waitingMsg, { id: stableToastId ?? null })
2292
+
2293
+ const failureHandler = defaultFailureMessageHandler<E, [], never, never>(
2294
+ hasCustomFailure ? intl.formatMessage({ id: customFailure }, cc.state) : cc.action,
2295
+ options?.errorRenderer as ErrorRenderer<E, []> | undefined
2296
+ )
2297
+
2298
+ let lastValue: A | undefined = undefined
2299
+ let didFail = false
2300
+
2301
+ const composed = rawStream.pipe(
2302
+ Stream.tap((v) =>
2303
+ Effect.sync(() => {
2304
+ lastValue = v
2305
+ })
2306
+ ),
2307
+ Stream.tapCause(Effect.fnUntraced(function*(cause) {
2308
+ didFail = true
2309
+ if (Cause.hasInterruptsOnly(cause)) {
2310
+ if (toastId !== undefined) yield* toast.dismiss(toastId)
2311
+ return
2312
+ }
2313
+
2314
+ const spanInfo = options?.showSpanInfo !== false
2315
+ ? yield* Effect.currentSpan.pipe(
2316
+ Effect.map((span) => `\nTrace: ${span.traceId}\nSpan: ${span.spanId}`),
2317
+ Effect.orElseSucceed(() => "")
2318
+ )
2319
+ : ""
2320
+
2321
+ const t = yield* failureHandler(Cause.findErrorOption(cause))
2322
+ const opts = { timeout: baseTimeout * 2 }
2323
+
2324
+ if (typeof t === "object") {
2325
+ const message = t.message + spanInfo
2326
+ yield* t.level === "warn"
2327
+ ? toast.warning(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
2328
+ : toast.error(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
2329
+ } else {
2330
+ yield* toast.error(t + spanInfo, toastId !== undefined ? { ...opts, id: toastId } : opts)
2331
+ }
2332
+ }, Effect.uninterruptible)),
2333
+ Stream.ensuring(Effect.suspend(() => {
2334
+ if (didFail) return Effect.void
2335
+
2336
+ if (options?.onSuccess === null) return Effect.void
2337
+
2338
+ const successMsg: string | null = typeof options?.onSuccess === "string"
2339
+ ? options.onSuccess
2340
+ : typeof options?.onSuccess === "function"
2341
+ ? (options.onSuccess as (...a: any[]) => string | null | undefined)(lastValue, cc.action, ...args) ?? null
2342
+ : hasCustomSuccess
2343
+ ? intl.formatMessage({ id: customSuccess }, cc.state)
2344
+ : intl.formatMessage({ id: "handle.success" }, { action: cc.action })
2345
+ + (S.is(OperationSuccess)(lastValue) && lastValue.message ? "\n" + lastValue.message : "")
2346
+
2347
+ if (successMsg === null) return Effect.void
2348
+
2349
+ return toast.success(
2350
+ successMsg,
2351
+ toastId !== undefined ? { id: toastId, timeout: baseTimeout } : { timeout: baseTimeout }
2352
+ )
2353
+ }))
2354
+ )
2355
+
2356
+ return (toastId !== undefined
2357
+ ? composed.pipe(Stream.provideService(CurrentToastId, CurrentToastId.of({ toastId })))
2358
+ : composed) as unknown as Stream.Stream<A, E, R>
2359
+ }))
2360
+ },
2361
+
1984
2362
  /** borrowing the idea from Families in Effect Atom */
1985
2363
  family: <T extends object, Arg, ArgIn = Arg>(
1986
2364
  maker: (arg: Arg) => T,
@@ -2465,7 +2843,322 @@ export class CommanderImpl<RT, RTHooks> {
2465
2843
  )
2466
2844
  }
2467
2845
 
2846
+ /**
2847
+ * Internal factory for stream-backed commands. Accepts a handler that returns a `Stream` directly.
2848
+ * Services (`CommandContext`, `stateTag`) are provided to the stream via `Stream.provideServiceEffect`.
2849
+ */
2850
+ readonly makeStreamCommand = <
2851
+ const Id extends string,
2852
+ const State extends IntlRecord | undefined,
2853
+ const I18nKey extends string = Id
2854
+ >(
2855
+ id_: Id | { id: Id },
2856
+ options?: FnOptions<Id, I18nKey, State>,
2857
+ errorDef?: Error
2858
+ ) => {
2859
+ const id = typeof id_ === "string" ? id_ : id_.id
2860
+ const state = getStateValues(options)
2861
+
2862
+ return Object.assign(
2863
+ <Arg, SA, SE, SR>(
2864
+ handler: (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>
2865
+ ) => {
2866
+ const limit = Error.stackTraceLimit
2867
+ Error.stackTraceLimit = 2
2868
+ const localErrorDef = new Error()
2869
+ Error.stackTraceLimit = limit
2870
+ if (!errorDef) {
2871
+ errorDef = localErrorDef
2872
+ }
2873
+
2874
+ const key = `Commander.Command.${id}.state` as const
2875
+ const stateTag = Context.Service<typeof key, State>(key)
2876
+
2877
+ const makeContext_ = () => this.makeContext(id, { ...options, state: state?.value })
2878
+ const initialContext = makeContext_()
2879
+ const context = computed(() => makeContext_())
2880
+ const action = computed(() => context.value.action)
2881
+ const label = computed(() => context.value.label)
2882
+
2883
+ const currentState = Effect.sync(() => state.value)
2884
+
2885
+ // Reactive ref driven by the CommandProgress service — updated imperatively
2886
+ // from inside the stream via `Command.mapProgress(fn)` or `Command.updateProgress(p)`.
2887
+ const progressRef = ref<Progress | undefined>(undefined)
2888
+ const commandProgressService = {
2889
+ update: (p: Progress | undefined) =>
2890
+ Effect.sync(() => {
2891
+ progressRef.value = p
2892
+ })
2893
+ }
2894
+
2895
+ const streamErrorReporter = <A, E, R>(self: Stream.Stream<A, E, R>) =>
2896
+ self.pipe(
2897
+ Stream.tapCause(
2898
+ Effect.fnUntraced(function*(cause) {
2899
+ if (Cause.hasInterruptsOnly(cause)) {
2900
+ console.info(`Interrupted while trying to ${id}`)
2901
+ return
2902
+ }
2903
+
2904
+ const fail = Cause.findErrorOption(cause)
2905
+ if (Option.isSome(fail)) {
2906
+ const message = `Failure trying to ${id}`
2907
+ yield* reportMessage(message, {
2908
+ action: id,
2909
+ error: fail.value
2910
+ })
2911
+ return
2912
+ }
2913
+
2914
+ const ctx = yield* CommandContext
2915
+ const extra = {
2916
+ action: ctx.action,
2917
+ message: `Unexpected Error trying to ${id}`
2918
+ }
2919
+ yield* reportRuntimeError(cause, extra)
2920
+ }, Effect.uninterruptible)
2921
+ )
2922
+ )
2923
+
2924
+ const theStreamHandler = (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) =>
2925
+ handler(arg, ctx).pipe(
2926
+ streamErrorReporter,
2927
+ Stream.provideService(CommandProgress, commandProgressService),
2928
+ Stream.provideServiceEffect(stateTag, currentState),
2929
+ Stream.provideServiceEffect(CommandContext, Effect.sync(() => makeContext_()))
2930
+ )
2931
+
2932
+ const waitId = options?.waitKey ? options.waitKey(id) : undefined
2933
+ const blockId = options?.blockKey ? options.blockKey(id) : undefined
2934
+
2935
+ const [result, exec_] = asStreamResult(theStreamHandler)
2936
+
2937
+ const exec = Effect
2938
+ .fnUntraced(
2939
+ function*(...args: [any, any]) {
2940
+ if (waitId !== undefined) registerWait(waitId)
2941
+ if (blockId !== undefined && blockId !== waitId) {
2942
+ registerWait(blockId)
2943
+ }
2944
+ return yield* exec_(...args)
2945
+ },
2946
+ Effect.onExit(() =>
2947
+ Effect.sync(() => {
2948
+ if (waitId !== undefined) unregisterWait(waitId)
2949
+ if (blockId !== undefined && blockId !== waitId) {
2950
+ unregisterWait(blockId)
2951
+ }
2952
+ })
2953
+ )
2954
+ )
2955
+
2956
+ const waiting = waitId !== undefined
2957
+ ? computed(() => result.value.waiting || (waitState.value[waitId] ?? 0) > 0)
2958
+ : computed(() => result.value.waiting)
2959
+
2960
+ const blocked = blockId !== undefined
2961
+ ? computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0)
2962
+ : computed(() => waiting.value)
2963
+
2964
+ const computeAllowed = options?.allowed
2965
+ const allowed = computeAllowed ? computed(() => computeAllowed(id, state)) : true
2966
+
2967
+ const rt = Effect.context<RT | RTHooks>().pipe(Effect.provide(this.hooks)).pipe(Effect.runSyncWith(this.rt))
2968
+ const runFork = Effect.runForkWith(rt)
2969
+
2970
+ const progress = progressRef
2971
+
2972
+ const handle = Object.assign((arg: Arg) => {
2973
+ arg = toRaw(arg)
2974
+ progressRef.value = undefined // reset progress on new invocation
2975
+ const limit = Error.stackTraceLimit
2976
+ Error.stackTraceLimit = 2
2977
+ const errorCall = new Error()
2978
+ Error.stackTraceLimit = limit
2979
+
2980
+ let cache: false | string = false
2981
+ const captureStackTrace = () => {
2982
+ if (cache !== false) {
2983
+ return cache
2984
+ }
2985
+ if (errorCall.stack) {
2986
+ const stackDef = errorDef!.stack!.trim().split("\n")
2987
+ const stackCall = errorCall.stack.trim().split("\n")
2988
+ let endStackDef = stackDef.slice(2).join("\n").trim()
2989
+ if (!endStackDef.includes(`(`)) {
2990
+ endStackDef = endStackDef.replace(/at (.*)/, "at ($1)")
2991
+ }
2992
+ let endStackCall = stackCall.slice(2).join("\n").trim()
2993
+ if (!endStackCall.includes(`(`)) {
2994
+ endStackCall = endStackCall.replace(/at (.*)/, "at ($1)")
2995
+ }
2996
+ cache = `${endStackDef}\n${endStackCall}`
2997
+ return cache
2998
+ }
2999
+ }
3000
+
3001
+ const command = currentState.pipe(Effect.flatMap((state) => {
3002
+ const rawArg = deepToRaw(arg)
3003
+ const rawState = deepToRaw(state)
3004
+ return Effect.withSpan(
3005
+ exec(arg, { ...context.value, state } as any),
3006
+ id,
3007
+ {
3008
+ captureStackTrace,
3009
+ attributes: {
3010
+ input: rawArg,
3011
+ state: rawState,
3012
+ action: initialContext.action,
3013
+ label: initialContext.label,
3014
+ id: initialContext.id,
3015
+ i18nKey: initialContext.i18nKey
3016
+ }
3017
+ }
3018
+ )
3019
+ }))
3020
+
3021
+ return runFork(command as any)
3022
+ }, { action, label })
3023
+
3024
+ return reactive({
3025
+ id,
3026
+ i18nKey: initialContext.i18nKey,
3027
+ namespace: initialContext.namespace,
3028
+ namespaced: initialContext.namespaced,
3029
+ result,
3030
+ /** always undefined for streamFn commands — `result` already exposes the live stream state */
3031
+ running: undefined,
3032
+ /** reactive – progress driven by `Command.mapProgress` or `Command.updateProgress` inside the stream */
3033
+ progress,
3034
+ waiting,
3035
+ blocked,
3036
+ allowed,
3037
+ action,
3038
+ label,
3039
+ state,
3040
+ handle
3041
+ })
3042
+ },
3043
+ { id }
3044
+ )
3045
+ }
3046
+
3047
+ /**
3048
+ * Define a stream-backed Command for handling user actions.
3049
+ *
3050
+ * Like `fn`, but the body generator (or function) must **return** a `Stream` rather than
3051
+ * an `Effect`. The command's `waiting` state stays `true` while the stream is running and
3052
+ * is set to `false` once it terminates. The reactive `result` ref is updated for every
3053
+ * value emitted by the stream.
3054
+ *
3055
+ * Three handler shapes are accepted:
3056
+ * 1. **Generator returning a Stream** (primary) — may yield Effects freely before returning the stream:
3057
+ * ```ts
3058
+ * Command.streamFn("exportData")(
3059
+ * function*(arg, ctx) {
3060
+ * const token = yield* getAuthToken
3061
+ * return Stream.fromEffect(startExport(token, arg.id)).pipe(
3062
+ * Stream.flatMap((job) => pollProgress(job.id))
3063
+ * )
3064
+ * }
3065
+ * )
3066
+ * ```
3067
+ * 2. **Function returning a Stream directly**: `(arg, ctx) => Stream.make(1, 2, 3)`
3068
+ * 3. **Function returning `Effect<Stream>`**: `(arg, ctx) => Effect.map(setup, (s) => s.stream)`
3069
+ *
3070
+ * @param id The internal identifier for the action (used for tracing and i18n lookup).
3071
+ * @param options Same options as `fn` (`state`, `blockKey`, `waitKey`, `allowed`, `i18nCustomKey`).
3072
+ *
3073
+ * **Progress** — use `Command.mapProgress(fn)` as a stream pipe operator; the mapper receives
3074
+ * `AsyncResult<A, E>` (each value wrapped as `AsyncResult.success(v, { waiting: true })`),
3075
+ * matching the same shape as CommandButton’s `:progress-map` prop. Or call
3076
+ * `Command.updateProgress(p)` for imperative control:
3077
+ *
3078
+ * ```ts
3079
+ * // mapProgress as a combinator arg (outside the handler):
3080
+ * Command.streamFn("exportData")(
3081
+ * function*(arg, ctx) { return makeExportStream(arg.id) },
3082
+ * (s) => s.pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress" ? { text: `${r.value.completed}/${r.value.total}` } : undefined))
3083
+ * )
3084
+ *
3085
+ * // Or inline inside the handler body:
3086
+ * Command.streamFn("exportData")(function*(arg, ctx) {
3087
+ * return makeExportStream(arg.id).pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) ? ... : undefined))
3088
+ * })
3089
+ * ```
3090
+ *
3091
+ * **Pipeable combinators** — the 2nd–Nth args follow the same pattern as `fn`: each combinator
3092
+ * receives `(stream, arg, ctx)` and returns a transformed stream:
3093
+ * ```ts
3094
+ * Command.streamFn("exportData")(
3095
+ * handler,
3096
+ * (s, arg, ctx) => s.pipe(Command.mapProgress(fn), Stream.take(100))
3097
+ * )
3098
+ * ```
3099
+ *
3100
+ * **Returned Properties**: `action`, `label`, `result`, `progress`, `waiting`, `blocked`,
3101
+ * `allowed`, `handle`, `i18nKey`, `namespace`, `namespaced`.
3102
+ */
3103
+ streamFn = <
3104
+ const Id extends string,
3105
+ const State extends IntlRecord = IntlRecord,
3106
+ const I18nKey extends string = Id
3107
+ >(
3108
+ id: Id | { id: Id },
3109
+ options?: FnOptions<Id, I18nKey, State>
3110
+ ):
3111
+ & Commander.StreamGen<RT | RTHooks, Id, I18nKey, State>
3112
+ & Commander.NonGenStream<RT | RTHooks, Id, I18nKey, State>
3113
+ & {
3114
+ state: Context.Service<`Commander.Command.${Id}.state`, State>
3115
+ } =>
3116
+ {
3117
+ const resolvedId = typeof id === "string" ? id : id.id
3118
+
3119
+ type StreamOrEffect = Stream.Stream<any, any, any> | Effect.Effect<Stream.Stream<any, any, any>, any, any>
3120
+
3121
+ const toRawHandler = (fn: any): (arg: any, ctx: any) => StreamOrEffect => {
3122
+ if (isGeneratorFunction(fn)) {
3123
+ return Effect.fnUntraced(function*(arg: any, ctx: any) {
3124
+ return yield* (fn as (arg: any, ctx: any) => Generator<any, Stream.Stream<any, any, any>, any>)(arg, ctx)
3125
+ })
3126
+ }
3127
+ return fn
3128
+ }
3129
+
3130
+ const toFinalStream = (value: StreamOrEffect): Stream.Stream<any, any, any> =>
3131
+ Stream.isStream(value) ? value : Stream.unwrap(value as Effect.Effect<Stream.Stream<any, any, any>, any, any>)
3132
+
3133
+ return Object.assign(
3134
+ (fn: any, ...combinators: Array<(s: any, arg: any, ctx: any) => any>): any => {
3135
+ const limit = Error.stackTraceLimit
3136
+ Error.stackTraceLimit = 2
3137
+ const errorDef = new Error()
3138
+ Error.stackTraceLimit = limit
3139
+
3140
+ const rawHandler = toRawHandler(fn)
3141
+ const handler = (arg: any, ctx: any) => {
3142
+ let current: any = rawHandler(arg, ctx)
3143
+ for (const combinator of combinators) {
3144
+ current = combinator(current, arg, ctx)
3145
+ }
3146
+ return toFinalStream(current)
3147
+ }
3148
+
3149
+ return this.makeStreamCommand(id, options, errorDef)(handler)
3150
+ },
3151
+ makeBaseInfo(resolvedId, options),
3152
+ {
3153
+ state: Context.Service<`Commander.Command.${Id}.state`, State>(
3154
+ `Commander.Command.${resolvedId}.state`
3155
+ )
3156
+ }
3157
+ )
3158
+ }
3159
+
2468
3160
  /** @deprecated */
3161
+
2469
3162
  alt2: <
2470
3163
  const Id extends string,
2471
3164
  MutArg,