54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
/**
|
|
* A minimal, type-safe reactive signal with zero dependencies.
|
|
*
|
|
* This is the seed of the framework's reactivity. Today it powers nothing on
|
|
* its own, but it is shaped so client islands (and later the `.wrn` compiler's
|
|
* `state` blocks) can build reactive bindings on top of it.
|
|
*
|
|
* const count = signal(0)
|
|
* count.get() // 0
|
|
* count.set(1) // notifies subscribers
|
|
* const off = count.subscribe(v => console.log(v))
|
|
* off() // unsubscribe
|
|
*/
|
|
|
|
export type Subscriber<T> = (value: T) => void;
|
|
export type Unsubscribe = () => void;
|
|
|
|
export interface Signal<T> {
|
|
/** Read the current value. */
|
|
get(): T;
|
|
/** Write a new value; subscribers run only when the value actually changes. */
|
|
set(next: T): void;
|
|
/** Apply a function to the current value. */
|
|
update(fn: (current: T) => T): void;
|
|
/** Subscribe to changes; returns an unsubscribe function. */
|
|
subscribe(fn: Subscriber<T>): Unsubscribe;
|
|
}
|
|
|
|
export function signal<T>(initial: T): Signal<T> {
|
|
let value = initial;
|
|
const subscribers = new Set<Subscriber<T>>();
|
|
|
|
return {
|
|
get(): T {
|
|
return value;
|
|
},
|
|
set(next: T): void {
|
|
if (Object.is(next, value)) return; // skip no-op updates
|
|
value = next;
|
|
// Iterate a copy so a subscriber may unsubscribe during notification.
|
|
for (const fn of [...subscribers]) fn(value);
|
|
},
|
|
update(fn: (current: T) => T): void {
|
|
this.set(fn(value));
|
|
},
|
|
subscribe(fn: Subscriber<T>): Unsubscribe {
|
|
subscribers.add(fn);
|
|
return () => {
|
|
subscribers.delete(fn);
|
|
};
|
|
},
|
|
};
|
|
}
|