From 6e67d8ab7ccef5e1513427fadce7487ffbe3991d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 18 May 2026 14:12:48 +0200 Subject: [PATCH 01/59] Add provide --- packages/effect-lens/README.md | 14 ------- packages/effect-lens/src/Lens.test.ts | 5 +-- packages/effect-lens/src/Lens.ts | 54 ++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index d177c27..c976d80 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -95,20 +95,6 @@ const lens = Effect.all([ Note: while Lens supports asynchronous effects for the proxy logic, we would recommend keeping them synchronous to preserve atomicity. -If a `Lens` depends on a service in its environment, you can provide that service directly to the lens: -```typescript -class Offset extends Context.Tag("Offset")() {} - -const root = Lens.fromSubscriptionRef(ref) -const offsetLens = Lens.mapEffect( - root, - n => Effect.map(Offset, ({ value }) => n + value), - (_n, next) => Effect.map(Offset, ({ value }) => next - value), -) - -const runnableLens = Lens.provide(offsetLens, Offset, { value: 5 }) -``` - ### Focusing diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index e417b42..c0707c3 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Chunk, Context, Effect, Option, SubscriptionRef } from "effect" +import { Chunk, Context, Effect, Layer, Option, SubscriptionRef } from "effect" import * as Lens from "./Lens.js" @@ -67,8 +67,7 @@ describe("Lens", () => { n => Effect.map(Offset, ({ value }) => n + value), (_n, next) => Effect.map(Offset, ({ value }) => next - value), ), - Offset, - { value: 5 }, + Layer.succeed(Offset, { value: 5 }), ) return Effect.flatMap( diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 9c6663b..7ad71fe 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, Readable, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, Effect, Function, identity, type Layer, Option, Pipeable, Predicate, Readable, Stream, type Context, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -273,6 +273,58 @@ export const mapStream: { * Provides a single service to a `Lens`, removing it from both the read and write environments. */ export const provide: { + ]>( + layers: Layers, + ): (self: Lens) => Lens< + A, + ER | { [k in keyof Layers]: Layer.Layer.Error }[number], + EW | { [k in keyof Layers]: Layer.Layer.Error }[number], + { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]>, + { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]> + > + ( + layer: Layer.Layer, + ): (self: Lens) => Lens, RIn | Exclude> + ( + context: Context.Context, + ): (self: Lens) => Lens, Exclude> + ]>( + self: Lens, + layers: Layers, + ): Lens< + A, + ER | { [k in keyof Layers]: Layer.Layer.Error }[number], + EW | { [k in keyof Layers]: Layer.Layer.Error }[number], + { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]>, + { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]> + > + ( + self: Lens, + layer: Layer.Layer, + ): Lens, RIn | Exclude> + ( + self: Lens, + context: Context.Context, + ): Lens, Exclude> +} = Function.dual(2, ( + self: Lens, + provider: unknown, +): Lens => make({ + get get() { return Effect.provide(self.get, provider as any) }, + get changes() { + return Stream.unwrapScoped( + Effect.map( + Effect.provide(Effect.context(), provider as any), + (context) => Stream.provideContext(self.changes as Stream.Stream, context), + ), + ) + }, + modify: ( + f: (a: A) => Effect.Effect + ) => Effect.provide(self.modify(f), provider as any), +})) + +export const provideService: { ( self: Lens, tag: Context.Tag, -- 2.52.0 From 7fa5f54f88b1346fd33e45bca2fda95e20ef8ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 18 May 2026 14:32:26 +0200 Subject: [PATCH 02/59] Fix provide --- packages/effect-lens/src/Lens.ts | 43 +++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 7ad71fe..491235d 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, Effect, Function, identity, type Layer, Option, Pipeable, Predicate, Readable, Stream, type Context, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, type Context, Effect, Function, identity, type Layer, type ManagedRuntime, Option, Pipeable, Predicate, Readable, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -270,7 +270,12 @@ export const mapStream: { })) /** - * Provides a single service to a `Lens`, removing it from both the read and write environments. + * Provides the requirements of a `Lens`, removing them from both the read and write environments. + * + * Accepts the same provider forms as `Effect.provide`: one or more `Layer`s, a `Context`, + * a `Runtime`, or a `ManagedRuntime`. + * + * Layer providers may add their construction errors to both the read and write error channels. */ export const provide: { ]>( @@ -288,6 +293,12 @@ export const provide: { ( context: Context.Context, ): (self: Lens) => Lens, Exclude> + ( + runtime: Runtime.Runtime, + ): (self: Lens) => Lens, Exclude> + ( + managedRuntime: ManagedRuntime.ManagedRuntime, + ): (self: Lens) => Lens, Exclude> ]>( self: Lens, layers: Layers, @@ -306,24 +317,38 @@ export const provide: { self: Lens, context: Context.Context, ): Lens, Exclude> -} = Function.dual(2, ( + ( + self: Lens, + runtime: Runtime.Runtime, + ): Lens, Exclude> + ( + self: Lens, + runtime: ManagedRuntime.ManagedRuntime, + ): Lens, Exclude> +} = Function.dual(2, ( self: Lens, - provider: unknown, -): Lens => make({ - get get() { return Effect.provide(self.get, provider as any) }, + layer: Layer.Layer, +): Lens, RIn | Exclude> => make({ + get get() { return Effect.provide(self.get, layer) }, get changes() { return Stream.unwrapScoped( Effect.map( - Effect.provide(Effect.context(), provider as any), - (context) => Stream.provideContext(self.changes as Stream.Stream, context), + Effect.provide(Effect.context(), layer), + context => Stream.provideContext(self.changes, context), ), ) }, modify: ( f: (a: A) => Effect.Effect - ) => Effect.provide(self.modify(f), provider as any), + ) => Effect.provide(self.modify(f), layer), })) +/** + * Provides a single service to a `Lens`, removing it from both the read and write environments. + * + * This is the `Lens` equivalent of `Effect.provideService`: use it when a lens requires one + * `Context.Tag` and you already have the concrete service value. + */ export const provideService: { ( self: Lens, -- 2.52.0 From 4a0a53ae3b264429ec64c9991955c661e3dd0781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 18 May 2026 20:28:51 +0200 Subject: [PATCH 03/59] Fix provide --- packages/effect-lens/src/Lens.test.ts | 8 +-- packages/effect-lens/src/Lens.ts | 83 +++++++++++---------------- 2 files changed, 38 insertions(+), 53 deletions(-) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index c0707c3..0bd111c 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Chunk, Context, Effect, Layer, Option, SubscriptionRef } from "effect" +import { Chunk, Context, Effect, Option, SubscriptionRef } from "effect" import * as Lens from "./Lens.js" @@ -56,18 +56,18 @@ describe("Lens", () => { expect(result[1]).toEqual(Option.some(50)) // 100 / 2 }) - test("provide supplies a service to get and modify", async () => { + test("provideContext supplies a service to get and modify", async () => { const result = await Effect.runPromise( Effect.flatMap( SubscriptionRef.make(10), parent => { - const lens = Lens.provide( + const lens = Lens.provideContext( Lens.mapEffect( Lens.fromSubscriptionRef(parent), n => Effect.map(Offset, ({ value }) => n + value), (_n, next) => Effect.map(Offset, ({ value }) => next - value), ), - Layer.succeed(Offset, { value: 5 }), + Context.make(Offset, { value: 5 }), ) return Effect.flatMap( diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 491235d..4a23c27 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, type Context, Effect, Function, identity, type Layer, type ManagedRuntime, Option, Pipeable, Predicate, Readable, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, type Context, Effect, Function, identity, type ManagedRuntime, Option, Pipeable, Predicate, Readable, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -269,54 +269,41 @@ export const mapStream: { get modify() { return self.modify }, })) + /** - * Provides the requirements of a `Lens`, removing them from both the read and write environments. - * - * Accepts the same provider forms as `Effect.provide`: one or more `Layer`s, a `Context`, - * a `Runtime`, or a `ManagedRuntime`. - * - * Layer providers may add their construction errors to both the read and write error channels. + * Provides a `Context` to a `Lens`, removing it from both the read and write environments. */ -export const provide: { - ]>( - layers: Layers, - ): (self: Lens) => Lens< - A, - ER | { [k in keyof Layers]: Layer.Layer.Error }[number], - EW | { [k in keyof Layers]: Layer.Layer.Error }[number], - { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]>, - { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]> - > - ( - layer: Layer.Layer, - ): (self: Lens) => Lens, RIn | Exclude> +export const provideContext: { + ( + self: Lens, + context: Context.Context, + ): Lens, Exclude> ( context: Context.Context, ): (self: Lens) => Lens, Exclude> +} = Function.dual(2, ( + self: Lens, + context: Context.Context, +): Lens, Exclude> => make({ + get get() { return Effect.provide(self.get, context) }, + get changes() { return Stream.provideSomeContext(self.changes, context) }, + modify: ( + f: (a: A) => Effect.Effect + ) => Effect.provide(self.modify(f), context), +})) + +/** + * Provides a `Runtime` or `ManagedRuntime` to a `Lens`, removing it from both the read and write environments. + * + * `ManagedRuntime` may add its construction errors to both the read and write error channels. + */ +export const provideRuntime: { ( runtime: Runtime.Runtime, ): (self: Lens) => Lens, Exclude> ( managedRuntime: ManagedRuntime.ManagedRuntime, ): (self: Lens) => Lens, Exclude> - ]>( - self: Lens, - layers: Layers, - ): Lens< - A, - ER | { [k in keyof Layers]: Layer.Layer.Error }[number], - EW | { [k in keyof Layers]: Layer.Layer.Error }[number], - { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]>, - { [k in keyof Layers]: Layer.Layer.Context }[number] | Exclude }[number]> - > - ( - self: Lens, - layer: Layer.Layer, - ): Lens, RIn | Exclude> - ( - self: Lens, - context: Context.Context, - ): Lens, Exclude> ( self: Lens, runtime: Runtime.Runtime, @@ -325,22 +312,20 @@ export const provide: { self: Lens, runtime: ManagedRuntime.ManagedRuntime, ): Lens, Exclude> -} = Function.dual(2, ( +} = Function.dual(2, ( self: Lens, - layer: Layer.Layer, -): Lens, RIn | Exclude> => make({ - get get() { return Effect.provide(self.get, layer) }, + runtime: Runtime.Runtime, +) => make, Exclude>({ + get get() { return Effect.provide(self.get, runtime) }, get changes() { - return Stream.unwrapScoped( - Effect.map( - Effect.provide(Effect.context(), layer), - context => Stream.provideContext(self.changes, context), - ), - ) + return Stream.unwrap(Effect.map( + Effect.provide(Effect.context(), runtime), + context => Stream.provideContext(self.changes, context), + )) }, modify: ( f: (a: A) => Effect.Effect - ) => Effect.provide(self.modify(f), layer), + ) => Effect.provide(self.modify(f), runtime), })) /** -- 2.52.0 From 31f275ec448476c890a00edae3c28dc911e814bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 19 May 2026 13:38:50 +0200 Subject: [PATCH 04/59] Add Subscribable helpers --- packages/effect-lens/README.md | 3 + packages/effect-lens/src/Subscribable.test.ts | 66 +++++++++++++++++++ packages/effect-lens/src/Subscribable.ts | 23 ++++++- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 packages/effect-lens/src/Subscribable.test.ts diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index c976d80..22887dc 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -241,8 +241,11 @@ Currently available: | - | - | | `focusObjectOn` | Focuses to the field of an object | | `focusArrayAt` | Focuses to an indexed entry of an array | +| `focusArrayLength` | Focuses to the length of an array | | `focusTupleAt` | Focuses to an indexed entry of a tuple | | `focusChunkAt` | Focuses to an indexed entry of a `Chunk` | +| `focusChunkSize` | Focuses to the size of a `Chunk` | +| `focusIterableSize` | Focuses to the size of an iterable | ## Todo diff --git a/packages/effect-lens/src/Subscribable.test.ts b/packages/effect-lens/src/Subscribable.test.ts new file mode 100644 index 0000000..19d470f --- /dev/null +++ b/packages/effect-lens/src/Subscribable.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "bun:test" +import { Chunk, Effect, SubscriptionRef } from "effect" +import * as Subscribable from "./Subscribable.js" + + +describe("Subscribable", () => { + test("focusArrayLength reads the current array length and reflects updates", async () => { + const result = await Effect.runPromise( + Effect.flatMap( + SubscriptionRef.make([1, 2, 3]), + parent => { + const sizeSub = Subscribable.focusArrayLength(parent) + return Effect.flatMap( + sizeSub.get, + initial => Effect.flatMap( + SubscriptionRef.set(parent, [1, 2, 3, 4, 5]), + () => Effect.map(sizeSub.get, next => [initial, next] as const), + ), + ) + }, + ), + ) + + expect(result).toEqual([3, 5]) + }) + + test("focusChunkSize reads the current chunk size and reflects updates", async () => { + const result = await Effect.runPromise( + Effect.flatMap( + SubscriptionRef.make(Chunk.make(1, 2) as Chunk.Chunk), + parent => { + const sizeSub = Subscribable.focusChunkSize(parent) + return Effect.flatMap( + sizeSub.get, + initial => Effect.flatMap( + SubscriptionRef.set(parent, Chunk.make(1, 2, 3, 4)), + () => Effect.map(sizeSub.get, next => [initial, next] as const), + ), + ) + }, + ), + ) + + expect(result).toEqual([2, 4]) + }) + + test("focusIterableSize also works for array values", async () => { + const result = await Effect.runPromise( + Effect.flatMap( + SubscriptionRef.make([1, 2, 3]), + parent => { + const sizeSub = Subscribable.focusIterableSize(parent) + return Effect.flatMap( + sizeSub.get, + initial => Effect.flatMap( + SubscriptionRef.set(parent, [1, 2, 3, 4, 5]), + () => Effect.map(sizeSub.get, next => [initial, next] as const), + ), + ) + }, + ), + ) + + expect(result).toEqual([3, 5]) + }) +}) diff --git a/packages/effect-lens/src/Subscribable.ts b/packages/effect-lens/src/Subscribable.ts index 2eb98a5..6a04f69 100644 --- a/packages/effect-lens/src/Subscribable.ts +++ b/packages/effect-lens/src/Subscribable.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, Effect, Function, Option, Subscribable } from "effect" +import { Array, Chunk, Effect, Function, Iterable, Option, Subscribable } from "effect" import type { NoSuchElementException } from "effect/Cause" @@ -71,6 +71,13 @@ export const focusArrayAt: { index: number, ): Subscribable.Subscribable => Subscribable.mapEffect(self, Array.get(index))) +/** + * Narrows the focus to the length of an array. + */ +export const focusArrayLength = ( + self: Subscribable.Subscribable, +): Subscribable.Subscribable => Subscribable.map(self, a => a.length) + /** * Narrows the focus to an indexed element of a readonly tuple. */ @@ -102,3 +109,17 @@ export const focusChunkAt: { self: Subscribable.Subscribable, E, R>, index: number, ): Subscribable.Subscribable => Subscribable.mapEffect(self, Chunk.get(index))) + +/** + * Narrows the focus to the size of a `Chunk`. + */ +export const focusChunkSize = ( + self: Subscribable.Subscribable, E, R>, +): Subscribable.Subscribable => Subscribable.map(self, Chunk.size) + +/** + * Narrows the focus to the size of a `Iterable`. + */ +export const focusIterableSize = ( + self: Subscribable.Subscribable, E, R>, +): Subscribable.Subscribable => Subscribable.map(self, Iterable.size) -- 2.52.0 From 838a0003e9b1b3d7532e641d3f4094ec6205fa61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 19 May 2026 13:39:38 +0200 Subject: [PATCH 05/59] Fix --- packages/effect-lens/src/Subscribable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect-lens/src/Subscribable.ts b/packages/effect-lens/src/Subscribable.ts index 6a04f69..149efa9 100644 --- a/packages/effect-lens/src/Subscribable.ts +++ b/packages/effect-lens/src/Subscribable.ts @@ -76,7 +76,7 @@ export const focusArrayAt: { */ export const focusArrayLength = ( self: Subscribable.Subscribable, -): Subscribable.Subscribable => Subscribable.map(self, a => a.length) +): Subscribable.Subscribable => Subscribable.map(self, Array.length) /** * Narrows the focus to an indexed element of a readonly tuple. -- 2.52.0 From b21422470870cfa33b39694daffd0e93d9101e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 19 May 2026 14:04:29 +0200 Subject: [PATCH 06/59] Add error management --- packages/effect-lens/src/Lens.test.ts | 119 +++++++++++++++++- packages/effect-lens/src/Lens.ts | 167 ++++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 1 deletion(-) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 0bd111c..58b118f 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -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")() {} + test("mapErrorRead transforms read errors", async () => { + const lens = Lens.mapErrorRead( + Lens.make({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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( diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 4a23c27..1468d87 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -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: { + ( + self: Lens, + f: (error: NoInfer) => E2, + ): Lens + ( + f: (error: NoInfer) => E2, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: NoInfer) => E2, +): Lens => 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: { + ( + self: Lens, + f: (error: unknown) => E2, + ): Lens + ( + f: (error: unknown) => E2, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: unknown) => E2, +): Lens => make({ + get get() { return self.get }, + get changes() { return self.changes }, + modify: ( + g: (a: A) => Effect.Effect + ) => 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: { + ( + self: Lens, + f: (error: unknown) => E2, + ): Lens + ( + f: (error: unknown) => E2, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: unknown) => E2, +): Lens => make({ + get get() { return Effect.mapError(self.get, f) }, + get changes() { return Stream.mapError(self.changes, f) }, + modify: ( + g: (a: A) => Effect.Effect + ) => 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: { + ( + self: Lens, + f: (error: NoInfer) => Subscribable.Subscribable, + ): Lens + ( + f: (error: NoInfer) => Subscribable.Subscribable, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: NoInfer) => Subscribable.Subscribable, +): Lens => 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: { + ( + self: Lens, + f: (error: NoInfer) => Effect.Effect, + ): Lens + ( + f: (error: NoInfer) => Effect.Effect, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: NoInfer) => Effect.Effect, +): Lens => 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: { + ( + self: Lens, + f: (error: unknown) => Effect.Effect, + ): Lens + ( + f: (error: unknown) => Effect.Effect, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: unknown) => Effect.Effect, +): Lens => make({ + get get() { return self.get }, + get changes() { return self.changes }, + modify: ( + g: (a: A) => Effect.Effect + ) => 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: { + ( + self: Lens, + f: (error: unknown) => Effect.Effect, + ): Lens + ( + f: (error: unknown) => Effect.Effect, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: unknown) => Effect.Effect, +): Lens => make({ + get get() { return Effect.tapError(self.get, f) }, + get changes() { return Stream.tapError(self.changes, f) }, + modify: ( + g: (a: A) => Effect.Effect + ) => Effect.tapError(self.modify(g), f), +})) + + /** * Provides a `Context` to a `Lens`, removing it from both the read and write environments. */ -- 2.52.0 From 27bb0d59e19b3d9c386e6ef44403eae468fbadd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 20 May 2026 03:14:05 +0200 Subject: [PATCH 07/59] Refactor --- packages/effect-lens/src/Lens.ts | 204 ++++++++++++++++++++++--------- 1 file changed, 145 insertions(+), 59 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 1468d87..5ef8119 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, type Context, Effect, Function, identity, type ManagedRuntime, Option, Pipeable, Predicate, Readable, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, type Context, Effect, Function, identity, type ManagedRuntime, Option, Pipeable, Predicate, PubSub, Readable, Ref, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -17,80 +17,167 @@ export interface Lens { readonly [LensTypeId]: LensTypeId - readonly modify: ( + readonly modifyEffect: ( f: (a: A) => Effect.Effect ) => Effect.Effect } -/** - * Internal `Lens` implementation. - */ -export class LensImpl -extends Pipeable.Class() implements Lens { - readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId - readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId - readonly [LensTypeId]: LensTypeId = LensTypeId - - constructor( - readonly get: Effect.Effect, - readonly changes: Stream.Stream, - readonly modify: ( - f: (a: A) => Effect.Effect - ) => Effect.Effect, - ) { - super() - } -} - - /** * Checks whether a value is a `Lens`. */ export const isLens = (u: unknown): u is Lens => Predicate.hasProperty(u, LensTypeId) +export const LensWithInternalsTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensWithInternals") +export type LensWithInternalsTypeId = typeof LensWithInternalsTypeId + +export interface LensWithInternals +extends Lens { + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId + + readonly update: (a: A) => Effect.Effect + readonly semaphore: Effect.Semaphore +} + +export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) + + +export declare namespace LensImpl { + export interface Source { + readonly get: Effect.Effect, + readonly changes: Stream.Stream, + readonly update: (a: A) => Effect.Effect, + readonly semaphore: Effect.Semaphore, + } +} + +export class LensImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + constructor( + readonly get: Effect.Effect, + readonly changes: Stream.Stream, + readonly update: (a: A) => Effect.Effect, + readonly semaphore: Effect.Semaphore, + ) { + super() + } + + modifyEffect( + f: (a: A) => Effect.Effect + ) { + return this.semaphore.withPermits(1)( + Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + )) + ) + } +} /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. - * - * Either `modify` or `set` needs to be supplied. */ export const make = ( - options: { - readonly get: Effect.Effect - readonly changes: Stream.Stream - } & ( - | { - readonly modify: ( - f: (a: A) => Effect.Effect - ) => Effect.Effect - } - | { readonly set: (a: A) => Effect.Effect } - ) -): Lens => new LensImpl( - options.get, - options.changes, - Predicate.hasProperty(options, "modify") - ? options.modify - : ( - f: (a: A) => Effect.Effect - ) => Effect.flatMap( - options.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(options.set(next), b) - )), -) + source: LensImpl.Source +): Lens => new LensImpl(source.get, source.changes, source.update, source.semaphore) + + +export class LensLazyImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + constructor( + readonly source: LensImpl.Source + ) { + super() + } + + get get() { return this.source.get } + get changes() { return this.source.changes } + get update() { return this.source.update } + get semaphore() { return this.source.semaphore } + + modifyEffect( + f: (a: A) => Effect.Effect + ) { + return this.semaphore.withPermits(1)( + Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + )) + ) + } +} + +/** + * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. + */ +export const makeLazy = ( + source: LensImpl.Source +): Lens => new LensLazyImpl(source) + + +export declare namespace SubscriptionRefLensImpl { + export interface SubscriptionRefWithInternals + extends SubscriptionRef.SubscriptionRef { + readonly ref: Ref.Ref + readonly pubsub: PubSub.PubSub + readonly semaphore: Effect.Semaphore + } +} + +export class SubscriptionRefLensImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals + + constructor( + ref: SubscriptionRef.SubscriptionRef + ) { + super() + this.ref = ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals + } + + get get() { return this.ref.get } + get changes() { return this.ref.changes } + update(a: A) { + return Effect.zipLeft( + Ref.set(this.ref.ref, a), + PubSub.publish(this.ref.pubsub, a), + ) + } + get semaphore() { return this.ref.semaphore } + + modifyEffect( + f: (a: A) => Effect.Effect + ) { + return this.semaphore.withPermits(1)( + Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + )) + ) + } +} /** * Creates a `Lens` that proxies a `SubscriptionRef`. */ export const fromSubscriptionRef = ( ref: SubscriptionRef.SubscriptionRef -): Lens => make({ - get get() { return ref.get }, - get changes() { return ref.changes }, - modify: ( - f: (a: A) => Effect.Effect - ) => ref.modifyEffect(f), -}) +): Lens => new SubscriptionRefLensImpl(ref) + /** * Creates a `Lens` that proxies a `SynchronizedRef`. @@ -108,6 +195,7 @@ export const fromSynchronizedRef = ( ) => ref.modifyEffect(f), }) + /** * Flattens an effectful `Lens`. */ @@ -116,11 +204,9 @@ export const unwrap = ( ): Lens => make({ get: Effect.flatMap(effect, l => l.get), changes: Stream.unwrap(Effect.map(effect, l => l.changes)), - modify: ( - f: (a: A) => Effect.Effect - ) => Effect.flatMap(effect, l => l.modify(f)), -}) + update: a => Effect.flatMap(effect, l => (l as LensWithInternals).update(a)) +}) /** * Derives a new `Lens` by applying synchronous getters and setters over the focused value. -- 2.52.0 From 326894be1da2c68415026ab9bff1ce99d5c4fa9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 20 May 2026 03:20:05 +0200 Subject: [PATCH 08/59] Refactor --- packages/effect-lens/src/Lens.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 5ef8119..b3384b7 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -57,13 +57,20 @@ extends Pipeable.Class() implements LensWithInternals { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + readonly get: Effect.Effect + readonly changes: Stream.Stream + readonly update: (a: A) => Effect.Effect + readonly semaphore: Effect.Semaphore + constructor( - readonly get: Effect.Effect, - readonly changes: Stream.Stream, - readonly update: (a: A) => Effect.Effect, - readonly semaphore: Effect.Semaphore, + source: LensImpl.Source ) { super() + + this.get = source.get + this.changes = source.changes + this.update = source.update + this.semaphore = source.semaphore } modifyEffect( @@ -83,7 +90,7 @@ extends Pipeable.Class() implements LensWithInternals { */ export const make = ( source: LensImpl.Source -): Lens => new LensImpl(source.get, source.changes, source.update, source.semaphore) +): Lens => new LensImpl(source) export class LensLazyImpl -- 2.52.0 From a65f139858a06dc2516cb50af8447de043b88c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 20 May 2026 17:08:40 +0200 Subject: [PATCH 09/59] Refactor --- packages/effect-lens/src/Lens.ts | 73 +++++++++++++++----------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index b3384b7..b5ddd94 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -35,18 +35,24 @@ extends Lens { readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId readonly update: (a: A) => Effect.Effect - readonly semaphore: Effect.Semaphore + readonly withLock: (self: Effect.Effect) => Effect.Effect } export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) +export const asLensWithInternals = ( + lens: Lens +): Effect.Effect, never, never> => isLensWithInternals(lens) + ? Effect.succeed(lens as LensWithInternals) + : Effect.die("Not a 'LensWithInternals'.") + export declare namespace LensImpl { export interface Source { readonly get: Effect.Effect, readonly changes: Stream.Stream, readonly update: (a: A) => Effect.Effect, - readonly semaphore: Effect.Semaphore, + readonly withLock: (self: Effect.Effect) => Effect.Effect, } } @@ -60,7 +66,7 @@ extends Pipeable.Class() implements LensWithInternals { readonly get: Effect.Effect readonly changes: Stream.Stream readonly update: (a: A) => Effect.Effect - readonly semaphore: Effect.Semaphore + readonly withLock: (self: Effect.Effect) => Effect.Effect constructor( source: LensImpl.Source @@ -70,19 +76,20 @@ extends Pipeable.Class() implements LensWithInternals { this.get = source.get this.changes = source.changes this.update = source.update - this.semaphore = source.semaphore + this.withLock = source.withLock } - modifyEffect( - f: (a: A) => Effect.Effect - ) { - return this.semaphore.withPermits(1)( - Effect.flatMap( - this.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), - )) - ) - } + modifyEffect = modifyEffect +} + +function modifyEffect( + this: LensWithInternals, + f: (a: A) => Effect.Effect, +): Effect.Effect { + return this.withLock(Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + ))) } /** @@ -109,18 +116,9 @@ extends Pipeable.Class() implements LensWithInternals { get get() { return this.source.get } get changes() { return this.source.changes } get update() { return this.source.update } - get semaphore() { return this.source.semaphore } + get withLock() { return this.source.withLock } - modifyEffect( - f: (a: A) => Effect.Effect - ) { - return this.semaphore.withPermits(1)( - Effect.flatMap( - this.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), - )) - ) - } + modifyEffect = modifyEffect } /** @@ -164,18 +162,9 @@ extends Pipeable.Class() implements LensWithInternals( - f: (a: A) => Effect.Effect - ) { - return this.semaphore.withPermits(1)( - Effect.flatMap( - this.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), - )) - ) - } + modifyEffect = modifyEffect } /** @@ -211,8 +200,16 @@ export const unwrap = ( ): Lens => make({ get: Effect.flatMap(effect, l => l.get), changes: Stream.unwrap(Effect.map(effect, l => l.changes)), - update: a => Effect.flatMap(effect, l => (l as LensWithInternals).update(a)) - + update: a => Effect.flatMap( + effect, + l => Effect.flatMap(asLensWithInternals(l), l => l.update(a)) + ), + withLock: ( + effect2: Effect.Effect + ): Effect.Effect => Effect.flatMap( + effect, + l => Effect.flatMap(asLensWithInternals(l), l2 => l2.withLock(effect2)), + ), }) /** -- 2.52.0 From 6205bf36165f66a44a6946af1e4d0a5d8b29bd28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 20 May 2026 17:19:03 +0200 Subject: [PATCH 10/59] Refactor --- packages/effect-lens/src/Lens.ts | 60 +++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index b5ddd94..d7faf36 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -129,6 +129,49 @@ export const makeLazy = ( ): Lens => new LensLazyImpl(source) +export declare namespace SynchronizedRefLensImpl { + export interface SynchronizedRefWithInternals + extends SynchronizedRef.SynchronizedRef { + readonly ref: Ref.Ref + readonly withLock: (self: Effect.Effect) => Effect.Effect + } +} + +export class SynchronizedRefLensImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals + + constructor( + ref: SynchronizedRef.SynchronizedRef + ) { + super() + this.ref = ref as SynchronizedRefLensImpl.SynchronizedRefWithInternals + } + + get get() { return this.ref.get } + get changes() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } + update(a: A) { return Ref.set(this.ref.ref, a) } + get withLock() { return this.ref.withLock } + + modifyEffect = modifyEffect +} + +/** + * Creates a `Lens` that proxies a `SynchronizedRef`. + * + * Note: since `SynchronizedRef` does not provide any kind of reactivity mechanism, the produced `Lens` will be non-reactive. + * This means its `changes` stream will only emit the current value once when evaluated and nothing else. + */ +export const fromSynchronizedRef = ( + ref: SynchronizedRef.SynchronizedRef +): Lens => new SynchronizedRefLensImpl(ref) + + export declare namespace SubscriptionRefLensImpl { export interface SubscriptionRefWithInternals extends SubscriptionRef.SubscriptionRef { @@ -175,23 +218,6 @@ export const fromSubscriptionRef = ( ): Lens => new SubscriptionRefLensImpl(ref) -/** - * Creates a `Lens` that proxies a `SynchronizedRef`. - * - * Note: since `SynchronizedRef` does not provide any kind of reactivity mechanism, the produced `Lens` will be non-reactive. - * This means its `changes` stream will only emit the current value once when evaluated and nothing else. - */ -export const fromSynchronizedRef = ( - ref: SynchronizedRef.SynchronizedRef -): Lens => make({ - get get() { return ref.get }, - get changes() { return Stream.unwrap(Effect.map(ref.get, Stream.make)) }, - modify: ( - f: (a: A) => Effect.Effect - ) => ref.modifyEffect(f), -}) - - /** * Flattens an effectful `Lens`. */ -- 2.52.0 From 279c0f4a0c1fd7c6eb3afc1f8c0153acba470177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 20 May 2026 18:11:13 +0200 Subject: [PATCH 11/59] Refactor --- packages/effect-lens/src/Lens.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index d7faf36..a891549 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -245,17 +245,17 @@ export const map: { ( self: Lens, get: (a: NoInfer) => B, - set: (a: NoInfer, b: B) => NoInfer, + update: (a: NoInfer, b: B) => NoInfer, ): Lens ( get: (a: NoInfer) => B, - set: (a: NoInfer, b: B) => NoInfer, + update: (a: NoInfer, b: B) => NoInfer, ): (self: Lens) => Lens } = Function.dual(3, ( self: Lens, get: (a: NoInfer) => B, - set: (a: NoInfer, b: B) => NoInfer, -): Lens => make({ + update: (a: NoInfer, b: B) => NoInfer, +): Lens => makeLazy({ get get() { return Effect.map(self.get, get) }, get changes() { return Stream.map(self.changes, get) }, modify: ( -- 2.52.0 From 74519010b30719c4ee4183ebd70d7381301182b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 20 May 2026 22:14:04 +0200 Subject: [PATCH 12/59] Refactor --- packages/effect-lens/src/Lens.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index a891549..77b7b54 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -35,16 +35,18 @@ extends Lens { readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId readonly update: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly withLock: (self: Effect.Effect) => Effect.Effect } export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) export const asLensWithInternals = ( lens: Lens -): Effect.Effect, never, never> => isLensWithInternals(lens) - ? Effect.succeed(lens as LensWithInternals) - : Effect.die("Not a 'LensWithInternals'.") +): LensWithInternals => { + if (!isLensWithInternals(lens)) + throw new Error("Not a 'LensWithInternals'.") + return lens as LensWithInternals +} export declare namespace LensImpl { @@ -52,7 +54,7 @@ export declare namespace LensImpl { readonly get: Effect.Effect, readonly changes: Stream.Stream, readonly update: (a: A) => Effect.Effect, - readonly withLock: (self: Effect.Effect) => Effect.Effect, + readonly withLock: (self: Effect.Effect) => Effect.Effect, } } @@ -66,7 +68,7 @@ extends Pipeable.Class() implements LensWithInternals { readonly get: Effect.Effect readonly changes: Stream.Stream readonly update: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly withLock: (self: Effect.Effect) => Effect.Effect constructor( source: LensImpl.Source @@ -226,16 +228,10 @@ export const unwrap = ( ): Lens => make({ get: Effect.flatMap(effect, l => l.get), changes: Stream.unwrap(Effect.map(effect, l => l.changes)), - update: a => Effect.flatMap( - effect, - l => Effect.flatMap(asLensWithInternals(l), l => l.update(a)) - ), + update: a => Effect.flatMap(effect, l => asLensWithInternals(l).update(a)), withLock: ( effect2: Effect.Effect - ): Effect.Effect => Effect.flatMap( - effect, - l => Effect.flatMap(asLensWithInternals(l), l2 => l2.withLock(effect2)), - ), + ) => Effect.flatMap(effect, l => asLensWithInternals(l).withLock(effect2)), }) /** @@ -258,11 +254,8 @@ export const map: { ): Lens => makeLazy({ get get() { return Effect.map(self.get, get) }, get changes() { return Stream.map(self.changes, get) }, - modify: ( - f: (b: B) => Effect.Effect - ) => self.modify(a => - Effect.flatMap(f(get(a)), ([c, next]) => Effect.succeed([c, set(a, next)])) - ), + update(a: A) { return }, + get withLock() { return asLensWithInternals(self).withLock }, })) /** -- 2.52.0 From b37480b74b0fac15f87c84f94f73c19fd772b508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 21 May 2026 23:59:59 +0200 Subject: [PATCH 13/59] Refactor --- packages/effect-lens/src/Lens.ts | 71 ++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 77b7b54..52035ee 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -27,6 +27,10 @@ extends Subscribable.Subscribable { */ export const isLens = (u: unknown): u is Lens => Predicate.hasProperty(u, LensTypeId) + + + + export const LensWithInternalsTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensWithInternals") export type LensWithInternalsTypeId = typeof LensWithInternalsTypeId @@ -49,41 +53,56 @@ export const asLensWithInternals = ( } -export declare namespace LensImpl { - export interface Source { - readonly get: Effect.Effect, - readonly changes: Stream.Stream, - readonly update: (a: A) => Effect.Effect, - readonly withLock: (self: Effect.Effect) => Effect.Effect, - } -} - -export class LensImpl -extends Pipeable.Class() implements LensWithInternals { +export abstract class LensImpl +extends Pipeable.Class() { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId - readonly get: Effect.Effect - readonly changes: Stream.Stream - readonly update: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect - - constructor( - source: LensImpl.Source - ) { - super() - - this.get = source.get - this.changes = source.changes - this.update = source.update - this.withLock = source.withLock - } + abstract readonly get: Effect.Effect + abstract readonly changes: Stream.Stream + abstract readonly update: (a: A) => Effect.Effect + abstract readonly withLock: (self: Effect.Effect) => Effect.Effect modifyEffect = modifyEffect } +// export declare namespace LensImpl { +// export interface Source { +// readonly get: Effect.Effect, +// readonly changes: Stream.Stream, +// readonly update: (a: A) => Effect.Effect, +// readonly withLock: (self: Effect.Effect) => Effect.Effect, +// } +// } + +// export class LensImpl +// extends Pipeable.Class() implements LensWithInternals { +// readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId +// readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId +// readonly [LensTypeId]: LensTypeId = LensTypeId +// readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + +// readonly get: Effect.Effect +// readonly changes: Stream.Stream +// readonly update: (a: A) => Effect.Effect +// readonly withLock: (self: Effect.Effect) => Effect.Effect + +// constructor( +// source: LensImpl.Source +// ) { +// super() + +// this.get = source.get +// this.changes = source.changes +// this.update = source.update +// this.withLock = source.withLock +// } + +// modifyEffect = modifyEffect +// } + function modifyEffect( this: LensWithInternals, f: (a: A) => Effect.Effect, -- 2.52.0 From f074ae2978297b3a4e4d258ede257b538c4c8fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 00:32:14 +0200 Subject: [PATCH 14/59] Refactor --- packages/effect-lens/src/Lens.ts | 109 +++++++++++++++++-------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 52035ee..3a7359d 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -28,44 +28,63 @@ extends Subscribable.Subscribable { export const isLens = (u: unknown): u is Lens => Predicate.hasProperty(u, LensTypeId) +export const LensStepTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensStep") +export type LensStepTypeId = typeof LensTypeId - - -export const LensWithInternalsTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensWithInternals") -export type LensWithInternalsTypeId = typeof LensWithInternalsTypeId - -export interface LensWithInternals -extends Lens { - readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId - - readonly update: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect +export interface LensStep { + readonly [LensStepTypeId]: LensStepTypeId + readonly transform: (parent: B) => Effect.Effect + readonly update: (next: A, parent: B) => Effect.Effect } -export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) +export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) -export const asLensWithInternals = ( - lens: Lens -): LensWithInternals => { - if (!isLensWithInternals(lens)) - throw new Error("Not a 'LensWithInternals'.") - return lens as LensWithInternals -} +// export const LensWithInternalsTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensWithInternals") +// export type LensWithInternalsTypeId = typeof LensWithInternalsTypeId + +// export interface LensWithInternals +// extends Lens { +// readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId + +// readonly update: (a: A) => Effect.Effect +// readonly withLock: (self: Effect.Effect) => Effect.Effect +// } + +// export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) + +// export const asLensWithInternals = ( +// lens: Lens +// ): LensWithInternals => { +// if (!isLensWithInternals(lens)) +// throw new Error("Not a 'LensWithInternals'.") +// return lens as LensWithInternals +// } + + +export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImpl") +export type LensImplTypeId = typeof LensImplTypeId export abstract class LensImpl -extends Pipeable.Class() { +extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [LensTypeId]: LensTypeId = LensTypeId - readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId abstract readonly get: Effect.Effect abstract readonly changes: Stream.Stream - abstract readonly update: (a: A) => Effect.Effect + abstract readonly commit: (a: A) => Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - modifyEffect = modifyEffect + modifyEffect( + f: (a: A) => Effect.Effect, + ): Effect.Effect { + return this.withLock(Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.commit(next), b), + ))) + } } // export declare namespace LensImpl { @@ -103,16 +122,6 @@ extends Pipeable.Class() { // modifyEffect = modifyEffect // } -function modifyEffect( - this: LensWithInternals, - f: (a: A) => Effect.Effect, -): Effect.Effect { - return this.withLock(Effect.flatMap( - this.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), - ))) -} - /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ @@ -121,26 +130,26 @@ export const make = ( ): Lens => new LensImpl(source) -export class LensLazyImpl -extends Pipeable.Class() implements LensWithInternals { - readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId - readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId - readonly [LensTypeId]: LensTypeId = LensTypeId - readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId +// export class LensLazyImpl +// extends Pipeable.Class() implements LensWithInternals { +// readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId +// readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId +// readonly [LensTypeId]: LensTypeId = LensTypeId +// readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId - constructor( - readonly source: LensImpl.Source - ) { - super() - } +// constructor( +// readonly source: LensImpl.Source +// ) { +// super() +// } - get get() { return this.source.get } - get changes() { return this.source.changes } - get update() { return this.source.update } - get withLock() { return this.source.withLock } +// get get() { return this.source.get } +// get changes() { return this.source.changes } +// get update() { return this.source.update } +// get withLock() { return this.source.withLock } - modifyEffect = modifyEffect -} +// modifyEffect = modifyEffect +// } /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. -- 2.52.0 From c34720c8fe5e6344c8770d604b81d220025bdf8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 00:40:12 +0200 Subject: [PATCH 15/59] Refactor --- packages/effect-lens/src/Lens.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 3a7359d..ba6e678 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -72,11 +72,17 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - abstract readonly get: Effect.Effect + readonly steps: readonly LensStep[] = [] + + abstract readonly read: Effect.Effect abstract readonly changes: Stream.Stream abstract readonly commit: (a: A) => Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect + get get(): Effect.Effect { + + } + modifyEffect( f: (a: A) => Effect.Effect, ): Effect.Effect { -- 2.52.0 From 14a23e8040b4abe53a065b61adde18f3d23b9551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 00:55:40 +0200 Subject: [PATCH 16/59] Refactor --- packages/effect-lens/src/Lens.ts | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index ba6e678..e9bf203 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -40,19 +40,6 @@ export interface LensStep => Predicate.hasProperty(u, LensStepTypeId) -// export const LensWithInternalsTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensWithInternals") -// export type LensWithInternalsTypeId = typeof LensWithInternalsTypeId - -// export interface LensWithInternals -// extends Lens { -// readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId - -// readonly update: (a: A) => Effect.Effect -// readonly withLock: (self: Effect.Effect) => Effect.Effect -// } - -// export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) - // export const asLensWithInternals = ( // lens: Lens // ): LensWithInternals => { @@ -74,13 +61,17 @@ extends Pipeable.Class() implements Lens { readonly steps: readonly LensStep[] = [] - abstract readonly read: Effect.Effect - abstract readonly changes: Stream.Stream - abstract readonly commit: (a: A) => Effect.Effect + abstract readonly sourceGet: Effect.Effect + abstract readonly sourceChanges: Stream.Stream + abstract readonly sourceCommit: (a: A) => Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect get get(): Effect.Effect { + return this.sourceGet + } + get changes(): Stream.Stream { + return this.sourceChanges } modifyEffect( @@ -88,7 +79,7 @@ extends Pipeable.Class() implements Lens { ): Effect.Effect { return this.withLock(Effect.flatMap( this.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.commit(next), b), + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.sourceCommit(next), b), ))) } } -- 2.52.0 From 88a4b9286d1eb9c9a55e1cbdec87fb72463b9e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 01:23:33 +0200 Subject: [PATCH 17/59] Refactor --- packages/effect-lens/src/Lens.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index e9bf203..82eabc0 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -34,6 +34,7 @@ export type LensStepTypeId = typeof LensTypeId export interface LensStep { readonly [LensStepTypeId]: LensStepTypeId readonly transform: (parent: B) => Effect.Effect + readonly transformStream: (parent: B) => Stream.Stream readonly update: (next: A, parent: B) => Effect.Effect } @@ -52,18 +53,29 @@ export const isLensStep = (u: unknown): u is LensStep +export abstract class LensImpl< + in out A, + in out B, + in out ER = never, + in out EW = never, + in out RR = never, + in out RW = never, + in out SourceER = never, + in out SourceEW = never, + in out SourceRR = never, + in out SourceRW = never, +> extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - readonly steps: readonly LensStep[] = [] + readonly steps: readonly LensStep[] = [] - abstract readonly sourceGet: Effect.Effect - abstract readonly sourceChanges: Stream.Stream - abstract readonly sourceCommit: (a: A) => Effect.Effect + abstract readonly sourceGet: Effect.Effect + abstract readonly sourceChanges: Stream.Stream + abstract readonly sourceCommit: (b: B) => Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect get get(): Effect.Effect { -- 2.52.0 From 6285c32c03d8265ebd4f853367b90b072f854f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 02:36:21 +0200 Subject: [PATCH 18/59] Refactor --- packages/effect-lens/src/Lens.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 82eabc0..57b23d5 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -31,11 +31,22 @@ export const isLens = (u: unknown): u is Lens { +export interface LensStep< + in out A, + in out B, + in out ER = never, + in out EW = never, + in out RR = never, + in out RW = never, + in out SourceER = never, + in out SourceEW = never, + in out SourceRR = never, + in out SourceRW = never, +> { readonly [LensStepTypeId]: LensStepTypeId - readonly transform: (parent: B) => Effect.Effect - readonly transformStream: (parent: B) => Stream.Stream - readonly update: (next: A, parent: B) => Effect.Effect + readonly transform: (effect: Effect.Effect) => Effect.Effect + readonly transformStream: (stream: Stream.Stream) => Stream.Stream + readonly update: (next: Effect.Effect, parent: B) => Effect.Effect } export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) @@ -71,7 +82,7 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - readonly steps: readonly LensStep[] = [] + readonly steps: readonly LensStep[] = [] abstract readonly sourceGet: Effect.Effect abstract readonly sourceChanges: Stream.Stream -- 2.52.0 From 09d513530b6d6ed05e1e4985f652a7b6d92e05d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 02:40:46 +0200 Subject: [PATCH 19/59] Refactor --- packages/effect-lens/src/Lens.ts | 52 +++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 57b23d5..bfda515 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -90,20 +90,56 @@ extends Pipeable.Class() implements Lens { abstract readonly withLock: (self: Effect.Effect) => Effect.Effect get get(): Effect.Effect { - return this.sourceGet + let effect: Effect.Effect = this.sourceGet + for (const step of this.steps) { + effect = step.transform(effect as never) + } + return effect as Effect.Effect } get changes(): Stream.Stream { - return this.sourceChanges + let stream: Stream.Stream = this.sourceChanges + for (const step of this.steps) { + stream = step.transformStream(stream as never) + } + return stream as Stream.Stream } - modifyEffect( - f: (a: A) => Effect.Effect, - ): Effect.Effect { + modifyEffect( + f: (a: A) => Effect.Effect, + ): Effect.Effect { return this.withLock(Effect.flatMap( - this.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.sourceCommit(next), b), - ))) + this.sourceGet, + source => { + const parents: unknown[] = [] + let current: Effect.Effect = Effect.succeed(source) + + for (const step of this.steps) { + current = Effect.flatMap(current, parent => { + parents.push(parent) + return step.transform(Effect.succeed(parent) as never) + }) + } + + return Effect.flatMap(current, a => Effect.flatMap( + f(a as A), + ([c, next]) => { + let rebuilt: Effect.Effect = Effect.succeed(next) + + for (let i = this.steps.length - 1; i >= 0; i--) { + const step = this.steps[i]! + const parent = parents[i]! + rebuilt = step.update(rebuilt as never, parent as never) + } + + return Effect.as( + Effect.flatMap(rebuilt, b => this.sourceCommit(b as B)), + c, + ) + }, + )) + }, + )) as Effect.Effect } } -- 2.52.0 From 019e9518d222761c3c29c6327d57da7e632a3cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 02:54:20 +0200 Subject: [PATCH 20/59] Refactor --- packages/effect-lens/src/Lens.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index bfda515..5a1e02c 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -91,17 +91,15 @@ extends Pipeable.Class() implements Lens { get get(): Effect.Effect { let effect: Effect.Effect = this.sourceGet - for (const step of this.steps) { + for (const step of this.steps) effect = step.transform(effect as never) - } return effect as Effect.Effect } get changes(): Stream.Stream { let stream: Stream.Stream = this.sourceChanges - for (const step of this.steps) { + for (const step of this.steps) stream = step.transformStream(stream as never) - } return stream as Stream.Stream } @@ -114,12 +112,11 @@ extends Pipeable.Class() implements Lens { const parents: unknown[] = [] let current: Effect.Effect = Effect.succeed(source) - for (const step of this.steps) { + for (const step of this.steps) current = Effect.flatMap(current, parent => { parents.push(parent) return step.transform(Effect.succeed(parent) as never) }) - } return Effect.flatMap(current, a => Effect.flatMap( f(a as A), @@ -127,7 +124,9 @@ extends Pipeable.Class() implements Lens { let rebuilt: Effect.Effect = Effect.succeed(next) for (let i = this.steps.length - 1; i >= 0; i--) { + // biome-ignore lint/style/noNonNullAssertion: won't throw const step = this.steps[i]! + // biome-ignore lint/style/noNonNullAssertion: won't throw const parent = parents[i]! rebuilt = step.update(rebuilt as never, parent as never) } -- 2.52.0 From adfa262a5dc36635a6201d6c2afd93e0f35465ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 May 2026 02:57:00 +0200 Subject: [PATCH 21/59] Fix --- packages/effect-lens/src/Lens.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 5a1e02c..d9c1910 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -82,7 +82,7 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - readonly steps: readonly LensStep[] = [] + readonly steps: readonly LensStep[] = [] abstract readonly sourceGet: Effect.Effect abstract readonly sourceChanges: Stream.Stream @@ -92,14 +92,14 @@ extends Pipeable.Class() implements Lens { get get(): Effect.Effect { let effect: Effect.Effect = this.sourceGet for (const step of this.steps) - effect = step.transform(effect as never) + effect = step.transform(effect) return effect as Effect.Effect } get changes(): Stream.Stream { let stream: Stream.Stream = this.sourceChanges for (const step of this.steps) - stream = step.transformStream(stream as never) + stream = step.transformStream(stream) return stream as Stream.Stream } @@ -115,7 +115,7 @@ extends Pipeable.Class() implements Lens { for (const step of this.steps) current = Effect.flatMap(current, parent => { parents.push(parent) - return step.transform(Effect.succeed(parent) as never) + return step.transform(Effect.succeed(parent)) }) return Effect.flatMap(current, a => Effect.flatMap( @@ -128,7 +128,7 @@ extends Pipeable.Class() implements Lens { const step = this.steps[i]! // biome-ignore lint/style/noNonNullAssertion: won't throw const parent = parents[i]! - rebuilt = step.update(rebuilt as never, parent as never) + rebuilt = step.update(rebuilt, parent) } return Effect.as( -- 2.52.0 From 365eb05dd4c01c733856bfa90ee7b3d6cb796d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 23 May 2026 16:20:06 +0200 Subject: [PATCH 22/59] Refactor --- packages/effect-lens/src/Lens.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index d9c1910..0016c73 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -35,21 +35,21 @@ export interface LensStep< in out A, in out B, in out ER = never, + in out ESR = never, in out EW = never, + in out ESW = never, in out RR = never, + in out RSR = never, in out RW = never, - in out SourceER = never, - in out SourceEW = never, - in out SourceRR = never, - in out SourceRW = never, + in out RSW = never, > { readonly [LensStepTypeId]: LensStepTypeId - readonly transform: (effect: Effect.Effect) => Effect.Effect - readonly transformStream: (stream: Stream.Stream) => Stream.Stream - readonly update: (next: Effect.Effect, parent: B) => Effect.Effect + readonly transform: (effect: Effect.Effect) => Effect.Effect + readonly transformStream: (stream: Stream.Stream) => Stream.Stream + readonly update: (next: Effect.Effect, parent: B) => Effect.Effect } -export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) +export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) // export const asLensWithInternals = ( @@ -68,13 +68,13 @@ export abstract class LensImpl< in out A, in out B, in out ER = never, + in out ESR = never, in out EW = never, + in out ESW = never, in out RR = never, + in out RSR = never, in out RW = never, - in out SourceER = never, - in out SourceEW = never, - in out SourceRR = never, - in out SourceRW = never, + in out RSW = never, > extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId @@ -84,9 +84,9 @@ extends Pipeable.Class() implements Lens { readonly steps: readonly LensStep[] = [] - abstract readonly sourceGet: Effect.Effect - abstract readonly sourceChanges: Stream.Stream - abstract readonly sourceCommit: (b: B) => Effect.Effect + abstract readonly sourceGet: Effect.Effect + abstract readonly sourceChanges: Stream.Stream + abstract readonly sourceCommit: (b: B) => Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect get get(): Effect.Effect { -- 2.52.0 From 694f02807714b433eb19804cefdee1c57dd61a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 23 May 2026 17:57:54 +0200 Subject: [PATCH 23/59] Refactor --- packages/effect-lens/src/Lens.ts | 70 ++++++++++++++------------------ 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 0016c73..8b28408 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -29,7 +29,14 @@ export const isLens = (u: unknown): u is Lens { + readonly value: A + readonly commit: ( + next: Effect.Effect + ) => Effect.Effect +} export interface LensStep< in out A, @@ -44,9 +51,8 @@ export interface LensStep< in out RSW = never, > { readonly [LensStepTypeId]: LensStepTypeId - readonly transform: (effect: Effect.Effect) => Effect.Effect + readonly access: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> readonly transformStream: (stream: Stream.Stream) => Stream.Stream - readonly update: (next: Effect.Effect, parent: B) => Effect.Effect } export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) @@ -89,11 +95,23 @@ extends Pipeable.Class() implements Lens { abstract readonly sourceCommit: (b: B) => Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - get get(): Effect.Effect { - let effect: Effect.Effect = this.sourceGet + private get access(): Effect.Effect, ER, RR> { + let effect: Effect.Effect, unknown, unknown> = Effect.map( + this.sourceGet, + value => ({ + value, + commit: next => Effect.flatMap(next, value => this.sourceCommit(value as B)), + }), + ) + for (const step of this.steps) - effect = step.transform(effect) - return effect as Effect.Effect + effect = step.access(effect as never) as Effect.Effect, unknown, unknown> + + return effect as Effect.Effect, ER, RR> + } + + get get(): Effect.Effect { + return Effect.map(this.access, frame => frame.value) } get changes(): Stream.Stream { @@ -107,38 +125,12 @@ extends Pipeable.Class() implements Lens { f: (a: A) => Effect.Effect, ): Effect.Effect { return this.withLock(Effect.flatMap( - this.sourceGet, - source => { - const parents: unknown[] = [] - let current: Effect.Effect = Effect.succeed(source) - - for (const step of this.steps) - current = Effect.flatMap(current, parent => { - parents.push(parent) - return step.transform(Effect.succeed(parent)) - }) - - return Effect.flatMap(current, a => Effect.flatMap( - f(a as A), - ([c, next]) => { - let rebuilt: Effect.Effect = Effect.succeed(next) - - for (let i = this.steps.length - 1; i >= 0; i--) { - // biome-ignore lint/style/noNonNullAssertion: won't throw - const step = this.steps[i]! - // biome-ignore lint/style/noNonNullAssertion: won't throw - const parent = parents[i]! - rebuilt = step.update(rebuilt, parent) - } - - return Effect.as( - Effect.flatMap(rebuilt, b => this.sourceCommit(b as B)), - c, - ) - }, - )) - }, - )) as Effect.Effect + this.access, + frame => Effect.flatMap( + f(frame.value), + ([c, next]) => Effect.as(frame.commit(Effect.succeed(next)), c), + ), + )) } } -- 2.52.0 From afdaf53da3eada9a6d6c418b4b3aebd7b18e2303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 23 May 2026 20:32:26 +0200 Subject: [PATCH 24/59] Refactor --- packages/effect-lens/src/Lens.ts | 106 +++++++------------------------ 1 file changed, 23 insertions(+), 83 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 8b28408..4d9063c 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -58,15 +58,6 @@ export interface LensStep< export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) -// export const asLensWithInternals = ( -// lens: Lens -// ): LensWithInternals => { -// if (!isLensWithInternals(lens)) -// throw new Error("Not a 'LensWithInternals'.") -// return lens as LensWithInternals -// } - - export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImpl") export type LensImplTypeId = typeof LensImplTypeId @@ -92,7 +83,7 @@ extends Pipeable.Class() implements Lens { abstract readonly sourceGet: Effect.Effect abstract readonly sourceChanges: Stream.Stream - abstract readonly sourceCommit: (b: B) => Effect.Effect + abstract sourceCommit(b: B): Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect private get access(): Effect.Effect, ER, RR> { @@ -134,40 +125,25 @@ extends Pipeable.Class() implements Lens { } } -// export declare namespace LensImpl { -// export interface Source { -// readonly get: Effect.Effect, -// readonly changes: Stream.Stream, -// readonly update: (a: A) => Effect.Effect, -// readonly withLock: (self: Effect.Effect) => Effect.Effect, -// } -// } -// export class LensImpl -// extends Pipeable.Class() implements LensWithInternals { -// readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId -// readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId -// readonly [LensTypeId]: LensTypeId = LensTypeId -// readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId -// readonly get: Effect.Effect -// readonly changes: Stream.Stream -// readonly update: (a: A) => Effect.Effect -// readonly withLock: (self: Effect.Effect) => Effect.Effect -// constructor( -// source: LensImpl.Source -// ) { -// super() +export abstract class LensLazyImpl< + in out A, + in out B, + in out ER = never, + in out ESR = never, + in out EW = never, + in out ESW = never, + in out RR = never, + in out RSR = never, + in out RW = never, + in out RSW = never, +> +extends Pipeable.Class() implements Lens { -// this.get = source.get -// this.changes = source.changes -// this.update = source.update -// this.withLock = source.withLock -// } +} -// modifyEffect = modifyEffect -// } /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. @@ -176,28 +152,6 @@ export const make = ( source: LensImpl.Source ): Lens => new LensImpl(source) - -// export class LensLazyImpl -// extends Pipeable.Class() implements LensWithInternals { -// readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId -// readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId -// readonly [LensTypeId]: LensTypeId = LensTypeId -// readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId - -// constructor( -// readonly source: LensImpl.Source -// ) { -// super() -// } - -// get get() { return this.source.get } -// get changes() { return this.source.changes } -// get update() { return this.source.update } -// get withLock() { return this.source.withLock } - -// modifyEffect = modifyEffect -// } - /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ @@ -215,12 +169,7 @@ export declare namespace SynchronizedRefLensImpl { } export class SynchronizedRefLensImpl -extends Pipeable.Class() implements LensWithInternals { - readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId - readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId - readonly [LensTypeId]: LensTypeId = LensTypeId - readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId - +extends LensImpl { readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals constructor( @@ -230,12 +179,10 @@ extends Pipeable.Class() implements LensWithInternals } - get get() { return this.ref.get } - get changes() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } - update(a: A) { return Ref.set(this.ref.ref, a) } + get sourceGet() { return this.ref.get } + get sourceChanges() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } + sourceCommit(a: A) { return Ref.set(this.ref.ref, a) } get withLock() { return this.ref.withLock } - - modifyEffect = modifyEffect } /** @@ -259,12 +206,7 @@ export declare namespace SubscriptionRefLensImpl { } export class SubscriptionRefLensImpl -extends Pipeable.Class() implements LensWithInternals { - readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId - readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId - readonly [LensTypeId]: LensTypeId = LensTypeId - readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId - +extends LensImpl { readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals constructor( @@ -274,17 +216,15 @@ extends Pipeable.Class() implements LensWithInternals } - get get() { return this.ref.get } - get changes() { return this.ref.changes } - update(a: A) { + get sourceGet() { return this.ref.get } + get sourceChanges() { return this.ref.changes } + sourceCommit(a: A) { return Effect.zipLeft( Ref.set(this.ref.ref, a), PubSub.publish(this.ref.pubsub, a), ) } get withLock() { return this.ref.semaphore.withPermits(1) } - - modifyEffect = modifyEffect } /** -- 2.52.0 From b02ca29b761f89e1288ea3f24af27fc55bd20764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sun, 24 May 2026 18:29:05 +0200 Subject: [PATCH 25/59] Refactor --- packages/effect-lens/src/Lens.ts | 41 +++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 4d9063c..6ee763b 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -96,7 +96,7 @@ extends Pipeable.Class() implements Lens { ) for (const step of this.steps) - effect = step.access(effect as never) as Effect.Effect, unknown, unknown> + effect = step.access(effect) as Effect.Effect, unknown, unknown> return effect as Effect.Effect, ER, RR> } @@ -126,9 +126,24 @@ extends Pipeable.Class() implements Lens { } +export declare namespace LensLazyImpl { + export interface Source< + in out B, + in out EW = never, + in out ESW = never, + in out ESR = never, + in out RSR = never, + in out RW = never, + in out RSW = never, + > { + readonly sourceGet: Effect.Effect + readonly sourceChanges: Stream.Stream + readonly sourceCommit: (b: B) => Effect.Effect + readonly withLock: (self: Effect.Effect) => Effect.Effect + } +} - -export abstract class LensLazyImpl< +export class LensLazyImpl< in out A, in out B, in out ER = never, @@ -140,23 +155,31 @@ export abstract class LensLazyImpl< in out RW = never, in out RSW = never, > -extends Pipeable.Class() implements Lens { +extends LensImpl { + constructor( + readonly source: LensLazyImpl.Source + ) { + super() + } + get sourceGet() { return this.source.sourceGet } + get sourceChanges() { return this.source.sourceChanges } + sourceCommit(b: B) { return this.source.sourceCommit(b) } + get withLock() { return this.source.withLock } } - /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ -export const make = ( - source: LensImpl.Source -): Lens => new LensImpl(source) +// export const make = ( +// source: LensImpl.Source +// ): Lens => new LensImpl(source) /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ export const makeLazy = ( - source: LensImpl.Source + source: LensLazyImpl.Source ): Lens => new LensLazyImpl(source) -- 2.52.0 From b751d001c93cf6b860b80c3a90fb71b23f488a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sun, 24 May 2026 19:24:23 +0200 Subject: [PATCH 26/59] Refactor --- packages/effect-lens/src/Lens.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 6ee763b..43b0a11 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -127,19 +127,11 @@ extends Pipeable.Class() implements Lens { export declare namespace LensLazyImpl { - export interface Source< - in out B, - in out EW = never, - in out ESW = never, - in out ESR = never, - in out RSR = never, - in out RW = never, - in out RSW = never, - > { + export interface Source { readonly sourceGet: Effect.Effect readonly sourceChanges: Stream.Stream readonly sourceCommit: (b: B) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly withLock: (self: Effect.Effect) => Effect.Effect } } @@ -157,7 +149,7 @@ export class LensLazyImpl< > extends LensImpl { constructor( - readonly source: LensLazyImpl.Source + readonly source: LensLazyImpl.Source ) { super() } @@ -178,9 +170,9 @@ extends LensImpl { /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ -export const makeLazy = ( - source: LensLazyImpl.Source -): Lens => new LensLazyImpl(source) +export const makeLazy = ( + source: LensLazyImpl.Source +): Lens => new LensLazyImpl(source) export declare namespace SynchronizedRefLensImpl { -- 2.52.0 From 5018cbe7772fc25e313f511fe7cb64be07b23c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 25 May 2026 05:10:57 +0200 Subject: [PATCH 27/59] Refactor --- packages/effect-lens/src/Lens.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 43b0a11..1f52957 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -149,7 +149,7 @@ export class LensLazyImpl< > extends LensImpl { constructor( - readonly source: LensLazyImpl.Source + readonly source: LensLazyImpl.Source, ) { super() } @@ -174,6 +174,36 @@ export const makeLazy = ( source: LensLazyImpl.Source ): Lens => new LensLazyImpl(source) +/** + * Derives a new `Lens` by immutably appending a step to an existing `LensImpl`. + */ +export const derive: { + ( + self: LensImpl, + step: LensStep, + ): Lens + ( + step: LensStep, + ): ( + self: LensImpl + ) => Lens +} = Function.dual(2, ( + self: LensImpl, + step: LensStep, +): Lens => Object.defineProperty( + Object.defineProperties( + Object.create(Object.getPrototypeOf(self)), + Object.getOwnPropertyDescriptors(self), + ), + "steps", + { + configurable: true, + enumerable: true, + value: [...self.steps, step as LensStep], + writable: false, + }, +) as Lens) + export declare namespace SynchronizedRefLensImpl { export interface SynchronizedRefWithInternals -- 2.52.0 From dd3207aefb081c1b337b8bf5c2acbd657f89e416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 25 May 2026 05:30:18 +0200 Subject: [PATCH 28/59] Refactor --- packages/effect-lens/src/Lens.ts | 264 +++++++++++++++++-------------- 1 file changed, 145 insertions(+), 119 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 1f52957..ecb2adf 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -61,6 +61,16 @@ export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensImplTypeId) + +export const asLensImpl = ( + lens: Lens +): LensImpl => { + if (!isLensImpl(lens)) + throw new Error("Not a 'LensImpl'.") + return lens as LensImpl +} + export abstract class LensImpl< in out A, in out B, @@ -86,7 +96,7 @@ extends Pipeable.Class() implements Lens { abstract sourceCommit(b: B): Effect.Effect abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - private get access(): Effect.Effect, ER, RR> { + get access(): Effect.Effect, ER, RR> { let effect: Effect.Effect, unknown, unknown> = Effect.map( this.sourceGet, value => ({ @@ -160,19 +170,12 @@ extends LensImpl { get withLock() { return this.source.withLock } } -/** - * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. - */ -// export const make = ( -// source: LensImpl.Source -// ): Lens => new LensImpl(source) - /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ export const makeLazy = ( source: LensLazyImpl.Source -): Lens => new LensLazyImpl(source) +): Lens => new LensLazyImpl(source) /** * Derives a new `Lens` by immutably appending a step to an existing `LensImpl`. @@ -285,13 +288,14 @@ export const fromSubscriptionRef = ( */ export const unwrap = ( effect: Effect.Effect, E1, R1> -): Lens => make({ - get: Effect.flatMap(effect, l => l.get), - changes: Stream.unwrap(Effect.map(effect, l => l.changes)), - update: a => Effect.flatMap(effect, l => asLensWithInternals(l).update(a)), - withLock: ( - effect2: Effect.Effect - ) => Effect.flatMap(effect, l => asLensWithInternals(l).withLock(effect2)), +): Lens => makeLazy({ + sourceGet: Effect.flatMap(effect, l => l.get), + sourceChanges: Stream.unwrap(Effect.map(effect, l => l.changes)), + sourceCommit: a => Effect.flatMap( + effect, + l => Effect.flatMap(asLensImpl(l).access, frame => frame.commit(Effect.succeed(a))), + ), + withLock: identity, }) /** @@ -311,12 +315,11 @@ export const map: { self: Lens, get: (a: NoInfer) => B, update: (a: NoInfer, b: B) => NoInfer, -): Lens => makeLazy({ - get get() { return Effect.map(self.get, get) }, - get changes() { return Stream.map(self.changes, get) }, - update(a: A) { return }, - get withLock() { return asLensWithInternals(self).withLock }, -})) +): Lens => mapEffect( + self, + a => Effect.succeed(get(a)), + (a, b) => Effect.succeed(update(a, b)), +)) /** * Derives a new `Lens` by applying effectful getters and setters over the focused value. @@ -335,22 +338,25 @@ export const mapEffect: { self: Lens, get: (a: NoInfer) => Effect.Effect, set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, -): Lens => make({ - get get() { return Effect.flatMap(self.get, get) }, - get changes() { return Stream.mapEffect(self.changes, get) }, - modify: ( - f: (b: B) => Effect.Effect - ) => self.modify(a => Effect.flatMap( - get(a), - b => Effect.flatMap( - f(b), - ([c, bNext]) => Effect.flatMap( - set(a, bNext), - nextA => Effect.succeed([c, nextA] as const), +): Lens => { + return derive( + asLensImpl(self), + { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.flatMap( + parent, + frame => Effect.map( + get(frame.value), + value => ({ + value, + commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), + }), + ), ), - ) - )), -})) + transformStream: stream => Stream.mapEffect(stream, get), + }, + ) +}) /** * Derives a new `Lens` by applying synchronous getters and setters over the value inside an `Option`. @@ -432,11 +438,11 @@ export const mapStream: { } = Function.dual(2, ( self: Lens, f: (changes: Stream.Stream, NoInfer, NoInfer>) => Stream.Stream, NoInfer, NoInfer>, -): Lens => make({ - get get() { return self.get }, - get changes() { return f(self.changes) }, - get modify() { return self.modify }, -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => parent, + transformStream: f, +} as LensStep)) /** @@ -455,11 +461,11 @@ export const mapErrorRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => E2, -): Lens => make({ - get get() { return Effect.mapError(self.get, f) }, - get changes() { return Stream.mapError(self.changes, f) }, - get modify() { return self.modify as any }, -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.mapError(parent, f), + transformStream: stream => Stream.mapError(stream, f), +} as LensStep)) /** * Transforms modify errors of a `Lens`. @@ -478,13 +484,14 @@ export const mapErrorWrite: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => E2, -): Lens => make({ - get get() { return self.get }, - get changes() { return self.changes }, - modify: ( - g: (a: A) => Effect.Effect - ) => Effect.mapError(self.modify(g), f), -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map(parent, frame => ({ + value: frame.value, + commit: next => Effect.mapError(frame.commit(next), f), + })), + transformStream: stream => stream, +} as LensStep)) /** * Transforms all errors of a `Lens`. @@ -503,13 +510,17 @@ export const mapError: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => E2, -): Lens => make({ - get get() { return Effect.mapError(self.get, f) }, - get changes() { return Stream.mapError(self.changes, f) }, - modify: ( - g: (a: A) => Effect.Effect - ) => Effect.mapError(self.modify(g), f), -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map( + Effect.mapError(parent, f), + frame => ({ + value: frame.value, + commit: next => Effect.mapError(frame.commit(next), f), + }), + ), + transformStream: stream => Stream.mapError(stream, f), +} as LensStep)) /** * Recovers from read failures of a `Lens`. @@ -527,11 +538,11 @@ export const catchAllRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => Subscribable.Subscribable, -): Lens => 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 }, -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.catchAll(parent, error => asLensImpl(f(error) as Lens).access), + transformStream: stream => Stream.catchAll(stream, error => f(error).changes), +} as LensStep)) /** * Runs an effect when read failures occur. @@ -549,11 +560,11 @@ export const tapErrorRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => Effect.Effect, -): Lens => make({ - get get() { return Effect.tapError(self.get, f) }, - get changes() { return Stream.tapError(self.changes, f) }, - get modify() { return self.modify as any }, -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.tapError(parent, f), + transformStream: stream => Stream.tapError(stream, f), +} as LensStep)) /** * Runs an effect when modify failures occur. @@ -572,13 +583,14 @@ export const tapErrorWrite: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => Effect.Effect, -): Lens => make({ - get get() { return self.get }, - get changes() { return self.changes }, - modify: ( - g: (a: A) => Effect.Effect - ) => Effect.tapError(self.modify(g), f), -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map(parent, frame => ({ + value: frame.value, + commit: next => Effect.tapError(frame.commit(next), f), + })), + transformStream: stream => stream, +} as LensStep)) /** * Runs an effect when any `Lens` failure occurs. @@ -597,13 +609,17 @@ export const tapError: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => Effect.Effect, -): Lens => make({ - get get() { return Effect.tapError(self.get, f) }, - get changes() { return Stream.tapError(self.changes, f) }, - modify: ( - g: (a: A) => Effect.Effect - ) => Effect.tapError(self.modify(g), f), -})) +): Lens => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map( + Effect.tapError(parent, f), + frame => ({ + value: frame.value, + commit: next => Effect.tapError(frame.commit(next), f), + }), + ), + transformStream: stream => Stream.tapError(stream, f), +} as LensStep)) /** @@ -620,13 +636,17 @@ export const provideContext: { } = Function.dual(2, ( self: Lens, context: Context.Context, -): Lens, Exclude> => make({ - get get() { return Effect.provide(self.get, context) }, - get changes() { return Stream.provideSomeContext(self.changes, context) }, - modify: ( - f: (a: A) => Effect.Effect - ) => Effect.provide(self.modify(f), context), -})) +): Lens, Exclude> => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map( + Effect.provide(parent, context), + frame => ({ + value: frame.value, + commit: next => Effect.provide(frame.commit(Effect.provide(next, context)), context), + }), + ), + transformStream: stream => Stream.provideSomeContext(stream, context), +} as LensStep, RR, Exclude, RW>)) /** * Provides a `Runtime` or `ManagedRuntime` to a `Lens`, removing it from both the read and write environments. @@ -651,18 +671,20 @@ export const provideRuntime: { } = Function.dual(2, ( self: Lens, runtime: Runtime.Runtime, -) => make, Exclude>({ - get get() { return Effect.provide(self.get, runtime) }, - get changes() { - return Stream.unwrap(Effect.map( +) => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map( + Effect.provide(parent, runtime), + frame => ({ + value: frame.value, + commit: next => Effect.provide(frame.commit(Effect.provide(next, runtime)), runtime), + }), + ), + transformStream: stream => Stream.unwrap(Effect.map( Effect.provide(Effect.context(), runtime), - context => Stream.provideContext(self.changes, context), - )) - }, - modify: ( - f: (a: A) => Effect.Effect - ) => Effect.provide(self.modify(f), runtime), -})) + context => Stream.provideContext(stream, context), + )), +} as LensStep, RR, Exclude, RW>)) /** * Provides a single service to a `Lens`, removing it from both the read and write environments. @@ -684,13 +706,17 @@ export const provideService: { self: Lens, tag: Context.Tag, service: NoInfer, -): Lens, Exclude> => make({ - get get() { return Effect.provideService(self.get, tag, service) }, - get changes() { return Stream.provideService(self.changes, tag, service) }, - modify: ( - f: (a: A) => Effect.Effect - ) => Effect.provideService(self.modify(f), tag, service), -})) +): Lens, Exclude> => derive(asLensImpl(self), { + [LensStepTypeId]: LensStepTypeId, + access: parent => Effect.map( + Effect.provideService(parent, tag, service), + frame => ({ + value: frame.value, + commit: next => Effect.provideService(frame.commit(Effect.provideService(next, tag, service)), tag, service), + }), + ), + transformStream: stream => Stream.provideService(stream, tag, service), +} as LensStep, RR, Exclude, RW>)) /** @@ -876,7 +902,7 @@ export const set: { (value: A): (self: Lens) => Effect.Effect (self: Lens, value: A): Effect.Effect } = Function.dual(2, (self: Lens, value: A) => - self.modify(() => Effect.succeed([void 0, value] as const)), + self.modifyEffect(() => Effect.succeed([void 0, value] as const)), ) /** @@ -886,7 +912,7 @@ export const getAndSet: { (value: A): (self: Lens) => Effect.Effect (self: Lens, value: A): Effect.Effect } = Function.dual(2, (self: Lens, value: A) => - self.modify(a => Effect.succeed([a, value] as const)), + self.modifyEffect(a => Effect.succeed([a, value] as const)), ) /** @@ -896,7 +922,7 @@ export const update: { (f: (a: A) => A): (self: Lens) => Effect.Effect (self: Lens, f: (a: A) => A): Effect.Effect } = Function.dual(2, (self: Lens, f: (a: A) => A) => - self.modify(a => Effect.succeed([void 0, f(a)] as const)), + self.modifyEffect(a => Effect.succeed([void 0, f(a)] as const)), ) /** @@ -906,7 +932,7 @@ export const updateEffect: { (f: (a: A) => Effect.Effect): (self: Lens) => Effect.Effect (self: Lens, f: (a: A) => Effect.Effect): Effect.Effect } = Function.dual(2, (self: Lens, f: (a: A) => Effect.Effect) => - self.modify(a => Effect.flatMap( + self.modifyEffect(a => Effect.flatMap( f(a), next => Effect.succeed([void 0, next] as const), )), @@ -919,7 +945,7 @@ export const getAndUpdate: { (f: (a: A) => A): (self: Lens) => Effect.Effect (self: Lens, f: (a: A) => A): Effect.Effect } = Function.dual(2, (self: Lens, f: (a: A) => A) => - self.modify(a => Effect.succeed([a, f(a)] as const)), + self.modifyEffect(a => Effect.succeed([a, f(a)] as const)), ) /** @@ -929,7 +955,7 @@ export const getAndUpdateEffect: { (f: (a: A) => Effect.Effect): (self: Lens) => Effect.Effect (self: Lens, f: (a: A) => Effect.Effect): Effect.Effect } = Function.dual(2, (self: Lens, f: (a: A) => Effect.Effect) => - self.modify(a => Effect.flatMap( + self.modifyEffect(a => Effect.flatMap( f(a), next => Effect.succeed([a, next] as const) )), @@ -942,7 +968,7 @@ export const setAndGet: { (value: A): (self: Lens) => Effect.Effect (self: Lens, value: A): Effect.Effect } = Function.dual(2, (self: Lens, value: A) => - self.modify(() => Effect.succeed([value, value] as const)), + self.modifyEffect(() => Effect.succeed([value, value] as const)), ) /** @@ -952,7 +978,7 @@ export const updateAndGet: { (f: (a: A) => A): (self: Lens) => Effect.Effect (self: Lens, f: (a: A) => A): Effect.Effect } = Function.dual(2, (self: Lens, f: (a: A) => A) => - self.modify(a => { + self.modifyEffect(a => { const next = f(a) return Effect.succeed([next, next] as const) }), @@ -965,7 +991,7 @@ export const updateAndGetEffect: { (f: (a: A) => Effect.Effect): (self: Lens) => Effect.Effect (self: Lens, f: (a: A) => Effect.Effect): Effect.Effect } = Function.dual(2, (self: Lens, f: (a: A) => Effect.Effect) => - self.modify(a => Effect.flatMap( + self.modifyEffect(a => Effect.flatMap( f(a), next => Effect.succeed([next, next] as const), )), -- 2.52.0 From 253d19be3a173d2542cd6ef13a124cf370b71caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 25 May 2026 17:38:59 +0200 Subject: [PATCH 29/59] Add tests --- packages/effect-lens/src/tests.ts | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/effect-lens/src/tests.ts diff --git a/packages/effect-lens/src/tests.ts b/packages/effect-lens/src/tests.ts new file mode 100644 index 0000000..d1d4138 --- /dev/null +++ b/packages/effect-lens/src/tests.ts @@ -0,0 +1,46 @@ +import { Console, Effect, Stream, SubscriptionRef } from "effect" +import { Lens } from "./index.js" + + +class State extends Effect.Service()("State", { + effect: Effect.gen(function*() { + const ref = yield* SubscriptionRef.make({ + users: [ + { name: "Adolf" } + ] + }) + + return { + lens: Lens.fromSubscriptionRef(ref) + } as const + }) +}) {} + + +Effect.gen(function*() { + const lens = Lens.unwrap(Effect.andThen( + State, + state => state.lens, + )).pipe( + Lens.provideContext(yield* Effect.provide(Effect.context(), State.Default)) + ) + + const adolfNameLens = lens.pipe( + Lens.focusObjectOn("users"), + Lens.focusArrayAt(0), + Lens.focusObjectOn("name"), + Lens.catchAllRead(() => Lens.makeLazy({ + sourceGet: Effect.succeed("User not found"), + sourceChanges: Stream.make("User not found"), + sourceCommit: () => Console.log("Test"), + withLock: Effect.unsafeMakeSemaphore(1).withPermits(1), + })), + ) + + console.log(yield* Lens.get(adolfNameLens)) + yield* Lens.set(lens, { users: [] }) + yield* Lens.set(adolfNameLens, "Himmler") + console.log(yield* Lens.get(adolfNameLens)) +}).pipe( + Effect.runSync, +) -- 2.52.0 From 5c1fdbad431efcd96154415d0aa4e68b43854344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 04:18:59 +0200 Subject: [PATCH 30/59] Refactor --- packages/effect-lens/src/Lens.ts | 395 ++++++++++++++----------------- 1 file changed, 174 insertions(+), 221 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index ecb2adf..f467d71 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, type Context, Effect, Function, identity, type ManagedRuntime, Option, Pipeable, Predicate, PubSub, Readable, Ref, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, PubSub, Readable, Ref, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -28,60 +28,24 @@ extends Subscribable.Subscribable { export const isLens = (u: unknown): u is Lens => Predicate.hasProperty(u, LensTypeId) -export const LensStepTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensStep") -export type LensStepTypeId = typeof LensStepTypeId - -export interface LensFrame { - readonly value: A - readonly commit: ( - next: Effect.Effect - ) => Effect.Effect -} - -export interface LensStep< - in out A, - in out B, - in out ER = never, - in out ESR = never, - in out EW = never, - in out ESW = never, - in out RR = never, - in out RSR = never, - in out RW = never, - in out RSW = never, -> { - readonly [LensStepTypeId]: LensStepTypeId - readonly access: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> - readonly transformStream: (stream: Stream.Stream) => Stream.Stream -} - -export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) - - export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImpl") export type LensImplTypeId = typeof LensImplTypeId -export const isLensImpl = (u: unknown): u is LensImpl => Predicate.hasProperty(u, LensImplTypeId) - -export const asLensImpl = ( - lens: Lens -): LensImpl => { - if (!isLensImpl(lens)) - throw new Error("Not a 'LensImpl'.") - return lens as LensImpl +export declare namespace LensImpl { + export interface Frame { + readonly value: A + readonly commit: ( + next: Effect.Effect + ) => Effect.Effect + } } export abstract class LensImpl< in out A, - in out B, in out ER = never, - in out ESR = never, in out EW = never, - in out ESW = never, in out RR = never, - in out RSR = never, in out RW = never, - in out RSW = never, > extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId @@ -89,39 +53,14 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - readonly steps: readonly LensStep[] = [] - - abstract readonly sourceGet: Effect.Effect - abstract readonly sourceChanges: Stream.Stream - abstract sourceCommit(b: B): Effect.Effect + abstract readonly access: Effect.Effect, ER, RR> + abstract readonly changes: Stream.Stream abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - get access(): Effect.Effect, ER, RR> { - let effect: Effect.Effect, unknown, unknown> = Effect.map( - this.sourceGet, - value => ({ - value, - commit: next => Effect.flatMap(next, value => this.sourceCommit(value as B)), - }), - ) - - for (const step of this.steps) - effect = step.access(effect) as Effect.Effect, unknown, unknown> - - return effect as Effect.Effect, ER, RR> - } - get get(): Effect.Effect { return Effect.map(this.access, frame => frame.value) } - get changes(): Stream.Stream { - let stream: Stream.Stream = this.sourceChanges - for (const step of this.steps) - stream = step.transformStream(stream) - return stream as Stream.Stream - } - modifyEffect( f: (a: A) => Effect.Effect, ): Effect.Effect { @@ -135,77 +74,128 @@ extends Pipeable.Class() implements Lens { } } +export const isLensImpl = (u: unknown): u is LensImpl => Predicate.hasProperty(u, LensImplTypeId) + +export const asLensImpl = ( + lens: Lens +): LensImpl => { + if (!isLensImpl(lens)) + throw new Error("Not a 'LensImpl'.") + return lens as LensImpl +} + export declare namespace LensLazyImpl { export interface Source { - readonly sourceGet: Effect.Effect - readonly sourceChanges: Stream.Stream - readonly sourceCommit: (b: B) => Effect.Effect + readonly get: Effect.Effect + readonly changes: Stream.Stream + readonly commit: (b: B) => Effect.Effect readonly withLock: (self: Effect.Effect) => Effect.Effect } } export class LensLazyImpl< - in out A, in out B, - in out ER = never, - in out ESR = never, - in out EW = never, in out ESW = never, - in out RR = never, + in out ESR = never, in out RSR = never, - in out RW = never, in out RSW = never, > -extends LensImpl { +extends LensImpl { constructor( readonly source: LensLazyImpl.Source, ) { super() } - get sourceGet() { return this.source.sourceGet } - get sourceChanges() { return this.source.sourceChanges } - sourceCommit(b: B) { return this.source.sourceCommit(b) } + get access(): Effect.Effect, ESR, RSR> { + return Effect.map( + this.source.get, + value => ({ + value, + commit: next => Effect.flatMap(next, value => this.source.commit(value)), + }), + ) + } + + get changes() { return this.source.changes } get withLock() { return this.source.withLock } } /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ -export const makeLazy = ( +export const make = ( source: LensLazyImpl.Source ): Lens => new LensLazyImpl(source) + +export declare namespace DerivedLensImpl { + export interface Source< + in out A, + in out B, + in out ER = never, + in out ESR = never, + in out EW = never, + in out ESW = never, + in out RR = never, + in out RSR = never, + in out RW = never, + in out RSW = never, + > { + readonly access: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> + readonly transformStream: (stream: Stream.Stream) => Stream.Stream + } +} + +export class DerivedLensImpl< + in out A, + in out B, + in out ER = never, + in out PER = never, + in out EW = never, + in out PEW = never, + in out RR = never, + in out PRR = never, + in out RW = never, + in out PRW = never, +> +extends LensImpl { + constructor( + readonly parent: LensImpl, + readonly source: DerivedLensImpl.Source, + ) { + super() + } + + get access(): Effect.Effect, ER, RR> { + return this.source.access(this.parent.access) + } + + get changes(): Stream.Stream { + return this.source.transformStream(this.parent.changes) + } + + get withLock() { + return this.parent.withLock + } +} + /** - * Derives a new `Lens` by immutably appending a step to an existing `LensImpl`. + * Derives a new `Lens` by linking a step to an existing parent lens. */ export const derive: { - ( - self: LensImpl, - step: LensStep, + ( + self: Lens, + source: DerivedLensImpl.Source, ): Lens ( - step: LensStep, - ): ( - self: LensImpl - ) => Lens -} = Function.dual(2, ( - self: LensImpl, - step: LensStep, -): Lens => Object.defineProperty( - Object.defineProperties( - Object.create(Object.getPrototypeOf(self)), - Object.getOwnPropertyDescriptors(self), - ), - "steps", - { - configurable: true, - enumerable: true, - value: [...self.steps, step as LensStep], - writable: false, - }, -) as Lens) + source: DerivedLensImpl.Source, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + source: DerivedLensImpl.Source, +): Lens => new DerivedLensImpl(asLensImpl(self), source)) export declare namespace SynchronizedRefLensImpl { @@ -217,7 +207,7 @@ export declare namespace SynchronizedRefLensImpl { } export class SynchronizedRefLensImpl -extends LensImpl { +extends LensImpl { readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals constructor( @@ -227,9 +217,17 @@ extends LensImpl { this.ref = ref as SynchronizedRefLensImpl.SynchronizedRefWithInternals } - get sourceGet() { return this.ref.get } - get sourceChanges() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } - sourceCommit(a: A) { return Ref.set(this.ref.ref, a) } + get access(): Effect.Effect, never, never> { + return Effect.map( + this.ref.get, + value => ({ + value, + commit: next => Effect.flatMap(next, value => Ref.set(this.ref.ref, value)), + }), + ) + } + + get changes() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } get withLock() { return this.ref.withLock } } @@ -254,7 +252,7 @@ export declare namespace SubscriptionRefLensImpl { } export class SubscriptionRefLensImpl -extends LensImpl { +extends LensImpl { readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals constructor( @@ -264,9 +262,18 @@ extends LensImpl { this.ref = ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals } - get sourceGet() { return this.ref.get } - get sourceChanges() { return this.ref.changes } - sourceCommit(a: A) { + get access(): Effect.Effect, never, never> { + return Effect.map( + this.ref.get, + value => ({ + value, + commit: next => Effect.flatMap(next, value => this.commit(value)), + }), + ) + } + + get changes() { return this.ref.changes } + commit(a: A) { return Effect.zipLeft( Ref.set(this.ref.ref, a), PubSub.publish(this.ref.pubsub, a), @@ -288,10 +295,10 @@ export const fromSubscriptionRef = ( */ export const unwrap = ( effect: Effect.Effect, E1, R1> -): Lens => makeLazy({ - sourceGet: Effect.flatMap(effect, l => l.get), - sourceChanges: Stream.unwrap(Effect.map(effect, l => l.changes)), - sourceCommit: a => Effect.flatMap( +): Lens => make({ + get: Effect.flatMap(effect, l => l.get), + changes: Stream.unwrap(Effect.map(effect, l => l.changes)), + commit: a => Effect.flatMap( effect, l => Effect.flatMap(asLensImpl(l).access, frame => frame.commit(Effect.succeed(a))), ), @@ -338,25 +345,19 @@ export const mapEffect: { self: Lens, get: (a: NoInfer) => Effect.Effect, set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, -): Lens => { - return derive( - asLensImpl(self), - { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.flatMap( - parent, - frame => Effect.map( - get(frame.value), - value => ({ - value, - commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), - }), - ), - ), - transformStream: stream => Stream.mapEffect(stream, get), - }, - ) -}) +): Lens => derive(self, { + access: parent => Effect.flatMap( + parent, + frame => Effect.map( + get(frame.value), + value => ({ + value, + commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), + }), + ), + ), + transformStream: Stream.mapEffect(get), +})) /** * Derives a new `Lens` by applying synchronous getters and setters over the value inside an `Option`. @@ -438,11 +439,10 @@ export const mapStream: { } = Function.dual(2, ( self: Lens, f: (changes: Stream.Stream, NoInfer, NoInfer>) => Stream.Stream, NoInfer, NoInfer>, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => parent, +): Lens => derive(self, { + access: identity, transformStream: f, -} as LensStep)) +})) /** @@ -461,11 +461,10 @@ export const mapErrorRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => E2, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.mapError(parent, f), - transformStream: stream => Stream.mapError(stream, f), -} as LensStep)) +): Lens => derive(self, { + access: Effect.mapError(f), + transformStream: Stream.mapError(f), +})) /** * Transforms modify errors of a `Lens`. @@ -484,14 +483,13 @@ export const mapErrorWrite: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => E2, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map(parent, frame => ({ value: frame.value, commit: next => Effect.mapError(frame.commit(next), f), })), - transformStream: stream => stream, -} as LensStep)) + transformStream: identity, +})) /** * Transforms all errors of a `Lens`. @@ -510,8 +508,7 @@ export const mapError: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => E2, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map( Effect.mapError(parent, f), frame => ({ @@ -519,8 +516,8 @@ export const mapError: { commit: next => Effect.mapError(frame.commit(next), f), }), ), - transformStream: stream => Stream.mapError(stream, f), -} as LensStep)) + transformStream: Stream.mapError(f), +})) /** * Recovers from read failures of a `Lens`. @@ -538,11 +535,10 @@ export const catchAllRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => Subscribable.Subscribable, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.catchAll(parent, error => asLensImpl(f(error) as Lens).access), - transformStream: stream => Stream.catchAll(stream, error => f(error).changes), -} as LensStep)) +): Lens => derive(self, { + access: Effect.catchAll(error => asLensImpl(f(error) as Lens).access), + transformStream: Stream.catchAll(error => f(error).changes), +})) /** * Runs an effect when read failures occur. @@ -560,11 +556,10 @@ export const tapErrorRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => Effect.Effect, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.tapError(parent, f), - transformStream: stream => Stream.tapError(stream, f), -} as LensStep)) +): Lens => derive(self, { + access: Effect.tapError(f), + transformStream: Stream.tapError(f), +})) /** * Runs an effect when modify failures occur. @@ -583,14 +578,13 @@ export const tapErrorWrite: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => Effect.Effect, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map(parent, frame => ({ value: frame.value, commit: next => Effect.tapError(frame.commit(next), f), })), - transformStream: stream => stream, -} as LensStep)) + transformStream: identity, +})) /** * Runs an effect when any `Lens` failure occurs. @@ -609,8 +603,7 @@ export const tapError: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => Effect.Effect, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map( Effect.tapError(parent, f), frame => ({ @@ -618,8 +611,8 @@ export const tapError: { commit: next => Effect.tapError(frame.commit(next), f), }), ), - transformStream: stream => Stream.tapError(stream, f), -} as LensStep)) + transformStream: Stream.tapError(f), +})) /** @@ -636,8 +629,7 @@ export const provideContext: { } = Function.dual(2, ( self: Lens, context: Context.Context, -): Lens, Exclude> => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens, Exclude> => derive(self, { access: parent => Effect.map( Effect.provide(parent, context), frame => ({ @@ -645,46 +637,8 @@ export const provideContext: { commit: next => Effect.provide(frame.commit(Effect.provide(next, context)), context), }), ), - transformStream: stream => Stream.provideSomeContext(stream, context), -} as LensStep, RR, Exclude, RW>)) - -/** - * Provides a `Runtime` or `ManagedRuntime` to a `Lens`, removing it from both the read and write environments. - * - * `ManagedRuntime` may add its construction errors to both the read and write error channels. - */ -export const provideRuntime: { - ( - runtime: Runtime.Runtime, - ): (self: Lens) => Lens, Exclude> - ( - managedRuntime: ManagedRuntime.ManagedRuntime, - ): (self: Lens) => Lens, Exclude> - ( - self: Lens, - runtime: Runtime.Runtime, - ): Lens, Exclude> - ( - self: Lens, - runtime: ManagedRuntime.ManagedRuntime, - ): Lens, Exclude> -} = Function.dual(2, ( - self: Lens, - runtime: Runtime.Runtime, -) => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.map( - Effect.provide(parent, runtime), - frame => ({ - value: frame.value, - commit: next => Effect.provide(frame.commit(Effect.provide(next, runtime)), runtime), - }), - ), - transformStream: stream => Stream.unwrap(Effect.map( - Effect.provide(Effect.context(), runtime), - context => Stream.provideContext(stream, context), - )), -} as LensStep, RR, Exclude, RW>)) + transformStream: Stream.provideSomeContext(context), +})) /** * Provides a single service to a `Lens`, removing it from both the read and write environments. @@ -706,8 +660,7 @@ export const provideService: { self: Lens, tag: Context.Tag, service: NoInfer, -): Lens, Exclude> => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens, Exclude> => derive(self, { access: parent => Effect.map( Effect.provideService(parent, tag, service), frame => ({ @@ -715,8 +668,8 @@ export const provideService: { commit: next => Effect.provideService(frame.commit(Effect.provideService(next, tag, service)), tag, service), }), ), - transformStream: stream => Stream.provideService(stream, tag, service), -} as LensStep, RR, Exclude, RW>)) + transformStream: Stream.provideService(tag, service), +})) /** -- 2.52.0 From 1320fbce187ec2005cc4ff1642e7e6617a58c32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 04:20:26 +0200 Subject: [PATCH 31/59] Fix --- packages/effect-lens/src/tests.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/effect-lens/src/tests.ts b/packages/effect-lens/src/tests.ts index d1d4138..09d16b7 100644 --- a/packages/effect-lens/src/tests.ts +++ b/packages/effect-lens/src/tests.ts @@ -29,10 +29,10 @@ Effect.gen(function*() { Lens.focusObjectOn("users"), Lens.focusArrayAt(0), Lens.focusObjectOn("name"), - Lens.catchAllRead(() => Lens.makeLazy({ - sourceGet: Effect.succeed("User not found"), - sourceChanges: Stream.make("User not found"), - sourceCommit: () => Console.log("Test"), + Lens.catchAllRead(() => Lens.make({ + get: Effect.succeed("User not found"), + changes: Stream.make("User not found"), + commit: () => Console.log("Test"), withLock: Effect.unsafeMakeSemaphore(1).withPermits(1), })), ) -- 2.52.0 From 2e5ef92bb128021a8b263378ea785242461469f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 20:55:39 +0200 Subject: [PATCH 32/59] Refactor --- packages/effect-lens/src/Lens.test.ts | 17 ++++--- packages/effect-lens/src/Lens.ts | 70 +++++++++------------------ 2 files changed, 35 insertions(+), 52 deletions(-) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 58b118f..7ee6ed8 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Chunk, Context, Effect, Option, Stream, SubscriptionRef } from "effect" +import { Chunk, Context, Effect, identity, Option, Stream, SubscriptionRef } from "effect" import * as Lens from "./Lens.js" @@ -11,7 +11,8 @@ describe("Lens", () => { Lens.make({ get: Effect.fail("read" as const), changes: Stream.fail("read" as const), - set: () => Effect.void, + commit: () => Effect.void, + withLock: identity, }), error => `mapped:${ error }`, ) @@ -26,7 +27,8 @@ describe("Lens", () => { Lens.make({ get: Effect.succeed(1), changes: Stream.make(1), - set: () => Effect.fail("write" as const), + commit: () => Effect.fail("write" as const), + withLock: identity, }), () => "mapped-write", ) @@ -41,7 +43,8 @@ describe("Lens", () => { Lens.make({ get: Effect.fail("read" as const), changes: Stream.fail("read" as const), - set: () => Effect.fail("write" as const), + commit: () => Effect.fail("write" as const), + withLock: identity, }), () => "mapped", ) @@ -84,7 +87,8 @@ describe("Lens", () => { Lens.make({ get: Effect.fail("read" as const), changes: Stream.fail("read" as const), - set: () => Effect.void, + commit: () => Effect.void, + withLock: identity, }), () => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const), ) @@ -108,7 +112,8 @@ describe("Lens", () => { Lens.make({ get: Effect.succeed(1), changes: Stream.make(1), - set: () => Effect.fail("write" as const), + commit: () => Effect.fail("write" as const), + withLock: identity, }), () => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const), ) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index f467d71..0db61d0 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -40,13 +40,7 @@ export declare namespace LensImpl { } } -export abstract class LensImpl< - in out A, - in out ER = never, - in out EW = never, - in out RR = never, - in out RW = never, -> +export abstract class LensImpl extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId @@ -57,13 +51,11 @@ extends Pipeable.Class() implements Lens { abstract readonly changes: Stream.Stream abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - get get(): Effect.Effect { - return Effect.map(this.access, frame => frame.value) - } + get get() { return Effect.map(this.access, frame => frame.value) } - modifyEffect( - f: (a: A) => Effect.Effect, - ): Effect.Effect { + modifyEffect( + f: (a: A) => Effect.Effect, + ): Effect.Effect { return this.withLock(Effect.flatMap( this.access, frame => Effect.flatMap( @@ -86,29 +78,23 @@ export const asLensImpl = ( export declare namespace LensLazyImpl { - export interface Source { - readonly get: Effect.Effect - readonly changes: Stream.Stream - readonly commit: (b: B) => Effect.Effect + export interface Source { + readonly get: Effect.Effect + readonly changes: Stream.Stream + readonly commit: (a: A) => Effect.Effect readonly withLock: (self: Effect.Effect) => Effect.Effect } } -export class LensLazyImpl< - in out B, - in out ESW = never, - in out ESR = never, - in out RSR = never, - in out RSW = never, -> -extends LensImpl { +export class LensLazyImpl +extends LensImpl { constructor( - readonly source: LensLazyImpl.Source, + readonly source: LensLazyImpl.Source, ) { super() } - get access(): Effect.Effect, ESR, RSR> { + get access(): Effect.Effect, ER, RR> { return Effect.map( this.source.get, value => ({ @@ -117,7 +103,6 @@ extends LensImpl { }), ) } - get changes() { return this.source.changes } get withLock() { return this.source.withLock } } @@ -125,9 +110,9 @@ extends LensImpl { /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ -export const make = ( - source: LensLazyImpl.Source -): Lens => new LensLazyImpl(source) +export const make = ( + source: LensLazyImpl.Source +): Lens => new LensLazyImpl(source) export declare namespace DerivedLensImpl { @@ -168,17 +153,9 @@ extends LensImpl { super() } - get access(): Effect.Effect, ER, RR> { - return this.source.access(this.parent.access) - } - - get changes(): Stream.Stream { - return this.source.transformStream(this.parent.changes) - } - - get withLock() { - return this.parent.withLock - } + get access() { return this.source.access(this.parent.access) } + get changes() { return this.source.transformStream(this.parent.changes) } + get withLock() { return this.parent.withLock } } /** @@ -222,13 +199,14 @@ extends LensImpl { this.ref.get, value => ({ value, - commit: next => Effect.flatMap(next, value => Ref.set(this.ref.ref, value)), + commit: next => Effect.flatMap(next, value => this.commit(value)), }), ) } - get changes() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } get withLock() { return this.ref.withLock } + + commit(a: A) { return Ref.set(this.ref.ref, a) } } /** @@ -271,15 +249,15 @@ extends LensImpl { }), ) } - get changes() { return this.ref.changes } + get withLock() { return this.ref.semaphore.withPermits(1) } + commit(a: A) { return Effect.zipLeft( Ref.set(this.ref.ref, a), PubSub.publish(this.ref.pubsub, a), ) } - get withLock() { return this.ref.semaphore.withPermits(1) } } /** -- 2.52.0 From e46446ade78ee69f0e78f900fb1d84e9185881f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 20:58:33 +0200 Subject: [PATCH 33/59] Fix tests --- packages/effect-lens/src/Lens.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 7ee6ed8..1c3c794 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Chunk, Context, Effect, identity, Option, Stream, SubscriptionRef } from "effect" +import { Chunk, Context, Effect, Either, identity, Option, Stream, SubscriptionRef } from "effect" import * as Lens from "./Lens.js" @@ -19,7 +19,7 @@ describe("Lens", () => { const result = await Effect.runPromise(Effect.either(Lens.get(lens))) - expect(result.left).toBe("mapped:read") + expect(result).toEqual(Either.left("mapped:read")) }) test("mapErrorWrite transforms modify errors", async () => { @@ -35,7 +35,7 @@ describe("Lens", () => { const result = await Effect.runPromise(Effect.either(Lens.set(lens, 2))) - expect(result.left).toBe("mapped-write") + expect(result).toEqual(Either.left("mapped-write")) }) test("mapError transforms read and modify errors", async () => { @@ -54,8 +54,8 @@ describe("Lens", () => { Effect.either(Lens.set(lens, 1)), ] as const)) - expect(result[0].left).toBe("mapped") - expect(result[1].left).toBe("mapped") + expect(result[0]).toEqual(Either.left("mapped")) + expect(result[1]).toEqual(Either.left("mapped")) }) test("catchAllRead recovers from read failures", async () => { @@ -67,7 +67,8 @@ describe("Lens", () => { Lens.make({ get: Effect.fail("read" as const), changes: Stream.fail("read" as const), - set: () => Effect.void, + commit: () => Effect.void, + withLock: identity, }), () => Lens.fromSubscriptionRef(fallback), ), -- 2.52.0 From 33a941b448e48be2f5991991e53b17aec155a224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 21:11:15 +0200 Subject: [PATCH 34/59] Fix --- packages/effect-lens/src/Lens.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 0db61d0..fd9a55d 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -185,13 +185,10 @@ export declare namespace SynchronizedRefLensImpl { export class SynchronizedRefLensImpl extends LensImpl { - readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals - constructor( - ref: SynchronizedRef.SynchronizedRef + readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals ) { super() - this.ref = ref as SynchronizedRefLensImpl.SynchronizedRefWithInternals } get access(): Effect.Effect, never, never> { @@ -217,7 +214,7 @@ extends LensImpl { */ export const fromSynchronizedRef = ( ref: SynchronizedRef.SynchronizedRef -): Lens => new SynchronizedRefLensImpl(ref) +): Lens => new SynchronizedRefLensImpl(ref as SynchronizedRefLensImpl.SynchronizedRefWithInternals) export declare namespace SubscriptionRefLensImpl { @@ -231,13 +228,10 @@ export declare namespace SubscriptionRefLensImpl { export class SubscriptionRefLensImpl extends LensImpl { - readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals - constructor( - ref: SubscriptionRef.SubscriptionRef + readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals ) { super() - this.ref = ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals } get access(): Effect.Effect, never, never> { @@ -265,7 +259,7 @@ extends LensImpl { */ export const fromSubscriptionRef = ( ref: SubscriptionRef.SubscriptionRef -): Lens => new SubscriptionRefLensImpl(ref) +): Lens => new SubscriptionRefLensImpl(ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals) /** -- 2.52.0 From cdc390cde697890b02fd9e047ba54efdfa4e91db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 21:13:44 +0200 Subject: [PATCH 35/59] Add concurrency test --- packages/effect-lens/src/Lens.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 1c3c794..8d84d2f 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -208,6 +208,31 @@ describe("Lens", () => { expect(result[1]).toBe(25) }) + test("modifyEffect updates are atomic under concurrency", async () => { + const iterations = 100 + + const result = await Effect.runPromise(Effect.flatMap( + SubscriptionRef.make({ count: 0 }), + parent => { + const countLens = Lens.focusObjectOn(Lens.fromSubscriptionRef(parent), "count") + + return Effect.flatMap( + Effect.forEach( + Array.from({ length: iterations }), + () => Lens.updateEffect( + countLens, + count => Effect.as(Effect.yieldNow(), count + 1), + ), + { concurrency: "unbounded", discard: true }, + ), + () => parent.get, + ) + }, + )) + + expect(result.count).toBe(iterations) + }) + test("focusObjectOn focuses a nested property without touching other fields", async () => { const [initialCount, updatedState] = await Effect.runPromise( Effect.flatMap( -- 2.52.0 From e9378c7542f988fc1b0a02338bb86b227c1c7a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 27 May 2026 21:15:07 +0200 Subject: [PATCH 36/59] Fix --- packages/effect-lens/src/Lens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index fd9a55d..50b629c 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -82,7 +82,7 @@ export declare namespace LensLazyImpl { readonly get: Effect.Effect readonly changes: Stream.Stream readonly commit: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly withLock: (self: Effect.Effect) => Effect.Effect } } @@ -274,7 +274,7 @@ export const unwrap = ( effect, l => Effect.flatMap(asLensImpl(l).access, frame => frame.commit(Effect.succeed(a))), ), - withLock: identity, + withLock: self => Effect.flatMap(effect, l => asLensImpl(l).withLock(self)), }) /** -- 2.52.0 From a02c7c3a3516c15ee306a147d9fc70fbe03db331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 27 May 2026 21:27:36 +0200 Subject: [PATCH 37/59] Fix --- packages/effect-lens/src/Lens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 50b629c..728112f 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -274,7 +274,7 @@ export const unwrap = ( effect, l => Effect.flatMap(asLensImpl(l).access, frame => frame.commit(Effect.succeed(a))), ), - withLock: self => Effect.flatMap(effect, l => asLensImpl(l).withLock(self)), + withLock: (self: Effect.Effect) => Effect.flatMap(effect, l => asLensImpl(l).withLock(self)), }) /** -- 2.52.0 From 02770b115a57968a64c19b06f2fda363ad33a9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 00:44:36 +0200 Subject: [PATCH 38/59] Fix --- packages/effect-lens/src/Lens.ts | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 728112f..189a74b 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -47,17 +47,17 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - abstract readonly access: Effect.Effect, ER, RR> + abstract readonly resolve: Effect.Effect, ER, RR> abstract readonly changes: Stream.Stream abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - get get() { return Effect.map(this.access, frame => frame.value) } + get get() { return Effect.map(this.resolve, frame => frame.value) } modifyEffect( f: (a: A) => Effect.Effect, ): Effect.Effect { return this.withLock(Effect.flatMap( - this.access, + this.resolve, frame => Effect.flatMap( f(frame.value), ([c, next]) => Effect.as(frame.commit(Effect.succeed(next)), c), @@ -94,7 +94,7 @@ extends LensImpl { super() } - get access(): Effect.Effect, ER, RR> { + get resolve(): Effect.Effect, ER, RR> { return Effect.map( this.source.get, value => ({ @@ -128,7 +128,7 @@ export declare namespace DerivedLensImpl { in out RW = never, in out RSW = never, > { - readonly access: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> + readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> readonly transformStream: (stream: Stream.Stream) => Stream.Stream } } @@ -153,7 +153,7 @@ extends LensImpl { super() } - get access() { return this.source.access(this.parent.access) } + get resolve() { return this.source.resolve(this.parent.resolve) } get changes() { return this.source.transformStream(this.parent.changes) } get withLock() { return this.parent.withLock } } @@ -191,7 +191,7 @@ extends LensImpl { super() } - get access(): Effect.Effect, never, never> { + get resolve(): Effect.Effect, never, never> { return Effect.map( this.ref.get, value => ({ @@ -234,7 +234,7 @@ extends LensImpl { super() } - get access(): Effect.Effect, never, never> { + get resolve(): Effect.Effect, never, never> { return Effect.map( this.ref.get, value => ({ @@ -272,7 +272,7 @@ export const unwrap = ( changes: Stream.unwrap(Effect.map(effect, l => l.changes)), commit: a => Effect.flatMap( effect, - l => Effect.flatMap(asLensImpl(l).access, frame => frame.commit(Effect.succeed(a))), + l => Effect.flatMap(asLensImpl(l).resolve, frame => frame.commit(Effect.succeed(a))), ), withLock: (self: Effect.Effect) => Effect.flatMap(effect, l => asLensImpl(l).withLock(self)), }) @@ -318,7 +318,7 @@ export const mapEffect: { get: (a: NoInfer) => Effect.Effect, set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): Lens => derive(self, { - access: parent => Effect.flatMap( + resolve: parent => Effect.flatMap( parent, frame => Effect.map( get(frame.value), @@ -412,7 +412,7 @@ export const mapStream: { self: Lens, f: (changes: Stream.Stream, NoInfer, NoInfer>) => Stream.Stream, NoInfer, NoInfer>, ): Lens => derive(self, { - access: identity, + resolve: identity, transformStream: f, })) @@ -434,7 +434,7 @@ export const mapErrorRead: { self: Lens, f: (error: NoInfer) => E2, ): Lens => derive(self, { - access: Effect.mapError(f), + resolve: Effect.mapError(f), transformStream: Stream.mapError(f), })) @@ -456,7 +456,7 @@ export const mapErrorWrite: { self: Lens, f: (error: unknown) => E2, ): Lens => derive(self, { - access: parent => Effect.map(parent, frame => ({ + resolve: parent => Effect.map(parent, frame => ({ value: frame.value, commit: next => Effect.mapError(frame.commit(next), f), })), @@ -481,7 +481,7 @@ export const mapError: { self: Lens, f: (error: unknown) => E2, ): Lens => derive(self, { - access: parent => Effect.map( + resolve: parent => Effect.map( Effect.mapError(parent, f), frame => ({ value: frame.value, @@ -508,7 +508,7 @@ export const catchAllRead: { self: Lens, f: (error: NoInfer) => Subscribable.Subscribable, ): Lens => derive(self, { - access: Effect.catchAll(error => asLensImpl(f(error) as Lens).access), + resolve: Effect.catchAll(error => asLensImpl(f(error) as Lens).resolve), transformStream: Stream.catchAll(error => f(error).changes), })) @@ -529,7 +529,7 @@ export const tapErrorRead: { self: Lens, f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { - access: Effect.tapError(f), + resolve: Effect.tapError(f), transformStream: Stream.tapError(f), })) @@ -551,7 +551,7 @@ export const tapErrorWrite: { self: Lens, f: (error: unknown) => Effect.Effect, ): Lens => derive(self, { - access: parent => Effect.map(parent, frame => ({ + resolve: parent => Effect.map(parent, frame => ({ value: frame.value, commit: next => Effect.tapError(frame.commit(next), f), })), @@ -576,7 +576,7 @@ export const tapError: { self: Lens, f: (error: unknown) => Effect.Effect, ): Lens => derive(self, { - access: parent => Effect.map( + resolve: parent => Effect.map( Effect.tapError(parent, f), frame => ({ value: frame.value, @@ -602,7 +602,7 @@ export const provideContext: { self: Lens, context: Context.Context, ): Lens, Exclude> => derive(self, { - access: parent => Effect.map( + resolve: parent => Effect.map( Effect.provide(parent, context), frame => ({ value: frame.value, @@ -633,7 +633,7 @@ export const provideService: { tag: Context.Tag, service: NoInfer, ): Lens, Exclude> => derive(self, { - access: parent => Effect.map( + resolve: parent => Effect.map( Effect.provideService(parent, tag, service), frame => ({ value: frame.value, -- 2.52.0 From 7ed5135aa6b9b78153727a423a893f5680640d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 01:02:18 +0200 Subject: [PATCH 39/59] Add test --- packages/effect-lens/src/Lens.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 8d84d2f..de10c5d 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -233,6 +233,31 @@ describe("Lens", () => { expect(result.count).toBe(iterations) }) + test("unwrap delegates reads, writes, and locking to the inner lens", async () => { + const iterations = 100 + + const result = await Effect.runPromise(Effect.flatMap( + SubscriptionRef.make(0), + parent => { + const lens = Lens.unwrap(Effect.succeed(Lens.fromSubscriptionRef(parent))) + + return Effect.flatMap( + Effect.forEach( + Array.from({ length: iterations }), + () => Lens.updateEffect( + lens, + count => Effect.as(Effect.yieldNow(), count + 1), + ), + { concurrency: "unbounded", discard: true }, + ), + () => Effect.all([Lens.get(lens), parent.get] as const), + ) + }, + )) + + expect(result).toEqual([iterations, iterations]) + }) + test("focusObjectOn focuses a nested property without touching other fields", async () => { const [initialCount, updatedState] = await Effect.runPromise( Effect.flatMap( -- 2.52.0 From e3b6c89a1ef898035e2a682e54d7ae879b853f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 01:08:59 +0200 Subject: [PATCH 40/59] Fix --- packages/effect-lens/src/Lens.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 189a74b..922c9a5 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -162,17 +162,17 @@ extends LensImpl { * Derives a new `Lens` by linking a step to an existing parent lens. */ export const derive: { - ( - self: Lens, - source: DerivedLensImpl.Source, - ): Lens - ( - source: DerivedLensImpl.Source, - ): (self: Lens) => Lens -} = Function.dual(2, ( - self: Lens, - source: DerivedLensImpl.Source, -): Lens => new DerivedLensImpl(asLensImpl(self), source)) + ( + self: Lens, + source: DerivedLensImpl.Source, + ): Lens + ( + source: DerivedLensImpl.Source, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + source: DerivedLensImpl.Source, +): Lens => new DerivedLensImpl(asLensImpl(self), source)) export declare namespace SynchronizedRefLensImpl { -- 2.52.0 From b1cc4c9702f7b3aefe7b123b34080d98a155d198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 01:26:39 +0200 Subject: [PATCH 41/59] Fix --- packages/effect-lens/src/Lens.ts | 43 ++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 922c9a5..2f8ac7f 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -497,19 +497,46 @@ export const mapError: { * Applies to `get` and `changes` while leaving `modify` unchanged. */ export const catchAllRead: { + ( + self: Lens, + f: (error: NoInfer) => Lens, + ): Lens + ( + f: (error: NoInfer) => Lens, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + f: (error: NoInfer) => Lens, +): Lens => derive(self, { + resolve: parent => Effect.catchAll( + parent, + error => asLensImpl(f(error)).resolve as Effect.Effect, E2, R2>, + ), + transformStream: Stream.catchAll(error => f(error).changes), +} as DerivedLensImpl.Source)) + +/** + * Recovers from modify failures of a `Lens`. + * + * Applies to the commit/rebuild portion of `modifyEffect` while leaving reads unchanged. + */ +export const catchAllWrite: { ( self: Lens, - f: (error: NoInfer) => Subscribable.Subscribable, - ): Lens + f: (error: unknown) => Effect.Effect, + ): Lens ( - f: (error: NoInfer) => Subscribable.Subscribable, - ): (self: Lens) => Lens + f: (error: unknown) => Effect.Effect, + ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: NoInfer) => Subscribable.Subscribable, -): Lens => derive(self, { - resolve: Effect.catchAll(error => asLensImpl(f(error) as Lens).resolve), - transformStream: Stream.catchAll(error => f(error).changes), + f: (error: unknown) => Effect.Effect, +): Lens => derive(self, { + resolve: parent => Effect.map(parent, frame => ({ + value: frame.value, + commit: next => Effect.catchAll(frame.commit(next), f), + })), + transformStream: identity, })) /** -- 2.52.0 From 2aa4e4eb97b44966e4afa91a3e9006adf3bd08bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 02:14:52 +0200 Subject: [PATCH 42/59] Fix --- packages/effect-lens/src/Lens.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 2f8ac7f..9355e9b 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -294,11 +294,16 @@ export const map: { self: Lens, get: (a: NoInfer) => B, update: (a: NoInfer, b: B) => NoInfer, -): Lens => mapEffect( - self, - a => Effect.succeed(get(a)), - (a, b) => Effect.succeed(update(a, b)), -)) +): Lens => derive(self, { + resolve: parent => Effect.map( + parent, + frame => ({ + value: get(frame.value), + commit: next => frame.commit(Effect.map(next, b => update(frame.value, b))), + }), + ), + transformStream: Stream.map(get), +})) /** * Derives a new `Lens` by applying effectful getters and setters over the focused value. @@ -307,16 +312,16 @@ export const mapEffect: { ( self: Lens, get: (a: NoInfer) => Effect.Effect, - set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, + update: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): Lens ( get: (a: NoInfer) => Effect.Effect, - set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, + update: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): (self: Lens) => Lens } = Function.dual(3, ( self: Lens, get: (a: NoInfer) => Effect.Effect, - set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, + update: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): Lens => derive(self, { resolve: parent => Effect.flatMap( parent, @@ -324,7 +329,7 @@ export const mapEffect: { get(frame.value), value => ({ value, - commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), + commit: next => frame.commit(Effect.flatMap(next, b => update(frame.value, b))), }), ), ), -- 2.52.0 From f7bee21ade9987eaf77c68ba4c069b907a9c8f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 02:46:27 +0200 Subject: [PATCH 43/59] Fix --- packages/effect-lens/src/Lens.ts | 77 +++++++++++++++++++------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 9355e9b..f6a213b 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -446,24 +446,27 @@ export const mapErrorRead: { /** * 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`. + * Applies to the commit/rebuild portion of `modifyEffect` while leaving failures from the + * user-supplied callback unchanged. */ export const mapErrorWrite: { ( self: Lens, - f: (error: unknown) => E2, + f: (error: NoInfer) => E2, ): Lens ( - f: (error: unknown) => E2, + f: (error: NoInfer) => E2, ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: unknown) => E2, + f: (error: NoInfer) => E2, ): Lens => derive(self, { resolve: parent => Effect.map(parent, frame => ({ value: frame.value, - commit: next => Effect.mapError(frame.commit(next), f), + commit: next => Effect.flatMap( + next, + value => Effect.mapError(frame.commit(Effect.succeed(value)), f), + ), })), transformStream: identity, })) @@ -471,26 +474,29 @@ export const mapErrorWrite: { /** * 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`. + * Applies to `get`, `changes`, and the commit/rebuild portion of `modifyEffect` while leaving + * failures from the user-supplied callback unchanged. */ export const mapError: { ( self: Lens, - f: (error: unknown) => E2, + f: (error: NoInfer) => E2, ): Lens ( - f: (error: unknown) => E2, + f: (error: NoInfer) => E2, ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: unknown) => E2, + f: (error: NoInfer) => E2, ): Lens => derive(self, { resolve: parent => Effect.map( Effect.mapError(parent, f), frame => ({ value: frame.value, - commit: next => Effect.mapError(frame.commit(next), f), + commit: next => Effect.flatMap( + next, + value => Effect.mapError(frame.commit(Effect.succeed(value)), f), + ), }), ), transformStream: Stream.mapError(f), @@ -528,18 +534,21 @@ export const catchAllRead: { export const catchAllWrite: { ( self: Lens, - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens ( - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { resolve: parent => Effect.map(parent, frame => ({ value: frame.value, - commit: next => Effect.catchAll(frame.commit(next), f), + commit: next => Effect.flatMap( + next, + value => Effect.catchAll(frame.commit(Effect.succeed(value)), f), + ), })), transformStream: identity, })) @@ -552,14 +561,14 @@ export const catchAllWrite: { export const tapErrorRead: { ( self: Lens, - f: (error: NoInfer) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens ( - f: (error: NoInfer) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: NoInfer) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { resolve: Effect.tapError(f), transformStream: Stream.tapError(f), @@ -568,24 +577,27 @@ export const tapErrorRead: { /** * 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`. + * Applies to the commit/rebuild portion of `modifyEffect` while leaving failures from the + * user-supplied callback unchanged. */ export const tapErrorWrite: { ( self: Lens, - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens ( - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { resolve: parent => Effect.map(parent, frame => ({ value: frame.value, - commit: next => Effect.tapError(frame.commit(next), f), + commit: next => Effect.flatMap( + next, + value => Effect.tapError(frame.commit(Effect.succeed(value)), f), + ), })), transformStream: identity, })) @@ -593,26 +605,29 @@ export const tapErrorWrite: { /** * 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`. + * Applies to `get`, `changes`, and the commit/rebuild portion of `modifyEffect` while leaving + * failures from the user-supplied callback unchanged. */ export const tapError: { ( self: Lens, - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens ( - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): (self: Lens) => Lens } = Function.dual(2, ( self: Lens, - f: (error: unknown) => Effect.Effect, + f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { resolve: parent => Effect.map( Effect.tapError(parent, f), frame => ({ value: frame.value, - commit: next => Effect.tapError(frame.commit(next), f), + commit: next => Effect.flatMap( + next, + value => Effect.tapError(frame.commit(Effect.succeed(value)), f), + ), }), ), transformStream: Stream.tapError(f), -- 2.52.0 From 75e67dc6a6c41f80f91379bbbb4066c7717380d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 20:30:17 +0200 Subject: [PATCH 44/59] Fix --- packages/effect-lens/src/Lens.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index f6a213b..9959aa1 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -193,17 +193,18 @@ extends LensImpl { get resolve(): Effect.Effect, never, never> { return Effect.map( - this.ref.get, + Ref.get(this.ref.ref), value => ({ value, - commit: next => Effect.flatMap(next, value => this.commit(value)), + commit: next => Effect.flatMap( + next, + value => Ref.set(this.ref.ref, value), + ), }), ) } - get changes() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } + get changes() { return Stream.unwrap(Effect.map(Ref.get(this.ref.ref), Stream.make)) } get withLock() { return this.ref.withLock } - - commit(a: A) { return Ref.set(this.ref.ref, a) } } /** @@ -239,19 +240,18 @@ extends LensImpl { this.ref.get, value => ({ value, - commit: next => Effect.flatMap(next, value => this.commit(value)), + commit: next => Effect.flatMap( + next, + value => Effect.zipLeft( + Ref.set(this.ref.ref, value), + PubSub.publish(this.ref.pubsub, value), + ), + ), }), ) } get changes() { return this.ref.changes } get withLock() { return this.ref.semaphore.withPermits(1) } - - commit(a: A) { - return Effect.zipLeft( - Ref.set(this.ref.ref, a), - PubSub.publish(this.ref.pubsub, a), - ) - } } /** -- 2.52.0 From 0494511b663903448f593ad8e0b854a98f0ef168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 28 May 2026 21:07:41 +0200 Subject: [PATCH 45/59] Add Ref impl --- packages/effect-lens/src/Lens.ts | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 9959aa1..3579b6a 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -175,6 +175,44 @@ export const derive: { ): Lens => new DerivedLensImpl(asLensImpl(self), source)) +export class RefLensImpl +extends LensImpl { + constructor( + readonly ref: Ref.Ref, + readonly semaphore: Effect.Semaphore, + ) { + super() + } + + get resolve(): Effect.Effect, never, never> { + return Effect.map( + Ref.get(this.ref), + value => ({ + value, + commit: next => Effect.flatMap( + next, + value => Ref.set(this.ref, value), + ), + }), + ) + } + get changes() { return Stream.unwrap(Effect.map(Ref.get(this.ref), Stream.make)) } + get withLock() { return this.semaphore.withPermits(1) } +} + +/** + * Creates a `Lens` that proxies a `Ref`. + * + * Note: since `Ref` does not provide any kind of reactivity mechanism, the produced `Lens` will be non-reactive. + * This means its `changes` stream will only emit the current value once when evaluated and nothing else. + */ +export const fromRef = Effect.fnUntraced(function* ( + ref: Ref.Ref +): Effect.fn.Return, never, never> { + return new RefLensImpl(ref, yield* Effect.makeSemaphore(1)) +}) + + export declare namespace SynchronizedRefLensImpl { export interface SynchronizedRefWithInternals extends SynchronizedRef.SynchronizedRef { -- 2.52.0 From c3b375ddc3cd079d5d9d0e1511402ed99b974c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 04:12:12 +0200 Subject: [PATCH 46/59] Refactor --- packages/effect-lens/src/Lens.ts | 60 ++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 3579b6a..5edc410 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -49,7 +49,7 @@ extends Pipeable.Class() implements Lens { abstract readonly resolve: Effect.Effect, ER, RR> abstract readonly changes: Stream.Stream - abstract readonly withLock: (self: Effect.Effect) => Effect.Effect + abstract readonly withLock: (self: Effect.Effect) => Effect.Effect get get() { return Effect.map(this.resolve, frame => frame.value) } @@ -82,7 +82,7 @@ export declare namespace LensLazyImpl { readonly get: Effect.Effect readonly changes: Stream.Stream readonly commit: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly withLock: (self: Effect.Effect) => Effect.Effect } } @@ -129,7 +129,10 @@ export declare namespace DerivedLensImpl { in out RSW = never, > { readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> - readonly transformStream: (stream: Stream.Stream) => Stream.Stream + readonly mapStream: (stream: Stream.Stream) => Stream.Stream + readonly withLock: ( + withLock: (self: Effect.Effect) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect } } @@ -154,8 +157,8 @@ extends LensImpl { } get resolve() { return this.source.resolve(this.parent.resolve) } - get changes() { return this.source.transformStream(this.parent.changes) } - get withLock() { return this.parent.withLock } + get changes() { return this.source.mapStream(this.parent.changes) } + get withLock() { return this.source.withLock(this.parent.withLock) } } /** @@ -322,25 +325,26 @@ export const map: { ( self: Lens, get: (a: NoInfer) => B, - update: (a: NoInfer, b: B) => NoInfer, + set: (a: NoInfer, b: B) => NoInfer, ): Lens ( get: (a: NoInfer) => B, - update: (a: NoInfer, b: B) => NoInfer, + set: (a: NoInfer, b: B) => NoInfer, ): (self: Lens) => Lens } = Function.dual(3, ( self: Lens, get: (a: NoInfer) => B, - update: (a: NoInfer, b: B) => NoInfer, + set: (a: NoInfer, b: B) => NoInfer, ): Lens => derive(self, { resolve: parent => Effect.map( parent, frame => ({ value: get(frame.value), - commit: next => frame.commit(Effect.map(next, b => update(frame.value, b))), + commit: next => frame.commit(Effect.map(next, b => set(frame.value, b))), }), ), - transformStream: Stream.map(get), + mapStream: Stream.map(get), + withLock: identity, })) /** @@ -371,7 +375,8 @@ export const mapEffect: { }), ), ), - transformStream: Stream.mapEffect(get), + mapStream: Stream.mapEffect(get), + withLock: identity, })) /** @@ -456,7 +461,8 @@ export const mapStream: { f: (changes: Stream.Stream, NoInfer, NoInfer>) => Stream.Stream, NoInfer, NoInfer>, ): Lens => derive(self, { resolve: identity, - transformStream: f, + mapStream: f, + withLock: identity, })) @@ -478,7 +484,8 @@ export const mapErrorRead: { f: (error: NoInfer) => E2, ): Lens => derive(self, { resolve: Effect.mapError(f), - transformStream: Stream.mapError(f), + mapStream: Stream.mapError(f), + withLock: identity, })) /** @@ -506,7 +513,8 @@ export const mapErrorWrite: { value => Effect.mapError(frame.commit(Effect.succeed(value)), f), ), })), - transformStream: identity, + mapStream: identity, + withLock: identity, })) /** @@ -537,7 +545,8 @@ export const mapError: { ), }), ), - transformStream: Stream.mapError(f), + mapStream: Stream.mapError(f), + withLock: identity, })) /** @@ -561,7 +570,8 @@ export const catchAllRead: { parent, error => asLensImpl(f(error)).resolve as Effect.Effect, E2, R2>, ), - transformStream: Stream.catchAll(error => f(error).changes), + mapStream: Stream.catchAll(error => f(error).changes), + withLock: identity, } as DerivedLensImpl.Source)) /** @@ -588,7 +598,8 @@ export const catchAllWrite: { value => Effect.catchAll(frame.commit(Effect.succeed(value)), f), ), })), - transformStream: identity, + mapStream: identity, + withLock: identity, })) /** @@ -609,7 +620,8 @@ export const tapErrorRead: { f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { resolve: Effect.tapError(f), - transformStream: Stream.tapError(f), + mapStream: Stream.tapError(f), + withLock: identity, })) /** @@ -637,7 +649,8 @@ export const tapErrorWrite: { value => Effect.tapError(frame.commit(Effect.succeed(value)), f), ), })), - transformStream: identity, + mapStream: identity, + withLock: identity, })) /** @@ -668,7 +681,8 @@ export const tapError: { ), }), ), - transformStream: Stream.tapError(f), + mapStream: Stream.tapError(f), + withLock: identity, })) @@ -694,7 +708,8 @@ export const provideContext: { commit: next => Effect.provide(frame.commit(Effect.provide(next, context)), context), }), ), - transformStream: Stream.provideSomeContext(context), + mapStream: Stream.provideSomeContext(context), + withLock: parentWithLock => self => Effect.provide(parentWithLock(Effect.provide(self, context)), context), })) /** @@ -725,7 +740,8 @@ export const provideService: { commit: next => Effect.provideService(frame.commit(Effect.provideService(next, tag, service)), tag, service), }), ), - transformStream: Stream.provideService(tag, service), + mapStream: Stream.provideService(tag, service), + withLock: parentWithLock => self => Effect.provideService(parentWithLock(Effect.provideService(self, tag, service)), tag, service), })) -- 2.52.0 From b817d1f8b3a42524282da928c476c56b473d4778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 04:44:03 +0200 Subject: [PATCH 47/59] Refactor --- packages/effect-lens/src/Lens.test.ts | 12 ++--- packages/effect-lens/src/Lens.ts | 78 ++++++++++++++++----------- packages/effect-lens/src/tests.ts | 2 +- 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index de10c5d..9a62dce 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -12,7 +12,7 @@ describe("Lens", () => { get: Effect.fail("read" as const), changes: Stream.fail("read" as const), commit: () => Effect.void, - withLock: identity, + lock: Effect.succeed(identity), }), error => `mapped:${ error }`, ) @@ -28,7 +28,7 @@ describe("Lens", () => { get: Effect.succeed(1), changes: Stream.make(1), commit: () => Effect.fail("write" as const), - withLock: identity, + lock: Effect.succeed(identity), }), () => "mapped-write", ) @@ -44,7 +44,7 @@ describe("Lens", () => { get: Effect.fail("read" as const), changes: Stream.fail("read" as const), commit: () => Effect.fail("write" as const), - withLock: identity, + lock: Effect.succeed(identity), }), () => "mapped", ) @@ -68,7 +68,7 @@ describe("Lens", () => { get: Effect.fail("read" as const), changes: Stream.fail("read" as const), commit: () => Effect.void, - withLock: identity, + lock: Effect.succeed(identity), }), () => Lens.fromSubscriptionRef(fallback), ), @@ -89,7 +89,7 @@ describe("Lens", () => { get: Effect.fail("read" as const), changes: Stream.fail("read" as const), commit: () => Effect.void, - withLock: identity, + lock: Effect.succeed(identity), }), () => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const), ) @@ -114,7 +114,7 @@ describe("Lens", () => { get: Effect.succeed(1), changes: Stream.make(1), commit: () => Effect.fail("write" as const), - withLock: identity, + lock: Effect.succeed(identity), }), () => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const), ) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 5edc410..c581347 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -38,6 +38,10 @@ export declare namespace LensImpl { next: Effect.Effect ) => Effect.Effect } + + export interface Lock { + (self: Effect.Effect): Effect.Effect + } } export abstract class LensImpl @@ -49,20 +53,23 @@ extends Pipeable.Class() implements Lens { abstract readonly resolve: Effect.Effect, ER, RR> abstract readonly changes: Stream.Stream - abstract readonly withLock: (self: Effect.Effect) => Effect.Effect + abstract readonly lock: Effect.Effect get get() { return Effect.map(this.resolve, frame => frame.value) } modifyEffect( f: (a: A) => Effect.Effect, ): Effect.Effect { - return this.withLock(Effect.flatMap( - this.resolve, - frame => Effect.flatMap( - f(frame.value), - ([c, next]) => Effect.as(frame.commit(Effect.succeed(next)), c), - ), - )) + return Effect.flatMap( + this.lock, + lock => lock(Effect.flatMap( + this.resolve, + frame => Effect.flatMap( + f(frame.value), + ([c, next]) => Effect.as(frame.commit(Effect.succeed(next)), c), + ), + )), + ) } } @@ -82,7 +89,7 @@ export declare namespace LensLazyImpl { readonly get: Effect.Effect readonly changes: Stream.Stream readonly commit: (a: A) => Effect.Effect - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly lock: Effect.Effect } } @@ -104,7 +111,7 @@ extends LensImpl { ) } get changes() { return this.source.changes } - get withLock() { return this.source.withLock } + get lock() { return this.source.lock } } /** @@ -130,9 +137,7 @@ export declare namespace DerivedLensImpl { > { readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> readonly mapStream: (stream: Stream.Stream) => Stream.Stream - readonly withLock: ( - withLock: (self: Effect.Effect) => Effect.Effect - ) => (self: Effect.Effect) => Effect.Effect + readonly mapLock: (lock: Effect.Effect) => Effect.Effect } } @@ -158,7 +163,7 @@ extends LensImpl { get resolve() { return this.source.resolve(this.parent.resolve) } get changes() { return this.source.mapStream(this.parent.changes) } - get withLock() { return this.source.withLock(this.parent.withLock) } + get lock() { return this.source.mapLock(this.parent.lock) } } /** @@ -200,7 +205,7 @@ extends LensImpl { ) } get changes() { return Stream.unwrap(Effect.map(Ref.get(this.ref), Stream.make)) } - get withLock() { return this.semaphore.withPermits(1) } + get lock() { return Effect.succeed(this.semaphore.withPermits(1)) } } /** @@ -220,7 +225,7 @@ export declare namespace SynchronizedRefLensImpl { export interface SynchronizedRefWithInternals extends SynchronizedRef.SynchronizedRef { readonly ref: Ref.Ref - readonly withLock: (self: Effect.Effect) => Effect.Effect + readonly withLock: LensImpl.Lock } } @@ -245,7 +250,7 @@ extends LensImpl { ) } get changes() { return Stream.unwrap(Effect.map(Ref.get(this.ref.ref), Stream.make)) } - get withLock() { return this.ref.withLock } + get lock() { return Effect.succeed(this.ref.withLock) } } /** @@ -292,7 +297,7 @@ extends LensImpl { ) } get changes() { return this.ref.changes } - get withLock() { return this.ref.semaphore.withPermits(1) } + get lock() { return Effect.succeed(this.ref.semaphore.withPermits(1)) } } /** @@ -315,7 +320,7 @@ export const unwrap = ( effect, l => Effect.flatMap(asLensImpl(l).resolve, frame => frame.commit(Effect.succeed(a))), ), - withLock: (self: Effect.Effect) => Effect.flatMap(effect, l => asLensImpl(l).withLock(self)), + lock: Effect.flatMap(effect, l => asLensImpl(l).lock), }) /** @@ -344,7 +349,7 @@ export const map: { }), ), mapStream: Stream.map(get), - withLock: identity, + mapLock: identity, })) /** @@ -376,7 +381,7 @@ export const mapEffect: { ), ), mapStream: Stream.mapEffect(get), - withLock: identity, + mapLock: lock => lock as Effect.Effect, })) /** @@ -462,7 +467,7 @@ export const mapStream: { ): Lens => derive(self, { resolve: identity, mapStream: f, - withLock: identity, + mapLock: identity, })) @@ -485,7 +490,7 @@ export const mapErrorRead: { ): Lens => derive(self, { resolve: Effect.mapError(f), mapStream: Stream.mapError(f), - withLock: identity, + mapLock: identity, })) /** @@ -514,7 +519,7 @@ export const mapErrorWrite: { ), })), mapStream: identity, - withLock: identity, + mapLock: Effect.mapError(f), })) /** @@ -546,7 +551,7 @@ export const mapError: { }), ), mapStream: Stream.mapError(f), - withLock: identity, + mapLock: Effect.mapError(f), })) /** @@ -571,7 +576,7 @@ export const catchAllRead: { error => asLensImpl(f(error)).resolve as Effect.Effect, E2, R2>, ), mapStream: Stream.catchAll(error => f(error).changes), - withLock: identity, + mapLock: lock => lock as Effect.Effect, } as DerivedLensImpl.Source)) /** @@ -599,7 +604,10 @@ export const catchAllWrite: { ), })), mapStream: identity, - withLock: identity, + mapLock: lock => Effect.catchAll( + lock, + error => Effect.as(f(error), identityLock), + ), })) /** @@ -621,7 +629,7 @@ export const tapErrorRead: { ): Lens => derive(self, { resolve: Effect.tapError(f), mapStream: Stream.tapError(f), - withLock: identity, + mapLock: lock => lock, })) /** @@ -650,7 +658,7 @@ export const tapErrorWrite: { ), })), mapStream: identity, - withLock: identity, + mapLock: lock => Effect.tapError(lock, f), })) /** @@ -682,7 +690,7 @@ export const tapError: { }), ), mapStream: Stream.tapError(f), - withLock: identity, + mapLock: lock => Effect.tapError(lock, f), })) @@ -709,7 +717,10 @@ export const provideContext: { }), ), mapStream: Stream.provideSomeContext(context), - withLock: parentWithLock => self => Effect.provide(parentWithLock(Effect.provide(self, context)), context), + mapLock: lock => Effect.map( + Effect.provide(lock, context), + lock => self => Effect.provide(lock(Effect.provide(self, context)), context), + ), })) /** @@ -741,7 +752,10 @@ export const provideService: { }), ), mapStream: Stream.provideService(tag, service), - withLock: parentWithLock => self => Effect.provideService(parentWithLock(Effect.provideService(self, tag, service)), tag, service), + mapLock: lock => Effect.map( + Effect.provideService(lock, tag, service), + lock => self => Effect.provideService(lock(Effect.provideService(self, tag, service)), tag, service), + ), })) diff --git a/packages/effect-lens/src/tests.ts b/packages/effect-lens/src/tests.ts index 09d16b7..6cb06f5 100644 --- a/packages/effect-lens/src/tests.ts +++ b/packages/effect-lens/src/tests.ts @@ -33,7 +33,7 @@ Effect.gen(function*() { get: Effect.succeed("User not found"), changes: Stream.make("User not found"), commit: () => Console.log("Test"), - withLock: Effect.unsafeMakeSemaphore(1).withPermits(1), + lock: Effect.succeed(Effect.unsafeMakeSemaphore(1).withPermits(1)), })), ) -- 2.52.0 From 1da20a96c024f9ff30a69c83f6cc651784beec8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 04:49:49 +0200 Subject: [PATCH 48/59] Fix --- packages/effect-lens/src/Lens.ts | 46 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index c581347..0a87838 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -183,6 +183,35 @@ export const derive: { ): Lens => new DerivedLensImpl(asLensImpl(self), source)) +export class UnwrappedLensImpl +extends LensImpl { + constructor( + readonly effect: Effect.Effect, E1, R1> + ) { + super() + } + + get resolve(): Effect.Effect, ER | E1, RR | R1> { + return Effect.map( + Effect.flatMap(this.effect, l => asLensImpl(l).resolve), + frame => ({ + value: frame.value, + commit: next => frame.commit(next), + }), + ) + } + get changes() { return Stream.unwrap(Effect.map(this.effect, l => l.changes)) } + get lock() { return Effect.flatMap(this.effect, l => asLensImpl(l).lock) } +} + +/** + * Flattens an effectful `Lens`. + */ +export const unwrap = ( + effect: Effect.Effect, E1, R1> +): Lens => new UnwrappedLensImpl(effect) + + export class RefLensImpl extends LensImpl { constructor( @@ -308,21 +337,6 @@ export const fromSubscriptionRef = ( ): Lens => new SubscriptionRefLensImpl(ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals) -/** - * Flattens an effectful `Lens`. - */ -export const unwrap = ( - effect: Effect.Effect, E1, R1> -): Lens => make({ - get: Effect.flatMap(effect, l => l.get), - changes: Stream.unwrap(Effect.map(effect, l => l.changes)), - commit: a => Effect.flatMap( - effect, - l => Effect.flatMap(asLensImpl(l).resolve, frame => frame.commit(Effect.succeed(a))), - ), - lock: Effect.flatMap(effect, l => asLensImpl(l).lock), -}) - /** * Derives a new `Lens` by applying synchronous getters and setters over the focused value. */ @@ -606,7 +620,7 @@ export const catchAllWrite: { mapStream: identity, mapLock: lock => Effect.catchAll( lock, - error => Effect.as(f(error), identityLock), + error => Effect.as(f(error), identity), ), })) -- 2.52.0 From 595a51a8711ca576c4f3112771f78c71c75aec5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:24:24 +0200 Subject: [PATCH 49/59] Refactor --- packages/effect-lens/src/Lens.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 0a87838..6e4ae57 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -373,16 +373,16 @@ export const mapEffect: { ( self: Lens, get: (a: NoInfer) => Effect.Effect, - update: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, + set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): Lens ( get: (a: NoInfer) => Effect.Effect, - update: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, + set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): (self: Lens) => Lens } = Function.dual(3, ( self: Lens, get: (a: NoInfer) => Effect.Effect, - update: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, + set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, ): Lens => derive(self, { resolve: parent => Effect.flatMap( parent, @@ -390,12 +390,12 @@ export const mapEffect: { get(frame.value), value => ({ value, - commit: next => frame.commit(Effect.flatMap(next, b => update(frame.value, b))), + commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), }), ), ), mapStream: Stream.mapEffect(get), - mapLock: lock => lock as Effect.Effect, + mapLock: identity>, })) /** @@ -643,7 +643,7 @@ export const tapErrorRead: { ): Lens => derive(self, { resolve: Effect.tapError(f), mapStream: Stream.tapError(f), - mapLock: lock => lock, + mapLock: identity, })) /** @@ -672,7 +672,7 @@ export const tapErrorWrite: { ), })), mapStream: identity, - mapLock: lock => Effect.tapError(lock, f), + mapLock: Effect.tapError(f), })) /** @@ -704,7 +704,7 @@ export const tapError: { }), ), mapStream: Stream.tapError(f), - mapLock: lock => Effect.tapError(lock, f), + mapLock: Effect.tapError(f), })) @@ -727,14 +727,11 @@ export const provideContext: { Effect.provide(parent, context), frame => ({ value: frame.value, - commit: next => Effect.provide(frame.commit(Effect.provide(next, context)), context), + commit: next => Effect.provide(frame.commit(next), context), }), ), mapStream: Stream.provideSomeContext(context), - mapLock: lock => Effect.map( - Effect.provide(lock, context), - lock => self => Effect.provide(lock(Effect.provide(self, context)), context), - ), + mapLock: Effect.provide(context), })) /** @@ -762,14 +759,11 @@ export const provideService: { Effect.provideService(parent, tag, service), frame => ({ value: frame.value, - commit: next => Effect.provideService(frame.commit(Effect.provideService(next, tag, service)), tag, service), + commit: next => Effect.provideService(frame.commit(next), tag, service), }), ), mapStream: Stream.provideService(tag, service), - mapLock: lock => Effect.map( - Effect.provideService(lock, tag, service), - lock => self => Effect.provideService(lock(Effect.provideService(self, tag, service)), tag, service), - ), + mapLock: Effect.provideService(tag, service), })) -- 2.52.0 From 709dbb339a57a054105b534b46f2ab8cb2b1cb26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:28:43 +0200 Subject: [PATCH 50/59] Cleanup --- packages/effect-lens/src/Lens.ts | 56 -------------------------------- 1 file changed, 56 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 6e4ae57..e205877 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -568,62 +568,6 @@ export const mapError: { mapLock: Effect.mapError(f), })) -/** - * Recovers from read failures of a `Lens`. - * - * Applies to `get` and `changes` while leaving `modify` unchanged. - */ -export const catchAllRead: { - ( - self: Lens, - f: (error: NoInfer) => Lens, - ): Lens - ( - f: (error: NoInfer) => Lens, - ): (self: Lens) => Lens -} = Function.dual(2, ( - self: Lens, - f: (error: NoInfer) => Lens, -): Lens => derive(self, { - resolve: parent => Effect.catchAll( - parent, - error => asLensImpl(f(error)).resolve as Effect.Effect, E2, R2>, - ), - mapStream: Stream.catchAll(error => f(error).changes), - mapLock: lock => lock as Effect.Effect, -} as DerivedLensImpl.Source)) - -/** - * Recovers from modify failures of a `Lens`. - * - * Applies to the commit/rebuild portion of `modifyEffect` while leaving reads unchanged. - */ -export const catchAllWrite: { - ( - self: Lens, - f: (error: NoInfer) => Effect.Effect, - ): Lens - ( - f: (error: NoInfer) => Effect.Effect, - ): (self: Lens) => Lens -} = Function.dual(2, ( - self: Lens, - f: (error: NoInfer) => Effect.Effect, -): Lens => derive(self, { - resolve: parent => Effect.map(parent, frame => ({ - value: frame.value, - commit: next => Effect.flatMap( - next, - value => Effect.catchAll(frame.commit(Effect.succeed(value)), f), - ), - })), - mapStream: identity, - mapLock: lock => Effect.catchAll( - lock, - error => Effect.as(f(error), identity), - ), -})) - /** * Runs an effect when read failures occur. * -- 2.52.0 From 3ccd8f17dc26dc6b13a5ae34e05aa2911f0749c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:29:48 +0200 Subject: [PATCH 51/59] Cleanup --- packages/effect-lens/src/Lens.test.ts | 21 ------------ packages/effect-lens/src/tests.ts | 46 --------------------------- 2 files changed, 67 deletions(-) delete mode 100644 packages/effect-lens/src/tests.ts diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 9a62dce..dd413f2 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -58,27 +58,6 @@ describe("Lens", () => { expect(result[1]).toEqual(Either.left("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({ - get: Effect.fail("read" as const), - changes: Stream.fail("read" as const), - commit: () => Effect.void, - lock: Effect.succeed(identity), - }), - () => Lens.fromSubscriptionRef(fallback), - ), - ), - ), - ) - - expect(result).toBe(42) - }) - test("tapErrorRead runs an effect on read failures", async () => { const result = await Effect.runPromise( Effect.flatMap( diff --git a/packages/effect-lens/src/tests.ts b/packages/effect-lens/src/tests.ts deleted file mode 100644 index 6cb06f5..0000000 --- a/packages/effect-lens/src/tests.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Console, Effect, Stream, SubscriptionRef } from "effect" -import { Lens } from "./index.js" - - -class State extends Effect.Service()("State", { - effect: Effect.gen(function*() { - const ref = yield* SubscriptionRef.make({ - users: [ - { name: "Adolf" } - ] - }) - - return { - lens: Lens.fromSubscriptionRef(ref) - } as const - }) -}) {} - - -Effect.gen(function*() { - const lens = Lens.unwrap(Effect.andThen( - State, - state => state.lens, - )).pipe( - Lens.provideContext(yield* Effect.provide(Effect.context(), State.Default)) - ) - - const adolfNameLens = lens.pipe( - Lens.focusObjectOn("users"), - Lens.focusArrayAt(0), - Lens.focusObjectOn("name"), - Lens.catchAllRead(() => Lens.make({ - get: Effect.succeed("User not found"), - changes: Stream.make("User not found"), - commit: () => Console.log("Test"), - lock: Effect.succeed(Effect.unsafeMakeSemaphore(1).withPermits(1)), - })), - ) - - console.log(yield* Lens.get(adolfNameLens)) - yield* Lens.set(lens, { users: [] }) - yield* Lens.set(adolfNameLens, "Himmler") - console.log(yield* Lens.get(adolfNameLens)) -}).pipe( - Effect.runSync, -) -- 2.52.0 From 1cc85f2e3c6707e5d77b5019e02ed9078179c780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:32:52 +0200 Subject: [PATCH 52/59] Refactor --- packages/effect-lens/src/Lens.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index e205877..5210b17 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -32,7 +32,7 @@ export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImp export type LensImplTypeId = typeof LensImplTypeId export declare namespace LensImpl { - export interface Frame { + export interface Resolved { readonly value: A readonly commit: ( next: Effect.Effect @@ -51,7 +51,7 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - abstract readonly resolve: Effect.Effect, ER, RR> + abstract readonly resolve: Effect.Effect, ER, RR> abstract readonly changes: Stream.Stream abstract readonly lock: Effect.Effect @@ -101,7 +101,7 @@ extends LensImpl { super() } - get resolve(): Effect.Effect, ER, RR> { + get resolve(): Effect.Effect, ER, RR> { return Effect.map( this.source.get, value => ({ @@ -135,7 +135,7 @@ export declare namespace DerivedLensImpl { in out RW = never, in out RSW = never, > { - readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> + readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> readonly mapStream: (stream: Stream.Stream) => Stream.Stream readonly mapLock: (lock: Effect.Effect) => Effect.Effect } @@ -191,7 +191,7 @@ extends LensImpl { super() } - get resolve(): Effect.Effect, ER | E1, RR | R1> { + get resolve(): Effect.Effect, ER | E1, RR | R1> { return Effect.map( Effect.flatMap(this.effect, l => asLensImpl(l).resolve), frame => ({ @@ -221,7 +221,7 @@ extends LensImpl { super() } - get resolve(): Effect.Effect, never, never> { + get resolve(): Effect.Effect, never, never> { return Effect.map( Ref.get(this.ref), value => ({ @@ -266,7 +266,7 @@ extends LensImpl { super() } - get resolve(): Effect.Effect, never, never> { + get resolve(): Effect.Effect, never, never> { return Effect.map( Ref.get(this.ref.ref), value => ({ @@ -310,7 +310,7 @@ extends LensImpl { super() } - get resolve(): Effect.Effect, never, never> { + get resolve(): Effect.Effect, never, never> { return Effect.map( this.ref.get, value => ({ -- 2.52.0 From 188ccc20d2ade11f6f661486681214e884746609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:41:11 +0200 Subject: [PATCH 53/59] Version bump --- bun.lock | 66 +++++++++++++++--------------- package.json | 14 +++---- packages/effect-lens/package.json | 2 +- packages/effect-lens/tsconfig.json | 2 +- packages/example/package.json | 6 +-- packages/example/tsconfig.json | 2 +- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/bun.lock b/bun.lock index d2c88e8..0a126eb 100644 --- a/bun.lock +++ b/bun.lock @@ -5,18 +5,18 @@ "": { "name": "@effect-lens/monorepo", "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@effect/language-service": "^0.80.0", - "@types/bun": "^1.3.6", - "npm-check-updates": "^19.3.1", + "@biomejs/biome": "^2.4.16", + "@effect/language-service": "^0.86.2", + "@types/bun": "^1.3.14", + "npm-check-updates": "^22.2.1", "npm-sort": "^0.0.4", - "turbo": "^2.7.5", - "typescript": "^5.9.3", + "turbo": "^2.9.16", + "typescript": "^6.0.3", }, }, "packages/effect-lens": { "name": "effect-lens", - "version": "0.1.0", + "version": "0.1.5", "peerDependencies": { "effect": "^3.21.0", }, @@ -25,37 +25,37 @@ "name": "@effect-lens/example", "version": "0.0.0", "dependencies": { - "@effect/platform": "^0.96.0", + "@effect/platform": "^0.96.1", "@effect/platform-browser": "^0.76.0", - "effect": "^3.21.0", + "effect": "^3.21.2", "effect-lens": "workspace:*", }, }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], + "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], "@effect-lens/example": ["@effect-lens/example@workspace:packages/example"], - "@effect/language-service": ["@effect/language-service@0.80.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-dKMATT1fDzaCpNrICpXga7sjJBtFLpKCAoE/1MiGXI8UwcHA9rmAZ2t52JO9g/kJpERWyomkJ+rl+VFlwNIofg=="], + "@effect/language-service": ["@effect/language-service@0.86.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-SaPln+8srOqDJDUwNTDmP5e+IYpEDr9+1epGznnsLqu8xvo6VnxyWARdeLpqvZJlb0Pgy9ca7ppqvvdWbHPXAg=="], - "@effect/platform": ["@effect/platform@0.96.0", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.21.0" } }, "sha512-U7PLhkVzg7zzrgFvyWATOzD6reL87KG/fcdOxgLWBQ/J5CCU6qdPAVG+0o6o+IxcsLoqGwxs+rFxaFzrdtDV1A=="], + "@effect/platform": ["@effect/platform@0.96.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.10", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.21.2" } }, "sha512-cjB1QZZYEP8JXCFNGvBLVi0T6YUBQTmOVEUA3SDbiQ6RUO+p6CE3eyD2vMWmrz5nE8yY5QSAuOV9v0boEcUv+A=="], "@effect/platform-browser": ["@effect/platform-browser@0.76.0", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/platform": "^0.96.0", "effect": "^3.21.0" } }, "sha512-cUyBpcLstrP/HiNsIePMBAI6R1+u6aRFlAUZb4wf08y1d1Vqf/Dmxsq14ZjBfnSYiqBPrCeYf1ZI+qMGQQL0RA=="], @@ -73,27 +73,27 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw=="], - "@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.16", "", { "os": "linux", "cpu": "x64" }, "sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ=="], - "@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.16", "", { "os": "win32", "cpu": "x64" }, "sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], + "effect": ["effect@3.21.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-rXd2FGDM8KdjSIrc+mqEELo7ScW7xTVxEf1iInmPSpIde9/nyGuFM710cjTo7/EreGXiUX2MOonPpprbz2XHCg=="], "effect-lens": ["effect-lens@workspace:packages/effect-lens"], @@ -101,7 +101,7 @@ "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], + "msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], @@ -109,15 +109,15 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "npm-check-updates": ["npm-check-updates@19.6.5", "", { "bin": { "npm-check-updates": "build/cli.js", "ncu": "build/cli.js" } }, "sha512-XlBUMC30relXfEerrnX239W9iB30U6Woz0Hj42Sv6iSF4EGOvj2mS2r45sZ3RglH0VPBxXOWooMxObZ/SMZhrw=="], + "npm-check-updates": ["npm-check-updates@22.2.1", "", { "bin": { "npm-check-updates": "build/cli.js", "ncu": "build/cli.js" } }, "sha512-mGdIJfhtg+q0BzhbOpbOL73zhMZlgBQMG4wnBwPMMD5k96028UCuV0753YeSYk9odoh7HWK6/cY69bWxT7o+yg=="], "npm-sort": ["npm-sort@0.0.4", "", { "bin": { "npm-sort": "./index.js" } }, "sha512-S5Id/3Jvr7Cf/QnWjRteprngERCBhhEFOM+wMhUrAYP060/HUBC1aL5GoXS3xITlgacJCWaSmP4HQaAt91nNYQ=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], - "turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="], + "turbo": ["turbo@2.9.16", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.16", "@turbo/darwin-arm64": "2.9.16", "@turbo/linux-64": "2.9.16", "@turbo/linux-arm64": "2.9.16", "@turbo/windows-64": "2.9.16", "@turbo/windows-arm64": "2.9.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], } diff --git a/package.json b/package.json index 53119a7..d67ce38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@effect-lens/monorepo", - "packageManager": "bun@1.3.6", + "packageManager": "bun@1.3.14", "private": true, "workspaces": [ "./packages/*" @@ -15,12 +15,12 @@ "clean:modules": "turbo clean:modules && rm -rf node_modules" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@effect/language-service": "^0.80.0", - "@types/bun": "^1.3.6", - "npm-check-updates": "^19.3.1", + "@biomejs/biome": "^2.4.16", + "@effect/language-service": "^0.86.2", + "@types/bun": "^1.3.14", + "npm-check-updates": "^22.2.1", "npm-sort": "^0.0.4", - "turbo": "^2.7.5", - "typescript": "^5.9.3" + "turbo": "^2.9.16", + "typescript": "^6.0.3" } } diff --git a/packages/effect-lens/package.json b/packages/effect-lens/package.json index 098c20b..8e92f02 100644 --- a/packages/effect-lens/package.json +++ b/packages/effect-lens/package.json @@ -1,7 +1,7 @@ { "name": "effect-lens", "description": "An effectful Lens type to easily manage nested state", - "version": "0.1.5", + "version": "0.2.0", "type": "module", "files": [ "./README.md", diff --git a/packages/effect-lens/tsconfig.json b/packages/effect-lens/tsconfig.json index 9d120e6..00c866c 100644 --- a/packages/effect-lens/tsconfig.json +++ b/packages/effect-lens/tsconfig.json @@ -6,7 +6,6 @@ "module": "NodeNext", "moduleDetection": "force", "jsx": "react-jsx", - // "allowJs": true, // Bundler mode "moduleResolution": "NodeNext", @@ -25,6 +24,7 @@ "noPropertyAccessFromIndexSignature": false, // Build + "rootDir": "./src", "outDir": "./dist", "declaration": true, "sourceMap": true, diff --git a/packages/example/package.json b/packages/example/package.json index 094ed98..427a73c 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -10,12 +10,12 @@ "clean:modules": "rm -rf node_modules" }, "dependencies": { - "@effect/platform": "^0.96.0", + "@effect/platform": "^0.96.1", "@effect/platform-browser": "^0.76.0", - "effect": "^3.21.0", + "effect": "^3.21.2", "effect-lens": "workspace:*" }, "overrides": { - "effect": "^3.21.0" + "effect": "^3.21.2" } } diff --git a/packages/example/tsconfig.json b/packages/example/tsconfig.json index 9d120e6..00c866c 100644 --- a/packages/example/tsconfig.json +++ b/packages/example/tsconfig.json @@ -6,7 +6,6 @@ "module": "NodeNext", "moduleDetection": "force", "jsx": "react-jsx", - // "allowJs": true, // Bundler mode "moduleResolution": "NodeNext", @@ -25,6 +24,7 @@ "noPropertyAccessFromIndexSignature": false, // Build + "rootDir": "./src", "outDir": "./dist", "declaration": true, "sourceMap": true, -- 2.52.0 From aad84f9ba964724a998a50543fc7e19813b4d86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:46:48 +0200 Subject: [PATCH 54/59] Update docs --- packages/effect-lens/README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index 22887dc..ce345eb 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -28,7 +28,7 @@ Lens< A, // Type of the value the lens is focused on ER, // Errors that can happen when reading EW, // Errors that can happen when writing - RE, // Requirements for reading + RR, // Requirements for reading RW // Requirements for writing > ``` @@ -55,11 +55,16 @@ yield* Lens.update(lens, Array.replace(1, 1664)) Currently available: - `fromSubscriptionRef` - `fromSynchronizedRef` (note: since `SynchronizedRef` is not reactive (does not produce a stream of value changes), the resulting Lens' `changes` stream will only emit the current value of the lens when evaluated, and nothing else) +- `fromRef` (returns an effect because it creates an internal lock) More to come! #### Manually -You can also create Lenses manually using `make` by providing a getter, a stream of changes and either a `set` or `modify` function depending on your needs. +You can also create Lenses manually using `make` by providing: +- `get`: an effect that reads the current value, +- `changes`: a stream of value changes, +- `commit`: an effectful write primitive, +- `lock`: an effect that produces the lock used to serialize writes. You can get pretty creative! Here's an example of a Lens that points to a specific key of the browser `LocalStorage`: ```typescript @@ -83,9 +88,11 @@ const lens = Effect.all([ Stream.unwrap, ), - set: a => Option.isSome(a) + commit: a => Option.isSome(a) ? kv.set(key, a.value) : kv.remove(key), + + lock: Effect.succeed(effect => effect), })), Effect.provide(BrowserKeyValueStore.layerLocalStorage), -- 2.52.0 From 96f2cfccef5ca4a4b3b913cb2c7dfdda56eed73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:51:02 +0200 Subject: [PATCH 55/59] Refactor --- packages/effect-lens/src/Lens.ts | 124 +++++++++++++++---------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 5210b17..a5d1082 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -79,7 +79,7 @@ export const asLensImpl = ( lens: Lens ): LensImpl => { if (!isLensImpl(lens)) - throw new Error("Not a 'LensImpl'.") + throw new Error("Not a 'LensImpl'") return lens as LensImpl } @@ -122,67 +122,6 @@ export const make = ( ): Lens => new LensLazyImpl(source) -export declare namespace DerivedLensImpl { - export interface Source< - in out A, - in out B, - in out ER = never, - in out ESR = never, - in out EW = never, - in out ESW = never, - in out RR = never, - in out RSR = never, - in out RW = never, - in out RSW = never, - > { - readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> - readonly mapStream: (stream: Stream.Stream) => Stream.Stream - readonly mapLock: (lock: Effect.Effect) => Effect.Effect - } -} - -export class DerivedLensImpl< - in out A, - in out B, - in out ER = never, - in out PER = never, - in out EW = never, - in out PEW = never, - in out RR = never, - in out PRR = never, - in out RW = never, - in out PRW = never, -> -extends LensImpl { - constructor( - readonly parent: LensImpl, - readonly source: DerivedLensImpl.Source, - ) { - super() - } - - get resolve() { return this.source.resolve(this.parent.resolve) } - get changes() { return this.source.mapStream(this.parent.changes) } - get lock() { return this.source.mapLock(this.parent.lock) } -} - -/** - * Derives a new `Lens` by linking a step to an existing parent lens. - */ -export const derive: { - ( - self: Lens, - source: DerivedLensImpl.Source, - ): Lens - ( - source: DerivedLensImpl.Source, - ): (self: Lens) => Lens -} = Function.dual(2, ( - self: Lens, - source: DerivedLensImpl.Source, -): Lens => new DerivedLensImpl(asLensImpl(self), source)) - - export class UnwrappedLensImpl extends LensImpl { constructor( @@ -337,6 +276,67 @@ export const fromSubscriptionRef = ( ): Lens => new SubscriptionRefLensImpl(ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals) +export declare namespace DerivedLensImpl { + export interface Source< + in out A, + in out B, + in out ER = never, + in out ESR = never, + in out EW = never, + in out ESW = never, + in out RR = never, + in out RSR = never, + in out RW = never, + in out RSW = never, + > { + readonly resolve: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> + readonly mapStream: (stream: Stream.Stream) => Stream.Stream + readonly mapLock: (lock: Effect.Effect) => Effect.Effect + } +} + +export class DerivedLensImpl< + in out A, + in out B, + in out ER = never, + in out PER = never, + in out EW = never, + in out PEW = never, + in out RR = never, + in out PRR = never, + in out RW = never, + in out PRW = never, +> +extends LensImpl { + constructor( + readonly parent: LensImpl, + readonly source: DerivedLensImpl.Source, + ) { + super() + } + + get resolve() { return this.source.resolve(this.parent.resolve) } + get changes() { return this.source.mapStream(this.parent.changes) } + get lock() { return this.source.mapLock(this.parent.lock) } +} + +/** + * Derives a new `Lens` by linking a step to an existing parent lens. + */ +export const derive: { + ( + self: Lens, + source: DerivedLensImpl.Source, + ): Lens + ( + source: DerivedLensImpl.Source, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + source: DerivedLensImpl.Source, +): Lens => new DerivedLensImpl(asLensImpl(self), source)) + + /** * Derives a new `Lens` by applying synchronous getters and setters over the focused value. */ -- 2.52.0 From 612c26bcad7a37fef2e07749966f38b536c40815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:57:50 +0200 Subject: [PATCH 56/59] Update README --- packages/effect-lens/README.md | 40 ++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index ce345eb..8e641a0 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -57,8 +57,6 @@ Currently available: - `fromSynchronizedRef` (note: since `SynchronizedRef` is not reactive (does not produce a stream of value changes), the resulting Lens' `changes` stream will only emit the current value of the lens when evaluated, and nothing else) - `fromRef` (returns an effect because it creates an internal lock) -More to come! - #### Manually You can also create Lenses manually using `make` by providing: - `get`: an effect that reads the current value, @@ -174,8 +172,6 @@ Currently available: | `focusChunkAt` | Focuses to an indexed entry of a `Chunk`. Replaces the parent `Chunk` immutably when writing to the focused element | Immutable | | | `focusOption` | Focuses to the value inside an `Option`. Wraps writes back into `Option.some` | Immutable | Reading or writing fails with `NoSuchElementException` when the parent option is `None` | -Also more to come! - #### Manually You can create focused Lenses by composing them manually using `map`, `mapEffect` and `unwrap`: ```typescript @@ -204,6 +200,42 @@ const benzemonstreLens = ref.pipe( // As you can see, this is automatically tracked by the Lens type ``` +#### Low-level derived lenses +For advanced cases, you can derive a Lens manually using `derive`. This is the primitive used by the built-in transforms. + +A derived Lens describes how to transform three parent channels: +- `resolve`: reads the parent and returns the focused value plus a `commit` function to rebuild the parent, +- `mapStream`: transforms the parent `changes` stream, +- `mapLock`: transforms the parent write lock. + +Most custom focusing logic should use `map` or `mapEffect`, but `derive` is useful when you need full control over read, stream, lock, and write-back behavior. + +```typescript +declare const lens: Lens.Lens + +const nameLens = lens.pipe( + Lens.derive({ + resolve: parent => Effect.map( + parent, + resolved => ({ + value: resolved.value.name, + commit: next => resolved.commit( + Effect.map(next, name => ({ + ...resolved.value, + name, + })), + ), + }), + ), + + mapStream: Stream.map(user => user.name), + + // This derived Lens does not add lock behavior, so it reuses the parent lock. + mapLock: identity, + }), +) +``` + ### Subscribable -- 2.52.0 From a76800676f14532d1711df610ef12e9401ae6f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 05:59:53 +0200 Subject: [PATCH 57/59] Update README --- packages/effect-lens/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index 8e641a0..813b7f9 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -70,8 +70,9 @@ You can get pretty creative! Here's an example of a Lens that points to a specif const lens = Effect.all([ KeyValueStore.KeyValueStore, Effect.succeed("someKey"), + Effect.makeSemaphore(1), ]).pipe( - Effect.map(([kv, key]) => Lens.make({ + Effect.map(([kv, key, semaphore]) => Lens.make({ get: kv.get(key), changes: kv.get(key).pipe( @@ -90,7 +91,7 @@ const lens = Effect.all([ ? kv.set(key, a.value) : kv.remove(key), - lock: Effect.succeed(effect => effect), + lock: Effect.succeed(semaphore.withPermits(1)), })), Effect.provide(BrowserKeyValueStore.layerLocalStorage), -- 2.52.0 From 9ae5cba8a8e318d1006d6bbafd494fd1e1357563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 06:02:36 +0200 Subject: [PATCH 58/59] Refactor --- packages/effect-lens/src/Lens.ts | 62 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index a5d1082..1ca5127 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -55,7 +55,7 @@ extends Pipeable.Class() implements Lens { abstract readonly changes: Stream.Stream abstract readonly lock: Effect.Effect - get get() { return Effect.map(this.resolve, frame => frame.value) } + get get() { return Effect.map(this.resolve, resolved => resolved.value) } modifyEffect( f: (a: A) => Effect.Effect, @@ -64,9 +64,9 @@ extends Pipeable.Class() implements Lens { this.lock, lock => lock(Effect.flatMap( this.resolve, - frame => Effect.flatMap( - f(frame.value), - ([c, next]) => Effect.as(frame.commit(Effect.succeed(next)), c), + resolved => Effect.flatMap( + f(resolved.value), + ([c, next]) => Effect.as(resolved.commit(Effect.succeed(next)), c), ), )), ) @@ -133,9 +133,9 @@ extends LensImpl { get resolve(): Effect.Effect, ER | E1, RR | R1> { return Effect.map( Effect.flatMap(this.effect, l => asLensImpl(l).resolve), - frame => ({ - value: frame.value, - commit: next => frame.commit(next), + resolved => ({ + value: resolved.value, + commit: next => resolved.commit(next), }), ) } @@ -357,9 +357,9 @@ export const map: { ): Lens => derive(self, { resolve: parent => Effect.map( parent, - frame => ({ - value: get(frame.value), - commit: next => frame.commit(Effect.map(next, b => set(frame.value, b))), + resolved => ({ + value: get(resolved.value), + commit: next => resolved.commit(Effect.map(next, b => set(resolved.value, b))), }), ), mapStream: Stream.map(get), @@ -386,11 +386,11 @@ export const mapEffect: { ): Lens => derive(self, { resolve: parent => Effect.flatMap( parent, - frame => Effect.map( - get(frame.value), + resolved => Effect.map( + get(resolved.value), value => ({ value, - commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), + commit: next => resolved.commit(Effect.flatMap(next, b => set(resolved.value, b))), }), ), ), @@ -525,11 +525,11 @@ export const mapErrorWrite: { self: Lens, f: (error: NoInfer) => E2, ): Lens => derive(self, { - resolve: parent => Effect.map(parent, frame => ({ - value: frame.value, + resolve: parent => Effect.map(parent, resolved => ({ + value: resolved.value, commit: next => Effect.flatMap( next, - value => Effect.mapError(frame.commit(Effect.succeed(value)), f), + value => Effect.mapError(resolved.commit(Effect.succeed(value)), f), ), })), mapStream: identity, @@ -556,11 +556,11 @@ export const mapError: { ): Lens => derive(self, { resolve: parent => Effect.map( Effect.mapError(parent, f), - frame => ({ - value: frame.value, + resolved => ({ + value: resolved.value, commit: next => Effect.flatMap( next, - value => Effect.mapError(frame.commit(Effect.succeed(value)), f), + value => Effect.mapError(resolved.commit(Effect.succeed(value)), f), ), }), ), @@ -608,11 +608,11 @@ export const tapErrorWrite: { self: Lens, f: (error: NoInfer) => Effect.Effect, ): Lens => derive(self, { - resolve: parent => Effect.map(parent, frame => ({ - value: frame.value, + resolve: parent => Effect.map(parent, resolved => ({ + value: resolved.value, commit: next => Effect.flatMap( next, - value => Effect.tapError(frame.commit(Effect.succeed(value)), f), + value => Effect.tapError(resolved.commit(Effect.succeed(value)), f), ), })), mapStream: identity, @@ -639,11 +639,11 @@ export const tapError: { ): Lens => derive(self, { resolve: parent => Effect.map( Effect.tapError(parent, f), - frame => ({ - value: frame.value, + resolved => ({ + value: resolved.value, commit: next => Effect.flatMap( next, - value => Effect.tapError(frame.commit(Effect.succeed(value)), f), + value => Effect.tapError(resolved.commit(Effect.succeed(value)), f), ), }), ), @@ -669,9 +669,9 @@ export const provideContext: { ): Lens, Exclude> => derive(self, { resolve: parent => Effect.map( Effect.provide(parent, context), - frame => ({ - value: frame.value, - commit: next => Effect.provide(frame.commit(next), context), + resolved => ({ + value: resolved.value, + commit: next => Effect.provide(resolved.commit(next), context), }), ), mapStream: Stream.provideSomeContext(context), @@ -701,9 +701,9 @@ export const provideService: { ): Lens, Exclude> => derive(self, { resolve: parent => Effect.map( Effect.provideService(parent, tag, service), - frame => ({ - value: frame.value, - commit: next => Effect.provideService(frame.commit(next), tag, service), + resolved => ({ + value: resolved.value, + commit: next => Effect.provideService(resolved.commit(next), tag, service), }), ), mapStream: Stream.provideService(tag, service), -- 2.52.0 From 20d84a10e590c695071365315faccbfdd83a05ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 30 May 2026 06:09:56 +0200 Subject: [PATCH 59/59] Cleanup --- bun.lock | 59 +++++++++++++++++++++++- packages/example/package.json | 1 + packages/example/src/localstorage.ts | 67 +++++++++++++++------------- 3 files changed, 95 insertions(+), 32 deletions(-) diff --git a/bun.lock b/bun.lock index 0a126eb..e6bdb5d 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ }, "packages/effect-lens": { "name": "effect-lens", - "version": "0.1.5", + "version": "0.2.0", "peerDependencies": { "effect": "^3.21.0", }, @@ -27,6 +27,7 @@ "dependencies": { "@effect/platform": "^0.96.1", "@effect/platform-browser": "^0.76.0", + "@effect/platform-bun": "^0.89.0", "effect": "^3.21.2", "effect-lens": "workspace:*", }, @@ -53,12 +54,26 @@ "@effect-lens/example": ["@effect-lens/example@workspace:packages/example"], + "@effect/cluster": ["@effect/cluster@0.58.2", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.96.1", "@effect/rpc": "^0.75.1", "@effect/sql": "^0.51.1", "@effect/workflow": "^0.18.0", "effect": "^3.21.2" } }, "sha512-oxQ3zUhXq0mJA7Y4TliALMP39Bx0LtAIxcqOW1Bdjh6uk+nG7kul/Puw80SwlcYGv3ul50SG+gvSRUTXB8d3JQ=="], + + "@effect/experimental": ["@effect/experimental@0.60.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.96.0", "effect": "^3.21.0", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-i5zIg7Xup2KgHyqHlYtkgqSE1bNzCL0GbbTQxrpIzKF0q/ebknOk/ox8B/gIq2vImjoEE81h/oxU+6i1NH210g=="], + "@effect/language-service": ["@effect/language-service@0.86.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-SaPln+8srOqDJDUwNTDmP5e+IYpEDr9+1epGznnsLqu8xvo6VnxyWARdeLpqvZJlb0Pgy9ca7ppqvvdWbHPXAg=="], "@effect/platform": ["@effect/platform@0.96.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.10", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.21.2" } }, "sha512-cjB1QZZYEP8JXCFNGvBLVi0T6YUBQTmOVEUA3SDbiQ6RUO+p6CE3eyD2vMWmrz5nE8yY5QSAuOV9v0boEcUv+A=="], "@effect/platform-browser": ["@effect/platform-browser@0.76.0", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/platform": "^0.96.0", "effect": "^3.21.0" } }, "sha512-cUyBpcLstrP/HiNsIePMBAI6R1+u6aRFlAUZb4wf08y1d1Vqf/Dmxsq14ZjBfnSYiqBPrCeYf1ZI+qMGQQL0RA=="], + "@effect/platform-bun": ["@effect/platform-bun@0.89.0", "", { "dependencies": { "@effect/platform-node-shared": "^0.59.0", "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/cluster": "^0.58.0", "@effect/platform": "^0.96.0", "@effect/rpc": "^0.75.0", "@effect/sql": "^0.51.0", "effect": "^3.21.0" } }, "sha512-ReT5f2vujJfffMOBexrgwJd2RLxgfr2G0c1FyCsoflcjdQJ7RZE3cwHDp1M3hAzmG67wWAssMHqLsX6H/n27sQ=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@0.59.0", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.7", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.58.0", "@effect/platform": "^0.96.0", "@effect/rpc": "^0.75.0", "@effect/sql": "^0.51.0", "effect": "^3.21.0" } }, "sha512-3bq2YKKfLY7UFauZSxqZUneCXoA3SMSls82V+0RKunvRlfPuPQW0hVn6t1RkvEdh0PDoygWG2mZXYQa6Iqgp9A=="], + + "@effect/rpc": ["@effect/rpc@0.75.1", "", { "dependencies": { "msgpackr": "^1.11.10" }, "peerDependencies": { "@effect/platform": "^0.96.1", "effect": "^3.21.2" } }, "sha512-8yxF8+mMGGEbF8BUCp34HjdJj7CvTpGeZxBcpsDF6v7zPiGbJL1UDLzA8ZqYjmcngBHhPecbmeONTk/LiLAaEg=="], + + "@effect/sql": ["@effect/sql@0.51.1", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.60.0", "@effect/platform": "^0.96.1", "effect": "^3.21.2" } }, "sha512-iPDAefrJcI0HcTk9keP9Gq8Pg08K1HmpnmZZt85AqyTcvorhoNsXDFiKBbPldfV2CortwVkacX8KjO9GPpSYCA=="], + + "@effect/workflow": ["@effect/workflow@0.18.1", "", { "peerDependencies": { "@effect/experimental": "^0.60.0", "@effect/platform": "^0.96.1", "@effect/rpc": "^0.75.1", "effect": "^3.21.2" } }, "sha512-FxsUxkyvd7CyN7tw4bQgmAJv8tf8hUwy72bwGYzKGpeuiEObiUKgO1pg8xM49gB6EtwOdVRJhytwcFc8eM/6ow=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], @@ -71,6 +86,34 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@turbo/darwin-64": ["@turbo/darwin-64@2.9.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw=="], @@ -101,18 +144,28 @@ "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], "npm-check-updates": ["npm-check-updates@22.2.1", "", { "bin": { "npm-check-updates": "build/cli.js", "ncu": "build/cli.js" } }, "sha512-mGdIJfhtg+q0BzhbOpbOL73zhMZlgBQMG4wnBwPMMD5k96028UCuV0753YeSYk9odoh7HWK6/cY69bWxT7o+yg=="], "npm-sort": ["npm-sort@0.0.4", "", { "bin": { "npm-sort": "./index.js" } }, "sha512-S5Id/3Jvr7Cf/QnWjRteprngERCBhhEFOM+wMhUrAYP060/HUBC1aL5GoXS3xITlgacJCWaSmP4HQaAt91nNYQ=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "turbo": ["turbo@2.9.16", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.16", "@turbo/darwin-arm64": "2.9.16", "@turbo/linux-64": "2.9.16", "@turbo/linux-arm64": "2.9.16", "@turbo/windows-64": "2.9.16", "@turbo/windows-arm64": "2.9.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg=="], @@ -120,5 +173,9 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], } } diff --git a/packages/example/package.json b/packages/example/package.json index 427a73c..e3a96c3 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -12,6 +12,7 @@ "dependencies": { "@effect/platform": "^0.96.1", "@effect/platform-browser": "^0.76.0", + "@effect/platform-bun": "^0.89.0", "effect": "^3.21.2", "effect-lens": "workspace:*" }, diff --git a/packages/example/src/localstorage.ts b/packages/example/src/localstorage.ts index 56f1d46..74eae7c 100644 --- a/packages/example/src/localstorage.ts +++ b/packages/example/src/localstorage.ts @@ -1,36 +1,41 @@ -import { KeyValueStore } from "@effect/platform" -import { BrowserKeyValueStore, BrowserStream } from "@effect/platform-browser" -import { Effect, Option, Stream } from "effect" -import { Lens } from "effect-lens" +// import { KeyValueStore } from "@effect/platform" +// import { BrowserKeyValueStore, BrowserStream } from "@effect/platform-browser" +// import { Effect, Option, Stream } from "effect" +// import { Lens } from "effect-lens" -Effect.gen(function*() { - // \/ Lens, PlatformError, PlatformError, never, never> - Effect.all([ - KeyValueStore.KeyValueStore, - Effect.succeed("someKey"), - ]).pipe( - Effect.map(([kv, key]) => Lens.make({ - get: kv.get(key), +// Effect.gen(function*() { +// // \/ Lens, PlatformError, PlatformError, never, never> +// const lens = Effect.all([ +// KeyValueStore.KeyValueStore, +// Effect.succeed("someKey"), +// Effect.makeSemaphore(1), +// ]).pipe( +// Effect.map(([kv, key, semaphore]) => Lens.make({ +// get: kv.get(key), - changes: kv.get(key).pipe( - Effect.map(Stream.make), - Effect.map(a => Stream.concat( - a, - BrowserStream.fromEventListenerWindow("storage").pipe( - Stream.filter(event => event.key === key), - Stream.map(event => Option.fromNullable(event.newValue)), - ), - )), - Stream.unwrap, - ), +// changes: kv.get(key).pipe( +// Effect.map(Stream.make), +// Effect.map(a => Stream.concat( +// a, +// BrowserStream.fromEventListenerWindow("storage").pipe( +// Stream.filter(event => event.key === key), +// Stream.map(event => Option.fromNullable(event.newValue)), +// ), +// )), +// Stream.unwrap, +// ), - set: a => Option.isSome(a) - ? kv.set(key, a.value) - : kv.remove(key), - })), +// commit: a => Option.isSome(a) +// ? kv.set(key, a.value) +// : kv.remove(key), - Effect.provide(BrowserKeyValueStore.layerLocalStorage), - Lens.unwrap, - ) -}) +// lock: Effect.succeed(semaphore.withPermits(1)), +// })), + +// Effect.provide(BrowserKeyValueStore.layerLocalStorage), +// Lens.unwrap, +// ) + +// console.log(yield* Lens.get(lens)) +// }) -- 2.52.0