@effect-app/vue 4.0.0-beta.19 → 4.0.0-beta.190

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 (103) hide show
  1. package/CHANGELOG.md +1383 -0
  2. package/dist/commander.d.ts +620 -0
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +1056 -0
  5. package/dist/confirm.d.ts +19 -0
  6. package/dist/confirm.d.ts.map +1 -0
  7. package/dist/confirm.js +24 -0
  8. package/dist/errorReporter.d.ts +4 -4
  9. package/dist/errorReporter.d.ts.map +1 -1
  10. package/dist/errorReporter.js +12 -18
  11. package/dist/form.d.ts +13 -4
  12. package/dist/form.d.ts.map +1 -1
  13. package/dist/form.js +41 -12
  14. package/dist/index.d.ts +1 -1
  15. package/dist/intl.d.ts +15 -0
  16. package/dist/intl.d.ts.map +1 -0
  17. package/dist/intl.js +9 -0
  18. package/dist/lib.d.ts +6 -8
  19. package/dist/lib.d.ts.map +1 -1
  20. package/dist/lib.js +34 -7
  21. package/dist/makeClient.d.ts +191 -292
  22. package/dist/makeClient.d.ts.map +1 -1
  23. package/dist/makeClient.js +217 -369
  24. package/dist/makeContext.d.ts +1 -1
  25. package/dist/makeContext.d.ts.map +1 -1
  26. package/dist/makeIntl.d.ts +1 -1
  27. package/dist/makeIntl.d.ts.map +1 -1
  28. package/dist/makeUseCommand.d.ts +8 -0
  29. package/dist/makeUseCommand.d.ts.map +1 -0
  30. package/dist/makeUseCommand.js +13 -0
  31. package/dist/mutate.d.ts +56 -25
  32. package/dist/mutate.d.ts.map +1 -1
  33. package/dist/mutate.js +132 -33
  34. package/dist/query.d.ts +24 -16
  35. package/dist/query.d.ts.map +1 -1
  36. package/dist/query.js +119 -37
  37. package/dist/routeParams.d.ts +1 -1
  38. package/dist/runtime.d.ts +5 -2
  39. package/dist/runtime.d.ts.map +1 -1
  40. package/dist/runtime.js +27 -17
  41. package/dist/toast.d.ts +46 -0
  42. package/dist/toast.d.ts.map +1 -0
  43. package/dist/toast.js +32 -0
  44. package/dist/withToast.d.ts +26 -0
  45. package/dist/withToast.d.ts.map +1 -0
  46. package/dist/withToast.js +54 -0
  47. package/eslint.config.mjs +2 -2
  48. package/examples/streamMutation.ts +70 -0
  49. package/package.json +48 -48
  50. package/src/commander.ts +3378 -0
  51. package/src/{experimental/confirm.ts → confirm.ts} +10 -14
  52. package/src/errorReporter.ts +62 -74
  53. package/src/form.ts +55 -16
  54. package/src/intl.ts +12 -0
  55. package/src/lib.ts +46 -13
  56. package/src/makeClient.ts +623 -1043
  57. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +6 -4
  58. package/src/mutate.ts +273 -72
  59. package/src/query.ts +181 -68
  60. package/src/runtime.ts +39 -18
  61. package/src/{experimental/toast.ts → toast.ts} +11 -25
  62. package/src/{experimental/withToast.ts → withToast.ts} +28 -10
  63. package/test/Mutation.test.ts +105 -11
  64. package/test/dist/form.test.d.ts.map +1 -1
  65. package/test/dist/lib.test.d.ts.map +1 -0
  66. package/test/dist/streamFinal.test.d.ts.map +1 -0
  67. package/test/dist/streamFn.test.d.ts.map +1 -0
  68. package/test/dist/stubs.d.ts +3289 -114
  69. package/test/dist/stubs.d.ts.map +1 -1
  70. package/test/dist/stubs.js +152 -25
  71. package/test/form-validation-errors.test.ts +23 -19
  72. package/test/form.test.ts +20 -2
  73. package/test/lib.test.ts +240 -0
  74. package/test/makeClient.test.ts +286 -38
  75. package/test/streamFinal.test.ts +63 -0
  76. package/test/streamFn.test.ts +436 -0
  77. package/test/stubs.ts +192 -42
  78. package/tsconfig.examples.json +20 -0
  79. package/tsconfig.json +0 -1
  80. package/tsconfig.json.bak +5 -2
  81. package/tsconfig.src.json +34 -34
  82. package/tsconfig.test.json +2 -2
  83. package/vitest.config.ts +5 -5
  84. package/dist/experimental/commander.d.ts +0 -359
  85. package/dist/experimental/commander.d.ts.map +0 -1
  86. package/dist/experimental/commander.js +0 -557
  87. package/dist/experimental/confirm.d.ts +0 -19
  88. package/dist/experimental/confirm.d.ts.map +0 -1
  89. package/dist/experimental/confirm.js +0 -28
  90. package/dist/experimental/intl.d.ts +0 -16
  91. package/dist/experimental/intl.d.ts.map +0 -1
  92. package/dist/experimental/intl.js +0 -5
  93. package/dist/experimental/makeUseCommand.d.ts +0 -8
  94. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  95. package/dist/experimental/makeUseCommand.js +0 -13
  96. package/dist/experimental/toast.d.ts +0 -47
  97. package/dist/experimental/toast.d.ts.map +0 -1
  98. package/dist/experimental/toast.js +0 -41
  99. package/dist/experimental/withToast.d.ts +0 -25
  100. package/dist/experimental/withToast.d.ts.map +0 -1
  101. package/dist/experimental/withToast.js +0 -45
  102. package/src/experimental/commander.ts +0 -1835
  103. package/src/experimental/intl.ts +0 -9
