# `URP.Bridge`

Mid-level UNO operations over a URP connection.

Manages a TCP connection to a `soffice` process and provides functions for
the UNO operations needed for document conversion: loading documents,
storing to URL, and closing.

Each connection performs a handshake and bootstraps a Desktop reference
on `open/2`. A connection handles one conversion at a time (soffice is
single-threaded). Close the connection with `close!/1` when done.

All functions take and return `conn`, following the Plug pattern. Document
OIDs and conversion state are stashed on the conn struct — no separate
values to track.

## Example

    "localhost"
    |> URP.Bridge.open(2002)
    |> URP.Bridge.load_document("file:///tmp/input.docx")
    |> URP.Bridge.store_to_url("file:///tmp/output.pdf", "writer_pdf_Export")
    |> URP.Bridge.close_document()
    |> URP.Bridge.close!()

## Streaming

`load_document_stream/2` and `store_to_stream/2` use UNO's
`XInputStream`/`XOutputStream` interfaces to transfer document bytes
over the URP socket, eliminating the need for a shared filesystem.

    conn =
      "localhost"
      |> URP.Bridge.open(2002)
      |> URP.Bridge.load_document_stream(File.read!("input.docx"))
      |> URP.Bridge.store_to_stream(filter: "writer_pdf_Export")

    pdf = conn.reply
    URP.Bridge.close!(conn)

## Private storage

Diagnostic functions stash results in `conn.private`:

    conn =
      "localhost"
      |> URP.Bridge.open(2002)
      |> URP.Bridge.version()

    conn.private.version
    # => "26.2.0.3"

# `t`

```elixir
@type t() :: %URP.Bridge{
  cleanup_url: String.t() | nil,
  ctx_oid: String.t() | nil,
  desktop_oid: String.t() | nil,
  doc_oid: String.t() | nil,
  error: String.t() | nil,
  input_ctx: map() | nil,
  max_frame_size: pos_integer(),
  private: map(),
  reader_type: non_neg_integer() | nil,
  recv_timeout: timeout(),
  reply: term(),
  reply_tid: binary() | nil,
  sfa_oid: String.t() | nil,
  smgr_oid: String.t() | nil,
  sock: :gen_tcp.socket() | nil,
  tid_cache: map()
}
```

# `apply_settings`

```elixir
@spec apply_settings(t(), list()) :: t()
```

Apply soffice configuration settings via
[ConfigurationUpdateAccess](https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1configuration_1_1ConfigurationUpdateAccess.html).

Each setting is a `{path, property, value}` triplet where `path` is the
registry node (e.g. `"org.openoffice.Office.Common/Cache/GraphicManager"`),
`property` is the property name, and `value` is a boolean, integer, or string.

Settings sharing the same `path` are batched into a single ConfigurationUpdateAccess
call for efficiency.

# `close!`

```elixir
@spec close!(t()) :: :ok
```

Close the TCP connection.

# `close_document`

```elixir
@spec close_document(t()) :: t()
```

Close the loaded document, releasing soffice resources.

# `delete_file`

```elixir
@spec delete_file(t(), String.t()) :: t()
```

Delete a temp file on soffice's filesystem via XSimpleFileAccess.kill().

# `filters`

```elixir
@spec filters(t()) :: t()
```

List all export filter names registered in soffice.

Creates a `FilterFactory` instance and calls `getElementNames()` via `XNameAccess`.
Stashes the result in `conn.private.filters`.

# `load_document`

```elixir
@spec load_document(t(), String.t()) :: t()
```

Load a document from a `file://` URL. Stashes the document OID on `conn.doc_oid`.

On failure, stashes the error on `conn.error`.

# `load_document_enum_stream`

```elixir
@spec load_document_enum_stream(t(), Enumerable.t()) :: t()
```

Load a document from an enumerable via XInputStream.

Like `load_document_stream/2` but pulls chunks lazily from any `Enumerable`
(e.g. `File.stream!/2`, an S3 download stream). The enumerable is iterated
in a linked process; chunks are buffered and fed to soffice on demand.
Stashes the document OID on `conn.doc_oid`.

