feat: note how far each account's sync caught up

This commit is contained in:
Tomás Romero committed 2026-11-09 19:38:33 +01:00
1 parent ce44504ba9
commit a38c77eb06
7 files changed
+49 -5

No files matched your search

@@ -48,6 +48,7 @@ const MIGRATIONS: &[Migration] = &[
Migration::Plain(include_str!("migrations/029_mail_transfers.sql")),
Migration::Plain(include_str!("migrations/030_folder_server_only_unseen.sql")),
Migration::Plain(include_str!("migrations/031_notification_tracking.sql")),
Migration::Plain(include_str!("migrations/032_account_caught_up_at.sql")),
];
pub const LATEST_VERSION: i64 = MIGRATIONS.len() as i64;
@@ -0,0 +1,5 @@
-- When the account's last complete sync started: everything its server had
-- received by then is stored here. Mail that arrives after it, while no copy
-- of the app is following the account, is what a check in the background
-- tells the user about.
ALTER TABLE accounts ADD COLUMN caught_up_at INTEGER;
@@ -555,6 +555,16 @@ pub fn update_sync_state_with_error(
)? != 0)
}
/// Note that a sync of the account that started at `started` stored all the
/// mail its Inbox had by then.
pub fn set_caught_up(conn: &Connection, id: i64, started: i64) -> Result<()> {
conn.execute(
"UPDATE accounts SET caught_up_at = ?2 WHERE id = ?1",
params![id, started],
)?;
Ok(())
}
/// Move one account before or after another and compact the persisted order.
/// A complete rewrite keeps ordering deterministic after any number of moves
/// and avoids fragile fractional positions.
+4 -1
View File
@@ -8,7 +8,8 @@ use crate::events::CoreEvent;
use crate::jmap::client::{self, ConnectedClient};
use crate::models::{AccountConfig, Address, MailProtocol, SenderDeliveryStatus, now_ms, roles};
use crate::sync::engine::{
PriorityFetchCmd, SyncCmd, SyncCtx, configured_sync_interval, set_state, set_state_error,
PriorityFetchCmd, SyncCmd, SyncCtx, caught_up, configured_sync_interval, set_state,
set_state_error,
};
use futures::{FutureExt, Stream, StreamExt};
use jmap_client::core::error::MethodErrorType;
@@ -122,9 +123,11 @@ async fn actor(
),
}
}
let started = now_ms();
set_state(&ctx, config.id, "syncing").await;
sync_mailboxes(&ctx, config.id, &connected).await?;
sync_emails(&ctx, &config, &connected, live_since).await?;
caught_up(&ctx, config.id, started).await;
execute_actions(&ctx, &config, &connected).await?;
// The counts only explain the badges, so they never fail a sync.
if let Err(error) = sync_mailbox_counts(&ctx, config.id, &connected).await {
@@ -1280,6 +1280,20 @@ pub(crate) async fn set_state_error(ctx: &SyncCtx, account_id: i64, state: &str,
set_state_inner(ctx, account_id, state, Some(error)).await;
}
/// Note that a sync of the account that started at `started` stored all the
/// mail its Inbox had by then. A check in the background tells of what
/// arrives after it while the app isn't running (see
/// `Core::announce_missed`).
pub(crate) async fn caught_up(ctx: &SyncCtx, account_id: i64, started: i64) {
let noted = ctx
.db
.write(move |conn| repo::accounts::set_caught_up(conn, account_id, started))
.await;
if let Err(error) = noted {
tracing::warn!(account_id, %error, "could not note how far a sync caught up");
}
}
async fn set_state_inner(ctx: &SyncCtx, account_id: i64, state: &str, error: Option<&str>) {
let st = state.to_string();
let error = error.map(str::to_owned);
@@ -1469,6 +1483,7 @@ async fn run_cycle(
inbox_pushed,
} = mode;
let account_id = config.id;
let started = now_ms();
// The folder list: first thing, then once it is FOLDER_LIST_INTERVAL old.
let listed_now = !immediate_follow_up && listing.due(tokio::time::Instant::now());
@@ -1545,6 +1560,9 @@ async fn run_cycle(
inbox_baseline,
)
.await?;
if !inbox_forward_remaining {
caught_up(ctx, account_id, started).await;
}
}
// A manual Sync Now resolves at the foreground Inbox boundary. Finish any
+7 -2
View File
@@ -7,8 +7,8 @@
//! pending-action queue remain provider-neutral and fully offline-first.
use super::engine::{
PriorityFetchCmd, SyncCmd, SyncCtx, configured_sync_interval, emit_sync_status, set_state,
set_state_error,
PriorityFetchCmd, SyncCmd, SyncCtx, caught_up, configured_sync_interval, emit_sync_status,
set_state, set_state_error,
};
use crate::db::repo::{self, messages::NewMessage};
use crate::error::{CoreError, Result};
@@ -2556,6 +2556,7 @@ async fn sync_once(
live_since: i64,
) -> Result<bool> {
let account_id = config.id;
let started = now_ms();
let mut state = ctx
.db
.read(move |conn| repo::gmail::state(conn, account_id))
@@ -2764,6 +2765,10 @@ async fn sync_once(
if page.next_page_token.is_none() && !page.affected_threads.is_empty() {
sync_draft_map(ctx, config, api).await?;
}
if page.next_page_token.is_none() {
// The history ran out: everything up to this pass is stored.
caught_up(ctx, account_id, started).await;
}
Ok(true)
}
+4 -2
View File
@@ -10,8 +10,8 @@
//! was.
use super::engine::{
PriorityFetchCmd, RestoreRequest, SyncCmd, SyncCtx, configured_sync_interval, set_state,
set_state_error,
PriorityFetchCmd, RestoreRequest, SyncCmd, SyncCtx, caught_up, configured_sync_interval,
set_state, set_state_error,
};
use super::ingest::{self, Delivery, Importer};
use crate::accounts::credentials::{self, Slot};
@@ -75,6 +75,7 @@ pub(crate) async fn run(ctx: SyncCtx, mut config: AccountConfig, mut inputs: Inp
tracing::warn!(account_id, %error, "completing local POP3 actions failed");
}
if next_sync.is_some_and(|at| at <= Instant::now()) {
let started = now_ms();
next_sync = match sync(&ctx, &config, &mut catch_up).await {
Ok(synced) => {
backoff = Duration::from_secs(1);
@@ -83,6 +84,7 @@ pub(crate) async fn run(ctx: SyncCtx, mut config: AccountConfig, mut inputs: Inp
ctx.bus.emit(CoreEvent::NetworkState { online: true });
let next = match synced {
Synced::Complete => {
caught_up(&ctx, account_id, started).await;
finish(&mut waiters, Ok(()));
Instant::now() + configured_sync_interval(&ctx.db).await
}