| author | |
| committer | |
| log | 0b3748b14dba39c904ac5359c0560831ef382139 |
| tree | 483bb450dab6f033d7b7352d94b32d5ab1cdea19 |
| parent | 849fdb2dfd099502ca37d1e55ec80762b65dcd96 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
Seed section synchronization with the current Work Offline policy. Reconcile mobile policy while registering open sections, preserving replacement inheritance and explicit Sync Now. Release upgraded section references after unlocking to keep replica teardown outside the registry lock.
Fixes #135
Assisted-by: gpt-6.1-sol26 files changed, 549 insertions(+), 99 deletions(-)
crates/mobile/src/library.rs+22-3| ... | ... | @@ -631,11 +631,13 @@ impl Library { |
| 631 | 631 | path: &str, |
| 632 | 632 | notify: impl Fn() + Send + 'static, |
| 633 | 633 | ) -> Result<session::Section> { |
| 634 | let offline = self.offline.load(Ordering::Relaxed); | |
| 634 | 635 | match &self.place { |
| 635 | 636 | Place::File(file) => Ok(session::Section::open_with( |
| 636 | 637 | file, |
| 637 | 638 | &self.cache, |
| 638 | 639 | |file| Ok(Coordinated(file.to_owned())), |
| 640 | offline, | |
| 639 | 641 | notify, |
| 640 | 642 | )?), |
| 641 | 643 | Place::Folder(_) => { |
| ... | ... | @@ -643,8 +645,10 @@ impl Library { |
| 643 | 645 | let notebook = notebook.as_ref().ok_or("A lone section has no notebook")?; |
| 644 | 646 | let connect = |file: &Path| Ok(Coordinated(file.to_owned())); |
| 645 | 647 | Ok(match self.unlocked().get(path) { |
| 646 | Some(key) => notebook.section_unlocked_with(path, key, connect, notify)?, | |
| 647 | None => notebook.section_with(path, connect, notify)?, | |
| 648 | Some(key) => { | |
| 649 | notebook.section_unlocked_with(path, key, connect, offline, notify)? | |
| 650 | } | |
| 651 | None => notebook.section_with(path, connect, offline, notify)?, | |
| 648 | 652 | }) |
| 649 | 653 | } |
| 650 | 654 | Place::Share { server, root, .. } => { |
| ... | ... | @@ -682,6 +686,7 @@ impl Library { |
| 682 | 686 | replica, |
| 683 | 687 | LIMIT, |
| 684 | 688 | move || server.connect(), |
| 689 | offline, | |
| 685 | 690 | notify, |
| 686 | 691 | )?) |
| 687 | 692 | } |
| ... | ... | @@ -857,6 +862,9 @@ impl Section { |
| 857 | 862 | .open |
| 858 | 863 | .lock() |
| 859 | 864 | .unwrap_or_else(|error| error.into_inner()); |
| 865 | shared | |
| 866 | .section | |
| 867 | .set_offline(library.offline.load(Ordering::Relaxed)); | |
| 860 | 868 | open.push((path.to_owned(), Arc::downgrade(&shared))); |
| 861 | 869 | drop(open); |
| 862 | 870 | Ok(Self { shared, library }) |
| ... | ... | @@ -1785,13 +1793,24 @@ pub extern "C" fn sb_library_sync_status(library: &Library) -> *mut c_char { |
| 1785 | 1793 | /// `sb_library_sync_now`. Sections opened later follow. |
| 1786 | 1794 | #[unsafe(no_mangle)] |
| 1787 | 1795 | pub extern "C" fn sb_library_set_offline(library: &Library, offline: bool) { |
| 1796 | let mut open = library | |
| 1797 | .open | |
| 1798 | .lock() | |
| 1799 | .unwrap_or_else(|error| error.into_inner()); | |
| 1788 | 1800 | library.offline.store(offline, Ordering::Relaxed); |
| 1789 | 1801 | if let Some(background) = &library.background { |
| 1790 | 1802 | background.set_offline(offline); |
| 1791 | 1803 | } |
| 1792 | for (_, shared) in library.open_sections() { | |
| 1804 | open.retain(|(_, shared)| shared.strong_count() > 0); | |
| 1805 | let sections: Vec<_> = open | |
| 1806 | .iter() | |
| 1807 | .filter_map(|(_, shared)| shared.upgrade()) | |
| 1808 | .collect(); | |
| 1809 | for shared in &sections { | |
| 1793 | 1810 | shared.section.set_offline(offline); |
| 1794 | 1811 | } |
| 1812 | // Releasing the last Shared can join its replica's threads. | |
| 1813 | drop(open); | |
| 1795 | 1814 | } |
| 1796 | 1815 | |
| 1797 | 1816 | /// Checks the sections at or below `path`, `/`-separated from the notebook's folder, as the |
crates/mobile/src/tests.rs+131-1| ... | ... | @@ -376,6 +376,91 @@ fn cached_cloud_sections_reopen_with_offline_edits_after_eviction() { |
| 376 | 376 | assert_eq!(stored.page(space).unwrap().color, Some(0x00ef_cdab)); |
| 377 | 377 | } |
| 378 | 378 | |
| 379 | #[test] | |
| 380 | fn reopening_a_reachable_source_while_offline_retains_its_original_queue() { | |
| 381 | extern "C" fn wake(_: usize) {} | |
| 382 | ||
| 383 | for lone in [false, true] { | |
| 384 | let directory = tempfile::tempdir().unwrap(); | |
| 385 | let root = directory.path().join("notebook"); | |
| 386 | let cache = directory.path().join("cache"); | |
| 387 | let creation = onestore::PageCreation::new(None, Some("Original"), "Fixture").unwrap(); | |
| 388 | let notebook = | |
| 389 | notebook::session::Notebook::create(&root, &cache, 0x00f0_a0c0, &creation).unwrap(); | |
| 390 | let path = notebook.catalog().sections[0].path.clone(); | |
| 391 | drop(notebook); | |
| 392 | let file = root.join(&path); | |
| 393 | let source = std::fs::read(&file).unwrap(); | |
| 394 | let (location, path) = if lone { | |
| 395 | (file.clone(), file.to_string_lossy().into_owned()) | |
| 396 | } else { | |
| 397 | (root, path) | |
| 398 | }; | |
| 399 | let library = Arc::new(Library::open(&location, &cache, false).unwrap()); | |
| 400 | sb_library_set_offline(&library, true); | |
| 401 | let section = open(&library, &path); | |
| 402 | let creation = | |
| 403 | onestore::PageCreation::new(None, Some("Mobile offline reopen marker"), "Fixture") | |
| 404 | .unwrap(); | |
| 405 | section | |
| 406 | .shared | |
| 407 | .apply(Edit { | |
| 408 | at: library::filetime(), | |
| 409 | ops: vec![Op::Section(onestore::op::SectionOp::Create(creation))], | |
| 410 | }) | |
| 411 | .unwrap(); | |
| 412 | section.shared.section.replica().written().unwrap(); | |
| 413 | let pending = section.shared.section.pending().unwrap(); | |
| 414 | assert_eq!(pending.len(), 1); | |
| 415 | let shared = Arc::clone(&section.shared); | |
| 416 | drop(section); | |
| 417 | Arc::into_inner(shared).unwrap().section.close().unwrap(); | |
| 418 | let path = std::ffi::CString::new(path).unwrap(); | |
| 419 | for reopen in 0..2 { | |
| 420 | let mut error = std::ptr::null_mut(); | |
| 421 | let raw = unsafe { | |
| 422 | library::sb_section_open( | |
| 423 | &library, | |
| 424 | path.as_ptr(), | |
| 425 | c"Fixture".as_ptr(), | |
| 426 | wake, | |
| 427 | 0, | |
| 428 | &mut error, | |
| 429 | ) | |
| 430 | }; | |
| 431 | assert!(error.is_null()); | |
| 432 | assert!(!raw.is_null()); | |
| 433 | let section = unsafe { Box::from_raw(raw) }; | |
| 434 | for _ in 0..20 { | |
| 435 | assert_eq!(section.shared.section.pending().unwrap(), pending); | |
| 436 | assert_eq!(std::fs::read(&file).unwrap(), source); | |
| 437 | std::thread::sleep(Duration::from_millis(10)); | |
| 438 | } | |
| 439 | if reopen == 1 { | |
| 440 | sb_library_sync_now(&library); | |
| 441 | assert!(eventually(|| section | |
| 442 | .shared | |
| 443 | .section | |
| 444 | .pending() | |
| 445 | .unwrap() | |
| 446 | .is_empty())); | |
| 447 | let pages = | |
| 448 | notebook::session::stored_pages(&std::fs::read(&file).unwrap()).unwrap(); | |
| 449 | assert_eq!( | |
| 450 | pages | |
| 451 | .iter() | |
| 452 | .filter(|page| page.page.title == "Mobile offline reopen marker") | |
| 453 | .count(), | |
| 454 | 1 | |
| 455 | ); | |
| 456 | } | |
| 457 | let shared = Arc::clone(&section.shared); | |
| 458 | drop(section); | |
| 459 | Arc::into_inner(shared).unwrap().section.close().unwrap(); | |
| 460 | } | |
| 461 | } | |
| 462 | } | |
| 463 | ||
| 379 | 464 | #[test] |
| 380 | 465 | fn a_new_page_and_subpage_list_after_their_parent_and_delete_to_the_recycle_bin() { |
| 381 | 466 | let (directory, root) = copy("m6/native-features-01/notebook"); |
| ... | ... | @@ -666,8 +751,53 @@ fn a_notebook_on_a_share_saves_and_opens_offline() { |
| 666 | 751 | let arena = onestore::Arena::default(); |
| 667 | 752 | let stored = onestore::Section::open(&arena, published).unwrap(); |
| 668 | 753 | assert!(canvas::search::page_text(&stored.page(canvas.space).unwrap()).contains("Shared ")); |
| 754 | sb_library_set_offline(&library, true); | |
| 755 | let source = client | |
| 756 | .read(&format!("{root}/Features.one"), 1 << 26) | |
| 757 | .unwrap(); | |
| 758 | canvas.insert("Reachable offline marker ".into()).unwrap(); | |
| 759 | section | |
| 760 | .shared | |
| 761 | .apply(canvas.edit().unwrap().unwrap()) | |
| 762 | .unwrap(); | |
| 763 | section.shared.section.replica().written().unwrap(); | |
| 764 | let pending = section.shared.section.pending().unwrap(); | |
| 765 | assert_eq!(pending.len(), 1); | |
| 766 | let page_space = canvas.space; | |
| 669 | 767 | let shared = Arc::clone(&section.shared); |
| 670 | drop((canvas, section, library)); | |
| 768 | drop((canvas, section)); | |
| 769 | Arc::into_inner(shared).unwrap().section.close().unwrap(); | |
| 770 | let section = open(&library, "Features.one"); | |
| 771 | for _ in 0..10 { | |
| 772 | assert_eq!(section.shared.section.pending().unwrap(), pending); | |
| 773 | assert_eq!( | |
| 774 | client | |
| 775 | .read(&format!("{root}/Features.one"), 1 << 26) | |
| 776 | .unwrap(), | |
| 777 | source | |
| 778 | ); | |
| 779 | std::thread::sleep(Duration::from_millis(20)); | |
| 780 | } | |
| 781 | sb_library_sync_now(&library); | |
| 782 | assert!(eventually(|| section | |
| 783 | .shared | |
| 784 | .section | |
| 785 | .pending() | |
| 786 | .unwrap() | |
| 787 | .is_empty())); | |
| 788 | let arena = onestore::Arena::default(); | |
| 789 | let published = client | |
| 790 | .read(&format!("{root}/Features.one"), 1 << 26) | |
| 791 | .unwrap(); | |
| 792 | let stored = onestore::Section::open(&arena, published).unwrap(); | |
| 793 | assert_eq!( | |
| 794 | canvas::search::page_text(&stored.page(page_space).unwrap()) | |
| 795 | .matches("Reachable offline marker ") | |
| 796 | .count(), | |
| 797 | 1 | |
| 798 | ); | |
| 799 | let shared = Arc::clone(&section.shared); | |
| 800 | drop((section, library)); | |
| 671 | 801 | Arc::into_inner(shared).unwrap().section.close().unwrap(); |
| 672 | 802 | // The same notebook while its server does not answer: a closed port on the lab's host. |
| 673 | 803 | let host = address.split(':').next().unwrap(); |
crates/notebook/README.md+8-7| ... | ... | @@ -52,14 +52,15 @@ thread rereads the queue. |
| 52 | 52 | `session::Notebook::open(root, cache_dir)` discovers a notebook directory, reading only |
| 53 | 53 | the files its folders list otherwise than when last opened (`discover::Cache`, kept under |
| 54 | 54 | `cache_dir/listings`), and |
| 55 | `session::Section::open(file, cache_dir, notify)` opens one section file through | |
| 55 | `session::Section::open(file, cache_dir, offline, notify)` opens one section file through | |
| 56 | 56 | a replica named by the file's location and the section's document identity, so the same |
| 57 | 57 | file reopens the same queue after a relaunch and a copy of it elsewhere has its own |
| 58 | 58 | (`notebook::location`: `folder`, `local`, `smb`, `moved` for a notebook the app moves, |
| 59 | 59 | and `forget` for one it deletes). |
| 60 | `Notebook::section(path, notify)` opens a mounted notebook's section; a replica already | |
| 60 | `Notebook::section(path, offline, notify)` opens a mounted notebook's section; a replica already | |
| 61 | 61 | named by the identity its discovery read resumes without reading the file, which the |
| 62 | worker checks next, as on a share. | |
| 62 | worker checks next, as on a share. `offline` seeds Work Offline before the worker starts; | |
| 63 | explicit `wake()` still synchronizes while offline. | |
| 63 | 64 | A section is `Send + Sync`: `apply(author, edit)` returns at once and reports a refusal as `Event::Rejected`; `events()` drains |
| 64 | 65 | remote changes (`Changed(spaces)`), publication outcomes, unreachable files and |
| 65 | 66 | failures; `notify` runs on a background thread whenever an event waits. |
| ... | ... | @@ -96,9 +97,9 @@ changed, `wake` and `set_offline` follow Sync Now and Work Offline, and `discard |
| 96 | 97 | thread and deletes the replicas holding nothing unpublished, as when the notebook closes. |
| 97 | 98 | `Notebook::replica_path(path)` names a section's replica for either kind of notebook. |
| 98 | 99 | |
| 99 | `Section::resume(file, replica, notify)` starts from an owned `Replica` without | |
| 100 | `Section::resume(file, replica, offline, notify)` starts from an owned `Replica` without | |
| 100 | 101 | consulting the remote file; with the `smb` feature, `Section::resume_smb(path, |
| 101 | replica, limit, connect, notify)` binds a share-relative path, `connect` running on | |
| 102 | replica, limit, connect, offline, notify)` binds a share-relative path, `connect` running on | |
| 102 | 103 | the worker again after transport failure. |
| 103 | 104 | |
| 104 | 105 | ## Reconciliation |
| ... | ... | @@ -358,7 +359,7 @@ sections it has not opened this way, and unread changes leave out the reader's o |
| 358 | 359 | |
| 359 | 360 | A password-protected section lists as `Locked`. `Notebook::unlock(path, password)` opens |
| 360 | 361 | its key (a wrong password is `Error::Protected(PasswordMismatch)`), and |
| 361 | `section_unlocked(path, &key, notify)` (`section_unlocked_with`, or | |
| 362 | `section_unlocked(path, &key, offline, notify)` (`section_unlocked_with`, or | |
| 362 | 363 | `Replica::open_or_create(path, Some(&key), source)` for a host that opens replicas itself) |
| 363 | 364 | opens it as any section opens: edits queue, publish, merge and become conflict pages as an ordinary section's do, |
| 364 | 365 | each revision sealed under the section's key. The replica keeps none of it in the clear: |
| ... | ... | @@ -567,7 +568,7 @@ on, and `stop()` lets every guest go. `join(hello, code, password, reach, relay) |
| 567 | 568 | `Welcome` (the share and its secret), or a `Refusal` saying why not: a wrong code, no one |
| 568 | 569 | sharing it, an expired code, too many wrong tries, or no relay. `Guest::start` joins the share; |
| 569 | 570 | `Notebook::open_hosted(guest, cache)`, `Background::hosted(guest, notify)` and |
| 570 | `Section::resume_hosted(path, replica, guest, notify)` then work as on a share, with | |
| 571 | `Section::resume_hosted(path, replica, guest, offline, notify)` then work as on a share, with | |
| 571 | 572 | `Guest::host()` and `stopped()` for whether the host is there and still sharing. |
| 572 | 573 | |
| 573 | 574 | ## Queue measurement |
crates/notebook/examples/browser_receipts.rs+1-1| ... | ... | @@ -190,7 +190,7 @@ mod web { |
| 190 | 190 | ) |
| 191 | 191 | .unwrap(); |
| 192 | 192 | let original_cache = notebook.replica_path("New Section 1.one").unwrap(); |
| 193 | let section = notebook.section("New Section 1.one", || {}).unwrap(); | |
| 193 | let section = notebook.section("New Section 1.one", false, || {}).unwrap(); | |
| 194 | 194 | section.set_offline(true); |
| 195 | 195 | let (space, text_id, _) = text(&section.replica().snapshot().unwrap()); |
| 196 | 196 | let replica = Arc::clone(section.replica()); |
crates/notebook/examples/live_crowd.rs+8-2| ... | ... | @@ -191,8 +191,14 @@ fn join(name: &str, code: &str, url: &str, cache: &Path) -> Open { |
| 191 | 191 | let image = notebook.read_section("Garden.one").unwrap(); |
| 192 | 192 | let file = onestore::Header::parse(&image[..1024]).unwrap().file_id; |
| 193 | 193 | let replica = Replica::open_or_create(&replica, None, || Ok(image)).unwrap(); |
| 194 | let section = | |
| 195 | Section::resume_hosted("Garden.one".into(), replica, Arc::clone(&guest), || {}).unwrap(); | |
| 194 | let section = Section::resume_hosted( | |
| 195 | "Garden.one".into(), | |
| 196 | replica, | |
| 197 | Arc::clone(&guest), | |
| 198 | false, | |
| 199 | || {}, | |
| 200 | ) | |
| 201 | .unwrap(); | |
| 196 | 202 | background.hold("Garden.one", &section); |
| 197 | 203 | Open { |
| 198 | 204 | guest, |
crates/notebook/examples/live_latency.rs+8-2| ... | ... | @@ -69,8 +69,14 @@ fn guest( |
| 69 | 69 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); |
| 70 | 70 | let replica = |
| 71 | 71 | Replica::open_or_create(&replica, None, || notebook.read_section("Garden.one")).unwrap(); |
| 72 | let section = | |
| 73 | Section::resume_hosted("Garden.one".into(), replica, Arc::clone(&guest), || {}).unwrap(); | |
| 72 | let section = Section::resume_hosted( | |
| 73 | "Garden.one".into(), | |
| 74 | replica, | |
| 75 | Arc::clone(&guest), | |
| 76 | false, | |
| 77 | || {}, | |
| 78 | ) | |
| 79 | .unwrap(); | |
| 74 | 80 | background.hold("Garden.one", &section); |
| 75 | 81 | ( |
| 76 | 82 | guest, |
crates/notebook/examples/open_probe.rs+1-1| ... | ... | @@ -133,7 +133,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 133 | 133 | println!("section {} bytes", source.len()); |
| 134 | 134 | for label in ["fresh cache", "reopened cache"] { |
| 135 | 135 | let start = Instant::now(); |
| 136 | let section = Section::open(&file, &cache, || {})?; | |
| 136 | let section = Section::open(&file, &cache, false, || {})?; | |
| 137 | 137 | let opened = start.elapsed(); |
| 138 | 138 | let pages = section.pages()?; |
| 139 | 139 | let listed = start.elapsed(); |
crates/notebook/examples/save_probe.rs+1-1| ... | ... | @@ -21,7 +21,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 21 | 21 | let directory = tempfile::tempdir()?; |
| 22 | 22 | let file = directory.path().join("probe.one"); |
| 23 | 23 | std::fs::write(&file, std::fs::read(source)?)?; |
| 24 | let section = Section::open(&file, directory.path().join("cache"), || {})?; | |
| 24 | let section = Section::open(&file, directory.path().join("cache"), false, || {})?; | |
| 25 | 25 | let words = [ |
| 26 | 26 | "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", |
| 27 | 27 | ]; |
crates/notebook/examples/session_client.rs+1-1| ... | ... | @@ -115,7 +115,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 115 | 115 | return Err("Usage: session_client SECTION CACHE_DIR LABEL".into()); |
| 116 | 116 | }; |
| 117 | 117 | let (notify, notified) = mpsc::channel(); |
| 118 | let section = Section::open(file, cache, move || { | |
| 118 | let section = Section::open(file, cache, false, move || { | |
| 119 | 119 | let _ = notify.send(()); |
| 120 | 120 | })?; |
| 121 | 121 | let pending = section.pending()?; |
crates/notebook/src/background.rs+1-1| ... | ... | @@ -1479,7 +1479,7 @@ mod tests { |
| 1479 | 1479 | let source = onestore::create_section("Held.one", "", "Author").unwrap(); |
| 1480 | 1480 | fs::write(&file, &source).unwrap(); |
| 1481 | 1481 | let (changed, changes) = mpsc::channel(); |
| 1482 | let section = Section::open(&file, directory.path().join("cache"), move || { | |
| 1482 | let section = Section::open(&file, directory.path().join("cache"), false, move || { | |
| 1483 | 1483 | let _ = changed.send(()); |
| 1484 | 1484 | }) |
| 1485 | 1485 | .unwrap(); |
crates/notebook/src/session.rs+51-12| ... | ... | @@ -1748,8 +1748,18 @@ impl Notebook { |
| 1748 | 1748 | } |
| 1749 | 1749 | |
| 1750 | 1750 | /// Opens a section of a mounted notebook by its catalog path. |
| 1751 | pub fn section(&self, path: &str, notify: impl Fn() + Send + 'static) -> Result<Section> { | |
| 1752 | self.section_with(path, |file| Ok(FileRemote(file.to_owned())), notify) | |
| 1751 | pub fn section( | |
| 1752 | &self, | |
| 1753 | path: &str, | |
| 1754 | offline: bool, | |
| 1755 | notify: impl Fn() + Send + 'static, | |
| 1756 | ) -> Result<Section> { | |
| 1757 | self.section_with( | |
| 1758 | path, | |
| 1759 | |file| Ok(FileRemote(file.to_owned())), | |
| 1760 | offline, | |
| 1761 | notify, | |
| 1762 | ) | |
| 1753 | 1763 | } |
| 1754 | 1764 | |
| 1755 | 1765 | /// `section`, reading and publishing through the remote `connect` makes for its file |
| ... | ... | @@ -1758,9 +1768,10 @@ impl Notebook { |
| 1758 | 1768 | &self, |
| 1759 | 1769 | path: &str, |
| 1760 | 1770 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 1771 | offline: bool, | |
| 1761 | 1772 | notify: impl Fn() + Send + 'static, |
| 1762 | 1773 | ) -> Result<Section> { |
| 1763 | self.open_section(path, None, connect, notify) | |
| 1774 | self.open_section(path, None, connect, offline, notify) | |
| 1764 | 1775 | } |
| 1765 | 1776 | |
| 1766 | 1777 | /// `section` for a password-protected section, under the `key` `unlock` gave. |
| ... | ... | @@ -1768,9 +1779,16 @@ impl Notebook { |
| 1768 | 1779 | &self, |
| 1769 | 1780 | path: &str, |
| 1770 | 1781 | key: &protected::Key, |
| 1782 | offline: bool, | |
| 1771 | 1783 | notify: impl Fn() + Send + 'static, |
| 1772 | 1784 | ) -> Result<Section> { |
| 1773 | self.section_unlocked_with(path, key, |file| Ok(FileRemote(file.to_owned())), notify) | |
| 1785 | self.section_unlocked_with( | |
| 1786 | path, | |
| 1787 | key, | |
| 1788 | |file| Ok(FileRemote(file.to_owned())), | |
| 1789 | offline, | |
| 1790 | notify, | |
| 1791 | ) | |
| 1774 | 1792 | } |
| 1775 | 1793 | |
| 1776 | 1794 | /// `section_with` for a password-protected section, under its `key`. |
| ... | ... | @@ -1779,9 +1797,10 @@ impl Notebook { |
| 1779 | 1797 | path: &str, |
| 1780 | 1798 | key: &protected::Key, |
| 1781 | 1799 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 1800 | offline: bool, | |
| 1782 | 1801 | notify: impl Fn() + Send + 'static, |
| 1783 | 1802 | ) -> Result<Section> { |
| 1784 | self.open_section(path, Some(key), connect, notify) | |
| 1803 | self.open_section(path, Some(key), connect, offline, notify) | |
| 1785 | 1804 | } |
| 1786 | 1805 | |
| 1787 | 1806 | fn open_section<R: Remote + 'static>( |
| ... | ... | @@ -1789,6 +1808,7 @@ impl Notebook { |
| 1789 | 1808 | path: &str, |
| 1790 | 1809 | key: Option<&protected::Key>, |
| 1791 | 1810 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 1811 | offline: bool, | |
| 1792 | 1812 | notify: impl Fn() + Send + 'static, |
| 1793 | 1813 | ) -> Result<Section> { |
| 1794 | 1814 | let section = self.section_path(path)?; |
| ... | ... | @@ -1823,14 +1843,14 @@ impl Notebook { |
| 1823 | 1843 | if fs::metadata(&cache).is_ok() { |
| 1824 | 1844 | let (replica, remote, mut connect) = |
| 1825 | 1845 | (Replica::open(&cache)?, file.clone(), connect); |
| 1826 | return Section::start(file, replica, move || connect(&remote), notify); | |
| 1846 | return Section::start(file, replica, move || connect(&remote), offline, notify); | |
| 1827 | 1847 | } |
| 1828 | 1848 | } |
| 1829 | 1849 | let replica = |identity: &[u8; 16], _: &[u8]| { |
| 1830 | 1850 | fs::create_dir_all(&replicas)?; |
| 1831 | 1851 | Ok(replica_file(&replicas, identity)) |
| 1832 | 1852 | }; |
| 1833 | Section::open_in(file, key, replica, connect, notify) | |
| 1853 | Section::open_in(file, key, replica, connect, offline, notify) | |
| 1834 | 1854 | } |
| 1835 | 1855 | } |
| 1836 | 1856 | |
| ... | ... | @@ -2217,9 +2237,16 @@ impl Section { |
| 2217 | 2237 | pub fn open( |
| 2218 | 2238 | file: impl AsRef<Path>, |
| 2219 | 2239 | cache: impl AsRef<Path>, |
| 2240 | offline: bool, | |
| 2220 | 2241 | notify: impl Fn() + Send + 'static, |
| 2221 | 2242 | ) -> Result<Self> { |
| 2222 | Self::open_with(file, cache, |file| Ok(FileRemote(file.to_owned())), notify) | |
| 2243 | Self::open_with( | |
| 2244 | file, | |
| 2245 | cache, | |
| 2246 | |file| Ok(FileRemote(file.to_owned())), | |
| 2247 | offline, | |
| 2248 | notify, | |
| 2249 | ) | |
| 2223 | 2250 | } |
| 2224 | 2251 | |
| 2225 | 2252 | /// `open`, reading and publishing the file through the remote `connect` makes for it, |
| ... | ... | @@ -2229,6 +2256,7 @@ impl Section { |
| 2229 | 2256 | file: impl AsRef<Path>, |
| 2230 | 2257 | cache: impl AsRef<Path>, |
| 2231 | 2258 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 2259 | offline: bool, | |
| 2232 | 2260 | notify: impl Fn() + Send + 'static, |
| 2233 | 2261 | ) -> Result<Self> { |
| 2234 | 2262 | let file = fs::canonicalize(file)?; |
| ... | ... | @@ -2239,7 +2267,7 @@ impl Section { |
| 2239 | 2267 | })?; |
| 2240 | 2268 | Ok(replica_file(&folder, identity)) |
| 2241 | 2269 | }; |
| 2242 | Self::open_in(file, None, replicas, connect, notify) | |
| 2270 | Self::open_in(file, None, replicas, connect, offline, notify) | |
| 2243 | 2271 | } |
| 2244 | 2272 | |
| 2245 | 2273 | /// Opens the canonical section `file`, a protected one under `key`, through the replica |
| ... | ... | @@ -2249,6 +2277,7 @@ impl Section { |
| 2249 | 2277 | key: Option<&protected::Key>, |
| 2250 | 2278 | replica: impl FnOnce(&[u8; 16], &[u8]) -> Result<PathBuf>, |
| 2251 | 2279 | mut connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 2280 | offline: bool, | |
| 2252 | 2281 | notify: impl Fn() + Send + 'static, |
| 2253 | 2282 | ) -> Result<Self> { |
| 2254 | 2283 | let source = connect(&file)?.read()?; |
| ... | ... | @@ -2257,7 +2286,7 @@ impl Section { |
| 2257 | 2286 | let cache = replica(&identity.guid, &source)?; |
| 2258 | 2287 | let replica = Replica::open_or_create(&cache, key, || Ok(source))?; |
| 2259 | 2288 | let remote = file.clone(); |
| 2260 | Self::start(file, replica, move || connect(&remote), notify) | |
| 2289 | Self::start(file, replica, move || connect(&remote), offline, notify) | |
| 2261 | 2290 | } |
| 2262 | 2291 | |
| 2263 | 2292 | /// Resumes an owned local replica without reading the publication target. |
| ... | ... | @@ -2265,6 +2294,7 @@ impl Section { |
| 2265 | 2294 | pub fn resume( |
| 2266 | 2295 | file: impl AsRef<Path>, |
| 2267 | 2296 | replica: Replica, |
| 2297 | offline: bool, | |
| 2268 | 2298 | notify: impl Fn() + Send + 'static, |
| 2269 | 2299 | ) -> Result<Self> { |
| 2270 | 2300 | let file = fs::absolute(file)?; |
| ... | ... | @@ -2273,6 +2303,7 @@ impl Section { |
| 2273 | 2303 | file, |
| 2274 | 2304 | replica, |
| 2275 | 2305 | move || Ok(FileRemote(remote.clone())), |
| 2306 | offline, | |
| 2276 | 2307 | notify, |
| 2277 | 2308 | ) |
| 2278 | 2309 | } |
| ... | ... | @@ -2285,12 +2316,14 @@ impl Section { |
| 2285 | 2316 | replica: Replica, |
| 2286 | 2317 | limit: usize, |
| 2287 | 2318 | mut connect: impl FnMut() -> io::Result<crate::smb::Client> + Send + 'static, |
| 2319 | offline: bool, | |
| 2288 | 2320 | notify: impl Fn() + Send + 'static, |
| 2289 | 2321 | ) -> Result<Self> { |
| 2290 | 2322 | Self::start( |
| 2291 | 2323 | PathBuf::from(&path), |
| 2292 | 2324 | replica, |
| 2293 | 2325 | move || Ok(crate::SmbRemote::new(connect()?, path.clone(), limit)), |
| 2326 | offline, | |
| 2294 | 2327 | notify, |
| 2295 | 2328 | ) |
| 2296 | 2329 | } |
| ... | ... | @@ -2301,12 +2334,14 @@ impl Section { |
| 2301 | 2334 | path: String, |
| 2302 | 2335 | replica: Replica, |
| 2303 | 2336 | guest: Arc<crate::live::share::Guest>, |
| 2337 | offline: bool, | |
| 2304 | 2338 | notify: impl Fn() + Send + 'static, |
| 2305 | 2339 | ) -> Result<Self> { |
| 2306 | 2340 | Self::start( |
| 2307 | 2341 | PathBuf::from(&path), |
| 2308 | 2342 | replica, |
| 2309 | 2343 | move || Ok(crate::live::share::HostedRemote::new(&guest, &path)), |
| 2344 | offline, | |
| 2310 | 2345 | notify, |
| 2311 | 2346 | ) |
| 2312 | 2347 | } |
| ... | ... | @@ -2315,6 +2350,7 @@ impl Section { |
| 2315 | 2350 | file: PathBuf, |
| 2316 | 2351 | replica: Replica, |
| 2317 | 2352 | connect: impl FnMut() -> io::Result<R> + Send + 'static, |
| 2353 | offline: bool, | |
| 2318 | 2354 | notify: impl Fn() + Send + 'static, |
| 2319 | 2355 | ) -> Result<Self> { |
| 2320 | 2356 | let replica = Arc::new(replica); |
| ... | ... | @@ -2335,19 +2371,21 @@ impl Section { |
| 2335 | 2371 | notify, |
| 2336 | 2372 | observed, |
| 2337 | 2373 | }; |
| 2338 | section.worker = Some(section.start_worker(connect, None)?); | |
| 2374 | section.worker = Some(section.start_worker(connect, offline, None)?); | |
| 2339 | 2375 | Ok(section) |
| 2340 | 2376 | } |
| 2341 | 2377 | |
| 2342 | 2378 | fn start_worker<R: Remote + 'static>( |
| 2343 | 2379 | &self, |
| 2344 | 2380 | connect: impl FnMut() -> io::Result<R> + Send + 'static, |
| 2381 | offline: bool, | |
| 2345 | 2382 | replacing: Option<&SyncWorker>, |
| 2346 | 2383 | ) -> Result<SyncWorker> { |
| 2347 | 2384 | let (sender, notify) = (self.sender.clone(), Arc::clone(&self.notify)); |
| 2348 | 2385 | let observed = Arc::clone(&self.observed); |
| 2349 | 2386 | Ok(self.replica.start_sync_replacing( |
| 2350 | 2387 | Duration::from_secs(2), |
| 2388 | offline, | |
| 2351 | 2389 | connect, |
| 2352 | 2390 | move |result| { |
| 2353 | 2391 | let error = result.as_ref().err().map(reached); |
| ... | ... | @@ -2413,7 +2451,8 @@ impl Section { |
| 2413 | 2451 | worker.cancel(); |
| 2414 | 2452 | } |
| 2415 | 2453 | self.observed = Arc::new(Mutex::new(None)); |
| 2416 | self.worker = Some(self.start_worker(move || connect(&target), previous.as_ref())?); | |
| 2454 | self.worker = | |
| 2455 | Some(self.start_worker(move || connect(&target), false, previous.as_ref())?); | |
| 2417 | 2456 | self.file = file; |
| 2418 | 2457 | Ok(()) |
| 2419 | 2458 | } |
crates/notebook/src/session/tests.rs+126-3| ... | ... | @@ -9,14 +9,18 @@ fn cached_sections_refuse_links_outside_the_notebook_and_dangling_links() { |
| 9 | 9 | let creation = PageCreation::new(None, Some("Mine"), "Fixture").unwrap(); |
| 10 | 10 | let notebook = Notebook::create(&folder, &cache, 0x00f0_a0c0, &creation).unwrap(); |
| 11 | 11 | let path = &notebook.catalog().sections[0].path; |
| 12 | notebook.section(path, || {}).unwrap().close().unwrap(); | |
| 12 | notebook | |
| 13 | .section(path, false, || {}) | |
| 14 | .unwrap() | |
| 15 | .close() | |
| 16 | .unwrap(); | |
| 13 | 17 | let file = folder.join(path); |
| 14 | 18 | let outside = temporary.path().join("outside.one"); |
| 15 | 19 | fs::rename(&file, &outside).unwrap(); |
| 16 | 20 | std::os::unix::fs::symlink(&outside, &file).unwrap(); |
| 17 | assert!(notebook.section(path, || {}).is_err()); | |
| 21 | assert!(notebook.section(path, false, || {}).is_err()); | |
| 18 | 22 | fs::remove_file(outside).unwrap(); |
| 19 | assert!(notebook.section(path, || {}).is_err()); | |
| 23 | assert!(notebook.section(path, false, || {}).is_err()); | |
| 20 | 24 | } |
| 21 | 25 | |
| 22 | 26 | #[test] |
| ... | ... | @@ -501,3 +505,122 @@ fn recycling_rejects_unconfirmed_appends_and_can_retry_each_stage() { |
| 501 | 505 | assert_eq!(fs::read(folder.join(&path)).unwrap(), source); |
| 502 | 506 | } |
| 503 | 507 | } |
| 508 | ||
| 509 | #[test] | |
| 510 | fn initially_offline_workers_keep_the_original_queue_until_explicitly_woken() { | |
| 511 | use std::sync::atomic::{AtomicUsize, Ordering}; | |
| 512 | use std::time::Instant; | |
| 513 | ||
| 514 | struct Counted { | |
| 515 | file: FileRemote, | |
| 516 | calls: Arc<[AtomicUsize; 5]>, | |
| 517 | } | |
| 518 | impl Remote for Counted { | |
| 519 | fn read(&mut self) -> io::Result<Vec<u8>> { | |
| 520 | self.calls[1].fetch_add(1, Ordering::SeqCst); | |
| 521 | self.file.read() | |
| 522 | } | |
| 523 | fn stamp(&mut self) -> io::Result<Stamp> { | |
| 524 | self.calls[2].fetch_add(1, Ordering::SeqCst); | |
| 525 | self.file.stamp() | |
| 526 | } | |
| 527 | fn publish(&mut self, transaction: &Transaction) -> std::result::Result<(), CommitError> { | |
| 528 | self.calls[3].fetch_add(1, Ordering::SeqCst); | |
| 529 | self.file.publish(transaction) | |
| 530 | } | |
| 531 | fn confirm(&mut self, base: &Stamp) -> std::result::Result<(), CommitError> { | |
| 532 | self.calls[4].fetch_add(1, Ordering::SeqCst); | |
| 533 | self.file.confirm(base) | |
| 534 | } | |
| 535 | } | |
| 536 | ||
| 537 | for relocate in [false, true] { | |
| 538 | let temporary = tempfile::tempdir().unwrap(); | |
| 539 | let file = temporary.path().join("Original.one"); | |
| 540 | let cache = temporary.path().join("original.sqlite"); | |
| 541 | let source = onestore::create_section("Original.one", "Original", "Fixture").unwrap(); | |
| 542 | fs::write(&file, &source).unwrap(); | |
| 543 | let replica = Replica::create(&cache, &source).unwrap(); | |
| 544 | let creation = | |
| 545 | PageCreation::new(None, Some("Offline initialization marker"), "Fixture").unwrap(); | |
| 546 | let id = replica | |
| 547 | .apply( | |
| 548 | "Fixture", | |
| 549 | Edit { | |
| 550 | at: crate::now(), | |
| 551 | ops: vec![Op::Section(SectionOp::Create(creation))], | |
| 552 | }, | |
| 553 | ) | |
| 554 | .unwrap(); | |
| 555 | let pending = replica.pending().unwrap(); | |
| 556 | drop(replica); | |
| 557 | let calls = Arc::new(std::array::from_fn(|_| AtomicUsize::new(0))); | |
| 558 | let counter = Arc::clone(&calls); | |
| 559 | let remote = file.clone(); | |
| 560 | let (notify, notified) = mpsc::channel(); | |
| 561 | let mut section = Section::start( | |
| 562 | file.clone(), | |
| 563 | Replica::open(&cache).unwrap(), | |
| 564 | move || { | |
| 565 | counter[0].fetch_add(1, Ordering::SeqCst); | |
| 566 | Ok(Counted { | |
| 567 | file: FileRemote(remote.clone()), | |
| 568 | calls: Arc::clone(&counter), | |
| 569 | }) | |
| 570 | }, | |
| 571 | true, | |
| 572 | move || { | |
| 573 | let _ = notify.send(()); | |
| 574 | }, | |
| 575 | ) | |
| 576 | .unwrap(); | |
| 577 | let target = if relocate { | |
| 578 | let target = temporary.path().join("Moved.one"); | |
| 579 | fs::rename(&file, &target).unwrap(); | |
| 580 | let counter = Arc::clone(&calls); | |
| 581 | section | |
| 582 | .relocate_with(&target, move |file| { | |
| 583 | counter[0].fetch_add(1, Ordering::SeqCst); | |
| 584 | Ok(Counted { | |
| 585 | file: FileRemote(file.to_owned()), | |
| 586 | calls: Arc::clone(&counter), | |
| 587 | }) | |
| 588 | }) | |
| 589 | .unwrap(); | |
| 590 | target | |
| 591 | } else { | |
| 592 | file | |
| 593 | }; | |
| 594 | let deadline = Instant::now() + Duration::from_millis(300); | |
| 595 | while Instant::now() < deadline { | |
| 596 | assert_eq!(section.pending().unwrap(), pending); | |
| 597 | assert_eq!(fs::read(&target).unwrap(), source); | |
| 598 | assert!(calls.iter().all(|count| count.load(Ordering::SeqCst) == 0)); | |
| 599 | std::thread::sleep(Duration::from_millis(10)); | |
| 600 | } | |
| 601 | section.wake(); | |
| 602 | let deadline = Instant::now() + Duration::from_secs(10); | |
| 603 | loop { | |
| 604 | notified | |
| 605 | .recv_timeout(deadline.saturating_duration_since(Instant::now())) | |
| 606 | .unwrap(); | |
| 607 | if section.events().iter().any(|event| matches!(event, | |
| 608 | Event::Attempt { id: published, status: EditStatus::Published { .. } } if *published == id)) { | |
| 609 | break; | |
| 610 | } | |
| 611 | } | |
| 612 | assert!(section.pending().unwrap().is_empty()); | |
| 613 | assert_eq!(calls[0].load(Ordering::SeqCst), 1); | |
| 614 | assert_eq!(calls[3].load(Ordering::SeqCst), 1); | |
| 615 | section.close().unwrap(); | |
| 616 | let pages = stored_pages(&fs::read(&target).unwrap()).unwrap(); | |
| 617 | assert_eq!( | |
| 618 | pages | |
| 619 | .iter() | |
| 620 | .filter(|page| page.page.title == "Offline initialization marker") | |
| 621 | .count(), | |
| 622 | 1 | |
| 623 | ); | |
| 624 | assert!(Replica::open(&cache).unwrap().pending().unwrap().is_empty()); | |
| 625 | } | |
| 626 | } |
crates/notebook/src/smb/tests.rs+1-1| ... | ... | @@ -783,7 +783,7 @@ fn live_structure() { |
| 783 | 783 | let header = onestore::Store::parse(&renamed).unwrap().header; |
| 784 | 784 | assert_eq!(header.ancestor, toc_id); |
| 785 | 785 | assert_eq!(header.name_crc, 0x6108912a); |
| 786 | assert!(fresh.section("Renamed.one", || {}).is_err()); | |
| 786 | assert!(fresh.section("Renamed.one", false, || {}).is_err()); | |
| 787 | 787 | // A password supersedes the file under writer coordination, never over a newer stamp, and |
| 788 | 788 | // while a reader holds it open, as OneNote's readers share it with deletion. |
| 789 | 789 | let section = format!("{root}/Renamed.one"); |
crates/notebook/src/worker.rs+3-1| ... | ... | @@ -179,12 +179,13 @@ impl Replica { |
| 179 | 179 | F: FnMut() -> io::Result<R> + Send + 'static, |
| 180 | 180 | O: FnMut(&Result<Synced>) + Send + 'static, |
| 181 | 181 | { |
| 182 | self.start_sync_replacing(interval, connect, observe, None) | |
| 182 | self.start_sync_replacing(interval, false, connect, observe, None) | |
| 183 | 183 | } |
| 184 | 184 | |
| 185 | 185 | pub(crate) fn start_sync_replacing<R, F, O>( |
| 186 | 186 | self: &Arc<Self>, |
| 187 | 187 | interval: Duration, |
| 188 | offline: bool, | |
| 188 | 189 | mut connect: F, |
| 189 | 190 | mut observe: O, |
| 190 | 191 | replacing: Option<&SyncWorker>, |
| ... | ... | @@ -214,6 +215,7 @@ impl Replica { |
| 214 | 215 | return Err(io::ErrorKind::WouldBlock.into()); |
| 215 | 216 | } |
| 216 | 217 | let (signal, receiver) = Signal::new(); |
| 218 | signal.offline.store(offline, Ordering::Release); | |
| 217 | 219 | if let Some(previous) = previous { |
| 218 | 220 | signal |
| 219 | 221 | .offline |
crates/notebook/tests/copy.rs+1-1| ... | ... | @@ -152,7 +152,7 @@ fn pages_copy_into_another_section_with_their_content_and_fresh_identities() { |
| 152 | 152 | onestore::create_section("copies.one", "Destination", "Author").unwrap(), |
| 153 | 153 | ) |
| 154 | 154 | .unwrap(); |
| 155 | let section = Section::open(&file, &cache, || {}).unwrap(); | |
| 155 | let section = Section::open(&file, &cache, false, || {}).unwrap(); | |
| 156 | 156 | let mut expected = Vec::new(); |
| 157 | 157 | let mut grouped = Vec::new(); |
| 158 | 158 | for source in SOURCES { |
crates/notebook/tests/links.rs+1-1| ... | ... | @@ -30,7 +30,7 @@ fn internal_links_find_their_page_by_identity() { |
| 30 | 30 | ) |
| 31 | 31 | .unwrap(); |
| 32 | 32 | let mut notebook = Notebook::open(&root, temporary.path().join("cache")).unwrap(); |
| 33 | let section = notebook.section("First.one", || {}).unwrap(); | |
| 33 | let section = notebook.section("First.one", false, || {}).unwrap(); | |
| 34 | 34 | let (space, _, _) = section.pages().unwrap()[0]; |
| 35 | 35 | let page = section.page(space).unwrap(); |
| 36 | 36 | section.close().unwrap(); |
crates/notebook/tests/live_share.rs+56| ... | ... | @@ -53,6 +53,62 @@ fn a_guest_edits_the_host_s_notebook() { |
| 53 | 53 | }); |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | #[test] | |
| 57 | fn an_offline_guest_reopens_its_queue_while_the_host_remains_reachable() { | |
| 58 | use notebook::{Remote, Replica, live::share::HostedRemote, session::Section}; | |
| 59 | use std::{thread, time::Duration}; | |
| 60 | ||
| 61 | let directory = tempfile::tempdir().unwrap(); | |
| 62 | let folder = notebook(directory.path()); | |
| 63 | let url = relay(Default::default()); | |
| 64 | let sharing = Sharing::new("").unwrap(); | |
| 65 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | |
| 66 | let (guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace")); | |
| 67 | let file = folder.join("Garden.one"); | |
| 68 | let source = std::fs::read(&file).unwrap(); | |
| 69 | let cache = notebook.replica_path("Garden.one").unwrap(); | |
| 70 | std::fs::create_dir_all(cache.parent().unwrap()).unwrap(); | |
| 71 | let replica = | |
| 72 | Replica::open_or_create(&cache, None, || notebook.read_section("Garden.one")).unwrap(); | |
| 73 | let section = Section::resume_hosted( | |
| 74 | "Garden.one".into(), | |
| 75 | replica, | |
| 76 | Arc::clone(&guest), | |
| 77 | true, | |
| 78 | || {}, | |
| 79 | ) | |
| 80 | .unwrap(); | |
| 81 | let id = replace(&section, &source, 0..0, "Hosted offline reopen marker "); | |
| 82 | let pending = section.pending().unwrap(); | |
| 83 | assert_eq!(pending.len(), 1); | |
| 84 | section.close().unwrap(); | |
| 85 | let section = Section::resume_hosted( | |
| 86 | "Garden.one".into(), | |
| 87 | Replica::open(&cache).unwrap(), | |
| 88 | Arc::clone(&guest), | |
| 89 | true, | |
| 90 | || {}, | |
| 91 | ) | |
| 92 | .unwrap(); | |
| 93 | let mut probe = HostedRemote::new(&guest, "Garden.one"); | |
| 94 | let stamp = onestore::Stamp::of(&source).unwrap(); | |
| 95 | for _ in 0..10 { | |
| 96 | assert_eq!(probe.stamp().unwrap(), stamp); | |
| 97 | assert_eq!(section.pending().unwrap(), pending); | |
| 98 | assert_eq!(std::fs::read(&file).unwrap(), source); | |
| 99 | thread::sleep(Duration::from_millis(20)); | |
| 100 | } | |
| 101 | section.wake(); | |
| 102 | published(&section, id); | |
| 103 | assert_eq!( | |
| 104 | server::text(&std::fs::read(&file).unwrap()).2, | |
| 105 | "Hosted offline reopen marker Original text" | |
| 106 | ); | |
| 107 | assert!(section.pending().unwrap().is_empty()); | |
| 108 | section.close().unwrap(); | |
| 109 | host.stop(); | |
| 110 | } | |
| 111 | ||
| 56 | 112 | #[test] |
| 57 | 113 | fn presence_reconnect_reconciles_missed_changes_without_reconnecting_access() { |
| 58 | 114 | use notebook::{ |
crates/notebook/tests/location.rs+8-8| ... | ... | @@ -91,7 +91,7 @@ fn until(what: &str, mut accept: impl FnMut() -> bool) { |
| 91 | 91 | /// edit queued in its replica. |
| 92 | 92 | fn queued(root: &Path, cache: &Path, text: &str) -> ExGuid { |
| 93 | 93 | let notebook = Notebook::open(root, cache).unwrap(); |
| 94 | let section = notebook.section("First.one", || {}).unwrap(); | |
| 94 | let section = notebook.section("First.one", false, || {}).unwrap(); | |
| 95 | 95 | section.set_offline(true); |
| 96 | 96 | let space = typed(&section, text); |
| 97 | 97 | assert_eq!(section.pending().unwrap().len(), 1); |
| ... | ... | @@ -102,7 +102,7 @@ fn queued(root: &Path, cache: &Path, text: &str) -> ExGuid { |
| 102 | 102 | /// Opens `First.one` of `root` and waits for it to publish what it holds. |
| 103 | 103 | fn published(root: &Path, cache: &Path) -> usize { |
| 104 | 104 | let notebook = Notebook::open(root, cache).unwrap(); |
| 105 | let section = notebook.section("First.one", || {}).unwrap(); | |
| 105 | let section = notebook.section("First.one", false, || {}).unwrap(); | |
| 106 | 106 | let pending = section.pending().unwrap().len(); |
| 107 | 107 | until("the queue published", || { |
| 108 | 108 | section.sync_status().unwrap().queued == 0 |
| ... | ... | @@ -128,8 +128,8 @@ fn copies_of_a_notebook_keep_their_own_replicas_and_publish_only_to_themselves() |
| 128 | 128 | ); |
| 129 | 129 | // Both open at once: neither waits on the other's replica. |
| 130 | 130 | let (mine, theirs) = ( |
| 131 | a.section("First.one", || {}).unwrap(), | |
| 132 | b.section("First.one", || {}).unwrap(), | |
| 131 | a.section("First.one", false, || {}).unwrap(), | |
| 132 | b.section("First.one", false, || {}).unwrap(), | |
| 133 | 133 | ); |
| 134 | 134 | mine.set_offline(true); |
| 135 | 135 | let space = typed(&mine, "Original "); |
| ... | ... | @@ -242,7 +242,7 @@ fn a_replica_newer_than_a_surviving_copy_stays_behind() { |
| 242 | 242 | // The original moves on past the copy, then queues an edit and goes away. |
| 243 | 243 | { |
| 244 | 244 | let notebook = Notebook::open(&root, &cache).unwrap(); |
| 245 | let section = notebook.section("First.one", || {}).unwrap(); | |
| 245 | let section = notebook.section("First.one", false, || {}).unwrap(); | |
| 246 | 246 | typed(&section, "Published "); |
| 247 | 247 | until("the original published", || { |
| 248 | 248 | section.sync_status().unwrap().queued == 0 |
| ... | ... | @@ -254,7 +254,7 @@ fn a_replica_newer_than_a_surviving_copy_stays_behind() { |
| 254 | 254 | assert_eq!(published(&copy, &cache), 0); |
| 255 | 255 | let space = Notebook::open(&copy, &cache) |
| 256 | 256 | .unwrap() |
| 257 | .section("First.one", || {}) | |
| 257 | .section("First.one", false, || {}) | |
| 258 | 258 | .unwrap() |
| 259 | 259 | .pages() |
| 260 | 260 | .unwrap()[0] |
| ... | ... | @@ -297,8 +297,8 @@ fn a_section_copy_beside_its_original_opens_with_its_own_replica() { |
| 297 | 297 | .count(), |
| 298 | 298 | 2 |
| 299 | 299 | ); |
| 300 | let original = notebook.section("First.one", || {}).unwrap(); | |
| 301 | let copy = notebook.section("First 2.one", || {}).unwrap(); | |
| 300 | let original = notebook.section("First.one", false, || {}).unwrap(); | |
| 301 | let copy = notebook.section("First 2.one", false, || {}).unwrap(); | |
| 302 | 302 | original.set_offline(true); |
| 303 | 303 | let space = typed(&original, "Mine "); |
| 304 | 304 | assert!(copy.pending().unwrap().is_empty()); |
crates/notebook/tests/protected.rs+24-16| ... | ... | @@ -107,10 +107,10 @@ fn a_locked_section_unlocks_into_a_session() { |
| 107 | 107 | notebook.unlock(&section.path, "wrong"), |
| 108 | 108 | Err(notebook::Error::Protected(Error::PasswordMismatch)) |
| 109 | 109 | )); |
| 110 | assert!(notebook.section(&section.path, || {}).is_err()); | |
| 110 | assert!(notebook.section(&section.path, false, || {}).is_err()); | |
| 111 | 111 | let key = notebook.unlock(&section.path, &password).unwrap(); |
| 112 | 112 | let session = notebook |
| 113 | .section_unlocked(&section.path, &key, || {}) | |
| 113 | .section_unlocked(&section.path, &key, false, || {}) | |
| 114 | 114 | .unwrap(); |
| 115 | 115 | let pages = session.pages().unwrap(); |
| 116 | 116 | assert_eq!(pages.len(), 11); |
| ... | ... | @@ -274,7 +274,11 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 274 | 274 | let mut notebook = Notebook::create(&folder, &cache, 0x00f0_a0c0, &creation).unwrap(); |
| 275 | 275 | let path = notebook.catalog().sections[0].path.clone(); |
| 276 | 276 | let plain = notebook.replica_path(&path).unwrap(); |
| 277 | notebook.section(&path, || {}).unwrap().close().unwrap(); | |
| 277 | notebook | |
| 278 | .section(&path, false, || {}) | |
| 279 | .unwrap() | |
| 280 | .close() | |
| 281 | .unwrap(); | |
| 278 | 282 | assert!(plain.exists()); |
| 279 | 283 | let identity = notebook.catalog().sections[0].file_id; |
| 280 | 284 | |
| ... | ... | @@ -300,7 +304,9 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 300 | 304 | assert!(!image.windows(utf16.len()).any(|w| w == utf16)); |
| 301 | 305 | let key = notebook.unlock(&path, "first").unwrap(); |
| 302 | 306 | assert_eq!(key.secret(), first.secret()); |
| 303 | let session = notebook.section_unlocked(&path, &key, || {}).unwrap(); | |
| 307 | let session = notebook | |
| 308 | .section_unlocked(&path, &key, false, || {}) | |
| 309 | .unwrap(); | |
| 304 | 310 | assert_eq!(session.pages().unwrap()[0].1, TEXT); |
| 305 | 311 | session.close().unwrap(); |
| 306 | 312 | assert!(!leaks(&notebook.replica_path(&path).unwrap(), TEXT)); |
| ... | ... | @@ -311,7 +317,9 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 311 | 317 | .unwrap() |
| 312 | 318 | .unwrap(); |
| 313 | 319 | assert!(notebook.unlock(&path, "first").is_err()); |
| 314 | let session = notebook.section_unlocked(&path, &second, || {}).unwrap(); | |
| 320 | let session = notebook | |
| 321 | .section_unlocked(&path, &second, false, || {}) | |
| 322 | .unwrap(); | |
| 315 | 323 | assert_eq!(session.pages().unwrap()[0].1, TEXT); |
| 316 | 324 | session.close().unwrap(); |
| 317 | 325 | |
| ... | ... | @@ -323,7 +331,7 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 323 | 331 | ); |
| 324 | 332 | let section = &notebook.catalog().sections[0]; |
| 325 | 333 | assert!(matches!(section.state, SectionState::Readable { .. })); |
| 326 | let session = notebook.section(&path, || {}).unwrap(); | |
| 334 | let session = notebook.section(&path, false, || {}).unwrap(); | |
| 327 | 335 | assert_eq!(session.pages().unwrap()[0].1, TEXT); |
| 328 | 336 | } |
| 329 | 337 | |
| ... | ... | @@ -341,7 +349,7 @@ fn a_password_waits_for_queued_edits() { |
| 341 | 349 | ) |
| 342 | 350 | .unwrap(); |
| 343 | 351 | let path = notebook.catalog().sections[0].path.clone(); |
| 344 | let session = notebook.section(&path, || {}).unwrap(); | |
| 352 | let session = notebook.section(&path, false, || {}).unwrap(); | |
| 345 | 353 | session.set_offline(true); |
| 346 | 354 | let (space, ..) = session.pages().unwrap()[0].clone(); |
| 347 | 355 | let page = session.page(space).unwrap(); |
| ... | ... | @@ -384,7 +392,7 @@ fn a_replica_drops_its_plaintext_once_its_section_is_protected_elsewhere() { |
| 384 | 392 | const TITLE: &str = "Read on the other device"; |
| 385 | 393 | let temporary = tempfile::tempdir().unwrap(); |
| 386 | 394 | let (mut here, mut there, path) = two_devices(temporary.path(), TITLE); |
| 387 | there.section(&path, || {}).unwrap().close().unwrap(); | |
| 395 | there.section(&path, false, || {}).unwrap().close().unwrap(); | |
| 388 | 396 | let replica = there.replica_path(&path).unwrap(); |
| 389 | 397 | assert!(leaks(&replica, TITLE)); |
| 390 | 398 | here.set_password(&path, None, Some("secret")).unwrap(); |
| ... | ... | @@ -408,7 +416,7 @@ fn an_open_section_protected_elsewhere_stops_as_protected() { |
| 408 | 416 | const TITLE: &str = "Open while protected"; |
| 409 | 417 | let temporary = tempfile::tempdir().unwrap(); |
| 410 | 418 | let (mut here, mut there, path) = two_devices(temporary.path(), TITLE); |
| 411 | let session = there.section(&path, || {}).unwrap(); | |
| 419 | let session = there.section(&path, false, || {}).unwrap(); | |
| 412 | 420 | let replica = there.replica_path(&path).unwrap(); |
| 413 | 421 | here.set_password(&path, None, Some("secret")).unwrap(); |
| 414 | 422 | let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); |
| ... | ... | @@ -435,7 +443,7 @@ fn edits_queued_before_a_password_publish_under_it_once_unlocked() { |
| 435 | 443 | const TYPED: &str = "Typed offline meanwhile"; |
| 436 | 444 | let temporary = tempfile::tempdir().unwrap(); |
| 437 | 445 | let (mut here, mut there, path) = two_devices(temporary.path(), TITLE); |
| 438 | let session = there.section(&path, || {}).unwrap(); | |
| 446 | let session = there.section(&path, false, || {}).unwrap(); | |
| 439 | 447 | session.set_offline(true); |
| 440 | 448 | let (space, ..) = session.pages().unwrap()[0].clone(); |
| 441 | 449 | let page = session.page(space).unwrap(); |
| ... | ... | @@ -457,7 +465,7 @@ fn edits_queued_before_a_password_publish_under_it_once_unlocked() { |
| 457 | 465 | |
| 458 | 466 | let key = there.unlock(&path, "secret").unwrap(); |
| 459 | 467 | assert!(!held.exists()); |
| 460 | let session = there.section_unlocked(&path, &key, || {}).unwrap(); | |
| 468 | let session = there.section_unlocked(&path, &key, false, || {}).unwrap(); | |
| 461 | 469 | let titles: Vec<String> = session |
| 462 | 470 | .pages() |
| 463 | 471 | .unwrap() |
| ... | ... | @@ -523,7 +531,7 @@ fn a_notebook_protected_through_its_sessions() { |
| 523 | 531 | let names = ["Protected", "Edited", "Changed", "Removed", "Conflicted"]; |
| 524 | 532 | for name in names { |
| 525 | 533 | let path = notebook.create_section("", name, &creation).unwrap(); |
| 526 | let section = notebook.section(&path, || {}).unwrap(); | |
| 534 | let section = notebook.section(&path, false, || {}).unwrap(); | |
| 527 | 535 | for stored in &pages { |
| 528 | 536 | section.import_page(&stored.page, "Snowbound Test").unwrap(); |
| 529 | 537 | } |
| ... | ... | @@ -544,7 +552,7 @@ fn a_notebook_protected_through_its_sessions() { |
| 544 | 552 | } |
| 545 | 553 | // Edited under its key: text on every page, published as dependent revisions. |
| 546 | 554 | let section = notebook |
| 547 | .section_unlocked(&path("Edited"), &keys["Edited"], || {}) | |
| 555 | .section_unlocked(&path("Edited"), &keys["Edited"], false, || {}) | |
| 548 | 556 | .unwrap(); |
| 549 | 557 | for round in ["first", "second", "third"] { |
| 550 | 558 | append(&section, &format!(" Edited {round} under the key.")); |
| ... | ... | @@ -562,7 +570,7 @@ fn a_notebook_protected_through_its_sessions() { |
| 562 | 570 | .unwrap(); |
| 563 | 571 | passwords.insert(path("Changed"), "Changed again".into()); |
| 564 | 572 | let section = notebook |
| 565 | .section_unlocked(&path("Changed"), &changed, || {}) | |
| 573 | .section_unlocked(&path("Changed"), &changed, false, || {}) | |
| 566 | 574 | .unwrap(); |
| 567 | 575 | append(&section, " Written under the changed password."); |
| 568 | 576 | published(&section); |
| ... | ... | @@ -580,10 +588,10 @@ fn a_notebook_protected_through_its_sessions() { |
| 580 | 588 | let second = Notebook::open(&folder, temporary.path().join("second")).unwrap(); |
| 581 | 589 | let key = &keys["Conflicted"]; |
| 582 | 590 | let ours = notebook |
| 583 | .section_unlocked(&path("Conflicted"), key, || {}) | |
| 591 | .section_unlocked(&path("Conflicted"), key, false, || {}) | |
| 584 | 592 | .unwrap(); |
| 585 | 593 | let theirs = second |
| 586 | .section_unlocked(&path("Conflicted"), key, || {}) | |
| 594 | .section_unlocked(&path("Conflicted"), key, false, || {}) | |
| 587 | 595 | .unwrap(); |
| 588 | 596 | for (section, word) in [(&ours, " Ours offline."), (&theirs, " Theirs offline.")] { |
| 589 | 597 | section.set_offline(true); |
crates/notebook/tests/session.rs+53-18| ... | ... | @@ -65,7 +65,7 @@ fn typed( |
| 65 | 65 | fn open(file: &Path, cache: &Path) -> (Section, Arc<AtomicUsize>) { |
| 66 | 66 | let notified = Arc::new(AtomicUsize::new(0)); |
| 67 | 67 | let counter = Arc::clone(&notified); |
| 68 | let section = Section::open(file, cache, move || { | |
| 68 | let section = Section::open(file, cache, false, move || { | |
| 69 | 69 | counter.fetch_add(1, Ordering::SeqCst); |
| 70 | 70 | }) |
| 71 | 71 | .unwrap(); |
| ... | ... | @@ -369,7 +369,7 @@ fn a_notebook_directory_lists_its_sections_and_opens_them() { |
| 369 | 369 | assert_eq!(catalog.groups.len(), 1); |
| 370 | 370 | assert_eq!(catalog.groups[0].sections.len(), 1); |
| 371 | 371 | let path = catalog.groups[0].sections[0].path.clone(); |
| 372 | let section = notebook.section(&path, || {}).unwrap(); | |
| 372 | let section = notebook.section(&path, false, || {}).unwrap(); | |
| 373 | 373 | let (space, _, _) = section.pages().unwrap()[0]; |
| 374 | 374 | let page = section.page(space).unwrap(); |
| 375 | 375 | let text = first_text(&page); |
| ... | ... | @@ -409,7 +409,7 @@ fn a_notebook_only_opens_discovered_section_paths() { |
| 409 | 409 | std::fs::write(root.join("added.one"), &source).unwrap(); |
| 410 | 410 | for path in ["../outside.one", outside.to_str().unwrap(), "added.one"] { |
| 411 | 411 | assert!(matches!( |
| 412 | notebook.section(path, || {}), | |
| 412 | notebook.section(path, false, || {}), | |
| 413 | 413 | Err(notebook::Error::Io(error)) if error.kind() == std::io::ErrorKind::NotFound |
| 414 | 414 | )); |
| 415 | 415 | } |
| ... | ... | @@ -433,7 +433,7 @@ fn a_catalog_section_replaced_by_an_outside_symlink_is_rejected() { |
| 433 | 433 | std::fs::remove_file(&file).unwrap(); |
| 434 | 434 | std::os::unix::fs::symlink(&outside, &file).unwrap(); |
| 435 | 435 | assert!(matches!( |
| 436 | notebook.section("notes.one", || {}), | |
| 436 | notebook.section("notes.one", false, || {}), | |
| 437 | 437 | Err(notebook::Error::Io(error)) if error.kind() == std::io::ErrorKind::PermissionDenied |
| 438 | 438 | )); |
| 439 | 439 | assert_eq!(replicas(&cache), 0); |
| ... | ... | @@ -447,7 +447,7 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { |
| 447 | 447 | let cache = directory.path().join("replica.sqlite"); |
| 448 | 448 | let source = onestore::create_section("remote.one", "Original", "Author").unwrap(); |
| 449 | 449 | let replica = notebook::Replica::create(&cache, &source).unwrap(); |
| 450 | let section = Section::resume(&file, replica, || {}).unwrap(); | |
| 450 | let section = Section::resume(&file, replica, false, || {}).unwrap(); | |
| 451 | 451 | let space = section.pages().unwrap()[0].0; |
| 452 | 452 | let before = section.page(space).unwrap(); |
| 453 | 453 | let after = edited(&before, "First "); |
| ... | ... | @@ -460,7 +460,13 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { |
| 460 | 460 | section.close().unwrap(); |
| 461 | 461 | assert!(!file.exists()); |
| 462 | 462 | |
| 463 | let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), || {}).unwrap(); | |
| 463 | let section = Section::resume( | |
| 464 | &file, | |
| 465 | notebook::Replica::open(&cache).unwrap(), | |
| 466 | false, | |
| 467 | || {}, | |
| 468 | ) | |
| 469 | .unwrap(); | |
| 464 | 470 | assert_same(section.page(space).unwrap(), &after); |
| 465 | 471 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); |
| 466 | 472 | let before = section.page(space).unwrap(); |
| ... | ... | @@ -474,7 +480,13 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { |
| 474 | 480 | published(&section, next); |
| 475 | 481 | assert_same(stored_page(&file, space), &after); |
| 476 | 482 | section.close().unwrap(); |
| 477 | let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), || {}).unwrap(); | |
| 483 | let section = Section::resume( | |
| 484 | &file, | |
| 485 | notebook::Replica::open(&cache).unwrap(), | |
| 486 | false, | |
| 487 | || {}, | |
| 488 | ) | |
| 489 | .unwrap(); | |
| 478 | 490 | assert_same(section.page(space).unwrap(), &after); |
| 479 | 491 | assert!(section.pending().unwrap().is_empty()); |
| 480 | 492 | section.close().unwrap(); |
| ... | ... | @@ -487,7 +499,7 @@ fn resuming_against_another_document_preserves_both_remote_and_pending_edit() { |
| 487 | 499 | let cache = directory.path().join("replica.sqlite"); |
| 488 | 500 | let source = onestore::create_section("notes.one", "Original", "Author").unwrap(); |
| 489 | 501 | let replica = notebook::Replica::create(&cache, &source).unwrap(); |
| 490 | let section = Section::resume(&file, replica, || {}).unwrap(); | |
| 502 | let section = Section::resume(&file, replica, false, || {}).unwrap(); | |
| 491 | 503 | let space = section.pages().unwrap()[0].0; |
| 492 | 504 | let before = section.page(space).unwrap(); |
| 493 | 505 | let after = edited(&before, "Local "); |
| ... | ... | @@ -497,7 +509,13 @@ fn resuming_against_another_document_preserves_both_remote_and_pending_edit() { |
| 497 | 509 | |
| 498 | 510 | let other = onestore::create_section("other.one", "Unrelated", "Other").unwrap(); |
| 499 | 511 | std::fs::write(&file, &other).unwrap(); |
| 500 | let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), || {}).unwrap(); | |
| 512 | let section = Section::resume( | |
| 513 | &file, | |
| 514 | notebook::Replica::open(&cache).unwrap(), | |
| 515 | false, | |
| 516 | || {}, | |
| 517 | ) | |
| 518 | .unwrap(); | |
| 501 | 519 | wait(&section, |event| matches!(event, Event::Failed(_))); |
| 502 | 520 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); |
| 503 | 521 | assert_same(section.page(space).unwrap(), &after); |
| ... | ... | @@ -515,7 +533,7 @@ fn offline_save_process() { |
| 515 | 533 | }; |
| 516 | 534 | let root = std::path::PathBuf::from(root); |
| 517 | 535 | let replica = notebook::Replica::open(root.join("replica.sqlite")).unwrap(); |
| 518 | let section = Section::resume(root.join("absent.one"), replica, || {}).unwrap(); | |
| 536 | let section = Section::resume(root.join("absent.one"), replica, false, || {}).unwrap(); | |
| 519 | 537 | let space = section.pages().unwrap()[0].0; |
| 520 | 538 | let before = section.page(space).unwrap(); |
| 521 | 539 | let id = typed(&section, space, &before, 0..0, "Durable "); |
| ... | ... | @@ -542,7 +560,13 @@ fn an_acknowledged_offline_save_survives_process_exit_without_cleanup() { |
| 542 | 560 | .unwrap(); |
| 543 | 561 | let file = directory.path().join("absent.one"); |
| 544 | 562 | assert!(!file.exists()); |
| 545 | let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), || {}).unwrap(); | |
| 563 | let section = Section::resume( | |
| 564 | &file, | |
| 565 | notebook::Replica::open(&cache).unwrap(), | |
| 566 | false, | |
| 567 | || {}, | |
| 568 | ) | |
| 569 | .unwrap(); | |
| 546 | 570 | let space = section.pages().unwrap()[0].0; |
| 547 | 571 | let original = model_ops::page_of(&source, space); |
| 548 | 572 | let after = edited(&original, "Durable "); |
| ... | ... | @@ -568,6 +592,7 @@ fn smb_connection_failure_keeps_the_session_locally_editable_and_retries() { |
| 568 | 592 | counter.fetch_add(1, Ordering::SeqCst); |
| 569 | 593 | Err(std::io::ErrorKind::PermissionDenied.into()) |
| 570 | 594 | }, |
| 595 | false, | |
| 571 | 596 | || {}, |
| 572 | 597 | ) |
| 573 | 598 | .unwrap(); |
| ... | ... | @@ -619,6 +644,7 @@ fn live_smb_session_save_publishes_and_reopens() { |
| 619 | 644 | replica, |
| 620 | 645 | 1024 * 1024, |
| 621 | 646 | connect.clone(), |
| 647 | false, | |
| 622 | 648 | || {}, |
| 623 | 649 | ) |
| 624 | 650 | .unwrap(); |
| ... | ... | @@ -635,6 +661,7 @@ fn live_smb_session_save_publishes_and_reopens() { |
| 635 | 661 | notebook::Replica::open(&cache).unwrap(), |
| 636 | 662 | 1024 * 1024, |
| 637 | 663 | connect, |
| 664 | false, | |
| 638 | 665 | || {}, |
| 639 | 666 | ) |
| 640 | 667 | .unwrap(); |
| ... | ... | @@ -666,6 +693,7 @@ fn dropping_during_connection_keeps_cache_owned_until_the_worker_finishes() { |
| 666 | 693 | stalled.recv_timeout(Duration::from_secs(120)).unwrap(); |
| 667 | 694 | Err(std::io::ErrorKind::TimedOut.into()) |
| 668 | 695 | }, |
| 696 | false, | |
| 669 | 697 | || {}, |
| 670 | 698 | ) |
| 671 | 699 | .unwrap(); |
| ... | ... | @@ -817,9 +845,14 @@ fn an_uncertain_attempt_is_released_after_restart_by_review() { |
| 817 | 845 | |
| 818 | 846 | let notified = Arc::new(AtomicUsize::new(0)); |
| 819 | 847 | let counter = Arc::clone(&notified); |
| 820 | let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), move || { | |
| 821 | counter.fetch_add(1, Ordering::SeqCst); | |
| 822 | }) | |
| 848 | let section = Section::resume( | |
| 849 | &file, | |
| 850 | notebook::Replica::open(&cache).unwrap(), | |
| 851 | false, | |
| 852 | move || { | |
| 853 | counter.fetch_add(1, Ordering::SeqCst); | |
| 854 | }, | |
| 855 | ) | |
| 823 | 856 | .unwrap(); |
| 824 | 857 | wait( |
| 825 | 858 | &section, |
| ... | ... | @@ -1043,7 +1076,7 @@ fn a_closed_sections_queued_edits_publish_in_the_background_when_online() { |
| 1043 | 1076 | background.watch(notebook.replicas()); |
| 1044 | 1077 | // The round started with the background ends before the edit exists. |
| 1045 | 1078 | std::thread::sleep(Duration::from_millis(200)); |
| 1046 | let section = notebook.section("Second.one", || {}).unwrap(); | |
| 1079 | let section = notebook.section("Second.one", false, || {}).unwrap(); | |
| 1047 | 1080 | section.set_offline(true); |
| 1048 | 1081 | let space = section.pages().unwrap()[0].0; |
| 1049 | 1082 | let before = section.page(space).unwrap(); |
| ... | ... | @@ -1070,7 +1103,7 @@ fn a_closed_sections_queued_edits_publish_in_the_background_when_online() { |
| 1070 | 1103 | drop(background); |
| 1071 | 1104 | let mut reopened = None; |
| 1072 | 1105 | until("the background released the replica", || { |
| 1073 | reopened = notebook.section("Second.one", || {}).ok(); | |
| 1106 | reopened = notebook.section("Second.one", false, || {}).ok(); | |
| 1074 | 1107 | reopened.is_some() |
| 1075 | 1108 | }); |
| 1076 | 1109 | let reopened = reopened.unwrap(); |
| ... | ... | @@ -1084,7 +1117,7 @@ fn a_remote_change_to_a_closed_section_is_noticed_and_rebases_its_replica() { |
| 1084 | 1117 | let mut notebook = two_sections(directory.path()); |
| 1085 | 1118 | // Second has a replica from being opened once; First has never been opened. |
| 1086 | 1119 | notebook |
| 1087 | .section("Second.one", || {}) | |
| 1120 | .section("Second.one", false, || {}) | |
| 1088 | 1121 | .unwrap() |
| 1089 | 1122 | .close() |
| 1090 | 1123 | .unwrap(); |
| ... | ... | @@ -1190,7 +1223,7 @@ fn every_section_gets_an_offline_copy_that_closing_the_notebook_discards() { |
| 1190 | 1223 | .all(|(_, status)| status.synced.is_some()) |
| 1191 | 1224 | }); |
| 1192 | 1225 | // Opening a section starts from its copy, and edits made offline wait in it. |
| 1193 | let section = notebook.section("Second.one", || {}).unwrap(); | |
| 1226 | let section = notebook.section("Second.one", false, || {}).unwrap(); | |
| 1194 | 1227 | section.set_offline(true); |
| 1195 | 1228 | let space = section.pages().unwrap()[0].0; |
| 1196 | 1229 | let before = section.page(space).unwrap(); |
| ... | ... | @@ -1313,6 +1346,7 @@ fn a_moved_section_keeps_its_replica_while_the_old_remote_is_stalled() { |
| 1313 | 1346 | late_error, |
| 1314 | 1347 | }) |
| 1315 | 1348 | }, |
| 1349 | false, | |
| 1316 | 1350 | || {}, |
| 1317 | 1351 | ) |
| 1318 | 1352 | .unwrap(); |
| ... | ... | @@ -1392,6 +1426,7 @@ fn an_open_section_the_background_holds_checks_its_file_when_the_watch_reports_i |
| 1392 | 1426 | .section_with( |
| 1393 | 1427 | "Second.one", |
| 1394 | 1428 | move |file| Ok(Counted(file.to_owned(), Arc::clone(&counter))), |
| 1429 | false, | |
| 1395 | 1430 | || {}, |
| 1396 | 1431 | ) |
| 1397 | 1432 | .unwrap(); |
crates/notebook/tests/smb_queue.rs+1| ... | ... | @@ -197,6 +197,7 @@ fn live_session_applies_edits_through_the_embedded_client_and_reopens() { |
| 197 | 197 | Duration::from_secs(5), |
| 198 | 198 | ) |
| 199 | 199 | }, |
| 200 | false, | |
| 200 | 201 | || {}, |
| 201 | 202 | ) |
| 202 | 203 | .unwrap(); |
crates/notebook/tests/support/live.rs+1-1| ... | ... | @@ -121,7 +121,7 @@ pub fn open(notebook: &Notebook, guest: &Arc<Guest>, path: &str, key: Option<&Ke |
| 121 | 121 | let replica = notebook.replica_path(path).unwrap(); |
| 122 | 122 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); |
| 123 | 123 | let replica = Replica::open_or_create(&replica, key, || notebook.read_section(path)).unwrap(); |
| 124 | Section::resume_hosted(path.into(), replica, Arc::clone(guest), || {}).unwrap() | |
| 124 | Section::resume_hosted(path.into(), replica, Arc::clone(guest), false, || {}).unwrap() | |
| 125 | 125 | } |
| 126 | 126 | |
| 127 | 127 | pub fn replace(section: &Section, image: &[u8], range: std::ops::Range<u32>, with: &str) -> u64 { |
crates/snowbound/src/icloud_linux.rs+5-3| ... | ... | @@ -26,20 +26,22 @@ pub fn section( |
| 26 | 26 | _: &Path, |
| 27 | 27 | path: &str, |
| 28 | 28 | key: Option<&onestore::protected::Key>, |
| 29 | offline: bool, | |
| 29 | 30 | notify: impl Fn() + Send + 'static, |
| 30 | 31 | ) -> Result<Section, notebook::Error> { |
| 31 | 32 | match key { |
| 32 | Some(key) => notebook.section_unlocked(path, key, notify), | |
| 33 | None => notebook.section(path, notify), | |
| 33 | Some(key) => notebook.section_unlocked(path, key, offline, notify), | |
| 34 | None => notebook.section(path, offline, notify), | |
| 34 | 35 | } |
| 35 | 36 | } |
| 36 | 37 | |
| 37 | 38 | pub fn lone_section( |
| 38 | 39 | file: &Path, |
| 39 | 40 | cache: &Path, |
| 41 | offline: bool, | |
| 40 | 42 | notify: impl Fn() + Send + 'static, |
| 41 | 43 | ) -> Result<Section, notebook::Error> { |
| 42 | Section::open(file, cache, notify) | |
| 44 | Section::open(file, cache, offline, notify) | |
| 43 | 45 | } |
| 44 | 46 | |
| 45 | 47 | pub fn background( |
crates/snowbound/src/icloud_macos.rs+12-4| ... | ... | @@ -365,6 +365,7 @@ pub fn section( |
| 365 | 365 | root: &Path, |
| 366 | 366 | path: &str, |
| 367 | 367 | key: Option<&onestore::protected::Key>, |
| 368 | offline: bool, | |
| 368 | 369 | notify: impl Fn() + Send + 'static, |
| 369 | 370 | ) -> Result<Section, notebook::Error> { |
| 370 | 371 | if !notebook.replica_path(path)?.exists() { |
| ... | ... | @@ -372,8 +373,8 @@ pub fn section( |
| 372 | 373 | } |
| 373 | 374 | let connect = |file: &Path| Ok(Coordinated(file.to_owned())); |
| 374 | 375 | let section = match key { |
| 375 | Some(key) => notebook.section_unlocked_with(path, key, connect, notify)?, | |
| 376 | None => notebook.section_with(path, connect, notify)?, | |
| 376 | Some(key) => notebook.section_unlocked_with(path, key, connect, offline, notify)?, | |
| 377 | None => notebook.section_with(path, connect, offline, notify)?, | |
| 377 | 378 | }; |
| 378 | 379 | section.set_pause(PAUSE); |
| 379 | 380 | Ok(section) |
| ... | ... | @@ -392,10 +393,17 @@ fn fetch(file: &Path) -> io::Result<()> { |
| 392 | 393 | pub fn lone_section( |
| 393 | 394 | file: &Path, |
| 394 | 395 | cache: &Path, |
| 396 | offline: bool, | |
| 395 | 397 | notify: impl Fn() + Send + 'static, |
| 396 | 398 | ) -> Result<Section, notebook::Error> { |
| 397 | 399 | fetch(file)?; |
| 398 | let section = Section::open_with(file, cache, |file| Ok(Coordinated(file.to_owned())), notify)?; | |
| 400 | let section = Section::open_with( | |
| 401 | file, | |
| 402 | cache, | |
| 403 | |file| Ok(Coordinated(file.to_owned())), | |
| 404 | offline, | |
| 405 | notify, | |
| 406 | )?; | |
| 399 | 407 | section.set_pause(PAUSE); |
| 400 | 408 | Ok(section) |
| 401 | 409 | } |
| ... | ... | @@ -582,7 +590,7 @@ mod tests { |
| 582 | 590 | let notebook = Notebook::create(&root, &cache, Notebook::NEW_COLOR, &page).unwrap(); |
| 583 | 591 | assert!(ubiquitous(&root)); |
| 584 | 592 | let path = notebook.catalog().sections[0].path.clone(); |
| 585 | let section = section(&notebook, &root, &path, None, || {}).unwrap(); | |
| 593 | let section = section(&notebook, &root, &path, None, false, || {}).unwrap(); | |
| 586 | 594 | let (space, ..) = section.pages().unwrap()[0].clone(); |
| 587 | 595 | let title = section |
| 588 | 596 | .page(space) |
crates/snowbound/src/library.rs+21-7| ... | ... | @@ -991,7 +991,13 @@ impl Library { |
| 991 | 991 | }) |
| 992 | 992 | .and_then(|replica| { |
| 993 | 993 | let guest = Arc::clone(&joined.guest); |
| 994 | Section::resume_hosted(path.to_owned(), replica, guest, notifier()) | |
| 994 | Section::resume_hosted( | |
| 995 | path.to_owned(), | |
| 996 | replica, | |
| 997 | guest, | |
| 998 | offline(), | |
| 999 | notifier(), | |
| 1000 | ) | |
| 995 | 1001 | }) |
| 996 | 1002 | } |
| 997 | 1003 | #[cfg(target_arch = "wasm32")] |
| ... | ... | @@ -1010,21 +1016,28 @@ impl Library { |
| 1010 | 1016 | replica.and_then(|replica| { |
| 1011 | 1017 | let server = Arc::clone(server); |
| 1012 | 1018 | let connect = move || server.connect(); |
| 1013 | Section::resume_smb(file, replica, LIMIT, connect, notifier()) | |
| 1019 | Section::resume_smb(file, replica, LIMIT, connect, offline(), notifier()) | |
| 1014 | 1020 | }) |
| 1015 | 1021 | } |
| 1016 | 1022 | (Ok(Some(notebook)), None) if self.in_icloud() => { |
| 1017 | 1023 | let root = Path::new(&self.location); |
| 1018 | crate::icloud::section(notebook, root, path, key.as_ref(), notifier()) | |
| 1024 | crate::icloud::section( | |
| 1025 | notebook, | |
| 1026 | root, | |
| 1027 | path, | |
| 1028 | key.as_ref(), | |
| 1029 | offline(), | |
| 1030 | notifier(), | |
| 1031 | ) | |
| 1019 | 1032 | } |
| 1020 | 1033 | (Ok(Some(notebook)), None) => match &key { |
| 1021 | Some(key) => notebook.section_unlocked(path, key, notifier()), | |
| 1022 | None => notebook.section(path, notifier()), | |
| 1034 | Some(key) => notebook.section_unlocked(path, key, offline(), notifier()), | |
| 1035 | None => notebook.section(path, offline(), notifier()), | |
| 1023 | 1036 | }, |
| 1024 | 1037 | (Ok(None), _) if self.in_icloud() => { |
| 1025 | crate::icloud::lone_section(Path::new(path), &self.cache, notifier()) | |
| 1038 | crate::icloud::lone_section(Path::new(path), &self.cache, offline(), notifier()) | |
| 1026 | 1039 | } |
| 1027 | (Ok(None), _) => Section::open(path, &self.cache, notifier()), | |
| 1040 | (Ok(None), _) => Section::open(path, &self.cache, offline(), notifier()), | |
| 1028 | 1041 | (Err(error), _) => return Err(error.clone().into()), |
| 1029 | 1042 | }; |
| 1030 | 1043 | match opened { |
| ... | ... | @@ -1763,6 +1776,7 @@ mod tests { |
| 1763 | 1776 | release: Arc::clone(&gate), |
| 1764 | 1777 | }) |
| 1765 | 1778 | }, |
| 1779 | false, | |
| 1766 | 1780 | || {}, |
| 1767 | 1781 | ) |
| 1768 | 1782 | .unwrap(); |
crates/snowbound/src/main.rs+3-3| ... | ... | @@ -7246,7 +7246,7 @@ mod tests { |
| 7246 | 7246 | ) |
| 7247 | 7247 | .unwrap(); |
| 7248 | 7248 | let section = |
| 7249 | notebook::session::Section::open(&file, directory.join("cache"), || {}).unwrap(); | |
| 7249 | notebook::session::Section::open(&file, directory.join("cache"), false, || {}).unwrap(); | |
| 7250 | 7250 | let mut engine = TextEngine::default(); |
| 7251 | 7251 | for (space, ..) in section.pages().unwrap().into_iter().take(3) { |
| 7252 | 7252 | let mut editor = |
| ... | ... | @@ -7348,7 +7348,7 @@ mod tests { |
| 7348 | 7348 | &file, |
| 7349 | 7349 | ) |
| 7350 | 7350 | .unwrap(); |
| 7351 | let ours = Section::open(&file, directory.join("ours"), || {}).unwrap(); | |
| 7351 | let ours = Section::open(&file, directory.join("ours"), false, || {}).unwrap(); | |
| 7352 | 7352 | let space = ours.pages().unwrap()[0].0; |
| 7353 | 7353 | let mut engine = TextEngine::default(); |
| 7354 | 7354 | let mut editor = CanvasEditor::from_page(ours.page(space).unwrap(), &mut engine).unwrap(); |
| ... | ... | @@ -7378,7 +7378,7 @@ mod tests { |
| 7378 | 7378 | "{seen:?}" |
| 7379 | 7379 | ); |
| 7380 | 7380 | |
| 7381 | let theirs = Section::open(&file, directory.join("theirs"), || {}).unwrap(); | |
| 7381 | let theirs = Section::open(&file, directory.join("theirs"), false, || {}).unwrap(); | |
| 7382 | 7382 | let mut other = CanvasEditor::from_page(theirs.page(space).unwrap(), &mut engine).unwrap(); |
| 7383 | 7383 | other.focus_outline(outline).unwrap(); |
| 7384 | 7384 | let last = other.active_outline().document().paragraphs().count() - 1; |