Skip to content

Phase14 server trait surface#90

Closed
JustinKovacich wants to merge 3 commits into
feature/phase13_6_static_channelsfrom
feature/phase14_server_trait_surface
Closed

Phase14 server trait surface#90
JustinKovacich wants to merge 3 commits into
feature/phase13_6_static_channelsfrom
feature/phase14_server_trait_surface

Conversation

@JustinKovacich

Copy link
Copy Markdown
Contributor

Make the same changes that split off the client traits to the server. A server can be produced without tokio or socket2 dependencies.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the “Phase14 server trait surface” split by making the server implementation generic over transport/timer/subscription/E2E handles so that a server can be built without pulling in tokio/socket2 (those are now confined to the server-tokio feature).

Changes:

  • Split server feature flags into server (trait-surface) and server-tokio (tokio + socket2 convenience constructors/defaults).
  • Generalize Server, EventPublisher, and SD send paths over TransportFactory/TransportSocket and Timer; add ServerDeps and *_with_deps constructors.
  • Add a compile-witness test (bare_metal_server) proving Server is constructible without server-tokio, and update existing tests/examples accordingly.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tests/client_server.rs Adjusts test helpers to use explicit tokio-flavored server/publisher aliases.
tests/bare_metal_server.rs New witness test exercising server construction via trait-surface deps (no server-tokio).
src/tokio_transport.rs Tightens feature gating and adjusts multicast loop option application; updates related tests.
src/server/subscription_manager.rs Gates tokio-backed SubscriptionHandle impl behind server-tokio.
src/server/service_info.rs Adds #[must_use] on Subscriber::new.
src/server/sd_state.rs Makes SD offer sending generic over TransportSocket; gates tests behind server-tokio.
src/server/mod.rs Introduces ServerDeps, makes Server generic over transport/timer, adds *_with_deps constructors, updates run loop to transport trait types, updates tests.
src/server/event_publisher.rs Makes publisher generic over TransportSocket; gates tests behind server-tokio and updates test bindings.
src/server/error.rs Adds Error::Transport for transport-layer failures.
src/lib.rs Updates documentation and exports for server/server-tokio split; adjusts tokio backend gating.
src/client/socket_manager.rs Updates docs to reflect Phase 14b status.
examples/client_server/Cargo.toml Switches example to depend on server-tokio instead of server.
examples/bare_metal/src/main.rs Updates docs to reflect Phase 14a/14b completion and new witness test.
Cargo.toml Implements server/server-tokio feature split; updates required-features; adds bare_metal_server test target.
.gitignore Adds .claude/ / CLAUDE.md patterns and reorders entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server/mod.rs
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let (len, addr) = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same formatting issue as above: multiple let statements are chained on a single line, reducing readability. Please run rustfmt or split these into separate lines.

Suggested change
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = std::net::SocketAddr::V4(datagram.source);

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let (len, addr) = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same formatting issue as above: multiple let statements are chained on a single line, reducing readability. Please run rustfmt or split these into separate lines.

Suggested change
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = std::net::SocketAddr::V4(datagram.source);

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let (len, addr) = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same formatting issue as above: multiple let statements are chained on a single line, reducing readability. Please run rustfmt or split these into separate lines.

Suggested change
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = std::net::SocketAddr::V4(datagram.source);

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let (len, addr) = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same formatting issue as above: multiple let statements are chained on a single line, reducing readability. Please run rustfmt or split these into separate lines.

Suggested change
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = std::net::SocketAddr::V4(datagram.source);

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +543 to +545
Err(_) => Err(std::io::Error::other(
"transport: failed to read local_addr",
)),

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unicast_local_addr discards the underlying TransportError and always returns a generic std::io::Error::other(...). This makes debugging harder and drops the original error kind (e.g., Unsupported vs AddressInUse). Consider either (a) returning Result<SocketAddrV4, TransportError> here, or (b) mapping TransportError into an io::Error with a meaningful ErrorKind and message (preserving details).

Suggested change
Err(_) => Err(std::io::Error::other(
"transport: failed to read local_addr",
)),
Err(err) => Err(std::io::Error::other(err)),

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let (len, addr) = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same formatting issue as above: multiple let statements are chained on a single line, reducing readability. Please run rustfmt or split these into separate lines.

Suggested change
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap(); let len = datagram.bytes_received; let addr = std::net::SocketAddr::V4(datagram.source);
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = std::net::SocketAddr::V4(datagram.source);

