signal
signal<T>(initialValue: T): Signal<T>A reactive value. The accessor is .value.
interface Signal<T = unknown> {
value: T;
subscribe(callback: (value: T) => void, options?: { immediate?: boolean }): () => void;
}Usage
import { signal } from '@studiometa/js-toolkit';
const count = signal(0);
const unsubscribe = count.subscribe((value) => console.log(value), { immediate: true });
count.value = 1;
count.value += 1;
unsubscribe();Parameters
initialValue(T).
Return value
Signal<T>.subscribe()returns the unsubscribe function.
A write settles synchronously, and the newest value wins
The delivery loop re-reads the value after each callback. If the value moved, the loop abandons the round and starts again on the new value, so a subscriber that was not reached yet skips the old value entirely.
Delivery stays in the same task. There is no batching and no microtask.
A subscriber that writes on every delivery live-locks the loop
Each write restarts the round, so the loop never finishes. Guard the write, or move it out of the subscriber.
It is a factory over a closure
There is no class and no proxy. signal() closes over the value and a subscriber set, which is why it is the right thing to provide: the value crossing a context boundary is the signal itself, and the type of the key says so.
class Counter extends Base {
static config = { name: 'Counter' };
count = this.$provide(CountContext, signal(0));
increment() {
this.count.value += 1;
}
}
class CounterOutput extends Base {
static config = { name: 'CounterOutput' };
async mounted() {
const count = await this.$inject(CountContext);
return count.subscribe((value) => {
this.$el.textContent = String(value);
});
}
}Failures
A subscriber that throws is isolated and reported as callback.signal-failed, so one subscriber cannot stop the others.
toggle() works on it
toggle(subscribe) takes anything that returns its own unsubscribe function, a Signal included.