| 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,11 +631,13 @@ impl Library { |
| 631 | path: &str, | 631 | path: &str, |
| 632 | notify: impl Fn() + Send + 'static, | 632 | notify: impl Fn() + Send + 'static, |
| 633 | ) -> Result<session::Section> { | 633 | ) -> Result<session::Section> { |
| 634 | let offline = self.offline.load(Ordering::Relaxed); | ||
| 634 | match &self.place { | 635 | match &self.place { |
| 635 | Place::File(file) => Ok(session::Section::open_with( | 636 | Place::File(file) => Ok(session::Section::open_with( |
| 636 | file, | 637 | file, |
| 637 | &self.cache, | 638 | &self.cache, |
| 638 | |file| Ok(Coordinated(file.to_owned())), | 639 | |file| Ok(Coordinated(file.to_owned())), |
| 640 | offline, | ||
| 639 | notify, | 641 | notify, |
| 640 | )?), | 642 | )?), |
| 641 | Place::Folder(_) => { | 643 | Place::Folder(_) => { |
| ... | @@ -643,8 +645,10 @@ impl Library { | ... | @@ -643,8 +645,10 @@ impl Library { |
| 643 | let notebook = notebook.as_ref().ok_or("A lone section has no notebook")?; | 645 | let notebook = notebook.as_ref().ok_or("A lone section has no notebook")?; |
| 644 | let connect = |file: &Path| Ok(Coordinated(file.to_owned())); | 646 | let connect = |file: &Path| Ok(Coordinated(file.to_owned())); |
| 645 | Ok(match self.unlocked().get(path) { | 647 | Ok(match self.unlocked().get(path) { |
| 646 | Some(key) => notebook.section_unlocked_with(path, key, connect, notify)?, | 648 | Some(key) => { |
| 647 | None => notebook.section_with(path, connect, notify)?, | 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 | Place::Share { server, root, .. } => { | 654 | Place::Share { server, root, .. } => { |
| ... | @@ -682,6 +686,7 @@ impl Library { | ... | @@ -682,6 +686,7 @@ impl Library { |
| 682 | replica, | 686 | replica, |
| 683 | LIMIT, | 687 | LIMIT, |
| 684 | move || server.connect(), | 688 | move || server.connect(), |
| 689 | offline, | ||
| 685 | notify, | 690 | notify, |
| 686 | )?) | 691 | )?) |
| 687 | } | 692 | } |
| ... | @@ -857,6 +862,9 @@ impl Section { | ... | @@ -857,6 +862,9 @@ impl Section { |
| 857 | .open | 862 | .open |
| 858 | .lock() | 863 | .lock() |
| 859 | .unwrap_or_else(|error| error.into_inner()); | 864 | .unwrap_or_else(|error| error.into_inner()); |
| 865 | shared | ||
| 866 | .section | ||
| 867 | .set_offline(library.offline.load(Ordering::Relaxed)); | ||
| 860 | open.push((path.to_owned(), Arc::downgrade(&shared))); | 868 | open.push((path.to_owned(), Arc::downgrade(&shared))); |
| 861 | drop(open); | 869 | drop(open); |
| 862 | Ok(Self { shared, library }) | 870 | Ok(Self { shared, library }) |
| ... | @@ -1785,13 +1793,24 @@ pub extern "C" fn sb_library_sync_status(library: &Library) -> *mut c_char { | ... | @@ -1785,13 +1793,24 @@ pub extern "C" fn sb_library_sync_status(library: &Library) -> *mut c_char { |
| 1785 | /// `sb_library_sync_now`. Sections opened later follow. | 1793 | /// `sb_library_sync_now`. Sections opened later follow. |
| 1786 | #[unsafe(no_mangle)] | 1794 | #[unsafe(no_mangle)] |
| 1787 | pub extern "C" fn sb_library_set_offline(library: &Library, offline: bool) { | 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 | library.offline.store(offline, Ordering::Relaxed); | 1800 | library.offline.store(offline, Ordering::Relaxed); |
| 1789 | if let Some(background) = &library.background { | 1801 | if let Some(background) = &library.background { |
| 1790 | background.set_offline(offline); | 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 | shared.section.set_offline(offline); | 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 | /// Checks the sections at or below `path`, `/`-separated from the notebook's folder, as the | 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,6 +376,91 @@ fn cached_cloud_sections_reopen_with_offline_edits_after_eviction() { |
| 376 | assert_eq!(stored.page(space).unwrap().color, Some(0x00ef_cdab)); | 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 | #[test] | 464 | #[test] |
| 380 | fn a_new_page_and_subpage_list_after_their_parent_and_delete_to_the_recycle_bin() { | 465 | fn a_new_page_and_subpage_list_after_their_parent_and_delete_to_the_recycle_bin() { |
| 381 | let (directory, root) = copy("m6/native-features-01/notebook"); | 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,8 +751,53 @@ fn a_notebook_on_a_share_saves_and_opens_offline() { |
| 666 | let arena = onestore::Arena::default(); | 751 | let arena = onestore::Arena::default(); |
| 667 | let stored = onestore::Section::open(&arena, published).unwrap(); | 752 | let stored = onestore::Section::open(&arena, published).unwrap(); |
| 668 | assert!(canvas::search::page_text(&stored.page(canvas.space).unwrap()).contains("Shared ")); | 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 | let shared = Arc::clone(&section.shared); | 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 | Arc::into_inner(shared).unwrap().section.close().unwrap(); | 801 | Arc::into_inner(shared).unwrap().section.close().unwrap(); |
| 672 | // The same notebook while its server does not answer: a closed port on the lab's host. | 802 | // The same notebook while its server does not answer: a closed port on the lab's host. |
| 673 | let host = address.split(':').next().unwrap(); | 803 | let host = address.split(':').next().unwrap(); |
crates/notebook/README.md+8-7| ... | @@ -52,14 +52,15 @@ thread rereads the queue. | ... | @@ -52,14 +52,15 @@ thread rereads the queue. |
| 52 | `session::Notebook::open(root, cache_dir)` discovers a notebook directory, reading only | 52 | `session::Notebook::open(root, cache_dir)` discovers a notebook directory, reading only |
| 53 | the files its folders list otherwise than when last opened (`discover::Cache`, kept under | 53 | the files its folders list otherwise than when last opened (`discover::Cache`, kept under |
| 54 | `cache_dir/listings`), and | 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 | a replica named by the file's location and the section's document identity, so the same | 56 | a replica named by the file's location and the section's document identity, so the same |
| 57 | file reopens the same queue after a relaunch and a copy of it elsewhere has its own | 57 | file reopens the same queue after a relaunch and a copy of it elsewhere has its own |
| 58 | (`notebook::location`: `folder`, `local`, `smb`, `moved` for a notebook the app moves, | 58 | (`notebook::location`: `folder`, `local`, `smb`, `moved` for a notebook the app moves, |
| 59 | and `forget` for one it deletes). | 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 | named by the identity its discovery read resumes without reading the file, which the | 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 | A section is `Send + Sync`: `apply(author, edit)` returns at once and reports a refusal as `Event::Rejected`; `events()` drains | 64 | A section is `Send + Sync`: `apply(author, edit)` returns at once and reports a refusal as `Event::Rejected`; `events()` drains |
| 64 | remote changes (`Changed(spaces)`), publication outcomes, unreachable files and | 65 | remote changes (`Changed(spaces)`), publication outcomes, unreachable files and |
| 65 | failures; `notify` runs on a background thread whenever an event waits. | 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,9 +97,9 @@ changed, `wake` and `set_offline` follow Sync Now and Work Offline, and `discard |
| 96 | thread and deletes the replicas holding nothing unpublished, as when the notebook closes. | 97 | thread and deletes the replicas holding nothing unpublished, as when the notebook closes. |
| 97 | `Notebook::replica_path(path)` names a section's replica for either kind of notebook. | 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 | consulting the remote file; with the `smb` feature, `Section::resume_smb(path, | 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 | the worker again after transport failure. | 103 | the worker again after transport failure. |
| 103 | 104 | ||
| 104 | ## Reconciliation | 105 | ## Reconciliation |
| ... | @@ -358,7 +359,7 @@ sections it has not opened this way, and unread changes leave out the reader's o | ... | @@ -358,7 +359,7 @@ sections it has not opened this way, and unread changes leave out the reader's o |
| 358 | 359 | ||
| 359 | A password-protected section lists as `Locked`. `Notebook::unlock(path, password)` opens | 360 | A password-protected section lists as `Locked`. `Notebook::unlock(path, password)` opens |
| 360 | its key (a wrong password is `Error::Protected(PasswordMismatch)`), and | 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 | `Replica::open_or_create(path, Some(&key), source)` for a host that opens replicas itself) | 363 | `Replica::open_or_create(path, Some(&key), source)` for a host that opens replicas itself) |
| 363 | opens it as any section opens: edits queue, publish, merge and become conflict pages as an ordinary section's do, | 364 | opens it as any section opens: edits queue, publish, merge and become conflict pages as an ordinary section's do, |
| 364 | each revision sealed under the section's key. The replica keeps none of it in the clear: | 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,7 +568,7 @@ on, and `stop()` lets every guest go. `join(hello, code, password, reach, relay) |
| 567 | `Welcome` (the share and its secret), or a `Refusal` saying why not: a wrong code, no one | 568 | `Welcome` (the share and its secret), or a `Refusal` saying why not: a wrong code, no one |
| 568 | sharing it, an expired code, too many wrong tries, or no relay. `Guest::start` joins the share; | 569 | sharing it, an expired code, too many wrong tries, or no relay. `Guest::start` joins the share; |
| 569 | `Notebook::open_hosted(guest, cache)`, `Background::hosted(guest, notify)` and | 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 | `Guest::host()` and `stopped()` for whether the host is there and still sharing. | 572 | `Guest::host()` and `stopped()` for whether the host is there and still sharing. |
| 572 | 573 | ||
| 573 | ## Queue measurement | 574 | ## Queue measurement |
crates/notebook/examples/browser_receipts.rs+1-1| ... | @@ -190,7 +190,7 @@ mod web { | ... | @@ -190,7 +190,7 @@ mod web { |
| 190 | ) | 190 | ) |
| 191 | .unwrap(); | 191 | .unwrap(); |
| 192 | let original_cache = notebook.replica_path("New Section 1.one").unwrap(); | 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 | section.set_offline(true); | 194 | section.set_offline(true); |
| 195 | let (space, text_id, _) = text(&section.replica().snapshot().unwrap()); | 195 | let (space, text_id, _) = text(&section.replica().snapshot().unwrap()); |
| 196 | let replica = Arc::clone(section.replica()); | 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,8 +191,14 @@ fn join(name: &str, code: &str, url: &str, cache: &Path) -> Open { |
| 191 | let image = notebook.read_section("Garden.one").unwrap(); | 191 | let image = notebook.read_section("Garden.one").unwrap(); |
| 192 | let file = onestore::Header::parse(&image[..1024]).unwrap().file_id; | 192 | let file = onestore::Header::parse(&image[..1024]).unwrap().file_id; |
| 193 | let replica = Replica::open_or_create(&replica, None, || Ok(image)).unwrap(); | 193 | let replica = Replica::open_or_create(&replica, None, || Ok(image)).unwrap(); |
| 194 | let section = | 194 | let section = Section::resume_hosted( |
| 195 | Section::resume_hosted("Garden.one".into(), replica, Arc::clone(&guest), || {}).unwrap(); | 195 | "Garden.one".into(), |
| 196 | replica, | ||
| 197 | Arc::clone(&guest), | ||
| 198 | false, | ||
| 199 | || {}, | ||
| 200 | ) | ||
| 201 | .unwrap(); | ||
| 196 | background.hold("Garden.one", &section); | 202 | background.hold("Garden.one", &section); |
| 197 | Open { | 203 | Open { |
| 198 | guest, | 204 | guest, |
crates/notebook/examples/live_latency.rs+8-2| ... | @@ -69,8 +69,14 @@ fn guest( | ... | @@ -69,8 +69,14 @@ fn guest( |
| 69 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); | 69 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); |
| 70 | let replica = | 70 | let replica = |
| 71 | Replica::open_or_create(&replica, None, || notebook.read_section("Garden.one")).unwrap(); | 71 | Replica::open_or_create(&replica, None, || notebook.read_section("Garden.one")).unwrap(); |
| 72 | let section = | 72 | let section = Section::resume_hosted( |
| 73 | Section::resume_hosted("Garden.one".into(), replica, Arc::clone(&guest), || {}).unwrap(); | 73 | "Garden.one".into(), |
| 74 | replica, | ||
| 75 | Arc::clone(&guest), | ||
| 76 | false, | ||
| 77 | || {}, | ||
| 78 | ) | ||
| 79 | .unwrap(); | ||
| 74 | background.hold("Garden.one", &section); | 80 | background.hold("Garden.one", &section); |
| 75 | ( | 81 | ( |
| 76 | guest, | 82 | guest, |
crates/notebook/examples/open_probe.rs+1-1| ... | @@ -133,7 +133,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { | ... | @@ -133,7 +133,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 133 | println!("section {} bytes", source.len()); | 133 | println!("section {} bytes", source.len()); |
| 134 | for label in ["fresh cache", "reopened cache"] { | 134 | for label in ["fresh cache", "reopened cache"] { |
| 135 | let start = Instant::now(); | 135 | let start = Instant::now(); |
| 136 | let section = Section::open(&file, &cache, || {})?; | 136 | let section = Section::open(&file, &cache, false, || {})?; |
| 137 | let opened = start.elapsed(); | 137 | let opened = start.elapsed(); |
| 138 | let pages = section.pages()?; | 138 | let pages = section.pages()?; |
| 139 | let listed = start.elapsed(); | 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,7 +21,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 21 | let directory = tempfile::tempdir()?; | 21 | let directory = tempfile::tempdir()?; |
| 22 | let file = directory.path().join("probe.one"); | 22 | let file = directory.path().join("probe.one"); |
| 23 | std::fs::write(&file, std::fs::read(source)?)?; | 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 | let words = [ | 25 | let words = [ |
| 26 | "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", | 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,7 +115,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 115 | return Err("Usage: session_client SECTION CACHE_DIR LABEL".into()); | 115 | return Err("Usage: session_client SECTION CACHE_DIR LABEL".into()); |
| 116 | }; | 116 | }; |
| 117 | let (notify, notified) = mpsc::channel(); | 117 | let (notify, notified) = mpsc::channel(); |
| 118 | let section = Section::open(file, cache, move || { | 118 | let section = Section::open(file, cache, false, move || { |
| 119 | let _ = notify.send(()); | 119 | let _ = notify.send(()); |
| 120 | })?; | 120 | })?; |
| 121 | let pending = section.pending()?; | 121 | let pending = section.pending()?; |
crates/notebook/src/background.rs+1-1| ... | @@ -1479,7 +1479,7 @@ mod tests { | ... | @@ -1479,7 +1479,7 @@ mod tests { |
| 1479 | let source = onestore::create_section("Held.one", "", "Author").unwrap(); | 1479 | let source = onestore::create_section("Held.one", "", "Author").unwrap(); |
| 1480 | fs::write(&file, &source).unwrap(); | 1480 | fs::write(&file, &source).unwrap(); |
| 1481 | let (changed, changes) = mpsc::channel(); | 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 | let _ = changed.send(()); | 1483 | let _ = changed.send(()); |
| 1484 | }) | 1484 | }) |
| 1485 | .unwrap(); | 1485 | .unwrap(); |
crates/notebook/src/session.rs+51-12| ... | @@ -1748,8 +1748,18 @@ impl Notebook { | ... | @@ -1748,8 +1748,18 @@ impl Notebook { |
| 1748 | } | 1748 | } |
| 1749 | 1749 | ||
| 1750 | /// Opens a section of a mounted notebook by its catalog path. | 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> { | 1751 | pub fn section( |
| 1752 | self.section_with(path, |file| Ok(FileRemote(file.to_owned())), notify) | 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 | /// `section`, reading and publishing through the remote `connect` makes for its file | 1765 | /// `section`, reading and publishing through the remote `connect` makes for its file |
| ... | @@ -1758,9 +1768,10 @@ impl Notebook { | ... | @@ -1758,9 +1768,10 @@ impl Notebook { |
| 1758 | &self, | 1768 | &self, |
| 1759 | path: &str, | 1769 | path: &str, |
| 1760 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, | 1770 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 1771 | offline: bool, | ||
| 1761 | notify: impl Fn() + Send + 'static, | 1772 | notify: impl Fn() + Send + 'static, |
| 1762 | ) -> Result<Section> { | 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 | /// `section` for a password-protected section, under the `key` `unlock` gave. | 1777 | /// `section` for a password-protected section, under the `key` `unlock` gave. |
| ... | @@ -1768,9 +1779,16 @@ impl Notebook { | ... | @@ -1768,9 +1779,16 @@ impl Notebook { |
| 1768 | &self, | 1779 | &self, |
| 1769 | path: &str, | 1780 | path: &str, |
| 1770 | key: &protected::Key, | 1781 | key: &protected::Key, |
| 1782 | offline: bool, | ||
| 1771 | notify: impl Fn() + Send + 'static, | 1783 | notify: impl Fn() + Send + 'static, |
| 1772 | ) -> Result<Section> { | 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 | /// `section_with` for a password-protected section, under its `key`. | 1794 | /// `section_with` for a password-protected section, under its `key`. |
| ... | @@ -1779,9 +1797,10 @@ impl Notebook { | ... | @@ -1779,9 +1797,10 @@ impl Notebook { |
| 1779 | path: &str, | 1797 | path: &str, |
| 1780 | key: &protected::Key, | 1798 | key: &protected::Key, |
| 1781 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, | 1799 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 1800 | offline: bool, | ||
| 1782 | notify: impl Fn() + Send + 'static, | 1801 | notify: impl Fn() + Send + 'static, |
| 1783 | ) -> Result<Section> { | 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 | fn open_section<R: Remote + 'static>( | 1806 | fn open_section<R: Remote + 'static>( |
| ... | @@ -1789,6 +1808,7 @@ impl Notebook { | ... | @@ -1789,6 +1808,7 @@ impl Notebook { |
| 1789 | path: &str, | 1808 | path: &str, |
| 1790 | key: Option<&protected::Key>, | 1809 | key: Option<&protected::Key>, |
| 1791 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, | 1810 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 1811 | offline: bool, | ||
| 1792 | notify: impl Fn() + Send + 'static, | 1812 | notify: impl Fn() + Send + 'static, |
| 1793 | ) -> Result<Section> { | 1813 | ) -> Result<Section> { |
| 1794 | let section = self.section_path(path)?; | 1814 | let section = self.section_path(path)?; |
| ... | @@ -1823,14 +1843,14 @@ impl Notebook { | ... | @@ -1823,14 +1843,14 @@ impl Notebook { |
| 1823 | if fs::metadata(&cache).is_ok() { | 1843 | if fs::metadata(&cache).is_ok() { |
| 1824 | let (replica, remote, mut connect) = | 1844 | let (replica, remote, mut connect) = |
| 1825 | (Replica::open(&cache)?, file.clone(), connect); | 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 | let replica = |identity: &[u8; 16], _: &[u8]| { | 1849 | let replica = |identity: &[u8; 16], _: &[u8]| { |
| 1830 | fs::create_dir_all(&replicas)?; | 1850 | fs::create_dir_all(&replicas)?; |
| 1831 | Ok(replica_file(&replicas, identity)) | 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,9 +2237,16 @@ impl Section { |
| 2217 | pub fn open( | 2237 | pub fn open( |
| 2218 | file: impl AsRef<Path>, | 2238 | file: impl AsRef<Path>, |
| 2219 | cache: impl AsRef<Path>, | 2239 | cache: impl AsRef<Path>, |
| 2240 | offline: bool, | ||
| 2220 | notify: impl Fn() + Send + 'static, | 2241 | notify: impl Fn() + Send + 'static, |
| 2221 | ) -> Result<Self> { | 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 | /// `open`, reading and publishing the file through the remote `connect` makes for it, | 2252 | /// `open`, reading and publishing the file through the remote `connect` makes for it, |
| ... | @@ -2229,6 +2256,7 @@ impl Section { | ... | @@ -2229,6 +2256,7 @@ impl Section { |
| 2229 | file: impl AsRef<Path>, | 2256 | file: impl AsRef<Path>, |
| 2230 | cache: impl AsRef<Path>, | 2257 | cache: impl AsRef<Path>, |
| 2231 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, | 2258 | connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 2259 | offline: bool, | ||
| 2232 | notify: impl Fn() + Send + 'static, | 2260 | notify: impl Fn() + Send + 'static, |
| 2233 | ) -> Result<Self> { | 2261 | ) -> Result<Self> { |
| 2234 | let file = fs::canonicalize(file)?; | 2262 | let file = fs::canonicalize(file)?; |
| ... | @@ -2239,7 +2267,7 @@ impl Section { | ... | @@ -2239,7 +2267,7 @@ impl Section { |
| 2239 | })?; | 2267 | })?; |
| 2240 | Ok(replica_file(&folder, identity)) | 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 | /// Opens the canonical section `file`, a protected one under `key`, through the replica | 2273 | /// Opens the canonical section `file`, a protected one under `key`, through the replica |
| ... | @@ -2249,6 +2277,7 @@ impl Section { | ... | @@ -2249,6 +2277,7 @@ impl Section { |
| 2249 | key: Option<&protected::Key>, | 2277 | key: Option<&protected::Key>, |
| 2250 | replica: impl FnOnce(&[u8; 16], &[u8]) -> Result<PathBuf>, | 2278 | replica: impl FnOnce(&[u8; 16], &[u8]) -> Result<PathBuf>, |
| 2251 | mut connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, | 2279 | mut connect: impl FnMut(&Path) -> io::Result<R> + Send + 'static, |
| 2280 | offline: bool, | ||
| 2252 | notify: impl Fn() + Send + 'static, | 2281 | notify: impl Fn() + Send + 'static, |
| 2253 | ) -> Result<Self> { | 2282 | ) -> Result<Self> { |
| 2254 | let source = connect(&file)?.read()?; | 2283 | let source = connect(&file)?.read()?; |
| ... | @@ -2257,7 +2286,7 @@ impl Section { | ... | @@ -2257,7 +2286,7 @@ impl Section { |
| 2257 | let cache = replica(&identity.guid, &source)?; | 2286 | let cache = replica(&identity.guid, &source)?; |
| 2258 | let replica = Replica::open_or_create(&cache, key, || Ok(source))?; | 2287 | let replica = Replica::open_or_create(&cache, key, || Ok(source))?; |
| 2259 | let remote = file.clone(); | 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 | /// Resumes an owned local replica without reading the publication target. | 2292 | /// Resumes an owned local replica without reading the publication target. |
| ... | @@ -2265,6 +2294,7 @@ impl Section { | ... | @@ -2265,6 +2294,7 @@ impl Section { |
| 2265 | pub fn resume( | 2294 | pub fn resume( |
| 2266 | file: impl AsRef<Path>, | 2295 | file: impl AsRef<Path>, |
| 2267 | replica: Replica, | 2296 | replica: Replica, |
| 2297 | offline: bool, | ||
| 2268 | notify: impl Fn() + Send + 'static, | 2298 | notify: impl Fn() + Send + 'static, |
| 2269 | ) -> Result<Self> { | 2299 | ) -> Result<Self> { |
| 2270 | let file = fs::absolute(file)?; | 2300 | let file = fs::absolute(file)?; |
| ... | @@ -2273,6 +2303,7 @@ impl Section { | ... | @@ -2273,6 +2303,7 @@ impl Section { |
| 2273 | file, | 2303 | file, |
| 2274 | replica, | 2304 | replica, |
| 2275 | move || Ok(FileRemote(remote.clone())), | 2305 | move || Ok(FileRemote(remote.clone())), |
| 2306 | offline, | ||
| 2276 | notify, | 2307 | notify, |
| 2277 | ) | 2308 | ) |
| 2278 | } | 2309 | } |
| ... | @@ -2285,12 +2316,14 @@ impl Section { | ... | @@ -2285,12 +2316,14 @@ impl Section { |
| 2285 | replica: Replica, | 2316 | replica: Replica, |
| 2286 | limit: usize, | 2317 | limit: usize, |
| 2287 | mut connect: impl FnMut() -> io::Result<crate::smb::Client> + Send + 'static, | 2318 | mut connect: impl FnMut() -> io::Result<crate::smb::Client> + Send + 'static, |
| 2319 | offline: bool, | ||
| 2288 | notify: impl Fn() + Send + 'static, | 2320 | notify: impl Fn() + Send + 'static, |
| 2289 | ) -> Result<Self> { | 2321 | ) -> Result<Self> { |
| 2290 | Self::start( | 2322 | Self::start( |
| 2291 | PathBuf::from(&path), | 2323 | PathBuf::from(&path), |
| 2292 | replica, | 2324 | replica, |
| 2293 | move || Ok(crate::SmbRemote::new(connect()?, path.clone(), limit)), | 2325 | move || Ok(crate::SmbRemote::new(connect()?, path.clone(), limit)), |
| 2326 | offline, | ||
| 2294 | notify, | 2327 | notify, |
| 2295 | ) | 2328 | ) |
| 2296 | } | 2329 | } |
| ... | @@ -2301,12 +2334,14 @@ impl Section { | ... | @@ -2301,12 +2334,14 @@ impl Section { |
| 2301 | path: String, | 2334 | path: String, |
| 2302 | replica: Replica, | 2335 | replica: Replica, |
| 2303 | guest: Arc<crate::live::share::Guest>, | 2336 | guest: Arc<crate::live::share::Guest>, |
| 2337 | offline: bool, | ||
| 2304 | notify: impl Fn() + Send + 'static, | 2338 | notify: impl Fn() + Send + 'static, |
| 2305 | ) -> Result<Self> { | 2339 | ) -> Result<Self> { |
| 2306 | Self::start( | 2340 | Self::start( |
| 2307 | PathBuf::from(&path), | 2341 | PathBuf::from(&path), |
| 2308 | replica, | 2342 | replica, |
| 2309 | move || Ok(crate::live::share::HostedRemote::new(&guest, &path)), | 2343 | move || Ok(crate::live::share::HostedRemote::new(&guest, &path)), |
| 2344 | offline, | ||
| 2310 | notify, | 2345 | notify, |
| 2311 | ) | 2346 | ) |
| 2312 | } | 2347 | } |
| ... | @@ -2315,6 +2350,7 @@ impl Section { | ... | @@ -2315,6 +2350,7 @@ impl Section { |
| 2315 | file: PathBuf, | 2350 | file: PathBuf, |
| 2316 | replica: Replica, | 2351 | replica: Replica, |
| 2317 | connect: impl FnMut() -> io::Result<R> + Send + 'static, | 2352 | connect: impl FnMut() -> io::Result<R> + Send + 'static, |
| 2353 | offline: bool, | ||
| 2318 | notify: impl Fn() + Send + 'static, | 2354 | notify: impl Fn() + Send + 'static, |
| 2319 | ) -> Result<Self> { | 2355 | ) -> Result<Self> { |
| 2320 | let replica = Arc::new(replica); | 2356 | let replica = Arc::new(replica); |
| ... | @@ -2335,19 +2371,21 @@ impl Section { | ... | @@ -2335,19 +2371,21 @@ impl Section { |
| 2335 | notify, | 2371 | notify, |
| 2336 | observed, | 2372 | observed, |
| 2337 | }; | 2373 | }; |
| 2338 | section.worker = Some(section.start_worker(connect, None)?); | 2374 | section.worker = Some(section.start_worker(connect, offline, None)?); |
| 2339 | Ok(section) | 2375 | Ok(section) |
| 2340 | } | 2376 | } |
| 2341 | 2377 | ||
| 2342 | fn start_worker<R: Remote + 'static>( | 2378 | fn start_worker<R: Remote + 'static>( |
| 2343 | &self, | 2379 | &self, |
| 2344 | connect: impl FnMut() -> io::Result<R> + Send + 'static, | 2380 | connect: impl FnMut() -> io::Result<R> + Send + 'static, |
| 2381 | offline: bool, | ||
| 2345 | replacing: Option<&SyncWorker>, | 2382 | replacing: Option<&SyncWorker>, |
| 2346 | ) -> Result<SyncWorker> { | 2383 | ) -> Result<SyncWorker> { |
| 2347 | let (sender, notify) = (self.sender.clone(), Arc::clone(&self.notify)); | 2384 | let (sender, notify) = (self.sender.clone(), Arc::clone(&self.notify)); |
| 2348 | let observed = Arc::clone(&self.observed); | 2385 | let observed = Arc::clone(&self.observed); |
| 2349 | Ok(self.replica.start_sync_replacing( | 2386 | Ok(self.replica.start_sync_replacing( |
| 2350 | Duration::from_secs(2), | 2387 | Duration::from_secs(2), |
| 2388 | offline, | ||
| 2351 | connect, | 2389 | connect, |
| 2352 | move |result| { | 2390 | move |result| { |
| 2353 | let error = result.as_ref().err().map(reached); | 2391 | let error = result.as_ref().err().map(reached); |
| ... | @@ -2413,7 +2451,8 @@ impl Section { | ... | @@ -2413,7 +2451,8 @@ impl Section { |
| 2413 | worker.cancel(); | 2451 | worker.cancel(); |
| 2414 | } | 2452 | } |
| 2415 | self.observed = Arc::new(Mutex::new(None)); | 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 | self.file = file; | 2456 | self.file = file; |
| 2418 | Ok(()) | 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,14 +9,18 @@ fn cached_sections_refuse_links_outside_the_notebook_and_dangling_links() { |
| 9 | let creation = PageCreation::new(None, Some("Mine"), "Fixture").unwrap(); | 9 | let creation = PageCreation::new(None, Some("Mine"), "Fixture").unwrap(); |
| 10 | let notebook = Notebook::create(&folder, &cache, 0x00f0_a0c0, &creation).unwrap(); | 10 | let notebook = Notebook::create(&folder, &cache, 0x00f0_a0c0, &creation).unwrap(); |
| 11 | let path = &notebook.catalog().sections[0].path; | 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 | let file = folder.join(path); | 17 | let file = folder.join(path); |
| 14 | let outside = temporary.path().join("outside.one"); | 18 | let outside = temporary.path().join("outside.one"); |
| 15 | fs::rename(&file, &outside).unwrap(); | 19 | fs::rename(&file, &outside).unwrap(); |
| 16 | std::os::unix::fs::symlink(&outside, &file).unwrap(); | 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 | fs::remove_file(outside).unwrap(); | 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 | #[test] | 26 | #[test] |
| ... | @@ -501,3 +505,122 @@ fn recycling_rejects_unconfirmed_appends_and_can_retry_each_stage() { | ... | @@ -501,3 +505,122 @@ fn recycling_rejects_unconfirmed_appends_and_can_retry_each_stage() { |
| 501 | assert_eq!(fs::read(folder.join(&path)).unwrap(), source); | 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,7 +783,7 @@ fn live_structure() { |
| 783 | let header = onestore::Store::parse(&renamed).unwrap().header; | 783 | let header = onestore::Store::parse(&renamed).unwrap().header; |
| 784 | assert_eq!(header.ancestor, toc_id); | 784 | assert_eq!(header.ancestor, toc_id); |
| 785 | assert_eq!(header.name_crc, 0x6108912a); | 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 | // A password supersedes the file under writer coordination, never over a newer stamp, and | 787 | // A password supersedes the file under writer coordination, never over a newer stamp, and |
| 788 | // while a reader holds it open, as OneNote's readers share it with deletion. | 788 | // while a reader holds it open, as OneNote's readers share it with deletion. |
| 789 | let section = format!("{root}/Renamed.one"); | 789 | let section = format!("{root}/Renamed.one"); |
crates/notebook/src/worker.rs+3-1| ... | @@ -179,12 +179,13 @@ impl Replica { | ... | @@ -179,12 +179,13 @@ impl Replica { |
| 179 | F: FnMut() -> io::Result<R> + Send + 'static, | 179 | F: FnMut() -> io::Result<R> + Send + 'static, |
| 180 | O: FnMut(&Result<Synced>) + Send + 'static, | 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 | pub(crate) fn start_sync_replacing<R, F, O>( | 185 | pub(crate) fn start_sync_replacing<R, F, O>( |
| 186 | self: &Arc<Self>, | 186 | self: &Arc<Self>, |
| 187 | interval: Duration, | 187 | interval: Duration, |
| 188 | offline: bool, | ||
| 188 | mut connect: F, | 189 | mut connect: F, |
| 189 | mut observe: O, | 190 | mut observe: O, |
| 190 | replacing: Option<&SyncWorker>, | 191 | replacing: Option<&SyncWorker>, |
| ... | @@ -214,6 +215,7 @@ impl Replica { | ... | @@ -214,6 +215,7 @@ impl Replica { |
| 214 | return Err(io::ErrorKind::WouldBlock.into()); | 215 | return Err(io::ErrorKind::WouldBlock.into()); |
| 215 | } | 216 | } |
| 216 | let (signal, receiver) = Signal::new(); | 217 | let (signal, receiver) = Signal::new(); |
| 218 | signal.offline.store(offline, Ordering::Release); | ||
| 217 | if let Some(previous) = previous { | 219 | if let Some(previous) = previous { |
| 218 | signal | 220 | signal |
| 219 | .offline | 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,7 +152,7 @@ fn pages_copy_into_another_section_with_their_content_and_fresh_identities() { |
| 152 | onestore::create_section("copies.one", "Destination", "Author").unwrap(), | 152 | onestore::create_section("copies.one", "Destination", "Author").unwrap(), |
| 153 | ) | 153 | ) |
| 154 | .unwrap(); | 154 | .unwrap(); |
| 155 | let section = Section::open(&file, &cache, || {}).unwrap(); | 155 | let section = Section::open(&file, &cache, false, || {}).unwrap(); |
| 156 | let mut expected = Vec::new(); | 156 | let mut expected = Vec::new(); |
| 157 | let mut grouped = Vec::new(); | 157 | let mut grouped = Vec::new(); |
| 158 | for source in SOURCES { | 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,7 +30,7 @@ fn internal_links_find_their_page_by_identity() { |
| 30 | ) | 30 | ) |
| 31 | .unwrap(); | 31 | .unwrap(); |
| 32 | let mut notebook = Notebook::open(&root, temporary.path().join("cache")).unwrap(); | 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 | let (space, _, _) = section.pages().unwrap()[0]; | 34 | let (space, _, _) = section.pages().unwrap()[0]; |
| 35 | let page = section.page(space).unwrap(); | 35 | let page = section.page(space).unwrap(); |
| 36 | section.close().unwrap(); | 36 | section.close().unwrap(); |
crates/notebook/tests/live_share.rs+56| ... | @@ -53,6 +53,62 @@ fn a_guest_edits_the_host_s_notebook() { | ... | @@ -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 | #[test] | 112 | #[test] |
| 57 | fn presence_reconnect_reconciles_missed_changes_without_reconnecting_access() { | 113 | fn presence_reconnect_reconciles_missed_changes_without_reconnecting_access() { |
| 58 | use notebook::{ | 114 | use notebook::{ |
crates/notebook/tests/location.rs+8-8| ... | @@ -91,7 +91,7 @@ fn until(what: &str, mut accept: impl FnMut() -> bool) { | ... | @@ -91,7 +91,7 @@ fn until(what: &str, mut accept: impl FnMut() -> bool) { |
| 91 | /// edit queued in its replica. | 91 | /// edit queued in its replica. |
| 92 | fn queued(root: &Path, cache: &Path, text: &str) -> ExGuid { | 92 | fn queued(root: &Path, cache: &Path, text: &str) -> ExGuid { |
| 93 | let notebook = Notebook::open(root, cache).unwrap(); | 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 | section.set_offline(true); | 95 | section.set_offline(true); |
| 96 | let space = typed(&section, text); | 96 | let space = typed(&section, text); |
| 97 | assert_eq!(section.pending().unwrap().len(), 1); | 97 | assert_eq!(section.pending().unwrap().len(), 1); |
| ... | @@ -102,7 +102,7 @@ fn queued(root: &Path, cache: &Path, text: &str) -> ExGuid { | ... | @@ -102,7 +102,7 @@ fn queued(root: &Path, cache: &Path, text: &str) -> ExGuid { |
| 102 | /// Opens `First.one` of `root` and waits for it to publish what it holds. | 102 | /// Opens `First.one` of `root` and waits for it to publish what it holds. |
| 103 | fn published(root: &Path, cache: &Path) -> usize { | 103 | fn published(root: &Path, cache: &Path) -> usize { |
| 104 | let notebook = Notebook::open(root, cache).unwrap(); | 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 | let pending = section.pending().unwrap().len(); | 106 | let pending = section.pending().unwrap().len(); |
| 107 | until("the queue published", || { | 107 | until("the queue published", || { |
| 108 | section.sync_status().unwrap().queued == 0 | 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,8 +128,8 @@ fn copies_of_a_notebook_keep_their_own_replicas_and_publish_only_to_themselves() |
| 128 | ); | 128 | ); |
| 129 | // Both open at once: neither waits on the other's replica. | 129 | // Both open at once: neither waits on the other's replica. |
| 130 | let (mine, theirs) = ( | 130 | let (mine, theirs) = ( |
| 131 | a.section("First.one", || {}).unwrap(), | 131 | a.section("First.one", false, || {}).unwrap(), |
| 132 | b.section("First.one", || {}).unwrap(), | 132 | b.section("First.one", false, || {}).unwrap(), |
| 133 | ); | 133 | ); |
| 134 | mine.set_offline(true); | 134 | mine.set_offline(true); |
| 135 | let space = typed(&mine, "Original "); | 135 | let space = typed(&mine, "Original "); |
| ... | @@ -242,7 +242,7 @@ fn a_replica_newer_than_a_surviving_copy_stays_behind() { | ... | @@ -242,7 +242,7 @@ fn a_replica_newer_than_a_surviving_copy_stays_behind() { |
| 242 | // The original moves on past the copy, then queues an edit and goes away. | 242 | // The original moves on past the copy, then queues an edit and goes away. |
| 243 | { | 243 | { |
| 244 | let notebook = Notebook::open(&root, &cache).unwrap(); | 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 | typed(&section, "Published "); | 246 | typed(&section, "Published "); |
| 247 | until("the original published", || { | 247 | until("the original published", || { |
| 248 | section.sync_status().unwrap().queued == 0 | 248 | section.sync_status().unwrap().queued == 0 |
| ... | @@ -254,7 +254,7 @@ fn a_replica_newer_than_a_surviving_copy_stays_behind() { | ... | @@ -254,7 +254,7 @@ fn a_replica_newer_than_a_surviving_copy_stays_behind() { |
| 254 | assert_eq!(published(&copy, &cache), 0); | 254 | assert_eq!(published(&copy, &cache), 0); |
| 255 | let space = Notebook::open(&copy, &cache) | 255 | let space = Notebook::open(&copy, &cache) |
| 256 | .unwrap() | 256 | .unwrap() |
| 257 | .section("First.one", || {}) | 257 | .section("First.one", false, || {}) |
| 258 | .unwrap() | 258 | .unwrap() |
| 259 | .pages() | 259 | .pages() |
| 260 | .unwrap()[0] | 260 | .unwrap()[0] |
| ... | @@ -297,8 +297,8 @@ fn a_section_copy_beside_its_original_opens_with_its_own_replica() { | ... | @@ -297,8 +297,8 @@ fn a_section_copy_beside_its_original_opens_with_its_own_replica() { |
| 297 | .count(), | 297 | .count(), |
| 298 | 2 | 298 | 2 |
| 299 | ); | 299 | ); |
| 300 | let original = notebook.section("First.one", || {}).unwrap(); | 300 | let original = notebook.section("First.one", false, || {}).unwrap(); |
| 301 | let copy = notebook.section("First 2.one", || {}).unwrap(); | 301 | let copy = notebook.section("First 2.one", false, || {}).unwrap(); |
| 302 | original.set_offline(true); | 302 | original.set_offline(true); |
| 303 | let space = typed(&original, "Mine "); | 303 | let space = typed(&original, "Mine "); |
| 304 | assert!(copy.pending().unwrap().is_empty()); | 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,10 +107,10 @@ fn a_locked_section_unlocks_into_a_session() { |
| 107 | notebook.unlock(&section.path, "wrong"), | 107 | notebook.unlock(&section.path, "wrong"), |
| 108 | Err(notebook::Error::Protected(Error::PasswordMismatch)) | 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 | let key = notebook.unlock(&section.path, &password).unwrap(); | 111 | let key = notebook.unlock(&section.path, &password).unwrap(); |
| 112 | let session = notebook | 112 | let session = notebook |
| 113 | .section_unlocked(&section.path, &key, || {}) | 113 | .section_unlocked(&section.path, &key, false, || {}) |
| 114 | .unwrap(); | 114 | .unwrap(); |
| 115 | let pages = session.pages().unwrap(); | 115 | let pages = session.pages().unwrap(); |
| 116 | assert_eq!(pages.len(), 11); | 116 | assert_eq!(pages.len(), 11); |
| ... | @@ -274,7 +274,11 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { | ... | @@ -274,7 +274,11 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 274 | let mut notebook = Notebook::create(&folder, &cache, 0x00f0_a0c0, &creation).unwrap(); | 274 | let mut notebook = Notebook::create(&folder, &cache, 0x00f0_a0c0, &creation).unwrap(); |
| 275 | let path = notebook.catalog().sections[0].path.clone(); | 275 | let path = notebook.catalog().sections[0].path.clone(); |
| 276 | let plain = notebook.replica_path(&path).unwrap(); | 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 | assert!(plain.exists()); | 282 | assert!(plain.exists()); |
| 279 | let identity = notebook.catalog().sections[0].file_id; | 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,7 +304,9 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 300 | assert!(!image.windows(utf16.len()).any(|w| w == utf16)); | 304 | assert!(!image.windows(utf16.len()).any(|w| w == utf16)); |
| 301 | let key = notebook.unlock(&path, "first").unwrap(); | 305 | let key = notebook.unlock(&path, "first").unwrap(); |
| 302 | assert_eq!(key.secret(), first.secret()); | 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 | assert_eq!(session.pages().unwrap()[0].1, TEXT); | 310 | assert_eq!(session.pages().unwrap()[0].1, TEXT); |
| 305 | session.close().unwrap(); | 311 | session.close().unwrap(); |
| 306 | assert!(!leaks(&notebook.replica_path(&path).unwrap(), TEXT)); | 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,7 +317,9 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 311 | .unwrap() | 317 | .unwrap() |
| 312 | .unwrap(); | 318 | .unwrap(); |
| 313 | assert!(notebook.unlock(&path, "first").is_err()); | 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 | assert_eq!(session.pages().unwrap()[0].1, TEXT); | 323 | assert_eq!(session.pages().unwrap()[0].1, TEXT); |
| 316 | session.close().unwrap(); | 324 | session.close().unwrap(); |
| 317 | 325 | ||
| ... | @@ -323,7 +331,7 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { | ... | @@ -323,7 +331,7 @@ fn a_password_is_set_changed_and_removed_in_a_notebook() { |
| 323 | ); | 331 | ); |
| 324 | let section = &notebook.catalog().sections[0]; | 332 | let section = &notebook.catalog().sections[0]; |
| 325 | assert!(matches!(section.state, SectionState::Readable { .. })); | 333 | assert!(matches!(section.state, SectionState::Readable { .. })); |
| 326 | let session = notebook.section(&path, || {}).unwrap(); | 334 | let session = notebook.section(&path, false, || {}).unwrap(); |
| 327 | assert_eq!(session.pages().unwrap()[0].1, TEXT); | 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,7 +349,7 @@ fn a_password_waits_for_queued_edits() { |
| 341 | ) | 349 | ) |
| 342 | .unwrap(); | 350 | .unwrap(); |
| 343 | let path = notebook.catalog().sections[0].path.clone(); | 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 | session.set_offline(true); | 353 | session.set_offline(true); |
| 346 | let (space, ..) = session.pages().unwrap()[0].clone(); | 354 | let (space, ..) = session.pages().unwrap()[0].clone(); |
| 347 | let page = session.page(space).unwrap(); | 355 | let page = session.page(space).unwrap(); |
| ... | @@ -384,7 +392,7 @@ fn a_replica_drops_its_plaintext_once_its_section_is_protected_elsewhere() { | ... | @@ -384,7 +392,7 @@ fn a_replica_drops_its_plaintext_once_its_section_is_protected_elsewhere() { |
| 384 | const TITLE: &str = "Read on the other device"; | 392 | const TITLE: &str = "Read on the other device"; |
| 385 | let temporary = tempfile::tempdir().unwrap(); | 393 | let temporary = tempfile::tempdir().unwrap(); |
| 386 | let (mut here, mut there, path) = two_devices(temporary.path(), TITLE); | 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 | let replica = there.replica_path(&path).unwrap(); | 396 | let replica = there.replica_path(&path).unwrap(); |
| 389 | assert!(leaks(&replica, TITLE)); | 397 | assert!(leaks(&replica, TITLE)); |
| 390 | here.set_password(&path, None, Some("secret")).unwrap(); | 398 | here.set_password(&path, None, Some("secret")).unwrap(); |
| ... | @@ -408,7 +416,7 @@ fn an_open_section_protected_elsewhere_stops_as_protected() { | ... | @@ -408,7 +416,7 @@ fn an_open_section_protected_elsewhere_stops_as_protected() { |
| 408 | const TITLE: &str = "Open while protected"; | 416 | const TITLE: &str = "Open while protected"; |
| 409 | let temporary = tempfile::tempdir().unwrap(); | 417 | let temporary = tempfile::tempdir().unwrap(); |
| 410 | let (mut here, mut there, path) = two_devices(temporary.path(), TITLE); | 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 | let replica = there.replica_path(&path).unwrap(); | 420 | let replica = there.replica_path(&path).unwrap(); |
| 413 | here.set_password(&path, None, Some("secret")).unwrap(); | 421 | here.set_password(&path, None, Some("secret")).unwrap(); |
| 414 | let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); | 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,7 +443,7 @@ fn edits_queued_before_a_password_publish_under_it_once_unlocked() { |
| 435 | const TYPED: &str = "Typed offline meanwhile"; | 443 | const TYPED: &str = "Typed offline meanwhile"; |
| 436 | let temporary = tempfile::tempdir().unwrap(); | 444 | let temporary = tempfile::tempdir().unwrap(); |
| 437 | let (mut here, mut there, path) = two_devices(temporary.path(), TITLE); | 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 | session.set_offline(true); | 447 | session.set_offline(true); |
| 440 | let (space, ..) = session.pages().unwrap()[0].clone(); | 448 | let (space, ..) = session.pages().unwrap()[0].clone(); |
| 441 | let page = session.page(space).unwrap(); | 449 | let page = session.page(space).unwrap(); |
| ... | @@ -457,7 +465,7 @@ fn edits_queued_before_a_password_publish_under_it_once_unlocked() { | ... | @@ -457,7 +465,7 @@ fn edits_queued_before_a_password_publish_under_it_once_unlocked() { |
| 457 | 465 | ||
| 458 | let key = there.unlock(&path, "secret").unwrap(); | 466 | let key = there.unlock(&path, "secret").unwrap(); |
| 459 | assert!(!held.exists()); | 467 | assert!(!held.exists()); |
| 460 | let session = there.section_unlocked(&path, &key, || {}).unwrap(); | 468 | let session = there.section_unlocked(&path, &key, false, || {}).unwrap(); |
| 461 | let titles: Vec<String> = session | 469 | let titles: Vec<String> = session |
| 462 | .pages() | 470 | .pages() |
| 463 | .unwrap() | 471 | .unwrap() |
| ... | @@ -523,7 +531,7 @@ fn a_notebook_protected_through_its_sessions() { | ... | @@ -523,7 +531,7 @@ fn a_notebook_protected_through_its_sessions() { |
| 523 | let names = ["Protected", "Edited", "Changed", "Removed", "Conflicted"]; | 531 | let names = ["Protected", "Edited", "Changed", "Removed", "Conflicted"]; |
| 524 | for name in names { | 532 | for name in names { |
| 525 | let path = notebook.create_section("", name, &creation).unwrap(); | 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 | for stored in &pages { | 535 | for stored in &pages { |
| 528 | section.import_page(&stored.page, "Snowbound Test").unwrap(); | 536 | section.import_page(&stored.page, "Snowbound Test").unwrap(); |
| 529 | } | 537 | } |
| ... | @@ -544,7 +552,7 @@ fn a_notebook_protected_through_its_sessions() { | ... | @@ -544,7 +552,7 @@ fn a_notebook_protected_through_its_sessions() { |
| 544 | } | 552 | } |
| 545 | // Edited under its key: text on every page, published as dependent revisions. | 553 | // Edited under its key: text on every page, published as dependent revisions. |
| 546 | let section = notebook | 554 | let section = notebook |
| 547 | .section_unlocked(&path("Edited"), &keys["Edited"], || {}) | 555 | .section_unlocked(&path("Edited"), &keys["Edited"], false, || {}) |
| 548 | .unwrap(); | 556 | .unwrap(); |
| 549 | for round in ["first", "second", "third"] { | 557 | for round in ["first", "second", "third"] { |
| 550 | append(&section, &format!(" Edited {round} under the key.")); | 558 | append(&section, &format!(" Edited {round} under the key.")); |
| ... | @@ -562,7 +570,7 @@ fn a_notebook_protected_through_its_sessions() { | ... | @@ -562,7 +570,7 @@ fn a_notebook_protected_through_its_sessions() { |
| 562 | .unwrap(); | 570 | .unwrap(); |
| 563 | passwords.insert(path("Changed"), "Changed again".into()); | 571 | passwords.insert(path("Changed"), "Changed again".into()); |
| 564 | let section = notebook | 572 | let section = notebook |
| 565 | .section_unlocked(&path("Changed"), &changed, || {}) | 573 | .section_unlocked(&path("Changed"), &changed, false, || {}) |
| 566 | .unwrap(); | 574 | .unwrap(); |
| 567 | append(&section, " Written under the changed password."); | 575 | append(&section, " Written under the changed password."); |
| 568 | published(&section); | 576 | published(&section); |
| ... | @@ -580,10 +588,10 @@ fn a_notebook_protected_through_its_sessions() { | ... | @@ -580,10 +588,10 @@ fn a_notebook_protected_through_its_sessions() { |
| 580 | let second = Notebook::open(&folder, temporary.path().join("second")).unwrap(); | 588 | let second = Notebook::open(&folder, temporary.path().join("second")).unwrap(); |
| 581 | let key = &keys["Conflicted"]; | 589 | let key = &keys["Conflicted"]; |
| 582 | let ours = notebook | 590 | let ours = notebook |
| 583 | .section_unlocked(&path("Conflicted"), key, || {}) | 591 | .section_unlocked(&path("Conflicted"), key, false, || {}) |
| 584 | .unwrap(); | 592 | .unwrap(); |
| 585 | let theirs = second | 593 | let theirs = second |
| 586 | .section_unlocked(&path("Conflicted"), key, || {}) | 594 | .section_unlocked(&path("Conflicted"), key, false, || {}) |
| 587 | .unwrap(); | 595 | .unwrap(); |
| 588 | for (section, word) in [(&ours, " Ours offline."), (&theirs, " Theirs offline.")] { | 596 | for (section, word) in [(&ours, " Ours offline."), (&theirs, " Theirs offline.")] { |
| 589 | section.set_offline(true); | 597 | section.set_offline(true); |
crates/notebook/tests/session.rs+53-18| ... | @@ -65,7 +65,7 @@ fn typed( | ... | @@ -65,7 +65,7 @@ fn typed( |
| 65 | fn open(file: &Path, cache: &Path) -> (Section, Arc<AtomicUsize>) { | 65 | fn open(file: &Path, cache: &Path) -> (Section, Arc<AtomicUsize>) { |
| 66 | let notified = Arc::new(AtomicUsize::new(0)); | 66 | let notified = Arc::new(AtomicUsize::new(0)); |
| 67 | let counter = Arc::clone(&notified); | 67 | let counter = Arc::clone(&notified); |
| 68 | let section = Section::open(file, cache, move || { | 68 | let section = Section::open(file, cache, false, move || { |
| 69 | counter.fetch_add(1, Ordering::SeqCst); | 69 | counter.fetch_add(1, Ordering::SeqCst); |
| 70 | }) | 70 | }) |
| 71 | .unwrap(); | 71 | .unwrap(); |
| ... | @@ -369,7 +369,7 @@ fn a_notebook_directory_lists_its_sections_and_opens_them() { | ... | @@ -369,7 +369,7 @@ fn a_notebook_directory_lists_its_sections_and_opens_them() { |
| 369 | assert_eq!(catalog.groups.len(), 1); | 369 | assert_eq!(catalog.groups.len(), 1); |
| 370 | assert_eq!(catalog.groups[0].sections.len(), 1); | 370 | assert_eq!(catalog.groups[0].sections.len(), 1); |
| 371 | let path = catalog.groups[0].sections[0].path.clone(); | 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 | let (space, _, _) = section.pages().unwrap()[0]; | 373 | let (space, _, _) = section.pages().unwrap()[0]; |
| 374 | let page = section.page(space).unwrap(); | 374 | let page = section.page(space).unwrap(); |
| 375 | let text = first_text(&page); | 375 | let text = first_text(&page); |
| ... | @@ -409,7 +409,7 @@ fn a_notebook_only_opens_discovered_section_paths() { | ... | @@ -409,7 +409,7 @@ fn a_notebook_only_opens_discovered_section_paths() { |
| 409 | std::fs::write(root.join("added.one"), &source).unwrap(); | 409 | std::fs::write(root.join("added.one"), &source).unwrap(); |
| 410 | for path in ["../outside.one", outside.to_str().unwrap(), "added.one"] { | 410 | for path in ["../outside.one", outside.to_str().unwrap(), "added.one"] { |
| 411 | assert!(matches!( | 411 | assert!(matches!( |
| 412 | notebook.section(path, || {}), | 412 | notebook.section(path, false, || {}), |
| 413 | Err(notebook::Error::Io(error)) if error.kind() == std::io::ErrorKind::NotFound | 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,7 +433,7 @@ fn a_catalog_section_replaced_by_an_outside_symlink_is_rejected() { |
| 433 | std::fs::remove_file(&file).unwrap(); | 433 | std::fs::remove_file(&file).unwrap(); |
| 434 | std::os::unix::fs::symlink(&outside, &file).unwrap(); | 434 | std::os::unix::fs::symlink(&outside, &file).unwrap(); |
| 435 | assert!(matches!( | 435 | assert!(matches!( |
| 436 | notebook.section("notes.one", || {}), | 436 | notebook.section("notes.one", false, || {}), |
| 437 | Err(notebook::Error::Io(error)) if error.kind() == std::io::ErrorKind::PermissionDenied | 437 | Err(notebook::Error::Io(error)) if error.kind() == std::io::ErrorKind::PermissionDenied |
| 438 | )); | 438 | )); |
| 439 | assert_eq!(replicas(&cache), 0); | 439 | assert_eq!(replicas(&cache), 0); |
| ... | @@ -447,7 +447,7 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { | ... | @@ -447,7 +447,7 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { |
| 447 | let cache = directory.path().join("replica.sqlite"); | 447 | let cache = directory.path().join("replica.sqlite"); |
| 448 | let source = onestore::create_section("remote.one", "Original", "Author").unwrap(); | 448 | let source = onestore::create_section("remote.one", "Original", "Author").unwrap(); |
| 449 | let replica = notebook::Replica::create(&cache, &source).unwrap(); | 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 | let space = section.pages().unwrap()[0].0; | 451 | let space = section.pages().unwrap()[0].0; |
| 452 | let before = section.page(space).unwrap(); | 452 | let before = section.page(space).unwrap(); |
| 453 | let after = edited(&before, "First "); | 453 | let after = edited(&before, "First "); |
| ... | @@ -460,7 +460,13 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { | ... | @@ -460,7 +460,13 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { |
| 460 | section.close().unwrap(); | 460 | section.close().unwrap(); |
| 461 | assert!(!file.exists()); | 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 | assert_same(section.page(space).unwrap(), &after); | 470 | assert_same(section.page(space).unwrap(), &after); |
| 465 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); | 471 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); |
| 466 | let before = section.page(space).unwrap(); | 472 | let before = section.page(space).unwrap(); |
| ... | @@ -474,7 +480,13 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { | ... | @@ -474,7 +480,13 @@ fn an_absent_remote_does_not_prevent_local_relaunch_or_further_saves() { |
| 474 | published(&section, next); | 480 | published(&section, next); |
| 475 | assert_same(stored_page(&file, space), &after); | 481 | assert_same(stored_page(&file, space), &after); |
| 476 | section.close().unwrap(); | 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 | assert_same(section.page(space).unwrap(), &after); | 490 | assert_same(section.page(space).unwrap(), &after); |
| 479 | assert!(section.pending().unwrap().is_empty()); | 491 | assert!(section.pending().unwrap().is_empty()); |
| 480 | section.close().unwrap(); | 492 | section.close().unwrap(); |
| ... | @@ -487,7 +499,7 @@ fn resuming_against_another_document_preserves_both_remote_and_pending_edit() { | ... | @@ -487,7 +499,7 @@ fn resuming_against_another_document_preserves_both_remote_and_pending_edit() { |
| 487 | let cache = directory.path().join("replica.sqlite"); | 499 | let cache = directory.path().join("replica.sqlite"); |
| 488 | let source = onestore::create_section("notes.one", "Original", "Author").unwrap(); | 500 | let source = onestore::create_section("notes.one", "Original", "Author").unwrap(); |
| 489 | let replica = notebook::Replica::create(&cache, &source).unwrap(); | 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 | let space = section.pages().unwrap()[0].0; | 503 | let space = section.pages().unwrap()[0].0; |
| 492 | let before = section.page(space).unwrap(); | 504 | let before = section.page(space).unwrap(); |
| 493 | let after = edited(&before, "Local "); | 505 | let after = edited(&before, "Local "); |
| ... | @@ -497,7 +509,13 @@ fn resuming_against_another_document_preserves_both_remote_and_pending_edit() { | ... | @@ -497,7 +509,13 @@ fn resuming_against_another_document_preserves_both_remote_and_pending_edit() { |
| 497 | 509 | ||
| 498 | let other = onestore::create_section("other.one", "Unrelated", "Other").unwrap(); | 510 | let other = onestore::create_section("other.one", "Unrelated", "Other").unwrap(); |
| 499 | std::fs::write(&file, &other).unwrap(); | 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 | wait(&section, |event| matches!(event, Event::Failed(_))); | 519 | wait(&section, |event| matches!(event, Event::Failed(_))); |
| 502 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); | 520 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); |
| 503 | assert_same(section.page(space).unwrap(), &after); | 521 | assert_same(section.page(space).unwrap(), &after); |
| ... | @@ -515,7 +533,7 @@ fn offline_save_process() { | ... | @@ -515,7 +533,7 @@ fn offline_save_process() { |
| 515 | }; | 533 | }; |
| 516 | let root = std::path::PathBuf::from(root); | 534 | let root = std::path::PathBuf::from(root); |
| 517 | let replica = notebook::Replica::open(root.join("replica.sqlite")).unwrap(); | 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 | let space = section.pages().unwrap()[0].0; | 537 | let space = section.pages().unwrap()[0].0; |
| 520 | let before = section.page(space).unwrap(); | 538 | let before = section.page(space).unwrap(); |
| 521 | let id = typed(&section, space, &before, 0..0, "Durable "); | 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,7 +560,13 @@ fn an_acknowledged_offline_save_survives_process_exit_without_cleanup() { |
| 542 | .unwrap(); | 560 | .unwrap(); |
| 543 | let file = directory.path().join("absent.one"); | 561 | let file = directory.path().join("absent.one"); |
| 544 | assert!(!file.exists()); | 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 | let space = section.pages().unwrap()[0].0; | 570 | let space = section.pages().unwrap()[0].0; |
| 547 | let original = model_ops::page_of(&source, space); | 571 | let original = model_ops::page_of(&source, space); |
| 548 | let after = edited(&original, "Durable "); | 572 | let after = edited(&original, "Durable "); |
| ... | @@ -568,6 +592,7 @@ fn smb_connection_failure_keeps_the_session_locally_editable_and_retries() { | ... | @@ -568,6 +592,7 @@ fn smb_connection_failure_keeps_the_session_locally_editable_and_retries() { |
| 568 | counter.fetch_add(1, Ordering::SeqCst); | 592 | counter.fetch_add(1, Ordering::SeqCst); |
| 569 | Err(std::io::ErrorKind::PermissionDenied.into()) | 593 | Err(std::io::ErrorKind::PermissionDenied.into()) |
| 570 | }, | 594 | }, |
| 595 | false, | ||
| 571 | || {}, | 596 | || {}, |
| 572 | ) | 597 | ) |
| 573 | .unwrap(); | 598 | .unwrap(); |
| ... | @@ -619,6 +644,7 @@ fn live_smb_session_save_publishes_and_reopens() { | ... | @@ -619,6 +644,7 @@ fn live_smb_session_save_publishes_and_reopens() { |
| 619 | replica, | 644 | replica, |
| 620 | 1024 * 1024, | 645 | 1024 * 1024, |
| 621 | connect.clone(), | 646 | connect.clone(), |
| 647 | false, | ||
| 622 | || {}, | 648 | || {}, |
| 623 | ) | 649 | ) |
| 624 | .unwrap(); | 650 | .unwrap(); |
| ... | @@ -635,6 +661,7 @@ fn live_smb_session_save_publishes_and_reopens() { | ... | @@ -635,6 +661,7 @@ fn live_smb_session_save_publishes_and_reopens() { |
| 635 | notebook::Replica::open(&cache).unwrap(), | 661 | notebook::Replica::open(&cache).unwrap(), |
| 636 | 1024 * 1024, | 662 | 1024 * 1024, |
| 637 | connect, | 663 | connect, |
| 664 | false, | ||
| 638 | || {}, | 665 | || {}, |
| 639 | ) | 666 | ) |
| 640 | .unwrap(); | 667 | .unwrap(); |
| ... | @@ -666,6 +693,7 @@ fn dropping_during_connection_keeps_cache_owned_until_the_worker_finishes() { | ... | @@ -666,6 +693,7 @@ fn dropping_during_connection_keeps_cache_owned_until_the_worker_finishes() { |
| 666 | stalled.recv_timeout(Duration::from_secs(120)).unwrap(); | 693 | stalled.recv_timeout(Duration::from_secs(120)).unwrap(); |
| 667 | Err(std::io::ErrorKind::TimedOut.into()) | 694 | Err(std::io::ErrorKind::TimedOut.into()) |
| 668 | }, | 695 | }, |
| 696 | false, | ||
| 669 | || {}, | 697 | || {}, |
| 670 | ) | 698 | ) |
| 671 | .unwrap(); | 699 | .unwrap(); |
| ... | @@ -817,9 +845,14 @@ fn an_uncertain_attempt_is_released_after_restart_by_review() { | ... | @@ -817,9 +845,14 @@ fn an_uncertain_attempt_is_released_after_restart_by_review() { |
| 817 | 845 | ||
| 818 | let notified = Arc::new(AtomicUsize::new(0)); | 846 | let notified = Arc::new(AtomicUsize::new(0)); |
| 819 | let counter = Arc::clone(&notified); | 847 | let counter = Arc::clone(&notified); |
| 820 | let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), move || { | 848 | let section = Section::resume( |
| 821 | counter.fetch_add(1, Ordering::SeqCst); | 849 | &file, |
| 822 | }) | 850 | notebook::Replica::open(&cache).unwrap(), |
| 851 | false, | ||
| 852 | move || { | ||
| 853 | counter.fetch_add(1, Ordering::SeqCst); | ||
| 854 | }, | ||
| 855 | ) | ||
| 823 | .unwrap(); | 856 | .unwrap(); |
| 824 | wait( | 857 | wait( |
| 825 | &section, | 858 | &section, |
| ... | @@ -1043,7 +1076,7 @@ fn a_closed_sections_queued_edits_publish_in_the_background_when_online() { | ... | @@ -1043,7 +1076,7 @@ fn a_closed_sections_queued_edits_publish_in_the_background_when_online() { |
| 1043 | background.watch(notebook.replicas()); | 1076 | background.watch(notebook.replicas()); |
| 1044 | // The round started with the background ends before the edit exists. | 1077 | // The round started with the background ends before the edit exists. |
| 1045 | std::thread::sleep(Duration::from_millis(200)); | 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 | section.set_offline(true); | 1080 | section.set_offline(true); |
| 1048 | let space = section.pages().unwrap()[0].0; | 1081 | let space = section.pages().unwrap()[0].0; |
| 1049 | let before = section.page(space).unwrap(); | 1082 | let before = section.page(space).unwrap(); |
| ... | @@ -1070,7 +1103,7 @@ fn a_closed_sections_queued_edits_publish_in_the_background_when_online() { | ... | @@ -1070,7 +1103,7 @@ fn a_closed_sections_queued_edits_publish_in_the_background_when_online() { |
| 1070 | drop(background); | 1103 | drop(background); |
| 1071 | let mut reopened = None; | 1104 | let mut reopened = None; |
| 1072 | until("the background released the replica", || { | 1105 | until("the background released the replica", || { |
| 1073 | reopened = notebook.section("Second.one", || {}).ok(); | 1106 | reopened = notebook.section("Second.one", false, || {}).ok(); |
| 1074 | reopened.is_some() | 1107 | reopened.is_some() |
| 1075 | }); | 1108 | }); |
| 1076 | let reopened = reopened.unwrap(); | 1109 | let reopened = reopened.unwrap(); |
| ... | @@ -1084,7 +1117,7 @@ fn a_remote_change_to_a_closed_section_is_noticed_and_rebases_its_replica() { | ... | @@ -1084,7 +1117,7 @@ fn a_remote_change_to_a_closed_section_is_noticed_and_rebases_its_replica() { |
| 1084 | let mut notebook = two_sections(directory.path()); | 1117 | let mut notebook = two_sections(directory.path()); |
| 1085 | // Second has a replica from being opened once; First has never been opened. | 1118 | // Second has a replica from being opened once; First has never been opened. |
| 1086 | notebook | 1119 | notebook |
| 1087 | .section("Second.one", || {}) | 1120 | .section("Second.one", false, || {}) |
| 1088 | .unwrap() | 1121 | .unwrap() |
| 1089 | .close() | 1122 | .close() |
| 1090 | .unwrap(); | 1123 | .unwrap(); |
| ... | @@ -1190,7 +1223,7 @@ fn every_section_gets_an_offline_copy_that_closing_the_notebook_discards() { | ... | @@ -1190,7 +1223,7 @@ fn every_section_gets_an_offline_copy_that_closing_the_notebook_discards() { |
| 1190 | .all(|(_, status)| status.synced.is_some()) | 1223 | .all(|(_, status)| status.synced.is_some()) |
| 1191 | }); | 1224 | }); |
| 1192 | // Opening a section starts from its copy, and edits made offline wait in it. | 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 | section.set_offline(true); | 1227 | section.set_offline(true); |
| 1195 | let space = section.pages().unwrap()[0].0; | 1228 | let space = section.pages().unwrap()[0].0; |
| 1196 | let before = section.page(space).unwrap(); | 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,6 +1346,7 @@ fn a_moved_section_keeps_its_replica_while_the_old_remote_is_stalled() { |
| 1313 | late_error, | 1346 | late_error, |
| 1314 | }) | 1347 | }) |
| 1315 | }, | 1348 | }, |
| 1349 | false, | ||
| 1316 | || {}, | 1350 | || {}, |
| 1317 | ) | 1351 | ) |
| 1318 | .unwrap(); | 1352 | .unwrap(); |
| ... | @@ -1392,6 +1426,7 @@ fn an_open_section_the_background_holds_checks_its_file_when_the_watch_reports_i | ... | @@ -1392,6 +1426,7 @@ fn an_open_section_the_background_holds_checks_its_file_when_the_watch_reports_i |
| 1392 | .section_with( | 1426 | .section_with( |
| 1393 | "Second.one", | 1427 | "Second.one", |
| 1394 | move |file| Ok(Counted(file.to_owned(), Arc::clone(&counter))), | 1428 | move |file| Ok(Counted(file.to_owned(), Arc::clone(&counter))), |
| 1429 | false, | ||
| 1395 | || {}, | 1430 | || {}, |
| 1396 | ) | 1431 | ) |
| 1397 | .unwrap(); | 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,6 +197,7 @@ fn live_session_applies_edits_through_the_embedded_client_and_reopens() { |
| 197 | Duration::from_secs(5), | 197 | Duration::from_secs(5), |
| 198 | ) | 198 | ) |
| 199 | }, | 199 | }, |
| 200 | false, | ||
| 200 | || {}, | 201 | || {}, |
| 201 | ) | 202 | ) |
| 202 | .unwrap(); | 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,7 +121,7 @@ pub fn open(notebook: &Notebook, guest: &Arc<Guest>, path: &str, key: Option<&Ke |
| 121 | let replica = notebook.replica_path(path).unwrap(); | 121 | let replica = notebook.replica_path(path).unwrap(); |
| 122 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); | 122 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); |
| 123 | let replica = Replica::open_or_create(&replica, key, || notebook.read_section(path)).unwrap(); | 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 | pub fn replace(section: &Section, image: &[u8], range: std::ops::Range<u32>, with: &str) -> u64 { | 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,20 +26,22 @@ pub fn section( |
| 26 | _: &Path, | 26 | _: &Path, |
| 27 | path: &str, | 27 | path: &str, |
| 28 | key: Option<&onestore::protected::Key>, | 28 | key: Option<&onestore::protected::Key>, |
| 29 | offline: bool, | ||
| 29 | notify: impl Fn() + Send + 'static, | 30 | notify: impl Fn() + Send + 'static, |
| 30 | ) -> Result<Section, notebook::Error> { | 31 | ) -> Result<Section, notebook::Error> { |
| 31 | match key { | 32 | match key { |
| 32 | Some(key) => notebook.section_unlocked(path, key, notify), | 33 | Some(key) => notebook.section_unlocked(path, key, offline, notify), |
| 33 | None => notebook.section(path, notify), | 34 | None => notebook.section(path, offline, notify), |
| 34 | } | 35 | } |
| 35 | } | 36 | } |
| 36 | 37 | ||
| 37 | pub fn lone_section( | 38 | pub fn lone_section( |
| 38 | file: &Path, | 39 | file: &Path, |
| 39 | cache: &Path, | 40 | cache: &Path, |
| 41 | offline: bool, | ||
| 40 | notify: impl Fn() + Send + 'static, | 42 | notify: impl Fn() + Send + 'static, |
| 41 | ) -> Result<Section, notebook::Error> { | 43 | ) -> Result<Section, notebook::Error> { |
| 42 | Section::open(file, cache, notify) | 44 | Section::open(file, cache, offline, notify) |
| 43 | } | 45 | } |
| 44 | 46 | ||
| 45 | pub fn background( | 47 | pub fn background( |
crates/snowbound/src/icloud_macos.rs+12-4| ... | @@ -365,6 +365,7 @@ pub fn section( | ... | @@ -365,6 +365,7 @@ pub fn section( |
| 365 | root: &Path, | 365 | root: &Path, |
| 366 | path: &str, | 366 | path: &str, |
| 367 | key: Option<&onestore::protected::Key>, | 367 | key: Option<&onestore::protected::Key>, |
| 368 | offline: bool, | ||
| 368 | notify: impl Fn() + Send + 'static, | 369 | notify: impl Fn() + Send + 'static, |
| 369 | ) -> Result<Section, notebook::Error> { | 370 | ) -> Result<Section, notebook::Error> { |
| 370 | if !notebook.replica_path(path)?.exists() { | 371 | if !notebook.replica_path(path)?.exists() { |
| ... | @@ -372,8 +373,8 @@ pub fn section( | ... | @@ -372,8 +373,8 @@ pub fn section( |
| 372 | } | 373 | } |
| 373 | let connect = |file: &Path| Ok(Coordinated(file.to_owned())); | 374 | let connect = |file: &Path| Ok(Coordinated(file.to_owned())); |
| 374 | let section = match key { | 375 | let section = match key { |
| 375 | Some(key) => notebook.section_unlocked_with(path, key, connect, notify)?, | 376 | Some(key) => notebook.section_unlocked_with(path, key, connect, offline, notify)?, |
| 376 | None => notebook.section_with(path, connect, notify)?, | 377 | None => notebook.section_with(path, connect, offline, notify)?, |
| 377 | }; | 378 | }; |
| 378 | section.set_pause(PAUSE); | 379 | section.set_pause(PAUSE); |
| 379 | Ok(section) | 380 | Ok(section) |
| ... | @@ -392,10 +393,17 @@ fn fetch(file: &Path) -> io::Result<()> { | ... | @@ -392,10 +393,17 @@ fn fetch(file: &Path) -> io::Result<()> { |
| 392 | pub fn lone_section( | 393 | pub fn lone_section( |
| 393 | file: &Path, | 394 | file: &Path, |
| 394 | cache: &Path, | 395 | cache: &Path, |
| 396 | offline: bool, | ||
| 395 | notify: impl Fn() + Send + 'static, | 397 | notify: impl Fn() + Send + 'static, |
| 396 | ) -> Result<Section, notebook::Error> { | 398 | ) -> Result<Section, notebook::Error> { |
| 397 | fetch(file)?; | 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 | section.set_pause(PAUSE); | 407 | section.set_pause(PAUSE); |
| 400 | Ok(section) | 408 | Ok(section) |
| 401 | } | 409 | } |
| ... | @@ -582,7 +590,7 @@ mod tests { | ... | @@ -582,7 +590,7 @@ mod tests { |
| 582 | let notebook = Notebook::create(&root, &cache, Notebook::NEW_COLOR, &page).unwrap(); | 590 | let notebook = Notebook::create(&root, &cache, Notebook::NEW_COLOR, &page).unwrap(); |
| 583 | assert!(ubiquitous(&root)); | 591 | assert!(ubiquitous(&root)); |
| 584 | let path = notebook.catalog().sections[0].path.clone(); | 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 | let (space, ..) = section.pages().unwrap()[0].clone(); | 594 | let (space, ..) = section.pages().unwrap()[0].clone(); |
| 587 | let title = section | 595 | let title = section |
| 588 | .page(space) | 596 | .page(space) |
crates/snowbound/src/library.rs+21-7| ... | @@ -991,7 +991,13 @@ impl Library { | ... | @@ -991,7 +991,13 @@ impl Library { |
| 991 | }) | 991 | }) |
| 992 | .and_then(|replica| { | 992 | .and_then(|replica| { |
| 993 | let guest = Arc::clone(&joined.guest); | 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 | #[cfg(target_arch = "wasm32")] | 1003 | #[cfg(target_arch = "wasm32")] |
| ... | @@ -1010,21 +1016,28 @@ impl Library { | ... | @@ -1010,21 +1016,28 @@ impl Library { |
| 1010 | replica.and_then(|replica| { | 1016 | replica.and_then(|replica| { |
| 1011 | let server = Arc::clone(server); | 1017 | let server = Arc::clone(server); |
| 1012 | let connect = move || server.connect(); | 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 | (Ok(Some(notebook)), None) if self.in_icloud() => { | 1022 | (Ok(Some(notebook)), None) if self.in_icloud() => { |
| 1017 | let root = Path::new(&self.location); | 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 | (Ok(Some(notebook)), None) => match &key { | 1033 | (Ok(Some(notebook)), None) => match &key { |
| 1021 | Some(key) => notebook.section_unlocked(path, key, notifier()), | 1034 | Some(key) => notebook.section_unlocked(path, key, offline(), notifier()), |
| 1022 | None => notebook.section(path, notifier()), | 1035 | None => notebook.section(path, offline(), notifier()), |
| 1023 | }, | 1036 | }, |
| 1024 | (Ok(None), _) if self.in_icloud() => { | 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 | (Err(error), _) => return Err(error.clone().into()), | 1041 | (Err(error), _) => return Err(error.clone().into()), |
| 1029 | }; | 1042 | }; |
| 1030 | match opened { | 1043 | match opened { |
| ... | @@ -1763,6 +1776,7 @@ mod tests { | ... | @@ -1763,6 +1776,7 @@ mod tests { |
| 1763 | release: Arc::clone(&gate), | 1776 | release: Arc::clone(&gate), |
| 1764 | }) | 1777 | }) |
| 1765 | }, | 1778 | }, |
| 1779 | false, | ||
| 1766 | || {}, | 1780 | || {}, |
| 1767 | ) | 1781 | ) |
| 1768 | .unwrap(); | 1782 | .unwrap(); |
crates/snowbound/src/main.rs+3-3| ... | @@ -7246,7 +7246,7 @@ mod tests { | ... | @@ -7246,7 +7246,7 @@ mod tests { |
| 7246 | ) | 7246 | ) |
| 7247 | .unwrap(); | 7247 | .unwrap(); |
| 7248 | let section = | 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 | let mut engine = TextEngine::default(); | 7250 | let mut engine = TextEngine::default(); |
| 7251 | for (space, ..) in section.pages().unwrap().into_iter().take(3) { | 7251 | for (space, ..) in section.pages().unwrap().into_iter().take(3) { |
| 7252 | let mut editor = | 7252 | let mut editor = |
| ... | @@ -7348,7 +7348,7 @@ mod tests { | ... | @@ -7348,7 +7348,7 @@ mod tests { |
| 7348 | &file, | 7348 | &file, |
| 7349 | ) | 7349 | ) |
| 7350 | .unwrap(); | 7350 | .unwrap(); |
| 7351 | let ours = Section::open(&file, directory.join("ours"), || {}).unwrap(); | 7351 | let ours = Section::open(&file, directory.join("ours"), false, || {}).unwrap(); |
| 7352 | let space = ours.pages().unwrap()[0].0; | 7352 | let space = ours.pages().unwrap()[0].0; |
| 7353 | let mut engine = TextEngine::default(); | 7353 | let mut engine = TextEngine::default(); |
| 7354 | let mut editor = CanvasEditor::from_page(ours.page(space).unwrap(), &mut engine).unwrap(); | 7354 | let mut editor = CanvasEditor::from_page(ours.page(space).unwrap(), &mut engine).unwrap(); |
| ... | @@ -7378,7 +7378,7 @@ mod tests { | ... | @@ -7378,7 +7378,7 @@ mod tests { |
| 7378 | "{seen:?}" | 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 | let mut other = CanvasEditor::from_page(theirs.page(space).unwrap(), &mut engine).unwrap(); | 7382 | let mut other = CanvasEditor::from_page(theirs.page(space).unwrap(), &mut engine).unwrap(); |
| 7383 | other.focus_outline(outline).unwrap(); | 7383 | other.focus_outline(outline).unwrap(); |
| 7384 | let last = other.active_outline().document().paragraphs().count() - 1; | 7384 | let last = other.active_outline().document().paragraphs().count() - 1; |