Copilot uses AI. Check for mistakes.
Comment thread src/tokio_transport.rs
Comment on lines +269 to +275
// Only set the multicast-loop flag when the caller is doing
// multicast (i.e. they configured a multicast interface). Calling
// `set_multicast_loop_v4` on a plain-unicast socket on some
// backends can return EOPNOTSUPP / EINVAL; even on Linux where it
// succeeds, it's a meaningless syscall. Mirrors the behavior of
// the `client::SocketManager` discovery-bind path.
if options.multicast_if_v4.is_some() {

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SocketOptions::multicast_if_v4 is documented as optional ("None lets the backend choose"), but this code only applies multicast_loop_v4 when multicast_if_v4 is Some. That means a caller who wants multicast loopback behavior while letting the backend choose the interface (i.e. multicast_if_v4 = None) will silently get the OS default instead of the requested loop setting. Consider applying set_multicast_loop_v4 whenever the caller is configuring multicast loopback (or adjusting SocketOptions to make the loop flag tri-state so "unspecified" can avoid the syscall on pure-unicast binds).

Suggested change
// Only set the multicast-loop flag when the caller is doing
// multicast (i.e. they configured a multicast interface). Calling
// `set_multicast_loop_v4` on a plain-unicast socket on some
// backends can return EOPNOTSUPP / EINVAL; even on Linux where it
// succeeds, it's a meaningless syscall. Mirrors the behavior of
// the `client::SocketManager` discovery-bind path.
if options.multicast_if_v4.is_some() {
// Apply the multicast-loop flag independently from interface
// selection. `multicast_if_v4 = None` means "let the backend choose
// the interface", not "ignore multicast loopback configuration".
//
// Keep avoiding a needless multicast-specific syscall for the
// common default case on pure-unicast binds, but still honor an
// explicit loopback change even when the interface is left
// unspecified.
if options.multicast_if_v4.is_some() || !options.multicast_loop_v4 {

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +278 to 285
// Bind unicast socket for receiving subscriptions.
let unicast_addr = SocketAddrV4::new(config.interface, config.local_port);
let unicast_socket = Arc::new(UdpSocket::bind(unicast_addr).await?);
let unicast_socket = Arc::new(factory.bind(unicast_addr, &SocketOptions::new()).await?);
tracing::info!(
"Server bound to {} for service 0x{:04X}",
unicast_addr,
config.service_id
);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When config.local_port is 0 (ephemeral), the socket bind will pick a real port but config.local_port is stored unchanged. SD offers (and other places that use self.config.local_port) will then advertise port 0 unless the caller manually calls set_local_port. Consider reading unicast_socket.local_addr()?.port() after bind and updating the stored config before constructing the publisher / returning the Server.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +346 to 353
// Bind unicast socket at the configured local_port.
let unicast_addr = SocketAddrV4::new(config.interface, config.local_port);
let unicast_socket = Arc::new(UdpSocket::bind(unicast_addr).await?);
let unicast_socket = Arc::new(factory.bind(unicast_addr, &SocketOptions::new()).await?);
tracing::info!(
"Passive server bound to {} for service 0x{:04X}",
unicast_addr,
config.service_id
);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the non-passive constructor: if config.local_port is 0, the unicast socket will bind to an ephemeral port but config.local_port remains 0. That can cause the passive server to publish events with source/advertised port 0 unless callers manually patch it via set_local_port. Consider updating the stored config from unicast_socket.local_addr() after binding.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines 633 to +637
pin_mut!(unicast_fut, sd_fut);
select! {
result = unicast_fut => {
let (len, addr) = result?;
(len, addr, "unicast", true)
let datagram = result?;
(

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The surrounding SAFETY comment (just above this select!) still references tokio::net::UdpSocket::recv_from cancel-safety. Since this code now selects over TransportSocket::recv_from futures, that guarantee is transport-dependent. Please update the comment (or encode the invariant in the TransportSocket contract) so it doesn't rely on tokio-specific behavior.

Copilot uses AI. Check for mistakes.
JustinKovacich and others added 3 commits April 28, 2026 10:09
Splits the `server` Cargo feature so the strategic-goal feature combo
`features = ["bare_metal", "client", "server"]` builds without tokio.
Phase 14b will retarget the server engine to the trait surface and
expose a working server under the bare `server` feature; this commit
is purely the topology change.

# Cargo features

Before:
    server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]

After:
    server       = ["std"]                                          # topology marker
    server-tokio = ["server", "dep:tokio", "dep:socket2", "dep:futures"]

# Module gates flipped
- `pub mod server;`           feature = "server" → "server-tokio"
- `pub use server::Server;`   ditto
- `pub use server::SubscriptionHandle;` ditto
- `tokio_transport` mod gate `client-tokio or server` → `client-tokio or server-tokio`

# Tests / examples
- `[[test]] client_server` requires `["client-tokio", "server-tokio"]`
- `examples/client_server` uses `["client-tokio", "server-tokio"]`
- `examples/bare_metal/main.rs` status note + lib.rs feature-flag table updated

# Verification
- All 12 feature-matrix combos build clean, including the strategic
  combo `client,server,bare_metal`.
- 457 lib + 11 + 1 + 1 + 9 doc tests pass with --all-features.
- clippy clean with --all-features --all-targets.
- bare_metal example runs end-to-end; bare_metal_client witness test passes.

# What this leaves for 14b
The bare `server` feature compiles to nothing useful today — every
production code path in src/server/* still uses tokio internals.
Phase 14b mirrors phase 13.5 on the server: introduces
`ServerDeps<F, Tm, R, S>`, makes `Server<R, S, F, Tm>` and
`EventPublisher<R, S, T>` generic over the transport+timer, replaces
the hand-rolled `socket2::Socket` SD bind with `factory.bind()`, and
ungates the engine from `server-tokio`. Estimate per the phase 14
scoping report: ~1.5-2 ew.

Per phase 13.5 lessons doc finding #5: introduce a `TestServer`
type alias before any default-type-param drops in 14b.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three doc-text references still named the pre-split feature gates
or the wrong phase number:

- src/tokio_transport.rs:9 / :16 — "feature = client" / "server"
  → "client-tokio" / "server-tokio". The actual cfg attribute on the
  module declaration was already correct; this is the surrounding
  prose + an inline doctest cfg gate that mismatched the gate it was
  describing.

- src/client/socket_manager.rs:43 — "deferred to Phase 14" → updated
  to reflect the 14a/14b split (14a topology landed in b7fc30f;
  the substantive engine-retargeting work is 14b).

No code or feature behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors phase 13.5 on the server side. `Server` is now generic over
`<R, S, F, Tm>`; the bare `server` feature exposes a working
trait-surface server reachable via `Server::new_with_deps`, and
`server-tokio` provides the `TokioTransport` / `TokioTimer` /
`Arc<Mutex<E2ERegistry>>` / `Arc<RwLock<SubscriptionManager>>`
convenience defaults.

# Public API

New `pub struct ServerDeps<F, Tm, R, S>` bundle (4 fields: factory,
timer, e2e_registry, subscriptions). Mirrors `ClientDeps`. No
`Spawner` (server has no internal task spawning), no `InterfaceHandle`
(interface lives in `ServerConfig`).

New constructors under just `feature = "server"`:
- `Server::new_with_deps(deps, config, multicast_loopback)` —
  binds unicast + SD multicast via `factory.bind(...)`.
- `Server::new_passive_with_deps(deps, config)` — binds unicast +
  ephemeral SD placeholder for external-SD-dispatcher integration.

Tokio convenience constructors (`Server::new`, `new_with_loopback`,
`new_passive`) are gated `server-tokio` and now delegate to
`new_with_deps` / `new_passive_with_deps` after constructing a
`ServerDeps` with tokio defaults.

`ServerDeps` re-exported from the crate root as
`simple_someip::ServerDeps`. `Subscriber` newly re-exported from
`simple_someip::server` (it's the return type of
`SubscriptionHandle::get_subscribers`; was implicitly part of the
public trait surface but not nameable).

# Engine refactor

`Server<R, S, F, Tm>` stores:
- `unicast_socket: Arc<F::Socket>`, `sd_socket: Arc<F::Socket>` —
  was `Arc<UdpSocket>`.
- `publisher: Arc<EventPublisher<R, S, F::Socket>>` — `EventPublisher`
  is now `<R, S, T>` generic over its socket type.
- `factory: F`, `timer: Tm` — both stored to support bare-metal
  factories carrying state and the announcement-loop's 1-second tick.

`announcement_loop` replaced `TokioTimer.sleep(...)` with
`self.timer.sleep(...)`. `sd_state::send_offer_service` now generic
over `T: TransportSocket`.

`subscription_manager`: `impl SubscriptionHandle for Arc<RwLock<SubscriptionManager>>`
and the `tokio::sync::RwLock` import gated to `server-tokio`.

# Cargo features

Before:
    server       = ["std"]                                   # topology marker
    server-tokio = ["server", "dep:tokio", "dep:socket2", "dep:futures"]

After:
    server       = ["std", "dep:futures"]                    # working trait-surface server
    server-tokio = ["server", "dep:tokio", "dep:socket2"]    # tokio convenience defaults

`futures` moves to `server` because the engine uses `futures::select!`.
`tokio` and `socket2` stay only on the `server-tokio` flavor.

# Bind path consolidation

The hand-rolled `socket2::Socket::new(...)` SD-multicast bind in
`Server::new_with_loopback` is gone. `new_with_deps` calls
`factory.bind(sd_addr, &SocketOptions { reuse_address, reuse_port,
multicast_if_v4: Some(interface), multicast_loop_v4 })` which routes
through `TokioTransport::bind`'s already-existing socket2 path. No
behavior change on the tokio side; bare-metal callers control the
bind path entirely.

# Tests

- `tests/bare_metal_server.rs` (new): witness test gated on
  `["server", "bare_metal"]`. Builds `MockFactory` + `MockSocket` +
  `MockTimer` + `MockSubscriptions` (a hand-rolled `SubscriptionHandle`
  impl backed by `std::sync::Mutex<Vec<...>>`) and proves
  `Server::new_with_deps` + `new_passive_with_deps` succeed and
  return a `Server` whose announcement-loop future is `Send + 'static`.
  Compile witness is the load-bearing assertion.
- `tests/client_server.rs`: `TestServer` / `TestEventPublisher`
  type aliases introduced (per phase 13.5 lessons #5) so existing
  callers don't churn over the new generic params.
- Server's internal `#[cfg(test)] mod tests` blocks tightened to
  `#[cfg(all(test, feature = "server-tokio"))]` since they use
  `tokio::test` / `tokio::net::UdpSocket` (per lesson #7).

# `tokio_transport::bind_with_options` bug fix folded in

`set_multicast_loop_v4` was called unconditionally regardless of
whether the caller configured a multicast interface — this can fail
on backends that error on the call for plain-unicast sockets. Now
only called when `multicast_if_v4` is `Some`. Surfaced by the new
SD-bind path; mirrors the same conditional in the client's
discovery-bind path.

# Verification

- `cargo test --all-features -- --test-threads=1`: 457 lib + 1 + 1
  + 2 (new bare_metal_server witness) + 11 + 9 doc. 0 failures.
- `cargo clippy --all-features --all-targets`: clean.
- Feature matrix `''`, `client,server`, `client,server,bare_metal`,
  `server`, `client-tokio,server-tokio` all build clean.
- `bare_metal_client` witness still passes.

# What this leaves for follow-on phases

- Phase 13.6 (static-pool ChannelFactory): unaffected by 14b but
  still pending. The const-N quirk fix landed separately on the
  13.6 branch.
- Phase 16 (no-alloc CI): `Server::new_with_deps` still uses
  `Arc<EventPublisher>` and `Arc<F::Socket>` internally, so a strict
  no_alloc build does not yet pass. Phase 13.6 (static channels) +
  follow-on `Arc` elimination will close this.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JustinKovacich JustinKovacich force-pushed the feature/phase14_server_trait_surface branch from 7453b75 to 22a1737 Compare April 28, 2026 14:12
@JustinKovacich JustinKovacich changed the base branch from feature/phase13_5_no_std_client to feature/phase13_6_static_channels April 28, 2026 14:26
@JustinKovacich JustinKovacich requested a review from Copilot April 28, 2026 14:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server/mod.rs
Comment on lines +867 to +881
/// Convert a [`std::net::SocketAddr`] into a [`SocketAddrV4`] for the
/// transport layer. SOME/IP-SD is IPv4-only at this layer; if a V6
/// address ever surfaces here it indicates a misconfiguration upstream
/// (a V6 socket binding the SD port, or a V6 source address surfaced
/// by a transport that should not produce one). Returns
/// [`std::io::ErrorKind::Unsupported`] in that case so the caller can
/// log and drop the message instead of panicking.
fn socket_addr_v4(addr: std::net::SocketAddr) -> Result<SocketAddrV4, Error> {
match addr {
std::net::SocketAddr::V4(v4) => Ok(v4),
std::net::SocketAddr::V6(_) => Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"IPv6 SD address is not supported",
))),
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

socket_addr_v4 treats an IPv6 address as Error::Io(std::io::ErrorKind::Unsupported). Since this is an unsupported transport-layer capability (IPv4-only surface), it would be more consistent to return Error::Transport(TransportError::Unsupported) (or otherwise surface it via the transport error path) so callers can match/report it uniformly with other transport limitations.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +50
//! - **Working server without tokio** (Phase 14b): the bare `server`
//! feature is currently a topology marker only (Phase 14a, commit
//! `b7fc30f`). The actual server engine still requires
//! `server-tokio` because `server::sd_state` /
//! `server::subscription_manager` reference tokio types directly.
//! Phase 14b retargets the engine to the trait surface (mirroring
//! phase 13.5 on the client) so a working server lives under just
//! `server`.

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Remaining gaps” bullet still states that the bare server feature is only a topology marker and that a working server still requires server-tokio. In this PR the server engine has been retargeted to the transport/timer/subscription trait surface and server is now functional without tokio, so this doc comment is now inaccurate. Update or remove this bullet to reflect the current feature split/state (e.g. describe server as trait-surface and server-tokio as convenience defaults).

Copilot uses AI. Check for mistakes.
@JustinKovacich

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants