diff --git a/PROGRESS.md b/PROGRESS.md index b4645bca1..fb607b97b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -39,6 +39,9 @@ error classes. Resume U2 via delegated sink, receiver, and CLI framing units. AC-4 fixture generation is supervisor-owned; U6 remains blocked until the supervisor supplies its fixture commit. +- The delegated U2 `WsByteSink` sibling is accepted after its returned strict-lint correction. + It uses the accepted U3 `impl Future + Send` public trait shape with no waiver, preserving + the read-only reader boundary for `receive_blob` and U4's concurrent pipe. - Checkpoint gates after the correction-driven units: `cargo fmt --all -- --check`, strict combined clippy, and combined locked tests are green (85 SPL + 12 HPKE); both HPKE and SPL libraries pass the explicit `aarch64-apple-ios` check without an exclusion; `cargo deny` @@ -67,6 +70,10 @@ before legacy fallback. Integration then exposed a direct unused import and test-only `assert!(false)` arms under the crate's no-panic clippy gate. This is a contract-review catch plus a brief defect: the initial unit brief did not require the crate gate. +- **U2 split write half — returned.** The first return used public `async fn` trait methods + verbatim from the frozen illustrative signature. Strict clippy rejected the implicit future + bounds. The return must match U3's public `impl Future + Send` seam with no lint waiver. + This is a lane-brief omission caught by review, not supervisor contract rework. ### Explained twice diff --git a/core/crates/solstone-core-spl/src/lib.rs b/core/crates/solstone-core-spl/src/lib.rs index 4b394ff49..bfb72d0ce 100644 --- a/core/crates/solstone-core-spl/src/lib.rs +++ b/core/crates/solstone-core-spl/src/lib.rs @@ -25,6 +25,7 @@ mod service_shutdown; mod service_transition; mod tunnel_route; mod ws_buffer; +mod ws_sink; pub use admission::BlobAdmissionGate; pub use authenticated_blob::{ @@ -69,3 +70,4 @@ pub use service_transition::{ }; pub use tunnel_route::{TunnelRoute, route_tunnel_prefix}; pub use ws_buffer::{BufferedWsReader, WsBufferError, WsByteSource, WsClosed}; +pub use ws_sink::WsByteSink; diff --git a/core/crates/solstone-core-spl/src/ws_sink.rs b/core/crates/solstone-core-spl/src/ws_sink.rs new file mode 100644 index 000000000..80139a44c --- /dev/null +++ b/core/crates/solstone-core-spl/src/ws_sink.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +//! The write half of the frozen split WebSocket transport contract. +//! +//! Blob receive owns a [`crate::BufferedWsReader`] over the read half and a +//! separate `WsByteSink` over the write half. The split is required because +//! the relay tunnel concurrently carries WebSocket-to-TCP and TCP-to-WebSocket +//! traffic; it is not a reader escape hatch. +//! +//! This replaces `solstone/think/spl/blob_receiver.py:443-452`: +//! +//! ```python +//! async def _send_ready(ws: Any, status: int) -> None: +//! await ws.send(_READY_MAGIC + bytes([_VERSION, status])) +//! async def _close_ws(ws: Any) -> None: +//! close = getattr(ws, "close", None) +//! if close is not None: +//! result = close() +//! if hasattr(result, "__await__"): +//! await result +//! ``` + +use std::future::Future; + +use bytes::Bytes; + +use crate::WsClosed; + +/// The write half of a split WebSocket transport. +pub trait WsByteSink { + fn send(&mut self, bytes: Bytes) -> impl Future> + Send; + + fn close(&mut self) -> impl Future> + Send; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Default)] + struct RecordingSink { + sent: Vec, + closed: bool, + } + + impl WsByteSink for RecordingSink { + fn send(&mut self, bytes: Bytes) -> impl Future> + Send { + self.sent.push(bytes); + std::future::ready(Ok(())) + } + + fn close(&mut self) -> impl Future> + Send { + self.closed = true; + std::future::ready(Ok(())) + } + } + + #[tokio::test] + async fn split_write_half_sends_then_closes() { + let mut sink = RecordingSink::default(); + + assert!(sink.send(Bytes::from_static(b"SBR1\x00\x01")).await.is_ok()); + assert!(sink.close().await.is_ok()); + + assert_eq!(sink.sent, [Bytes::from_static(b"SBR1\x00\x01")]); + assert!(sink.closed); + } +}