rvx
Version:
A signal based rendering library
338 lines (324 loc) • 12.3 kB
TypeScript
/*!
MIT License
Copyright (c) 2025 Max J. Polster
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Context } from './rvx.js';
import { Component, Falsy } from './rvx.js';
import { View } from './rvx.js';
import { Expression } from './rvx.js';
/**
* Create a new abort controller that aborts when the current lifecycle is disposed.
*/
declare function useAbortController(reason?: unknown): AbortController;
/**
* Get an abort signal that aborts when the current lifecycle is disposed.
*/
declare function useAbortSignal(reason?: unknown): AbortSignal;
interface AsyncContextParent {
/**
* Called by async contexts to track a pending task.
*/
track(task: Promise<unknown>): void;
}
/**
* Represents pending operations in an asynchronously rendered tree.
*
* This can be used to wait until an entire async tree is rendered or to check if any unhandled errors occurred.
*/
declare class AsyncContext {
#private;
constructor(parent?: AsyncContextParent);
/**
* Reactively check if there are any pending tasks in this context.
*
* @example
* ```tsx
* <Show when={() => asyncCtx.pending}>
* <div class="overlay">Please wait...</div>
* </Show>
* ```
*/
get pending(): boolean;
/**
* Track the specified task in this and all parent contexts.
*/
track(task: Promise<unknown>): void;
/**
* Wait until all tracked tasks in this and all child contexts have completed.
*
* This also includes new tasks that are tracked while waiting.
*
* @throws Errors thrown by any tracked task or an {@link AsyncError} if multiple tasks failed.
*/
complete(): Promise<void>;
/**
* Create a new async context using the current context as parent.
*/
static fork(): AsyncContext;
}
/**
* Thrown by {@link AsyncContext.complete} if multiple unhandled {@link errors} occurred.
*/
declare class AsyncError extends Error {
errors: unknown[];
constructor(errors: unknown[]);
}
/**
* Context for the current {@link AsyncContext}.
*/
declare const ASYNC: Context<AsyncContext | undefined>;
/**
* Render content depending on the state of an async function or promise.
*
* See {@link Async `<Async>`} when using JSX or when named properties are preferred.
*
* This task is tracked using the current {@link ASYNC async context} if any. It is guaranteed, that the view is updated before the tracked task completes.
*
* @param source The async function or promise.
* + If this is a function, it runs {@link isolate isolated}.
* @param component A component to render content when resolved.
* + The resolved value is passed as the first argument.
* + Nothing is rendered by default.
* @param pending A component to render while pending.
* + Nothing is rendered by default.
* @param rejected A component to render content when rejected.
* + The rejected error is passed as the first argument.
* + Nothing is rendered by default.
*/
declare function nestAsync<T>(source: (() => Promise<T>) | Promise<T>, component?: Component<T>, pending?: Component, rejected?: Component<unknown>): View;
/**
* Render content depending on the state of an async function or promise.
*
* See {@link nestAsync} when not using JSX or when positional arguments are preferred.
*
* This task is tracked using the current {@link ASYNC async context} if any. It is guaranteed, that the view is updated before the tracked task completes.
*/
declare function Async<T>(props: {
/**
* The async function or promise.
*
* If this is a function, it runs {@link isolate isolated}.
*/
source: (() => Promise<T>) | Promise<T>;
/**
* A component to render content when resolved.
*
* + The resolved value is passed as the first argument.
* + Nothing is rendered by default.
*/
children?: Component<T>;
/**
* A component render content while pending.
*
* + Nothing is rendered by default.
*/
pending?: Component;
/**
* A component to render content when rejected.
*
* + The rejected error is passed as the first argument.
* + Nothing is rendered by default.
*/
rejected?: Component<unknown>;
}): View;
/**
* A queue for sequentially running async tasks that can be triggered by both the user and side effects.
*/
declare class Queue {
#private;
/**
* Create a new queue.
*
* When the current lifecycle is disposed, all side effects are aborted and removed from the queue.
*/
constructor();
/**
* Queue a side effect to run if this queue isn't currently blocked.
*
* This will abort and remove all other side effects from the queue.
*
* @param task The side effect to queue.
*/
sideEffect(task: (signal: AbortSignal) => unknown | Promise<unknown>): void;
/**
* Queue a task to run and block this queue until it completes.
*
* This will abort and remove all other side effects from the queue.
*
* @param task The blocking task to queue.
* @returns The result of the task.
*/
block<T>(task: () => T | Promise<T>): Promise<T>;
}
type TaskSource = (() => unknown) | Promise<unknown> | null | undefined;
interface TasksOptions {
/**
* If true, focus is restored on the last active element when there are no more pending tasks in this instance.
*
* By default, this is inherited from the parent or true of there is none.
*/
restoreFocus?: boolean;
}
/**
* Represents a set of pending tasks in a specific context.
*
* This is meant to be used for preventing concurrent user interaction in a specific context.
*/
declare class Tasks {
#private;
/**
* Create a new tasks instance with the specified parent.
*
* @param parent The parent to use. Default is no parent.
*/
constructor(parent?: Tasks, options?: TasksOptions);
/**
* The parent instance or undefined if there is none.
*/
get parent(): Tasks | undefined;
/**
* True if this instance has any pending tasks.
*
* @example
* ```tsx
* <div inert={() => tasks.selfPending}>...</div>
* ```
*/
get selfPending(): boolean;
/**
* True if this instance or any of it's parents has any pending tasks.
*
* @example
* ```tsx
* <button disabled={() => tasks.pending}>...</button>
* ```
*/
get pending(): boolean;
/**
* Pretend, that there is a pending task until the current lifecycle is disposed.
*/
setPending(): void;
/**
* Wait for an async function or a promise.
*
* @param source The async function or promise to wait for.
* + If this is a function, it runs {@link isolate isolated}.
*/
waitFor(source: TaskSource): void;
/**
* Create a new tasks instance using the current instance as parent.
*/
static fork(options?: TasksOptions): Tasks;
}
/**
* Context for the current {@link Tasks} instance.
*/
declare const TASKS: Context<Tasks | undefined>;
/**
* Check if there are any pending tasks in the current tasks instance.
*
* This can be used in conjuction with {@link Tasks.prototype.waitFor `TASKS.current.waitFor`} to indicate if there are any pending tasks.
*
* This is meant to be used for preventing concurrent user interaction in a specific context.
*
* @example
* ```tsx
* <div inert={isSelfPending}>...</div>
* ```
*/
declare function isSelfPending(): boolean;
/**
* Check if there are any {@link Tasks.prototype.pending pending} tasks in the current tasks instance or any of it's parents.
*
* This can be used in conjunction with {@link Tasks.prototype.waitFor `TASKS.current.waitFor`} to disable inputs and buttons while there are any pending tasks.
*
* This is meant to be used for preventing concurrent user interaction in a specific context.
*
* @example
* ```tsx
* <button disabled={isPending}>...</button>
* ```
*/
declare function isPending(): boolean;
/**
* @deprecated Use `TASKS.current.setPending()` instead. This silently fails if there is no `Tasks` instance in the current context.
*/
declare function setPending(): void;
/**
* @deprecated Use {@link Tasks.prototype.waitFor `TASKS.current.waitFor`} insead. This silently fails if there is no `Tasks` instance in the current context.
*/
declare function waitFor(source: TaskSource): void;
/**
* The same as {@link queueMicrotask}, but with lifecycle support.
*
* + If the current lifecycle is disposed, the callback is never called.
* + The lifecycle within the callback is treated as the current lifecycle.
*
* @param callback The callback to run as a microtask.
* @throws An error if teardown hooks are explicitly un-supported in this context.
*/
declare function useMicrotask(callback: () => void): void;
/**
* The same as {@link setTimeout}, but with lifecycle support.
*
* + If the current lifecycle is disposed, the timeout is {@link clearTimeout cleared}.
* + The lifecycle within the callback is treated as the current lifecycle.
*
* @param callback The callback to run.
* @param timeout The timeout in milliseconds. See {@link setTimeout} for details.
* @throws An error if teardown hooks are explicitly un-supported in this context.
*/
declare function useTimeout(callback: () => void, timeout: number): void;
/**
* The same as {@link setInterval}, but with lifecycle support.
*
* + If the current lifecycle is disposed, the interval is {@link clearInterval cleared}.
* + The lifecycle within the callback is disposed when the interval is cleared and before each call.
*
* @param callback The callback to run.
* @param interval The interval in milliseconds. See {@link setInterval} for details.
* @throws An error if teardown hooks are explicitly un-supported in this context.
*/
declare function useInterval(callback: () => void, interval: number): void;
/**
* Repeatedly {@link requestAnimationFrame request animation frames}.
*
* + If the current lifecycle is disposed, the latest request is cancelled.
* + The lifecycle within the callback is disposed before each call and when the current lifecycle is disposed.
*
* @param callback The callback to run with a {@link performance.now high resolution timestamp}.
* @throws An error if teardown hooks are explicitly un-supported in this context.
*/
declare function useAnimation(callback: (now: number) => void): void;
type WatchGuardCondition<T, R extends T> = (value: T) => value is R;
type WatchCondition<T> = (value: T) => boolean;
declare class WatchForTimeoutError extends Error {
}
/**
* Utility to watch an expression until it's output satisfies a condition.
*
* @param expr The expression to watch.
* @param condition The condition to test. By default, all truthy values are matched.
* @param timeout An optional timeout. Default is no timeout.
* @returns A promise that resolves with the first matched output or rejects with a {@link WatchForTimeoutError}.
*/
declare function watchFor<T>(expr: Expression<T | Falsy>, timeout?: number): Promise<T>;
declare function watchFor<T, R extends T>(expr: Expression<T>, condition?: WatchGuardCondition<T, R>, timeout?: number): Promise<R>;
declare function watchFor<T>(expr: Expression<T>, condition?: WatchCondition<T>, timeout?: number): Promise<T>;
export { ASYNC, Async, AsyncContext, AsyncError, Queue, TASKS, Tasks, WatchForTimeoutError, isPending, isSelfPending, nestAsync, setPending, useAbortController, useAbortSignal, useAnimation, useInterval, useMicrotask, useTimeout, waitFor, watchFor };
export type { AsyncContextParent, TaskSource, TasksOptions, WatchCondition, WatchGuardCondition };