rvx
Version:
A signal based rendering library
110 lines (100 loc) • 2.77 kB
text/typescript
import { teardown } from "../core/lifecycle.js";
interface SideEffect {
blocking: false;
task: (signal: AbortSignal) => unknown | Promise<unknown>;
resolve: undefined;
reject: undefined;
}
interface Blocking {
blocking: true;
task: () => unknown | Promise<unknown>;
resolve: (value: unknown) => void;
reject: (value: unknown) => void;
}
type Task = SideEffect | Blocking;
/**
* A queue for sequentially running async tasks that can be triggered by both the user and side effects.
*/
export class Queue {
#queue: Task[] = [];
/**
* Number of tasks that need to be dequeued until this queue isn't blocked or any negative number. Zero indicates, that the last blocking task is currently running.
*/
#blocked = -1;
#controller: AbortController | undefined = undefined;
#running: Promise<void> | undefined = undefined;
/**
* Create a new queue.
*
* When the current lifecycle is disposed, all side effects are aborted and removed from the queue.
*/
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: Task | undefined;
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;
})();
}
}
/**
* 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 {
if (this.#blocked >= 0) {
return;
}
this.#abort();
this.#queue.push({ blocking: false, task, resolve: undefined, reject: undefined });
this.#run();
}
/**
* 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> {
return new Promise<unknown>((resolve, reject) => {
this.#abort();
this.#blocked = this.#queue.push({ blocking: true, task, resolve, reject });
this.#run();
}) as Promise<T>;
}
}