|
12 | 12 | // See the License for the specific language governing permissions and |
13 | 13 | // limitations under the License. |
14 | 14 |
|
| 15 | +import assert from 'node:assert' |
15 | 16 | import * as globbyModule from 'globby' |
16 | 17 | import minimist from 'minimist' |
17 | 18 | import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch' |
18 | 19 | import { createInterface } from 'node:readline' |
19 | | -import { $, ProcessOutput } from './core.js' |
| 20 | +import { $, within, ProcessOutput } from './core.js' |
20 | 21 | import { Duration, isString, parseDuration } from './util.js' |
| 22 | +import chalk from 'chalk' |
21 | 23 |
|
22 | 24 | export { default as chalk } from 'chalk' |
23 | 25 | export { default as fs } from 'fs-extra' |
@@ -112,3 +114,91 @@ export async function stdin() { |
112 | 114 | } |
113 | 115 | return buf |
114 | 116 | } |
| 117 | + |
| 118 | +export async function retry<T>(count: number, callback: () => T): Promise<T> |
| 119 | +export async function retry<T>( |
| 120 | + count: number, |
| 121 | + duration: Duration | Generator<number>, |
| 122 | + callback: () => T |
| 123 | +): Promise<T> |
| 124 | +export async function retry<T>( |
| 125 | + count: number, |
| 126 | + a: Duration | Generator<number> | (() => T), |
| 127 | + b?: () => T |
| 128 | +): Promise<T> { |
| 129 | + const total = count |
| 130 | + let callback: () => T |
| 131 | + let delayStatic = 0 |
| 132 | + let delayGen: Generator<number> | undefined |
| 133 | + if (typeof a == 'function') { |
| 134 | + callback = a |
| 135 | + } else { |
| 136 | + if (typeof a == 'object') { |
| 137 | + delayGen = a |
| 138 | + } else { |
| 139 | + delayStatic = parseDuration(a) |
| 140 | + } |
| 141 | + assert(b) |
| 142 | + callback = b |
| 143 | + } |
| 144 | + let lastErr: unknown |
| 145 | + let attempt = 0 |
| 146 | + while (count-- > 0) { |
| 147 | + attempt++ |
| 148 | + try { |
| 149 | + return await callback() |
| 150 | + } catch (err) { |
| 151 | + let delay = 0 |
| 152 | + if (delayStatic > 0) delay = delayStatic |
| 153 | + if (delayGen) delay = delayGen.next().value |
| 154 | + $.log({ |
| 155 | + kind: 'retry', |
| 156 | + error: |
| 157 | + chalk.bgRed.white(' FAIL ') + |
| 158 | + ` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` + |
| 159 | + (delay > 0 ? `; next in ${delay}ms` : ''), |
| 160 | + }) |
| 161 | + lastErr = err |
| 162 | + if (count == 0) break |
| 163 | + if (delay) await sleep(delay) |
| 164 | + } |
| 165 | + } |
| 166 | + throw lastErr |
| 167 | +} |
| 168 | + |
| 169 | +export function* expBackoff(max: Duration = '60s', rand: Duration = '100ms') { |
| 170 | + const maxMs = parseDuration(max) |
| 171 | + const randMs = parseDuration(rand) |
| 172 | + let n = 1 |
| 173 | + while (true) { |
| 174 | + const ms = Math.floor(Math.random() * randMs) |
| 175 | + yield Math.min(2 ** n++, maxMs) + ms |
| 176 | + } |
| 177 | +} |
| 178 | + |
| 179 | +export async function spinner<T>(callback: () => T): Promise<T> |
| 180 | +export async function spinner<T>(title: string, callback: () => T): Promise<T> |
| 181 | +export async function spinner<T>( |
| 182 | + title: string | (() => T), |
| 183 | + callback?: () => T |
| 184 | +): Promise<T> { |
| 185 | + if (typeof title == 'function') { |
| 186 | + callback = title |
| 187 | + title = '' |
| 188 | + } |
| 189 | + let i = 0 |
| 190 | + const spin = () => |
| 191 | + process.stderr.write(` ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`) |
| 192 | + return within(async () => { |
| 193 | + $.verbose = false |
| 194 | + const id = setInterval(spin, 100) |
| 195 | + let result: T |
| 196 | + try { |
| 197 | + result = await callback!() |
| 198 | + } finally { |
| 199 | + clearInterval(id) |
| 200 | + process.stderr.write(' '.repeat(process.stdout.columns - 1) + '\r') |
| 201 | + } |
| 202 | + return result |
| 203 | + }) |
| 204 | +} |
0 commit comments