Skip to content

Commit 7b97d2e

Browse files
committed
Move spinner() and retry() to goods from experimental
1 parent 79222d7 commit 7b97d2e

8 files changed

+195
-229
lines changed

README.md

+30-37
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,32 @@ let version = await within(async () => {
228228
})
229229
```
230230

231+
### `retry()`
232+
233+
Retries a callback for a few times. Will return after the first
234+
successful attempt, or will throw after specifies attempts count.
235+
236+
```js
237+
let p = await retry(10, () => $`curl https://medv.io`)
238+
239+
// With a specified delay between attempts.
240+
let p = await retry(20, '1s', () => $`curl https://medv.io`)
241+
242+
// With an exponential backoff.
243+
let p = await retry(30, expBackoff(), () => $`curl https://medv.io`)
244+
```
245+
246+
### `spinner()`
247+
248+
Starts a simple CLI spinner.
249+
250+
```js
251+
await spinner(() => $`long-running command`)
252+
253+
// With a message.
254+
await spinner('working...', () => $`sleep 99`)
255+
```
256+
231257
## Packages
232258

233259
The following packages are available without importing inside scripts.
@@ -386,42 +412,6 @@ files (when using `zx` executable).
386412
let {version} = require('./package.json')
387413
```
388414

389-
## Experimental
390-
391-
The zx provides a few experimental functions. Please leave feedback about
392-
those features in [the discussion](https://github.com/google/zx/discussions/299).
393-
To enable new features via CLI pass `--experimental` flag.
394-
395-
### `retry()`
396-
397-
Retries a callback for a few times. Will return after the first
398-
successful attempt, or will throw after specifies attempts count.
399-
400-
```js
401-
import { retry, expBackoff } from 'zx/experimental'
402-
403-
let p = await retry(10, () => $`curl https://medv.io`)
404-
405-
// With a specified delay between attempts.
406-
let p = await retry(20, '1s', () => $`curl https://medv.io`)
407-
408-
// With an exponential backoff.
409-
let p = await retry(30, expBackoff(), () => $`curl https://medv.io`)
410-
```
411-
412-
### `spinner()`
413-
414-
Starts a simple CLI spinner.
415-
416-
```js
417-
import { spinner } from 'zx/experimental'
418-
419-
await spinner(() => $`long-running command`)
420-
421-
// With a message.
422-
await spinner('working...', () => $`sleep 99`)
423-
```
424-
425415
## FAQ
426416

427417
### Passing env variables
@@ -563,7 +553,10 @@ jobs:
563553
```
564554
565555
### Canary / Beta / RC builds
566-
Impatient early adopters can try the experimental zx versions. But keep in mind: these builds are ⚠️️ __unstable__ in every sense.
556+
557+
Impatient early adopters can try the experimental zx versions.
558+
But keep in mind: these builds are ⚠️️ __beta__ in every sense.
559+
567560
```bash
568561
npm i zx@dev
569562
npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)'

src/experimental.ts

+2-93
Original file line numberDiff line numberDiff line change
@@ -12,96 +12,5 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import assert from 'node:assert'
16-
import chalk from 'chalk'
17-
import { $, within } from './core.js'
18-
import { sleep } from './goods.js'
19-
import { Duration, parseDuration } from './util.js'
20-
21-
export async function retry<T>(count: number, callback: () => T): Promise<T>
22-
export async function retry<T>(
23-
count: number,
24-
duration: Duration | Generator<number>,
25-
callback: () => T
26-
): Promise<T>
27-
export async function retry<T>(
28-
count: number,
29-
a: Duration | Generator<number> | (() => T),
30-
b?: () => T
31-
): Promise<T> {
32-
const total = count
33-
let callback: () => T
34-
let delayStatic = 0
35-
let delayGen: Generator<number> | undefined
36-
if (typeof a == 'function') {
37-
callback = a
38-
} else {
39-
if (typeof a == 'object') {
40-
delayGen = a
41-
} else {
42-
delayStatic = parseDuration(a)
43-
}
44-
assert(b)
45-
callback = b
46-
}
47-
let lastErr: unknown
48-
let attempt = 0
49-
while (count-- > 0) {
50-
attempt++
51-
try {
52-
return await callback()
53-
} catch (err) {
54-
let delay = 0
55-
if (delayStatic > 0) delay = delayStatic
56-
if (delayGen) delay = delayGen.next().value
57-
$.log({
58-
kind: 'retry',
59-
error:
60-
chalk.bgRed.white(' FAIL ') +
61-
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
62-
(delay > 0 ? `; next in ${delay}ms` : ''),
63-
})
64-
lastErr = err
65-
if (count == 0) break
66-
if (delay) await sleep(delay)
67-
}
68-
}
69-
throw lastErr
70-
}
71-
72-
export function* expBackoff(max: Duration = '60s', rand: Duration = '100ms') {
73-
const maxMs = parseDuration(max)
74-
const randMs = parseDuration(rand)
75-
let n = 1
76-
while (true) {
77-
const ms = Math.floor(Math.random() * randMs)
78-
yield Math.min(2 ** n++, maxMs) + ms
79-
}
80-
}
81-
82-
export async function spinner<T>(callback: () => T): Promise<T>
83-
export async function spinner<T>(title: string, callback: () => T): Promise<T>
84-
export async function spinner<T>(
85-
title: string | (() => T),
86-
callback?: () => T
87-
): Promise<T> {
88-
if (typeof title == 'function') {
89-
callback = title
90-
title = ''
91-
}
92-
let i = 0
93-
const spin = () =>
94-
process.stderr.write(` ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`)
95-
return within(async () => {
96-
$.verbose = false
97-
const id = setInterval(spin, 100)
98-
let result: T
99-
try {
100-
result = await callback!()
101-
} finally {
102-
clearInterval(id)
103-
process.stderr.write(' '.repeat(process.stdout.columns - 1) + '\r')
104-
}
105-
return result
106-
})
107-
}
15+
// TODO(antonmedv): Remove this export in next v8 release.
16+
export { spinner, retry, expBackoff, echo } from './goods.js'

src/goods.ts

+91-1
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import assert from 'node:assert'
1516
import * as globbyModule from 'globby'
1617
import minimist from 'minimist'
1718
import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch'
1819
import { createInterface } from 'node:readline'
19-
import { $, ProcessOutput } from './core.js'
20+
import { $, within, ProcessOutput } from './core.js'
2021
import { Duration, isString, parseDuration } from './util.js'
22+
import chalk from 'chalk'
2123

2224
export { default as chalk } from 'chalk'
2325
export { default as fs } from 'fs-extra'
@@ -112,3 +114,91 @@ export async function stdin() {
112114
}
113115
return buf
114116
}
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+
}

src/index.ts

+2-28
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,8 @@
1414

1515
import { ProcessPromise } from './core.js'
1616

17-
export {
18-
$,
19-
Shell,
20-
Options,
21-
ProcessPromise,
22-
ProcessOutput,
23-
within,
24-
cd,
25-
log,
26-
LogEntry,
27-
} from './core.js'
28-
29-
export {
30-
argv,
31-
chalk,
32-
echo,
33-
fetch,
34-
fs,
35-
glob,
36-
globby,
37-
os,
38-
path,
39-
question,
40-
sleep,
41-
stdin,
42-
which,
43-
YAML,
44-
} from './goods.js'
17+
export * from './core.js'
18+
export * from './goods.js'
4519

4620
export { Duration, quote, quotePowerShell } from './util.js'
4721

test-d/experimental.test-d.ts

-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,3 @@
1313
// limitations under the License.
1414

1515
import { expectType } from 'tsd'
16-
import { spinner } from '../src/experimental.js'
17-
18-
expectType<string>(await spinner(() => 'foo'))
19-
expectType<string>(await spinner('title', () => 'bar'))

test-d/goods.test-d.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import { expectType } from 'tsd'
1516
import { $ } from '../src/core.js'
16-
import { echo, sleep } from '../src/goods.js'
17+
import { echo, sleep, spinner, retry, expBackoff } from '../src/goods.js'
1718

1819
echo`Date is ${await $`date`}`
1920
echo('Hello, world!')
2021

2122
await sleep('1s')
2223
await sleep(1000)
24+
25+
expectType<'foo'>(await spinner(() => 'foo' as 'foo'))
26+
expectType<'bar'>(await spinner('title', () => 'bar' as 'bar'))
27+
expectType<'foo'>(await retry(0, () => 'foo' as 'foo'))
28+
expectType<Generator<number, void, unknown>>(expBackoff())

0 commit comments

Comments
 (0)