@@ -21,6 +21,16 @@
|
||||
"effect": "^3.21.0",
|
||||
},
|
||||
},
|
||||
"packages/effect-lens-next": {
|
||||
"name": "effect-lens-next",
|
||||
"version": "0.1.0-beta.0",
|
||||
"devDependencies": {
|
||||
"effect": "4.0.0-beta.85",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.85",
|
||||
},
|
||||
},
|
||||
"packages/example": {
|
||||
"name": "@effect-lens/example",
|
||||
"version": "0.0.0",
|
||||
@@ -140,10 +150,14 @@
|
||||
|
||||
"effect-lens": ["effect-lens@workspace:packages/effect-lens"],
|
||||
|
||||
"effect-lens-next": ["effect-lens-next@workspace:packages/effect-lens-next"],
|
||||
|
||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||
|
||||
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
|
||||
|
||||
"ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="],
|
||||
|
||||
"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=="],
|
||||
@@ -168,14 +182,44 @@
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
|
||||
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||
|
||||
"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@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=="],
|
||||
"uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"@effect/experimental/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
|
||||
|
||||
"@effect/sql/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
|
||||
|
||||
"effect-lens-next/effect": ["effect@4.0.0-beta.85", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Cjv9YQyv4CiIccmRAQIWAoeESCpCpiuHYY8zb5vqiYs3Ac2yE5RQAnKq7z4Ir/3VJYZb8kxh6rS8czDDnpTdkQ=="],
|
||||
|
||||
"effect-lens-next/effect/fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
||||
|
||||
"effect-lens-next/effect/fast-check/pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# Effect Lens Next
|
||||
|
||||
An effectful Lens for Effect v4.
|
||||
|
||||
`effect-lens-next` provides a bidirectional view into shared state. A Lens can read, observe, and atomically update a value while focusing on a nested part of a larger structure.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install effect@4.0.0-beta.85 effect-lens-next
|
||||
```
|
||||
|
||||
This release targets Effect `4.0.0-beta.85` exactly while Effect v4 is in beta.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ts
|
||||
import { Effect, Stream, SubscriptionRef } from "effect"
|
||||
import { Lens } from "effect-lens-next"
|
||||
|
||||
const program = Effect.gen(function*() {
|
||||
const state = yield* SubscriptionRef.make({ count: 0, label: "Counter" })
|
||||
|
||||
const count = state.pipe(
|
||||
Lens.fromSubscriptionRef,
|
||||
Lens.focusObjectOn("count")
|
||||
)
|
||||
|
||||
yield* Lens.update(count, (value) => value + 1)
|
||||
const current = yield* Lens.get(count)
|
||||
|
||||
yield* count.changes.pipe(
|
||||
Stream.take(1),
|
||||
Stream.runForEach((value) => Effect.log(`count: ${value}`))
|
||||
)
|
||||
|
||||
return current
|
||||
})
|
||||
```
|
||||
|
||||
## Lens Type
|
||||
|
||||
```ts
|
||||
Lens.Lens<
|
||||
A, // focused value
|
||||
ER, // read errors
|
||||
EW, // write errors
|
||||
RR, // read requirements
|
||||
RW // write requirements
|
||||
>
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
- `Lens.fromSubscriptionRef` creates a reactive Lens.
|
||||
- `Lens.fromSynchronizedRef` creates a non-reactive Lens whose stream emits its value when evaluated.
|
||||
- `Lens.fromRef` creates a non-reactive Lens and returns it in an Effect because it allocates a semaphore.
|
||||
- `Lens.make` creates a Lens from custom `get`, `changes`, `commit`, and `lock` operations.
|
||||
- `Lens.unwrap` flattens an effect that produces a Lens.
|
||||
|
||||
## Focusing
|
||||
|
||||
```ts
|
||||
import { Effect, SubscriptionRef } from "effect"
|
||||
import { Lens } from "effect-lens-next"
|
||||
|
||||
const program = Effect.gen(function*() {
|
||||
const state = yield* SubscriptionRef.make({
|
||||
users: [{ name: "Ada", score: 10 }]
|
||||
})
|
||||
|
||||
const score = state.pipe(
|
||||
Lens.fromSubscriptionRef,
|
||||
Lens.focusObjectOn("users"),
|
||||
Lens.focusArrayAt(0),
|
||||
Lens.focusObjectOn("score")
|
||||
)
|
||||
|
||||
yield* Lens.set(score, 11)
|
||||
})
|
||||
```
|
||||
|
||||
Available focus operations include object fields, mutable object fields, arrays, mutable arrays, tuples, mutable tuples, `Chunk` values, and `Option` values. Safe indexed operations fail with `Cause.NoSuchElementError` when the index is absent.
|
||||
|
||||
Effect v4 no longer treats `Option` as an Effect. Custom effectful mappings should convert optional values explicitly:
|
||||
|
||||
```ts
|
||||
import { Array, Effect } from "effect"
|
||||
|
||||
Lens.mapEffect(
|
||||
(values: ReadonlyArray<number>) => Effect.fromOption(Array.get(values, 0)),
|
||||
(values, value) => Effect.fromOption(Array.replace(values, 0, value))
|
||||
)
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
The update API follows Effect v4's `SubscriptionRef` conventions:
|
||||
|
||||
- `get`, `set`, `getAndSet`, `setAndGet`
|
||||
- `modify`, `modifyEffect`
|
||||
- `update`, `updateEffect`
|
||||
- `getAndUpdate`, `getAndUpdateEffect`
|
||||
- `updateAndGet`, `updateAndGetEffect`
|
||||
- `modifySome`, `modifySomeEffect`
|
||||
- `getAndUpdateSome`, `getAndUpdateSomeEffect`
|
||||
- `updateSome`, `updateSomeEffect`
|
||||
- `updateSomeAndGet`, `updateSomeAndGetEffect`
|
||||
|
||||
Conditional updates use `Option.none()` to skip both the write and change notification:
|
||||
|
||||
```ts
|
||||
import { Effect, Option } from "effect"
|
||||
|
||||
yield* Lens.updateSome(lens, (value) =>
|
||||
value < 10 ? Option.some(value + 1) : Option.none()
|
||||
)
|
||||
|
||||
yield* Lens.updateSomeEffect(lens, (value) =>
|
||||
Effect.succeed(value < 10 ? Option.some(value + 1) : Option.none())
|
||||
)
|
||||
```
|
||||
|
||||
## Subscribable
|
||||
|
||||
Effect v4 removed its `Readable` and `Subscribable` modules. This package provides a small compatible abstraction with `get` and `changes` properties:
|
||||
|
||||
```ts
|
||||
import { Subscribable } from "effect-lens-next"
|
||||
|
||||
const users = Subscribable.fromSubscriptionRef(usersRef)
|
||||
const count = Subscribable.focusArrayLength(users)
|
||||
|
||||
const current = yield* count.get
|
||||
```
|
||||
|
||||
The module includes `make`, `map`, `mapEffect`, `unwrap`, `fromSubscriptionRef`, and the same readonly focus helpers used by Lens.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"files": {
|
||||
"includes": ["./src/**"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "effect-lens-next",
|
||||
"description": "An effectful Lens type for Effect v4",
|
||||
"version": "0.1.0-beta.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
"./dist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"url": "git+https://github.com/Thiladev/effect-lens.git"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./Lens": {
|
||||
"types": "./dist/Lens.d.ts",
|
||||
"default": "./dist/Lens.js"
|
||||
},
|
||||
"./Subscribable": {
|
||||
"types": "./dist/Subscribable.d.ts",
|
||||
"default": "./dist/Subscribable.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint:biome": "biome lint",
|
||||
"pack": "npm pack",
|
||||
"clean:cache": "rm -rf .turbo tsconfig.tsbuildinfo",
|
||||
"clean:dist": "rm -rf dist",
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.85"
|
||||
},
|
||||
"devDependencies": {
|
||||
"effect": "4.0.0-beta.85"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Chunk, Context, Effect, Fiber, identity, Option, Ref, Result, Stream, SubscriptionRef, SynchronizedRef } from "effect"
|
||||
import * as Lens from "./Lens.js"
|
||||
|
||||
|
||||
describe("Lens", () => {
|
||||
class Offset extends Context.Service<Offset, { readonly value: number }>()("Offset") {}
|
||||
|
||||
test("mapErrorRead transforms read errors", async () => {
|
||||
const lens = Lens.mapErrorRead(
|
||||
Lens.make<number, "read", never, never, never>({
|
||||
get: Effect.fail("read" as const),
|
||||
changes: Stream.fail("read" as const),
|
||||
commit: () => Effect.void,
|
||||
lock: Effect.succeed(identity),
|
||||
}),
|
||||
error => `mapped:${ error }`,
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(Effect.result(Lens.get(lens)))
|
||||
|
||||
expect(result).toEqual(Result.fail("mapped:read"))
|
||||
})
|
||||
|
||||
test("mapErrorWrite transforms modify errors", async () => {
|
||||
const lens = Lens.mapErrorWrite(
|
||||
Lens.make<number, never, "write", never, never>({
|
||||
get: Effect.succeed(1),
|
||||
changes: Stream.make(1),
|
||||
commit: () => Effect.fail("write" as const),
|
||||
lock: Effect.succeed(identity),
|
||||
}),
|
||||
() => "mapped-write",
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(Effect.result(Lens.set(lens, 2)))
|
||||
|
||||
expect(result).toEqual(Result.fail("mapped-write"))
|
||||
})
|
||||
|
||||
test("mapError transforms read and modify errors", async () => {
|
||||
const lens = Lens.mapError(
|
||||
Lens.make<number, "read", "write", never, never>({
|
||||
get: Effect.fail("read" as const),
|
||||
changes: Stream.fail("read" as const),
|
||||
commit: () => Effect.fail("write" as const),
|
||||
lock: Effect.succeed(identity),
|
||||
}),
|
||||
() => "mapped",
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(Effect.all([
|
||||
Effect.result(Lens.get(lens)),
|
||||
Effect.result(Lens.set(lens, 1)),
|
||||
] as const))
|
||||
|
||||
expect(result[0]).toEqual(Result.fail("mapped"))
|
||||
expect(result[1]).toEqual(Result.fail("mapped"))
|
||||
})
|
||||
|
||||
test("tapErrorRead runs an effect on read failures", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(0),
|
||||
counter => {
|
||||
const lens = Lens.tapErrorRead(
|
||||
Lens.make<number, "read", never, never, never>({
|
||||
get: Effect.fail("read" as const),
|
||||
changes: Stream.fail("read" as const),
|
||||
commit: () => Effect.void,
|
||||
lock: Effect.succeed(identity),
|
||||
}),
|
||||
() => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const),
|
||||
)
|
||||
return Effect.flatMap(
|
||||
Effect.result(Lens.get(lens)),
|
||||
() => SubscriptionRef.get(counter),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
test("tapErrorWrite runs an effect on modify failures", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(0),
|
||||
counter => {
|
||||
const lens = Lens.tapErrorWrite(
|
||||
Lens.make<number, never, "write", never, never>({
|
||||
get: Effect.succeed(1),
|
||||
changes: Stream.make(1),
|
||||
commit: () => Effect.fail("write" as const),
|
||||
lock: Effect.succeed(identity),
|
||||
}),
|
||||
() => SubscriptionRef.modify(counter, n => [void 0, n + 1] as const),
|
||||
)
|
||||
return Effect.flatMap(
|
||||
Effect.result(Lens.set(lens, 2)),
|
||||
() => SubscriptionRef.get(counter),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
test("mapOption transforms Some values and preserves None", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make<Option.Option<number>>(Option.some(42)),
|
||||
parent => {
|
||||
const lens = Lens.mapOption(
|
||||
Lens.fromSubscriptionRef(parent),
|
||||
n => n * 2,
|
||||
(_n, doubled) => doubled / 2,
|
||||
)
|
||||
return Effect.flatMap(
|
||||
Lens.get(lens),
|
||||
value => Effect.flatMap(
|
||||
Lens.set(lens, Option.some(100)),
|
||||
() => Effect.map(SubscriptionRef.get(parent), parentValue => [value, parentValue] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result[0]).toEqual(Option.some(84)) // 42 * 2
|
||||
expect(result[1]).toEqual(Option.some(50)) // 100 / 2
|
||||
})
|
||||
|
||||
test("mapOptionEffect transforms Some values with effects", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make<Option.Option<number>>(Option.some(42)),
|
||||
parent => {
|
||||
const lens = Lens.mapOptionEffect(
|
||||
Lens.fromSubscriptionRef(parent),
|
||||
n => Effect.succeed(n * 2),
|
||||
(_n, doubled) => Effect.succeed(doubled / 2),
|
||||
)
|
||||
return Effect.flatMap(
|
||||
Lens.get(lens),
|
||||
value => Effect.flatMap(
|
||||
Lens.set(lens, Option.some(100)),
|
||||
() => Effect.map(SubscriptionRef.get(parent), parentValue => [value, parentValue] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result[0]).toEqual(Option.some(84)) // 42 * 2
|
||||
expect(result[1]).toEqual(Option.some(50)) // 100 / 2
|
||||
})
|
||||
|
||||
test("provideContext supplies a service to get and modify", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(10),
|
||||
parent => {
|
||||
const lens = Lens.provideContext(
|
||||
Lens.mapEffect(
|
||||
Lens.fromSubscriptionRef(parent),
|
||||
n => Effect.map(Offset, ({ value }) => n + value),
|
||||
(_n, next) => Effect.map(Offset, ({ value }) => next - value),
|
||||
),
|
||||
Context.make(Offset, { value: 5 }),
|
||||
)
|
||||
|
||||
return Effect.flatMap(
|
||||
Lens.get(lens),
|
||||
value => Effect.flatMap(
|
||||
Lens.set(lens, 30),
|
||||
() => Effect.map(SubscriptionRef.get(parent), parentValue => [value, parentValue] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result[0]).toBe(15)
|
||||
expect(result[1]).toBe(25)
|
||||
})
|
||||
|
||||
test("Ref and SynchronizedRef adapters read and update their sources", async () => {
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const ref = yield* Ref.make(1)
|
||||
const refLens = yield* Lens.fromRef(ref)
|
||||
yield* Lens.update(refLens, n => n + 1)
|
||||
|
||||
const synchronizedRef = yield* SynchronizedRef.make(10)
|
||||
const synchronizedLens = Lens.fromSynchronizedRef(synchronizedRef)
|
||||
yield* Lens.updateEffect(synchronizedLens, n => Effect.succeed(n + 5))
|
||||
|
||||
return [yield* Ref.get(ref), yield* SynchronizedRef.get(synchronizedRef)] as const
|
||||
}))
|
||||
|
||||
expect(result).toEqual([2, 15])
|
||||
})
|
||||
|
||||
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 },
|
||||
),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
))
|
||||
|
||||
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), SubscriptionRef.get(parent)] 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(
|
||||
SubscriptionRef.make({ count: 1, label: "original" }),
|
||||
parent => {
|
||||
const countLens = Lens.focusObjectOn(Lens.fromSubscriptionRef(parent), "count")
|
||||
return Effect.flatMap(
|
||||
Lens.get(countLens),
|
||||
count => Effect.flatMap(
|
||||
Lens.set(countLens, count + 5),
|
||||
() => Effect.map(SubscriptionRef.get(parent), state => [count, state] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(initialCount).toBe(1)
|
||||
expect(updatedState).toEqual({ count: 6, label: "original" })
|
||||
})
|
||||
|
||||
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.focusObjectOnWritable(Lens.fromSubscriptionRef(parent), "detail")
|
||||
return Effect.flatMap(
|
||||
Lens.set(detailLens, "mutated"),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toBe(original)
|
||||
expect(updated.detail).toBe("mutated")
|
||||
})
|
||||
|
||||
test("focusArrayAt updates the selected index", async () => {
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([10, 20, 30]),
|
||||
parent => {
|
||||
const elementLens = Lens.focusArrayAt(Lens.fromSubscriptionRef(parent), 1)
|
||||
return Effect.flatMap(
|
||||
Lens.update(elementLens, value => value + 5),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toEqual([10, 25, 30])
|
||||
})
|
||||
|
||||
test("focusMutableArrayAt mutates the array reference in place", async () => {
|
||||
const original = ["foo", "bar"]
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(original),
|
||||
parent => {
|
||||
const elementLens = Lens.focusMutableArrayAt(Lens.fromSubscriptionRef(parent), 0)
|
||||
return Effect.flatMap(
|
||||
Lens.set(elementLens, "baz"),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toBe(original)
|
||||
expect(updated).toEqual(["baz", "bar"])
|
||||
})
|
||||
|
||||
test("focusTupleAt updates the selected tuple index immutably", async () => {
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make<readonly [string, string, string]>(["a", "b", "c"]),
|
||||
parent => {
|
||||
const elementLens = Lens.focusTupleAt(Lens.fromSubscriptionRef(parent), 1)
|
||||
return Effect.flatMap(
|
||||
Lens.set(elementLens, "updated"),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toEqual(["a", "updated", "c"])
|
||||
})
|
||||
|
||||
test("focusMutableTupleAt mutates the tuple reference in place", async () => {
|
||||
const original: [string, string] = ["foo", "bar"]
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(original),
|
||||
parent => {
|
||||
const elementLens = Lens.focusMutableTupleAt(Lens.fromSubscriptionRef(parent), 0)
|
||||
return Effect.flatMap(
|
||||
Lens.set(elementLens, "baz"),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toBe(original)
|
||||
expect(updated).toEqual(["baz", "bar"])
|
||||
})
|
||||
|
||||
test("focusChunkAt replaces the focused chunk element", async () => {
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(Chunk.make(1, 2, 3) as Chunk.Chunk<number>),
|
||||
parent => {
|
||||
const elementLens = Lens.focusChunkAt(Lens.fromSubscriptionRef(parent), 2)
|
||||
return Effect.flatMap(
|
||||
Lens.set(elementLens, 99),
|
||||
() => SubscriptionRef.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(Chunk.toReadonlyArray(updated)).toEqual([1, 2, 99])
|
||||
})
|
||||
|
||||
test("focusOption reads and writes the inner Some value", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make<Option.Option<number>>(Option.some(42)),
|
||||
parent => {
|
||||
const lens = Lens.focusOption(Lens.fromSubscriptionRef(parent))
|
||||
return Effect.flatMap(
|
||||
Lens.get(lens),
|
||||
value => Effect.flatMap(
|
||||
Lens.set(lens, 100),
|
||||
() => Effect.map(SubscriptionRef.get(parent), parentValue => [value, parentValue] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result[0]).toBe(42)
|
||||
expect(result[1]).toEqual(Option.some(100))
|
||||
})
|
||||
|
||||
test("focusOption fails when the parent option is None", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make<Option.Option<number>>(Option.none()),
|
||||
parent => {
|
||||
const lens = Lens.focusOption(Lens.fromSubscriptionRef(parent))
|
||||
return Effect.all([
|
||||
Effect.result(Lens.get(lens)),
|
||||
Effect.result(Lens.set(lens, 100)),
|
||||
SubscriptionRef.get(parent),
|
||||
] as const)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result[0]._tag).toBe("Failure")
|
||||
expect(result[1]._tag).toBe("Failure")
|
||||
expect(result[2]).toEqual(Option.none())
|
||||
})
|
||||
|
||||
test("modify and modifyEffect atomically update and return a result", async () => {
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const parent = yield* SubscriptionRef.make(1)
|
||||
const lens = Lens.fromSubscriptionRef(parent)
|
||||
const previous = yield* Lens.modify(lens, n => [`value:${ n }`, n + 1] as const)
|
||||
const doubled = yield* lens.pipe(Lens.modifyEffect(n => Effect.succeed([n * 2, n + 2] as const)))
|
||||
const current = yield* SubscriptionRef.get(parent)
|
||||
return [previous, doubled, current] as const
|
||||
}))
|
||||
|
||||
expect(result).toEqual(["value:1", 4, 4])
|
||||
})
|
||||
|
||||
test("conditional synchronous operations preserve values on None", async () => {
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const parent = yield* SubscriptionRef.make(1)
|
||||
const lens = Lens.fromSubscriptionRef(parent)
|
||||
const fallback = yield* Lens.modifySome(lens, () => ["fallback", Option.none()] as const)
|
||||
const modified = yield* lens.pipe(Lens.modifySome(n => {
|
||||
n satisfies number
|
||||
return [`value:${ n }`, Option.some(n + 1)] as const
|
||||
}))
|
||||
const previous = yield* lens.pipe(Lens.getAndUpdateSome(n => {
|
||||
n satisfies number
|
||||
return Option.some(n + 1)
|
||||
}))
|
||||
yield* Lens.updateSome(lens, () => Option.none())
|
||||
yield* lens.pipe(Lens.updateSome(n => {
|
||||
n satisfies number
|
||||
return Option.some(n + 1)
|
||||
}))
|
||||
const current = yield* Lens.updateSomeAndGet(lens, () => Option.none())
|
||||
const updated = yield* lens.pipe(Lens.updateSomeAndGet(n => {
|
||||
n satisfies number
|
||||
return Option.some(n + 1)
|
||||
}))
|
||||
return [fallback, modified, previous, current, updated, yield* SubscriptionRef.get(parent)] as const
|
||||
}))
|
||||
|
||||
expect(result).toEqual(["fallback", "value:1", 2, 4, 5, 5])
|
||||
})
|
||||
|
||||
test("conditional effectful operations preserve values on None", async () => {
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const parent = yield* SubscriptionRef.make(10)
|
||||
const lens = Lens.fromSubscriptionRef(parent)
|
||||
const modifyNone: Effect.Effect<string> = Lens.modifySomeEffect(
|
||||
lens,
|
||||
() => Effect.succeed(["fallback", Option.none()] as const),
|
||||
)
|
||||
const fallback = yield* modifyNone
|
||||
const modify: Effect.Effect<string> = lens.pipe(Lens.modifySomeEffect(
|
||||
n => {
|
||||
n satisfies number
|
||||
return Effect.succeed([`value:${ n }`, Option.some(n + 1)] as const)
|
||||
},
|
||||
))
|
||||
const modified = yield* modify
|
||||
const getAndUpdate: Effect.Effect<number> = lens.pipe(Lens.getAndUpdateSomeEffect(n => {
|
||||
n satisfies number
|
||||
return Effect.succeed(Option.some(n + 1))
|
||||
}))
|
||||
const getAndUpdateNone: Effect.Effect<number> = lens.pipe(Lens.getAndUpdateSomeEffect(() => Effect.succeed(Option.none())))
|
||||
const updateNone: Effect.Effect<void> = lens.pipe(Lens.updateSomeEffect(() => Effect.succeed(Option.none())))
|
||||
const update: Effect.Effect<void> = lens.pipe(Lens.updateSomeEffect(n => {
|
||||
n satisfies number
|
||||
return Effect.succeed(Option.some(n + 1))
|
||||
}))
|
||||
const previous = yield* getAndUpdate
|
||||
const unchanged = yield* getAndUpdateNone
|
||||
yield* updateNone
|
||||
yield* update
|
||||
const updateAndGetNone: Effect.Effect<number> = lens.pipe(Lens.updateSomeAndGetEffect(() => Effect.succeed(Option.none())))
|
||||
const current = yield* updateAndGetNone
|
||||
const updated = yield* lens.pipe(Lens.updateSomeAndGetEffect(n => {
|
||||
n satisfies number
|
||||
return Effect.succeed(Option.some(n + 1))
|
||||
}))
|
||||
return [fallback, modified, previous, unchanged, current, updated, yield* SubscriptionRef.get(parent)] as const
|
||||
}))
|
||||
|
||||
expect(result).toEqual(["fallback", "value:10", 11, 12, 13, 14, 14])
|
||||
})
|
||||
|
||||
test("conditional updates do not publish when the next value is None", async () => {
|
||||
const events = await Effect.runPromise(Effect.gen(function*() {
|
||||
const parent = yield* SubscriptionRef.make(0)
|
||||
const lens = Lens.fromSubscriptionRef(parent)
|
||||
const fiber = yield* Effect.forkChild(Stream.runCollect(Stream.take(lens.changes, 2)))
|
||||
|
||||
yield* Effect.yieldNow
|
||||
yield* Lens.updateSome(lens, () => Option.none())
|
||||
yield* Lens.updateSome(lens, n => Option.some(n + 1))
|
||||
|
||||
return yield* Fiber.join(fiber)
|
||||
}))
|
||||
|
||||
expect(events).toEqual([0, 1])
|
||||
})
|
||||
|
||||
// test("changes stream emits updates when lens mutates state", async () => {
|
||||
// const events = await Effect.runPromise(
|
||||
// Effect.flatMap(
|
||||
// SubscriptionRef.make({ count: 0 }),
|
||||
// parent => {
|
||||
// const lens = Lens.mapField(Lens.fromSubscriptionRef(parent), "count")
|
||||
// return Effect.fork(Stream.runCollect(Stream.take(lens.changes, 2))).pipe(
|
||||
// Effect.tap(Lens.set(lens, 1)),
|
||||
// Effect.tap(Lens.set(lens, 1)),
|
||||
// Effect.andThen(Fiber.join),
|
||||
// Effect.map(Chunk.toReadonlyArray),
|
||||
// )
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
|
||||
// expect(events).toEqual([1, 2])
|
||||
// })
|
||||
|
||||
// test("mapped changes stream can derive transformed values", async () => {
|
||||
// const derived = await Effect.runPromise(
|
||||
// Effect.flatMap(
|
||||
// SubscriptionRef.make({ count: 10 }),
|
||||
// parent => {
|
||||
// const lens = Lens.mapField(Lens.fromSubscriptionRef(parent), "count")
|
||||
// const transformed = Stream.map(lens.changes, count => `count:${ count }`)
|
||||
// return Effect.scoped(() => Effect.flatMap(
|
||||
// Effect.forkScoped(Stream.runCollect(Stream.take(transformed, 1))),
|
||||
// fiber => Effect.flatMap(
|
||||
// Lens.set(lens, 42),
|
||||
// () => Effect.join(fiber),
|
||||
// ),
|
||||
// ))
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
|
||||
// expect(derived).toEqual(["count:42"])
|
||||
// })
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(Subscribable.fromSubscriptionRef(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<number>),
|
||||
parent => {
|
||||
const sizeSub = Subscribable.focusChunkSize(Subscribable.fromSubscriptionRef(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(Subscribable.fromSubscriptionRef(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])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
Array,
|
||||
type Cause,
|
||||
Chunk,
|
||||
Effect,
|
||||
Function,
|
||||
Iterable,
|
||||
Option,
|
||||
Pipeable,
|
||||
Predicate,
|
||||
Stream,
|
||||
SubscriptionRef,
|
||||
} from "effect"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/Subscribable")
|
||||
export type TypeId = typeof TypeId
|
||||
|
||||
export interface Subscribable<in out A, in out E = never, in out R = never> extends Pipeable.Pipeable {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}
|
||||
|
||||
class SubscribableImpl<in out A, in out E, in out R>
|
||||
extends Pipeable.Class implements Subscribable<A, E, R> {
|
||||
readonly [TypeId]: TypeId = TypeId
|
||||
|
||||
constructor(
|
||||
readonly get: Effect.Effect<A, E, R>,
|
||||
readonly changes: Stream.Stream<A, E, R>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
export const isSubscribable = (u: unknown): u is Subscribable<unknown, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const make = <A, E, R>(options: {
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}): Subscribable<A, E, R> => new SubscribableImpl(options.get, options.changes)
|
||||
|
||||
export const fromSubscriptionRef = <A>(self: SubscriptionRef.SubscriptionRef<A>): Subscribable<A> => make({
|
||||
get: SubscriptionRef.get(self),
|
||||
changes: SubscriptionRef.changes(self),
|
||||
})
|
||||
|
||||
export const map: {
|
||||
<A, B>(f: (a: NoInfer<A>) => B): <E, R>(self: Subscribable<A, E, R>) => Subscribable<B, E, R>
|
||||
<A, E, R, B>(self: Subscribable<A, E, R>, f: (a: NoInfer<A>) => B): Subscribable<B, E, R>
|
||||
} = Function.dual(2, <A, E, R, B>(self: Subscribable<A, E, R>, f: (a: NoInfer<A>) => B) => make({
|
||||
get: Effect.map(self.get, f),
|
||||
changes: Stream.map(self.changes, f),
|
||||
}))
|
||||
|
||||
export const mapEffect: {
|
||||
<A, B, E2, R2>(f: (a: NoInfer<A>) => Effect.Effect<B, E2, R2>): <E, R>(self: Subscribable<A, E, R>) => Subscribable<B, E | E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: Subscribable<A, E, R>, f: (a: NoInfer<A>) => Effect.Effect<B, E2, R2>): Subscribable<B, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
f: (a: NoInfer<A>) => Effect.Effect<B, E2, R2>,
|
||||
) => make({
|
||||
get: Effect.flatMap(self.get, f),
|
||||
changes: Stream.mapEffect(self.changes, f),
|
||||
}))
|
||||
|
||||
export const unwrap = <A, E, R, E1, R1>(
|
||||
effect: Effect.Effect<Subscribable<A, E, R>, E1, R1>,
|
||||
): Subscribable<A, E | E1, R | R1> => make({
|
||||
get: Effect.flatMap(effect, self => self.get),
|
||||
changes: Stream.unwrap(Effect.map(effect, self => self.changes)),
|
||||
})
|
||||
|
||||
/** Maps over an `Option` value in the `Subscribable`. */
|
||||
export const mapOption: {
|
||||
<A, B>(f: (a: A) => B): <E, R>(self: Subscribable<Option.Option<A>, E, R>) => Subscribable<Option.Option<B>, E, R>
|
||||
<A, B, E, R>(self: Subscribable<Option.Option<A>, E, R>, f: (a: A) => B): Subscribable<Option.Option<B>, E, R>
|
||||
} = Function.dual(2, <A, B, E, R>(self: Subscribable<Option.Option<A>, E, R>, f: (a: A) => B) =>
|
||||
map(self, Option.map(f)),
|
||||
)
|
||||
|
||||
/** Maps over an `Option` value in the `Subscribable` with an Effect. */
|
||||
export const mapOptionEffect: {
|
||||
<A, B, E2, R2>(f: (a: A) => Effect.Effect<B, E2, R2>): <E, R>(self: Subscribable<Option.Option<A>, E, R>) => Subscribable<Option.Option<B>, E | E2, R | R2>
|
||||
<A, B, E, R, E2, R2>(self: Subscribable<Option.Option<A>, E, R>, f: (a: A) => Effect.Effect<B, E2, R2>): Subscribable<Option.Option<B>, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, B, E, R, E2, R2>(
|
||||
self: Subscribable<Option.Option<A>, E, R>,
|
||||
f: (a: A) => Effect.Effect<B, E2, R2>,
|
||||
) => mapEffect(self, Option.match({
|
||||
onSome: a => Effect.map(f(a), Option.some),
|
||||
onNone: () => Effect.succeed(Option.none()),
|
||||
})))
|
||||
|
||||
/** Narrows the focus to a field of an object. */
|
||||
export const focusObjectOn: {
|
||||
<A extends object, K extends keyof A>(key: K): <E, R>(self: Subscribable<A, E, R>) => Subscribable<A[K], E, R>
|
||||
<A extends object, K extends keyof A, E, R>(self: Subscribable<A, E, R>, key: K): Subscribable<A[K], E, R>
|
||||
} = Function.dual(2, <A extends object, K extends keyof A, E, R>(self: Subscribable<A, E, R>, key: K) =>
|
||||
map(self, a => a[key]),
|
||||
)
|
||||
|
||||
/** Narrows the focus to an indexed element of an array. */
|
||||
export const focusArrayAt: {
|
||||
<A extends readonly any[]>(index: number): <E, R>(self: Subscribable<A, E, R>) => Subscribable<A[number], E | Cause.NoSuchElementError, R>
|
||||
<A extends readonly any[], E, R>(self: Subscribable<A, E, R>, index: number): Subscribable<A[number], E | Cause.NoSuchElementError, R>
|
||||
} = Function.dual(2, <A extends readonly any[], E, R>(self: Subscribable<A, E, R>, index: number) =>
|
||||
mapEffect(self, a => Effect.fromOption(Array.get(a, index))),
|
||||
)
|
||||
|
||||
export const focusArrayLength = <A extends readonly any[], E, R>(
|
||||
self: Subscribable<A, E, R>,
|
||||
): Subscribable<number, E, R> => map(self, Array.length)
|
||||
|
||||
/** Narrows the focus to an indexed element of a readonly tuple. */
|
||||
export const focusTupleAt: {
|
||||
<T extends readonly [any, ...any[]], I extends number>(index: I): <E, R>(self: Subscribable<T, E, R>) => Subscribable<T[I], E, R>
|
||||
<T extends readonly [any, ...any[]], I extends number, E, R>(self: Subscribable<T, E, R>, index: I): Subscribable<T[I], E, R>
|
||||
} = Function.dual(2, <T extends readonly [any, ...any[]], I extends number, E, R>(self: Subscribable<T, E, R>, index: I) =>
|
||||
map(self, Array.getUnsafe(index)),
|
||||
)
|
||||
|
||||
/** Narrows the focus to an indexed element of `Chunk`. */
|
||||
export const focusChunkAt: {
|
||||
<A>(index: number): <E, R>(self: Subscribable<Chunk.Chunk<A>, E, R>) => Subscribable<A, E | Cause.NoSuchElementError, R>
|
||||
<A, E, R>(self: Subscribable<Chunk.Chunk<A>, E, R>, index: number): Subscribable<A, E | Cause.NoSuchElementError, R>
|
||||
} = Function.dual(2, <A, E, R>(self: Subscribable<Chunk.Chunk<A>, E, R>, index: number) =>
|
||||
mapEffect(self, chunk => Effect.fromOption(Chunk.get(chunk, index))),
|
||||
)
|
||||
|
||||
export const focusChunkSize = <A, E, R>(
|
||||
self: Subscribable<Chunk.Chunk<A>, E, R>,
|
||||
): Subscribable<number, E, R> => map(self, Chunk.size)
|
||||
|
||||
export const focusIterableSize = <A extends Iterable<any>, E, R>(
|
||||
self: Subscribable<A, E, R>,
|
||||
): Subscribable<number, E, R> => map(self, Iterable.size)
|
||||
@@ -0,0 +1,2 @@
|
||||
export * as Lens from "./Lens.js"
|
||||
export * as Subscribable from "./Subscribable.js"
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"types": ["bun"],
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "NodeNext",
|
||||
// "allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
// "noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
// Build
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
|
||||
"plugins": [
|
||||
{ "name": "@effect/language-service" }
|
||||
]
|
||||
},
|
||||
|
||||
"include": ["./src"],
|
||||
}
|
||||
Reference in New Issue
Block a user