diff --git a/packages/effect-lens-next/README.md b/packages/effect-lens-next/README.md index 677bea4..67dc5ae 100644 --- a/packages/effect-lens-next/README.md +++ b/packages/effect-lens-next/README.md @@ -240,21 +240,21 @@ const nameLens = lens.pipe( ``` -### Subscribable +### View -Effect 4 no longer provides `Subscribable` and `Readable`. This library provides its own `Subscribable` interface, which you can use as a constraint to allow some parts of your app to only read and subscribe to the Lenses you provide them: +Effect 4 no longer provides its former `Subscribable` and `Readable` abstractions. This library provides a `View` interface, which you can use as a constraint to allow some parts of your app to only read and subscribe to the Lenses you provide them: ```typescript const ref = yield* SubscriptionRef.make<{ readonly users: readonly User[] }>({ users: [...] }) const someFunctionThatShouldOnlyHaveReadonlyAccessToTheState = ( - usersSub: Subscribable.Subscribable + usersView: View.View ) => Effect.gen(function*() { // Do whatever - const usersCountSub = Subscribable.map(usersSub, a => a.length) - const users = yield* usersSub.get - yield* Effect.forkScoped(Stream.runForEach(usersSub.changes, ...)) + const usersCountView = View.map(usersView, a => a.length) + const users = yield* usersView.get + yield* Effect.forkScoped(Stream.runForEach(usersView.changes, ...)) }) const lens = ref.pipe( @@ -265,16 +265,16 @@ yield* someFunctionThatShouldOnlyHaveReadonlyAccessToTheState(lens) ``` #### Focusing -This library provides a `Subscribable` module with value and error transforms, recovery (`catch`, `catchCause`, `orElse`, `orElseSucceed`, and `retry`), and transforms to narrow the focus of `Subscribable`'s, same as Lenses: +This library provides a `View` module with value and error transforms, recovery (`catch`, `catchCause`, `orElse`, `orElseSucceed`, and `retry`), and transforms to narrow the focus of `View`'s, same as Lenses: ```typescript -import { Subscribable } from "effect-lens" +import { View } from "effect-lens" -declare const sub: Subscribable.Subscribable +declare const view: View.View -// \/ Subscribable.Subscribable -const nameSub = sub.pipe( - Subscribable.focusArrayAt(1), - Subscribable.focusObjectOn("name"), +// \/ View.View +const nameView = view.pipe( + View.focusArrayAt(1), + View.focusObjectOn("name"), ) ``` diff --git a/packages/effect-lens-next/package.json b/packages/effect-lens-next/package.json index 85c0a18..e324056 100644 --- a/packages/effect-lens-next/package.json +++ b/packages/effect-lens-next/package.json @@ -21,9 +21,9 @@ "types": "./dist/Lens.d.ts", "default": "./dist/Lens.js" }, - "./Subscribable": { - "types": "./dist/Subscribable.d.ts", - "default": "./dist/Subscribable.js" + "./View": { + "types": "./dist/View.d.ts", + "default": "./dist/View.js" } }, "scripts": { diff --git a/packages/effect-lens-next/src/Lens.ts b/packages/effect-lens-next/src/Lens.ts index c7ee35a..784c5d6 100644 --- a/packages/effect-lens-next/src/Lens.ts +++ b/packages/effect-lens-next/src/Lens.ts @@ -16,7 +16,7 @@ import { SubscriptionRef, SynchronizedRef, } from "effect" -import * as Subscribable from "./Subscribable.js" +import * as View from "./View.js" export const LensTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/Lens") @@ -30,7 +30,7 @@ export type LensTypeId = typeof LensTypeId * 3. a `modify` effect that can transform the current value. */ export interface Lens -extends Subscribable.Subscribable { +extends View.View { readonly [LensTypeId]: LensTypeId readonly modifyEffect: ( @@ -66,7 +66,7 @@ export declare namespace LensImpl { export abstract class LensImpl extends Pipeable.Class implements Lens { - readonly [Subscribable.SubscribableTypeId]: Subscribable.SubscribableTypeId = Subscribable.SubscribableTypeId + readonly [View.ViewTypeId]: View.ViewTypeId = View.ViewTypeId readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId @@ -120,9 +120,9 @@ export const asLensImpl = ( return lens as LensImpl } -export const asSubscribable = ( +export const asView = ( lens: Lens -): Subscribable.Subscribable => lens +): View.View => lens export declare namespace LensLazyImpl { diff --git a/packages/effect-lens-next/src/Subscribable.ts b/packages/effect-lens-next/src/Subscribable.ts deleted file mode 100644 index bb9b53f..0000000 --- a/packages/effect-lens-next/src/Subscribable.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { Array, type Cause, Chunk, Effect, Function, Iterable, Option, Pipeable, Predicate, type Result, type Schedule, Stream } from "effect" - - -export const SubscribableTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/Subscribable") -export type SubscribableTypeId = typeof SubscribableTypeId - -export interface Subscribable extends Pipeable.Pipeable { - readonly [SubscribableTypeId]: SubscribableTypeId - readonly get: Effect.Effect - readonly changes: Stream.Stream -} - -export const isSubscribable = (u: unknown): u is Subscribable => Predicate.hasProperty(u, SubscribableTypeId) - - -export const SubscribableImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/SubscribableImpl") -export type SubscribableImplTypeId = typeof SubscribableImplTypeId - -export declare namespace SubscribableImpl { - export interface Source { - readonly get: Effect.Effect - readonly changes: Stream.Stream - } -} - -export class SubscribableImpl -extends Pipeable.Class implements Subscribable { - readonly [SubscribableTypeId]: SubscribableTypeId = SubscribableTypeId - readonly [SubscribableImplTypeId]: SubscribableImplTypeId = SubscribableImplTypeId - - constructor( - readonly source: SubscribableImpl.Source, - ) { - super() - } - - get get() { return this.source.get } - get changes() { return this.source.changes } -} - -export const isSubscribableImpl = (u: unknown): u is SubscribableImpl => Predicate.hasProperty(u, SubscribableImplTypeId) - -export const asSubscribableImpl = ( - subscribable: Subscribable -): SubscribableImpl => { - if (!isSubscribableImpl(subscribable)) - throw new Error("Not a 'SubscribableImpl'") - return subscribable as SubscribableImpl -} - -export const make = ( - source: SubscribableImpl.Source -): Subscribable => new SubscribableImpl(source) - -export const unwrap = ( - effect: Effect.Effect, E1, R1>, -): Subscribable => make({ - get: Effect.flatMap(effect, self => self.get), - changes: Stream.unwrap(Effect.map(effect, self => self.changes)), -}) - - -export const map: { - (f: (a: NoInfer) => B): (self: Subscribable) => Subscribable - (self: Subscribable, f: (a: NoInfer) => B): Subscribable -} = Function.dual(2, (self: Subscribable, f: (a: NoInfer) => B) => make({ - get get() { return Effect.map(self.get, f) }, - get changes() { return Stream.map(self.changes, f) }, -})) - -export const mapEffect: { - (f: (a: NoInfer) => Effect.Effect): (self: Subscribable) => Subscribable - (self: Subscribable, f: (a: NoInfer) => Effect.Effect): Subscribable -} = Function.dual(2, ( - self: Subscribable, - f: (a: NoInfer) => Effect.Effect, -) => make({ - get get() { return Effect.flatMap(self.get, f) }, - get changes() { return Stream.mapEffect(self.changes, f) }, -})) - -/** Maps over an `Option` value in the `Subscribable`. */ -export const mapOption: { - (f: (a: A) => B): (self: Subscribable, E, R>) => Subscribable, E, R> - (self: Subscribable, E, R>, f: (a: A) => B): Subscribable, E, R> -} = Function.dual(2, (self: Subscribable, E, R>, f: (a: A) => B) => - map(self, Option.map(f)), -) - -/** Maps over an `Option` value in the `Subscribable` with an Effect. */ -export const mapOptionEffect: { - (f: (a: A) => Effect.Effect): (self: Subscribable, E, R>) => Subscribable, E | E2, R | R2> - (self: Subscribable, E, R>, f: (a: A) => Effect.Effect): Subscribable, E | E2, R | R2> -} = Function.dual(2, ( - self: Subscribable, E, R>, - f: (a: A) => Effect.Effect, -) => mapEffect(self, Option.match({ - onSome: a => Effect.map(f(a), Option.some), - onNone: () => Effect.succeed(Option.none()), -}))) - - -/** Maps errors from both the current value and the stream of changes. */ -export const mapError: { - (f: (error: NoInfer) => E2): (self: Subscribable) => Subscribable - (self: Subscribable, f: (error: NoInfer) => E2): Subscribable -} = Function.dual(2, ( - self: Subscribable, - f: (error: NoInfer) => E2, -) => make({ - get get() { return Effect.mapError(self.get, f) }, - get changes() { return Stream.mapError(self.changes, f) }, -})) - -/** Runs an effect when either the current value or the stream of changes fails. */ -export const tapError: { - (f: (error: NoInfer) => Effect.Effect): (self: Subscribable) => Subscribable - (self: Subscribable, f: (error: NoInfer) => Effect.Effect): Subscribable -} = Function.dual(2, ( - self: Subscribable, - f: (error: NoInfer) => Effect.Effect, -) => make({ - get get() { return Effect.tapError(self.get, f) }, - get changes() { return Stream.tapError(self.changes, f) }, -})) - -const catch_: { - (f: (error: NoInfer) => Subscribable): (self: Subscribable) => Subscribable - (self: Subscribable, f: (error: NoInfer) => Subscribable): Subscribable -} = Function.dual(2, ( - self: Subscribable, - f: (error: NoInfer) => Subscribable, -) => make({ - get get() { return Effect.catch(self.get, error => f(error).get) }, - get changes() { return Stream.catch(self.changes, error => f(error).changes) }, -})) - -/** Recovers from typed errors with another `Subscribable`. */ -export { catch_ as catch } - -/** Recovers from all failure causes with another `Subscribable`. */ -export const catchCause: { - (f: (cause: Cause.Cause>) => Subscribable): (self: Subscribable) => Subscribable - (self: Subscribable, f: (cause: Cause.Cause>) => Subscribable): Subscribable -} = Function.dual(2, ( - self: Subscribable, - f: (cause: Cause.Cause>) => Subscribable, -) => make({ - get get() { return Effect.catchCause(self.get, cause => f(cause).get) }, - get changes() { return Stream.catchCause(self.changes, cause => f(cause).changes) }, -})) - -/** Runs an effect when either channel fails, exposing the complete failure cause. */ -export const tapCause: { - (f: (cause: Cause.Cause>) => Effect.Effect): (self: Subscribable) => Subscribable - (self: Subscribable, f: (cause: Cause.Cause>) => Effect.Effect): Subscribable -} = Function.dual(2, ( - self: Subscribable, - f: (cause: Cause.Cause>) => Effect.Effect, -) => make({ - get get() { return Effect.tapCause(self.get, f) }, - get changes() { return Stream.tapCause(self.changes, f) }, -})) - -/** Falls back to another `Subscribable` when either channel fails. */ -export const orElse: { - (that: () => Subscribable): (self: Subscribable) => Subscribable - (self: Subscribable, that: () => Subscribable): Subscribable -} = Function.dual(2, ( - self: Subscribable, - that: () => Subscribable, -) => catch_(self, that)) - -/** Replaces typed errors from either channel with a lazily evaluated value. */ -export const orElseSucceed: { - (value: () => B): (self: Subscribable) => Subscribable - (self: Subscribable, value: () => B): Subscribable -} = Function.dual(2, (self: Subscribable, value: () => B) => make({ - get get() { return Effect.orElseSucceed(self.get, value) }, - get changes() { return Stream.orElseSucceed(self.changes, value) }, -})) - -/** Retries failures from both channels according to the supplied schedule. */ -export const retry: { - (policy: Schedule.Schedule, E2, R2>): (self: Subscribable) => Subscribable - (self: Subscribable, policy: Schedule.Schedule, E2, R2>): Subscribable -} = Function.dual(2, ( - self: Subscribable, - policy: Schedule.Schedule, E2, R2>, -) => make({ - get get() { return Effect.retry(self.get, policy) }, - get changes() { return Stream.retry(self.changes, policy) }, -})) - -/** Converts typed failures from both channels into `Result` values. */ -export const result = ( - self: Subscribable, -): Subscribable, never, R> => make({ - get get() { return Effect.result(self.get) }, - get changes() { return Stream.result(self.changes) }, -}) - - -/** Narrows the focus to a field of an object. */ -export const focusObjectOn: { - (key: K): (self: Subscribable) => Subscribable - (self: Subscribable, key: K): Subscribable -} = Function.dual(2, (self: Subscribable, key: K) => - map(self, a => a[key]), -) - -/** Narrows the focus to an indexed element of an array. */ -export const focusArrayAt: { - (index: number): (self: Subscribable) => Subscribable - (self: Subscribable, index: number): Subscribable -} = Function.dual(2, (self: Subscribable, index: number) => - mapEffect(self, a => Effect.fromOption(Array.get(a, index))), -) - -export const focusArrayLength = ( - self: Subscribable, -): Subscribable => map(self, Array.length) - -/** Narrows the focus to an indexed element of a readonly tuple. */ -export const focusTupleAt: { - (index: I): (self: Subscribable) => Subscribable - (self: Subscribable, index: I): Subscribable -} = Function.dual(2, (self: Subscribable, index: I) => - map(self, Array.getUnsafe(index)), -) - -/** Narrows the focus to an indexed element of `Chunk`. */ -export const focusChunkAt: { - (index: number): (self: Subscribable, E, R>) => Subscribable - (self: Subscribable, E, R>, index: number): Subscribable -} = Function.dual(2, (self: Subscribable, E, R>, index: number) => - mapEffect(self, chunk => Effect.fromOption(Chunk.get(chunk, index))), -) - -export const focusChunkSize = ( - self: Subscribable, E, R>, -): Subscribable => map(self, Chunk.size) - -export const focusIterableSize = , E, R>( - self: Subscribable, -): Subscribable => map(self, Iterable.size) diff --git a/packages/effect-lens-next/src/Subscribable.test.ts b/packages/effect-lens-next/src/View.test.ts similarity index 75% rename from packages/effect-lens-next/src/Subscribable.test.ts rename to packages/effect-lens-next/src/View.test.ts index a79d2b1..8ba4fb0 100644 --- a/packages/effect-lens-next/src/Subscribable.test.ts +++ b/packages/effect-lens-next/src/View.test.ts @@ -1,16 +1,16 @@ import { describe, expect, test } from "bun:test" import { Chunk, Effect, Stream, SubscriptionRef } from "effect" import * as Lens from "./Lens.js" -import * as Subscribable from "./Subscribable.js" +import * as View from "./View.js" -describe("Subscribable", () => { +describe("View", () => { test("mapError transforms errors from get and changes", async () => { - const source = Subscribable.make({ + const source = View.make({ get: Effect.fail("get"), changes: Stream.fail("changes"), }) - const mapped = source.pipe(Subscribable.mapError((error: string) => `mapped:${error}`)) + const mapped = source.pipe(View.mapError((error: string) => `mapped:${error}`)) const result = await Effect.runPromise(Effect.gen(function*() { const getError = yield* Effect.flip(mapped.get) @@ -23,11 +23,11 @@ describe("Subscribable", () => { test("tapError observes errors from get and changes", async () => { const observed: Array = [] - const source = Subscribable.make({ + const source = View.make({ get: Effect.fail("get"), changes: Stream.fail("changes"), }) - const tapped = Subscribable.tapError(source, error => Effect.sync(() => observed.push(error))) + const tapped = View.tapError(source, error => Effect.sync(() => observed.push(error))) await Effect.runPromise(Effect.gen(function*() { yield* Effect.flip(tapped.get) @@ -38,11 +38,11 @@ describe("Subscribable", () => { }) test("catch recovers get and changes with the corresponding fallback channel", async () => { - const source = Subscribable.make({ + const source = View.make({ get: Effect.fail("get"), changes: Stream.fail("changes"), }) - const recovered = source.pipe(Subscribable.catch(() => Subscribable.make({ + const recovered = source.pipe(View.catch(() => View.make({ get: Effect.succeed("fallback-get"), changes: Stream.succeed("fallback-changes"), }))) @@ -57,11 +57,11 @@ describe("Subscribable", () => { }) test("orElseSucceed recovers errors from get and changes", async () => { - const source = Subscribable.make({ + const source = View.make({ get: Effect.fail("get"), changes: Stream.fail("changes"), }) - const recovered = Subscribable.orElseSucceed(source, () => "fallback") + const recovered = View.orElseSucceed(source, () => "fallback") const result = await Effect.runPromise(Effect.gen(function*() { const current = yield* recovered.get @@ -77,12 +77,12 @@ describe("Subscribable", () => { Effect.flatMap( SubscriptionRef.make([1, 2, 3]), parent => { - const sizeSub = Subscribable.focusArrayLength(Lens.fromSubscriptionRef(parent)) + const sizeView = View.focusArrayLength(Lens.fromSubscriptionRef(parent)) return Effect.flatMap( - sizeSub.get, + sizeView.get, initial => Effect.flatMap( SubscriptionRef.set(parent, [1, 2, 3, 4, 5]), - () => Effect.map(sizeSub.get, next => [initial, next] as const), + () => Effect.map(sizeView.get, next => [initial, next] as const), ), ) }, @@ -97,12 +97,12 @@ describe("Subscribable", () => { Effect.flatMap( SubscriptionRef.make(Chunk.make(1, 2) as Chunk.Chunk), parent => { - const sizeSub = Subscribable.focusChunkSize(Lens.fromSubscriptionRef(parent)) + const sizeView = View.focusChunkSize(Lens.fromSubscriptionRef(parent)) return Effect.flatMap( - sizeSub.get, + sizeView.get, initial => Effect.flatMap( SubscriptionRef.set(parent, Chunk.make(1, 2, 3, 4)), - () => Effect.map(sizeSub.get, next => [initial, next] as const), + () => Effect.map(sizeView.get, next => [initial, next] as const), ), ) }, @@ -117,12 +117,12 @@ describe("Subscribable", () => { Effect.flatMap( SubscriptionRef.make([1, 2, 3]), parent => { - const sizeSub = Subscribable.focusIterableSize(Lens.fromSubscriptionRef(parent)) + const sizeView = View.focusIterableSize(Lens.fromSubscriptionRef(parent)) return Effect.flatMap( - sizeSub.get, + sizeView.get, initial => Effect.flatMap( SubscriptionRef.set(parent, [1, 2, 3, 4, 5]), - () => Effect.map(sizeSub.get, next => [initial, next] as const), + () => Effect.map(sizeView.get, next => [initial, next] as const), ), ) }, diff --git a/packages/effect-lens-next/src/View.ts b/packages/effect-lens-next/src/View.ts new file mode 100644 index 0000000..a32e8a7 --- /dev/null +++ b/packages/effect-lens-next/src/View.ts @@ -0,0 +1,246 @@ +import { Array, type Cause, Chunk, Effect, Function, Iterable, Option, Pipeable, Predicate, type Result, type Schedule, Stream } from "effect" + + +export const ViewTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/View") +export type ViewTypeId = typeof ViewTypeId + +export interface View extends Pipeable.Pipeable { + readonly [ViewTypeId]: ViewTypeId + readonly get: Effect.Effect + readonly changes: Stream.Stream +} + +export const isView = (u: unknown): u is View => Predicate.hasProperty(u, ViewTypeId) + + +export const ViewImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/ViewImpl") +export type ViewImplTypeId = typeof ViewImplTypeId + +export declare namespace ViewImpl { + export interface Source { + readonly get: Effect.Effect + readonly changes: Stream.Stream + } +} + +export class ViewImpl +extends Pipeable.Class implements View { + readonly [ViewTypeId]: ViewTypeId = ViewTypeId + readonly [ViewImplTypeId]: ViewImplTypeId = ViewImplTypeId + + constructor( + readonly source: ViewImpl.Source, + ) { + super() + } + + get get() { return this.source.get } + get changes() { return this.source.changes } +} + +export const isViewImpl = (u: unknown): u is ViewImpl => Predicate.hasProperty(u, ViewImplTypeId) + +export const asViewImpl = ( + view: View +): ViewImpl => { + if (!isViewImpl(view)) + throw new Error("Not a 'ViewImpl'") + return view as ViewImpl +} + +export const make = ( + source: ViewImpl.Source +): View => new ViewImpl(source) + +export const unwrap = ( + effect: Effect.Effect, E1, R1>, +): View => make({ + get: Effect.flatMap(effect, self => self.get), + changes: Stream.unwrap(Effect.map(effect, self => self.changes)), +}) + + +export const map: { + (f: (a: NoInfer) => B): (self: View) => View + (self: View, f: (a: NoInfer) => B): View +} = Function.dual(2, (self: View, f: (a: NoInfer) => B) => make({ + get get() { return Effect.map(self.get, f) }, + get changes() { return Stream.map(self.changes, f) }, +})) + +export const mapEffect: { + (f: (a: NoInfer) => Effect.Effect): (self: View) => View + (self: View, f: (a: NoInfer) => Effect.Effect): View +} = Function.dual(2, ( + self: View, + f: (a: NoInfer) => Effect.Effect, +) => make({ + get get() { return Effect.flatMap(self.get, f) }, + get changes() { return Stream.mapEffect(self.changes, f) }, +})) + +/** Maps over an `Option` value in the `View`. */ +export const mapOption: { + (f: (a: A) => B): (self: View, E, R>) => View, E, R> + (self: View, E, R>, f: (a: A) => B): View, E, R> +} = Function.dual(2, (self: View, E, R>, f: (a: A) => B) => + map(self, Option.map(f)), +) + +/** Maps over an `Option` value in the `View` with an Effect. */ +export const mapOptionEffect: { + (f: (a: A) => Effect.Effect): (self: View, E, R>) => View, E | E2, R | R2> + (self: View, E, R>, f: (a: A) => Effect.Effect): View, E | E2, R | R2> +} = Function.dual(2, ( + self: View, E, R>, + f: (a: A) => Effect.Effect, +) => mapEffect(self, Option.match({ + onSome: a => Effect.map(f(a), Option.some), + onNone: () => Effect.succeed(Option.none()), +}))) + + +/** Maps errors from both the current value and the stream of changes. */ +export const mapError: { + (f: (error: NoInfer) => E2): (self: View) => View + (self: View, f: (error: NoInfer) => E2): View +} = Function.dual(2, ( + self: View, + f: (error: NoInfer) => E2, +) => make({ + get get() { return Effect.mapError(self.get, f) }, + get changes() { return Stream.mapError(self.changes, f) }, +})) + +/** Runs an effect when either the current value or the stream of changes fails. */ +export const tapError: { + (f: (error: NoInfer) => Effect.Effect): (self: View) => View + (self: View, f: (error: NoInfer) => Effect.Effect): View +} = Function.dual(2, ( + self: View, + f: (error: NoInfer) => Effect.Effect, +) => make({ + get get() { return Effect.tapError(self.get, f) }, + get changes() { return Stream.tapError(self.changes, f) }, +})) + +const catch_: { + (f: (error: NoInfer) => View): (self: View) => View + (self: View, f: (error: NoInfer) => View): View +} = Function.dual(2, ( + self: View, + f: (error: NoInfer) => View, +) => make({ + get get() { return Effect.catch(self.get, error => f(error).get) }, + get changes() { return Stream.catch(self.changes, error => f(error).changes) }, +})) + +/** Recovers from typed errors with another `View`. */ +export { catch_ as catch } + +/** Recovers from all failure causes with another `View`. */ +export const catchCause: { + (f: (cause: Cause.Cause>) => View): (self: View) => View + (self: View, f: (cause: Cause.Cause>) => View): View +} = Function.dual(2, ( + self: View, + f: (cause: Cause.Cause>) => View, +) => make({ + get get() { return Effect.catchCause(self.get, cause => f(cause).get) }, + get changes() { return Stream.catchCause(self.changes, cause => f(cause).changes) }, +})) + +/** Runs an effect when either channel fails, exposing the complete failure cause. */ +export const tapCause: { + (f: (cause: Cause.Cause>) => Effect.Effect): (self: View) => View + (self: View, f: (cause: Cause.Cause>) => Effect.Effect): View +} = Function.dual(2, ( + self: View, + f: (cause: Cause.Cause>) => Effect.Effect, +) => make({ + get get() { return Effect.tapCause(self.get, f) }, + get changes() { return Stream.tapCause(self.changes, f) }, +})) + +/** Falls back to another `View` when either channel fails. */ +export const orElse: { + (that: () => View): (self: View) => View + (self: View, that: () => View): View +} = Function.dual(2, ( + self: View, + that: () => View, +) => catch_(self, that)) + +/** Replaces typed errors from either channel with a lazily evaluated value. */ +export const orElseSucceed: { + (value: () => B): (self: View) => View + (self: View, value: () => B): View +} = Function.dual(2, (self: View, value: () => B) => make({ + get get() { return Effect.orElseSucceed(self.get, value) }, + get changes() { return Stream.orElseSucceed(self.changes, value) }, +})) + +/** Retries failures from both channels according to the supplied schedule. */ +export const retry: { + (policy: Schedule.Schedule, E2, R2>): (self: View) => View + (self: View, policy: Schedule.Schedule, E2, R2>): View +} = Function.dual(2, ( + self: View, + policy: Schedule.Schedule, E2, R2>, +) => make({ + get get() { return Effect.retry(self.get, policy) }, + get changes() { return Stream.retry(self.changes, policy) }, +})) + +/** Converts typed failures from both channels into `Result` values. */ +export const result = ( + self: View, +): View, never, R> => make({ + get get() { return Effect.result(self.get) }, + get changes() { return Stream.result(self.changes) }, +}) + + +/** Narrows the focus to a field of an object. */ +export const focusObjectOn: { + (key: K): (self: View) => View + (self: View, key: K): View +} = Function.dual(2, (self: View, key: K) => + map(self, a => a[key]), +) + +/** Narrows the focus to an indexed element of an array. */ +export const focusArrayAt: { + (index: number): (self: View) => View + (self: View, index: number): View +} = Function.dual(2, (self: View, index: number) => + mapEffect(self, a => Effect.fromOption(Array.get(a, index))), +) + +export const focusArrayLength = ( + self: View, +): View => map(self, Array.length) + +/** Narrows the focus to an indexed element of a readonly tuple. */ +export const focusTupleAt: { + (index: I): (self: View) => View + (self: View, index: I): View +} = Function.dual(2, (self: View, index: I) => + map(self, Array.getUnsafe(index)), +) + +/** Narrows the focus to an indexed element of `Chunk`. */ +export const focusChunkAt: { + (index: number): (self: View, E, R>) => View + (self: View, E, R>, index: number): View +} = Function.dual(2, (self: View, E, R>, index: number) => + mapEffect(self, chunk => Effect.fromOption(Chunk.get(chunk, index))), +) + +export const focusChunkSize = ( + self: View, E, R>, +): View => map(self, Chunk.size) + +export const focusIterableSize = , E, R>( + self: View, +): View => map(self, Iterable.size) diff --git a/packages/effect-lens-next/src/index.ts b/packages/effect-lens-next/src/index.ts index 24e4bb8..d5df55e 100644 --- a/packages/effect-lens-next/src/index.ts +++ b/packages/effect-lens-next/src/index.ts @@ -1,2 +1,2 @@ export * as Lens from "./Lens.js" -export * as Subscribable from "./Subscribable.js" +export * as View from "./View.js"