1use std::collections::{BTreeMap, VecDeque};
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::{Arc, Mutex};
21use std::task::{Context, Poll, Waker};
22use std::time::{Duration, Instant};
23
24use futures::future::BoxFuture;
25use futures::Stream;
26use nostr_sdk::prelude::EventId;
27
28use crate::core::types::JsonRpcNotification;
29
30use super::constants::MAX_OPEN_STREAM_PING_NONCE_BYTES;
31use super::errors::OpenStreamError;
32use super::frame::OpenStreamFrame;
33
34pub type PublishFrame =
40 Arc<dyn Fn(JsonRpcNotification) -> BoxFuture<'static, crate::Result<EventId>> + Send + Sync>;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum FrameOutcome {
48 None,
50 SendPong(String),
52 Closed,
54 Aborted(Option<String>),
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum KeepaliveAction {
61 None,
63 SendPing(String),
65 Abort(String),
67}
68
69pub struct OpenStreamSessionOptions {
71 pub progress_token: String,
73 pub max_buffered_chunks: usize,
75 pub max_buffered_bytes: u64,
77 pub idle_timeout_ms: u64,
79 pub probe_timeout_ms: u64,
81 pub close_grace_period_ms: u64,
83 pub publish_frame: Option<PublishFrame>,
85}
86
87struct SessionState {
92 progress_token: String,
93
94 max_buffered_chunks: usize,
96 max_buffered_bytes: u64,
97 idle_timeout: Duration,
98 probe_timeout: Duration,
99 close_grace_period: Duration,
100
101 queue: VecDeque<String>,
104 buffered: BTreeMap<u64, String>,
106 buffered_bytes: u64,
108 queued_bytes: u64,
110 next_expected_chunk: u64,
112 last_progress: i64,
114 started: bool,
115 active: bool,
116 closed_remotely: bool,
117 expected_last_chunk_index: Option<u64>,
119 terminal: Option<OpenStreamError>,
121 waker: Option<Waker>,
123 closed_wakers: Vec<Waker>,
125
126 last_activity: Instant,
128 pending_probe_nonce: Option<String>,
129 control_nonce: u64,
130 probe_deadline: Option<Instant>,
131 close_grace_deadline: Option<Instant>,
132 outbound_progress: u64,
134}
135
136impl SessionState {
137 fn assert_active(&self) -> Result<(), OpenStreamError> {
138 if !self.active {
139 return Err(OpenStreamError::Sequence(format!(
140 "Received frame for inactive stream {}",
141 self.progress_token
142 )));
143 }
144 Ok(())
145 }
146
147 fn assert_started(&self) -> Result<(), OpenStreamError> {
148 if !self.started {
149 return Err(OpenStreamError::Sequence(format!(
150 "Received non-start frame before start for {}",
151 self.progress_token
152 )));
153 }
154 Ok(())
155 }
156
157 fn assert_progress(&mut self, progress: i64) -> Result<(), OpenStreamError> {
159 if progress <= self.last_progress {
160 return Err(OpenStreamError::Sequence(format!(
161 "Non-increasing progress for stream {}",
162 self.progress_token
163 )));
164 }
165 self.last_progress = progress;
166 Ok(())
167 }
168
169 fn buffer_chunk(&mut self, chunk_index: u64, data: String) -> Result<(), OpenStreamError> {
170 if chunk_index < self.next_expected_chunk {
171 return Err(OpenStreamError::Sequence(format!(
172 "Stale chunkIndex {chunk_index} for {}",
173 self.progress_token
174 )));
175 }
176 if self.buffered.contains_key(&chunk_index) {
177 return Err(OpenStreamError::Sequence(format!(
178 "Duplicate chunkIndex {chunk_index} for {}",
179 self.progress_token
180 )));
181 }
182
183 let chunk_bytes = data.len() as u64;
184 if self.buffered.len() + self.queue.len() >= self.max_buffered_chunks {
185 return Err(OpenStreamError::Sequence(format!(
186 "Buffered chunk limit exceeded for stream {}",
187 self.progress_token
188 )));
189 }
190 if self.buffered_bytes + self.queued_bytes + chunk_bytes > self.max_buffered_bytes {
191 return Err(OpenStreamError::Sequence(format!(
192 "Buffered byte limit exceeded for stream {}",
193 self.progress_token
194 )));
195 }
196
197 self.buffered.insert(chunk_index, data);
198 self.buffered_bytes += chunk_bytes;
199 Ok(())
200 }
201
202 fn flush_contiguous_chunks(&mut self) -> Result<bool, OpenStreamError> {
206 while let Some(data) = self.buffered.remove(&self.next_expected_chunk) {
207 self.buffered_bytes = self.buffered_bytes.saturating_sub(data.len() as u64);
208 self.emit(data);
209 self.next_expected_chunk += 1;
210 }
211
212 if self.closed_remotely {
213 return self.maybe_finish_gracefully();
214 }
215 Ok(false)
216 }
217
218 fn maybe_finish_gracefully(&mut self) -> Result<bool, OpenStreamError> {
222 if !self.closed_remotely || !self.buffered.is_empty() {
223 return Ok(false);
224 }
225
226 if let Some(expected) = self.expected_last_chunk_index {
227 if self.next_expected_chunk != expected + 1 {
228 return Err(OpenStreamError::Sequence(format!(
229 "Incomplete stream for {}: expected chunks through {expected}",
230 self.progress_token
231 )));
232 }
233 }
234
235 self.finalize(None);
236 Ok(true)
237 }
238
239 fn emit(&mut self, data: String) {
241 self.queued_bytes += data.len() as u64;
242 self.queue.push_back(data);
243 self.wake_stream();
244 }
245
246 fn handle_pong(&mut self, nonce: &str) {
247 if self.pending_probe_nonce.as_deref() == Some(nonce) {
248 self.pending_probe_nonce = None;
249 self.probe_deadline = None;
250 }
251 }
252
253 fn next_control_nonce(&mut self) -> String {
254 self.control_nonce += 1;
255 format!("{}:{}", self.progress_token, self.control_nonce)
256 }
257
258 fn tick(&mut self, now: Instant) -> KeepaliveAction {
261 if !self.active {
262 return KeepaliveAction::None;
263 }
264
265 if let (Some(deadline), true) = (self.probe_deadline, self.pending_probe_nonce.is_some()) {
267 if now >= deadline {
268 let token = self.progress_token.clone();
269 self.finalize(Some(OpenStreamError::abort(
270 token,
271 Some("Probe timeout".to_string()),
272 )));
273 return KeepaliveAction::Abort("Probe timeout".to_string());
274 }
275 }
276
277 if let Some(deadline) = self.close_grace_deadline {
279 if self.closed_remotely && !self.buffered.is_empty() && now >= deadline {
280 let token = self.progress_token.clone();
281 self.finalize(Some(OpenStreamError::abort(
282 token,
283 Some("Close grace period expired".to_string()),
284 )));
285 return KeepaliveAction::Abort("Close grace period expired".to_string());
286 }
287 }
288
289 if !self.closed_remotely
291 && self.pending_probe_nonce.is_none()
292 && now.saturating_duration_since(self.last_activity) >= self.idle_timeout
293 {
294 let nonce = self.next_control_nonce();
295 self.pending_probe_nonce = Some(nonce.clone());
296 self.probe_deadline = Some(now + self.probe_timeout);
297 return KeepaliveAction::SendPing(nonce);
298 }
299
300 KeepaliveAction::None
301 }
302
303 fn finalize(&mut self, error: Option<OpenStreamError>) {
307 if !self.active {
308 return;
309 }
310 self.active = false;
311 self.terminal = error;
312 self.pending_probe_nonce = None;
313 self.probe_deadline = None;
314 self.close_grace_deadline = None;
315 self.wake_stream();
316 for waker in self.closed_wakers.drain(..) {
317 waker.wake();
318 }
319 }
320
321 fn wake_stream(&mut self) {
322 if let Some(waker) = self.waker.take() {
323 waker.wake();
324 }
325 }
326}
327
328#[derive(Clone)]
333pub struct OpenStreamSession {
334 progress_token: String,
336 state: Arc<Mutex<SessionState>>,
337 publish_frame: Option<PublishFrame>,
338}
339
340impl std::fmt::Debug for OpenStreamSession {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("OpenStreamSession")
343 .field("progress_token", &self.progress_token)
344 .finish_non_exhaustive()
345 }
346}
347
348impl OpenStreamSession {
349 pub fn new(options: OpenStreamSessionOptions) -> Self {
351 let publish_frame = options.publish_frame.clone();
352 let progress_token = options.progress_token.clone();
353 let state = SessionState {
354 progress_token: options.progress_token,
355 max_buffered_chunks: options.max_buffered_chunks,
356 max_buffered_bytes: options.max_buffered_bytes,
357 idle_timeout: Duration::from_millis(options.idle_timeout_ms),
358 probe_timeout: Duration::from_millis(options.probe_timeout_ms),
359 close_grace_period: Duration::from_millis(options.close_grace_period_ms),
360 queue: VecDeque::new(),
361 buffered: BTreeMap::new(),
362 buffered_bytes: 0,
363 queued_bytes: 0,
364 next_expected_chunk: 0,
365 last_progress: -1,
366 started: false,
367 active: true,
368 closed_remotely: false,
369 expected_last_chunk_index: None,
370 terminal: None,
371 waker: None,
372 closed_wakers: Vec::new(),
373 last_activity: Instant::now(),
374 pending_probe_nonce: None,
375 control_nonce: 0,
376 probe_deadline: None,
377 close_grace_deadline: None,
378 outbound_progress: 0,
379 };
380 Self {
381 progress_token,
382 state: Arc::new(Mutex::new(state)),
383 publish_frame,
384 }
385 }
386
387 pub fn progress_token(&self) -> &str {
389 &self.progress_token
390 }
391
392 pub fn is_active(&self) -> bool {
394 self.state.lock().unwrap().active
395 }
396
397 pub fn has_started(&self) -> bool {
399 self.state.lock().unwrap().started
400 }
401
402 pub fn process_frame(
409 &self,
410 now: Instant,
411 progress: i64,
412 frame: OpenStreamFrame,
413 ) -> Result<FrameOutcome, OpenStreamError> {
414 let mut s = self.state.lock().unwrap();
415 s.assert_active()?;
416 s.assert_progress(progress)?;
417 s.last_activity = now;
419
420 match frame {
421 OpenStreamFrame::Start { .. } => {
422 if s.started {
423 return Err(OpenStreamError::Sequence(format!(
424 "Duplicate start frame for stream {}",
425 s.progress_token
426 )));
427 }
428 s.started = true;
429 Ok(FrameOutcome::None)
430 }
431 OpenStreamFrame::Accept => Ok(FrameOutcome::None),
432 OpenStreamFrame::Ping { nonce } => {
433 s.assert_started()?;
434 if nonce.len() > MAX_OPEN_STREAM_PING_NONCE_BYTES {
435 return Err(OpenStreamError::Sequence(format!(
436 "ping nonce exceeds {MAX_OPEN_STREAM_PING_NONCE_BYTES}-byte cap (got {} bytes)",
437 nonce.len()
438 )));
439 }
440 Ok(FrameOutcome::SendPong(nonce))
441 }
442 OpenStreamFrame::Pong { nonce } => {
443 s.assert_started()?;
444 s.handle_pong(&nonce);
445 Ok(FrameOutcome::None)
446 }
447 OpenStreamFrame::Chunk { chunk_index, data } => {
448 s.assert_started()?;
449 s.buffer_chunk(chunk_index, data)?;
450 let finished = s.flush_contiguous_chunks()?;
451 Ok(if finished {
452 FrameOutcome::Closed
453 } else {
454 FrameOutcome::None
455 })
456 }
457 OpenStreamFrame::Close { last_chunk_index } => {
458 s.assert_started()?;
459 s.closed_remotely = true;
460 s.expected_last_chunk_index = last_chunk_index;
461 let finished = s.flush_contiguous_chunks()?;
462 if finished {
463 Ok(FrameOutcome::Closed)
464 } else {
465 s.close_grace_deadline = Some(now + s.close_grace_period);
468 Ok(FrameOutcome::None)
469 }
470 }
471 OpenStreamFrame::Abort { reason } => {
472 let token = s.progress_token.clone();
473 s.finalize(Some(OpenStreamError::abort(token, reason.clone())));
474 Ok(FrameOutcome::Aborted(reason))
475 }
476 }
477 }
478
479 pub fn tick(&self, now: Instant) -> KeepaliveAction {
482 self.state.lock().unwrap().tick(now)
483 }
484
485 pub fn fail(&self, error: OpenStreamError) {
488 self.state.lock().unwrap().finalize(Some(error));
489 }
490
491 pub fn dispose(&self) {
494 self.state.lock().unwrap().finalize(None);
495 }
496
497 pub async fn abort(&self, reason: Option<String>) {
501 let publish = {
502 let mut s = self.state.lock().unwrap();
503 if !s.active {
504 return;
505 }
506 let token = s.progress_token.clone();
507 s.finalize(Some(OpenStreamError::abort(token.clone(), reason.clone())));
508 self.publish_frame.as_ref().map(|publisher| {
509 s.outbound_progress += 1;
510 (publisher.clone(), token, s.outbound_progress)
511 })
512 };
513
514 if let Some((publisher, token, progress)) = publish {
515 if let Ok(notification) = (OpenStreamFrame::Abort {
516 reason: reason.clone(),
517 })
518 .into_progress_notification(&token, progress, None)
519 {
520 let _ = publisher(notification).await;
522 }
523 }
524 }
525
526 pub fn closed(&self) -> Closed {
530 Closed {
531 state: self.state.clone(),
532 }
533 }
534
535 #[cfg(test)]
538 pub(crate) fn shares_state_with(&self, other: &Self) -> bool {
539 Arc::ptr_eq(&self.state, &other.state)
540 }
541}
542
543impl Stream for OpenStreamSession {
544 type Item = Result<String, OpenStreamError>;
545
546 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
547 let mut s = self.state.lock().unwrap();
548 if let Some(item) = s.queue.pop_front() {
549 s.queued_bytes = s.queued_bytes.saturating_sub(item.len() as u64);
550 return Poll::Ready(Some(Ok(item)));
551 }
552 if !s.active {
553 return match s.terminal.take() {
554 Some(error) => Poll::Ready(Some(Err(error))),
555 None => Poll::Ready(None),
556 };
557 }
558 s.waker = Some(cx.waker().clone());
559 Poll::Pending
560 }
561}
562
563pub struct Closed {
565 state: Arc<Mutex<SessionState>>,
566}
567
568impl Future for Closed {
569 type Output = ();
570
571 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
572 let mut s = self.state.lock().unwrap();
573 if !s.active {
574 Poll::Ready(())
575 } else {
576 s.closed_wakers.push(cx.waker().clone());
577 Poll::Pending
578 }
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use futures::{poll, StreamExt};
586
587 fn make_session(token: &str, max_chunks: usize, max_bytes: u64) -> OpenStreamSession {
588 OpenStreamSession::new(OpenStreamSessionOptions {
589 progress_token: token.to_string(),
590 max_buffered_chunks: max_chunks,
591 max_buffered_bytes: max_bytes,
592 idle_timeout_ms: 30_000,
593 probe_timeout_ms: 20_000,
594 close_grace_period_ms: 5_000,
595 publish_frame: None,
596 })
597 }
598
599 fn make_session_timers(token: &str, idle: u64, probe: u64, grace: u64) -> OpenStreamSession {
600 OpenStreamSession::new(OpenStreamSessionOptions {
601 progress_token: token.to_string(),
602 max_buffered_chunks: 8,
603 max_buffered_bytes: 1024,
604 idle_timeout_ms: idle,
605 probe_timeout_ms: probe,
606 close_grace_period_ms: grace,
607 publish_frame: None,
608 })
609 }
610
611 fn start() -> OpenStreamFrame {
612 OpenStreamFrame::Start { content_type: None }
613 }
614
615 fn chunk(index: u64, data: &str) -> OpenStreamFrame {
616 OpenStreamFrame::Chunk {
617 chunk_index: index,
618 data: data.to_string(),
619 }
620 }
621
622 fn close(last: Option<u64>) -> OpenStreamFrame {
623 OpenStreamFrame::Close {
624 last_chunk_index: last,
625 }
626 }
627
628 async fn drain_ok(session: &mut OpenStreamSession) -> Vec<String> {
630 let mut out = Vec::new();
631 while let Some(item) = session.next().await {
632 out.push(item.expect("graceful stream must not yield an error"));
633 }
634 out
635 }
636
637 #[tokio::test]
640 async fn yields_ordered_chunks_and_finishes_after_close() {
641 let now = Instant::now();
642 let mut s = make_session("token-1", 8, 1024);
643 s.process_frame(now, 1, start()).unwrap();
644 s.process_frame(now, 2, chunk(0, "hello")).unwrap();
645 s.process_frame(now, 3, chunk(1, " world")).unwrap();
646 assert_eq!(
647 s.process_frame(now, 4, close(None)).unwrap(),
648 FrameOutcome::Closed
649 );
650
651 assert_eq!(drain_ok(&mut s).await, vec!["hello", " world"]);
652 s.closed().await;
653 }
654
655 #[tokio::test]
656 async fn buffers_out_of_order_chunks_until_contiguous() {
657 let now = Instant::now();
658 let mut s = make_session("token-2", 8, 1024);
659 s.process_frame(now, 1, start()).unwrap();
660 s.process_frame(now, 2, chunk(1, "world")).unwrap();
661 s.process_frame(now, 3, chunk(0, "hello ")).unwrap();
662 s.process_frame(now, 4, close(None)).unwrap();
663
664 assert_eq!(drain_ok(&mut s).await, vec!["hello ", "world"]);
665 }
666
667 #[tokio::test]
668 async fn fails_when_progress_does_not_increase() {
669 let now = Instant::now();
670 let s = make_session("token-3", 8, 1024);
671 s.process_frame(now, 1, start()).unwrap();
672 let err = s.process_frame(now, 1, chunk(0, "repeat")).unwrap_err();
673 assert!(matches!(err, OpenStreamError::Sequence(_)));
674 }
675
676 #[tokio::test]
677 async fn rejects_a_duplicate_start_frame() {
678 let now = Instant::now();
679 let s = make_session("token-dup-start", 8, 1024);
680 s.process_frame(now, 1, start()).unwrap();
681 let err = s.process_frame(now, 2, start()).unwrap_err();
683 assert!(matches!(err, OpenStreamError::Sequence(_)));
684 }
685
686 #[tokio::test]
687 async fn abort_frame_terminates_stream_with_error_on_next_poll() {
688 let now = Instant::now();
689 let mut s = make_session("token-4", 8, 1024);
690 s.process_frame(now, 1, start()).unwrap();
691 assert_eq!(
692 s.process_frame(
693 now,
694 2,
695 OpenStreamFrame::Abort {
696 reason: Some("boom".to_string())
697 }
698 )
699 .unwrap(),
700 FrameOutcome::Aborted(Some("boom".to_string()))
701 );
702
703 match s.next().await {
704 Some(Err(OpenStreamError::Abort { reason, .. })) => {
705 assert_eq!(reason.as_deref(), Some("boom"));
706 }
707 other => panic!("expected abort error, got {other:?}"),
708 }
709 assert!(s.next().await.is_none());
711 s.closed().await;
712 }
713
714 #[tokio::test]
715 async fn fails_when_buffered_chunk_count_exceeds_limit() {
716 let now = Instant::now();
717 let s = make_session("token-buffer-count", 1, 1024);
718 s.process_frame(now, 1, start()).unwrap();
719 s.process_frame(now, 2, chunk(1, "late")).unwrap();
720 let err = s.process_frame(now, 3, chunk(2, "later")).unwrap_err();
721 assert!(matches!(err, OpenStreamError::Sequence(_)));
722 }
723
724 #[tokio::test]
725 async fn fails_when_buffered_byte_count_exceeds_limit_utf8() {
726 let now = Instant::now();
727 let s = make_session("token-buffer-bytes", 4, 4);
728 s.process_frame(now, 1, start()).unwrap();
729 let err = s.process_frame(now, 2, chunk(1, "héllo")).unwrap_err();
732 assert!(matches!(err, OpenStreamError::Sequence(_)));
733 }
734
735 #[tokio::test]
736 async fn fail_terminates_stream_and_closed_resolves() {
737 let now = Instant::now();
738 let mut s = make_session("token-explicit-fail", 4, 16);
739 s.process_frame(now, 1, start()).unwrap();
740 s.fail(OpenStreamError::Sequence("synthetic failure".to_string()));
741
742 match s.next().await {
743 Some(Err(OpenStreamError::Sequence(msg))) => assert!(msg.contains("synthetic")),
744 other => panic!("expected sequence error, got {other:?}"),
745 }
746 s.closed().await;
748 }
749
750 #[tokio::test]
751 async fn counts_unread_queued_chunks_against_the_byte_limit() {
752 let now = Instant::now();
753 let s = make_session("token-queued-bytes", 4, 5);
754 s.process_frame(now, 1, start()).unwrap();
755 s.process_frame(now, 2, chunk(0, "abc")).unwrap();
757 let err = s.process_frame(now, 3, chunk(1, "def")).unwrap_err();
758 assert!(matches!(err, OpenStreamError::Sequence(_)));
759 }
760
761 #[tokio::test]
762 async fn releases_queued_byte_budget_after_consume() {
763 let now = Instant::now();
764 let mut s = make_session("token-queued-release", 4, 6);
765 s.process_frame(now, 1, start()).unwrap();
766 s.process_frame(now, 2, chunk(0, "abc")).unwrap();
767 s.process_frame(now, 3, chunk(1, "def")).unwrap();
768 assert!(matches!(
770 s.process_frame(now, 4, chunk(2, "g")).unwrap_err(),
771 OpenStreamError::Sequence(_)
772 ));
773
774 assert_eq!(s.next().await.unwrap().unwrap(), "abc");
776 s.process_frame(now, 5, chunk(2, "g")).unwrap();
777 }
778
779 #[tokio::test]
780 async fn rejects_frames_after_close() {
781 let now = Instant::now();
782 let s = make_session("token-post-close", 8, 1024);
783 s.process_frame(now, 1, start()).unwrap();
784 assert_eq!(
785 s.process_frame(now, 2, close(None)).unwrap(),
786 FrameOutcome::Closed
787 );
788 let err = s.process_frame(now, 3, chunk(0, "late")).unwrap_err();
789 assert!(matches!(err, OpenStreamError::Sequence(_)));
790 }
791
792 #[tokio::test]
793 async fn rejects_frames_after_abort() {
794 let now = Instant::now();
795 let s = make_session("token-post-abort", 8, 1024);
796 s.process_frame(now, 1, start()).unwrap();
797 s.process_frame(
798 now,
799 2,
800 OpenStreamFrame::Abort {
801 reason: Some("boom".to_string()),
802 },
803 )
804 .unwrap();
805 assert!(matches!(
806 s.process_frame(now, 3, chunk(0, "late")).unwrap_err(),
807 OpenStreamError::Sequence(_)
808 ));
809 assert!(matches!(
810 s.process_frame(now, 4, close(None)).unwrap_err(),
811 OpenStreamError::Sequence(_)
812 ));
813 }
814
815 #[tokio::test]
816 async fn rejects_stale_and_duplicate_chunk_indexes() {
817 let now = Instant::now();
818 let s = make_session("token-stale", 8, 1024);
820 s.process_frame(now, 1, start()).unwrap();
821 s.process_frame(now, 2, chunk(0, "hello")).unwrap();
822 assert!(matches!(
823 s.process_frame(now, 3, chunk(0, "late-duplicate"))
824 .unwrap_err(),
825 OpenStreamError::Sequence(_)
826 ));
827
828 let s2 = make_session("token-dup", 8, 1024);
830 s2.process_frame(now, 1, start()).unwrap();
831 s2.process_frame(now, 2, chunk(2, "a")).unwrap();
832 assert!(matches!(
833 s2.process_frame(now, 3, chunk(2, "b")).unwrap_err(),
834 OpenStreamError::Sequence(_)
835 ));
836 }
837
838 #[tokio::test]
839 async fn requires_all_chunks_through_close_last_chunk_index() {
840 let now = Instant::now();
841 let s = make_session("token-last-chunk-index", 8, 1024);
842 s.process_frame(now, 1, start()).unwrap();
843 s.process_frame(now, 2, chunk(0, "hello")).unwrap();
844 let err = s.process_frame(now, 3, close(Some(1))).unwrap_err();
845 assert!(matches!(err, OpenStreamError::Sequence(_)));
846 }
847
848 #[tokio::test]
849 async fn allows_graceful_close_when_last_chunk_index_matches() {
850 let now = Instant::now();
851 let mut s = make_session("token-last-chunk-complete", 8, 1024);
852 s.process_frame(now, 1, start()).unwrap();
853 s.process_frame(now, 2, chunk(0, "hello")).unwrap();
854 assert_eq!(
855 s.process_frame(now, 3, close(Some(0))).unwrap(),
856 FrameOutcome::Closed
857 );
858 assert_eq!(drain_ok(&mut s).await, vec!["hello"]);
859 s.closed().await;
860 }
861
862 #[tokio::test]
863 async fn rejects_malformed_close_last_chunk_index_and_stays_active() {
864 let now = Instant::now();
865 let mut s = make_session("token-malformed-last", 8, 1024);
866 s.process_frame(now, 1, start()).unwrap();
867 s.process_frame(now, 2, chunk(0, "hello")).unwrap();
868 assert!(matches!(
871 s.process_frame(now, 3, close(Some(5))).unwrap_err(),
872 OpenStreamError::Sequence(_)
873 ));
874 assert!(s.is_active());
875
876 s.abort(Some("cleanup".to_string())).await;
877 assert_eq!(s.next().await.unwrap().unwrap(), "hello");
879 match s.next().await {
880 Some(Err(OpenStreamError::Abort { reason, .. })) => {
881 assert_eq!(reason.as_deref(), Some("cleanup"));
882 }
883 other => panic!("expected abort error, got {other:?}"),
884 }
885 s.closed().await;
886 }
887
888 #[tokio::test]
889 async fn rejects_negative_chunk_index_and_stays_active() {
890 use crate::transport::open_stream::OpenStreamRegistry;
891 use serde_json::json;
892
893 let negative = json!({
898 "type": "open-stream",
899 "frameType": "chunk",
900 "chunkIndex": -1,
901 "data": "oops",
902 });
903 assert_eq!(OpenStreamFrame::from_cvm_value(&negative), None);
904 let fractional = json!({
905 "type": "open-stream",
906 "frameType": "chunk",
907 "chunkIndex": 1.5,
908 "data": "oops",
909 });
910 assert_eq!(OpenStreamFrame::from_cvm_value(&fractional), None);
911
912 let now = Instant::now();
918 let mut registry = OpenStreamRegistry::new();
919 registry
920 .process_frame(
921 now,
922 &start()
923 .into_progress_notification("tok-neg", 1, None)
924 .unwrap(),
925 )
926 .await
927 .unwrap();
928 assert_eq!(registry.size(), 1);
929
930 let bad = JsonRpcNotification {
931 jsonrpc: "2.0".to_string(),
932 method: "notifications/progress".to_string(),
933 params: Some(json!({
934 "progressToken": "tok-neg",
935 "progress": 2,
936 "cvm": negative,
937 })),
938 };
939 assert!(matches!(
940 registry.process_frame(now, &bad).await.unwrap_err(),
941 OpenStreamError::Sequence(_)
942 ));
943
944 assert_eq!(registry.size(), 1);
946 assert!(registry.get_session("tok-neg").unwrap().is_active());
947 }
948
949 #[tokio::test]
950 async fn zero_chunk_close_succeeds() {
951 let now = Instant::now();
952 let mut s = make_session("token-zero-chunk", 8, 1024);
953 s.process_frame(now, 1, start()).unwrap();
954 assert_eq!(
955 s.process_frame(now, 2, close(None)).unwrap(),
956 FrameOutcome::Closed
957 );
958 assert!(s.next().await.is_none());
959 s.closed().await;
960 }
961
962 #[tokio::test]
963 async fn chunk_filling_last_gap_after_close_yields_closed() {
964 let now = Instant::now();
965 let mut s = make_session("token-fill-gap", 8, 1024);
966 s.process_frame(now, 1, start()).unwrap();
967 s.process_frame(now, 2, chunk(1, "world")).unwrap();
969 assert_eq!(
970 s.process_frame(now, 3, close(Some(1))).unwrap(),
971 FrameOutcome::None
972 );
973 assert_eq!(
975 s.process_frame(now, 4, chunk(0, "hello ")).unwrap(),
976 FrameOutcome::Closed
977 );
978 assert_eq!(drain_ok(&mut s).await, vec!["hello ", "world"]);
979 }
980
981 #[tokio::test]
982 async fn parked_reader_is_woken_by_an_arriving_chunk() {
983 let now = Instant::now();
984 let mut s = make_session("token-park", 8, 1024);
985 s.process_frame(now, 1, start()).unwrap();
986 assert!(poll!(s.next()).is_pending());
988 s.process_frame(now, 2, chunk(0, "x")).unwrap();
989 match poll!(s.next()) {
990 Poll::Ready(Some(Ok(value))) => assert_eq!(value, "x"),
991 other => panic!("expected the queued chunk, got {other:?}"),
992 }
993 }
994
995 #[tokio::test]
996 async fn consumer_abort_publishes_an_abort_frame_then_finalizes() {
997 use nostr_sdk::prelude::EventId;
998
999 let now = Instant::now();
1000 let published: Arc<Mutex<Vec<JsonRpcNotification>>> = Arc::new(Mutex::new(Vec::new()));
1001 let recorder = published.clone();
1002 let publish: PublishFrame = Arc::new(move |frame: JsonRpcNotification| {
1003 let recorder = recorder.clone();
1004 Box::pin(async move {
1005 recorder.lock().unwrap().push(frame);
1006 Ok(EventId::all_zeros())
1007 })
1008 });
1009 let mut s = OpenStreamSession::new(OpenStreamSessionOptions {
1010 progress_token: "token-consumer-abort".to_string(),
1011 max_buffered_chunks: 8,
1012 max_buffered_bytes: 1024,
1013 idle_timeout_ms: 30_000,
1014 probe_timeout_ms: 20_000,
1015 close_grace_period_ms: 5_000,
1016 publish_frame: Some(publish),
1017 });
1018 s.process_frame(now, 1, start()).unwrap();
1019
1020 s.abort(Some("user cancelled".to_string())).await;
1021
1022 {
1024 let frames = published.lock().unwrap();
1025 assert_eq!(frames.len(), 1);
1026 let cvm = &frames[0].params.as_ref().unwrap()["cvm"];
1027 assert_eq!(cvm["frameType"], "abort");
1028 assert_eq!(cvm["reason"], "user cancelled");
1029 }
1030
1031 match s.next().await {
1033 Some(Err(OpenStreamError::Abort { reason, .. })) => {
1034 assert_eq!(reason.as_deref(), Some("user cancelled"));
1035 }
1036 other => panic!("expected abort error, got {other:?}"),
1037 }
1038 }
1039
1040 #[tokio::test]
1043 async fn ping_frame_requests_a_pong() {
1044 let now = Instant::now();
1045 let s = make_session("token-ping", 8, 1024);
1046 s.process_frame(now, 1, start()).unwrap();
1047 assert_eq!(
1048 s.process_frame(
1049 now,
1050 2,
1051 OpenStreamFrame::Ping {
1052 nonce: "nonce-1".to_string()
1053 }
1054 )
1055 .unwrap(),
1056 FrameOutcome::SendPong("nonce-1".to_string())
1057 );
1058 }
1059
1060 #[tokio::test]
1061 async fn rejects_ping_nonce_exceeding_64_bytes() {
1062 let now = Instant::now();
1063 let s = make_session("token-ping-cap", 8, 1024);
1064 s.process_frame(now, 1, start()).unwrap();
1065
1066 let at_cap = "x".repeat(MAX_OPEN_STREAM_PING_NONCE_BYTES);
1068 assert_eq!(at_cap.len(), 64);
1069 assert_eq!(
1070 s.process_frame(
1071 now,
1072 2,
1073 OpenStreamFrame::Ping {
1074 nonce: at_cap.clone()
1075 }
1076 )
1077 .unwrap(),
1078 FrameOutcome::SendPong(at_cap)
1079 );
1080
1081 let over_cap = "x".repeat(MAX_OPEN_STREAM_PING_NONCE_BYTES + 1);
1084 assert!(matches!(
1085 s.process_frame(now, 3, OpenStreamFrame::Ping { nonce: over_cap }),
1086 Err(OpenStreamError::Sequence(_))
1087 ));
1088 assert_eq!(
1090 s.process_frame(
1091 now,
1092 4,
1093 OpenStreamFrame::Ping {
1094 nonce: "ok".to_string()
1095 }
1096 )
1097 .unwrap(),
1098 FrameOutcome::SendPong("ok".to_string())
1099 );
1100 }
1101
1102 #[tokio::test]
1103 async fn idle_sends_ping_then_probe_timeout_aborts() {
1104 let t0 = Instant::now();
1105 let mut s = make_session_timers("token-probe", 10, 10, 100);
1106 s.process_frame(t0, 1, start()).unwrap();
1107
1108 assert_eq!(s.tick(t0 + Duration::from_millis(5)), KeepaliveAction::None);
1110 assert_eq!(
1112 s.tick(t0 + Duration::from_millis(10)),
1113 KeepaliveAction::SendPing("token-probe:1".to_string())
1114 );
1115 assert_eq!(
1117 s.tick(t0 + Duration::from_millis(15)),
1118 KeepaliveAction::None
1119 );
1120 assert_eq!(
1122 s.tick(t0 + Duration::from_millis(20)),
1123 KeepaliveAction::Abort("Probe timeout".to_string())
1124 );
1125
1126 match s.next().await {
1127 Some(Err(OpenStreamError::Abort { reason, .. })) => {
1128 assert_eq!(reason.as_deref(), Some("Probe timeout"));
1129 }
1130 other => panic!("expected probe-timeout abort, got {other:?}"),
1131 }
1132 }
1133
1134 #[tokio::test]
1135 async fn matching_pong_clears_probe_and_stream_survives() {
1136 let t0 = Instant::now();
1137 let s = make_session_timers("token-probe-ok", 10, 20, 100);
1138 s.process_frame(t0, 1, start()).unwrap();
1139
1140 let nonce = match s.tick(t0 + Duration::from_millis(10)) {
1141 KeepaliveAction::SendPing(nonce) => nonce,
1142 other => panic!("expected a ping, got {other:?}"),
1143 };
1144 s.process_frame(
1146 t0 + Duration::from_millis(15),
1147 2,
1148 OpenStreamFrame::Pong { nonce },
1149 )
1150 .unwrap();
1151
1152 assert!(!matches!(
1154 s.tick(t0 + Duration::from_millis(30)),
1155 KeepaliveAction::Abort(_)
1156 ));
1157 assert!(s.is_active());
1158 }
1159
1160 #[tokio::test]
1161 async fn unexpected_pong_is_ignored_for_liveness() {
1162 let t0 = Instant::now();
1163 let mut s = make_session_timers("token-bad-pong", 10, 10, 100);
1164 s.process_frame(t0, 1, start()).unwrap();
1165 assert!(matches!(
1166 s.tick(t0 + Duration::from_millis(10)),
1167 KeepaliveAction::SendPing(_)
1168 ));
1169
1170 s.process_frame(
1172 t0 + Duration::from_millis(12),
1173 2,
1174 OpenStreamFrame::Pong {
1175 nonce: "unexpected".to_string(),
1176 },
1177 )
1178 .unwrap();
1179
1180 assert_eq!(
1181 s.tick(t0 + Duration::from_millis(20)),
1182 KeepaliveAction::Abort("Probe timeout".to_string())
1183 );
1184 match s.next().await {
1185 Some(Err(OpenStreamError::Abort { .. })) => {}
1186 other => panic!("expected abort, got {other:?}"),
1187 }
1188 }
1189
1190 #[tokio::test]
1191 async fn close_grace_deadline_with_missing_chunks_aborts() {
1192 let t0 = Instant::now();
1193 let mut s = make_session_timers("token-grace", 100, 100, 10);
1194 s.process_frame(t0, 1, start()).unwrap();
1195 s.process_frame(t0, 2, chunk(1, "late")).unwrap();
1197 assert_eq!(
1198 s.process_frame(t0, 3, close(None)).unwrap(),
1199 FrameOutcome::None
1200 );
1201
1202 assert_eq!(s.tick(t0 + Duration::from_millis(5)), KeepaliveAction::None);
1204 assert_eq!(
1205 s.tick(t0 + Duration::from_millis(10)),
1206 KeepaliveAction::Abort("Close grace period expired".to_string())
1207 );
1208 match s.next().await {
1209 Some(Err(OpenStreamError::Abort { reason, .. })) => {
1210 assert_eq!(reason.as_deref(), Some("Close grace period expired"));
1211 }
1212 other => panic!("expected grace abort, got {other:?}"),
1213 }
1214 }
1215
1216 #[tokio::test]
1217 async fn close_with_missing_tail_and_nothing_buffered_fails_immediately_without_grace() {
1218 let t0 = Instant::now();
1219 let s = make_session_timers("token-no-grace", 100, 100, 10);
1220 s.process_frame(t0, 1, start()).unwrap();
1221 s.process_frame(t0, 2, chunk(0, "hello")).unwrap();
1222 assert!(matches!(
1225 s.process_frame(t0, 3, close(Some(1))).unwrap_err(),
1226 OpenStreamError::Sequence(_)
1227 ));
1228 assert_eq!(s.tick(t0 + Duration::from_secs(60)), KeepaliveAction::None);
1230 }
1231}