diff --git a/crates/notebook/examples/browser_receipts.rs b/crates/notebook/examples/browser_receipts.rs index 2291a762d3be4b11e5f554214ed677521a64015d..88d4b08d0e0a47b6e3c2da255b295e6613940f15 100644 --- a/crates/notebook/examples/browser_receipts.rs +++ b/crates/notebook/examples/browser_receipts.rs @@ -177,5 +177,74 @@ mod web { }; assert_eq!(reopened.pending().unwrap().len(), 1); assert_eq!(text(&reopened.snapshot().unwrap()).2, "once original"); + drop(reopened); + + phase("renaming past an evicted pending receipt"); + let cache = std::path::Path::new("/receipts/rename-cache"); + let page = onestore::PageCreation::new(None, Some("original"), "Fixture").unwrap(); + let notebook = notebook::session::Notebook::create( + "/receipts/Before", + cache, + notebook::session::Notebook::NEW_COLOR, + &page, + ) + .unwrap(); + let original_cache = notebook.replica_path("New Section 1.one").unwrap(); + let section = notebook.section("New Section 1.one", || {}).unwrap(); + section.set_offline(true); + let (space, text_id, _) = text(§ion.replica().snapshot().unwrap()); + let replica = Arc::clone(section.replica()); + fail_storage(1); + { + let mut applying = std::pin::pin!(replica.apply_async( + "Fixture", + Edit { + at, + ops: vec![Op::Page { + space, + op: PageOp::Text { + text: text_id, + range: 0..0, + with: "once ".into(), + }, + }], + } + )); + assert!(matches!( + applying + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())), + Poll::Pending + )); + } + section.close().unwrap(); + drop(replica); + next_turn().await; + match notebook.rename_folder("Refused") { + Err(error) => assert!(error.busy()), + Ok(_) => panic!("A notebook moved before its replica closed"), + } + assert!(notebook::fs::metadata("/receipts/Before").is_ok()); + assert!(notebook::fs::metadata("/receipts/Refused").is_err()); + let notebook = notebook::session::Notebook::open("/receipts/Before", cache).unwrap(); + let mut renaming = std::pin::pin!(notebook.rename_folder_async("After")); + assert!(matches!( + renaming + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())), + Poll::Pending + )); + next_turn().await; + assert!(notebook::fs::metadata("/receipts/Before").is_ok()); + assert!(notebook::fs::metadata("/receipts/After").is_err()); + fail_storage(0); + renaming.await.unwrap(); + let notebook = notebook::session::Notebook::open("/receipts/After", cache).unwrap(); + let moved_cache = notebook.replica_path("New Section 1.one").unwrap(); + assert_eq!(original_cache.file_name(), moved_cache.file_name()); + assert!(notebook::fs::metadata(original_cache).is_err()); + let replica = Replica::open(moved_cache).unwrap(); + assert_eq!(replica.pending().unwrap().len(), 1); + assert_eq!(text(&replica.snapshot().unwrap()).2, "once original"); } } diff --git a/crates/notebook/src/background.rs b/crates/notebook/src/background.rs index 105207df8b680e21990c68644c35750b6431ebef..c87b9d23cf803520dd166395df339ba349bf74a4 100644 --- a/crates/notebook/src/background.rs +++ b/crates/notebook/src/background.rs @@ -521,6 +521,21 @@ impl Background { drop(thread); } + /// Stops future checks and awaits the current step before files can move. + pub async fn stop_async(&self) { + #[cfg(not(target_arch = "wasm32"))] + self.stop(); + #[cfg(target_arch = "wasm32")] + { + self.0.signal.stopped.store(true, Ordering::Release); + self.0.signal.wake(); + let thread = self.1.lock().ok().and_then(|mut thread| thread.take()); + if let Some(thread) = thread { + let _ = thread.join().await; + } + } + } + /// Working offline, nothing is checked until `wake`, or until working online again, which /// lists every folder again. pub fn set_offline(&self, offline: bool) { diff --git a/crates/notebook/src/session.rs b/crates/notebook/src/session.rs index 4b11e2b5c55289727a8916f4b8ef6db7113bc6fa..cee67f696a786d7eba414ff01ec6da746bdd34e3 100644 --- a/crates/notebook/src/session.rs +++ b/crates/notebook/src/session.rs @@ -839,6 +839,7 @@ impl Notebook { if !folder_name(name) { return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); } + self.replicas_closed()?; let from = self.storage.location(); let files: Vec = (self.catalog.folders()) .flat_map(|folder| { @@ -868,6 +869,51 @@ impl Notebook { Ok(to) } + /// Waits for browser cache receipts before moving the notebook and its replicas. + pub async fn rename_folder_async(self, name: &str) -> Result { + #[cfg(target_arch = "wasm32")] + { + let deadline = web_time::Instant::now() + Duration::from_secs(30); + let (_, retry) = crate::task::channel(); + loop { + match self.replicas_closed() { + Err(error) if error.busy() && web_time::Instant::now() < deadline => { + crate::task::wait(&retry, Some(Duration::from_millis(50))).await; + } + result => { + result?; + break; + } + } + } + } + self.rename_folder(name) + } + + fn replicas_closed(&self) -> Result<()> { + let from = self.storage.location(); + let locations = std::iter::once(from.clone()).chain( + self.catalog + .sections() + .filter(|section| section.copy) + .map(|section| replica_location(&from, section)), + ); + for location in locations { + let entries = match fs::read_dir(crate::location::folder(&self.cache, &location)) { + Ok(entries) => entries, + Err(error) if error.kind() == io::ErrorKind::NotFound => continue, + Err(error) => return Err(error.into()), + }; + for entry in entries { + let path = entry?.path(); + if path.extension() == Some("sqlite".as_ref()) { + drop(crate::closed(&path)?); + } + } + } + Ok(()) + } + /// Sets a section's colour (COLORREF) in its own metadata, where OneNote keeps it. pub fn set_section_color(&mut self, path: &str, color: Option) -> Result<()> { let path = self.section_path(path)?.path.clone(); diff --git a/crates/snowbound/src/library.rs b/crates/snowbound/src/library.rs index 23e0f0cc01d7055fa377de0781591eac0aed2ab3..9da2a4a5bc323efdbf3e883169b2ca7a62f6f7ea 100644 --- a/crates/snowbound/src/library.rs +++ b/crates/snowbound/src/library.rs @@ -548,17 +548,23 @@ impl Library { /// place of a display name. Refused, with nothing changed, while another writer holds a file /// of it. Returns the notebook's new location, where it opens again, or why not, as its /// reader is told. - pub fn rename_folder(&self, name: &str) -> Result { + pub async fn rename_folder(&self, name: &str) -> Result { let to = self .renamed_location(name) .ok_or("This notebook’s folder can’t be renamed.")?; if let Some(background) = &self.background { - background.stop(); + background.stop_async().await; } - self.close_kept(); - let renamed = self - .reopen() - .and_then(|notebook| Ok(notebook.rename_folder(name)?)); + self.close_kept_async() + .await + .map_err(|error| crate::plain(&*error, "section"))?; + let renamed = match self.reopen() { + Ok(notebook) => notebook + .rename_folder_async(name) + .await + .map_err(|error| Box::new(error) as Box), + Err(error) => Err(error), + }; if let Err(error) = renamed { return Err(match crate::io_kind(&*error) { Some(io::ErrorKind::WouldBlock | io::ErrorKind::ResourceBusy) => { @@ -2108,7 +2114,7 @@ mod tests { section.apply("Rust Author", typed).unwrap(); section.close().unwrap(); assert_eq!( - library.rename_folder("After").unwrap(), + pollster::block_on(library.rename_folder("After")).unwrap(), mount("After").url() ); assert!(client.read_dir(&mount("Before").root, 10).is_err()); @@ -2154,11 +2160,20 @@ mod tests { let location = |name: &str| directory.join(name).to_string_lossy().into_owned(); let library = Library::notebook(&location("Mine"), &cache); library.set_display_name("Shown").unwrap(); - let refused = library.rename_folder("Taken").unwrap_err(); + let held = library.open("New Section 1.one", || {}).unwrap(); + held.set_offline(true); + assert!(pollster::block_on(library.rename_folder("Busy")).is_err()); + assert!(notebook::fs::metadata(location("Mine")).is_ok()); + assert!(notebook::fs::metadata(location("Busy")).is_err()); + held.close().unwrap(); + let refused = pollster::block_on(library.rename_folder("Taken")).unwrap_err(); assert!(refused.starts_with("A folder named “Taken”"), "{refused}"); let library = Library::notebook(&location("Mine"), &cache); assert_eq!(library.name, "Shown"); - assert_eq!(library.rename_folder("Ours").unwrap(), location("Ours")); + assert_eq!( + pollster::block_on(library.rename_folder("Ours")).unwrap(), + location("Ours") + ); assert!(notebook::fs::metadata(location("Mine")).is_err()); let renamed = Library::notebook(&location("Ours"), &cache); assert_eq!(renamed.name, "Ours"); diff --git a/crates/snowbound/src/manage.rs b/crates/snowbound/src/manage.rs index d283635230e43072334fa9d6e60a2cdae22046cf..a73e77ac533eef1b77aa60c6a3924bf108b81695 100644 --- a/crates/snowbound/src/manage.rs +++ b/crates/snowbound/src/manage.rs @@ -718,18 +718,18 @@ impl State { .as_ref() .filter(|session| ours(&session.library)) .map(|session| session.tabs[session.tab].path.clone()); - if shown.is_some() { - let closed = self.persist().and_then(|()| match self.session.take() { - Some(session) => Ok(session.section.close()?), - None => Ok(()), - }); - if let Err(error) = closed { - return platform::alert( - "Couldn't rename the folder", - &crate::plain(&*error, "section"), - ); - } + if shown.is_some() + && let Err(error) = self.persist() + { + return platform::alert( + "Couldn't rename the folder", + &crate::plain(&*error, "section"), + ); } + let closing = shown + .as_ref() + .and_then(|_| self.session.take()) + .map(|session| session.section); let showing = shown.is_some() || self.sectionless.as_ref().is_some_and(ours); if showing { self.sectionless = Some(Arc::clone(&library)); @@ -737,8 +737,19 @@ impl State { } self.folder_rename = Some((library.location.clone(), to.clone())); let (cache, proxy) = (self.cache.clone(), self.proxy.clone()); - crate::spawn(move || { - let renamed = library.rename_folder(&name); + crate::spawn_async(move || async move { + let structure = Arc::clone(&library.structure); + let _structure = structure.lock().await; + let renamed = async { + if let Some(section) = closing { + section + .close_async() + .await + .map_err(|error| crate::plain(&error, "section"))?; + } + library.rename_folder(&name).await + } + .await; // A failure after the folder moved, as its replicas followed, leaves it there. let moved = library .folder()