Getting started

A complete Conduit app is a model, an action enum, a handler, and a runtime. derives Optics unlocks Optics[M](_.field) so update / updated never write copy by hand.

The four pieces

import conduit.*
import zio.*

case class CounterState(count: Int, history: List[Int]) derives Optics

enum CounterAction extends Action:
  case Inc, Dec, Reset
  case Set(v: Int)

val countHandler: ActionHandler[CounterState, Int, Nothing] =
  handle[CounterState, Int, Nothing](Optics[CounterState](_.count)):
    case CounterAction.Inc    => update(_ + 1)
    case CounterAction.Dec    => update(_ - 1)
    case CounterAction.Reset  => updated(0)
    case CounterAction.Set(v) => updated(v)
Mermoid.diagram(pieces)

Dispatch is enqueue, then run

c(actions*) offers onto the queue and returns. c.run() (default terminate = true) drains until the queue is empty, including follow-ups. This example dispatches four actions and asserts the final model.

for
  c <- Conduit(CounterState(0, Nil))(countHandler)
  _ <- c(CounterAction.Inc, CounterAction.Inc, CounterAction.Set(10), CounterAction.Dec)
  _ <- c.run()
  s <- c.currentModel
yield s
CounterState(9,List())

Live counter

The widget below is a real Conduit with run(false) forked for the example scope, exposed to the view as Ctx from ascent-conduit (docs-only). ctx(Inc) enqueues; the loop applies it; ctx.squawk(_.count) patches the text node. Use / + / Reset.

for
  (_, ctx) <- DocsRuntime.live(CounterState(0, Nil))(countHandler)
  count    <- ctx.squawk(_.count)
yield E.div(
  E.button(Events.onClick(_ => ctx(CounterAction.Dec)), "−"),
  E.span(" ", count.map(_.toString), " "),
  E.button(Events.onClick(_ => ctx(CounterAction.Inc)), "+"),
  E.button(Events.onClick(_ => ctx(CounterAction.Reset)), "Reset"),
)
0

focus per case

One handle(Optics[M]) can retarget the ambient lens per branch with focus(_.field)(...). Same counter, handler focused on the whole model:

for
  c <- Conduit(CounterState(0, Nil))(focusedHandler)
  _ <- c(CounterAction.Inc, CounterAction.Set(4))
  _ <- c.run()
  s <- c.currentModel
yield s.count
4