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.
derived!, effect!, and prop! macros make Rust feel like a scripting language.TrackedSlotArray and TrackedSlot for fine-grained ECS and layout optimization.[dependencies]
spark-signals = "0.3.0"
Spark Signals provides macros that handle Rc cloning and closure moving for you. Just list your dependencies and write code.
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
}
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
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
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));
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!
Cross-language reactive shared memory primitives for connecting independent reactive graphs (e.g., Rust and TypeScript) through shared memory with zero serialization.
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();
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
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
This library implements the "Push-Pull" reactivity model:
DIRTY or MAYBE_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.
MIT