@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
127 lines (122 loc) • 3.79 kB
text/typescript
import { isBrowserWindow, isDeno, isNodeLike } from "../env.ts";
export type ProgressState = {
/**
* Once set, the progress bar will be updated to display the given
* percentage. Valid values are between `0` and `100`.
*/
percent?: number;
/**
* Once set, the progress dialog will be updated to display the given message.
*/
message?: string;
};
export type ProgressFunc<T> = (set: (state: ProgressState) => void, signal: AbortSignal) => Promise<T>;
export type ProgressAbortHandler<T> = () => T | never | Promise<T | never>;
/**
* Displays a dialog with a progress bar indicating the ongoing state of the
* `fn` function, and to wait until the job finishes or the user cancels the
* dialog.
*
* @param onAbort If provided, the dialog will show a cancel button (or listen
* for Escape in CLI) that allows the user to abort the task. This function can
* either return a default/fallback result or throw an error to indicate the
* cancellation.
*
* @example
* ```ts
* // default usage
* import { progress } from "@ayonli/jsext/dialog";
*
* const result = await progress("Processing...", async () => {
* // ... some long-running task
* return { ok: true };
* });
*
* console.log(result); // { ok: true }
* ```
*
* @example
* ```ts
* // update state
* import { progress } from "@ayonli/jsext/dialog";
*
* const result = await progress("Processing...", async (set) => {
* set({ percent: 0 });
* // ... some long-running task
* set({ percent: 50, message: "Halfway there!" });
* // ... some long-running task
* set({ percent: 100 });
*
* return { ok: true };
* });
*
* console.log(result); // { ok: true }
* ```
*
* @example
* ```ts
* // abortable
* import { progress } from "@ayonli/jsext/dialog";
*
* const result = await progress("Processing...", async (set, signal) => {
* set({ percent: 0 });
*
* if (!signal.aborted) {
* // ... some long-running task
* set({ percent: 50, message: "Halfway there!" });
* }
*
* if (!signal.aborted) {
* // ... some long-running task
* set({ percent: 100 });
* }
*
* return { ok: true };
* }, () => {
* return { ok: false };
* });
*
* console.log(result); // { ok: true } or { ok: false }
* ```
*/
export default async function progress<T>(
message: string,
fn: ProgressFunc<T>,
onAbort: ProgressAbortHandler<T> | undefined = undefined
): Promise<T | null> {
const ctrl = new AbortController();
const signal = ctrl.signal;
let fallback: { value: T; } | null = null;
const abort = !onAbort ? undefined : async () => {
try {
const result = await onAbort();
fallback = { value: result };
ctrl.abort();
} catch (err) {
ctrl.abort(err);
}
};
const listenForAbort = !onAbort ? undefined : () => new Promise<T>((resolve, reject) => {
signal.addEventListener("abort", () => {
if (fallback) {
resolve(fallback.value);
} else {
reject(signal.reason);
}
});
});
if (isBrowserWindow) {
const { progressInBrowser } = await import("./browser/index.ts");
return await progressInBrowser(message, fn, { signal, abort, listenForAbort });
} else if (isDeno || isNodeLike) {
const { lockStdin } = await import("../cli.ts");
const { handleTerminalProgress } = await import("./terminal/progress.ts");
return await lockStdin(() => handleTerminalProgress(message, fn, {
signal,
abort,
listenForAbort,
}));
} else {
throw new Error("Unsupported runtime");
}
}