Skip to main content

Crate tincan

Crate tincan 

Source
Expand description

§Tincan - A Fast, Lock-Free Reactive Primitives Library

Tincan provides fine-grained reactivity with signals, computed values (memos), and side effects. It’s designed for building reactive applications with automatic dependency tracking and efficient updates.

§Overview

Tincan uses a context object pattern where all reactive primitives are created through a Scope. This provides explicit ownership, automatic cleanup, and lock-free performance for single-threaded contexts.

§Core Concepts

  • Scope: The reactive context that owns all reactive primitives
  • Signal: Mutable reactive state that notifies observers when changed
  • Memo: Computed values that cache results and only recompute when dependencies change
  • Effect: Side effects that automatically re-run when dependencies change

§Quick Start

use tincan::Scope;

// Create a reactive scope
let cx = Scope::new();

// Create a signal (reactive state)
let count = cx.signal(0);

// Create a memo (computed value)
let doubled = cx.memo({
    let count = count.clone();
    move || count.get() * 2
});

// Create an effect (side effect)
let count_clone = count.clone();
let doubled_clone = doubled.clone();
cx.effect(move || {
    println!("Count: {}, Doubled: {}", count_clone.get(), doubled_clone.get());
});
// Prints: Count: 0, Doubled: 0

// Update the signal
count.set(5);
// Effect runs again, prints: Count: 5, Doubled: 10

§Features

§Automatic Dependency Tracking

Effects and memos automatically track which signals they read:

use tincan::Scope;

let cx = Scope::new();
let first = cx.signal("John");
let last = cx.signal("Doe");

let full_name = cx.memo({
    let first = first.clone();
    let last = last.clone();
    move || format!("{} {}", first.get(), last.get())
});

assert_eq!(full_name.get(), "John Doe");

first.set("Jane");
assert_eq!(full_name.get(), "Jane Doe");

§Efficient Updates

Memos cache their values and only recompute when dependencies change:

use tincan::Scope;
use std::cell::Cell;
use std::rc::Rc;

let cx = Scope::new();
let input = cx.signal(5);
let compute_count = Rc::new(Cell::new(0));

let expensive = cx.memo({
    let input = input.clone();
    let compute_count = Rc::clone(&compute_count);
    move || {
        compute_count.set(compute_count.get() + 1);
        input.get() * 2
    }
});

assert_eq!(expensive.get(), 10);
assert_eq!(compute_count.get(), 1);

// Reading again uses cached value
assert_eq!(expensive.get(), 10);
assert_eq!(compute_count.get(), 1); // Not recomputed!

// Only recomputes when input changes
input.set(10);
assert_eq!(expensive.get(), 20);
assert_eq!(compute_count.get(), 2); // Recomputed once

§Isolation and Cleanup

Scopes provide automatic cleanup and isolation:

use tincan::Scope;

// Each scope is completely independent
let cx1 = Scope::new();
let signal1 = cx1.signal(1);

let cx2 = Scope::new();
let signal2 = cx2.signal(2);

// They don't interfere with each other
assert_eq!(signal1.get(), 1);
assert_eq!(signal2.get(), 2);

// Dropping the scope cleans up all resources
drop(cx1);
// signal1 is now unusable (would panic if accessed)

§Signal Combinators

Transform and combine signals:

use tincan::Scope;

let cx = Scope::new();
let celsius = cx.signal(0);

// Map: Transform a signal
let fahrenheit = celsius.map(|c| c * 9 / 5 + 32);
assert_eq!(fahrenheit.get(), 32);

celsius.set(100);
assert_eq!(fahrenheit.get(), 212);

// Zip: Combine two signals
let width = cx.signal(10);
let height = cx.signal(5);
let area = width.clone().zip(height).map(|(w, h)| w * h);
assert_eq!(area.get(), 50);

§Watching for Changes

React to specific signal changes:

use tincan::Scope;
use std::cell::Cell;
use std::rc::Rc;

let cx = Scope::new();
let count = cx.signal(0);
let call_count = Rc::new(Cell::new(0));

let call_count_clone = Rc::clone(&call_count);
let _guard = count.watch(move |_value| {
    call_count_clone.set(call_count_clone.get() + 1);
});

assert_eq!(call_count.get(), 1); // Called immediately

count.set(5);
assert_eq!(call_count.get(), 2); // Called on change

§Testing

Scopes make testing easy:

use tincan::Scope;

#[test]
fn test_counter() {
    let cx = Scope::new();
    let count = cx.signal(0);
     
    count.set(42);
    assert_eq!(count.get(), 42);
} // Scope dropped, everything cleaned up

§Examples

See the examples/ directory for more complete examples:

  • basic_signals.rs - Simple signal operations
  • effects.rs - Working with effects
  • memos.rs - Computed values and caching

Re-exports§

pub use effect::Effect;
pub use memo::Memo;
pub use runtime::Scope;
pub use signal::Signal;
pub use signal::WatchGuard;

Modules§

effect
memo
runtime
signal