feat(net): WebSocket transport contract — net-ws-api + net-ws-mock (ADR-0032/0033)#65
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds the WebSocket transport contract crate ChangesWebSocket Transport Contract
Estimated code review effort: 3 (Moderate) | ~30 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
crates/adapter/net/ws/api/src/error.rs (1)
30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider surfacing close code/reason in the
Closedvariant's Display message.
WsError::Closed's#[error("connection closed by peer")]message doesn't include the RFC 6455 code/reason carried inCloseFrame, even though the data is captured. Logs formatted via{err}will lose this diagnostic detail.♻️ Optional: interpolate close details into the error message
- /// The peer closed the connection (close frame, possibly with code+reason). - #[error("connection closed by peer")] + /// The peer closed the connection (close frame, possibly with code+reason). + #[error("connection closed by peer{}", .0.as_ref().map_or_else(String::new, |c| format!(" (code={}, reason={})", c.code, c.reason)))] Closed(Option<CloseFrame>),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/adapter/net/ws/api/src/error.rs` around lines 30 - 32, The WsError::Closed display text is too generic and drops the CloseFrame diagnostics that are already carried in the variant. Update the Closed(Option<CloseFrame>) error message in error.rs so its Display formatting includes the close code and reason when a frame is present, while still handling the no-frame case cleanly; use the WsError::Closed variant and its existing #[error(...)] annotation as the place to make this change.crates/adapter/net/ws/api/src/lifecycle.rs (1)
21-59: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueEpoch duplicated between
ConnStatevariants andLifecycleSnapshot.
epochis stored both inConnState::Connected/Resumedand asLifecycleSnapshot.epoch. OnlyLifecycleSnapshot::connected()keeps them in sync; nothing at the type level stops a future caller (e.g. the reconnect actor in a later slice) from constructing a snapshot where the two values diverge, which would break the "epoch is canonical" invariant the module doc describes.Since this is explicitly called out as an intentional design choice in the doc comments (epoch "echoes" the outer field) and is well covered by the current tests, this is a nice-to-have hardening rather than a blocker — e.g. a
debug_assert_eq!in a constructor helper, or restructuring so the phase-level epoch is derived rather than duplicated, could be considered in the slice that adds writers other thanconnected().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/adapter/net/ws/api/src/lifecycle.rs` around lines 21 - 59, The epoch value is duplicated between ConnState::Connected/Resumed and LifecycleSnapshot::epoch, so a future constructor or writer could let them diverge. Harden the invariant around LifecycleSnapshot::connected() and any other snapshot builders by either deriving the phase epoch from the outer epoch or adding a debug_assert_eq! when constructing the phase, ensuring ConnState and LifecycleSnapshot stay synchronized.docs/superpowers/plans/2026-07-02-net-ws-contract.md (1)
236-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNested backticks in inline code trip markdownlint (MD038).
The module-doc-line instruction embeds backtick-quoted identifiers inside a single backtick-delimited span, which markdownlint flags as a space-inside-code-span issue. Purely cosmetic in a planning doc.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/plans/2026-07-02-net-ws-contract.md` around lines 236 - 239, The module-doc-line instruction in the planning doc uses nested backticks inside a single inline-code span, which triggers markdownlint MD038. Update the wording around the `lib.rs` module-doc line and the `error` reference so the identifiers are not wrapped in conflicting backtick spans, while keeping the instruction to add `pub mod error;`, the `pub use error::{BoxError, WsError};` re-export, and the `//! - [\`error\`] — \`WsError\` and its \`HasErrorKind\` impl` doc entry clear and lint-safe.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/adapter/net/ws/api/src/error.rs`:
- Around line 30-32: The WsError::Closed display text is too generic and drops
the CloseFrame diagnostics that are already carried in the variant. Update the
Closed(Option<CloseFrame>) error message in error.rs so its Display formatting
includes the close code and reason when a frame is present, while still handling
the no-frame case cleanly; use the WsError::Closed variant and its existing
#[error(...)] annotation as the place to make this change.
In `@crates/adapter/net/ws/api/src/lifecycle.rs`:
- Around line 21-59: The epoch value is duplicated between
ConnState::Connected/Resumed and LifecycleSnapshot::epoch, so a future
constructor or writer could let them diverge. Harden the invariant around
LifecycleSnapshot::connected() and any other snapshot builders by either
deriving the phase epoch from the outer epoch or adding a debug_assert_eq! when
constructing the phase, ensuring ConnState and LifecycleSnapshot stay
synchronized.
In `@docs/superpowers/plans/2026-07-02-net-ws-contract.md`:
- Around line 236-239: The module-doc-line instruction in the planning doc uses
nested backticks inside a single inline-code span, which triggers markdownlint
MD038. Update the wording around the `lib.rs` module-doc line and the `error`
reference so the identifiers are not wrapped in conflicting backtick spans,
while keeping the instruction to add `pub mod error;`, the `pub use
error::{BoxError, WsError};` re-export, and the `//! - [\`error\`] — \`WsError\`
and its \`HasErrorKind\` impl` doc entry clear and lint-safe.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f2f1cdbc-a304-4b9d-9d8c-31371c0a5f89
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/*.lock
📒 Files selected for processing (17)
CHANGELOG.mdCargo.tomlREADME.mdcrates/adapter/net/ws/api/Cargo.tomlcrates/adapter/net/ws/api/src/connector.rscrates/adapter/net/ws/api/src/error.rscrates/adapter/net/ws/api/src/frame.rscrates/adapter/net/ws/api/src/lib.rscrates/adapter/net/ws/api/src/lifecycle.rscrates/adapter/net/ws/api/src/sink.rscrates/adapter/net/ws/api/src/source.rscrates/adapter/net/ws/mock/Cargo.tomlcrates/adapter/net/ws/mock/src/connector.rscrates/adapter/net/ws/mock/src/lib.rscrates/adapter/net/ws/mock/src/sink.rscrates/adapter/net/ws/mock/src/source.rsdocs/superpowers/plans/2026-07-02-net-ws-contract.md
CodeRabbit nitpick on #65: the Closed variant carried a CloseFrame but its Display dropped the RFC 6455 code/reason, so {err} logs lost the detail. Interpolate them when a frame is present; empty-frame case unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @coderabbitai — triaged the 3 nitpicks:
|
|
Tip For best results, initiate chat on the files or code changes.
Nice work closing out the Display bug quickly. ᕦ(ò_óˇ)ᕤ |
Closes #63.
Implements the WebSocket transport contract slice — the WS twin of the merged HTTP contract (#60) — per ADR-0032 as amended by ADR-0033 §5. Built test-first, one commit per task, each spec+quality reviewed; a final whole-branch review (opus) returned ready to merge, 0 Critical / 0 Important.
New crate
oath-adapter-net-ws-api(zero-runtime, zero-I/O)Frame/CloseFrame— RFC 6455 frame vocabulary; untyped duplex channel withis_data/is_control(§1/§3).WsError+HasErrorKind— one concrete transport error (transport/middleware failures only; venue data flows through asOkframes). MirrorsHttpError;Closed(Option<CloseFrame>)→ErrorKind::Connection.WsSink/WsSource— the split owned halves (§2): one-shot RPITITsend+ terminalclose(self)(post-close send is a compile error);WsSourceis a blanketStream<Item = Result<Frame, WsError>>marker (theHttpClientpattern).ConnState/LifecycleSnapshot/Lifecycle/LifecycleSender— the epoch-stamped, out-of-band lifecycle channel: a last-value watch over runtime-neutralasync-watch(§4 + ADR-0033 §5). Never-block producer, level/monotonic-cumulative fields, terminalchanged(), lossless coalescing via the epoch delta.WsConnector— the leaf dependency-inversion seam (§7):connect(http::Request<()>)→(Sink, Source, Lifecycle).New dev-only crate
oath-adapter-net-ws-mockMockWsConnector(scriptable frames + injectable connect errors, per-connection lifecycle sender, recorded handshakes/sends/close),MockSink,MockSource. Consumed only via[dev-dependencies]— no dev-dep cycle.Charter / constraints
net-ws-apistays free of any async runtime — the only new production dep isasync-watch, verified to pull onlyevent-listener(no tokio) and to clearcargo deny. Nounsafe/unwrap/expect/indexing in non-test code.just cigreen (fmt, lint, test, deny, doc, machete, …); 18 crate tests, 50/50 workspace.Deferred to later slices (correctly absent here)
AuthSource, the ADR-0033 resilience stack (Spawn, reconnect actor, heartbeat, dual-bound buffer,SendRateLimit,stack(),WsConfig,ReconnectingConnector/WsControl), thenet-ws-tungsteniteleaf, andMockTimer/MockSpawn.Notes for the resilience slice (from review)
ConnStateis#[non_exhaustive]— downstream matches carry a wildcard arm (intentional forward-compat).ConnStatevariant epoch ==snapshot.epoch) via constructors before the reconnect actor writes snapshots.futures-util(dev-only today) needs explicit minimal features when promoted to a production dep.http::Request<()>is notClone→ the reconnect actor rebuilds the handshake per attempt (aligns with per-reconnectAuthSourcere-stamping).🤖 Generated with Claude Code
Summary by CodeRabbit