@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/CHANGELOG.md +47 -0
- package/dist/commander.d.ts +217 -2
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +426 -2
- package/dist/makeClient.d.ts +54 -7
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +46 -13
- package/dist/makeUseCommand.d.ts +2 -2
- package/dist/makeUseCommand.d.ts.map +1 -1
- package/dist/makeUseCommand.js +1 -1
- package/dist/mutate.d.ts +12 -8
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +28 -8
- package/dist/query.d.ts +14 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +105 -15
- package/package.json +2 -2
- package/src/commander.ts +695 -2
- package/src/makeClient.ts +117 -13
- package/src/makeUseCommand.ts +3 -1
- package/src/mutate.ts +46 -7
- package/src/query.ts +148 -24
- package/test/dist/stubs.d.ts +184 -38
- package/test/dist/stubs.d.ts.map +1 -1
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
|
|
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,
|