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 {...@@ -284,7 +284,7 @@ impl Background {
284 news |= match outcome {284 news |= match outcome {
285 Ok((stamp, moved)) => {285 Ok((stamp, moved)) => {
286 watch.stamp = Some(stamp);286 watch.stamp = Some(stamp);
287 watch.due = now + interval;287 watch.due = watch.due.min(now + interval);
288 if moved {288 if moved {
289 changed.push(path);289 changed.push(path);
290 }290 }
...@@ -299,7 +299,7 @@ impl Background {...@@ -299,7 +299,7 @@ impl Background {
299 moved || before != summary(&watch.status)299 moved || before != summary(&watch.status)
300 }300 }
301 Err(error) => {301 Err(error) => {
302 watch.due = now + RETRY;302 watch.due = watch.due.min(now + RETRY);
303 watch.fail(&error, queued)303 watch.fail(&error, queued)
304 }304 }
305 };305 };
...@@ -846,6 +846,8 @@ impl Watched {...@@ -846,6 +846,8 @@ impl Watched {
846 watch.due = now + interval;846 watch.due = now + interval;
847 continue;847 continue;
848 }848 }
849 // Anything asking for the section during the check can only bring this forward.
850 watch.due = now + interval.max(RETRY);
849 return Next::Check {851 return Next::Check {
850 path: watch.path.clone(),852 path: watch.path.clone(),
851 replica: watch.replica.clone(),853 replica: watch.replica.clone(),
...@@ -1200,7 +1202,7 @@ mod tests {...@@ -1200,7 +1202,7 @@ mod tests {
1200 .find(|watch| watch.path == path)1202 .find(|watch| watch.path == path)
1201 .unwrap();1203 .unwrap();
1202 watch.current = false;1204 watch.current = false;
1203 watch.due = now + interval;1205 watch.due = watch.due.min(now + interval);
1204 checks.push((now, path, !current));1206 checks.push((now, path, !current));
1205 }1207 }
1206 }1208 }
...@@ -1468,6 +1470,45 @@ mod tests {...@@ -1468,6 +1470,45 @@ mod tests {
1468 drop(rewatch);1470 drop(rewatch);
1469 }1471 }
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
1471 #[test]1512 #[test]
1472 fn held_status_tracks_its_worker_without_retaining_the_replica() {1513 fn held_status_tracks_its_worker_without_retaining_the_replica() {
1473 use crate::session::SyncState;1514 use crate::session::SyncState;