* Shared tick: reads the file tail for every actively-polled task. * Non-async body (.then) to avoid stacking if I/O is slow.
()
| 107 | * Non-async body (.then) to avoid stacking if I/O is slow. |
| 108 | */ |
| 109 | static #tick(): void { |
| 110 | for (const [, entry] of TaskOutput.#activePolling) { |
| 111 | if (!entry.#onProgress) { |
| 112 | continue |
| 113 | } |
| 114 | void tailFile(entry.path, PROGRESS_TAIL_BYTES).then( |
| 115 | ({ content, bytesRead, bytesTotal }) => { |
| 116 | if (!entry.#onProgress) { |
| 117 | return |
| 118 | } |
| 119 | // Always call onProgress even when content is empty, so the |
| 120 | // progress loop wakes up and can check for backgrounding. |
| 121 | // Commands like `git log -S` produce no output for long periods. |
| 122 | if (!content) { |
| 123 | entry.#onProgress('', '', entry.#totalLines, bytesTotal, false) |
| 124 | return |
| 125 | } |
| 126 | // Count all newlines in the tail and capture slice points for the |
| 127 | // last 5 and last 100 lines. Uncapped so extrapolation stays accurate |
| 128 | // for dense output (short lines → >100 newlines in 4KB). |
| 129 | let pos = content.length |
| 130 | let n5 = 0 |
| 131 | let n100 = 0 |
| 132 | let lineCount = 0 |
| 133 | while (pos > 0) { |
| 134 | pos = content.lastIndexOf('\n', pos - 1) |
| 135 | lineCount++ |
| 136 | if (lineCount === 5) n5 = pos <= 0 ? 0 : pos + 1 |
| 137 | if (lineCount === 100) n100 = pos <= 0 ? 0 : pos + 1 |
| 138 | } |
| 139 | // lineCount is exact when the whole file fits in PROGRESS_TAIL_BYTES. |
| 140 | // Otherwise extrapolate from the tail sample; monotone max keeps the |
| 141 | // counter from going backwards when the tail has longer lines on one tick. |
| 142 | const totalLines = |
| 143 | bytesRead >= bytesTotal |
| 144 | ? lineCount |
| 145 | : Math.max( |
| 146 | entry.#totalLines, |
| 147 | Math.round((bytesTotal / bytesRead) * lineCount), |
| 148 | ) |
| 149 | entry.#totalLines = totalLines |
| 150 | entry.#totalBytes = bytesTotal |
| 151 | entry.#onProgress( |
| 152 | content.slice(n5), |
| 153 | content.slice(n100), |
| 154 | totalLines, |
| 155 | bytesTotal, |
| 156 | bytesRead < bytesTotal, |
| 157 | ) |
| 158 | }, |
| 159 | () => { |
| 160 | // File may not exist yet |
| 161 | }, |
| 162 | ) |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | /** Write stdout data (pipe mode only — used by hooks). */ |