Crate nami

Crate nami 

Source
Expand description

§Nami

A powerful, lightweight reactive framework for Rust.

Crates.io Docs.rs

  • no_std with alloc
  • Consistent Signal trait across computed values
  • Ergonomic two-way Binding<T> with helpers
  • Composition primitives: map, zip, cached, debounce, throttle, utils::{add,max,min}
  • Typed watcher context with metadata
  • Optional derive macros

§Quick Start

use nami::{binding, Binding, Signal};

// Create mutable reactive state with automatic type conversion
let mut counter: Binding<i32> = binding(0);
let mut message: Binding<String> = binding("hello");  // &str -> String conversion

// Derive a new computation from it
let doubled = nami::map::map(counter.clone(), |n: i32| n * 2);

// Read current values
assert_eq!(counter.get(), 0);
assert_eq!(doubled.get(), 0);

// Update the source and observe derived changes
counter.set(3);
assert_eq!(doubled.get(), 6);

// set_from() also accepts Into<T> for ergonomic updates
message.set_from("world");  // &str works directly!

§The Signal Trait

All reactive values implement a single trait:

use nami::watcher::{Context, WatcherGuard};

pub trait Signal: Clone + 'static {
    type Output;
    type Guard: WatcherGuard;

    fn get(&self) -> Self::Output;
    fn watch(&self, watcher: impl Fn(Context<Self::Output>) + 'static) -> Self::Guard;
}
  • get: compute and return the current value.
  • watch: subscribe to changes; returns a guard. Drop the guard to unsubscribe.

Binding, Computed, and all adapters implement Signal so you can compose them freely.

§Bindings

Binding<T> is two-way reactive state with ergonomic helpers. Both binding() and set_from() accept any value implementing Into<T>, eliminating the need for manual conversions:

use nami::{binding, Binding};

// Automatic type conversion with Into trait
let mut text: Binding<String> = binding("hello");           // &str -> String
let mut counter: Binding<i32> = binding(0);                 // Direct initialization
let mut bignum: Binding<i64> = binding(0i64);
let items: Binding<Vec<i32>> = binding(vec![1, 2, 3]);  // Vec<i32> binding

// set_from() also uses Into<T> for ergonomic updates
text.set_from("world");                                // Direct &str, no .into() needed
counter.set(5);
counter.add_assign(1);
assert_eq!(counter.get(), 6);

// Works with type conversions
let mut bignum: Binding<i64> = binding(0);
bignum.set(42);

§Watchers

React to changes via watch. Keep the returned guard alive to stay subscribed.

use nami::{binding, Binding, Signal, watcher::Context};

let mut name: Binding<String> = binding("World");

let _guard = name.watch(|ctx: Context<String>| {
    // metadata is available via ctx.metadata
    // println! is just an example side-effect
    println!("Hello, {}!", ctx.value());
});

name.set_from("Universe");

The Context carries typed metadata to power advanced features (e.g., animations).

§Composition Primitives

  • map(source, f): transform values while preserving reactivity
  • zip(a, b): combine two signals into (A::Output, B::Output)
  • cached(signal): cache last value and avoid recomputation
  • debounce(signal, duration): delay updates until a quiet period
  • throttle(signal, duration): limit update rate to at most once per duration
  • utils::{add, max, min}: convenient combinators built on zip + map
use nami::{binding, Binding, Signal};
use nami::{map::map, zip::zip};

let a: Binding<i32> = binding(2);
let b: Binding<i32> = binding(3);

let sum = nami::utils::add(a.clone(), b.clone());
assert_eq!(sum.get(), 5);

let pair = zip(a, b);
assert_eq!(pair.get(), (2, 3));

let squared = map(sum, |n: i32| n * n);
assert_eq!(squared.get(), 25);

§Rate Limiting: Debounce and Throttle

Control the rate of updates with debounce and throttle utilities:

use nami::{binding, debounce::Debounce, throttle::Throttle, Binding};
use core::time::Duration;

let mut input: Binding<String> = binding("");

// Debounce: delay updates until 300ms of quiet time
let debounced = Debounce::new(input.clone(), Duration::from_millis(300));

// Throttle: limit to at most one update per 100ms
let throttled = Throttle::new(input.clone(), Duration::from_millis(100));

// Both preserve reactivity while controlling update frequency
input.set_from("typing...");

Debounce vs Throttle:

  • Debounce: Waits for a quiet period, useful for search input, API calls
  • Throttle: Limits maximum update rate, useful for scroll events, animations

§Type-Erased Computed<T>

Computed<T> stores any Signal<Output = T> behind a stable, type-erased handle.

