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 sCounterState(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.count4