# `load_document_file_stream`

```elixir
@spec load_document_file_stream(t(), Path.t()) :: t()
```

Load a document from a local file via XInputStream.

Like `load_document_stream/2` but reads from a file on demand instead of
holding the entire document in memory. The file must be accessible to the
Elixir node (not soffice).
Stashes the document OID on `conn.doc_oid`.

# `load_document_stream`

```elixir
@spec load_document_stream(t(), binary()) :: t()
```

Load a document from in-memory bytes via XInputStream.

No shared filesystem needed — bytes are streamed over the URP socket.
soffice calls `readBytes()` on our exported stream object.
Stashes the document OID on `conn.doc_oid`.

# `load_document_write`

```elixir
@spec load_document_write(t(), binary()) :: t()
```

Load a document by writing bytes to soffice's filesystem via XSimpleFileAccess.

Instead of streaming bytes through XInputStream (which causes thousands of
tiny read/seek round-trips), this writes the entire document to a temp file
on soffice's filesystem and loads from a `file://` URL.

Stashes the document OID on `conn.doc_oid` and the temp file URL on
`conn.cleanup_url`. The caller should delete the temp file after conversion
via `delete_file/2`.

# `locale`

```elixir
@spec locale(t()) :: t()
```

Query the soffice locale string over URP.

Reads `ooLocale` from `/org.openoffice.Setup/L10N` via the configuration API.
Stashes the result in `conn.private.locale`.

# `open`

```elixir
@spec open(String.t(), non_neg_integer()) :: t()
```

Connect to soffice, perform URP handshake, and bootstrap a Desktop reference.

# `read_file`

```elixir
@spec read_file(t(), String.t()) :: t()
```

Read a file from soffice's filesystem via XSimpleFileAccess.

Opens the file, reads all bytes in one frame, closes the stream.
Stashes the file contents in `conn.reply`.

# `services`

```elixir
@spec services(t()) :: t()
```

List all service names registered in the UNO service manager.

Calls `XMultiComponentFactory.getAvailableServiceNames()`.
Stashes the result in `conn.private.services`.

# `store_document_write`

```elixir
@spec store_document_write(
  t(),
  keyword()
) :: t()
```

Store a document to soffice's filesystem and read back the result.

Uses `store_to_url/4` to write the converted output to a temp file on
soffice's filesystem, then reads it back in one shot via `read_file/2`.
Replaces hundreds of round-trips with ~6. Stashes the output bytes in
`conn.reply`.

# `store_to_stream`

```elixir
@spec store_to_stream(
  t(),
  keyword()
) :: t()
```

Store a document via XOutputStream.

No shared filesystem needed — output bytes are streamed back over the URP socket.
soffice calls `writeBytes()` on our exported stream object.

Stashes the result in `conn.reply`:

  * no sink (default) — `conn.reply` is the accumulated output bytes
  * `sink: {:path, path}` — writes to file, `conn.reply` is `:ok`
  * `sink: fun/1` — calls with each chunk, `conn.reply` is `:ok`

# `store_to_url`

```elixir
@spec store_to_url(t(), String.t(), String.t(), keyword()) :: t()
```

Store a document to a `file://` URL with the given [export filter](https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html).

Common filters: `"writer_pdf_Export"`, `"calc_pdf_Export"`, `"impress_pdf_Export"`.

`filter_data` is an optional keyword list of export-specific options passed
as a nested `FilterData` property (e.g. `[UseLosslessCompression: true]`).

# `types`

```elixir
@spec types(t()) :: t()
```

List all document type names registered in soffice.

Creates a `TypeDetection` instance and calls `getElementNames()` via `XNameAccess`.
Stashes the result in `conn.private.types`.

# `version`

```elixir
@spec version(t()) :: t()
```

Query the soffice version string over URP.

Reads `ooSetupVersionAboutBox` from the configuration API via 5 UNO
round trips. Stashes the result in `conn.private.version`.

---

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