Forcola.Stream (forcola v0.1.0)

Copy Markdown View Source

Line-by-line output from a shim-supervised process as an Enumerable.

For CLIs that emit NDJSON or line-oriented progress (agent CLIs, docker events, git clone --progress). The child runs in its own process group; halting the stream, timing out, or BEAM death all kill the whole group.

Forcola.Stream.lines(["claude", "-p", prompt], timeout_ms: 300_000)
|> Stream.map(&:json.decode/1)
|> Enum.to_list()

Termination semantics

  • A zero exit ends the stream cleanly.
  • A non-zero exit, death by signal, timeout, or spawn failure raises Forcola.Stream.Error after every line produced before death has been emitted. The consumers this is built for are NDJSON CLIs where mid-stream death matters: silently dropping the exit reproduces the leak this library exists to close, and a tagged final element would force every Stream.map(&decode/1) pipeline to special-case it. Stderr captured during the run rides in the exception.
  • Halting the stream early (Enum.take/2, Stream.take_while/2, an exception downstream) kills the process group and blocks until the shim confirms the group is dead.
  • If the consuming process dies, the port closes, the shim sees stdin EOF, and the group is killed.

Summary

Functions

Run argv and return its stdout as a lazy stream of lines.

Functions

lines(argv, opts)

@spec lines(
  [String.t(), ...],
  keyword()
) :: Enumerable.t()

Run argv and return its stdout as a lazy stream of lines.

Takes the same options as Forcola.run/2; :timeout_ms is required and bounds the whole run, not the gap between lines (an idle-timeout option is tracked in #33).

Lines are emitted without their trailing newline. A partial line held across frame boundaries is emitted once its newline arrives; a final partial line with no newline is emitted before the stream terminates.

See the module docs for how termination is surfaced.