@fimbul-works/observable
Version:
A lightweight, strongly-typed TypeScript library for reactive programming patterns, providing observable collections, values, and event handling.
116 lines (115 loc) • 3.8 kB
JavaScript
import { Signal } from "./signal";
/**
* Represents a value that can be observed for changes.
* Notifies observers whenever the value is updated.
*
* @template T The type of value being stored and observed
* @implements {Observable<T>}
*/
export class ObservableValue {
/** Internal signal used to emit value changes */
#signal = new Signal();
/** The current value being stored */
#value;
/**
* Creates a new ObservableValue instance.
* @param initial - The initial value to store
*/
constructor(initial) {
this.#value = initial;
}
/**
* Retrieves the current value.
* @returns {T} The current stored value
*/
get() {
return this.#value;
}
/**
* Updates the stored value and notifies observers.
* @param newValue - The new value to store
* @returns {this}
*/
set(newValue) {
if (!Object.is(this.#value, newValue)) {
this.#value = newValue;
this.#signal.emit(newValue);
}
return this;
}
/**
* Updates the stored value and waits for all observer callbacks to complete.
* @param newValue - The new value to store
* @returns {Promise<this>}
*/
async setAsync(newValue) {
if (!Object.is(this.#value, newValue)) {
this.#value = newValue;
await this.#signal.emitAsync(newValue);
}
return this;
}
/**
* Updates the value using a transformation function and notifies observers.
* The update is atomic - observers will only be notified once with the final value.
* @param updateFn - Function that receives the current value and returns the new value
* @returns {this}
*/
update(updateFn) {
const newValue = updateFn(this.#value);
this.set(newValue);
return this;
}
/**
* Updates the value using a transformation function and waits for all observer callbacks to complete.
* @param updateFn - Function that receives the current value and returns the new value
* @returns {Promise<this>}
*/
async updateAsync(updateFn) {
const newValue = updateFn(this.#value);
await this.setAsync(newValue);
return this;
}
/**
* Subscribes to value changes and immediately receives the current value.
* @param fn - Function to be called with the current value and subsequent changes
* @returns {() => void} A cleanup function that removes the event handler
*/
subscribe(fn) {
fn(this.#value); // Immediate call with current value
return this.onChange(fn);
}
/**
* Registers a function to be called when the value changes.
* @param fn - Function to be called with the new value
* @returns {() => void} A cleanup function that removes the event handler
*/
onChange(fn) {
return this.#signal.connect(fn);
}
/**
* Checks if there are any active subscribers.
* @returns {boolean} True if there are any subscribers, false otherwise
*/
hasObservers() {
return this.#signal.hasHandlers();
}
/**
* Returns the number of active subscribers.
* @returns {number} The number of active subscribers
*/
observerCount() {
return this.#signal.listenerCount();
}
/**
* Creates a derived ObservableValue that updates whenever this one changes.
* @template U The type of the derived value
* @param transform - Function to transform the value
* @returns {ObservableValue<U>} A new ObservableValue instance
*/
map(transform) {
const derived = new ObservableValue(transform(this.#value));
this.onChange((value) => derived.set(transform(value)));
return derived;
}
}