@zeix/cause-effect
Version:
Cause & Effect - reactive state management primitives library for TypeScript.
156 lines (144 loc) • 4.63 kB
text/typescript
import {
validateCallback,
validateReadValue,
validateSignalValue,
} from '../errors'
import {
activeSink,
type Cleanup,
DEFAULT_EQUALITY,
link,
type SignalOptions,
type StateNode,
setState,
TYPE_SENSOR,
} from '../graph'
import { isSignalOfType, isSyncFunction } from '../util'
/* === Types === */
/**
* A read-only signal that tracks external input and updates a state value as long as it is active.
*
* @template T - The type of value produced by the sensor
*/
type Sensor<T extends {}> = {
readonly [Symbol.toStringTag]: 'Sensor'
/**
* Gets the current value of the sensor.
* When called inside another reactive context, creates a dependency.
* @returns The sensor value
* @throws UnsetSignalValueError If the sensor value is still unset when read.
*/
get(): T
}
/**
* Configuration options for `createSensor`.
*
* @template T - The type of value produced by the sensor
*/
type SensorOptions<T extends {}> = SignalOptions<T> & {
/**
* Optional initial value. Avoids `UnsetSignalValueError` on first read
* before the watched callback fires.
*/
value?: T
}
/**
* Setup callback for `createSensor`. Invoked when the sensor gains its first downstream
* subscriber; receives a `set` function to push new values into the graph.
*
* @template T - The type of value produced by the sensor
* @param set - Updates the sensor value and propagates the change to subscribers
* @returns A cleanup function invoked when the sensor loses all subscribers
*/
type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
/* === Exported Functions === */
/**
* Creates a sensor that tracks external input and updates a state value as long as it is active.
* Sensors get activated when they are first accessed by an effect and deactivated when they are
* no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
*
* @since 0.18.0
* @template T - The type of value produced by the sensor
* @param watched - The callback invoked when the sensor starts being watched, receives a `set` function and returns a cleanup function.
* @param options - Optional configuration for the sensor.
* @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
* before the watched callback fires. Essential for the mutable-object observation pattern.
* @param options.equals - Optional equality function. Defaults to strict equality (`===`). Use `SKIP_EQUALITY`
* for mutable objects where the reference stays the same but internal state changes.
* @param options.guard - Optional type guard to validate values.
* @returns A read-only sensor signal.
*
* @example Tracking external values
* ```ts
* const mousePos = createSensor<{ x: number; y: number }>((set) => {
* const handler = (e: MouseEvent) => {
* set({ x: e.clientX, y: e.clientY });
* };
* window.addEventListener('mousemove', handler);
* return () => window.removeEventListener('mousemove', handler);
* });
* ```
*
* @example Observing a mutable object
* ```ts
* import { createSensor, SKIP_EQUALITY } from 'cause-effect';
*
* const el = createSensor<HTMLElement>((set) => {
* const node = document.getElementById('box')!;
* set(node);
* const obs = new MutationObserver(() => set(node));
* obs.observe(node, { attributes: true });
* return () => obs.disconnect();
* }, { value: node, equals: SKIP_EQUALITY });
* ```
*/
function createSensor<T extends {}>(
watched: SensorCallback<T>,
options?: SensorOptions<T>,
): Sensor<T> {
validateCallback(TYPE_SENSOR, watched, isSyncFunction)
if (options?.value !== undefined)
validateSignalValue(TYPE_SENSOR, options.value, options?.guard)
const node: StateNode<T> = {
value: options?.value as T,
sinks: null,
sinksTail: null,
equals: options?.equals ?? DEFAULT_EQUALITY,
guard: options?.guard,
stop: undefined,
}
return {
[Symbol.toStringTag]: TYPE_SENSOR,
get(): T {
if (activeSink) {
if (!node.sinks)
node.stop = watched((next: T): void => {
validateSignalValue(TYPE_SENSOR, next, node.guard)
setState(node, next)
})
link(node, activeSink)
}
validateReadValue(TYPE_SENSOR, node.value)
return node.value
},
}
}
/**
* Checks if a value is a Sensor signal.
*
* @since 0.18.0
* @param value - The value to check
* @returns True if the value is a Sensor
*/
function isSensor<T extends {} = unknown & {}>(
value: unknown,
): value is Sensor<T> {
return isSignalOfType(value, TYPE_SENSOR)
}
export {
createSensor,
isSensor,
type Sensor,
type SensorCallback,
type SensorOptions,
}