Effect 3.8 (Release)

Sep 16th, 2024

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 to Ref.get
  • SynchronizedRef<A> - Effect<A>, equivalent to SynchronizedRef.get
  • SubscriptionRef<A> - Effect<A>, equivalent to SubscriptionRef.get
  • Deferred<A, E> - Effect<A, E>, equivalent to Deferred.await
  • Dequeue<A> - Effect<A>, equivalent to Queue.take
  • Fiber<A, E> - Effect<A, E>, equivalent to Fiber.join
  • FiberRef<A> - Effect<A>, equivalent to FiberRef.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 state
const latch = yield* Effect.makeLatch(false)
// Fork a fiber that logs "open sesame" when the latch is opened
const fiber = yield* Effect.log("open sesame").pipe(
latch.whenOpen,
Effect.fork
)
// Open the latch
yield* latch.open
// Wait for the latch to be opened
yield* fiber.await
// Release all waiters, without opening the latch
yield* latch.release
})
ts
import { Effect } from "effect"
Effect.gen(function* () {
// Create a latch, starting in the closed state
const latch = yield* Effect.makeLatch(false)
// Fork a fiber that logs "open sesame" when the latch is opened
const fiber = yield* Effect.log("open sesame").pipe(
latch.whenOpen,
Effect.fork
)
// Open the latch
yield* latch.open
// Wait for the latch to be opened
yield* fiber.await
// Release all waiters, without opening the latch
yield* 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 yet
yield* 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 yet
yield* 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 request
yield* client.get("https://jsonplaceholder.typicode.com/todos/1")
}).pipe(
Effect.scoped,
// the fetch client has been moved to the `FetchHttpClient` module
Effect.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 request
yield* client.get("https://jsonplaceholder.typicode.com/todos/1")
}).pipe(
Effect.scoped,
// the fetch client has been moved to the `FetchHttpClient` module
Effect.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 request
yield* client.get("https://jsonplaceholder.typicode.com/todos/1")
// make a post request
yield* client.post("https://jsonplaceholder.typicode.com/todos")
// execute a request instance
yield* 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 request
yield* client.get("https://jsonplaceholder.typicode.com/todos/1")
// make a post request
yield* client.post("https://jsonplaceholder.typicode.com/todos")
// execute a request instance
yield* 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.HttpClient
yield* 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.HttpClient
yield* 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 mailbox
yield* mailbox.offer(1)
yield* mailbox.offer(2)
yield* mailbox.offerAll([3, 4, 5])
// take messages from the mailbox
const [messages, done] = yield* mailbox.takeAll
assert.deepStrictEqual(Chunk.toReadonlyArray(messages), [1, 2, 3, 4, 5])
assert.strictEqual(done, false)
// signal that the mailbox is done
yield* mailbox.end
const [messages2, done2] = yield* mailbox.takeAll
assert.deepStrictEqual(messages2, Chunk.empty())
assert.strictEqual(done2, true)
// signal that the mailbox is failed
yield* mailbox.fail("boom")
// turn the mailbox into a stream
const 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 mailbox
yield* mailbox.offer(1)
yield* mailbox.offer(2)
yield* mailbox.offerAll([3, 4, 5])
// take messages from the mailbox
const [messages, done] = yield* mailbox.takeAll
assert.deepStrictEqual(Chunk.toReadonlyArray(messages), [1, 2, 3, 4, 5])
assert.strictEqual(done, false)
// signal that the mailbox is done
yield* mailbox.end
const [messages2, done2] = yield* mailbox.takeAll
assert.deepStrictEqual(messages2, Chunk.empty())
assert.strictEqual(done2, true)
// signal that the mailbox is failed
yield* mailbox.fail("boom")
// turn the mailbox into a stream
const 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!