# `JsonlPort`
[🔗](https://github.com/yoonka/beamports/blob/v1.1.0/jsonl_port/lib/jsonl_port.ex#L1)

Runs a program that speaks the beamports `--jsonl` request protocol as an
Erlang port, and sends it one request at a time.

The protocol is one JSON object per line in each direction: a request
`{"version":1,"request_id":N,"operation":"...","payload":{...}}` and a
reply with the same `request_id` and either `"ok":true,"data":...` or
`"ok":false,"error":"..."`. duckport, oxiport and tantiport speak it when
started with `--jsonl`.

Each `JsonlPort` process owns one program, started on the first request
and restarted on the next request after it exits. Requests are serialised:
a request waits until the one before it has been answered.

A request that gets no valid reply in time ends with an error, and the
program is killed before the next request starts. A write that the
program finishes later therefore cannot overlap the next request, but it
may still have happened: the caller has to treat
`{:error, :uncertain_native_outcome}` as unknown, not as failed.

    {:ok, pid} =
      JsonlPort.start_link(
        executable: "/usr/local/bin/duckport",
        args: ["--jsonl", "--database", "/var/db/app/analytics.duckdb"]
      )

    {:ok, %{"rows" => [%{"answer" => 42}]}} =
      JsonlPort.request(pid, "query", %{sql: "SELECT 42 AS answer", params: []})

# `error`

```elixir
@type error() ::
  :missing_executable
  | :packet_limit
  | :invalid_protocol
  | :uncertain_native_outcome
  | {:native_exit, term()}
  | String.t()
```

Why a request failed.

  * `:missing_executable` - `:executable` is not a regular file.
  * `:packet_limit` - the request or the reply is larger than 16 MiB.
  * `:invalid_protocol` - the reply is not JSON, has another version, or
    answers another request.
  * `:uncertain_native_outcome` - no reply before the timeout; the
    program was killed, and the request may or may not have taken effect.
  * `{:native_exit, status}` - the program exited before replying.
  * a binary - the program's own error, from `"ok":false`; or the message
    of an exception raised while handling the request.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `request`

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

Sends one request and waits up to `timeout` milliseconds for its reply.

`payload` is encoded with Jason. Returns `{:ok, data}` with the reply's
decoded `data`, or `{:error, reason}` (see `t:error/0`).

# `reset`

```elixir
@spec reset(GenServer.server(), timeout()) :: :ok
```

Kills the program, if it is running, once the request in progress (if
any) has been answered. The next request starts it again.

# `start_link`

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

Starts a process for one program. The program itself starts with the
first request.

Options:

  * `:executable` - absolute path of the program (required).
  * `:args` - its arguments, for example `["--jsonl", "--index", path]`.
  * `:name` - registers the process, as in `GenServer.start_link/3`.

---

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