Skip to main content

mcpls_core/lsp/
publish_mailbox.rs

1//! Delivery of `textDocument/publishDiagnostics` from a server's message loop
2//! to the bridge.
3//!
4//! A server may publish for many files in one burst, faster than the bridge
5//! drains them. A bounded channel would drop the tail silently, so
6//! diagnostics travel through a per-server mailbox instead: one pending
7//! publish per file (a later publish supersedes an earlier one), bounded in
8//! files and in bytes. A publish the mailbox cannot hold is not discarded
9//! quietly: its file is recorded as lost, and the reader hands the lost files
10//! over so the cache can say so ([`PublishDelivery::Lost`],
11//! [`PublishDelivery::LostUnnamed`]).
12
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::sync::{Arc, Mutex as StdMutex};
15use std::time::{Duration, Instant};
16
17use lsp_types::{LogMessageParams, PublishDiagnosticsParams, ShowMessageParams, Uri};
18use tokio::sync::{Notify, mpsc};
19use tracing::{debug, warn};
20
21use crate::bridge::{BoundedDiagnostics, DiagnosticsKey, MAX_DIAGNOSTIC_ENTRIES};
22use crate::util::{WarnLimiter, lock_std};
23
24/// Most files with an undelivered publish; as many as the cache holds, so the
25/// mailbox is never the narrower bound.
26pub const MAX_PENDING_PUBLISHES: usize = MAX_DIAGNOSTIC_ENTRIES;
27
28/// Most bytes of undelivered diagnostics per server.
29pub const MAILBOX_MAX_BYTES: usize = 64 * 1024 * 1024;
30
31/// Most lost files remembered per server before only the fact of a loss is.
32const MAX_LOST_FILES: usize = MAX_DIAGNOSTIC_ENTRIES;
33
34/// Period of the dropped-frame warning of one lane.
35pub const DROP_WARN_PERIOD: Duration = WarnLimiter::DEFAULT_PERIOD;
36
37/// What the mailbox holds at most.
38#[derive(Debug, Clone, Copy)]
39pub struct MailboxLimits {
40    /// Most files with an undelivered publish.
41    pub max_pending: usize,
42    /// Most bytes of undelivered diagnostics.
43    pub max_bytes: usize,
44}
45
46impl Default for MailboxLimits {
47    fn default() -> Self {
48        Self {
49            max_pending: MAX_PENDING_PUBLISHES,
50            max_bytes: MAILBOX_MAX_BYTES,
51        }
52    }
53}
54
55/// Counts dropped frames and says when one is worth a warning: at most once
56/// per [`DROP_WARN_PERIOD`], so a flood cannot flood the log.
57#[derive(Debug, Default)]
58pub struct DropCounter {
59    warn: WarnLimiter,
60    suppressed: usize,
61}
62
63impl DropCounter {
64    /// A counter whose first drop warns at once.
65    pub const fn new() -> Self {
66        Self {
67            warn: WarnLimiter::new(),
68            suppressed: 0,
69        }
70    }
71
72    /// Records one drop at `now`. `Some(n)` when a warning is due, with `n` the
73    /// drops silenced since the previous warning; the caller logs it once it
74    /// holds no lock.
75    pub fn record(&mut self, now: Instant) -> Option<usize> {
76        if self.warn.due(now, DROP_WARN_PERIOD) {
77            Some(std::mem::take(&mut self.suppressed))
78        } else {
79            self.suppressed = self.suppressed.saturating_add(1);
80            None
81        }
82    }
83}
84
85/// A `publishDiagnostics` whose diagnostics are already bounded the way the
86/// cache stores them, so no later stage bounds them again.
87#[derive(Debug)]
88pub struct BoundedPublish {
89    uri: Uri,
90    version: Option<i32>,
91    diagnostics: BoundedDiagnostics,
92}
93
94impl BoundedPublish {
95    fn new(params: PublishDiagnosticsParams) -> (Self, usize) {
96        let PublishDiagnosticsParams {
97            uri,
98            diagnostics,
99            version,
100        } = params;
101        let diagnostics = BoundedDiagnostics::new(&uri, diagnostics);
102        let bytes = diagnostics.buffered_bytes();
103        (
104            Self {
105                uri,
106                version,
107                diagnostics,
108            },
109            bytes,
110        )
111    }
112
113    /// The file the server published for, exactly as it spelled it.
114    #[must_use]
115    pub const fn uri(&self) -> &Uri {
116        &self.uri
117    }
118
119    /// The document version the diagnostics are for, if the server said.
120    #[must_use]
121    pub const fn version(&self) -> Option<i32> {
122        self.version
123    }
124
125    /// The bounded diagnostics.
126    #[must_use]
127    pub const fn diagnostics(&self) -> &BoundedDiagnostics {
128        &self.diagnostics
129    }
130
131    /// The URI, version and bounded diagnostics.
132    #[must_use]
133    pub fn into_parts(self) -> (Uri, Option<i32>, BoundedDiagnostics) {
134        (self.uri, self.version, self.diagnostics)
135    }
136}
137
138/// A non-empty list of files whose publish the mailbox could not deliver,
139/// oldest loss first.
140#[derive(Debug, PartialEq, Eq)]
141pub struct LostFiles(Vec<Uri>);
142
143impl LostFiles {
144    fn new(uris: Vec<Uri>) -> Option<Self> {
145        (!uris.is_empty()).then_some(Self(uris))
146    }
147
148    /// The lost files.
149    #[must_use]
150    pub fn uris(&self) -> &[Uri] {
151        &self.0
152    }
153}
154
155/// One thing the reader takes out of the mailbox.
156#[derive(Debug)]
157pub enum PublishDelivery {
158    /// The latest pending publish of a file.
159    Publish(BoundedPublish),
160    /// Named files whose publish never made it into the mailbox.
161    Lost(LostFiles),
162    /// More files were lost than the mailbox could name. Handed over on its
163    /// own, after the pending publishes.
164    LostUnnamed,
165}
166
167/// A frame of the log and `showMessage` lane.
168#[derive(Debug)]
169pub enum ServerMessage {
170    /// A `window/logMessage`.
171    Log(LogMessageParams),
172    /// A `window/showMessage`.
173    Show(ShowMessageParams),
174}
175
176#[derive(Debug)]
177struct Pending {
178    publish: BoundedPublish,
179    bytes: usize,
180}
181
182#[derive(Debug, Default)]
183struct State {
184    pending: HashMap<DiagnosticsKey, Pending>,
185    /// Arrival order of the keys of `pending`; a key whose publish was
186    /// withdrawn or already taken is skipped.
187    order: VecDeque<DiagnosticsKey>,
188    bytes: usize,
189    lost: Vec<Uri>,
190    lost_keys: HashSet<DiagnosticsKey>,
191    lost_overflowed: bool,
192    drops: DropCounter,
193    writer_gone: bool,
194    reader_gone: bool,
195}
196
197#[derive(Debug)]
198struct Shared {
199    state: StdMutex<State>,
200    wake: Notify,
201    limits: MailboxLimits,
202}
203
204impl State {
205    fn withdraw(&mut self, key: &DiagnosticsKey) {
206        if let Some(pending) = self.pending.remove(key) {
207            self.bytes = self.bytes.saturating_sub(pending.bytes);
208        }
209    }
210
211    /// Drops the keys of `order` that no longer have a pending publish once
212    /// they outnumber what the mailbox can hold twice over, so a stalled
213    /// reader cannot make the queue grow with withdrawn files.
214    fn compact_order(&mut self, max_pending: usize) {
215        if self.order.len() > max_pending.saturating_mul(2).max(16) {
216            let pending = &self.pending;
217            self.order.retain(|key| pending.contains_key(key));
218        }
219    }
220
221    /// The next delivery that is ready: lost files, then pending publishes in
222    /// arrival order, then the unnamed losses.
223    fn next_ready(&mut self) -> Option<PublishDelivery> {
224        if let Some(lost) = self.take_lost_files() {
225            return Some(PublishDelivery::Lost(lost));
226        }
227        while let Some(key) = self.order.pop_front() {
228            if let Some(pending) = self.pending.remove(&key) {
229                self.bytes = self.bytes.saturating_sub(pending.bytes);
230                return Some(PublishDelivery::Publish(pending.publish));
231            }
232        }
233        std::mem::take(&mut self.lost_overflowed).then_some(PublishDelivery::LostUnnamed)
234    }
235
236    fn mark_lost(&mut self, key: DiagnosticsKey, uri: Uri) {
237        if self.lost_keys.contains(&key) {
238            return;
239        }
240        if self.lost.len() >= MAX_LOST_FILES {
241            self.lost_overflowed = true;
242            return;
243        }
244        self.lost_keys.insert(key);
245        self.lost.push(uri);
246    }
247
248    fn take_lost_files(&mut self) -> Option<LostFiles> {
249        let lost = LostFiles::new(std::mem::take(&mut self.lost))?;
250        self.lost_keys.clear();
251        Some(lost)
252    }
253
254    /// Counts one dropped publish; `Some(n)` when a warning is due.
255    fn count_drop(&mut self) -> Option<usize> {
256        self.drops.record(Instant::now())
257    }
258}
259
260/// The message loop's side of the mailbox. Dropping it closes the mailbox, so
261/// the reader ends once what is pending is taken.
262#[derive(Debug)]
263pub struct PublishWriter {
264    shared: Arc<Shared>,
265}
266
267/// The bridge's side of the mailbox. Dropping it discards what is pending and
268/// makes the writer stop buffering.
269#[derive(Debug)]
270pub struct PublishReader {
271    shared: Arc<Shared>,
272}
273
274/// A mailbox bounded by `limits`: the message loop's writer and the bridge's
275/// reader of one server's `publishDiagnostics`.
276pub fn mailbox(limits: MailboxLimits) -> (PublishWriter, PublishReader) {
277    let shared = Arc::new(Shared {
278        state: StdMutex::new(State::default()),
279        wake: Notify::new(),
280        limits,
281    });
282    (
283        PublishWriter {
284            shared: Arc::clone(&shared),
285        },
286        PublishReader { shared },
287    )
288}
289
290impl PublishWriter {
291    /// Buffers `params`, replacing a pending publish of the same file.
292    ///
293    /// The diagnostics are bounded like the cache bounds an entry before they
294    /// are counted. A publish that does not fit is dropped and its file
295    /// recorded as lost; when a replacement does not fit, the older pending
296    /// publish of the file is withdrawn too, so the file never reads as
297    /// published with content older than what the server last said.
298    pub fn publish(&self, params: PublishDiagnosticsParams) {
299        let (publish, bytes) = BoundedPublish::new(params);
300        let key = DiagnosticsKey::of(publish.uri());
301        let limits = self.shared.limits;
302        let mut state = lock_std(&self.shared.state);
303        if state.reader_gone {
304            debug!(
305                "dropping a publish for {}: nobody reads the mailbox",
306                publish.uri().as_ref()
307            );
308            return;
309        }
310        let replaced_bytes = state.pending.get(&key).map(|pending| pending.bytes);
311        let is_new = replaced_bytes.is_none();
312        let fits = state
313            .bytes
314            .saturating_sub(replaced_bytes.unwrap_or(0))
315            .saturating_add(bytes)
316            <= limits.max_bytes
317            && (!is_new || state.pending.len() < limits.max_pending);
318        state.compact_order(limits.max_pending);
319        state.withdraw(&key);
320        let warn_due = if fits {
321            state.bytes = state.bytes.saturating_add(bytes);
322            if is_new {
323                state.order.push_back(key.clone());
324            }
325            state.pending.insert(key, Pending { publish, bytes });
326            None
327        } else {
328            let (uri, ..) = publish.into_parts();
329            state.mark_lost(key, uri);
330            state.count_drop()
331        };
332        drop(state);
333        if let Some(suppressed) = warn_due {
334            warn!(
335                "publishDiagnostics mailbox is full: dropped a publish ({suppressed} others \
336                 since the last warning); the files read as evicted until the server publishes \
337                 again"
338            );
339        }
340        self.shared.wake.notify_one();
341    }
342}
343
344impl Drop for PublishWriter {
345    fn drop(&mut self) {
346        lock_std(&self.shared.state).writer_gone = true;
347        self.shared.wake.notify_one();
348    }
349}
350
351impl PublishReader {
352    /// A reader of a mailbox nothing will ever be written to.
353    #[must_use]
354    pub fn closed() -> Self {
355        mailbox(MailboxLimits::default()).1
356    }
357
358    /// The next delivery if one is ready now, without waiting.
359    pub fn try_recv(&mut self) -> Option<PublishDelivery> {
360        lock_std(&self.shared.state).next_ready()
361    }
362
363    /// The next delivery: lost files first, then pending publishes in arrival
364    /// order; `None` once the writer is gone and nothing is left.
365    pub async fn recv(&mut self) -> Option<PublishDelivery> {
366        loop {
367            {
368                let mut state = lock_std(&self.shared.state);
369                if let Some(delivery) = state.next_ready() {
370                    return Some(delivery);
371                }
372                if state.writer_gone {
373                    return None;
374                }
375            }
376            self.shared.wake.notified().await;
377        }
378    }
379}
380
381impl Drop for PublishReader {
382    fn drop(&mut self) {
383        let mut state = lock_std(&self.shared.state);
384        state.reader_gone = true;
385        state.order.clear();
386        state.bytes = 0;
387        let pending = std::mem::take(&mut state.pending);
388        drop(state);
389        drop(pending);
390    }
391}
392
393/// The message loop's handle for everything a server notifies on the
394/// notification lane: log and `showMessage` frames take a bounded channel and
395/// yield under pressure, diagnostics take the mailbox and are never dropped
396/// unrecorded.
397#[derive(Debug)]
398pub struct NotificationSink {
399    messages: mpsc::Sender<ServerMessage>,
400    publishes: PublishWriter,
401}
402
403impl NotificationSink {
404    /// A sink over `messages` with a mailbox of the default limits.
405    pub fn new(messages: mpsc::Sender<ServerMessage>) -> (Self, PublishReader) {
406        Self::with_limits(messages, MailboxLimits::default())
407    }
408
409    /// As [`Self::new`] with explicit mailbox limits.
410    pub fn with_limits(
411        messages: mpsc::Sender<ServerMessage>,
412        limits: MailboxLimits,
413    ) -> (Self, PublishReader) {
414        let (publishes, reader) = mailbox(limits);
415        (
416            Self {
417                messages,
418                publishes,
419            },
420            reader,
421        )
422    }
423
424    /// The channel log and `showMessage` frames go through.
425    pub const fn messages(&self) -> &mpsc::Sender<ServerMessage> {
426        &self.messages
427    }
428
429    /// The mailbox diagnostics go through.
430    pub const fn publishes(&self) -> &PublishWriter {
431        &self.publishes
432    }
433}
434
435/// Everything a server's notification lane delivers to the bridge: log and
436/// `showMessage` frames from a bounded channel, and the diagnostics mailbox.
437#[derive(Debug)]
438pub struct NotificationInbox {
439    /// The log/showMessage channel.
440    pub messages: mpsc::Receiver<ServerMessage>,
441    /// The diagnostics mailbox.
442    pub publishes: PublishReader,
443}
444
445impl NotificationInbox {
446    /// An inbox over `messages` and `publishes`.
447    #[must_use]
448    pub const fn new(messages: mpsc::Receiver<ServerMessage>, publishes: PublishReader) -> Self {
449        Self {
450            messages,
451            publishes,
452        }
453    }
454}
455
456#[cfg(test)]
457impl From<mpsc::Receiver<ServerMessage>> for NotificationInbox {
458    fn from(messages: mpsc::Receiver<ServerMessage>) -> Self {
459        Self::new(messages, PublishReader::closed())
460    }
461}
462
463/// A notification lane that can drop a frame under pressure.
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465pub enum Lane {
466    /// Log and `showMessage` frames.
467    Notification,
468    /// `$/progress` `begin`/`end` and unrecognized notifications.
469    Lifecycle,
470}
471
472impl Lane {
473    const fn name(self) -> &'static str {
474        match self {
475            Self::Notification => "notification",
476            Self::Lifecycle => "lifecycle",
477        }
478    }
479}
480
481/// Counts the frames one lane dropped and warns about them at most once per
482/// [`DROP_WARN_PERIOD`], naming the lane and the count since the last warning.
483#[derive(Debug)]
484pub struct DropLog {
485    lane: Lane,
486    drops: StdMutex<DropCounter>,
487}
488
489impl DropLog {
490    const fn new(lane: Lane) -> Self {
491        Self {
492            lane,
493            drops: StdMutex::new(DropCounter::new()),
494        }
495    }
496
497    /// The log of `lane`; shared by every server, so a flood on one lane warns
498    /// once a period.
499    pub fn of(lane: Lane) -> &'static Self {
500        static NOTIFICATION: DropLog = DropLog::new(Lane::Notification);
501        static LIFECYCLE: DropLog = DropLog::new(Lane::Lifecycle);
502        match lane {
503            Lane::Notification => &NOTIFICATION,
504            Lane::Lifecycle => &LIFECYCLE,
505        }
506    }
507
508    /// Records one dropped frame of `method`; the frame itself is logged at
509    /// DEBUG only.
510    pub fn record(&self, method: &str) {
511        let lane = self.lane.name();
512        debug!("dropping notification: lane={lane}, method={method}");
513        let warn_due = lock_std(&self.drops).record(Instant::now());
514        if let Some(suppressed) = warn_due {
515            warn!(
516                "dropped a notification on the {lane} lane ({suppressed} others since the last \
517                 warning; channel full or closed)"
518            );
519        }
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use std::assert_matches;
526
527    use lsp_types::Diagnostic;
528
529    use super::*;
530
531    fn uri(n: usize) -> Uri {
532        Uri::from(format!("file:///ws/f{n}.rs"))
533    }
534
535    fn publish_of(n: usize, message: &str) -> PublishDiagnosticsParams {
536        PublishDiagnosticsParams {
537            uri: uri(n),
538            diagnostics: vec![Diagnostic {
539                message: message.to_owned().into(),
540                ..Diagnostic::default()
541            }],
542            version: None,
543        }
544    }
545
546    fn bytes_of(n: usize, message: &str) -> usize {
547        let params = publish_of(n, message);
548        BoundedDiagnostics::new(&params.uri, params.diagnostics).buffered_bytes()
549    }
550
551    fn small(max_pending: usize, max_bytes: usize) -> MailboxLimits {
552        MailboxLimits {
553            max_pending,
554            max_bytes,
555        }
556    }
557
558    async fn next_publish(reader: &mut PublishReader) -> BoundedPublish {
559        match reader.recv().await {
560            Some(PublishDelivery::Publish(publish)) => publish,
561            other => panic!("expected a publish, got {other:?}"),
562        }
563    }
564
565    #[tokio::test]
566    async fn burst_below_the_cap_is_delivered_whole_in_order() {
567        let (writer, mut reader) = mailbox(small(10, usize::MAX));
568        for n in 0..10 {
569            writer.publish(publish_of(n, "e"));
570        }
571        drop(writer);
572
573        for n in 0..10 {
574            assert_eq!(next_publish(&mut reader).await.uri(), &uri(n));
575        }
576        assert!(reader.recv().await.is_none());
577    }
578
579    #[tokio::test]
580    async fn later_publish_replaces_the_pending_one_of_the_same_file() {
581        let (writer, mut reader) = mailbox(small(10, usize::MAX));
582        writer.publish(publish_of(1, "old"));
583        writer.publish(publish_of(1, "new"));
584        drop(writer);
585
586        let delivered = next_publish(&mut reader).await;
587        assert_eq!(delivered.diagnostics().as_slice().len(), 1);
588        assert_eq!(
589            delivered
590                .diagnostics()
591                .as_slice()
592                .first()
593                .map(|d| d.message.clone()),
594            Some("new".to_owned().into())
595        );
596        assert!(reader.recv().await.is_none());
597    }
598
599    #[tokio::test]
600    async fn publish_over_the_file_cap_is_reported_lost_not_dropped_quietly() {
601        let (writer, mut reader) = mailbox(small(2, usize::MAX));
602        for n in 0..4 {
603            writer.publish(publish_of(n, "e"));
604        }
605
606        let Some(PublishDelivery::Lost(lost)) = reader.recv().await else {
607            panic!("the loss must be handed over first");
608        };
609        assert_eq!(lost.uris(), [uri(2), uri(3)]);
610        assert_eq!(next_publish(&mut reader).await.uri(), &uri(0));
611        assert_eq!(next_publish(&mut reader).await.uri(), &uri(1));
612    }
613
614    /// A replacement that does not fit withdraws the older pending publish
615    /// too, so the file reads lost rather than with older content.
616    #[tokio::test]
617    async fn oversized_replacement_withdraws_the_older_publish_and_marks_the_file_lost() {
618        let one = bytes_of(1, "x");
619        let (writer, mut reader) = mailbox(small(10, one + one / 2));
620        writer.publish(publish_of(1, "x"));
621        writer.publish(publish_of(1, &"y".repeat(one * 2)));
622        drop(writer);
623
624        let Some(PublishDelivery::Lost(lost)) = reader.recv().await else {
625            panic!("expected the lost file");
626        };
627        assert_eq!(lost.uris(), [uri(1)]);
628        assert!(reader.recv().await.is_none());
629    }
630
631    /// A stalled reader cannot make the arrival queue grow with withdrawn files.
632    #[tokio::test]
633    async fn arrival_queue_stays_bounded_under_withdrawals() {
634        let one = bytes_of(0, "x");
635        let (writer, _reader) = mailbox(small(4, one * 2));
636        for n in 0..1000 {
637            writer.publish(publish_of(n, "x"));
638            writer.publish(publish_of(n, &"y".repeat(one * 4)));
639        }
640
641        assert!(lock_std(&writer.shared.state).order.len() <= 16);
642    }
643
644    #[tokio::test]
645    async fn replacement_after_order_compaction_is_still_delivered() {
646        let one = bytes_of(0, "x");
647        let (writer, mut reader) = mailbox(small(4, one * 2));
648        for n in 0..16 {
649            writer.publish(publish_of(n, "x"));
650            writer.publish(publish_of(n, &"y".repeat(one * 4)));
651        }
652        writer.publish(publish_of(100, "x"));
653        writer.publish(publish_of(100, "z"));
654        drop(writer);
655
656        let mut delivered = None;
657        while let Some(delivery) = reader.recv().await {
658            if let PublishDelivery::Publish(params) = delivery {
659                delivered = Some(params);
660            }
661        }
662        let delivered = delivered.expect("the replaced publish must reach the reader");
663        assert_eq!(delivered.uri(), &uri(100));
664        assert_eq!(
665            delivered
666                .diagnostics()
667                .as_slice()
668                .first()
669                .map(|d| d.message.clone()),
670            Some("z".to_owned().into())
671        );
672    }
673
674    #[tokio::test]
675    async fn delivered_publish_carries_bounded_diagnostics() {
676        let (writer, mut reader) = mailbox(small(10, usize::MAX));
677        let oversized = 512 * 1024;
678        writer.publish(publish_of(1, &"m".repeat(oversized)));
679        drop(writer);
680
681        let delivered = next_publish(&mut reader).await;
682
683        let message = delivered
684            .diagnostics()
685            .as_slice()
686            .first()
687            .map(|d| d.message.clone());
688        assert_matches!(
689            message,
690            Some(lsp_types::Message::String(text)) if text.len() < oversized
691        );
692    }
693
694    #[test]
695    fn drop_counter_warns_once_per_period_and_counts_the_silenced() {
696        let mut counter = DropCounter::new();
697        let start = Instant::now();
698        let at = |secs| start + Duration::from_secs(secs);
699
700        assert_eq!(counter.record(at(0)), Some(0));
701        assert_eq!(counter.record(at(1)), None);
702        assert_eq!(counter.record(at(2)), None);
703        assert_eq!(counter.record(at(61)), Some(2));
704    }
705
706    #[test]
707    fn each_lane_has_its_own_drop_log() {
708        for lane in [Lane::Notification, Lane::Lifecycle] {
709            assert_eq!(DropLog::of(lane).lane, lane);
710        }
711    }
712
713    #[tokio::test]
714    async fn byte_cap_bounds_what_is_buffered() {
715        let one = bytes_of(0, "e");
716        let (writer, mut reader) = mailbox(small(100, one * 3));
717        for n in 0..10 {
718            writer.publish(publish_of(n, "e"));
719        }
720        drop(writer);
721
722        let mut delivered = 0;
723        let mut lost = 0;
724        while let Some(item) = reader.recv().await {
725            match item {
726                PublishDelivery::Publish(_) => delivered += 1,
727                PublishDelivery::Lost(files) => lost += files.uris().len(),
728                PublishDelivery::LostUnnamed => panic!("no unnamed losses expected"),
729            }
730        }
731        assert_eq!((delivered, lost), (3, 7));
732    }
733
734    #[tokio::test]
735    async fn lost_list_is_capped_and_then_only_the_fact_is_kept() {
736        let (writer, mut reader) = mailbox(small(0, usize::MAX));
737        for n in 0..=MAX_LOST_FILES {
738            writer.publish(publish_of(n, "e"));
739        }
740        drop(writer);
741
742        let Some(PublishDelivery::Lost(lost)) = reader.recv().await else {
743            panic!("expected losses");
744        };
745        assert_eq!(lost.uris().len(), MAX_LOST_FILES);
746        assert_matches!(reader.recv().await, Some(PublishDelivery::LostUnnamed));
747        assert!(reader.recv().await.is_none());
748    }
749
750    #[tokio::test]
751    async fn dropping_the_writer_ends_the_reader_and_dropping_the_reader_stops_buffering() {
752        let (writer, reader) = mailbox(small(10, usize::MAX));
753        writer.publish(publish_of(1, "e"));
754        drop(reader);
755        writer.publish(publish_of(2, "e"));
756        assert_eq!(lock_std(&writer.shared.state).bytes, 0);
757        assert!(lock_std(&writer.shared.state).pending.is_empty());
758
759        let (writer, mut reader) = mailbox(small(10, usize::MAX));
760        drop(writer);
761        assert!(reader.recv().await.is_none());
762    }
763
764    #[tokio::test]
765    async fn a_waiting_reader_wakes_for_a_new_publish() {
766        let (writer, mut reader) = mailbox(small(10, usize::MAX));
767        let waiting = tokio::spawn(async move { next_publish(&mut reader).await.uri });
768        tokio::task::yield_now().await;
769
770        writer.publish(publish_of(7, "e"));
771
772        assert_eq!(waiting.await.unwrap(), uri(7));
773    }
774}