Effect 3.8 (Release)
Effect 3.8 has been released! This release includes a number of new features and improvements. Here's a summary of what's new:
Logger.withLeveledConsole
With this api you can create a logger that uses the console.{log,info,warn,error,trace}
functions,
depending on the log level of each message.
This is useful in environments such as browsers, where the different log levels are styled differently.
For example:
ts
import { Effect, Logger } from "effect"const loggerLayer = Logger.replace(Logger.defaultLogger,Logger.withLeveledConsole(Logger.stringLogger))Effect.gen(function* () {yield* Effect.logError("an error")yield* Effect.logInfo("an info")}).pipe(Effect.provide(loggerLayer))
ts
import { Effect, Logger } from "effect"const loggerLayer = Logger.replace(Logger.defaultLogger,Logger.withLeveledConsole(Logger.stringLogger))Effect.gen(function* () {yield* Effect.logError("an error")yield* Effect.logInfo("an info")}).pipe(Effect.provide(loggerLayer))
More types implement Effect
Many of the data types in Effect can now be used directly as Effect
's.
These include:
Ref<A>
-Effect<A>
, equivalent toRef.get
SynchronizedRef<A>
-Effect<A>
, equivalent toSynchronizedRef.get
SubscriptionRef<A>
-Effect<A>
, equivalent toSubscriptionRef.get
Deferred<A, E>
-Effect<A, E>
, equivalent toDeferred.await
Dequeue<A>
-Effect<A>
, equivalent toQueue.take
Fiber<A, E>
-Effect<A, E>
, equivalent toFiber.join
FiberRef<A>
-Effect<A>
, equivalent toFiberRef.get
Semaphore.withPermitsIfAvailable
Semaphore.withPermitsIfAvailable
will attempt to run an effect immediately if permits are available.
It will return an Option<A>
from the result of the effect, depending on whether the permits were available.
ts
import { Effect } from "effect"Effect.gen(function*() {const semaphore = yield* Effect.makeSemaphore(1)// returns Option.some("foo")yield* semaphore.withPermitsIfAvailable(1)(Effect.succeed("foo"))// returns Option.none()yield* semaphore.withPermitsIfAvailable(2)(Effect.succeed("bar"))})
ts
import { Effect } from "effect"Effect.gen(function*() {const semaphore = yield* Effect.makeSemaphore(1)// returns Option.some("foo")yield* semaphore.withPermitsIfAvailable(1)(Effect.succeed("foo"))// returns Option.none()yield* semaphore.withPermitsIfAvailable(2)(Effect.succeed("bar"))})
Effect.makeLatch
You can create an Effect.Latch
with Effect.makeLatch
, which can be used to synchronize multiple effects.
The latch can be opened, closed and waited on.
ts
import { Effect } from "effect"Effect.gen(function* () {// Create a latch, starting in the closed stateconst latch = yield* Effect.makeLatch(false)// Fork a fiber that logs "open sesame" when the latch is openedconst fiber = yield* Effect.log("open sesame").pipe(latch.whenOpen,Effect.fork)// Open the latchyield* latch.open// Wait for the latch to be openedyield* fiber.await// Release all waiters, without opening the latchyield* latch.release})
ts
import { Effect } from "effect"Effect.gen(function* () {// Create a latch, starting in the closed stateconst latch = yield* Effect.makeLatch(false)// Fork a fiber that logs "open sesame" when the latch is openedconst fiber = yield* Effect.log("open sesame").pipe(latch.whenOpen,Effect.fork)// Open the latchyield* latch.open// Wait for the latch to be openedyield* fiber.await// Release all waiters, without opening the latchyield* latch.release})
Stream.share
Stream.share
is a reference counted equivalent of the Stream.broadcastDynamic
api.
It is useful when you want to share a Stream
, and ensure any resources are finalized when no more consumers are subscribed.
ts
import { Effect, Stream } from "effect"Effect.gen(function*() {const sharedStream = yield* Effect.acquireRelease(Effect.log("Stream acquired").pipe(Effect.as(Stream.never)),() => Effect.log("Stream released")).pipe(Stream.unwrapScoped,Stream.share({ capacity: 16 }))// Nothing is logged yetyield* Stream.runDrain(sharedStream)// The upstream will now start emitting values.// If the downstream is interrupted, the upstream will also be finalized.})
ts
import { Effect, Stream } from "effect"Effect.gen(function*() {const sharedStream = yield* Effect.acquireRelease(Effect.log("Stream acquired").pipe(Effect.as(Stream.never)),() => Effect.log("Stream released")).pipe(Stream.unwrapScoped,Stream.share({ capacity: 16 }))// Nothing is logged yetyield* Stream.runDrain(sharedStream)// The upstream will now start emitting values.// If the downstream is interrupted, the upstream will also be finalized.})
HttpClient refactor
The @effect/platform/HttpClient
module has been refactored to reduce and simplify the api surface.
HttpClient.fetch removed
The HttpClient.fetch
client implementation has been removed. Instead, you can
access a HttpClient
using the corresponding Context.Tag
.
ts
import { FetchHttpClient, HttpClient } from "@effect/platform"import { Effect } from "effect"Effect.gen(function* () {const client = yield* HttpClient.HttpClient// make a get requestyield* client.get("https://jsonplaceholder.typicode.com/todos/1")}).pipe(Effect.scoped,// the fetch client has been moved to the `FetchHttpClient` moduleEffect.provide(FetchHttpClient.layer))
ts
import { FetchHttpClient, HttpClient } from "@effect/platform"import { Effect } from "effect"Effect.gen(function* () {const client = yield* HttpClient.HttpClient// make a get requestyield* client.get("https://jsonplaceholder.typicode.com/todos/1")}).pipe(Effect.scoped,// the fetch client has been moved to the `FetchHttpClient` moduleEffect.provide(FetchHttpClient.layer))
HttpClient interface now uses methods
Instead of being a function that returns the response, the HttpClient
interface now uses methods to make requests.
Some shorthand methods have been added to the HttpClient
interface to make
less complex requests easier to implement.
ts
import {FetchHttpClient,HttpClient,HttpClientRequest} from "@effect/platform"import { Effect } from "effect"Effect.gen(function* () {const client = yield* HttpClient.HttpClient// make a get requestyield* client.get("https://jsonplaceholder.typicode.com/todos/1")// make a post requestyield* client.post("https://jsonplaceholder.typicode.com/todos")// execute a request instanceyield* client.execute(HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1"))})
ts
import {FetchHttpClient,HttpClient,HttpClientRequest} from "@effect/platform"import { Effect } from "effect"Effect.gen(function* () {const client = yield* HttpClient.HttpClient// make a get requestyield* client.get("https://jsonplaceholder.typicode.com/todos/1")// make a post requestyield* client.post("https://jsonplaceholder.typicode.com/todos")// execute a request instanceyield* client.execute(HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1"))})
Scoped HttpClientResponse helpers removed
The HttpClientResponse
helpers that also supplied the Scope
have been removed.
Instead, you can use the HttpClientResponse
methods directly, and explicitly
add a Effect.scoped
to the pipeline.
ts
import { FetchHttpClient, HttpClient } from "@effect/platform"import { Effect } from "effect"Effect.gen(function* () {const client = yield* HttpClient.HttpClientyield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe(Effect.flatMap((response) => response.json),Effect.scoped // supply the `Scope`)})
ts
import { FetchHttpClient, HttpClient } from "@effect/platform"import { Effect } from "effect"Effect.gen(function* () {const client = yield* HttpClient.HttpClientyield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe(Effect.flatMap((response) => response.json),Effect.scoped // supply the `Scope`)})
Some apis have been renamed
Including the HttpClientRequest
body apis, which is to make them more
discoverable.
Mailbox module
A new experimental effect/Mailbox
module has been added. Mailbox
is an asynchronous queue
that can have a done / failure signal.
It is useful when you want to communicate between effects, and have a way to determine that the communication is complete.
ts
import { Chunk, Effect, Mailbox } from "effect"import * as assert from "node:assert"Effect.gen(function* () {const mailbox = yield* Mailbox.make<number, string>()// add messages to the mailboxyield* mailbox.offer(1)yield* mailbox.offer(2)yield* mailbox.offerAll([3, 4, 5])// take messages from the mailboxconst [messages, done] = yield* mailbox.takeAllassert.deepStrictEqual(Chunk.toReadonlyArray(messages), [1, 2, 3, 4, 5])assert.strictEqual(done, false)// signal that the mailbox is doneyield* mailbox.endconst [messages2, done2] = yield* mailbox.takeAllassert.deepStrictEqual(messages2, Chunk.empty())assert.strictEqual(done2, true)// signal that the mailbox is failedyield* mailbox.fail("boom")// turn the mailbox into a streamconst stream = Mailbox.toStream(mailbox)})
ts
import { Chunk, Effect, Mailbox } from "effect"import * as assert from "node:assert"Effect.gen(function* () {const mailbox = yield* Mailbox.make<number, string>()// add messages to the mailboxyield* mailbox.offer(1)yield* mailbox.offer(2)yield* mailbox.offerAll([3, 4, 5])// take messages from the mailboxconst [messages, done] = yield* mailbox.takeAllassert.deepStrictEqual(Chunk.toReadonlyArray(messages), [1, 2, 3, 4, 5])assert.strictEqual(done, false)// signal that the mailbox is doneyield* mailbox.endconst [messages2, done2] = yield* mailbox.takeAllassert.deepStrictEqual(messages2, Chunk.empty())assert.strictEqual(done2, true)// signal that the mailbox is failedyield* mailbox.fail("boom")// turn the mailbox into a streamconst stream = Mailbox.toStream(mailbox)})
FiberRef performance improvements
Some common FiberRef
instances are now cached, which improves performance of the Effect runtime.
RcMap.keys & MutableMap.keys
You can now access the available keys of a RcMap
or MutableMap
using the keys
api.
For example:
ts
const map = MutableHashMap.make([["a", "a"], ["b", "b"], ["c", "c"]])const keys = MutableHashMap.keys(map) // ["a", "b", "c"]
ts
const map = MutableHashMap.make([["a", "a"], ["b", "b"], ["c", "c"]])const keys = MutableHashMap.keys(map) // ["a", "b", "c"]
And for RcMap
:
ts
Effect.gen(function* () {const map = yield* RcMap.make({lookup: (key) => Effect.succeed(key)})yield* RcMap.get(map, "a")yield* RcMap.get(map, "b")yield* RcMap.get(map, "c")const keys = yield* RcMap.keys(map) // ["a", "b", "c"]})
ts
Effect.gen(function* () {const map = yield* RcMap.make({lookup: (key) => Effect.succeed(key)})yield* RcMap.get(map, "a")yield* RcMap.get(map, "b")yield* RcMap.get(map, "c")const keys = yield* RcMap.keys(map) // ["a", "b", "c"]})
Duration conversion apis
Some new apis for converting between a Duration
and it's corresponding parts have been added:
Duration.toMinutes
Duration.toHours
Duration.toDays
Duration.toWeeks
Duration.parts
Other changes
There were several other smaller changes made. Take a look through the CHANGELOG to see them all: CHANGELOG.
Don't forget to join our Discord Community to follow the last updates and discuss every tiny detail!