@@ -1,95 +1,343 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { type Effect } from "effect-app"
3
- import { Something, useClient, useExperimental } from "./stubs.js"
4
-
5
- it.skip("works2", () => {
6
- const { legacy } = useClient()
7
- const n = legacy.useQuery({
8
- Request: null as any,
9
- handler: null as any as (a: string) => Effect.Effect<number>,
10
- id: "id"
2
+ import { expect, expectTypeOf, it } from "@effect/vitest"
3
+ import { S } from "effect-app"
4
+ import { configureInvalidation, makeQueryKey } from "effect-app/client"
5
+ import type { CommandFromRequest } from "../src/makeClient.js"
6
+ import * as Exit from "effect/Exit"
7
+ import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js"
8
+
9
+ const somethingInvalidationResources = {
10
+ Something: {
11
+ GetSomething2: Something.GetSomething2,
12
+ GetSomething2WithDependencies: Something.GetSomething2WithDependencies,
13
+ GetSomething3: Something.GetSomething3,
14
+ GetSomething4: Something.GetSomething4
15
+ }
16
+ }
17
+
18
+ it("TaggedRequestFor .moduleName and request .id / .moduleName", () => {
19
+ expectTypeOf(SomethingReq.moduleName).toEqualTypeOf<"Something">()
20
+ expectTypeOf(SomethingElseReq.moduleName).toEqualTypeOf<"SomethingElse">()
21
+
22
+ expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">()
23
+ expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">()
24
+ expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">()
25
+ expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">()
26
+
27
+ expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">()
28
+ expectTypeOf(SomethingElse.GetSomething2.id).toEqualTypeOf<"SomethingElse.GetSomething2">()
29
+
30
+ const invalidates = configureInvalidation<{
31
+ Something: typeof Something
32
+ SomethingElse: typeof SomethingElse
33
+ }>()((queryKey, { Something, SomethingElse }) => [
34
+ { filters: { queryKey } },
35
+ { filters: { queryKey: makeQueryKey(Something.GetSomething2) } },
36
+ { filters: { queryKey: makeQueryKey(SomethingElse.GetSomething2) } }
37
+ ])
38
+
39
+ expectTypeOf(invalidates.invalidatesQueries).toBeFunction()
40
+ configureInvalidation<{ Something: typeof Something }>()((_queryKey, { Something }) => {
41
+ // @ts-expect-error commands are intentionally excluded from configured resources
42
+ void Something.DoSomething
43
+ return []
11
44
  })
12
45
 
13
- const [, z] = n("a")
46
+ const { clientFor } = useClient()
47
+ const client = clientFor(
48
+ Something,
49
+ undefined,
50
+ somethingInvalidationResources
51
+ )
52
+
53
+ // only queries, no commands, and no commands who require resources; shouldn't require invalidation resources args!
54
+ clientFor({ GetSomething: Something.GetSomething2 })
55
+
56
+ // @ts-expect-error invalidation resources should be required when any command configures them
57
+ clientFor(Something)
58
+
59
+ // @ts-expect-error invalidation resources for this module reject extra top-level resources
60
+ clientFor(Something, undefined, { ...somethingInvalidationResources, SomethingElse })
61
+
62
+ const doSomethingInvalidation = client.DoSomething.Request.config["invalidatesQueries"]
63
+ if (doSomethingInvalidation) {
64
+ const entries = doSomethingInvalidation(
65
+ ["$Something"],
66
+ somethingInvalidationResources,
67
+ { id: "abc" },
68
+ Exit.succeed(123)
69
+ )
70
+ expect(Array.isArray(entries)).toBe(true)
71
+ }
72
+
73
+ const SomethingCommand = SomethingReq.Command
74
+
75
+ class TypeInferenceWithSuccess extends SomethingCommand<TypeInferenceWithSuccess>()("TypeInferenceWithSuccess", {
76
+ id: S.String
77
+ }, {
78
+ success: S.FiniteFromString
79
+ }, (_queryKey, _resources, input, result) => {
80
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
81
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<number, never>>()
82
+ return []
83
+ }) {}
84
+ void TypeInferenceWithSuccess
85
+
86
+ class TypeInferenceWithoutSuccess extends SomethingCommand<TypeInferenceWithoutSuccess>()(
87
+ "TypeInferenceWithoutSuccess",
88
+ {
89
+ id: S.String
90
+ },
91
+ {},
92
+ (_queryKey, _resources, input, result) => {
93
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
94
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<void, never>>()
95
+ return []
96
+ }
97
+ ) {}
98
+ void TypeInferenceWithoutSuccess
99
+
100
+ type MixedResources = {
101
+ Something: typeof Something
102
+ Misc: {
103
+ value: number
104
+ GetSomething2: typeof Something.GetSomething2
105
+ }
106
+ }
107
+
108
+ class TypeInferenceResourceFiltering extends SomethingCommand<
109
+ TypeInferenceResourceFiltering,
110
+ MixedResources
111
+ >()("TypeInferenceResourceFiltering", {
112
+ id: S.String
113
+ }, {
114
+ success: S.FiniteFromString
115
+ }, (_queryKey, resources, _input, _result) => {
116
+ expectTypeOf(resources.Something.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
117
+ expectTypeOf(resources.Misc.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
118
+
119
+ // @ts-expect-error commands must be filtered from invalidation resources
120
+ void resources.Something.DoSomething
121
+ // @ts-expect-error non-query values must be filtered from invalidation resources
122
+ void resources.Misc.value
123
+
124
+ return []
125
+ }) {}
126
+ void TypeInferenceResourceFiltering
127
+
128
+ type WithSuccessInvalidation = NonNullable<typeof TypeInferenceWithSuccess.config.invalidatesQueries> // @ts-expect-error input should be required when command payload is non-empty
129
+ ;((_queryKey, _resources) => []) satisfies WithSuccessInvalidation
130
+ })
131
+
132
+ it("clientFor handler shape — props variants", () => {
133
+ const { clientFor } = useClient()
134
+ const client = clientFor(
135
+ Something,
136
+ undefined,
137
+ somethingInvalidationResources
138
+ )
139
+ expect(client).toBeDefined()
140
+
141
+ // no-props (no fields): handler is the Effect itself (RequestHandler), not a function
142
+ expectTypeOf(client.DoNoProps.handler).not.toBeFunction()
143
+
144
+ // optional-only: any fields → function handler. Input matches `make`, which for
145
+ // fully-optional payload is omittable.
146
+ expectTypeOf(client.DoOptionalOnly.handler).toBeFunction()
147
+ // arg may be omitted entirely
148
+ client.DoOptionalOnly.handler()
149
+ // or supplied with all-optional payload
150
+ client.DoOptionalOnly.handler({})
151
+ client.DoOptionalOnly.handler({ name: "x" })
152
+
153
+ // required-only: function, `id` required
154
+ expectTypeOf(client.DoRequiredOnly.handler).toBeFunction()
155
+ client.DoRequiredOnly.handler({ id: "x" })
156
+ // @ts-expect-error id is required
157
+ client.DoRequiredOnly.handler({})
158
+ // @ts-expect-error arg cannot be omitted
159
+ client.DoRequiredOnly.handler()
160
+
161
+ // mixed: id required, name optional
162
+ expectTypeOf(client.DoMixed.handler).toBeFunction()
163
+ client.DoMixed.handler({ id: "x" })
164
+ client.DoMixed.handler({ id: "x", name: "y" })
165
+ // @ts-expect-error id required
166
+ client.DoMixed.handler({ name: "y" })
167
+ })
168
+
169
+ it("CommandFromRequest input shape — props variants", () => {
170
+ type NoPropsArg = Parameters<CommandFromRequest<typeof Something.DoNoProps>["handle"]>[0]
171
+
172
+ // no-props (no fields) → void input
173
+ expectTypeOf<NoPropsArg>().toBeVoid()
174
+
175
+ // type-only assignability checks for the remaining variants
176
+ if (false as boolean) {
177
+ const optOnly = null as unknown as CommandFromRequest<typeof Something.DoOptionalOnly>
178
+ const reqOnly = null as unknown as CommandFromRequest<typeof Something.DoRequiredOnly>
179
+ const mixed = null as unknown as CommandFromRequest<typeof Something.DoMixed>
180
+
181
+ // optional-only → matches `make` (fully optional, arg omittable)
182
+ optOnly.handle()
183
+ optOnly.handle({})
184
+ optOnly.handle({ name: "x" })
185
+
186
+ // required-only → id required
187
+ reqOnly.handle({ id: "x" })
188
+ // @ts-expect-error id required
189
+ reqOnly.handle({})
190
+
191
+ // mixed → id required, name optional
192
+ mixed.handle({ id: "x" })
193
+ mixed.handle({ id: "x", name: "y" })
194
+ // @ts-expect-error id required
195
+ mixed.handle({ name: "y" })
196
+ }
197
+ })
198
+
199
+ it.skip("query type tests", () => {
200
+ const { clientFor } = useClient()
201
+ const client = clientFor(
202
+ Something,
203
+ () => ({
204
+ GetSomething2WithDependencies: (queryKey) => [
205
+ { filters: { queryKey } },
206
+ {
207
+ filters: {
208
+ queryKey: makeQueryKey(
209
+ SomethingElse
210
+ .GetSomething2
211
+ )
212
+ }
213
+ }
214
+ ]
215
+ }),
216
+ somethingInvalidationResources
217
+ )
218
+
219
+ const q = client.GetSomething2.query
220
+
221
+ const [, z] = q({ id: "a" })
14
222
  const valz = z.value
15
223
  expectTypeOf(valz).toEqualTypeOf<number | undefined>()
16
224
 
17
- const [, a] = n("a", { placeholderData: () => 123 })
225
+ const [, a] = q({ id: "a" }, { placeholderData: () => 123 })
18
226
  const val1 = a.value
19
227
  expectTypeOf(val1).toEqualTypeOf<number>()
20
228
 
21
- const [, bbbb] = n("a", { select: (data) => data.toString() })
229
+ const [, bbbb] = q({ id: "a" }, { select: (data) => data.toString() })
22
230
  const val = bbbb.value
23
231
  expectTypeOf(val).toEqualTypeOf<string | undefined>()
24
232
 
25
- const [, ccc] = n("a", { placeholderData: () => 123, select: (data) => data.toString() })
233
+ const [, ccc] = q({ id: "a" }, { placeholderData: () => 123, select: (data) => data.toString() })
26
234
  const val2 = ccc.value
27
235
  expectTypeOf(val2).toEqualTypeOf<string>()
28
236
 
29
- const [, ddd] = n("a", { initialData: 123, select: (data) => data.toString() })
237
+ const [, ddd] = q({ id: "a" }, { initialData: 123, select: (data) => data.toString() })
30
238
  const val3 = ddd.value
31
239
  expectTypeOf(val3).toEqualTypeOf<string>()
32
240
 
33
- const [, eee] = n("a", { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
241
+ const [, eee] = q({ id: "a" }, { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
34
242
  const val4 = eee.value
35
243
  expectTypeOf(val4).toEqualTypeOf<string>()
36
244
  })
37
245
 
38
246
  it.skip("works", () => {
39
- const { clientFor, legacy } = useClient()
40
- const client = clientFor(Something)
247
+ const { clientFor } = useClient()
248
+ const client = clientFor(Something, undefined, somethingInvalidationResources)
41
249
  const Command = useExperimental()
42
250
 
43
251
  // just for jsdoc / type testing.
44
- const a0 = client.GetSomething2(null as any)
45
- const a00 = client.GetSomething2.mutate(null as any)
252
+ const a0 = client.GetSomething2.request(null as any)
253
+ const a00 = client.DoSomething.mutate(null as any)
46
254
  const a = client.GetSomething2.suspense(null as any)
47
255
  const b = client.GetSomething2.query(null as any)
48
256
 
49
- const c0 = legacy.useSafeMutation(null as any)
50
- const c = legacy.useQuery(null as any)
51
- const d = legacy.useSuspenseQuery(null as any)
257
+ const de = client.GetSomething3.handler(null as any)
258
+ const de2 = client.GetSomething3.handler({ id: null })
259
+
260
+ // @ts-expect-error not callable as it requires no input
261
+ const de3 = client.GetSomething4.handler(null as any)
262
+ void client.GetSomething4.handler
52
263
 
264
+ // @ts-expect-error query requests no longer expose command helpers
53
265
  const e = client.GetSomething2.wrap(null as any)
266
+ // @ts-expect-error query requests no longer expose command helpers
54
267
  const f = client.GetSomething2.fn(null as any)
55
268
 
56
- // @ts-expect-error dependencies required that are not provided
57
- const e0 = client.GetSomething2WithDependencies.wrap().handle // not available as we require dependencies not provided by the runtime
58
- // @ts-expect-error dependencies required that are not provided
59
- const e000 = Command.wrap(client.GetSomething2WithDependencies)().handle // not available as we require dependencies not provided by the runtime
60
- const e00 = client.GetSomething2WithDependencies.wrap((_) => _ as Effect.Effect<number, never, never>).handle(
61
- null as any
62
- )
63
- const e0000 =
64
- Command.wrap(client.GetSomething2WithDependencies)((_) => _ as Effect.Effect<number, never, never>).handle
269
+ // @ts-expect-error query requests no longer expose command helpers
270
+ const e0 = client.GetSomething2WithDependencies.wrap
271
+ // @ts-expect-error query request does not match Command.wrap mutation signature
272
+ const e000 = Command.wrap(client.GetSomething2WithDependencies)
273
+ const e00 = client.GetSomething2WithDependencies.request(null as any)
65
274
  // @ts-expect-error dependencies required that are not provided
66
275
  const e1 = client.GetSomething2WithDependencies.suspense(null as any)
67
276
  // @ts-expect-error dependencies required that are not provided
68
277
  const e2 = client.GetSomething2WithDependencies.query(null as any)
278
+ // @ts-expect-error query requests no longer expose command helpers
69
279
  const f0 = client.GetSomething2WithDependencies.fn(null as any)
70
280
 
71
- const g = client.GetSomething2.mutate.wrap(null as any)
72
- const h = client.GetSomething2.mutate.fn(null as any)
281
+ const g0 = client.DoSomething.wrap(null as any)
282
+ const g = client.DoSomething.mutate.wrap(null as any)
283
+ const g1 = client.DoSomething.mutate.project(S.String)
284
+ const g2 = g1(null as any)
285
+ const g3 = g1.wrap(null as any)
286
+ const g4 = client.helpers.doSomethingMutation.project(S.String)
287
+ const g5 = g4(null as any)
288
+ const g6 = g4.wrap(null as any)
289
+ // @ts-expect-error mutate no longer exposes fn, use client.DoSomething.fn
290
+ const h = client.DoSomething.mutate.fn(null as any)
291
+
292
+ // projection
293
+ // GetSomething2 uses FiniteFromString, that means Codec is String -> Number
294
+ // when we project that to S.String, it should work as the encoded shapes are identical
295
+ // aka, when we project, we skip decoding with the original codec, and instead use the provided one
296
+ // we have to make sure the Encoded shape of the provided projection schema matches the Encoded Shape of the original codec.
297
+ const projected = client.GetSomething2.project(S.String)
298
+ // @ts-expect-error encoded type mismatch: original encodes to string, S.Number encodes to number
299
+ client.GetSomething2.project(S.Number)
300
+ const p0 = projected.request(null as any)
301
+
302
+ // struct example: success schema encodes to { a: string | null }
303
+ // good: projection schema also expects { a: string | null } on the encoded side
304
+ const projectedStruct = client.GetStructNullable.project(S.Struct({ a: S.NullOr(S.String) }))
305
+ // bad: { a: S.String } has encoded type { a: string } — does not accept null
306
+ // @ts-expect-error encoded type mismatch: original encodes to { a: string | null }, projection expects { a: string }
307
+ client.GetStructNullable.project(S.Struct({ a: S.String }))
308
+
309
+ const p00 = projected.query(null as any)
310
+ const p = projected.suspense(null as any)
73
311
 
74
312
  expect(true).toBe(true)
75
313
  console.log({
76
314
  a,
77
315
  a0,
78
316
  a00,
79
- c0,
80
317
  b,
81
- c,
82
- d,
83
318
  e,
319
+ de,
320
+ de2,
321
+ de3,
84
322
  e0,
85
323
  e00,
86
324
  e000,
87
- e0000,
88
325
  e1,
89
326
  e2,
90
327
  f,
91
328
  f0,
329
+ g0,
92
330
  g,
93
- h
331
+ g1,
332
+ g2,
333
+ g3,
334
+ g4,
335
+ g5,
336
+ g6,
337
+ h,
338
+ p0,
339
+ p00,
340
+ p,
341
+ projectedStruct
94
342
  })
95
343
  })
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Runtime and type tests for the `final` schema on stream requests.
3
+ *
4
+ * The `final` option on a stream request schema lets callers model which type
5
+ * the last emitted stream element is.
6
+ */
7
+ import { expect, it } from "@effect/vitest"
8
+ import { Effect, S } from "effect-app"
9
+ import * as Stream from "effect/Stream"
10
+ import { asStreamResult } from "../src/mutate.js"
11
+ import { ExportComplete, OperationProgress, Something } from "./stubs.js"
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // asStreamResult — low-level primitive, always returns void
15
+ // ---------------------------------------------------------------------------
16
+
17
+ it.live("asStreamResult returns void and updates ref with each element", () =>
18
+ Effect.gen(function*() {
19
+ const events: number[] = [1, 2, 3]
20
+ const [ref, execute] = asStreamResult(() => Stream.fromIterable(events))
21
+
22
+ yield* execute()
23
+
24
+ // ref should hold the last emitted value
25
+ expect(ref.value._tag).toBe("Success")
26
+ if (ref.value._tag === "Success") {
27
+ expect(ref.value.value).toBe(3)
28
+ expect(ref.value.waiting).toBe(false)
29
+ }
30
+ }))
31
+
32
+ it("stream request without final: .final is undefined", () => {
33
+ const req = Something.StreamWithoutFinal
34
+ expect((req as any).final).toBeUndefined()
35
+ })
36
+
37
+ it("stream request with final: .final holds the ExportComplete schema", () => {
38
+ const req = Something.StreamWithFinal
39
+ expect((req as any).final).toBeDefined()
40
+ // Verify the schema decodes correctly
41
+ const decoded = S.decodeUnknownSync((req as any).final)({ _tag: "ExportComplete", fileUrl: "https://x.com" })
42
+ expect(decoded).toBeInstanceOf(ExportComplete)
43
+ })
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Runtime: last stream value is accessible via the reactive ref after stream ends
47
+ // ---------------------------------------------------------------------------
48
+
49
+ it.live("last stream value is accessible via reactive ref after stream ends", () =>
50
+ Effect.gen(function*() {
51
+ const progress = new OperationProgress({ completed: 1 as S.NonNegativeInt, total: 2 as S.NonNegativeInt })
52
+ const complete = new ExportComplete({ fileUrl: "https://example.com/file.csv" as S.NonEmptyString })
53
+
54
+ const [ref, execute] = asStreamResult(() => Stream.make(progress, complete))
55
+
56
+ yield* execute()
57
+
58
+ expect(ref.value._tag).toBe("Success")
59
+ if (ref.value._tag === "Success") {
60
+ expect(ref.value.value).toBe(complete)
61
+ expect(ref.value.waiting).toBe(false)
62
+ }
63
+ }))