Skip to content

Commit 49a82c4

Browse files
feat: expose ProcessPromise stage (#1077)
* chore: fix grep alias * refactor: add internal state marker * refactor: delete unused false * refactor(code): introduce internal state machine * test: add test for stage `ProcessPromise` * docs: add `ProcessPromise` stage doc * build: update size-limit * docs: add `ProcessPromise` stage doc for site * docs: clean up * test: prettify * test: check `ProcessPromise` getters --------- Co-authored-by: Anton Golub <antongolub@antongolub.com>
1 parent 437a80f commit 49a82c4

File tree

4 files changed

+112
-23
lines changed

4 files changed

+112
-23
lines changed

.size-limit.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
{
1717
"name": "dts libdefs",
1818
"path": "build/*.d.ts",
19-
"limit": "38.1 kB",
19+
"limit": "38.7 kB",
2020
"brotli": false,
2121
"gzip": false
2222
},
@@ -30,7 +30,7 @@
3030
{
3131
"name": "all",
3232
"path": "build/*",
33-
"limit": "847.5 kB",
33+
"limit": "849 kB",
3434
"brotli": false,
3535
"gzip": false
3636
}

docs/process-promise.md

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ const p = $({halt: true})`command`
1414
const o = await p.run()
1515
```
1616

17+
## `stage`
18+
19+
Shows the current process stage: `initial` | `halted` | `running` | `fulfilled` | `rejected`
20+
21+
```ts
22+
const p = $`echo foo`
23+
p.stage // 'running'
24+
await p
25+
p.stage // 'fulfilled'
26+
```
27+
28+
1729
## `stdin`
1830

1931
Returns a writable stream of the stdin process. Accessing

src/core.ts

+30-15
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ export const $: Shell & Options = new Proxy<Shell & Options>(
203203
},
204204
}
205205
)
206+
/**
207+
* State machine stages
208+
*/
209+
type ProcessStage = 'initial' | 'halted' | 'running' | 'fulfilled' | 'rejected'
206210

207211
type Resolve = (out: ProcessOutput) => void
208212

@@ -214,6 +218,7 @@ type PipeMethod = {
214218
}
215219

216220
export class ProcessPromise extends Promise<ProcessOutput> {
221+
private _stage: ProcessStage = 'initial'
217222
private _id = randomId()
218223
private _command = ''
219224
private _from = ''
@@ -225,11 +230,8 @@ export class ProcessPromise extends Promise<ProcessOutput> {
225230
private _timeout?: number
226231
private _timeoutSignal?: NodeJS.Signals
227232
private _timeoutId?: NodeJS.Timeout
228-
private _resolved = false
229-
private _halted?: boolean
230233
private _piped = false
231234
private _pipedFrom?: ProcessPromise
232-
private _run = false
233235
private _ee = new EventEmitter()
234236
private _stdin = new VoidStream()
235237
private _zurk: ReturnType<typeof exec> | null = null
@@ -249,12 +251,12 @@ export class ProcessPromise extends Promise<ProcessOutput> {
249251
this._resolve = resolve
250252
this._reject = reject
251253
this._snapshot = { ac: new AbortController(), ...options }
254+
if (this._snapshot.halt) this._stage = 'halted'
252255
}
253256

254257
run(): ProcessPromise {
255-
if (this._run) return this // The _run() can be called from a few places.
256-
this._halted = false
257-
this._run = true
258+
if (this.isRunning() || this.isSettled()) return this // The _run() can be called from a few places.
259+
this._stage = 'running'
258260
this._pipedFrom?.run()
259261

260262
const self = this
@@ -310,7 +312,6 @@ export class ProcessPromise extends Promise<ProcessOutput> {
310312
$.log({ kind: 'stderr', data, verbose: !self.isQuiet(), id })
311313
},
312314
end: (data, c) => {
313-
self._resolved = true
314315
const { error, status, signal, duration, ctx } = data
315316
const { stdout, stderr, stdall } = ctx.store
316317
const dto: ProcessOutputLazyDto = {
@@ -341,8 +342,10 @@ export class ProcessPromise extends Promise<ProcessOutput> {
341342
const output = self._output = new ProcessOutput(dto)
342343

343344
if (error || status !== 0 && !self.isNothrow()) {
345+
self._stage = 'rejected'
344346
self._reject(output)
345347
} else {
348+
self._stage = 'fulfilled'
346349
self._resolve(output)
347350
}
348351
},
@@ -388,9 +391,9 @@ export class ProcessPromise extends Promise<ProcessOutput> {
388391
for (const chunk of this._zurk!.store[source]) from.write(chunk)
389392
return true
390393
}
391-
const fillEnd = () => this._resolved && fill() && from.end()
394+
const fillEnd = () => this.isSettled() && fill() && from.end()
392395

393-
if (!this._resolved) {
396+
if (!this.isSettled()) {
394397
const onData = (chunk: string | Buffer) => from.write(chunk)
395398
ee.once(source, () => {
396399
fill()
@@ -495,6 +498,10 @@ export class ProcessPromise extends Promise<ProcessOutput> {
495498
return this._output
496499
}
497500

501+
get stage(): ProcessStage {
502+
return this._stage
503+
}
504+
498505
// Configurators
499506
stdio(
500507
stdin: IOType,
@@ -524,13 +531,13 @@ export class ProcessPromise extends Promise<ProcessOutput> {
524531
d: Duration,
525532
signal = this._timeoutSignal || $.timeoutSignal
526533
): ProcessPromise {
527-
if (this._resolved) return this
534+
if (this.isSettled()) return this
528535

529536
this._timeout = parseDuration(d)
530537
this._timeoutSignal = signal
531538

532539
if (this._timeoutId) clearTimeout(this._timeoutId)
533-
if (this._timeout && this._run) {
540+
if (this._timeout && this.isRunning()) {
534541
this._timeoutId = setTimeout(
535542
() => this.kill(this._timeoutSignal),
536543
this._timeout
@@ -562,10 +569,6 @@ export class ProcessPromise extends Promise<ProcessOutput> {
562569
}
563570

564571
// Status checkers
565-
isHalted(): boolean {
566-
return this._halted ?? this._snapshot.halt ?? false
567-
}
568-
569572
isQuiet(): boolean {
570573
return this._quiet ?? this._snapshot.quiet
571574
}
@@ -578,6 +581,18 @@ export class ProcessPromise extends Promise<ProcessOutput> {
578581
return this._nothrow ?? this._snapshot.nothrow
579582
}
580583

584+
isHalted(): boolean {
585+
return this.stage === 'halted'
586+
}
587+
588+
private isSettled(): boolean {
589+
return !!this.output
590+
}
591+
592+
private isRunning(): boolean {
593+
return this.stage === 'running'
594+
}
595+
581596
// Promise API
582597
then<R = ProcessOutput, E = ProcessOutput>(
583598
onfulfilled?:

test/core.test.js

+68-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { basename } from 'node:path'
1919
import { WriteStream } from 'node:fs'
2020
import { Readable, Transform, Writable } from 'node:stream'
2121
import { Socket } from 'node:net'
22+
import { ChildProcess } from 'node:child_process'
2223
import {
2324
$,
2425
ProcessPromise,
@@ -42,6 +43,7 @@ import {
4243
which,
4344
nothrow,
4445
} from '../build/index.js'
46+
import { noop } from '../build/util.js'
4547

4648
describe('core', () => {
4749
describe('resolveDefaults()', () => {
@@ -392,6 +394,72 @@ describe('core', () => {
392394
})
393395

394396
describe('ProcessPromise', () => {
397+
test('getters', async () => {
398+
const p = $`echo foo`
399+
assert.ok(p.pid > 0)
400+
assert.ok(typeof p.id === 'string')
401+
assert.ok(typeof p.cmd === 'string')
402+
assert.ok(typeof p.fullCmd === 'string')
403+
assert.ok(typeof p.stage === 'string')
404+
assert.ok(p.child instanceof ChildProcess)
405+
assert.ok(p.stdout instanceof Socket)
406+
assert.ok(p.stderr instanceof Socket)
407+
assert.ok(p.exitCode instanceof Promise)
408+
assert.ok(p.signal instanceof AbortSignal)
409+
assert.equal(p.output, null)
410+
411+
await p
412+
assert.ok(p.output instanceof ProcessOutput)
413+
})
414+
415+
describe('state machine transitions', () => {
416+
it('running > fulfilled', async () => {
417+
const p = $`echo foo`
418+
assert.equal(p.stage, 'running')
419+
await p
420+
assert.equal(p.stage, 'fulfilled')
421+
})
422+
423+
it('running > rejected', async () => {
424+
const p = $`foo`
425+
assert.equal(p.stage, 'running')
426+
427+
try {
428+
await p
429+
} catch {}
430+
assert.equal(p.stage, 'rejected')
431+
})
432+
433+
it('halted > running > fulfilled', async () => {
434+
const p = $({ halt: true })`echo foo`
435+
assert.equal(p.stage, 'halted')
436+
p.run()
437+
assert.equal(p.stage, 'running')
438+
await p
439+
assert.equal(p.stage, 'fulfilled')
440+
})
441+
442+
it('all transition', async () => {
443+
const { promise, resolve, reject } = Promise.withResolvers()
444+
const process = new ProcessPromise(noop, noop)
445+
446+
assert.equal(process.stage, 'initial')
447+
process._bind('echo foo', 'test', resolve, reject, {
448+
...resolveDefaults(),
449+
halt: true,
450+
})
451+
452+
assert.equal(process.stage, 'halted')
453+
process.run()
454+
455+
assert.equal(process.stage, 'running')
456+
await promise
457+
458+
assert.equal(process.stage, 'fulfilled')
459+
assert.equal(process.output?.stdout, 'foo\n')
460+
})
461+
})
462+
395463
test('inherits native Promise', async () => {
396464
const p1 = $`echo 1`
397465
const p2 = p1.then((v) => v)
@@ -424,12 +492,6 @@ describe('core', () => {
424492
assert.equal(p.fullCmd, "set -euo pipefail;echo $'#bar' --t 1")
425493
})
426494

427-
test('exposes pid & id', () => {
428-
const p = $`echo foo`
429-
assert.ok(p.pid > 0)
430-
assert.ok(typeof p.id === 'string')
431-
})
432-
433495
test('stdio() works', async () => {
434496
const p1 = $`printf foo`
435497
await p1

0 commit comments

Comments
 (0)