0.2.0 (#5)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -28,7 +28,7 @@ Lens<
|
||||
A, // Type of the value the lens is focused on
|
||||
ER, // Errors that can happen when reading
|
||||
EW, // Errors that can happen when writing
|
||||
RE, // Requirements for reading
|
||||
RR, // Requirements for reading
|
||||
RW // Requirements for writing
|
||||
>
|
||||
```
|
||||
@@ -55,11 +55,14 @@ yield* Lens.update(lens, Array.replace(1, 1664))
|
||||
Currently available:
|
||||
- `fromSubscriptionRef`
|
||||
- `fromSynchronizedRef` (note: since `SynchronizedRef` is not reactive (does not produce a stream of value changes), the resulting Lens' `changes` stream will only emit the current value of the lens when evaluated, and nothing else)
|
||||
|
||||
More to come!
|
||||
- `fromRef` (returns an effect because it creates an internal lock)
|
||||
|
||||
#### Manually
|
||||
You can also create Lenses manually using `make` by providing a getter, a stream of changes and either a `set` or `modify` function depending on your needs.
|
||||
You can also create Lenses manually using `make` by providing:
|
||||
- `get`: an effect that reads the current value,
|
||||
- `changes`: a stream of value changes,
|
||||
- `commit`: an effectful write primitive,
|
||||
- `lock`: an effect that produces the lock used to serialize writes.
|
||||
|
||||
You can get pretty creative! Here's an example of a Lens that points to a specific key of the browser `LocalStorage`:
|
||||
```typescript
|
||||
@@ -67,8 +70,9 @@ You can get pretty creative! Here's an example of a Lens that points to a specif
|
||||
const lens = Effect.all([
|
||||
KeyValueStore.KeyValueStore,
|
||||
Effect.succeed("someKey"),
|
||||
Effect.makeSemaphore(1),
|
||||
]).pipe(
|
||||
Effect.map(([kv, key]) => Lens.make({
|
||||
Effect.map(([kv, key, semaphore]) => Lens.make({
|
||||
get: kv.get(key),
|
||||
|
||||
changes: kv.get(key).pipe(
|
||||
@@ -83,9 +87,11 @@ const lens = Effect.all([
|
||||
Stream.unwrap,
|
||||
),
|
||||
|
||||
set: a => Option.isSome(a)
|
||||
commit: a => Option.isSome(a)
|
||||
? kv.set(key, a.value)
|
||||
: kv.remove(key),
|
||||
|
||||
lock: Effect.succeed(semaphore.withPermits(1)),
|
||||
})),
|
||||
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
@@ -95,20 +101,6 @@ const lens = Effect.all([
|
||||
|
||||
Note: while Lens supports asynchronous effects for the proxy logic, we would recommend keeping them synchronous to preserve atomicity.
|
||||
|
||||
If a `Lens` depends on a service in its environment, you can provide that service directly to the lens:
|
||||
```typescript
|
||||
class Offset extends Context.Tag("Offset")<Offset, { readonly value: number }>() {}
|
||||
|
||||
const root = Lens.fromSubscriptionRef(ref)
|
||||
const offsetLens = Lens.mapEffect(
|
||||
root,
|
||||
n => Effect.map(Offset, ({ value }) => n + value),
|
||||
(_n, next) => Effect.map(Offset, ({ value }) => next - value),
|
||||
)
|
||||
|
||||
const runnableLens = Lens.provide(offsetLens, Offset, { value: 5 })
|
||||
```
|
||||
|
||||
|
||||
### Focusing
|
||||
|
||||
@@ -181,8 +173,6 @@ Currently available:
|
||||
| `focusChunkAt` | Focuses to an indexed entry of a `Chunk`. Replaces the parent `Chunk` immutably when writing to the focused element | Immutable | |
|
||||
| `focusOption` | Focuses to the value inside an `Option`. Wraps writes back into `Option.some` | Immutable | Reading or writing fails with `NoSuchElementException` when the parent option is `None` |
|
||||
|
||||
Also more to come!
|
||||
|
||||
#### Manually
|
||||
You can create focused Lenses by composing them manually using `map`, `mapEffect` and `unwrap`:
|
||||
```typescript
|
||||
@@ -211,6 +201,42 @@ const benzemonstreLens = ref.pipe(
|
||||
// As you can see, this is automatically tracked by the Lens type
|
||||
```
|
||||
|
||||
#### Low-level derived lenses
|
||||
For advanced cases, you can derive a Lens manually using `derive`. This is the primitive used by the built-in transforms.
|
||||
|
||||
A derived Lens describes how to transform three parent channels:
|
||||
- `resolve`: reads the parent and returns the focused value plus a `commit` function to rebuild the parent,
|
||||
- `mapStream`: transforms the parent `changes` stream,
|
||||
- `mapLock`: transforms the parent write lock.
|
||||
|
||||
Most custom focusing logic should use `map` or `mapEffect`, but `derive` is useful when you need full control over read, stream, lock, and write-back behavior.
|
||||
|
||||
```typescript
|
||||
declare const lens: Lens.Lens<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
|
||||
|
||||
@@ -255,8 +281,11 @@ Currently available:
|
||||
| - | - |
|
||||
| `focusObjectOn` | Focuses to the field of an object |
|
||||
| `focusArrayAt` | Focuses to an indexed entry of an array |
|
||||
| `focusArrayLength` | Focuses to the length of an array |
|
||||
| `focusTupleAt` | Focuses to an indexed entry of a tuple |
|
||||
| `focusChunkAt` | Focuses to an indexed entry of a `Chunk` |
|
||||
| `focusChunkSize` | Focuses to the size of a `Chunk` |
|
||||
| `focusIterableSize` | Focuses to the size of an iterable |
|
||||
|
||||
|
||||
## Todo
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "effect-lens",
|
||||
"description": "An effectful Lens type to easily manage nested state",
|
||||
"version": "0.1.5",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
|
||||
@@ -1,11 +1,113 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Chunk, Context, Effect, Option, SubscriptionRef } from "effect"
|
||||
import { Chunk, Context, Effect, Either, identity, Option, Stream, SubscriptionRef } from "effect"
|
||||
import * as Lens from "./Lens.js"
|
||||
|
||||
|
||||
describe("Lens", () => {
|
||||
class Offset extends Context.Tag("Offset")<Offset, { readonly value: number }>() {}
|
||||
|
||||
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.either(Lens.get(lens)))
|
||||
|
||||
expect(result).toEqual(Either.left("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.either(Lens.set(lens, 2)))
|
||||
|
||||
expect(result).toEqual(Either.left("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.either(Lens.get(lens)),
|
||||
Effect.either(Lens.set(lens, 1)),
|
||||
] as const))
|
||||
|
||||
expect(result[0]).toEqual(Either.left("mapped"))
|
||||
expect(result[1]).toEqual(Either.left("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.either(Lens.get(lens)),
|
||||
() => counter.get,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
test("tapErrorWrite runs an effect on modify failures", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(0),
|
||||
counter => {
|
||||
const lens = Lens.tapErrorWrite(
|
||||
Lens.make<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.either(Lens.set(lens, 2)),
|
||||
() => counter.get,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
test("mapOption transforms Some values and preserves None", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
@@ -56,19 +158,18 @@ describe("Lens", () => {
|
||||
expect(result[1]).toEqual(Option.some(50)) // 100 / 2
|
||||
})
|
||||
|
||||
test("provide supplies a service to get and modify", async () => {
|
||||
test("provideContext supplies a service to get and modify", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(10),
|
||||
parent => {
|
||||
const lens = Lens.provide(
|
||||
const lens = Lens.provideContext(
|
||||
Lens.mapEffect(
|
||||
Lens.fromSubscriptionRef(parent),
|
||||
n => Effect.map(Offset, ({ value }) => n + value),
|
||||
(_n, next) => Effect.map(Offset, ({ value }) => next - value),
|
||||
),
|
||||
Offset,
|
||||
{ value: 5 },
|
||||
Context.make(Offset, { value: 5 }),
|
||||
)
|
||||
|
||||
return Effect.flatMap(
|
||||
@@ -86,6 +187,56 @@ describe("Lens", () => {
|
||||
expect(result[1]).toBe(25)
|
||||
})
|
||||
|
||||
test("modifyEffect updates are atomic under concurrency", async () => {
|
||||
const iterations = 100
|
||||
|
||||
const result = await Effect.runPromise(Effect.flatMap(
|
||||
SubscriptionRef.make({ count: 0 }),
|
||||
parent => {
|
||||
const countLens = Lens.focusObjectOn(Lens.fromSubscriptionRef(parent), "count")
|
||||
|
||||
return Effect.flatMap(
|
||||
Effect.forEach(
|
||||
Array.from({ length: iterations }),
|
||||
() => Lens.updateEffect(
|
||||
countLens,
|
||||
count => Effect.as(Effect.yieldNow(), count + 1),
|
||||
),
|
||||
{ concurrency: "unbounded", discard: true },
|
||||
),
|
||||
() => parent.get,
|
||||
)
|
||||
},
|
||||
))
|
||||
|
||||
expect(result.count).toBe(iterations)
|
||||
})
|
||||
|
||||
test("unwrap delegates reads, writes, and locking to the inner lens", async () => {
|
||||
const iterations = 100
|
||||
|
||||
const result = await Effect.runPromise(Effect.flatMap(
|
||||
SubscriptionRef.make(0),
|
||||
parent => {
|
||||
const lens = Lens.unwrap(Effect.succeed(Lens.fromSubscriptionRef(parent)))
|
||||
|
||||
return Effect.flatMap(
|
||||
Effect.forEach(
|
||||
Array.from({ length: iterations }),
|
||||
() => Lens.updateEffect(
|
||||
lens,
|
||||
count => Effect.as(Effect.yieldNow(), count + 1),
|
||||
),
|
||||
{ concurrency: "unbounded", discard: true },
|
||||
),
|
||||
() => Effect.all([Lens.get(lens), parent.get] as const),
|
||||
)
|
||||
},
|
||||
))
|
||||
|
||||
expect(result).toEqual([iterations, iterations])
|
||||
})
|
||||
|
||||
test("focusObjectOn focuses a nested property without touching other fields", async () => {
|
||||
const [initialCount, updatedState] = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
|
||||
+534
-118
@@ -1,4 +1,4 @@
|
||||
import { Array, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, Readable, Stream, type SubscriptionRef, type SynchronizedRef } from "effect"
|
||||
import { Array, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, PubSub, Readable, Ref, Stream, type SubscriptionRef, type SynchronizedRef } from "effect"
|
||||
import type { NoSuchElementException } from "effect/Cause"
|
||||
import * as Subscribable from "./Subscribable.js"
|
||||
|
||||
@@ -17,81 +17,210 @@ export interface Lens<in out A, in out ER = never, in out EW = never, in out RR
|
||||
extends Subscribable.Subscribable<A, ER, RR> {
|
||||
readonly [LensTypeId]: LensTypeId
|
||||
|
||||
readonly modify: <B, E1 = never, R1 = never>(
|
||||
readonly modifyEffect: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.Effect<B, ER | EW | E1, RR | RW | R1>
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal `Lens` implementation.
|
||||
*/
|
||||
export class LensImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
extends Pipeable.Class() implements Lens<A, ER, EW, RR, RW> {
|
||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||
readonly [LensTypeId]: LensTypeId = LensTypeId
|
||||
|
||||
constructor(
|
||||
readonly get: Effect.Effect<A, ER, RR>,
|
||||
readonly changes: Stream.Stream<A, ER, RR>,
|
||||
readonly modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.Effect<B, ER | EW | E1, RR | RW | R1>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether a value is a `Lens`.
|
||||
*/
|
||||
export const isLens = (u: unknown): u is Lens<unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, LensTypeId)
|
||||
|
||||
|
||||
/**
|
||||
* Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations.
|
||||
*
|
||||
* Either `modify` or `set` needs to be supplied.
|
||||
*/
|
||||
export const make = <A, ER, EW, RR, RW>(
|
||||
options: {
|
||||
export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImpl")
|
||||
export type LensImplTypeId = typeof LensImplTypeId
|
||||
|
||||
export declare namespace LensImpl {
|
||||
export interface Resolved<in out A, in out EW = never, in out RW = never> {
|
||||
readonly value: A
|
||||
readonly commit: <E = never, R = never>(
|
||||
next: Effect.Effect<A, E, R>
|
||||
) => Effect.Effect<void, EW | E, RW | R>
|
||||
}
|
||||
|
||||
export interface Lock {
|
||||
<A1, E1, R1>(self: Effect.Effect<A1, E1, R1>): Effect.Effect<A1, E1, R1>
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class LensImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
extends Pipeable.Class() implements Lens<A, ER, EW, RR, RW> {
|
||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||
readonly [LensTypeId]: LensTypeId = LensTypeId
|
||||
readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId
|
||||
|
||||
abstract readonly resolve: Effect.Effect<LensImpl.Resolved<A, EW, RW>, ER, RR>
|
||||
abstract readonly changes: Stream.Stream<A, ER, RR>
|
||||
abstract readonly lock: Effect.Effect<LensImpl.Lock, EW, RW>
|
||||
|
||||
get get() { return Effect.map(this.resolve, resolved => resolved.value) }
|
||||
|
||||
modifyEffect<B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>,
|
||||
): Effect.Effect<B, ER | EW | E1, RR | RW | R1> {
|
||||
return Effect.flatMap(
|
||||
this.lock,
|
||||
lock => lock(Effect.flatMap(
|
||||
this.resolve,
|
||||
resolved => Effect.flatMap(
|
||||
f(resolved.value),
|
||||
([c, next]) => Effect.as(resolved.commit(Effect.succeed(next)), c),
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const isLensImpl = (u: unknown): u is LensImpl<unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, LensImplTypeId)
|
||||
|
||||
export const asLensImpl = <A, ER, EW, RR, RW>(
|
||||
lens: Lens<A, ER, EW, RR, RW>
|
||||
): LensImpl<A, ER, EW, RR, RW> => {
|
||||
if (!isLensImpl(lens))
|
||||
throw new Error("Not a 'LensImpl'")
|
||||
return lens as LensImpl<A, ER, EW, RR, RW>
|
||||
}
|
||||
|
||||
|
||||
export declare namespace LensLazyImpl {
|
||||
export interface Source<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never> {
|
||||
readonly get: Effect.Effect<A, ER, RR>
|
||||
readonly changes: Stream.Stream<A, ER, RR>
|
||||
} & (
|
||||
| {
|
||||
readonly modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.Effect<B, ER | EW | E1, RR | RW | R1>
|
||||
}
|
||||
| { readonly set: (a: A) => Effect.Effect<void, EW, RW> }
|
||||
)
|
||||
): Lens<A, ER, EW, RR, RW> => new LensImpl<A, ER, EW, RR, RW>(
|
||||
options.get,
|
||||
options.changes,
|
||||
Predicate.hasProperty(options, "modify")
|
||||
? options.modify
|
||||
: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.flatMap(
|
||||
options.get,
|
||||
a => Effect.flatMap(f(a), ([b, next]) => Effect.as(options.set(next), b)
|
||||
)),
|
||||
)
|
||||
readonly commit: (a: A) => Effect.Effect<void, EW, RW>
|
||||
readonly lock: Effect.Effect<LensImpl.Lock, EW, RW>
|
||||
}
|
||||
}
|
||||
|
||||
export class LensLazyImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
extends LensImpl<A, ER, EW, RR, RW> {
|
||||
constructor(
|
||||
readonly source: LensLazyImpl.Source<A, ER, EW, RR, RW>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get resolve(): Effect.Effect<LensImpl.Resolved<A, EW, RW>, ER, RR> {
|
||||
return Effect.map(
|
||||
this.source.get,
|
||||
value => ({
|
||||
value,
|
||||
commit: next => Effect.flatMap(next, value => this.source.commit(value)),
|
||||
}),
|
||||
)
|
||||
}
|
||||
get changes() { return this.source.changes }
|
||||
get lock() { return this.source.lock }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `Lens` that proxies a `SubscriptionRef`.
|
||||
* Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations.
|
||||
*/
|
||||
export const fromSubscriptionRef = <A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
): Lens<A, never, never, never, never> => make({
|
||||
get get() { return ref.get },
|
||||
get changes() { return ref.changes },
|
||||
modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => ref.modifyEffect(f),
|
||||
export const make = <A, ER, EW, RR, RW>(
|
||||
source: LensLazyImpl.Source<A, ER, EW, RR, RW>
|
||||
): Lens<A, ER, EW, RR, RW> => new LensLazyImpl(source)
|
||||
|
||||
|
||||
export class UnwrappedLensImpl<in out A, in out ER, in out EW, in out RR, in out RW, in out E1, in out R1>
|
||||
extends LensImpl<A, ER | E1, EW | E1, RR | R1, RW | R1> {
|
||||
constructor(
|
||||
readonly effect: Effect.Effect<Lens<A, ER, EW, RR, RW>, E1, R1>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get resolve(): Effect.Effect<LensImpl.Resolved<A, EW | E1, RW | R1>, ER | E1, RR | R1> {
|
||||
return Effect.map(
|
||||
Effect.flatMap(this.effect, l => asLensImpl(l).resolve),
|
||||
resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => resolved.commit(next),
|
||||
}),
|
||||
)
|
||||
}
|
||||
get changes() { return Stream.unwrap(Effect.map(this.effect, l => l.changes)) }
|
||||
get lock() { return Effect.flatMap(this.effect, l => asLensImpl(l).lock) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens an effectful `Lens`.
|
||||
*/
|
||||
export const unwrap = <A, ER, EW, RR, RW, E1, R1>(
|
||||
effect: Effect.Effect<Lens<A, ER, EW, RR, RW>, E1, R1>
|
||||
): Lens<A, ER | E1, EW | E1, RR | R1, RW | R1> => new UnwrappedLensImpl(effect)
|
||||
|
||||
|
||||
export class RefLensImpl<in out A>
|
||||
extends LensImpl<A, never, never, never, never> {
|
||||
constructor(
|
||||
readonly ref: Ref.Ref<A>,
|
||||
readonly semaphore: Effect.Semaphore,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get resolve(): Effect.Effect<LensImpl.Resolved<A>, never, never> {
|
||||
return Effect.map(
|
||||
Ref.get(this.ref),
|
||||
value => ({
|
||||
value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Ref.set(this.ref, value),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
get changes() { return Stream.unwrap(Effect.map(Ref.get(this.ref), Stream.make)) }
|
||||
get lock() { return Effect.succeed(this.semaphore.withPermits(1)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `Lens` that proxies a `Ref`.
|
||||
*
|
||||
* Note: since `Ref` does not provide any kind of reactivity mechanism, the produced `Lens` will be non-reactive.
|
||||
* This means its `changes` stream will only emit the current value once when evaluated and nothing else.
|
||||
*/
|
||||
export const fromRef = Effect.fnUntraced(function* <A>(
|
||||
ref: Ref.Ref<A>
|
||||
): Effect.fn.Return<Lens<A, never, never, never, never>, never, never> {
|
||||
return new RefLensImpl(ref, yield* Effect.makeSemaphore(1))
|
||||
})
|
||||
|
||||
|
||||
export declare namespace SynchronizedRefLensImpl {
|
||||
export interface SynchronizedRefWithInternals<in out A>
|
||||
extends SynchronizedRef.SynchronizedRef<A> {
|
||||
readonly ref: Ref.Ref<A>
|
||||
readonly withLock: LensImpl.Lock
|
||||
}
|
||||
}
|
||||
|
||||
export class SynchronizedRefLensImpl<in out A>
|
||||
extends LensImpl<A, never, never, never, never> {
|
||||
constructor(
|
||||
readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals<A>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get resolve(): Effect.Effect<LensImpl.Resolved<A>, never, never> {
|
||||
return Effect.map(
|
||||
Ref.get(this.ref.ref),
|
||||
value => ({
|
||||
value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Ref.set(this.ref.ref, value),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
get changes() { return Stream.unwrap(Effect.map(Ref.get(this.ref.ref), Stream.make)) }
|
||||
get lock() { return Effect.succeed(this.ref.withLock) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `Lens` that proxies a `SynchronizedRef`.
|
||||
*
|
||||
@@ -100,26 +229,112 @@ export const fromSubscriptionRef = <A>(
|
||||
*/
|
||||
export const fromSynchronizedRef = <A>(
|
||||
ref: SynchronizedRef.SynchronizedRef<A>
|
||||
): Lens<A, never, never, never, never> => make({
|
||||
get get() { return ref.get },
|
||||
get changes() { return Stream.unwrap(Effect.map(ref.get, Stream.make)) },
|
||||
modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => ref.modifyEffect(f),
|
||||
})
|
||||
): Lens<A, never, never, never, never> => new SynchronizedRefLensImpl(ref as SynchronizedRefLensImpl.SynchronizedRefWithInternals<A>)
|
||||
|
||||
|
||||
export declare namespace SubscriptionRefLensImpl {
|
||||
export interface SubscriptionRefWithInternals<in out A>
|
||||
extends SubscriptionRef.SubscriptionRef<A> {
|
||||
readonly ref: Ref.Ref<A>
|
||||
readonly pubsub: PubSub.PubSub<A>
|
||||
readonly semaphore: Effect.Semaphore
|
||||
}
|
||||
}
|
||||
|
||||
export class SubscriptionRefLensImpl<in out A>
|
||||
extends LensImpl<A, never, never, never, never> {
|
||||
constructor(
|
||||
readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals<A>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get resolve(): Effect.Effect<LensImpl.Resolved<A>, never, never> {
|
||||
return Effect.map(
|
||||
this.ref.get,
|
||||
value => ({
|
||||
value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Effect.zipLeft(
|
||||
Ref.set(this.ref.ref, value),
|
||||
PubSub.publish(this.ref.pubsub, value),
|
||||
),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
get changes() { return this.ref.changes }
|
||||
get lock() { return Effect.succeed(this.ref.semaphore.withPermits(1)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens an effectful `Lens`.
|
||||
* Creates a `Lens` that proxies a `SubscriptionRef`.
|
||||
*/
|
||||
export const unwrap = <A, ER, EW, RR, RW, E1, R1>(
|
||||
effect: Effect.Effect<Lens<A, ER, EW, RR, RW>, E1, R1>
|
||||
): Lens<A, ER | E1, EW | E1, RR | R1, RW | R1> => make({
|
||||
get: Effect.flatMap(effect, l => l.get),
|
||||
changes: Stream.unwrap(Effect.map(effect, l => l.changes)),
|
||||
modify: <B, E2 = never, R2 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E2, R2>
|
||||
) => Effect.flatMap(effect, l => l.modify(f)),
|
||||
})
|
||||
export const fromSubscriptionRef = <A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
): Lens<A, never, never, never, never> => new SubscriptionRefLensImpl(ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals<A>)
|
||||
|
||||
|
||||
export declare namespace DerivedLensImpl {
|
||||
export interface Source<
|
||||
in out A,
|
||||
in out B,
|
||||
in out ER = never,
|
||||
in out ESR = never,
|
||||
in out EW = never,
|
||||
in out ESW = never,
|
||||
in out RR = never,
|
||||
in out RSR = never,
|
||||
in out RW = never,
|
||||
in out RSW = never,
|
||||
> {
|
||||
readonly resolve: (effect: Effect.Effect<LensImpl.Resolved<B, ESW, RSW>, ESR, RSR>) => Effect.Effect<LensImpl.Resolved<A, EW, RW>, ER, RR>
|
||||
readonly mapStream: (stream: Stream.Stream<B, ESR, RSR>) => Stream.Stream<A, ER, RR>
|
||||
readonly mapLock: (lock: Effect.Effect<LensImpl.Lock, ESW, RSW>) => Effect.Effect<LensImpl.Lock, EW, RW>
|
||||
}
|
||||
}
|
||||
|
||||
export class DerivedLensImpl<
|
||||
in out A,
|
||||
in out B,
|
||||
in out ER = never,
|
||||
in out PER = never,
|
||||
in out EW = never,
|
||||
in out PEW = never,
|
||||
in out RR = never,
|
||||
in out PRR = never,
|
||||
in out RW = never,
|
||||
in out PRW = never,
|
||||
>
|
||||
extends LensImpl<A, ER, EW, RR, RW> {
|
||||
constructor(
|
||||
readonly parent: LensImpl<B, PER, PEW, PRR, PRW>,
|
||||
readonly source: DerivedLensImpl.Source<A, B, ER, PER, EW, PEW, RR, PRR, RW, PRW>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get resolve() { return this.source.resolve(this.parent.resolve) }
|
||||
get changes() { return this.source.mapStream(this.parent.changes) }
|
||||
get lock() { return this.source.mapLock(this.parent.lock) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a new `Lens` by linking a step to an existing parent lens.
|
||||
*/
|
||||
export const derive: {
|
||||
<A, B, ER, EW, RR, RW, ER2, EW2, RR2, RW2>(
|
||||
self: Lens<B, ER, EW, RR, RW>,
|
||||
source: DerivedLensImpl.Source<A, B, ER2, ER, EW2, EW, RR2, RR, RW2, RW>,
|
||||
): Lens<A, ER2, EW2, RR2, RW2>
|
||||
<A, B, ER, EW, RR, RW, ER2, EW2, RR2, RW2>(
|
||||
source: DerivedLensImpl.Source<A, B, ER2, ER, EW2, EW, RR2, RR, RW2, RW>,
|
||||
): (self: Lens<B, ER, EW, RR, RW>) => Lens<A, ER2, EW2, RR2, RW2>
|
||||
} = Function.dual(2, <A, B, ER, EW, RR, RW, ER2, EW2, RR2, RW2>(
|
||||
self: Lens<B, ER, EW, RR, RW>,
|
||||
source: DerivedLensImpl.Source<A, B, ER2, ER, EW2, EW, RR2, RR, RW2, RW>,
|
||||
): Lens<A, ER2, EW2, RR2, RW2> => new DerivedLensImpl(asLensImpl(self), source))
|
||||
|
||||
|
||||
/**
|
||||
@@ -139,14 +354,16 @@ export const map: {
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
get: (a: NoInfer<A>) => B,
|
||||
set: (a: NoInfer<A>, b: B) => NoInfer<A>,
|
||||
): Lens<B, ER, EW, RR, RW> => make({
|
||||
get get() { return Effect.map(self.get, get) },
|
||||
get changes() { return Stream.map(self.changes, get) },
|
||||
modify: <C, E1 = never, R1 = never>(
|
||||
f: (b: B) => Effect.Effect<readonly [C, B], E1, R1>
|
||||
) => self.modify(a =>
|
||||
Effect.flatMap(f(get(a)), ([c, next]) => Effect.succeed([c, set(a, next)]))
|
||||
): Lens<B, ER, EW, RR, RW> => derive(self, {
|
||||
resolve: parent => Effect.map(
|
||||
parent,
|
||||
resolved => ({
|
||||
value: get(resolved.value),
|
||||
commit: next => resolved.commit(Effect.map(next, b => set(resolved.value, b))),
|
||||
}),
|
||||
),
|
||||
mapStream: Stream.map(get),
|
||||
mapLock: identity,
|
||||
}))
|
||||
|
||||
/**
|
||||
@@ -166,21 +383,19 @@ export const mapEffect: {
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
get: (a: NoInfer<A>) => Effect.Effect<B, EGet, RGet>,
|
||||
set: (a: NoInfer<A>, b: B) => Effect.Effect<NoInfer<A>, ESet, RSet>,
|
||||
): Lens<B, ER | EGet, EW | ESet, RR | RGet, RW | RSet> => make({
|
||||
get get() { return Effect.flatMap(self.get, get) },
|
||||
get changes() { return Stream.mapEffect(self.changes, get) },
|
||||
modify: <C, E1 = never, R1 = never>(
|
||||
f: (b: B) => Effect.Effect<readonly [C, B], E1, R1>
|
||||
) => self.modify(a => Effect.flatMap(
|
||||
get(a),
|
||||
b => Effect.flatMap(
|
||||
f(b),
|
||||
([c, bNext]) => Effect.flatMap(
|
||||
set(a, bNext),
|
||||
nextA => Effect.succeed([c, nextA] as const),
|
||||
),
|
||||
)
|
||||
)),
|
||||
): Lens<B, ER | EGet, EW | ESet, RR | RGet, RW | RSet> => derive(self, {
|
||||
resolve: parent => Effect.flatMap(
|
||||
parent,
|
||||
resolved => Effect.map(
|
||||
get(resolved.value),
|
||||
value => ({
|
||||
value,
|
||||
commit: next => resolved.commit(Effect.flatMap(next, b => set(resolved.value, b))),
|
||||
}),
|
||||
),
|
||||
),
|
||||
mapStream: Stream.mapEffect(get),
|
||||
mapLock: identity<Effect.Effect<LensImpl.Lock, EW | ESet, RW | RSet>>,
|
||||
}))
|
||||
|
||||
/**
|
||||
@@ -263,16 +478,213 @@ export const mapStream: {
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>) => Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>,
|
||||
): Lens<A, ER, EW, RR, RW> => make({
|
||||
get get() { return self.get },
|
||||
get changes() { return f(self.changes) },
|
||||
get modify() { return self.modify },
|
||||
): Lens<A, ER, EW, RR, RW> => derive(self, {
|
||||
resolve: identity,
|
||||
mapStream: f,
|
||||
mapLock: identity,
|
||||
}))
|
||||
|
||||
|
||||
/**
|
||||
* Transforms read errors of a `Lens`.
|
||||
*
|
||||
* Applies to `get` and `changes` while leaving `modify` unchanged.
|
||||
*/
|
||||
export const mapErrorRead: {
|
||||
<A, ER, EW, RR, RW, E2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER>) => E2,
|
||||
): Lens<A, E2, EW, RR, RW>
|
||||
<A, ER, EW, RR, RW, E2>(
|
||||
f: (error: NoInfer<ER>) => E2,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, E2, EW, RR, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER>) => E2,
|
||||
): Lens<A, E2, EW, RR, RW> => derive(self, {
|
||||
resolve: Effect.mapError(f),
|
||||
mapStream: Stream.mapError(f),
|
||||
mapLock: identity,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Transforms modify errors of a `Lens`.
|
||||
*
|
||||
* Applies to the commit/rebuild portion of `modifyEffect` while leaving failures from the
|
||||
* user-supplied callback unchanged.
|
||||
*/
|
||||
export const mapErrorWrite: {
|
||||
<A, ER, EW, RR, RW, E2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<EW>) => E2,
|
||||
): Lens<A, ER, E2, RR, RW>
|
||||
<A, ER, EW, RR, RW, E2>(
|
||||
f: (error: NoInfer<EW>) => E2,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER, E2, RR, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<EW>) => E2,
|
||||
): Lens<A, ER, E2, RR, RW> => derive(self, {
|
||||
resolve: parent => Effect.map(parent, resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Effect.mapError(resolved.commit(Effect.succeed(value)), f),
|
||||
),
|
||||
})),
|
||||
mapStream: identity,
|
||||
mapLock: Effect.mapError(f),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Transforms all errors of a `Lens`.
|
||||
*
|
||||
* Applies to `get`, `changes`, and the commit/rebuild portion of `modifyEffect` while leaving
|
||||
* failures from the user-supplied callback unchanged.
|
||||
*/
|
||||
export const mapError: {
|
||||
<A, ER, EW, RR, RW, E2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER | EW>) => E2,
|
||||
): Lens<A, E2, E2, RR, RW>
|
||||
<A, ER, EW, RR, RW, E2>(
|
||||
f: (error: NoInfer<ER | EW>) => E2,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, E2, E2, RR, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER | EW>) => E2,
|
||||
): Lens<A, E2, E2, RR, RW> => derive(self, {
|
||||
resolve: parent => Effect.map(
|
||||
Effect.mapError(parent, f),
|
||||
resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Effect.mapError(resolved.commit(Effect.succeed(value)), f),
|
||||
),
|
||||
}),
|
||||
),
|
||||
mapStream: Stream.mapError(f),
|
||||
mapLock: Effect.mapError(f),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Runs an effect when read failures occur.
|
||||
*
|
||||
* Applies to `get` and `changes` while leaving `modify` unchanged.
|
||||
*/
|
||||
export const tapErrorRead: {
|
||||
<A, ER, EW, RR, RW, E2, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER>) => Effect.Effect<unknown, E2, R2>,
|
||||
): Lens<A, ER | E2, EW, RR | R2, RW>
|
||||
<A, ER, EW, RR, RW, E2, R2>(
|
||||
f: (error: NoInfer<ER>) => Effect.Effect<unknown, E2, R2>,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER | E2, EW, RR | R2, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER>) => Effect.Effect<unknown, E2, R2>,
|
||||
): Lens<A, ER | E2, EW, RR | R2, RW> => derive(self, {
|
||||
resolve: Effect.tapError(f),
|
||||
mapStream: Stream.tapError(f),
|
||||
mapLock: identity,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Runs an effect when modify failures occur.
|
||||
*
|
||||
* Applies to the commit/rebuild portion of `modifyEffect` while leaving failures from the
|
||||
* user-supplied callback unchanged.
|
||||
*/
|
||||
export const tapErrorWrite: {
|
||||
<A, ER, EW, RR, RW, E2, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<EW>) => Effect.Effect<unknown, E2, R2>,
|
||||
): Lens<A, ER, EW | E2, RR, RW | R2>
|
||||
<A, ER, EW, RR, RW, E2, R2>(
|
||||
f: (error: NoInfer<EW>) => Effect.Effect<unknown, E2, R2>,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER, EW | E2, RR, RW | R2>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<EW>) => Effect.Effect<unknown, E2, R2>,
|
||||
): Lens<A, ER, EW | E2, RR, RW | R2> => derive(self, {
|
||||
resolve: parent => Effect.map(parent, resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Effect.tapError(resolved.commit(Effect.succeed(value)), f),
|
||||
),
|
||||
})),
|
||||
mapStream: identity,
|
||||
mapLock: Effect.tapError(f),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Runs an effect when any `Lens` failure occurs.
|
||||
*
|
||||
* Applies to `get`, `changes`, and the commit/rebuild portion of `modifyEffect` while leaving
|
||||
* failures from the user-supplied callback unchanged.
|
||||
*/
|
||||
export const tapError: {
|
||||
<A, ER, EW, RR, RW, E2, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER | EW>) => Effect.Effect<unknown, E2, R2>,
|
||||
): Lens<A, ER | E2, EW | E2, RR | R2, RW | R2>
|
||||
<A, ER, EW, RR, RW, E2, R2>(
|
||||
f: (error: NoInfer<ER | EW>) => Effect.Effect<unknown, E2, R2>,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER | E2, EW | E2, RR | R2, RW | R2>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E2, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (error: NoInfer<ER | EW>) => Effect.Effect<unknown, E2, R2>,
|
||||
): Lens<A, ER | E2, EW | E2, RR | R2, RW | R2> => derive(self, {
|
||||
resolve: parent => Effect.map(
|
||||
Effect.tapError(parent, f),
|
||||
resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => Effect.flatMap(
|
||||
next,
|
||||
value => Effect.tapError(resolved.commit(Effect.succeed(value)), f),
|
||||
),
|
||||
}),
|
||||
),
|
||||
mapStream: Stream.tapError(f),
|
||||
mapLock: Effect.tapError(f),
|
||||
}))
|
||||
|
||||
|
||||
/**
|
||||
* Provides a `Context` to a `Lens`, removing it from both the read and write environments.
|
||||
*/
|
||||
export const provideContext: {
|
||||
<A, ER, EW, RR, RW, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
context: Context.Context<R2>,
|
||||
): Lens<A, ER, EW, Exclude<RR, R2>, Exclude<RW, R2>>
|
||||
<R2>(
|
||||
context: Context.Context<R2>,
|
||||
): <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER, EW, Exclude<RR, R2>, Exclude<RW, R2>>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, R2>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
context: Context.Context<R2>,
|
||||
): Lens<A, ER, EW, Exclude<RR, R2>, Exclude<RW, R2>> => derive(self, {
|
||||
resolve: parent => Effect.map(
|
||||
Effect.provide(parent, context),
|
||||
resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => Effect.provide(resolved.commit(next), context),
|
||||
}),
|
||||
),
|
||||
mapStream: Stream.provideSomeContext(context),
|
||||
mapLock: Effect.provide(context),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Provides a single service to a `Lens`, removing it from both the read and write environments.
|
||||
*
|
||||
* This is the `Lens` equivalent of `Effect.provideService`: use it when a lens requires one
|
||||
* `Context.Tag` and you already have the concrete service value.
|
||||
*/
|
||||
export const provide: {
|
||||
export const provideService: {
|
||||
<A, ER, EW, RR, RW, I, S>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
tag: Context.Tag<I, S>,
|
||||
@@ -286,12 +698,16 @@ export const provide: {
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
tag: Context.Tag<I, S>,
|
||||
service: NoInfer<S>,
|
||||
): Lens<A, ER, EW, Exclude<RR, I>, Exclude<RW, I>> => make({
|
||||
get get() { return Effect.provideService(self.get, tag, service) },
|
||||
get changes() { return Stream.provideService(self.changes, tag, service) },
|
||||
modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.provideService(self.modify(f), tag, service),
|
||||
): Lens<A, ER, EW, Exclude<RR, I>, Exclude<RW, I>> => derive(self, {
|
||||
resolve: parent => Effect.map(
|
||||
Effect.provideService(parent, tag, service),
|
||||
resolved => ({
|
||||
value: resolved.value,
|
||||
commit: next => Effect.provideService(resolved.commit(next), tag, service),
|
||||
}),
|
||||
),
|
||||
mapStream: Stream.provideService(tag, service),
|
||||
mapLock: Effect.provideService(tag, service),
|
||||
}))
|
||||
|
||||
|
||||
@@ -478,7 +894,7 @@ export const set: {
|
||||
<A, ER, EW, RR, RW>(value: A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<void, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A): Effect.Effect<void, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A) =>
|
||||
self.modify<void, never, never>(() => Effect.succeed([void 0, value] as const)),
|
||||
self.modifyEffect<void, never, never>(() => Effect.succeed([void 0, value] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -488,7 +904,7 @@ export const getAndSet: {
|
||||
<A, ER, EW, RR, RW>(value: A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A) =>
|
||||
self.modify<A, never, never>(a => Effect.succeed([a, value] as const)),
|
||||
self.modifyEffect<A, never, never>(a => Effect.succeed([a, value] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -498,7 +914,7 @@ export const update: {
|
||||
<A, ER, EW, RR, RW>(f: (a: A) => A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<void, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A): Effect.Effect<void, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A) =>
|
||||
self.modify<void, never, never>(a => Effect.succeed([void 0, f(a)] as const)),
|
||||
self.modifyEffect<void, never, never>(a => Effect.succeed([void 0, f(a)] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -508,7 +924,7 @@ export const updateEffect: {
|
||||
<A, ER, EW, RR, RW, E, R>(f: (a: A) => Effect.Effect<A, E, R>): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<void, ER | EW | E, RR | RW | R>
|
||||
<A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>): Effect.Effect<void, ER | EW | E, RR | RW | R>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>) =>
|
||||
self.modify<void, E, R>(a => Effect.flatMap(
|
||||
self.modifyEffect<void, E, R>(a => Effect.flatMap(
|
||||
f(a),
|
||||
next => Effect.succeed([void 0, next] as const),
|
||||
)),
|
||||
@@ -521,7 +937,7 @@ export const getAndUpdate: {
|
||||
<A, ER, EW, RR, RW>(f: (a: A) => A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A) =>
|
||||
self.modify<A, never, never>(a => Effect.succeed([a, f(a)] as const)),
|
||||
self.modifyEffect<A, never, never>(a => Effect.succeed([a, f(a)] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -531,7 +947,7 @@ export const getAndUpdateEffect: {
|
||||
<A, ER, EW, RR, RW, E, R>(f: (a: A) => Effect.Effect<A, E, R>): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
<A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>): Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>) =>
|
||||
self.modify<A, E, R>(a => Effect.flatMap(
|
||||
self.modifyEffect<A, E, R>(a => Effect.flatMap(
|
||||
f(a),
|
||||
next => Effect.succeed([a, next] as const)
|
||||
)),
|
||||
@@ -544,7 +960,7 @@ export const setAndGet: {
|
||||
<A, ER, EW, RR, RW>(value: A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A) =>
|
||||
self.modify<A, never, never>(() => Effect.succeed([value, value] as const)),
|
||||
self.modifyEffect<A, never, never>(() => Effect.succeed([value, value] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -554,7 +970,7 @@ export const updateAndGet: {
|
||||
<A, ER, EW, RR, RW>(f: (a: A) => A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A) =>
|
||||
self.modify<A, never, never>(a => {
|
||||
self.modifyEffect<A, never, never>(a => {
|
||||
const next = f(a)
|
||||
return Effect.succeed([next, next] as const)
|
||||
}),
|
||||
@@ -567,7 +983,7 @@ export const updateAndGetEffect: {
|
||||
<A, ER, EW, RR, RW, E, R>(f: (a: A) => Effect.Effect<A, E, R>): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
<A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>): Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>) =>
|
||||
self.modify<A, E, R>(a => Effect.flatMap(
|
||||
self.modifyEffect<A, E, R>(a => Effect.flatMap(
|
||||
f(a),
|
||||
next => Effect.succeed([next, next] as const),
|
||||
)),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Chunk, Effect, SubscriptionRef } from "effect"
|
||||
import * as Subscribable from "./Subscribable.js"
|
||||
|
||||
|
||||
describe("Subscribable", () => {
|
||||
test("focusArrayLength reads the current array length and reflects updates", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([1, 2, 3]),
|
||||
parent => {
|
||||
const sizeSub = Subscribable.focusArrayLength(parent)
|
||||
return Effect.flatMap(
|
||||
sizeSub.get,
|
||||
initial => Effect.flatMap(
|
||||
SubscriptionRef.set(parent, [1, 2, 3, 4, 5]),
|
||||
() => Effect.map(sizeSub.get, next => [initial, next] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([3, 5])
|
||||
})
|
||||
|
||||
test("focusChunkSize reads the current chunk size and reflects updates", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(Chunk.make(1, 2) as Chunk.Chunk<number>),
|
||||
parent => {
|
||||
const sizeSub = Subscribable.focusChunkSize(parent)
|
||||
return Effect.flatMap(
|
||||
sizeSub.get,
|
||||
initial => Effect.flatMap(
|
||||
SubscriptionRef.set(parent, Chunk.make(1, 2, 3, 4)),
|
||||
() => Effect.map(sizeSub.get, next => [initial, next] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([2, 4])
|
||||
})
|
||||
|
||||
test("focusIterableSize also works for array values", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([1, 2, 3]),
|
||||
parent => {
|
||||
const sizeSub = Subscribable.focusIterableSize(parent)
|
||||
return Effect.flatMap(
|
||||
sizeSub.get,
|
||||
initial => Effect.flatMap(
|
||||
SubscriptionRef.set(parent, [1, 2, 3, 4, 5]),
|
||||
() => Effect.map(sizeSub.get, next => [initial, next] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([3, 5])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Array, Chunk, Effect, Function, Option, Subscribable } from "effect"
|
||||
import { Array, Chunk, Effect, Function, Iterable, Option, Subscribable } from "effect"
|
||||
import type { NoSuchElementException } from "effect/Cause"
|
||||
|
||||
|
||||
@@ -71,6 +71,13 @@ export const focusArrayAt: {
|
||||
index: number,
|
||||
): Subscribable.Subscribable<A[number], E | NoSuchElementException, R> => Subscribable.mapEffect(self, Array.get(index)))
|
||||
|
||||
/**
|
||||
* Narrows the focus to the length of an array.
|
||||
*/
|
||||
export const focusArrayLength = <A extends readonly any[], E, R>(
|
||||
self: Subscribable.Subscribable<A, E, R>,
|
||||
): Subscribable.Subscribable<number, E, R> => Subscribable.map(self, Array.length)
|
||||
|
||||
/**
|
||||
* Narrows the focus to an indexed element of a readonly tuple.
|
||||
*/
|
||||
@@ -102,3 +109,17 @@ export const focusChunkAt: {
|
||||
self: Subscribable.Subscribable<Chunk.Chunk<A>, E, R>,
|
||||
index: number,
|
||||
): Subscribable.Subscribable<A, E | NoSuchElementException, R> => Subscribable.mapEffect(self, Chunk.get(index)))
|
||||
|
||||
/**
|
||||
* Narrows the focus to the size of a `Chunk`.
|
||||
*/
|
||||
export const focusChunkSize = <A, E, R>(
|
||||
self: Subscribable.Subscribable<Chunk.Chunk<A>, E, R>,
|
||||
): Subscribable.Subscribable<number, E, R> => Subscribable.map(self, Chunk.size)
|
||||
|
||||
/**
|
||||
* Narrows the focus to the size of a `Iterable`.
|
||||
*/
|
||||
export const focusIterableSize = <A, E, R>(
|
||||
self: Subscribable.Subscribable<Iterable<A>, E, R>,
|
||||
): Subscribable.Subscribable<number, E, R> => Subscribable.map(self, Iterable.size)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
// "allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "NodeNext",
|
||||
@@ -25,6 +24,7 @@
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
// Build
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.96.0",
|
||||
"@effect/platform": "^0.96.1",
|
||||
"@effect/platform-browser": "^0.76.0",
|
||||
"effect": "^3.21.0",
|
||||
"@effect/platform-bun": "^0.89.0",
|
||||
"effect": "^3.21.2",
|
||||
"effect-lens": "workspace:*"
|
||||
},
|
||||
"overrides": {
|
||||
"effect": "^3.21.0"
|
||||
"effect": "^3.21.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
import { KeyValueStore } from "@effect/platform"
|
||||
import { BrowserKeyValueStore, BrowserStream } from "@effect/platform-browser"
|
||||
import { Effect, Option, Stream } from "effect"
|
||||
import { Lens } from "effect-lens"
|
||||
// import { KeyValueStore } from "@effect/platform"
|
||||
// import { BrowserKeyValueStore, BrowserStream } from "@effect/platform-browser"
|
||||
// import { Effect, Option, Stream } from "effect"
|
||||
// import { Lens } from "effect-lens"
|
||||
|
||||
|
||||
Effect.gen(function*() {
|
||||
// \/ Lens<Option.Option<string>, PlatformError, PlatformError, never, never>
|
||||
Effect.all([
|
||||
KeyValueStore.KeyValueStore,
|
||||
Effect.succeed("someKey"),
|
||||
]).pipe(
|
||||
Effect.map(([kv, key]) => Lens.make({
|
||||
get: kv.get(key),
|
||||
// Effect.gen(function*() {
|
||||
// // \/ Lens<Option.Option<string>, PlatformError, PlatformError, never, never>
|
||||
// const lens = Effect.all([
|
||||
// KeyValueStore.KeyValueStore,
|
||||
// Effect.succeed("someKey"),
|
||||
// Effect.makeSemaphore(1),
|
||||
// ]).pipe(
|
||||
// Effect.map(([kv, key, semaphore]) => Lens.make({
|
||||
// get: kv.get(key),
|
||||
|
||||
changes: kv.get(key).pipe(
|
||||
Effect.map(Stream.make),
|
||||
Effect.map(a => Stream.concat(
|
||||
a,
|
||||
BrowserStream.fromEventListenerWindow("storage").pipe(
|
||||
Stream.filter(event => event.key === key),
|
||||
Stream.map(event => Option.fromNullable(event.newValue)),
|
||||
),
|
||||
)),
|
||||
Stream.unwrap,
|
||||
),
|
||||
// changes: kv.get(key).pipe(
|
||||
// Effect.map(Stream.make),
|
||||
// Effect.map(a => Stream.concat(
|
||||
// a,
|
||||
// BrowserStream.fromEventListenerWindow("storage").pipe(
|
||||
// Stream.filter(event => event.key === key),
|
||||
// Stream.map(event => Option.fromNullable(event.newValue)),
|
||||
// ),
|
||||
// )),
|
||||
// Stream.unwrap,
|
||||
// ),
|
||||
|
||||
set: a => Option.isSome(a)
|
||||
? kv.set(key, a.value)
|
||||
: kv.remove(key),
|
||||
})),
|
||||
// commit: a => Option.isSome(a)
|
||||
// ? kv.set(key, a.value)
|
||||
// : kv.remove(key),
|
||||
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
Lens.unwrap,
|
||||
)
|
||||
})
|
||||
// lock: Effect.succeed(semaphore.withPermits(1)),
|
||||
// })),
|
||||
|
||||
// Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
// Lens.unwrap,
|
||||
// )
|
||||
|
||||
// console.log(yield* Lens.get(lens))
|
||||
// })
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
// "allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "NodeNext",
|
||||
@@ -25,6 +24,7 @@
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
// Build
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
|
||||
Reference in New Issue
Block a user