Skip to main content

contextvm_sdk/transport/oversized_transfer/
mod.rs

1//! CEP-22 oversized payload transfer — transport-agnostic framing engine.
2//!
3//! A serialized JSON-RPC message too large to publish as a single relay event is
4//! split into an ordered sequence of frames carried inside MCP
5//! `notifications/progress` messages, transmitted as ordinary kind-`25910`
6//! events, and reassembled by the receiver after SHA-256 + size validation.
7//! See the CEP-22 spec and the TypeScript reference at
8//! `sdk/src/transport/oversized-transfer/`.
9//!
10//! This module is the **pure engine**: building frames ([`codec`]) and
11//! reassembling them ([`receiver`]). It carries no transport, I/O, or live
12//! timers — the client and server transports drive it. The hard per-transfer
13//! watchdog (`transfer_timeout_ms`) is tracked from `start` admission and
14//! reaped via [`OversizedTransferReceiver::remove_expired`] when the owning
15//! transport sweeps.
16//!
17//! ```
18//! use contextvm_sdk::transport::oversized_transfer::{
19//!     build_oversized_frames, OversizedSenderOptions, OversizedTransferReceiver,
20//! };
21//! use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcResponse};
22//! use serde_json::json;
23//!
24//! let message = JsonRpcMessage::Response(JsonRpcResponse {
25//!     jsonrpc: "2.0".to_string(),
26//!     id: json!(1),
27//!     result: json!({ "value": "a large payload" }),
28//! });
29//! let serialized = serde_json::to_string(&message).unwrap();
30//!
31//! // Sender: split into ordered frames.
32//! let opts = OversizedSenderOptions::new("token-1").with_chunk_size(8);
33//! let frames = build_oversized_frames(&serialized, &opts).unwrap();
34//!
35//! // Receiver: feed frames back; the last frame yields the reassembled message.
36//! let mut receiver = OversizedTransferReceiver::new();
37//! let mut reassembled = None;
38//! for frame in frames.into_ordered() {
39//!     if let Some(message) = receiver.process_frame(&frame).unwrap() {
40//!         reassembled = Some(message);
41//!     }
42//! }
43//! assert_eq!(reassembled.unwrap().id(), message.id());
44//! ```
45
46pub mod codec;
47pub mod constants;
48pub mod errors;
49pub mod frame;
50pub mod receiver;
51pub mod sender;
52pub mod sizing;
53
54pub use codec::{
55    build_oversized_frames, sha256_digest, split_string_by_byte_size, utf8_byte_len,
56    BuiltOversizedFrames, OversizedSenderOptions,
57};
58pub use constants::*;
59pub use errors::OversizedTransferError;
60pub use frame::{progress_token_string, CompletionMode, OversizedFrame};
61pub use receiver::{OversizedTransferReceiver, TransferPolicy};
62pub use sender::send_oversized_transfer;
63pub use sizing::{measure_published_event_size, resolve_safe_chunk_size};
64
65/// CEP-22 oversized-transfer configuration shared by both transports.
66///
67/// Bundles the capability gate plus the sender/receiver tuning knobs so the
68/// nine numeric defaults don't clutter the flat transport configs. Attached to
69/// [`NostrServerTransportConfig`](crate::transport::NostrServerTransportConfig)
70/// and [`NostrClientTransportConfig`](crate::transport::NostrClientTransportConfig)
71/// via their `with_oversized_transfer` / `with_oversized_enabled` builders.
72///
73/// **Enabled by default** (TS parity) — opt out with
74/// [`with_enabled(false)`](Self::with_enabled) or the transports'
75/// `with_oversized_enabled(false)` builders. The negotiation gates make the
76/// default safe for non-oversized peers: the server activates only for
77/// clients that advertise support, and the client fragments only requests
78/// carrying a `progressToken` — a disabled peer just sees one extra
79/// `support_oversized_transfer` tag.
80#[derive(Debug, Clone)]
81#[non_exhaustive]
82pub struct OversizedTransferConfig {
83    /// Master gate, `true` by default. When `false` the capability is neither
84    /// advertised nor activated, and the server does not learn a client's flag.
85    pub enabled: bool,
86    /// Serialized byte length at or above which the sender switches to oversized transfer.
87    pub threshold: usize,
88    /// Per-chunk data size (bytes).
89    pub chunk_size: usize,
90    /// Upper bound on the total reassembled payload a receiver will accept (bytes).
91    pub max_transfer_bytes: u64,
92    /// Upper bound on the number of chunks a receiver will accept.
93    pub max_transfer_chunks: u64,
94    /// Upper bound on concurrently active receiver-side transfers.
95    pub max_concurrent_transfers: usize,
96    /// Hard timeout for an in-flight transfer (milliseconds), measured from
97    /// admission. `0` disables the receiver-side watchdog.
98    pub transfer_timeout_ms: u64,
99    /// Maximum forward gap between the next expected chunk and an out-of-order
100    /// chunk that will still be buffered.
101    pub max_out_of_order_window: u64,
102    /// Maximum number of buffered out-of-order chunks.
103    pub max_out_of_order_chunks: usize,
104    /// Timeout a sender waits for an `accept` frame before giving up (milliseconds).
105    ///
106    /// Used by the **client** transport only. The client is the sole party that
107    /// sends a `start` frame with an accept handshake and then waits for the
108    /// `accept`. On the **server** transport this field is inert: a server is
109    /// always the *receiver* of the handshake — it emits the `accept`, it never
110    /// waits for one — so its value is never read server-side.
111    pub accept_timeout_ms: u64,
112}
113
114impl Default for OversizedTransferConfig {
115    fn default() -> Self {
116        Self {
117            enabled: true,
118            threshold: DEFAULT_OVERSIZED_THRESHOLD,
119            chunk_size: DEFAULT_CHUNK_SIZE,
120            max_transfer_bytes: DEFAULT_MAX_TRANSFER_BYTES,
121            max_transfer_chunks: DEFAULT_MAX_TRANSFER_CHUNKS,
122            max_concurrent_transfers: DEFAULT_MAX_CONCURRENT_TRANSFERS,
123            transfer_timeout_ms: DEFAULT_TRANSFER_TIMEOUT_MS,
124            max_out_of_order_window: DEFAULT_MAX_OUT_OF_ORDER_WINDOW,
125            max_out_of_order_chunks: DEFAULT_MAX_OUT_OF_ORDER_CHUNKS,
126            accept_timeout_ms: DEFAULT_ACCEPT_TIMEOUT_MS,
127        }
128    }
129}
130
131impl OversizedTransferConfig {
132    /// An explicitly enabled config with all other knobs at their defaults.
133    ///
134    /// Redundant since `enabled` defaulted to `true` (kept for API
135    /// stability); equivalent to [`OversizedTransferConfig::default`].
136    pub fn enabled() -> Self {
137        Self {
138            enabled: true,
139            ..Self::default()
140        }
141    }
142
143    /// Enable or disable oversized transfer.
144    pub fn with_enabled(mut self, enabled: bool) -> Self {
145        self.enabled = enabled;
146        self
147    }
148
149    /// Set the serialized-byte threshold at which the sender fragments.
150    pub fn with_threshold(mut self, threshold: usize) -> Self {
151        self.threshold = threshold;
152        self
153    }
154
155    /// Set the per-chunk data size (bytes).
156    pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
157        self.chunk_size = chunk_size;
158        self
159    }
160
161    /// Set the upper bound on the total reassembled payload (bytes).
162    pub fn with_max_transfer_bytes(mut self, max: u64) -> Self {
163        self.max_transfer_bytes = max;
164        self
165    }
166
167    /// Set the upper bound on the number of chunks a receiver will accept.
168    pub fn with_max_transfer_chunks(mut self, max: u64) -> Self {
169        self.max_transfer_chunks = max;
170        self
171    }
172
173    /// Set the upper bound on concurrently active receiver-side transfers.
174    pub fn with_max_concurrent_transfers(mut self, max: usize) -> Self {
175        self.max_concurrent_transfers = max;
176        self
177    }
178
179    /// Set the hard per-transfer timeout (milliseconds).
180    pub fn with_transfer_timeout_ms(mut self, ms: u64) -> Self {
181        self.transfer_timeout_ms = ms;
182        self
183    }
184
185    /// Set the maximum forward gap for buffering out-of-order chunks.
186    pub fn with_max_out_of_order_window(mut self, window: u64) -> Self {
187        self.max_out_of_order_window = window;
188        self
189    }
190
191    /// Set the maximum number of buffered out-of-order chunks.
192    pub fn with_max_out_of_order_chunks(mut self, max: usize) -> Self {
193        self.max_out_of_order_chunks = max;
194        self
195    }
196
197    /// Set the sender's `accept`-frame wait timeout (milliseconds).
198    pub fn with_accept_timeout_ms(mut self, ms: u64) -> Self {
199        self.accept_timeout_ms = ms;
200        self
201    }
202}
203
204impl From<&OversizedTransferConfig> for TransferPolicy {
205    /// Project the receiver-relevant knobs of an [`OversizedTransferConfig`] into
206    /// a [`TransferPolicy`] (the receiver admission policy).
207    fn from(config: &OversizedTransferConfig) -> Self {
208        TransferPolicy {
209            max_transfer_bytes: config.max_transfer_bytes,
210            max_transfer_chunks: config.max_transfer_chunks,
211            max_concurrent_transfers: config.max_concurrent_transfers,
212            max_out_of_order_window: config.max_out_of_order_window,
213            max_out_of_order_chunks: config.max_out_of_order_chunks,
214            transfer_timeout_ms: config.transfer_timeout_ms,
215        }
216    }
217}