Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std::reactive — reference

Signals, effects, ownership, turns, and the higher-level cells. Concepts and usage patterns: the reactive guide.

Import what you use:

import std::reactive::{
	Signal, Source, Subscription, Disposable, combine,
	Owner, owner_scope, get_owner, run_with_owner, comp,
	Turn, FlushPolicy, turn_scope, turn, batch, flush,
	optimistic, draft, Draft, DraftState,
	reconcile, ReconcilePlan, RowStep,
};

At a glance

ItemKindOne line
Signal<T>structmutable cell with subscribers
Source<T>traitanything readable + subscribable (get/sub/effect)
Subscriptionstructan explicit subscription; Disposable
combinefntuple-signal over 2+ signals
Ownerstructdisposal bag; the lifetime unit
run_with_owner, comp, get_owner, owner_scopefns/contextestablish/read the ambient owner
turn, batch, flush, FlushPolicy, turn_scopefns/contextwrite batching
optimisticfnpaint → commit → confirm-or-rollback
draft, Draft<T>, DraftStatefn/struct/enumlocal-first editing cell
reconcile, ReconcilePlan, RowStepfn/structskeyed list diffing engine

Signal

impl Signal<type T> {
	fun new(value: T): Signal<T>
	fun set(self, value: T)                 // write + notify
	fun set_with(self, transform: sync |T| T)    // read-modify-write
	fun map<U>(self, transform: sync |T| U): Signal<U>
}
impl Signal<type T> with Source<T> {
	fun get(self): T
	fun sub(self, observer: |T| void): Subscription
	// from the trait default:
	fun effect(self, observer: |T| void)    // fires now + on change; owner-registered
}
impl Signal<Signal<type U>> {
	fun flatten(self): Signal<U>            // follow the current inner signal
}
  • set notifies through the ambient turn when one exists (writes coalesce); outside any turn it notifies immediately.
  • map’s result is a live derived signal; its internal subscription is unowned (lives as long as the source).
  • effect requires an ambient owner — calling it outside every owner is a compile error (context coverage). It fires once immediately.
  • sub does not fire immediately, and its Subscription is yours to dispose (or hand to owner.take).

combine

fun combine<T: (2..)>(sources: (U in T: Signal<U>)): Signal<T>

A signal of the tuple of the sources’ current values, firing when any source changes. Variadic over tuples of signals of mixed element types:

import std::print;
import std::reactive::{ Signal, combine };

fun main() {
	let flag = Signal::new(true);
	let count = Signal::new(2);
	let both: Signal<(bool, i32)> = combine((flag, count));
	let (_on, current) = both.get();
	print(current);
}

(Bind the tuple before taking elements — chained access on a call result, both.get().1, doesn’t type yet; see the gotchas appendix.)

Subscription, Disposable

trait Disposable { fun dispose(self); }
struct Subscription { … }        // impl Disposable

Disposing a subscription guarantees no later deliveries; a delivery already queued in the currently-draining turn may still land once.

Owner

impl Owner {
	fun new(): Owner
	fun take<T: Disposable>(self, item: T): T   // adopt a disposable; returns it
	fun defer(self, cleanup: || void)           // run cleanup at dispose
}
impl Owner with Disposable {
	fun dispose(self)   // dispose everything collected + run defers
}

let owner_scope: Context<Owner>
fun get_owner(): Owner                                        // read the ambient owner
fun run_with_owner<T>(owner: Owner, body: (sync || T) context owner_scope): T
fun comp<T>(body: (sync || T) context owner_scope): (T, Owner)     // fresh owner + result

body parameters marked context owner_scope receive the ambient owner implicitly — your component functions thread ownership without mentioning it. Establish owners at disposal boundaries (places where a subtree can die), not per object; in UI code the framework’s boundaries (mount_root, bind_each rows, when/swap bodies) already do this.

Turns

enum FlushPolicy { AtEnd, AtSuspension }
let turn_scope: Context<Turn>

fun turn<T>(policy: FlushPolicy, body: (|| T) context turn_scope): T
fun batch<T>(body: (sync || T) context turn_scope): T   // join or create
fun flush()                                             // drain the ambient turn now

Inside a turn, signal writes are recorded and each subscriber runs once with final values when the turn settles. The body is asyncness-polymorphic (spec §7.4): a synchronous body settles at the end of its synchronous extent, and an awaiting body holds every notification until it fully completes — a transaction. Framework boundaries establish turns for you: UI event handlers and mount_root (AtSuspension), RPC service handlers (AtEnd). Writes landing after a settle (from spawned work) drain in per-segment microtasks.

optimistic

fun optimistic<T, E>(signal: Signal<T>, value: T, commit: async || Result<T, E>): Result<T, E>

Paint value into signal now, await commit, then reconcile: the confirmed value on Ok, the previous value rolled back on Err. Returns the outcome for error UX. For continuous editing, use draft instead — rollback is wrong mid-typing.

Draft — local-first cells

enum DraftState {
	Synced,       // local matches the last pushed/adopted value
	Dirty,        // local edits not yet confirmed (in-flight included)
	Failed(str),  // last push errored; local KEPT, not rolled back
}

struct Draft<T> {
	local: Signal<T>,           // bind inputs to this; read like any signal
	state: Signal<DraftState>,  // bind a status label to this
	…                           // internals: synced value, generation counter
}

fun draft<T: PartialEq>(initial: T, commit: async |T| Option<str>): Draft<T>

impl Draft<type T: PartialEq> {
	fun push(self, value: T)    // set local + SPAWN the commit (returns immediately)
	fun adopt(self, remote: T)  // fold in a remote value
}
  • commit returns None on success, Some(reason) on failure. The parameter is async-typed so an RPC-calling closure flows in directly; a plain synchronous closure works too.
  • push is per-keystroke-safe: local-first (never waits on the wire), and a generation counter ensures only the newest push settles state — a slow older commit landing late is discarded.
  • adopt rules: value equal to the last synced value (an echo of your own push) → no-op; clean local (no unpushed edits) → adopt into local; dirty local → local wins, the remote value is remembered so the eventual push knowingly overwrites (last-write-wins).
  • On failure, state carries the reason and local keeps the user’s text; the next push retries naturally.

UI wiring: View.bind_draft(draft) — see the browser reference.

reconcile — keyed list diffing

enum RowStep {
	Keep(i32),     // reuse old row at index (moved into the new order)
	Refresh(i32),  // same key, changed value: rebuild, dispose old index
	Fresh,         // a new row
}
struct ReconcilePlan {
	steps: List<RowStep>,  // one per NEW item, in the new order
	removed: List<i32>,    // old indices gone entirely
}
fun reconcile<T: PartialEq, K: PartialEq>(
	old_keys: List<K>, old_items: List<T>, items: List<T>, key_of: sync |T| K,
): ReconcilePlan

The pure engine under ui.bind_each; duplicate keys claim the first surviving row once. Reach for it directly only when building a custom list-rendering primitive.