1import { assert, describe, it, vi } from "@effect/vitest"
2import { assertExitFailure } from "@effect/vitest/utils"
3import {
4 Cause,
5 Context,
6 Data,
7 Deferred,
8 Duration,
9 Effect,
10 Effectable,
11 Exit,
12 Fiber,
13 type Filter,
14 Latch,
15 Layer,
16 Logger,
17 type LogLevel,
18 Option,
19 Pool,
20 Ref,
21 References,
22 Result,
23 Schedule,
24 Scheduler,
25 Scope,
26 TxRef
27} from "effect"
28import { constFalse, constTrue, pipe } from "effect/Function"
29import { TestClock } from "effect/testing"
30import { AsyncLocalStorage } from "node:async_hooks"
31import { assertCauseFail } from "./utils/assert.ts"
32
33class ATag extends Context.Service<ATag, "A">()("ATag") {}
34
35const assertExitDefect = <A, E>(exit: Exit.Exit<A, E>, defect: unknown) => {
36 assert.isTrue(Exit.hasDies(exit))
37 const result = Exit.findDefect(exit)
38 assert.isTrue(Result.isSuccess(result))
39 if (Result.isSuccess(result)) {
40 assert.strictEqual(result.success, defect)
41 }
42}
43
44const assertUnknownError = <A>(exit: Exit.Exit<A, Cause.UnknownError>, cause: unknown, message: string) => {
45 assert.isTrue(Exit.isFailure(exit))
46 if (Exit.isFailure(exit)) {
47 const result = Cause.findError(exit.cause)
48 assert.isTrue(Result.isSuccess(result))
49 if (Result.isSuccess(result)) {
50 assert.isTrue(Cause.isUnknownError(result.success))
51 assert.strictEqual((result.success as Error).cause, cause)
52 assert.strictEqual(result.success.message, message)
53 }
54 }
55}
56
57describe("Effect", () => {
58 describe("interruption before cleanup registration", () => {
59 const interruptAfterContextChange = (changed: (context: Context.Context<never>) => boolean) => {
60 let yielded = false
61 const tasks: Array<() => void> = []
62 const scheduler: Scheduler.Scheduler = {
63 executionMode: "async",
64 makeDispatcher: () => ({
65 scheduleTask: (task) => {
66 tasks.push(task)
67 },
68 flush() {}
69 }),
70 shouldYield(fiber) {
71 if (!yielded && changed(fiber.context)) {
72 yielded = true
73 return true
74 }
75 return false
76 }
77 }
78 return { scheduler, tasks, wasYielded: () => yielded }
79 }
80
81 it.effect("scoped closes its scope when interrupted after installing it", () =>
82 Effect.gen(function*() {
83 let finalized = 0
84 let original: Context.Context<never> | undefined
85 let observed: Context.Context<never> | undefined
86 let innerScope: Scope.Scope | undefined
87 const { scheduler, tasks, wasYielded } = interruptAfterContextChange((context) => {
88 const scope = Context.getOrUndefined(context, Scope.Scope)
89 if (original === undefined || scope === Context.getOrUndefined(original, Scope.Scope)) return false
90 innerScope = scope
91 return true
92 })
93 const child = yield* Effect.gen(function*() {
94 original = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context))
95 yield* Effect.scoped(Effect.addFinalizer(() =>
96 Effect.sync(() => {
97 finalized++
98 })
99 ))
100 }).pipe(
101 Effect.onExit(() =>
102 Effect.withFiber((fiber) =>
103 Effect.sync(() => {
104 observed = fiber.context
105 })
106 )
107 ),
108 Effect.provideService(Scheduler.Scheduler, scheduler),
109 Effect.forkChild({ startImmediately: true })
110 )
111 assert.isTrue(wasYielded())
112 child.interruptUnsafe()
113 while (tasks.length > 0) tasks.shift()!()
114 yield* Fiber.await(child)
115 assert.isDefined(innerScope)
116 yield* Scope.addFinalizer(
117 innerScope!,
118 Effect.sync(() => {
119 finalized++
120 })
121 )
122 assert.strictEqual(finalized, 1)
123 assert.strictEqual(observed, original)
124 }))
125
126 it.effect("provideService restores the original context if interrupted before its body", () =>
127 Effect.gen(function*() {
128 class Value extends Context.Service<Value, number>()("InterruptedValue") {}
129 let original: Context.Context<never> | undefined
130 let observed: Context.Context<never> | undefined
131 const { scheduler, tasks, wasYielded } = interruptAfterContextChange((context) =>
132 original !== undefined && Context.getOrUndefined(context, Value) === 2
133 )
134 const child = yield* Effect.gen(function*() {
135 original = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context))
136 yield* Effect.provideService(Effect.void, Value, 2)
137 }).pipe(
138 Effect.onExit(() =>
139 Effect.withFiber((fiber) =>
140 Effect.sync(() => {
141 observed = fiber.context
142 })
143 )
144 ),
145 Effect.provideService(Value, 1),
146 Effect.provideService(Scheduler.Scheduler, scheduler),
147 Effect.forkChild({ startImmediately: true })
148 )
149 assert.isTrue(wasYielded())
150 child.interruptUnsafe()
151 while (tasks.length > 0) tasks.shift()!()
152 yield* Fiber.await(child)
153 assert.strictEqual(observed, original)
154 }))
155
156 for (const cacheKind of ["cached", "cachedInvalidateWithTTL"] as const) {
157 it.effect(
158 cacheKind + " never strands a second caller when the owner yields before registering cleanup",
159 () =>
160 Effect.gen(function*() {
161 let interruptedBeforeSource = 0
162 // Sweep each scheduler boundary after entering the cache. A fixed
163 // step count can stop exercising the gap when the runtime adds an op.
164 for (let target = 1; target <= 32; target++) {
165 let armed = false
166 let seen = 0
167 let computed = 0
168 const tasks: Array<() => void> = []
169 const scheduler: Scheduler.Scheduler = {
170 executionMode: "async",
171 makeDispatcher: () => ({
172 scheduleTask: (task) => {
173 tasks.push(task)
174 },
175 flush() {}
176 }),
177 shouldYield() {
178 return armed && ++seen === target
179 }
180 }
181 const source = Effect.sync(() => ++computed)
182 const cached = cacheKind === "cached"
183 ? yield* Effect.cached(source)
184 : (yield* Effect.cachedInvalidateWithTTL(source, "1 minute"))[0]
185 const owner = yield* Effect.gen(function*() {
186 yield* Effect.sync(() => {
187 armed = true
188 })
189 yield* cached
190 }).pipe(
191 Effect.provideService(Scheduler.Scheduler, scheduler),
192 Effect.forkChild({ startImmediately: true })
193 )
194 if (tasks.length === 0) continue // No boundary left before the source completed.
195 owner.interruptUnsafe()
196 while (tasks.length > 0) tasks.shift()!()
197 yield* Fiber.await(owner)
198 if (computed !== 0) continue
199 interruptedBeforeSource++
200 const waiter = yield* Effect.forkChild(cached, { startImmediately: true })
201 yield* Effect.yieldNow
202 const settled = waiter.pollUnsafe()
203 if (settled === undefined) waiter.interruptUnsafe()
204 assert.isDefined(settled, "cache waiter stuck after owner interruption at boundary " + target)
205 }
206 assert.isAbove(interruptedBeforeSource, 0, "the sweep never interrupted before the source")
207 })
208 )
209 }
210 })
211 it("isEffect", () => {
212 assert.isTrue(Effect.isEffect(Effect.succeed(0)))
213 assert.isFalse(Effect.isEffect([0]))
214 })
215
216 describe("Effectable.Class", () => {
217 it.effect("runs using asEffect", () =>
218 Effect.gen(function*() {
219 class Answer extends Effectable.Class<number> {
220 asEffect() {
221 return Effect.succeed(42)
222 }
223 }
224
225 assert.strictEqual(yield* new Answer(), 42)
226 }))
227
228 it.effect("evaluates asEffect on every execution", () =>
229 Effect.gen(function*() {
230 class Answer extends Effectable.Class<number> {
231 value = 1
232
233 asEffect() {
234 return Effect.succeed(this.value)
235 }
236 }
237
238 const answer = new Answer()
239 assert.strictEqual(yield* answer, 1)
240 answer.value = 2
241 assert.strictEqual(yield* answer, 2)
242 }))
243 })
244
245 describe("structural compare", () => {
246 it("should pass structural comparison", () => {
247 assert.deepEqual(Effect.succeed(0), Effect.succeed(0))
248 })
249 it("should fail structural comparison", () => {
250 assert.notDeepEqual(Effect.succeed(0), Effect.succeed(1))
251 })
252 })
253
254 describe("tracing", () => {
255 it("terminal root failure captures stack frame", () => {
256 const frame: References.StackFrame = {
257 name: "root frame",
258 stack: () => undefined,
259 parent: undefined
260 }
261 const failure = Exit.fail("boom")
262 const exit = Effect.runForkWith(Context.make(References.CurrentStackFrame, frame))(failure).pollUnsafe()
263
264 assert.isDefined(exit)
265 assert.isTrue(Exit.isFailure(exit!))
266 if (Exit.isFailure(exit!)) {
267 const trace = Context.getOrUndefined(Cause.annotations(exit.cause), Cause.StackTrace)
268 assert.strictEqual(trace, frame)
269 }
270 })
271
272 it.effect("caught sandboxed failure captures stack frame", () =>
273 Effect.gen(function*() {
274 const cause = yield* Effect.failCause(Cause.die(new Error("boom"))).pipe(
275 Effect.withSpan("test span"),
276 Effect.sandbox,
277 Effect.flip
278 )
279 const annotations = Cause.annotations(cause)
280 const trace = Context.getUnsafe(annotations, Cause.StackTrace)
281 assert.strictEqual(trace.name, "test span")
282 }))
283
284 it("failure without a current frame has no stack annotation and reuses the original exit", () => {
285 const failure = Exit.fail("boom")
286 const exit = Effect.runFork(failure).pollUnsafe()
287
288 assert.strictEqual(exit, failure)
289 assert.isTrue(Exit.isFailure(exit!))
290 if (Exit.isFailure(exit!)) {
291 assert.isUndefined(Context.getOrUndefined(Cause.annotations(exit.cause), Cause.StackTrace))
292 }
293 })
294 })
295
296 it("callback can branch over sync/async", async () => {
297 const program = Effect.callback<number>(function(resume) {
298 if (this.executionMode === "sync") {
299 resume(Effect.succeed(1))
300 } else {
301 Promise.resolve().then(() => resume(Effect.succeed(2)))
302 }
303 })
304
305 const isSync = Effect.runSync(program)
306 const isAsync = await Effect.runPromise(program)
307
308 assert.strictEqual(isSync, 1)
309 assert.strictEqual(isAsync, 2)
310 })
311
312 it("runPromise", async () => {
313 const result = await Effect.runPromise(Effect.succeed(1))
314 assert.strictEqual(result, 1)
315 })
316
317 it("acquireUseRelease interrupt", async () => {
318 let acquire = false
319 let use = false
320 let release = false
321 const fiber = Effect.acquireUseRelease(
322 Effect.sync(() => {
323 acquire = true
324 return 123
325 }).pipe(Effect.delay(100)),
326 () =>
327 Effect.sync(() => {
328 use = true
329 }),
330 (_) =>
331 Effect.sync(() => {
332 assert.strictEqual(_, 123)
333 release = true
334 })
335 ).pipe(Effect.runFork)
336 fiber.interruptUnsafe()
337 const result = await Effect.runPromise(Fiber.await(fiber))
338 assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt()))
339 assert.isTrue(acquire)
340 assert.isFalse(use)
341 assert.isTrue(release)
342 })
343
344 it.effect("acquireUseRelease runs release when use throws", () =>
345 Effect.gen(function*() {
346 const resource = { id: 1 }
347 const defect = new Error("boom")
348 let releases = 0
349 let releasedResource: typeof resource | undefined
350 let releaseExit: Exit.Exit<never> | undefined
351
352 const exit = yield* Effect.acquireUseRelease(
353 Effect.succeed(resource),
354 (): Effect.Effect<never> => {
355 throw defect
356 },
357 (resource, exit) =>
358 Effect.sync(() => {
359 releases++
360 releasedResource = resource
361 releaseExit = exit
362 })
363 ).pipe(Effect.exit)
364
365 assertExitDefect(exit, defect)
366 assert.strictEqual(releases, 1)
367 assert.strictEqual(releasedResource, resource)
368 assert.isDefined(releaseExit)
369 assertExitDefect(releaseExit, defect)
370 }))
371
372 it("acquireUseRelease uninterruptible", async () => {
373 let acquire = false
374 let use = false
375 let release = false
376 const fiber = Effect.acquireUseRelease(
377 Effect.sync(() => {
378 acquire = true
379 return 123
380 }).pipe(Effect.delay(100)),
381 (_) =>
382 Effect.sync(() => {
383 use = true
384 return _
385 }),
386 (_) =>
387 Effect.sync(() => {
388 assert.strictEqual(_, 123)
389 release = true
390 })
391 ).pipe(Effect.uninterruptible, Effect.runFork)
392 fiber.interruptUnsafe()
393 const result = await Effect.runPromise(Fiber.await(fiber))
394 assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt()))
395 assert.isTrue(acquire)
396 assert.isTrue(use)
397 assert.isTrue(release)
398 })
399
400 it.effect("Context.Service", () =>
401 ATag.pipe(
402 Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
403 Effect.provideService(ATag, "A")
404 ))
405
406 describe("fromOption", () => {
407 it.effect("from a some", () =>
408 Option.some("A").pipe(
409 Effect.fromOption,
410 Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A")))
411 ))
412
413 it.effect("from a none", () =>
414 Option.none().pipe(
415 Effect.fromOption,
416 Effect.flip,
417 Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError)))
418 ))
419
420 it.effect("from a none with a custom error", () =>
421 Effect.gen(function*() {
422 const error = new Error("Missing value")
423 const cause = yield* Effect.fromOption(Option.none(), () => error).pipe(Effect.flip)
424 assert.strictEqual(cause, error)
425 }))
426
427 it.effect("from a none with a custom error data-last", () =>
428 Effect.gen(function*() {
429 const error = new Error("Missing value")
430 const cause = yield* Option.none().pipe(
431 Effect.fromOption(() => error),
432 Effect.flip
433 )
434 assert.strictEqual(cause, error)
435 }))
436 })
437
438 describe("transposeOption", () => {
439 it.effect("transposes a none", () =>
440 Effect.gen(function*() {
441 const result = yield* Effect.transposeOption(Option.none())
442 assert.deepStrictEqual(result, Option.none())
443 }))
444
445 it.effect("transposes a some containing a success", () =>
446 Effect.gen(function*() {
447 const result = yield* Effect.transposeOption(Option.some(Effect.succeed("A")))
448 assert.deepStrictEqual(result, Option.some("A"))
449 }))
450
451 it.effect("transposes a some containing a failure", () =>
452 Effect.gen(function*() {
453 const error = yield* Effect.transposeOption(Option.some(Effect.fail("error"))).pipe(Effect.flip)
454 assert.strictEqual(error, "error")
455 }))
456 })
457
458 describe("fromResult", () => {
459 it.effect("from a success", () =>
460 Result.succeed("A").pipe(
461 Effect.fromResult,
462 Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A")))
463 ))
464
465 it.effect("from a failure", () =>
466 Result.fail("error").pipe(
467 Effect.fromResult,
468 Effect.flip,
469 Effect.tap((error) => Effect.sync(() => assert.strictEqual(error, "error")))
470 ))
471 })
472
473 describe("try", () => {
474 it.effect("succeeds with the returned value in direct-thunk form", () =>
475 Effect.gen(function*() {
476 const result = yield* Effect.try(() => 1)
477 assert.strictEqual(result, 1)
478 }))
479
480 it.effect("maps thrown values to UnknownError in direct-thunk form", () =>
481 Effect.gen(function*() {
482 const thrown = new Error("try")
483 const exit = yield* Effect.try<number>(() => {
484 throw thrown
485 }).pipe(Effect.exit)
486 assertUnknownError(exit, thrown, "An error occurred in Effect.try")
487 }))
488
489 it.effect("succeeds with the returned value", () =>
490 Effect.gen(function*() {
491 let catchCalled = false
492 const result = yield* Effect.try({
493 try: () => 1,
494 catch: () => {
495 catchCalled = true
496 return "error" as const
497 }
498 })
499 assert.strictEqual(result, 1)
500 assert.isFalse(catchCalled)
501 }))
502
503 it.effect("maps thrown values into typed failures", () =>
504 Effect.gen(function*() {
505 const thrown = new Error("try")
506 const mapped = { cause: thrown }
507 const exit = yield* Effect.try({
508 try: () => {
509 throw thrown
510 },
511 catch: () => mapped
512 }).pipe(Effect.exit)
513 assertExitFailure(exit, Cause.fail(mapped))
514 }))
515
516 it.effect("turns a throwing catch mapper into a defect", () =>
517 Effect.gen(function*() {
518 const thrown = new Error("try")
519 const defect = new Error("catch")
520 const exit = yield* Effect.try({
521 try: () => {
522 throw thrown
523 },
524 catch: () => {
525 throw defect
526 }
527 }).pipe(Effect.exit)
528 assertExitDefect(exit, defect)
529 }))
530 })
531
532 describe("tryPromise", () => {
533 it.effect("succeeds with the resolved value in direct-thunk form", () =>
534 Effect.gen(function*() {
535 const result = yield* Effect.tryPromise(() => Promise.resolve(1))
536 assert.strictEqual(result, 1)
537 }))
538
539 it.effect("does not allocate AbortController for zero-argument thunks", () =>
540 Effect.gen(function*() {
541 const originalAbortController = globalThis.AbortController
542 let allocations = 0
543 class TestAbortController extends originalAbortController {
544 constructor() {
545 allocations += 1
546 super()
547 }
548 }
549
550 yield* Effect.acquireUseRelease(
551 Effect.sync(() => {
552 globalThis.AbortController = TestAbortController
553 }),
554 () =>
555 Effect.gen(function*() {
556 const direct = yield* Effect.tryPromise(() => Promise.resolve(1))
557 const options = yield* Effect.tryPromise({
558 try: () => Promise.resolve(2),
559 catch: () => "error" as const
560 })
561 assert.strictEqual(direct, 1)
562 assert.strictEqual(options, 2)
563 assert.strictEqual(allocations, 0)
564 }),
565 () =>
566 Effect.sync(() => {
567 globalThis.AbortController = originalAbortController
568 })
569 )
570 }))
571
572 it.effect("maps synchronous throws to UnknownError in direct-thunk form", () =>
573 Effect.gen(function*() {
574 const thrown = new Error("try")
575 const exit = yield* Effect.tryPromise<number>(() => {
576 throw thrown
577 }).pipe(Effect.exit)
578 assertUnknownError(exit, thrown, "An error occurred in Effect.tryPromise")
579 }))
580
581 it.effect("maps promise rejections to UnknownError in direct-thunk form", () =>
582 Effect.gen(function*() {
583 const rejected = new Error("reject")
584 const exit = yield* Effect.tryPromise<number>(() => Promise.reject(rejected)).pipe(Effect.exit)
585 assertUnknownError(exit, rejected, "An error occurred in Effect.tryPromise")
586 }))
587
588 it.effect("succeeds with the resolved value in options form", () =>
589 Effect.gen(function*() {
590 let catchCalled = false
591 const result = yield* Effect.tryPromise({
592 try: () => Promise.resolve(1),
593 catch: () => {
594 catchCalled = true
595 return "error" as const
596 }
597 })
598 assert.strictEqual(result, 1)
599 assert.isFalse(catchCalled)
600 }))
601
602 it.effect("maps synchronous throws with catch in options form", () =>
603 Effect.gen(function*() {
604 const thrown = new Error("try")
605 const mapped = { cause: thrown }
606 const exit = yield* Effect.tryPromise({
607 try: () => {
608 throw thrown
609 },
610 catch: () => mapped
611 }).pipe(Effect.exit)
612 assertExitFailure(exit, Cause.fail(mapped))
613 }))
614
615 it.effect("maps promise rejections with catch in options form", () =>
616 Effect.gen(function*() {
617 const rejected = new Error("reject")
618 const mapped = { cause: rejected }
619 const exit = yield* Effect.tryPromise({
620 try: () => Promise.reject(rejected),
621 catch: () => mapped
622 }).pipe(Effect.exit)
623 assertExitFailure(exit, Cause.fail(mapped))
624 }))
625
626 it.effect("turns a throwing catch mapper for a synchronous throw into a defect", () =>
627 Effect.gen(function*() {
628 const thrown = new Error("try")
629 const defect = new Error("catch")
630 const exit = yield* Effect.tryPromise({
631 try: () => {
632 throw thrown
633 },
634 catch: () => {
635 throw defect
636 }
637 }).pipe(Effect.exit)
638 assertExitDefect(exit, defect)
639 }))
640
641 it.effect("turns a throwing catch mapper for a promise rejection into a defect", () =>
642 Effect.gen(function*() {
643 const rejected = new Error("reject")
644 const defect = new Error("catch")
645 const exit = yield* Effect.tryPromise({
646 try: () => Promise.reject(rejected),
647 catch: () => {
648 throw defect
649 }
650 }).pipe(Effect.exit)
651 assertExitDefect(exit, defect)
652 }))
653
654 it.effect("aborts the provided AbortSignal on interruption", () =>
655 Effect.gen(function*() {
656 let signal: AbortSignal | undefined
657 const fiber = yield* Effect.tryPromise((signal_) => {
658 signal = signal_
659 return new Promise<never>(() => {})
660 }).pipe(Effect.forkChild({ startImmediately: true }))
661 yield* Fiber.interrupt(fiber)
662 assert.strictEqual(signal?.aborted, true)
663 }))
664 })
665
666 describe("gen", () => {
667 it("gen", () =>
668 Effect.gen(function*() {
669 const result = yield* Effect.succeed(1)
670 assert.strictEqual(result, 1)
671 return result
672 }).pipe(Effect.runPromise).then((_) => assert.deepStrictEqual(_, 1)))
673
674 it("gen with context", () =>
675 Effect.gen({ self: { a: 1, b: 2 } }, function*() {
676 const result = yield* Effect.succeed(this.a)
677 assert.strictEqual(result, 1)
678 return result + this.b
679 }).pipe(Effect.runPromise).then((_) => assert.deepStrictEqual(_, 3)))
680
681 it("iterator yields the effect once, then completes with the sent value", () => {
682 const effect = Effect.succeed(1)
683 const iterator = effect[Symbol.iterator]()
684 const yielded = iterator.next()
685 const returned = iterator.next(2)
686 assert.deepStrictEqual({ ...yielded }, { value: effect, done: false })
687 assert.deepStrictEqual({ ...returned }, { value: 2, done: true })
688 assert.deepStrictEqual({ ...iterator.next(3) }, { value: 3, done: true })
689 })
690 })
691
692 describe("forEach", () => {
693 it.effect("sequential", () =>
694 Effect.gen(function*() {
695 const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_))
696 assert.deepStrictEqual(results, [1, 2, 3])
697 }))
698
699 it.effect("unbounded", () =>
700 Effect.gen(function*() {
701 const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_), { concurrency: "unbounded" })
702 assert.deepStrictEqual(results, [1, 2, 3])
703 }))
704
705 it.effect("bounded", () =>
706 Effect.gen(function*() {
707 const results = yield* Effect.forEach([1, 2, 3, 4, 5], (_) => Effect.succeed(_), { concurrency: 2 })
708 assert.deepStrictEqual(results, [1, 2, 3, 4, 5])
709 }))
710
711 it.effect("sequential interrupt", () =>
712 Effect.gen(function*() {
713 const done: Array<number> = []
714 const fiber = yield* Effect.forEach([1, 2, 3, 4, 5, 6], (i) =>
715 Effect.sync(() => {
716 done.push(i)
717 return i
718 }).pipe(Effect.delay(300))).pipe(Effect.forkChild)
719 yield* TestClock.adjust(800)
720 yield* Fiber.interrupt(fiber)
721 const result = yield* Fiber.await(fiber)
722 assert.isTrue(Exit.hasInterrupts(result))
723 assert.deepStrictEqual(done, [1, 2])
724 }))
725
726 it.effect("unbounded interrupt", () =>
727 Effect.gen(function*() {
728 const done: Array<number> = []
729 const fiber = yield* Effect.forEach([1, 2, 3], (i) =>
730 Effect.sync(() => {
731 done.push(i)
732 return i
733 }).pipe(Effect.delay(150)), { concurrency: "unbounded" }).pipe(Effect.forkChild)
734 yield* TestClock.adjust(50)
735 yield* Fiber.interrupt(fiber)
736 const result = yield* Fiber.await(fiber)
737 assert.isTrue(Exit.hasInterrupts(result))
738 assert.deepStrictEqual(done, [])
739 }))
740
741 it.effect("bounded interrupt", () =>
742 Effect.gen(function*() {
743 const done: Array<number> = []
744 const fiber = yield* Effect.forEach([1, 2, 3, 4, 5, 6], (i) =>
745 Effect.sync(() => {
746 done.push(i)
747 return i
748 }).pipe(Effect.delay(200)), { concurrency: 2 }).pipe(Effect.forkChild)
749 yield* TestClock.adjust(350)
750 yield* Fiber.interrupt(fiber)
751 const result = yield* Fiber.await(fiber)
752 assert.isTrue(Exit.hasInterrupts(result))
753 assert.deepStrictEqual(done, [1, 2])
754 }))
755
756 it.effect("preserves interruption when a child cleanup dies", () =>
757 Effect.gen(function*() {
758 const started = yield* Deferred.make<void>()
759 const defect = new Error("cleanup defect")
760 const fiber = yield* Effect.forEach([0, 1], (i) =>
761 i === 0
762 ? Deferred.succeed(started, void 0).pipe(
763 Effect.andThen(Effect.callback<void>(() => Effect.die(defect)))
764 )
765 : Effect.never, { concurrency: 2 }).pipe(Effect.forkChild({ startImmediately: true }))
766
767 yield* Deferred.await(started)
768 yield* Fiber.interrupt(fiber)
769 const exit = yield* Fiber.await(fiber)
770 assert.isTrue(Exit.hasInterrupts(exit))
771 assertExitDefect(exit, defect)
772 }))
773
774 it.effect("preserves a recorded sibling failure on external interruption", () =>
775 Effect.gen(function*() {
776 const started = yield* Deferred.make<void>()
777 const cleanupStarted = yield* Deferred.make<void>()
778 const releaseCleanup = yield* Deferred.make<void>()
779 const fiber = yield* Effect.forEach([0, 1], (i) =>
780 i === 0
781 ? Deferred.succeed(started, void 0).pipe(
782 Effect.andThen(Effect.never),
783 Effect.onInterrupt(() =>
784 Deferred.succeed(cleanupStarted, void 0).pipe(
785 Effect.andThen(Deferred.await(releaseCleanup))
786 )
787 )
788 )
789 : Deferred.await(started).pipe(Effect.andThen(Effect.fail("sibling failure"))), {
790 concurrency: 2
791 }).pipe(Effect.forkChild({ startImmediately: true }))
792
793 yield* Deferred.await(cleanupStarted)
794 fiber.interruptUnsafe()
795 yield* Deferred.succeed(releaseCleanup, void 0)
796 const exit = yield* Fiber.await(fiber)
797 assert.isTrue(Exit.hasInterrupts(exit))
798 assert.isTrue(Exit.isFailure(exit))
799 if (Exit.isFailure(exit)) {
800 assert.deepStrictEqual(Cause.findError(exit.cause), Result.succeed("sibling failure"))
801 }
802 }))
803
804 it.effect("unbounded fail", () =>
805 Effect.gen(function*() {
806 const done: Array<number> = []
807 const handle = yield* Effect.forEach([1, 2, 3, 4, 5], (i) =>
808 Effect.suspend(() => {
809 done.push(i)
810 return i === 3 ? Effect.fail("error") : Effect.succeed(i)
811 }).pipe(Effect.delay(i * 100)), {
812 concurrency: "unbounded"
813 }).pipe(Effect.forkChild)
814 yield* TestClock.adjust(500)
815 const result = yield* Fiber.await(handle)
816 assert.deepStrictEqual(result, Exit.fail("error"))
817 assert.deepStrictEqual(done, [1, 2, 3])
818 }))
819
820 it.effect("interrupts started workers when the mapper throws during initial pumping", () =>
821 Effect.gen(function*() {
822 const interruptStarted = yield* Deferred.make<void>()
823 const interruptFinished = yield* Deferred.make<void>()
824 const defect = new Error("mapper defect")
825 const fiber = yield* Effect.forEach([0, 1], (i) => {
826 if (i === 0) {
827 return Effect.never.pipe(
828 Effect.onInterrupt(() =>
829 Deferred.succeed(interruptStarted, void 0).pipe(
830 Effect.andThen(Deferred.await(interruptFinished))
831 )
832 )
833 )
834 }
835 throw defect
836 }, { concurrency: 2 }).pipe(Effect.forkChild({ startImmediately: true }))
837
838 assert.isTrue(yield* Deferred.isDone(interruptStarted))
839 assert.isUndefined(fiber.pollUnsafe())
840 yield* Deferred.succeed(interruptFinished, void 0)
841 const exit = fiber.pollUnsafe()
842 assert.isDefined(exit)
843 assertExitDefect(exit!, defect)
844 }))
845
846 it.effect("preserves a sibling cleanup defect when the mapper throws", () =>
847 Effect.gen(function*() {
848 const mapperDefect = new Error("mapper defect")
849 const cleanupDefect = new Error("cleanup defect")
850 let cleanedUp = false
851 const exit = yield* Effect.forEach([0, 1], (i) => {
852 if (i === 0) {
853 return Effect.callback<void>(() =>
854 Effect.sync(() => {
855 cleanedUp = true
856 throw cleanupDefect
857 })
858 )
859 }
860 throw mapperDefect
861 }, { concurrency: 2 }).pipe(Effect.exit)
862
863 assert.isTrue(cleanedUp)
864 assert.isTrue(Exit.isFailure(exit))
865 if (Exit.isFailure(exit)) {
866 assert.deepStrictEqual(
867 exit.cause.reasons.filter(Cause.isDieReason).map((reason) => reason.defect),
868 [mapperDefect, cleanupDefect]
869 )
870 }
871 }))
872
873 it.effect("interrupts started workers when observer-driven refill throws", () =>
874 Effect.gen(function*() {
875 const release = yield* Deferred.make<void>()
876 const interruptStarted = yield* Deferred.make<void>()
877 const interruptFinished = yield* Deferred.make<void>()
878 const defect = new Error("refill defect")
879 const fiber = yield* Effect.forEach([0, 1, 2], (i) => {
880 if (i === 0) return Deferred.await(release)
881 if (i === 1) {
882 return Effect.never.pipe(
883 Effect.onInterrupt(() =>
884 Deferred.succeed(interruptStarted, void 0).pipe(
885 Effect.andThen(Deferred.await(interruptFinished))
886 )
887 )
888 )
889 }
890 throw defect
891 }, { concurrency: 2 }).pipe(Effect.forkChild({ startImmediately: true }))
892
893 assert.isUndefined(fiber.pollUnsafe())
894 yield* Deferred.succeed(release, void 0)
895 assert.isTrue(yield* Deferred.isDone(interruptStarted))
896 assert.isUndefined(fiber.pollUnsafe())
897 yield* Deferred.succeed(interruptFinished, void 0)
898 const exit = fiber.pollUnsafe()
899 assert.isDefined(exit)
900 assertExitDefect(exit!, defect)
901 }))
902
903 it.effect("length = 0", () =>
904 Effect.gen(function*() {
905 const results = yield* Effect.forEach([], (_) => Effect.succeed(_))
906 assert.deepStrictEqual(results, [])
907 }))
908
909 it.effect("string", () =>
910 Effect.gen(function*() {
911 const results = yield* Effect.forEach("abc", (_) => Effect.succeed(_))
912 assert.deepStrictEqual(results, ["a", "b", "c"])
913 }))
914 })
915
916 describe("head", () => {
917 it.effect("returns the first element", () =>
918 Effect.gen(function*() {
919 const result = yield* Effect.head(Effect.succeed([1, 2, 3]))
920 assert.strictEqual(result, 1)
921 }))
922
923 it.effect("fails with NoSuchElementError for an empty iterable", () =>
924 Effect.gen(function*() {
925 const error = yield* Effect.head(Effect.succeed([] as Array<number>)).pipe(Effect.flip)
926 assert.isTrue(Cause.isNoSuchElementError(error))
927 }))
928
929 it.effect("preserves the source failure", () =>
930 Effect.gen(function*() {
931 const source: Effect.Effect<Iterable<number>, "failure"> = Effect.fail("failure")
932 const error = yield* Effect.head(source).pipe(Effect.flip)
933 assert.strictEqual(error, "failure")
934 }))
935 })
936
937 describe("all", () => {
938 it.effect("tuple", () =>
939 Effect.gen(function*() {
940 const results = (yield* Effect.all([
941 Effect.succeed(1),
942 Effect.succeed(2),
943 Effect.succeed(3)
944 ])) satisfies [
945 number,
946 number,
947 number
948 ]
949 assert.deepStrictEqual(results, [1, 2, 3])
950 }))
951
952 it.effect("record", () =>
953 Effect.gen(function*() {
954 const results = (yield* Effect.all({
955 a: Effect.succeed(1),
956 b: Effect.succeed("2"),
957 c: Effect.succeed(true)
958 })) satisfies {
959 a: number
960 b: string
961 c: boolean
962 }
963 assert.deepStrictEqual(results, {
964 a: 1,
965 b: "2",
966 c: true
967 })
968 }))
969
970 it.effect("record discard", () =>
971 Effect.gen(function*() {
972 const results = (yield* Effect.all({
973 a: Effect.succeed(1),
974 b: Effect.succeed("2"),
975 c: Effect.succeed(true)
976 }, { discard: true })) satisfies void
977 assert.deepStrictEqual(results, void 0)
978 }))
979
980 it.effect("iterable", () =>
981 Effect.gen(function*() {
982 const results = (yield* Effect.all(
983 new Set([
984 Effect.succeed(1),
985 Effect.succeed(2),
986 Effect.succeed(3)
987 ])
988 )) satisfies Array<number>
989 assert.deepStrictEqual(results, [1, 2, 3])
990 }))
991
992 it.effect("tuple result mode", () =>
993 Effect.gen(function*() {
994 const executed: Array<number> = []
995 const results = (yield* Effect.all(
996 [
997 Effect.sync(() => {
998 executed.push(1)
999 return 1
1000 }),
1001 Effect.sync(() => {
1002 executed.push(2)
1003 }).pipe(Effect.andThen(Effect.fail("boom"))),
1004 Effect.sync(() => {
1005 executed.push(3)
1006 return 3
1007 })
1008 ] as const,
1009 { mode: "result" }
1010 )) satisfies [
1011 Result.Result<number, never>,
1012 Result.Result<never, string>,
1013 Result.Result<number, never>
1014 ]
1015 assert.deepStrictEqual(executed, [1, 2, 3])
1016 assert.deepStrictEqual(results, [
1017 Result.succeed(1),
1018 Result.fail("boom"),
1019 Result.succeed(3)
1020 ])
1021 }))
1022
1023 it.effect("record result mode", () =>
1024 Effect.gen(function*() {
1025 const results = (yield* Effect.all({
1026 a: Effect.succeed(1),
1027 b: Effect.fail("boom"),
1028 c: Effect.succeed(true)
1029 }, { mode: "result" })) satisfies {
1030 a: Result.Result<number, never>
1031 b: Result.Result<never, string>
1032 c: Result.Result<boolean, never>
1033 }
1034 assert.deepStrictEqual(results, {
1035 a: Result.succeed(1),
1036 b: Result.fail("boom"),
1037 c: Result.succeed(true)
1038 })
1039 }))
1040
1041 it.effect("concurrency interrupts started siblings on failure", () =>
1042 Effect.gen(function*() {
1043 const started: Array<number> = []
1044 const interrupted: Array<number> = []
1045 const make = (i: number) =>
1046 Effect.suspend(() => {
1047 started.push(i)
1048 return Effect.sleep(500)
1049 }).pipe(
1050 Effect.as(i),
1051 Effect.onInterrupt(() =>
1052 Effect.sync(() => {
1053 interrupted.push(i)
1054 })
1055 )
1056 )
1057 const fiber = yield* Effect.all([
1058 make(1),
1059 Effect.fail("boom").pipe(Effect.delay(100)),
1060 make(3),
1061 make(4)
1062 ], { concurrency: 3 }).pipe(Effect.forkChild)
1063 yield* TestClock.adjust(100)
1064 const result = yield* Fiber.await(fiber)
1065 assert.deepStrictEqual(result, Exit.fail("boom"))
1066 assert.deepStrictEqual(started, [1, 3])
1067 assert.deepStrictEqual(interrupted, [1, 3])
1068 }))
1069 })
1070
1071 describe("partition", () => {
1072 it.effect("collects only successes", () =>
1073 Effect.gen(function*() {
1074 const values = [0, 1, 2, 3, 4]
1075 const [passes, fails] = yield* Effect.partition(values, Effect.succeed)
1076 assert.deepStrictEqual(passes, values)
1077 assert.deepStrictEqual(fails, [])
1078 }))
1079
1080 it.effect("collects only failures", () =>
1081 Effect.gen(function*() {
1082 const values = [0, 1, 2, 3, 4]
1083 const [passes, fails] = yield* Effect.partition(values, Effect.fail)
1084 assert.deepStrictEqual(passes, [])
1085 assert.deepStrictEqual(fails, values)
1086 }))
1087
1088 it.effect("collects successes and failures", () =>
1089 Effect.gen(function*() {
1090 const values = [0, 1, 2, 3, 4, 5]
1091 const [passes, fails] = yield* Effect.partition(values, (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n))
1092 assert.deepStrictEqual(passes, [1, 3, 5])
1093 assert.deepStrictEqual(fails, [0, 2, 4])
1094 }))
1095
1096 it.effect("supports concurrency option", () =>
1097 Effect.gen(function*() {
1098 const values = [0, 1, 2, 3, 4, 5]
1099 const [passes, fails] = yield* Effect.partition(
1100 values,
1101 (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n),
1102 { concurrency: "unbounded" }
1103 )
1104 assert.deepStrictEqual(passes, [1, 3, 5])
1105 assert.deepStrictEqual(fails, [0, 2, 4])
1106 }))
1107 })
1108
1109 describe("reduce", () => {
1110 it.effect("runs sequentially from left to right and passes the index", () =>
1111 Effect.gen(function*() {
1112 const visited: Array<string> = []
1113 const result = yield* Effect.reduce([1, 2, 3, 4, 5], () => 0, (acc, value, index) =>
1114 Effect.sync(() => {
1115 visited.push(`${index}:${value}`)
1116 return acc + value
1117 }))
1118
1119 assert.strictEqual(result, 15)
1120 assert.deepStrictEqual(visited, ["0:1", "1:2", "2:3", "3:4", "4:5"])
1121 }))
1122
1123 it.effect("stops at the first failure", () =>
1124 Effect.gen(function*() {
1125 const visited: Array<number> = []
1126 const result = yield* pipe(
1127 [1, 2, 3, 4, 5],
1128 Effect.reduce(() => 0, (acc, value) =>
1129 Effect.sync(() => visited.push(value)).pipe(
1130 Effect.andThen(value === 3 ? Effect.fail("fail") : Effect.succeed(acc + value))
1131 )),
1132 Effect.exit
1133 )
1134
1135 assert.deepStrictEqual(result, Exit.fail("fail"))
1136 assert.deepStrictEqual(visited, [1, 2, 3])
1137 }))
1138
1139 it.effect("returns zero for an empty iterable", () =>
1140 Effect.gen(function*() {
1141 const result = yield* Effect.reduce([], () => 42, (acc, value: number) => Effect.succeed(acc + value))
1142 assert.strictEqual(result, 42)
1143 }))
1144
1145 it.effect("evaluates zero lazily for each run", () =>
1146 Effect.gen(function*() {
1147 let evaluations = 0
1148 const reduced = Effect.reduce([1], () => {
1149 evaluations++
1150 return [] as Array<number>
1151 }, (acc, value) => Effect.succeed([...acc, value]))
1152
1153 assert.strictEqual(evaluations, 0)
1154 assert.deepStrictEqual(yield* reduced, [1])
1155 assert.deepStrictEqual(yield* reduced, [1])
1156 assert.strictEqual(evaluations, 2)
1157 }))
1158 })
1159
1160 describe("validate", () => {
1161 it.effect("collects successes when all effects succeed", () =>
1162 Effect.gen(function*() {
1163 const values = [0, 1, 2, 3, 4]
1164 const satisfying = yield* Effect.validate(values, Effect.succeed)
1165 assert.deepStrictEqual(satisfying, values)
1166 }))
1167
1168 it.effect("accumulates all failures", () =>
1169 Effect.gen(function*() {
1170 const values = [0, 1, 2, 3, 4, 5]
1171 const errors = yield* Effect.validate(values, (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n)).pipe(
1172 Effect.flip
1173 )
1174 assert.deepStrictEqual(errors, [0, 2, 4])
1175 }))
1176
1177 it.effect("supports discard option", () =>
1178 Effect.gen(function*() {
1179 const result = yield* Effect.validate([1, 2, 3], Effect.succeed, { discard: true })
1180 assert.strictEqual(result, undefined)
1181 }))
1182 })
1183
1184 describe("findFirst", () => {
1185 it.effect("returns first matching element", () =>
1186 Effect.gen(function*() {
1187 const result = yield* Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 2))
1188 assert.deepStrictEqual(result, Option.some(3))
1189 }))
1190
1191 it.effect("returns none when no element matches", () =>
1192 Effect.gen(function*() {
1193 const result = yield* Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 10))
1194 assert.deepStrictEqual(result, Option.none())
1195 }))
1196
1197 it.effect("short-circuits on first match", () =>
1198 Effect.gen(function*() {
1199 const seen: Array<number> = []
1200 const result = yield* Effect.findFirst([1, 2, 3, 4], (n) =>
1201 Effect.sync(() => {
1202 seen.push(n)
1203 return n > 2
1204 }))
1205 assert.deepStrictEqual(result, Option.some(3))
1206 assert.deepStrictEqual(seen, [1, 2, 3])
1207 }))
1208 })
1209
1210 describe("findFirstFilter", () => {
1211 it.effect("returns first successful transformed value", () =>
1212 Effect.gen(function*() {
1213 const result = yield* Effect.findFirstFilter([1, 2, 3, 4], (n) =>
1214 Effect.succeed(n % 2 === 0 ? Result.succeed(`n=${n}`) : Result.failVoid))
1215 assert.deepStrictEqual(result, Option.some("n=2"))
1216 }))
1217
1218 it.effect("passes index to filter", () =>
1219 Effect.gen(function*() {
1220 const result = yield* Effect.findFirstFilter([10, 11, 12], (n, i) =>
1221 Effect.succeed(i === 2 ? Result.succeed(`${n}:${i}`) : Result.failVoid))
1222 assert.deepStrictEqual(result, Option.some("12:2"))
1223 }))
1224
1225 it.effect("returns none when filter never succeeds", () =>
1226 Effect.gen(function*() {
1227 const result = yield* Effect.findFirstFilter([1, 2, 3], () => Effect.succeed(Result.failVoid))
1228 assert.deepStrictEqual(result, Option.none())
1229 }))
1230 })
1231
1232 describe("filter", () => {
1233 it.live("odd numbers", () =>
1234 Effect.gen(function*() {
1235 const results = yield* Effect.filter([1, 2, 3, 4, 5], (value) => Effect.succeed(value % 2 === 1))
1236 assert.deepStrictEqual(results, [1, 3, 5])
1237 }))
1238
1239 it.live("iterable", () =>
1240 Effect.gen(function*() {
1241 const results = yield* Effect.filter(new Set([1, 2, 3, 4, 5]), (value) => Effect.succeed(value % 2 === 1))
1242 assert.deepStrictEqual(results, [1, 3, 5])
1243 }))
1244 })
1245
1246 describe("acquireDisposable", () => {
1247 it.effect("releases disposables", ({ expect }) =>
1248 Effect.gen(function*() {
1249 const acquire = Effect.sync((): Disposable => ({ [Symbol.dispose]: release }))
1250 const release = vi.fn(() => void 0)
1251
1252 yield* Effect.scoped(Effect.acquireDisposable(acquire))
1253 expect(release).toHaveBeenCalledTimes(1)
1254 }))
1255
1256 it.effect("releases async disposables", ({ expect }) =>
1257 Effect.gen(function*() {
1258 const acquire = Effect.sync((): AsyncDisposable => ({ [Symbol.asyncDispose]: release }))
1259 const release = vi.fn(async () => void 0)
1260
1261 yield* Effect.scoped(Effect.acquireDisposable(acquire))
1262 expect(release).toHaveBeenCalledTimes(1)
1263 }))
1264 })
1265
1266 describe("acquireRelease", () => {
1267 it.live("releases on interrupt", () =>
1268 Effect.gen(function*() {
1269 let release = false
1270 const fiber = yield* Effect.acquireRelease(
1271 Effect.delay(Effect.succeed("foo"), 100),
1272 () =>
1273 Effect.sync(() => {
1274 release = true
1275 })
1276 ).pipe(
1277 Effect.scoped,
1278 Effect.forkChild({ startImmediately: true })
1279 )
1280 fiber.interruptUnsafe()
1281 yield* Fiber.await(fiber)
1282 assert.strictEqual(release, true)
1283 }))
1284
1285 it.effect("supports release dependencies", () =>
1286 Effect.gen(function*() {
1287 let release = false
1288 const scope = yield* Scope.make()
1289 yield* Scope.provide(scope)(
1290 Effect.provideService(
1291 Effect.acquireRelease(
1292 Effect.succeed("foo"),
1293 () =>
1294 Effect.flatMap(Effect.service(ATag), () =>
1295 Effect.sync(() => {
1296 release = true
1297 }))
1298 ),
1299 ATag,
1300 "A"
1301 )
1302 )
1303 yield* Scope.close(scope, Exit.void)
1304 assert.strictEqual(release, true)
1305 }))
1306 })
1307
1308 it.effect("raceAll", () =>
1309 Effect.gen(function*() {
1310 const interrupted: Array<number> = []
1311 const fiber = yield* Effect.raceAll([500, 300, 200, 0, 100].map((ms) =>
1312 (ms === 0 ? Effect.fail("boom") : Effect.succeed(ms)).pipe(
1313 Effect.delay(ms),
1314 Effect.onInterrupt(() =>
1315 Effect.sync(() => {
1316 interrupted.push(ms)
1317 })
1318 )
1319 )
1320 )).pipe(Effect.forkChild)
1321 yield* TestClock.adjust("500 millis")
1322 const result = yield* Fiber.join(fiber)
1323 assert.strictEqual(result, 100)
1324 assert.deepStrictEqual(interrupted, [500, 300, 200])
1325 }))
1326
1327 it.effect("raceAllFirst", () =>
1328 Effect.gen(function*() {
1329 const interrupted: Array<number> = []
1330 const fiber = yield* Effect.raceAllFirst([500, 300, 200, 0, 100].map((ms) =>
1331 (ms === 0 ? Effect.fail("boom") : Effect.succeed(ms)).pipe(
1332 Effect.delay(ms),
1333 Effect.onInterrupt(() =>
1334 Effect.sync(() => {
1335 interrupted.push(ms)
1336 })
1337 )
1338 )
1339 )).pipe(Effect.exit, Effect.forkChild)
1340 yield* TestClock.adjust("500 millis")
1341 const result = yield* Fiber.join(fiber)
1342 assert.deepStrictEqual(result, Exit.fail("boom"))
1343 // 100 doesn't start because 0 finishes the race first
1344 assert.deepStrictEqual(interrupted, [500, 300, 200])
1345 }))
1346
1347 it.effect("race interrupts the loser when the other side succeeds", () =>
1348 Effect.gen(function*() {
1349 const interrupted: Array<string> = []
1350 const onInterrupt = (label: string) =>
1351 Effect.onInterrupt(() =>
1352 Effect.sync(() => {
1353 interrupted.push(label)
1354 })
1355 )
1356 const fiber = yield* Effect.race(
1357 Effect.succeed("fast").pipe(Effect.delay(100), onInterrupt("fast")),
1358 Effect.succeed("slow").pipe(Effect.delay(500), onInterrupt("slow"))
1359 ).pipe(Effect.forkChild)
1360 yield* TestClock.adjust("500 millis")
1361 const result = yield* Fiber.join(fiber)
1362 assert.strictEqual(result, "fast")
1363 assert.deepStrictEqual(interrupted, ["slow"])
1364 }))
1365
1366 it.effect("raceFirst interrupts the loser when the other side succeeds", () =>
1367 Effect.gen(function*() {
1368 const interrupted: Array<string> = []
1369 const onInterrupt = (label: string) =>
1370 Effect.onInterrupt(() =>
1371 Effect.sync(() => {
1372 interrupted.push(label)
1373 })
1374 )
1375 const fiber = yield* Effect.raceFirst(
1376 Effect.succeed("fast").pipe(Effect.delay(100), onInterrupt("fast")),
1377 Effect.succeed("slow").pipe(Effect.delay(500), onInterrupt("slow"))
1378 ).pipe(Effect.forkChild)
1379 yield* TestClock.adjust("500 millis")
1380 const result = yield* Fiber.join(fiber)
1381 assert.strictEqual(result, "fast")
1382 assert.deepStrictEqual(interrupted, ["slow"])
1383 }))
1384
1385 it.effect("raceFirst interrupts the loser when the other side fails", () =>
1386 Effect.gen(function*() {
1387 const interrupted: Array<string> = []
1388 const fiber = yield* Effect.raceFirst(
1389 Effect.fail("boom").pipe(Effect.delay(100)),
1390 Effect.succeed("slow").pipe(
1391 Effect.delay(500),
1392 Effect.onInterrupt(() =>
1393 Effect.sync(() => {
1394 interrupted.push("slow")
1395 })
1396 )
1397 )
1398 ).pipe(Effect.exit, Effect.forkChild)
1399 yield* TestClock.adjust("500 millis")
1400 const result = yield* Fiber.join(fiber)
1401 assert.deepStrictEqual(result, Exit.fail("boom"))
1402 assert.deepStrictEqual(interrupted, ["slow"])
1403 }))
1404
1405 for (const [name, race] of [["race", Effect.race], ["raceFirst", Effect.raceFirst]] as const) {
1406 it.effect(`${name} interrupts a loser that settles the race while starting`, () =>
1407 Effect.gen(function*() {
1408 let interrupted = false
1409 const deferred = yield* Deferred.make<void>()
1410 yield* race(
1411 Deferred.await(deferred),
1412 Deferred.succeed(deferred, void 0).pipe(
1413 Effect.andThen(Effect.never),
1414 Effect.onInterrupt(() =>
1415 Effect.sync(() => {
1416 interrupted = true
1417 })
1418 )
1419 )
1420 )
1421 assert.isTrue(interrupted)
1422 }))
1423
1424 it.effect(`${name} interrupts the loser when interrupted while starting`, () =>
1425 Effect.gen(function*() {
1426 let interrupted = false
1427 const fiber = yield* Effect.withFiber((parent) =>
1428 race(
1429 Effect.sync(() => parent.interruptUnsafe()).pipe(
1430 Effect.andThen(Effect.never),
1431 Effect.onInterrupt(() =>
1432 Effect.sync(() => {
1433 interrupted = true
1434 })
1435 )
1436 ),
1437 Effect.void
1438 )
1439 ).pipe(Effect.forkChild)
1440 yield* Fiber.await(fiber)
1441 assert.isTrue(interrupted)
1442 }))
1443 }
1444
1445 describe("repeat", () => {
1446 it.effect("is interruptible", () =>
1447 Effect.gen(function*() {
1448 const fiber = yield* Effect.void.pipe(
1449 Effect.forever,
1450 Effect.timeoutOption(50),
1451 Effect.forkChild
1452 )
1453 yield* TestClock.adjust(50)
1454 const result = yield* Fiber.join(fiber)
1455 assert.deepStrictEqual(result, Option.none())
1456 }))
1457
1458 it.effect("repeat/until - repeats until a condition is true", () =>
1459 Effect.gen(function*() {
1460 let input = 10
1461 let output = 0
1462 const decrement = Effect.sync(() => --input)
1463 const increment = Effect.sync(() => output++)
1464 const result = yield* decrement.pipe(
1465 Effect.tap(increment),
1466 Effect.repeat({ until: (n) => n === 0 })
1467 )
1468 assert.strictEqual(result, 0)
1469 assert.strictEqual(output, 10)
1470 }))
1471
1472 it.effect("repeat/until - repeats until an effectful condition is true", () =>
1473 Effect.gen(function*() {
1474 let input = 10
1475 let output = 0
1476 const decrement = Effect.sync(() => --input)
1477 const increment = Effect.sync(() => output++)
1478 const result = yield* decrement.pipe(
1479 Effect.tap(increment),
1480 Effect.repeat({ until: (n) => Effect.succeed(n === 0) })
1481 )
1482 assert.strictEqual(result, 0)
1483 assert.strictEqual(output, 10)
1484 }))
1485
1486 it.effect("repeat/until - always evaluates at least once", () =>
1487 Effect.gen(function*() {
1488 let n = 0
1489 const increment = Effect.sync(() => n++)
1490 yield* Effect.repeat(increment, { until: constTrue })
1491 assert.strictEqual(n, 1)
1492 }))
1493
1494 it.effect("repeat/while - repeats while a condition is true", () =>
1495 Effect.gen(function*() {
1496 let input = 10
1497 let output = 0
1498 const decrement = Effect.sync(() => --input)
1499 const increment = Effect.sync(() => output++)
1500 const result = yield* decrement.pipe(
1501 Effect.tap(increment),
1502 Effect.repeat({ while: (n) => n > 0 })
1503 )
1504 assert.strictEqual(result, 0)
1505 assert.strictEqual(output, 10)
1506 }))
1507
1508 it.effect("repeat/while - repeats while an effectful condition is true", () =>
1509 Effect.gen(function*() {
1510 let input = 10
1511 let output = 0
1512 const decrement = Effect.sync(() => --input)
1513 const increment = Effect.sync(() => output++)
1514 const result = yield* decrement.pipe(
1515 Effect.tap(increment),
1516 Effect.repeat({ while: (n) => Effect.succeed(n > 0) })
1517 )
1518 assert.strictEqual(result, 0)
1519 assert.strictEqual(output, 10)
1520 }))
1521
1522 it.effect("repeat/while - always evaluates at least once", () =>
1523 Effect.gen(function*() {
1524 let n = 0
1525 const increment = Effect.sync(() => n++)
1526 yield* Effect.repeat(increment, { while: constFalse })
1527 assert.strictEqual(n, 1)
1528 }))
1529
1530 it.effect("repeat/times", () =>
1531 Effect.gen(function*() {
1532 let n = 0
1533 const increment = Effect.sync(() => ++n)
1534 const result = yield* Effect.repeat(increment, {
1535 times: 2
1536 })
1537 assert.strictEqual(n, 3)
1538 assert.strictEqual(result, 3)
1539 }))
1540
1541 it.effect("repeat/schedule - repeats according to the specified schedule", () =>
1542 Effect.gen(function*() {
1543 let n = 0
1544 const increment = Effect.sync(() => ++n)
1545 const result = yield* Effect.repeat(increment, Schedule.recurs(3))
1546 assert.strictEqual(result, 3)
1547 }))
1548
1549 it.effect("repeat/schedule - with until", () =>
1550 Effect.gen(function*() {
1551 let n = 0
1552 const increment = Effect.sync(() => ++n)
1553 const result = yield* Effect.repeat(increment, {
1554 schedule: Schedule.recurs(3),
1555 until: (n) => n === 3
1556 })
1557 assert.strictEqual(n, 3)
1558 assert.strictEqual(result, 3)
1559 }))
1560
1561 it.effect("repeat/schedule - with while", () =>
1562 Effect.gen(function*() {
1563 let n = 0
1564 const increment = Effect.sync(() => ++n)
1565 const result = yield* Effect.repeat(increment, {
1566 schedule: Schedule.recurs(3),
1567 while: (n) => n < 3
1568 })
1569 assert.strictEqual(n, 3)
1570 assert.strictEqual(result, 3) // schedule result
1571 }))
1572 })
1573
1574 describe("retry", () => {
1575 it.live("nothing on success", () =>
1576 Effect.gen(function*() {
1577 let count = 0
1578 yield* Effect.sync(() => count++).pipe(
1579 Effect.retry({ times: 10000 })
1580 )
1581 assert.strictEqual(count, 1)
1582 }))
1583
1584 it.effect("retry/until - retries until a condition is true", () =>
1585 Effect.gen(function*() {
1586 let input = 10
1587 let output = 0
1588 const decrement = Effect.sync(() => --input)
1589 const increment = Effect.sync(() => output++)
1590 const result = yield* decrement.pipe(
1591 Effect.tap(increment),
1592 Effect.flip,
1593 Effect.retry({ until: (n) => n === 0 }),
1594 Effect.flip
1595 )
1596 assert.strictEqual(result, 0)
1597 assert.strictEqual(output, 10)
1598 }))
1599
1600 it.effect("retry/until - retries until an effectful condition is true", () =>
1601 Effect.gen(function*() {
1602 let input = 10
1603 let output = 0
1604 const decrement = Effect.sync(() => --input)
1605 const increment = Effect.sync(() => output++)
1606 const result = yield* decrement.pipe(
1607 Effect.tap(increment),
1608 Effect.flip,
1609 Effect.retry({ until: (n) => Effect.succeed(n === 0) }),
1610 Effect.flip
1611 )
1612 assert.strictEqual(result, 0)
1613 assert.strictEqual(output, 10)
1614 }))
1615
1616 it.effect("retry/until - always evaluates at least once", () =>
1617 Effect.gen(function*() {
1618 let n = 0
1619 const increment = Effect.failSync(() => n++)
1620 yield* increment.pipe(
1621 Effect.retry({ until: constTrue }),
1622 Effect.flip
1623 )
1624 assert.strictEqual(n, 1)
1625 }))
1626
1627 it.effect("retry/while - retries while a condition is true", () =>
1628 Effect.gen(function*() {
1629 let input = 10
1630 let output = 0
1631 const decrement = Effect.sync(() => --input)
1632 const increment = Effect.sync(() => output++)
1633 const result = yield* decrement.pipe(
1634 Effect.tap(increment),
1635 Effect.flip,
1636 Effect.retry({ while: (n) => n > 0 }),
1637 Effect.flip
1638 )
1639 assert.strictEqual(result, 0)
1640 assert.strictEqual(output, 10)
1641 }))
1642
1643 it.effect("retry/while - retries while an effectful condition is true", () =>
1644 Effect.gen(function*() {
1645 let input = 10
1646 let output = 0
1647 const decrement = Effect.sync(() => --input)
1648 const increment = Effect.sync(() => output++)
1649 const result = yield* decrement.pipe(
1650 Effect.tap(increment),
1651 Effect.flip,
1652 Effect.retry({ while: (n) => Effect.succeed(n > 0) }),
1653 Effect.flip
1654 )
1655 assert.strictEqual(result, 0)
1656 assert.strictEqual(output, 10)
1657 }))
1658
1659 it.effect("retry/while - always evaluates at least once", () =>
1660 Effect.gen(function*() {
1661 let n = 0
1662 const increment = Effect.failSync(() => n++)
1663 yield* increment.pipe(
1664 Effect.retry({ while: constFalse }),
1665 Effect.flip
1666 )
1667 assert.strictEqual(n, 1)
1668 }))
1669
1670 it.effect("retry/schedule - retries according to the specified schedule", () =>
1671 Effect.gen(function*() {
1672 let n = 0
1673 const increment = Effect.failSync(() => n++)
1674 yield* increment.pipe(
1675 Effect.retry(Schedule.recurs(3)),
1676 Effect.flip
1677 )
1678 assert.strictEqual(n, 4)
1679 }))
1680
1681 it.effect("retry/schedule - with until", () =>
1682 Effect.gen(function*() {
1683 let n = 0
1684 const increment = Effect.failSync(() => ++n)
1685 yield* increment.pipe(
1686 Effect.retry({
1687 schedule: Schedule.recurs(3),
1688 until: (n) => n === 3
1689 }),
1690 Effect.flip
1691 )
1692 assert.strictEqual(n, 3)
1693 }))
1694
1695 it.effect("retry/schedule - until errors", () =>
1696 Effect.gen(function*() {
1697 let n = 0
1698 const increment = Effect.failSync(() => ++n)
1699 const result = yield* increment.pipe(
1700 Effect.retry({
1701 schedule: Schedule.recurs(3),
1702 until: () => Effect.fail("boom")
1703 }),
1704 Effect.flip
1705 )
1706 assert.strictEqual(n, 1)
1707 assert.strictEqual(result, "boom")
1708 }))
1709
1710 it.effect("retry/schedule - with while", () =>
1711 Effect.gen(function*() {
1712 let n = 0
1713 const increment = Effect.failSync(() => ++n)
1714 yield* increment.pipe(
1715 Effect.retry({
1716 schedule: Schedule.recurs(3),
1717 while: (n) => n < 3
1718 }),
1719 Effect.flip
1720 )
1721 assert.strictEqual(n, 3)
1722 }))
1723
1724 it.effect("retry/schedule - while errors", () =>
1725 Effect.gen(function*() {
1726 let n = 0
1727 const increment = Effect.failSync(() => ++n)
1728 const result = yield* increment.pipe(
1729 Effect.retry({
1730 schedule: Schedule.recurs(3),
1731 while: () => Effect.fail("boom")
1732 }),
1733 Effect.flip
1734 )
1735 assert.strictEqual(n, 1)
1736 assert.strictEqual(result, "boom")
1737 }))
1738
1739 it.effect("retry/schedule - CurrentMetadata", () =>
1740 Effect.gen(function*() {
1741 const metadata: Array<Schedule.Metadata> = []
1742 yield* pipe(
1743 Effect.gen(function*() {
1744 const meta = yield* Schedule.CurrentMetadata
1745 metadata.push(meta)
1746 }),
1747 Effect.flip,
1748 Effect.retry(Schedule.recurs(3)),
1749 Effect.flip
1750 )
1751 assert.deepStrictEqual(metadata, [
1752 {
1753 elapsed: 0,
1754 elapsedSincePrevious: 0,
1755 attempt: 0,
1756 input: undefined,
1757 output: undefined,
1758 now: 0,
1759 start: 0,
1760 duration: Duration.zero
1761 },
1762 {
1763 elapsed: 0,
1764 elapsedSincePrevious: 0,
1765 attempt: 1,
1766 input: undefined,
1767 output: 0,
1768 now: 0,
1769 start: 0,
1770 duration: Duration.zero
1771 },
1772 {
1773 elapsed: 0,
1774 elapsedSincePrevious: 0,
1775 attempt: 2,
1776 input: undefined,
1777 output: 1,
1778 now: 0,
1779 start: 0,
1780 duration: Duration.zero
1781 },
1782 {
1783 elapsed: 0,
1784 elapsedSincePrevious: 0,
1785 attempt: 3,
1786 input: undefined,
1787 output: 2,
1788 now: 0,
1789 start: 0,
1790 duration: Duration.zero
1791 }
1792 ])
1793 }))
1794 })
1795
1796 describe("timed", () => {
1797 it.effect("uses monotonic time when wall time moves backward", () =>
1798 Effect.gen(function*() {
1799 yield* TestClock.setTime(1_000)
1800 const [duration, result] = yield* Effect.gen(function*() {
1801 yield* TestClock.adjust("100 millis")
1802 yield* TestClock.setTime(0)
1803 return "done"
1804 }).pipe(Effect.timed)
1805
1806 assert.strictEqual(result, "done")
1807 assert.strictEqual(Duration.toMillis(duration), 100)
1808 }))
1809 })
1810
1811 describe("timeoutOrElse", () => {
1812 const forms = ["data-first", "pipe"] as const
1813
1814 for (const form of forms) {
1815 it.live(`${form}: factory and body follow cleanup and replay`, () =>
1816 Effect.gen(function*() {
1817 let stopped = false
1818 let factories = 0
1819 let bodies = 0
1820 const order: Array<string> = []
1821 const value = { fallback: true }
1822 const source = Effect.never.pipe(Effect.ensuring(Effect.sync(() => {
1823 stopped = true
1824 order.push("stopped")
1825 })))
1826 const options = {
1827 duration: 0,
1828 orElse: () => {
1829 factories++
1830 order.push(`factory:${stopped}`)
1831 return Effect.sync(() => {
1832 bodies++
1833 order.push(`body:${stopped}`)
1834 return value
1835 })
1836 }
1837 }
1838 const program = form === "data-first"
1839 ? Effect.timeoutOrElse(source, options)
1840 : source.pipe(Effect.timeoutOrElse(options))
1841 const before = [factories, bodies]
1842 const first = yield* program
1843 const firstStopped = stopped
1844 stopped = false
1845 const second = yield* program
1846 assert.deepStrictEqual({
1847 before,
1848 factories,
1849 bodies,
1850 firstStopped,
1851 stopped,
1852 identity: [first === value, second === value],
1853 order
1854 }, {
1855 before: [0, 0],
1856 factories: 2,
1857 bodies: 2,
1858 firstStopped: true,
1859 stopped: true,
1860 identity: [true, true],
1861 order: ["stopped", "factory:true", "body:true", "stopped", "factory:true", "body:true"]
1862 })
1863 }))
1864 }
1865
1866 it.live("a ready source value is not confused with a zero-duration timeout", () =>
1867 Effect.gen(function*() {
1868 const value = Option.none()
1869 let calls = 0
1870 const result = yield* Effect.timeoutOrElse(Effect.succeed(value), {
1871 duration: 0,
1872 orElse: () => {
1873 calls++
1874 return Effect.succeed(Option.some("unexpected fallback"))
1875 }
1876 })
1877 assert.deepStrictEqual([result, calls], [value, 0])
1878 }))
1879
1880 it.live("slow fallback cannot let the original source win after timeout", () =>
1881 Effect.gen(function*() {
1882 const sourceGate = yield* Deferred.make<void>()
1883 const sourceStopped = yield* Deferred.make<void>()
1884 const fallbackStarted = yield* Deferred.make<void>()
1885 const fallbackRelease = yield* Deferred.make<void>()
1886 const sourceCompleted = yield* Ref.make(false)
1887 const value = { fallback: true }
1888 let fallbackInterrupted = false
1889 const source = Deferred.await(sourceGate).pipe(
1890 Effect.andThen(Ref.set(sourceCompleted, true)),
1891 Effect.as({ source: true }),
1892 Effect.ensuring(Deferred.succeed(sourceStopped, undefined))
1893 )
1894 const fiber = yield* Effect.timeoutOrElse(source, {
1895 duration: 0,
1896 orElse: () =>
1897 Deferred.succeed(fallbackStarted, undefined).pipe(
1898 Effect.andThen(Deferred.await(fallbackRelease)),
1899 Effect.as(value),
1900 Effect.onInterrupt(() =>
1901 Effect.sync(() => {
1902 fallbackInterrupted = true
1903 })
1904 )
1905 )
1906 }).pipe(Effect.forkChild)
1907 yield* Deferred.await(fallbackStarted)
1908 yield* Deferred.succeed(sourceGate, undefined)
1909 yield* Deferred.await(sourceStopped)
1910 yield* Deferred.succeed(fallbackRelease, undefined)
1911 const completed = yield* Ref.get(sourceCompleted)
1912 const result = yield* Fiber.join(fiber)
1913 assert.deepStrictEqual([result === value, completed, fallbackInterrupted], [true, false, false])
1914 }))
1915
1916 class SourceService extends Context.Service<SourceService, { source: string }>()("timeout-test/SourceService") {}
1917 class FallbackService
1918 extends Context.Service<FallbackService, { fallback: string }>()("timeout-test/FallbackService")
1919 {}
1920 const Local = Context.Reference<string>("timeout-test/Local", { defaultValue: () => "default" })
1921
1922 it.live("source and fallback inherit caller context", () =>
1923 Effect.gen(function*() {
1924 const sourceService = { source: "source service" }
1925 const fallbackService = { fallback: "fallback service" }
1926 const observed: Array<unknown> = []
1927 const result = yield* Effect.gen(function*() {
1928 const callerId = yield* Effect.fiberId
1929 const parentSpan = yield* Effect.currentSpan
1930 const source = Effect.gen(function*() {
1931 observed.push(
1932 (yield* SourceService) === sourceService,
1933 yield* Local,
1934 (yield* Effect.currentSpan) === parentSpan
1935 )
1936 return yield* Effect.never
1937 }).pipe(Effect.provideService(Local, "source-local"))
1938 const value = yield* Effect.timeoutOrElse(source, {
1939 duration: 0,
1940 orElse: () =>
1941 Effect.gen(function*() {
1942 const annotations = yield* References.CurrentLogAnnotations
1943 observed.push(
1944 (yield* Effect.fiberId) === callerId,
1945 yield* Local,
1946 (yield* Effect.currentSpan) === parentSpan,
1947 annotations["request"]
1948 )
1949 return yield* FallbackService
1950 })
1951 })
1952 observed.push(yield* Local, (yield* Effect.currentSpan) === parentSpan)
1953 return value
1954 }).pipe(
1955 Effect.provideService(SourceService, sourceService),
1956 Effect.provideService(FallbackService, fallbackService),
1957 Effect.provideService(Local, "parent-local"),
1958 Effect.annotateLogs({ request: "request-value" }),
1959 Effect.withSpan("timeout-parent")
1960 )
1961 assert.deepStrictEqual([result === fallbackService, observed], [true, [
1962 true,
1963 "source-local",
1964 true,
1965 true,
1966 "parent-local",
1967 true,
1968 "request-value",
1969 "parent-local",
1970 true
1971 ]])
1972 }))
1973
1974 it.live("external interruption stops a pending fallback", () =>
1975 Effect.gen(function*() {
1976 const started = yield* Deferred.make<void>()
1977 let sourceStopped = false
1978 let fallbackStopped = false
1979 let calls = 0
1980 const fiber = yield* Effect.never.pipe(
1981 Effect.ensuring(Effect.sync(() => {
1982 sourceStopped = true
1983 })),
1984 Effect.timeoutOrElse({
1985 duration: 0,
1986 orElse: () => {
1987 calls++
1988 return Deferred.succeed(started, undefined).pipe(
1989 Effect.andThen(Effect.never),
1990 Effect.ensuring(Effect.sync(() => {
1991 fallbackStopped = true
1992 }))
1993 )
1994 }
1995 }),
1996 Effect.forkChild
1997 )
1998 yield* Deferred.await(started)
1999 yield* Fiber.interrupt(fiber)
2000 const exit = yield* Fiber.await(fiber)
2001 assert.deepStrictEqual([Exit.hasInterrupts(exit), sourceStopped, fallbackStopped, calls], [true, true, true, 1])
2002 }))
2003 })
2004
2005 describe("timeoutOption", () => {
2006 it.live("timeout a long computation", () =>
2007 Effect.gen(function*() {
2008 const result = yield* pipe(
2009 Effect.sleep(60_000),
2010 Effect.andThen(Effect.succeed(true)),
2011 Effect.timeoutOption(10)
2012 )
2013 assert.deepStrictEqual(result, Option.none())
2014 }))
2015 it.live("timeout a long computation with a failure", () =>
2016 Effect.gen(function*() {
2017 const error = new Error("boom")
2018 const result = yield* pipe(
2019 Effect.sleep(5000),
2020 Effect.andThen(Effect.succeed(true)),
2021 Effect.timeoutOrElse({
2022 orElse: () => Effect.die(error),
2023 duration: 10
2024 }),
2025 Effect.sandbox,
2026 Effect.flip
2027 )
2028 assert.deepStrictEqual(result, Cause.die(error))
2029 }))
2030 it.effect("timeout repetition of uninterruptible effect", () =>
2031 Effect.gen(function*() {
2032 const fiber = yield* pipe(
2033 Effect.void,
2034 Effect.uninterruptible,
2035 Effect.forever,
2036 Effect.timeoutOption(10),
2037 Effect.forkChild
2038 )
2039 yield* TestClock.adjust(10)
2040 const result = yield* Fiber.join(fiber)
2041 assert.deepStrictEqual(result, Option.none())
2042 }))
2043 it.effect("timeout in uninterruptible region", () =>
2044 Effect.void.pipe(
2045 Effect.timeoutOption(20_000),
2046 Effect.uninterruptible
2047 ), { timeout: 1000 })
2048 })
2049
2050 describe("timeout", () => {
2051 it.effect("timeout produces a useful error message", () =>
2052 Effect.gen(function*() {
2053 const duration = Duration.millis(1500)
2054 const fiber = yield* Effect.never.pipe(
2055 Effect.timeout(duration),
2056 Effect.flip,
2057 Effect.forkChild
2058 )
2059 yield* TestClock.adjust(Duration.millis(2000))
2060 const result = yield* Fiber.join(fiber)
2061 assert.include(
2062 result.toString(),
2063 "TimeoutError: Operation timed out after '1s 500ms'"
2064 )
2065 }))
2066 it.live("timeout a long computation", () =>
2067 Effect.gen(function*() {
2068 const result = yield* pipe(
2069 Effect.sleep(60_000),
2070 Effect.andThen(Effect.succeed(true)),
2071 Effect.timeout(10),
2072 Effect.flip
2073 )
2074 assert.deepStrictEqual(
2075 result,
2076 new Cause.TimeoutError("Operation timed out after '10ms'")
2077 )
2078 }))
2079 it.effect("timeout interrupts the effect", () =>
2080 Effect.gen(function*() {
2081 let interrupted = false
2082 const fiber = yield* Effect.never.pipe(
2083 Effect.onInterrupt(() =>
2084 Effect.sync(() => {
2085 interrupted = true
2086 })
2087 ),
2088 Effect.timeout(10),
2089 Effect.flip,
2090 Effect.forkChild
2091 )
2092 yield* TestClock.adjust(10)
2093 const result = yield* Fiber.join(fiber)
2094 assert.deepStrictEqual(
2095 result,
2096 new Cause.TimeoutError("Operation timed out after '10ms'")
2097 )
2098 assert.isTrue(interrupted)
2099 }))
2100 })
2101
2102 describe("interruption", () => {
2103 it.effect("skipped handlers drop failures and keep defects when interrupted", () =>
2104 Effect.gen(function*() {
2105 const latch = yield* Latch.make()
2106 const fiber = yield* latch.await.pipe(
2107 Effect.andThen(Effect.failCause(Cause.combine(Cause.fail("error"), Cause.die("defect")))),
2108 Effect.uninterruptible,
2109 Effect.catchCause(() => Effect.void),
2110 Effect.forkChild({ startImmediately: true })
2111 )
2112 fiber.interruptUnsafe(123)
2113 yield* latch.open
2114 const exit = yield* Fiber.await(fiber)
2115 assert.isTrue(Exit.isFailure(exit))
2116 if (Exit.isFailure(exit)) {
2117 assert.isFalse(Cause.hasFails(exit.cause))
2118 assert.isTrue(Cause.hasDies(exit.cause))
2119 assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
2120 }
2121 }))
2122
2123 it.effect("uninterruptible handlers observe the interruption after skipped handlers", () =>
2124 Effect.gen(function*() {
2125 const latch = yield* Latch.make()
2126 let result: Exit.Exit<void> | undefined
2127 const fiber = yield* latch.await.pipe(
2128 Effect.andThen(Effect.fail("error")),
2129 Effect.uninterruptible,
2130 Effect.catch(() => Effect.void),
2131 Effect.interruptible,
2132 Effect.exit,
2133 Effect.map((exit) => {
2134 result = exit
2135 }),
2136 Effect.uninterruptible,
2137 Effect.forkChild({ startImmediately: true })
2138 )
2139 fiber.interruptUnsafe(123)
2140 yield* latch.open
2141 yield* Fiber.await(fiber)
2142 assert.isTrue(result !== undefined && Exit.isFailure(result) && Cause.hasInterruptsOnly(result.cause))
2143 }))
2144
2145 it.effect("handlers inside an uninterruptible region recover with interruption pending", () =>
2146 Effect.gen(function*() {
2147 const latch = yield* Latch.make()
2148 let recovered = false
2149 const fiber = yield* latch.await.pipe(
2150 Effect.andThen(Effect.fail("error")),
2151 Effect.catch(() =>
2152 Effect.sync(() => {
2153 recovered = true
2154 })
2155 ),
2156 Effect.uninterruptible,
2157 Effect.forkChild({ startImmediately: true })
2158 )
2159 fiber.interruptUnsafe(123)
2160 yield* latch.open
2161 const exit = yield* Fiber.await(fiber)
2162 assert.isTrue(recovered)
2163 assert.isTrue(Exit.hasInterrupts(exit))
2164 }))
2165
2166 it("a map callback that interrupts its own fiber skips the next map", () => {
2167 let ran = false
2168 const exit = Effect.runSyncExit(
2169 Effect.sync(() => 0).pipe(
2170 Effect.map(() => Fiber.getCurrent()!.interruptUnsafe()),
2171 Effect.map(() => {
2172 ran = true
2173 })
2174 )
2175 )
2176 assert.isFalse(ran)
2177 assert.isTrue(Exit.hasInterrupts(exit))
2178 })
2179
2180 it.effect("sync forever is interruptible", () =>
2181 Effect.gen(function*() {
2182 const fiber = yield* pipe(Effect.succeed(1), Effect.forever, Effect.forkChild)
2183 yield* Fiber.interrupt(fiber)
2184 assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
2185 }))
2186
2187 it.effect("interrupt of never is interrupted with cause", () =>
2188 Effect.gen(function*() {
2189 const fiber = yield* Effect.forkChild(Effect.never)
2190 yield* Fiber.interrupt(fiber)
2191 assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
2192 }))
2193
2194 it.effect("catch + ensuring + interrupt", () =>
2195 Effect.gen(function*() {
2196 let catchFailure = false
2197 let ensuring = false
2198 const handle = yield* Effect.never.pipe(
2199 Effect.catchCause((_) =>
2200 Effect.sync(() => {
2201 catchFailure = true
2202 })
2203 ),
2204 Effect.ensuring(Effect.sync(() => {
2205 ensuring = true
2206 })),
2207 Effect.forkChild({ startImmediately: true })
2208 )
2209 yield* Fiber.interrupt(handle)
2210 assert.isFalse(catchFailure)
2211 assert.isTrue(ensuring)
2212 }))
2213
2214 it.effect("run of interruptible", () =>
2215 Effect.gen(function*() {
2216 let recovered = false
2217 const fiber = yield* Effect.never.pipe(
2218 Effect.interruptible,
2219 Effect.exit,
2220 Effect.flatMap((result) =>
2221 Effect.sync(() => {
2222 recovered = result._tag === "Failure" && Cause.hasInterruptsOnly(result.cause)
2223 })
2224 ),
2225 Effect.uninterruptible,
2226 Effect.forkChild({ startImmediately: true })
2227 )
2228 yield* Fiber.interrupt(fiber)
2229 assert.isTrue(recovered)
2230 }))
2231
2232 it.effect("alternating interruptibility", () =>
2233 Effect.gen(function*() {
2234 let counter = 0
2235 const fiber = yield* Effect.never.pipe(
2236 Effect.interruptible,
2237 Effect.exit,
2238 Effect.andThen(Effect.sync(() => {
2239 counter++
2240 })),
2241 Effect.uninterruptible,
2242 Effect.interruptible,
2243 Effect.exit,
2244 Effect.andThen(Effect.sync(() => {
2245 counter++
2246 })),
2247 Effect.uninterruptible,
2248 Effect.forkChild({ startImmediately: true })
2249 )
2250 yield* Fiber.interrupt(fiber)
2251 assert.strictEqual(counter, 2)
2252 }))
2253
2254 it.live("acquireUseRelease use inherits interrupt status", () =>
2255 Effect.gen(function*() {
2256 let ref = false
2257 const fiber = yield* Effect.acquireUseRelease(
2258 Effect.succeed(123),
2259 (_) =>
2260 Effect.sync(() => {
2261 ref = true
2262 }).pipe(
2263 Effect.delay(10)
2264 ),
2265 () => Effect.void
2266 ).pipe(
2267 Effect.uninterruptible,
2268 Effect.forkChild({ startImmediately: true })
2269 )
2270 yield* Fiber.interrupt(fiber)
2271 assert.isTrue(ref)
2272 }))
2273
2274 it.effect("acquireUseRelease release runs when use is interrupted", () =>
2275 Effect.gen(function*() {
2276 let acquired = false
2277 let releaseExit: Exit.Exit<never> | undefined
2278 const fiber = yield* Effect.acquireUseRelease(
2279 Effect.sync(() => {
2280 acquired = true
2281 return 123
2282 }),
2283 () => Effect.never,
2284 (resource, exit) =>
2285 Effect.sync(() => {
2286 assert.strictEqual(resource, 123)
2287 releaseExit = exit
2288 })
2289 ).pipe(Effect.forkChild({ startImmediately: true }))
2290 yield* Fiber.interrupt(fiber)
2291 assert.isTrue(acquired)
2292 assert.isDefined(releaseExit)
2293 assert.isTrue(Exit.hasInterrupts(releaseExit!))
2294 assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
2295 }))
2296
2297 it.live("async can be uninterruptible", () =>
2298 Effect.gen(function*() {
2299 let ref = false
2300 const fiber = yield* Effect.sleep(10).pipe(
2301 Effect.andThen(Effect.sync(() => {
2302 ref = true
2303 })),
2304 Effect.uninterruptible,
2305 Effect.forkChild({ startImmediately: true })
2306 )
2307 yield* Fiber.interrupt(fiber)
2308 assert.isTrue(ref)
2309 }))
2310
2311 it.live("callback cannot resume on interrupt", () =>
2312 Effect.gen(function*() {
2313 const fiber = yield* Effect.callback<string>((resume) => {
2314 setTimeout(() => {
2315 resume(Effect.succeed("foo"))
2316 }, 10)
2317 }).pipe(
2318 Effect.onInterrupt(() => Effect.sleep(30)),
2319 Effect.forkChild({ startImmediately: true })
2320 )
2321 yield* Fiber.interrupt(fiber)
2322 assert.isTrue(Exit.hasInterrupts(fiber.pollUnsafe()!))
2323 }))
2324
2325 it.live("closing scope is uninterruptible", () =>
2326 Effect.gen(function*() {
2327 let ref = false
2328 const child = pipe(
2329 Effect.sleep(10),
2330 Effect.andThen(Effect.sync(() => {
2331 ref = true
2332 }))
2333 )
2334 const fiber = yield* child.pipe(Effect.uninterruptible, Effect.forkChild({ startImmediately: true }))
2335 yield* Fiber.interrupt(fiber)
2336 assert.isTrue(ref)
2337 }))
2338
2339 it.effect("AbortSignal is aborted", () =>
2340 Effect.gen(function*() {
2341 let signal: AbortSignal
2342 const fiber = yield* Effect.callback<void>((_cb, signal_) => {
2343 signal = signal_
2344 }).pipe(Effect.forkChild({ startImmediately: true }))
2345 yield* Fiber.interrupt(fiber)
2346 assert.strictEqual(signal!.aborted, true)
2347 }))
2348
2349 it.effect("callback cleanup effect runs on interrupt", () =>
2350 Effect.gen(function*() {
2351 let cleanedUp = false
2352 const fiber = yield* Effect.callback<void>((_resume) =>
2353 Effect.sync(() => {
2354 cleanedUp = true
2355 })
2356 ).pipe(Effect.forkChild({ startImmediately: true }))
2357 yield* Fiber.interrupt(fiber)
2358 assert.isTrue(cleanedUp)
2359 }))
2360
2361 describe("uninterruptibleMask", () => {
2362 it.effect("defers a pending interrupt until the masked region completes", () =>
2363 Effect.gen(function*() {
2364 const masked = yield* Latch.make()
2365 const resume = yield* Latch.make()
2366 const events: Array<string> = []
2367
2368 const child = yield* Effect.uninterruptibleMask(() =>
2369 Effect.gen(function*() {
2370 yield* masked.open
2371 yield* resume.await
2372 events.push("masked region completed")
2373 })
2374 ).pipe(Effect.forkChild({ startImmediately: true }))
2375
2376 yield* masked.await
2377 events.push("masked")
2378
2379 yield* Effect.sync(() => {
2380 child.interruptUnsafe(123)
2381 events.push("interrupted")
2382 })
2383 assert.isUndefined(child.pollUnsafe())
2384
2385 yield* resume.open
2386 events.push("resumed")
2387 yield* Effect.yieldNow
2388 yield* Effect.yieldNow
2389
2390 const exit = child.pollUnsafe()
2391 if (exit === undefined) {
2392 return assert.fail("fiber did not exit after the masked region completed")
2393 }
2394 assert.isTrue(Exit.hasInterrupts(exit))
2395 if (exit._tag !== "Failure") {
2396 return assert.fail("expected interrupted fiber to exit with failure")
2397 }
2398 assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
2399 assert.deepStrictEqual(events, ["masked", "interrupted", "resumed", "masked region completed"])
2400 }))
2401
2402 it.effect("delivers a pending interrupt when restore re-enables interruptibility", () =>
2403 Effect.gen(function*() {
2404 const masked = yield* Latch.make()
2405 const resume = yield* Latch.make()
2406 const events: Array<string> = []
2407
2408 const child = yield* Effect.uninterruptibleMask((restore) =>
2409 Effect.gen(function*() {
2410 yield* masked.open
2411 yield* resume.await
2412 // the pending interrupt is delivered at the restore boundary, so
2413 // the restored effect never runs
2414 return yield* restore(Effect.suspend(() => {
2415 events.push("restored")
2416 return Effect.never
2417 }))
2418 })
2419 ).pipe(Effect.forkChild({ startImmediately: true }))
2420
2421 yield* masked.await
2422 events.push("masked")
2423
2424 yield* Effect.sync(() => {
2425 child.interruptUnsafe(123)
2426 events.push("interrupted")
2427 })
2428 assert.isUndefined(child.pollUnsafe())
2429
2430 yield* resume.open
2431 events.push("resumed")
2432 yield* Effect.yieldNow
2433 yield* Effect.yieldNow
2434
2435 const exit = child.pollUnsafe()
2436 if (exit === undefined) {
2437 return assert.fail("fiber did not exit after restore re-enabled interruptibility")
2438 }
2439 assert.isTrue(Exit.hasInterrupts(exit))
2440 if (exit._tag !== "Failure") {
2441 return assert.fail("expected interrupted fiber to exit with failure")
2442 }
2443 assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
2444 assert.deepStrictEqual(events, ["masked", "interrupted", "resumed"])
2445 }))
2446
2447 it.effect("region after a restored section stays uninterruptible", () =>
2448 Effect.gen(function*() {
2449 const afterRestore = yield* Latch.make()
2450 const resume = yield* Latch.make()
2451 const events: Array<string> = []
2452
2453 const child = yield* Effect.uninterruptibleMask((restore) =>
2454 Effect.gen(function*() {
2455 yield* restore(Effect.sync(() => {
2456 events.push("restored section completed")
2457 }))
2458 yield* afterRestore.open
2459 yield* resume.await
2460 events.push("masked region completed")
2461 })
2462 ).pipe(Effect.forkChild({ startImmediately: true }))
2463
2464 yield* afterRestore.await
2465 events.push("after restore")
2466
2467 yield* Effect.sync(() => {
2468 child.interruptUnsafe(123)
2469 events.push("interrupted")
2470 })
2471 assert.isUndefined(child.pollUnsafe())
2472
2473 yield* resume.open
2474 events.push("resumed")
2475 yield* Effect.yieldNow
2476 yield* Effect.yieldNow
2477
2478 const exit = child.pollUnsafe()
2479 if (exit === undefined) {
2480 return assert.fail("fiber did not exit after the masked region completed")
2481 }
2482 assert.isTrue(Exit.hasInterrupts(exit))
2483 if (exit._tag !== "Failure") {
2484 return assert.fail("expected interrupted fiber to exit with failure")
2485 }
2486 assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
2487 assert.deepStrictEqual(events, [
2488 "restored section completed",
2489 "after restore",
2490 "interrupted",
2491 "resumed",
2492 "masked region completed"
2493 ])
2494 }))
2495 })
2496 })
2497
2498 describe("awaitAllChildren", () => {
2499 it.effect("completes immediately when no children are forked", () =>
2500 Effect.gen(function*() {
2501 const result = yield* Effect.succeed(1).pipe(Effect.awaitAllChildren)
2502 assert.strictEqual(result, 1)
2503 }))
2504
2505 it.effect("awaits children forked by the wrapped effect", () =>
2506 Effect.gen(function*() {
2507 const latch = yield* Deferred.make<void>()
2508 const fiber = yield* Effect.gen(function*() {
2509 yield* Deferred.await(latch).pipe(Effect.forkChild)
2510 return 1
2511 }).pipe(
2512 Effect.awaitAllChildren,
2513 Effect.forkChild({ startImmediately: true })
2514 )
2515 yield* Effect.yieldNow
2516 assert.strictEqual(fiber.pollUnsafe(), undefined)
2517 yield* Deferred.succeed(latch, void 0)
2518 const result = yield* Fiber.join(fiber)
2519 assert.strictEqual(result, 1)
2520 }))
2521
2522 it.effect("can be interrupted while awaiting children after success", () =>
2523 Effect.gen(function*() {
2524 const childStarted = yield* Deferred.make<void>()
2525 const childFiber = yield* Deferred.make<Fiber.Fiber<void>>()
2526 const releaseChild = yield* Deferred.make<void>()
2527 const fiber = yield* Effect.gen(function*() {
2528 const child = yield* Effect.gen(function*() {
2529 yield* Deferred.succeed(childStarted, void 0)
2530 yield* Deferred.await(releaseChild)
2531 }).pipe(Effect.forkChild({ startImmediately: true }))
2532 yield* Deferred.succeed(childFiber, child)
2533 return 1
2534 }).pipe(
2535 Effect.awaitAllChildren,
2536 Effect.forkChild({ startImmediately: true })
2537 )
2538
2539 const child = yield* Deferred.await(childFiber)
2540 yield* Deferred.await(childStarted)
2541 fiber.interruptUnsafe()
2542 yield* Effect.yieldNow
2543 yield* Effect.yieldNow
2544 const exit = fiber.pollUnsafe()
2545 const childExit = child.pollUnsafe()
2546
2547 yield* Deferred.succeed(releaseChild, void 0)
2548 yield* Fiber.await(fiber)
2549
2550 assert(exit !== undefined && Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause))
2551 assert(childExit !== undefined && Exit.isFailure(childExit) && Cause.hasInterruptsOnly(childExit.cause))
2552 }))
2553
2554 it.effect("can be interrupted while awaiting children after failure", () =>
2555 Effect.gen(function*() {
2556 const childStarted = yield* Deferred.make<void>()
2557 const childFiber = yield* Deferred.make<Fiber.Fiber<void>>()
2558 const releaseChild = yield* Deferred.make<void>()
2559 const fiber = yield* Effect.gen(function*() {
2560 const child = yield* Effect.gen(function*() {
2561 yield* Deferred.succeed(childStarted, void 0)
2562 yield* Deferred.await(releaseChild)
2563 }).pipe(Effect.forkChild({ startImmediately: true }))
2564 yield* Deferred.succeed(childFiber, child)
2565 return yield* Effect.fail("boom")
2566 }).pipe(
2567 Effect.awaitAllChildren,
2568 Effect.forkChild({ startImmediately: true })
2569 )
2570
2571 const child = yield* Deferred.await(childFiber)
2572 yield* Deferred.await(childStarted)
2573 fiber.interruptUnsafe()
2574 yield* Effect.yieldNow
2575 yield* Effect.yieldNow
2576 const exit = fiber.pollUnsafe()
2577 const childExit = child.pollUnsafe()
2578
2579 yield* Deferred.succeed(releaseChild, void 0)
2580 yield* Fiber.await(fiber)
2581
2582 assert(exit !== undefined && Exit.isFailure(exit))
2583 assert.isTrue(Cause.hasInterrupts(exit.cause))
2584 assert.deepStrictEqual(Cause.findError(exit.cause), Result.succeed("boom"))
2585 assert(childExit !== undefined && Exit.isFailure(childExit) && Cause.hasInterruptsOnly(childExit.cause))
2586 }))
2587
2588 it.effect("respects an enclosing uninterruptible region while awaiting children", () =>
2589 Effect.gen(function*() {
2590 const childStarted = yield* Deferred.make<void>()
2591 const childFiber = yield* Deferred.make<Fiber.Fiber<void>>()
2592 const releaseChild = yield* Deferred.make<void>()
2593 const fiber = yield* Effect.gen(function*() {
2594 const child = yield* Effect.gen(function*() {
2595 yield* Deferred.succeed(childStarted, void 0)
2596 yield* Deferred.await(releaseChild)
2597 }).pipe(Effect.forkChild({ startImmediately: true }))
2598 yield* Deferred.succeed(childFiber, child)
2599 return 1
2600 }).pipe(
2601 Effect.awaitAllChildren,
2602 Effect.uninterruptible,
2603 Effect.forkChild({ startImmediately: true })
2604 )
2605
2606 const child = yield* Deferred.await(childFiber)
2607 yield* Deferred.await(childStarted)
2608 fiber.interruptUnsafe()
2609 yield* Effect.yieldNow
2610 yield* Effect.yieldNow
2611
2612 assert.strictEqual(fiber.pollUnsafe(), undefined)
2613 assert.strictEqual(child.pollUnsafe(), undefined)
2614
2615 yield* Deferred.succeed(releaseChild, void 0)
2616 const exit = yield* Fiber.await(fiber)
2617
2618 assert(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause))
2619 }))
2620
2621 it.effect("does not await children forked outside the wrapped effect", () =>
2622 Effect.gen(function*() {
2623 const preexisting = yield* Effect.never.pipe(Effect.forkChild({ startImmediately: true }))
2624 const fiber = yield* Effect.succeed(1).pipe(
2625 Effect.awaitAllChildren,
2626 Effect.forkChild({ startImmediately: true })
2627 )
2628 yield* Effect.yieldNow
2629 assert.deepStrictEqual(fiber.pollUnsafe(), Exit.succeed(1))
2630 yield* Fiber.interrupt(preexisting)
2631 }))
2632
2633 it.effect("does not await preexisting children", () =>
2634 Effect.gen(function*() {
2635 const preexisting = yield* Effect.never.pipe(Effect.forkChild({ startImmediately: true }))
2636 const result = yield* Effect.succeed(1).pipe(Effect.awaitAllChildren)
2637 assert.strictEqual(result, 1)
2638 yield* Fiber.interrupt(preexisting)
2639 }))
2640
2641 it.effect("does not await preexisting children in the same fiber", () =>
2642 Effect.gen(function*() {
2643 const preexistingLatch = yield* Deferred.make<void>()
2644 const scopedLatch = yield* Deferred.make<void>()
2645 const fiber = yield* Effect.gen(function*() {
2646 yield* Deferred.await(preexistingLatch).pipe(Effect.forkChild)
2647 return yield* Effect.gen(function*() {
2648 yield* Deferred.await(scopedLatch).pipe(Effect.forkChild)
2649 return 1
2650 }).pipe(Effect.awaitAllChildren)
2651 }).pipe(
2652 Effect.forkChild({ startImmediately: true })
2653 )
2654 yield* Effect.yieldNow
2655 assert.strictEqual(fiber.pollUnsafe(), undefined)
2656 yield* Deferred.succeed(scopedLatch, void 0)
2657 yield* Effect.yieldNow
2658 assert.deepStrictEqual(fiber.pollUnsafe(), Exit.succeed(1))
2659 }))
2660 })
2661
2662 describe("fork", () => {
2663 it.effect("is interrupted with parent", () =>
2664 Effect.gen(function*() {
2665 let child = false
2666 let parent = false
2667 const fiber = yield* Effect.never.pipe(
2668 Effect.onInterrupt(() =>
2669 Effect.sync(() => {
2670 child = true
2671 })
2672 ),
2673 Effect.forkChild({ startImmediately: true }),
2674 Effect.andThen(Effect.never),
2675 Effect.onInterrupt(() =>
2676 Effect.sync(() => {
2677 parent = true
2678 })
2679 ),
2680 Effect.forkChild({ startImmediately: true })
2681 )
2682 yield* Fiber.interrupt(fiber)
2683 assert.isTrue(child)
2684 assert.isTrue(parent)
2685 }))
2686 })
2687
2688 describe("forkDaemon", () => {
2689 it.effect("is not interrupted with parent", () =>
2690 Effect.gen(function*() {
2691 let child = false
2692 let parent = false
2693 const handle = yield* Effect.never.pipe(
2694 Effect.onInterrupt(() =>
2695 Effect.sync(() => {
2696 child = true
2697 })
2698 ),
2699 Effect.forkDetach,
2700 Effect.andThen(Effect.never),
2701 Effect.onInterrupt(() =>
2702 Effect.sync(() => {
2703 parent = true
2704 })
2705 ),
2706 Effect.forkChild({ startImmediately: true })
2707 )
2708 yield* Fiber.interrupt(handle)
2709 assert.isFalse(child)
2710 assert.isTrue(parent)
2711 }))
2712 })
2713
2714 describe("forkIn", () => {
2715 it.effect("is interrupted when scope is closed", () =>
2716 Effect.gen(function*() {
2717 let interrupted = false
2718 const scope = yield* Scope.make()
2719 yield* Effect.never.pipe(
2720 Effect.onInterrupt(() =>
2721 Effect.sync(() => {
2722 interrupted = true
2723 })
2724 ),
2725 Effect.forkIn(scope, { startImmediately: true })
2726 )
2727 yield* Scope.close(scope, Exit.void)
2728 assert.isTrue(interrupted)
2729 }))
2730 })
2731
2732 describe("forkScoped", () => {
2733 it.effect("is interrupted when scope is closed", () =>
2734 Effect.gen(function*() {
2735 let interrupted = false
2736 const scope = yield* Scope.make()
2737 yield* Effect.never.pipe(
2738 Effect.onInterrupt(() =>
2739 Effect.sync(() => {
2740 interrupted = true
2741 })
2742 ),
2743 Effect.forkScoped({ startImmediately: true }),
2744 Scope.provide(scope)
2745 )
2746 yield* Scope.close(scope, Exit.void)
2747 assert.isTrue(interrupted)
2748 }))
2749 })
2750
2751 describe("do notation", () => {
2752 it.effect("works", () =>
2753 Effect.succeed(1).pipe(
2754 Effect.bindTo("a"),
2755 Effect.let("b", ({ a }) => a + 1),
2756 Effect.bind("b", ({ b }) => Effect.succeed(b.toString())),
2757 Effect.tap((value) =>
2758 Effect.sync(() => {
2759 assert.deepStrictEqual(value, {
2760 a: 1,
2761 b: "2"
2762 })
2763 })
2764 )
2765 ))
2766 })
2767
2768 describe("stack safety", () => {
2769 it.live("recursion", () => {
2770 const loop: Effect.Effect<void> = Effect.void.pipe(
2771 Effect.flatMap((_) => loop)
2772 )
2773 return loop.pipe(
2774 Effect.timeoutOption(50)
2775 )
2776 })
2777
2778 it("lazily nested map continuations", () => {
2779 const loop = (n: number): Effect.Effect<number> =>
2780 n === 0 ? Effect.succeed(0) : Effect.map(Effect.suspend(() => loop(n - 1)), (n) => n + 1)
2781 assert.strictEqual(Effect.runSync(loop(100_000)), 100_000)
2782 })
2783 })
2784
2785 describe("finalization", () => {
2786 const ExampleError = new Error("Oh noes!")
2787
2788 const throwing = (): never => {
2789 throw "finalizer defect"
2790 }
2791
2792 it.effect("onExit preserves the original failure when the finalizer throws", () =>
2793 Effect.gen(function*() {
2794 const result = yield* Effect.fail("body failure").pipe(Effect.onExit(throwing), Effect.exit)
2795 assert.deepStrictEqual(
2796 result,
2797 Exit.failCause(Cause.combine(Cause.fail("body failure"), Cause.die("finalizer defect")))
2798 )
2799 }))
2800
2801 it.effect("onExit reports a thrown finalizer on success", () =>
2802 Effect.gen(function*() {
2803 const result = yield* Effect.succeed(1).pipe(Effect.onExit(throwing), Effect.exit)
2804 assert.deepStrictEqual(result, Exit.die("finalizer defect"))
2805 }))
2806
2807 it.effect("an outer onInterrupt still sees the interruption after an inner finalizer throws", () =>
2808 Effect.gen(function*() {
2809 let finalized = false
2810 const result = yield* Effect.interrupt.pipe(
2811 Effect.onExit(throwing),
2812 Effect.onInterrupt(() =>
2813 Effect.sync(() => {
2814 finalized = true
2815 })
2816 ),
2817 Effect.exit
2818 )
2819 assert.isTrue(finalized)
2820 assert.isTrue(Exit.isFailure(result))
2821 if (Exit.isFailure(result)) {
2822 assert.deepStrictEqual(result.cause.reasons.map((reason) => reason._tag), ["Interrupt", "Die"])
2823 assert.deepStrictEqual(result.cause.reasons[1], Cause.die("finalizer defect").reasons[0])
2824 }
2825 }))
2826
2827 it("nested throwing finalizers do not overflow the stack", () => {
2828 let program: Effect.Effect<unknown> = Effect.succeed(1)
2829 for (let i = 0; i < 20_000; i++) {
2830 program = Effect.onExit(Effect.exit(program), throwing)
2831 }
2832 assert.deepStrictEqual(Effect.runSyncExit(program), Exit.die("finalizer defect"))
2833 })
2834
2835 it.effect("fail ensuring", () =>
2836 Effect.gen(function*() {
2837 let finalized = false
2838 const result = yield* Effect.fail(ExampleError).pipe(
2839 Effect.ensuring(Effect.sync(() => {
2840 finalized = true
2841 })),
2842 Effect.exit
2843 )
2844 assert.deepStrictEqual(result, Exit.fail(ExampleError))
2845 assert.isTrue(finalized)
2846 }))
2847
2848 it.effect("fail on error", () =>
2849 Effect.gen(function*() {
2850 let finalized = false
2851 const result = yield* Effect.fail(ExampleError).pipe(
2852 Effect.onError(() =>
2853 Effect.sync(() => {
2854 finalized = true
2855 })
2856 ),
2857 Effect.exit
2858 )
2859 assert.deepStrictEqual(result, Exit.fail(ExampleError))
2860 assert.isTrue(finalized)
2861 }))
2862
2863 it.effect("finalizer errors merged", () =>
2864 Effect.gen(function*() {
2865 const e2 = new Error("e2")
2866 const e3 = new Error("e3")
2867 const result = yield* pipe(
2868 Effect.fail(ExampleError),
2869 Effect.ensuring(Effect.die(e2)),
2870 Effect.ensuring(Effect.die(e3)),
2871 Effect.sandbox,
2872 Effect.flip,
2873 Effect.map((cause) => cause)
2874 )
2875 assert.deepStrictEqual(
2876 result,
2877 Cause.combine(Cause.combine(Cause.fail(ExampleError), Cause.die(e2)), Cause.die(e3))
2878 )
2879 }))
2880
2881 it.effect("finalizer errors reported", () =>
2882 Effect.gen(function*() {
2883 let reported: Exit.Exit<number> | undefined
2884 const result = yield* pipe(
2885 Effect.succeed(42),
2886 Effect.ensuring(Effect.die(ExampleError)),
2887 Effect.forkChild,
2888 Effect.flatMap((fiber) =>
2889 pipe(
2890 Fiber.await(fiber),
2891 Effect.flatMap((e) =>
2892 Effect.sync(() => {
2893 reported = e
2894 })
2895 )
2896 )
2897 )
2898 )
2899 assert.isUndefined(result)
2900 assert.isFalse(reported !== undefined && Exit.isSuccess(reported))
2901 }))
2902
2903 it.effect("scoped combines usage and finalizer failures", () =>
2904 Effect.gen(function*() {
2905 const result = yield* Effect.gen(function*() {
2906 yield* Effect.addFinalizer(() => Effect.die("finalizer failure"))
2907 return yield* Effect.fail("usage failure")
2908 }).pipe(Effect.scoped, Effect.exit)
2909 assert.deepStrictEqual(
2910 result,
2911 Exit.failCause(Cause.combine(Cause.fail("usage failure"), Cause.die("finalizer failure")))
2912 )
2913 }))
2914
2915 it.effect("acquireUseRelease usage result", () =>
2916 Effect.gen(function*() {
2917 const result = yield* Effect.acquireUseRelease(
2918 Effect.void,
2919 () => Effect.succeed(42),
2920 () => Effect.void
2921 )
2922 assert.strictEqual(result, 42)
2923 }))
2924
2925 it.effect("error in just acquisition", () =>
2926 Effect.gen(function*() {
2927 const result = yield* pipe(
2928 Effect.acquireUseRelease(
2929 Effect.fail(ExampleError),
2930 () => Effect.void,
2931 () => Effect.void
2932 ),
2933 Effect.exit
2934 )
2935 assert.deepStrictEqual(result, Exit.fail(ExampleError))
2936 }))
2937
2938 it.effect("error in just release", () =>
2939 Effect.gen(function*() {
2940 const result = yield* pipe(
2941 Effect.acquireUseRelease(
2942 Effect.void,
2943 () => Effect.void,
2944 () => Effect.die(ExampleError)
2945 ),
2946 Effect.exit
2947 )
2948 assert.deepStrictEqual(result, Exit.die(ExampleError))
2949 }))
2950
2951 it.effect("error in just usage", () =>
2952 Effect.gen(function*() {
2953 const result = yield* pipe(
2954 Effect.acquireUseRelease(
2955 Effect.void,
2956 () => Effect.fail(ExampleError),
2957 () => Effect.void
2958 ),
2959 Effect.exit
2960 )
2961 assert.deepStrictEqual(result, Exit.fail(ExampleError))
2962 }))
2963
2964 it.effect("combines usage and release failures", () =>
2965 Effect.gen(function*() {
2966 const result = yield* Effect.acquireUseRelease(
2967 Effect.void,
2968 () => Effect.fail("usage failure"),
2969 () => Effect.fail("release failure")
2970 ).pipe(Effect.exit)
2971 assert.deepStrictEqual(
2972 result,
2973 Exit.failCause(Cause.combine(Cause.fail("usage failure"), Cause.fail("release failure")))
2974 )
2975 }))
2976
2977 it.effect("rethrown caught error in acquisition", () =>
2978 Effect.gen(function*() {
2979 const result = yield* Effect.acquireUseRelease(
2980 Effect.fail(ExampleError),
2981 () => Effect.void,
2982 () => Effect.void
2983 ).pipe(Effect.flip)
2984 assert.deepEqual(result, ExampleError)
2985 }))
2986
2987 it.effect("rethrown caught error in release", () =>
2988 Effect.gen(function*() {
2989 const result = yield* pipe(
2990 Effect.acquireUseRelease(
2991 Effect.void,
2992 () => Effect.void,
2993 () => Effect.die(ExampleError)
2994 ),
2995 Effect.exit
2996 )
2997 assert.deepStrictEqual(result, Exit.die(ExampleError))
2998 }))
2999
3000 it.effect("rethrown caught error in usage", () =>
3001 Effect.gen(function*() {
3002 const result = yield* Effect.acquireUseRelease(
3003 Effect.void,
3004 () => Effect.fail(ExampleError),
3005 () => Effect.void
3006 ).pipe(Effect.exit)
3007 assert.deepEqual(result, Exit.fail(ExampleError))
3008 }))
3009
3010 it.effect("onResult - ensures that a cleanup function runs when an effect fails", () =>
3011 Effect.gen(function*() {
3012 let ref = false
3013 yield* Effect.die("boom").pipe(
3014 Effect.onExit((result) =>
3015 Exit.hasDies(result) ?
3016 Effect.sync(() => {
3017 ref = true
3018 }) :
3019 Effect.void
3020 ),
3021 Effect.sandbox,
3022 Effect.ignore
3023 )
3024 assert.isTrue(ref)
3025 }))
3026
3027 it.effect("onExit - callback observes interrupt exit when the fiber is interrupted", () =>
3028 Effect.gen(function*() {
3029 let observedInterrupt = false
3030 const fiber = yield* Effect.never.pipe(
3031 Effect.onExit((exit) =>
3032 Effect.sync(() => {
3033 observedInterrupt = Exit.hasInterrupts(exit)
3034 })
3035 ),
3036 Effect.forkChild({ startImmediately: true })
3037 )
3038 yield* Fiber.interrupt(fiber)
3039 assert.isTrue(observedInterrupt)
3040 }))
3041 })
3042
3043 describe("Effect.ignore", () => {
3044 type IgnoreOptions = { readonly log?: boolean | LogLevel.Severity; readonly message?: string }
3045
3046 const makeTestLogger = () => {
3047 const capturedLogs: Array<{
3048 readonly logLevel: LogLevel.LogLevel
3049 readonly cause: Cause.Cause<unknown>
3050 readonly message: unknown
3051 }> = []
3052 const testLogger = Logger.make<unknown, void>((options) => {
3053 capturedLogs.push({ logLevel: options.logLevel, cause: options.cause, message: options.message })
3054 })
3055 return { capturedLogs, testLogger }
3056 }
3057
3058 const runIgnore = (options?: IgnoreOptions, currentLogLevel: LogLevel.Severity = "Info") =>
3059 Effect.gen(function*() {
3060 const { capturedLogs, testLogger } = makeTestLogger()
3061 const program = options === undefined
3062 ? Effect.fail("boom").pipe(Effect.ignore)
3063 : Effect.fail("boom").pipe(Effect.ignore(options))
3064 yield* program.pipe(
3065 Effect.provide(Logger.layer([testLogger])),
3066 Effect.provideService(References.MinimumLogLevel, "Trace"),
3067 Effect.provideService(References.CurrentLogLevel, currentLogLevel)
3068 )
3069 return capturedLogs
3070 })
3071
3072 it.effect("does not log when log is omitted", () =>
3073 Effect.gen(function*() {
3074 const logs = yield* runIgnore()
3075 assert.strictEqual(logs.length, 0)
3076 }))
3077
3078 it.effect("does not log when log is false", () =>
3079 Effect.gen(function*() {
3080 const logs = yield* runIgnore({ log: false })
3081 assert.strictEqual(logs.length, 0)
3082 }))
3083
3084 it.effect("logs with the current level when log is true", () =>
3085 Effect.gen(function*() {
3086 const logs = yield* runIgnore({ log: true }, "Warn")
3087 assert.strictEqual(logs.length, 1)
3088 assert.strictEqual(logs[0].logLevel, "Warn")
3089 assertCauseFail(logs[0].cause, "boom")
3090 }))
3091
3092 it.effect("logs with the provided level when log is a LogLevel", () =>
3093 Effect.gen(function*() {
3094 const logs = yield* runIgnore({ log: "Error" }, "Warn")
3095 assert.strictEqual(logs.length, 1)
3096 assert.strictEqual(logs[0].logLevel, "Error")
3097 assertCauseFail(logs[0].cause, "boom")
3098 }))
3099
3100 it.effect("prepends the provided message when logging", () =>
3101 Effect.gen(function*() {
3102 const logs = yield* runIgnore({ log: true, message: "Ignoring failure" })
3103 assert.strictEqual(logs.length, 1)
3104 assert.deepStrictEqual(logs[0].message, ["Ignoring failure"])
3105 assertCauseFail(logs[0].cause, "boom")
3106 }))
3107 })
3108
3109 describe("Effect.ignoreCause", () => {
3110 type IgnoreCauseOptions = { readonly log?: boolean | LogLevel.Severity; readonly message?: string }
3111
3112 const makeTestLogger = () => {
3113 const capturedLogs: Array<{
3114 readonly logLevel: LogLevel.LogLevel
3115 readonly cause: Cause.Cause<unknown>
3116 readonly message: unknown
3117 }> = []
3118 const testLogger = Logger.make<unknown, void>((options) => {
3119 capturedLogs.push({ logLevel: options.logLevel, cause: options.cause, message: options.message })
3120 })
3121 return { capturedLogs, testLogger }
3122 }
3123
3124 const runIgnoreCause = (options?: IgnoreCauseOptions, currentLogLevel: LogLevel.Severity = "Info") =>
3125 Effect.gen(function*() {
3126 const { capturedLogs, testLogger } = makeTestLogger()
3127 const program = options === undefined
3128 ? Effect.fail("boom").pipe(Effect.ignoreCause)
3129 : Effect.fail("boom").pipe(Effect.ignoreCause(options))
3130 yield* program.pipe(
3131 Effect.provide(Logger.layer([testLogger])),
3132 Effect.provideService(References.MinimumLogLevel, "Trace"),
3133 Effect.provideService(References.CurrentLogLevel, currentLogLevel)
3134 )
3135 return capturedLogs
3136 })
3137
3138 it.effect("ignores defects", () =>
3139 Effect.gen(function*() {
3140 const exit = yield* Effect.die("boom").pipe(Effect.ignoreCause, Effect.exit)
3141 assert.deepStrictEqual(exit, Exit.void)
3142 }))
3143
3144 it.effect("ignores interrupts", () =>
3145 Effect.gen(function*() {
3146 const ignored = yield* Effect.interrupt.pipe(Effect.ignoreCause, Effect.exit)
3147 assert.deepStrictEqual(ignored, Exit.void)
3148 }))
3149
3150 it.effect("does not log when log is omitted", () =>
3151 Effect.gen(function*() {
3152 const logs = yield* runIgnoreCause()
3153 assert.strictEqual(logs.length, 0)
3154 }))
3155
3156 it.effect("does not log when log is false", () =>
3157 Effect.gen(function*() {
3158 const logs = yield* runIgnoreCause({ log: false })
3159 assert.strictEqual(logs.length, 0)
3160 }))
3161
3162 it.effect("logs with the current level when log is true", () =>
3163 Effect.gen(function*() {
3164 const logs = yield* runIgnoreCause({ log: true }, "Warn")
3165 assert.strictEqual(logs.length, 1)
3166 assert.strictEqual(logs[0].logLevel, "Warn")
3167 assertCauseFail(logs[0].cause, "boom")
3168 }))
3169
3170 it.effect("logs with the provided level when log is a LogLevel", () =>
3171 Effect.gen(function*() {
3172 const logs = yield* runIgnoreCause({ log: "Error" }, "Warn")
3173 assert.strictEqual(logs.length, 1)
3174 assert.strictEqual(logs[0].logLevel, "Error")
3175 assertCauseFail(logs[0].cause, "boom")
3176 }))
3177
3178 it.effect("prepends the provided message when logging", () =>
3179 Effect.gen(function*() {
3180 const logs = yield* runIgnoreCause({ log: true, message: "Ignoring cause" })
3181 assert.strictEqual(logs.length, 1)
3182 assert.deepStrictEqual(logs[0].message, ["Ignoring cause"])
3183 assertCauseFail(logs[0].cause, "boom")
3184 }))
3185 })
3186
3187 describe("error handling", () => {
3188 class ErrorA extends Data.TaggedError("A") {}
3189 class ErrorB extends Data.TaggedError("B") {}
3190 class ErrorC extends Data.Error {}
3191
3192 it.effect("catchTag", () =>
3193 Effect.gen(function*() {
3194 let error: ErrorA | ErrorB | ErrorC = new ErrorA()
3195 const effect = Effect.failSync(() => error).pipe(
3196 Effect.catchTag("A", (_) => Effect.succeed(1)),
3197 Effect.catchTag("B", (_) => Effect.succeed(2)),
3198 Effect.orElseSucceed(() => 3)
3199 )
3200 assert.strictEqual(yield* effect, 1)
3201 error = new ErrorB()
3202 assert.strictEqual(yield* effect, 2)
3203 error = new ErrorC()
3204 assert.strictEqual(yield* effect, 3)
3205 }))
3206
3207 it.effect("catchTag orElse", () =>
3208 Effect.gen(function*() {
3209 let error: ErrorA | ErrorB | ErrorC = new ErrorA()
3210 const effect = Effect.failSync(() => error).pipe(
3211 Effect.catchTag(["A", "B"], (_) => Effect.succeed(1), (_) => {
3212 return Effect.succeed(2)
3213 })
3214 )
3215 assert.strictEqual(yield* effect, 1)
3216 error = new ErrorB()
3217 assert.strictEqual(yield* effect, 1)
3218 error = new ErrorC()
3219 assert.strictEqual(yield* effect, 2)
3220 }))
3221
3222 it.effect("catchTags orElse", () =>
3223 Effect.gen(function*() {
3224 let error: ErrorA | ErrorB | ErrorC = new ErrorA()
3225 const effect = Effect.failSync(() => error).pipe(
3226 Effect.catchTags(
3227 {
3228 A: (_) => Effect.succeed(1),
3229 B: (_) => Effect.succeed(2)
3230 },
3231 (_) => Effect.succeed(3)
3232 )
3233 )
3234 assert.strictEqual(yield* effect, 1)
3235 error = new ErrorB()
3236 assert.strictEqual(yield* effect, 2)
3237 error = new ErrorC()
3238 assert.strictEqual(yield* effect, 3)
3239 }))
3240
3241 it.effect("tapErrorTag", () =>
3242 Effect.gen(function*() {
3243 let error: ErrorA | ErrorB | ErrorC = new ErrorA()
3244 const tapped: Array<string> = []
3245 const effect = Effect.failSync(() => error).pipe(
3246 Effect.tapErrorTag("A", () =>
3247 Effect.sync(() => {
3248 tapped.push("A")
3249 })),
3250 Effect.tapErrorTag("B", () =>
3251 Effect.sync(() => {
3252 tapped.push("B")
3253 })),
3254 Effect.exit
3255 )
3256 assert.deepStrictEqual(yield* effect, Exit.fail(error))
3257 assert.deepStrictEqual(tapped, ["A"])
3258
3259 tapped.length = 0
3260 error = new ErrorB()
3261 assert.deepStrictEqual(yield* effect, Exit.fail(error))
3262 assert.deepStrictEqual(tapped, ["B"])
3263
3264 tapped.length = 0
3265 error = new ErrorC()
3266 assert.deepStrictEqual(yield* effect, Exit.fail(error))
3267 assert.deepStrictEqual(tapped, [])
3268 }))
3269
3270 it.effect("catchIf with refinement", () =>
3271 Effect.gen(function*() {
3272 interface ErrorA {
3273 readonly _tag: "ErrorA"
3274 }
3275 interface ErrorB {
3276 readonly _tag: "ErrorB"
3277 }
3278 const effect: Effect.Effect<never, ErrorA | ErrorB> = Effect.fail({ _tag: "ErrorB" as const })
3279 const result = yield* pipe(
3280 effect,
3281 Effect.catchIf((e): e is ErrorA => e._tag === "ErrorA", Effect.succeed),
3282 Effect.exit
3283 )
3284 assert.deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" as const }))
3285 }))
3286
3287 it.effect("catchIf with refinement orElse", () =>
3288 Effect.gen(function*() {
3289 interface ErrorA {
3290 readonly _tag: "ErrorA"
3291 }
3292 interface ErrorB {
3293 readonly _tag: "ErrorB"
3294 }
3295 const effect: Effect.Effect<never, ErrorA | ErrorB> = Effect.fail({ _tag: "ErrorB" as const })
3296 const result = yield* pipe(
3297 effect,
3298 Effect.catchIf((e): e is ErrorA => e._tag === "ErrorA", Effect.succeed, (_) => {
3299 return Effect.succeed(1)
3300 })
3301 )
3302 assert.deepStrictEqual(result, 1)
3303 }))
3304
3305 it.effect("catchNoSuchElement", () =>
3306 Effect.gen(function*() {
3307 const some = yield* Effect.fromNullishOr("value").pipe(Effect.catchNoSuchElement)
3308 assert.deepStrictEqual(some, Option.some("value"))
3309
3310 const none = yield* Effect.fromNullishOr(null as string | null).pipe(Effect.catchNoSuchElement)
3311 assert.deepStrictEqual(none, Option.none())
3312 }))
3313
3314 it.effect("catchNoSuchElement preserves other errors", () =>
3315 Effect.gen(function*() {
3316 const error = new ErrorA()
3317 const result = yield* Effect.fail(error).pipe(Effect.catchNoSuchElement, Effect.exit)
3318 assert.deepStrictEqual(result, Exit.fail(error))
3319 }))
3320 })
3321
3322 describe("match", () => {
3323 it("preserves handler receivers and treats thrown exceptions as defects", () => {
3324 const options = {
3325 value: 42,
3326 onFailure(this: { value: number }) {
3327 return this.value
3328 },
3329 onSuccess(this: { value: number }) {
3330 return this.value
3331 }
3332 }
3333 assert.strictEqual(Effect.runSync(Effect.match(Effect.succeed(1), options)), 42)
3334 assert.strictEqual(Effect.runSync(Effect.match(Effect.fail("error"), options)), 42)
3335 assert.strictEqual(Effect.runSync(Effect.matchCause(Effect.succeed(1), options)), 42)
3336 assert.strictEqual(Effect.runSync(Effect.matchCause(Effect.fail("error"), options)), 42)
3337 const boom = new Error("boom")
3338 assertExitDefect(
3339 Effect.runSyncExit(Effect.match(Effect.succeed(1), {
3340 onFailure: () => 0,
3341 onSuccess: () => {
3342 throw boom
3343 }
3344 })),
3345 boom
3346 )
3347 })
3348
3349 it("passes only the outcome to handlers", () => {
3350 function handler(_outcome: unknown) {
3351 // eslint-disable-next-line prefer-rest-params
3352 return arguments.length
3353 }
3354 const options = { onFailure: handler, onSuccess: handler }
3355 for (const source of [Effect.succeed(1), Effect.fail("error")]) {
3356 assert.strictEqual(Effect.runSync(Effect.match(source, options)), 1)
3357 assert.strictEqual(Effect.runSync(Effect.matchCause(source, options)), 1)
3358 }
3359 })
3360
3361 it("passes defects through without calling onFailure", () => {
3362 const boom = new Error("boom")
3363 assertExitDefect(
3364 Effect.runSyncExit(Effect.match(Effect.die(boom), { onFailure: () => "handled", onSuccess: () => "ok" })),
3365 boom
3366 )
3367 })
3368 })
3369
3370 describe("flatMap", () => {
3371 it("passes only the value to callbacks regardless of declared arity", () => {
3372 function callback(this: unknown, _value: number) {
3373 // eslint-disable-next-line prefer-rest-params
3374 return Effect.succeed([this, arguments.length])
3375 }
3376 assert.deepStrictEqual(Effect.runSync(Effect.flatMap(Effect.succeed(1), callback)), [undefined, 1])
3377 })
3378 })
3379
3380 describe("zip", () => {
3381 it.effect("concurrent: false", () => {
3382 const executionOrder: Array<string> = []
3383 const task1 = Effect.succeed("a").pipe(
3384 Effect.delay(50),
3385 Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
3386 )
3387 const task2 = Effect.succeed(1).pipe(
3388 Effect.delay(1),
3389 Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
3390 )
3391 return Effect.gen(function*() {
3392 const fiber = yield* Effect.forkChild(Effect.zip(task1, task2))
3393 yield* TestClock.adjust(51)
3394 const result = yield* Fiber.join(fiber)
3395 assert.deepStrictEqual(result, ["a", 1])
3396 assert.deepStrictEqual(executionOrder, ["task1", "task2"])
3397 })
3398 })
3399 it.effect("concurrent: true", () => {
3400 const executionOrder: Array<string> = []
3401 const task1 = Effect.succeed("a").pipe(
3402 Effect.delay(50),
3403 Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
3404 )
3405 const task2 = Effect.succeed(1).pipe(
3406 Effect.delay(1),
3407 Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
3408 )
3409 return Effect.gen(function*() {
3410 const fiber = yield* Effect.forkChild(Effect.zip(task1, task2, { concurrent: true }))
3411 yield* TestClock.adjust(50)
3412 const result = yield* Fiber.join(fiber)
3413 assert.deepStrictEqual(result, ["a", 1])
3414 assert.deepStrictEqual(executionOrder, ["task2", "task1"])
3415 })
3416 })
3417 it.effect("concurrent: true interrupts the other side on failure", () => {
3418 const interrupted: Array<string> = []
3419 const task1 = Effect.never.pipe(
3420 Effect.onInterrupt(() => Effect.sync(() => interrupted.push("task1")))
3421 )
3422 const task2 = Effect.fail("boom").pipe(Effect.delay(10))
3423 return Effect.gen(function*() {
3424 const fiber = yield* Effect.forkChild(Effect.zip(task1, task2, { concurrent: true }))
3425 yield* TestClock.adjust(10)
3426 const result = yield* Fiber.await(fiber)
3427 assert.deepStrictEqual(result, Exit.fail("boom"))
3428 assert.deepStrictEqual(interrupted, ["task1"])
3429 })
3430 })
3431 })
3432
3433 describe("zipWith", () => {
3434 it.effect("concurrent: false", () => {
3435 const executionOrder: Array<string> = []
3436 const task1 = Effect.succeed("a").pipe(
3437 Effect.delay(50),
3438 Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
3439 )
3440 const task2 = Effect.succeed(1).pipe(
3441 Effect.delay(1),
3442 Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
3443 )
3444 return Effect.gen(function*() {
3445 const fiber = yield* Effect.forkChild(Effect.zipWith(task1, task2, (a, b) => a + b))
3446 yield* TestClock.adjust(51)
3447 const result = yield* Fiber.join(fiber)
3448 assert.deepStrictEqual(result, "a1")
3449 assert.deepStrictEqual(executionOrder, ["task1", "task2"])
3450 })
3451 })
3452 it.effect("concurrent: true", () => {
3453 const executionOrder: Array<string> = []
3454 const task1 = Effect.succeed("a").pipe(
3455 Effect.delay(50),
3456 Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
3457 )
3458 const task2 = Effect.succeed(1).pipe(
3459 Effect.delay(1),
3460 Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
3461 )
3462 return Effect.gen(function*() {
3463 const fiber = yield* Effect.forkChild(Effect.zipWith(task1, task2, (a, b) => a + b, { concurrent: true }))
3464 yield* TestClock.adjust(50)
3465 const result = yield* Fiber.join(fiber)
3466 assert.deepStrictEqual(result, "a1")
3467 assert.deepStrictEqual(executionOrder, ["task2", "task1"])
3468 })
3469 })
3470 it.effect("concurrent: true interrupts the other side on failure", () => {
3471 const interrupted: Array<string> = []
3472 const task1 = Effect.fail("boom").pipe(Effect.delay(10))
3473 const task2 = Effect.never.pipe(
3474 Effect.onInterrupt(() => Effect.sync(() => interrupted.push("task2")))
3475 )
3476 return Effect.gen(function*() {
3477 const fiber = yield* Effect.forkChild(
3478 Effect.zipWith(task1, task2, (a, b) => `${a}${b}`, { concurrent: true })
3479 )
3480 yield* TestClock.adjust(10)
3481 const result = yield* Fiber.await(fiber)
3482 assert.deepStrictEqual(result, Exit.fail("boom"))
3483 assert.deepStrictEqual(interrupted, ["task2"])
3484 })
3485 })
3486 })
3487
3488 describe("catchCauseFilter", () => {
3489 it.effect("first argument as success", () =>
3490 Effect.gen(function*() {
3491 const result = yield* Effect.catchCauseFilter(Effect.succeed(1), (_) => Result.fail(_), () => Effect.fail("e2"))
3492 assert.deepStrictEqual(result, 1)
3493 }))
3494 it.effect("first argument as failure and predicate return false", () =>
3495 Effect.gen(function*() {
3496 const result = yield* Effect.flip(
3497 Effect.catchCauseFilter(Effect.fail("e1" as const), (_) => Result.fail(_), () => Effect.fail("e2" as const))
3498 )
3499 assert.deepStrictEqual(result, "e1")
3500 }))
3501 it.effect("first argument as failure and predicate return true", () =>
3502 Effect.gen(function*() {
3503 const result = yield* Effect.flip(
3504 Effect.catchCauseFilter(
3505 Effect.fail("e1" as const),
3506 (e) => Result.succeed(e),
3507 () => Effect.fail("e2" as const)
3508 )
3509 )
3510 assert.deepStrictEqual(result, "e2")
3511 }))
3512 })
3513
3514 describe("catchIf with predicate", () => {
3515 it.effect("predicate match recovers", () =>
3516 Effect.gen(function*() {
3517 const result = yield* Effect.catchIf(
3518 Effect.fail("e1"),
3519 (e) => typeof e === "string",
3520 (e) => Effect.succeed(`recovered: ${e}`)
3521 )
3522 assert.deepStrictEqual(result, "recovered: e1")
3523 }))
3524 it.effect("predicate no match preserves error", () =>
3525 Effect.gen(function*() {
3526 const result = yield* Effect.exit(
3527 Effect.catchIf(
3528 Effect.fail("e1" as const),
3529 (_e) => false,
3530 () => Effect.succeed("recovered")
3531 )
3532 )
3533 assert.deepStrictEqual(result, Exit.fail("e1"))
3534 }))
3535 })
3536
3537 describe("catchCauseIf with predicate", () => {
3538 it.effect("predicate match recovers", () =>
3539 Effect.gen(function*() {
3540 const result = yield* Effect.catchCauseIf(
3541 Effect.fail("e1"),
3542 Cause.hasFails,
3543 (cause) => Effect.succeed(`recovered: ${Cause.squash(cause)}`)
3544 )
3545 assert.deepStrictEqual(result, "recovered: e1")
3546 }))
3547 it.effect("predicate no match preserves error", () =>
3548 Effect.gen(function*() {
3549 const result = yield* Effect.exit(
3550 Effect.catchCauseIf(
3551 Effect.fail("e1" as const),
3552 constFalse,
3553 () => Effect.succeed("recovered")
3554 )
3555 )
3556 assert.deepStrictEqual(result, Exit.fail("e1"))
3557 }))
3558 })
3559
3560 describe("tapCauseIf", () => {
3561 it.effect("filter match taps", () =>
3562 Effect.gen(function*() {
3563 const tapped: Array<string> = []
3564 const result = yield* Effect.exit(
3565 Effect.tapCauseFilter(
3566 Effect.fail("e1"),
3567 (cause) => Result.succeed(cause),
3568 (cause) => Effect.sync(() => tapped.push(Cause.squash(cause) as string))
3569 )
3570 )
3571 assert.deepStrictEqual(tapped, ["e1"])
3572 assert.deepStrictEqual(result, Exit.fail("e1"))
3573 }))
3574 it.effect("filter no match skips tap", () =>
3575 Effect.gen(function*() {
3576 const tapped: Array<string> = []
3577 const result = yield* Effect.exit(
3578 Effect.tapCauseFilter(
3579 Effect.fail("e1"),
3580 (cause) => Result.fail(cause),
3581 () => Effect.sync(() => tapped.push("tapped"))
3582 )
3583 )
3584 assert.deepStrictEqual(tapped, [])
3585 assert.deepStrictEqual(result, Exit.fail("e1"))
3586 }))
3587 it.effect("predicate match taps", () =>
3588 Effect.gen(function*() {
3589 const tapped: Array<string> = []
3590 const result = yield* Effect.exit(
3591 Effect.tapCauseIf(
3592 Effect.fail("e1"),
3593 Cause.hasFails,
3594 (cause) => Effect.sync(() => tapped.push(Cause.squash(cause) as string))
3595 )
3596 )
3597 assert.deepStrictEqual(tapped, ["e1"])
3598 assert.deepStrictEqual(result, Exit.fail("e1"))
3599 }))
3600 it.effect("predicate no match skips tap", () =>
3601 Effect.gen(function*() {
3602 const tapped: Array<string> = []
3603 const result = yield* Effect.exit(
3604 Effect.tapCauseIf(
3605 Effect.fail("e1"),
3606 constFalse,
3607 () => Effect.sync(() => tapped.push("tapped"))
3608 )
3609 )
3610 assert.deepStrictEqual(tapped, [])
3611 assert.deepStrictEqual(result, Exit.fail("e1"))
3612 }))
3613 it.effect("success skips tap", () =>
3614 Effect.gen(function*() {
3615 const tapped: Array<string> = []
3616 const result = yield* Effect.tapCauseIf(
3617 Effect.succeed(42),
3618 constTrue,
3619 () => Effect.sync(() => tapped.push("tapped"))
3620 )
3621 assert.deepStrictEqual(tapped, [])
3622 assert.deepStrictEqual(result, 42)
3623 }))
3624 })
3625
3626 describe("onErrorIf", () => {
3627 it.effect("predicate match runs finalizer", () =>
3628 Effect.gen(function*() {
3629 const finalized: Array<string> = []
3630 const result = yield* Effect.exit(
3631 Effect.onErrorIf(
3632 Effect.fail("e1"),
3633 Cause.hasFails,
3634 (cause) => Effect.sync(() => finalized.push(Cause.squash(cause) as string))
3635 )
3636 )
3637 assert.deepStrictEqual(finalized, ["e1"])
3638 assert.deepStrictEqual(result, Exit.fail("e1"))
3639 }))
3640 it.effect("predicate no match skips finalizer", () =>
3641 Effect.gen(function*() {
3642 const finalized: Array<string> = []
3643 const result = yield* Effect.exit(
3644 Effect.onErrorIf(
3645 Effect.fail("e1"),
3646 constFalse,
3647 () => Effect.sync(() => finalized.push("finalized"))
3648 )
3649 )
3650 assert.deepStrictEqual(finalized, [])
3651 assert.deepStrictEqual(result, Exit.fail("e1"))
3652 }))
3653 it.effect("filter match runs finalizer", () =>
3654 Effect.gen(function*() {
3655 const finalized: Array<string> = []
3656 const result = yield* Effect.exit(
3657 Effect.onErrorFilter(
3658 Effect.fail("e1"),
3659 Cause.findError as Filter.Filter<Cause.Cause<string>, string>,
3660 (error: string) => Effect.sync(() => finalized.push(error))
3661 )
3662 )
3663 assert.deepStrictEqual(finalized, ["e1"])
3664 assert.deepStrictEqual(result, Exit.fail("e1"))
3665 }))
3666 it.effect("success skips finalizer", () =>
3667 Effect.gen(function*() {
3668 const finalized: Array<string> = []
3669 const result = yield* Effect.onErrorIf(
3670 Effect.succeed(42),
3671 constTrue,
3672 () => Effect.sync(() => finalized.push("finalized"))
3673 )
3674 assert.deepStrictEqual(finalized, [])
3675 assert.deepStrictEqual(result, 42)
3676 }))
3677 })
3678
3679 describe("onExitIf", () => {
3680 it.effect("predicate match on success runs finalizer", () =>
3681 Effect.gen(function*() {
3682 const finalized: Array<string> = []
3683 const result = yield* Effect.onExitIf(
3684 Effect.succeed(42),
3685 Exit.isSuccess,
3686 (exit) => {
3687 if (Exit.isSuccess(exit)) {
3688 return Effect.sync(() => finalized.push(`success:${exit.value}`))
3689 }
3690 return Effect.void
3691 }
3692 )
3693 assert.deepStrictEqual(finalized, ["success:42"])
3694 assert.deepStrictEqual(result, 42)
3695 }))
3696 it.effect("predicate no match on failure skips finalizer", () =>
3697 Effect.gen(function*() {
3698 const finalized: Array<string> = []
3699 const result = yield* Effect.exit(
3700 Effect.onExitIf(
3701 Effect.fail("e1"),
3702 Exit.isSuccess,
3703 () => Effect.sync(() => finalized.push("finalized"))
3704 )
3705 )
3706 assert.deepStrictEqual(finalized, [])
3707 assert.deepStrictEqual(result, Exit.fail("e1"))
3708 }))
3709 it.effect("predicate match on failure runs finalizer", () =>
3710 Effect.gen(function*() {
3711 const finalized: Array<string> = []
3712 const result = yield* Effect.exit(
3713 Effect.onExitIf(
3714 Effect.fail("e1"),
3715 Exit.isFailure,
3716 (exit) => {
3717 if (Exit.isFailure(exit)) {
3718 return Effect.sync(() => finalized.push(`failure:${Cause.squash(exit.cause)}`))
3719 }
3720 return Effect.void
3721 }
3722 )
3723 )
3724 assert.deepStrictEqual(finalized, ["failure:e1"])
3725 assert.deepStrictEqual(result, Exit.fail("e1"))
3726 }))
3727 it.effect("filter match receives pass value and exit", () =>
3728 Effect.gen(function*() {
3729 const finalized: Array<string> = []
3730 const result = yield* Effect.onExitFilter(
3731 Effect.succeed(42),
3732 (exit) => Exit.isSuccess(exit) ? Result.succeed(`value:${exit.value}`) : Result.fail(exit),
3733 (value, exit) =>
3734 Effect.sync(() => finalized.push(`${value}:${Exit.isSuccess(exit) ? `success:${exit.value}` : "failure"}`))
3735 )
3736 assert.deepStrictEqual(finalized, ["value:42:success:42"])
3737 assert.deepStrictEqual(result, 42)
3738 }))
3739 })
3740
3741 describe("filter with predicate/refinement", () => {
3742 it.effect("predicate filters iterable", () =>
3743 Effect.gen(function*() {
3744 const result = yield* Effect.filter(
3745 [1, 2, 3, 4, 5],
3746 (n: number) => n % 2 === 0
3747 )
3748 assert.deepStrictEqual(result, [2, 4])
3749 }))
3750 it.effect("refinement narrows iterable", () =>
3751 Effect.gen(function*() {
3752 const result = yield* Effect.filter(
3753 [1, "a", 2, "b", 3] as Array<number | string>,
3754 (x): x is number => typeof x === "number"
3755 )
3756 assert.deepStrictEqual(result, [1, 2, 3])
3757 }))
3758 it.effect("Filter.Filter overload maps pass values", () =>
3759 Effect.gen(function*() {
3760 const filter: Filter.Filter<number, string, number> = (n) =>
3761 n % 2 === 0 ? Result.succeed(`n=${n}`) : Result.fail(n)
3762 const result = yield* Effect.filterMap(
3763 [1, 2, 3, 4],
3764 filter
3765 )
3766 assert.deepStrictEqual(result, ["n=2", "n=4"])
3767 }))
3768 })
3769
3770 describe("catch", () => {
3771 it.effect("first argument as success", () =>
3772 Effect.gen(function*() {
3773 const result = yield* Effect.catch(Effect.succeed(1), () => Effect.fail("e2" as const))
3774 assert.deepStrictEqual(result, 1)
3775 }))
3776 it.effect("first argument as failure", () =>
3777 Effect.gen(function*() {
3778 const result = yield* Effect.flip(Effect.catch(Effect.fail("e1" as const), () => Effect.fail("e2" as const)))
3779 assert.deepStrictEqual(result, "e2")
3780 }))
3781 })
3782
3783 describe("firstSuccessOf", () => {
3784 it.effect("returns the first success and does not run later effects", () =>
3785 Effect.gen(function*() {
3786 const executed: Array<string> = []
3787 const result = yield* Effect.firstSuccessOf([
3788 Effect.sync(() => executed.push("first")).pipe(
3789 Effect.flatMap(() => Effect.fail("e1" as const))
3790 ),
3791 Effect.sync(() => executed.push("second")).pipe(
3792 Effect.as("success" as const)
3793 ),
3794 Effect.sync(() => executed.push("third")).pipe(
3795 Effect.as("unreachable" as const)
3796 )
3797 ])
3798
3799 assert.strictEqual(result, "success")
3800 assert.deepStrictEqual(executed, ["first", "second"])
3801 }))
3802
3803 it.effect("fails with the last failure when all effects fail", () =>
3804 Effect.gen(function*() {
3805 const result = yield* Effect.firstSuccessOf([
3806 Effect.fail("e1" as const),
3807 Effect.fail("e2" as const),
3808 Effect.fail("e3" as const)
3809 ]).pipe(Effect.flip)
3810
3811 assert.strictEqual(result, "e3")
3812 }))
3813
3814 it.effect("defects on an empty collection", () =>
3815 Effect.gen(function*() {
3816 const result = yield* Effect.firstSuccessOf([]).pipe(Effect.sandbox, Effect.flip)
3817 const reason = result.reasons[0]
3818
3819 assert.isTrue(Cause.isDieReason(reason))
3820 if (Cause.isDieReason(reason)) {
3821 assert.instanceOf(reason.defect, Error)
3822 assert.strictEqual(reason.defect.message, "Received an empty collection of effects")
3823 }
3824 }))
3825 })
3826
3827 describe("catchCause", () => {
3828 it.effect("first argument as success", () =>
3829 Effect.gen(function*() {
3830 const result = yield* Effect.catchCause(Effect.succeed(1), () => Effect.fail("e2" as const))
3831 assert.deepStrictEqual(result, 1)
3832 }))
3833 it.effect("first argument as failure", () =>
3834 Effect.gen(function*() {
3835 const result = yield* Effect.flip(
3836 Effect.catchCause(Effect.fail("e1" as const), () => Effect.fail("e2" as const))
3837 )
3838 assert.deepStrictEqual(result, "e2")
3839 }))
3840 })
3841
3842 describe("transaction (composable transactions)", () => {
3843 describe("basic behavior", () => {
3844 it.effect("should create transaction boundaries when no transaction is active", () =>
3845 Effect.gen(function*() {
3846 const ref1 = TxRef.makeUnsafe(0)
3847 const ref2 = TxRef.makeUnsafe(100)
3848
3849 yield* Effect.tx(TxRef.set(ref1, 10))
3850
3851 yield* Effect.tx(TxRef.set(ref2, 200))
3852
3853 const val1 = yield* Effect.tx(TxRef.get(ref1))
3854 const val2 = yield* Effect.tx(TxRef.get(ref2))
3855
3856 assert.strictEqual(val1, 10)
3857 assert.strictEqual(val2, 200)
3858 }))
3859
3860 it.effect("should allow TxRef.modify outside an existing transaction", () =>
3861 Effect.gen(function*() {
3862 const ref = TxRef.makeUnsafe(0)
3863
3864 const result = yield* TxRef.modify(ref, (current) => [current + 1, current + 10])
3865 const value = yield* Effect.tx(TxRef.get(ref))
3866
3867 assert.strictEqual(result, 1)
3868 assert.strictEqual(value, 10)
3869 }))
3870
3871 it.effect("should roll back nested changes when the outer transaction fails", () =>
3872 Effect.gen(function*() {
3873 const ref1 = TxRef.makeUnsafe(0)
3874 const ref2 = TxRef.makeUnsafe(100)
3875
3876 const parentError = yield* Effect.tx(Effect.gen(function*() {
3877 yield* TxRef.set(ref1, 10)
3878
3879 yield* Effect.tx(TxRef.set(ref2, 200))
3880
3881 return yield* Effect.fail("parent failed")
3882 })).pipe(Effect.flip)
3883
3884 const val1 = yield* Effect.tx(TxRef.get(ref1))
3885 const val2 = yield* Effect.tx(TxRef.get(ref2))
3886
3887 assert.strictEqual(parentError, "parent failed")
3888 assert.strictEqual(val1, 0)
3889 assert.strictEqual(val2, 100)
3890 }))
3891
3892 it.effect("should allow catching nested failures in the same transaction", () =>
3893 Effect.gen(function*() {
3894 const ref1 = TxRef.makeUnsafe(0)
3895 const ref2 = TxRef.makeUnsafe(100)
3896
3897 yield* Effect.tx(Effect.gen(function*() {
3898 yield* TxRef.set(ref1, 10)
3899
3900 const childResult = yield* Effect.tx(Effect.gen(function*() {
3901 yield* TxRef.set(ref2, 200)
3902 return yield* Effect.fail("child failed")
3903 })).pipe(Effect.result)
3904
3905 yield* TxRef.set(ref1, 20)
3906
3907 assert.strictEqual(Result.isFailure(childResult), true)
3908 }))
3909
3910 const val1 = yield* Effect.tx(TxRef.get(ref1))
3911 const val2 = yield* Effect.tx(TxRef.get(ref2))
3912
3913 assert.strictEqual(val1, 20)
3914 assert.strictEqual(val2, 200)
3915 }))
3916 })
3917
3918 describe("transaction nesting", () => {
3919 it.effect("should handle multiple levels of nested composed transactions", () =>
3920 Effect.gen(function*() {
3921 const ref1 = TxRef.makeUnsafe(0)
3922 const ref2 = TxRef.makeUnsafe(0)
3923 const ref3 = TxRef.makeUnsafe(0)
3924
3925 yield* Effect.tx(Effect.gen(function*() {
3926 yield* TxRef.set(ref1, 1)
3927
3928 yield* Effect.tx(Effect.gen(function*() {
3929 yield* TxRef.set(ref2, 2)
3930
3931 yield* Effect.tx(TxRef.set(ref3, 3))
3932 }))
3933 }))
3934
3935 const val1 = yield* Effect.tx(TxRef.get(ref1))
3936 const val2 = yield* Effect.tx(TxRef.get(ref2))
3937 const val3 = yield* Effect.tx(TxRef.get(ref3))
3938
3939 assert.strictEqual(val1, 1)
3940 assert.strictEqual(val2, 2)
3941 assert.strictEqual(val3, 3)
3942 }))
3943 })
3944
3945 describe("transaction composition behavior", () => {
3946 it.effect("should compose nested tx calls into the same transaction", () =>
3947 Effect.gen(function*() {
3948 const ref1 = TxRef.makeUnsafe(0)
3949 const ref2 = TxRef.makeUnsafe(0)
3950
3951 yield* Effect.tx(Effect.gen(function*() {
3952 yield* TxRef.set(ref1, 10)
3953
3954 yield* Effect.tx(TxRef.set(ref2, 20))
3955 }))
3956
3957 const val1 = yield* Effect.tx(TxRef.get(ref1))
3958 const val2 = yield* Effect.tx(TxRef.get(ref2))
3959
3960 assert.strictEqual(val1, 10)
3961 assert.strictEqual(val2, 20)
3962 }))
3963
3964 it.effect("should rollback the entire composed transaction on failure", () =>
3965 Effect.gen(function*() {
3966 const ref = TxRef.makeUnsafe(0)
3967
3968 const txError = yield* Effect.tx(Effect.gen(function*() {
3969 yield* TxRef.set(ref, 10)
3970
3971 return yield* Effect.tx(Effect.gen(function*() {
3972 yield* TxRef.set(ref, 20)
3973 return yield* Effect.fail("nested failure")
3974 }))
3975 })).pipe(Effect.flip)
3976
3977 const valueAfterFailure = yield* Effect.tx(TxRef.get(ref))
3978
3979 assert.strictEqual(txError, "nested failure")
3980 assert.strictEqual(valueAfterFailure, 0)
3981 }))
3982
3983 it.effect("should preserve nested writes when nested failure is caught", () =>
3984 Effect.gen(function*() {
3985 const ref = TxRef.makeUnsafe(0)
3986
3987 yield* Effect.tx(Effect.gen(function*() {
3988 yield* TxRef.set(ref, 10)
3989
3990 const childError = yield* Effect.tx(Effect.gen(function*() {
3991 yield* TxRef.set(ref, 20)
3992 return yield* Effect.fail("transaction nested failure")
3993 })).pipe(Effect.flip)
3994
3995 assert.strictEqual(childError, "transaction nested failure")
3996 }))
3997
3998 const transactionValue = yield* Effect.tx(TxRef.get(ref))
3999
4000 assert.strictEqual(transactionValue, 20)
4001 }))
4002 })
4003
4004 describe("retry", () => {
4005 it.effect("should rerun when a read ref changed while the transaction was suspended", () =>
4006 Effect.gen(function*() {
4007 const ref = TxRef.makeUnsafe(0)
4008 const read = yield* Deferred.make<void>()
4009 const committed = yield* Deferred.make<void>()
4010
4011 const transaction = yield* Effect.tx(Effect.gen(function*() {
4012 const value = yield* TxRef.get(ref)
4013 if (value === 0) {
4014 yield* Deferred.succeed(read, undefined)
4015 yield* Deferred.await(committed)
4016 return yield* Effect.txRetry
4017 }
4018 return value
4019 })).pipe(Effect.forkChild)
4020
4021 yield* Deferred.await(read)
4022 yield* Effect.tx(TxRef.set(ref, 1))
4023 yield* Deferred.succeed(committed, undefined)
4024
4025 const joined = yield* Fiber.join(transaction).pipe(
4026 Effect.timeoutOption("1 second"),
4027 Effect.forkChild
4028 )
4029 yield* TestClock.adjust("1 second")
4030
4031 assert.deepStrictEqual(yield* Fiber.join(joined), Option.some(1))
4032 }))
4033
4034 it.effect("commits wake the waiters of refs they write, not refs they read", () =>
4035 Effect.gen(function*() {
4036 const gate = TxRef.makeUnsafe(0)
4037 const other = TxRef.makeUnsafe(0)
4038 let runs = 0
4039 const waiter = yield* Effect.tx(Effect.gen(function*() {
4040 runs++
4041 const value = yield* TxRef.get(gate)
4042 if (value === 0) return yield* Effect.txRetry
4043 return value
4044 })).pipe(Effect.forkChild({ startImmediately: true }))
4045
4046 yield* Effect.tx(Effect.gen(function*() {
4047 yield* TxRef.get(gate)
4048 yield* TxRef.set(other, 1)
4049 }))
4050 yield* Effect.repeat(Effect.yieldNow, { times: 10 })
4051 assert.strictEqual(runs, 1, "reading a ref should not wake its waiters")
4052
4053 yield* Effect.tx(TxRef.set(gate, 0))
4054 yield* Effect.repeat(Effect.yieldNow, { times: 10 })
4055 assert.strictEqual(runs, 2, "writing the same value should wake its waiters")
4056
4057 yield* Effect.tx(TxRef.set(gate, 1))
4058 assert.strictEqual(yield* Fiber.join(waiter), 1)
4059 }))
4060
4061 it.effect("a write of -0 over 0 is published and wakes its waiters", () =>
4062 Effect.gen(function*() {
4063 const zero = TxRef.makeUnsafe(0)
4064 const negativeZero = yield* Effect.tx(Effect.gen(function*() {
4065 const value = yield* TxRef.get(zero)
4066 if (Object.is(value, 0)) return yield* Effect.txRetry
4067 return 1 / value
4068 })).pipe(Effect.forkChild({ startImmediately: true }))
4069 yield* Effect.tx(TxRef.set(zero, -0))
4070
4071 yield* Effect.repeat(Effect.yieldNow, { times: 10 })
4072 assert.deepStrictEqual(negativeZero.pollUnsafe(), Exit.succeed(-Infinity), "-0 over 0 never woke its waiter")
4073 }))
4074 })
4075 })
4076
4077 describe("Effect.fn", () => {
4078 it.effect("should support pipeable arguments", () => {
4079 const fn = Effect.fn(function*(s: string) {
4080 return s.length
4081 }, (effect, ...args) => effect.pipe(Effect.map((result) => [result, ...args])))
4082 return Effect.gen(function*() {
4083 const result = yield* fn("a")
4084 assert.deepStrictEqual(result, [1, "a"])
4085 })
4086 })
4087
4088 it.effect("should support self with pipeable arguments", () => {
4089 const self = { prefix: "bound" }
4090 const fn = Effect.fn(
4091 { self },
4092 function*(this: typeof self, value: string) {
4093 return `${this.prefix}:${value}`
4094 },
4095 Effect.map((value) => value.toUpperCase())
4096 )
4097 return Effect.gen(function*() {
4098 const result = yield* fn("value")
4099 assert.strictEqual(result, "BOUND:VALUE")
4100 })
4101 })
4102
4103 it("should proxy body length", () => {
4104 const traced = Effect.fn(function*(a: string, b: number) {
4105 return a.length + b
4106 })
4107 const named = Effect.fn("named")(function*(a: string, b: number, c: boolean) {
4108 return c ? a.length + b : b
4109 })
4110 const untraced = Effect.fnUntraced(function*(a: string, b: number) {
4111 return a.length + b
4112 }, Effect.map((n) => n))
4113
4114 assert.strictEqual(traced.length, 2)
4115 assert.strictEqual(named.length, 3)
4116 assert.strictEqual(untraced.length, 2)
4117 })
4118
4119 it.effect("should reuse the definition frame across calls", () => {
4120 const fn = Effect.fn("traced")(function*() {
4121 return (yield* References.CurrentStackFrame)!
4122 })
4123 return Effect.gen(function*() {
4124 const first = yield* fn()
4125 const second = yield* fn()
4126 assert.strictEqual(first.parent?.name, "traced (definition)")
4127 assert.include(first.parent?.stack(), "Effect.test.ts")
4128 assert.strictEqual(first.parent?.stack(), second.parent?.stack())
4129 assert.notStrictEqual(first.stack(), second.stack())
4130 })
4131 })
4132 })
4133
4134 describe("catchReason", () => {
4135 class RateLimitError extends Data.TaggedError("RateLimitError")<{
4136 readonly retryAfter: number
4137 }> {}
4138
4139 class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
4140 readonly limit: number
4141 }> {}
4142
4143 class AiError extends Data.TaggedError("AiError")<{
4144 readonly reason: RateLimitError | QuotaExceededError
4145 }> {}
4146
4147 class OtherError extends Data.TaggedError("OtherError")<{
4148 readonly message: string
4149 }> {}
4150
4151 it.effect("catches matching reason - handler succeeds", () =>
4152 Effect.gen(function*() {
4153 const result = yield* Effect.fail(
4154 new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
4155 ).pipe(
4156 Effect.catchReason("AiError", "RateLimitError", (r) => Effect.succeed(`retry: ${r.retryAfter}`))
4157 )
4158 assert.strictEqual(result, "retry: 60")
4159 }))
4160
4161 it.effect("orElse", () =>
4162 Effect.gen(function*() {
4163 const result = yield* Effect.fail(
4164 new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
4165 ).pipe(
4166 Effect.catchReason(
4167 "AiError",
4168 "RateLimitError",
4169 (r) => Effect.succeed(`retry: ${r.retryAfter}`),
4170 (_) => Effect.succeed("quota")
4171 )
4172 )
4173 assert.strictEqual(result, "quota")
4174 }))
4175
4176 it.effect("catches matching reason - handler fails", () =>
4177 Effect.gen(function*() {
4178 const reason = new RateLimitError({ retryAfter: 60 })
4179 const error = new OtherError({ message: "handled" })
4180 const exit = yield* Effect.fail(new AiError({ reason })).pipe(
4181 Effect.catchReason("AiError", "RateLimitError", () => Effect.fail(error)),
4182 Effect.exit
4183 )
4184 assertExitFailure(exit, Cause.fail(error))
4185 }))
4186
4187 it.effect("ignores non-matching reason", () =>
4188 Effect.gen(function*() {
4189 const reason = new QuotaExceededError({ limit: 100 })
4190 const exit = yield* Effect.fail(new AiError({ reason })).pipe(
4191 Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
4192 Effect.exit
4193 )
4194 assertExitFailure(exit, Cause.fail(new AiError({ reason })))
4195 }))
4196
4197 it.effect("preserves the cause for a non-matching reason", () =>
4198 Effect.gen(function*() {
4199 const error = new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
4200 const result = yield* Effect.gen(function*() {
4201 yield* Effect.addFinalizer(() => Effect.die("finalizer failure"))
4202 return yield* Effect.fail(error)
4203 }).pipe(
4204 Effect.scoped,
4205 Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
4206 Effect.exit
4207 )
4208 assert.deepStrictEqual(
4209 result,
4210 Exit.failCause(Cause.combine(Cause.fail(error), Cause.die("finalizer failure")))
4211 )
4212 }))
4213
4214 it.effect("ignores non-matching parent tag", () =>
4215 Effect.gen(function*() {
4216 const error = new OtherError({ message: "test" })
4217 const exit = yield* (Effect.fail(error) as Effect.Effect<never, AiError | OtherError>).pipe(
4218 Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
4219 Effect.exit
4220 )
4221 assertExitFailure(exit, Cause.fail(error))
4222 }))
4223 })
4224
4225 describe("catchReasons", () => {
4226 class RateLimitError extends Data.TaggedError("RateLimitError")<{
4227 readonly retryAfter: number
4228 }> {}
4229
4230 class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
4231 readonly limit: number
4232 }> {}
4233
4234 class AiError extends Data.TaggedError("AiError")<{
4235 readonly reason: RateLimitError | QuotaExceededError
4236 }> {}
4237
4238 it.effect("catches with object handlers", () =>
4239 Effect.gen(function*() {
4240 const result = yield* Effect.fail(
4241 new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
4242 ).pipe(
4243 Effect.catchReasons("AiError", {
4244 RateLimitError: (r) => Effect.succeed(`rate: ${r.retryAfter}`),
4245 QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
4246 })
4247 )
4248 assert.strictEqual(result, "rate: 60")
4249 }))
4250
4251 it.effect("orElse", () =>
4252 Effect.gen(function*() {
4253 const result = yield* Effect.fail(
4254 new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
4255 ).pipe(
4256 Effect.catchReasons("AiError", {
4257 QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
4258 }, (_) => Effect.succeed("orElse"))
4259 )
4260 assert.strictEqual(result, "orElse")
4261 }))
4262
4263 it.effect("catches second reason type", () =>
4264 Effect.gen(function*() {
4265 const result = yield* Effect.fail(
4266 new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
4267 ).pipe(
4268 Effect.catchReasons("AiError", {
4269 RateLimitError: (r) => Effect.succeed(`rate: ${r.retryAfter}`),
4270 QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
4271 })
4272 )
4273 assert.strictEqual(result, "quota: 100")
4274 }))
4275
4276 it.effect("partial handlers - unhandled passes through", () =>
4277 Effect.gen(function*() {
4278 const reason = new QuotaExceededError({ limit: 100 })
4279 const exit = yield* Effect.fail(new AiError({ reason })).pipe(
4280 Effect.catchReasons("AiError", {
4281 RateLimitError: () => Effect.succeed("handled")
4282 }),
4283 Effect.exit
4284 )
4285 assertExitFailure(exit, Cause.fail(new AiError({ reason })))
4286 }))
4287
4288 it.effect("preserves the cause for an unhandled reason", () =>
4289 Effect.gen(function*() {
4290 const error = new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
4291 const result = yield* Effect.gen(function*() {
4292 yield* Effect.addFinalizer(() => Effect.die("finalizer failure"))
4293 return yield* Effect.fail(error)
4294 }).pipe(
4295 Effect.scoped,
4296 Effect.catchReasons("AiError", {
4297 RateLimitError: () => Effect.succeed("handled")
4298 }),
4299 Effect.exit
4300 )
4301 assert.deepStrictEqual(
4302 result,
4303 Exit.failCause(Cause.combine(Cause.fail(error), Cause.die("finalizer failure")))
4304 )
4305 }))
4306 })
4307
4308 describe("unwrapReason", () => {
4309 class RateLimitError extends Data.TaggedError("RateLimitError")<{
4310 readonly retryAfter: number
4311 }> {}
4312
4313 class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
4314 readonly limit: number
4315 }> {}
4316
4317 class AiError extends Data.TaggedError("AiError")<{
4318 readonly reason: RateLimitError | QuotaExceededError
4319 }> {}
4320
4321 class OtherError extends Data.TaggedError("OtherError")<{
4322 readonly message: string
4323 }> {}
4324
4325 it.effect("extracts reason into error channel", () =>
4326 Effect.gen(function*() {
4327 const reason = new RateLimitError({ retryAfter: 60 })
4328 const exit = yield* Effect.fail(new AiError({ reason })).pipe(
4329 Effect.unwrapReason("AiError"),
4330 Effect.exit
4331 )
4332 assertExitFailure(exit, Cause.fail(reason))
4333 }))
4334
4335 it.effect("extracts second reason type", () =>
4336 Effect.gen(function*() {
4337 const reason = new QuotaExceededError({ limit: 100 })
4338 const exit = yield* Effect.fail(new AiError({ reason })).pipe(
4339 Effect.unwrapReason("AiError"),
4340 Effect.exit
4341 )
4342 assertExitFailure(exit, Cause.fail(reason))
4343 }))
4344
4345 it.effect("preserves other errors", () =>
4346 Effect.gen(function*() {
4347 const error = new OtherError({ message: "test" })
4348 const exit = yield* (Effect.fail(error) as Effect.Effect<never, AiError | OtherError>).pipe(
4349 Effect.unwrapReason("AiError"),
4350 Effect.exit
4351 )
4352 assertExitFailure(exit, Cause.fail(error))
4353 }))
4354 })
4355
4356 describe("cached", () => {
4357 it.effect("runs the effect once for concurrent callers", () =>
4358 Effect.gen(function*() {
4359 const release = yield* Deferred.make<void>()
4360 let count = 0
4361 const cached = yield* Effect.cached(
4362 Effect.gen(function*() {
4363 count++
4364 yield* Deferred.await(release)
4365 return count
4366 })
4367 )
4368
4369 const fibers = yield* Effect.forEach([1, 2, 3], () => cached.pipe(Effect.forkChild({ startImmediately: true })))
4370 assert.strictEqual(count, 1)
4371
4372 yield* Deferred.succeed(release, void 0)
4373 assert.deepStrictEqual(yield* Fiber.awaitAll(fibers), [Exit.succeed(1), Exit.succeed(1), Exit.succeed(1)])
4374 }))
4375
4376 it.effect("replays failures", () =>
4377 Effect.gen(function*() {
4378 let count = 0
4379 const cached = yield* Effect.cached(
4380 Effect.sync(() => count++).pipe(Effect.andThen(Effect.fail("boom")))
4381 )
4382
4383 assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
4384 assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
4385 assert.strictEqual(count, 1)
4386 }))
4387
4388 it.effect("interrupting a waiter leaves the owner running", () =>
4389 Effect.gen(function*() {
4390 const started = yield* Deferred.make<void>()
4391 const release = yield* Deferred.make<void>()
4392 const cached = yield* Effect.cached(
4393 Deferred.succeed(started, void 0).pipe(
4394 Effect.andThen(Deferred.await(release)),
4395 Effect.as(42)
4396 )
4397 )
4398
4399 const owner = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
4400 yield* Deferred.await(started)
4401 const waiter = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
4402 yield* Fiber.interrupt(waiter)
4403 assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(waiter)))
4404
4405 yield* Deferred.succeed(release, void 0)
4406 assert.strictEqual(yield* Fiber.join(owner), 42)
4407 assert.strictEqual(yield* cached, 42)
4408 }))
4409
4410 it.effect("shares a run until every caller is interrupted, then starts fresh", () =>
4411 Effect.gen(function*() {
4412 let runs = 0
4413 const started = yield* Deferred.make<void>()
4414 const finalizing = yield* Deferred.make<void>()
4415 const finishFinalizer = yield* Deferred.make<void>()
4416 const cached = yield* Effect.cached(
4417 Effect.suspend(() => {
4418 if (++runs > 1) return Effect.succeed(runs)
4419 return Deferred.succeed(started, void 0).pipe(
4420 Effect.andThen(Effect.never),
4421 Effect.onInterrupt(() =>
4422 Deferred.succeed(finalizing, void 0).pipe(Effect.andThen(Deferred.await(finishFinalizer)))
4423 )
4424 )
4425 })
4426 )
4427
4428 const first = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
4429 yield* Deferred.await(started)
4430 const second = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
4431 yield* Fiber.interrupt(first)
4432 assert.isFalse(yield* Deferred.isDone(finalizing))
4433
4434 // A call made while the abandoned run finalizes starts a fresh run.
4435 const interruptSecond = yield* Effect.forkChild(Fiber.interrupt(second), { startImmediately: true })
4436 yield* Deferred.await(finalizing)
4437 const fresh = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
4438 yield* Deferred.succeed(finishFinalizer, void 0)
4439 yield* Fiber.join(interruptSecond)
4440 assert.deepStrictEqual(yield* Fiber.await(fresh), Exit.succeed(2))
4441 }))
4442 })
4443
4444 describe("cachedWithTTL", () => {
4445 it.effect("skips the ttl callback on interruption and invokes it once on the next successful run", () =>
4446 Effect.gen(function*() {
4447 let runs = 0
4448 const exits: Array<Exit.Exit<number>> = []
4449 const cached = yield* Effect.cachedWithTTL(
4450 Effect.suspend(() => ++runs === 1 ? Effect.interrupt : Effect.succeed(runs)),
4451 (exit: Exit.Exit<number>) => {
4452 exits.push(exit)
4453 return "1 minute"
4454 }
4455 )
4456
4457 assert.isTrue(Exit.hasInterrupts(yield* Effect.exit(cached)))
4458 assert.deepStrictEqual(exits, [])
4459 assert.strictEqual(yield* cached, 2)
4460 assert.strictEqual(yield* cached, 2)
4461 assert.deepStrictEqual(exits, [Exit.succeed(2)])
4462 }))
4463
4464 it.effect("selects ttl from each fresh exit without evaluating on cache hits", () =>
4465 Effect.gen(function*() {
4466 let count = 0
4467 const exits: Array<Exit.Exit<number>> = []
4468 const cached = yield* Effect.cachedWithTTL(
4469 Effect.sync(() => ++count),
4470 (exit: Exit.Exit<number>) => {
4471 exits.push(exit)
4472 return Exit.isSuccess(exit) && exit.value === 1 ? "1 second" : Duration.seconds(3)
4473 }
4474 )
4475
4476 assert.strictEqual(count, 0)
4477 assert.deepStrictEqual(exits, [])
4478 assert.strictEqual(yield* cached, 1)
4479 yield* TestClock.adjust("999 millis")
4480 assert.strictEqual(yield* cached, 1)
4481 assert.deepStrictEqual(exits, [Exit.succeed(1)])
4482
4483 yield* TestClock.adjust("1 milli")
4484 assert.strictEqual(yield* cached, 2)
4485 yield* TestClock.adjust("1 second")
4486 assert.strictEqual(yield* cached, 2)
4487 yield* TestClock.adjust("2 seconds")
4488 assert.strictEqual(yield* cached, 3)
4489 assert.deepStrictEqual(exits, [Exit.succeed(1), Exit.succeed(2), Exit.succeed(3)])
4490 }))
4491
4492 it.effect("supports a piped callback that skips failures and caches successes", () =>
4493 Effect.gen(function*() {
4494 let count = 0
4495 const exits: Array<Exit.Exit<number, string>> = []
4496 const cached = yield* Effect.suspend(() => ++count === 1 ? Effect.fail("boom") : Effect.succeed(count)).pipe(
4497 Effect.cachedWithTTL((exit: Exit.Exit<number, string>) => {
4498 exits.push(exit)
4499 return Exit.isFailure(exit) ? 0 : "1 second"
4500 })
4501 )
4502
4503 assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
4504 assert.strictEqual(yield* cached, 2)
4505 assert.strictEqual(yield* cached, 2)
4506 assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2)])
4507 yield* TestClock.adjust("1 second")
4508 assert.strictEqual(yield* cached, 3)
4509 assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2), Exit.succeed(3)])
4510 }))
4511
4512 it.effect("starts ttl from value creation", () =>
4513 Effect.gen(function*() {
4514 let count = 0
4515 const cached = yield* Effect.cachedWithTTL(
4516 Effect.sleep("2 seconds").pipe(
4517 Effect.flatMap(() =>
4518 Effect.sync(() => {
4519 count += 1
4520 return count
4521 })
4522 )
4523 ),
4524 "1 second"
4525 )
4526
4527 const firstFiber = yield* Effect.forkChild(cached)
4528 yield* Effect.yieldNow
4529 yield* TestClock.adjust("2 seconds")
4530 assert.strictEqual(yield* Fiber.join(firstFiber), 1)
4531
4532 const secondFiber = yield* Effect.forkChild(cached)
4533 yield* Effect.yieldNow
4534 yield* TestClock.adjust("2 seconds")
4535 assert.strictEqual(yield* Fiber.join(secondFiber), 1)
4536 assert.strictEqual(count, 1)
4537 }))
4538 })
4539
4540 describe("cachedInvalidateWithTTL", () => {
4541 it.effect("supports a piped callback that skips failures and caches successes", () =>
4542 Effect.gen(function*() {
4543 let count = 0
4544 const exits: Array<Exit.Exit<number, string>> = []
4545 const [cached] = yield* Effect.suspend(() => ++count === 1 ? Effect.fail("boom") : Effect.succeed(count)).pipe(
4546 Effect.cachedInvalidateWithTTL((exit: Exit.Exit<number, string>) => {
4547 exits.push(exit)
4548 return Exit.isFailure(exit) ? 0 : "1 second"
4549 })
4550 )
4551
4552 assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
4553 assert.strictEqual(yield* cached, 2)
4554 assert.strictEqual(yield* cached, 2)
4555 assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2)])
4556 yield* TestClock.adjust("1 second")
4557 assert.strictEqual(yield* cached, 3)
4558 assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2), Exit.succeed(3)])
4559 }))
4560 })
4561
4562 describe("updateServiceScoped", () => {
4563 class CurrentNumber extends Context.Service<CurrentNumber, number>()("CurrentNumber") {}
4564
4565 const CurrentNumberReference = Context.Reference<number>("CurrentNumberReference", {
4566 defaultValue: () => 1
4567 })
4568
4569 const CurrentValues = Context.Reference<ReadonlyArray<string>>("CurrentValues", {
4570 defaultValue: () => []
4571 })
4572
4573 it.effect("skips restoration when the provider expires before scope closure", () =>
4574 Effect.gen(function*() {
4575 const result = yield* Effect.gen(function*() {
4576 yield* Effect.updateServiceScoped(CurrentNumber, (value) => value + 1)
4577 return yield* CurrentNumber
4578 }).pipe(Effect.provideService(CurrentNumber, 1), Effect.scoped)
4579
4580 assert.strictEqual(result, 2)
4581 assert.deepStrictEqual(yield* Effect.serviceOption(CurrentNumber), Option.none())
4582 }))
4583
4584 it.effect("updates a Context.Service until the scope closes", () =>
4585 Effect.gen(function*() {
4586 const before = yield* CurrentNumber
4587 const during = yield* Effect.scoped(
4588 Effect.gen(function*() {
4589 yield* Effect.updateServiceScoped(CurrentNumber, (value) => value + 1)
4590 return yield* CurrentNumber
4591 })
4592 )
4593 const after = yield* CurrentNumber
4594
4595 assert.deepStrictEqual([before, during, after], [1, 2, 1])
4596 }).pipe(Effect.provideService(CurrentNumber, 1)))
4597
4598 it.effect("updates a Context.Reference until the scope closes", () =>
4599 Effect.gen(function*() {
4600 const before = yield* CurrentNumberReference
4601 const during = yield* Effect.scoped(
4602 Effect.gen(function*() {
4603 yield* Effect.updateServiceScoped(CurrentNumberReference, (value) => value + 1)
4604 return yield* CurrentNumberReference
4605 })
4606 )
4607 const after = yield* CurrentNumberReference
4608
4609 assert.deepStrictEqual([before, during, after], [1, 2, 1])
4610 }))
4611
4612 it.effect("supports merging the current value on reset", () =>
4613 Effect.gen(function*() {
4614 const scope = Scope.makeUnsafe()
4615 let resetValues: ReadonlyArray<ReadonlyArray<string>> | undefined
4616
4617 const result = yield* Effect.gen(function*() {
4618 yield* Effect.updateServiceScoped(
4619 CurrentValues,
4620 (values) => [...values, "scoped"],
4621 {
4622 reset: (original, updated, current) => {
4623 resetValues = [original, updated, current]
4624 return [...original, ...current.filter((value) => !updated.includes(value))]
4625 }
4626 }
4627 )
4628 yield* Effect.withFiber((fiber) =>
4629 Effect.sync(() => {
4630 fiber.setContext(Context.add(fiber.context, CurrentValues, ["scoped", "external"]))
4631 })
4632 )
4633 yield* Scope.close(scope, Exit.void)
4634 return yield* CurrentValues
4635 }).pipe(Effect.provideService(Scope.Scope, scope))
4636
4637 assert.deepStrictEqual(resetValues, [[], ["scoped"], ["scoped", "external"]])
4638 assert.deepStrictEqual(result, ["external"])
4639 }))
4640 })
4641
4642 describe("provide", () => {
4643 class MyNumber extends Context.Service<MyNumber, number>()("MyNumber") {}
4644
4645 it.effect("subsequent calls share MemoMap", () =>
4646 Effect.gen(function*() {
4647 let buildCount = 0
4648 const layer = Layer.sync(MyNumber, () => {
4649 buildCount += 1
4650 return 42
4651 })
4652
4653 // @effect-diagnostics multipleEffectProvide:off
4654 yield* Effect.void.pipe(
4655 Effect.provide(layer, { local: true }), // local always builds the layer
4656 Effect.provide(layer),
4657 Effect.provide(layer)
4658 )
4659
4660 assert.strictEqual(buildCount, 2)
4661 }))
4662 })
4663
4664 describe("AsyncLocalStorage", () => {
4665 const storage = new AsyncLocalStorage<string>()
4666 const current = () => storage.getStore() ?? "none"
4667
4668 const runWithStore = <A, E>(store: string | undefined, effect: Effect.Effect<A, E>): Promise<A> =>
4669 store === undefined ? Effect.runPromise(effect) : storage.run(store, () => Effect.runPromise(effect))
4670
4671 const observe = Effect.gen(function*() {
4672 const resumed = current()
4673 const promise = yield* Effect.promise(async () => current())
4674 const timer = yield* Effect.callback<string>((resume) => {
4675 setTimeout(() => resume(Effect.succeed(current())), 1)
4676 })
4677 yield* Effect.sleep(1)
4678 const afterSleep = current()
4679 return { resumed, promise, timer, afterSleep }
4680 })
4681
4682 const expected = (store: string) => ({ resumed: store, promise: store, timer: store, afterSleep: store })
4683
4684 for (const waiterStore of [undefined, "waiter"]) {
4685 const label = waiterStore === undefined ? "without a store" : "with its own store"
4686
4687 describe(`waiter ${label}`, () => {
4688 it("Deferred.await resumes in the awaiter's context", async () => {
4689 const deferred = Deferred.makeUnsafe<void>()
4690 const waiter = runWithStore(waiterStore, Effect.andThen(Deferred.await(deferred), observe))
4691 const completer = await runWithStore(
4692 "waker",
4693 Effect.sleep(5).pipe(
4694 Effect.andThen(Deferred.succeed(deferred, undefined)),
4695 Effect.andThen(Effect.sync(current))
4696 )
4697 )
4698 assert.strictEqual(completer, "waker")
4699 assert.deepStrictEqual(await waiter, expected(waiterStore ?? "none"))
4700 })
4701
4702 it("Pool.get resumes in the waiter's context", async () => {
4703 const scope = Scope.makeUnsafe()
4704 try {
4705 const pool = await Effect.runPromise(
4706 Pool.make({ acquire: Effect.succeed("conn"), size: 1 }).pipe(
4707 // Warm the pool before starting the holder.
4708 Effect.tap((pool) => Effect.scoped(Pool.get(pool))),
4709 Scope.provide(scope)
4710 )
4711 )
4712 const holderAcquired = Deferred.makeUnsafe<string>()
4713 const holder = runWithStore(
4714 "waker",
4715 Effect.scoped(
4716 Pool.get(pool).pipe(
4717 Effect.andThen(Effect.sync(current)),
4718 Effect.tap((store) => Deferred.succeed(holderAcquired, store)),
4719 Effect.andThen(Effect.sleep(10)),
4720 Effect.andThen(Effect.sync(current))
4721 )
4722 )
4723 )
4724 assert.strictEqual(await Effect.runPromise(Deferred.await(holderAcquired)), "waker")
4725 const waiter = runWithStore(waiterStore, Effect.andThen(Effect.scoped(Pool.get(pool)), observe))
4726 assert.strictEqual(await holder, "waker")
4727 assert.deepStrictEqual(await waiter, expected(waiterStore ?? "none"))
4728 } finally {
4729 await Effect.runPromise(Scope.close(scope, Exit.void))
4730 }
4731 })
4732 })
4733 }
4734 })
4735})