perf(ui): stream task pages and lazy-load truncated email bodies
Close the two ultra-fuzz Run #26 Performance items (silent truncation):
- Task list no longer caps at 500 rows. VirtualScroller gains an onNeedMore
infinite-scroll hook; tasks.js streams 200-row pages via the existing
list_tasks_filtered offset/limit. Count chip reads "X of N" until fully
loaded.
- JMAP bodies over 100KB are no longer silently truncated. Migration 052 adds
jmap_id + body_truncated; sync captures BodyValue.isTruncated; a new
fetch_email_full_body command (shared all_commands! macro) re-fetches the
full body on demand, surfaced via a "Load full message" affordance in the
reader. JMAP client construction extracted to a shared build_jmap_client.
Gate: cargo test --workspace green, node --check clean on touched JS.
- Co-Authored-By
- Claude Opus 4.8 <noreply@anthropic.com>
15 files changed,
+307 insertions,
-45 deletions
| 110 |
110 |
|
$crate::commands::list_emails,
|
| 111 |
111 |
|
$crate::commands::list_emails_threaded,
|
| 112 |
112 |
|
$crate::commands::get_email,
|
|
113 |
+ |
$crate::commands::fetch_email_full_body,
|
| 113 |
114 |
|
$crate::commands::open_email_in_browser,
|
| 114 |
115 |
|
$crate::commands::create_email,
|
| 115 |
116 |
|
$crate::commands::send_email,
|
| 32 |
32 |
|
pub imap_uid: Option<i64>,
|
| 33 |
33 |
|
pub is_archived: bool,
|
| 34 |
34 |
|
pub attachment_meta: Option<String>,
|
|
35 |
+ |
/// Whether the body was truncated at fetch (JMAP >100KB cap).
|
|
36 |
+ |
pub body_truncated: bool,
|
|
37 |
+ |
/// Provider email id (JMAP), used to re-fetch a truncated body. `None` for IMAP.
|
|
38 |
+ |
pub jmap_id: Option<String>,
|
| 35 |
39 |
|
}
|
| 36 |
40 |
|
|
| 37 |
41 |
|
/// Result of processing a batch of fetched emails.
|
| 126 |
130 |
|
email_account_id: Some(account_id),
|
| 127 |
131 |
|
is_outgoing: false,
|
| 128 |
132 |
|
attachment_meta: email.attachment_meta,
|
|
133 |
+ |
body_truncated: email.body_truncated,
|
|
134 |
+ |
jmap_id: email.jmap_id,
|
| 129 |
135 |
|
});
|
| 130 |
136 |
|
}
|
| 131 |
137 |
|
|
| 173 |
179 |
|
imap_uid: Some(42),
|
| 174 |
180 |
|
is_archived: false,
|
| 175 |
181 |
|
attachment_meta: None,
|
|
182 |
+ |
body_truncated: false,
|
|
183 |
+ |
jmap_id: None,
|
| 176 |
184 |
|
};
|
| 177 |
185 |
|
assert_eq!(email.from, "sender@example.com");
|
| 178 |
186 |
|
assert_eq!(email.imap_uid, Some(42));
|
| 337 |
337 |
|
/// Retrieves an email by ID.
|
| 338 |
338 |
|
async fn get_by_id(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
|
| 339 |
339 |
|
|
|
340 |
+ |
/// Replaces an email's body with a full (re-fetched) version and clears the
|
|
341 |
+ |
/// `body_truncated` flag. Used to lazily load a body truncated at sync.
|
|
342 |
+ |
async fn set_full_body(&self, id: EmailId, user_id: UserId, body: &str) -> Result<()>;
|
|
343 |
+ |
|
| 340 |
344 |
|
/// Creates a new email record.
|
| 341 |
345 |
|
async fn create(&self, user_id: UserId, email: NewEmail) -> Result<Email>;
|
| 342 |
346 |
|
|
| 47 |
47 |
|
sqlx::query("PRAGMA foreign_keys = ON")
|
| 48 |
48 |
|
.execute(pool)
|
| 49 |
49 |
|
.await
|
| 50 |
|
- |
.map_err(|e| sqlx::migrate::MigrateError::Execute(e))?;
|
|
50 |
+ |
.map_err(sqlx::migrate::MigrateError::Execute)?;
|
| 51 |
51 |
|
|
| 52 |
52 |
|
Ok(())
|
| 53 |
53 |
|
}
|