UNPKG

@zeix/cause-effect

Version:

Cause & Effect - reactive state management with signals.

173 lines (152 loc) 3.9 kB
/* === Types === */ type Cleanup = () => void type Watcher = { (): void off(cleanup: Cleanup): void cleanup(): void } type Updater = <T>() => T | boolean | void /* === Internal === */ // Currently active watcher let active: Watcher | undefined // Pending queue for batched change notifications const pending = new Set<Watcher>() let batchDepth = 0 // Map of deduplication symbols to update functions (using Symbol keys prevents unintended overwrites) const updateMap = new Map<symbol, Updater>() let requestId: number | undefined const updateDOM = () => { requestId = undefined const updates = Array.from(updateMap.values()) updateMap.clear() for (const update of updates) { update() } } const requestTick = () => { if (requestId) cancelAnimationFrame(requestId) requestId = requestAnimationFrame(updateDOM) } // Initial render when the call stack is empty queueMicrotask(updateDOM) /* === Functions === */ /** * Create a watcher that can be used to observe changes to a signal * * @since 0.14.1 * @param {() => void} notice - function to be called when the state changes * @returns {Watcher} - watcher object with off and cleanup methods */ const watch = (notice: () => void): Watcher => { const cleanups = new Set<Cleanup>() const w = notice as Partial<Watcher> w.off = (on: Cleanup) => { cleanups.add(on) } w.cleanup = () => { for (const cleanup of cleanups) { cleanup() } cleanups.clear() } return w as Watcher } /** * Add active watcher to the Set of watchers * * @param {Set<Watcher>} watchers - watchers of the signal */ const subscribe = (watchers: Set<Watcher>) => { if (active && !watchers.has(active)) { const watcher = active watchers.add(watcher) active.off(() => { watchers.delete(watcher) }) } } /** * Add watchers to the pending set of change notifications * * @param {Set<Watcher>} watchers - watchers of the signal */ const notify = (watchers: Set<Watcher>) => { for (const watcher of watchers) { if (batchDepth) pending.add(watcher) else watcher() } } /** * Flush all pending changes to notify watchers */ const flush = () => { while (pending.size) { const watchers = Array.from(pending) pending.clear() for (const watcher of watchers) { watcher() } } } /** * Batch multiple changes in a single signal graph and DOM update cycle * * @param {() => void} fn - function with multiple signal writes to be batched */ const batch = (fn: () => void) => { batchDepth++ try { fn() } finally { flush() batchDepth-- } } /** * Run a function in a reactive context * * @param {() => void} run - function to run the computation or effect * @param {Watcher} watcher - function to be called when the state changes or undefined for temporary unwatching while inserting auto-hydrating DOM nodes that might read signals (e.g., web components) */ const observe = (run: () => void, watcher?: Watcher): void => { const prev = active active = watcher try { run() } finally { active = prev } } /** * Enqueue a function to be executed on the next animation frame * * If the same Symbol is provided for multiple calls before the next animation frame, * only the latest call will be executed (deduplication). * * @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void * @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed */ const enqueue = <T>(fn: Updater, dedupe?: symbol) => new Promise<T | boolean | void>((resolve, reject) => { updateMap.set(dedupe || Symbol(), () => { try { resolve(fn()) } catch (error) { reject(error) } }) requestTick() }) /* === Exports === */ export { type Cleanup, type Watcher, type Updater, subscribe, notify, flush, batch, watch, observe, enqueue, }