@fuzdev/fuz_util 0.45.2 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/process.js CHANGED
@@ -2,161 +2,528 @@ import { spawn as spawn_child_process, } from 'node:child_process';
2
2
  import { styleText as st } from 'node:util';
3
3
  import { Logger } from './log.js';
4
4
  import { print_error, print_key_value } from './print.js';
5
+ import { noop } from './function.js';
5
6
  const log = new Logger('process');
7
+ //
8
+ // Type Guards
9
+ //
6
10
  /**
7
- * A convenient promise wrapper around `spawn_process`
8
- * intended for commands that have an end, not long running-processes like watchers.
9
- * Any more advanced usage should use `spawn_process` directly for access to the `child` process.
11
+ * Type guard for spawn errors (process failed to start).
10
12
  */
11
- export const spawn = (...args) => spawn_process(...args).closed;
13
+ export const spawn_result_is_error = (result) => 'error' in result;
12
14
  /**
13
- * Similar to `spawn` but buffers and returns `stdout` and `stderr` as strings.
15
+ * Type guard for signal termination.
14
16
  */
15
- export const spawn_out = async (command, args = [], options) => {
16
- const { child, closed } = spawn_process(command, args, { ...options, stdio: 'pipe' });
17
- let stdout = null;
18
- child.stdout.on('data', (data) => {
19
- stdout = (stdout ?? '') + data.toString();
20
- });
21
- let stderr = null;
22
- child.stderr.on('data', (data) => {
23
- stderr = (stderr ?? '') + data.toString();
24
- });
25
- const result = await closed;
26
- return { result, stdout, stderr };
27
- };
17
+ export const spawn_result_is_signaled = (result) => 'signal' in result && result.signal !== null;
18
+ /**
19
+ * Type guard for normal exit with code.
20
+ */
21
+ export const spawn_result_is_exited = (result) => 'code' in result && result.code !== null;
22
+ //
23
+ // Internal Helpers
24
+ //
28
25
  /**
29
- * Wraps the normal Node `childProcess.spawn` with graceful child shutdown behavior.
30
- * Also returns a convenient `closed` promise.
31
- * If you only need `closed`, prefer the shorthand function `spawn`.
32
- * @mutates global_spawn calls `register_global_spawn()` which adds to the module-level Set
26
+ * Creates a promise that resolves when the child process closes.
27
+ *
28
+ * Handles both 'error' and 'close' events with deduplication because Node.js
29
+ * can emit both for certain failures (e.g., spawn ENOENT emits 'error' then 'close').
30
+ * The `resolved` flag ensures we only resolve once with the first event's data.
33
31
  */
