use std::future::Future;
use std::time::Duration;
use nostr_sdk::prelude::EventId;
use tokio::sync::oneshot;
use crate::core::types::JsonRpcNotification;
use super::codec::BuiltOversizedFrames;
use super::errors::OversizedTransferError;
fn progress_token_of(frame: &JsonRpcNotification) -> String {
frame
.params
.as_ref()
.and_then(|params| params.get("progressToken"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string()
}
pub async fn send_oversized_transfer<P, Fut>(
frames: BuiltOversizedFrames,
needs_accept: bool,
await_accept: Option<oneshot::Receiver<()>>,
accept_timeout: Duration,
mut publish: P,
) -> crate::Result<EventId>
where
P: FnMut(JsonRpcNotification) -> Fut,
Fut: Future<Output = crate::Result<EventId>>,
{
let token = progress_token_of(&frames.start);
publish(frames.start).await?;
if needs_accept {
let outcome = match await_accept {
Some(rx) => tokio::time::timeout(accept_timeout, rx).await,
None => {
return Err(OversizedTransferError::abort(
token,
Some("no accept waiter registered".to_string()),
)
.into());
}
};
if matches!(outcome, Err(_) | Ok(Err(_))) {
return Err(OversizedTransferError::abort(
token,
Some("timed out waiting for accept frame".to_string()),
)
.into());
}
}
for chunk in frames.chunks {
publish(chunk).await?;
}
let end_id = publish(frames.end).await?;
Ok(end_id)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
use crate::transport::oversized_transfer::codec::{
build_oversized_frames, OversizedSenderOptions,
};
fn frame_type(notification: &JsonRpcNotification) -> String {
notification.params.as_ref().unwrap()["cvm"]["frameType"]
.as_str()
.unwrap()
.to_string()
}
fn nth_event_id(n: usize) -> EventId {
EventId::from_hex(&format!("{n:064x}")).unwrap()
}
fn sample_frames(token: &str, needs_accept: bool) -> BuiltOversizedFrames {
let payload = "x".repeat(50);
let options = OversizedSenderOptions::new(token)
.with_chunk_size(8)
.with_accept_handshake(needs_accept);
build_oversized_frames(&payload, &options).unwrap()
}
fn recording_publisher(
log: Rc<RefCell<Vec<JsonRpcNotification>>>,
) -> impl FnMut(JsonRpcNotification) -> std::pin::Pin<Box<dyn Future<Output = crate::Result<EventId>>>>
{
move |frame: JsonRpcNotification| {
let log = log.clone();
Box::pin(async move {
log.borrow_mut().push(frame);
let n = log.borrow().len();
Ok(nth_event_id(n))
})
}
}
#[tokio::test]
async fn publishes_frames_in_canonical_order_and_returns_end_id() {
let frames = sample_frames("tok", false);
let expected_count = frames.frame_count();
let log = Rc::new(RefCell::new(Vec::new()));
let publish = recording_publisher(log.clone());
let end_id = send_oversized_transfer(frames, false, None, Duration::from_secs(1), publish)
.await
.unwrap();
let frames_log = log.borrow();
assert_eq!(frames_log.len(), expected_count);
assert!(expected_count > 2, "payload should span multiple chunks");
assert_eq!(frame_type(&frames_log[0]), "start");
assert_eq!(frame_type(frames_log.last().unwrap()), "end");
for mid in &frames_log[1..frames_log.len() - 1] {
assert_eq!(frame_type(mid), "chunk");
}
assert_eq!(end_id, nth_event_id(expected_count));
}
#[tokio::test]
async fn accept_is_awaited_and_satisfied_when_needed() {
let frames = sample_frames("tok", true);
let expected_count = frames.frame_count();
let log = Rc::new(RefCell::new(Vec::new()));
let publish = recording_publisher(log.clone());
let (tx, rx) = oneshot::channel();
tx.send(()).unwrap();
let end_id =
send_oversized_transfer(frames, true, Some(rx), Duration::from_secs(1), publish)
.await
.unwrap();
let frames_log = log.borrow();
assert_eq!(frames_log.len(), expected_count);
assert_eq!(frame_type(&frames_log[0]), "start");
assert_eq!(frame_type(frames_log.last().unwrap()), "end");
assert_eq!(end_id, nth_event_id(expected_count));
}
#[tokio::test]
async fn accept_is_not_awaited_when_not_needed() {
let frames = sample_frames("tok", false);
let log = Rc::new(RefCell::new(Vec::new()));
let publish = recording_publisher(log.clone());
let (_tx, rx) = oneshot::channel();
let result =
send_oversized_transfer(frames, false, Some(rx), Duration::from_millis(5), publish)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn missed_accept_returns_abort() {
let frames = sample_frames("tok", true);
let log = Rc::new(RefCell::new(Vec::new()));
let publish = recording_publisher(log.clone());
let (_tx, rx) = oneshot::channel();
let result =
send_oversized_transfer(frames, true, Some(rx), Duration::from_millis(20), publish)
.await;
match result {
Err(crate::Error::OversizedTransfer(OversizedTransferError::Abort {
token, ..
})) => assert_eq!(token, "tok"),
other => panic!("expected abort error, got {other:?}"),
}
assert_eq!(log.borrow().len(), 1);
}
}