From d0b328bcbd530bd53677593d64df530556240485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kwa=C5=9Bniak?= Date: Fri, 3 Nov 2023 11:33:27 +0100 Subject: [PATCH] Add code documentation --- src/job.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++++++----- src/task.ts | 42 +++++++++++++++++++++++++ src/work.ts | 27 ++++++++++++++++ 3 files changed, 150 insertions(+), 7 deletions(-) diff --git a/src/job.ts b/src/job.ts index 67619ab..9415689 100644 --- a/src/job.ts +++ b/src/job.ts @@ -12,10 +12,7 @@ export enum JobStatus { Pending = "pending", } -export enum JobMode { - Drop = "drop", - Restart = "restart", -} +export type JobMode = (typeof JobMode)[keyof typeof JobMode]; export interface JobOptions { mode?: JobMode; @@ -31,39 +28,99 @@ interface ReactiveState { lastAborted?: Task; } +export const JobMode = { + Drop: "drop", + Restart: "restart", +} as const; + +/** + * A Job is a wrapper around a task function that provides + * a reactive interface to the task's state. + * @template T The return type of the task function. + * @template Args The argument types of the task function. + * @param taskFn The task function to wrap. + * @param options Options for the job. + * @returns A job instance. + * @example + * ```ts + * const job = createJob(async (signal, url: string) => { + * const response = await fetch(url, { signal }); + * return response.json(); + * }); + * + * const task = job.perform("https://example.test"); + * + * console.log(job.status); // "pending" + * console.log(job.isPending); // true + * console.log(job.isIdle); // false + * + * await task; + * + * console.log(job.status); // "idle" + * console.log(job.isPending); // false + * console.log(job.isIdle); // true + * ``` + */ export class Job { + /** + * The current status of the job. + */ get status(): ReactiveState["status"] { return this.#reactiveState.status; } + /** + * Whether the job is currently idle. Not performing a task. + */ get isIdle(): boolean { return this.status === JobStatus.Idle; } + /** + * Whether the job is currently pending. Performing a task. + */ get isPending(): boolean { return this.status === JobStatus.Pending; } + /** + * Last pending task. + */ get lastPending(): ReactiveState["lastPending"] { return this.#reactiveState.lastPending; } + /** + * Last fulfilled task. + */ get lastFulfilled(): ReactiveState["lastFulfilled"] { return this.#reactiveState.lastFulfilled; } + /** + * Last rejected task. + */ get lastRejected(): ReactiveState["lastRejected"] { return this.#reactiveState.lastRejected; } + /** + * Last settled task. + */ get lastSettled(): ReactiveState["lastSettled"] { return this.#reactiveState.lastSettled; } + /** + * Last aborted task. + */ get lastAborted(): ReactiveState["lastAborted"] { return this.#reactiveState.lastAborted; } + /** + * Number of times the job has performed a task, fulfilled or not. + */ get performCount(): ReactiveState["performCount"] { return this.#reactiveState.performCount; } @@ -76,12 +133,16 @@ export class Job { performCount: 0, }); - constructor(taskFn: TaskFunction, options: JobOptions = {}) { - options.mode ??= JobMode.Drop; + constructor(taskFn: TaskFunction, { mode }: JobOptions = {}) { this.#taskFn = taskFn; - this.#options = options; + this.#options = { mode: mode ?? JobMode.Drop }; } + /** + * Perform a task. + * @param args Arguments to pass to the task function. + * @returns A task instance. + */ perform(...args: Args): Task { return untrack(() => { const task = createTask((signal) => this.#taskFn(signal, ...args)); @@ -107,6 +168,11 @@ export class Job { }); } + /** + * Abort the last pending task. + * @param reason A reason for aborting the task. + * @returns A promise that resolves when the task is aborted. + */ async abort(reason?: string): Promise { return untrack(() => this.lastPending?.abort(reason)); } @@ -143,6 +209,14 @@ export class Job { } } +/** + * Create a job. + * @template T The return type of the task function. + * @template Args The argument types of the task function. + * @param taskFn The task function to wrap. + * @param options Options for the job. + * @returns A job instance. + */ export function createJob( taskFn: TaskFunction, options: JobOptions = {} diff --git a/src/task.ts b/src/task.ts index b2d54bb..48e21eb 100644 --- a/src/task.ts +++ b/src/task.ts @@ -10,47 +10,83 @@ export enum TaskStatus { Aborted = "aborted", } +/** + * An error that is thrown when a task is aborted. + */ export class TaskAbortError extends Error { name = "TaskAbortError"; } +/** + * A task is a promise that can be aborted, aware of its state. + */ export class Task implements Promise { + /** + * The current value of the task. + */ get value(): T | null | undefined { return this.#reactiveState.value; } + /** + * The current error of the task. + */ get error(): unknown { return this.#reactiveState.error; } + /** + * Whether the task is currently idle. + */ get isIdle(): boolean { return this.status === TaskStatus.Idle; } + /** + * Whether the task is currently pending. + */ get isPending(): boolean { return this.status === TaskStatus.Pending; } + /** + * Whether the task is currently fulfilled. + */ get isFulfilled(): boolean { return this.status === TaskStatus.Fulfilled; } + /** + * Whether the task is currently rejected. + */ get isRejected(): boolean { return this.status === TaskStatus.Rejected; } + /** + * Whether the task is currently settled. + */ get isSettled(): boolean { return [TaskStatus.Fulfilled, TaskStatus.Rejected].includes(this.status); } + /** + * Whether the task is currently aborted. + */ get isAborted(): boolean { return this.status === TaskStatus.Aborted; } + /** + * The current status of the task. + */ get status(): TaskStatus { return this.#reactiveState.status; } + /** + * The signal of the task. Used to abort the task. + */ get signal(): AbortSignal { return this.#abortController.signal; } @@ -132,6 +168,9 @@ export class Task implements Promise { this.#eventTarget.dispatchEvent(new Event(type)); } + /** + * Aborts the task. + */ abort(cancelReason = "The task was aborted."): Promise { return untrack(async () => { if (!this.isIdle && !this.isPending) return; @@ -149,6 +188,9 @@ export class Task implements Promise { }); } + /** + * Performs the task. + */ perform(): Task { this.#execute(); return this; diff --git a/src/work.ts b/src/work.ts index e74f32f..e550581 100644 --- a/src/work.ts +++ b/src/work.ts @@ -23,6 +23,20 @@ function abortablePromise(signal: AbortSignal) { }; } +/** + * Run a promise with an abort signal. + * @param signal An abort signal. + * @param promise A promise to run. + * @returns A promise that resolves when the given promise resolves or the abort signal is aborted. + * @throws If the abort signal is aborted. + * @example + * ```ts + * const controller = new AbortController(); + * const promise = new Promise((resolve) => setTimeout(resolve, 1000)); + * + * await work(controller.signal, promise); + * ``` + */ export async function work(signal: AbortSignal, promise: Promise) { signal.throwIfAborted(); @@ -35,6 +49,19 @@ export async function work(signal: AbortSignal, promise: Promise) { } } +/** + * Creates abortable timeout. + * @param signal An abort signal. + * @param ms The number of milliseconds to wait before resolving the promise. + * @returns A promise that resolves after the given number of milliseconds or the abort signal is aborted. + * @throws If the abort signal is aborted. + * @example + * ```ts + * const controller = new AbortController(); + * + * await timeout(controller.signal, 1000); + * ``` + */ export async function timeout(signal: AbortSignal, ms: number): Promise { return work(signal, new Promise((resolve) => setTimeout(resolve, ms))); }