@effect-app/vue 4.0.0-beta.99 → 4.0.0

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.
Files changed (128) hide show
  1. package/CHANGELOG.md +2701 -0
  2. package/dist/atomQuery.d.ts +120 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +434 -0
  5. package/dist/commander.d.ts +318 -43
  6. package/dist/commander.d.ts.map +1 -1
  7. package/dist/commander.js +593 -81
  8. package/dist/confirm.d.ts +5 -3
  9. package/dist/confirm.d.ts.map +1 -1
  10. package/dist/confirm.js +12 -14
  11. package/dist/dependencyMetadata.d.ts +18 -0
  12. package/dist/dependencyMetadata.d.ts.map +1 -0
  13. package/dist/dependencyMetadata.js +67 -0
  14. package/dist/errorReporter.d.ts +6 -4
  15. package/dist/errorReporter.d.ts.map +1 -1
  16. package/dist/errorReporter.js +14 -19
  17. package/dist/form.d.ts +5 -5
  18. package/dist/form.d.ts.map +1 -1
  19. package/dist/form.js +16 -10
  20. package/dist/internal/tanstackQuery.d.ts +9 -0
  21. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  22. package/dist/internal/tanstackQuery.js +204 -0
  23. package/dist/intl.d.ts +4 -4
  24. package/dist/intl.d.ts.map +1 -1
  25. package/dist/intl.js +2 -2
  26. package/dist/lib.d.ts +8 -10
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +37 -12
  29. package/dist/liveQueryInvalidation.d.ts +16 -0
  30. package/dist/liveQueryInvalidation.d.ts.map +1 -0
  31. package/dist/liveQueryInvalidation.js +115 -0
  32. package/dist/makeClient.d.ts +232 -109
  33. package/dist/makeClient.d.ts.map +1 -1
  34. package/dist/makeClient.js +416 -84
  35. package/dist/makeContext.d.ts.map +1 -1
  36. package/dist/makeIntl.d.ts.map +1 -1
  37. package/dist/makeUseCommand.d.ts +3 -2
  38. package/dist/makeUseCommand.d.ts.map +1 -1
  39. package/dist/makeUseCommand.js +2 -2
  40. package/dist/mutate.d.ts +110 -41
  41. package/dist/mutate.d.ts.map +1 -1
  42. package/dist/mutate.js +236 -59
  43. package/dist/query.d.ts +161 -44
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +382 -113
  46. package/dist/queryLifetime.d.ts +25 -0
  47. package/dist/queryLifetime.d.ts.map +1 -0
  48. package/dist/queryLifetime.js +54 -0
  49. package/dist/routeParams.d.ts +4 -4
  50. package/dist/routeParams.d.ts.map +1 -1
  51. package/dist/routeParams.js +4 -3
  52. package/dist/runtime.d.ts +1 -17
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +2 -38
  55. package/dist/suspense.d.ts +19 -0
  56. package/dist/suspense.d.ts.map +1 -0
  57. package/dist/suspense.js +38 -0
  58. package/dist/toast.d.ts +1 -45
  59. package/dist/toast.d.ts.map +1 -1
  60. package/dist/toast.js +2 -32
  61. package/dist/withToast.d.ts +1 -24
  62. package/dist/withToast.d.ts.map +1 -1
  63. package/dist/withToast.js +2 -45
  64. package/docs/atom-query-api-redesign.md +207 -0
  65. package/docs/mutation-command-atoms.md +40 -0
  66. package/docs/query-key-invalidation.md +107 -0
  67. package/examples/streamMutation.ts +72 -0
  68. package/package.json +35 -94
  69. package/src/atomQuery.ts +600 -0
  70. package/src/commander.ts +1064 -116
  71. package/src/confirm.ts +12 -14
  72. package/src/dependencyMetadata.ts +84 -0
  73. package/src/errorReporter.ts +65 -75
  74. package/src/form.ts +22 -15
  75. package/src/index.ts +7 -7
  76. package/src/internal/tanstackQuery.ts +308 -0
  77. package/src/intl.ts +2 -2
  78. package/src/lib.ts +51 -23
  79. package/src/liveQueryInvalidation.ts +138 -0
  80. package/src/makeClient.ts +972 -263
  81. package/src/makeIntl.ts +2 -2
  82. package/src/makeUseCommand.ts +8 -5
  83. package/src/mutate.ts +407 -154
  84. package/src/query.ts +758 -242
  85. package/src/queryLifetime.ts +74 -0
  86. package/src/routeParams.ts +7 -7
  87. package/src/runtime.ts +1 -54
  88. package/src/suspense.ts +43 -0
  89. package/src/toast.ts +1 -52
  90. package/src/withToast.ts +1 -99
  91. package/test/Mutation.test.ts +104 -17
  92. package/test/dependencyInvalidation.test.ts +444 -0
  93. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  94. package/test/dist/form.test.d.ts.map +1 -1
  95. package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
  96. package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
  97. package/test/dist/lib.test.d.ts.map +1 -0
  98. package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
  99. package/test/dist/query-span.test.d.ts.map +1 -0
  100. package/test/dist/queryOptions.test.d.ts.map +1 -0
  101. package/test/dist/streamFinal.test.d.ts.map +1 -0
  102. package/test/dist/streamFn.test.d.ts.map +1 -0
  103. package/test/dist/stubs.d.ts +3681 -256
  104. package/test/dist/stubs.d.ts.map +1 -1
  105. package/test/dist/stubs.js +166 -25
  106. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  107. package/test/dist/suspense.test.d.ts.map +1 -0
  108. package/test/form-validation-errors.test.ts +2 -1
  109. package/test/form.test.ts +2 -1
  110. package/test/inactiveInvalidation.test.ts +172 -0
  111. package/test/interrupt-refetch-repro.test.ts +625 -0
  112. package/test/lib.test.ts +240 -0
  113. package/test/liveQueryInvalidation.test.ts +108 -0
  114. package/test/makeClient.test.ts +414 -32
  115. package/test/query-span.test.ts +92 -0
  116. package/test/queryOptions.test.ts +88 -0
  117. package/test/streamFinal.test.ts +64 -0
  118. package/test/streamFn.test.ts +457 -0
  119. package/test/stubs.ts +194 -36
  120. package/test/suspense-regression.test.ts +168 -0
  121. package/test/suspense.test.ts +160 -0
  122. package/tsconfig.examples.json +20 -0
  123. package/tsconfig.json +9 -1
  124. package/tsconfig.src.json +34 -34
  125. package/tsconfig.test.json +2 -2
  126. package/vitest.config.ts +5 -5
  127. package/eslint.config.mjs +0 -24
  128. package/tsconfig.json.bak +0 -12
package/src/commander.ts CHANGED
@@ -1,19 +1,44 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { asResult, type MissingDependencies, reportRuntimeError } from "@effect-app/vue"
3
2
  import { reportMessage } from "@effect-app/vue/errorReporter"
4
- import { Cause, Context, Effect, type Exit, type Fiber, flow, Layer, Match, MutableHashMap, Option, Predicate, S } from "effect-app"
3
+ import { deepToRaw, reportRuntimeError } from "@effect-app/vue/lib"
4
+ import { type MissingDependencies } from "@effect-app/vue/makeClient"
5
+ import { asResult, asStreamResult } from "@effect-app/vue/mutate"
5
6
  import { SupportedErrors } from "effect-app/client"
6
- import { OperationFailure, OperationSuccess } from "effect-app/Operations"
7
+ import * as Context from "effect-app/Context"
8
+ import * as Effect from "effect-app/Effect"
9
+ import * as Layer from "effect-app/Layer"
10
+ import * as Option from "effect-app/Option"
11
+ import * as S from "effect-app/Schema"
7
12
  import { isGeneratorFunction, wrapEffect } from "effect-app/utils"
8
- import { type AsyncResult } from "effect/unstable/reactivity/AsyncResult"
13
+ import * as Cause from "effect/Cause"
14
+ import type * as Exit from "effect/Exit"
15
+ import type * as Fiber from "effect/Fiber"
16
+ import { flow } from "effect/Function"
17
+ import * as MutableHashMap from "effect/MutableHashMap"
18
+ import * as Predicate from "effect/Predicate"
19
+ import { type Refinement } from "effect/Predicate"
20
+ import * as AsyncResult from "effect/reactivity/AsyncResult"
21
+ import * as Stream from "effect/Stream"
9
22
  import { type FormatXMLElementFn, type PrimitiveType } from "intl-messageformat"
10
- import { computed, type ComputedRef, reactive, ref } from "vue"
11
- import { Confirm } from "./confirm.js"
12
- import { I18n } from "./intl.js"
13
- import { WithToast } from "./withToast.js"
23
+ import { computed, type ComputedRef, reactive, ref, toRaw } from "vue"
24
+ import { Confirm } from "./confirm.ts"
25
+ import { I18n } from "./intl.ts"
26
+ import { CurrentToastId, Toast } from "./toast.ts"
27
+ import { WithToast } from "./withToast.ts"
14
28
 
15
29
  type IntlRecord = Record<string, PrimitiveType | FormatXMLElementFn<string, string>>
