t3-code-android-nightly/.repos/effect-smol/packages/effect/test/Effect.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

4735 lines
163 KiB
TypeScript

import { assert, describe, it, vi } from "@effect/vitest"
import { assertExitFailure } from "@effect/vitest/utils"
import {
Cause,
Context,
Data,
Deferred,
Duration,
Effect,
Effectable,
Exit,
Fiber,
type Filter,
Latch,
Layer,
Logger,
type LogLevel,
Option,
Pool,
Ref,
References,
Result,
Schedule,
Scheduler,
Scope,
TxRef
} from "effect"
import { constFalse, constTrue, pipe } from "effect/Function"
import { TestClock } from "effect/testing"
import { AsyncLocalStorage } from "node:async_hooks"
import { assertCauseFail } from "./utils/assert.ts"
class ATag extends Context.Service<ATag, "A">()("ATag") {}
const assertExitDefect = <A, E>(exit: Exit.Exit<A, E>, defect: unknown) => {
assert.isTrue(Exit.hasDies(exit))
const result = Exit.findDefect(exit)
assert.isTrue(Result.isSuccess(result))
if (Result.isSuccess(result)) {
assert.strictEqual(result.success, defect)
}
}
const assertUnknownError = <A>(exit: Exit.Exit<A, Cause.UnknownError>, cause: unknown, message: string) => {
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
const result = Cause.findError(exit.cause)
assert.isTrue(Result.isSuccess(result))
if (Result.isSuccess(result)) {
assert.isTrue(Cause.isUnknownError(result.success))
assert.strictEqual((result.success as Error).cause, cause)
assert.strictEqual(result.success.message, message)
}
}
}
describe("Effect", () => {
describe("interruption before cleanup registration", () => {
const interruptAfterContextChange = (changed: (context: Context.Context<never>) => boolean) => {
let yielded = false
const tasks: Array<() => void> = []
const scheduler: Scheduler.Scheduler = {
executionMode: "async",
makeDispatcher: () => ({
scheduleTask: (task) => {
tasks.push(task)
},
flush() {}
}),
shouldYield(fiber) {
if (!yielded && changed(fiber.context)) {
yielded = true
return true
}
return false
}
}
return { scheduler, tasks, wasYielded: () => yielded }
}
it.effect("scoped closes its scope when interrupted after installing it", () =>
Effect.gen(function*() {
let finalized = 0
let original: Context.Context<never> | undefined
let observed: Context.Context<never> | undefined
let innerScope: Scope.Scope | undefined
const { scheduler, tasks, wasYielded } = interruptAfterContextChange((context) => {
const scope = Context.getOrUndefined(context, Scope.Scope)
if (original === undefined || scope === Context.getOrUndefined(original, Scope.Scope)) return false
innerScope = scope
return true
})
const child = yield* Effect.gen(function*() {
original = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context))
yield* Effect.scoped(Effect.addFinalizer(() =>
Effect.sync(() => {
finalized++
})
))
}).pipe(
Effect.onExit(() =>
Effect.withFiber((fiber) =>
Effect.sync(() => {
observed = fiber.context
})
)
),
Effect.provideService(Scheduler.Scheduler, scheduler),
Effect.forkChild({ startImmediately: true })
)
assert.isTrue(wasYielded())
child.interruptUnsafe()
while (tasks.length > 0) tasks.shift()!()
yield* Fiber.await(child)
assert.isDefined(innerScope)
yield* Scope.addFinalizer(
innerScope!,
Effect.sync(() => {
finalized++
})
)
assert.strictEqual(finalized, 1)
assert.strictEqual(observed, original)
}))
it.effect("provideService restores the original context if interrupted before its body", () =>
Effect.gen(function*() {
class Value extends Context.Service<Value, number>()("InterruptedValue") {}
let original: Context.Context<never> | undefined
let observed: Context.Context<never> | undefined
const { scheduler, tasks, wasYielded } = interruptAfterContextChange((context) =>
original !== undefined && Context.getOrUndefined(context, Value) === 2
)
const child = yield* Effect.gen(function*() {
original = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context))
yield* Effect.provideService(Effect.void, Value, 2)
}).pipe(
Effect.onExit(() =>
Effect.withFiber((fiber) =>
Effect.sync(() => {
observed = fiber.context
})
)
),
Effect.provideService(Value, 1),
Effect.provideService(Scheduler.Scheduler, scheduler),
Effect.forkChild({ startImmediately: true })
)
assert.isTrue(wasYielded())
child.interruptUnsafe()
while (tasks.length > 0) tasks.shift()!()
yield* Fiber.await(child)
assert.strictEqual(observed, original)
}))
for (const cacheKind of ["cached", "cachedInvalidateWithTTL"] as const) {
it.effect(
cacheKind + " never strands a second caller when the owner yields before registering cleanup",
() =>
Effect.gen(function*() {
let interruptedBeforeSource = 0
// Sweep each scheduler boundary after entering the cache. A fixed
// step count can stop exercising the gap when the runtime adds an op.
for (let target = 1; target <= 32; target++) {
let armed = false
let seen = 0
let computed = 0
const tasks: Array<() => void> = []
const scheduler: Scheduler.Scheduler = {
executionMode: "async",
makeDispatcher: () => ({
scheduleTask: (task) => {
tasks.push(task)
},
flush() {}
}),
shouldYield() {
return armed && ++seen === target
}
}
const source = Effect.sync(() => ++computed)
const cached = cacheKind === "cached"
? yield* Effect.cached(source)
: (yield* Effect.cachedInvalidateWithTTL(source, "1 minute"))[0]
const owner = yield* Effect.gen(function*() {
yield* Effect.sync(() => {
armed = true
})
yield* cached
}).pipe(
Effect.provideService(Scheduler.Scheduler, scheduler),
Effect.forkChild({ startImmediately: true })
)
if (tasks.length === 0) continue // No boundary left before the source completed.
owner.interruptUnsafe()
while (tasks.length > 0) tasks.shift()!()
yield* Fiber.await(owner)
if (computed !== 0) continue
interruptedBeforeSource++
const waiter = yield* Effect.forkChild(cached, { startImmediately: true })
yield* Effect.yieldNow
const settled = waiter.pollUnsafe()
if (settled === undefined) waiter.interruptUnsafe()
assert.isDefined(settled, "cache waiter stuck after owner interruption at boundary " + target)
}
assert.isAbove(interruptedBeforeSource, 0, "the sweep never interrupted before the source")
})
)
}
})
it("isEffect", () => {
assert.isTrue(Effect.isEffect(Effect.succeed(0)))
assert.isFalse(Effect.isEffect([0]))
})
describe("Effectable.Class", () => {
it.effect("runs using asEffect", () =>
Effect.gen(function*() {
class Answer extends Effectable.Class<number> {
asEffect() {
return Effect.succeed(42)
}
}
assert.strictEqual(yield* new Answer(), 42)
}))
it.effect("evaluates asEffect on every execution", () =>
Effect.gen(function*() {
class Answer extends Effectable.Class<number> {
value = 1
asEffect() {
return Effect.succeed(this.value)
}
}
const answer = new Answer()
assert.strictEqual(yield* answer, 1)
answer.value = 2
assert.strictEqual(yield* answer, 2)
}))
})
describe("structural compare", () => {
it("should pass structural comparison", () => {
assert.deepEqual(Effect.succeed(0), Effect.succeed(0))
})
it("should fail structural comparison", () => {
assert.notDeepEqual(Effect.succeed(0), Effect.succeed(1))
})
})
describe("tracing", () => {
it("terminal root failure captures stack frame", () => {
const frame: References.StackFrame = {
name: "root frame",
stack: () => undefined,
parent: undefined
}
const failure = Exit.fail("boom")
const exit = Effect.runForkWith(Context.make(References.CurrentStackFrame, frame))(failure).pollUnsafe()
assert.isDefined(exit)
assert.isTrue(Exit.isFailure(exit!))
if (Exit.isFailure(exit!)) {
const trace = Context.getOrUndefined(Cause.annotations(exit.cause), Cause.StackTrace)
assert.strictEqual(trace, frame)
}
})
it.effect("caught sandboxed failure captures stack frame", () =>
Effect.gen(function*() {
const cause = yield* Effect.failCause(Cause.die(new Error("boom"))).pipe(
Effect.withSpan("test span"),
Effect.sandbox,
Effect.flip
)
const annotations = Cause.annotations(cause)
const trace = Context.getUnsafe(annotations, Cause.StackTrace)
assert.strictEqual(trace.name, "test span")
}))
it("failure without a current frame has no stack annotation and reuses the original exit", () => {
const failure = Exit.fail("boom")
const exit = Effect.runFork(failure).pollUnsafe()
assert.strictEqual(exit, failure)
assert.isTrue(Exit.isFailure(exit!))
if (Exit.isFailure(exit!)) {
assert.isUndefined(Context.getOrUndefined(Cause.annotations(exit.cause), Cause.StackTrace))
}
})
})
it("callback can branch over sync/async", async () => {
const program = Effect.callback<number>(function(resume) {
if (this.executionMode === "sync") {
resume(Effect.succeed(1))
} else {
Promise.resolve().then(() => resume(Effect.succeed(2)))
}
})
const isSync = Effect.runSync(program)
const isAsync = await Effect.runPromise(program)
assert.strictEqual(isSync, 1)
assert.strictEqual(isAsync, 2)
})
it("runPromise", async () => {
const result = await Effect.runPromise(Effect.succeed(1))
assert.strictEqual(result, 1)
})
it("acquireUseRelease interrupt", async () => {
let acquire = false
let use = false
let release = false
const fiber = Effect.acquireUseRelease(
Effect.sync(() => {
acquire = true
return 123
}).pipe(Effect.delay(100)),
() =>
Effect.sync(() => {
use = true
}),
(_) =>
Effect.sync(() => {
assert.strictEqual(_, 123)
release = true
})
).pipe(Effect.runFork)
fiber.interruptUnsafe()
const result = await Effect.runPromise(Fiber.await(fiber))
assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt()))
assert.isTrue(acquire)
assert.isFalse(use)
assert.isTrue(release)
})
it.effect("acquireUseRelease runs release when use throws", () =>
Effect.gen(function*() {
const resource = { id: 1 }
const defect = new Error("boom")
let releases = 0
let releasedResource: typeof resource | undefined
let releaseExit: Exit.Exit<never> | undefined
const exit = yield* Effect.acquireUseRelease(
Effect.succeed(resource),
(): Effect.Effect<never> => {
throw defect
},
(resource, exit) =>
Effect.sync(() => {
releases++
releasedResource = resource
releaseExit = exit
})
).pipe(Effect.exit)
assertExitDefect(exit, defect)
assert.strictEqual(releases, 1)
assert.strictEqual(releasedResource, resource)
assert.isDefined(releaseExit)
assertExitDefect(releaseExit, defect)
}))
it("acquireUseRelease uninterruptible", async () => {
let acquire = false
let use = false
let release = false
const fiber = Effect.acquireUseRelease(
Effect.sync(() => {
acquire = true
return 123
}).pipe(Effect.delay(100)),
(_) =>
Effect.sync(() => {
use = true
return _
}),
(_) =>
Effect.sync(() => {
assert.strictEqual(_, 123)
release = true
})
).pipe(Effect.uninterruptible, Effect.runFork)
fiber.interruptUnsafe()
const result = await Effect.runPromise(Fiber.await(fiber))
assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt()))
assert.isTrue(acquire)
assert.isTrue(use)
assert.isTrue(release)
})
it.effect("Context.Service", () =>
ATag.pipe(
Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
Effect.provideService(ATag, "A")
))
describe("fromOption", () => {
it.effect("from a some", () =>
Option.some("A").pipe(
Effect.fromOption,
Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A")))
))
it.effect("from a none", () =>
Option.none().pipe(
Effect.fromOption,
Effect.flip,
Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError)))
))
it.effect("from a none with a custom error", () =>
Effect.gen(function*() {
const error = new Error("Missing value")
const cause = yield* Effect.fromOption(Option.none(), () => error).pipe(Effect.flip)
assert.strictEqual(cause, error)
}))
it.effect("from a none with a custom error data-last", () =>
Effect.gen(function*() {
const error = new Error("Missing value")
const cause = yield* Option.none().pipe(
Effect.fromOption(() => error),
Effect.flip
)
assert.strictEqual(cause, error)
}))
})
describe("transposeOption", () => {
it.effect("transposes a none", () =>
Effect.gen(function*() {
const result = yield* Effect.transposeOption(Option.none())
assert.deepStrictEqual(result, Option.none())
}))
it.effect("transposes a some containing a success", () =>
Effect.gen(function*() {
const result = yield* Effect.transposeOption(Option.some(Effect.succeed("A")))
assert.deepStrictEqual(result, Option.some("A"))
}))
it.effect("transposes a some containing a failure", () =>
Effect.gen(function*() {
const error = yield* Effect.transposeOption(Option.some(Effect.fail("error"))).pipe(Effect.flip)
assert.strictEqual(error, "error")
}))
})
describe("fromResult", () => {
it.effect("from a success", () =>
Result.succeed("A").pipe(
Effect.fromResult,
Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A")))
))
it.effect("from a failure", () =>
Result.fail("error").pipe(
Effect.fromResult,
Effect.flip,
Effect.tap((error) => Effect.sync(() => assert.strictEqual(error, "error")))
))
})
describe("try", () => {
it.effect("succeeds with the returned value in direct-thunk form", () =>
Effect.gen(function*() {
const result = yield* Effect.try(() => 1)
assert.strictEqual(result, 1)
}))
it.effect("maps thrown values to UnknownError in direct-thunk form", () =>
Effect.gen(function*() {
const thrown = new Error("try")
const exit = yield* Effect.try<number>(() => {
throw thrown
}).pipe(Effect.exit)
assertUnknownError(exit, thrown, "An error occurred in Effect.try")
}))
it.effect("succeeds with the returned value", () =>
Effect.gen(function*() {
let catchCalled = false
const result = yield* Effect.try({
try: () => 1,
catch: () => {
catchCalled = true
return "error" as const
}
})
assert.strictEqual(result, 1)
assert.isFalse(catchCalled)
}))
it.effect("maps thrown values into typed failures", () =>
Effect.gen(function*() {
const thrown = new Error("try")
const mapped = { cause: thrown }
const exit = yield* Effect.try({
try: () => {
throw thrown
},
catch: () => mapped
}).pipe(Effect.exit)
assertExitFailure(exit, Cause.fail(mapped))
}))
it.effect("turns a throwing catch mapper into a defect", () =>
Effect.gen(function*() {
const thrown = new Error("try")
const defect = new Error("catch")
const exit = yield* Effect.try({
try: () => {
throw thrown
},
catch: () => {
throw defect
}
}).pipe(Effect.exit)
assertExitDefect(exit, defect)
}))
})
describe("tryPromise", () => {
it.effect("succeeds with the resolved value in direct-thunk form", () =>
Effect.gen(function*() {
const result = yield* Effect.tryPromise(() => Promise.resolve(1))
assert.strictEqual(result, 1)
}))
it.effect("does not allocate AbortController for zero-argument thunks", () =>
Effect.gen(function*() {
const originalAbortController = globalThis.AbortController
let allocations = 0
class TestAbortController extends originalAbortController {
constructor() {
allocations += 1
super()
}
}
yield* Effect.acquireUseRelease(
Effect.sync(() => {
globalThis.AbortController = TestAbortController
}),
() =>
Effect.gen(function*() {
const direct = yield* Effect.tryPromise(() => Promise.resolve(1))
const options = yield* Effect.tryPromise({
try: () => Promise.resolve(2),
catch: () => "error" as const
})
assert.strictEqual(direct, 1)
assert.strictEqual(options, 2)
assert.strictEqual(allocations, 0)
}),
() =>
Effect.sync(() => {
globalThis.AbortController = originalAbortController
})
)
}))
it.effect("maps synchronous throws to UnknownError in direct-thunk form", () =>
Effect.gen(function*() {
const thrown = new Error("try")
const exit = yield* Effect.tryPromise<number>(() => {
throw thrown
}).pipe(Effect.exit)
assertUnknownError(exit, thrown, "An error occurred in Effect.tryPromise")
}))
it.effect("maps promise rejections to UnknownError in direct-thunk form", () =>
Effect.gen(function*() {
const rejected = new Error("reject")
const exit = yield* Effect.tryPromise<number>(() => Promise.reject(rejected)).pipe(Effect.exit)
assertUnknownError(exit, rejected, "An error occurred in Effect.tryPromise")
}))
it.effect("succeeds with the resolved value in options form", () =>
Effect.gen(function*() {
let catchCalled = false
const result = yield* Effect.tryPromise({
try: () => Promise.resolve(1),
catch: () => {
catchCalled = true
return "error" as const
}
})
assert.strictEqual(result, 1)
assert.isFalse(catchCalled)
}))
it.effect("maps synchronous throws with catch in options form", () =>
Effect.gen(function*() {
const thrown = new Error("try")
const mapped = { cause: thrown }
const exit = yield* Effect.tryPromise({
try: () => {
throw thrown
},
catch: () => mapped
}).pipe(Effect.exit)
assertExitFailure(exit, Cause.fail(mapped))
}))
it.effect("maps promise rejections with catch in options form", () =>
Effect.gen(function*() {
const rejected = new Error("reject")
const mapped = { cause: rejected }
const exit = yield* Effect.tryPromise({
try: () => Promise.reject(rejected),
catch: () => mapped
}).pipe(Effect.exit)
assertExitFailure(exit, Cause.fail(mapped))
}))
it.effect("turns a throwing catch mapper for a synchronous throw into a defect", () =>
Effect.gen(function*() {
const thrown = new Error("try")
const defect = new Error("catch")
const exit = yield* Effect.tryPromise({
try: () => {
throw thrown
},
catch: () => {
throw defect
}
}).pipe(Effect.exit)
assertExitDefect(exit, defect)
}))
it.effect("turns a throwing catch mapper for a promise rejection into a defect", () =>
Effect.gen(function*() {
const rejected = new Error("reject")
const defect = new Error("catch")
const exit = yield* Effect.tryPromise({
try: () => Promise.reject(rejected),
catch: () => {
throw defect
}
}).pipe(Effect.exit)
assertExitDefect(exit, defect)
}))
it.effect("aborts the provided AbortSignal on interruption", () =>
Effect.gen(function*() {
let signal: AbortSignal | undefined
const fiber = yield* Effect.tryPromise((signal_) => {
signal = signal_
return new Promise<never>(() => {})
}).pipe(Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(fiber)
assert.strictEqual(signal?.aborted, true)
}))
})
describe("gen", () => {
it("gen", () =>
Effect.gen(function*() {
const result = yield* Effect.succeed(1)
assert.strictEqual(result, 1)
return result
}).pipe(Effect.runPromise).then((_) => assert.deepStrictEqual(_, 1)))
it("gen with context", () =>
Effect.gen({ self: { a: 1, b: 2 } }, function*() {
const result = yield* Effect.succeed(this.a)
assert.strictEqual(result, 1)
return result + this.b
}).pipe(Effect.runPromise).then((_) => assert.deepStrictEqual(_, 3)))
it("iterator yields the effect once, then completes with the sent value", () => {
const effect = Effect.succeed(1)
const iterator = effect[Symbol.iterator]()
const yielded = iterator.next()
const returned = iterator.next(2)
assert.deepStrictEqual({ ...yielded }, { value: effect, done: false })
assert.deepStrictEqual({ ...returned }, { value: 2, done: true })
assert.deepStrictEqual({ ...iterator.next(3) }, { value: 3, done: true })
})
})
describe("forEach", () => {
it.effect("sequential", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_))
assert.deepStrictEqual(results, [1, 2, 3])
}))
it.effect("unbounded", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_), { concurrency: "unbounded" })
assert.deepStrictEqual(results, [1, 2, 3])
}))
it.effect("bounded", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([1, 2, 3, 4, 5], (_) => Effect.succeed(_), { concurrency: 2 })
assert.deepStrictEqual(results, [1, 2, 3, 4, 5])
}))
it.effect("sequential interrupt", () =>
Effect.gen(function*() {
const done: Array<number> = []
const fiber = yield* Effect.forEach([1, 2, 3, 4, 5, 6], (i) =>
Effect.sync(() => {
done.push(i)
return i
}).pipe(Effect.delay(300))).pipe(Effect.forkChild)
yield* TestClock.adjust(800)
yield* Fiber.interrupt(fiber)
const result = yield* Fiber.await(fiber)
assert.isTrue(Exit.hasInterrupts(result))
assert.deepStrictEqual(done, [1, 2])
}))
it.effect("unbounded interrupt", () =>
Effect.gen(function*() {
const done: Array<number> = []
const fiber = yield* Effect.forEach([1, 2, 3], (i) =>
Effect.sync(() => {
done.push(i)
return i
}).pipe(Effect.delay(150)), { concurrency: "unbounded" }).pipe(Effect.forkChild)
yield* TestClock.adjust(50)
yield* Fiber.interrupt(fiber)
const result = yield* Fiber.await(fiber)
assert.isTrue(Exit.hasInterrupts(result))
assert.deepStrictEqual(done, [])
}))
it.effect("bounded interrupt", () =>
Effect.gen(function*() {
const done: Array<number> = []
const fiber = yield* Effect.forEach([1, 2, 3, 4, 5, 6], (i) =>
Effect.sync(() => {
done.push(i)
return i
}).pipe(Effect.delay(200)), { concurrency: 2 }).pipe(Effect.forkChild)
yield* TestClock.adjust(350)
yield* Fiber.interrupt(fiber)
const result = yield* Fiber.await(fiber)
assert.isTrue(Exit.hasInterrupts(result))
assert.deepStrictEqual(done, [1, 2])
}))
it.effect("preserves interruption when a child cleanup dies", () =>
Effect.gen(function*() {
const started = yield* Deferred.make<void>()
const defect = new Error("cleanup defect")
const fiber = yield* Effect.forEach([0, 1], (i) =>
i === 0
? Deferred.succeed(started, void 0).pipe(
Effect.andThen(Effect.callback<void>(() => Effect.die(defect)))
)
: Effect.never, { concurrency: 2 }).pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.await(started)
yield* Fiber.interrupt(fiber)
const exit = yield* Fiber.await(fiber)
assert.isTrue(Exit.hasInterrupts(exit))
assertExitDefect(exit, defect)
}))
it.effect("preserves a recorded sibling failure on external interruption", () =>
Effect.gen(function*() {
const started = yield* Deferred.make<void>()
const cleanupStarted = yield* Deferred.make<void>()
const releaseCleanup = yield* Deferred.make<void>()
const fiber = yield* Effect.forEach([0, 1], (i) =>
i === 0
? Deferred.succeed(started, void 0).pipe(
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Deferred.succeed(cleanupStarted, void 0).pipe(
Effect.andThen(Deferred.await(releaseCleanup))
)
)
)
: Deferred.await(started).pipe(Effect.andThen(Effect.fail("sibling failure"))), {
concurrency: 2
}).pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.await(cleanupStarted)
fiber.interruptUnsafe()
yield* Deferred.succeed(releaseCleanup, void 0)
const exit = yield* Fiber.await(fiber)
assert.isTrue(Exit.hasInterrupts(exit))
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
assert.deepStrictEqual(Cause.findError(exit.cause), Result.succeed("sibling failure"))
}
}))
it.effect("unbounded fail", () =>
Effect.gen(function*() {
const done: Array<number> = []
const handle = yield* Effect.forEach([1, 2, 3, 4, 5], (i) =>
Effect.suspend(() => {
done.push(i)
return i === 3 ? Effect.fail("error") : Effect.succeed(i)
}).pipe(Effect.delay(i * 100)), {
concurrency: "unbounded"
}).pipe(Effect.forkChild)
yield* TestClock.adjust(500)
const result = yield* Fiber.await(handle)
assert.deepStrictEqual(result, Exit.fail("error"))
assert.deepStrictEqual(done, [1, 2, 3])
}))
it.effect("interrupts started workers when the mapper throws during initial pumping", () =>
Effect.gen(function*() {
const interruptStarted = yield* Deferred.make<void>()
const interruptFinished = yield* Deferred.make<void>()
const defect = new Error("mapper defect")
const fiber = yield* Effect.forEach([0, 1], (i) => {
if (i === 0) {
return Effect.never.pipe(
Effect.onInterrupt(() =>
Deferred.succeed(interruptStarted, void 0).pipe(
Effect.andThen(Deferred.await(interruptFinished))
)
)
)
}
throw defect
}, { concurrency: 2 }).pipe(Effect.forkChild({ startImmediately: true }))
assert.isTrue(yield* Deferred.isDone(interruptStarted))
assert.isUndefined(fiber.pollUnsafe())
yield* Deferred.succeed(interruptFinished, void 0)
const exit = fiber.pollUnsafe()
assert.isDefined(exit)
assertExitDefect(exit!, defect)
}))
it.effect("preserves a sibling cleanup defect when the mapper throws", () =>
Effect.gen(function*() {
const mapperDefect = new Error("mapper defect")
const cleanupDefect = new Error("cleanup defect")
let cleanedUp = false
const exit = yield* Effect.forEach([0, 1], (i) => {
if (i === 0) {
return Effect.callback<void>(() =>
Effect.sync(() => {
cleanedUp = true
throw cleanupDefect
})
)
}
throw mapperDefect
}, { concurrency: 2 }).pipe(Effect.exit)
assert.isTrue(cleanedUp)
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
assert.deepStrictEqual(
exit.cause.reasons.filter(Cause.isDieReason).map((reason) => reason.defect),
[mapperDefect, cleanupDefect]
)
}
}))
it.effect("interrupts started workers when observer-driven refill throws", () =>
Effect.gen(function*() {
const release = yield* Deferred.make<void>()
const interruptStarted = yield* Deferred.make<void>()
const interruptFinished = yield* Deferred.make<void>()
const defect = new Error("refill defect")
const fiber = yield* Effect.forEach([0, 1, 2], (i) => {
if (i === 0) return Deferred.await(release)
if (i === 1) {
return Effect.never.pipe(
Effect.onInterrupt(() =>
Deferred.succeed(interruptStarted, void 0).pipe(
Effect.andThen(Deferred.await(interruptFinished))
)
)
)
}
throw defect
}, { concurrency: 2 }).pipe(Effect.forkChild({ startImmediately: true }))
assert.isUndefined(fiber.pollUnsafe())
yield* Deferred.succeed(release, void 0)
assert.isTrue(yield* Deferred.isDone(interruptStarted))
assert.isUndefined(fiber.pollUnsafe())
yield* Deferred.succeed(interruptFinished, void 0)
const exit = fiber.pollUnsafe()
assert.isDefined(exit)
assertExitDefect(exit!, defect)
}))
it.effect("length = 0", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([], (_) => Effect.succeed(_))
assert.deepStrictEqual(results, [])
}))
it.effect("string", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach("abc", (_) => Effect.succeed(_))
assert.deepStrictEqual(results, ["a", "b", "c"])
}))
})
describe("head", () => {
it.effect("returns the first element", () =>
Effect.gen(function*() {
const result = yield* Effect.head(Effect.succeed([1, 2, 3]))
assert.strictEqual(result, 1)
}))
it.effect("fails with NoSuchElementError for an empty iterable", () =>
Effect.gen(function*() {
const error = yield* Effect.head(Effect.succeed([] as Array<number>)).pipe(Effect.flip)
assert.isTrue(Cause.isNoSuchElementError(error))
}))
it.effect("preserves the source failure", () =>
Effect.gen(function*() {
const source: Effect.Effect<Iterable<number>, "failure"> = Effect.fail("failure")
const error = yield* Effect.head(source).pipe(Effect.flip)
assert.strictEqual(error, "failure")
}))
})
describe("all", () => {
it.effect("tuple", () =>
Effect.gen(function*() {
const results = (yield* Effect.all([
Effect.succeed(1),
Effect.succeed(2),
Effect.succeed(3)
])) satisfies [
number,
number,
number
]
assert.deepStrictEqual(results, [1, 2, 3])
}))
it.effect("record", () =>
Effect.gen(function*() {
const results = (yield* Effect.all({
a: Effect.succeed(1),
b: Effect.succeed("2"),
c: Effect.succeed(true)
})) satisfies {
a: number
b: string
c: boolean
}
assert.deepStrictEqual(results, {
a: 1,
b: "2",
c: true
})
}))
it.effect("record discard", () =>
Effect.gen(function*() {
const results = (yield* Effect.all({
a: Effect.succeed(1),
b: Effect.succeed("2"),
c: Effect.succeed(true)
}, { discard: true })) satisfies void
assert.deepStrictEqual(results, void 0)
}))
it.effect("iterable", () =>
Effect.gen(function*() {
const results = (yield* Effect.all(
new Set([
Effect.succeed(1),
Effect.succeed(2),
Effect.succeed(3)
])
)) satisfies Array<number>
assert.deepStrictEqual(results, [1, 2, 3])
}))
it.effect("tuple result mode", () =>
Effect.gen(function*() {
const executed: Array<number> = []
const results = (yield* Effect.all(
[
Effect.sync(() => {
executed.push(1)
return 1
}),
Effect.sync(() => {
executed.push(2)
}).pipe(Effect.andThen(Effect.fail("boom"))),
Effect.sync(() => {
executed.push(3)
return 3
})
] as const,
{ mode: "result" }
)) satisfies [
Result.Result<number, never>,
Result.Result<never, string>,
Result.Result<number, never>
]
assert.deepStrictEqual(executed, [1, 2, 3])
assert.deepStrictEqual(results, [
Result.succeed(1),
Result.fail("boom"),
Result.succeed(3)
])
}))
it.effect("record result mode", () =>
Effect.gen(function*() {
const results = (yield* Effect.all({
a: Effect.succeed(1),
b: Effect.fail("boom"),
c: Effect.succeed(true)
}, { mode: "result" })) satisfies {
a: Result.Result<number, never>
b: Result.Result<never, string>
c: Result.Result<boolean, never>
}
assert.deepStrictEqual(results, {
a: Result.succeed(1),
b: Result.fail("boom"),
c: Result.succeed(true)
})
}))
it.effect("concurrency interrupts started siblings on failure", () =>
Effect.gen(function*() {
const started: Array<number> = []
const interrupted: Array<number> = []
const make = (i: number) =>
Effect.suspend(() => {
started.push(i)
return Effect.sleep(500)
}).pipe(
Effect.as(i),
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted.push(i)
})
)
)
const fiber = yield* Effect.all([
make(1),
Effect.fail("boom").pipe(Effect.delay(100)),
make(3),
make(4)
], { concurrency: 3 }).pipe(Effect.forkChild)
yield* TestClock.adjust(100)
const result = yield* Fiber.await(fiber)
assert.deepStrictEqual(result, Exit.fail("boom"))
assert.deepStrictEqual(started, [1, 3])
assert.deepStrictEqual(interrupted, [1, 3])
}))
})
describe("partition", () => {
it.effect("collects only successes", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4]
const [passes, fails] = yield* Effect.partition(values, Effect.succeed)
assert.deepStrictEqual(passes, values)
assert.deepStrictEqual(fails, [])
}))
it.effect("collects only failures", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4]
const [passes, fails] = yield* Effect.partition(values, Effect.fail)
assert.deepStrictEqual(passes, [])
assert.deepStrictEqual(fails, values)
}))
it.effect("collects successes and failures", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4, 5]
const [passes, fails] = yield* Effect.partition(values, (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n))
assert.deepStrictEqual(passes, [1, 3, 5])
assert.deepStrictEqual(fails, [0, 2, 4])
}))
it.effect("supports concurrency option", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4, 5]
const [passes, fails] = yield* Effect.partition(
values,
(n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n),
{ concurrency: "unbounded" }
)
assert.deepStrictEqual(passes, [1, 3, 5])
assert.deepStrictEqual(fails, [0, 2, 4])
}))
})
describe("reduce", () => {
it.effect("runs sequentially from left to right and passes the index", () =>
Effect.gen(function*() {
const visited: Array<string> = []
const result = yield* Effect.reduce([1, 2, 3, 4, 5], () => 0, (acc, value, index) =>
Effect.sync(() => {
visited.push(`${index}:${value}`)
return acc + value
}))
assert.strictEqual(result, 15)
assert.deepStrictEqual(visited, ["0:1", "1:2", "2:3", "3:4", "4:5"])
}))
it.effect("stops at the first failure", () =>
Effect.gen(function*() {
const visited: Array<number> = []
const result = yield* pipe(
[1, 2, 3, 4, 5],
Effect.reduce(() => 0, (acc, value) =>
Effect.sync(() => visited.push(value)).pipe(
Effect.andThen(value === 3 ? Effect.fail("fail") : Effect.succeed(acc + value))
)),
Effect.exit
)
assert.deepStrictEqual(result, Exit.fail("fail"))
assert.deepStrictEqual(visited, [1, 2, 3])
}))
it.effect("returns zero for an empty iterable", () =>
Effect.gen(function*() {
const result = yield* Effect.reduce([], () => 42, (acc, value: number) => Effect.succeed(acc + value))
assert.strictEqual(result, 42)
}))
it.effect("evaluates zero lazily for each run", () =>
Effect.gen(function*() {
let evaluations = 0
const reduced = Effect.reduce([1], () => {
evaluations++
return [] as Array<number>
}, (acc, value) => Effect.succeed([...acc, value]))
assert.strictEqual(evaluations, 0)
assert.deepStrictEqual(yield* reduced, [1])
assert.deepStrictEqual(yield* reduced, [1])
assert.strictEqual(evaluations, 2)
}))
})
describe("validate", () => {
it.effect("collects successes when all effects succeed", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4]
const satisfying = yield* Effect.validate(values, Effect.succeed)
assert.deepStrictEqual(satisfying, values)
}))
it.effect("accumulates all failures", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4, 5]
const errors = yield* Effect.validate(values, (n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n)).pipe(
Effect.flip
)
assert.deepStrictEqual(errors, [0, 2, 4])
}))
it.effect("supports discard option", () =>
Effect.gen(function*() {
const result = yield* Effect.validate([1, 2, 3], Effect.succeed, { discard: true })
assert.strictEqual(result, undefined)
}))
})
describe("findFirst", () => {
it.effect("returns first matching element", () =>
Effect.gen(function*() {
const result = yield* Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 2))
assert.deepStrictEqual(result, Option.some(3))
}))
it.effect("returns none when no element matches", () =>
Effect.gen(function*() {
const result = yield* Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 10))
assert.deepStrictEqual(result, Option.none())
}))
it.effect("short-circuits on first match", () =>
Effect.gen(function*() {
const seen: Array<number> = []
const result = yield* Effect.findFirst([1, 2, 3, 4], (n) =>
Effect.sync(() => {
seen.push(n)
return n > 2
}))
assert.deepStrictEqual(result, Option.some(3))
assert.deepStrictEqual(seen, [1, 2, 3])
}))
})
describe("findFirstFilter", () => {
it.effect("returns first successful transformed value", () =>
Effect.gen(function*() {
const result = yield* Effect.findFirstFilter([1, 2, 3, 4], (n) =>
Effect.succeed(n % 2 === 0 ? Result.succeed(`n=${n}`) : Result.failVoid))
assert.deepStrictEqual(result, Option.some("n=2"))
}))
it.effect("passes index to filter", () =>
Effect.gen(function*() {
const result = yield* Effect.findFirstFilter([10, 11, 12], (n, i) =>
Effect.succeed(i === 2 ? Result.succeed(`${n}:${i}`) : Result.failVoid))
assert.deepStrictEqual(result, Option.some("12:2"))
}))
it.effect("returns none when filter never succeeds", () =>
Effect.gen(function*() {
const result = yield* Effect.findFirstFilter([1, 2, 3], () => Effect.succeed(Result.failVoid))
assert.deepStrictEqual(result, Option.none())
}))
})
describe("filter", () => {
it.live("odd numbers", () =>
Effect.gen(function*() {
const results = yield* Effect.filter([1, 2, 3, 4, 5], (value) => Effect.succeed(value % 2 === 1))
assert.deepStrictEqual(results, [1, 3, 5])
}))
it.live("iterable", () =>
Effect.gen(function*() {
const results = yield* Effect.filter(new Set([1, 2, 3, 4, 5]), (value) => Effect.succeed(value % 2 === 1))
assert.deepStrictEqual(results, [1, 3, 5])
}))
})
describe("acquireDisposable", () => {
it.effect("releases disposables", ({ expect }) =>
Effect.gen(function*() {
const acquire = Effect.sync((): Disposable => ({ [Symbol.dispose]: release }))
const release = vi.fn(() => void 0)
yield* Effect.scoped(Effect.acquireDisposable(acquire))
expect(release).toHaveBeenCalledTimes(1)
}))
it.effect("releases async disposables", ({ expect }) =>
Effect.gen(function*() {
const acquire = Effect.sync((): AsyncDisposable => ({ [Symbol.asyncDispose]: release }))
const release = vi.fn(async () => void 0)
yield* Effect.scoped(Effect.acquireDisposable(acquire))
expect(release).toHaveBeenCalledTimes(1)
}))
})
describe("acquireRelease", () => {
it.live("releases on interrupt", () =>
Effect.gen(function*() {
let release = false
const fiber = yield* Effect.acquireRelease(
Effect.delay(Effect.succeed("foo"), 100),
() =>
Effect.sync(() => {
release = true
})
).pipe(
Effect.scoped,
Effect.forkChild({ startImmediately: true })
)
fiber.interruptUnsafe()
yield* Fiber.await(fiber)
assert.strictEqual(release, true)
}))
it.effect("supports release dependencies", () =>
Effect.gen(function*() {
let release = false
const scope = yield* Scope.make()
yield* Scope.provide(scope)(
Effect.provideService(
Effect.acquireRelease(
Effect.succeed("foo"),
() =>
Effect.flatMap(Effect.service(ATag), () =>
Effect.sync(() => {
release = true
}))
),
ATag,
"A"
)
)
yield* Scope.close(scope, Exit.void)
assert.strictEqual(release, true)
}))
})
it.effect("raceAll", () =>
Effect.gen(function*() {
const interrupted: Array<number> = []
const fiber = yield* Effect.raceAll([500, 300, 200, 0, 100].map((ms) =>
(ms === 0 ? Effect.fail("boom") : Effect.succeed(ms)).pipe(
Effect.delay(ms),
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted.push(ms)
})
)
)
)).pipe(Effect.forkChild)
yield* TestClock.adjust("500 millis")
const result = yield* Fiber.join(fiber)
assert.strictEqual(result, 100)
assert.deepStrictEqual(interrupted, [500, 300, 200])
}))
it.effect("raceAllFirst", () =>
Effect.gen(function*() {
const interrupted: Array<number> = []
const fiber = yield* Effect.raceAllFirst([500, 300, 200, 0, 100].map((ms) =>
(ms === 0 ? Effect.fail("boom") : Effect.succeed(ms)).pipe(
Effect.delay(ms),
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted.push(ms)
})
)
)
)).pipe(Effect.exit, Effect.forkChild)
yield* TestClock.adjust("500 millis")
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, Exit.fail("boom"))
// 100 doesn't start because 0 finishes the race first
assert.deepStrictEqual(interrupted, [500, 300, 200])
}))
it.effect("race interrupts the loser when the other side succeeds", () =>
Effect.gen(function*() {
const interrupted: Array<string> = []
const onInterrupt = (label: string) =>
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted.push(label)
})
)
const fiber = yield* Effect.race(
Effect.succeed("fast").pipe(Effect.delay(100), onInterrupt("fast")),
Effect.succeed("slow").pipe(Effect.delay(500), onInterrupt("slow"))
).pipe(Effect.forkChild)
yield* TestClock.adjust("500 millis")
const result = yield* Fiber.join(fiber)
assert.strictEqual(result, "fast")
assert.deepStrictEqual(interrupted, ["slow"])
}))
it.effect("raceFirst interrupts the loser when the other side succeeds", () =>
Effect.gen(function*() {
const interrupted: Array<string> = []
const onInterrupt = (label: string) =>
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted.push(label)
})
)
const fiber = yield* Effect.raceFirst(
Effect.succeed("fast").pipe(Effect.delay(100), onInterrupt("fast")),
Effect.succeed("slow").pipe(Effect.delay(500), onInterrupt("slow"))
).pipe(Effect.forkChild)
yield* TestClock.adjust("500 millis")
const result = yield* Fiber.join(fiber)
assert.strictEqual(result, "fast")
assert.deepStrictEqual(interrupted, ["slow"])
}))
it.effect("raceFirst interrupts the loser when the other side fails", () =>
Effect.gen(function*() {
const interrupted: Array<string> = []
const fiber = yield* Effect.raceFirst(
Effect.fail("boom").pipe(Effect.delay(100)),
Effect.succeed("slow").pipe(
Effect.delay(500),
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted.push("slow")
})
)
)
).pipe(Effect.exit, Effect.forkChild)
yield* TestClock.adjust("500 millis")
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, Exit.fail("boom"))
assert.deepStrictEqual(interrupted, ["slow"])
}))
for (const [name, race] of [["race", Effect.race], ["raceFirst", Effect.raceFirst]] as const) {
it.effect(`${name} interrupts a loser that settles the race while starting`, () =>
Effect.gen(function*() {
let interrupted = false
const deferred = yield* Deferred.make<void>()
yield* race(
Deferred.await(deferred),
Deferred.succeed(deferred, void 0).pipe(
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted = true
})
)
)
)
assert.isTrue(interrupted)
}))
it.effect(`${name} interrupts the loser when interrupted while starting`, () =>
Effect.gen(function*() {
let interrupted = false
const fiber = yield* Effect.withFiber((parent) =>
race(
Effect.sync(() => parent.interruptUnsafe()).pipe(
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted = true
})
)
),
Effect.void
)
).pipe(Effect.forkChild)
yield* Fiber.await(fiber)
assert.isTrue(interrupted)
}))
}
describe("repeat", () => {
it.effect("is interruptible", () =>
Effect.gen(function*() {
const fiber = yield* Effect.void.pipe(
Effect.forever,
Effect.timeoutOption(50),
Effect.forkChild
)
yield* TestClock.adjust(50)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, Option.none())
}))
it.effect("repeat/until - repeats until a condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.repeat({ until: (n) => n === 0 })
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("repeat/until - repeats until an effectful condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.repeat({ until: (n) => Effect.succeed(n === 0) })
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("repeat/until - always evaluates at least once", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.sync(() => n++)
yield* Effect.repeat(increment, { until: constTrue })
assert.strictEqual(n, 1)
}))
it.effect("repeat/while - repeats while a condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.repeat({ while: (n) => n > 0 })
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("repeat/while - repeats while an effectful condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.repeat({ while: (n) => Effect.succeed(n > 0) })
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("repeat/while - always evaluates at least once", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.sync(() => n++)
yield* Effect.repeat(increment, { while: constFalse })
assert.strictEqual(n, 1)
}))
it.effect("repeat/times", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.sync(() => ++n)
const result = yield* Effect.repeat(increment, {
times: 2
})
assert.strictEqual(n, 3)
assert.strictEqual(result, 3)
}))
it.effect("repeat/schedule - repeats according to the specified schedule", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.sync(() => ++n)
const result = yield* Effect.repeat(increment, Schedule.recurs(3))
assert.strictEqual(result, 3)
}))
it.effect("repeat/schedule - with until", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.sync(() => ++n)
const result = yield* Effect.repeat(increment, {
schedule: Schedule.recurs(3),
until: (n) => n === 3
})
assert.strictEqual(n, 3)
assert.strictEqual(result, 3)
}))
it.effect("repeat/schedule - with while", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.sync(() => ++n)
const result = yield* Effect.repeat(increment, {
schedule: Schedule.recurs(3),
while: (n) => n < 3
})
assert.strictEqual(n, 3)
assert.strictEqual(result, 3) // schedule result
}))
})
describe("retry", () => {
it.live("nothing on success", () =>
Effect.gen(function*() {
let count = 0
yield* Effect.sync(() => count++).pipe(
Effect.retry({ times: 10000 })
)
assert.strictEqual(count, 1)
}))
it.effect("retry/until - retries until a condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.flip,
Effect.retry({ until: (n) => n === 0 }),
Effect.flip
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("retry/until - retries until an effectful condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.flip,
Effect.retry({ until: (n) => Effect.succeed(n === 0) }),
Effect.flip
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("retry/until - always evaluates at least once", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => n++)
yield* increment.pipe(
Effect.retry({ until: constTrue }),
Effect.flip
)
assert.strictEqual(n, 1)
}))
it.effect("retry/while - retries while a condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.flip,
Effect.retry({ while: (n) => n > 0 }),
Effect.flip
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("retry/while - retries while an effectful condition is true", () =>
Effect.gen(function*() {
let input = 10
let output = 0
const decrement = Effect.sync(() => --input)
const increment = Effect.sync(() => output++)
const result = yield* decrement.pipe(
Effect.tap(increment),
Effect.flip,
Effect.retry({ while: (n) => Effect.succeed(n > 0) }),
Effect.flip
)
assert.strictEqual(result, 0)
assert.strictEqual(output, 10)
}))
it.effect("retry/while - always evaluates at least once", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => n++)
yield* increment.pipe(
Effect.retry({ while: constFalse }),
Effect.flip
)
assert.strictEqual(n, 1)
}))
it.effect("retry/schedule - retries according to the specified schedule", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => n++)
yield* increment.pipe(
Effect.retry(Schedule.recurs(3)),
Effect.flip
)
assert.strictEqual(n, 4)
}))
it.effect("retry/schedule - with until", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => ++n)
yield* increment.pipe(
Effect.retry({
schedule: Schedule.recurs(3),
until: (n) => n === 3
}),
Effect.flip
)
assert.strictEqual(n, 3)
}))
it.effect("retry/schedule - until errors", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => ++n)
const result = yield* increment.pipe(
Effect.retry({
schedule: Schedule.recurs(3),
until: () => Effect.fail("boom")
}),
Effect.flip
)
assert.strictEqual(n, 1)
assert.strictEqual(result, "boom")
}))
it.effect("retry/schedule - with while", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => ++n)
yield* increment.pipe(
Effect.retry({
schedule: Schedule.recurs(3),
while: (n) => n < 3
}),
Effect.flip
)
assert.strictEqual(n, 3)
}))
it.effect("retry/schedule - while errors", () =>
Effect.gen(function*() {
let n = 0
const increment = Effect.failSync(() => ++n)
const result = yield* increment.pipe(
Effect.retry({
schedule: Schedule.recurs(3),
while: () => Effect.fail("boom")
}),
Effect.flip
)
assert.strictEqual(n, 1)
assert.strictEqual(result, "boom")
}))
it.effect("retry/schedule - CurrentMetadata", () =>
Effect.gen(function*() {
const metadata: Array<Schedule.Metadata> = []
yield* pipe(
Effect.gen(function*() {
const meta = yield* Schedule.CurrentMetadata
metadata.push(meta)
}),
Effect.flip,
Effect.retry(Schedule.recurs(3)),
Effect.flip
)
assert.deepStrictEqual(metadata, [
{
elapsed: 0,
elapsedSincePrevious: 0,
attempt: 0,
input: undefined,
output: undefined,
now: 0,
start: 0,
duration: Duration.zero
},
{
elapsed: 0,
elapsedSincePrevious: 0,
attempt: 1,
input: undefined,
output: 0,
now: 0,
start: 0,
duration: Duration.zero
},
{
elapsed: 0,
elapsedSincePrevious: 0,
attempt: 2,
input: undefined,
output: 1,
now: 0,
start: 0,
duration: Duration.zero
},
{
elapsed: 0,
elapsedSincePrevious: 0,
attempt: 3,
input: undefined,
output: 2,
now: 0,
start: 0,
duration: Duration.zero
}
])
}))
})
describe("timed", () => {
it.effect("uses monotonic time when wall time moves backward", () =>
Effect.gen(function*() {
yield* TestClock.setTime(1_000)
const [duration, result] = yield* Effect.gen(function*() {
yield* TestClock.adjust("100 millis")
yield* TestClock.setTime(0)
return "done"
}).pipe(Effect.timed)
assert.strictEqual(result, "done")
assert.strictEqual(Duration.toMillis(duration), 100)
}))
})
describe("timeoutOrElse", () => {
const forms = ["data-first", "pipe"] as const
for (const form of forms) {
it.live(`${form}: factory and body follow cleanup and replay`, () =>
Effect.gen(function*() {
let stopped = false
let factories = 0
let bodies = 0
const order: Array<string> = []
const value = { fallback: true }
const source = Effect.never.pipe(Effect.ensuring(Effect.sync(() => {
stopped = true
order.push("stopped")
})))
const options = {
duration: 0,
orElse: () => {
factories++
order.push(`factory:${stopped}`)
return Effect.sync(() => {
bodies++
order.push(`body:${stopped}`)
return value
})
}
}
const program = form === "data-first"
? Effect.timeoutOrElse(source, options)
: source.pipe(Effect.timeoutOrElse(options))
const before = [factories, bodies]
const first = yield* program
const firstStopped = stopped
stopped = false
const second = yield* program
assert.deepStrictEqual({
before,
factories,
bodies,
firstStopped,
stopped,
identity: [first === value, second === value],
order
}, {
before: [0, 0],
factories: 2,
bodies: 2,
firstStopped: true,
stopped: true,
identity: [true, true],
order: ["stopped", "factory:true", "body:true", "stopped", "factory:true", "body:true"]
})
}))
}
it.live("a ready source value is not confused with a zero-duration timeout", () =>
Effect.gen(function*() {
const value = Option.none()
let calls = 0
const result = yield* Effect.timeoutOrElse(Effect.succeed(value), {
duration: 0,
orElse: () => {
calls++
return Effect.succeed(Option.some("unexpected fallback"))
}
})
assert.deepStrictEqual([result, calls], [value, 0])
}))
it.live("slow fallback cannot let the original source win after timeout", () =>
Effect.gen(function*() {
const sourceGate = yield* Deferred.make<void>()
const sourceStopped = yield* Deferred.make<void>()
const fallbackStarted = yield* Deferred.make<void>()
const fallbackRelease = yield* Deferred.make<void>()
const sourceCompleted = yield* Ref.make(false)
const value = { fallback: true }
let fallbackInterrupted = false
const source = Deferred.await(sourceGate).pipe(
Effect.andThen(Ref.set(sourceCompleted, true)),
Effect.as({ source: true }),
Effect.ensuring(Deferred.succeed(sourceStopped, undefined))
)
const fiber = yield* Effect.timeoutOrElse(source, {
duration: 0,
orElse: () =>
Deferred.succeed(fallbackStarted, undefined).pipe(
Effect.andThen(Deferred.await(fallbackRelease)),
Effect.as(value),
Effect.onInterrupt(() =>
Effect.sync(() => {
fallbackInterrupted = true
})
)
)
}).pipe(Effect.forkChild)
yield* Deferred.await(fallbackStarted)
yield* Deferred.succeed(sourceGate, undefined)
yield* Deferred.await(sourceStopped)
yield* Deferred.succeed(fallbackRelease, undefined)
const completed = yield* Ref.get(sourceCompleted)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual([result === value, completed, fallbackInterrupted], [true, false, false])
}))
class SourceService extends Context.Service<SourceService, { source: string }>()("timeout-test/SourceService") {}
class FallbackService
extends Context.Service<FallbackService, { fallback: string }>()("timeout-test/FallbackService")
{}
const Local = Context.Reference<string>("timeout-test/Local", { defaultValue: () => "default" })
it.live("source and fallback inherit caller context", () =>
Effect.gen(function*() {
const sourceService = { source: "source service" }
const fallbackService = { fallback: "fallback service" }
const observed: Array<unknown> = []
const result = yield* Effect.gen(function*() {
const callerId = yield* Effect.fiberId
const parentSpan = yield* Effect.currentSpan
const source = Effect.gen(function*() {
observed.push(
(yield* SourceService) === sourceService,
yield* Local,
(yield* Effect.currentSpan) === parentSpan
)
return yield* Effect.never
}).pipe(Effect.provideService(Local, "source-local"))
const value = yield* Effect.timeoutOrElse(source, {
duration: 0,
orElse: () =>
Effect.gen(function*() {
const annotations = yield* References.CurrentLogAnnotations
observed.push(
(yield* Effect.fiberId) === callerId,
yield* Local,
(yield* Effect.currentSpan) === parentSpan,
annotations["request"]
)
return yield* FallbackService
})
})
observed.push(yield* Local, (yield* Effect.currentSpan) === parentSpan)
return value
}).pipe(
Effect.provideService(SourceService, sourceService),
Effect.provideService(FallbackService, fallbackService),
Effect.provideService(Local, "parent-local"),
Effect.annotateLogs({ request: "request-value" }),
Effect.withSpan("timeout-parent")
)
assert.deepStrictEqual([result === fallbackService, observed], [true, [
true,
"source-local",
true,
true,
"parent-local",
true,
"request-value",
"parent-local",
true
]])
}))
it.live("external interruption stops a pending fallback", () =>
Effect.gen(function*() {
const started = yield* Deferred.make<void>()
let sourceStopped = false
let fallbackStopped = false
let calls = 0
const fiber = yield* Effect.never.pipe(
Effect.ensuring(Effect.sync(() => {
sourceStopped = true
})),
Effect.timeoutOrElse({
duration: 0,
orElse: () => {
calls++
return Deferred.succeed(started, undefined).pipe(
Effect.andThen(Effect.never),
Effect.ensuring(Effect.sync(() => {
fallbackStopped = true
}))
)
}
}),
Effect.forkChild
)
yield* Deferred.await(started)
yield* Fiber.interrupt(fiber)
const exit = yield* Fiber.await(fiber)
assert.deepStrictEqual([Exit.hasInterrupts(exit), sourceStopped, fallbackStopped, calls], [true, true, true, 1])
}))
})
describe("timeoutOption", () => {
it.live("timeout a long computation", () =>
Effect.gen(function*() {
const result = yield* pipe(
Effect.sleep(60_000),
Effect.andThen(Effect.succeed(true)),
Effect.timeoutOption(10)
)
assert.deepStrictEqual(result, Option.none())
}))
it.live("timeout a long computation with a failure", () =>
Effect.gen(function*() {
const error = new Error("boom")
const result = yield* pipe(
Effect.sleep(5000),
Effect.andThen(Effect.succeed(true)),
Effect.timeoutOrElse({
orElse: () => Effect.die(error),
duration: 10
}),
Effect.sandbox,
Effect.flip
)
assert.deepStrictEqual(result, Cause.die(error))
}))
it.effect("timeout repetition of uninterruptible effect", () =>
Effect.gen(function*() {
const fiber = yield* pipe(
Effect.void,
Effect.uninterruptible,
Effect.forever,
Effect.timeoutOption(10),
Effect.forkChild
)
yield* TestClock.adjust(10)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, Option.none())
}))
it.effect("timeout in uninterruptible region", () =>
Effect.void.pipe(
Effect.timeoutOption(20_000),
Effect.uninterruptible
), { timeout: 1000 })
})
describe("timeout", () => {
it.effect("timeout produces a useful error message", () =>
Effect.gen(function*() {
const duration = Duration.millis(1500)
const fiber = yield* Effect.never.pipe(
Effect.timeout(duration),
Effect.flip,
Effect.forkChild
)
yield* TestClock.adjust(Duration.millis(2000))
const result = yield* Fiber.join(fiber)
assert.include(
result.toString(),
"TimeoutError: Operation timed out after '1s 500ms'"
)
}))
it.live("timeout a long computation", () =>
Effect.gen(function*() {
const result = yield* pipe(
Effect.sleep(60_000),
Effect.andThen(Effect.succeed(true)),
Effect.timeout(10),
Effect.flip
)
assert.deepStrictEqual(
result,
new Cause.TimeoutError("Operation timed out after '10ms'")
)
}))
it.effect("timeout interrupts the effect", () =>
Effect.gen(function*() {
let interrupted = false
const fiber = yield* Effect.never.pipe(
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted = true
})
),
Effect.timeout(10),
Effect.flip,
Effect.forkChild
)
yield* TestClock.adjust(10)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(
result,
new Cause.TimeoutError("Operation timed out after '10ms'")
)
assert.isTrue(interrupted)
}))
})
describe("interruption", () => {
it.effect("skipped handlers drop failures and keep defects when interrupted", () =>
Effect.gen(function*() {
const latch = yield* Latch.make()
const fiber = yield* latch.await.pipe(
Effect.andThen(Effect.failCause(Cause.combine(Cause.fail("error"), Cause.die("defect")))),
Effect.uninterruptible,
Effect.catchCause(() => Effect.void),
Effect.forkChild({ startImmediately: true })
)
fiber.interruptUnsafe(123)
yield* latch.open
const exit = yield* Fiber.await(fiber)
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
assert.isFalse(Cause.hasFails(exit.cause))
assert.isTrue(Cause.hasDies(exit.cause))
assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
}
}))
it.effect("uninterruptible handlers observe the interruption after skipped handlers", () =>
Effect.gen(function*() {
const latch = yield* Latch.make()
let result: Exit.Exit<void> | undefined
const fiber = yield* latch.await.pipe(
Effect.andThen(Effect.fail("error")),
Effect.uninterruptible,
Effect.catch(() => Effect.void),
Effect.interruptible,
Effect.exit,
Effect.map((exit) => {
result = exit
}),
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
fiber.interruptUnsafe(123)
yield* latch.open
yield* Fiber.await(fiber)
assert.isTrue(result !== undefined && Exit.isFailure(result) && Cause.hasInterruptsOnly(result.cause))
}))
it.effect("handlers inside an uninterruptible region recover with interruption pending", () =>
Effect.gen(function*() {
const latch = yield* Latch.make()
let recovered = false
const fiber = yield* latch.await.pipe(
Effect.andThen(Effect.fail("error")),
Effect.catch(() =>
Effect.sync(() => {
recovered = true
})
),
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
fiber.interruptUnsafe(123)
yield* latch.open
const exit = yield* Fiber.await(fiber)
assert.isTrue(recovered)
assert.isTrue(Exit.hasInterrupts(exit))
}))
it("a map callback that interrupts its own fiber skips the next map", () => {
let ran = false
const exit = Effect.runSyncExit(
Effect.sync(() => 0).pipe(
Effect.map(() => Fiber.getCurrent()!.interruptUnsafe()),
Effect.map(() => {
ran = true
})
)
)
assert.isFalse(ran)
assert.isTrue(Exit.hasInterrupts(exit))
})
it.effect("sync forever is interruptible", () =>
Effect.gen(function*() {
const fiber = yield* pipe(Effect.succeed(1), Effect.forever, Effect.forkChild)
yield* Fiber.interrupt(fiber)
assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
}))
it.effect("interrupt of never is interrupted with cause", () =>
Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.never)
yield* Fiber.interrupt(fiber)
assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
}))
it.effect("catch + ensuring + interrupt", () =>
Effect.gen(function*() {
let catchFailure = false
let ensuring = false
const handle = yield* Effect.never.pipe(
Effect.catchCause((_) =>
Effect.sync(() => {
catchFailure = true
})
),
Effect.ensuring(Effect.sync(() => {
ensuring = true
})),
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(handle)
assert.isFalse(catchFailure)
assert.isTrue(ensuring)
}))
it.effect("run of interruptible", () =>
Effect.gen(function*() {
let recovered = false
const fiber = yield* Effect.never.pipe(
Effect.interruptible,
Effect.exit,
Effect.flatMap((result) =>
Effect.sync(() => {
recovered = result._tag === "Failure" && Cause.hasInterruptsOnly(result.cause)
})
),
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.isTrue(recovered)
}))
it.effect("alternating interruptibility", () =>
Effect.gen(function*() {
let counter = 0
const fiber = yield* Effect.never.pipe(
Effect.interruptible,
Effect.exit,
Effect.andThen(Effect.sync(() => {
counter++
})),
Effect.uninterruptible,
Effect.interruptible,
Effect.exit,
Effect.andThen(Effect.sync(() => {
counter++
})),
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.strictEqual(counter, 2)
}))
it.live("acquireUseRelease use inherits interrupt status", () =>
Effect.gen(function*() {
let ref = false
const fiber = yield* Effect.acquireUseRelease(
Effect.succeed(123),
(_) =>
Effect.sync(() => {
ref = true
}).pipe(
Effect.delay(10)
),
() => Effect.void
).pipe(
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.isTrue(ref)
}))
it.effect("acquireUseRelease release runs when use is interrupted", () =>
Effect.gen(function*() {
let acquired = false
let releaseExit: Exit.Exit<never> | undefined
const fiber = yield* Effect.acquireUseRelease(
Effect.sync(() => {
acquired = true
return 123
}),
() => Effect.never,
(resource, exit) =>
Effect.sync(() => {
assert.strictEqual(resource, 123)
releaseExit = exit
})
).pipe(Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(fiber)
assert.isTrue(acquired)
assert.isDefined(releaseExit)
assert.isTrue(Exit.hasInterrupts(releaseExit!))
assert(Exit.hasInterrupts(fiber.pollUnsafe()!))
}))
it.live("async can be uninterruptible", () =>
Effect.gen(function*() {
let ref = false
const fiber = yield* Effect.sleep(10).pipe(
Effect.andThen(Effect.sync(() => {
ref = true
})),
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.isTrue(ref)
}))
it.live("callback cannot resume on interrupt", () =>
Effect.gen(function*() {
const fiber = yield* Effect.callback<string>((resume) => {
setTimeout(() => {
resume(Effect.succeed("foo"))
}, 10)
}).pipe(
Effect.onInterrupt(() => Effect.sleep(30)),
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.isTrue(Exit.hasInterrupts(fiber.pollUnsafe()!))
}))
it.live("closing scope is uninterruptible", () =>
Effect.gen(function*() {
let ref = false
const child = pipe(
Effect.sleep(10),
Effect.andThen(Effect.sync(() => {
ref = true
}))
)
const fiber = yield* child.pipe(Effect.uninterruptible, Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(fiber)
assert.isTrue(ref)
}))
it.effect("AbortSignal is aborted", () =>
Effect.gen(function*() {
let signal: AbortSignal
const fiber = yield* Effect.callback<void>((_cb, signal_) => {
signal = signal_
}).pipe(Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(fiber)
assert.strictEqual(signal!.aborted, true)
}))
it.effect("callback cleanup effect runs on interrupt", () =>
Effect.gen(function*() {
let cleanedUp = false
const fiber = yield* Effect.callback<void>((_resume) =>
Effect.sync(() => {
cleanedUp = true
})
).pipe(Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(fiber)
assert.isTrue(cleanedUp)
}))
describe("uninterruptibleMask", () => {
it.effect("defers a pending interrupt until the masked region completes", () =>
Effect.gen(function*() {
const masked = yield* Latch.make()
const resume = yield* Latch.make()
const events: Array<string> = []
const child = yield* Effect.uninterruptibleMask(() =>
Effect.gen(function*() {
yield* masked.open
yield* resume.await
events.push("masked region completed")
})
).pipe(Effect.forkChild({ startImmediately: true }))
yield* masked.await
events.push("masked")
yield* Effect.sync(() => {
child.interruptUnsafe(123)
events.push("interrupted")
})
assert.isUndefined(child.pollUnsafe())
yield* resume.open
events.push("resumed")
yield* Effect.yieldNow
yield* Effect.yieldNow
const exit = child.pollUnsafe()
if (exit === undefined) {
return assert.fail("fiber did not exit after the masked region completed")
}
assert.isTrue(Exit.hasInterrupts(exit))
if (exit._tag !== "Failure") {
return assert.fail("expected interrupted fiber to exit with failure")
}
assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
assert.deepStrictEqual(events, ["masked", "interrupted", "resumed", "masked region completed"])
}))
it.effect("delivers a pending interrupt when restore re-enables interruptibility", () =>
Effect.gen(function*() {
const masked = yield* Latch.make()
const resume = yield* Latch.make()
const events: Array<string> = []
const child = yield* Effect.uninterruptibleMask((restore) =>
Effect.gen(function*() {
yield* masked.open
yield* resume.await
// the pending interrupt is delivered at the restore boundary, so
// the restored effect never runs
return yield* restore(Effect.suspend(() => {
events.push("restored")
return Effect.never
}))
})
).pipe(Effect.forkChild({ startImmediately: true }))
yield* masked.await
events.push("masked")
yield* Effect.sync(() => {
child.interruptUnsafe(123)
events.push("interrupted")
})
assert.isUndefined(child.pollUnsafe())
yield* resume.open
events.push("resumed")
yield* Effect.yieldNow
yield* Effect.yieldNow
const exit = child.pollUnsafe()
if (exit === undefined) {
return assert.fail("fiber did not exit after restore re-enabled interruptibility")
}
assert.isTrue(Exit.hasInterrupts(exit))
if (exit._tag !== "Failure") {
return assert.fail("expected interrupted fiber to exit with failure")
}
assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
assert.deepStrictEqual(events, ["masked", "interrupted", "resumed"])
}))
it.effect("region after a restored section stays uninterruptible", () =>
Effect.gen(function*() {
const afterRestore = yield* Latch.make()
const resume = yield* Latch.make()
const events: Array<string> = []
const child = yield* Effect.uninterruptibleMask((restore) =>
Effect.gen(function*() {
yield* restore(Effect.sync(() => {
events.push("restored section completed")
}))
yield* afterRestore.open
yield* resume.await
events.push("masked region completed")
})
).pipe(Effect.forkChild({ startImmediately: true }))
yield* afterRestore.await
events.push("after restore")
yield* Effect.sync(() => {
child.interruptUnsafe(123)
events.push("interrupted")
})
assert.isUndefined(child.pollUnsafe())
yield* resume.open
events.push("resumed")
yield* Effect.yieldNow
yield* Effect.yieldNow
const exit = child.pollUnsafe()
if (exit === undefined) {
return assert.fail("fiber did not exit after the masked region completed")
}
assert.isTrue(Exit.hasInterrupts(exit))
if (exit._tag !== "Failure") {
return assert.fail("expected interrupted fiber to exit with failure")
}
assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123]))
assert.deepStrictEqual(events, [
"restored section completed",
"after restore",
"interrupted",
"resumed",
"masked region completed"
])
}))
})
})
describe("awaitAllChildren", () => {
it.effect("completes immediately when no children are forked", () =>
Effect.gen(function*() {
const result = yield* Effect.succeed(1).pipe(Effect.awaitAllChildren)
assert.strictEqual(result, 1)
}))
it.effect("awaits children forked by the wrapped effect", () =>
Effect.gen(function*() {
const latch = yield* Deferred.make<void>()
const fiber = yield* Effect.gen(function*() {
yield* Deferred.await(latch).pipe(Effect.forkChild)
return 1
}).pipe(
Effect.awaitAllChildren,
Effect.forkChild({ startImmediately: true })
)
yield* Effect.yieldNow
assert.strictEqual(fiber.pollUnsafe(), undefined)
yield* Deferred.succeed(latch, void 0)
const result = yield* Fiber.join(fiber)
assert.strictEqual(result, 1)
}))
it.effect("can be interrupted while awaiting children after success", () =>
Effect.gen(function*() {
const childStarted = yield* Deferred.make<void>()
const childFiber = yield* Deferred.make<Fiber.Fiber<void>>()
const releaseChild = yield* Deferred.make<void>()
const fiber = yield* Effect.gen(function*() {
const child = yield* Effect.gen(function*() {
yield* Deferred.succeed(childStarted, void 0)
yield* Deferred.await(releaseChild)
}).pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.succeed(childFiber, child)
return 1
}).pipe(
Effect.awaitAllChildren,
Effect.forkChild({ startImmediately: true })
)
const child = yield* Deferred.await(childFiber)
yield* Deferred.await(childStarted)
fiber.interruptUnsafe()
yield* Effect.yieldNow
yield* Effect.yieldNow
const exit = fiber.pollUnsafe()
const childExit = child.pollUnsafe()
yield* Deferred.succeed(releaseChild, void 0)
yield* Fiber.await(fiber)
assert(exit !== undefined && Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause))
assert(childExit !== undefined && Exit.isFailure(childExit) && Cause.hasInterruptsOnly(childExit.cause))
}))
it.effect("can be interrupted while awaiting children after failure", () =>
Effect.gen(function*() {
const childStarted = yield* Deferred.make<void>()
const childFiber = yield* Deferred.make<Fiber.Fiber<void>>()
const releaseChild = yield* Deferred.make<void>()
const fiber = yield* Effect.gen(function*() {
const child = yield* Effect.gen(function*() {
yield* Deferred.succeed(childStarted, void 0)
yield* Deferred.await(releaseChild)
}).pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.succeed(childFiber, child)
return yield* Effect.fail("boom")
}).pipe(
Effect.awaitAllChildren,
Effect.forkChild({ startImmediately: true })
)
const child = yield* Deferred.await(childFiber)
yield* Deferred.await(childStarted)
fiber.interruptUnsafe()
yield* Effect.yieldNow
yield* Effect.yieldNow
const exit = fiber.pollUnsafe()
const childExit = child.pollUnsafe()
yield* Deferred.succeed(releaseChild, void 0)
yield* Fiber.await(fiber)
assert(exit !== undefined && Exit.isFailure(exit))
assert.isTrue(Cause.hasInterrupts(exit.cause))
assert.deepStrictEqual(Cause.findError(exit.cause), Result.succeed("boom"))
assert(childExit !== undefined && Exit.isFailure(childExit) && Cause.hasInterruptsOnly(childExit.cause))
}))
it.effect("respects an enclosing uninterruptible region while awaiting children", () =>
Effect.gen(function*() {
const childStarted = yield* Deferred.make<void>()
const childFiber = yield* Deferred.make<Fiber.Fiber<void>>()
const releaseChild = yield* Deferred.make<void>()
const fiber = yield* Effect.gen(function*() {
const child = yield* Effect.gen(function*() {
yield* Deferred.succeed(childStarted, void 0)
yield* Deferred.await(releaseChild)
}).pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.succeed(childFiber, child)
return 1
}).pipe(
Effect.awaitAllChildren,
Effect.uninterruptible,
Effect.forkChild({ startImmediately: true })
)
const child = yield* Deferred.await(childFiber)
yield* Deferred.await(childStarted)
fiber.interruptUnsafe()
yield* Effect.yieldNow
yield* Effect.yieldNow
assert.strictEqual(fiber.pollUnsafe(), undefined)
assert.strictEqual(child.pollUnsafe(), undefined)
yield* Deferred.succeed(releaseChild, void 0)
const exit = yield* Fiber.await(fiber)
assert(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause))
}))
it.effect("does not await children forked outside the wrapped effect", () =>
Effect.gen(function*() {
const preexisting = yield* Effect.never.pipe(Effect.forkChild({ startImmediately: true }))
const fiber = yield* Effect.succeed(1).pipe(
Effect.awaitAllChildren,
Effect.forkChild({ startImmediately: true })
)
yield* Effect.yieldNow
assert.deepStrictEqual(fiber.pollUnsafe(), Exit.succeed(1))
yield* Fiber.interrupt(preexisting)
}))
it.effect("does not await preexisting children", () =>
Effect.gen(function*() {
const preexisting = yield* Effect.never.pipe(Effect.forkChild({ startImmediately: true }))
const result = yield* Effect.succeed(1).pipe(Effect.awaitAllChildren)
assert.strictEqual(result, 1)
yield* Fiber.interrupt(preexisting)
}))
it.effect("does not await preexisting children in the same fiber", () =>
Effect.gen(function*() {
const preexistingLatch = yield* Deferred.make<void>()
const scopedLatch = yield* Deferred.make<void>()
const fiber = yield* Effect.gen(function*() {
yield* Deferred.await(preexistingLatch).pipe(Effect.forkChild)
return yield* Effect.gen(function*() {
yield* Deferred.await(scopedLatch).pipe(Effect.forkChild)
return 1
}).pipe(Effect.awaitAllChildren)
}).pipe(
Effect.forkChild({ startImmediately: true })
)
yield* Effect.yieldNow
assert.strictEqual(fiber.pollUnsafe(), undefined)
yield* Deferred.succeed(scopedLatch, void 0)
yield* Effect.yieldNow
assert.deepStrictEqual(fiber.pollUnsafe(), Exit.succeed(1))
}))
})
describe("fork", () => {
it.effect("is interrupted with parent", () =>
Effect.gen(function*() {
let child = false
let parent = false
const fiber = yield* Effect.never.pipe(
Effect.onInterrupt(() =>
Effect.sync(() => {
child = true
})
),
Effect.forkChild({ startImmediately: true }),
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Effect.sync(() => {
parent = true
})
),
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.isTrue(child)
assert.isTrue(parent)
}))
})
describe("forkDaemon", () => {
it.effect("is not interrupted with parent", () =>
Effect.gen(function*() {
let child = false
let parent = false
const handle = yield* Effect.never.pipe(
Effect.onInterrupt(() =>
Effect.sync(() => {
child = true
})
),
Effect.forkDetach,
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Effect.sync(() => {
parent = true
})
),
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(handle)
assert.isFalse(child)
assert.isTrue(parent)
}))
})
describe("forkIn", () => {
it.effect("is interrupted when scope is closed", () =>
Effect.gen(function*() {
let interrupted = false
const scope = yield* Scope.make()
yield* Effect.never.pipe(
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted = true
})
),
Effect.forkIn(scope, { startImmediately: true })
)
yield* Scope.close(scope, Exit.void)
assert.isTrue(interrupted)
}))
})
describe("forkScoped", () => {
it.effect("is interrupted when scope is closed", () =>
Effect.gen(function*() {
let interrupted = false
const scope = yield* Scope.make()
yield* Effect.never.pipe(
Effect.onInterrupt(() =>
Effect.sync(() => {
interrupted = true
})
),
Effect.forkScoped({ startImmediately: true }),
Scope.provide(scope)
)
yield* Scope.close(scope, Exit.void)
assert.isTrue(interrupted)
}))
})
describe("do notation", () => {
it.effect("works", () =>
Effect.succeed(1).pipe(
Effect.bindTo("a"),
Effect.let("b", ({ a }) => a + 1),
Effect.bind("b", ({ b }) => Effect.succeed(b.toString())),
Effect.tap((value) =>
Effect.sync(() => {
assert.deepStrictEqual(value, {
a: 1,
b: "2"
})
})
)
))
})
describe("stack safety", () => {
it.live("recursion", () => {
const loop: Effect.Effect<void> = Effect.void.pipe(
Effect.flatMap((_) => loop)
)
return loop.pipe(
Effect.timeoutOption(50)
)
})
it("lazily nested map continuations", () => {
const loop = (n: number): Effect.Effect<number> =>
n === 0 ? Effect.succeed(0) : Effect.map(Effect.suspend(() => loop(n - 1)), (n) => n + 1)
assert.strictEqual(Effect.runSync(loop(100_000)), 100_000)
})
})
describe("finalization", () => {
const ExampleError = new Error("Oh noes!")
const throwing = (): never => {
throw "finalizer defect"
}
it.effect("onExit preserves the original failure when the finalizer throws", () =>
Effect.gen(function*() {
const result = yield* Effect.fail("body failure").pipe(Effect.onExit(throwing), Effect.exit)
assert.deepStrictEqual(
result,
Exit.failCause(Cause.combine(Cause.fail("body failure"), Cause.die("finalizer defect")))
)
}))
it.effect("onExit reports a thrown finalizer on success", () =>
Effect.gen(function*() {
const result = yield* Effect.succeed(1).pipe(Effect.onExit(throwing), Effect.exit)
assert.deepStrictEqual(result, Exit.die("finalizer defect"))
}))
it.effect("an outer onInterrupt still sees the interruption after an inner finalizer throws", () =>
Effect.gen(function*() {
let finalized = false
const result = yield* Effect.interrupt.pipe(
Effect.onExit(throwing),
Effect.onInterrupt(() =>
Effect.sync(() => {
finalized = true
})
),
Effect.exit
)
assert.isTrue(finalized)
assert.isTrue(Exit.isFailure(result))
if (Exit.isFailure(result)) {
assert.deepStrictEqual(result.cause.reasons.map((reason) => reason._tag), ["Interrupt", "Die"])
assert.deepStrictEqual(result.cause.reasons[1], Cause.die("finalizer defect").reasons[0])
}
}))
it("nested throwing finalizers do not overflow the stack", () => {
let program: Effect.Effect<unknown> = Effect.succeed(1)
for (let i = 0; i < 20_000; i++) {
program = Effect.onExit(Effect.exit(program), throwing)
}
assert.deepStrictEqual(Effect.runSyncExit(program), Exit.die("finalizer defect"))
})
it.effect("fail ensuring", () =>
Effect.gen(function*() {
let finalized = false
const result = yield* Effect.fail(ExampleError).pipe(
Effect.ensuring(Effect.sync(() => {
finalized = true
})),
Effect.exit
)
assert.deepStrictEqual(result, Exit.fail(ExampleError))
assert.isTrue(finalized)
}))
it.effect("fail on error", () =>
Effect.gen(function*() {
let finalized = false
const result = yield* Effect.fail(ExampleError).pipe(
Effect.onError(() =>
Effect.sync(() => {
finalized = true
})
),
Effect.exit
)
assert.deepStrictEqual(result, Exit.fail(ExampleError))
assert.isTrue(finalized)
}))
it.effect("finalizer errors merged", () =>
Effect.gen(function*() {
const e2 = new Error("e2")
const e3 = new Error("e3")
const result = yield* pipe(
Effect.fail(ExampleError),
Effect.ensuring(Effect.die(e2)),
Effect.ensuring(Effect.die(e3)),
Effect.sandbox,
Effect.flip,
Effect.map((cause) => cause)
)
assert.deepStrictEqual(
result,
Cause.combine(Cause.combine(Cause.fail(ExampleError), Cause.die(e2)), Cause.die(e3))
)
}))
it.effect("finalizer errors reported", () =>
Effect.gen(function*() {
let reported: Exit.Exit<number> | undefined
const result = yield* pipe(
Effect.succeed(42),
Effect.ensuring(Effect.die(ExampleError)),
Effect.forkChild,
Effect.flatMap((fiber) =>
pipe(
Fiber.await(fiber),
Effect.flatMap((e) =>
Effect.sync(() => {
reported = e
})
)
)
)
)
assert.isUndefined(result)
assert.isFalse(reported !== undefined && Exit.isSuccess(reported))
}))
it.effect("scoped combines usage and finalizer failures", () =>
Effect.gen(function*() {
const result = yield* Effect.gen(function*() {
yield* Effect.addFinalizer(() => Effect.die("finalizer failure"))
return yield* Effect.fail("usage failure")
}).pipe(Effect.scoped, Effect.exit)
assert.deepStrictEqual(
result,
Exit.failCause(Cause.combine(Cause.fail("usage failure"), Cause.die("finalizer failure")))
)
}))
it.effect("acquireUseRelease usage result", () =>
Effect.gen(function*() {
const result = yield* Effect.acquireUseRelease(
Effect.void,
() => Effect.succeed(42),
() => Effect.void
)
assert.strictEqual(result, 42)
}))
it.effect("error in just acquisition", () =>
Effect.gen(function*() {
const result = yield* pipe(
Effect.acquireUseRelease(
Effect.fail(ExampleError),
() => Effect.void,
() => Effect.void
),
Effect.exit
)
assert.deepStrictEqual(result, Exit.fail(ExampleError))
}))
it.effect("error in just release", () =>
Effect.gen(function*() {
const result = yield* pipe(
Effect.acquireUseRelease(
Effect.void,
() => Effect.void,
() => Effect.die(ExampleError)
),
Effect.exit
)
assert.deepStrictEqual(result, Exit.die(ExampleError))
}))
it.effect("error in just usage", () =>
Effect.gen(function*() {
const result = yield* pipe(
Effect.acquireUseRelease(
Effect.void,
() => Effect.fail(ExampleError),
() => Effect.void
),
Effect.exit
)
assert.deepStrictEqual(result, Exit.fail(ExampleError))
}))
it.effect("combines usage and release failures", () =>
Effect.gen(function*() {
const result = yield* Effect.acquireUseRelease(
Effect.void,
() => Effect.fail("usage failure"),
() => Effect.fail("release failure")
).pipe(Effect.exit)
assert.deepStrictEqual(
result,
Exit.failCause(Cause.combine(Cause.fail("usage failure"), Cause.fail("release failure")))
)
}))
it.effect("rethrown caught error in acquisition", () =>
Effect.gen(function*() {
const result = yield* Effect.acquireUseRelease(
Effect.fail(ExampleError),
() => Effect.void,
() => Effect.void
).pipe(Effect.flip)
assert.deepEqual(result, ExampleError)
}))
it.effect("rethrown caught error in release", () =>
Effect.gen(function*() {
const result = yield* pipe(
Effect.acquireUseRelease(
Effect.void,
() => Effect.void,
() => Effect.die(ExampleError)
),
Effect.exit
)
assert.deepStrictEqual(result, Exit.die(ExampleError))
}))
it.effect("rethrown caught error in usage", () =>
Effect.gen(function*() {
const result = yield* Effect.acquireUseRelease(
Effect.void,
() => Effect.fail(ExampleError),
() => Effect.void
).pipe(Effect.exit)
assert.deepEqual(result, Exit.fail(ExampleError))
}))
it.effect("onResult - ensures that a cleanup function runs when an effect fails", () =>
Effect.gen(function*() {
let ref = false
yield* Effect.die("boom").pipe(
Effect.onExit((result) =>
Exit.hasDies(result) ?
Effect.sync(() => {
ref = true
}) :
Effect.void
),
Effect.sandbox,
Effect.ignore
)
assert.isTrue(ref)
}))
it.effect("onExit - callback observes interrupt exit when the fiber is interrupted", () =>
Effect.gen(function*() {
let observedInterrupt = false
const fiber = yield* Effect.never.pipe(
Effect.onExit((exit) =>
Effect.sync(() => {
observedInterrupt = Exit.hasInterrupts(exit)
})
),
Effect.forkChild({ startImmediately: true })
)
yield* Fiber.interrupt(fiber)
assert.isTrue(observedInterrupt)
}))
})
describe("Effect.ignore", () => {
type IgnoreOptions = { readonly log?: boolean | LogLevel.Severity; readonly message?: string }
const makeTestLogger = () => {
const capturedLogs: Array<{
readonly logLevel: LogLevel.LogLevel
readonly cause: Cause.Cause<unknown>
readonly message: unknown
}> = []
const testLogger = Logger.make<unknown, void>((options) => {
capturedLogs.push({ logLevel: options.logLevel, cause: options.cause, message: options.message })
})
return { capturedLogs, testLogger }
}
const runIgnore = (options?: IgnoreOptions, currentLogLevel: LogLevel.Severity = "Info") =>
Effect.gen(function*() {
const { capturedLogs, testLogger } = makeTestLogger()
const program = options === undefined
? Effect.fail("boom").pipe(Effect.ignore)
: Effect.fail("boom").pipe(Effect.ignore(options))
yield* program.pipe(
Effect.provide(Logger.layer([testLogger])),
Effect.provideService(References.MinimumLogLevel, "Trace"),
Effect.provideService(References.CurrentLogLevel, currentLogLevel)
)
return capturedLogs
})
it.effect("does not log when log is omitted", () =>
Effect.gen(function*() {
const logs = yield* runIgnore()
assert.strictEqual(logs.length, 0)
}))
it.effect("does not log when log is false", () =>
Effect.gen(function*() {
const logs = yield* runIgnore({ log: false })
assert.strictEqual(logs.length, 0)
}))
it.effect("logs with the current level when log is true", () =>
Effect.gen(function*() {
const logs = yield* runIgnore({ log: true }, "Warn")
assert.strictEqual(logs.length, 1)
assert.strictEqual(logs[0].logLevel, "Warn")
assertCauseFail(logs[0].cause, "boom")
}))
it.effect("logs with the provided level when log is a LogLevel", () =>
Effect.gen(function*() {
const logs = yield* runIgnore({ log: "Error" }, "Warn")
assert.strictEqual(logs.length, 1)
assert.strictEqual(logs[0].logLevel, "Error")
assertCauseFail(logs[0].cause, "boom")
}))
it.effect("prepends the provided message when logging", () =>
Effect.gen(function*() {
const logs = yield* runIgnore({ log: true, message: "Ignoring failure" })
assert.strictEqual(logs.length, 1)
assert.deepStrictEqual(logs[0].message, ["Ignoring failure"])
assertCauseFail(logs[0].cause, "boom")
}))
})
describe("Effect.ignoreCause", () => {
type IgnoreCauseOptions = { readonly log?: boolean | LogLevel.Severity; readonly message?: string }
const makeTestLogger = () => {
const capturedLogs: Array<{
readonly logLevel: LogLevel.LogLevel
readonly cause: Cause.Cause<unknown>
readonly message: unknown
}> = []
const testLogger = Logger.make<unknown, void>((options) => {
capturedLogs.push({ logLevel: options.logLevel, cause: options.cause, message: options.message })
})
return { capturedLogs, testLogger }
}
const runIgnoreCause = (options?: IgnoreCauseOptions, currentLogLevel: LogLevel.Severity = "Info") =>
Effect.gen(function*() {
const { capturedLogs, testLogger } = makeTestLogger()
const program = options === undefined
? Effect.fail("boom").pipe(Effect.ignoreCause)
: Effect.fail("boom").pipe(Effect.ignoreCause(options))
yield* program.pipe(
Effect.provide(Logger.layer([testLogger])),
Effect.provideService(References.MinimumLogLevel, "Trace"),
Effect.provideService(References.CurrentLogLevel, currentLogLevel)
)
return capturedLogs
})
it.effect("ignores defects", () =>
Effect.gen(function*() {
const exit = yield* Effect.die("boom").pipe(Effect.ignoreCause, Effect.exit)
assert.deepStrictEqual(exit, Exit.void)
}))
it.effect("ignores interrupts", () =>
Effect.gen(function*() {
const ignored = yield* Effect.interrupt.pipe(Effect.ignoreCause, Effect.exit)
assert.deepStrictEqual(ignored, Exit.void)
}))
it.effect("does not log when log is omitted", () =>
Effect.gen(function*() {
const logs = yield* runIgnoreCause()
assert.strictEqual(logs.length, 0)
}))
it.effect("does not log when log is false", () =>
Effect.gen(function*() {
const logs = yield* runIgnoreCause({ log: false })
assert.strictEqual(logs.length, 0)
}))
it.effect("logs with the current level when log is true", () =>
Effect.gen(function*() {
const logs = yield* runIgnoreCause({ log: true }, "Warn")
assert.strictEqual(logs.length, 1)
assert.strictEqual(logs[0].logLevel, "Warn")
assertCauseFail(logs[0].cause, "boom")
}))
it.effect("logs with the provided level when log is a LogLevel", () =>
Effect.gen(function*() {
const logs = yield* runIgnoreCause({ log: "Error" }, "Warn")
assert.strictEqual(logs.length, 1)
assert.strictEqual(logs[0].logLevel, "Error")
assertCauseFail(logs[0].cause, "boom")
}))
it.effect("prepends the provided message when logging", () =>
Effect.gen(function*() {
const logs = yield* runIgnoreCause({ log: true, message: "Ignoring cause" })
assert.strictEqual(logs.length, 1)
assert.deepStrictEqual(logs[0].message, ["Ignoring cause"])
assertCauseFail(logs[0].cause, "boom")
}))
})
describe("error handling", () => {
class ErrorA extends Data.TaggedError("A") {}
class ErrorB extends Data.TaggedError("B") {}
class ErrorC extends Data.Error {}
it.effect("catchTag", () =>
Effect.gen(function*() {
let error: ErrorA | ErrorB | ErrorC = new ErrorA()
const effect = Effect.failSync(() => error).pipe(
Effect.catchTag("A", (_) => Effect.succeed(1)),
Effect.catchTag("B", (_) => Effect.succeed(2)),
Effect.orElseSucceed(() => 3)
)
assert.strictEqual(yield* effect, 1)
error = new ErrorB()
assert.strictEqual(yield* effect, 2)
error = new ErrorC()
assert.strictEqual(yield* effect, 3)
}))
it.effect("catchTag orElse", () =>
Effect.gen(function*() {
let error: ErrorA | ErrorB | ErrorC = new ErrorA()
const effect = Effect.failSync(() => error).pipe(
Effect.catchTag(["A", "B"], (_) => Effect.succeed(1), (_) => {
return Effect.succeed(2)
})
)
assert.strictEqual(yield* effect, 1)
error = new ErrorB()
assert.strictEqual(yield* effect, 1)
error = new ErrorC()
assert.strictEqual(yield* effect, 2)
}))
it.effect("catchTags orElse", () =>
Effect.gen(function*() {
let error: ErrorA | ErrorB | ErrorC = new ErrorA()
const effect = Effect.failSync(() => error).pipe(
Effect.catchTags(
{
A: (_) => Effect.succeed(1),
B: (_) => Effect.succeed(2)
},
(_) => Effect.succeed(3)
)
)
assert.strictEqual(yield* effect, 1)
error = new ErrorB()
assert.strictEqual(yield* effect, 2)
error = new ErrorC()
assert.strictEqual(yield* effect, 3)
}))
it.effect("tapErrorTag", () =>
Effect.gen(function*() {
let error: ErrorA | ErrorB | ErrorC = new ErrorA()
const tapped: Array<string> = []
const effect = Effect.failSync(() => error).pipe(
Effect.tapErrorTag("A", () =>
Effect.sync(() => {
tapped.push("A")
})),
Effect.tapErrorTag("B", () =>
Effect.sync(() => {
tapped.push("B")
})),
Effect.exit
)
assert.deepStrictEqual(yield* effect, Exit.fail(error))
assert.deepStrictEqual(tapped, ["A"])
tapped.length = 0
error = new ErrorB()
assert.deepStrictEqual(yield* effect, Exit.fail(error))
assert.deepStrictEqual(tapped, ["B"])
tapped.length = 0
error = new ErrorC()
assert.deepStrictEqual(yield* effect, Exit.fail(error))
assert.deepStrictEqual(tapped, [])
}))
it.effect("catchIf with refinement", () =>
Effect.gen(function*() {
interface ErrorA {
readonly _tag: "ErrorA"
}
interface ErrorB {
readonly _tag: "ErrorB"
}
const effect: Effect.Effect<never, ErrorA | ErrorB> = Effect.fail({ _tag: "ErrorB" as const })
const result = yield* pipe(
effect,
Effect.catchIf((e): e is ErrorA => e._tag === "ErrorA", Effect.succeed),
Effect.exit
)
assert.deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" as const }))
}))
it.effect("catchIf with refinement orElse", () =>
Effect.gen(function*() {
interface ErrorA {
readonly _tag: "ErrorA"
}
interface ErrorB {
readonly _tag: "ErrorB"
}
const effect: Effect.Effect<never, ErrorA | ErrorB> = Effect.fail({ _tag: "ErrorB" as const })
const result = yield* pipe(
effect,
Effect.catchIf((e): e is ErrorA => e._tag === "ErrorA", Effect.succeed, (_) => {
return Effect.succeed(1)
})
)
assert.deepStrictEqual(result, 1)
}))
it.effect("catchNoSuchElement", () =>
Effect.gen(function*() {
const some = yield* Effect.fromNullishOr("value").pipe(Effect.catchNoSuchElement)
assert.deepStrictEqual(some, Option.some("value"))
const none = yield* Effect.fromNullishOr(null as string | null).pipe(Effect.catchNoSuchElement)
assert.deepStrictEqual(none, Option.none())
}))
it.effect("catchNoSuchElement preserves other errors", () =>
Effect.gen(function*() {
const error = new ErrorA()
const result = yield* Effect.fail(error).pipe(Effect.catchNoSuchElement, Effect.exit)
assert.deepStrictEqual(result, Exit.fail(error))
}))
})
describe("match", () => {
it("preserves handler receivers and treats thrown exceptions as defects", () => {
const options = {
value: 42,
onFailure(this: { value: number }) {
return this.value
},
onSuccess(this: { value: number }) {
return this.value
}
}
assert.strictEqual(Effect.runSync(Effect.match(Effect.succeed(1), options)), 42)
assert.strictEqual(Effect.runSync(Effect.match(Effect.fail("error"), options)), 42)
assert.strictEqual(Effect.runSync(Effect.matchCause(Effect.succeed(1), options)), 42)
assert.strictEqual(Effect.runSync(Effect.matchCause(Effect.fail("error"), options)), 42)
const boom = new Error("boom")
assertExitDefect(
Effect.runSyncExit(Effect.match(Effect.succeed(1), {
onFailure: () => 0,
onSuccess: () => {
throw boom
}
})),
boom
)
})
it("passes only the outcome to handlers", () => {
function handler(_outcome: unknown) {
// eslint-disable-next-line prefer-rest-params
return arguments.length
}
const options = { onFailure: handler, onSuccess: handler }
for (const source of [Effect.succeed(1), Effect.fail("error")]) {
assert.strictEqual(Effect.runSync(Effect.match(source, options)), 1)
assert.strictEqual(Effect.runSync(Effect.matchCause(source, options)), 1)
}
})
it("passes defects through without calling onFailure", () => {
const boom = new Error("boom")
assertExitDefect(
Effect.runSyncExit(Effect.match(Effect.die(boom), { onFailure: () => "handled", onSuccess: () => "ok" })),
boom
)
})
})
describe("flatMap", () => {
it("passes only the value to callbacks regardless of declared arity", () => {
function callback(this: unknown, _value: number) {
// eslint-disable-next-line prefer-rest-params
return Effect.succeed([this, arguments.length])
}
assert.deepStrictEqual(Effect.runSync(Effect.flatMap(Effect.succeed(1), callback)), [undefined, 1])
})
})
describe("zip", () => {
it.effect("concurrent: false", () => {
const executionOrder: Array<string> = []
const task1 = Effect.succeed("a").pipe(
Effect.delay(50),
Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
)
const task2 = Effect.succeed(1).pipe(
Effect.delay(1),
Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
)
return Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.zip(task1, task2))
yield* TestClock.adjust(51)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, ["a", 1])
assert.deepStrictEqual(executionOrder, ["task1", "task2"])
})
})
it.effect("concurrent: true", () => {
const executionOrder: Array<string> = []
const task1 = Effect.succeed("a").pipe(
Effect.delay(50),
Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
)
const task2 = Effect.succeed(1).pipe(
Effect.delay(1),
Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
)
return Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.zip(task1, task2, { concurrent: true }))
yield* TestClock.adjust(50)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, ["a", 1])
assert.deepStrictEqual(executionOrder, ["task2", "task1"])
})
})
it.effect("concurrent: true interrupts the other side on failure", () => {
const interrupted: Array<string> = []
const task1 = Effect.never.pipe(
Effect.onInterrupt(() => Effect.sync(() => interrupted.push("task1")))
)
const task2 = Effect.fail("boom").pipe(Effect.delay(10))
return Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.zip(task1, task2, { concurrent: true }))
yield* TestClock.adjust(10)
const result = yield* Fiber.await(fiber)
assert.deepStrictEqual(result, Exit.fail("boom"))
assert.deepStrictEqual(interrupted, ["task1"])
})
})
})
describe("zipWith", () => {
it.effect("concurrent: false", () => {
const executionOrder: Array<string> = []
const task1 = Effect.succeed("a").pipe(
Effect.delay(50),
Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
)
const task2 = Effect.succeed(1).pipe(
Effect.delay(1),
Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
)
return Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.zipWith(task1, task2, (a, b) => a + b))
yield* TestClock.adjust(51)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, "a1")
assert.deepStrictEqual(executionOrder, ["task1", "task2"])
})
})
it.effect("concurrent: true", () => {
const executionOrder: Array<string> = []
const task1 = Effect.succeed("a").pipe(
Effect.delay(50),
Effect.tap(() => Effect.sync(() => executionOrder.push("task1")))
)
const task2 = Effect.succeed(1).pipe(
Effect.delay(1),
Effect.tap(() => Effect.sync(() => executionOrder.push("task2")))
)
return Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.zipWith(task1, task2, (a, b) => a + b, { concurrent: true }))
yield* TestClock.adjust(50)
const result = yield* Fiber.join(fiber)
assert.deepStrictEqual(result, "a1")
assert.deepStrictEqual(executionOrder, ["task2", "task1"])
})
})
it.effect("concurrent: true interrupts the other side on failure", () => {
const interrupted: Array<string> = []
const task1 = Effect.fail("boom").pipe(Effect.delay(10))
const task2 = Effect.never.pipe(
Effect.onInterrupt(() => Effect.sync(() => interrupted.push("task2")))
)
return Effect.gen(function*() {
const fiber = yield* Effect.forkChild(
Effect.zipWith(task1, task2, (a, b) => `${a}${b}`, { concurrent: true })
)
yield* TestClock.adjust(10)
const result = yield* Fiber.await(fiber)
assert.deepStrictEqual(result, Exit.fail("boom"))
assert.deepStrictEqual(interrupted, ["task2"])
})
})
})
describe("catchCauseFilter", () => {
it.effect("first argument as success", () =>
Effect.gen(function*() {
const result = yield* Effect.catchCauseFilter(Effect.succeed(1), (_) => Result.fail(_), () => Effect.fail("e2"))
assert.deepStrictEqual(result, 1)
}))
it.effect("first argument as failure and predicate return false", () =>
Effect.gen(function*() {
const result = yield* Effect.flip(
Effect.catchCauseFilter(Effect.fail("e1" as const), (_) => Result.fail(_), () => Effect.fail("e2" as const))
)
assert.deepStrictEqual(result, "e1")
}))
it.effect("first argument as failure and predicate return true", () =>
Effect.gen(function*() {
const result = yield* Effect.flip(
Effect.catchCauseFilter(
Effect.fail("e1" as const),
(e) => Result.succeed(e),
() => Effect.fail("e2" as const)
)
)
assert.deepStrictEqual(result, "e2")
}))
})
describe("catchIf with predicate", () => {
it.effect("predicate match recovers", () =>
Effect.gen(function*() {
const result = yield* Effect.catchIf(
Effect.fail("e1"),
(e) => typeof e === "string",
(e) => Effect.succeed(`recovered: ${e}`)
)
assert.deepStrictEqual(result, "recovered: e1")
}))
it.effect("predicate no match preserves error", () =>
Effect.gen(function*() {
const result = yield* Effect.exit(
Effect.catchIf(
Effect.fail("e1" as const),
(_e) => false,
() => Effect.succeed("recovered")
)
)
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
})
describe("catchCauseIf with predicate", () => {
it.effect("predicate match recovers", () =>
Effect.gen(function*() {
const result = yield* Effect.catchCauseIf(
Effect.fail("e1"),
Cause.hasFails,
(cause) => Effect.succeed(`recovered: ${Cause.squash(cause)}`)
)
assert.deepStrictEqual(result, "recovered: e1")
}))
it.effect("predicate no match preserves error", () =>
Effect.gen(function*() {
const result = yield* Effect.exit(
Effect.catchCauseIf(
Effect.fail("e1" as const),
constFalse,
() => Effect.succeed("recovered")
)
)
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
})
describe("tapCauseIf", () => {
it.effect("filter match taps", () =>
Effect.gen(function*() {
const tapped: Array<string> = []
const result = yield* Effect.exit(
Effect.tapCauseFilter(
Effect.fail("e1"),
(cause) => Result.succeed(cause),
(cause) => Effect.sync(() => tapped.push(Cause.squash(cause) as string))
)
)
assert.deepStrictEqual(tapped, ["e1"])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("filter no match skips tap", () =>
Effect.gen(function*() {
const tapped: Array<string> = []
const result = yield* Effect.exit(
Effect.tapCauseFilter(
Effect.fail("e1"),
(cause) => Result.fail(cause),
() => Effect.sync(() => tapped.push("tapped"))
)
)
assert.deepStrictEqual(tapped, [])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("predicate match taps", () =>
Effect.gen(function*() {
const tapped: Array<string> = []
const result = yield* Effect.exit(
Effect.tapCauseIf(
Effect.fail("e1"),
Cause.hasFails,
(cause) => Effect.sync(() => tapped.push(Cause.squash(cause) as string))
)
)
assert.deepStrictEqual(tapped, ["e1"])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("predicate no match skips tap", () =>
Effect.gen(function*() {
const tapped: Array<string> = []
const result = yield* Effect.exit(
Effect.tapCauseIf(
Effect.fail("e1"),
constFalse,
() => Effect.sync(() => tapped.push("tapped"))
)
)
assert.deepStrictEqual(tapped, [])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("success skips tap", () =>
Effect.gen(function*() {
const tapped: Array<string> = []
const result = yield* Effect.tapCauseIf(
Effect.succeed(42),
constTrue,
() => Effect.sync(() => tapped.push("tapped"))
)
assert.deepStrictEqual(tapped, [])
assert.deepStrictEqual(result, 42)
}))
})
describe("onErrorIf", () => {
it.effect("predicate match runs finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.exit(
Effect.onErrorIf(
Effect.fail("e1"),
Cause.hasFails,
(cause) => Effect.sync(() => finalized.push(Cause.squash(cause) as string))
)
)
assert.deepStrictEqual(finalized, ["e1"])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("predicate no match skips finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.exit(
Effect.onErrorIf(
Effect.fail("e1"),
constFalse,
() => Effect.sync(() => finalized.push("finalized"))
)
)
assert.deepStrictEqual(finalized, [])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("filter match runs finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.exit(
Effect.onErrorFilter(
Effect.fail("e1"),
Cause.findError as Filter.Filter<Cause.Cause<string>, string>,
(error: string) => Effect.sync(() => finalized.push(error))
)
)
assert.deepStrictEqual(finalized, ["e1"])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("success skips finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.onErrorIf(
Effect.succeed(42),
constTrue,
() => Effect.sync(() => finalized.push("finalized"))
)
assert.deepStrictEqual(finalized, [])
assert.deepStrictEqual(result, 42)
}))
})
describe("onExitIf", () => {
it.effect("predicate match on success runs finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.onExitIf(
Effect.succeed(42),
Exit.isSuccess,
(exit) => {
if (Exit.isSuccess(exit)) {
return Effect.sync(() => finalized.push(`success:${exit.value}`))
}
return Effect.void
}
)
assert.deepStrictEqual(finalized, ["success:42"])
assert.deepStrictEqual(result, 42)
}))
it.effect("predicate no match on failure skips finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.exit(
Effect.onExitIf(
Effect.fail("e1"),
Exit.isSuccess,
() => Effect.sync(() => finalized.push("finalized"))
)
)
assert.deepStrictEqual(finalized, [])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("predicate match on failure runs finalizer", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.exit(
Effect.onExitIf(
Effect.fail("e1"),
Exit.isFailure,
(exit) => {
if (Exit.isFailure(exit)) {
return Effect.sync(() => finalized.push(`failure:${Cause.squash(exit.cause)}`))
}
return Effect.void
}
)
)
assert.deepStrictEqual(finalized, ["failure:e1"])
assert.deepStrictEqual(result, Exit.fail("e1"))
}))
it.effect("filter match receives pass value and exit", () =>
Effect.gen(function*() {
const finalized: Array<string> = []
const result = yield* Effect.onExitFilter(
Effect.succeed(42),
(exit) => Exit.isSuccess(exit) ? Result.succeed(`value:${exit.value}`) : Result.fail(exit),
(value, exit) =>
Effect.sync(() => finalized.push(`${value}:${Exit.isSuccess(exit) ? `success:${exit.value}` : "failure"}`))
)
assert.deepStrictEqual(finalized, ["value:42:success:42"])
assert.deepStrictEqual(result, 42)
}))
})
describe("filter with predicate/refinement", () => {
it.effect("predicate filters iterable", () =>
Effect.gen(function*() {
const result = yield* Effect.filter(
[1, 2, 3, 4, 5],
(n: number) => n % 2 === 0
)
assert.deepStrictEqual(result, [2, 4])
}))
it.effect("refinement narrows iterable", () =>
Effect.gen(function*() {
const result = yield* Effect.filter(
[1, "a", 2, "b", 3] as Array<number | string>,
(x): x is number => typeof x === "number"
)
assert.deepStrictEqual(result, [1, 2, 3])
}))
it.effect("Filter.Filter overload maps pass values", () =>
Effect.gen(function*() {
const filter: Filter.Filter<number, string, number> = (n) =>
n % 2 === 0 ? Result.succeed(`n=${n}`) : Result.fail(n)
const result = yield* Effect.filterMap(
[1, 2, 3, 4],
filter
)
assert.deepStrictEqual(result, ["n=2", "n=4"])
}))
})
describe("catch", () => {
it.effect("first argument as success", () =>
Effect.gen(function*() {
const result = yield* Effect.catch(Effect.succeed(1), () => Effect.fail("e2" as const))
assert.deepStrictEqual(result, 1)
}))
it.effect("first argument as failure", () =>
Effect.gen(function*() {
const result = yield* Effect.flip(Effect.catch(Effect.fail("e1" as const), () => Effect.fail("e2" as const)))
assert.deepStrictEqual(result, "e2")
}))
})
describe("firstSuccessOf", () => {
it.effect("returns the first success and does not run later effects", () =>
Effect.gen(function*() {
const executed: Array<string> = []
const result = yield* Effect.firstSuccessOf([
Effect.sync(() => executed.push("first")).pipe(
Effect.flatMap(() => Effect.fail("e1" as const))
),
Effect.sync(() => executed.push("second")).pipe(
Effect.as("success" as const)
),
Effect.sync(() => executed.push("third")).pipe(
Effect.as("unreachable" as const)
)
])
assert.strictEqual(result, "success")
assert.deepStrictEqual(executed, ["first", "second"])
}))
it.effect("fails with the last failure when all effects fail", () =>
Effect.gen(function*() {
const result = yield* Effect.firstSuccessOf([
Effect.fail("e1" as const),
Effect.fail("e2" as const),
Effect.fail("e3" as const)
]).pipe(Effect.flip)
assert.strictEqual(result, "e3")
}))
it.effect("defects on an empty collection", () =>
Effect.gen(function*() {
const result = yield* Effect.firstSuccessOf([]).pipe(Effect.sandbox, Effect.flip)
const reason = result.reasons[0]
assert.isTrue(Cause.isDieReason(reason))
if (Cause.isDieReason(reason)) {
assert.instanceOf(reason.defect, Error)
assert.strictEqual(reason.defect.message, "Received an empty collection of effects")
}
}))
})
describe("catchCause", () => {
it.effect("first argument as success", () =>
Effect.gen(function*() {
const result = yield* Effect.catchCause(Effect.succeed(1), () => Effect.fail("e2" as const))
assert.deepStrictEqual(result, 1)
}))
it.effect("first argument as failure", () =>
Effect.gen(function*() {
const result = yield* Effect.flip(
Effect.catchCause(Effect.fail("e1" as const), () => Effect.fail("e2" as const))
)
assert.deepStrictEqual(result, "e2")
}))
})
describe("transaction (composable transactions)", () => {
describe("basic behavior", () => {
it.effect("should create transaction boundaries when no transaction is active", () =>
Effect.gen(function*() {
const ref1 = TxRef.makeUnsafe(0)
const ref2 = TxRef.makeUnsafe(100)
yield* Effect.tx(TxRef.set(ref1, 10))
yield* Effect.tx(TxRef.set(ref2, 200))
const val1 = yield* Effect.tx(TxRef.get(ref1))
const val2 = yield* Effect.tx(TxRef.get(ref2))
assert.strictEqual(val1, 10)
assert.strictEqual(val2, 200)
}))
it.effect("should allow TxRef.modify outside an existing transaction", () =>
Effect.gen(function*() {
const ref = TxRef.makeUnsafe(0)
const result = yield* TxRef.modify(ref, (current) => [current + 1, current + 10])
const value = yield* Effect.tx(TxRef.get(ref))
assert.strictEqual(result, 1)
assert.strictEqual(value, 10)
}))
it.effect("should roll back nested changes when the outer transaction fails", () =>
Effect.gen(function*() {
const ref1 = TxRef.makeUnsafe(0)
const ref2 = TxRef.makeUnsafe(100)
const parentError = yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref1, 10)
yield* Effect.tx(TxRef.set(ref2, 200))
return yield* Effect.fail("parent failed")
})).pipe(Effect.flip)
const val1 = yield* Effect.tx(TxRef.get(ref1))
const val2 = yield* Effect.tx(TxRef.get(ref2))
assert.strictEqual(parentError, "parent failed")
assert.strictEqual(val1, 0)
assert.strictEqual(val2, 100)
}))
it.effect("should allow catching nested failures in the same transaction", () =>
Effect.gen(function*() {
const ref1 = TxRef.makeUnsafe(0)
const ref2 = TxRef.makeUnsafe(100)
yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref1, 10)
const childResult = yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref2, 200)
return yield* Effect.fail("child failed")
})).pipe(Effect.result)
yield* TxRef.set(ref1, 20)
assert.strictEqual(Result.isFailure(childResult), true)
}))
const val1 = yield* Effect.tx(TxRef.get(ref1))
const val2 = yield* Effect.tx(TxRef.get(ref2))
assert.strictEqual(val1, 20)
assert.strictEqual(val2, 200)
}))
})
describe("transaction nesting", () => {
it.effect("should handle multiple levels of nested composed transactions", () =>
Effect.gen(function*() {
const ref1 = TxRef.makeUnsafe(0)
const ref2 = TxRef.makeUnsafe(0)
const ref3 = TxRef.makeUnsafe(0)
yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref1, 1)
yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref2, 2)
yield* Effect.tx(TxRef.set(ref3, 3))
}))
}))
const val1 = yield* Effect.tx(TxRef.get(ref1))
const val2 = yield* Effect.tx(TxRef.get(ref2))
const val3 = yield* Effect.tx(TxRef.get(ref3))
assert.strictEqual(val1, 1)
assert.strictEqual(val2, 2)
assert.strictEqual(val3, 3)
}))
})
describe("transaction composition behavior", () => {
it.effect("should compose nested tx calls into the same transaction", () =>
Effect.gen(function*() {
const ref1 = TxRef.makeUnsafe(0)
const ref2 = TxRef.makeUnsafe(0)
yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref1, 10)
yield* Effect.tx(TxRef.set(ref2, 20))
}))
const val1 = yield* Effect.tx(TxRef.get(ref1))
const val2 = yield* Effect.tx(TxRef.get(ref2))
assert.strictEqual(val1, 10)
assert.strictEqual(val2, 20)
}))
it.effect("should rollback the entire composed transaction on failure", () =>
Effect.gen(function*() {
const ref = TxRef.makeUnsafe(0)
const txError = yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref, 10)
return yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref, 20)
return yield* Effect.fail("nested failure")
}))
})).pipe(Effect.flip)
const valueAfterFailure = yield* Effect.tx(TxRef.get(ref))
assert.strictEqual(txError, "nested failure")
assert.strictEqual(valueAfterFailure, 0)
}))
it.effect("should preserve nested writes when nested failure is caught", () =>
Effect.gen(function*() {
const ref = TxRef.makeUnsafe(0)
yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref, 10)
const childError = yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.set(ref, 20)
return yield* Effect.fail("transaction nested failure")
})).pipe(Effect.flip)
assert.strictEqual(childError, "transaction nested failure")
}))
const transactionValue = yield* Effect.tx(TxRef.get(ref))
assert.strictEqual(transactionValue, 20)
}))
})
describe("retry", () => {
it.effect("should rerun when a read ref changed while the transaction was suspended", () =>
Effect.gen(function*() {
const ref = TxRef.makeUnsafe(0)
const read = yield* Deferred.make<void>()
const committed = yield* Deferred.make<void>()
const transaction = yield* Effect.tx(Effect.gen(function*() {
const value = yield* TxRef.get(ref)
if (value === 0) {
yield* Deferred.succeed(read, undefined)
yield* Deferred.await(committed)
return yield* Effect.txRetry
}
return value
})).pipe(Effect.forkChild)
yield* Deferred.await(read)
yield* Effect.tx(TxRef.set(ref, 1))
yield* Deferred.succeed(committed, undefined)
const joined = yield* Fiber.join(transaction).pipe(
Effect.timeoutOption("1 second"),
Effect.forkChild
)
yield* TestClock.adjust("1 second")
assert.deepStrictEqual(yield* Fiber.join(joined), Option.some(1))
}))
it.effect("commits wake the waiters of refs they write, not refs they read", () =>
Effect.gen(function*() {
const gate = TxRef.makeUnsafe(0)
const other = TxRef.makeUnsafe(0)
let runs = 0
const waiter = yield* Effect.tx(Effect.gen(function*() {
runs++
const value = yield* TxRef.get(gate)
if (value === 0) return yield* Effect.txRetry
return value
})).pipe(Effect.forkChild({ startImmediately: true }))
yield* Effect.tx(Effect.gen(function*() {
yield* TxRef.get(gate)
yield* TxRef.set(other, 1)
}))
yield* Effect.repeat(Effect.yieldNow, { times: 10 })
assert.strictEqual(runs, 1, "reading a ref should not wake its waiters")
yield* Effect.tx(TxRef.set(gate, 0))
yield* Effect.repeat(Effect.yieldNow, { times: 10 })
assert.strictEqual(runs, 2, "writing the same value should wake its waiters")
yield* Effect.tx(TxRef.set(gate, 1))
assert.strictEqual(yield* Fiber.join(waiter), 1)
}))
it.effect("a write of -0 over 0 is published and wakes its waiters", () =>
Effect.gen(function*() {
const zero = TxRef.makeUnsafe(0)
const negativeZero = yield* Effect.tx(Effect.gen(function*() {
const value = yield* TxRef.get(zero)
if (Object.is(value, 0)) return yield* Effect.txRetry
return 1 / value
})).pipe(Effect.forkChild({ startImmediately: true }))
yield* Effect.tx(TxRef.set(zero, -0))
yield* Effect.repeat(Effect.yieldNow, { times: 10 })
assert.deepStrictEqual(negativeZero.pollUnsafe(), Exit.succeed(-Infinity), "-0 over 0 never woke its waiter")
}))
})
})
describe("Effect.fn", () => {
it.effect("should support pipeable arguments", () => {
const fn = Effect.fn(function*(s: string) {
return s.length
}, (effect, ...args) => effect.pipe(Effect.map((result) => [result, ...args])))
return Effect.gen(function*() {
const result = yield* fn("a")
assert.deepStrictEqual(result, [1, "a"])
})
})
it.effect("should support self with pipeable arguments", () => {
const self = { prefix: "bound" }
const fn = Effect.fn(
{ self },
function*(this: typeof self, value: string) {
return `${this.prefix}:${value}`
},
Effect.map((value) => value.toUpperCase())
)
return Effect.gen(function*() {
const result = yield* fn("value")
assert.strictEqual(result, "BOUND:VALUE")
})
})
it("should proxy body length", () => {
const traced = Effect.fn(function*(a: string, b: number) {
return a.length + b
})
const named = Effect.fn("named")(function*(a: string, b: number, c: boolean) {
return c ? a.length + b : b
})
const untraced = Effect.fnUntraced(function*(a: string, b: number) {
return a.length + b
}, Effect.map((n) => n))
assert.strictEqual(traced.length, 2)
assert.strictEqual(named.length, 3)
assert.strictEqual(untraced.length, 2)
})
it.effect("should reuse the definition frame across calls", () => {
const fn = Effect.fn("traced")(function*() {
return (yield* References.CurrentStackFrame)!
})
return Effect.gen(function*() {
const first = yield* fn()
const second = yield* fn()
assert.strictEqual(first.parent?.name, "traced (definition)")
assert.include(first.parent?.stack(), "Effect.test.ts")
assert.strictEqual(first.parent?.stack(), second.parent?.stack())
assert.notStrictEqual(first.stack(), second.stack())
})
})
})
describe("catchReason", () => {
class RateLimitError extends Data.TaggedError("RateLimitError")<{
readonly retryAfter: number
}> {}
class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
readonly limit: number
}> {}
class AiError extends Data.TaggedError("AiError")<{
readonly reason: RateLimitError | QuotaExceededError
}> {}
class OtherError extends Data.TaggedError("OtherError")<{
readonly message: string
}> {}
it.effect("catches matching reason - handler succeeds", () =>
Effect.gen(function*() {
const result = yield* Effect.fail(
new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
).pipe(
Effect.catchReason("AiError", "RateLimitError", (r) => Effect.succeed(`retry: ${r.retryAfter}`))
)
assert.strictEqual(result, "retry: 60")
}))
it.effect("orElse", () =>
Effect.gen(function*() {
const result = yield* Effect.fail(
new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
).pipe(
Effect.catchReason(
"AiError",
"RateLimitError",
(r) => Effect.succeed(`retry: ${r.retryAfter}`),
(_) => Effect.succeed("quota")
)
)
assert.strictEqual(result, "quota")
}))
it.effect("catches matching reason - handler fails", () =>
Effect.gen(function*() {
const reason = new RateLimitError({ retryAfter: 60 })
const error = new OtherError({ message: "handled" })
const exit = yield* Effect.fail(new AiError({ reason })).pipe(
Effect.catchReason("AiError", "RateLimitError", () => Effect.fail(error)),
Effect.exit
)
assertExitFailure(exit, Cause.fail(error))
}))
it.effect("ignores non-matching reason", () =>
Effect.gen(function*() {
const reason = new QuotaExceededError({ limit: 100 })
const exit = yield* Effect.fail(new AiError({ reason })).pipe(
Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
Effect.exit
)
assertExitFailure(exit, Cause.fail(new AiError({ reason })))
}))
it.effect("preserves the cause for a non-matching reason", () =>
Effect.gen(function*() {
const error = new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
const result = yield* Effect.gen(function*() {
yield* Effect.addFinalizer(() => Effect.die("finalizer failure"))
return yield* Effect.fail(error)
}).pipe(
Effect.scoped,
Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
Effect.exit
)
assert.deepStrictEqual(
result,
Exit.failCause(Cause.combine(Cause.fail(error), Cause.die("finalizer failure")))
)
}))
it.effect("ignores non-matching parent tag", () =>
Effect.gen(function*() {
const error = new OtherError({ message: "test" })
const exit = yield* (Effect.fail(error) as Effect.Effect<never, AiError | OtherError>).pipe(
Effect.catchReason("AiError", "RateLimitError", () => Effect.succeed("no")),
Effect.exit
)
assertExitFailure(exit, Cause.fail(error))
}))
})
describe("catchReasons", () => {
class RateLimitError extends Data.TaggedError("RateLimitError")<{
readonly retryAfter: number
}> {}
class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
readonly limit: number
}> {}
class AiError extends Data.TaggedError("AiError")<{
readonly reason: RateLimitError | QuotaExceededError
}> {}
it.effect("catches with object handlers", () =>
Effect.gen(function*() {
const result = yield* Effect.fail(
new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
).pipe(
Effect.catchReasons("AiError", {
RateLimitError: (r) => Effect.succeed(`rate: ${r.retryAfter}`),
QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
})
)
assert.strictEqual(result, "rate: 60")
}))
it.effect("orElse", () =>
Effect.gen(function*() {
const result = yield* Effect.fail(
new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
).pipe(
Effect.catchReasons("AiError", {
QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
}, (_) => Effect.succeed("orElse"))
)
assert.strictEqual(result, "orElse")
}))
it.effect("catches second reason type", () =>
Effect.gen(function*() {
const result = yield* Effect.fail(
new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
).pipe(
Effect.catchReasons("AiError", {
RateLimitError: (r) => Effect.succeed(`rate: ${r.retryAfter}`),
QuotaExceededError: (r) => Effect.succeed(`quota: ${r.limit}`)
})
)
assert.strictEqual(result, "quota: 100")
}))
it.effect("partial handlers - unhandled passes through", () =>
Effect.gen(function*() {
const reason = new QuotaExceededError({ limit: 100 })
const exit = yield* Effect.fail(new AiError({ reason })).pipe(
Effect.catchReasons("AiError", {
RateLimitError: () => Effect.succeed("handled")
}),
Effect.exit
)
assertExitFailure(exit, Cause.fail(new AiError({ reason })))
}))
it.effect("preserves the cause for an unhandled reason", () =>
Effect.gen(function*() {
const error = new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
const result = yield* Effect.gen(function*() {
yield* Effect.addFinalizer(() => Effect.die("finalizer failure"))
return yield* Effect.fail(error)
}).pipe(
Effect.scoped,
Effect.catchReasons("AiError", {
RateLimitError: () => Effect.succeed("handled")
}),
Effect.exit
)
assert.deepStrictEqual(
result,
Exit.failCause(Cause.combine(Cause.fail(error), Cause.die("finalizer failure")))
)
}))
})
describe("unwrapReason", () => {
class RateLimitError extends Data.TaggedError("RateLimitError")<{
readonly retryAfter: number
}> {}
class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
readonly limit: number
}> {}
class AiError extends Data.TaggedError("AiError")<{
readonly reason: RateLimitError | QuotaExceededError
}> {}
class OtherError extends Data.TaggedError("OtherError")<{
readonly message: string
}> {}
it.effect("extracts reason into error channel", () =>
Effect.gen(function*() {
const reason = new RateLimitError({ retryAfter: 60 })
const exit = yield* Effect.fail(new AiError({ reason })).pipe(
Effect.unwrapReason("AiError"),
Effect.exit
)
assertExitFailure(exit, Cause.fail(reason))
}))
it.effect("extracts second reason type", () =>
Effect.gen(function*() {
const reason = new QuotaExceededError({ limit: 100 })
const exit = yield* Effect.fail(new AiError({ reason })).pipe(
Effect.unwrapReason("AiError"),
Effect.exit
)
assertExitFailure(exit, Cause.fail(reason))
}))
it.effect("preserves other errors", () =>
Effect.gen(function*() {
const error = new OtherError({ message: "test" })
const exit = yield* (Effect.fail(error) as Effect.Effect<never, AiError | OtherError>).pipe(
Effect.unwrapReason("AiError"),
Effect.exit
)
assertExitFailure(exit, Cause.fail(error))
}))
})
describe("cached", () => {
it.effect("runs the effect once for concurrent callers", () =>
Effect.gen(function*() {
const release = yield* Deferred.make<void>()
let count = 0
const cached = yield* Effect.cached(
Effect.gen(function*() {
count++
yield* Deferred.await(release)
return count
})
)
const fibers = yield* Effect.forEach([1, 2, 3], () => cached.pipe(Effect.forkChild({ startImmediately: true })))
assert.strictEqual(count, 1)
yield* Deferred.succeed(release, void 0)
assert.deepStrictEqual(yield* Fiber.awaitAll(fibers), [Exit.succeed(1), Exit.succeed(1), Exit.succeed(1)])
}))
it.effect("replays failures", () =>
Effect.gen(function*() {
let count = 0
const cached = yield* Effect.cached(
Effect.sync(() => count++).pipe(Effect.andThen(Effect.fail("boom")))
)
assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
assert.strictEqual(count, 1)
}))
it.effect("interrupting a waiter leaves the owner running", () =>
Effect.gen(function*() {
const started = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const cached = yield* Effect.cached(
Deferred.succeed(started, void 0).pipe(
Effect.andThen(Deferred.await(release)),
Effect.as(42)
)
)
const owner = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.await(started)
const waiter = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(waiter)
assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(waiter)))
yield* Deferred.succeed(release, void 0)
assert.strictEqual(yield* Fiber.join(owner), 42)
assert.strictEqual(yield* cached, 42)
}))
it.effect("shares a run until every caller is interrupted, then starts fresh", () =>
Effect.gen(function*() {
let runs = 0
const started = yield* Deferred.make<void>()
const finalizing = yield* Deferred.make<void>()
const finishFinalizer = yield* Deferred.make<void>()
const cached = yield* Effect.cached(
Effect.suspend(() => {
if (++runs > 1) return Effect.succeed(runs)
return Deferred.succeed(started, void 0).pipe(
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Deferred.succeed(finalizing, void 0).pipe(Effect.andThen(Deferred.await(finishFinalizer)))
)
)
})
)
const first = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.await(started)
const second = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
yield* Fiber.interrupt(first)
assert.isFalse(yield* Deferred.isDone(finalizing))
// A call made while the abandoned run finalizes starts a fresh run.
const interruptSecond = yield* Effect.forkChild(Fiber.interrupt(second), { startImmediately: true })
yield* Deferred.await(finalizing)
const fresh = yield* cached.pipe(Effect.forkChild({ startImmediately: true }))
yield* Deferred.succeed(finishFinalizer, void 0)
yield* Fiber.join(interruptSecond)
assert.deepStrictEqual(yield* Fiber.await(fresh), Exit.succeed(2))
}))
})
describe("cachedWithTTL", () => {
it.effect("skips the ttl callback on interruption and invokes it once on the next successful run", () =>
Effect.gen(function*() {
let runs = 0
const exits: Array<Exit.Exit<number>> = []
const cached = yield* Effect.cachedWithTTL(
Effect.suspend(() => ++runs === 1 ? Effect.interrupt : Effect.succeed(runs)),
(exit: Exit.Exit<number>) => {
exits.push(exit)
return "1 minute"
}
)
assert.isTrue(Exit.hasInterrupts(yield* Effect.exit(cached)))
assert.deepStrictEqual(exits, [])
assert.strictEqual(yield* cached, 2)
assert.strictEqual(yield* cached, 2)
assert.deepStrictEqual(exits, [Exit.succeed(2)])
}))
it.effect("selects ttl from each fresh exit without evaluating on cache hits", () =>
Effect.gen(function*() {
let count = 0
const exits: Array<Exit.Exit<number>> = []
const cached = yield* Effect.cachedWithTTL(
Effect.sync(() => ++count),
(exit: Exit.Exit<number>) => {
exits.push(exit)
return Exit.isSuccess(exit) && exit.value === 1 ? "1 second" : Duration.seconds(3)
}
)
assert.strictEqual(count, 0)
assert.deepStrictEqual(exits, [])
assert.strictEqual(yield* cached, 1)
yield* TestClock.adjust("999 millis")
assert.strictEqual(yield* cached, 1)
assert.deepStrictEqual(exits, [Exit.succeed(1)])
yield* TestClock.adjust("1 milli")
assert.strictEqual(yield* cached, 2)
yield* TestClock.adjust("1 second")
assert.strictEqual(yield* cached, 2)
yield* TestClock.adjust("2 seconds")
assert.strictEqual(yield* cached, 3)
assert.deepStrictEqual(exits, [Exit.succeed(1), Exit.succeed(2), Exit.succeed(3)])
}))
it.effect("supports a piped callback that skips failures and caches successes", () =>
Effect.gen(function*() {
let count = 0
const exits: Array<Exit.Exit<number, string>> = []
const cached = yield* Effect.suspend(() => ++count === 1 ? Effect.fail("boom") : Effect.succeed(count)).pipe(
Effect.cachedWithTTL((exit: Exit.Exit<number, string>) => {
exits.push(exit)
return Exit.isFailure(exit) ? 0 : "1 second"
})
)
assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
assert.strictEqual(yield* cached, 2)
assert.strictEqual(yield* cached, 2)
assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2)])
yield* TestClock.adjust("1 second")
assert.strictEqual(yield* cached, 3)
assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2), Exit.succeed(3)])
}))
it.effect("starts ttl from value creation", () =>
Effect.gen(function*() {
let count = 0
const cached = yield* Effect.cachedWithTTL(
Effect.sleep("2 seconds").pipe(
Effect.flatMap(() =>
Effect.sync(() => {
count += 1
return count
})
)
),
"1 second"
)
const firstFiber = yield* Effect.forkChild(cached)
yield* Effect.yieldNow
yield* TestClock.adjust("2 seconds")
assert.strictEqual(yield* Fiber.join(firstFiber), 1)
const secondFiber = yield* Effect.forkChild(cached)
yield* Effect.yieldNow
yield* TestClock.adjust("2 seconds")
assert.strictEqual(yield* Fiber.join(secondFiber), 1)
assert.strictEqual(count, 1)
}))
})
describe("cachedInvalidateWithTTL", () => {
it.effect("supports a piped callback that skips failures and caches successes", () =>
Effect.gen(function*() {
let count = 0
const exits: Array<Exit.Exit<number, string>> = []
const [cached] = yield* Effect.suspend(() => ++count === 1 ? Effect.fail("boom") : Effect.succeed(count)).pipe(
Effect.cachedInvalidateWithTTL((exit: Exit.Exit<number, string>) => {
exits.push(exit)
return Exit.isFailure(exit) ? 0 : "1 second"
})
)
assert.deepStrictEqual(yield* Effect.exit(cached), Exit.fail("boom"))
assert.strictEqual(yield* cached, 2)
assert.strictEqual(yield* cached, 2)
assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2)])
yield* TestClock.adjust("1 second")
assert.strictEqual(yield* cached, 3)
assert.deepStrictEqual(exits, [Exit.fail("boom"), Exit.succeed(2), Exit.succeed(3)])
}))
})
describe("updateServiceScoped", () => {
class CurrentNumber extends Context.Service<CurrentNumber, number>()("CurrentNumber") {}
const CurrentNumberReference = Context.Reference<number>("CurrentNumberReference", {
defaultValue: () => 1
})
const CurrentValues = Context.Reference<ReadonlyArray<string>>("CurrentValues", {
defaultValue: () => []
})
it.effect("skips restoration when the provider expires before scope closure", () =>
Effect.gen(function*() {
const result = yield* Effect.gen(function*() {
yield* Effect.updateServiceScoped(CurrentNumber, (value) => value + 1)
return yield* CurrentNumber
}).pipe(Effect.provideService(CurrentNumber, 1), Effect.scoped)
assert.strictEqual(result, 2)
assert.deepStrictEqual(yield* Effect.serviceOption(CurrentNumber), Option.none())
}))
it.effect("updates a Context.Service until the scope closes", () =>
Effect.gen(function*() {
const before = yield* CurrentNumber
const during = yield* Effect.scoped(
Effect.gen(function*() {
yield* Effect.updateServiceScoped(CurrentNumber, (value) => value + 1)
return yield* CurrentNumber
})
)
const after = yield* CurrentNumber
assert.deepStrictEqual([before, during, after], [1, 2, 1])
}).pipe(Effect.provideService(CurrentNumber, 1)))
it.effect("updates a Context.Reference until the scope closes", () =>
Effect.gen(function*() {
const before = yield* CurrentNumberReference
const during = yield* Effect.scoped(
Effect.gen(function*() {
yield* Effect.updateServiceScoped(CurrentNumberReference, (value) => value + 1)
return yield* CurrentNumberReference
})
)
const after = yield* CurrentNumberReference
assert.deepStrictEqual([before, during, after], [1, 2, 1])
}))
it.effect("supports merging the current value on reset", () =>
Effect.gen(function*() {
const scope = Scope.makeUnsafe()
let resetValues: ReadonlyArray<ReadonlyArray<string>> | undefined
const result = yield* Effect.gen(function*() {
yield* Effect.updateServiceScoped(
CurrentValues,
(values) => [...values, "scoped"],
{
reset: (original, updated, current) => {
resetValues = [original, updated, current]
return [...original, ...current.filter((value) => !updated.includes(value))]
}
}
)
yield* Effect.withFiber((fiber) =>
Effect.sync(() => {
fiber.setContext(Context.add(fiber.context, CurrentValues, ["scoped", "external"]))
})
)
yield* Scope.close(scope, Exit.void)
return yield* CurrentValues
}).pipe(Effect.provideService(Scope.Scope, scope))
assert.deepStrictEqual(resetValues, [[], ["scoped"], ["scoped", "external"]])
assert.deepStrictEqual(result, ["external"])
}))
})
describe("provide", () => {
class MyNumber extends Context.Service<MyNumber, number>()("MyNumber") {}
it.effect("subsequent calls share MemoMap", () =>
Effect.gen(function*() {
let buildCount = 0
const layer = Layer.sync(MyNumber, () => {
buildCount += 1
return 42
})
// @effect-diagnostics multipleEffectProvide:off
yield* Effect.void.pipe(
Effect.provide(layer, { local: true }), // local always builds the layer
Effect.provide(layer),
Effect.provide(layer)
)
assert.strictEqual(buildCount, 2)
}))
})
describe("AsyncLocalStorage", () => {
const storage = new AsyncLocalStorage<string>()
const current = () => storage.getStore() ?? "none"
const runWithStore = <A, E>(store: string | undefined, effect: Effect.Effect<A, E>): Promise<A> =>
store === undefined ? Effect.runPromise(effect) : storage.run(store, () => Effect.runPromise(effect))
const observe = Effect.gen(function*() {
const resumed = current()
const promise = yield* Effect.promise(async () => current())
const timer = yield* Effect.callback<string>((resume) => {
setTimeout(() => resume(Effect.succeed(current())), 1)
})
yield* Effect.sleep(1)
const afterSleep = current()
return { resumed, promise, timer, afterSleep }
})
const expected = (store: string) => ({ resumed: store, promise: store, timer: store, afterSleep: store })
for (const waiterStore of [undefined, "waiter"]) {
const label = waiterStore === undefined ? "without a store" : "with its own store"
describe(`waiter ${label}`, () => {
it("Deferred.await resumes in the awaiter's context", async () => {
const deferred = Deferred.makeUnsafe<void>()
const waiter = runWithStore(waiterStore, Effect.andThen(Deferred.await(deferred), observe))
const completer = await runWithStore(
"waker",
Effect.sleep(5).pipe(
Effect.andThen(Deferred.succeed(deferred, undefined)),
Effect.andThen(Effect.sync(current))
)
)
assert.strictEqual(completer, "waker")
assert.deepStrictEqual(await waiter, expected(waiterStore ?? "none"))
})
it("Pool.get resumes in the waiter's context", async () => {
const scope = Scope.makeUnsafe()
try {
const pool = await Effect.runPromise(
Pool.make({ acquire: Effect.succeed("conn"), size: 1 }).pipe(
// Warm the pool before starting the holder.
Effect.tap((pool) => Effect.scoped(Pool.get(pool))),
Scope.provide(scope)
)
)
const holderAcquired = Deferred.makeUnsafe<string>()
const holder = runWithStore(
"waker",
Effect.scoped(
Pool.get(pool).pipe(
Effect.andThen(Effect.sync(current)),
Effect.tap((store) => Deferred.succeed(holderAcquired, store)),
Effect.andThen(Effect.sleep(10)),
Effect.andThen(Effect.sync(current))
)
)
)
assert.strictEqual(await Effect.runPromise(Deferred.await(holderAcquired)), "waker")
const waiter = runWithStore(waiterStore, Effect.andThen(Effect.scoped(Pool.get(pool)), observe))
assert.strictEqual(await holder, "waker")
assert.deepStrictEqual(await waiter, expected(waiterStore ?? "none"))
} finally {
await Effect.runPromise(Scope.close(scope, Exit.void))
}
})
})
}
})
})