1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Exit, Fiber } from "effect"
3import { TestClock } from "effect/testing"
4
5describe("Effect Eager Operations", () => {
6 describe("fnUntracedEager", () => {
7 describe("eager computation", () => {
8 it("computes sync effects immediately", () => {
9 let execCount = 0
10 const fn = Effect.fnUntracedEager(function*() {
11 execCount++
12 yield* Effect.succeed(1)
13 yield* Effect.succeed(2)
14 return execCount
15 })
16
17 const effect = fn()
18
19 assert.strictEqual(execCount, 1)
20 assert.strictEqual((effect as any)._tag, "Success")
21 assert.strictEqual((effect as any).value, 1)
22 })
23
24 it("computes failures immediately", () => {
25 const fn = Effect.fnUntracedEager(function*() {
26 yield* Effect.succeed(1)
27 // @effect-diagnostics-next-line missingReturnYieldStar:off
28 yield* Effect.fail("error")
29 return "unreachable"
30 })
31
32 const effect = fn()
33 assert.strictEqual((effect as any)._tag, "Failure")
34 })
35
36 it("stops at first async effect", () => {
37 let syncSteps = 0
38 let asyncReached = false
39
40 const fn = Effect.fnUntracedEager(function*() {
41 syncSteps++
42 yield* Effect.succeed(1)
43 syncSteps++
44 yield* Effect.promise(() => {
45 asyncReached = true
46 return Promise.resolve(2)
47 })
48 syncSteps++
49 return "done"
50 })
51
52 const effect = fn()
53
54 assert.strictEqual(syncSteps, 2)
55 assert.strictEqual(asyncReached, false)
56 assert.notStrictEqual((effect as any)._tag, "Success")
57 })
58
59 it("handles exceptions", () => {
60 const fn = Effect.fnUntracedEager(function*() {
61 yield* Effect.succeed(1)
62 throw new Error("sync error")
63 })
64
65 const effect = fn()
66 assert.strictEqual((effect as any)._tag, "Failure")
67 })
68 })
69
70 it.effect("executes sync generators", () =>
71 Effect.gen(function*() {
72 const fn = Effect.fnUntracedEager(function*() {
73 yield* Effect.succeed(1)
74 yield* Effect.succeed(2)
75 return "done"
76 })
77
78 const result = yield* fn()
79 assert.strictEqual(result, "done")
80 }))
81
82 it.effect("handles mixed sync/async effects", () =>
83 Effect.gen(function*() {
84 const fn = Effect.fnUntracedEager(function*() {
85 const a = yield* Effect.succeed(1)
86 const b = yield* Effect.promise(() => Promise.resolve(2))
87 return a + b
88 })
89
90 const result = yield* fn()
91 assert.strictEqual(result, 3)
92 }))
93
94 it.effect("handles failures", () =>
95 Effect.gen(function*() {
96 const fn = Effect.fnUntracedEager(function*() {
97 // @effect-diagnostics-next-line missingReturnYieldStar:off
98 yield* Effect.fail("error")
99 return "unreachable"
100 })
101
102 const exit = yield* Effect.exit(fn())
103 assert.isTrue(Exit.isFailure(exit))
104 }))
105
106 it.effect("works with arguments and this context", () =>
107 Effect.gen(function*() {
108 const obj = {
109 value: 10,
110 fn: Effect.fnUntracedEager(function*(this: { value: number }, x: number) {
111 const a = yield* Effect.succeed(x)
112 return a + this.value
113 })
114 }
115
116 const result = yield* obj.fn(5)
117 assert.strictEqual(result, 15)
118 }))
119
120 it.effect("works with pipeable operators", () =>
121 Effect.gen(function*() {
122 const fn = Effect.fnUntracedEager(
123 function*() {
124 yield* Effect.succeed(1)
125 return 10
126 },
127 Effect.map((x: number) => x * 2)
128 )
129
130 const result = yield* fn()
131 assert.strictEqual(result, 20)
132 }))
133
134 it.effect("forwards arguments to pipeable operators", () =>
135 Effect.gen(function*() {
136 const fn = Effect.fnUntracedEager(
137 function*(value: string) {
138 return value.length
139 },
140 (effect, value) => Effect.map(effect, (length) => `${value}:${length}`)
141 )
142
143 const result = yield* fn("hello")
144 assert.strictEqual(result, "hello:5")
145 }))
146
147 it("proxies body length", () => {
148 const fn = Effect.fnUntracedEager(function*(a: string, b: number, c: boolean) {
149 return c ? a.length + b : b
150 })
151
152 assert.strictEqual(fn.length, 3)
153 })
154
155 it.effect("re-execution creates fresh iterators", () =>
156 Effect.gen(function*() {
157 let execCount = 0
158 const fn = Effect.fnUntracedEager(function*() {
159 execCount++
160 yield* Effect.succeed(1)
161 yield* Effect.promise(() => Promise.resolve(2))
162 return execCount
163 })
164
165 const effect = fn()
166
167 const result1 = yield* effect
168 assert.strictEqual(result1, 1)
169
170 const result2 = yield* effect
171 assert.strictEqual(result2, 2)
172 assert.strictEqual(execCount, 2)
173 }))
174
175 it.effect("handles long sync chains efficiently", () =>
176 Effect.gen(function*() {
177 const fn = Effect.fnUntracedEager(function*() {
178 let sum = 0
179 for (let i = 0; i < 100; i++) {
180 const val = yield* Effect.succeed(i)
181 sum += val
182 }
183 return sum
184 })
185
186 const result = yield* fn()
187 assert.strictEqual(result, 4950) // sum of 0 to 99
188 }))
189 })
190
191 describe("mapEager", () => {
192 it.effect("successful effect", () =>
193 Effect.gen(function*() {
194 const result = yield* Effect.mapEager(Effect.succeed(5), (n) => n * 2)
195 assert.strictEqual(result, 10)
196 }))
197
198 it.effect("failed effect preserves failure", () =>
199 Effect.gen(function*() {
200 const effect = Effect.fail("error")
201 const mapped = Effect.mapEager(effect, (n) => n * 2)
202 const exit = yield* Effect.exit(mapped)
203
204 assert.strictEqual(exit._tag, "Failure", "Expected effect to fail")
205 assert.ok(exit._tag === "Failure", "Type guard for exit failure")
206
207 const failure = exit.cause.reasons.find((failure: any) => failure._tag === "Fail")
208 assert.ok(failure, "Expected to find a Fail cause")
209 assert.strictEqual(failure._tag, "Fail", "Expected failure to be a Fail type")
210 assert.strictEqual((failure as any).error, "error", "Expected error to be preserved")
211 }))
212
213 it.effect("complex effect falls back to regular map", () =>
214 Effect.gen(function*() {
215 const effect = Effect.mapEager(Effect.delay(Effect.succeed(10), 1), (n) => n + 5)
216 const fiber = yield* Effect.forkChild(effect)
217 yield* TestClock.adjust(1)
218 const result = yield* Fiber.join(fiber)
219 assert.strictEqual(result, 15)
220 }))
221 })
222
223 describe("flatMapEager", () => {
224 it.effect("applies transformation eagerly for success effects", () =>
225 Effect.gen(function*() {
226 let executions = 0
227 const effect = Effect.succeed(5)
228 const flatMapped = Effect.flatMapEager(effect, (n) => {
229 executions++
230 return Effect.succeed(n * 2)
231 })
232 assert.strictEqual(yield* flatMapped, 10)
233 assert.strictEqual(yield* flatMapped, 10)
234 assert.strictEqual(yield* flatMapped, 10)
235 assert.strictEqual(executions, 1)
236 }))
237
238 it.effect("preserves failure for failed effects", () =>
239 Effect.gen(function*() {
240 const error = new Error("test error")
241 const effect = Effect.fail(error)
242 const flatMapped = Effect.flatMapEager(effect, (n) => Effect.succeed(n * 2))
243 const exit = yield* Effect.exit(flatMapped)
244
245 assert.strictEqual(exit._tag, "Failure", "Expected effect to fail")
246 assert.ok(exit._tag === "Failure", "Type guard for exit failure")
247
248 const failure = exit.cause.reasons.find((failure: any) => failure._tag === "Fail")
249 assert.ok(failure, "Expected to find a Fail cause")
250 assert.strictEqual(failure._tag, "Fail", "Expected failure to be a Fail type")
251 assert.strictEqual((failure as any).error, error, "Expected error to be preserved")
252 }))
253
254 it.effect("fallback to regular flatMap for complex effects", () =>
255 Effect.gen(function*() {
256 const effect = Effect.delay(Effect.succeed(10), "1 millis")
257 const flatMapped = Effect.flatMapEager(effect, (n) => Effect.succeed(n * 2))
258
259 const fiber = yield* Effect.forkChild(flatMapped)
260 yield* TestClock.adjust("1 millis")
261 const result = yield* Fiber.join(fiber)
262
263 assert.strictEqual(result, 20)
264 }))
265 })
266
267 describe("mapErrorEager", () => {
268 it.effect("preserves success for successful effects", () =>
269 Effect.gen(function*() {
270 const effect = Effect.succeed(5)
271 const mapped = Effect.mapErrorEager(effect, (err) => `mapped: ${err}`)
272 const result = yield* mapped
273 assert.strictEqual(result, 5)
274 }))
275
276 it.effect("applies transformation eagerly for failure effects", () =>
277 Effect.gen(function*() {
278 const effect = Effect.fail("original error")
279 const mapped = Effect.mapErrorEager(effect, (err) => `mapped: ${err}`)
280 const exit = yield* Effect.exit(mapped)
281
282 assert.strictEqual(exit._tag, "Failure", "Expected effect to fail")
283 assert.ok(exit._tag === "Failure", "Type guard for exit failure")
284
285 const failure = exit.cause.reasons.find((failure: any) => failure._tag === "Fail")
286 assert.ok(failure, "Expected to find a Fail cause")
287 assert.strictEqual(failure._tag, "Fail", "Expected failure to be a Fail type")
288 assert.strictEqual((failure as any).error, "mapped: original error", "Expected error to be transformed")
289 }))
290
291 it.effect("fallback to regular mapError for complex effects", () =>
292 Effect.gen(function*() {
293 const effect = Effect.delay(Effect.fail("error"), "1 millis")
294 const mapped = Effect.mapErrorEager(effect, (err) => `mapped: ${err}`)
295
296 const fiber = yield* Effect.forkChild(mapped)
297 yield* TestClock.adjust("1 millis")
298 const exit = yield* Fiber.await(fiber)
299
300 assert.strictEqual(exit._tag, "Failure", "Expected effect to fail")
301 assert.ok(exit._tag === "Failure", "Type guard for exit failure")
302
303 const failure = exit.cause.reasons.find((failure: any) => failure._tag === "Fail")
304 assert.ok(failure, "Expected to find a Fail cause")
305 assert.strictEqual(failure._tag, "Fail", "Expected failure to be a Fail type")
306 assert.strictEqual((failure as any).error, "mapped: error", "Expected error to be transformed")
307 }))
308 })
309
310 describe("mapBothEager", () => {
311 it.effect("applies onSuccess eagerly for success effects", () =>
312 Effect.gen(function*() {
313 const effect = Effect.succeed(5)
314 const mapped = Effect.mapBothEager(effect, {
315 onFailure: (err) => `Failed: ${err}`,
316 onSuccess: (n) => n * 2
317 })
318 const result = yield* mapped
319 assert.strictEqual(result, 10)
320 }))
321
322 it.effect("applies onFailure eagerly for failure effects", () =>
323 Effect.gen(function*() {
324 const effect = Effect.fail("original error")
325 const mapped = Effect.mapBothEager(effect, {
326 onFailure: (err) => `Failed: ${err}`,
327 onSuccess: (n) => n * 2
328 })
329 const exit = yield* Effect.exit(mapped)
330
331 assert.strictEqual(exit._tag, "Failure", "Expected effect to fail")
332 assert.ok(exit._tag === "Failure", "Type guard for exit failure")
333
334 const failure = exit.cause.reasons.find((failure: any) => failure._tag === "Fail")
335 assert.ok(failure, "Expected to find a Fail cause")
336 assert.strictEqual(failure._tag, "Fail", "Expected failure to be a Fail type")
337 assert.strictEqual((failure as any).error, "Failed: original error", "Expected error to be transformed")
338 }))
339
340 it.effect("fallback to regular mapBoth for complex effects", () =>
341 Effect.gen(function*() {
342 const effect = Effect.delay(Effect.succeed(10), "1 millis")
343 const mapped = Effect.mapBothEager(effect, {
344 onFailure: (err) => `Failed: ${err}`,
345 onSuccess: (n) => n * 2
346 })
347
348 const fiber = yield* Effect.forkChild(mapped)
349 yield* TestClock.adjust("1 millis")
350 const result = yield* Fiber.join(fiber)
351
352 assert.strictEqual(result, 20)
353 }))
354 })
355
356 describe("catchEager", () => {
357 it.effect("preserves success effects without applying catch", () =>
358 Effect.gen(function*() {
359 const effect = Effect.succeed(42)
360 const caught = Effect.catchEager(effect, (err) => Effect.succeed(`recovered: ${err}`))
361 const result = yield* caught
362 assert.strictEqual(result, 42)
363 }))
364
365 it.effect("applies catch function eagerly for failure effects", () =>
366 Effect.gen(function*() {
367 const effect = Effect.fail("original error")
368 const caught = Effect.catchEager(effect, (err) => Effect.succeed(`recovered: ${err}`))
369 const result = yield* caught
370 assert.strictEqual(result, "recovered: original error")
371 }))
372
373 it.effect("fallback to regular catch for complex effects", () =>
374 Effect.gen(function*() {
375 const effect = Effect.delay(Effect.fail("error"), "1 millis")
376 const caught = Effect.catchEager(effect, (err) => Effect.succeed(`recovered: ${err}`))
377
378 const fiber = yield* Effect.forkChild(caught)
379 yield* TestClock.adjust("1 millis")
380 const result = yield* Fiber.join(fiber)
381
382 assert.strictEqual(result, "recovered: error")
383 }))
384 })
385
386 describe("matchEager", () => {
387 it.effect("applies onSuccess eagerly for success effects", () =>
388 Effect.gen(function*() {
389 const effect = Effect.succeed(42)
390 const matched = Effect.matchEager(effect, {
391 onFailure: (err) => `Failed: ${err}`,
392 onSuccess: (n) => `Success: ${n}`
393 })
394 const result = yield* matched
395 assert.strictEqual(result, "Success: 42")
396 }))
397
398 it.effect("applies onFailure eagerly for failure effects", () =>
399 Effect.gen(function*() {
400 const effect = Effect.fail("original error")
401 const matched = Effect.matchEager(effect, {
402 onFailure: (err) => `Failed: ${err}`,
403 onSuccess: (n) => `Success: ${n}`
404 })
405 const result = yield* matched
406 assert.strictEqual(result, "Failed: original error")
407 }))
408
409 it.effect("fallback to regular match for complex effects", () =>
410 Effect.gen(function*() {
411 const effect = Effect.delay(Effect.succeed(10), "1 millis")
412 const matched = Effect.matchEager(effect, {
413 onFailure: (err) => `Failed: ${err}`,
414 onSuccess: (n) => `Success: ${n}`
415 })
416
417 const fiber = yield* Effect.forkChild(matched)
418 yield* TestClock.adjust("1 millis")
419 const result = yield* Fiber.join(fiber)
420
421 assert.strictEqual(result, "Success: 10")
422 }))
423
424 it.effect("eagerly handles complex failure scenarios", () =>
425 Effect.gen(function*() {
426 const effect = Effect.fail(new Error("test error"))
427 const matched = Effect.matchEager(effect, {
428 onFailure: (err) => `Recovered from: ${err.message}`,
429 onSuccess: (n) => `Success: ${n}`
430 })
431 const result = yield* matched
432 assert.strictEqual(result, "Recovered from: test error")
433 }))
434
435 it.effect("supports curried usage", () =>
436 Effect.gen(function*() {
437 const matcher = Effect.matchEager({
438 onFailure: (err: string) => `Failed: ${err}`,
439 onSuccess: (n: number) => `Success: ${n}`
440 })
441
442 const successResult = yield* matcher(Effect.succeed(100))
443 assert.strictEqual(successResult, "Success: 100")
444
445 const failureResult = yield* matcher(Effect.fail("error"))
446 assert.strictEqual(failureResult, "Failed: error")
447 }))
448
449 it.effect("never fails - always produces a result", () =>
450 Effect.gen(function*() {
451 const effect = Effect.fail("test error")
452 const matched = Effect.matchEager(effect, {
453 onFailure: (err) => `Handled: ${err}`,
454 onSuccess: (n) => `Value: ${n}`
455 })
456
457 // Should not throw, should produce a result
458 const result = yield* matched
459 assert.strictEqual(result, "Handled: test error")
460
461 // Test with success case
462 const successEffect = Effect.succeed(42)
463 const successMatched = Effect.matchEager(successEffect, {
464 onFailure: (err) => `Handled: ${err}`,
465 onSuccess: (n) => `Value: ${n}`
466 })
467
468 const successResult = yield* successMatched
469 assert.strictEqual(successResult, "Value: 42")
470 }))
471
472 it.effect("performance: applies transformations immediately for resolved effects", () =>
473 Effect.gen(function*() {
474 let onSuccessCount = 0
475 let onFailureCount = 0
476
477 const successEffect = Effect.succeed(10)
478 const failureEffect = Effect.fail("error")
479
480 const matcher = {
481 onSuccess: (n: number) => {
482 onSuccessCount++
483 return n * 2
484 },
485 onFailure: (err: string) => {
486 onFailureCount++
487 return err.toUpperCase()
488 }
489 }
490
491 // Multiple invocations of the same resolved effect should be efficient
492 const successMatched = Effect.matchEager(successEffect, matcher)
493 const failureMatched = Effect.matchEager(failureEffect, matcher)
494
495 const result1 = yield* successMatched
496 const result2 = yield* successMatched
497 const result3 = yield* failureMatched
498 const result4 = yield* failureMatched
499
500 assert.strictEqual(result1, 20)
501 assert.strictEqual(result2, 20)
502 assert.strictEqual(result3, "ERROR")
503 assert.strictEqual(result4, "ERROR")
504
505 // Handlers should be called only once per effect execution due to eager evaluation
506 assert.strictEqual(onSuccessCount, 1)
507 assert.strictEqual(onFailureCount, 1)
508 }))
509 })
510
511 describe("matchCauseEager", () => {
512 it("eagerly matches success effects", () => {
513 let onSuccessCount = 0
514 let onFailureCount = 0
515
516 const successEffect = Effect.succeed(42)
517 const matcher = {
518 onSuccess: (value: number) => {
519 onSuccessCount++
520 return value * 2
521 },
522 onFailure: (_cause: any) => {
523 onFailureCount++
524 return "ERROR"
525 }
526 }
527
528 const result = Effect.matchCauseEager(successEffect, matcher)
529
530 // Should be resolved immediately
531 assert.strictEqual((result as any)._tag, "Success")
532 assert.strictEqual((result as any).value, 84)
533 assert.strictEqual(onSuccessCount, 1)
534 assert.strictEqual(onFailureCount, 0)
535 })
536
537 it("eagerly matches failure effects", () => {
538 let onSuccessCount = 0
539 let onFailureCount = 0
540
541 const failureEffect = Effect.fail("test error")
542 const matcher = {
543 onSuccess: (value: any) => {
544 onSuccessCount++
545 return value * 2
546 },
547 onFailure: (_cause: any) => {
548 onFailureCount++
549 return "HANDLED"
550 }
551 }
552
553 const result = Effect.matchCauseEager(failureEffect, matcher)
554
555 // Should be resolved immediately
556 assert.strictEqual((result as any)._tag, "Success")
557 assert.strictEqual((result as any).value, "HANDLED")
558 assert.strictEqual(onSuccessCount, 0)
559 assert.strictEqual(onFailureCount, 1)
560 })
561
562 it.effect("falls back to regular matchCause for complex effects", () =>
563 Effect.gen(function*() {
564 let onSuccessCount = 0
565 let onFailureCount = 0
566
567 const delayedEffect = Effect.delay(Effect.succeed(10), "10 millis")
568 const matcher = {
569 onSuccess: (value: number) => {
570 onSuccessCount++
571 return value * 2
572 },
573 onFailure: (_cause: any) => {
574 onFailureCount++
575 return "ERROR"
576 }
577 }
578
579 const result = Effect.matchCauseEager(delayedEffect, matcher)
580
581 // Should not be resolved immediately - should be a complex effect
582 assert.notStrictEqual((result as any)._tag, "Success")
583 assert.notStrictEqual((result as any)._tag, "Failure")
584
585 // Verify handlers haven't been called yet
586 assert.strictEqual(onSuccessCount, 0)
587 assert.strictEqual(onFailureCount, 0)
588
589 // Fork the effect and advance time
590 const fiber = yield* Effect.forkChild(result)
591 yield* TestClock.adjust("10 millis")
592 const finalResult = yield* Fiber.join(fiber)
593
594 // Now handlers should have been called
595 assert.strictEqual(finalResult, 20)
596 assert.strictEqual(onSuccessCount, 1)
597 assert.strictEqual(onFailureCount, 0)
598 }))
599
600 it.effect("falls back to regular matchCause for delayed failure effects", () =>
601 Effect.gen(function*() {
602 let onSuccessCount = 0
603 let onFailureCount = 0
604
605 const delayedFailure = Effect.delay(Effect.fail("delayed error"), "5 millis")
606 const matcher = {
607 onSuccess: (value: any) => {
608 onSuccessCount++
609 return value * 2
610 },
611 onFailure: (_cause: any) => {
612 onFailureCount++
613 return "HANDLED_DELAYED"
614 }
615 }
616
617 const result = Effect.matchCauseEager(delayedFailure, matcher)
618
619 // Should not be resolved immediately
620 assert.notStrictEqual((result as any)._tag, "Success")
621 assert.notStrictEqual((result as any)._tag, "Failure")
622
623 // Verify handlers haven't been called yet
624 assert.strictEqual(onSuccessCount, 0)
625 assert.strictEqual(onFailureCount, 0)
626
627 // Fork the effect and advance time
628 const fiber = yield* Effect.forkChild(result)
629 yield* TestClock.adjust("5 millis")
630 const finalResult = yield* Fiber.join(fiber)
631
632 // Now handlers should have been called
633 assert.strictEqual(finalResult, "HANDLED_DELAYED")
634 assert.strictEqual(onSuccessCount, 0)
635 assert.strictEqual(onFailureCount, 1)
636 }))
637
638 it("handles cause information correctly", () => {
639 let capturedCause: any = null
640
641 const failureEffect = Effect.fail("test error")
642 const matcher = {
643 onSuccess: (value: any) => value,
644 onFailure: (cause: any) => {
645 capturedCause = cause
646 return "handled"
647 }
648 }
649
650 const result = Effect.matchCauseEager(failureEffect, matcher)
651
652 // Should be resolved immediately
653 assert.strictEqual((result as any)._tag, "Success")
654 assert.strictEqual((result as any).value, "handled")
655 assert.isNotNull(capturedCause)
656 })
657
658 it.effect("preserves effect execution behavior for non-eager paths", () =>
659 Effect.gen(function*() {
660 let executionCount = 0
661
662 const complexEffect = Effect.flatMap(
663 Effect.delay(Effect.succeed(42), "1 millis"),
664 (value) => {
665 executionCount++
666 return Effect.succeed(value * 2)
667 }
668 )
669
670 const matcher = {
671 onSuccess: (value: number) => value + 100,
672 onFailure: (_cause: any) => "ERROR"
673 }
674
675 const result = Effect.matchCauseEager(complexEffect, matcher)
676
677 // Should not be resolved immediately
678 assert.notStrictEqual((result as any)._tag, "Success")
679 assert.strictEqual(executionCount, 0)
680
681 // Fork the effect and advance time
682 const fiber = yield* Effect.forkChild(result)
683 yield* TestClock.adjust("1 millis")
684 const finalResult = yield* Fiber.join(fiber)
685
686 // Should have executed the effect chain properly
687 assert.strictEqual(finalResult, 184) // (42 * 2) + 100
688 assert.strictEqual(executionCount, 1)
689 }))
690 })
691})