16
- type FnOptions<Id extends string, I18nCustomKey extends string, State extends IntlRecord | undefined> = {
30
+
31
+ /**
32
+ * Progress information surfaced by a stream command. Either a plain text label
33
+ * or a `{ text, percentage }` pair when concrete progress is known.
34
+ */
35
+ export type Progress = string | { readonly text: string; readonly percentage: number }
36
+
37
+ type FnOptions<
38
+ Id extends string,
39
+ I18nCustomKey extends string,
40
+ State extends IntlRecord | undefined
41
+ > = {
17
42
  i18nCustomKey?: I18nCustomKey
18
43
  /**
19
44
  * passed to the i18n formatMessage calls so you can use it in translation messagee
@@ -78,6 +103,35 @@ export class CommandContext extends Context.Service<CommandContext, {
78
103
  "CommandContext"
79
104
  ) {}
80
105
 
106
+ /**
107
+ * Service available inside `streamFn` stream handlers that lets you imperatively push
108
+ * progress updates to the command's reactive `progress` ref.
109
+ *
110
+ * Use `Command.mapProgress(fn)` or `Command.updateProgress(progress)` to interact with this service.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * // Using mapProgress (recommended) — applied as a stream pipe operator:
115
+ * const exportCmd = Command.streamFn("exportData")(
116
+ * function*(arg, ctx) {
117
+ * return makeExportStream(arg.id).pipe(
118
+ * Command.mapProgress((r) =>
119
+ * AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
120
+ * ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
121
+ * : undefined
122
+ * )
123
+ * )
124
+ * }
125
+ * )
126
+ * // exportCmd.progress is updated for every OperationProgress event
127
+ * ```
128
+ */
129
+ export class CommandProgress extends Context.Reference<{
130
+ readonly update: (progress: Progress | undefined) => Effect.Effect<void>
131
+ }>("Commander.CommandProgress", {
132
+ defaultValue: () => ({ update: (_progress: Progress | undefined): Effect.Effect<void> => Effect.void })
133
+ }) {}
134
+
81
135
  export type EmitWithCallback<A, Event extends string> = (event: Event, value: A, onDone: () => void) => void
82
136
 
83
137
  /**
@@ -91,6 +145,19 @@ export const wrapEmit = <A, Event extends string>(
91
145
  (value: A) => new Promise<void>((resolve) => emit(event, value, resolve))
92
146
 
93
147
  export declare namespace Commander {
148
+ export type IntlRecord = Record<string, PrimitiveType | FormatXMLElementFn<string, string>>
149
+ export type FnOptions<
150
+ Id extends string,
151
+ I18nKey extends string,
152
+ State extends IntlRecord | undefined
153
+ > = {
154
+ i18nCustomKey?: I18nKey
155
+ state?: ComputedRef<State> | (() => State)
156
+ blockKey?: (id: Id) => string | undefined
157
+ waitKey?: (id: Id) => string | undefined
158
+ allowed?: (id: Id, state: ComputedRef<State>) => boolean
159
+ }
160
+
94
161
  export type CommanderBase<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> =
95
162
  & Gen<RT, Id, I18nKey, State>
96
163
  & NonGen<RT, Id, I18nKey, State>
@@ -138,7 +205,12 @@ export declare namespace Commander {
138
205
  /** reactive */
139
206
  label: string
140
207
  /** reactive */
141
- result: AsyncResult<A, E>
208
+ result: AsyncResult.AsyncResult<A, E>
209
+ /**
210
+ * reactive – formatted progress info driven by `Command.mapProgress` or `Command.updateProgress`
211
+ * inside a `streamFn` handler. Undefined for non-stream commands.
212
+ */
213
+ progress: Progress | undefined
142
214
  /** reactive */
143
215
  waiting: boolean
144
216
  /** reactive */
@@ -161,7 +233,7 @@ export declare namespace Commander {
161
233
  new(): {}
162
234
 
163
235
  /** click handlers */
164
- handle: ((arg: Arg) => Fiber.Fiber<Exit.Exit<A, E>, never>) & {
236
+ handle: ((arg: Arg) => Fiber.Fiber<Exit.Exit<A, E>>) & {
165
237
  /** @deprecated don't exist */
166
238
  effect: (arg: Arg) => Effect.Effect<A, E, R>
167
239
  }
@@ -208,7 +280,7 @@ export declare namespace Commander {
208
280
 
209
281
  export type Gen<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
210
282
  <
211
- Eff extends Effect.Yieldable<any, any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
283
+ Eff extends Effect.Effect<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
212
284
  AEff,
213
285
  Arg = void
214
286
  >(
@@ -217,17 +289,17 @@ export declare namespace Commander {
217
289
  Arg,
218
290
  AEff,
219
291
  [Eff] extends [never] ? never
220
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
292
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
221
293
  : never,
222
294
  [Eff] extends [never] ? never
223
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
295
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
224
296
  : never,
225
297
  Id,
226
298
  I18nKey,
227
299
  State
228
300
  >
229
301
  <
230
- Eff extends Effect.Yieldable<any, any, any, any>,
302
+ Eff extends Effect.Effect<any, any, any>,
231
303
  AEff,
232
304
  A extends Effect.Effect<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
233
305
  Arg = void
@@ -237,10 +309,10 @@ export declare namespace Commander {
237
309
  _: Effect.Effect<
238
310
  AEff,
239
311
  [Eff] extends [never] ? never
240
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
312
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
241
313
  : never,
242
314
  [Eff] extends [never] ? never
243
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
315
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
244
316
  : never
245
317
  >,
246
318
  arg: ArgForCombinator<Arg>,
@@ -248,7 +320,7 @@ export declare namespace Commander {
248
320
  ) => A
249
321
  ): CommandOutHelper<Arg, A, Id, I18nKey, State>
250
322
  <
251
- Eff extends Effect.Yieldable<any, any, any, any>,
323
+ Eff extends Effect.Effect<any, any, any>,
252
324
  AEff,
253
325
  A,
254
326
  B extends Effect.Effect<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
@@ -259,10 +331,10 @@ export declare namespace Commander {
259
331
  _: Effect.Effect<
260
332
  AEff,
261
333
  [Eff] extends [never] ? never
262
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
334
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
263
335
  : never,
264
336
  [Eff] extends [never] ? never
265
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
337
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
266
338
  : never
267
339
  >,
268
340
  arg: ArgForCombinator<Arg>,
@@ -275,7 +347,7 @@ export declare namespace Commander {
275
347
  ) => B
276
348
  ): CommandOutHelper<Arg, B, Id, I18nKey, State>
277
349
  <
278
- Eff extends Effect.Yieldable<any, any, any, any>,
350
+ Eff extends Effect.Effect<any, any, any>,
279
351
  AEff,
280
352
  A,
281
353
  B,
@@ -287,10 +359,10 @@ export declare namespace Commander {
287
359
  _: Effect.Effect<
288
360
  AEff,
289
361
  [Eff] extends [never] ? never
290
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
362
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
291
363
  : never,
292
364
  [Eff] extends [never] ? never
293
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
365
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
294
366
  : never
295
367
  >,
296
368
  arg: ArgForCombinator<Arg>,
@@ -308,7 +380,7 @@ export declare namespace Commander {
308
380
  ) => C
309
381
  ): CommandOutHelper<Arg, C, Id, I18nKey, State>
310
382
  <
311
- Eff extends Effect.Yieldable<any, any, any, any>,
383
+ Eff extends Effect.Effect<any, any, any>,
312
384
  AEff,
313
385
  A,
314
386
  B,
@@ -321,10 +393,10 @@ export declare namespace Commander {
321
393
  _: Effect.Effect<
322
394
  AEff,
323
395
  [Eff] extends [never] ? never
324
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
396
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
325
397
  : never,
326
398
  [Eff] extends [never] ? never
327
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
399
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
328
400
  : never
329
401
  >,
330
402
  arg: ArgForCombinator<Arg>,
@@ -347,7 +419,7 @@ export declare namespace Commander {
347
419
  ) => D
348
420
  ): CommandOutHelper<Arg, D, Id, I18nKey, State>
349
421
  <
350
- Eff extends Effect.Yieldable<any, any, any, any>,
422
+ Eff extends Effect.Effect<any, any, any>,
351
423
  AEff,
352
424
  A,
353
425
  B,
@@ -361,10 +433,10 @@ export declare namespace Commander {
361
433
  _: Effect.Effect<
362
434
  AEff,
363
435
  [Eff] extends [never] ? never
364
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
436
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
365
437
  : never,
366
438
  [Eff] extends [never] ? never
367
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
439
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
368
440
  : never
369
441
  >,
370
442
  arg: ArgForCombinator<Arg>,
@@ -392,7 +464,7 @@ export declare namespace Commander {
392
464
  ) => E
393
465
  ): CommandOutHelper<Arg, E, Id, I18nKey, State>
394
466
  <
395
- Eff extends Effect.Yieldable<any, any, any, any>,
467
+ Eff extends Effect.Effect<any, any, any>,
396
468
  AEff,
397
469
  A,
398
470
  B,
@@ -407,10 +479,10 @@ export declare namespace Commander {
407
479
  _: Effect.Effect<
408
480
  AEff,
409
481
  [Eff] extends [never] ? never
410
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
482
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
411
483
  : never,
412
484
  [Eff] extends [never] ? never
413
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
485
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
414
486
  : never
415
487
  >,
416
488
  arg: ArgForCombinator<Arg>,
@@ -443,7 +515,7 @@ export declare namespace Commander {
443
515
  ) => F
444
516
  ): CommandOutHelper<Arg, F, Id, I18nKey, State>
445
517
  <
446
- Eff extends Effect.Yieldable<any, any, any, any>,
518
+ Eff extends Effect.Effect<any, any, any>,
447
519
  AEff,
448
520
  A,
449
521
  B,
@@ -459,10 +531,10 @@ export declare namespace Commander {
459
531
  _: Effect.Effect<
460
532
  AEff,
461
533
  [Eff] extends [never] ? never
462
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
534
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
463
535
  : never,
464
536
  [Eff] extends [never] ? never
465
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
537
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
466
538
  : never
467
539
  >,
468
540
  arg: ArgForCombinator<Arg>,
@@ -500,7 +572,7 @@ export declare namespace Commander {
500
572
  ) => G
501
573
  ): CommandOutHelper<Arg, G, Id, I18nKey, State>
502
574
  <
503
- Eff extends Effect.Yieldable<any, any, any, any>,
575
+ Eff extends Effect.Effect<any, any, any>,
504
576
  AEff,
505
577
  A,
506
578
  B,
@@ -517,10 +589,10 @@ export declare namespace Commander {
517
589
  _: Effect.Effect<
518
590
  AEff,
519
591
  [Eff] extends [never] ? never
520
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
592
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
521
593
  : never,
522
594
  [Eff] extends [never] ? never
523
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
595
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
524
596
  : never
525
597
  >,
526
598
  arg: ArgForCombinator<Arg>,
@@ -563,7 +635,7 @@ export declare namespace Commander {
563
635
  ) => H
564
636
  ): CommandOutHelper<Arg, H, Id, I18nKey, State>
565
637
  <
566
- Eff extends Effect.Yieldable<any, any, any, any>,
638
+ Eff extends Effect.Effect<any, any, any>,
567
639
  AEff,
568
640
  A,
569
641
  B,
@@ -581,10 +653,10 @@ export declare namespace Commander {
581
653
  _: Effect.Effect<
582
654
  AEff,
583
655
  [Eff] extends [never] ? never
584
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer E, infer _R>] ? E
656
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
585
657
  : never,
586
658
  [Eff] extends [never] ? never
587
- : [Eff] extends [Effect.Yieldable<any, infer _A, infer _E, infer R>] ? R
659
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
588
660
  : never
589
661
  >,
590
662
  arg: ArgForCombinator<Arg>,
@@ -1657,46 +1729,288 @@ export declare namespace Commander {
1657
1729
  ) => Eff
1658
1730
  ): CommandOutHelper<Arg, Eff, Id, I18nKey, State>
1659
1731
  }
1732
+
1733
+ /**
1734
+ * Type for `streamFn` — generator overload where the body yields Effects and returns a `Stream`.
1735
+ * `waiting` stays `true` while the stream is running, and updates the `result` ref per emitted value.
1736
+ */
1737
+ export type StreamGen<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
1738
+ <
1739
+ Eff extends Effect.Effect<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
1740
+ SA,
1741
+ SE,
1742
+ SR,
1743
+ Arg = void
1744
+ >(
1745
+ body: (
1746
+ arg: Arg,
1747
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1748
+ ) => Generator<Eff, Stream.Stream<SA, SE, SR>, never>
1749
+ ): CommandOut<
1750
+ Arg,
1751
+ SA,
1752
+ | SE
1753
+ | ([Eff] extends [never] ? never
1754
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
1755
+ : never),
1756
+ | SR
1757
+ | ([Eff] extends [never] ? never
1758
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
1759
+ : never),
1760
+ Id,
1761
+ I18nKey,
1762
+ State
1763
+ >
1764
+ <
1765
+ Eff extends Effect.Effect<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
1766
+ SA,
1767
+ SE,
1768
+ SR,
1769
+ B,
1770
+ Arg = void
1771
+ >(
1772
+ body: (
1773
+ arg: Arg,
1774
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1775
+ ) => Generator<Eff, Stream.Stream<SA, SE, SR>, never>,
1776
+ a: (
1777
+ _: Effect.Effect<
1778
+ Stream.Stream<SA, SE, SR>,
1779
+ ([Eff] extends [never] ? never
1780
+ : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E
1781
+ : never),
1782
+ ([Eff] extends [never] ? never
1783
+ : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R
1784
+ : never)
1785
+ >,
1786
+ arg: ArgForCombinator<Arg>,
1787
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1788
+ ) => B
1789
+ ): B extends Stream.Stream<infer SA2, infer SE2, infer SR2> ? CommandOut<Arg, SA2, SE2, SR2, Id, I18nKey, State>
1790
+ : B extends Effect.Effect<Stream.Stream<infer SA2, infer SE2, infer SR2>, infer EE2, infer ER2>
1791
+ ? CommandOut<Arg, SA2, SE2 | EE2, SR2 | ER2, Id, I18nKey, State>
1792
+ : never
1793
+ }
1794
+
1795
+ /**
1796
+ * Type for `streamFn` — non-generator overload accepting a function that returns a `Stream` directly,
1797
+ * or an `Effect` that resolves to a `Stream`.
1798
+ */
1799
+ export type NonGenStream<RT, Id extends string, I18nKey extends string, State extends IntlRecord | undefined> = {
1800
+ <
1801
+ SA,
1802
+ SE,
1803
+ SR extends RT | CommandContext | `Commander.Command.${Id}.state`,
1804
+ Arg = void
1805
+ >(
1806
+ body: (arg: Arg, ctx: CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>
1807
+ ): CommandOut<Arg, SA, SE, SR, Id, I18nKey, State>
1808
+ <
1809
+ SA,
1810
+ SE,
1811
+ SR,
1812
+ A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>,
1813
+ Arg = void
1814
+ >(
1815
+ body: (arg: Arg, ctx: CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>,
1816
+ a: (
1817
+ _: Stream.Stream<SA, SE, SR>,
1818
+ arg: ArgForCombinator<Arg>,
1819
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1820
+ ) => A
1821
+ ): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
1822
+ <
1823
+ SA,
1824
+ SE,
1825
+ SR,
1826
+ EE,
1827
+ ER extends RT | CommandContext | `Commander.Command.${Id}.state`,
1828
+ Arg = void
1829
+ >(
1830
+ body: (
1831
+ arg: Arg,
1832
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1833
+ ) => Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>
1834
+ ): CommandOut<Arg, SA, SE | EE, SR | ER, Id, I18nKey, State>
1835
+ <
1836
+ SA,
1837
+ SE,
1838
+ SR,
1839
+ EE,
1840
+ ER,
1841
+ B,
1842
+ Arg = void
1843
+ >(
1844
+ body: (
1845
+ arg: Arg,
1846
+ ctx: CommandContextLocal2<Id, I18nKey, State>
1847
+ ) => Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>,
1848
+ a: (
1849
+ _: Effect.Effect<Stream.Stream<SA, SE, SR>, EE, ER>,
1850
+ arg: ArgForCombinator<Arg>,
1851
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1852
+ ) => B
1853
+ ): B extends Stream.Stream<infer SA2, infer SE2, infer SR2> ? CommandOut<Arg, SA2, SE2, SR2, Id, I18nKey, State>
1854
+ : B extends Effect.Effect<Stream.Stream<infer SA2, infer SE2, infer SR2>, infer EE2, infer ER2>
1855
+ ? CommandOut<Arg, SA2, SE2 | EE2, SR2 | ER2, Id, I18nKey, State>
1856
+ : never
1857
+ }
1858
+
1859
+ /**
1860
+ * Type returned by `mutate.wrap` on a stream handler — analogous to `CommanderWrap` but for streams.
1861
+ * The handler is pre-baked (from the stream mutation), so this is called with only optional combinators.
1862
+ */
1863
+ export type StreamerWrap<
1864
+ RT,
1865
+ Id extends string,
1866
+ I18nKey extends string,
1867
+ State extends IntlRecord | undefined,
1868
+ Arg,
1869
+ SA,
1870
+ SE,
1871
+ SR
1872
+ > =
1873
+ & CommandContextLocal<Id, I18nKey>
1874
+ & { readonly state: Context.Service<`Commander.Command.${Id}.state`, State> }
1875
+ & {
1876
+ (): Exclude<SR, RT> extends never ? CommandOut<Arg, SA, SE, SR, Id, I18nKey, State>
1877
+ : MissingDependencies<RT, SR> & {}
1878
+ <A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>>(
1879
+ a: (
1880
+ _: Stream.Stream<SA, SE, SR>,
1881
+ arg: ArgForCombinator<Arg>,
1882
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1883
+ ) => A
1884
+ ): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
1885
+ <
1886
+ B,
1887
+ A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>
1888
+ >(
1889
+ a: (
1890
+ _: Stream.Stream<SA, SE, SR>,
1891
+ arg: ArgForCombinator<Arg>,
1892
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1893
+ ) => B,
1894
+ b: (
1895
+ _: B,
1896
+ arg: ArgForCombinator<Arg>,
1897
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1898
+ ) => A
1899
+ ): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
1900
+ <
1901
+ B,
1902
+ C,
1903
+ A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>
1904
+ >(
1905
+ a: (
1906
+ _: Stream.Stream<SA, SE, SR>,
1907
+ arg: ArgForCombinator<Arg>,
1908
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1909
+ ) => B,
1910
+ b: (
1911
+ _: B,
1912
+ arg: ArgForCombinator<Arg>,
1913
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1914
+ ) => C,
1915
+ c: (
1916
+ _: C,
1917
+ arg: ArgForCombinator<Arg>,
1918
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1919
+ ) => A
1920
+ ): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
1921
+ <
1922
+ B,
1923
+ C,
1924
+ D,
1925
+ A extends Stream.Stream<any, any, RT | CommandContext | `Commander.Command.${Id}.state`>
1926
+ >(
1927
+ a: (
1928
+ _: Stream.Stream<SA, SE, SR>,
1929
+ arg: ArgForCombinator<Arg>,
1930
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1931
+ ) => B,
1932
+ b: (
1933
+ _: B,
1934
+ arg: ArgForCombinator<Arg>,
1935
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1936
+ ) => C,
1937
+ c: (
1938
+ _: C,
1939
+ arg: ArgForCombinator<Arg>,
1940
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1941
+ ) => D,
1942
+ d: (
1943
+ _: D,
1944
+ arg: ArgForCombinator<Arg>,
1945
+ ctx: CommandContextLocal2<NoInfer<Id>, NoInfer<I18nKey>, NoInfer<State>>
1946
+ ) => A
1947
+ ): CommandOut<Arg, Stream.Success<A>, Stream.Error<A>, Stream.Services<A>, Id, I18nKey, State>
1948
+ }
1660
1949
  }
1661
1950
 
1662
1951
  type ErrorRenderer<E, Args extends readonly any[]> = (e: E, action: string, ...args: Args) => string | undefined
1663
1952
 
1664
- const renderErrorMaker = I18n.useSync(
1665
- ({ intl }) =>
1666
- <E, Args extends readonly any[]>(action: string, errorRenderer?: ErrorRenderer<E, Args>) =>
1667
- (e: E, ...args: Args): string => {
1668
- if (errorRenderer) {
1669
- const m = errorRenderer(e, action, ...args)
1670
- if (m !== undefined) {
1671
- return m
1953
+ type RegisteredErrorRenderer<A> = {
1954
+ guard: Refinement<unknown, A>
1955
+ render: (guarded: A) => string | undefined
1956
+ }
1957
+
1958
+ export class CommanderErrorRenderers extends Context.Reference("Commander.ErrorRenderers", {
1959
+ defaultValue: () => [] as RegisteredErrorRenderer<any>[]
1960
+ }) {}
1961
+
1962
+ export const makeRegisteredErrorRenderer = <A>(
1963
+ guard: Predicate.Refinement<unknown, A>,
1964
+ render: (guarded: A) => string | undefined
1965
+ ): RegisteredErrorRenderer<A> => ({
1966
+ guard,
1967
+ render
1968
+ })
1969
+
1970
+ const renderErrorMaker = Effect.gen(function*() {
1971
+ const { intl } = yield* I18n
1972
+ const registeredRenderers = yield* CommanderErrorRenderers
1973
+ return (
1974
+ <E, Args extends readonly any[]>(action: string, errorRenderer?: ErrorRenderer<E, Args>) =>
1975
+ (e: E, ...args: Args): string => {
1976
+ if (errorRenderer) {
1977
+ const m = errorRenderer(e, action, ...args)
1978
+ if (m !== undefined) {
1979
+ return m
1980
+ }
1672
1981
  }
1673
- }
1674
- if (!S.is(SupportedErrors)(e) && !S.isSchemaError(e)) {
1675
- if (typeof e === "object" && e !== null) {
1676
- if ("message" in e) {
1677
- return `${e.message}`
1982
+ for (const entry of registeredRenderers) {
1983
+ if (!entry.guard(e)) {
1984
+ continue
1678
1985
  }
1679
- if ("_tag" in e) {
1680
- return `${e._tag}`
1986
+ const m = entry.render(e)
1987
+ if (m !== undefined) {
1988
+ return m
1681
1989
  }
1682
1990
  }
1683
- return ""
1684
- }
1685
- const e2: SupportedErrors | S.SchemaError = e
1686
- return Match.value(e2).pipe(
1687
- Match.tags({
1688
- NotFoundError: (e) => {
1689
- return intl.formatMessage({ id: "handle.not_found" }, { type: e.type, id: e.id })
1690
- },
1691
- SchemaError: (e) => {
1692
- console.warn(e.toString())
1693
- return intl.formatMessage({ id: "validation.failed" })
1991
+ if (!S.is(SupportedErrors)(e) && !S.isSchemaError(e)) {
1992
+ if (typeof e === "object" && e !== null) {
1993
+ if ("message" in e) {
1994
+ return `${e.message}`
1995
+ }
1996
+ if ("_tag" in e) {
1997
+ return `${e._tag}`
1998
+ }
1694
1999
  }
1695
- }),
1696
- Match.orElse((e) => `${e.message ?? e._tag ?? e}`)
1697
- )
1698
- }
1699
- )
2000
+ return ""
2001
+ }
2002
+ const e2: SupportedErrors | S.SchemaError = e
2003
+ if (e2._tag === "NotFoundError") {
2004
+ return intl.formatMessage({ id: "handle.not_found" }, { type: e2.type, id: String(e2.id) })
2005
+ }
2006
+ if (e2._tag === "SchemaError") {
2007
+ console.warn(e2.toString())
2008
+ return intl.formatMessage({ id: "validation.failed" })
2009
+ }
2010
+ return e2.message ?? e2._tag ?? e2
2011
+ }
2012
+ )
2013
+ })
1700
2014
 
1701
2015
  const defaultFailureMessageHandler = <E, Args extends Array<unknown>, AME, AMR>(
1702
2016
  actionMaker:
@@ -1719,23 +2033,18 @@ const defaultFailureMessageHandler = <E, Args extends Array<unknown>, AME, AMR>(
1719
2033
  error: "" // TODO consider again Cause.pretty(cause), // will be reported to Sentry/Otel anyway.. and we shouldn't bother users with error dumps?
1720
2034
  }
1721
2035
  ),
1722
- onSome: (e) =>
1723
- S.is(OperationFailure)(e)
1724
- ? {
1725
- level: "warn" as const,
1726
- message: intl.formatMessage(
1727
- { id: "handle.with_warnings" },
1728
- { action }
1729
- ) + e.message
1730
- ? "\n" + e.message
1731
- : ""
1732
- }
1733
- : `${
2036
+ onSome: (e) => {
2037
+ const rendered = renderError(action, errorRenderer)(e, ...args)
2038
+ return {
2039
+ level: "warn" as const,
2040
+ message: `${
1734
2041
  intl.formatMessage(
1735
2042
  { id: "handle.with_errors" },
1736
2043
  { action }
1737
2044
  )
1738
- }:\n` + renderError(action, errorRenderer)(e, ...args)
2045
+ }:\n` + rendered
2046
+ }
2047
+ }
1739
2048
  })
1740
2049
  })
1741
2050
 
@@ -1745,9 +2054,69 @@ export const CommanderStatic = {
1745
2054
  ) =>
1746
2055
  (self: In, arg: Arg, arg2: Arg2) => cb(arg, arg2)(self),
1747
2056
 
2057
+ /**
2058
+ * Stream pipe operator that maps each emitted value to a `Progress` entry and updates the
2059
+ * command's reactive `progress` ref via the `CommandProgress` service.
2060
+ *
2061
+ * The mapper receives an `AsyncResult<A, E>` (each emitted value wrapped as
2062
+ * `AsyncResult.success(value, { waiting: true })`), matching the same shape used by
2063
+ * `CommandButton`'s `:progress-map` prop.
2064
+ *
2065
+ * Designed to be used inside a `streamFn` handler (either directly with `.pipe()`, or as
2066
+ * a combinator argument):
2067
+ *
2068
+ * @example
2069
+ * ```ts
2070
+ * // Inside the handler body:
2071
+ * Command.streamFn("exportData")(function*(arg, ctx) {
2072
+ * return makeExportStream(arg.id).pipe(
2073
+ * Command.mapProgress((r) =>
2074
+ * AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
2075
+ * ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
2076
+ * : undefined
2077
+ * )
2078
+ * )
2079
+ * })
2080
+ *
2081
+ * // Or as a stream combinator argument:
2082
+ * Command.streamFn("exportData")(
2083
+ * function*(arg, ctx) { return makeExportStream(arg.id) },
2084
+ * (s) => s.pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress" ? { text: `${r.value.completed}/${r.value.total}` } : undefined))
2085
+ * )
2086
+ * ```
2087
+ */
2088
+ mapProgress:
2089
+ <A, E>(fn: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined) =>
2090
+ <R>(stream: Stream.Stream<A, E, R>): Stream.Stream<A, E, R> =>
2091
+ stream.pipe(
2092
+ Stream.tap((v) => {
2093
+ const p = fn(AsyncResult.success(v, { waiting: true }))
2094
+ return p !== undefined ? CommandProgress.use((s) => s.update(p)) : Effect.void
2095
+ })
2096
+ ),
2097
+
2098
+ /**
2099
+ * Imperatively push a progress update from inside a `streamFn` handler.
2100
+ * Requires `CommandProgress` to be in context — provided automatically for all `streamFn` streams.
2101
+ *
2102
+ * @example
2103
+ * ```ts
2104
+ * // In a streamFn handler:
2105
+ * stream.pipe(
2106
+ * Stream.tap((event) =>
2107
+ * event._tag === "OperationProgress"
2108
+ * ? Command.updateProgress({ text: `${event.completed}/${event.total}`, percentage: event.completed / event.total * 100 })
2109
+ * : Effect.void
2110
+ * )
2111
+ * )
2112
+ * ```
2113
+ */
2114
+ updateProgress: (progress: Progress | undefined): Effect.Effect<void> =>
2115
+ CommandProgress.use((s) => s.update(progress)),
2116
+
1748
2117
  /** Version of @see confirmOrInterrupt that automatically includes the action name in the default messages */
1749
2118
  confirmOrInterrupt: Effect.fnUntraced(function*(
1750
- message: string | undefined = undefined
2119
+ message?: string
1751
2120
  ) {
1752
2121
  const context = yield* CommandContext
1753
2122
  const { intl } = yield* I18n
@@ -1762,7 +2131,7 @@ export const CommanderStatic = {
1762
2131
  }),
1763
2132
  /** Version of @see confirm that automatically includes the action name in the default messages */
1764
2133
  confirm: Effect.fnUntraced(function*(
1765
- message: string | undefined = undefined
2134
+ message?: string
1766
2135
  ) {
1767
2136
  const context = yield* CommandContext
1768
2137
  const { intl } = yield* I18n
@@ -1782,6 +2151,17 @@ export const CommanderStatic = {
1782
2151
  CommandContext,
1783
2152
  (c) => ({ ...c, action: update(c.action, ...input) })
1784
2153
  ),
2154
+ registerErrorRenderer: <A>(
2155
+ guard: Predicate.Refinement<unknown, A>,
2156
+ render: (guarded: A) => string | undefined
2157
+ ) =>
2158
+ Layer.effect(
2159
+ CommanderErrorRenderers,
2160
+ Effect.gen(function*() {
2161
+ const current = yield* CommanderErrorRenderers
2162
+ return [...current, makeRegisteredErrorRenderer(guard, render)]
2163
+ })
2164
+ ),
1785
2165
  defaultFailureMessageHandler,
1786
2166
  renderError: renderErrorMaker,
1787
2167
  /**
@@ -1806,6 +2186,7 @@ export const CommanderStatic = {
1806
2186
  | string
1807
2187
  | ((id: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => true | string | undefined)
1808
2188
  errorRenderer?: (e: E, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | undefined
2189
+ showSpanInfo?: false
1809
2190
  onWaiting?:
1810
2191
  | null
1811
2192
  | undefined
@@ -1818,11 +2199,10 @@ export const CommanderStatic = {
1818
2199
  | ((a: A, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | null | undefined)
1819
2200
  }
1820
2201
  ) =>
1821
- (
1822
- self: Effect.Effect<A, E, R>,
1823
- ...args: Args
1824
- ) =>
1825
- Effect.gen(function*() {
2202
+ Effect.fnUntraced(function*(
2203
+ self: Effect.Effect<A, E, R>,
2204
+ ...args: Args
2205
+ ) {
1826
2206
  const cc = yield* CommandContext
1827
2207
  const { intl } = yield* I18n
1828
2208
  const withToast = yield* WithToast
@@ -1859,23 +2239,216 @@ export const CommanderStatic = {
1859
2239
  ),
1860
2240
  onSuccess: options?.onSuccess === null
1861
2241
  ? null
1862
- : (a, ..._args) =>
2242
+ : (_a, ..._args) =>
1863
2243
  hasCustomSuccess
1864
2244
  ? intl.formatMessage(
1865
2245
  { id: customSuccess },
1866
2246
  cc.state
1867
2247
  )
1868
- : (intl.formatMessage({ id: "handle.success" }, { action: cc.action })
1869
- + (S.is(OperationSuccess)(a) && a.message ? "\n" + a.message : "")),
2248
+ : intl.formatMessage({ id: "handle.success" }, { action: cc.action }),
1870
2249
  onFailure: defaultFailureMessageHandler(
1871
2250
  hasCustomFailure ? intl.formatMessage({ id: customFailure }, cc.state) : cc.action,
1872
2251
  options?.errorRenderer as ErrorRenderer<E, Args> | undefined
1873
2252
  ),
1874
- stableToastId
2253
+ stableToastId,
2254
+ groupId: cc.id,
2255
+ ...options?.showSpanInfo === false ? { showSpanInfo: options.showSpanInfo } : {}
1875
2256
  })(_, ...args)
1876
2257
  )
1877
2258
  }),
1878
2259
 
2260
+ /**
2261
+ * Stream-aware version of `withDefaultToast`. Use this as a combinator inside `streamFn`
2262
+ * (or anywhere a `Stream` needs toast lifecycle handling) instead of `withDefaultToast`.
2263
+ *
2264
+ * Unlike `withDefaultToast` (which only wraps the initial `Effect`), this combinator:
2265
+ * - Shows the "waiting" toast **before** the stream starts
2266
+ * - Updates the waiting toast with progress text when `progress` is set and a new element arrives
2267
+ * - Shows the "success" toast only **after** the stream drains fully without error
2268
+ * - Shows the "failure" toast if the stream errors or fails
2269
+ *
2270
+ * Accepts either a `Stream<A, E, R>` or an `Effect<Stream<A, E, R>, EE, ER>` as input,
2271
+ * so it works in both the `NonGenStream` and `StreamGen` overloads of `streamFn`.
2272
+ *
2273
+ * @example
2274
+ * ```ts
2275
+ * Command.streamFn("exportData")(
2276
+ * function*(arg, ctx) { return makeExportStream(arg.id) },
2277
+ * Command.withDefaultToastStream({
2278
+ * progress: (r) =>
2279
+ * AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress"
2280
+ * ? { text: `${r.value.completed}/${r.value.total}`, percentage: r.value.completed / r.value.total * 100 }
2281
+ * : undefined
2282
+ * })
2283
+ * )
2284
+ * ```
2285
+ */
2286
+ withDefaultToastStream: <A, E, R, Args extends Array<unknown>>(
2287
+ options?: {
2288
+ stableToastId?:
2289
+ | undefined
2290
+ | true
2291
+ | string
2292
+ | ((id: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => true | string | undefined)
2293
+ errorRenderer?: (e: E, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | undefined
2294
+ showSpanInfo?: false
2295
+ onWaiting?:
2296
+ | null
2297
+ | undefined
2298
+ | string
2299
+ | ((id: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | null | undefined)
2300
+ onSuccess?:
2301
+ | null
2302
+ | undefined
2303
+ | string
2304
+ | ((a: A, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | null | undefined)
2305
+ /** Map each stream element to a progress label. When non-`undefined`, updates both the active waiting toast and the `CommandProgress` service (for CommandButton progress display). */
2306
+ progress?: (result: AsyncResult.AsyncResult<A, E>) => Progress | undefined
2307
+ }
2308
+ ) =>
2309
+ (
2310
+ self: Stream.Stream<A, E, R> | Effect.Effect<Stream.Stream<A, E, R>, any, any>,
2311
+ ...args: Args
2312
+ ): Stream.Stream<A, E, R | I18n | Toast | CommandContext> => {
2313
+ const rawStream: Stream.Stream<A, E, R> = Stream.isStream(self)
2314
+ ? self
2315
+ : Stream.unwrap(self)
2316
+
2317
+ return Stream.unwrap(Effect.gen(function*() {
2318
+ const cc = yield* CommandContext
2319
+ const { intl } = yield* I18n
2320
+ const toast = yield* Toast
2321
+
2322
+ const customWaiting = cc.namespaced("waiting")
2323
+ const hasCustomWaiting = !!intl.messages[customWaiting]
2324
+ const customSuccess = cc.namespaced("success")
2325
+ const hasCustomSuccess = !!intl.messages[customSuccess]
2326
+ const customFailure = cc.namespaced("failure")
2327
+ const hasCustomFailure = !!intl.messages[customFailure]
2328
+
2329
+ const stableToastId: string | undefined = options?.stableToastId
2330
+ ? typeof options.stableToastId === "string"
2331
+ ? options.stableToastId
2332
+ : typeof options.stableToastId === "boolean"
2333
+ ? cc.id
2334
+ : typeof options.stableToastId === "function"
2335
+ ? (() => {
2336
+ const r = (options.stableToastId as (...a: any[]) => true | string | undefined)(cc.id, ...args)
2337
+ if (typeof r === "string") return r
2338
+ if (r === true) return cc.id
2339
+ return undefined
2340
+ })()
2341
+ : undefined
2342
+ : undefined
2343
+
2344
+ const baseTimeout = 3_000
2345
+
2346
+ const waitingMsg: string | null = options?.onWaiting === null
2347
+ ? null
2348
+ : typeof options?.onWaiting === "string"
2349
+ ? options.onWaiting
2350
+ : typeof options?.onWaiting === "function"
2351
+ ? (options.onWaiting as (...a: any[]) => string | null | undefined)(cc.id, ...args) ?? null
2352
+ : hasCustomWaiting
2353
+ ? intl.formatMessage({ id: customWaiting }, cc.state)
2354
+ : intl.formatMessage({ id: "handle.waiting" }, { action: cc.action })
2355
+
2356
+ const toastId: string | number | undefined = waitingMsg === null
2357
+ ? stableToastId
2358
+ : stableToastId ?? `wait-${Math.random().toString(36).slice(2)}`
2359
+
2360
+ const requestId: string = yield* Effect.currentSpan.pipe(
2361
+ Effect.map((span) => span.traceId),
2362
+ Effect.orElseSucceed(() => S.StringId.make())
2363
+ )
2364
+ const meta = { groupId: cc.id, requestId }
2365
+
2366
+ if (waitingMsg !== null) {
2367
+ yield* toast.info(waitingMsg, { id: toastId!, timeout: Infinity, ...meta })
2368
+ }
2369
+
2370
+ const failureHandler = defaultFailureMessageHandler<E, [], never, never>(
2371
+ hasCustomFailure ? intl.formatMessage({ id: customFailure }, cc.state) : cc.action,
2372
+ options?.errorRenderer as ErrorRenderer<E, []> | undefined
2373
+ )
2374
+
2375
+ let lastValue: A | undefined = undefined
2376
+ let didFail = false
2377
+
2378
+ const composed = rawStream.pipe(
2379
+ Stream.tap((v) =>
2380
+ Effect.gen(function*() {
2381
+ lastValue = v
2382
+ if (options?.progress !== undefined) {
2383
+ const p = options.progress(AsyncResult.success(v, { waiting: true }))
2384
+ if (p !== undefined) {
2385
+ // Update CommandProgress so CommandButton progress indicator is also driven
2386
+ yield* CommandProgress.use((s) => s.update(p))
2387
+ if (toastId !== undefined) {
2388
+ const progressText = typeof p === "string" ? p : p.text
2389
+ const msg = waitingMsg ? `${waitingMsg}\n${progressText}` : progressText
2390
+ yield* toast.info(msg, { id: toastId, timeout: Infinity, ...meta })
2391
+ }
2392
+ }
2393
+ }
2394
+ })
2395
+ ),
2396
+ Stream.tapCause(Effect.fnUntraced(function*(cause) {
2397
+ didFail = true
2398
+ if (Cause.hasInterruptsOnly(cause)) {
2399
+ if (toastId !== undefined) yield* toast.dismiss(toastId)
2400
+ return
2401
+ }
2402
+
2403
+ const spanInfo = options?.showSpanInfo !== false
2404
+ ? yield* Effect.currentSpan.pipe(
2405
+ Effect.map((span) => `\nTrace: ${span.traceId}\nSpan: ${span.spanId}`),
2406
+ Effect.orElseSucceed(() => "")
2407
+ )
2408
+ : ""
2409
+
2410
+ const t = yield* failureHandler(Cause.findErrorOption(cause))
2411
+ const opts = { timeout: baseTimeout * 2, ...meta }
2412
+
2413
+ if (typeof t === "object") {
2414
+ const message = t.message + spanInfo
2415
+ yield* t.level === "warn"
2416
+ ? toast.warning(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
2417
+ : toast.error(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
2418
+ } else {
2419
+ yield* toast.error(t + spanInfo, toastId !== undefined ? { ...opts, id: toastId } : opts)
2420
+ }
2421
+ }, Effect.uninterruptible)),
2422
+ Stream.ensuring(Effect.suspend(() => {
2423
+ if (didFail) return Effect.void
2424
+
2425
+ if (options?.onSuccess === null) return Effect.void
2426
+
2427
+ const successMsg: string | null = typeof options?.onSuccess === "string"
2428
+ ? options.onSuccess
2429
+ : typeof options?.onSuccess === "function"
2430
+ ? (options.onSuccess as (...a: any[]) => string | null | undefined)(lastValue, cc.action, ...args) ?? null
2431
+ : hasCustomSuccess
2432
+ ? intl.formatMessage({ id: customSuccess }, cc.state)
2433
+ : intl.formatMessage({ id: "handle.success" }, { action: cc.action })
2434
+
2435
+ if (successMsg === null) return Effect.void
2436
+
2437
+ return toast.success(
2438
+ successMsg,
2439
+ toastId !== undefined
2440
+ ? { id: toastId, timeout: baseTimeout, ...meta }
2441
+ : { timeout: baseTimeout, ...meta }
2442
+ )
2443
+ }))
2444
+ )
2445
+
2446
+ return (toastId !== undefined
2447
+ ? composed.pipe(Stream.provideService(CurrentToastId, CurrentToastId.of({ toastId })))
2448
+ : composed) as unknown as Stream.Stream<A, E, R>
2449
+ }))
2450
+ },
2451
+
1879
2452
  /** borrowing the idea from Families in Effect Atom */
1880
2453
  family: <T extends object, Arg, ArgIn = Arg>(
1881
2454
  maker: (arg: Arg) => T,
@@ -1936,7 +2509,11 @@ const unregisterWait = (id: string) => {
1936
2509
  }
1937
2510
  }
1938
2511
 
1939
- const getStateValues = <const Id extends string, const I18nKey extends string, State extends IntlRecord | undefined>(
2512
+ const getStateValues = <
2513
+ const Id extends string,
2514
+ const I18nKey extends string,
2515
+ State extends IntlRecord | undefined
2516
+ >(
1940
2517
  options?: FnOptions<Id, I18nKey, State>
1941
2518
  ): ComputedRef<State> => {
1942
2519
  const state_ = options?.state
@@ -1948,11 +2525,18 @@ const getStateValues = <const Id extends string, const I18nKey extends string, S
1948
2525
 
1949
2526
  // class preserves JSDoc throughout..
1950
2527
  export class CommanderImpl<RT, RTHooks> {
2528
+ private readonly rt: Context.Context<RT>
2529
+ private readonly intl: I18n
2530
+ private readonly hooks: Layer.Layer<RTHooks, never, RT>
2531
+
1951
2532
  constructor(
1952
- private readonly rt: Context.Context<RT>,
1953
- private readonly intl: I18n,
1954
- private readonly hooks: Layer.Layer<RTHooks, never, RT>
2533
+ rt: Context.Context<RT>,
2534
+ intl: I18n,
2535
+ hooks: Layer.Layer<RTHooks, never, RT>
1955
2536
  ) {
2537
+ this.rt = rt
2538
+ this.intl = intl
2539
+ this.hooks = hooks
1956
2540
  }
1957
2541
 
1958
2542
  readonly makeContext = <const Id extends string, const I18nKey extends string = Id>(
@@ -2109,6 +2693,7 @@ export class CommanderImpl<RT, RTHooks> {
2109
2693
  const runFork = Effect.runForkWith(rt)
2110
2694
 
2111
2695
  const handle = Object.assign((arg: Arg) => {
2696
+ arg = toRaw(arg) // remove outside vue proxy bs
2112
2697
  // we capture the call site stack here
2113
2698
  const limit = Error.stackTraceLimit
2114
2699
  Error.stackTraceLimit = 2
@@ -2139,15 +2724,17 @@ export class CommanderImpl<RT, RTHooks> {
2139
2724
  }
2140
2725
  }
2141
2726
 
2142
- const command = currentState.pipe(Effect.flatMap((state) =>
2143
- Effect.withSpan(
2727
+ const command = currentState.pipe(Effect.flatMap((state) => {
2728
+ const rawArg = deepToRaw(arg)
2729
+ const rawState = deepToRaw(state)
2730
+ return Effect.withSpan(
2144
2731
  exec(arg, { ...context.value, state } as any),
2145
2732
  id,
2146
2733
  {
2147
2734
  captureStackTrace,
2148
2735
  attributes: {
2149
- input: arg,
2150
- state,
2736
+ input: rawArg,
2737
+ state: rawState,
2151
2738
  action: initialContext.action,
2152
2739
  label: initialContext.label,
2153
2740
  id: initialContext.id,
@@ -2155,7 +2742,7 @@ export class CommanderImpl<RT, RTHooks> {
2155
2742
  }
2156
2743
  }
2157
2744
  )
2158
- ))
2745
+ }))
2159
2746
 
2160
2747
  return runFork(command)
2161
2748
  }, { action, label })
@@ -2174,6 +2761,8 @@ export class CommanderImpl<RT, RTHooks> {
2174
2761
 
2175
2762
  /** reactive */
2176
2763
  result,
2764
+ /** always undefined for non-stream commands */
2765
+ progress: undefined,
2177
2766
  /** reactive */
2178
2767
  waiting,
2179
2768
  /** reactive */
@@ -2281,8 +2870,9 @@ export class CommanderImpl<RT, RTHooks> {
2281
2870
  options?: FnOptions<Id, I18nKey, State>
2282
2871
  ): Commander.Gen<RT | RTHooks, Id, I18nKey, State> & Commander.NonGen<RT | RTHooks, Id, I18nKey, State> & {
2283
2872
  state: Context.Service<`Commander.Command.${Id}.state`, State>
2284
- } =>
2285
- Object.assign(
2873
+ } => {
2874
+ const resolvedId: Id = typeof id === "string" ? id : id.id
2875
+ return Object.assign(
2286
2876
  (
2287
2877
  fn: any,
2288
2878
  ...combinators: any[]
@@ -2293,7 +2883,7 @@ export class CommanderImpl<RT, RTHooks> {
2293
2883
  const errorDef = new Error()
2294
2884
  Error.stackTraceLimit = limit
2295
2885
 
2296
- return this.makeCommand(id, options, errorDef)(
2886
+ return this.makeCommand(resolvedId, options, errorDef)(
2297
2887
  Effect.fnUntraced(
2298
2888
  // fnUntraced only supports generators as first arg, so we convert to generator if needed
2299
2889
  isGeneratorFunction(fn) ? fn : function*(...args) {
@@ -2303,15 +2893,329 @@ export class CommanderImpl<RT, RTHooks> {
2303
2893
  ) as any
2304
2894
  )
2305
2895
  },
2306
- makeBaseInfo(typeof id === "string" ? id : id.id, options),
2896
+ makeBaseInfo(resolvedId, options),
2897
+ {
2898
+ state: Context.Service<`Commander.Command.${Id}.state`, State>(
2899
+ `Commander.Command.${resolvedId}.state`
2900
+ )
2901
+ }
2902
+ )
2903
+ }
2904
+
2905
+ /**
2906
+ * Internal factory for stream-backed commands. Accepts a handler that returns a `Stream` directly.
2907
+ * Services (`CommandContext`, `stateTag`) are provided to the stream via `Stream.provideServiceEffect`.
2908
+ */
2909
+ readonly makeStreamCommand = <
2910
+ const Id extends string,
2911
+ const State extends IntlRecord | undefined,
2912
+ const I18nKey extends string = Id
2913
+ >(
2914
+ id_: Id | { id: Id },
2915
+ options?: FnOptions<Id, I18nKey, State>,
2916
+ errorDef?: Error
2917
+ ) => {
2918
+ const id = typeof id_ === "string" ? id_ : id_.id
2919
+ const state = getStateValues(options)
2920
+
2921
+ return Object.assign(
2922
+ <Arg, SA, SE, SR>(
2923
+ handler: (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>
2924
+ ) => {
2925
+ const limit = Error.stackTraceLimit
2926
+ Error.stackTraceLimit = 2
2927
+ const localErrorDef = new Error()
2928
+ Error.stackTraceLimit = limit
2929
+ if (!errorDef) {
2930
+ errorDef = localErrorDef
2931
+ }
2932
+
2933
+ const key = `Commander.Command.${id}.state` as const
2934
+ const stateTag = Context.Service<typeof key, State>(key)
2935
+
2936
+ const makeContext_ = () => this.makeContext(id, { ...options, state: state?.value })
2937
+ const initialContext = makeContext_()
2938
+ const context = computed(() => makeContext_())
2939
+ const action = computed(() => context.value.action)
2940
+ const label = computed(() => context.value.label)
2941
+
2942
+ const currentState = Effect.sync(() => state.value)
2943
+
2944
+ // Reactive ref driven by the CommandProgress service — updated imperatively
2945
+ // from inside the stream via `Command.mapProgress(fn)` or `Command.updateProgress(p)`.
2946
+ const progressRef = ref<Progress | undefined>(undefined)
2947
+ const commandProgressService = {
2948
+ update: (p: Progress | undefined) =>
2949
+ Effect.sync(() => {
2950
+ progressRef.value = p
2951
+ })
2952
+ }
2953
+
2954
+ const streamErrorReporter = <A, E, R>(self: Stream.Stream<A, E, R>) =>
2955
+ self.pipe(
2956
+ Stream.tapCause(
2957
+ Effect.fnUntraced(function*(cause) {
2958
+ if (Cause.hasInterruptsOnly(cause)) {
2959
+ console.info(`Interrupted while trying to ${id}`)
2960
+ return
2961
+ }
2962
+
2963
+ const fail = Cause.findErrorOption(cause)
2964
+ if (Option.isSome(fail)) {
2965
+ const message = `Failure trying to ${id}`
2966
+ yield* reportMessage(message, {
2967
+ action: id,
2968
+ error: fail.value
2969
+ })
2970
+ return
2971
+ }
2972
+
2973
+ const ctx = yield* CommandContext
2974
+ const extra = {
2975
+ action: ctx.action,
2976
+ message: `Unexpected Error trying to ${id}`
2977
+ }
2978
+ yield* reportRuntimeError(cause, extra)
2979
+ }, Effect.uninterruptible)
2980
+ )
2981
+ )
2982
+
2983
+ const theStreamHandler = (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) =>
2984
+ handler(arg, ctx).pipe(
2985
+ streamErrorReporter,
2986
+ Stream.provideService(CommandProgress, commandProgressService),
2987
+ Stream.provideServiceEffect(stateTag, currentState),
2988
+ Stream.provideServiceEffect(CommandContext, Effect.sync(() => makeContext_()))
2989
+ )
2990
+
2991
+ const waitId = options?.waitKey ? options.waitKey(id) : undefined
2992
+ const blockId = options?.blockKey ? options.blockKey(id) : undefined
2993
+
2994
+ const [result, exec_] = asStreamResult(theStreamHandler)
2995
+
2996
+ const exec = Effect
2997
+ .fnUntraced(
2998
+ function*(...args: [any, any]) {
2999
+ if (waitId !== undefined) registerWait(waitId)
3000
+ if (blockId !== undefined && blockId !== waitId) {
3001
+ registerWait(blockId)
3002
+ }
3003
+ return yield* exec_(...args)
3004
+ },
3005
+ Effect.onExit(() =>
3006
+ Effect.sync(() => {
3007
+ if (waitId !== undefined) unregisterWait(waitId)
3008
+ if (blockId !== undefined && blockId !== waitId) {
3009
+ unregisterWait(blockId)
3010
+ }
3011
+ })
3012
+ )
3013
+ )
3014
+
3015
+ const waiting = waitId !== undefined
3016
+ ? computed(() => result.value.waiting || (waitState.value[waitId] ?? 0) > 0)
3017
+ : computed(() => result.value.waiting)
3018
+
3019
+ const blocked = blockId !== undefined
3020
+ ? computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0)
3021
+ : computed(() => waiting.value)
3022
+
3023
+ const computeAllowed = options?.allowed
3024
+ const allowed = computeAllowed ? computed(() => computeAllowed(id, state)) : true
3025
+
3026
+ const rt = Effect.context<RT | RTHooks>().pipe(Effect.provide(this.hooks)).pipe(Effect.runSyncWith(this.rt))
3027
+ const runFork = Effect.runForkWith(rt)
3028
+
3029
+ const progress = progressRef
3030
+
3031
+ const handle = Object.assign((arg: Arg) => {
3032
+ arg = toRaw(arg)
3033
+ progressRef.value = undefined // reset progress on new invocation
3034
+ const limit = Error.stackTraceLimit
3035
+ Error.stackTraceLimit = 2
3036
+ const errorCall = new Error()
3037
+ Error.stackTraceLimit = limit
3038
+
3039
+ let cache: false | string = false
3040
+ const captureStackTrace = () => {
3041
+ if (cache !== false) {
3042
+ return cache
3043
+ }
3044
+ if (errorCall.stack) {
3045
+ const stackDef = errorDef!.stack!.trim().split("\n")
3046
+ const stackCall = errorCall.stack.trim().split("\n")
3047
+ let endStackDef = stackDef.slice(2).join("\n").trim()
3048
+ if (!endStackDef.includes(`(`)) {
3049
+ endStackDef = endStackDef.replace(/at (.*)/, "at ($1)")
3050
+ }
3051
+ let endStackCall = stackCall.slice(2).join("\n").trim()
3052
+ if (!endStackCall.includes(`(`)) {
3053
+ endStackCall = endStackCall.replace(/at (.*)/, "at ($1)")
3054
+ }
3055
+ cache = `${endStackDef}\n${endStackCall}`
3056
+ return cache
3057
+ }
3058
+ }
3059
+
3060
+ const command = currentState.pipe(Effect.flatMap((state) => {
3061
+ const rawArg = deepToRaw(arg)
3062
+ const rawState = deepToRaw(state)
3063
+ return Effect.withSpan(
3064
+ exec(arg, { ...context.value, state } as any),
3065
+ id,
3066
+ {
3067
+ captureStackTrace,
3068
+ attributes: {
3069
+ input: rawArg,
3070
+ state: rawState,
3071
+ action: initialContext.action,
3072
+ label: initialContext.label,
3073
+ id: initialContext.id,
3074
+ i18nKey: initialContext.i18nKey
3075
+ }
3076
+ }
3077
+ )
3078
+ }))
3079
+
3080
+ return runFork(command as any)
3081
+ }, { action, label })
3082
+
3083
+ return reactive({
3084
+ id,
3085
+ i18nKey: initialContext.i18nKey,
3086
+ namespace: initialContext.namespace,
3087
+ namespaced: initialContext.namespaced,
3088
+ result,
3089
+ /** reactive – progress driven by `Command.mapProgress` or `Command.updateProgress` inside the stream */
3090
+ progress,
3091
+ waiting,
3092
+ blocked,
3093
+ allowed,
3094
+ action,
3095
+ label,
3096
+ state,
3097
+ handle
3098
+ })
3099
+ },
3100
+ { id }
3101
+ )
3102
+ }
3103
+
3104
+ /**
3105
+ * Define a stream-backed Command for handling user actions.
3106
+ *
3107
+ * Like `fn`, but the body generator (or function) must **return** a `Stream` rather than
3108
+ * an `Effect`. The command's `waiting` state stays `true` while the stream is running and
3109
+ * is set to `false` once it terminates. The reactive `result` ref is updated for every
3110
+ * value emitted by the stream.
3111
+ *
3112
+ * Three handler shapes are accepted:
3113
+ * 1. **Generator returning a Stream** (primary) — may yield Effects freely before returning the stream:
3114
+ * ```ts
3115
+ * Command.streamFn("exportData")(
3116
+ * function*(arg, ctx) {
3117
+ * const token = yield* getAuthToken
3118
+ * return Stream.fromEffect(startExport(token, arg.id)).pipe(
3119
+ * Stream.flatMap((job) => pollProgress(job.id))
3120
+ * )
3121
+ * }
3122
+ * )
3123
+ * ```
3124
+ * 2. **Function returning a Stream directly**: `(arg, ctx) => Stream.make(1, 2, 3)`
3125
+ * 3. **Function returning `Effect<Stream>`**: `(arg, ctx) => Effect.map(setup, (s) => s.stream)`
3126
+ *
3127
+ * @param id The internal identifier for the action (used for tracing and i18n lookup).
3128
+ * @param options Same options as `fn` (`state`, `blockKey`, `waitKey`, `allowed`, `i18nCustomKey`).
3129
+ *
3130
+ * **Progress** — use `Command.mapProgress(fn)` as a stream pipe operator; the mapper receives
3131
+ * `AsyncResult<A, E>` (each value wrapped as `AsyncResult.success(v, { waiting: true })`),
3132
+ * matching the same shape as CommandButton’s `:progress-map` prop. Or call
3133
+ * `Command.updateProgress(p)` for imperative control:
3134
+ *
3135
+ * ```ts
3136
+ * // mapProgress as a combinator arg (outside the handler):
3137
+ * Command.streamFn("exportData")(
3138
+ * function*(arg, ctx) { return makeExportStream(arg.id) },
3139
+ * (s) => s.pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) && r.value._tag === "OperationProgress" ? { text: `${r.value.completed}/${r.value.total}` } : undefined))
3140
+ * )
3141
+ *
3142
+ * // Or inline inside the handler body:
3143
+ * Command.streamFn("exportData")(function*(arg, ctx) {
3144
+ * return makeExportStream(arg.id).pipe(Command.mapProgress((r) => AsyncResult.isSuccess(r) ? ... : undefined))
3145
+ * })
3146
+ * ```
3147
+ *
3148
+ * **Pipeable combinators** — the 2nd–Nth args follow the same pattern as `fn`: each combinator
3149
+ * receives `(stream, arg, ctx)` and returns a transformed stream:
3150
+ * ```ts
3151
+ * Command.streamFn("exportData")(
3152
+ * handler,
3153
+ * (s, arg, ctx) => s.pipe(Command.mapProgress(fn), Stream.take(100))
3154
+ * )
3155
+ * ```
3156
+ *
3157
+ * **Returned Properties**: `action`, `label`, `result`, `progress`, `waiting`, `blocked`,
3158
+ * `allowed`, `handle`, `i18nKey`, `namespace`, `namespaced`.
3159
+ */
3160
+ streamFn = <
3161
+ const Id extends string,
3162
+ const State extends IntlRecord = IntlRecord,
3163
+ const I18nKey extends string = Id
3164
+ >(
3165
+ id: Id | { id: Id },
3166
+ options?: FnOptions<Id, I18nKey, State>
3167
+ ):
3168
+ & Commander.StreamGen<RT | RTHooks, Id, I18nKey, State>
3169
+ & Commander.NonGenStream<RT | RTHooks, Id, I18nKey, State>
3170
+ & {
3171
+ state: Context.Service<`Commander.Command.${Id}.state`, State>
3172
+ } =>
3173
+ {
3174
+ const resolvedId = typeof id === "string" ? id : id.id
3175
+
3176
+ type StreamOrEffect = Stream.Stream<any, any, any> | Effect.Effect<Stream.Stream<any, any, any>, any, any>
3177
+
3178
+ const toRawHandler = (fn: any): (arg: any, ctx: any) => StreamOrEffect => {
3179
+ if (isGeneratorFunction(fn)) {
3180
+ return Effect.fnUntraced(function*(arg: any, ctx: any) {
3181
+ return yield* (fn as (arg: any, ctx: any) => Generator<any, Stream.Stream<any, any, any>>)(arg, ctx)
3182
+ })
3183
+ }
3184
+ return fn
3185
+ }
3186
+
3187
+ const toFinalStream = (value: StreamOrEffect): Stream.Stream<any, any, any> =>
3188
+ Stream.isStream(value) ? value : Stream.unwrap(value as Effect.Effect<Stream.Stream<any, any, any>, any, any>)
3189
+
3190
+ return Object.assign(
3191
+ (fn: any, ...combinators: Array<(s: any, arg: any, ctx: any) => any>): any => {
3192
+ const limit = Error.stackTraceLimit
3193
+ Error.stackTraceLimit = 2
3194
+ const errorDef = new Error()
3195
+ Error.stackTraceLimit = limit
3196
+
3197
+ const rawHandler = toRawHandler(fn)
3198
+ const handler = (arg: any, ctx: any) => {
3199
+ let current: any = rawHandler(arg, ctx)
3200
+ for (const combinator of combinators) {
3201
+ current = combinator(current, arg, ctx)
3202
+ }
3203
+ return toFinalStream(current)
3204
+ }
3205
+
3206
+ return this.makeStreamCommand(id, options, errorDef)(handler)
3207
+ },
3208
+ makeBaseInfo(resolvedId, options),
2307
3209
  {
2308
3210
  state: Context.Service<`Commander.Command.${Id}.state`, State>(
2309
- `Commander.Command.${typeof id === "string" ? id : id.id}.state`
3211
+ `Commander.Command.${resolvedId}.state`
2310
3212
  )
2311
3213
  }
2312
3214
  )
3215
+ }
2313
3216
 
2314
3217
  /** @deprecated */
3218
+
2315
3219
  alt2: <
2316
3220
  const Id extends string,
2317
3221
  MutArg,
@@ -2403,6 +3307,46 @@ export class CommanderImpl<RT, RTHooks> {
2403
3307
  * **User Feedback**: Use the `withDefaultToast` helper for status notifications, or render
2404
3308
  * the `result` inline for custom UI feedback.
2405
3309
  */
3310
+ streamWrap = <
3311
+ const Id extends string,
3312
+ Arg,
3313
+ SA,
3314
+ SE,
3315
+ SR,
3316
+ const State extends IntlRecord = IntlRecord,
3317
+ I18nKey extends string = Id
3318
+ >(
3319
+ handler: (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) => Stream.Stream<SA, SE, SR>,
3320
+ id: Id,
3321
+ options?: FnOptions<Id, I18nKey, State>
3322
+ ): Commander.StreamerWrap<RT | RTHooks, Id, I18nKey, State, Arg, SA, SE, SR> => {
3323
+ return Object.assign(
3324
+ (...combinators: any[]): any => {
3325
+ const limit = Error.stackTraceLimit
3326
+ Error.stackTraceLimit = 2
3327
+ const errorDef = new Error()
3328
+ Error.stackTraceLimit = limit
3329
+ return this.makeStreamCommand(id, options, errorDef)(
3330
+ combinators.length === 0
3331
+ ? handler
3332
+ : (arg: Arg, ctx: Commander.CommandContextLocal2<Id, I18nKey, State>) => {
3333
+ let current: any = handler(arg, ctx)
3334
+ for (const combinator of combinators) {
3335
+ current = combinator(current, arg, ctx)
3336
+ }
3337
+ return current
3338
+ }
3339
+ )
3340
+ },
3341
+ makeBaseInfo(id, options),
3342
+ {
3343
+ state: Context.Service<`Commander.Command.${Id}.state`, State>(
3344
+ `Commander.Command.${id}.state`
3345
+ )
3346
+ }
3347
+ )
3348
+ }
3349
+
2406
3350
  wrap = <
2407
3351
  const Id extends string,
2408
3352
  Arg,
@@ -2416,8 +3360,9 @@ export class CommanderImpl<RT, RTHooks> {
2416
3360
  | { mutate: (arg: Arg) => Effect.Effect<A, E, R>; id: Id }
2417
3361
  | ((arg: Arg) => Effect.Effect<A, E, R>) & { id: Id },
2418
3362
  options?: FnOptions<Id, I18nKey, State>
2419
- ): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> =>
2420
- Object.assign(
3363
+ ): Commander.CommanderWrap<RT | RTHooks, Id, I18nKey, State, Arg, A, E, R> => {
3364
+ const callMutation = mutation
3365
+ return Object.assign(
2421
3366
  (
2422
3367
  ...combinators: any[]
2423
3368
  ): any => {
@@ -2426,9 +3371,11 @@ export class CommanderImpl<RT, RTHooks> {
2426
3371
  Error.stackTraceLimit = 2
2427
3372
  const errorDef = new Error()
2428
3373
  Error.stackTraceLimit = limit
2429
- const mutate = "mutate" in mutation ? mutation.mutate : mutation
3374
+ const mutate = "mutate" in callMutation
3375
+ ? callMutation.mutate
3376
+ : callMutation
2430
3377
 
2431
- return this.makeCommand(mutation.id, options, errorDef)(
3378
+ return this.makeCommand(callMutation.id, options, errorDef)(
2432
3379
  Effect.fnUntraced(
2433
3380
  // fnUntraced only supports generators as first arg, so we convert to generator if needed
2434
3381
  isGeneratorFunction(mutate) ? mutate : function*(arg: Arg) {
@@ -2438,13 +3385,14 @@ export class CommanderImpl<RT, RTHooks> {
2438
3385
  ) as any
2439
3386
  )
2440
3387
  },
2441
- makeBaseInfo(mutation.id, options),
3388
+ makeBaseInfo(callMutation.id, options),
2442
3389
  {
2443
3390
  state: Context.Service<`Commander.Command.${Id}.state`, State>(
2444
- `Commander.Command.${mutation.id}.state`
3391
+ `Commander.Command.${callMutation.id}.state`
2445
3392
  )
2446
3393
  }
2447
3394
  )
3395
+ }
2448
3396
  }
2449
3397
 
2450
3398
  // @effect-diagnostics-next-line missingEffectServiceDependency:off