feat: list the store live while a fetch runs

This commit is contained in:
randogoth 2026-10-02 00:00:41 +03:00
parent 50299547e5
commit 7ed2889966
3 changed files with 67 additions and 2 deletions

View file

@ -10,9 +10,10 @@ use std::sync::Arc;
use fumi_core::address::Address;
use fumi_core::crypto::{unb32, KEY_LEN};
use fumi_core::store::Store;
use iced::time::{self, Duration};
use iced::widget::qr_code;
use iced::widget::text_editor::{self, Action};
use iced::{clipboard, Task};
use iced::{clipboard, Subscription, Task};
use crate::ops::{self, Folder, OpenedMsg, Row, Snapshot};
@ -136,6 +137,9 @@ pub enum Message {
MessageSelected(Folder, String),
FetchPressed,
FetchCancelled,
/// A live re-list while a fetch runs: the store commits each envelope
/// as it verifies, so the list can show mail arriving mid-fetch.
FetchTick,
ComposePressed,
ComposeToEdited(String),
ComposeSubjectEdited(String),
@ -369,6 +373,16 @@ impl Carrier {
self.store.account().ok().flatten()
}
/// While a fetch runs, tick a few times a second so the list shows mail
/// as it lands instead of freezing until the whole fetch returns.
pub fn subscription(&self) -> Subscription<Message> {
if self.busy.as_deref() == Some("fetching") {
time::every(Duration::from_millis(250)).map(|_| Message::FetchTick)
} else {
Subscription::none()
}
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
// Text editor actions must survive the busy guard: typing in the
@ -389,6 +403,17 @@ impl Carrier {
}
Task::none()
}
Message::FetchTick => {
if self.busy.as_deref() != Some("fetching") {
return Task::none();
}
let master = self.master();
let read = self.marks.snapshot();
if let Ok(lists) = ops::lists(&self.store, &master, &read) {
self.lists = lists;
}
Task::none()
}
// Results pass regardless of the busy flag: each one clears the
// operation it belongs to, so swallowing it would deadlock.
Message::PinOffered(result) => {
@ -974,3 +999,39 @@ mod tests {
assert_eq!(app.busy.as_deref(), Some("fetching"));
}
}
#[cfg(test)]
mod tick_tests {
use super::*;
use fumi_core::message::{build_frontmatter, message_id, seal};
/// The store is committed envelope by envelope, so a tick mid-fetch
/// sees a message that the running fetch has already verified.
#[test]
fn fetch_tick_lists_mail_as_it_lands() {
let dir = tempfile::tempdir().unwrap().keep();
let (mut app, _task) = Carrier::boot(dir.clone());
let master = ops::new_master();
app.master = Some(master);
let me = ops::account(&app.store, &master).unwrap();
let addr = fumi_core::address::Address::parse("me@localhost").unwrap();
app.store.set_account(&addr).unwrap();
let body = build_frontmatter(&[("Subject", "live")], "tick");
let envelope = seal(me.me(), &me.me().pk(), body.as_bytes(), 1000, true).unwrap();
let id = message_id(&envelope);
app.store
.store_inbox(&id, &envelope, 1000, false, false)
.unwrap();
app.busy = Some("fetching".into());
let _ = app.update(Message::FetchTick);
assert_eq!(app.busy.as_deref(), Some("fetching"));
assert_eq!(app.lists[0].len(), 1);
assert_eq!(app.lists[0][0].subject, "live");
// A tick outside a fetch is a no-op.
app.busy = None;
let _ = app.update(Message::FetchTick);
}
}