rvx
Version:
A signal based rendering library
368 lines (357 loc) • 8.31 kB
JavaScript
/*!
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 };