30 lines
2.7 KiB
Markdown
30 lines
2.7 KiB
Markdown
# 0010. Dedicated `net` crate with a confined async runtime
|
|
|
|
- **Status:** Accepted
|
|
- **Date:** 2026-07-12
|
|
|
|
## Context
|
|
|
|
Our network transport uses QUIC via `quinn`, which is an asynchronous library built squarely on the `tokio` runtime and inherently requires TLS 1.3 through `rustls`. These are heavy dependencies that drag an entire async ecosystem into the build tree.
|
|
|
|
We have a strict mandate that the `shared` crate must stay incredibly lean and dependency-light. It acts as our core protocol and data layer, holding pure `serde` message types with absolutely no async, rendering, or engine internals. Dropping transport code straight into `shared` would brutally violate that mandate, forcing every single consumer of our protocol types to compile both `tokio` and `rustls`.
|
|
|
|
At the same time, our main simulation is completely synchronous. The `server` runs a synchronous `bevy_ecs` loop, and the `client` runs a synchronous `winit` event loop. Introducing an async runtime must absolutely not force those loops to become async or let `tokio` leak throughout the entire workspace.
|
|
|
|
## Decision
|
|
|
|
Transport logic lives completely isolated in a dedicated `net` crate, entirely separate from `shared`, and the `tokio` runtime is strictly confined to it.
|
|
|
|
- `net` completely owns the `quinn`, `tokio`, and `rustls` dependencies. It handles the QUIC endpoints, the full connection lifecycle, and all wire framing.
|
|
- `shared` remains perfectly clean, holding only the raw protocol message *types* (using `serde`, with zero async logic).
|
|
- Both `client` and `server` depend on `net`.
|
|
- We bridge the async runtime to the synchronous simulation using `crossbeam-channel`, which aligns perfectly with the message-passing concurrency model defined in `DEVELOPMENT.md`. The synchronous loops never ever call `.await`; they simply send and receive protocol messages across the channel boundary.
|
|
|
|
## Consequences
|
|
|
|
- `shared` stays extremely lean. Consumers that only need the protocol types do not have to compile the massive async stack.
|
|
- The async surface is perfectly quarantined. Only `net` actually deals with `tokio`, keeping both the `server` and `client` loops happily synchronous and completely unchanged.
|
|
- The workspace now contains six crates, with `net` sitting neatly between `shared` (which provides the types it carries) and the two binaries (which actively drive it).
|
|
- The channel bridge acts as an explicit, hard boundary that must be maintained. Any work crossing between the async runtime and the synchronous simulation must flow purely through channels. We never pass shared async state or force the simulation to become async.
|
|
- Because `rustls` requires a crypto provider backend, the transport code has to manually install one before building any QUIC configuration.
|