Commit d7d89c6

Anton Golub <antongolub@antongolub.com>
2025-07-19 19:28:02
refactor: introduce internal `ProcessPromise.isSync()` (#1276)
1 parent 56172ba
build/core.cjs
@@ -454,11 +454,10 @@ var $ = new Proxy(
       pieces,
       args
     );
-    const sync = snapshot[SYNC];
     boundCtxs.push([cmd, from, snapshot]);
     const process3 = new ProcessPromise(import_util.noop);
-    if (!process3.isHalted() || sync) process3.run();
-    return sync ? process3.output : process3;
+    if (!process3.isHalted()) process3.run();
+    return process3.output || process3;
   },
   {
     set(_, key, value) {
@@ -504,7 +503,7 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
       this._resolve = resolve;
       this._reject = (v) => {
         reject(v);
-        if (snapshot[SYNC]) throw v;
+        if (this.isSync()) throw v;
       };
       if (snapshot.halt) this._stage = "halted";
     } else _ProcessPromise.disarm(this);
@@ -517,7 +516,6 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
     const self = this;
     const $2 = self._snapshot;
     const id = self.id;
-    const sync = $2[SYNC];
     const timeout = (_b = self._timeout) != null ? _b : $2.timeout;
     const timeoutSignal = (_c = self._timeoutSignal) != null ? _c : $2.timeoutSignal;
     if ($2.preferLocal) {
@@ -525,8 +523,8 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
       $2.env = (0, import_util.preferLocalBin)($2.env, ...dirs);
     }
     this._zurk = (0, import_vendor_core2.exec)({
-      sync,
       id,
+      sync: self.isSync(),
       cmd: self.fullCmd,
       cwd: (_d = $2.cwd) != null ? _d : $2[CWD],
       input: (_f = (_e = $2.input) == null ? void 0 : _e.stdout) != null ? _f : $2.input,
@@ -554,7 +552,7 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
       on: {
         start: () => {
           $2.log({ kind: "cmd", cmd: self.cmd, verbose: self.isVerbose(), id });
-          !sync && timeout && self.timeout(timeout, timeoutSignal);
+          self.timeout(timeout, timeoutSignal);
         },
         stdout: (data) => {
           if (self._piped) return;
@@ -633,13 +631,13 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
     return promisifyStream(dest, this);
   }
   abort(reason) {
-    var _a, _b;
     if (this.isSettled()) throw new Error("Too late to abort the process.");
-    if (this.signal !== ((_a = this._snapshot.ac) == null ? void 0 : _a.signal))
+    const { ac } = this._snapshot;
+    if (this.signal !== ac.signal)
       throw new Error("The signal is controlled by another process.");
     if (!this.child)
       throw new Error("Trying to abort a process without creating one.");
-    (_b = this._zurk) == null ? void 0 : _b.ac.abort(reason);
+    ac.abort(reason);
   }
   kill(signal = $.killSignal) {
     if (this.isSettled()) throw new Error("Too late to kill the process.");
@@ -723,7 +721,7 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
     this._verbose = v;
     return this;
   }
-  timeout(d, signal = this._timeoutSignal || $.timeoutSignal) {
+  timeout(d = 0, signal = this._timeoutSignal || $.timeoutSignal) {
     if (this.isSettled()) return this;
     this._timeout = (0, import_util.parseDuration)(d);
     this._timeoutSignal = signal;
@@ -767,7 +765,10 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
     return (_a = this._nothrow) != null ? _a : this._snapshot.nothrow;
   }
   isHalted() {
-    return this.stage === "halted";
+    return this.stage === "halted" && !this.isSync();
+  }
+  isSync() {
+    return this._snapshot[SYNC];
   }
   isSettled() {
     return !!this.output;
build/core.d.ts
@@ -119,7 +119,7 @@ export declare class ProcessPromise extends Promise<ProcessOutput> {
     nothrow(v?: boolean): ProcessPromise;
     quiet(v?: boolean): ProcessPromise;
     verbose(v?: boolean): ProcessPromise;
-    timeout(d: Duration, signal?: NodeJS.Signals | undefined): ProcessPromise;
+    timeout(d?: Duration, signal?: NodeJS.Signals | undefined): ProcessPromise;
     json<T = any>(): Promise<T>;
     text(encoding?: Encoding): Promise<string>;
     lines(delimiter?: string | RegExp): Promise<string[]>;
@@ -129,6 +129,7 @@ export declare class ProcessPromise extends Promise<ProcessOutput> {
     isVerbose(): boolean;
     isNothrow(): boolean;
     isHalted(): boolean;
+    private isSync;
     private isSettled;
     private isRunning;
     then<R = ProcessOutput, E = ProcessOutput>(onfulfilled?: ((value: ProcessOutput) => PromiseLike<R> | R) | undefined | null, onrejected?: ((reason: ProcessOutput) => PromiseLike<E> | E) | undefined | null): Promise<R | E>;
docs/process-promise.md
@@ -366,7 +366,7 @@ await $`echo foo`.verbose(false) // Turn off verbose mode once
 
 ## `timeout()`
 
-Kills the process after a specified timeout.
+Kills the process after a specified period.
 
 ```js
 await $`sleep 999`.timeout('5s')
@@ -374,3 +374,5 @@ await $`sleep 999`.timeout('5s')
 // Or with a specific signal.
 await $`sleep 999`.timeout('5s', 'SIGKILL')
 ```
+
+If the process is already settled, the method does nothing. Passing nullish value will disable the timeout.
src/core.ts
@@ -192,13 +192,12 @@ export const $: Shell & Options = new Proxy<Shell & Options>(
       pieces as TemplateStringsArray,
       args
     ) as string
-    const sync = snapshot[SYNC]
     boundCtxs.push([cmd, from, snapshot])
     const process = new ProcessPromise(noop)
 
-    if (!process.isHalted() || sync) process.run()
+    if (!process.isHalted()) process.run()
 
-    return sync ? process.output : process
+    return process.output || process
   } as Shell & Options,
   {
     set(_, key, value) {
@@ -268,7 +267,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
       this._resolve = resolve!
       this._reject = (v: ProcessOutput) => {
         reject!(v)
-        if (snapshot[SYNC]) throw v
+        if (this.isSync()) throw v
       }
       if (snapshot.halt) this._stage = 'halted'
     } else ProcessPromise.disarm(this)
@@ -282,7 +281,6 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     const self = this
     const $ = self._snapshot
     const id = self.id
-    const sync = $[SYNC]
     const timeout = self._timeout ?? $.timeout
     const timeoutSignal = self._timeoutSignal ?? $.timeoutSignal
 
@@ -294,8 +292,8 @@ export class ProcessPromise extends Promise<ProcessOutput> {
 
     // prettier-ignore
     this._zurk = exec({
-      sync,
       id,
+      sync:     self.isSync(),
       cmd:      self.fullCmd,
       cwd:      $.cwd ?? $[CWD],
       input:    ($.input as ProcessPromise | ProcessOutput)?.stdout ?? $.input,
@@ -322,7 +320,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
       on: {
         start: () => {
           $.log({ kind: 'cmd', cmd: self.cmd, verbose: self.isVerbose(), id })
-          !sync && timeout && self.timeout(timeout, timeoutSignal)
+          self.timeout(timeout, timeoutSignal)
         },
         stdout: (data) => {
           // If the process is piped, don't print its output.
@@ -437,12 +435,13 @@ export class ProcessPromise extends Promise<ProcessOutput> {
 
   abort(reason?: string) {
     if (this.isSettled()) throw new Error('Too late to abort the process.')
-    if (this.signal !== this._snapshot.ac?.signal)
+    const { ac } = this._snapshot
+    if (this.signal !== ac!.signal)
       throw new Error('The signal is controlled by another process.')
     if (!this.child)
       throw new Error('Trying to abort a process without creating one.')
 
-    this._zurk?.ac.abort(reason)
+    ac!.abort(reason)
   }
 
   kill(signal = $.killSignal): Promise<void> {
@@ -549,7 +548,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   }
 
   timeout(
-    d: Duration,
+    d: Duration = 0,
     signal = this._timeoutSignal || $.timeoutSignal
   ): ProcessPromise {
     if (this.isSettled()) return this
@@ -603,7 +602,11 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   }
 
   isHalted(): boolean {
-    return this.stage === 'halted'
+    return this.stage === 'halted' && !this.isSync()
+  }
+
+  private isSync(): boolean {
+    return this._snapshot[SYNC]
   }
 
   private isSettled(): boolean {
test/util.test.js
@@ -70,6 +70,7 @@ describe('util', () => {
   })
 
   test('duration parsing works', () => {
+    assert.equal(parseDuration(0), 0)
     assert.equal(parseDuration(1000), 1000)
     assert.equal(parseDuration('100'), 100)
     assert.equal(parseDuration('2s'), 2000)
.size-limit.json
@@ -15,7 +15,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "122.10 kB",
+    "limit": "122.05 kB",
     "brotli": false,
     "gzip": false
   },
@@ -29,14 +29,14 @@
       "build/globals.js",
       "build/deno.js"
     ],
-    "limit": "812.95 kB",
+    "limit": "812.85 kB",
     "brotli": false,
     "gzip": false
   },
   {
     "name": "libdefs",
     "path": "build/*.d.ts",
-    "limit": "39.15 kB",
+    "limit": "39.16 kB",
     "brotli": false,
     "gzip": false
   },
@@ -62,7 +62,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "868.95 kB",
+    "limit": "868.90 kB",
     "brotli": false,
     "gzip": false
   }