spark-signals Svelte Themes

Spark Signals

Fine-grained reactive signals for Rust with TypeScript/Svelte 5 ergonomics

Spark Signals ⚡️

A standalone reactive signals library for Rust. Fine-grained reactivity, zero-overhead, and TypeScript-like ergonomics.

Spark Signals is a high-performance Rust port of the @rlabs-inc/signals TypeScript library. It solves the "hard problems" of Rust reactivity—type erasure, circular dependencies, and borrow checking—while providing an API that feels like writing TypeScript.

Features

  • ⚡️ Blazing Fast: Benchmarked at ~4ns reads, ~22ns writes. Optimized for game engines and TUIs.
  • 🧠 TypeScript-like Ergonomics: derived!, effect!, and prop! macros make Rust feel like a scripting language.
  • 🔄 Deep Reactivity: TrackedSlotArray and TrackedSlot for fine-grained ECS and layout optimization.
  • 🛡️ Memory Safe: Automatic dependency tracking and cleanup with zero unsafe code in the hot path.
  • 🔌 Framework Agnostic: Use it for UI, games, state management, or backend logic.

Installation

[dependencies]
spark-signals = "0.3.0"

The "Pure Magic" Syntax

Spark Signals provides macros that handle Rc cloning and closure moving for you. Just list your dependencies and write code.

Signals & Deriveds

use spark_signals::{signal, derived};

fn main() {
    let width = signal(10);
    let height = signal(20);

    // "Derived depends on width and height"
    // The macro handles cloning 'width' and 'height' for the closure
    let area = derived!(width, height => width.get() * height.get());

    println!("Area: {}", area.get()); // 200

    width.set(5);
    println!("Area: {}", area.get()); // 100
}

Effects

Side effects that run automatically when dependencies change.

use spark_signals::{signal, effect};

let count = signal(0);

// "Effect reads count"
effect!(count => {
    println!("Count changed to: {}", count.get());
});

count.set(1); // Prints: Count changed to: 1

Props (for Components)

Create getters that capture signals effortlessly.

use spark_signals::{signal, prop, reactive_prop};

let first = signal("Sherlock");
let last = signal("Holmes");

// Create a prop getter
let full_name_prop = prop!(first, last => format!("{} {}", first.get(), last.get()));

// Convert to derived for uniform access
let full_name = reactive_prop(full_name_prop);

println!("{}", full_name.get()); // Sherlock Holmes

Advanced Primitives

Slots & Binding

Slot<T> is a stable reference that can switch between static values, signals, or getters. Perfect for component inputs that might change source type at runtime.

use spark_signals::{slot, signal, PropValue};

let s = slot::<i32>(None);
let sig = signal(42);

// Bind to a signal
s.bind(PropValue::from_signal(&sig));
assert_eq!(s.get(), Some(42));

// Bind to a static value
s.bind(PropValue::Static(100));
assert_eq!(s.get(), Some(100));

Tracked Slots (Optimization)

TrackedSlot automatically reports changes to a shared DirtySet. This is critical for optimizing layout engines (like Taffy) or ECS systems where you only want to process changed items.

use spark_signals::{tracked_slot, dirty_set};

let dirty = dirty_set();
// Slot ID 0 reports to 'dirty' set on change
let width = tracked_slot(Some(10), dirty.clone(), 0);

width.set_value(20);

assert!(dirty.borrow().contains(&0)); // We know ID 0 changed!

Shared Memory

Cross-language reactive shared memory primitives for connecting independent reactive graphs (e.g., Rust and TypeScript) through shared memory with zero serialization.

SharedSlotBuffer

Reactive typed arrays backed by external shared memory. get() tracks dependencies via track_read(), set() writes + marks reactions dirty + notifies the other side.

use spark_signals::shared::{SharedSlotBuffer, NoopNotifier};

// Create a buffer over external shared memory
let mut data = vec![0.0f32; 4096];
let buffer = unsafe {
    SharedSlotBuffer::new(
        data.as_mut_ptr(),
        data.len(),
        0.0,
        NoopNotifier,
    )
};

// Reactive read — tracks dependency
let w = buffer.get(0);

// Write — updates memory + marks reactions dirty + notifies
buffer.set(0, 150.0);

// Non-reactive read
let raw = buffer.peek(0);

// Notify reactive graph from external wake (e.g., TS side wrote)
buffer.notify_changed();

Repeater

A new reactive graph primitive that runs inline during mark_reactions. Zero scheduling overhead. Connects any reactive source to a SharedSlotBuffer position.

use spark_signals::primitives::repeater::repeat;

// Bind signal → buffer position
// When width changes, the repeater forwards inline during mark_reactions
let _repeater = repeat(&width_signal, &width_buffer, 0);

// width.set(200) → buffer[0] = 200 during the same mark_reactions pass

Notifier

Pluggable cross-side notification via the Notifier trait.

use spark_signals::shared::notify::{AtomicsNotifier, NoopNotifier, Notifier};

// AtomicsNotifier — atomic store + platform wake (futex on Linux, ulock on macOS)
// NoopNotifier — silent, for testing

Architecture

This library implements the "Push-Pull" reactivity model:

  1. Push: When a signal changes, it marks dependents as DIRTY or MAYBE_DIRTY.
  2. Pull: When a derived is read, it re-executes only if its dependencies are dirty.

It uses a "Father State" pattern (inspired by ECS) where data lives in parallel arrays or stable slots, minimizing object allocation and pointer chasing.

License

MIT

Top categories

Loading Svelte Themes