authorclover caruso <git@paperclover.net> 2026-10-15 04:41:27-07:00
committerclover caruso <git@paperclover.net> 2026-10-15 04:52:26-07:00
logb6dd7686aeeb8a0ed33c0ec5f614550446b6ec68
treef12cf2316b7537b83bf542b88e1119e8c47f96d7
parentd87c4dd12ddaf3a9db36d09aa9d36d022f007734
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix(notebook): a section left during its background check publishes at once

When a session lets go of a section, the background schedules a check of it at once so that an edit still waiting publishes. If a check of that section was already in flight, its end set the next check an interval away, an hour where a watch reports the folder, overwriting the release; a change reported or a Sync Now during a check was lost the same way. A check now schedules its successor as it starts, and its end only brings that forward, so whatever asks for the section meanwhile keeps its place. Fixes #146 Assisted-by: claude-opus-5.5

1 files changed, 44 insertions(+), 3 deletions(-)

crates/notebook/src/background.rs+44-3
......@@ -284,7 +284,7 @@ impl Background {
284284 news |= match outcome {
285285 Ok((stamp, moved)) => {
286286 watch.stamp = Some(stamp);
287 watch.due = now + interval;
287 watch.due = watch.due.min(now + interval);
288288 if moved {
289289 changed.push(path);
290290 }
......@@ -299,7 +299,7 @@ impl Background {
299299 moved || before != summary(&watch.status)
300300 }
301301 Err(error) => {
302 watch.due = now + RETRY;
302 watch.due = watch.due.min(now + RETRY);
303303 watch.fail(&error, queued)
304304 }
305305 };
......@@ -846,6 +846,8 @@ impl Watched {
846846 watch.due = now + interval;
847847 continue;
848848 }
849 // Anything asking for the section during the check can only bring this forward.
850 watch.due = now + interval.max(RETRY);
849851 return Next::Check {
850852 path: watch.path.clone(),
851853 replica: watch.replica.clone(),
......@@ -1200,7 +1202,7 @@ mod tests {
12001202 .find(|watch| watch.path == path)
12011203 .unwrap();
12021204 watch.current = false;
1203 watch.due = now + interval;
1205 watch.due = watch.due.min(now + interval);
12041206 checks.push((now, path, !current));
12051207 }
12061208 }
......@@ -1468,6 +1470,45 @@ mod tests {
14681470 drop(rewatch);
14691471 }
14701472
1473 #[test]
1474 fn a_section_released_during_its_check_is_checked_again_at_once() {
1475 let (sender, stamps) = mpsc::channel();
1476 let files = Files {
1477 images: Arc::default(),
1478 stamps: sender,
1479 };
1480 let section = path(0);
1481 files.write(
1482 &section,
1483 onestore::create_section("s.one", &section, "Author").unwrap(),
1484 );
1485 let (armed, watches) = mpsc::channel();
1486 let remote = files.clone();
1487 let background = Background::start(
1488 false,
1489 move |reports| {
1490 armed.send(reports).unwrap();
1491 let (files, listed) = (remote.clone(), remote.clone());
1492 let bind = move |path: &str| File(files.clone(), path.to_owned());
1493 let list = move |folder: String| std::future::ready(Ok(listed.list(&folder)));
1494 Ok(((bind, list), true))
1495 },
1496 || {},
1497 )
1498 .unwrap();
1499 background.watch(vec![files.known(&section)]);
1500 let watch = watches.recv().unwrap();
1501 assert!(quiet(&stamps, SETTLE).is_empty());
1502 // Holding the files parks the check reported next inside its stamp read.
1503 let parked = files.images.lock().unwrap();
1504 watch.touched(std::slice::from_ref(&section));
1505 assert_eq!(stamps.recv_timeout(SETTLE * 2).unwrap(), section);
1506 background.0.released(&section);
1507 drop(parked);
1508 assert_eq!(stamps.recv_timeout(SETTLE).unwrap(), section);
1509 drop(background);
1510 }
1511
14711512 #[test]
14721513 fn held_status_tracks_its_worker_without_retaining_the_replica() {
14731514 use crate::session::SyncState;