ElyMark: renders asked of a failed page end at once (M-08)

The tally remembers why the page went down; until a load succeeds,
render() ends each new request with that reason instead of calling a
dead page. end_renders is a free function with a gpui test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 11:59:47 -04:00
1 parent 2c19d027a6
commit 22d2d04d1e
8 files changed
+98 -35

No files matched your search

-2
View File
@@ -201,8 +201,6 @@ fn clear(dir: &Path) -> io::Result<()> {
}
}
/// What tab `id` restores: by file, or untitled by number.
impl Desk {
/// A tab just shown takes its kept mode, place, text.
pub fn restore_shown(&mut self, id: u64, cx: &mut Context<Self>) {
+43 -10
View File
@@ -11,11 +11,13 @@ pub struct Tally<R> {
reported: u64,
renders: HashMap<u64, R>,
asked: u64,
/// Why the page is down, until it loads again.
down: Option<String>,
}
impl<R> Tally<R> {
pub fn new() -> Self {
Tally { load: 0, changed: 0, reported: 0, renders: HashMap::new(), asked: 0 }
Tally { load: 0, changed: 0, reported: 0, renders: HashMap::new(), asked: 0, down: None }
}
pub fn load(&self) -> u64 {
@@ -38,8 +40,19 @@ impl<R> Tally<R> {
self.lapse()
}
/// The renders owed, as the page fails or restarts.
pub fn lapse(&mut self) -> Vec<R> {
/// A page loaded: it answers again.
pub fn loaded(&mut self) -> Vec<R> {
self.down = None;
self.restart()
}
/// The page failed: owed and later renders end with `why`.
pub fn fail(&mut self, why: String) -> Vec<R> {
self.down = Some(why);
self.lapse()
}
fn lapse(&mut self) -> Vec<R> {
self.renders.drain().map(|(_, render)| render).collect()
}
@@ -62,10 +75,13 @@ impl<R> Tally<R> {
}
/// Owes `render`; its number goes to the page.
pub fn ask(&mut self, render: R) -> u64 {
pub fn ask(&mut self, render: R) -> Result<u64, (R, String)> {
if let Some(why) = &self.down {
return Err((render, why.clone()));
}
self.asked += 1;
self.renders.insert(self.asked, render);
self.asked
Ok(self.asked)
}
pub fn answered(&mut self, ask: u64) -> Option<R> {
@@ -99,12 +115,29 @@ mod tests {
}
#[test]
fn a_restart_ends_every_owed_render() {
fn a_load_or_failure_ends_every_owed_render() {
let mut tally = Tally::new();
let ask = tally.ask("export");
assert_eq!(tally.restart(), vec!["export"]);
// Its late answer finds nobody waiting.
assert_eq!(tally.answered(ask), None);
let (html, text) = (tally.ask("html").expect("up"), tally.ask("text").expect("up"));
let mut owed = tally.loaded();
owed.sort();
assert_eq!(owed, ["html", "text"]);
// Late answers find nobody waiting.
assert_eq!((tally.answered(html), tally.answered(text)), (None, None));
let pdf = tally.ask("pdf").expect("up");
assert_eq!(tally.fail("denied".into()), ["pdf"]);
assert_eq!(tally.answered(pdf), None);
}
#[test]
fn a_failed_page_refuses_renders_until_it_loads() {
let mut tally = Tally::new();
tally.fail("denied".into());
assert_eq!(tally.ask("html"), Err(("html", "denied".to_string())));
// A refresh is no load: the page is still down.
assert!(tally.restart().is_empty());
assert_eq!(tally.ask("text"), Err(("text", "denied".to_string())));
assert!(tally.loaded().is_empty());
assert!(tally.ask("pdf").is_ok());
}
#[test]
+29 -22
View File
@@ -37,6 +37,20 @@ pub const LARGE: usize = 2 * 1024 * 1024;
/// A render's answer: the page's reply, or why not.
type Rendered = Box<dyn FnOnce(Result<Value, String>, &mut gpui::App)>;
/// Renders the page will never answer end with `why`.
fn end_renders(pending: Vec<Rendered>, why: &str, cx: &mut gpui::App) {
if pending.is_empty() {
return;
}
log::warn!("mark: {} renders end: {why}", pending.len());
let why = why.to_string();
cx.defer(move |cx| {
for done in pending {
done(Err(why.clone()), cx);
}
});
}
/// The page failed; the shell shows why.
pub struct Failed(pub String);
@@ -186,7 +200,7 @@ impl MarkView {
self.saved = mark(&text);
// Reports and renders from before belong to the old text.
let lapsed = self.tally.restart();
self.end_renders(lapsed, "the text was replaced from disk", cx);
end_renders(lapsed, "the text was replaced from disk", cx);
crate::page::refresh(self.web.read(cx), &text, self.tally.load());
cx.emit(Reported);
}
@@ -209,9 +223,13 @@ impl MarkView {
done: impl FnOnce(Result<Value, String>, &mut gpui::App) + 'static,
cx: &mut Context<Self>,
) {
let ask = self.tally.ask(Box::new(done));
let ask = serde_json::json!({ "kind": kind, "ask": ask });
crate::page::call(self.web.read(cx), "render", &ask);
match self.tally.ask(Box::new(done)) {
Ok(ask) => {
let ask = serde_json::json!({ "kind": kind, "ask": ask });
crate::page::call(self.web.read(cx), "render", &ask);
}
Err((done, why)) => end_renders(vec![done], &why, cx),
}
}
/// `text` replaces the editor's, as one undoable edit.
@@ -257,8 +275,8 @@ impl MarkView {
match event {
// A reload shows the last text that arrived.
Event::Loaded => {
let lapsed = self.tally.restart();
self.end_renders(lapsed, "the page reloaded", cx);
let lapsed = self.tally.loaded();
end_renders(lapsed, "the page reloaded", cx);
self.shown = false;
let text = self.doc.lock().expect("the text lock").current.clone();
let at = (self.mode, self.tally.load(), self.place);
@@ -268,8 +286,8 @@ impl MarkView {
Event::Query(said, reply) => self.query(said, reply, cx),
Event::Leave(url) => self.follow(url, cx),
Event::Failed(why) => {
let lapsed = self.tally.lapse();
self.end_renders(lapsed, why, cx);
let lapsed = self.tally.fail(why.clone());
end_renders(lapsed, why, cx);
cx.emit(Failed(why.clone()));
}
Event::Printed(path, _) => {
@@ -278,20 +296,6 @@ impl MarkView {
}
}
/// Renders the old page will never answer end with `why`.
fn end_renders(&mut self, pending: Vec<Rendered>, why: &str, cx: &mut Context<Self>) {
if pending.is_empty() {
return;
}
log::warn!("mark: {} renders lost with the page", pending.len());
let why = why.to_string();
cx.defer(move |cx| {
for done in pending {
done(Err(why.clone()), cx);
}
});
}
/// A clicked link: web and mail open outside.
fn follow(&mut self, url: &str, cx: &mut Context<Self>) {
if ely_office::outward(url) {
@@ -486,3 +490,6 @@ impl Render for MarkView {
div().size_full().on_drop(drop).child(self.web.clone())
}
}
#[cfg(test)]
mod tests;
+18
View File
@@ -0,0 +1,18 @@
use std::{cell::RefCell, rc::Rc};
use serde_json::Value;
#[gpui::test]
fn each_owed_render_ends_once_with_the_reason(cx: &mut gpui::TestAppContext) {
let got = Rc::new(RefCell::new(Vec::<(u8, Result<Value, String>)>::new()));
let pending = (0..2)
.map(|n| {
let got = got.clone();
Box::new(move |rendered, _: &mut gpui::App| got.borrow_mut().push((n, rendered))) as super::Rendered
})
.collect();
cx.update(|cx| super::end_renders(pending, "denied", cx));
cx.run_until_parked();
let denied = || Err("denied".to_string());
assert_eq!(*got.borrow(), [(0, denied()), (1, denied())]);
}