livekit-data-stream 0.1.1

Data stream core logic for LiveKit
# AGENTS.md

## Architectural overview

- This crate encapsulates the business logic and public APIs for the data streams feature: [text streams](https://docs.livekit.io/transport/data/text-streams/) and [byte streams](https://docs.livekit.io/transport/data/byte-streams/)
- Not for direct consumption by developers
- Unlike most SDK features which live directly in the [`livekit`](../livekit/) crate, data streams are intentionally isolated here for several reasons:
  - Enforces decoupling from other components (e.g., data channel, signaling client, etc.)
  - Enables proper integration testing
  - Enables shared implementation amongst multiple _consumers_:
    - [`livekit`](../livekit/): Rust client SDK
    - [`livekit-uniffi`](../livekit-uniffi/): will eventually power downstream client SDKs such as Swift and Kotlin

## Incoming vs. outgoing split

- The crate is organized into two halves:
  - `incoming/`: receiving streams from remote participants (produces readers)
  - `outgoing/`: sending streams to remote participants (produces writers)
- The two halves never communicate with each other or share state
- Shared types live at the crate root rather than inside either side:
  - wire/domain packet types (`Header`, `Chunk`, `Trailer`, `Packet`, `StreamId`, ...) in `types/`
  - `ByteStreamInfo` / `TextStreamInfo` in `info.rs`
  - `StreamError`, `StreamResult`, `StreamProgress`, `SendError` in `utils.rs`
  - helpers such as UTF-8-aware chunking in `utf8_chunk.rs`

### The halves are deliberately *not* symmetric

Unlike the mirror-image `local/`/`remote/` split in [`livekit-datatrack`](../livekit-datatrack/AGENTS.md), `incoming/` and `outgoing/` do **not** share a parallel shape. Each has a `manager`, but they are built differently on purpose:

- The **incoming** manager (`incoming/manager.rs`) is an **actor**. It owns all receive-side state on a single task and is driven by `InputEvent`s fed over a channel, emitting `OutputEvent`s for the host to surface (see `incoming/events.rs`). This is required because inbound packets arrive from the engine's event loop in a context that cannot `.await` into the manager, inbound chunks must never be dropped (a dropped chunk is an unrecoverable `MissedChunk`), and processing must not head-of-line-block that loop. Owning its state directly also lets its handlers `.await` decompression on the run-loop task without holding a lock across the await point.
- The **outgoing** manager (`outgoing/manager.rs`) is a plain struct with `async` methods that callers `.await` directly. It runs in an already-async context (publishing), so awaiting into it is fine and the actor machinery (events, channels, a run loop) would add substantial complexity for no benefit.

This asymmetry is a conscious choice, **not** an oversight or unfinished work. Do not "symmetrize" the outgoing side into an actor preemptively. When changing behavior on one side, there is often no mirror to update on the other — verify rather than assume.

## Boundaries

- Two public modules get exported from this crate (see `lib.rs`):
  1. `api`: public APIs that get re-exported by _consumers_ and made available to developers (readers, writers, stream options, stream infos, `StreamError`, ...)
  2. `backend`: managers and supporting wire types used internally by _consumers_ to power the feature (`backend::incoming`, `backend::outgoing`, and the domain packet types)
- The incoming actor's events are decoupled from protocol messages for several reasons:
  - Protobuf is a wire format and cannot express Rust-level invariants
  - Events can carry in-process types proto cannot (e.g., the channel senders used to feed a reader)
  - Allows the protocol to evolve independently
- Wire <-> domain conversion lives in `types/packet.rs`, which owns the `From`/`TryFrom` impls between `livekit_protocol::data_stream` messages and this crate's own packet types. Consumers convert inbound proto into domain `Packet`s before feeding the incoming actor, so the incoming side only ever sees domain types.