0.2.1 + 2.0.0-beta.0 (#6)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
# Effect Lens
|
||||
|
||||
A Lens type for [Effect](https://effect.website/) to easily manage nested state.
|
||||
|
||||
## Install
|
||||
```
|
||||
npm install effect-lens@beta effect@4.0.0-beta.85
|
||||
yarn add effect-lens@beta effect@4.0.0-beta.85
|
||||
bun add effect-lens@beta effect@4.0.0-beta.85
|
||||
```
|
||||
|
||||
## Peer dependencies
|
||||
- `effect` 4.0.0-beta.85
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
A Lens is an effectful abstraction for focusing on (i.e., getting, subscribing to, setting, or modifying) a specific part of a larger immutable data structure, without losing the surrounding context.
|
||||
|
||||
Picture it as a proxy to a separate data source with a similar API to Effect's `SubscriptionRef`, that can point to
|
||||
a nested part of a data structure (I.E.: a `SubscriptionRef` holds an array of numbers, a Lens can proxy that array
|
||||
or the number at a specific index).
|
||||
|
||||
What makes a Lens effectful is the fact that the proxy logic uses effects, which means reading from or writing to a
|
||||
Lens can fail or have requirements:
|
||||
```typescript
|
||||
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
|
||||
RR, // Requirements for reading
|
||||
RW // Requirements for writing
|
||||
>
|
||||
```
|
||||
|
||||
|
||||
### Creating a Lens
|
||||
|
||||
#### 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 to and writing to that
|
||||
// data source with a similar API to Effect's SubscriptionRef
|
||||
const lens = Lens.fromSubscriptionRef(ref)
|
||||
// ^ Lens.Lens<number[], never, never, never, never>
|
||||
|
||||
const value = yield* Lens.get(lens)
|
||||
yield* Effect.forkScoped(Stream.runForEach(lens.changes, Console.log))
|
||||
yield* Lens.updateEffect(lens, values =>
|
||||
Effect.fromOption(Array.replace(values, 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)
|
||||
|
||||
#### Manually
|
||||
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
|
||||
// \/ Lens<Option.Option<string>, PlatformError, PlatformError, never, never>
|
||||
const lens = Effect.all([
|
||||
KeyValueStore.KeyValueStore,
|
||||
Effect.succeed("someKey"),
|
||||
Semaphore.make(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,
|
||||
),
|
||||
|
||||
commit: a => Option.isSome(a)
|
||||
? kv.set(key, a.value)
|
||||
: kv.remove(key),
|
||||
|
||||
lock: Effect.succeed(semaphore.withPermits(1)),
|
||||
})),
|
||||
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
Lens.unwrap,
|
||||
)
|
||||
```
|
||||
|
||||
Note: while Lens supports asynchronous effects for the proxy logic, we would recommend keeping them synchronous to preserve atomicity.
|
||||
|
||||
|
||||
### Focusing
|
||||
|
||||
Lenses can focus on a nested part of the data type they point to.
|
||||
|
||||
What does this mean? Let's say you have a Lens with this signature:
|
||||
```typescript
|
||||
Lens<{ readonly a: string, readonly b: number }, never, never, never, never>
|
||||
```
|
||||
|
||||
*Focusing this Lens on `a`* means deriving a new Lens that points to the `a` field of the struct the current Lens points to, resulting in a:
|
||||
```typescript
|
||||
Lens<string, never, never, never, never>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
This is a very powerful pattern as it enables you to keep your state in some shared data store while allowing you to pass specific parts of that state to some parts of your application. Very useful for frontend development!
|
||||
|
||||
#### Using built-in transforms
|
||||
We provide a few helpers to create focused Lenses:
|
||||
```typescript
|
||||
interface User {
|
||||
readonly name: string
|
||||
readonly age: DateTime.Utc
|
||||
}
|
||||
|
||||
// The state of your app
|
||||
const ref = yield* SubscriptionRef.make<{
|
||||
readonly users: readonly User[]
|
||||
}>({
|
||||
users: [
|
||||
{ name: "Jean Dupont", age: yield* Effect.fromOption(DateTime.make("03/25/1969")) },
|
||||
{ name: "Juan Joya Borja", age: yield* Effect.fromOption(DateTime.make("04/05/1956")) },
|
||||
{ name: "Benzemonstre", age: yield* Effect.fromOption(DateTime.make("06/12/2000")) },
|
||||
]
|
||||
})
|
||||
|
||||
// \/ Lens<User, NoSuchElementError, NoSuchElementError, never, never>
|
||||
const jeanDupontLens = ref.pipe(
|
||||
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 lens can fail with NoSuchElementError
|
||||
// This is because of Lens.focusArrayAt(0), as reading and writing to an array is an unsafe operation
|
||||
|
||||
const jeanDupont = yield* Lens.get(jeanDupontLens)
|
||||
|
||||
yield* Lens.set(
|
||||
// You can focus even further down
|
||||
Lens.focusObjectOn(jeanDupontLens, "age"),
|
||||
yield* Effect.fromOption(DateTime.make("03/25/1970")),
|
||||
)
|
||||
// Mutations with the parent state are performed immutably by default
|
||||
// unless you use a specific mutable transform such as 'focusObjectOnWritable'
|
||||
```
|
||||
|
||||
Currently available:
|
||||
| Name | Description | Parent state mutation behavior | Notes |
|
||||
| - | - | - | - |
|
||||
| `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 | |
|
||||
| `focusMutableTupleAt` | Focuses to an indexed entry of a mutable tuple. Mutates the parent tuple in place at the focused index | Mutable | Type-safe: will not allow you to mutate `readonly` tuples |
|
||||
| `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 `NoSuchElementError` when the parent option is `None` |
|
||||
|
||||
#### Manually
|
||||
You can create focused Lenses by composing them manually using `map`, `mapEffect` and `unwrap`:
|
||||
```typescript
|
||||
const ref = yield* SubscriptionRef.make<readonly User[]>([
|
||||
{ name: "Jean Dupont", age: yield* Effect.fromOption(DateTime.make("03/25/1969")) },
|
||||
{ name: "Juan Joya Borja", age: yield* Effect.fromOption(DateTime.make("04/05/1956")) },
|
||||
{ name: "Benzemonstre", age: yield* Effect.fromOption(DateTime.make("06/12/2000")) },
|
||||
])
|
||||
|
||||
// \/ Lens<User, NoSuchElementError, NoSuchElementError, never, never>
|
||||
const benzemonstreLens = ref.pipe(
|
||||
Lens.fromSubscriptionRef,
|
||||
|
||||
// Manually focus
|
||||
Lens.mapEffect(
|
||||
// Getter:
|
||||
a => Effect.fromOption(Array.get(a, 2)),
|
||||
// Setter:
|
||||
(a, b) => Effect.fromOption(Array.replace(a, 2, b)),
|
||||
// ^ The current Lens value (readonly User[])
|
||||
// ^ The new focused value to push (User)
|
||||
),
|
||||
)
|
||||
// Both Array.get and Array.replace return an Option, which Effect.fromOption
|
||||
// converts into Effect<A, NoSuchElementError>
|
||||
// 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<User, never, never, never, never>
|
||||
|
||||
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
|
||||
|
||||
Effect 4 no longer provides `Subscribable` and `Readable`. This library provides its own `Subscribable` interface, which you can use as a constraint to allow some parts of your app to only read and subscribe to the Lenses you provide them:
|
||||
```typescript
|
||||
const ref = yield* SubscriptionRef.make<{
|
||||
readonly users: readonly User[]
|
||||
}>({ users: [...] })
|
||||
|
||||
const someFunctionThatShouldOnlyHaveReadonlyAccessToTheState = (
|
||||
usersSub: Subscribable.Subscribable<readonly User[], never, never>
|
||||
) => Effect.gen(function*() {
|
||||
// Do whatever
|
||||
const usersCountSub = Subscribable.map(usersSub, a => a.length)
|
||||
const users = yield* usersSub.get
|
||||
yield* Effect.forkScoped(Stream.runForEach(usersSub.changes, ...))
|
||||
})
|
||||
|
||||
const lens = ref.pipe(
|
||||
Lens.fromSubscriptionRef,
|
||||
Lens.focusObjectOn("users"),
|
||||
)
|
||||
yield* someFunctionThatShouldOnlyHaveReadonlyAccessToTheState(lens)
|
||||
```
|
||||
|
||||
#### Focusing
|
||||
This library provides a `Subscribable` module with value and error transforms, recovery (`catch`, `catchCause`, `orElse`, `orElseSucceed`, and `retry`), and transforms to narrow the focus of `Subscribable`'s, same as Lenses:
|
||||
```typescript
|
||||
import { Subscribable } from "effect-lens"
|
||||
|
||||
declare const sub: Subscribable.Subscribable<readonly { name: string }[], never, never>
|
||||
|
||||
// \/ Subscribable.Subscribable<string, NoSuchElementError, never>
|
||||
const nameSub = sub.pipe(
|
||||
Subscribable.focusArrayAt(1),
|
||||
Subscribable.focusObjectOn("name"),
|
||||
)
|
||||
```
|
||||
|
||||
Currently available:
|
||||
| Name | Description |
|
||||
| - | - |
|
||||
| `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 |
|
||||
@@ -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 to easily manage nested state",
|
||||
"version": "2.0.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,134 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Chunk, Effect, Stream, SubscriptionRef } from "effect"
|
||||
import * as Lens from "./Lens.js"
|
||||
import * as Subscribable from "./Subscribable.js"
|
||||
|
||||
|
||||
describe("Subscribable", () => {
|
||||
test("mapError transforms errors from get and changes", async () => {
|
||||
const source = Subscribable.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const mapped = source.pipe(Subscribable.mapError((error: string) => `mapped:${error}`))
|
||||
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const getError = yield* Effect.flip(mapped.get)
|
||||
const changesError = yield* Effect.flip(Stream.runDrain(mapped.changes))
|
||||
return [getError, changesError]
|
||||
}))
|
||||
|
||||
expect(result).toEqual(["mapped:get", "mapped:changes"])
|
||||
})
|
||||
|
||||
test("tapError observes errors from get and changes", async () => {
|
||||
const observed: Array<string> = []
|
||||
const source = Subscribable.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const tapped = Subscribable.tapError(source, error => Effect.sync(() => observed.push(error)))
|
||||
|
||||
await Effect.runPromise(Effect.gen(function*() {
|
||||
yield* Effect.flip(tapped.get)
|
||||
yield* Effect.flip(Stream.runDrain(tapped.changes))
|
||||
}))
|
||||
|
||||
expect(observed).toEqual(["get", "changes"])
|
||||
})
|
||||
|
||||
test("catch recovers get and changes with the corresponding fallback channel", async () => {
|
||||
const source = Subscribable.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const recovered = source.pipe(Subscribable.catch(() => Subscribable.make({
|
||||
get: Effect.succeed("fallback-get"),
|
||||
changes: Stream.succeed("fallback-changes"),
|
||||
})))
|
||||
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const current = yield* recovered.get
|
||||
const changes = yield* Stream.runCollect(recovered.changes)
|
||||
return [current, Array.from(changes)]
|
||||
}))
|
||||
|
||||
expect(result).toEqual(["fallback-get", ["fallback-changes"]])
|
||||
})
|
||||
|
||||
test("orElseSucceed recovers errors from get and changes", async () => {
|
||||
const source = Subscribable.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const recovered = Subscribable.orElseSucceed(source, () => "fallback")
|
||||
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const current = yield* recovered.get
|
||||
const changes = yield* Stream.runCollect(recovered.changes)
|
||||
return [current, Array.from(changes)]
|
||||
}))
|
||||
|
||||
expect(result).toEqual(["fallback", ["fallback"]])
|
||||
})
|
||||
|
||||
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(Lens.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(Lens.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(Lens.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,246 @@
|
||||
import { Array, type Cause, Chunk, Effect, Function, Iterable, Option, Pipeable, Predicate, type Result, type Schedule, Stream } from "effect"
|
||||
|
||||
|
||||
export const SubscribableTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/Subscribable")
|
||||
export type SubscribableTypeId = typeof SubscribableTypeId
|
||||
|
||||
export interface Subscribable<in out A, in out E = never, in out R = never> extends Pipeable.Pipeable {
|
||||
readonly [SubscribableTypeId]: SubscribableTypeId
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}
|
||||
|
||||
export const isSubscribable = (u: unknown): u is Subscribable<unknown, unknown, unknown> => Predicate.hasProperty(u, SubscribableTypeId)
|
||||
|
||||
|
||||
export const SubscribableImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/SubscribableImpl")
|
||||
export type SubscribableImplTypeId = typeof SubscribableImplTypeId
|
||||
|
||||
export declare namespace SubscribableImpl {
|
||||
export interface Source<in out A, in out E = never, in out R = never> {
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}
|
||||
}
|
||||
|
||||
export class SubscribableImpl<in out A, in out E = never, in out R = never>
|
||||
extends Pipeable.Class implements Subscribable<A, E, R> {
|
||||
readonly [SubscribableTypeId]: SubscribableTypeId = SubscribableTypeId
|
||||
readonly [SubscribableImplTypeId]: SubscribableImplTypeId = SubscribableImplTypeId
|
||||
|
||||
constructor(
|
||||
readonly source: SubscribableImpl.Source<A, E, R>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get get() { return this.source.get }
|
||||
get changes() { return this.source.changes }
|
||||
}
|
||||
|
||||
export const isSubscribableImpl = (u: unknown): u is SubscribableImpl<unknown, unknown, unknown> => Predicate.hasProperty(u, SubscribableImplTypeId)
|
||||
|
||||
export const asSubscribableImpl = <A, E, R>(
|
||||
subscribable: Subscribable<A, E, R>
|
||||
): SubscribableImpl<A, E, R> => {
|
||||
if (!isSubscribableImpl(subscribable))
|
||||
throw new Error("Not a 'SubscribableImpl'")
|
||||
return subscribable as SubscribableImpl<A, E, R>
|
||||
}
|
||||
|
||||
export const make = <A, E, R>(
|
||||
source: SubscribableImpl.Source<A, E, R>
|
||||
): Subscribable<A, E, R> => new SubscribableImpl(source)
|
||||
|
||||
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)),
|
||||
})
|
||||
|
||||
|
||||
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 get() { return Effect.map(self.get, f) },
|
||||
get changes() { return 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 get() { return Effect.flatMap(self.get, f) },
|
||||
get changes() { return Stream.mapEffect(self.changes, f) },
|
||||
}))
|
||||
|
||||
/** Maps over an `Option` value in the `Subscribable`. */
|
||||
export const mapOption: {
|
||||
<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()),
|
||||
})))
|
||||
|
||||
|
||||
/** Maps errors from both the current value and the stream of changes. */
|
||||
export const mapError: {
|
||||
<E, E2>(f: (error: NoInfer<E>) => E2): <A, R>(self: Subscribable<A, E, R>) => Subscribable<A, E2, R>
|
||||
<A, E, R, E2>(self: Subscribable<A, E, R>, f: (error: NoInfer<E>) => E2): Subscribable<A, E2, R>
|
||||
} = Function.dual(2, <A, E, R, E2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
f: (error: NoInfer<E>) => E2,
|
||||
) => make({
|
||||
get get() { return Effect.mapError(self.get, f) },
|
||||
get changes() { return Stream.mapError(self.changes, f) },
|
||||
}))
|
||||
|
||||
/** Runs an effect when either the current value or the stream of changes fails. */
|
||||
export const tapError: {
|
||||
<E, B, E2, R2>(f: (error: NoInfer<E>) => Effect.Effect<B, E2, R2>): <A, R>(self: Subscribable<A, E, R>) => Subscribable<A, E | E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: Subscribable<A, E, R>, f: (error: NoInfer<E>) => Effect.Effect<B, E2, R2>): Subscribable<A, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
f: (error: NoInfer<E>) => Effect.Effect<B, E2, R2>,
|
||||
) => make({
|
||||
get get() { return Effect.tapError(self.get, f) },
|
||||
get changes() { return Stream.tapError(self.changes, f) },
|
||||
}))
|
||||
|
||||
const catch_: {
|
||||
<E, B, E2, R2>(f: (error: NoInfer<E>) => Subscribable<B, E2, R2>): <A, R>(self: Subscribable<A, E, R>) => Subscribable<A | B, E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: Subscribable<A, E, R>, f: (error: NoInfer<E>) => Subscribable<B, E2, R2>): Subscribable<A | B, E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
f: (error: NoInfer<E>) => Subscribable<B, E2, R2>,
|
||||
) => make({
|
||||
get get() { return Effect.catch(self.get, error => f(error).get) },
|
||||
get changes() { return Stream.catch(self.changes, error => f(error).changes) },
|
||||
}))
|
||||
|
||||
/** Recovers from typed errors with another `Subscribable`. */
|
||||
export { catch_ as catch }
|
||||
|
||||
/** Recovers from all failure causes with another `Subscribable`. */
|
||||
export const catchCause: {
|
||||
<E, B, E2, R2>(f: (cause: Cause.Cause<NoInfer<E>>) => Subscribable<B, E2, R2>): <A, R>(self: Subscribable<A, E, R>) => Subscribable<A | B, E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: Subscribable<A, E, R>, f: (cause: Cause.Cause<NoInfer<E>>) => Subscribable<B, E2, R2>): Subscribable<A | B, E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
f: (cause: Cause.Cause<NoInfer<E>>) => Subscribable<B, E2, R2>,
|
||||
) => make({
|
||||
get get() { return Effect.catchCause(self.get, cause => f(cause).get) },
|
||||
get changes() { return Stream.catchCause(self.changes, cause => f(cause).changes) },
|
||||
}))
|
||||
|
||||
/** Runs an effect when either channel fails, exposing the complete failure cause. */
|
||||
export const tapCause: {
|
||||
<E, B, E2, R2>(f: (cause: Cause.Cause<NoInfer<E>>) => Effect.Effect<B, E2, R2>): <A, R>(self: Subscribable<A, E, R>) => Subscribable<A, E | E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: Subscribable<A, E, R>, f: (cause: Cause.Cause<NoInfer<E>>) => Effect.Effect<B, E2, R2>): Subscribable<A, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
f: (cause: Cause.Cause<NoInfer<E>>) => Effect.Effect<B, E2, R2>,
|
||||
) => make({
|
||||
get get() { return Effect.tapCause(self.get, f) },
|
||||
get changes() { return Stream.tapCause(self.changes, f) },
|
||||
}))
|
||||
|
||||
/** Falls back to another `Subscribable` when either channel fails. */
|
||||
export const orElse: {
|
||||
<B, E2, R2>(that: () => Subscribable<B, E2, R2>): <A, E, R>(self: Subscribable<A, E, R>) => Subscribable<A | B, E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: Subscribable<A, E, R>, that: () => Subscribable<B, E2, R2>): Subscribable<A | B, E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
that: () => Subscribable<B, E2, R2>,
|
||||
) => catch_(self, that))
|
||||
|
||||
/** Replaces typed errors from either channel with a lazily evaluated value. */
|
||||
export const orElseSucceed: {
|
||||
<B>(value: () => B): <A, E, R>(self: Subscribable<A, E, R>) => Subscribable<A | B, never, R>
|
||||
<A, E, R, B>(self: Subscribable<A, E, R>, value: () => B): Subscribable<A | B, never, R>
|
||||
} = Function.dual(2, <A, E, R, B>(self: Subscribable<A, E, R>, value: () => B) => make({
|
||||
get get() { return Effect.orElseSucceed(self.get, value) },
|
||||
get changes() { return Stream.orElseSucceed(self.changes, value) },
|
||||
}))
|
||||
|
||||
/** Retries failures from both channels according to the supplied schedule. */
|
||||
export const retry: {
|
||||
<E, X, E2, R2>(policy: Schedule.Schedule<X, NoInfer<E>, E2, R2>): <A, R>(self: Subscribable<A, E, R>) => Subscribable<A, E | E2, R | R2>
|
||||
<A, E, R, X, E2, R2>(self: Subscribable<A, E, R>, policy: Schedule.Schedule<X, NoInfer<E>, E2, R2>): Subscribable<A, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, X, E2, R2>(
|
||||
self: Subscribable<A, E, R>,
|
||||
policy: Schedule.Schedule<X, NoInfer<E>, E2, R2>,
|
||||
) => make({
|
||||
get get() { return Effect.retry(self.get, policy) },
|
||||
get changes() { return Stream.retry(self.changes, policy) },
|
||||
}))
|
||||
|
||||
/** Converts typed failures from both channels into `Result` values. */
|
||||
export const result = <A, E, R>(
|
||||
self: Subscribable<A, E, R>,
|
||||
): Subscribable<Result.Result<A, E>, never, R> => make({
|
||||
get get() { return Effect.result(self.get) },
|
||||
get changes() { return Stream.result(self.changes) },
|
||||
})
|
||||
|
||||
|
||||
/** Narrows the focus to a field of an object. */
|
||||
export const focusObjectOn: {
|
||||
<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