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!, andprop!macros make Rust feel like a scripting language. - 🔄 Deep Reactivity:
TrackedSlotArrayandTrackedSlotfor 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
[]
= "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 ;
Effects
Side effects that run automatically when dependencies change.
use ;
let count = signal;
// "Effect reads count"
effect!;
count.set; // Prints: Count changed to: 1
Props (for Components)
Create getters that capture signals effortlessly.
use ;
let first = signal;
let last = signal;
// Create a prop getter
let full_name_prop = prop!;
// Convert to derived for uniform access
let full_name = reactive_prop;
println!; // 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 ;
let s = ;
let sig = signal;
// Bind to a signal
s.bind;
assert_eq!;
// Bind to a static value
s.bind;
assert_eq!;
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 ;
let dirty = dirty_set;
// Slot ID 0 reports to 'dirty' set on change
let width = tracked_slot;
width.set_value;
assert!; // 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 ;
// Create a buffer over external shared memory
let mut data = vec!;
let buffer = unsafe ;
// Reactive read — tracks dependency
let w = buffer.get;
// Write — updates memory + marks reactions dirty + notifies
buffer.set;
// Non-reactive read
let raw = buffer.peek;
// 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 repeat;
// Bind signal → buffer position
// When width changes, the repeater forwards inline during mark_reactions
let _repeater = repeat;
// width.set(200) → buffer[0] = 200 during the same mark_reactions pass
Notifier
Pluggable cross-side notification via the Notifier trait.
use ;
// AtomicsNotifier — atomic store + platform wake (futex on Linux, ulock on macOS)
// NoopNotifier — silent, for testing
Architecture
This library implements the "Push-Pull" reactivity model:
- Push: When a signal changes, it marks dependents as
DIRTYorMAYBE_DIRTY. - 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