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}