brazier/effect

Types

A pending after timer: the id carried by the {brazier_effect_after, Id} message that erlang:send_after delivers to the process that performed the effect.

pub type AfterTimer
pub opaque type Cancel

An effect is a list of callbacks that will be executed by the runtime. Each callback receives a dispatch function to send messages back to the update loop.

pub opaque type Effect(msg)

Values

pub fn after(delay_ms: Int, make_msg: fn() -> msg) -> Effect(msg)

Create a single delayed effect. Dispatches make_msg() once after delay_ms.

No process is started: the timer is an erlang:send_after into the runtime’s own mailbox (LiveView’s Process.send_after(self(), ...)), and make_msg runs in the runtime when it fires. The VM drops the timer if the runtime stops first.

A negative delay_ms is rejected with an error log. At most max_after_timers (100) timers can be pending per runtime; one more is rejected with a warning and [brazier, effect, after, rejected]. Key it (effect.keyed("toast", effect.after(...))) to replace the pending timer instead of adding one; effect.cancel cancels it.

Example:

effect.after(3000, fn() { HideNotification })
pub fn background(
  callback: fn(fn(msg) -> Nil) -> Nil,
) -> Effect(msg)

Create a background effect that runs in a separate BEAM process. Unlike from, this doesn’t block the main update loop. The callback runs in a spawned process and can dispatch messages back.

At most max_background_processes (32) workers can be alive per runtime; one more is rejected (it never runs) with a warning and [brazier, effect, background, rejected].

Reference: Reflex rx.event(background=True).

Example:

effect.background(fn(dispatch) {
  let data = expensive_database_query()
  dispatch(DataLoaded(data))
})
pub fn batch(effects: List(Effect(msg))) -> Effect(msg)

Combine multiple effects into one. All will be executed. No ordering guarantees between effects in the batch.

pub fn cancel(token: Cancel) -> Effect(msg)

Cancel a previously created cancellable effect token.

pub fn cancellable(inner: Effect(msg)) -> #(Effect(msg), Cancel)

Wrap an effect in a generated cancel key and return the cancel token.

pub fn every(
  interval_ms: Int,
  make_msg: fn() -> msg,
) -> Effect(msg)

Create a periodic timer effect. Dispatches make_msg() every interval_ms. The timer runs in a separate BEAM process owned by the runtime and stops when the runtime stops or crashes. Capped at max_timers (10) concurrent timers per runtime — additional timers are rejected with a warning and [brazier, effect, timer, rejected]. An interval_ms below 1 is rejected with an error log (it would be a busy loop).

Each un-keyed call creates a NEW timer. To replace a timer instead of adding one, key it: effect.keyed("clock", effect.every(1000, Tick)) — performing it again stops the previous “clock” timer first, and effect.cancel (via effect.cancellable) stops it outright.

Reference: Phoenix LiveView Process.send_after in handle_info.

Example:

effect.every(150, fn() { Tick })  // game loop at ~7fps
effect.every(1000, fn() { RefreshStats })  // dashboard update every second
pub fn from(callback: fn(fn(msg) -> Nil) -> Nil) -> Effect(msg)

Create an effect from a callback function. The callback receives a dispatch function that sends messages back to the runtime’s update loop.

Example:

effect.from(fn(dispatch) {
  // Do some async work...
  dispatch(DataLoaded(result))
})
pub fn is_current_key_generation(
  key: String,
  generation: Int,
) -> Bool
pub fn is_none(effect: Effect(msg)) -> Bool

Check if an effect has any callbacks to execute. An effect that carries stream ops is not none.

pub fn keyed(key: String, inner: Effect(msg)) -> Effect(msg)

Associate an effect with a stable key so stale async completions can be dropped.

Stream ops in inner are not keyed: they are applied when the update returns, so there is no late completion to drop.

pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b)

Transform the message type of an effect. Useful when composing effects from child components.

pub const max_after_timers: Int

Maximum pending after timers per runtime. A timer counts from the update that performed it until it fires or is cancelled.

pub const max_background_processes: Int

Maximum live background workers per runtime. Each is two processes (the worker and its guardian), so a session holds at most 64 of them. A worker counts until it exits.

pub const max_timers: Int

Maximum concurrent every timers per runtime process. Prevents runaway timer creation from buggy update handlers.

pub fn none() -> Effect(msg)

No effects to perform. Use when update doesn’t need side effects.

pub fn perform(
  effect: Effect(msg),
  dispatch: fn(msg) -> Nil,
  dispatch_keyed: fn(String, Int, msg) -> Nil,
) -> Nil

Execute all callbacks in the effect with the given dispatch function. Called by the runtime — not by user code. Started processes and timers are owned by (and die with) the calling process. Crashes on a stream op: take them out with take_stream_ops first.

pub fn perform_tracked(
  effect: Effect(msg),
  dispatch: fn(msg) -> Nil,
  dispatch_keyed: fn(String, Int, msg) -> Nil,
) -> List(process.Pid)

Execute callbacks and return the pids of the processes they started (background workers and every timers; an after timer is not a process). The calling process also tracks everything it owns, bounded by the limits, and stop_owned stops it all.

✨ Search Document