@effect-app/vue 4.0.0-beta.181 → 4.0.0-beta.183
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 +51 -0
- package/dist/commander.d.ts +205 -12
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +336 -24
- package/dist/makeClient.d.ts +66 -14
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +58 -25
- 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 +13 -8
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +32 -12
- package/dist/query.d.ts +14 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +105 -15
- package/examples/streamMutation.ts +8 -4
- package/package.json +2 -2
- package/src/commander.ts +578 -37
- package/src/makeClient.ts +143 -34
- package/src/makeUseCommand.ts +3 -1
- package/src/mutate.ts +55 -12
- package/src/query.ts +148 -24
- package/test/dist/stubs.d.ts +184 -38
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/makeClient.test.ts +2 -2
- package/test/streamFinal.test.ts +2 -2
package/src/commander.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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"
|
|
@@ -31,20 +32,31 @@ export type StreamMutationCallOptions<A, E> = {
|
|
|
31
32
|
progress?: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The result of invoking a `mutateStream` factory: the `execute` function (or
|
|
37
|
+
* `Effect`, when the request takes no input) carries `id`, plus `running` and
|
|
38
|
+
* `progress` when the factory was called with a `progress` formatter. Pass
|
|
39
|
+
* directly to `Command.fn` / `Command.wrap` / `Command.wrapStream`, or invoke
|
|
40
|
+
* to run the stream.
|
|
41
|
+
*/
|
|
42
|
+
type StreamMutationCallable<Id extends string, Arg, A, E, R> =
|
|
43
|
+
& (((arg: Arg) => Effect.Effect<any, E, R>) | Effect.Effect<any, E, R>)
|
|
39
44
|
& {
|
|
40
45
|
readonly id: Id
|
|
46
|
+
readonly _streamCallable: true
|
|
41
47
|
readonly running?: ComputedRef<AsyncResult.AsyncResult<A, E>>
|
|
42
48
|
readonly progress?: ComputedRef<Progress | undefined>
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
type StreamMutationFactory<Id extends string, Arg, A, E, R> =
|
|
46
|
-
& ((options?: StreamMutationCallOptions<A, E>) =>
|
|
47
|
-
& { readonly id: Id }
|
|
52
|
+
& ((options?: StreamMutationCallOptions<A, E>) => StreamMutationCallable<Id, Arg, A, E, R>)
|
|
53
|
+
& { readonly id: Id; readonly _streamFactory: true }
|
|
54
|
+
|
|
55
|
+
const isStreamFactory = (x: unknown): x is StreamMutationFactory<string, any, any, any, any> =>
|
|
56
|
+
typeof x === "function" && (x as any)._streamFactory === true
|
|
57
|
+
|
|
58
|
+
const isStreamCallable = (x: unknown): x is StreamMutationCallable<string, any, any, any, any> =>
|
|
59
|
+
x !== null && x !== undefined && (x as any)._streamCallable === true
|
|
48
60
|
type FnOptions<
|
|
49
61
|
Id extends string,
|
|
50
62
|
I18nCustomKey extends string,
|
|
@@ -114,6 +126,35 @@ export class CommandContext extends Context.Service<CommandContext, {
|
|
|
114
126
|
"CommandContext"
|
|
115
127
|
) {}
|
|
116
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Service available inside `streamFn` stream handlers that lets you imperatively push
|
|
131
|
+
* progress updates to the command's reactive `progress` ref.
|
|
132
|
+
*
|
|
133
|
+
* Use `Command.mapProgress(fn)` or `Command.updateProgress(progress)` to interact with this service.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* // Using mapProgress (recommended) — applied as a stream pipe operator:
|
|
138
|
+
* const exportCmd = Command.streamFn("exportData")(
|
|
139
|
+
* function*(arg, ctx) {
|
|
140
|
+
* return makeExportStream(arg.id).pipe(
|
|
141
|
+
* Command.mapProgress((r) =>
|
|
142
|
+
* AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
|
|
143
|
+
* ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
|
|
144
|
+
* : undefined
|
|
145
|
+
* )
|
|
146
|
+
* )
|
|
147
|
+
* }
|
|
148
|
+
* )
|
|
149
|
+
* // exportCmd.progress is updated for every OperationProgress event
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export class CommandProgress extends Context.Reference<{
|
|
153
|
+
readonly update: (progress: Progress | undefined) => Effect.Effect<void>
|
|
154
|
+
}>("Commander.CommandProgress", {
|
|
155
|
+
defaultValue: () => ({ update: (_progress: Progress | undefined): Effect.Effect<void> => Effect.void })
|
|
156
|
+
}) {}
|
|
157
|
+
|
|
117
158
|
export type EmitWithCallback<A, Event extends string> = (event: Event, value: A, onDone: () => void) => void
|
|
118
159
|
|
|
119
160
|
/**
|
|
@@ -1705,6 +1746,132 @@ export declare namespace Commander {
|
|
|
1705
1746
|
) => Eff
|
|
1706
1747
|
): CommandOutHelper<Arg, Eff, Id, I18nKey, State>
|
|
1707
1748
|
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Type for `streamFn` — generator overload where the body yields Effects and returns a `Stream`.
|
|
1752
|
+
* `waiting` stays `true` while the stream is running, and updates the `result` ref per emitted value.
|
|
1753
|
+
*/
|
|
1754
|
+
export type StreamGen<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
|
|
1755
|
+
<
|
|
1756
|
+
Eff extends Effect.Yieldable<any, any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
|
|
1757
|
+
SA,
|
|
1758
|
+
SE,
|
|
1759
|
+
SR,
|
|
1760
|
+
Arg = void
|
|
1761
|
+
>(
|
|
1762
|
+
body: (
|
|
1763
|
+
arg: Arg,
|
|
1764
|
+
ctx: CommandContextLocal2<Id, I18nKey, State>
|
|
1765
|
+
) => Generator<Eff, Stream.Stream<SA, SE, SR>, never>
|
|
1766
|
+
): CommandOut<
|
|
1767
|
+
Arg,
|
|
1768
|
+
SA,
|
|
1769
|
+
| SE
|
|
1770
|
+
| ([Eff] extends [never] ? never
|
|
1771
|
+
: [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
|
|
1772
|
+
: never),
|
|
1773
|
+
| SR
|
|
1774
|
+
| ([Eff] extends [never] ? never
|
|
1775
|
+
: [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
|
|
1776
|
+
: never),
|
|
1777
|
+
Id,
|
|
1778
|
+
I18nKey,
|
|
1779
|
+
State
|
|
1780
|
+
>
|
|
1781
|
+
<
|
|
1782
|
+
Eff extends Effect.Yieldable<any, any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
|
|
1783
|
+
SA,
|
|
1784
|
+
SE,
|
|
1785
|
+
SR,
|
|
1786
|
+
B,
|
|
1787
|
+
Arg = void
|
|
1788
|
+
>(
|
|
1789
|
+
body: (
|
|
1790
|
+
arg: Arg,
|
|
1791
|
+
ctx: CommandContextLocal2<Id, I18nKey, State>
|
|
1792
|
+
) => Generator<Eff, Stream.Stream<SA, SE, SR>, never>,
|
|
1793
|
+
a: (
|
|
1794
|
+
_: Effect.Effect<
|
|
1795
|
+
Stream.Stream<SA, SE, SR>,
|
|
1796
|
+
([Eff] extends [never] ? never
|
|
1797
|
+
: [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
|
|
1798
|
+
: never),
|
|
1799
|
+
([Eff] extends [never] ? never
|
|
1800
|
+
: [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
|
|
1801
|
+
: never)
|
|
1802
|
+
>,
|
|
1803
|
+
arg: ArgForCombinator<Arg>,
|
|
1804
|
+
ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
|
|
1805
|
+
) => B
|
|
1806
|
+
): B extends Stream.Stream<infer SA2, infer SE2, infer SR2> ? CommandOut<Arg, SA2, SE2, SR2, Id, I18nKey, State>
|
|
1807
|
+
: B extends Effect.Effect<Stream.Stream<infer SA2, infer SE2, infer SR2>, infer EE2, infer ER2>
|
|
1808
|
+
? CommandOut<Arg, SA2, SE2 | EE2, SR2 | ER2, Id, I18nKey, State>
|
|
1809
|
+
: never
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Type for `streamFn` — non-generator overload accepting a function that returns a `Stream` directly,
|
|
1814
|
+
* or an `Effect` that resolves to a `Stream`.
|
|
1815
|
+
*/
|
|
1816
|
+
export type NonGenStream<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
|
|
1817
|
+
<
|
|
1818
|
+
SA,
|
|
1819
|
+
SE,
|
|
1820
|
+
SR extends RT | CommandContext | `Commander.Command.${Id}.state`,
|
|
1821
|
+
Arg = void
|
|
1822
|
+
>(
|
|
1823
|
+
body: (arg: Arg, ctx: CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>
|
|
1824
|
+
): CommandOut<Arg, SA, SE, SR, Id, I18nKey, State>
|
|
1825
|
+
<
|
|
1826
|
+
SA,
|
|
1827
|
+
SE,
|
|
1828
|
+
SR,
|
|
1829
|
+
A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
|
|
1830
|
+
Arg = void
|
|
1831
|
+
>(
|
|
1832
|
+
body: (arg: Arg, ctx: CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>,
|
|
1833
|
+
a: (
|
|
1834
|
+
_: Stream.Stream<SA, SE, SR>,
|
|
1835
|
+
arg: ArgForCombinator<Arg>,
|
|
1836
|
+
ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
|
|
1837
|
+
) => A
|
|
1838
|
+
): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
|
|
1839
|
+
<
|
|
1840
|
+
SA,
|
|
1841
|
+
SE,
|
|
1842
|
+
SR,
|
|
1843
|
+
EE,
|
|
1844
|
+
ER extends RT | CommandContext | `Commander.Command.${Id}.state`,
|
|
1845
|
+
Arg = void
|
|
1846
|
+
>(
|
|
1847
|
+
body: (
|
|
1848
|
+
arg: Arg,
|
|
1849
|
+
ctx: CommandContextLocal2<Id, I18nKey, State>
|
|
1850
|
+
) => Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>
|
|
1851
|
+
): CommandOut<Arg, SA, SE | EE, SR | ER, Id, I18nKey, State>
|
|
1852
|
+
<
|
|
1853
|
+
SA,
|
|
1854
|
+
SE,
|
|
1855
|
+
SR,
|
|
1856
|
+
EE,
|
|
1857
|
+
ER,
|
|
1858
|
+
B,
|
|
1859
|
+
Arg = void
|
|
1860
|
+
>(
|
|
1861
|
+
body: (
|
|
1862
|
+
arg: Arg,
|
|
1863
|
+
ctx: CommandContextLocal2<Id, I18nKey, State>
|
|
1864
|
+
) => Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>,
|
|
1865
|
+
a: (
|
|
1866
|
+
_: Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>,
|
|
1867
|
+
arg: ArgForCombinator<Arg>,
|
|
1868
|
+
ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
|
|
1869
|
+
) => B
|
|
1870
|
+
): B extends Stream.Stream<infer SA2, infer SE2, infer SR2> ? CommandOut<Arg, SA2, SE2, SR2, Id, I18nKey, State>
|
|
1871
|
+
: B extends Effect.Effect<Stream.Stream<infer SA2, infer SE2, infer SR2>, infer EE2, infer ER2>
|
|
1872
|
+
? CommandOut<Arg, SA2, SE2 | EE2, SR2 | ER2, Id, I18nKey, State>
|
|
1873
|
+
: never
|
|
1874
|
+
}
|
|
1708
1875
|
}
|
|
1709
1876
|
|
|
1710
1877
|
type ErrorRenderer<E, Args extends readonly any[]> = (e: E, action: string, ...args: Args) => string | undefined
|
|
@@ -1827,6 +1994,66 @@ export const CommanderStatic = {
|
|
|
1827
1994
|
) =>
|
|
1828
1995
|
(self: In, arg: Arg, arg2: Arg2) => cb(arg, arg2)(self),
|
|
1829
1996
|
|
|
1997
|
+
/**
|
|
1998
|
+
* Stream pipe operator that maps each emitted value to a `Progress` entry and updates the
|
|
1999
|
+
* command's reactive `progress` ref via the `CommandProgress` service.
|
|
2000
|
+
*
|
|
2001
|
+
* The mapper receives an `AsyncResult<A, E>` (each emitted value wrapped as
|
|
2002
|
+
* `AsyncResult.success(value, { waiting: true })`), matching the same shape used by
|
|
2003
|
+
* `CommandButton`'s `:progress-map` prop.
|
|
2004
|
+
*
|
|
2005
|
+
* Designed to be used inside a `streamFn` handler (either directly with `.pipe()`, or as
|
|
2006
|
+
* a combinator argument):
|
|
2007
|
+
*
|
|
2008
|
+
* @example
|
|
2009
|
+
* ```ts
|
|
2010
|
+
* // Inside the handler body:
|
|
2011
|
+
* Command.streamFn("exportData")(function*(arg, ctx) {
|
|
2012
|
+
* return makeExportStream(arg.id).pipe(
|
|
2013
|
+
* Command.mapProgress((r) =>
|
|
2014
|
+
* AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
|
|
2015
|
+
* ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
|
|
2016
|
+
* : undefined
|
|
2017
|
+
* )
|
|
2018
|
+
* )
|
|
2019
|
+
* })
|
|
2020
|
+
*
|
|
2021
|
+
* // Or as a stream combinator argument:
|
|
2022
|
+
* Command.streamFn("exportData")(
|
|
2023
|
+
* function*(arg, ctx) { return makeExportStream(arg.id) },
|
|
2024
|
+
* (s) => s.pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress" ? { text: `${r.value.completed}/${r.value.total}` } : undefined))
|
|
2025
|
+
* )
|
|
2026
|
+
* ```
|
|
2027
|
+
*/
|
|
2028
|
+
mapProgress:
|
|
2029
|
+
<A, E>(fn: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined) =>
|
|
2030
|
+
<R>(stream: Stream.Stream<A, E, R>): Stream.Stream<A, E, R> =>
|
|
2031
|
+
stream.pipe(
|
|
2032
|
+
Stream.tap((v) => {
|
|
2033
|
+
const p = fn(AsyncResult.success(v, { waiting: true }))
|
|
2034
|
+
return p !== undefined ? CommandProgress.use((s) => s.update(p)) : Effect.void
|
|
2035
|
+
})
|
|
2036
|
+
),
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Imperatively push a progress update from inside a `streamFn` handler.
|
|
2040
|
+
* Requires `CommandProgress` to be in context — provided automatically for all `streamFn` streams.
|
|
2041
|
+
*
|
|
2042
|
+
* @example
|
|
2043
|
+
* ```ts
|
|
2044
|
+
* // In a streamFn handler:
|
|
2045
|
+
* stream.pipe(
|
|
2046
|
+
* Stream.tap((event) =>
|
|
2047
|
+
* event._tag === "OperationProgress"
|
|
2048
|
+
* ? Command.updateProgress({ text: `${event.completed}/${event.total}`, percentage: event.completed / event.total * 100 })
|
|
2049
|
+
* : Effect.void
|
|
2050
|
+
* )
|
|
2051
|
+
* )
|
|
2052
|
+
* ```
|
|
2053
|
+
*/
|
|
2054
|
+
updateProgress: (progress: Progress | undefined): Effect.Effect<void> =>
|
|
2055
|
+
CommandProgress.use((s) => s.update(progress)),
|
|
2056
|
+
|
|
1830
2057
|
/** Version of @see confirmOrInterrupt that automatically includes the action name in the default messages */
|
|
1831
2058
|
confirmOrInterrupt: Effect.fnUntraced(function*(
|
|
1832
2059
|
message: string | undefined = undefined
|
|
@@ -2395,7 +2622,7 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2395
2622
|
id:
|
|
2396
2623
|
| Id
|
|
2397
2624
|
| { id: Id }
|
|
2398
|
-
|
|
|
2625
|
+
| StreamMutationCallable<Id, any, RunningA, RunningE, any>
|
|
2399
2626
|
| StreamMutationFactory<Id, any, RunningA, RunningE, any>,
|
|
2400
2627
|
options?: FnOptions<Id, I18nKey, State>
|
|
2401
2628
|
): Commander.Gen<RT | RTHooks, Id, I18nKey, State> & Commander.NonGen<RT | RTHooks, Id, I18nKey, State> & {
|
|
@@ -2403,8 +2630,8 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2403
2630
|
} => {
|
|
2404
2631
|
// Resolve id and (optionally) per-build stream metadata.
|
|
2405
2632
|
const resolvedId: Id = typeof id === "string" ? id : (id as { id: Id }).id
|
|
2406
|
-
const
|
|
2407
|
-
const
|
|
2633
|
+
const factory = isStreamFactory(id)
|
|
2634
|
+
const callable = !factory && isStreamCallable(id)
|
|
2408
2635
|
const resolveStreamMeta = ():
|
|
2409
2636
|
| {
|
|
2410
2637
|
running?: ComputedRef<AsyncResult.AsyncResult<RunningA, RunningE>> | undefined
|
|
@@ -2412,13 +2639,13 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2412
2639
|
}
|
|
2413
2640
|
| undefined =>
|
|
2414
2641
|
{
|
|
2415
|
-
if (
|
|
2416
|
-
const
|
|
2417
|
-
return { running:
|
|
2642
|
+
if (factory) {
|
|
2643
|
+
const c = id()
|
|
2644
|
+
return { running: c.running, progress: c.progress }
|
|
2418
2645
|
}
|
|
2419
|
-
if (
|
|
2420
|
-
const
|
|
2421
|
-
return { running:
|
|
2646
|
+
if (callable) {
|
|
2647
|
+
const c = id as StreamMutationCallable<Id, any, RunningA, RunningE, any>
|
|
2648
|
+
return { running: c.running, progress: c.progress }
|
|
2422
2649
|
}
|
|
2423
2650
|
return undefined
|
|
2424
2651
|
}
|
|
@@ -2454,7 +2681,322 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2454
2681
|
)
|
|
2455
2682
|
}
|
|
2456
2683
|
|
|
2684
|
+
/**
|
|
2685
|
+
* Internal factory for stream-backed commands. Accepts a handler that returns a `Stream` directly.
|
|
2686
|
+
* Services (`CommandContext`, `stateTag`) are provided to the stream via `Stream.provideServiceEffect`.
|
|
2687
|
+
*/
|
|
2688
|
+
readonly makeStreamCommand = <
|
|
2689
|
+
const Id extends string,
|
|
2690
|
+
const State extends IntlRecord | undefined,
|
|
2691
|
+
const I18nKey extends string = Id
|
|
2692
|
+
>(
|
|
2693
|
+
id_: Id | { id: Id },
|
|
2694
|
+
options?: FnOptions<Id, I18nKey, State>,
|
|
2695
|
+
errorDef?: Error
|
|
2696
|
+
) => {
|
|
2697
|
+
const id = typeof id_ === "string" ? id_ : id_.id
|
|
2698
|
+
const state = getStateValues(options)
|
|
2699
|
+
|
|
2700
|
+
return Object.assign(
|
|
2701
|
+
<Arg, SA, SE, SR>(
|
|
2702
|
+
handler: (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>
|
|
2703
|
+
) => {
|
|
2704
|
+
const limit = Error.stackTraceLimit
|
|
2705
|
+
Error.stackTraceLimit = 2
|
|
2706
|
+
const localErrorDef = new Error()
|
|
2707
|
+
Error.stackTraceLimit = limit
|
|
2708
|
+
if (!errorDef) {
|
|
2709
|
+
errorDef = localErrorDef
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
const key = `Commander.Command.${id}.state` as const
|
|
2713
|
+
const stateTag = Context.Service<typeof key, State>(key)
|
|
2714
|
+
|
|
2715
|
+
const makeContext_ = () => this.makeContext(id, { ...options, state: state?.value })
|
|
2716
|
+
const initialContext = makeContext_()
|
|
2717
|
+
const context = computed(() => makeContext_())
|
|
2718
|
+
const action = computed(() => context.value.action)
|
|
2719
|
+
const label = computed(() => context.value.label)
|
|
2720
|
+
|
|
2721
|
+
const currentState = Effect.sync(() => state.value)
|
|
2722
|
+
|
|
2723
|
+
// Reactive ref driven by the CommandProgress service — updated imperatively
|
|
2724
|
+
// from inside the stream via `Command.mapProgress(fn)` or `Command.updateProgress(p)`.
|
|
2725
|
+
const progressRef = ref<Progress | undefined>(undefined)
|
|
2726
|
+
const commandProgressService = {
|
|
2727
|
+
update: (p: Progress | undefined) =>
|
|
2728
|
+
Effect.sync(() => {
|
|
2729
|
+
progressRef.value = p
|
|
2730
|
+
})
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const streamErrorReporter = <A, E, R>(self: Stream.Stream<A, E, R>) =>
|
|
2734
|
+
self.pipe(
|
|
2735
|
+
Stream.tapCause(
|
|
2736
|
+
Effect.fnUntraced(function*(cause) {
|
|
2737
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
2738
|
+
console.info(`Interrupted while trying to ${id}`)
|
|
2739
|
+
return
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
const fail = Cause.findErrorOption(cause)
|
|
2743
|
+
if (Option.isSome(fail)) {
|
|
2744
|
+
const message = `Failure trying to ${id}`
|
|
2745
|
+
yield* reportMessage(message, {
|
|
2746
|
+
action: id,
|
|
2747
|
+
error: fail.value
|
|
2748
|
+
})
|
|
2749
|
+
return
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
const ctx = yield* CommandContext
|
|
2753
|
+
const extra = {
|
|
2754
|
+
action: ctx.action,
|
|
2755
|
+
message: `Unexpected Error trying to ${id}`
|
|
2756
|
+
}
|
|
2757
|
+
yield* reportRuntimeError(cause, extra)
|
|
2758
|
+
}, Effect.uninterruptible)
|
|
2759
|
+
)
|
|
2760
|
+
)
|
|
2761
|
+
|
|
2762
|
+
const theStreamHandler = (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) =>
|
|
2763
|
+
handler(arg, ctx).pipe(
|
|
2764
|
+
streamErrorReporter,
|
|
2765
|
+
Stream.provideService(CommandProgress, commandProgressService),
|
|
2766
|
+
Stream.provideServiceEffect(stateTag, currentState),
|
|
2767
|
+
Stream.provideServiceEffect(CommandContext, Effect.sync(() => makeContext_()))
|
|
2768
|
+
)
|
|
2769
|
+
|
|
2770
|
+
const waitId = options?.waitKey ? options.waitKey(id) : undefined
|
|
2771
|
+
const blockId = options?.blockKey ? options.blockKey(id) : undefined
|
|
2772
|
+
|
|
2773
|
+
const [result, exec_] = asStreamResult(theStreamHandler)
|
|
2774
|
+
|
|
2775
|
+
const exec = Effect
|
|
2776
|
+
.fnUntraced(
|
|
2777
|
+
function*(...args: [any, any]) {
|
|
2778
|
+
if (waitId !== undefined) registerWait(waitId)
|
|
2779
|
+
if (blockId !== undefined && blockId !== waitId) {
|
|
2780
|
+
registerWait(blockId)
|
|
2781
|
+
}
|
|
2782
|
+
return yield* exec_(...args)
|
|
2783
|
+
},
|
|
2784
|
+
Effect.onExit(() =>
|
|
2785
|
+
Effect.sync(() => {
|
|
2786
|
+
if (waitId !== undefined) unregisterWait(waitId)
|
|
2787
|
+
if (blockId !== undefined && blockId !== waitId) {
|
|
2788
|
+
unregisterWait(blockId)
|
|
2789
|
+
}
|
|
2790
|
+
})
|
|
2791
|
+
)
|
|
2792
|
+
)
|
|
2793
|
+
|
|
2794
|
+
const waiting = waitId !== undefined
|
|
2795
|
+
? computed(() => result.value.waiting || (waitState.value[waitId] ?? 0) > 0)
|
|
2796
|
+
: computed(() => result.value.waiting)
|
|
2797
|
+
|
|
2798
|
+
const blocked = blockId !== undefined
|
|
2799
|
+
? computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0)
|
|
2800
|
+
: computed(() => waiting.value)
|
|
2801
|
+
|
|
2802
|
+
const computeAllowed = options?.allowed
|
|
2803
|
+
const allowed = computeAllowed ? computed(() => computeAllowed(id, state)) : true
|
|
2804
|
+
|
|
2805
|
+
const rt = Effect.context<RT | RTHooks>().pipe(Effect.provide(this.hooks)).pipe(Effect.runSyncWith(this.rt))
|
|
2806
|
+
const runFork = Effect.runForkWith(rt)
|
|
2807
|
+
|
|
2808
|
+
const progress = progressRef
|
|
2809
|
+
|
|
2810
|
+
const handle = Object.assign((arg: Arg) => {
|
|
2811
|
+
arg = toRaw(arg)
|
|
2812
|
+
progressRef.value = undefined // reset progress on new invocation
|
|
2813
|
+
const limit = Error.stackTraceLimit
|
|
2814
|
+
Error.stackTraceLimit = 2
|
|
2815
|
+
const errorCall = new Error()
|
|
2816
|
+
Error.stackTraceLimit = limit
|
|
2817
|
+
|
|
2818
|
+
let cache: false | string = false
|
|
2819
|
+
const captureStackTrace = () => {
|
|
2820
|
+
if (cache !== false) {
|
|
2821
|
+
return cache
|
|
2822
|
+
}
|
|
2823
|
+
if (errorCall.stack) {
|
|
2824
|
+
const stackDef = errorDef!.stack!.trim().split("\n")
|
|
2825
|
+
const stackCall = errorCall.stack.trim().split("\n")
|
|
2826
|
+
let endStackDef = stackDef.slice(2).join("\n").trim()
|
|
2827
|
+
if (!endStackDef.includes(`(`)) {
|
|
2828
|
+
endStackDef = endStackDef.replace(/at (.*)/, "at ($1)")
|
|
2829
|
+
}
|
|
2830
|
+
let endStackCall = stackCall.slice(2).join("\n").trim()
|
|
2831
|
+
if (!endStackCall.includes(`(`)) {
|
|
2832
|
+
endStackCall = endStackCall.replace(/at (.*)/, "at ($1)")
|
|
2833
|
+
}
|
|
2834
|
+
cache = `${endStackDef}\n${endStackCall}`
|
|
2835
|
+
return cache
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
const command = currentState.pipe(Effect.flatMap((state) => {
|
|
2840
|
+
const rawArg = deepToRaw(arg)
|
|
2841
|
+
const rawState = deepToRaw(state)
|
|
2842
|
+
return Effect.withSpan(
|
|
2843
|
+
exec(arg, { ...context.value, state } as any),
|
|
2844
|
+
id,
|
|
2845
|
+
{
|
|
2846
|
+
captureStackTrace,
|
|
2847
|
+
attributes: {
|
|
2848
|
+
input: rawArg,
|
|
2849
|
+
state: rawState,
|
|
2850
|
+
action: initialContext.action,
|
|
2851
|
+
label: initialContext.label,
|
|
2852
|
+
id: initialContext.id,
|
|
2853
|
+
i18nKey: initialContext.i18nKey
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
)
|
|
2857
|
+
}))
|
|
2858
|
+
|
|
2859
|
+
return runFork(command as any)
|
|
2860
|
+
}, { action, label })
|
|
2861
|
+
|
|
2862
|
+
return reactive({
|
|
2863
|
+
id,
|
|
2864
|
+
i18nKey: initialContext.i18nKey,
|
|
2865
|
+
namespace: initialContext.namespace,
|
|
2866
|
+
namespaced: initialContext.namespaced,
|
|
2867
|
+
result,
|
|
2868
|
+
/** always undefined for streamFn commands — `result` already exposes the live stream state */
|
|
2869
|
+
running: undefined,
|
|
2870
|
+
/** reactive – progress driven by `Command.mapProgress` or `Command.updateProgress` inside the stream */
|
|
2871
|
+
progress,
|
|
2872
|
+
waiting,
|
|
2873
|
+
blocked,
|
|
2874
|
+
allowed,
|
|
2875
|
+
action,
|
|
2876
|
+
label,
|
|
2877
|
+
state,
|
|
2878
|
+
handle
|
|
2879
|
+
})
|
|
2880
|
+
},
|
|
2881
|
+
{ id }
|
|
2882
|
+
)
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
/**
|
|
2886
|
+
* Define a stream-backed Command for handling user actions.
|
|
2887
|
+
*
|
|
2888
|
+
* Like `fn`, but the body generator (or function) must **return** a `Stream` rather than
|
|
2889
|
+
* an `Effect`. The command's `waiting` state stays `true` while the stream is running and
|
|
2890
|
+
* is set to `false` once it terminates. The reactive `result` ref is updated for every
|
|
2891
|
+
* value emitted by the stream.
|
|
2892
|
+
*
|
|
2893
|
+
* Three handler shapes are accepted:
|
|
2894
|
+
* 1. **Generator returning a Stream** (primary) — may yield Effects freely before returning the stream:
|
|
2895
|
+
* ```ts
|
|
2896
|
+
* Command.streamFn("exportData")(
|
|
2897
|
+
* function*(arg, ctx) {
|
|
2898
|
+
* const token = yield* getAuthToken
|
|
2899
|
+
* return Stream.fromEffect(startExport(token, arg.id)).pipe(
|
|
2900
|
+
* Stream.flatMap((job) => pollProgress(job.id))
|
|
2901
|
+
* )
|
|
2902
|
+
* }
|
|
2903
|
+
* )
|
|
2904
|
+
* ```
|
|
2905
|
+
* 2. **Function returning a Stream directly**: `(arg, ctx) => Stream.make(1, 2, 3)`
|
|
2906
|
+
* 3. **Function returning `Effect<Stream>`**: `(arg, ctx) => Effect.map(setup, (s) => s.stream)`
|
|
2907
|
+
*
|
|
2908
|
+
* @param id The internal identifier for the action (used for tracing and i18n lookup).
|
|
2909
|
+
* @param options Same options as `fn` (`state`, `blockKey`, `waitKey`, `allowed`, `i18nCustomKey`).
|
|
2910
|
+
*
|
|
2911
|
+
* **Progress** — use `Command.mapProgress(fn)` as a stream pipe operator; the mapper receives
|
|
2912
|
+
* `AsyncResult<A, E>` (each value wrapped as `AsyncResult.success(v, { waiting: true })`),
|
|
2913
|
+
* matching the same shape as CommandButton’s `:progress-map` prop. Or call
|
|
2914
|
+
* `Command.updateProgress(p)` for imperative control:
|
|
2915
|
+
*
|
|
2916
|
+
* ```ts
|
|
2917
|
+
* // mapProgress as a combinator arg (outside the handler):
|
|
2918
|
+
* Command.streamFn("exportData")(
|
|
2919
|
+
* function*(arg, ctx) { return makeExportStream(arg.id) },
|
|
2920
|
+
* (s) => s.pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress" ? { text: `${r.value.completed}/${r.value.total}` } : undefined))
|
|
2921
|
+
* )
|
|
2922
|
+
*
|
|
2923
|
+
* // Or inline inside the handler body:
|
|
2924
|
+
* Command.streamFn("exportData")(function*(arg, ctx) {
|
|
2925
|
+
* return makeExportStream(arg.id).pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) ? ... : undefined))
|
|
2926
|
+
* })
|
|
2927
|
+
* ```
|
|
2928
|
+
*
|
|
2929
|
+
* **Pipeable combinators** — the 2nd–Nth args follow the same pattern as `fn`: each combinator
|
|
2930
|
+
* receives `(stream, arg, ctx)` and returns a transformed stream:
|
|
2931
|
+
* ```ts
|
|
2932
|
+
* Command.streamFn("exportData")(
|
|
2933
|
+
* handler,
|
|
2934
|
+
* (s, arg, ctx) => s.pipe(Command.mapProgress(fn), Stream.take(100))
|
|
2935
|
+
* )
|
|
2936
|
+
* ```
|
|
2937
|
+
*
|
|
2938
|
+
* **Returned Properties**: `action`, `label`, `result`, `progress`, `waiting`, `blocked`,
|
|
2939
|
+
* `allowed`, `handle`, `i18nKey`, `namespace`, `namespaced`.
|
|
2940
|
+
*/
|
|
2941
|
+
streamFn = <
|
|
2942
|
+
const Id extends string,
|
|
2943
|
+
const State extends IntlRecord = IntlRecord,
|
|
2944
|
+
const I18nKey extends string = Id
|
|
2945
|
+
>(
|
|
2946
|
+
id: Id | { id: Id },
|
|
2947
|
+
options?: FnOptions<Id, I18nKey, State>
|
|
2948
|
+
):
|
|
2949
|
+
& Commander.StreamGen<RT | RTHooks, Id, I18nKey, State>
|
|
2950
|
+
& Commander.NonGenStream<RT | RTHooks, Id, I18nKey, State>
|
|
2951
|
+
& {
|
|
2952
|
+
state: Context.Service<`Commander.Command.${Id}.state`, State>
|
|
2953
|
+
} =>
|
|
2954
|
+
{
|
|
2955
|
+
const resolvedId = typeof id === "string" ? id : id.id
|
|
2956
|
+
|
|
2957
|
+
type StreamOrEffect = Stream.Stream<any, any, any> | Effect.Effect<Stream.Stream<any, any, any>, any, any>
|
|
2958
|
+
|
|
2959
|
+
const toRawHandler = (fn: any): (arg: any, ctx: any) => StreamOrEffect => {
|
|
2960
|
+
if (isGeneratorFunction(fn)) {
|
|
2961
|
+
return Effect.fnUntraced(function*(arg: any, ctx: any) {
|
|
2962
|
+
return yield* (fn as (arg: any, ctx: any) => Generator<any, Stream.Stream<any, any, any>, any>)(arg, ctx)
|
|
2963
|
+
})
|
|
2964
|
+
}
|
|
2965
|
+
return fn
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
const toFinalStream = (value: StreamOrEffect): Stream.Stream<any, any, any> =>
|
|
2969
|
+
Stream.isStream(value) ? value : Stream.unwrap(value as Effect.Effect<Stream.Stream<any, any, any>, any, any>)
|
|
2970
|
+
|
|
2971
|
+
return Object.assign(
|
|
2972
|
+
(fn: any, ...combinators: Array<(s: any, arg: any, ctx: any) => any>): any => {
|
|
2973
|
+
const limit = Error.stackTraceLimit
|
|
2974
|
+
Error.stackTraceLimit = 2
|
|
2975
|
+
const errorDef = new Error()
|
|
2976
|
+
Error.stackTraceLimit = limit
|
|
2977
|
+
|
|
2978
|
+
const rawHandler = toRawHandler(fn)
|
|
2979
|
+
const handler = (arg: any, ctx: any) => {
|
|
2980
|
+
let current: any = rawHandler(arg, ctx)
|
|
2981
|
+
for (const combinator of combinators) {
|
|
2982
|
+
current = combinator(current, arg, ctx)
|
|
2983
|
+
}
|
|
2984
|
+
return toFinalStream(current)
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
return this.makeStreamCommand(id, options, errorDef)(handler)
|
|
2988
|
+
},
|
|
2989
|
+
makeBaseInfo(resolvedId, options),
|
|
2990
|
+
{
|
|
2991
|
+
state: Context.Service<`Commander.Command.${Id}.state`, State>(
|
|
2992
|
+
`Commander.Command.${resolvedId}.state`
|
|
2993
|
+
)
|
|
2994
|
+
}
|
|
2995
|
+
)
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2457
2998
|
/** @deprecated */
|
|
2999
|
+
|
|
2458
3000
|
alt2: <
|
|
2459
3001
|
const Id extends string,
|
|
2460
3002
|
MutArg,
|
|
@@ -2563,18 +3105,15 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2563
3105
|
id: Id
|
|
2564
3106
|
mutateStream:
|
|
2565
3107
|
| StreamMutationFactory<Id, Arg, A, E, R>
|
|
2566
|
-
|
|
|
3108
|
+
| StreamMutationCallable<Id, Arg, A, E, R>
|
|
2567
3109
|
}
|
|
2568
|
-
|
|
|
3110
|
+
| StreamMutationCallable<Id, Arg, A, E, R>,
|
|
2569
3111
|
options?: FnOptions<Id, I18nKey, State>
|
|
2570
3112
|
): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> => {
|
|
2571
3113
|
if (mutation !== null && typeof mutation === "object" && "mutateStream" in mutation) {
|
|
2572
3114
|
return this.wrapStream(mutation as any, options) as any
|
|
2573
3115
|
}
|
|
2574
|
-
if (
|
|
2575
|
-
return this.wrapStream(mutation as any, options) as any
|
|
2576
|
-
}
|
|
2577
|
-
if (typeof mutation === "function" && "id" in mutation && (mutation as any).length <= 1) {
|
|
3116
|
+
if (isStreamCallable(mutation) || isStreamFactory(mutation)) {
|
|
2578
3117
|
return this.wrapStream(mutation as any, options) as any
|
|
2579
3118
|
}
|
|
2580
3119
|
// At this point mutation is either { mutate, id } or (fn & { id })
|
|
@@ -2655,19 +3194,22 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2655
3194
|
id: Id
|
|
2656
3195
|
mutateStream:
|
|
2657
3196
|
| StreamMutationFactory<Id, Arg, A, E, R>
|
|
2658
|
-
|
|
|
3197
|
+
| StreamMutationCallable<Id, Arg, A, E, R>
|
|
2659
3198
|
}
|
|
2660
3199
|
| StreamMutationFactory<Id, Arg, A, E, R>
|
|
2661
|
-
|
|
|
3200
|
+
| StreamMutationCallable<Id, Arg, A, E, R>,
|
|
2662
3201
|
options?: FnOptions<Id, I18nKey, State>
|
|
2663
3202
|
): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> => {
|
|
2664
3203
|
const id = mutation.id
|
|
2665
|
-
// Resolve `source` to the factory or already-
|
|
2666
|
-
const source: StreamMutationFactory<Id, Arg, A, E, R> |
|
|
3204
|
+
// Resolve `source` to the factory or already-invoked callable.
|
|
3205
|
+
const source: StreamMutationFactory<Id, Arg, A, E, R> | StreamMutationCallable<Id, Arg, A, E, R> =
|
|
2667
3206
|
mutation !== null && typeof mutation === "object" && "mutateStream" in mutation
|
|
2668
3207
|
? (mutation.mutateStream as any)
|
|
2669
3208
|
: (mutation as any)
|
|
2670
|
-
const
|
|
3209
|
+
const resolveCallable = (): StreamMutationCallable<Id, Arg, A, E, R> =>
|
|
3210
|
+
(isStreamFactory(source)
|
|
3211
|
+
? (source as StreamMutationFactory<Id, Arg, A, E, R>)()
|
|
3212
|
+
: source) as StreamMutationCallable<Id, Arg, A, E, R>
|
|
2671
3213
|
return Object.assign(
|
|
2672
3214
|
(...combinators: any[]): any => {
|
|
2673
3215
|
// we capture the definition stack here, so we can append it to later stack traces
|
|
@@ -2676,15 +3218,14 @@ export class CommanderImpl<RT, RTHooks> {
|
|
|
2676
3218
|
const errorDef = new Error()
|
|
2677
3219
|
Error.stackTraceLimit = limit
|
|
2678
3220
|
|
|
2679
|
-
// Fresh per build:
|
|
2680
|
-
// wrap call gets its own
|
|
3221
|
+
// Fresh per build: invoke the factory once per command instance so each
|
|
3222
|
+
// wrap call gets its own state + execute pair. `running`/`progress`
|
|
2681
3223
|
// are only surfaced when the factory was called with a `progress` formatter.
|
|
2682
|
-
const
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
const streamMeta = { running: tuple.running, progress: tuple.progress }
|
|
3224
|
+
const callable = resolveCallable()
|
|
3225
|
+
const mutate: (_arg: Arg) => Effect.Effect<any, E, R> = Effect.isEffect(callable)
|
|
3226
|
+
? (_arg: Arg) => callable
|
|
3227
|
+
: callable as (arg: Arg) => Effect.Effect<any, E, R>
|
|
3228
|
+
const streamMeta = { running: callable.running, progress: callable.progress }
|
|
2688
3229
|
|
|
2689
3230
|
return this.makeCommand(id, options, errorDef, streamMeta)(
|
|
2690
3231
|
Effect.fnUntraced(
|