@puppeteer/browsers
Version:
Download and launch browsers
143 lines • 5.25 kB
JavaScript
/**
* @license
* Copyright 2026 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
// Module-level active session. Concurrent bars share a session; a new session
// starts whenever no bar is currently active (or the stream changes).
let activeBar = null;
/**
* Manages one or more concurrent progress bars on separate terminal lines.
* Use {@link ProgressBar.createBarTicker} to add a bar; concurrent calls
* automatically share a session so bars don't overwrite each other.
*
* @internal
*/
export class ProgressBar {
#stream;
#totalRows = 0;
#doneCount = 0;
#bars = [];
#renderScheduled = false;
#lastRenderTime = 0;
#allDone = false;
constructor(stream) {
this.#stream = stream;
}
/**
* Returns a `tick(delta)` function for a new progress bar. If another bar
* is already active on the same stream, the new bar appears on the next line
* and both update in place. The trailing newline is emitted automatically
* once every bar in the session has reached 100%.
*/
static createBarTicker(title, total, stream = process.stderr) {
// Start a fresh session when none is active or the stream changed.
if (!activeBar || activeBar.#stream !== stream) {
activeBar = new ProgressBar(stream);
}
return activeBar.#addBar(title, total);
}
#claimRow() {
const row = this.#totalRows++;
if (row > 0 && this.#stream.isTTY) {
// Cursor is parked at col 0 of the previous bottom row; \n opens the
// new line and moves the cursor there, establishing the new bottom row.
this.#stream.write('\n');
}
return row;
}
#addBar(title, total) {
const row = this.#claimRow();
this.#bars.push({
title,
total,
downloaded: 0,
startTime: 0,
lastRendered: '',
});
let doneNotified = false;
return (delta) => {
const bar = this.#bars[row];
if (bar.startTime === 0) {
bar.startTime = Date.now();
}
bar.downloaded += delta;
const isDone = bar.downloaded >= bar.total;
if (isDone || Date.now() - this.#lastRenderTime >= 100) {
this.#scheduleRender();
}
if (isDone && !doneNotified) {
doneNotified = true;
this.#onBarDone();
}
};
}
// Defers to a microtask so multiple ticks in the same turn share one render
// pass and the cursor only moves once.
#scheduleRender() {
if (!this.#renderScheduled) {
this.#renderScheduled = true;
queueMicrotask(() => {
this.#renderScheduled = false;
this.#lastRenderTime = Date.now();
this.#renderAll();
});
}
}
#computeRendered(row) {
const bar = this.#bars[row];
const ratio = Math.min(1, Math.max(0, bar.downloaded / bar.total));
const percent = Math.round(ratio * 100);
const elapsedMs = bar.startTime > 0 ? Date.now() - bar.startTime : 0;
const rate = elapsedMs > 0 ? bar.downloaded / elapsedMs : 0; // bytes/ms
const etaSec = rate > 0 ? (bar.total - bar.downloaded) / rate / 1000 : 0;
const width = 20;
const completeCount = Math.round(width * ratio);
const barStr = '='.repeat(completeCount) + ' '.repeat(width - completeCount);
const status = ratio >= 1 ? 'unpacking' : `${percent}% ${etaSec.toFixed(1)}s`;
return `${bar.title} [${barStr}] ${status} `;
}
// Invariant: cursor is at col 0 of the bottom row before and after this call.
// Single top-down pass: go up to bar 0, render each bar, come back to bottom.
#renderAll() {
if (!this.#stream.isTTY) {
return;
}
const N = this.#totalRows;
if (N === 0) {
return;
}
// Move cursor to top of bar area.
if (N > 1) {
this.#stream.moveCursor(0, -(N - 1));
}
for (let row = 0; row < N; row++) {
const rendered = this.#computeRendered(row);
if (rendered !== this.#bars[row].lastRendered) {
this.#stream.cursorTo(0);
this.#stream.write(rendered);
this.#stream.clearLine(1);
this.#bars[row].lastRendered = rendered;
}
if (row < N - 1) {
this.#stream.moveCursor(0, 1);
}
}
// Park cursor at col 0 of the bottom row.
this.#stream.cursorTo(0);
if (this.#allDone) {
activeBar = null;
this.#stream.write('\n');
}
}
#onBarDone() {
this.#doneCount++;
if (this.#doneCount >= this.#totalRows && this.#totalRows > 0) {
this.#allDone = true;
// Don't emit \n here — the pending render microtask hasn't run yet.
// #renderAll() will emit it after the final pass so the cursor is
// already past the last bar line before the newline moves it.
}
}
}
//# sourceMappingURL=ProgressBar.js.map