Skip to content

Commit

Permalink
Add code documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Exelord committed Nov 3, 2023
1 parent d9446eb commit d0b328b
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 7 deletions.
88 changes: 81 additions & 7 deletions src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,39 +28,99 @@ interface ReactiveState<T> {
lastAborted?: Task<T>;
}

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<T, Args extends unknown[]> {
/**
* The current status of the job.
*/
get status(): ReactiveState<T>["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<T>["lastPending"] {
return this.#reactiveState.lastPending;
}

/**
* Last fulfilled task.
*/
get lastFulfilled(): ReactiveState<T>["lastFulfilled"] {
return this.#reactiveState.lastFulfilled;
}

/**
* Last rejected task.
*/
get lastRejected(): ReactiveState<T>["lastRejected"] {
return this.#reactiveState.lastRejected;
}

/**
* Last settled task.
*/
get lastSettled(): ReactiveState<T>["lastSettled"] {
return this.#reactiveState.lastSettled;
}

/**
* Last aborted task.
*/
get lastAborted(): ReactiveState<T>["lastAborted"] {
return this.#reactiveState.lastAborted;
}

/**
* Number of times the job has performed a task, fulfilled or not.
*/
get performCount(): ReactiveState<T>["performCount"] {
return this.#reactiveState.performCount;
}
Expand All @@ -76,12 +133,16 @@ export class Job<T, Args extends unknown[]> {
performCount: 0,
});

constructor(taskFn: TaskFunction<T, Args>, options: JobOptions = {}) {
options.mode ??= JobMode.Drop;
constructor(taskFn: TaskFunction<T, Args>, { 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<T> {
return untrack(() => {
const task = createTask((signal) => this.#taskFn(signal, ...args));
Expand All @@ -107,6 +168,11 @@ export class Job<T, Args extends unknown[]> {
});
}

/**
* 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<void> {
return untrack(() => this.lastPending?.abort(reason));
}
Expand Down Expand Up @@ -143,6 +209,14 @@ export class Job<T, Args extends unknown[]> {
}
}

/**
* 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<T, Args extends unknown[]>(
taskFn: TaskFunction<T, Args>,
options: JobOptions = {}
Expand Down
42 changes: 42 additions & 0 deletions src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> implements Promise<T> {
/**
* 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;
}
Expand Down Expand Up @@ -132,6 +168,9 @@ export class Task<T> implements Promise<T> {
this.#eventTarget.dispatchEvent(new Event(type));
}

/**
* Aborts the task.
*/
abort(cancelReason = "The task was aborted."): Promise<void> {
return untrack(async () => {
if (!this.isIdle && !this.isPending) return;
Expand All @@ -149,6 +188,9 @@ export class Task<T> implements Promise<T> {
});
}

/**
* Performs the task.
*/
perform(): Task<T> {
this.#execute();
return this;
Expand Down
27 changes: 27 additions & 0 deletions src/work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ function abortablePromise<T>(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<T>(signal: AbortSignal, promise: Promise<T>) {
signal.throwIfAborted();

Expand All @@ -35,6 +49,19 @@ export async function work<T>(signal: AbortSignal, promise: Promise<T>) {
}
}

/**
* 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<void> {
return work(signal, new Promise((resolve) => setTimeout(resolve, ms)));
}

0 comments on commit d0b328b

Please sign in to comment.