From 0eef8cbecb805718aacb2c110fa37c8bf61c6582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 27 Apr 2026 18:12:46 +0200 Subject: [PATCH 1/4] 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 bdb5ffe..a307e0e 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -26,7 +26,7 @@ extends Subscribable.Subscribable { * Internal `Lens` implementation. */ export class LensImpl - extends Pipeable.Class() implements Lens { +extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [LensTypeId]: LensTypeId = LensTypeId -- 2.49.1 From 90f4a50eb901eda3c809126e6a0126193029c125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 30 Apr 2026 01:33:58 +0200 Subject: [PATCH 2/4] Change API --- packages/effect-lens/README.md | 20 ++++++++++---------- packages/effect-lens/src/Lens.test.ts | 8 ++++---- packages/effect-lens/src/Lens.ts | 14 +++++++------- packages/effect-lens/src/Subscribable.ts | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index 9046d54..605da81 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -137,9 +137,9 @@ const ref = yield* SubscriptionRef.make<{ // \/ Lens const jeanDupontLens = ref.pipe( - Lens.fromSubscriptionRef, // Creates a lens that proxies the ref - Lens.focusObjectField("users"), // Creates a focused lens that points to the users field - Lens.focusArrayAt(0), // Creates a focused lens that points to the first entry of the user array + Lens.fromSubscriptionRef, // Creates a lens that proxies the ref + Lens.focusObjectOn("users"), // Creates a focused lens that points to the users field + Lens.focusArrayAt(0), // Creates a focused lens that points to the first entry of the user array ) // Reading or writing from this lense can fail with NoSuchElementException // This is because of Lens.focusArrayAt(0), as reading and writing to an array is an unsafe operation @@ -148,18 +148,18 @@ const jeanDupont = yield* Lens.get(jeanDupontLens) yield* Lens.set( // You can focus even further down - Lens.focusObjectField(jeanDupontLens, "age"), + Lens.focusObjectOn(jeanDupontLens, "age"), yield* DateTime.make("03/25/1970"), ) // Mutations with the parent state are performed immutably by default -// unless you use a specific mutable transform such as 'focusMutableField' +// unless you use a specific mutable transform such as 'focusObjectOnWritable' ``` Currently available: | Name | Description | Parent state mutation behavior | Notes | | - | - | - | - | -| `focusObjectField` | Focuses to the field of an object. Replaces the parent object immutably when writing to the focused field | Immutable | | -| `focusObjectMutableField` | Focuses to the writable field of an object. Mutates the parent object in place via the writable field | Mutable | Type-safe: will not allow you to mutate `readonly` fields | +| `focusObjectOn` | Focuses to a field of an object. Replaces the parent object immutably when writing to the focused field | Immutable | | +| `focusObjectOnWritable` | Focuses to a writable field of an object. Mutates the parent object in place via the writable field | Mutable | Type-safe: will not allow you to mutate `readonly` fields | | `focusArrayAt` | Focuses to an indexed entry of an array. Replaces the parent array immutably when writing to the focused index | Immutable | | | `focusMutableArrayAt` | Focuses to an indexed entry of an array. Mutates the parent array in place at the focused index | Mutable | Type-safe: will not allow you to mutate `readonly` arrays | | `focusTupleAt` | Focuses to an indexed entry of a readonly tuple. Replaces the parent tuple immutably when writing to the focused index | Immutable | | @@ -216,7 +216,7 @@ const someFunctionThatShouldOnlyHaveReadonlyAccessToTheState = ( const lens = ref.pipe( Lens.fromSubscriptionRef, - Lens.focusObjectField("users"), + Lens.focusObjectOn("users"), ) yield* someFunctionThatShouldOnlyHaveReadonlyAccessToTheState(lens) ``` @@ -231,14 +231,14 @@ declare const sub: Subscribabe.Subscribable const nameSub = sub.pipe( Subscribable.focusArrayAt(1), - Subscribable.focusObjectField("name"), + Subscribable.focusObjectOn("name"), ) ``` Currently available: | Name | Description | | - | - | -| `focusObjectField` | Focuses to the field of an object | +| `focusObjectOn` | Focuses to the field of an object | | `focusArrayAt` | Focuses to an indexed entry of an array | | `focusTupleAt` | Focuses to an indexed entry of a tuple | | `focusChunkAt` | Focuses to an indexed entry of a `Chunk` | diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 33d3191..ea0b5e9 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -54,12 +54,12 @@ describe("Lens", () => { expect(result[1]).toEqual(Option.some(50)) // 100 / 2 }) - test("focusObjectField focuses a nested property without touching other fields", async () => { + test("focusObjectOn focuses a nested property without touching other fields", async () => { const [initialCount, updatedState] = await Effect.runPromise( Effect.flatMap( SubscriptionRef.make({ count: 1, label: "original" }), parent => { - const countLens = Lens.focusObjectField(Lens.fromSubscriptionRef(parent), "count") + const countLens = Lens.focusObjectOn(Lens.fromSubscriptionRef(parent), "count") return Effect.flatMap( Lens.get(countLens), count => Effect.flatMap( @@ -75,13 +75,13 @@ describe("Lens", () => { expect(updatedState).toEqual({ count: 6, label: "original" }) }) - test("focusObjectMutableField preserves the root identity when mutating in place", async () => { + test("focusObjectOnWritable preserves the root identity when mutating in place", async () => { const original = { detail: "keep" } const updated = await Effect.runPromise( Effect.flatMap( SubscriptionRef.make(original), parent => { - const detailLens = Lens.focusObjectMutableField(Lens.fromSubscriptionRef(parent), "detail") + const detailLens = Lens.focusObjectOnWritable(Lens.fromSubscriptionRef(parent), "detail") return Effect.flatMap( Lens.set(detailLens, "mutated"), () => parent.get, diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index a307e0e..0f1a6a8 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -273,7 +273,7 @@ export const mapStream: { /** * Narrows the focus to a field of an object. Replaces the object in an immutable fashion when written to. */ -export const focusObjectField: { +export const focusObjectOn: { ( self: Lens, key: K, @@ -290,7 +290,7 @@ export const focusObjectField: { (a, b) => Object.setPrototypeOf({ ...a, [key]: b }, Object.getPrototypeOf(a)), )) -export declare namespace focusObjectMutableField { +export declare namespace focusObjectOnWritable { export type WritableKeys = { [K in keyof T]-?: IfEquals< { [P in K]: T[K] }, @@ -304,17 +304,17 @@ export declare namespace focusObjectMutableField { } /** - * Narrows the focus to a mutable field of an object. Mutates the object in place when written to. + * Narrows the focus to a writable field of an object. Mutates the object in place when written to. */ -export const focusObjectMutableField: { - >( +export const focusObjectOnWritable: { + >( self: Lens, key: K, ): Lens - >( + >( key: K, ): (self: Lens) => Lens -} = Function.dual(2, >( +} = Function.dual(2, >( self: Lens, key: K, ): Lens => map(self, a => a[key], (a, b) => { a[key] = b; return a })) diff --git a/packages/effect-lens/src/Subscribable.ts b/packages/effect-lens/src/Subscribable.ts index 10cf991..2eb98a5 100644 --- a/packages/effect-lens/src/Subscribable.ts +++ b/packages/effect-lens/src/Subscribable.ts @@ -42,7 +42,7 @@ export const mapOptionEffect: { /** * Narrows the focus to a field of an object. */ -export const focusObjectField: { +export const focusObjectOn: { ( self: Subscribable.Subscribable, key: K, -- 2.49.1 From 646918598441335cd5ad0b218e2d88f7d5c042c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 30 Apr 2026 01:34:58 +0200 Subject: [PATCH 3/4] Cleanup --- packages/effect-lens/src/PropertyPath.test.ts | 67 ------------- packages/effect-lens/src/PropertyPath.ts | 98 ------------------- packages/effect-lens/src/index.ts | 1 - 3 files changed, 166 deletions(-) delete mode 100644 packages/effect-lens/src/PropertyPath.test.ts delete mode 100644 packages/effect-lens/src/PropertyPath.ts diff --git a/packages/effect-lens/src/PropertyPath.test.ts b/packages/effect-lens/src/PropertyPath.test.ts deleted file mode 100644 index 4c68a38..0000000 --- a/packages/effect-lens/src/PropertyPath.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { Option } from "effect" -import * as PropertyPath from "./PropertyPath.js" - - -describe("immutableSet with arrays", () => { - test("sets a top-level array element", () => { - const arr = [1, 2, 3] - const result = PropertyPath.immutableSet(arr, [1], 99) - expect(result).toEqual(Option.some([1, 99, 3])) - }) - - test("does not mutate the original array", () => { - const arr = [1, 2, 3] - PropertyPath.immutableSet(arr, [0], 42) - expect(arr).toEqual([1, 2, 3]) - }) - - test("sets the first element of an array", () => { - const arr = ["a", "b", "c"] - const result = PropertyPath.immutableSet(arr, [0], "z") - expect(result).toEqual(Option.some(["z", "b", "c"])) - }) - - test("sets the last element of an array", () => { - const arr = [10, 20, 30] - const result = PropertyPath.immutableSet(arr, [2], 99) - expect(result).toEqual(Option.some([10, 20, 99])) - }) - - test("sets a nested array element inside an object", () => { - const obj = { tags: ["foo", "bar", "baz"] } - const result = PropertyPath.immutableSet(obj, ["tags", 1], "qux") - expect(result).toEqual(Option.some({ tags: ["foo", "qux", "baz"] })) - }) - - test("sets a deeply nested value inside an array of objects", () => { - const obj = { items: [{ name: "alice" }, { name: "bob" }] } - const result = PropertyPath.immutableSet(obj, ["items", 0, "name"], "charlie") - expect(result).toEqual(Option.some({ items: [{ name: "charlie" }, { name: "bob" }] })) - }) - - test("sets a value in a nested array", () => { - const matrix = [[1, 2], [3, 4]] - const result = PropertyPath.immutableSet(matrix, [1, 0], 99) - expect(result).toEqual(Option.some([[1, 2], [99, 4]])) - }) - - test("returns Option.none() for an out-of-bounds index", () => { - const arr = [1, 2, 3] - const result = PropertyPath.immutableSet(arr, [5], 99) - expect(result).toEqual(Option.none()) - }) - - test("returns Option.none() for a non-numeric key on an array", () => { - const arr = [1, 2, 3] - // @ts-expect-error intentionally wrong key type - const result = PropertyPath.immutableSet(arr, ["length"], 0) - expect(result).toEqual(Option.none()) - }) - - test("empty path returns Option.some of the value itself", () => { - const arr = [1, 2, 3] - const result = PropertyPath.immutableSet(arr, [], [9, 9, 9] as any) - expect(result).toEqual(Option.some([9, 9, 9])) - }) -}) diff --git a/packages/effect-lens/src/PropertyPath.ts b/packages/effect-lens/src/PropertyPath.ts deleted file mode 100644 index b73d24d..0000000 --- a/packages/effect-lens/src/PropertyPath.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Array, Equivalence, Function, Option, Predicate } from "effect" - - -export type PropertyPath = readonly PropertyKey[] - -type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - -export type Paths = readonly [] | ( - D extends never ? readonly [] : - T extends Seen ? readonly [] : - T extends readonly any[] ? { - [K in keyof T as K extends number ? K : never]: - | readonly [K] - | readonly [K, ...Paths] - } extends infer O - ? O[keyof O] - : never - : - T extends object ? { - [K in keyof T as K extends string | number | symbol ? K : never]-?: - NonNullable extends infer V - ? readonly [K] | readonly [K, ...Paths] - : never - } extends infer O - ? O[keyof O] - : never - : - never -) - -export type ValueFromPath = P extends readonly [infer Head, ...infer Tail] - ? Head extends keyof T - ? ValueFromPath - : T extends readonly any[] - ? Head extends number - ? ValueFromPath - : never - : never - : T - - -export const equivalence: Equivalence.Equivalence = Equivalence.array(Equivalence.strict()) - -export const unsafeGet: { - >(path: P): (self: T) => ValueFromPath - >(self: T, path: P): ValueFromPath -} = Function.dual(2, >(self: T, path: P): ValueFromPath => - path.reduce((acc: any, key: any) => acc?.[key], self) -) - -export const get: { - >(path: P): (self: T) => Option.Option> - >(self: T, path: P): Option.Option> -} = Function.dual(2, >(self: T, path: P): Option.Option> => - path.reduce( - (acc: Option.Option, key: any): Option.Option => Option.isSome(acc) - ? Predicate.hasProperty(acc.value, key) - ? Option.some(acc.value[key]) - : Option.none() - : acc, - - Option.some(self), - ) -) - -export const immutableSet: { - >(path: P, value: ValueFromPath): (self: T) => Option.Option - >(self: T, path: P, value: ValueFromPath): Option.Option -} = Function.dual(3, >(self: T, path: P, value: ValueFromPath): Option.Option => { - const key = Array.head(path as PropertyPath) - if (Option.isNone(key)) - return Option.some(value as T) - if (!Predicate.hasProperty(self, key.value)) - return Option.none() - - const child = immutableSet(self[key.value], Option.getOrThrow(Array.tail(path as PropertyPath)), value) - if (Option.isNone(child)) - return child - - if (Array.isArray(self)) - return typeof key.value === "number" - ? Option.some([ - ...self.slice(0, key.value), - child.value, - ...self.slice(key.value + 1), - ] as T) - : Option.none() - - if (typeof self === "object") - return Option.some( - Object.assign( - Object.create(Object.getPrototypeOf(self)), - { ...self, [key.value]: child.value }, - ) - ) - - return Option.none() -}) diff --git a/packages/effect-lens/src/index.ts b/packages/effect-lens/src/index.ts index 6eec098..24e4bb8 100644 --- a/packages/effect-lens/src/index.ts +++ b/packages/effect-lens/src/index.ts @@ -1,3 +1,2 @@ export * as Lens from "./Lens.js" -export * as PropertyPath from "./PropertyPath.js" export * as Subscribable from "./Subscribable.js" -- 2.49.1 From f1ab8a1af21082f03b15f58cd0c065a2aec9690e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 30 Apr 2026 01:41:46 +0200 Subject: [PATCH 4/4] Fix --- packages/effect-lens/README.md | 16 ++++++++-------- packages/effect-lens/package.json | 6 +----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index 605da81..e7a7bad 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -36,13 +36,13 @@ Lens< ### Creating a Lens -#### From an exisiting type +#### From an existing type We provide a few helpers to create Lenses from some Effect types: ```typescript // The ref is the data source const ref = yield* SubscriptionRef.make([12, 87, 69]) -// The lens acts as a proxy that allows reading, subscribing from and writing to that +// The lens acts as a proxy that allows reading, subscribing to and writing to that // data source with a similar API to Effect's SubscriptionRef const lens = Lens.fromSubscriptionRef(ref) // ^ Lens.Lens @@ -54,7 +54,7 @@ yield* Lens.update(lens, Array.replace(1, 1664)) Currently available: - `fromSubscriptionRef` -- `fromSynchronizedRef` (note: since `SynchronizedRef` is not reactive (does not produces a stream of value changes), the resulting Lens' `changes` stream will only emit the current value of the lens when evaluated, and nothing else) +- `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) More to come! @@ -110,7 +110,7 @@ Lens<{ readonly a: string, readonly b: number }, never, never, never, never> Lens ``` -Focuses Lenses work just the same as a Lens that points directly to a data source and can be read, subscribed to or written to. +Focused Lenses work just the same as a Lens that points directly to a data source and can be read, subscribed to or written to. Writing to them will properly update parent Lenses or data sources. Such updates can be performed in both a mutable or an immutable manner depending on your choice. @@ -141,7 +141,7 @@ const jeanDupontLens = ref.pipe( Lens.focusObjectOn("users"), // Creates a focused lens that points to the users field Lens.focusArrayAt(0), // Creates a focused lens that points to the first entry of the user array ) -// Reading or writing from this lense can fail with NoSuchElementException +// Reading or writing from this lens can fail with NoSuchElementException // This is because of Lens.focusArrayAt(0), as reading and writing to an array is an unsafe operation const jeanDupont = yield* Lens.get(jeanDupontLens) @@ -211,7 +211,7 @@ const someFunctionThatShouldOnlyHaveReadonlyAccessToTheState = ( // Do whatever const usersCountSub = Subscribable.map(usersSub, a => a.length) const users = yield* usersSub.get - yield* Effect.forkScoped(Stream.runForEach(users.changes, ...)) + yield* Effect.forkScoped(Stream.runForEach(usersSub.changes, ...)) }) const lens = ref.pipe( @@ -226,9 +226,9 @@ This library re-exports Effect's `Subscribable` module and adds a few transforms ```typescript import { Subscribable } from "effect-lens" -declare const sub: Subscribabe.Subscribable +declare const sub: Subscribable.Subscribable -// \/ Subscribabe.Subscribable +// \/ Subscribable.Subscribable const nameSub = sub.pipe( Subscribable.focusArrayAt(1), Subscribable.focusObjectOn("name"), diff --git a/packages/effect-lens/package.json b/packages/effect-lens/package.json index 5e3a238..098c20b 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.4", + "version": "0.1.5", "type": "module", "files": [ "./README.md", @@ -21,10 +21,6 @@ "types": "./dist/Lens.d.ts", "default": "./dist/Lens.js" }, - "./PropertyPath": { - "types": "./dist/PropertyPath.d.ts", - "default": "./dist/PropertyPath.js" - }, "./Subscribable": { "types": "./dist/Subscribable.d.ts", "default": "./dist/Subscribable.js" -- 2.49.1