diff --git a/src/caching.ts b/src/caching.ts index 2c47d48..64206e5 100644 --- a/src/caching.ts +++ b/src/caching.ts @@ -24,12 +24,17 @@ import { import type { ProcExecutor, ProcRequest, ProcResult } from "./contract.ts"; import { procRequestSchema } from "./contract.ts"; +/** A keyed store of {@link ProcResult}s backing a caching executor. */ export interface ProcCache { + /** The cached result for `key`, or `undefined` on a miss. */ get(key: string): ProcResult | undefined; + /** Cache `value` under `key`. */ set(key: string, value: ProcResult): void; + /** Drop everything. */ clear(): void; } +/** A simple in-memory {@link ProcCache} (a `Map`); not persisted across processes. */ export function inMemoryProcCache(): ProcCache { const map = new Map(); return { @@ -64,16 +69,21 @@ function cacheKey(req: ProcRequest): string { }); } +/** A {@link ProcExecutor} that memoizes cacheable requests, with an explicit drop. */ export interface CachingProcExecutor extends ProcExecutor { /** Drop all cached results — the next read of each re-derives lazily. */ invalidate(): void; } +/** Options for {@link cachingProcExecutor}. */ export interface CachingProcOptions { + /** The backing store (defaults to {@link inMemoryProcCache}). */ cache?: ProcCache; + /** Predicate deciding which requests may be cached (defaults to {@link policyCacheable}). */ isCacheable?: (req: ProcRequest) => boolean; } +/** Wrap an executor so cacheable requests are memoized; returns a {@link CachingProcExecutor}. */ export function cachingProcExecutor( inner: ProcExecutor, opts: CachingProcOptions = {}, diff --git a/src/capture.ts b/src/capture.ts index ad5bfc8..b5cec39 100644 --- a/src/capture.ts +++ b/src/capture.ts @@ -21,27 +21,37 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +/** Result of a temp-file-backed spawn capture (no in-memory output ceiling). */ export type SpawnCaptureResult = { + /** Exit code, or `null` if terminated by a signal. */ status: number | null; + /** Terminating signal, or `null`. */ signal: NodeJS.Signals | null; /** Fully-read child stdout — no in-memory ceiling. */ stdout: string; + /** Fully-read child stderr. */ stderr: string; + /** A spawn error (e.g. ENOENT, ETIMEDOUT), if any. */ error?: Error | undefined; }; +/** Options for {@link spawnCapture}. */ export type SpawnCaptureOptions = { + /** Working directory for the child. */ cwd?: string | undefined; + /** Environment for the child. */ env?: NodeJS.ProcessEnv | undefined; /** ms; passed through to spawnSync. */ timeout?: number | undefined; }; +/** The {@link spawnCapture} signature — an injectable capture seam. */ export type SpawnCaptureFn = ( cmd: readonly string[], options?: SpawnCaptureOptions, ) => SpawnCaptureResult; +/** Run a command capturing stdout/stderr via temp files (no in-memory cap — for output that overflows spawnSync's buffer). */ export const spawnCapture: SpawnCaptureFn = (cmd, options = {}) => { const [file, ...args] = cmd; const dir = fs.mkdtempSync(path.join(os.tmpdir(), "prx-spawn-")); @@ -111,6 +121,7 @@ export function captureFailureDetail(r: SpawnCaptureResult): string { return r.error?.message ?? (r.signal ? `killed by ${r.signal}` : (r.stderr || "").trim()); } +/** {@link SpawnCaptureOptions} plus stdin + per-chunk streaming callbacks. */ export type StreamCaptureOptions = SpawnCaptureOptions & { /** Serializable stdin written to the child then closed (no live stream). */ stdin?: string | undefined; diff --git a/src/contract.ts b/src/contract.ts index ab8d8dc..1351917 100644 --- a/src/contract.ts +++ b/src/contract.ts @@ -20,6 +20,7 @@ import { processEnv } from "@bounded-systems/env"; import { streamCapture } from "./capture.ts"; +/** Zod schema validating a {@link ProcRequest} (command + args + cwd/env/stdin/timeout/stdio). */ export const procRequestSchema = z.object({ command: z.string().min(1), args: z.array(z.string()).default([]), @@ -30,12 +31,18 @@ export const procRequestSchema = z.object({ stdio: z.enum(["pipe", "inherit"]).default("pipe"), }); +/** A subprocess request: the command to run and how (parsed from {@link procRequestSchema}). */ export type ProcRequest = z.input; +/** The outcome of a subprocess: exit status, captured streams, and terminating signal. */ export interface ProcResult { + /** Exit code (or a signal-derived status when killed). */ readonly status: number; + /** Captured standard output. */ readonly stdout: string; + /** Captured standard error. */ readonly stderr: string; + /** The signal that terminated the process, or `null`. */ readonly signal: string | null; } @@ -44,6 +51,7 @@ export interface ProcResult { * ship the request over a wire and run it elsewhere — the contract is identical. */ export interface ProcExecutor { + /** Run `req` and resolve its {@link ProcResult}. */ exec(req: ProcRequest): Promise; } diff --git a/src/index.ts b/src/index.ts index 8acf97c..9b7f2f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,16 +33,23 @@ export { captureFailureDetail, } from "./capture.ts"; +/** The captured result of a {@link CommandRunner} run. */ export type CommandResult = { + /** Captured standard output. */ stdout: string; + /** Captured standard error. */ stderr: string; + /** Exit code (signal-derived when killed). */ status: number; }; +/** Options for a {@link CommandRunner} run. */ export interface RunOptions { + /** Working directory for the child. */ cwd?: string; /** When not false (default), a non-zero exit throws. */ check?: boolean; + /** Environment for the child (defaults to the sanctioned `processEnv()`). */ env?: NodeJS.ProcessEnv; /** * "pipe" (default) captures stdout/stderr into the result; "inherit" wires @@ -72,6 +79,7 @@ function signalExitStatus(signal: NodeJS.Signals | string | null): number { return 128 + (typeof num === "number" ? num : 0); } +/** The default {@link CommandRunner}: a synchronous `spawnSync` with capture, timeout, and (by default) throw-on-nonzero-exit. */ export const defaultRunner: CommandRunner = (cmd, options = {}) => { const [file, ...args] = cmd; if (!file) { diff --git a/src/spawn-detached.ts b/src/spawn-detached.ts index 1e0930c..2b80104 100644 --- a/src/spawn-detached.ts +++ b/src/spawn-detached.ts @@ -14,8 +14,11 @@ import { closeSync, openSync } from "node:fs"; import { processEnv } from "@bounded-systems/env"; +/** Options for {@link spawnDetached} — a child that outlives the parent. */ export type SpawnDetachedOptions = { + /** Working directory for the child. */ cwd?: string; + /** Environment for the child. */ env?: NodeJS.ProcessEnv; /** * When set, the child's stdout+stderr are appended to this file (created if @@ -25,6 +28,7 @@ export type SpawnDetachedOptions = { logPath?: string; }; +/** Result of {@link spawnDetached}: the detached child's process id. */ export type SpawnDetachedResult = { pid: number }; /**