#pragma once
// SafeC Standard Library — Signal<T>: fine-grained reactive state.
//
// The client-side reactivity primitive for WASM-hydrated scx pages (see
// std/scx/scx.h for the server-side templating half). A Signal owns one
// value; std/wasm/hydrate.h's DOM bindings subscribe listeners onto it so
// that set() both updates the value and pushes the change straight to the
// DOM node(s) that depend on it — SolidJS-style signals, no virtual DOM.
//
// struct Signal<int> count = std::Signal::new(0);
// std::signal_subscribe(&count, on_count_changed, (?&Label)&label);
// count.set(count.get() + 1); // updates, bumps version, notifies
// count.free();
#include <std/collections/vec.h>
#include <std/erased.h>
namespace std {
// One subscriber. Listeners on the same signal carry unrelated context
// types, so each is stored type-erased; signal_subscribe() only ever pairs
// a fn void(?&U) with a ?&U, so the erased call is always well-typed
// (see std/erased.h).
struct SignalSub {
fn void(?&Erased) listener; // the fn void(?&U)
?&Erased userdata; // the ?&U
};
generic<T>
struct Signal {
T value;
unsigned long version; // bumped on every set()
struct Vec<struct SignalSub> subs;
static struct Signal<T> new(T val); // holds val, version 0, no subscribers
T get() const;
&T get_ref(); // in-place access; call notify() after mutating
void set(T val); // stores val, bumps version, notifies
unsigned long get_version() const;
// 1 (and updates *last_seen) if the version moved since *last_seen —
// polling-style change detection instead of signal_subscribe()'s push style.
int changed(&unsigned long last_seen) const;
void notify(); // calls every subscriber in registration order
void unsubscribe_all();
void drop(); // frees the subscriber list
void free() consume; // = drop()
};
generic<T>
struct Signal<T> Signal::new(T val) {
struct Signal<T> s;
s.value = val;
s.version = 0UL;
s.subs = Vec::new();
return s;
}
// Registers listener(userdata) to run on every future set()/notify(). Not
// called for the initial value — read it directly after subscribing.
// A free function: U is its own type parameter, and an instance method of
// a generic struct can't add type parameters of its own yet.
generic<T, U>
void signal_subscribe(&Signal<T> s, fn void(?&U) listener, ?&U userdata) {
struct SignalSub sub;
unsafe {
sub.listener = (fn void(?&Erased))listener;
sub.userdata = (?&Erased)userdata;
}
s.subs.push(sub);
}
// A copy of the value. For a value that owns something (a String), a copy
// would share it: use get_ref() (or clone what it refers to) instead.
generic<T>
T Signal::get() const {
if const (needs_drop(T)) panic_at("Signal::get: the value owns resources; use get_ref()", "signal.h", 0);
unsafe { return self.value; }
}
generic<T>
&T Signal::get_ref() {
unsafe { return &self.value; }
}
generic<T>
void Signal::set(T val) {
drop(std::replace(&self.value, val)); // the old value
self.version = self.version + 1UL;
self.notify();
}
generic<T>
unsigned long Signal::get_version() const { return self.version; }
generic<T>
int Signal::changed(&unsigned long last_seen) const {
if (*last_seen == self.version) return 0;
*last_seen = self.version;
return 1;
}
generic<T>
void Signal::notify() {
unsigned long i = 0UL;
while (i < self.subs.length()) {
&SignalSub sub = self.subs.at(i);
sub.listener(sub.userdata);
i = i + 1UL;
}
}
generic<T>
void Signal::unsubscribe_all() { self.subs.clear(); }
generic<T>
void Signal::free() { self.drop(); }
generic<T>
void Signal::drop() {
self.subs.free();
drop(self.value);
}
} // namespace std