Files

2911 lines
94 KiB
TypeScript

import { assert, describe, it, vi } from "@effect/vitest"
import { assertExitFailure } from "@effect/vitest/utils"
import {
Cause,
Context,
Data,
Deferred,
Duration,
Effect,
Exit,
Fiber,
type Filter,
Layer,
Logger,
type LogLevel,
Option,
References,
Result,
Schedule,
Scope,
TxRef
} from "effect"
import { constFalse, constTrue, pipe } from "effect/Function"
import { TestClock } from "effect/testing"
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", () => {
it("isEffect", () => {
assert.isTrue(Effect.isEffect(Effect.succeed(0)))
assert.isFalse(Effect.isEffect([0]))
})
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.effect("failCause 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("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("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("Context.Service", () =>
ATag.pipe(
Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
Effect.provideService(ATag, "A"),
Effect.runPromise
))
describe("fromOption", () => {
it("from a some", () =>
Option.some("A").pipe(
Effect.fromOption,
Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
Effect.runPromise
))
it("from a none", () =>
Option.none().pipe(
Effect.fromOption,
Effect.flip,
Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError))),
Effect.runPromise
))
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("from a success", () =>
Result.succeed("A").pipe(
Effect.fromResult,
Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))),
Effect.runPromise
))
it("from a failure", () =>
Result.fail("error").pipe(
Effect.fromResult,
Effect.flip,
Effect.tap((error) => Effect.sync(() => assert.strictEqual(error, "error"))),
Effect.runPromise
))
})
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)))
})
describe("forEach", () => {
it("sequential", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_))
assert.deepStrictEqual(results, [1, 2, 3])
}).pipe(Effect.runPromise))
it("unbounded", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_), { concurrency: "unbounded" })
assert.deepStrictEqual(results, [1, 2, 3])
}).pipe(Effect.runPromise))
it("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])
}).pipe(Effect.runPromise))
it.effect("inherit unbounded", () =>
Effect.gen(function*() {
const handle = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_).pipe(Effect.delay(50)), {
concurrency: "inherit"
}).pipe(
Effect.withConcurrency("unbounded"),
Effect.forkChild
)
yield* TestClock.adjust(90)
assert.deepStrictEqual(handle.pollUnsafe(), Exit.succeed([1, 2, 3]))
}))
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("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("length = 0", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach([], (_) => Effect.succeed(_))
assert.deepStrictEqual(results, [])
}).pipe(Effect.runPromise))
it("string", () =>
Effect.gen(function*() {
const results = yield* Effect.forEach("abc", (_) => Effect.succeed(_))
assert.deepStrictEqual(results, ["a", "b", "c"])
}).pipe(Effect.runPromise))
})
describe("all", () => {
it("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])
}).pipe(Effect.runPromise))
it("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
})
}).pipe(Effect.runPromise))
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)
})
}))
})
describe("partition", () => {
it.effect("collects only successes", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4]
const [excluded, satisfying] = yield* Effect.partition(values, Effect.succeed)
assert.deepStrictEqual(excluded, [])
assert.deepStrictEqual(satisfying, values)
}))
it.effect("collects only failures", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4]
const [excluded, satisfying] = yield* Effect.partition(values, Effect.fail)
assert.deepStrictEqual(excluded, values)
assert.deepStrictEqual(satisfying, [])
}))
it.effect("collects failures and successes", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4, 5]
const [excluded, satisfying] = yield* Effect.partition(values, (n) =>
n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n))
assert.deepStrictEqual(excluded, [0, 2, 4])
assert.deepStrictEqual(satisfying, [1, 3, 5])
}))
it.effect("supports concurrency option", () =>
Effect.gen(function*() {
const values = [0, 1, 2, 3, 4, 5]
const [excluded, satisfying] = yield* Effect.partition(
values,
(n) => n % 2 === 0 ? Effect.fail(n) : Effect.succeed(n),
{ concurrency: "unbounded" }
)
assert.deepStrictEqual(excluded, [0, 2, 4])
assert.deepStrictEqual(satisfying, [1, 3, 5])
}))
})
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("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)
}).pipe(Effect.runPromise))
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])
}))
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("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.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())
}))
})
describe("interruption", () => {
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.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)
}))
})
describe("awaitAllChildren", () => {
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("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 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)
)
})
})
describe("finalization", () => {
const ExampleError = new Error("Oh noes!")
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 not caught", () =>
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.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("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("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)
}))
})
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("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"])
})
})
})
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"])
})
})
})
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("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("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)
})
})
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("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 })))
}))
})
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("cachedWithTTL", () => {
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("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)
}))
})
})