UNPKG

@zeix/cause-effect

Version:

Cause & Effect - reactive state management primitives library for TypeScript.

183 lines (168 loc) 4.82 kB
import { validateCallback, validateReadValue, validateSignalValue, } from '../errors' import { batchDepth, type ComputedOptions, DEFAULT_EQUALITY, FLAG_DIRTY, flush, makeSubscribe, propagate, refresh, type SinkNode, type TaskCallback, type TaskNode, TYPE_TASK, type StateNode, setState, } from '../graph' import { isAsyncFunction, isSignalOfType } from '../util' /* === Types === */ /** * An asynchronous reactive computation (colorless async). * Automatically tracks dependencies and re-executes when they change. * Provides abort semantics and pending state tracking. * * @template T - The type of value resolved by the task */ type Task<T extends {}> = { readonly [Symbol.toStringTag]: 'Task' /** * Gets the current value of the task. * Returns the last resolved value, even while a new computation is pending. * When called inside another reactive context, creates a dependency. * @returns The current value * @throws UnsetSignalValueError If the task value is still unset when read. */ get(): T /** * Checks if the task is currently executing. * Used by `match()` to route to the `stale` handler when the task has a retained value. * @returns True if a computation is in progress */ isPending(): boolean /** * Aborts the current computation if one is running. * The task's AbortSignal will be triggered. */ abort(): void } /* === Exported Functions === */ /** * Creates an asynchronous reactive computation (colorless async). * The computation automatically tracks dependencies and re-executes when they change. * Provides abort semantics - in-flight computations are aborted when dependencies change. * * @since 0.18.0 * @template T - The type of value resolved by the task * @param fn - The async computation function that receives the previous value and an AbortSignal * @param options - Optional configuration for the task * @param options.value - Optional initial value for reducer patterns * @param options.equals - Optional equality function. Defaults to strict equality (`===`) * @param options.guard - Optional type guard to validate values * @param options.watched - Optional callback invoked when the task is first watched by an effect. * Receives an `invalidate` function to mark the task dirty and trigger re-execution. * Must return a cleanup function called when no effects are watching. * @returns A Task object with get(), isPending(), and abort() methods * * @example * ```ts * const userId = createState(1); * const user = createTask(async (prev, signal) => { * const response = await fetch(`/api/users/${userId.get()}`, { signal }); * return response.json(); * }); * * // When userId changes, the previous fetch is aborted * userId.set(2); * ``` * * @example * ```ts * // Check pending state * if (user.isPending()) { * console.log('Loading...'); * } * ``` */ function createTask<T extends {}>( fn: (prev: T, signal: AbortSignal) => Promise<T>, options: ComputedOptions<T> & { value: T }, ): Task<T> function createTask<T extends {}>( fn: TaskCallback<T>, options?: ComputedOptions<T>, ): Task<T> function createTask<T extends {}>( fn: TaskCallback<T>, options?: ComputedOptions<T>, ): Task<T> { validateCallback(TYPE_TASK, fn, isAsyncFunction) if (options?.value !== undefined) validateSignalValue(TYPE_TASK, options.value, options?.guard) const pendingNode: StateNode<boolean> = { value: false, sinks: null, sinksTail: null, equals: DEFAULT_EQUALITY, } const node: TaskNode<T> = { fn, value: options?.value as T, sources: null, sourcesTail: null, sinks: null, sinksTail: null, flags: FLAG_DIRTY, equals: options?.equals ?? DEFAULT_EQUALITY, controller: undefined, error: undefined, stop: undefined, pendingNode, } const watched = options?.watched const subscribe = makeSubscribe( node, watched ? () => watched(() => { propagate(node as unknown as SinkNode) if (batchDepth === 0) flush() }) : undefined, ) const pendingSubscribe = makeSubscribe(pendingNode) return { [Symbol.toStringTag]: TYPE_TASK, get(): T { subscribe() refresh(node as unknown as SinkNode) if (node.error) throw node.error validateReadValue(TYPE_TASK, node.value) return node.value }, isPending(): boolean { pendingSubscribe() return node.pendingNode.value }, abort(): void { node.controller?.abort() node.controller = undefined setState(node.pendingNode, false) }, } } /** * Checks if a value is a Task signal. * * @since 0.18.0 * @param value - The value to check * @returns True if the value is a Task */ function isTask<T extends {} = unknown & {}>(value: unknown): value is Task<T> { return isSignalOfType(value, TYPE_TASK) } export { createTask, isTask, type Task }