0.2.1 + 2.0.0-beta.1 (#7)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"packages/effect-lens": {
|
||||
"name": "effect-lens",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"devDependencies": {
|
||||
"effect": "^3.21.0",
|
||||
},
|
||||
@@ -26,12 +26,12 @@
|
||||
},
|
||||
"packages/effect-lens-next": {
|
||||
"name": "effect-lens-next",
|
||||
"version": "0.1.0-beta.0",
|
||||
"version": "2.0.0-beta.1",
|
||||
"devDependencies": {
|
||||
"effect": "4.0.0-beta.85",
|
||||
"effect": "4.0.0-beta.98",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.85",
|
||||
"effect": "4.0.0-beta.98",
|
||||
},
|
||||
},
|
||||
"packages/example": {
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
|
||||
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||
"toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
|
||||
|
||||
"turbo": ["turbo@2.9.18", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.18", "@turbo/darwin-arm64": "2.9.18", "@turbo/linux-64": "2.9.18", "@turbo/linux-arm64": "2.9.18", "@turbo/windows-64": "2.9.18", "@turbo/windows-arm64": "2.9.18" }, "bin": { "turbo": "bin/turbo" } }, "sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg=="],
|
||||
|
||||
@@ -203,12 +203,14 @@
|
||||
|
||||
"@effect/sql/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
|
||||
|
||||
"effect-lens-next/effect": ["effect@4.0.0-beta.85", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Cjv9YQyv4CiIccmRAQIWAoeESCpCpiuHYY8zb5vqiYs3Ac2yE5RQAnKq7z4Ir/3VJYZb8kxh6rS8czDDnpTdkQ=="],
|
||||
"effect-lens-next/effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="],
|
||||
|
||||
"effect-lens-next/effect/fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||
"effect-lens-next/effect/fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
|
||||
|
||||
"effect-lens-next/effect/msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
||||
|
||||
"effect-lens-next/effect/multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
|
||||
|
||||
"effect-lens-next/effect/fast-check/pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ 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
|
||||
npm install effect-lens@beta effect@4.0.0-beta.98
|
||||
yarn add effect-lens@beta effect@4.0.0-beta.98
|
||||
bun add effect-lens@beta effect@4.0.0-beta.98
|
||||
```
|
||||
|
||||
## Peer dependencies
|
||||
- `effect` 4.0.0-beta.85
|
||||
- `effect` 4.0.0-beta.98
|
||||
|
||||
|
||||
## Quickstart
|
||||
@@ -48,7 +48,7 @@ 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* Effect.forkScoped(Stream.runForEach(Lens.changes(lens), Console.log))
|
||||
yield* Lens.updateEffect(lens, values =>
|
||||
Effect.fromOption(Array.replace(values, 1, 1664))
|
||||
)
|
||||
@@ -64,7 +64,7 @@ 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.
|
||||
- `lock`: an effect that produces the lock used to serialize writes and preserve atomicity.
|
||||
|
||||
You can get pretty creative! Here's an example of a Lens that points to a specific key of the browser `LocalStorage`:
|
||||
```typescript
|
||||
@@ -101,9 +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.
|
||||
|
||||
|
||||
### Focusing
|
||||
|
||||
Lenses can focus on a nested part of the data type they point to.
|
||||
@@ -240,41 +237,44 @@ const nameLens = lens.pipe(
|
||||
```
|
||||
|
||||
|
||||
### Subscribable
|
||||
### View
|
||||
|
||||
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:
|
||||
A `View` is a read-only, reactive view of a value: it lets you read the current value and observe subsequent changes. Every `Lens` is also a `View`, 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. It replaces Effect v3's `Subscribable` module:
|
||||
```typescript
|
||||
const ref = yield* SubscriptionRef.make<{
|
||||
readonly users: readonly User[]
|
||||
}>({ users: [...] })
|
||||
}>({ 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 logUserCount = (users: View.View<readonly User[]>) =>
|
||||
Effect.gen(function*() {
|
||||
const userCount = View.focusArrayLength(users)
|
||||
yield* Console.log(`There are ${yield* View.get(userCount)} users`)
|
||||
yield* Stream.runForEach(
|
||||
View.changes(userCount),
|
||||
count => Console.log(`There are now ${count} users`),
|
||||
)
|
||||
})
|
||||
|
||||
const lens = ref.pipe(
|
||||
const usersLens = ref.pipe(
|
||||
Lens.fromSubscriptionRef,
|
||||
Lens.focusObjectOn("users"),
|
||||
)
|
||||
yield* someFunctionThatShouldOnlyHaveReadonlyAccessToTheState(lens)
|
||||
yield* Effect.forkScoped(logUserCount(usersLens))
|
||||
```
|
||||
|
||||
Use `Lens.asView` when you want to make that read-only boundary explicit.
|
||||
|
||||
#### 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:
|
||||
Views can be focused in the same way as Lenses. The `View` module also provides value and error transforms, plus recovery (`catch`, `catchCause`, `orElse`, `orElseSucceed`, and `retry`):
|
||||
```typescript
|
||||
import { Subscribable } from "effect-lens"
|
||||
import { View } from "effect-lens"
|
||||
|
||||
declare const sub: Subscribable.Subscribable<readonly { name: string }[], never, never>
|
||||
declare const view: View.View<readonly { name: string }[], never, never>
|
||||
|
||||
// \/ Subscribable.Subscribable<string, NoSuchElementError, never>
|
||||
const nameSub = sub.pipe(
|
||||
Subscribable.focusArrayAt(1),
|
||||
Subscribable.focusObjectOn("name"),
|
||||
// \/ View.View<string, NoSuchElementError, never>
|
||||
const nameView = view.pipe(
|
||||
View.focusArrayAt(1),
|
||||
View.focusObjectOn("name"),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "effect-lens-next",
|
||||
"description": "An effectful Lens type to easily manage nested state",
|
||||
"version": "2.0.0-beta.0",
|
||||
"version": "2.0.0-beta.1",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -21,9 +21,9 @@
|
||||
"types": "./dist/Lens.d.ts",
|
||||
"default": "./dist/Lens.js"
|
||||
},
|
||||
"./Subscribable": {
|
||||
"types": "./dist/Subscribable.d.ts",
|
||||
"default": "./dist/Subscribable.js"
|
||||
"./View": {
|
||||
"types": "./dist/View.d.ts",
|
||||
"default": "./dist/View.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -36,9 +36,9 @@
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.85"
|
||||
"effect": "4.0.0-beta.98"
|
||||
},
|
||||
"devDependencies": {
|
||||
"effect": "4.0.0-beta.85"
|
||||
"effect": "4.0.0-beta.98"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ describe("Lens", () => {
|
||||
expect(result[1]).toBe(25)
|
||||
})
|
||||
|
||||
test("Ref and SynchronizedRef adapters read and update their sources", async () => {
|
||||
test("Ref, SynchronizedRef, and SubscriptionRef 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)
|
||||
@@ -195,12 +195,20 @@ describe("Lens", () => {
|
||||
|
||||
const synchronizedRef = yield* SynchronizedRef.make(10)
|
||||
const synchronizedLens = Lens.fromSynchronizedRef(synchronizedRef)
|
||||
yield* Lens.updateEffect(synchronizedLens, n => Effect.succeed(n + 5))
|
||||
yield* Lens.update(synchronizedLens, n => n + 5)
|
||||
|
||||
return [yield* Ref.get(ref), yield* SynchronizedRef.get(synchronizedRef)] as const
|
||||
const subscriptionRef = yield* SubscriptionRef.make(100)
|
||||
const subscriptionLens = Lens.fromSubscriptionRef(subscriptionRef)
|
||||
yield* Lens.update(subscriptionLens, n => n + 2)
|
||||
|
||||
return [
|
||||
yield* Ref.get(ref),
|
||||
yield* SynchronizedRef.get(synchronizedRef),
|
||||
yield* SubscriptionRef.get(subscriptionRef),
|
||||
] as const
|
||||
}))
|
||||
|
||||
expect(result).toEqual([2, 15])
|
||||
expect(result).toEqual([2, 15, 102])
|
||||
})
|
||||
|
||||
test("modifyEffect updates are atomic under concurrency", async () => {
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
import {
|
||||
Array,
|
||||
type Cause,
|
||||
Chunk,
|
||||
type Context,
|
||||
Effect,
|
||||
Function,
|
||||
identity,
|
||||
Option,
|
||||
Pipeable,
|
||||
Predicate,
|
||||
PubSub,
|
||||
Ref,
|
||||
Semaphore,
|
||||
Stream,
|
||||
SubscriptionRef,
|
||||
SynchronizedRef,
|
||||
} from "effect"
|
||||
import * as Subscribable from "./Subscribable.js"
|
||||
import { Array, type Cause, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, PubSub, Ref, Semaphore, Stream, SubscriptionRef, SynchronizedRef } from "effect"
|
||||
import * as View from "./View.js"
|
||||
|
||||
|
||||
export const LensTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/Lens")
|
||||
export const LensTypeId: unique symbol = Symbol.for("@effect-lens/Lens/Lens")
|
||||
export type LensTypeId = typeof LensTypeId
|
||||
|
||||
/**
|
||||
@@ -29,8 +12,8 @@ export type LensTypeId = typeof LensTypeId
|
||||
* 2. a `changes` stream that emits every subsequent update to `A`, and
|
||||
* 3. a `modify` effect that can transform the current value.
|
||||
*/
|
||||
export interface Lens<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
extends Subscribable.Subscribable<A, ER, RR> {
|
||||
export interface Lens<in out A, out ER = never, out EW = never, out RR = never, out RW = never>
|
||||
extends View.View<A, ER, RR> {
|
||||
readonly [LensTypeId]: LensTypeId
|
||||
|
||||
readonly modifyEffect: <B, E1 = never, R1 = never>(
|
||||
@@ -52,7 +35,7 @@ export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/Lens
|
||||
export type LensImplTypeId = typeof LensImplTypeId
|
||||
|
||||
export declare namespace LensImpl {
|
||||
export interface Resolved<in out A, in out EW = never, in out RW = never> {
|
||||
export interface Resolved<in out A, out EW = never, out RW = never> {
|
||||
readonly value: A
|
||||
readonly commit: <E = never, R = never>(
|
||||
next: Effect.Effect<A, E, R>
|
||||
@@ -64,9 +47,9 @@ export declare namespace LensImpl {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class LensImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
export abstract class LensImpl<in out A, out ER = never, out EW = never, out RR = never, out RW = never>
|
||||
extends Pipeable.Class implements Lens<A, ER, EW, RR, RW> {
|
||||
readonly [Subscribable.SubscribableTypeId]: Subscribable.SubscribableTypeId = Subscribable.SubscribableTypeId
|
||||
readonly [View.ViewTypeId]: View.ViewTypeId = View.ViewTypeId
|
||||
readonly [LensTypeId]: LensTypeId = LensTypeId
|
||||
readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId
|
||||
|
||||
@@ -120,13 +103,13 @@ export const asLensImpl = <A, ER, EW, RR, RW>(
|
||||
return lens as LensImpl<A, ER, EW, RR, RW>
|
||||
}
|
||||
|
||||
export const asSubscribable = <A, ER, EW, RR, RW>(
|
||||
export const asView = <A, ER, EW, RR, RW>(
|
||||
lens: Lens<A, ER, EW, RR, RW>
|
||||
): Subscribable.Subscribable<A, ER, RR> => lens
|
||||
): View.View<A, ER, RR> => lens
|
||||
|
||||
|
||||
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> {
|
||||
export interface Source<in out A, out ER = never, out EW = never, out RR = never, out RW = never> {
|
||||
readonly get: Effect.Effect<A, ER, RR>
|
||||
readonly changes: Stream.Stream<A, ER, RR>
|
||||
readonly commit: (a: A) => Effect.Effect<void, EW, RW>
|
||||
@@ -134,7 +117,7 @@ export declare namespace LensLazyImpl {
|
||||
}
|
||||
}
|
||||
|
||||
export class LensLazyImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
export class LensLazyImpl<in out A, out ER = never, out EW = never, out RR = never, out RW = never>
|
||||
extends LensImpl<A, ER, EW, RR, RW> {
|
||||
constructor(
|
||||
readonly source: LensLazyImpl.Source<A, ER, EW, RR, RW>,
|
||||
@@ -163,7 +146,7 @@ export const make = <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>
|
||||
export class UnwrappedLensImpl<in out A, out ER, out EW, out RR, out RW, out E1, 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>
|
||||
@@ -912,6 +895,11 @@ export const focusOption: {
|
||||
*/
|
||||
export const get = <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>): Effect.Effect<A, ER, RR> => self.get
|
||||
|
||||
/**
|
||||
* Returns the stream of changes from a `Lens`.
|
||||
*/
|
||||
export const changes = <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>): Stream.Stream<A, ER, RR> => self.changes
|
||||
|
||||
/**
|
||||
* Atomically modifies the value of a `Lens` and returns a computed result.
|
||||
*/
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
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)
|
||||
+42
-19
@@ -1,16 +1,16 @@
|
||||
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"
|
||||
import * as View from "./View.js"
|
||||
|
||||
|
||||
describe("Subscribable", () => {
|
||||
describe("View", () => {
|
||||
test("mapError transforms errors from get and changes", async () => {
|
||||
const source = Subscribable.make({
|
||||
const source = View.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const mapped = source.pipe(Subscribable.mapError((error: string) => `mapped:${error}`))
|
||||
const mapped = source.pipe(View.mapError((error: string) => `mapped:${error}`))
|
||||
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const getError = yield* Effect.flip(mapped.get)
|
||||
@@ -23,11 +23,11 @@ describe("Subscribable", () => {
|
||||
|
||||
test("tapError observes errors from get and changes", async () => {
|
||||
const observed: Array<string> = []
|
||||
const source = Subscribable.make({
|
||||
const source = View.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const tapped = Subscribable.tapError(source, error => Effect.sync(() => observed.push(error)))
|
||||
const tapped = View.tapError(source, error => Effect.sync(() => observed.push(error)))
|
||||
|
||||
await Effect.runPromise(Effect.gen(function*() {
|
||||
yield* Effect.flip(tapped.get)
|
||||
@@ -38,11 +38,11 @@ describe("Subscribable", () => {
|
||||
})
|
||||
|
||||
test("catch recovers get and changes with the corresponding fallback channel", async () => {
|
||||
const source = Subscribable.make({
|
||||
const source = View.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const recovered = source.pipe(Subscribable.catch(() => Subscribable.make({
|
||||
const recovered = source.pipe(View.catch(() => View.make({
|
||||
get: Effect.succeed("fallback-get"),
|
||||
changes: Stream.succeed("fallback-changes"),
|
||||
})))
|
||||
@@ -57,11 +57,11 @@ describe("Subscribable", () => {
|
||||
})
|
||||
|
||||
test("orElseSucceed recovers errors from get and changes", async () => {
|
||||
const source = Subscribable.make({
|
||||
const source = View.make({
|
||||
get: Effect.fail("get"),
|
||||
changes: Stream.fail("changes"),
|
||||
})
|
||||
const recovered = Subscribable.orElseSucceed(source, () => "fallback")
|
||||
const recovered = View.orElseSucceed(source, () => "fallback")
|
||||
|
||||
const result = await Effect.runPromise(Effect.gen(function*() {
|
||||
const current = yield* recovered.get
|
||||
@@ -72,17 +72,40 @@ describe("Subscribable", () => {
|
||||
expect(result).toEqual(["fallback", ["fallback"]])
|
||||
})
|
||||
|
||||
test("zipLatestAll combines current values and change streams", async () => {
|
||||
const zipped = View.zipLatestAll(
|
||||
View.make({
|
||||
get: Effect.succeed(1),
|
||||
changes: Stream.succeed(2),
|
||||
}),
|
||||
View.make({
|
||||
get: Effect.succeed("one"),
|
||||
changes: Stream.succeed("two"),
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(Effect.all([
|
||||
zipped.get,
|
||||
Stream.runCollect(zipped.changes),
|
||||
]))
|
||||
|
||||
expect([result[0], Array.from(result[1])]).toEqual([
|
||||
[1, "one"],
|
||||
[[2, "two"]],
|
||||
])
|
||||
})
|
||||
|
||||
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))
|
||||
const sizeView = View.focusArrayLength(Lens.fromSubscriptionRef(parent))
|
||||
return Effect.flatMap(
|
||||
sizeSub.get,
|
||||
sizeView.get,
|
||||
initial => Effect.flatMap(
|
||||
SubscriptionRef.set(parent, [1, 2, 3, 4, 5]),
|
||||
() => Effect.map(sizeSub.get, next => [initial, next] as const),
|
||||
() => Effect.map(sizeView.get, next => [initial, next] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -97,12 +120,12 @@ describe("Subscribable", () => {
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(Chunk.make(1, 2) as Chunk.Chunk<number>),
|
||||
parent => {
|
||||
const sizeSub = Subscribable.focusChunkSize(Lens.fromSubscriptionRef(parent))
|
||||
const sizeView = View.focusChunkSize(Lens.fromSubscriptionRef(parent))
|
||||
return Effect.flatMap(
|
||||
sizeSub.get,
|
||||
sizeView.get,
|
||||
initial => Effect.flatMap(
|
||||
SubscriptionRef.set(parent, Chunk.make(1, 2, 3, 4)),
|
||||
() => Effect.map(sizeSub.get, next => [initial, next] as const),
|
||||
() => Effect.map(sizeView.get, next => [initial, next] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -117,12 +140,12 @@ describe("Subscribable", () => {
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([1, 2, 3]),
|
||||
parent => {
|
||||
const sizeSub = Subscribable.focusIterableSize(Lens.fromSubscriptionRef(parent))
|
||||
const sizeView = View.focusIterableSize(Lens.fromSubscriptionRef(parent))
|
||||
return Effect.flatMap(
|
||||
sizeSub.get,
|
||||
sizeView.get,
|
||||
initial => Effect.flatMap(
|
||||
SubscriptionRef.set(parent, [1, 2, 3, 4, 5]),
|
||||
() => Effect.map(sizeSub.get, next => [initial, next] as const),
|
||||
() => Effect.map(sizeView.get, next => [initial, next] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -0,0 +1,292 @@
|
||||
import { Array, type Cause, Chunk, Effect, Function, Iterable, Option, Pipeable, Predicate, type Result, type Schedule, Stream } from "effect"
|
||||
|
||||
|
||||
export const ViewTypeId: unique symbol = Symbol.for("@effect-lens/View/View")
|
||||
export type ViewTypeId = typeof ViewTypeId
|
||||
|
||||
export interface View<out A, out E = never, out R = never> extends Pipeable.Pipeable {
|
||||
readonly [ViewTypeId]: ViewTypeId
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}
|
||||
|
||||
export const isView = (u: unknown): u is View<unknown, unknown, unknown> => Predicate.hasProperty(u, ViewTypeId)
|
||||
|
||||
|
||||
export const ViewImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/v4/ViewImpl")
|
||||
export type ViewImplTypeId = typeof ViewImplTypeId
|
||||
|
||||
export declare namespace ViewImpl {
|
||||
export interface Source<out A, out E = never, out R = never> {
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewImpl<out A, out E = never, out R = never>
|
||||
extends Pipeable.Class implements View<A, E, R> {
|
||||
readonly [ViewTypeId]: ViewTypeId = ViewTypeId
|
||||
readonly [ViewImplTypeId]: ViewImplTypeId = ViewImplTypeId
|
||||
|
||||
constructor(
|
||||
readonly source: ViewImpl.Source<A, E, R>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get get() { return this.source.get }
|
||||
get changes() { return this.source.changes }
|
||||
}
|
||||
|
||||
export const isViewImpl = (u: unknown): u is ViewImpl<unknown, unknown, unknown> => Predicate.hasProperty(u, ViewImplTypeId)
|
||||
|
||||
export const asViewImpl = <A, E, R>(
|
||||
view: View<A, E, R>
|
||||
): ViewImpl<A, E, R> => {
|
||||
if (!isViewImpl(view))
|
||||
throw new Error("Not a 'ViewImpl'")
|
||||
return view as ViewImpl<A, E, R>
|
||||
}
|
||||
|
||||
export const make = <A, E, R>(
|
||||
source: ViewImpl.Source<A, E, R>
|
||||
): View<A, E, R> => new ViewImpl(source)
|
||||
|
||||
export const unwrap = <A, E, R, E1, R1>(
|
||||
effect: Effect.Effect<View<A, E, R>, E1, R1>,
|
||||
): View<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: View<A, E, R>) => View<B, E, R>
|
||||
<A, E, R, B>(self: View<A, E, R>, f: (a: NoInfer<A>) => B): View<B, E, R>
|
||||
} = Function.dual(2, <A, E, R, B>(self: View<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: View<A, E, R>) => View<B, E | E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: View<A, E, R>, f: (a: NoInfer<A>) => Effect.Effect<B, E2, R2>): View<B, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: View<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 `View`. */
|
||||
export const mapOption: {
|
||||
<A, B>(f: (a: A) => B): <E, R>(self: View<Option.Option<A>, E, R>) => View<Option.Option<B>, E, R>
|
||||
<A, B, E, R>(self: View<Option.Option<A>, E, R>, f: (a: A) => B): View<Option.Option<B>, E, R>
|
||||
} = Function.dual(2, <A, B, E, R>(self: View<Option.Option<A>, E, R>, f: (a: A) => B) =>
|
||||
map(self, Option.map(f)),
|
||||
)
|
||||
|
||||
/** Maps over an `Option` value in the `View` with an Effect. */
|
||||
export const mapOptionEffect: {
|
||||
<A, B, E2, R2>(f: (a: A) => Effect.Effect<B, E2, R2>): <E, R>(self: View<Option.Option<A>, E, R>) => View<Option.Option<B>, E | E2, R | R2>
|
||||
<A, B, E, R, E2, R2>(self: View<Option.Option<A>, E, R>, f: (a: A) => Effect.Effect<B, E2, R2>): View<Option.Option<B>, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, B, E, R, E2, R2>(
|
||||
self: View<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()),
|
||||
})))
|
||||
|
||||
/**
|
||||
* Allows transforming only the `changes` stream of a `View`.
|
||||
*/
|
||||
export const mapStream: {
|
||||
<A, E, R>(
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>) => Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>,
|
||||
): (self: View<A, E, R>) => View<A, E, R>
|
||||
<A, E, R>(
|
||||
self: View<A, E, R>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>) => Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>,
|
||||
): View<A, E, R>
|
||||
} = Function.dual(2, <A, E, R>(
|
||||
self: View<A, E, R>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>) => Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>,
|
||||
): View<A, E, R> => make({
|
||||
get get() { return self.get },
|
||||
get changes() { return f(self.changes) },
|
||||
}))
|
||||
|
||||
/** Converts typed failures from both channels into `Result` values. */
|
||||
export const result = <A, E, R>(
|
||||
self: View<A, E, R>,
|
||||
): View<Result.Result<A, E>, never, R> => make({
|
||||
get get() { return Effect.result(self.get) },
|
||||
get changes() { return Stream.result(self.changes) },
|
||||
})
|
||||
|
||||
/**
|
||||
* Combines the current values and streams of changes from multiple `View` values.
|
||||
*/
|
||||
export const zipLatestAll = <const T extends readonly View<any, any, any>[]>(
|
||||
...elements: T
|
||||
): View<
|
||||
[T[number]] extends [never]
|
||||
? never
|
||||
: { [K in keyof T]: T[K] extends View<infer A, infer _E, infer _R> ? A : never },
|
||||
[T[number]] extends [never] ? never : T[number] extends View<infer _A, infer E, infer _R> ? E : never,
|
||||
[T[number]] extends [never] ? never : T[number] extends View<infer _A, infer _E, infer R> ? R : never
|
||||
> => make({
|
||||
get: Effect.all(elements.map(view => view.get)),
|
||||
changes: Stream.zipLatestAll(...elements.map(view => view.changes)),
|
||||
}) as any
|
||||
|
||||
|
||||
/** 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: View<A, E, R>) => View<A, E2, R>
|
||||
<A, E, R, E2>(self: View<A, E, R>, f: (error: NoInfer<E>) => E2): View<A, E2, R>
|
||||
} = Function.dual(2, <A, E, R, E2>(
|
||||
self: View<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: View<A, E, R>) => View<A, E | E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: View<A, E, R>, f: (error: NoInfer<E>) => Effect.Effect<B, E2, R2>): View<A, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: View<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>) => View<B, E2, R2>): <A, R>(self: View<A, E, R>) => View<A | B, E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: View<A, E, R>, f: (error: NoInfer<E>) => View<B, E2, R2>): View<A | B, E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: View<A, E, R>,
|
||||
f: (error: NoInfer<E>) => View<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 `View`. */
|
||||
export { catch_ as catch }
|
||||
|
||||
/** Recovers from all failure causes with another `View`. */
|
||||
export const catchCause: {
|
||||
<E, B, E2, R2>(f: (cause: Cause.Cause<NoInfer<E>>) => View<B, E2, R2>): <A, R>(self: View<A, E, R>) => View<A | B, E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: View<A, E, R>, f: (cause: Cause.Cause<NoInfer<E>>) => View<B, E2, R2>): View<A | B, E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: View<A, E, R>,
|
||||
f: (cause: Cause.Cause<NoInfer<E>>) => View<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: View<A, E, R>) => View<A, E | E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: View<A, E, R>, f: (cause: Cause.Cause<NoInfer<E>>) => Effect.Effect<B, E2, R2>): View<A, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: View<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 `View` when either channel fails. */
|
||||
export const orElse: {
|
||||
<B, E2, R2>(that: () => View<B, E2, R2>): <A, E, R>(self: View<A, E, R>) => View<A | B, E2, R | R2>
|
||||
<A, E, R, B, E2, R2>(self: View<A, E, R>, that: () => View<B, E2, R2>): View<A | B, E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, B, E2, R2>(
|
||||
self: View<A, E, R>,
|
||||
that: () => View<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: View<A, E, R>) => View<A | B, never, R>
|
||||
<A, E, R, B>(self: View<A, E, R>, value: () => B): View<A | B, never, R>
|
||||
} = Function.dual(2, <A, E, R, B>(self: View<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: View<A, E, R>) => View<A, E | E2, R | R2>
|
||||
<A, E, R, X, E2, R2>(self: View<A, E, R>, policy: Schedule.Schedule<X, NoInfer<E>, E2, R2>): View<A, E | E2, R | R2>
|
||||
} = Function.dual(2, <A, E, R, X, E2, R2>(
|
||||
self: View<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) },
|
||||
}))
|
||||
|
||||
|
||||
/** Narrows the focus to a field of an object. */
|
||||
export const focusObjectOn: {
|
||||
<A extends object, K extends keyof A>(key: K): <E, R>(self: View<A, E, R>) => View<A[K], E, R>
|
||||
<A extends object, K extends keyof A, E, R>(self: View<A, E, R>, key: K): View<A[K], E, R>
|
||||
} = Function.dual(2, <A extends object, K extends keyof A, E, R>(self: View<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: View<A, E, R>) => View<A[number], E | Cause.NoSuchElementError, R>
|
||||
<A extends readonly any[], E, R>(self: View<A, E, R>, index: number): View<A[number], E | Cause.NoSuchElementError, R>
|
||||
} = Function.dual(2, <A extends readonly any[], E, R>(self: View<A, E, R>, index: number) =>
|
||||
mapEffect(self, a => Effect.fromOption(Array.get(a, index))),
|
||||
)
|
||||
|
||||
export const focusArrayLength = <A extends readonly any[], E, R>(
|
||||
self: View<A, E, R>,
|
||||
): View<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: View<T, E, R>) => View<T[I], E, R>
|
||||
<T extends readonly [any, ...any[]], I extends number, E, R>(self: View<T, E, R>, index: I): View<T[I], E, R>
|
||||
} = Function.dual(2, <T extends readonly [any, ...any[]], I extends number, E, R>(self: View<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: View<Chunk.Chunk<A>, E, R>) => View<A, E | Cause.NoSuchElementError, R>
|
||||
<A, E, R>(self: View<Chunk.Chunk<A>, E, R>, index: number): View<A, E | Cause.NoSuchElementError, R>
|
||||
} = Function.dual(2, <A, E, R>(self: View<Chunk.Chunk<A>, E, R>, index: number) =>
|
||||
mapEffect(self, chunk => Effect.fromOption(Chunk.get(chunk, index))),
|
||||
)
|
||||
|
||||
export const focusChunkSize = <A, E, R>(
|
||||
self: View<Chunk.Chunk<A>, E, R>,
|
||||
): View<number, E, R> => map(self, Chunk.size)
|
||||
|
||||
export const focusIterableSize = <A extends Iterable<any>, E, R>(
|
||||
self: View<A, E, R>,
|
||||
): View<number, E, R> => map(self, Iterable.size)
|
||||
|
||||
|
||||
/**
|
||||
* Reads the current value from a `View`.
|
||||
*/
|
||||
export const get = <A, E, R>(self: View<A, E, R>): Effect.Effect<A, E, R> => self.get
|
||||
|
||||
/**
|
||||
* Returns the stream of changes from a `View`.
|
||||
*/
|
||||
export const changes = <A, E, R>(self: View<A, E, R>): Stream.Stream<A, E, R> => self.changes
|
||||
@@ -1,2 +1,2 @@
|
||||
export * as Lens from "./Lens.js"
|
||||
export * as Subscribable from "./Subscribable.js"
|
||||
export * as View from "./View.js"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A Lens type for [Effect](https://effect.website/) to easily manage nested state.
|
||||
|
||||
This version is for Effect v3. For Effect v4, use version 2.X.X-beta.X.
|
||||
This version is for Effect v3. For Effect v4, use the [2.0.0 beta](https://www.npmjs.com/package/effect-lens/v/2.0.0-beta.1).
|
||||
|
||||
## Install
|
||||
```
|
||||
@@ -64,7 +64,7 @@ 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.
|
||||
- `lock`: an effect that produces the lock used to serialize writes and preserve atomicity.
|
||||
|
||||
You can get pretty creative! Here's an example of a Lens that points to a specific key of the browser `LocalStorage`:
|
||||
```typescript
|
||||
@@ -101,9 +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.
|
||||
|
||||
|
||||
### Focusing
|
||||
|
||||
Lenses can focus on a nested part of the data type they point to.
|
||||
@@ -242,30 +239,31 @@ const nameLens = lens.pipe(
|
||||
|
||||
### Subscribable
|
||||
|
||||
Lens implements both Effect's `Subscribable` and `Readable`, 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:
|
||||
Effect's `Subscribable` is a read-only, reactive view of a value: it lets you read the current value and observe subsequent changes. Every `Lens` implements both `Subscribable` and `Readable`, which you can use as constraints 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: [...] })
|
||||
}>({ 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 logUserCount = (users: Subscribable.Subscribable<readonly User[]>) =>
|
||||
Effect.gen(function*() {
|
||||
const userCount = Subscribable.focusArrayLength(users)
|
||||
yield* Console.log(`There are ${yield* userCount.get} users`)
|
||||
yield* Stream.runForEach(
|
||||
userCount.changes,
|
||||
count => Console.log(`There are now ${count} users`),
|
||||
)
|
||||
})
|
||||
|
||||
const lens = ref.pipe(
|
||||
const usersLens = ref.pipe(
|
||||
Lens.fromSubscriptionRef,
|
||||
Lens.focusObjectOn("users"),
|
||||
)
|
||||
yield* someFunctionThatShouldOnlyHaveReadonlyAccessToTheState(lens)
|
||||
yield* Effect.forkScoped(logUserCount(usersLens))
|
||||
```
|
||||
|
||||
#### Focusing
|
||||
This library re-exports Effect's `Subscribable` module and adds error transforms and recovery (`catchAll`, `catchAllCause`, `orElse`, `orElseSucceed`, and `retry`), plus transforms to narrow the focus of `Subscribable`'s, same as Lenses:
|
||||
Subscribables can be focused in the same way as Lenses. This library re-exports Effect's `Subscribable` module and adds value and error transforms, plus recovery (`catchAll`, `catchAllCause`, `orElse`, `orElseSucceed`, and `retry`):
|
||||
```typescript
|
||||
import { Subscribable } from "effect-lens"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "effect-lens",
|
||||
"description": "An effectful Lens type to easily manage nested state",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
|
||||
@@ -13,7 +13,7 @@ export type LensTypeId = typeof LensTypeId
|
||||
* 2. a `changes` stream that emits every subsequent update to `A`, and
|
||||
* 3. a `modify` effect that can transform the current value.
|
||||
*/
|
||||
export interface Lens<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
export interface Lens<in out A, out ER = never, out EW = never, out RR = never, out RW = never>
|
||||
extends Subscribable.Subscribable<A, ER, RR> {
|
||||
readonly [LensTypeId]: LensTypeId
|
||||
|
||||
@@ -32,7 +32,7 @@ export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImp
|
||||
export type LensImplTypeId = typeof LensImplTypeId
|
||||
|
||||
export declare namespace LensImpl {
|
||||
export interface Resolved<in out A, in out EW = never, in out RW = never> {
|
||||
export interface Resolved<in out A, out EW = never, out RW = never> {
|
||||
readonly value: A
|
||||
readonly commit: <E = never, R = never>(
|
||||
next: Effect.Effect<A, E, R>
|
||||
@@ -44,7 +44,7 @@ export declare namespace LensImpl {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class LensImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
export abstract class LensImpl<in out A, out ER = never, out EW = never, out RR = never, 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
|
||||
@@ -89,7 +89,7 @@ export const asSubscribable = <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> {
|
||||
export interface Source<in out A, out ER = never, out EW = never, out RR = never, out RW = never> {
|
||||
readonly get: Effect.Effect<A, ER, RR>
|
||||
readonly changes: Stream.Stream<A, ER, RR>
|
||||
readonly commit: (a: A) => Effect.Effect<void, EW, RW>
|
||||
@@ -97,7 +97,7 @@ export declare namespace LensLazyImpl {
|
||||
}
|
||||
}
|
||||
|
||||
export class LensLazyImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
export class LensLazyImpl<in out A, out ER = never, out EW = never, out RR = never, out RW = never>
|
||||
extends LensImpl<A, ER, EW, RR, RW> {
|
||||
constructor(
|
||||
readonly source: LensLazyImpl.Source<A, ER, EW, RR, RW>,
|
||||
@@ -126,7 +126,7 @@ export const make = <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>
|
||||
export class UnwrappedLensImpl<in out A, out ER, out EW, out RR, out RW, out E1, 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>
|
||||
|
||||
@@ -71,6 +71,29 @@ describe("Subscribable", () => {
|
||||
expect(result).toEqual(["fallback", ["fallback"]])
|
||||
})
|
||||
|
||||
test("zipLatestAll combines current values and change streams", async () => {
|
||||
const zipped = Subscribable.zipLatestAll(
|
||||
Subscribable.make({
|
||||
get: Effect.succeed(1),
|
||||
changes: Stream.succeed(2),
|
||||
}),
|
||||
Subscribable.make({
|
||||
get: Effect.succeed("one"),
|
||||
changes: Stream.succeed("two"),
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(Effect.all([
|
||||
zipped.get,
|
||||
Stream.runCollect(zipped.changes),
|
||||
]))
|
||||
|
||||
expect([result[0], Array.from(result[1])]).toEqual([
|
||||
[1, "one"],
|
||||
[[2, "two"]],
|
||||
])
|
||||
})
|
||||
|
||||
test("focusArrayLength reads the current array length and reflects updates", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
|
||||
@@ -40,6 +40,41 @@ export const mapOptionEffect: {
|
||||
onNone: () => Effect.succeed(Option.none()),
|
||||
})))
|
||||
|
||||
/**
|
||||
* Allows transforming only the `changes` stream of a `Subscribable`.
|
||||
*/
|
||||
export const mapStream: {
|
||||
<A, E, R>(
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>) => Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>,
|
||||
): (self: Subscribable.Subscribable<A, E, R>) => Subscribable.Subscribable<A, E, R>
|
||||
<A, E, R>(
|
||||
self: Subscribable.Subscribable<A, E, R>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>) => Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>,
|
||||
): Subscribable.Subscribable<A, E, R>
|
||||
} = Function.dual(2, <A, E, R>(
|
||||
self: Subscribable.Subscribable<A, E, R>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>) => Stream.Stream<NoInfer<A>, NoInfer<E>, NoInfer<R>>,
|
||||
): Subscribable.Subscribable<A, E, R> => Subscribable.make({
|
||||
get get() { return self.get },
|
||||
get changes() { return f(self.changes) },
|
||||
}))
|
||||
|
||||
/**
|
||||
* Combines the current values and streams of changes from multiple `Subscribable` values.
|
||||
*/
|
||||
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
...elements: T
|
||||
): Subscribable.Subscribable<
|
||||
[T[number]] extends [never]
|
||||
? never
|
||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
|
||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
||||
> => Subscribable.make({
|
||||
get: Effect.all(elements.map(view => view.get)),
|
||||
changes: Stream.zipLatestAll(...elements.map(view => view.changes)),
|
||||
}) as any
|
||||
|
||||
|
||||
/**
|
||||
* Maps errors from both the current value and the stream of changes.
|
||||
|
||||
Reference in New Issue
Block a user