UNPKG

rvx

Version:

A signal based rendering library

368 lines (357 loc) 8.31 kB
/*! This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { teardown, Context, $, isolate, nest, ENV, watch, capture, captureSelf } from './rvx.js'; function useAbortController(reason) { const controller = new AbortController(); teardown(() => controller.abort(reason)); return controller; } function useAbortSignal(reason) { return useAbortController(reason).signal; } class AsyncContext { #parent; #tasks = $(new Set()); #errorHandlers = new Set(); constructor(parent) { this.#parent = parent; } get pending() { return this.#tasks.value.size > 0; } track(task) { this.#parent?.track(task); this.#tasks.inert.add(task); this.#tasks.notify(); task.then(() => { this.#tasks.inert.delete(task); this.#tasks.notify(); }, error => { if (this.#errorHandlers.size > 0) { for (const errorHandler of this.#errorHandlers) { errorHandler.push(error); } } else { void Promise.reject(error); } this.#tasks.inert.delete(task); this.#tasks.notify(); }); } async complete() { const errors = []; this.#errorHandlers.add(errors); while (this.#tasks.value.size > 0) { await Promise.allSettled(this.#tasks.value); } this.#errorHandlers.delete(errors); if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { throw new AsyncError(errors); } } static fork() { return new AsyncContext(ASYNC.current); } } class AsyncError extends Error { errors; constructor(errors) { super(); this.errors = errors; } } const ASYNC = new Context(); function nestAsync(source, component, pending, rejected) { const state = $({ type: 0, value: undefined }); let promise; if (typeof source === "function") { promise = isolate(source); } else { promise = source; } const ac = ASYNC.current; promise.then(value => { state.value = { type: 1, value }; }, (value) => { state.value = { type: 2, value }; if (ac === undefined && rejected === undefined) { void Promise.reject(value); } }); ac?.track(promise); return nest(state, state => { switch (state.type) { case 0: return pending?.(); case 1: return component?.(state.value); case 2: return rejected?.(state.value); } }); } function Async(props) { return nestAsync(props.source, props.children, props.pending, props.rejected); } class Queue { #queue = []; #blocked = -1; #controller = undefined; #running = undefined; constructor() { teardown(() => this.#abort()); } #abort() { const queue = this.#queue; while (queue.length > 0 && !queue[0].blocking) { queue.shift(); this.#blocked--; } this.#controller?.abort(); } #run() { if (this.#running === undefined) { this.#running = (async () => { let task; while (task = this.#queue.shift()) { this.#blocked--; if (task.blocking) { try { task.resolve(await task.task()); } catch (error) { task.reject(error); } } else { const controller = new AbortController(); this.#controller = controller; try { await task.task(controller.signal); } catch (error) { void Promise.reject(error); } this.#controller = undefined; } } this.#blocked--; this.#running = undefined; })(); } } sideEffect(task) { if (this.#blocked >= 0) { return; } this.#abort(); this.#queue.push({ blocking: false, task, resolve: undefined, reject: undefined }); this.#run(); } block(task) { return new Promise((resolve, reject) => { this.#abort(); this.#blocked = this.#queue.push({ blocking: true, task, resolve, reject }); this.#run(); }); } } class Tasks { #pendingCount = 0; #pending = $(false); #restoreFocus; #parent; constructor(parent, options) { this.#parent = parent; this.#restoreFocus = options?.restoreFocus ?? (parent ? parent.#restoreFocus : true); if (this.#restoreFocus) { const env = ENV.current; let last = null; watch(this.#pending, pending => { if (pending) { last = env.document.activeElement; } else if (last && env.document.activeElement === env.document.body) { const target = last; queueMicrotask(() => { if (last === target && env.document.activeElement === env.document.body) { target.focus?.(); } }); } }); } } #setPending() { this.#pendingCount++; this.#pending.value = true; } #unsetPending() { this.#pendingCount--; this.#pending.value = this.#pendingCount > 0; } get parent() { return this.#parent; } get selfPending() { return this.#pending.value; } get pending() { return (this.#parent?.pending ?? false) || this.#pending.value; } setPending() { this.#setPending(); let disposed = false; teardown(() => { if (!disposed) { disposed = true; this.#unsetPending(); } }); } waitFor(source) { if (typeof source === "function") { this.#setPending(); void (async () => { try { return await isolate(source); } catch (error) { void Promise.reject(error); } finally { this.#unsetPending(); } })(); } else if (source instanceof Promise) { this.#setPending(); void source.finally(() => this.#unsetPending()); } } static fork(options) { return new Tasks(TASKS.current, options); } } const TASKS = new Context(); function isSelfPending() { return TASKS.current?.selfPending ?? false; } function isPending() { return TASKS.current?.pending ?? false; } function useMicrotask(callback) { callback = Context.bind(callback); let active = true; let dispose; teardown(() => { active = false; dispose?.(); }); queueMicrotask(() => { if (active) { dispose = capture(callback); if (!active) { dispose?.(); } } }); } function useTimeout(callback, timeout) { callback = Context.bind(callback); let active = true; let dispose; let handle; teardown(() => { active = false; clearTimeout(handle); dispose?.(); }); handle = setTimeout(() => { dispose = capture(callback); if (!active) { dispose(); } }, timeout); } function useInterval(callback, interval) { callback = Context.bind(callback); let active = true; let dispose; let handle; teardown(() => { active = false; clearInterval(handle); dispose?.(); }); handle = setInterval(() => { dispose?.(); dispose = undefined; dispose = capture(callback); if (!active) { dispose(); } }, interval); } function useAnimation(callback) { callback = Context.bind(callback); let active = true; let dispose; let handle; teardown(() => { active = false; cancelAnimationFrame(handle); dispose?.(); }); handle = requestAnimationFrame(function next(now) { handle = requestAnimationFrame(next); dispose?.(); dispose = undefined; dispose = capture(() => callback(now)); if (!active) { dispose(); dispose = undefined; } }); } class WatchForTimeoutError extends Error { } function watchFor(expr, condition, timeout) { if (typeof condition === "number") { timeout = condition; condition = Boolean; } else if (condition === undefined) { condition = Boolean; } return new Promise((resolve, reject) => { captureSelf(dispose => { watch(expr, value => { if (condition(value)) { dispose(); resolve(value); } }); if (timeout !== undefined) { const handle = setTimeout(() => { dispose(); reject(new WatchForTimeoutError()); }, timeout); teardown(() => clearTimeout(handle)); } }); }); } export { ASYNC, Async, AsyncContext, AsyncError, Queue, TASKS, Tasks, WatchForTimeoutError, isPending, isSelfPending, nestAsync, useAbortController, useAbortSignal, useAnimation, useInterval, useMicrotask, useTimeout, watchFor };