# `Denox.Run`
[🔗](https://github.com/gsmlg-dev/denox/blob/v0.9.0/lib/denox/run.ex#L1)

Run Deno programs as NIF-backed long-lived runtimes.

Uses an in-process `deno_runtime` MainWorker (no external `deno` binary
required) wrapped in a GenServer with bidirectional stdio and OTP supervision.

## Supported Specifiers

`Denox.Run` uses a built-in module loader that supports:

  - **Local files** — `file: "path/to/script.ts"` (absolute or relative path)
  - **HTTPS/HTTP modules** — `package: "https://deno.land/x/pkg/mod.ts"`

**`npm:` and `jsr:` packages are not supported** by the NIF backend.
The NIF module loader has no Node.js resolver or npm registry client.
For npm/jsr packages, use `Denox.CLI.Run` which delegates to the bundled
Deno binary that handles all module schemes natively.

## Examples

    # Run a local script
    {:ok, pid} = Denox.Run.start_link(
      file: "scripts/server.ts",
      permissions: :all
    )

    # Send JSON-RPC via stdin
    :ok = Denox.Run.send(pid, ~s|{"jsonrpc":"2.0","method":"initialize","id":1}|)

    # Receive response from stdout
    {:ok, line} = Denox.Run.recv(pid, timeout: 5000)

    # Or subscribe to all output
    Denox.Run.subscribe(pid)
    # => receives {:denox_run_stdout, ^pid, line} messages

    # Load an HTTPS module
    {:ok, pid} = Denox.Run.start_link(
      package: "https://deno.land/x/cowsay/mod.ts",
      permissions: :all
    )

    # For npm/jsr packages, use Denox.CLI.Run instead:
    {:ok, pid} = Denox.CLI.Run.start_link(
      package: "@modelcontextprotocol/server-github",
      permissions: :all,
      env: %{"GITHUB_PERSONAL_ACCESS_TOKEN" => token}
    )

    # Stop the process
    Denox.Run.stop(pid)

## Convenience Functions

Several higher-level helpers are available for common patterns:

    # One-shot: collect all output and return a list
    {:ok, lines} = Denox.Run.capture(file: "scripts/build.ts", permissions: :all)

    # Lazy stream: process lines one at a time (early exit supported)
    Denox.Run.stream(file: "scripts/generate.ts", permissions: :all)
    |> Stream.filter(&String.contains?(&1, "ERROR"))
    |> Enum.to_list()

    # Bracket-style: guaranteed cleanup even on exception
    Denox.Run.with_runtime([file: "scripts/server.ts", permissions: :all], fn pid ->
      :ok = Denox.Run.send(pid, Jason.encode!(%{method: "init"}))
      {:ok, response} = Denox.Run.recv(pid, timeout: 10_000)
      Jason.decode!(response)
    end)

    # Bracket + lazy stream from an existing PID
    Denox.Run.with_runtime([file: "scripts/gen.ts", permissions: :all], fn pid ->
      :ok = Denox.Run.send(pid, Jason.encode!(request))
      Denox.Run.stream_from(pid, timeout: 10_000) |> Enum.to_list()
    end)

## Telemetry Events

Denox.Run emits the following telemetry events:

  * `[:denox, :run, :start]` — emitted when the runtime starts
    * Measurements: `%{system_time: integer}`
    * Metadata: `%{package: string | nil, file: string | nil, backend: :nif}`

  * `[:denox, :run, :stop]` — emitted when the runtime exits
    * Measurements: `%{system_time: integer}`
    * Metadata: `%{package: string | nil, file: string | nil, exit_status: integer, backend: :nif}`

  * `[:denox, :run, :recv]` — emitted for each stdout line received
    * Measurements: `%{system_time: integer}`
    * Metadata: `%{line_bytes: integer, backend: :nif}`

## Environment Variables

The `:env` option passes environment variables to the Deno runtime via
`Deno.env.get()`. The variables are set in the OS process environment
before the worker starts. **Note:** concurrent `Denox.Run` instances may
see each other's env vars if started simultaneously. For strict isolation,
use `Denox.CLI.Run` with a subprocess-per-instance model, or ensure env
var names are unique across instances.

## Thread Scaling

Each `Denox.Run` instance occupies **1 OS thread** (the event loop thread
running the V8 `MainWorker`) plus **1 dirty I/O scheduler slot** while
`runtime_run_recv` blocks waiting for stdout.

OTP provides 10 dirty I/O schedulers by default. This limits concurrent
`Denox.Run` instances to approximately **10** before dirty I/O scheduler
contention occurs. For higher concurrency, increase the dirty I/O scheduler
count via the `+SDio N` BEAM flag (e.g. in `vm.args` or `rel/vm.args.eex`):

    ## rel/vm.args.eex
    +SDio 32

For subprocess-based isolation without dirty scheduler limits, use
`Denox.CLI.Run` instead, which spawns a separate OS process per instance.

# `t`

```elixir
@type t() :: %Denox.Run{
  backend_state: term(),
  exit_status: term(),
  file: term(),
  package: term(),
  recv_waiters: term(),
  stdout_buffer: term(),
  subscribers: term()
}
```

# `alive?`

```elixir
@spec alive?(GenServer.server()) :: boolean()
```

Check if the runtime is still running.

# `capture`

```elixir
@spec capture(keyword()) :: {:ok, [String.t()]} | {:error, term()}
```

Run a script and capture all stdout output.

Starts a managed runtime, collects all output lines until the script
exits or the per-line timeout is reached, then stops the runtime.

Returns `{:ok, lines}` where `lines` is a list of stdout lines in order,
or `{:error, reason}` if the runtime failed to start.

## Options

Same as `start_link/1`, plus:

  - `:timeout` - per-line receive timeout in milliseconds (default: `30_000`).
    If no new line arrives within this window, collection stops and the
    lines collected so far are returned.

## Example

    {:ok, lines} = Denox.Run.capture(
      file: "scripts/generate.ts",
      permissions: :all
    )
    # lines => ["line 1", "line 2", ...]

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `os_pid`

```elixir
@spec os_pid(GenServer.server()) ::
  {:ok, non_neg_integer()} | {:error, :not_available | :not_running}
```

Return the OS PID of the process, if available.

Returns `{:ok, pid}` for CLI-backed runtimes or `{:error, :not_available}`
for NIF-backed runtimes where no separate OS process exists.

# `recv`

```elixir
@spec recv(
  GenServer.server(),
  keyword()
) :: {:ok, String.t()} | {:error, :timeout | :closed}
```

Receive the next line from stdout.

## Options
  - `:timeout` - milliseconds to wait (default: 5000)

# `send`

```elixir
@spec send(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Send data to stdin of the running process.

A newline (`\n`) is automatically appended if `data` does not
already end with one.

Returns `:ok` on success, or `{:error, :closed}` if the process
has already exited.

# `send_and_recv`

```elixir
@spec send_and_recv(GenServer.server(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, term()}
```

Send data to stdin and wait for the next stdout line.

A convenience wrapper around `send/2` + `recv/2` for the common
request-response pattern (e.g. JSON-RPC over stdio, MCP servers).

Returns `{:ok, response_line}` or `{:error, reason}` where reason is
`:timeout`, `:closed`, or the send error.

## Options

  - `:timeout` - milliseconds to wait for a response (default: 5000)

## Example

    request = Jason.encode!(%{jsonrpc: "2.0", method: "ping", id: 1})
    {:ok, response_line} = Denox.Run.send_and_recv(pid, request, timeout: 5000)
    response = Jason.decode!(response_line)

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start a managed Deno runtime.

## Options

  - `:package` - JSR/npm package specifier
  - `:file` - local file path to run
  - `:permissions` - permission mode; defaults to `:none` (deny all) when omitted:
    - `:all` — allow all permissions (`-A` in Deno CLI)
    - `:none` — deny all permissions (Deno's default behaviour)
    - keyword list — granular permissions, e.g. `[allow_net: true, allow_read: ["/tmp"],
      deny_env: true]`. Supports both `allow_*` and `deny_*` keys.
  - `:env` - map of environment variables
  - `:args` - extra arguments after the specifier
  - `:name` - GenServer name for registration
  - `:buffer_size` - (NIF backend only) stdout channel capacity in lines;
    controls how many lines the NIF can buffer before applying backpressure.
    Range `[1, 100_000]`; `0` or omitted uses the default of `1024` lines.

# `stop`

```elixir
@spec stop(GenServer.server()) :: :ok
```

Stop the runtime.

# `stream`

```elixir
@spec stream(keyword()) :: Enumerable.t()
```

Stream stdout lines from a script as a lazy `Stream`.

Starts a managed runtime and returns an `Enumerable` that yields
stdout lines one at a time. The runtime is automatically stopped
when the stream is fully consumed or halted (e.g. via `Stream.take/2`
or `Enum.take/2`).

This is useful for processing large outputs without buffering all
lines in memory, or for early termination once a condition is met.

## Options

Same as `start_link/1`, plus:

  - `:timeout` - per-line receive timeout in milliseconds (default: `30_000`).
    If no new line arrives within this window, the stream halts.

## Example

    # Collect the first 5 lines of a long-running script
    Denox.Run.stream(file: "server.ts", permissions: :all)
    |> Enum.take(5)

    # Filter lines matching a pattern
    Denox.Run.stream(file: "generate.ts", permissions: :all)
    |> Stream.filter(&String.contains?(&1, "ERROR"))
    |> Enum.to_list()

> #### Errors on start {: .warning}
>
> If the runtime fails to start, the stream raises a `RuntimeError`
> when enumerated. Use `capture/1` if you prefer an `{:error, reason}`
> tuple instead.

# `stream_from`

```elixir
@spec stream_from(
  GenServer.server(),
  keyword()
) :: Enumerable.t()
```

Stream stdout lines from an already-running server as a lazy `Stream`.

Unlike `stream/1`, which starts a new runtime, `stream_from/2` works
with an existing server PID. The stream halts when the runtime exits or
a per-line timeout is reached. The server is **not** stopped when the
stream halts — the caller retains ownership.

This pairs naturally with `with_runtime/2` when you want lazy output
enumeration after sending input:

    Denox.Run.with_runtime([file: "script.ts", permissions: :all], fn pid ->
      :ok = Denox.Run.send(pid, Jason.encode!(request))
      Denox.Run.stream_from(pid) |> Enum.to_list()
    end)

## Options

  - `:timeout` - per-line receive timeout in milliseconds (default: `5000`).
    If no new line arrives within this window, the stream halts.

# `subscribe`

```elixir
@spec subscribe(GenServer.server()) :: :ok
```

Subscribe the calling process to stdout messages.

After subscribing, the calling process will receive:

  - `{:denox_run_stdout, server_pid, line}` for each stdout line
  - `{:denox_run_exit, server_pid, exit_status}` when the process exits

If the subscribing process dies, it is automatically removed from the
subscriber list without needing an explicit `unsubscribe/1` call.

# `unsubscribe`

```elixir
@spec unsubscribe(GenServer.server()) :: :ok
```

Unsubscribe from stdout messages.

The calling process will stop receiving `{:denox_run_stdout, ...}` and
`{:denox_run_exit, ...}` messages from this server.

# `with_runtime`

```elixir
@spec with_runtime(
  keyword(),
  (GenServer.server() -&gt; result)
) :: result | {:error, term()}
when result: term()
```

Execute a function with a managed runtime, ensuring cleanup on exit.

Starts a runtime with `opts`, calls `fun` with the server PID, then
stops the runtime — even if `fun` raises an exception.

Returns the value returned by `fun`. If `fun` returns an error tuple,
that tuple is passed through as-is. Returns `{:error, reason}` if the
runtime failed to start.

## Example

    Denox.Run.with_runtime([file: "scripts/tool.ts", permissions: :all], fn pid ->
      :ok = Denox.Run.send(pid, Jason.encode!(%{method: "init"}))
      {:ok, line} = Denox.Run.recv(pid, timeout: 10_000)
      Jason.decode!(line)
    end)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