use nami::{Signal, SignalExt};

let c = 10_i32.computed();
assert_eq!(c.get(), 10);

let plus_one = c.map(|n| n + 1);
assert_eq!(plus_one.get(), 11);

§Async Interop

Bridge async with reactive using adapters:

  • FutureSignal<T>: Option<T> becomes Some(T) when a future resolves
  • SignalStream<S>: treat a Signal as a Stream that yields on updates
  • BindingMailbox<T>: cross-thread reactive state with get(), set(), and get_as() for type conversion
use nami::future::FutureSignal;
use executor_core::LocalExecutor;

// Requires an executor; example omitted for brevity
// let sig = FutureSignal::new(executor, async { 42 });
// assert_eq!(sig.get(), None);
// ... later ... sig.get() == Some(42)
use nami::{Signal, stream::SignalStream};
// let s = /* some Signal */;
// let mut stream = SignalStream::new(s);
// while let Some(value) = stream.next().await { /* ... */ }

Enhanced Mailboxes (requires native-executor feature):

use nami::{binding, Binding};
use waterui_str::Str; // Example non-Send type

// Create binding with non-Send type
let text_binding:Binding<Str> = binding("hello");
let mailbox = text_binding.mailbox();

// Convert to Send type for cross-thread usage
let owned_string: String = mailbox.get_as().await;
assert_eq!(owned_string, "hello");

// Regular mailbox operations
mailbox.set("world").await;

§Debugging

Enable structured logging to trace signal behavior during development:

use nami::{binding, Binding, Signal, debug::{Debug, Config}};

let value: Binding<i32> = binding(42);

// Log only value changes (most common)
let debug = Debug::changes(value.clone());

// Log all operations (verbose mode)
let debug = Debug::verbose(value.clone());

// Log specific operations
let debug = Debug::compute_only(value.clone());        // Only computations
let debug = Debug::watchers(value.clone());            // Watcher lifecycle
let debug = Debug::compute_and_changes(value.clone()); // Both computations and changes

// Use custom configuration
let debug = Debug::with_config(value, Config::default());

The debug module uses the log crate for output, so configure your logger (e.g., env_logger) to see the debug messages.

§Derive Macros

Enable the derive feature (enabled by default) to access:

  • #[derive(nami::Project)]: project a struct binding into bindings for each field
use nami::{binding, Binding, project::Project};

#[derive(Clone, nami::Project)]
struct Person { name: String, age: u32 }

let p: Binding<Person> = binding(Person { name: "A".into(), age: 1 });
// The derive generates `PersonProjected`
let mut projected: PersonProjected = p.project();
projected.name.set_from("B");  // Automatic &str -> String conversion
assert_eq!(p.get().name, "B");

Feature flags:

  • derive (default): re-exports macros from nami-derive
  • native-executor (default): integrates with native-executor for mailbox helpers

§Notes

  • no_std: the crate is #![no_std] and uses alloc.
  • Keep watcher guards alive to remain subscribed; dropping the guard unsubscribes.
  • Many examples are no_run because they require an executor or side effects.

Modules§

binding
Reactive Bindings
cache
Cached Signal Implementation
collection
Reactive collections with watcher support.
constant
Constant Values for Reactive Computation
debounce
Debounce utilities for throttling signal updates.
debug
Debug utilities for Signal tracing
distinct
Distinct Signal Implementation
future
Future interop for reactive signals.
map
Map Module
project
Projection utilities for decomposing bindings into component parts.
signal
This module provides a framework for reactive computations that can track dependencies and automatically update when their inputs change.
stream
Stream interop for reactive signals.
throttle
Throttling utilities for limiting signal update rates.
utils
Addition Operations for Signal Types
watcher
Watcher Management
zip
Provides functionality for combining and transforming computations.

Macros§

impl_constant
Macro to implement the Signal trait for constant types.
s
Function-like procedural macro for creating formatted string signals with automatic variable capture.

Structs§

Binding
A Binding<T> represents a mutable value of type T that can be observed.
Computed
A wrapper around a boxed implementation of the ComputedImpl trait.
Container
A container for a value that can be observed.

Traits§

CustomBinding
The CustomBinding trait represents a computable value that can also be set.
Project
Trait for projecting bindings into their component parts.
Signal
The core trait for reactive system.
SignalExt
Extension trait providing convenient methods for all Signal types.

Functions§

binding
Creates a new binding from a value with automatic type conversion.
constant
Creates a new constant reactive value.

Derive Macros§

Project
Derive macro for implementing the Project trait on structs.