/** * Batched file reads over the transport. * * Every `sbx exec` is a round-trip, and measured tool latency is ≈0.5 s per * round-trip. Reading files one exec at a time is therefore the dominant cost * of the reimplemented `grep`: it does `exists` + `isDirectory` + * `git ls-files` + ONE read per candidate file, so a grep over ~170 files cost * ~170 round-trips — the measured p90 of 86.5 s matches that almost exactly. * * This module collapses those reads into one exec per *chunk* of files, with the * chunk bounds chosen so the generated argv stays far below `ARG_MAX`. The * matching logic in `operations.ts` is untouched: it runs over the buffered * contents, in the original candidate order, exactly as it did per file. * * ## Framing * * Per readable file the remote loop emits `\0\0`. NUL is * the delimiter — never a newline — for two reasons: * * - paths returned by `git ls-files -z` may legitimately CONTAIN newlines, so a * line-delimited protocol would split one path into two records, and * - base64 never contains NUL, so `\0\0` records are unambiguous * regardless of what bytes a path holds. * * A file the remote loop cannot read emits NO record. The caller treats "absent * from the map" as "skip", which is exactly the catch-and-continue behaviour the * per-file path had — a binary or permission-denied file contributes nothing * rather than failing the whole grep. * * Zero imports (not even node builtins beyond the `Buffer` global): the argv * builder, chunker, parser and `readManyBytes` are all unit-testable without the * pi packages. `shArgs`'s convention (values passed POSITIONALLY, never spliced * into script text) is replicated here rather than imported, because importing * `transport.ts` would pull in the pi packages and make this module untestable. */ /** The subset of an `ExecOutcome` the batched read needs. */ export interface BatchReadOutcome { /** null when the process was killed by a signal (our own abort/timeout). */ exitCode: number | null; stdout?: Uint8Array | string; stderr?: Uint8Array | string; } /** The subset of `ExecTransport` the batched read needs. */ export interface BatchReadTransport { exec( argv: string[], opts?: { timeout?: number; signal?: AbortSignal; onData?: (chunk: Uint8Array) => void }, ): Promise; } /** At most this many paths per exec (count bound). */ export const READ_MANY_MAX_FILES = 256; /** At most this many bytes of path per exec (size bound), keeping argv well under ARG_MAX. */ export const READ_MANY_MAX_PATH_BYTES = 96 * 1024; /** * Emit `\0\0` for each readable argument, skipping unreadable ones. * * POSIX sh only, and the file list is passed positionally (`"$@"`) — the same * convention as `shArgs`, so a path can never be reinterpreted as shell syntax * or as an option. `tr -d "\n"` makes the base64 independent of the line-wrap * default (GNU `-w0` is not universal). */ const READ_MANY_SCRIPT = [ 'for f in "$@"; do', ' [ -r "$f" ] || continue', ' printf "%s\\0" "$f"', ' base64 < "$f" | tr -d "\\n"', ' printf "\\0"', "done", ].join("\n"); /** Build `sh -c