Add error management
Lint / lint (push) Successful in 12s

This commit is contained in:
Julien Valverdé
2026-05-19 14:04:29 +02:00
parent 838a0003e9
commit b214224708
2 changed files with 285 additions and 1 deletions
+118 -1
View File
@@ -1,11 +1,128 @@
import { describe, expect, test } from "bun:test"
import { Chunk, Context, Effect, Option, SubscriptionRef } from "effect"
import { Chunk, Context, Effect, Option, Stream, SubscriptionRef } from "effect"
import * as Lens from "./Lens.js"
describe("Lens", () => {
class Offset extends Context.Tag("Offset")<Offset, { readonly value: number }>() {}
test("mapErrorRead transforms read errors", async () => {
const lens = Lens.mapErrorRead(
Lens.make<number, "read", never, never, never>({
get: Effect.fail("read" as const),
changes: Stream.fail("read" as const),
set: () => Effect.void,
}),
error => `mapped:${ error }`,
)
const result = await Effect.runPromise(Effect.either(Lens.get(lens)))
expect(result.left).toBe("mapped:read")
})
test("mapErrorWrite transforms modify errors", async () => {
const lens = Lens.mapErrorWrite(
Lens.make<number, never, "write", never, never>({
get: Effect.succeed(1),
changes: Stream.make(1),
set: () => Effect.fail("write" as const),
}),
() => "mapped-write",
)
const result = await Effect.runPromise(Effect.either(Lens.set(lens, 2)))
expect(result.left).toBe("mapped-write")
})
test("mapError transforms read and modify errors", async () => {
const lens = Lens.mapError(
Lens.make<number, "read", "write", never, never>({
get: Effect.fail("read" as const),
changes: Stream.fail("read" as const),
set: () => Effect.fail("write" as const),
}),
() => "mapped",
)
const result = await Effect.runPromise(Effect.all([
Effect.either(Lens.get(lens)),
Effect.either(Lens.set(lens, 1)),
] as const))
expect(result[0].left).toBe("mapped")
expect(result[1].left).toBe("mapped")
})
test("catchAllRead recovers from read failures", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(42),
fallback => Lens.get(
Lens.catchAllRead(
Lens.make<number, "read", never, never, never>({
get: Effect.fail("read" as const),
changes: Stream.fail("read" as const),
set: () => Effect.void,
}),
() => Lens.fromSubscriptionRef(fallback),
),
),
),
)
expect(result).toBe(42)
})
test("tapErrorRead runs an effect on read failures", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(0),
counter => {
const lens = Lens.tapErrorRead(
Lens.make<number, "read", never, never, never>({
get: Effect.fail("read" as const),
changes: Stream.fail("read" as const),
set: () => Effect.void,
}),
() => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const),
)
return Effect.flatMap(
Effect.either(Lens.get(lens)),
() => counter.get,
)
},
),
)
expect(result).toBe(1)
})
test("tapErrorWrite runs an effect on modify failures", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(0),
counter => {
const lens = Lens.tapErrorWrite(
Lens.make<number, never, "write", never, never>({
get: Effect.succeed(1),
changes: Stream.make(1),
set: () => Effect.fail("write" as const),
}),
() => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const),
)
return Effect.flatMap(
Effect.either(Lens.set(lens, 2)),
() => counter.get,
)
},
),
)
expect(result).toBe(1)
})
test("mapOption transforms Some values and preserves None", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
+167
View File
@@ -270,6 +270,173 @@ export const mapStream: {
}))
/**
* Transforms read errors of a `Lens`.
*
* Applies to `get` and `changes` while leaving `modify` unchanged.
*/
export const mapErrorRead: {
<A, ER, EW, RR, RW, E2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: NoInfer<ER>) => E2,
): Lens<A, E2, EW, RR, RW>
<A, ER, EW, RR, RW, E2>(
f: (error: NoInfer<ER>) => E2,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, E2, EW, RR, RW>
} = Function.dual(2, <A, ER, EW, RR, RW, E2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: NoInfer<ER>) => E2,
): Lens<A, E2, EW, RR, RW> => make({
get get() { return Effect.mapError(self.get, f) },
get changes() { return Stream.mapError(self.changes, f) },
get modify() { return self.modify as any },
}))
/**
* Transforms modify errors of a `Lens`.
*
* Applies to the `modify` effect. Since `modify` may also fail with errors coming from the
* user-supplied callback, the handler receives `unknown`.
*/
export const mapErrorWrite: {
<A, ER, EW, RR, RW, E2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => E2,
): Lens<A, ER, E2, RR, RW>
<A, ER, EW, RR, RW, E2>(
f: (error: unknown) => E2,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER, E2, RR, RW>
} = Function.dual(2, <A, ER, EW, RR, RW, E2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => E2,
): Lens<A, ER, E2, RR, RW> => make({
get get() { return self.get },
get changes() { return self.changes },
modify: <B, E1 = never, R1 = never>(
g: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
) => Effect.mapError(self.modify(g), f),
}))
/**
* Transforms all errors of a `Lens`.
*
* Applies to `get`, `changes`, and `modify`. Since `modify` may also fail with errors coming
* from the user-supplied callback, the handler receives `unknown`.
*/
export const mapError: {
<A, ER, EW, RR, RW, E2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => E2,
): Lens<A, E2, E2, RR, RW>
<A, ER, EW, RR, RW, E2>(
f: (error: unknown) => E2,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, E2, E2, RR, RW>
} = Function.dual(2, <A, ER, EW, RR, RW, E2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => E2,
): Lens<A, E2, E2, RR, RW> => make({
get get() { return Effect.mapError(self.get, f) },
get changes() { return Stream.mapError(self.changes, f) },
modify: <B, E1 = never, R1 = never>(
g: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
) => Effect.mapError(self.modify(g), f),
}))
/**
* Recovers from read failures of a `Lens`.
*
* Applies to `get` and `changes` while leaving `modify` unchanged.
*/
export const catchAllRead: {
<A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: NoInfer<ER>) => Subscribable.Subscribable<A, E2, R2>,
): Lens<A, E2, EW, RR | R2, RW>
<A, ER, EW, RR, RW, E2, R2>(
f: (error: NoInfer<ER>) => Subscribable.Subscribable<A, E2, R2>,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, E2, EW, RR | R2, RW>
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: NoInfer<ER>) => Subscribable.Subscribable<A, E2, R2>,
): Lens<A, E2, EW, RR | R2, RW> => make({
get get() { return Effect.catchAll(self.get, error => f(error).get) },
get changes() { return Stream.catchAll(self.changes, error => f(error).changes) },
get modify() { return self.modify as any },
}))
/**
* Runs an effect when read failures occur.
*
* Applies to `get` and `changes` while leaving `modify` unchanged.
*/
export const tapErrorRead: {
<A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: NoInfer<ER>) => Effect.Effect<any, E2, R2>,
): Lens<A, ER | E2, EW, RR | R2, RW>
<A, ER, EW, RR, RW, E2, R2>(
f: (error: NoInfer<ER>) => Effect.Effect<any, E2, R2>,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER | E2, EW, RR | R2, RW>
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: NoInfer<ER>) => Effect.Effect<any, E2, R2>,
): Lens<A, ER | E2, EW, RR | R2, RW> => make({
get get() { return Effect.tapError(self.get, f) },
get changes() { return Stream.tapError(self.changes, f) },
get modify() { return self.modify as any },
}))
/**
* Runs an effect when modify failures occur.
*
* Applies to the `modify` effect. Since `modify` may also fail with errors coming from the
* user-supplied callback, the handler receives `unknown`.
*/
export const tapErrorWrite: {
<A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => Effect.Effect<any, E2, R2>,
): Lens<A, ER, EW | E2, RR, RW | R2>
<A, ER, EW, RR, RW, E2, R2>(
f: (error: unknown) => Effect.Effect<any, E2, R2>,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER, EW | E2, RR, RW | R2>
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => Effect.Effect<any, E2, R2>,
): Lens<A, ER, EW | E2, RR, RW | R2> => make({
get get() { return self.get },
get changes() { return self.changes },
modify: <B, E1 = never, R1 = never>(
g: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
) => Effect.tapError(self.modify(g), f),
}))
/**
* Runs an effect when any `Lens` failure occurs.
*
* Applies to `get`, `changes`, and `modify`. Since `modify` may also fail with errors coming
* from the user-supplied callback, the handler receives `unknown`.
*/
export const tapError: {
<A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => Effect.Effect<any, E2, R2>,
): Lens<A, ER | E2, EW | E2, RR | R2, RW | R2>
<A, ER, EW, RR, RW, E2, R2>(
f: (error: unknown) => Effect.Effect<any, E2, R2>,
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER | E2, EW | E2, RR | R2, RW | R2>
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
self: Lens<A, ER, EW, RR, RW>,
f: (error: unknown) => Effect.Effect<any, E2, R2>,
): Lens<A, ER | E2, EW | E2, RR | R2, RW | R2> => make({
get get() { return Effect.tapError(self.get, f) },
get changes() { return Stream.tapError(self.changes, f) },
modify: <B, E1 = never, R1 = never>(
g: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
) => Effect.tapError(self.modify(g), f),
}))
/**
* Provides a `Context` to a `Lens`, removing it from both the read and write environments.
*/