# `URP.Stream`

Bidirectional URP dispatch for XInputStream/XOutputStream/XSeekable.

When we pass a stream object reference to soffice (e.g. as an InputStream
in a MediaDescriptor), soffice calls methods on our object over the same
TCP connection. This module handles those incoming calls while we wait
for the reply to our original request.

## XInputStream funcIDs (depth-first interface traversal)

    XInterface: queryInterface=0, acquire=1, release=2
    XInputStream: readBytes=3, readSomeBytes=4, skipBytes=5, available=6, closeInput=7

## XSeekable funcIDs

    XInterface: queryInterface=0, acquire=1, release=2
    XSeekable: seek=3, getPosition=4, getLength=5

Since XInputStream and XSeekable share funcIDs 3-5, we use the URP type
cache to disambiguate: each request carries a type reference telling us
which interface it targets.

## XOutputStream funcIDs

    XInterface: queryInterface=0, acquire=1, release=2
    XOutputStream: writeBytes=3, flush=4, closeOutput=5

# `input_source`

```elixir
@type input_source() ::
  binary() | {:file, pid(), non_neg_integer()} | {:enum, binary(), pid()}
```

# `sink`

```elixir
@type sink() :: nil | {:path, Path.t()} | (binary() -&gt; any())
```

# `recv_handling_input`

```elixir
@spec recv_handling_input(URP.Bridge.t(), input_source() | source(), String.t() | nil) ::
  {binary(), URP.Bridge.t()}
```

Receive frames while handling XInputStream/XSeekable calls from soffice.

`source` is either a binary (in-memory), `{:file, pid, size}` for
file-backed streaming, or `{:enum, buffer, reader}` for enumerables.

When `stream_oid` is provided and the source is seekable (memory or file),
queryInterface for XSeekable is supported — enabling ZIP-based formats
like docx and xlsx.

Dispatches calls until we receive the reply to our pending request.
Returns `{reply_payload, updated_conn}`.

# `recv_handling_output`

```elixir
@spec recv_handling_output(URP.Bridge.t(), sink()) ::
  {binary(), binary() | :ok, URP.Bridge.t()}
```

Receive frames while handling XOutputStream calls from soffice.

`sink` controls where output bytes go:

  * `nil` (default) — accumulate in memory, returns `{reply, binary, conn}`
  * `{:path, path}` — write chunks to file as they arrive, returns `{reply, :ok, conn}`
  * `fun/1` — call with each chunk, returns `{reply, :ok, conn}`

# `start_enum_reader`

```elixir
@spec start_enum_reader(Enumerable.t()) :: pid()
```

Spawn a linked process that iterates `enumerable`, sending chunks to the caller.

The reader sends `{pid, {:chunk, data}}` for each element and `{pid, :eof}`
when the enumerable is exhausted. The caller receives these in `fill_buffer/3`.

# `try_handle_input`

```elixir
@spec try_handle_input(URP.Bridge.t(), binary()) ::
  {:handled, URP.Bridge.t()} | :not_input
```

Try to handle a stray request as an input stream call.

Called by `Bridge.recv_reply/1` when it encounters a non-reply message
outside the main stream recv loop. Returns `:not_input` if the request
doesn't match the active input stream context.

---

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