phase 13a: feature-flag detangle (client side)#89
Conversation
Splits the `client` Cargo feature so consumers can depend on the
crate without pulling tokio + socket2. Adds a new `client-tokio`
feature that layers the tokio convenience defaults on top.
# Cargo features
Before:
client = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
After:
client = ["std", "dep:futures"]
client-tokio = ["client", "dep:tokio", "dep:socket2"]
server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
`server` is intentionally left unchanged. `src/server/sd_state.rs`,
`src/server/subscription_manager.rs`, and `src/server/mod.rs` still
reference `tokio::net::UdpSocket` / `tokio::sync::RwLock` /
`socket2::Socket` directly in production code; phase 14 (server
parallel) is the phase that retargets the server to the trait
surface, after which the same `server` + `server-tokio` split applies.
# Module gates
`tokio_transport` flips from `feature = "client" or "server"` to
`feature = "client-tokio" or "server"`. The full `Client` /
`Inner` / `SocketManager` types — including the `bind` /
`bind_discovery_seeded` convenience constructors that default to
`TokioTransport` + `TokioSpawner` — are gated behind `client-tokio`,
because their bind paths still hardcode `&TokioTransport` and
`Inner` calls `TokioTimer.sleep` directly. The trait-generic
`bind_with_transport` / `bind_discovery_seeded_with_transport` paths
(introduced in phase 12) work without tokio in principle, but a
caller cannot construct a `SocketManager` without going through
`Inner`, which is gated.
The `client` feature therefore exposes:
- The trait surface (`TransportFactory`, `TransportSocket`, `Timer`,
`Spawner`, `ChannelFactory`, `E2ERegistryHandle`,
`InterfaceHandle`, etc.)
- Data-type shells (`PendingResponse`, `ClientUpdate`,
`ClientUpdates`, `DiscoveryMessage`)
- `EmbassySyncChannels` (when `bare_metal` is also on)
It does not yet expose a constructible `Client`. That work — making
`Inner` generic over `TransportFactory` / `Timer` / `Spawner` and
ungating `Client` from `client-tokio` — is phase 13.5, not phase 13.
The plan's §6 phase 13 gate text reads "client is no_std-compatible",
which is partially aspirational; what this commit delivers is
"client compiles without tokio."
# Misfiled impls relocated
`impl E2ERegistryHandle for Arc<Mutex<E2ERegistry>>` and
`impl InterfaceHandle for Arc<RwLock<Ipv4Addr>>` lived in
`tokio_transport.rs` despite being pure std (Arc / Mutex / RwLock,
no tokio). Moved to `transport::std_handle_impls`, gated
`feature = "std"`. This is what unblocks `--features client` from
needing `tokio_transport` at all.
# Default type parameters dropped
The following public types lost their `= TokioChannels` /
`= TokioSpawner` / `= Arc<Mutex<E2ERegistry>>` defaults:
- `Client<M, R, I, C>`
- `ClientUpdates<M, C>`
- `PendingResponse<P, C>`
- `SocketManager<P, C>`
- `SendMessage<P, C>`
- `Outcome<P, C>`
- `ControlMessage<P, C>`
This is API-breaking. Callers writing `Client::<RawPayload>::new(...)`
must update to `Client::<RawPayload, _, _, _>::new(...)`. Callers
not using turbofish (plain `Client::new(...)`) are unaffected because
the convenience-constructor impl block fixes the other type
parameters concretely.
`Inner`'s defaults (`S = TokioSpawner`, `R = Arc<Mutex<...>>`,
`C = TokioChannels`) are intentionally kept; `Inner` is gated to
`client-tokio` entirely, and these defaults will be revisited in
phase 13.5 alongside the `TransportFactory` generic that lets
`Inner` drive a non-tokio backend.
# Tests / examples
- `[[test]] client_server` requires `["client-tokio", "server"]`.
- `examples/client_server` and `examples/discovery_client` updated
to `client-tokio`. `examples/bare_metal` unchanged.
- Doctests in `lib.rs:73` and `transport.rs:102` switched from
`# #[cfg(feature = "client")]` to `# #[cfg(feature = "client-tokio")]`
so they run cleanly under all per-feature doctest invocations.
- `tests/client_server.rs` spells out the full `Client<P, _, _, TokioChannels>`
type alias since the default is gone.
# Verification
- `cargo test --all-features -- --test-threads=1`: 455 lib + 11 integration + 9 doc + 1 bare_metal_example_builds, 0 failures.
- `cargo test --no-default-features --features client --doc`: 4 passed, 0 failed.
- `cargo clippy --all-features --all-targets`: clean.
- `cargo doc --all-features --no-deps`: clean.
- Feature matrix builds cleanly: '', 'std', 'bare_metal', 'client', 'client-tokio', 'client,server', 'client,bare_metal', 'client-tokio,server', 'client,server,bare_metal,client-tokio'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Refactors Cargo feature flags to split the client API into a tokio-free trait surface (client) and a tokio/socket2 convenience layer (client-tokio), reducing dependency footprint for consumers who don’t need tokio.
Changes:
- Split
clientintoclient(std + futures only) andclient-tokio(adds tokio + socket2), and updated module/re-export gating accordingly. - Moved std-only
E2ERegistryHandle/InterfaceHandleimpls out oftokio_transportintotransportto avoid pulling the tokio backend unnecessarily. - Updated tests, examples, and doctests for the removed default type parameters and the new feature flag.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.toml | Introduces client-tokio feature and adjusts client deps; updates integration test feature requirements. |
| src/lib.rs | Re-gates tokio_transport and re-exports to align with client-tokio vs client split; updates crate-level docs/doctests. |
| src/transport.rs | Adds std-backed handle impls and updates channel abstraction documentation. |
| src/tokio_transport.rs | Removes std-only handle impls now hosted in transport; keeps tokio backend focused on tokio/socket2 concerns. |
| src/client/mod.rs | Gates Client/inner modules behind client-tokio and removes default type params from public client-facing types. |
| src/client/inner.rs | Propagates removal of default channel type parameters into internal control types and socket manager usage. |
| src/client/socket_manager.rs | Removes tokio-defaulted generic params from public structs/enums; updates tests to specify TokioChannels. |
| tests/client_server.rs | Updates test aliases/usages to specify concrete generic parameters and TokioChannels. |
| examples/client_server/Cargo.toml | Switches example dependency from client to client-tokio (plus server). |
| examples/client_server/src/main.rs | Updates Client turbofish to include new generic placeholders. |
| examples/discovery_client/Cargo.toml | Switches example dependency from client to client-tokio. |
| examples/discovery_client/src/main.rs | Updates Client turbofish to include new generic placeholders. |
| examples/bare_metal/src/main.rs | Updates docs to reflect the phase-13 client-side split and remaining server-side work. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #[cfg(any(feature = "client-tokio", feature = "server"))] | ||
| pub mod tokio_transport; |
There was a problem hiding this comment.
tokio_transport currently contains the EmbassySyncChannels (bare_metal) channel backend, but the module itself is now gated behind client-tokio/server. That makes EmbassySyncChannels unavailable in a --no-default-features --features bare_metal (and even client,bare_metal) build, contradicting the crate/docs statements that it’s the no-tokio channel option. Consider moving the embassy channel backend into a tokio-free module gated by bare_metal (and re-exporting it), or update the docs/feature docs to reflect the new availability.
| // the channel primitive used by the client. `TokioChannels` (in | ||
| // `tokio_transport`) is the default for `std + tokio` builds; | ||
| // `EmbassySyncChannels` (in `tokio_transport`, gated behind `bare_metal`) | ||
| // is the alternative for no-tokio / no_std builds. |
There was a problem hiding this comment.
The channel-abstraction docs say EmbassySyncChannels lives in tokio_transport and is the alternative for “no-tokio / no_std builds”, but tokio_transport is feature-gated behind client-tokio/server and won’t be present in a bare-metal build. Update this comment (or relocate/re-export EmbassySyncChannels) so it matches the actual feature-gating.
| // the channel primitive used by the client. `TokioChannels` (in | |
| // `tokio_transport`) is the default for `std + tokio` builds; | |
| // `EmbassySyncChannels` (in `tokio_transport`, gated behind `bare_metal`) | |
| // is the alternative for no-tokio / no_std builds. | |
| // the channel primitive used by the client. `TokioChannels` is the default | |
| // for `std + tokio` builds. For no-tokio / no_std / bare-metal builds, use | |
| // the channel backend exposed by the corresponding feature-gated transport | |
| // module or re-export available in that configuration. |
| /// `tokio` + `socket2`. Bare-metal callers use | ||
| /// [`Self::new_with_spawner_and_loopback`] (always available under `client`) | ||
| /// and supply their own channel factory + spawner. |
There was a problem hiding this comment.
This doc comment suggests bare-metal callers can use Client::new_with_spawner_and_loopback “always available under client”, but the Client type itself is #[cfg(feature = "client-tokio")] (and the impl block here is also client-tokio). Please adjust the wording to avoid implying Client is usable under client/bare-metal today (e.g., point bare-metal users at the transport trait surface instead).
| /// `tokio` + `socket2`. Bare-metal callers use | |
| /// [`Self::new_with_spawner_and_loopback`] (always available under `client`) | |
| /// and supply their own channel factory + spawner. | |
| /// `tokio` + `socket2`. | |
| /// | |
| /// Bare-metal / non-`tokio` integrations should use the `transport` trait | |
| /// surface directly today; this `Client` type and its constructors are gated | |
| /// on `client-tokio`. |
| client = ["std", "dep:futures"] | ||
| client-tokio = ["client", "dep:tokio", "dep:socket2"] |
There was a problem hiding this comment.
Now that client is std + futures and client-tokio layers tokio/socket2 on top, the bare-metal feature docs later in this section (notably the statement that EmbassySyncChannels is available without tokio) don’t match the current module gating (EmbassySyncChannels still lives under tokio_transport). Please reconcile this: either move/re-export the embassy channel backend for bare_metal builds, or update the feature docs to reflect its actual availability.
|
Closing without merge to declutter the stack: this phase's changes are carried in full by the consolidated lineage under PR #114 (phase 21), which the next development stack builds on. Branch is retained. |
Splits the
clientCargo feature so consumers can depend on the crate without pulling tokio + socket2. Adds a newclient-tokiofeature that layers the tokio convenience defaults on top.Cargo features
Before:
client = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
After:
client = ["std", "dep:futures"]
client-tokio = ["client", "dep:tokio", "dep:socket2"]
server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
serveris intentionally left unchanged.src/server/sd_state.rs,src/server/subscription_manager.rs, andsrc/server/mod.rsstill referencetokio::net::UdpSocket/tokio::sync::RwLock/socket2::Socketdirectly in production code; phase 14 (server parallel) is the phase that retargets the server to the trait surface, after which the sameserver+server-tokiosplit applies.Module gates
tokio_transportflips fromfeature = "client" or "server"tofeature = "client-tokio" or "server". The fullClient/Inner/SocketManagertypes — including thebind/bind_discovery_seededconvenience constructors that default toTokioTransport+TokioSpawner— are gated behindclient-tokio, because their bind paths still hardcode&TokioTransportandInnercallsTokioTimer.sleepdirectly. The trait-genericbind_with_transport/bind_discovery_seeded_with_transportpaths (introduced in phase 12) work without tokio in principle, but a caller cannot construct aSocketManagerwithout going throughInner, which is gated.The
clientfeature therefore exposes:TransportFactory,TransportSocket,Timer,Spawner,ChannelFactory,E2ERegistryHandle,InterfaceHandle, etc.)PendingResponse,ClientUpdate,ClientUpdates,DiscoveryMessage)EmbassySyncChannels(whenbare_metalis also on)It does not yet expose a constructible
Client. That work — makingInnergeneric overTransportFactory/Timer/Spawnerand ungatingClientfromclient-tokio— is phase 13.5, not phase 13. The plan's §6 phase 13 gate text reads "client is no_std-compatible", which is partially aspirational; what this commit delivers is "client compiles without tokio."Misfiled impls relocated
impl E2ERegistryHandle for Arc<Mutex<E2ERegistry>>andimpl InterfaceHandle for Arc<RwLock<Ipv4Addr>>lived intokio_transport.rsdespite being pure std (Arc / Mutex / RwLock, no tokio). Moved totransport::std_handle_impls, gatedfeature = "std". This is what unblocks--features clientfrom needingtokio_transportat all.Default type parameters dropped
The following public types lost their
= TokioChannels/= TokioSpawner/= Arc<Mutex<E2ERegistry>>defaults:Client<M, R, I, C>ClientUpdates<M, C>PendingResponse<P, C>SocketManager<P, C>SendMessage<P, C>Outcome<P, C>ControlMessage<P, C>This is API-breaking. Callers writing
Client::<RawPayload>::new(...)must update toClient::<RawPayload, _, _, _>::new(...). Callers not using turbofish (plainClient::new(...)) are unaffected because the convenience-constructor impl block fixes the other type parameters concretely.Inner's defaults (S = TokioSpawner,R = Arc<Mutex<...>>,C = TokioChannels) are intentionally kept;Inneris gated toclient-tokioentirely, and these defaults will be revisited in phase 13.5 alongside theTransportFactorygeneric that letsInnerdrive a non-tokio backend.Tests / examples
[[test]] client_serverrequires["client-tokio", "server"].examples/client_serverandexamples/discovery_clientupdated toclient-tokio.examples/bare_metalunchanged.lib.rs:73andtransport.rs:102switched from# #[cfg(feature = "client")]to# #[cfg(feature = "client-tokio")]so they run cleanly under all per-feature doctest invocations.tests/client_server.rsspells out the fullClient<P, _, _, TokioChannels>type alias since the default is gone.Verification
cargo test --all-features -- --test-threads=1: 455 lib + 11 integration + 9 doc + 1 bare_metal_example_builds, 0 failures.cargo test --no-default-features --features client --doc: 4 passed, 0 failed.cargo clippy --all-features --all-targets: clean.cargo doc --all-features --no-deps: clean.