34
- export const spawn_process = (command, args = [], options) => {
32
+ const create_closed_promise = (child) => {
35
33
  let resolve;
34
+ let resolved = false;
36
35
  const closed = new Promise((r) => (resolve = r));
37
- const child = spawn_child_process(command, args, { stdio: 'inherit', ...options });
38
- const unregister = register_global_spawn(child);
36
+ child.once('error', (err) => {
37
+ if (resolved)
38
+ return;
39
+ resolved = true;
40
+ resolve({ ok: false, child, error: err });
41
+ });
39
42
  child.once('close', (code, signal) => {
40
- unregister();
41
- resolve(code ? { ok: false, child, code, signal } : { ok: true, child, code, signal });
43
+ if (resolved)
44
+ return;
45
+ resolved = true;
46
+ if (signal !== null) {
47
+ resolve({ ok: false, child, code: null, signal });
48
+ }
49
+ else {
50
+ resolve({ ok: code === 0, child, code: code ?? 0, signal: null });
51
+ }
42
52
  });
43
- return { closed, child };
53
+ return closed;
44
54
  };
45
- export const print_child_process = (child) => `${st('gray', 'pid(')}${child.pid}${st('gray', ')')} ← ${st('green', child.spawnargs.join(' '))}`;
46
55
  /**
47
- * We register spawned processes gloabally so we can gracefully exit child processes.
48
- * Otherwise, errors can cause zombie processes, sometimes blocking ports even!
56
+ * Sets up abort signal handling for a child process.
57
+ * @returns cleanup function to remove the listener
49
58
  */
50
- export const global_spawn = new Set();
59
+ const setup_abort_signal = (child, signal) => {
60
+ if (signal.aborted) {
61
+ child.kill();
62
+ return noop;
63
+ }
64
+ const on_abort = () => child.kill();
65
+ signal.addEventListener('abort', on_abort, { once: true });
66
+ return () => signal.removeEventListener('abort', on_abort);
67
+ };
51
68
  /**
52
- * Returns a function that unregisters the `child`.
53
- * @param child the child process to register
54
- * @returns cleanup function that removes the child from `global_spawn`
55
- * @mutates global_spawn adds child to the module-level Set, and the returned function removes it
69
+ * Validates timeout_ms option.
70
+ * @throws if timeout_ms is negative
56
71
  */
57
- export const register_global_spawn = (child) => {
58
- if (global_spawn.has(child)) {
59
- log.error(st('red', 'already registered global spawn:'), print_child_process(child));
72
+ const validate_timeout_ms = (timeout_ms) => {
73
+ if (timeout_ms !== undefined && timeout_ms < 0) {
74
+ throw new Error(`timeout_ms must be non-negative, got ${timeout_ms}`);
60
75
  }
61
- global_spawn.add(child);
62
- return () => {
63
- if (!global_spawn.has(child)) {
64
- log.error(st('red', 'spawn not registered:'), print_child_process(child));
65
- }
66
- global_spawn.delete(child);
67
- };
68
76
  };
69
77
  /**
70
- * Kills a child process and returns a `SpawnResult`.
78
+ * Sets up timeout handling for a child process.
79
+ * Note: timeout_ms of 0 triggers immediate SIGTERM (use with caution).
80
+ * @returns cleanup function to clear the timeout
71
81
  */
72
- export const despawn = (child) => {
73
- let resolve;
74
- const closed = new Promise((r) => (resolve = r));
75
- log.debug('despawning', print_child_process(child));
76
- child.once('close', (code, signal) => {
77
- resolve(code ? { ok: false, child, code, signal } : { ok: true, child, code, signal });
78
- });
79
- child.kill();
80
- return closed;
82
+ const setup_timeout = (child, timeout_ms) => {
83
+ const timeout_id = setTimeout(() => child.kill('SIGTERM'), timeout_ms);
84
+ return () => clearTimeout(timeout_id);
81
85
  };
86
+ //
87
+ // Process Registry
88
+ //
89
+ /**
90
+ * Manages a collection of spawned processes for lifecycle tracking and cleanup.
91
+ *
92
+ * The default instance `process_registry_default` is used by module-level functions.
93
+ * Create separate instances for isolated process groups or testing.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // Use default registry via module functions
98
+ * const result = await spawn('echo', ['hello']);
99
+ *
100
+ * // Or create isolated registry for testing
101
+ * const registry = new ProcessRegistry();
102
+ * const {child, closed} = registry.spawn('node', ['server.js']);
103
+ * await registry.despawn_all();
104
+ * ```
105
+ */
106
+ export class ProcessRegistry {
107
+ /** All currently tracked child processes */
108
+ processes = new Set();
109
+ #error_handler = null;
110
+ /**
111
+ * Spawns a process and tracks it in this registry.
112
+ * The process is automatically unregistered when it exits.
113
+ *
114
+ * @param command - The command to run
115
+ * @param args - Arguments to pass to the command
116
+ * @param options - Spawn options including `signal` and `timeout_ms`
117
+ * @returns Handle with `child` process and `closed` promise
118
+ */
119
+ spawn(command, args = [], options) {
120
+ const { signal, timeout_ms, ...spawn_options } = options ?? {};
121
+ validate_timeout_ms(timeout_ms);
122
+ const child = spawn_child_process(command, args, { stdio: 'inherit', ...spawn_options });
123
+ this.processes.add(child);
124
+ const closed = create_closed_promise(child);
125
+ let cleanup_abort;
126
+ if (signal) {
127
+ cleanup_abort = setup_abort_signal(child, signal);
128
+ }
129
+ let cleanup_timeout;
130
+ if (timeout_ms !== undefined) {
131
+ cleanup_timeout = setup_timeout(child, timeout_ms);
132
+ }
133
+ void closed.then(() => {
134
+ this.processes.delete(child);
135
+ cleanup_abort?.();
136
+ cleanup_timeout?.();
137
+ });
138
+ return { child, closed };
139
+ }
140
+ /**
141
+ * Spawns a process and captures stdout/stderr as strings.
142
+ * Sets `stdio: 'pipe'` automatically.
143
+ *
144
+ * @param command - The command to run
145
+ * @param args - Arguments to pass to the command
146
+ * @param options - Spawn options
147
+ * @returns Result with captured `stdout` and `stderr`.
148
+ * - `null` means spawn failed (ENOENT, etc.) or stream was unavailable
149
+ * - `''` (empty string) means process ran but produced no output
150
+ * - non-empty string contains the captured output
151
+ */
152
+ async spawn_out(command, args = [], options) {
153
+ const { child, closed } = this.spawn(command, args, { ...options, stdio: 'pipe' });
154
+ const stdout_chunks = [];
155
+ const stderr_chunks = [];
156
+ // Track whether streams were available (not null)
157
+ const stdout_available = child.stdout !== null;
158
+ const stderr_available = child.stderr !== null;
159
+ const on_stdout = (data) => {
160
+ stdout_chunks.push(data.toString());
161
+ };
162
+ const on_stderr = (data) => {
163
+ stderr_chunks.push(data.toString());
164
+ };
165
+ child.stdout?.on('data', on_stdout);
166
+ child.stderr?.on('data', on_stderr);
167
+ const result = await closed;
168
+ // Clean up listeners explicitly
169
+ child.stdout?.off('data', on_stdout);
170
+ child.stderr?.off('data', on_stderr);
171
+ // If spawn failed (error result), streams are meaningless - return null
172
+ // Otherwise: '' = available but empty, string = has content
173
+ const spawn_failed = spawn_result_is_error(result);
174
+ const stdout = spawn_failed || !stdout_available ? null : stdout_chunks.join('');
175
+ const stderr = spawn_failed || !stderr_available ? null : stderr_chunks.join('');
176
+ return { result, stdout, stderr };
177
+ }
178
+ /**
179
+ * Kills a child process and waits for it to exit.
180
+ *
181
+ * @param child - The child process to kill
182
+ * @param options - Kill options including signal and timeout
183
+ * @returns The spawn result after the process exits
184
+ */
185
+ async despawn(child, options) {
186
+ const { signal = 'SIGTERM', timeout_ms } = options ?? {};
187
+ validate_timeout_ms(timeout_ms);
188
+ // Already exited with code
189
+ if (child.exitCode !== null) {
190
+ return {
191
+ ok: child.exitCode === 0,
192
+ child,
193
+ code: child.exitCode,
194
+ signal: null,
195
+ };
196
+ }
197
+ // Already terminated by signal
198
+ if (child.signalCode !== null) {
199
+ return {
200
+ ok: false,
201
+ child,
202
+ code: null,
203
+ signal: child.signalCode,
204
+ };
205
+ }
206
+ log.debug('despawning', print_child_process(child));
207
+ const closed = create_closed_promise(child);
208
+ // Escalate to SIGKILL after timeout
209
+ if (timeout_ms !== undefined) {
210
+ const timeout_id = setTimeout(() => child.kill('SIGKILL'), timeout_ms);
211
+ void closed.then(() => clearTimeout(timeout_id));
212
+ }
213
+ child.kill(signal);
214
+ return closed;
215
+ }
216
+ /**
217
+ * Kills all processes in this registry.
218
+ *
219
+ * @param options - Kill options applied to all processes
220
+ * @returns Array of spawn results
221
+ */
222
+ async despawn_all(options) {
223
+ return Promise.all([...this.processes].map((child) => this.despawn(child, options)));
224
+ }
225
+ /**
226
+ * Attaches an `uncaughtException` handler that kills all processes before exiting.
227
+ * Prevents zombie processes when the parent crashes.
228
+ *
229
+ * By default uses SIGKILL for immediate termination. Set `graceful_timeout_ms`
230
+ * to attempt SIGTERM first (allowing processes to clean up) before escalating
231
+ * to SIGKILL after the timeout.
232
+ *
233
+ * Note: Node's uncaughtException handler cannot await async operations, so
234
+ * graceful shutdown uses a blocking busy-wait. This may not be sufficient
235
+ * for processes that need significant cleanup time.
236
+ *
237
+ * @param options - Configuration options
238
+ * @param options.to_error_label - Customize error label, return `null` for default
239
+ * @param options.map_error_text - Customize error text, return `''` to silence
240
+ * @param options.handle_error - Called after cleanup, defaults to `process.exit(1)`
241
+ * @param options.graceful_timeout_ms - If set, sends SIGTERM first and waits this
242
+ * many ms before SIGKILL. Recommended: 100-500ms. If null/undefined, uses
243
+ * immediate SIGKILL (default).
244
+ * @returns Cleanup function to remove the handler
245
+ */
246
+ attach_error_handler(options) {
247
+ if (this.#error_handler) {
248
+ throw new Error('Error handler already attached to this registry');
249
+ }
250
+ const { to_error_label, map_error_text, handle_error = () => process.exit(1), graceful_timeout_ms, } = options ?? {};
251
+ this.#error_handler = (err, origin) => {
252
+ const label = to_error_label?.(err, origin) ?? origin;
253
+ if (label) {
254
+ const error_text = map_error_text?.(err, origin) ?? print_error(err);
255
+ if (error_text) {
256
+ new Logger(label).error(error_text);
257
+ }
258
+ }
259
+ if (graceful_timeout_ms != null && graceful_timeout_ms > 0) {
260
+ // Attempt graceful shutdown with SIGTERM first
261
+ for (const child of this.processes) {
262
+ child.kill('SIGTERM');
263
+ }
264
+ // Busy-wait (blocking) - only option in sync handler.
265
+ // Warning: This will peg the CPU during the wait period.
266
+ const deadline = Date.now() + graceful_timeout_ms;
267
+ while (Date.now() < deadline) {
268
+ // spin
269
+ }
270
+ }
271
+ // Force kill all (including any that survived SIGTERM)
272
+ for (const child of this.processes) {
273
+ child.kill('SIGKILL');
274
+ }
275
+ this.processes.clear();
276
+ handle_error(err, origin);
277
+ };
278
+ process.on('uncaughtException', this.#error_handler);
279
+ return () => {
280
+ if (this.#error_handler) {
281
+ process.off('uncaughtException', this.#error_handler);
282
+ this.#error_handler = null;
283
+ }
284
+ };
285
+ }
286
+ }
287
+ //
288
+ // Default Registry
289
+ //
82
290
  /**
83
- * Kills all globally registered child processes.
84
- * @mutates global_spawn indirectly removes processes through `despawn()` calls
291
+ * Default process registry used by module-level spawn functions.
292
+ * For testing or isolated process groups, create a new `ProcessRegistry` instance.
85
293
  */
86
- export const despawn_all = () => Promise.all(Array.from(global_spawn, (child) => despawn(child)));
294
+ export const process_registry_default = new ProcessRegistry();
295
+ //
296
+ // Module-Level Spawn Functions
297
+ //
87
298
  /**
88
- * Attaches the `'uncoughtException'` event to despawn all processes,
89
- * and enables custom error logging with `to_error_label`.
90
- * @param to_error_label - Customize the error label or return `null` for the default `origin`.
91
- * @param map_error_text - Customize the error text. Return `''` to silence, or `null` for the default `print_error(err)`.
299
+ * Spawns a process with graceful shutdown behavior.
300
+ * Returns a handle with access to the `child` process and `closed` promise.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * const {child, closed} = spawn_process('node', ['server.js']);
305
+ * // Later...
306
+ * child.kill();
307
+ * const result = await closed;
308
+ * ```
92
309
  */
93
- export const attach_process_error_handlers = (to_error_label, map_error_text, handle_error = () => process.exit(1)) => {
94
- process.on('uncaughtException', async (err, origin) => {
95
- const label = to_error_label?.(err, origin) ?? origin;
96
- if (label) {
97
- const error_text = map_error_text?.(err, origin) ?? print_error(err);
98
- if (error_text) {
99
- new Logger(label).error(error_text);
100
- }
101
- }
102
- await despawn_all();
103
- handle_error(err, origin);
104
- });
105
- };
310
+ export const spawn_process = (command, args = [], options) => process_registry_default.spawn(command, args, options);
311
+ /**
312
+ * Spawns a process and returns a promise that resolves when it exits.
313
+ * Use this for commands that complete (not long-running processes).
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * const result = await spawn('npm', ['install']);
318
+ * if (!result.ok) console.error('Install failed');
319
+ * ```
320
+ */
321
+ export const spawn = (command, args = [], options) => spawn_process(command, args, options).closed;
322
+ /**
323
+ * Spawns a process and captures stdout/stderr as strings.
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * const {result, stdout} = await spawn_out('git', ['status', '--porcelain']);
328
+ * if (result.ok && stdout) console.log(stdout);
329
+ * ```
330
+ */
331
+ export const spawn_out = (command, args = [], options) => process_registry_default.spawn_out(command, args, options);
332
+ /**
333
+ * Kills a child process and returns the result.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * const result = await despawn(child, {timeout_ms: 5000});
338
+ * // If process ignores SIGTERM, SIGKILL sent after 5s
339
+ * ```
340
+ */
341
+ export const despawn = (child, options) => process_registry_default.despawn(child, options);
106
342
  /**
107
- * Formats a `SpawnResult` for printing.
343
+ * Kills all processes in the default registry.
344
+ */
345
+ export const despawn_all = (options) => process_registry_default.despawn_all(options);
346
+ /**
347
+ * Attaches an `uncaughtException` handler to the default registry.
348
+ *
349
+ * @see ProcessRegistry.attach_error_handler
350
+ */
351
+ export const attach_process_error_handler = (options) => process_registry_default.attach_error_handler(options);
352
+ //
353
+ // Formatting Utilities
354
+ //
355
+ /**
356
+ * Formats a child process for display.
357
+ *
358
+ * @example `pid(1234) <- node server.js`
359
+ */
360
+ export const print_child_process = (child) => `${st('gray', 'pid(')}${child.pid ?? 'none'}${st('gray', ')')} ← ${st('green', child.spawnargs.join(' '))}`;
361
+ /**
362
+ * Formats a spawn result for display.
363
+ * Returns `'ok'` for success, or the error/signal/code for failures.
108
364
  */
109
365
  export const print_spawn_result = (result) => {
110
366
  if (result.ok)
111
367
  return 'ok';
112
- let text = result.code === null ? '' : print_key_value('code', result.code);
113
- if (result.signal !== null)
114
- text += (text ? ' ' : '') + print_key_value('signal', result.signal);
115
- return text;
368
+ if (spawn_result_is_error(result))
369
+ return result.error.message;
370
+ if (spawn_result_is_signaled(result))
371
+ return print_key_value('signal', result.signal);
372
+ return print_key_value('code', result.code);
116
373
  };
117
374
  /**
118
- * Like `spawn_process` but with `restart` and `kill`,
119
- * handling many concurrent `restart` calls gracefully.
375
+ * Formats a spawn result for use in error messages.
376
+ */
377
+ export const spawn_result_to_message = (result) => {
378
+ if (spawn_result_is_error(result))
379
+ return `error: ${result.error.message}`;
380
+ if (spawn_result_is_signaled(result))
381
+ return `signal ${result.signal}`;
382
+ return `code ${result.code}`;
383
+ };
384
+ /**
385
+ * Spawns a process that can be restarted.
386
+ * Handles concurrent restart calls gracefully.
387
+ *
388
+ * Note: The `signal` and `timeout_ms` options are reapplied on each restart.
389
+ * If the AbortSignal is already aborted when `restart()` is called, the new
390
+ * process will be killed immediately.
391
+ *
392
+ * @example Simple restart on crash
393
+ * ```ts
394
+ * const rp = spawn_restartable_process('node', ['server.js']);
395
+ *
396
+ * while (rp.active) {
397
+ * const result = await rp.closed;
398
+ * if (result.ok) break; // Clean exit
399
+ * await rp.restart();
400
+ * }
401
+ * ```
402
+ *
403
+ * @example Restart with backoff
404
+ * ```ts
405
+ * const rp = spawn_restartable_process('node', ['server.js']);
406
+ * let failures = 0;
407
+ *
408
+ * while (rp.active) {
409
+ * const result = await rp.closed;
410
+ * if (result.ok || ++failures > 5) break;
411
+ * await new Promise((r) => setTimeout(r, 1000 * failures));
412
+ * await rp.restart();
413
+ * }
414
+ * ```
120
415
  */
121
416
  export const spawn_restartable_process = (command, args = [], options) => {
122
- let spawned = null;
123
- let restarting = null;
124
- const close = async () => {
125
- if (!spawned)
417
+ let spawned_process = null;
418
+ let pending_close = null;
419
+ let pending_restart = null;
420
+ let pending_kill = null;
421
+ // Deferred promise - resolves when first process spawns
422
+ let closed_promise;
423
+ let resolve_closed;
424
+ const reset_closed_promise = () => {
425
+ closed_promise = new Promise((r) => (resolve_closed = r));
426
+ };
427
+ reset_closed_promise();
428
+ // Resolve when first spawn completes to avoid race conditions
429
+ let resolve_spawned;
430
+ const spawned = new Promise((r) => (resolve_spawned = r));
431
+ const do_close = async () => {
432
+ if (!spawned_process)
126
433
  return;
127
- restarting = spawned.closed;
128
- spawned.child.kill();
129
- spawned = null;
130
- await restarting;
131
- restarting = null;
434
+ pending_close = spawned_process.closed;
435
+ spawned_process.child.kill();
436
+ spawned_process = null;
437
+ await pending_close;
438
+ pending_close = null;
132
439
  };
133
- const restart = async () => {
134
- if (restarting)
135
- return restarting;
136
- if (spawned)
137
- await close();
138
- spawned = spawn_process(command, args, { stdio: 'inherit', ...options });
440
+ const do_restart = async () => {
441
+ // Wait for any in-progress kill or close before restarting
442
+ if (pending_kill)
443
+ await pending_kill;
444
+ if (pending_close)
445
+ await pending_close;
446
+ if (spawned_process)
447
+ await do_close();
448
+ spawned_process = spawn_process(command, args, { stdio: 'inherit', ...options });
449
+ // Forward the spawned process's closed promise to our exposed one
450
+ void spawned_process.closed.then((result) => {
451
+ resolve_closed(result);
452
+ });
453
+ };
454
+ // Coalesce concurrent restart calls - multiple calls share one restart
455
+ const restart = () => {
456
+ if (!pending_restart) {
457
+ // Reset the closed promise for the new process
458
+ reset_closed_promise();
459
+ pending_restart = do_restart().finally(() => {
460
+ pending_restart = null;
461
+ });
462
+ }
463
+ return pending_restart;
139
464
  };
465
+ // Wait for any pending restart to complete first, ensuring we kill
466
+ // the newly spawned process rather than racing with it
140
467
  const kill = async () => {
141
- if (restarting)
142
- await restarting;
143
- await close();
468
+ if (pending_kill)
469
+ return pending_kill;
470
+ pending_kill = (async () => {
471
+ if (pending_restart)
472
+ await pending_restart;
473
+ if (pending_close)
474
+ await pending_close;
475
+ await do_close();
476
+ })();
477
+ try {
478
+ await pending_kill;
479
+ }
480
+ finally {
481
+ pending_kill = null;
482
+ }
483
+ };
484
+ // Start immediately and resolve spawned promise when done
485
+ void restart().then(() => resolve_spawned());
486
+ return {
487
+ restart,
488
+ kill,
489
+ get active() {
490
+ return spawned_process !== null;
491
+ },
492
+ get child() {
493
+ return spawned_process?.child ?? null;
494
+ },
495
+ get closed() {
496
+ return closed_promise;
497
+ },
498
+ get spawned() {
499
+ return spawned;
500
+ },
144
501
  };
145
- // Start immediately -- it sychronously starts the process so there's no need to await.
146
- void restart();
147
- return { restart, kill };
148
502
  };
503
+ //
504
+ // Utility Functions
505
+ //
149
506
  /**
150
- * Check if a PID is still running.
507
+ * Checks if a process with the given PID is running.
508
+ * Uses signal 0 which checks existence without sending a signal.
509
+ *
510
+ * @param pid - The process ID to check (must be a positive integer)
511
+ * @returns `true` if the process exists (even without permission to signal it),
512
+ * `false` if the process doesn't exist or if pid is invalid (non-positive, non-integer, NaN, Infinity)
151
513
  */
152
514
  export const process_is_pid_running = (pid) => {
515
+ // Handle NaN, Infinity, negative, zero, non-integers, and fractional values
516
+ if (!Number.isInteger(pid) || pid <= 0)
517
+ return false;
153
518
  try {
154
- // Sending signal 0 doesn't actually send a signal, just checks if process exists
155
519
  process.kill(pid, 0);
156
520
  return true;
157
521
  }
158
522
  catch (err) {
159
- // ESRCH = no such process, EPERM = exists but no permission
160
- return err.code === 'EPERM';
523
+ // ESRCH = no such process
524
+ // EPERM = process exists but we lack permission to signal it
525
+ // Safely access .code in case of unexpected error types
526
+ const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
527
+ return code === 'EPERM';
161
528
  }
162
529
  };
package/dist/random.d.ts CHANGED
@@ -19,7 +19,7 @@ export declare const random_boolean: (random?: () => number) => boolean;
19
19
  export declare const random_item: <T extends ReadonlyArray<any>>(arr: T, random?: () => number) => ArrayElement<T>;
20
20
  /**
21
21
  * Mutates `array` with random ordering.
22
- * @mutates array randomly reorders elements in place using Fisher-Yates shuffle
22
+ * @mutates array - randomly reorders elements in place using Fisher-Yates shuffle
23
23
  */
24
24
  export declare const shuffle: <T extends Array<any>>(array: T, random?: typeof random_int) => T;
25
25
  //# sourceMappingURL=random.d.ts.map
package/dist/random.js CHANGED
@@ -18,7 +18,7 @@ export const random_boolean = (random = Math.random) => random() > 0.5;
18
18
  export const random_item = (arr, random = Math.random) => arr[random_int(0, arr.length - 1, random)];
19
19
  /**
20
20
  * Mutates `array` with random ordering.
21
- * @mutates array randomly reorders elements in place using Fisher-Yates shuffle
21
+ * @mutates array - randomly reorders elements in place using Fisher-Yates shuffle
22
22
  */
23
23
  export const shuffle = (array, random = random_int) => {
24
24
  const { length } = array;
package/dist/regexp.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare const escape_regexp: (str: string) => string;
6
6
  /**
7
7
  * Reset a RegExp's lastIndex to 0 for global and sticky patterns.
8
8
  * Ensures consistent behavior by clearing state that affects subsequent matches.
9
- * @mutates regexp sets lastIndex to 0 if regexp is global or sticky
9
+ * @mutates regexp - sets lastIndex to 0 if regexp is global or sticky
10
10
  */
11
11
  export declare const reset_regexp: <T extends RegExp>(regexp: T) => T;
12
12
  //# sourceMappingURL=regexp.d.ts.map