Effect 3.5 (Release)

Jul 10th, 2024

Effect 3.5.0 has been released! This release includes a number of new features and improvements. Here's a summary of what's new:

Better support for Error.cause

If you add a cause property to Data.Error or Data.TaggedError, it will now be properly forwarded to the cause property of the Error instance.

typescript
import { Data } from "effect"
class MyError extends Data.Error<{ cause: Error }> {}
typescript
import { Data } from "effect"
class MyError extends Data.Error<{ cause: Error }> {}

If you Effect.log a Cause containing an error with a cause property, it will now be visible in the log output.

@effect/sql-d1

The @effect/sql-d1 package has been released. This package provides @effect/sql support for Cloudflare's D1 database.

Added RcRef & RcMap

RcRef and RcMap are new reference counted types that can be used to manage resources.

The wrapped resource will be acquired on the first access and released when no longer in use.

RcRef can be used to manage a single resource, and RcMap can be used to manage multiple resources referenced by a key.

typescript
import { Effect, RcMap } from "effect"
Effect.gen(function* () {
const map = yield* RcMap.make({
lookup: (key: string) =>
Effect.acquireRelease(Effect.succeed(`acquired ${key}`), () =>
Effect.log(`releasing ${key}`),
)
})
// Get "foo" from the map twice, which will only acquire it once
// It will then be released once the scope closes.
yield* RcMap.get(map, "foo").pipe(
Effect.andThen(RcMap.get(map, "foo")),
Effect.scoped
)
})
typescript
import { Effect, RcMap } from "effect"
Effect.gen(function* () {
const map = yield* RcMap.make({
lookup: (key: string) =>
Effect.acquireRelease(Effect.succeed(`acquired ${key}`), () =>
Effect.log(`releasing ${key}`),
)
})
// Get "foo" from the map twice, which will only acquire it once
// It will then be released once the scope closes.
yield* RcMap.get(map, "foo").pipe(
Effect.andThen(RcMap.get(map, "foo")),
Effect.scoped
)
})

Added Logger.pretty

Logger.pretty is a new logger that leverages the features of the console APIs to provide a more visually appealing output.

To try it out, provide it to your program:

typescript
import { Effect, Logger } from "effect"
Effect.log("Hello, World!").pipe(Effect.provide(Logger.pretty))
typescript
import { Effect, Logger } from "effect"
Effect.log("Hello, World!").pipe(Effect.provide(Logger.pretty))

In Effect 4.0, Logger.pretty will become default logger.

PubSub replay option

The replay option adds a replay buffer in front of the given PubSub. The buffer will replay the last n messages to any new subscriber.

typescript
Effect.gen(function*() {
const messages = [1, 2, 3, 4, 5]
const pubsub = yield* PubSub.bounded<number>({ capacity: 16, replay: 3 })
yield* PubSub.publishAll(pubsub, messages)
const sub = yield* PubSub.subscribe(pubsub)
assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5])
})
typescript
Effect.gen(function*() {
const messages = [1, 2, 3, 4, 5]
const pubsub = yield* PubSub.bounded<number>({ capacity: 16, replay: 3 })
yield* PubSub.publishAll(pubsub, messages)
const sub = yield* PubSub.subscribe(pubsub)
assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5])
})

Added Stream.raceAll

Stream.raceAll races the given streams, with the first stream to emit an item declared the winner. The resulting stream will emit the items from the winning stream.

typescript
import { Stream, Schedule, Console, Effect } from "effect"
const stream = Stream.raceAll(
Stream.fromSchedule(Schedule.spaced("1 millis")),
Stream.fromSchedule(Schedule.spaced("2 millis")),
Stream.fromSchedule(Schedule.spaced("4 millis"))
).pipe(Stream.take(6), Stream.tap(Console.log))
Effect.runPromise(Stream.runDrain(stream))
// Output only from the first stream, the rest streams are interrupted
// 0
// 1
// 2
// 3
// 4
// 5
typescript
import { Stream, Schedule, Console, Effect } from "effect"
const stream = Stream.raceAll(
Stream.fromSchedule(Schedule.spaced("1 millis")),
Stream.fromSchedule(Schedule.spaced("2 millis")),
Stream.fromSchedule(Schedule.spaced("4 millis"))
).pipe(Stream.take(6), Stream.tap(Console.log))
Effect.runPromise(Stream.runDrain(stream))
// Output only from the first stream, the rest streams are interrupted
// 0
// 1
// 2
// 3
// 4
// 5

Added Random.make

Random.make creates a new instance of the Random service from a seed value.

It will calculate the hash of the seed value, and use that to seed the random number generator.

Stream.async buffer options

You can now customize the output buffer options for Stream.async*:

typescript
import { Stream } from "effect"
Stream.async((emit) => {
// ...
}, { bufferSize: 16, strategy: "dropping" })
typescript
import { Stream } from "effect"
Stream.async((emit) => {
// ...
}, { bufferSize: 16, strategy: "dropping" })

Stream PubSub options

You can now customize the strategy and capacity of the underlying PubSub in the following Stream apis:

  • Stream.toPubSub
  • Stream.broadcast*
ts
import { Schedule, Stream } from "effect"
// toPubSub
Stream.fromSchedule(Schedule.spaced(1000)).pipe(
Stream.toPubSub({
capacity: 16, // or "unbounded"
strategy: "dropping", // or "sliding" / "suspend"
}),
)
// also for the broadcast apis
Stream.fromSchedule(Schedule.spaced(1000)).pipe(
Stream.broadcastDynamic({
capacity: 16,
strategy: "dropping",
}),
)
ts
import { Schedule, Stream } from "effect"
// toPubSub
Stream.fromSchedule(Schedule.spaced(1000)).pipe(
Stream.toPubSub({
capacity: 16, // or "unbounded"
strategy: "dropping", // or "sliding" / "suspend"
}),
)
// also for the broadcast apis
Stream.fromSchedule(Schedule.spaced(1000)).pipe(
Stream.broadcastDynamic({
capacity: 16,
strategy: "dropping",
}),
)

Stream type fixes

  • Stream & Channel run* methods now exclude Scope from the R type.
  • Use of Stream.DynamicTuple has been replaced with Types.TupleOf.
  • Use left / right naming instead of self / that in Stream.mergeRight & Stream.mergeLeft.

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!