| author | |
| committer | |
| log | 724f7aa7062d2a561f051778a85ee35deb71ebf7 |
| tree | 4ed075c3aeb20dbf1f12ec36c9d2b4ecaf620bfa |
| parent | 725072f4398ea34fd120598c30e2c7c90714ffd3 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Join desktop hosts through sealed WebSockets using the shared Live Share protocol. Await remote operations without blocking the browser, keep ordered SQLite and OPFS writes durable before publication, and retain queued edits across reloads and reconnects.
Refresh shared section catalogs, arm watches when all sections are held, and distinguish device removal from a stopped share. Hosting and section organization remain on desktop.
Validated all CI lanes, browser approval/removal, bidirectional edits, offline reload/recovery, live section discovery, and a fresh OneNote 2010 cold-open of browser-written Unicode.
Assisted-by: gpt-6.1-sol33 files changed, 2242 insertions(+), 496 deletions(-)
arc/platforms.md+4| ... | ... | @@ -185,6 +185,10 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 185 | 185 | `/Cache`. A storage worker keeps them in the origin's private file system, writing the |
| 186 | 186 | byte ranges each burst changed through OPFS's synchronous handles, which only workers get. |
| 187 | 187 | One tab at a time holds them. |
| 188 | - Live Share joins desktop hosts through sealed WebSockets. Approval, device removal, | |
| 189 | presence and edits use the desktop protocol; replicas await remote operations and an | |
| 190 | ordered OPFS flush before publishing or returning a durable receipt. Hosting and section | |
| 191 | organization stay on desktop. | |
| 188 | 192 | - Open Notebook, where the browser has the File System Access API (Chromium), opens a folder |
| 189 | 193 | of the user's: mirrored under `/Folders`, its handle kept in IndexedDB, other apps' writes |
| 190 | 194 | read every few seconds. A browser takes no locks, so a commit there stands only once the |
crates/notebook/Cargo.toml+7-5| ... | ... | @@ -26,19 +26,21 @@ aes-gcm = "0.11.1" |
| 26 | 26 | base64 = { version = "0.23.1", default-features = false, features = ["std"] } |
| 27 | 27 | getrandom = "0.4.3" |
| 28 | 28 | hmac = "0.13.0" |
| 29 | mdns-sd = { version = "0.21.4", default-features = false, optional = true } | |
| 30 | 29 | minicbor = { version = "2.3.0", features = ["derive", "alloc"], optional = true } |
| 31 | 30 | spake2 = { version = "=0.5.0-pre.0", features = ["getrandom"], optional = true } |
| 32 | 31 | relay = { path = "../relay", optional = true } |
| 33 | # A relay's TLS, trusting what the app's updates trust: the system's authorities, then Mozilla's. | |
| 34 | rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true } | |
| 35 | rustls-native-certs = { version = "0.8", optional = true } | |
| 36 | webpki-root-certs = { version = "1.0", optional = true } | |
| 37 | 32 | # OneNote packages (.onepkg) are cabinet files. |
| 38 | 33 | cab = "0.6" |
| 39 | 34 | # std's clock where there is one; the browser's on wasm32-unknown-unknown, whose std has none. |
| 40 | 35 | web-time = "1.1" |
| 41 | 36 | |
| 37 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] | |
| 38 | mdns-sd = { version = "0.21.4", default-features = false, optional = true } | |
| 39 | # A relay's TLS, trusting the system's authorities, then Mozilla's. | |
| 40 | rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true } | |
| 41 | rustls-native-certs = { version = "0.8", optional = true } | |
| 42 | webpki-root-certs = { version = "1.0", optional = true } | |
| 43 | ||
| 42 | 44 | # The browser: its randomness, clock, timers and event loop, and SQLite's files kept with |
| 43 | 45 | # the notebooks' (`fs`). |
| 44 | 46 | [target.'cfg(target_arch = "wasm32")'.dependencies] |
crates/notebook/src/background.rs+118-62| ... | ... | @@ -79,7 +79,7 @@ pub(crate) struct Reports { |
| 79 | 79 | #[derive(Default)] |
| 80 | 80 | struct Watched { |
| 81 | 81 | sections: Vec<Watch>, |
| 82 | /// Sections whose file changed since `changed` was last asked. | |
| 82 | /// Sections or cached folders changed since `changed` was last asked. | |
| 83 | 83 | changed: Vec<String>, |
| 84 | 84 | /// Folders a watch reported without naming the file, listed at `relist`. |
| 85 | 85 | folders: BTreeSet<String>, |
| ... | ... | @@ -127,7 +127,7 @@ impl Background { |
| 127 | 127 | /// lists each folder, both by catalog path, arming any watch that reports the folder's |
| 128 | 128 | /// changes through `Reports`, and whether one does; it runs again after a transport |
| 129 | 129 | /// failure. `copies` keeps an offline copy of every section. |
| 130 | pub(crate) fn start<R, B, L>( | |
| 130 | pub(crate) fn start<R, B, L, LF>( | |
| 131 | 131 | copies: bool, |
| 132 | 132 | mut connect: impl FnMut(Reports) -> io::Result<((B, L), bool)> + Send + 'static, |
| 133 | 133 | notify: impl Fn() + Send + 'static, |
| ... | ... | @@ -135,7 +135,8 @@ impl Background { |
| 135 | 135 | where |
| 136 | 136 | R: Remote, |
| 137 | 137 | B: FnMut(&str) -> R, |
| 138 | L: FnMut(&str) -> io::Result<Vec<Entry>>, | |
| 138 | L: FnMut(String) -> LF, | |
| 139 | LF: Future<Output = io::Result<Vec<Entry>>>, | |
| 139 | 140 | { |
| 140 | 141 | let (signal, receiver) = Signal::new(); |
| 141 | 142 | let shared = Arc::new(Shared { |
| ... | ... | @@ -192,33 +193,39 @@ impl Background { |
| 192 | 193 | connection: connection + 1, |
| 193 | 194 | }; |
| 194 | 195 | let connected = connect(reports); |
| 195 | let Ok(mut watched) = owner.watched.lock() else { | |
| 196 | return; | |
| 197 | }; | |
| 198 | // Whatever ended before now ended with the last connection. | |
| 199 | watched.connection += 1; | |
| 200 | watched.lost = false; | |
| 201 | match connected { | |
| 202 | Ok((mut files, reported)) => { | |
| 203 | watched.watching(reported); | |
| 204 | let folders = watched.rescanning(); | |
| 205 | drop(watched); | |
| 206 | let listings = list(&mut files.1, folders); | |
| 207 | let Ok(mut watched) = owner.watched.lock() else { | |
| 208 | return; | |
| 209 | }; | |
| 210 | watched.listed(&listings, Some(copies), Instant::now()); | |
| 211 | bound = Some(files); | |
| 212 | } | |
| 213 | Err(error) => { | |
| 214 | let error = Error::RemoteIo(error); | |
| 215 | let retry = now + RETRY; | |
| 216 | for watch in &mut watched.sections { | |
| 217 | news |= watch.fail(&error, None); | |
| 218 | watch.due = watch.due.max(retry); | |
| 196 | let folders = { | |
| 197 | let Ok(mut watched) = owner.watched.lock() else { | |
| 198 | return; | |
| 199 | }; | |
| 200 | watched.connection += 1; | |
| 201 | watched.lost = false; | |
| 202 | match &connected { | |
| 203 | Ok((_, reported)) => { | |
| 204 | watched.watching(*reported); | |
| 205 | Some(watched.rescanning()) | |
| 206 | } | |
| 207 | Err(error) => { | |
| 208 | let error = Error::RemoteIo(io::Error::new( | |
| 209 | error.kind(), | |
| 210 | error.to_string(), | |
| 211 | )); | |
| 212 | let retry = now + RETRY; | |
| 213 | for watch in &mut watched.sections { | |
| 214 | news |= watch.fail(&error, None); | |
| 215 | watch.due = watch.due.max(retry); | |
| 216 | } | |
| 217 | watched.relist = watched.relist.map(|due| due.max(retry)); | |
| 218 | None | |
| 219 | 219 | } |
| 220 | watched.relist = watched.relist.map(|due| due.max(retry)); | |
| 221 | 220 | } |
| 221 | }; | |
| 222 | if let (Ok((mut files, _)), Some(folders)) = (connected, folders) { | |
| 223 | let listings = list(&mut files.1, folders).await; | |
| 224 | let Ok(mut watched) = owner.watched.lock() else { | |
| 225 | return; | |
| 226 | }; | |
| 227 | watched.listed(&listings, Some(copies), Instant::now()); | |
| 228 | bound = Some(files); | |
| 222 | 229 | } |
| 223 | 230 | continue; |
| 224 | 231 | } |
| ... | ... | @@ -226,7 +233,7 @@ impl Background { |
| 226 | 233 | let Some((_, list_folder)) = &mut bound else { |
| 227 | 234 | continue; |
| 228 | 235 | }; |
| 229 | let listings = list(list_folder, folders); | |
| 236 | let listings = list(list_folder, folders).await; | |
| 230 | 237 | let Ok(mut watched) = owner.watched.lock() else { |
| 231 | 238 | return; |
| 232 | 239 | }; |
| ... | ... | @@ -244,14 +251,15 @@ impl Background { |
| 244 | 251 | let Some((bind, _)) = &mut bound else { |
| 245 | 252 | continue; |
| 246 | 253 | }; |
| 247 | let (queued, outcome) = step( | |
| 254 | let (queued, outcome) = step_async( | |
| 248 | 255 | &mut bind(&path), |
| 249 | 256 | replica.as_deref(), |
| 250 | 257 | seen.as_deref(), |
| 251 | 258 | current, |
| 252 | 259 | image, |
| 253 | 260 | copies, |
| 254 | ); | |
| 261 | ) | |
| 262 | .await; | |
| 255 | 263 | if outcome.as_ref().is_err_and(disconnected) { |
| 256 | 264 | bound = None; |
| 257 | 265 | } |
| ... | ... | @@ -339,10 +347,12 @@ impl Background { |
| 339 | 347 | crate::SmbRemote::new(Arc::clone(&bound), file, limit) |
| 340 | 348 | }; |
| 341 | 349 | let root = root.clone(); |
| 342 | let list = move |folder: &str| { | |
| 343 | use crate::discover::Source; | |
| 344 | crate::discover::Smb::new(&client, &root)? | |
| 345 | .entries(folder, crate::session::LIMITS.entries) | |
| 350 | let list = move |folder: String| { | |
| 351 | std::future::ready((|| { | |
| 352 | use crate::discover::Source; | |
| 353 | crate::discover::Smb::new(&client, &root)? | |
| 354 | .entries(&folder, crate::session::LIMITS.entries) | |
| 355 | })()) | |
| 346 | 356 | }; |
| 347 | 357 | Ok(((bind, list), reported)) |
| 348 | 358 | }, |
| ... | ... | @@ -364,7 +374,10 @@ impl Background { |
| 364 | 374 | guest.watch(reports)?; |
| 365 | 375 | let (bound, listing) = (Arc::clone(&guest), Arc::clone(&guest)); |
| 366 | 376 | let bind = move |path: &str| crate::live::share::HostedRemote::new(&bound, path); |
| 367 | let list = move |folder: &str| listing.entries(folder); | |
| 377 | let list = move |folder: String| { | |
| 378 | let listing = listing.clone(); | |
| 379 | async move { listing.entries_async(&folder).await } | |
| 380 | }; | |
| 368 | 381 | Ok(((bind, list), true)) |
| 369 | 382 | }, |
| 370 | 383 | notify, |
| ... | ... | @@ -553,6 +566,16 @@ impl Shared { |
| 553 | 566 | |
| 554 | 567 | #[cfg_attr(not(any(feature = "smb", feature = "live")), allow(dead_code))] |
| 555 | 568 | impl Reports { |
| 569 | #[cfg(all(feature = "live", target_arch = "wasm32"))] | |
| 570 | pub(crate) fn catalog(&self, folder: &str) { | |
| 571 | if let Some(shared) = self.shared.upgrade() | |
| 572 | && let Ok(mut watched) = shared.watched.lock() | |
| 573 | && !watched.changed.iter().any(|path| path == folder) | |
| 574 | { | |
| 575 | watched.changed.push(folder.to_owned()); | |
| 576 | } | |
| 577 | } | |
| 578 | ||
| 556 | 579 | /// The sections at or below these paths changed. |
| 557 | 580 | pub(crate) fn touched(&self, paths: &[String]) { |
| 558 | 581 | if let Some(shared) = self.shared.upgrade() { |
| ... | ... | @@ -768,14 +791,14 @@ impl Watched { |
| 768 | 791 | return Next::Wait(wait.map(|at| at.saturating_duration_since(now))); |
| 769 | 792 | }; |
| 770 | 793 | let watch = &mut self.sections[index]; |
| 794 | if !bound { | |
| 795 | return Next::Connect(self.connection); | |
| 796 | } | |
| 771 | 797 | if let Some(worker) = watch.held.as_ref().and_then(Weak::upgrade) { |
| 772 | 798 | worker.wake(); |
| 773 | 799 | watch.due = now + interval; |
| 774 | 800 | continue; |
| 775 | 801 | } |
| 776 | if !bound { | |
| 777 | return Next::Connect(self.connection); | |
| 778 | } | |
| 779 | 802 | return Next::Check { |
| 780 | 803 | path: watch.path.clone(), |
| 781 | 804 | replica: watch.replica.clone(), |
| ... | ... | @@ -789,17 +812,16 @@ impl Watched { |
| 789 | 812 | |
| 790 | 813 | /// Lists each of `folders` through `list`; a folder that cannot be listed lists nothing, so |
| 791 | 814 | /// that its sections are checked. |
| 792 | fn list( | |
| 793 | list: &mut impl FnMut(&str) -> io::Result<Vec<Entry>>, | |
| 815 | async fn list<F: Future<Output = io::Result<Vec<Entry>>>>( | |
| 816 | list: &mut impl FnMut(String) -> F, | |
| 794 | 817 | folders: BTreeSet<String>, |
| 795 | 818 | ) -> BTreeMap<String, Vec<Entry>> { |
| 796 | folders | |
| 797 | .into_iter() | |
| 798 | .map(|folder| { | |
| 799 | let entries = list(&folder).unwrap_or_default(); | |
| 800 | (folder, entries) | |
| 801 | }) | |
| 802 | .collect() | |
| 819 | let mut listings = BTreeMap::new(); | |
| 820 | for folder in folders { | |
| 821 | let entries = list(folder.clone()).await.unwrap_or_default(); | |
| 822 | listings.insert(folder, entries); | |
| 823 | } | |
| 824 | listings | |
| 803 | 825 | } |
| 804 | 826 | |
| 805 | 827 | /// Deletes the replica at `replica` if it holds nothing unpublished and no one holds it. |
| ... | ... | @@ -870,7 +892,7 @@ fn summary(status: &SyncStatus) -> (bool, Option<io::ErrorKind>, u64) { |
| 870 | 892 | /// with `current` is the stamp now, unread. With `copies`, a section without a replica gets |
| 871 | 893 | /// one from the file as it is now. `image`, the file as discovery read it, stands in for |
| 872 | 894 | /// reading it while the stamp is still its own. |
| 873 | fn step<R: Remote>( | |
| 895 | async fn step_async<R: Remote>( | |
| 874 | 896 | remote: &mut R, |
| 875 | 897 | replica: Option<&Path>, |
| 876 | 898 | seen: Option<&Stamp>, |
| ... | ... | @@ -880,7 +902,7 @@ fn step<R: Remote>( |
| 880 | 902 | ) -> (Option<u64>, Result<(Stamp, bool)>) { |
| 881 | 903 | let stamp = match seen.filter(|_| current) { |
| 882 | 904 | Some(seen) => seen.clone(), |
| 883 | None => match remote.stamp() { | |
| 905 | None => match crate::sync::awaited(remote, Remote::stamp).await { | |
| 884 | 906 | Ok(stamp) => stamp, |
| 885 | 907 | Err(error) => return (None, Err(Error::RemoteIo(error))), |
| 886 | 908 | }, |
| ... | ... | @@ -898,14 +920,18 @@ fn step<R: Remote>( |
| 898 | 920 | if !copies { |
| 899 | 921 | return (Some(0), Ok((stamp, moved))); |
| 900 | 922 | } |
| 901 | let copied = (|| { | |
| 902 | let image = remote.read().map_err(Error::RemoteIo)?; | |
| 923 | let copied = (async { | |
| 924 | let image = crate::sync::awaited(remote, Remote::read) | |
| 925 | .await | |
| 926 | .map_err(Error::RemoteIo)?; | |
| 903 | 927 | if let Some(folder) = replica.parent() { |
| 904 | 928 | fs::create_dir_all(folder)?; |
| 905 | 929 | } |
| 906 | 930 | Replica::seed(replica, &image, None)?; |
| 931 | fs::durable().await?; | |
| 907 | 932 | Ok(Stamp::of(&image)?) |
| 908 | })(); | |
| 933 | }) | |
| 934 | .await; | |
| 909 | 935 | return match copied { |
| 910 | 936 | Ok(stamp) => (Some(0), Ok((stamp, moved))), |
| 911 | 937 | // A session made it first. |
| ... | ... | @@ -916,7 +942,7 @@ fn step<R: Remote>( |
| 916 | 942 | }; |
| 917 | 943 | } |
| 918 | 944 | // A version kept beside the file leaves its stamp as it was. |
| 919 | let versions = match remote.versions() { | |
| 945 | let versions = match crate::sync::awaited(remote, Remote::versions).await { | |
| 920 | 946 | Ok(versions) => versions, |
| 921 | 947 | Err(error) => return (None, Err(Error::RemoteIo(error))), |
| 922 | 948 | }; |
| ... | ... | @@ -932,16 +958,22 @@ fn step<R: Remote>( |
| 932 | 958 | Err(error) if error.busy() => return (None, Ok((stamp, false))), |
| 933 | 959 | Err(error) => return (None, Err(error)), |
| 934 | 960 | }; |
| 935 | let synced = (|| { | |
| 961 | let synced = (async { | |
| 936 | 962 | let mut changed = moved; |
| 937 | 963 | loop { |
| 938 | let synced = replica.sync_once(remote)?; | |
| 964 | let synced = replica.sync_once_async(remote).await?; | |
| 939 | 965 | changed |= !synced.changed.is_empty(); |
| 940 | 966 | if !matches!(synced.edit, Some((_, EditStatus::Published { .. }))) { |
| 941 | return Ok((remote.stamp().map_err(Error::RemoteIo)?, changed)); | |
| 967 | return Ok(( | |
| 968 | crate::sync::awaited(remote, Remote::stamp) | |
| 969 | .await | |
| 970 | .map_err(Error::RemoteIo)?, | |
| 971 | changed, | |
| 972 | )); | |
| 942 | 973 | } |
| 943 | 974 | } |
| 944 | })(); | |
| 975 | }) | |
| 976 | .await; | |
| 945 | 977 | let queued = replica |
| 946 | 978 | .recovery_summary() |
| 947 | 979 | .ok() |
| ... | ... | @@ -958,6 +990,10 @@ struct Discovered<'a, R> { |
| 958 | 990 | } |
| 959 | 991 | |
| 960 | 992 | impl<R: Remote> Remote for Discovered<'_, R> { |
| 993 | fn pending(&mut self) -> Option<std::pin::Pin<Box<dyn Future<Output = ()> + '_>>> { | |
| 994 | self.remote.pending() | |
| 995 | } | |
| 996 | ||
| 961 | 997 | fn read(&mut self) -> io::Result<Vec<u8>> { |
| 962 | 998 | match self.image.take() { |
| 963 | 999 | Some((stamp, image)) if stamp == self.stamp => Ok(image), |
| ... | ... | @@ -1036,6 +1072,20 @@ mod tests { |
| 1036 | 1072 | .collect() |
| 1037 | 1073 | } |
| 1038 | 1074 | |
| 1075 | #[test] | |
| 1076 | fn a_notebook_with_every_section_held_still_connects_its_watch() { | |
| 1077 | let now = Instant::now(); | |
| 1078 | let mut watched = Watched::default(); | |
| 1079 | watched.watch(sections(1, true), now); | |
| 1080 | let (held, woken) = Signal::new(); | |
| 1081 | watched.sections[0].held = Some(Arc::downgrade(&held)); | |
| 1082 | assert!(matches!(watched.next(now, false), Next::Connect(0))); | |
| 1083 | watched.watching(true); | |
| 1084 | assert!(held.watched.load(Ordering::Acquire)); | |
| 1085 | assert!(matches!(watched.next(now, true), Next::Wait(_))); | |
| 1086 | assert!(woken.recv_timeout(Duration::from_millis(50)).is_ok()); | |
| 1087 | } | |
| 1088 | ||
| 1039 | 1089 | /// The notebook's folders as a listing shows its first `count` sections. |
| 1040 | 1090 | fn listing(count: usize) -> BTreeMap<String, Vec<Entry>> { |
| 1041 | 1091 | let mut folders: BTreeMap<String, Vec<Entry>> = BTreeMap::new(); |
| ... | ... | @@ -1311,7 +1361,7 @@ mod tests { |
| 1311 | 1361 | armed.send(reports).unwrap(); |
| 1312 | 1362 | let (files, listed) = (remote.clone(), remote.clone()); |
| 1313 | 1363 | let bind = move |path: &str| File(files.clone(), path.to_owned()); |
| 1314 | let list = move |folder: &str| Ok(listed.list(folder)); | |
| 1364 | let list = move |folder: String| std::future::ready(Ok(listed.list(&folder))); | |
| 1315 | 1365 | Ok(((bind, list), true)) |
| 1316 | 1366 | }, |
| 1317 | 1367 | || {}, |
| ... | ... | @@ -1387,7 +1437,7 @@ mod tests { |
| 1387 | 1437 | armed.send(reports).unwrap(); |
| 1388 | 1438 | let (files, listed) = (remote.clone(), remote.clone()); |
| 1389 | 1439 | let bind = move |path: &str| File(files.clone(), path.to_owned()); |
| 1390 | let list = move |folder: &str| Ok(listed.list(folder)); | |
| 1440 | let list = move |folder: String| std::future::ready(Ok(listed.list(&folder))); | |
| 1391 | 1441 | Ok(((bind, list), true)) |
| 1392 | 1442 | }, |
| 1393 | 1443 | || {}, |
| ... | ... | @@ -1412,10 +1462,16 @@ mod tests { |
| 1412 | 1462 | quiet(&stamps, SETTLE).is_empty(), |
| 1413 | 1463 | "the worker reads, not this" |
| 1414 | 1464 | ); |
| 1415 | // The watch ending hands polling back to the worker. | |
| 1416 | 1465 | watch.lost(); |
| 1417 | 1466 | assert!(woken.recv_timeout(SETTLE).is_ok()); |
| 1418 | assert!(!signal.watched.load(Ordering::Acquire)); | |
| 1467 | let rearmed = watches.recv_timeout(SETTLE).unwrap(); | |
| 1468 | while !signal.watched.load(Ordering::Acquire) { | |
| 1469 | woken.recv_timeout(SETTLE * 2).unwrap(); | |
| 1470 | } | |
| 1471 | assert!(signal.watched.load(Ordering::Acquire)); | |
| 1472 | rearmed.touched(std::slice::from_ref(&held)); | |
| 1473 | assert!(woken.recv_timeout(SETTLE * 2).is_ok()); | |
| 1474 | assert!(quiet(&stamps, SETTLE).is_empty()); | |
| 1419 | 1475 | drop(signal); |
| 1420 | 1476 | drop(background); |
| 1421 | 1477 | } |
crates/notebook/src/fs.rs+5| ... | ... | @@ -19,3 +19,8 @@ pub fn commit_file( |
| 19 | 19 | mod web; |
| 20 | 20 | #[cfg(target_arch = "wasm32")] |
| 21 | 21 | pub use web::*; |
| 22 | ||
| 23 | #[cfg(not(target_arch = "wasm32"))] | |
| 24 | pub async fn durable() -> std::io::Result<()> { | |
| 25 | Ok(()) | |
| 26 | } |
crates/notebook/src/fs/web.rs+71-32| ... | ... | @@ -32,6 +32,32 @@ struct Data { |
| 32 | 32 | const RANGES: usize = 64; |
| 33 | 33 | |
| 34 | 34 | impl Data { |
| 35 | fn flush(&mut self) { | |
| 36 | if let Some(path) = self.path.clone() { | |
| 37 | with(|files| { | |
| 38 | if files.changed.remove(&path) { | |
| 39 | files.durable.push((path, self.change())); | |
| 40 | } | |
| 41 | }); | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | fn change(&mut self) -> Change { | |
| 46 | let length = self.bytes.len(); | |
| 47 | let ranges = self | |
| 48 | .unwritten | |
| 49 | .replace(Vec::new()) | |
| 50 | .unwrap_or_else(|| std::iter::once(0..length).collect()); | |
| 51 | Change::File { | |
| 52 | length: length as u64, | |
| 53 | ranges: ranges | |
| 54 | .into_iter() | |
| 55 | .map(|range| range.start.min(length)..range.end.min(length)) | |
| 56 | .filter(|range| !range.is_empty()) | |
| 57 | .map(|range| (range.start as u64, self.bytes[range].to_vec())) | |
| 58 | .collect(), | |
| 59 | } | |
| 60 | } | |
| 35 | 61 | fn new( |
| 36 | 62 | bytes: Vec<u8>, |
| 37 | 63 | modified: f64, |
| ... | ... | @@ -79,6 +105,8 @@ struct Files { |
| 79 | 105 | nodes: BTreeMap<PathBuf, Node>, |
| 80 | 106 | /// Paths written, made or removed since the host last asked. |
| 81 | 107 | changed: BTreeSet<PathBuf>, |
| 108 | /// Flushes and removals in order, including SQLite's WAL before its database checkpoint. | |
| 109 | durable: Vec<(PathBuf, Change)>, | |
| 82 | 110 | /// Folders the host mirrors from elsewhere (`mount`). |
| 83 | 111 | mounts: BTreeSet<PathBuf>, |
| 84 | 112 | /// Images committed under a mount, waiting for the host to write them (`committed`). |
| ... | ... | @@ -164,39 +192,24 @@ pub enum Change { |
| 164 | 192 | |
| 165 | 193 | /// Whether any path changed since `changes` was last called. |
| 166 | 194 | pub fn changed() -> bool { |
| 167 | FILES.with_borrow(|files| !files.changed.is_empty() || !files.committed.is_empty()) | |
| 195 | FILES.with_borrow(|files| { | |
| 196 | !files.durable.is_empty() || !files.changed.is_empty() || !files.committed.is_empty() | |
| 197 | }) | |
| 168 | 198 | } |
| 169 | 199 | |
| 170 | /// The paths changed since the last call, each with how; a folder before what it holds. | |
| 200 | /// Ordered file flushes, followed by changes not yet flushed. | |
| 171 | 201 | pub fn changes() -> Vec<(PathBuf, Change)> { |
| 172 | 202 | FILES.with_borrow_mut(|files| { |
| 173 | std::mem::take(&mut files.changed) | |
| 174 | .into_iter() | |
| 175 | .map(|path| { | |
| 176 | let change = match files.nodes.get(&path) { | |
| 177 | None => Change::Removed, | |
| 178 | Some(Node::Directory) => Change::Directory, | |
| 179 | Some(Node::File(data)) => { | |
| 180 | let mut data = data.borrow_mut(); | |
| 181 | let length = data.bytes.len(); | |
| 182 | let ranges = data | |
| 183 | .unwritten | |
| 184 | .replace(Vec::new()) | |
| 185 | .unwrap_or_else(|| std::iter::once(0..length).collect()); | |
| 186 | Change::File { | |
| 187 | length: length as u64, | |
| 188 | ranges: ranges | |
| 189 | .into_iter() | |
| 190 | .map(|range| range.start.min(length)..range.end.min(length)) | |
| 191 | .filter(|range| !range.is_empty()) | |
| 192 | .map(|range| (range.start as u64, data.bytes[range].to_vec())) | |
| 193 | .collect(), | |
| 194 | } | |
| 195 | } | |
| 196 | }; | |
| 197 | (path, change) | |
| 198 | }) | |
| 199 | .collect() | |
| 203 | let mut changes = std::mem::take(&mut files.durable); | |
| 204 | changes.extend(std::mem::take(&mut files.changed).into_iter().map(|path| { | |
| 205 | let change = match files.nodes.get(&path) { | |
| 206 | None => Change::Removed, | |
| 207 | Some(Node::Directory) => Change::Directory, | |
| 208 | Some(Node::File(data)) => data.borrow_mut().change(), | |
| 209 | }; | |
| 210 | (path, change) | |
| 211 | })); | |
| 212 | changes | |
| 200 | 213 | }) |
| 201 | 214 | } |
| 202 | 215 | |
| ... | ... | @@ -445,9 +458,13 @@ pub fn remove_file(path: impl AsRef<Path>) -> io::Result<()> { |
| 445 | 458 | let path = normal(path.as_ref()); |
| 446 | 459 | with(|files| { |
| 447 | 460 | let data = files.file(&path)?; |
| 448 | data.borrow_mut().path = None; | |
| 461 | let mut data = data.borrow_mut(); | |
| 462 | if files.changed.remove(&path) { | |
| 463 | files.durable.push((path.clone(), data.change())); | |
| 464 | } | |
| 465 | data.path = None; | |
| 449 | 466 | files.nodes.remove(&path); |
| 450 | files.changed.insert(path); | |
| 467 | files.durable.push((path, Change::Removed)); | |
| 451 | 468 | Ok(()) |
| 452 | 469 | }) |
| 453 | 470 | } |
| ... | ... | @@ -687,11 +704,12 @@ impl File { |
| 687 | 704 | } |
| 688 | 705 | |
| 689 | 706 | pub fn sync_all(&self) -> io::Result<()> { |
| 707 | self.data.borrow_mut().flush(); | |
| 690 | 708 | Ok(()) |
| 691 | 709 | } |
| 692 | 710 | |
| 693 | 711 | pub fn sync_data(&self) -> io::Result<()> { |
| 694 | Ok(()) | |
| 712 | self.sync_all() | |
| 695 | 713 | } |
| 696 | 714 | |
| 697 | 715 | pub fn set_len(&self, size: u64) -> io::Result<()> { |
| ... | ... | @@ -848,3 +866,24 @@ pub fn supersede_file( |
| 848 | 866 | .map_err(not_committed)?; |
| 849 | 867 | rename(with, path).map_err(not_committed) |
| 850 | 868 | } |
| 869 | ||
| 870 | /// Waits until the browser host has durably written every ordered flush handed to it. | |
| 871 | pub async fn durable() -> io::Result<()> { | |
| 872 | use wasm_bindgen::prelude::*; | |
| 873 | #[wasm_bindgen] | |
| 874 | extern "C" { | |
| 875 | #[wasm_bindgen(catch, js_namespace = globalThis, js_name = snowboundFlushStorage)] | |
| 876 | fn flush_storage() -> Result<js_sys::Promise, JsValue>; | |
| 877 | } | |
| 878 | let failure = |error: JsValue| { | |
| 879 | io::Error::other( | |
| 880 | error | |
| 881 | .as_string() | |
| 882 | .unwrap_or_else(|| "Browser storage failed".into()), | |
| 883 | ) | |
| 884 | }; | |
| 885 | wasm_bindgen_futures::JsFuture::from(flush_storage().map_err(failure)?) | |
| 886 | .await | |
| 887 | .map_err(failure)?; | |
| 888 | Ok(()) | |
| 889 | } |
crates/notebook/src/fs/web/sqlite.rs+1| ... | ... | @@ -49,6 +49,7 @@ impl VfsFile for File { |
| 49 | 49 | } |
| 50 | 50 | |
| 51 | 51 | fn flush(&mut self) -> VfsResult<()> { |
| 52 | self.0.borrow_mut().flush(); | |
| 52 | 53 | Ok(()) |
| 53 | 54 | } |
| 54 | 55 |
crates/notebook/src/lib.rs+1| ... | ... | @@ -5,6 +5,7 @@ |
| 5 | 5 | pub mod discover; |
| 6 | 6 | pub mod fs; |
| 7 | 7 | #[cfg(feature = "live")] |
| 8 | #[cfg_attr(target_arch = "wasm32", path = "live/web.rs")] | |
| 8 | 9 | pub mod live; |
| 9 | 10 | #[cfg(feature = "smb")] |
| 10 | 11 | pub mod smb; |
crates/notebook/src/live.rs+3-123| ... | ... | @@ -19,7 +19,6 @@ pub use wire::{Caret, Guid, Hello, Presence, Spot}; |
| 19 | 19 | |
| 20 | 20 | use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent, ServiceInfo}; |
| 21 | 21 | use minicbor::Encode; |
| 22 | use sha2::{Digest, Sha256}; | |
| 23 | 22 | use std::{ |
| 24 | 23 | collections::BTreeMap, |
| 25 | 24 | io::{self, Read, Write}, |
| ... | ... | @@ -46,124 +45,9 @@ const PATIENCE: Duration = Duration::from_secs(30); |
| 46 | 45 | /// Wrong tries of a code met off any relay before it admits no one new, as a relay burns one. |
| 47 | 46 | const TRIES: u32 = 5; |
| 48 | 47 | |
| 49 | /// The secret peers meet through. | |
| 50 | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 51 | pub enum Room { | |
| 52 | /// A notebook's room, or a share's: a random secret only its members hold. | |
| 53 | Notebook([u8; 16]), | |
| 54 | /// A code typed on both ends (`code`), with a password where one is set: its room's | |
| 55 | /// number names it on the network, and its secret and password only the two people know. | |
| 56 | /// Its `owner`, the end sharing it, takes the number from a relay (the one the code has, | |
| 57 | /// coming back; any free one for a secret alone) or picks one where it has no relay, and | |
| 58 | /// judges every try, burning the code after too many wrong ones. | |
| 59 | Code { | |
| 60 | code: String, | |
| 61 | password: String, | |
| 62 | owner: bool, | |
| 63 | }, | |
| 64 | } | |
| 65 | ||
| 66 | impl Room { | |
| 67 | /// The room a code typed on this end leads to. | |
| 68 | pub fn join(code: &str, password: &str) -> Self { | |
| 69 | Self::Code { | |
| 70 | code: code.to_owned(), | |
| 71 | password: password.to_owned(), | |
| 72 | owner: false, | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | /// The room of a code this end shares: its secret, or a whole code to keep its number. | |
| 77 | pub fn share(code: &str, password: &str) -> Self { | |
| 78 | Self::Code { | |
| 79 | code: code.to_owned(), | |
| 80 | password: password.to_owned(), | |
| 81 | owner: true, | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | /// What names the room in the clear: a hash of a room's secret, a code's number; none for | |
| 86 | /// a code without one yet. | |
| 87 | fn tag(&self) -> Option<String> { | |
| 88 | match self { | |
| 89 | Room::Notebook(id) => Some(hex(&Sha256::digest( | |
| 90 | [&b"Snowbound room "[..], id].concat(), | |
| 91 | )[..8])), | |
| 92 | Room::Code { code, .. } => code_parts(code).0.map(|number| format!("code-{number}")), | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | fn secret(&self) -> Vec<u8> { | |
| 97 | match self { | |
| 98 | Room::Notebook(id) => id.to_vec(), | |
| 99 | Room::Code { code, password, .. } => { | |
| 100 | let mut secret = code_parts(code).1.into_bytes(); | |
| 101 | if !password.is_empty() { | |
| 102 | secret.push(b'\n'); | |
| 103 | secret.extend_from_slice(password.as_bytes()); | |
| 104 | } | |
| 105 | secret | |
| 106 | } | |
| 107 | } | |
| 108 | } | |
| 109 | ||
| 110 | fn owner(&self) -> bool { | |
| 111 | matches!(self, Room::Code { owner: true, .. }) | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | /// A code's room number, if it has one, and its secret: a whole code, or a secret alone. | |
| 116 | fn code_parts(code: &str) -> (Option<u32>, String) { | |
| 117 | match code::parse(code) { | |
| 118 | Some((number, secret)) => (Some(number), secret), | |
| 119 | None => (None, code.trim().to_uppercase()), | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | /// Where peers are looked for. | |
| 124 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 125 | pub enum Reach { | |
| 126 | /// Every network this computer is on. | |
| 127 | Network, | |
| 128 | /// This computer alone, for two copies of the app side by side. | |
| 129 | Loopback, | |
| 130 | } | |
| 131 | ||
| 132 | #[derive(Clone, Debug)] | |
| 133 | pub struct Peer { | |
| 134 | pub hello: Arc<Hello>, | |
| 135 | /// None until the peer first says where it is. | |
| 136 | pub presence: Option<Presence>, | |
| 137 | } | |
| 138 | ||
| 139 | /// What happened, as `Live::start`'s `events` hears it on a network thread. | |
| 140 | pub enum Event<'a> { | |
| 141 | /// The peers, their presence, the code or the relay's answer changed. | |
| 142 | Changed, | |
| 143 | /// A stream to a peer opened, with the line to it. | |
| 144 | Met(&'a Arc<Hello>, &'a Line), | |
| 145 | /// A peer's stream ended. | |
| 146 | Left(&'a Arc<Hello>), | |
| 147 | /// A frame of a kind presence doesn't read itself, on a stream or to the group. | |
| 148 | Frame { | |
| 149 | from: &'a Arc<Hello>, | |
| 150 | kind: u16, | |
| 151 | body: &'a [u8], | |
| 152 | }, | |
| 153 | } | |
| 154 | ||
| 155 | /// How the relay last answered. | |
| 156 | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 157 | pub enum Relayed { | |
| 158 | /// Not asked yet, or no relay. | |
| 159 | Unknown, | |
| 160 | /// In the room. | |
| 161 | Joined, | |
| 162 | /// Answered with this HTTP status, and how long it asked to wait. | |
| 163 | Refused(u16, Option<Duration>), | |
| 164 | /// Not reached, and why. | |
| 165 | Unreachable(Trouble), | |
| 166 | } | |
| 48 | mod model; | |
| 49 | pub use model::{Event, Peer, Reach, Relayed, Room}; | |
| 50 | use model::{code_parts, hex}; | |
| 167 | 51 | |
| 168 | 52 | /// Presence on the network while it lives; dropping it leaves. |
| 169 | 53 | pub struct Live { |
| ... | ... | @@ -909,9 +793,5 @@ fn decode(text: &str) -> String { |
| 909 | 793 | String::from_utf8_lossy(&decoded).into_owned() |
| 910 | 794 | } |
| 911 | 795 | |
| 912 | fn hex(bytes: &[u8]) -> String { | |
| 913 | bytes.iter().map(|byte| format!("{byte:02x}")).collect() | |
| 914 | } | |
| 915 | ||
| 916 | 796 | #[cfg(test)] |
| 917 | 797 | mod tests; |
crates/notebook/src/live/model.rs created+149| ... | ... | @@ -0,0 +1,149 @@ |
| 1 | use super::{Hello, Line, Presence, code}; | |
| 2 | use sha2::{Digest, Sha256}; | |
| 3 | use std::{sync::Arc, time::Duration}; | |
| 4 | ||
| 5 | pub(super) fn hex(bytes: &[u8]) -> String { | |
| 6 | bytes.iter().map(|byte| format!("{byte:02x}")).collect() | |
| 7 | } | |
| 8 | ||
| 9 | /// The secret peers meet through. | |
| 10 | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 11 | pub enum Room { | |
| 12 | /// A notebook's room, or a share's: a random secret only its members hold. | |
| 13 | Notebook([u8; 16]), | |
| 14 | /// A code typed on both ends (`code`), with a password where one is set: its room's | |
| 15 | /// number names it on the network, and its secret and password only the two people know. | |
| 16 | /// Its `owner`, the end sharing it, takes the number from a relay (the one the code has, | |
| 17 | /// coming back; any free one for a secret alone) or picks one where it has no relay, and | |
| 18 | /// judges every try, burning the code after too many wrong ones. | |
| 19 | Code { | |
| 20 | code: String, | |
| 21 | password: String, | |
| 22 | owner: bool, | |
| 23 | }, | |
| 24 | } | |
| 25 | ||
| 26 | impl Room { | |
| 27 | /// The room a code typed on this end leads to. | |
| 28 | pub fn join(code: &str, password: &str) -> Self { | |
| 29 | Self::Code { | |
| 30 | code: code.to_owned(), | |
| 31 | password: password.to_owned(), | |
| 32 | owner: false, | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | /// The room of a code this end shares: its secret, or a whole code to keep its number. | |
| 37 | pub fn share(code: &str, password: &str) -> Self { | |
| 38 | Self::Code { | |
| 39 | code: code.to_owned(), | |
| 40 | password: password.to_owned(), | |
| 41 | owner: true, | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | /// What names the room in the clear: a hash of a room's secret, a code's number; none for | |
| 46 | /// a code without one yet. | |
| 47 | pub(super) fn tag(&self) -> Option<String> { | |
| 48 | match self { | |
| 49 | Room::Notebook(id) => Some(hex(&Sha256::digest( | |
| 50 | [&b"Snowbound room "[..], id].concat(), | |
| 51 | )[..8])), | |
| 52 | Room::Code { code, .. } => code_parts(code).0.map(|number| format!("code-{number}")), | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | pub(super) fn secret(&self) -> Vec<u8> { | |
| 57 | match self { | |
| 58 | Room::Notebook(id) => id.to_vec(), | |
| 59 | Room::Code { code, password, .. } => { | |
| 60 | let mut secret = code_parts(code).1.into_bytes(); | |
| 61 | if !password.is_empty() { | |
| 62 | secret.push(b'\n'); | |
| 63 | secret.extend_from_slice(password.as_bytes()); | |
| 64 | } | |
| 65 | secret | |
| 66 | } | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | #[cfg(not(target_arch = "wasm32"))] | |
| 71 | pub(super) fn owner(&self) -> bool { | |
| 72 | matches!(self, Room::Code { owner: true, .. }) | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | /// A code's room number, if it has one, and its secret: a whole code, or a secret alone. | |
| 77 | pub(super) fn code_parts(code: &str) -> (Option<u32>, String) { | |
| 78 | match code::parse(code) { | |
| 79 | Some((number, secret)) => (Some(number), secret), | |
| 80 | None => (None, code.trim().to_uppercase()), | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | /// Where peers are looked for. | |
| 85 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 86 | pub enum Reach { | |
| 87 | /// Every network this computer is on. | |
| 88 | Network, | |
| 89 | /// This computer alone, for two copies of the app side by side. | |
| 90 | Loopback, | |
| 91 | } | |
| 92 | ||
| 93 | #[derive(Clone, Debug)] | |
| 94 | pub struct Peer { | |
| 95 | pub hello: Arc<Hello>, | |
| 96 | /// None until the peer first says where it is. | |
| 97 | pub presence: Option<Presence>, | |
| 98 | } | |
| 99 | ||
| 100 | /// What happened, as `Live::start`'s `events` hears it on a network thread. | |
| 101 | pub enum Event<'a> { | |
| 102 | /// The peers, their presence, the code or the relay's answer changed. | |
| 103 | Changed, | |
| 104 | /// A stream to a peer opened, with the line to it. | |
| 105 | Met(&'a Arc<Hello>, &'a Line), | |
| 106 | /// A peer's stream ended. | |
| 107 | Left(&'a Arc<Hello>), | |
| 108 | /// A frame of a kind presence doesn't read itself, on a stream or to the group. | |
| 109 | Frame { | |
| 110 | from: &'a Arc<Hello>, | |
| 111 | kind: u16, | |
| 112 | body: &'a [u8], | |
| 113 | }, | |
| 114 | } | |
| 115 | ||
| 116 | /// How the relay last answered. | |
| 117 | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 118 | pub enum Relayed { | |
| 119 | /// Not asked yet, or no relay. | |
| 120 | Unknown, | |
| 121 | /// In the room. | |
| 122 | Joined, | |
| 123 | /// Answered with this HTTP status, and how long it asked to wait. | |
| 124 | Refused(u16, Option<Duration>), | |
| 125 | /// Not reached, and why. | |
| 126 | Unreachable(Trouble), | |
| 127 | } | |
| 128 | ||
| 129 | /// Why a relay could not be reached, as a person can act on it. | |
| 130 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 131 | pub enum Trouble { | |
| 132 | /// The relay's name, or the proxy's, didn't resolve. | |
| 133 | Dns, | |
| 134 | /// Nothing answered at the relay's address: a firewall, or the relay is down. | |
| 135 | Unreachable, | |
| 136 | TimedOut, | |
| 137 | /// The proxy itself couldn't be reached. | |
| 138 | ProxyUnreachable, | |
| 139 | /// The proxy asks for a name and password (407). | |
| 140 | ProxyAuthentication, | |
| 141 | /// The proxy refused to connect to the relay, with this status. | |
| 142 | ProxyRefused(u16), | |
| 143 | /// The relay's certificate wasn't one the system trusts: something on the way presents its | |
| 144 | /// own. | |
| 145 | Certificate, | |
| 146 | /// Something on the way refused both WebSockets and plain requests to the relay. | |
| 147 | Blocked, | |
| 148 | Other, | |
| 149 | } |
crates/notebook/src/live/share.rs+316-122| ... | ... | @@ -8,9 +8,7 @@ |
| 8 | 8 | |
| 9 | 9 | use super::{ |
| 10 | 10 | Event, Hello, Line, Live, Peer, Presence, Reach, Relayed, Room, Sender, |
| 11 | wire::{ | |
| 12 | self, Delta, Failure, Reply, Request, Touched, Welcome, WireEntry, WireStamp, Written, kind, | |
| 13 | }, | |
| 11 | wire::{self, Delta, Failure, Reply, Request, Touched, Welcome, WireEntry, Written, kind}, | |
| 14 | 12 | }; |
| 15 | 13 | use crate::{Error, Result, background::Reports, discover, session::Storage}; |
| 16 | 14 | use onestore::{CommitError, CommitState, RevisionIndex, Stamp, Store, Transaction}; |
| ... | ... | @@ -25,11 +23,17 @@ use std::{ |
| 25 | 23 | mpsc, |
| 26 | 24 | }, |
| 27 | 25 | thread, |
| 28 | time::{Duration, Instant}, | |
| 26 | time::Duration, | |
| 29 | 27 | }; |
| 28 | use web_time::Instant; | |
| 30 | 29 | |
| 30 | #[path = "share/batch.rs"] | |
| 31 | 31 | mod batch; |
| 32 | #[path = "share/membership.rs"] | |
| 32 | 33 | mod membership; |
| 34 | #[cfg(target_arch = "wasm32")] | |
| 35 | #[path = "share/web.rs"] | |
| 36 | mod web; | |
| 33 | 37 | pub use membership::Host; |
| 34 | 38 | |
| 35 | 39 | /// The most bytes one message of a read or an upload carries. |
| ... | ... | @@ -168,12 +172,31 @@ pub fn join_while( |
| 168 | 172 | reach: Option<Reach>, |
| 169 | 173 | relay: Option<&str>, |
| 170 | 174 | continue_joining: impl Fn(bool) -> bool, |
| 175 | ) -> std::result::Result<Welcome, Refusal> { | |
| 176 | crate::task::ready(join_while_async( | |
| 177 | me, | |
| 178 | code, | |
| 179 | password, | |
| 180 | reach, | |
| 181 | relay, | |
| 182 | continue_joining, | |
| 183 | )) | |
| 184 | .map_err(|_| Refusal::Busy)? | |
| 185 | } | |
| 186 | ||
| 187 | pub async fn join_while_async( | |
| 188 | me: Hello, | |
| 189 | code: &str, | |
| 190 | password: &str, | |
| 191 | reach: Option<Reach>, | |
| 192 | relay: Option<&str>, | |
| 193 | continue_joining: impl Fn(bool) -> bool, | |
| 171 | 194 | ) -> std::result::Result<Welcome, Refusal> { |
| 172 | 195 | let code = self::code(code).ok_or(Refusal::Malformed)?; |
| 173 | 196 | let (welcomed, welcome) = mpsc::channel(); |
| 174 | 197 | let approving = Arc::new(AtomicBool::new(false)); |
| 175 | 198 | let approval = Arc::clone(&approving); |
| 176 | let (changed, waiting) = mpsc::channel(); | |
| 199 | let (changed, waiting) = crate::task::channel(); | |
| 177 | 200 | let live = Live::start( |
| 178 | 201 | me, |
| 179 | 202 | &Room::join(&code, password), |
| ... | ... | @@ -207,7 +230,7 @@ pub fn join_while( |
| 207 | 230 | } |
| 208 | 231 | } |
| 209 | 232 | _ => { |
| 210 | let _ = changed.send(()); | |
| 233 | let _ = changed.try_send(()); | |
| 211 | 234 | } |
| 212 | 235 | }, |
| 213 | 236 | ) |
| ... | ... | @@ -257,7 +280,7 @@ pub fn join_while( |
| 257 | 280 | } |
| 258 | 281 | _ => {} |
| 259 | 282 | } |
| 260 | let _ = waiting.recv_timeout(Duration::from_millis(100)); | |
| 283 | crate::task::wait(&waiting, Some(Duration::from_millis(100))).await; | |
| 261 | 284 | } |
| 262 | 285 | } |
| 263 | 286 | |
| ... | ... | @@ -963,26 +986,61 @@ struct Inner { |
| 963 | 986 | here: Mutex<Presence>, |
| 964 | 987 | /// The host and the line to it, while connected. |
| 965 | 988 | host: Mutex<Option<(Arc<Hello>, Line)>>, |
| 966 | pending: Mutex<HashMap<u64, mpsc::Sender<Reply>>>, | |
| 989 | pending: Mutex<HashMap<u64, crate::task::Answer<Reply>>>, | |
| 967 | 990 | next: AtomicU64, |
| 968 | 991 | /// Where the host's reports of changed files go, while a background watches. |
| 969 | 992 | watch: Mutex<Option<Reports>>, |
| 970 | /// The host stopped sharing. | |
| 993 | /// Why this device can no longer reach the share. | |
| 971 | 994 | ended: Mutex<Option<Ended>>, |
| 972 | 995 | /// Bytes of chunks asked for and not yet given. |
| 973 | asked: (Mutex<usize>, Condvar), | |
| 996 | asked: (Mutex<Chunks>, Condvar), | |
| 974 | 997 | /// The sections read lately, kept as the host's deltas change them, newest first. |
| 975 | 998 | images: Mutex<Vec<(String, Arc<Vec<u8>>)>>, |
| 976 | 999 | /// The stamps of sections whose image held is the host's now, as its last report of |
| 977 | 1000 | /// them said. |
| 978 | 1001 | current: Mutex<HashMap<String, Stamp>>, |
| 1002 | #[cfg(target_arch = "wasm32")] | |
| 1003 | catalog: Mutex<Option<PathBuf>>, | |
| 979 | 1004 | } |
| 980 | 1005 | |
| 981 | enum Ended { | |
| 1006 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 1007 | pub enum Ended { | |
| 982 | 1008 | Stopped, |
| 983 | 1009 | Removed, |
| 984 | 1010 | } |
| 985 | 1011 | |
| 1012 | #[derive(Default)] | |
| 1013 | struct Chunks { | |
| 1014 | bytes: usize, | |
| 1015 | #[cfg(target_arch = "wasm32")] | |
| 1016 | waiting: Vec<std::task::Waker>, | |
| 1017 | } | |
| 1018 | ||
| 1019 | struct Requested<'a> { | |
| 1020 | inner: &'a Inner, | |
| 1021 | id: u64, | |
| 1022 | chunked: bool, | |
| 1023 | } | |
| 1024 | ||
| 1025 | impl Drop for Requested<'_> { | |
| 1026 | fn drop(&mut self) { | |
| 1027 | self.inner.pending.lock().unwrap().remove(&self.id); | |
| 1028 | if self.chunked { | |
| 1029 | let (asked, room) = &self.inner.asked; | |
| 1030 | let mut asked = asked.lock().unwrap(); | |
| 1031 | asked.bytes -= CHUNK; | |
| 1032 | #[cfg(target_arch = "wasm32")] | |
| 1033 | let waiting = std::mem::take(&mut asked.waiting); | |
| 1034 | drop(asked); | |
| 1035 | room.notify_all(); | |
| 1036 | #[cfg(target_arch = "wasm32")] | |
| 1037 | for waker in waiting { | |
| 1038 | waker.wake(); | |
| 1039 | } | |
| 1040 | } | |
| 1041 | } | |
| 1042 | } | |
| 1043 | ||
| 986 | 1044 | impl Guest { |
| 987 | 1045 | /// Joins share `share` through its `secret` as `me`, where `reach` and `relay` say. |
| 988 | 1046 | /// `events` runs on a network thread whenever the host comes or goes, or the peers change. |
| ... | ... | @@ -1010,6 +1068,8 @@ impl Guest { |
| 1010 | 1068 | asked: Default::default(), |
| 1011 | 1069 | images: Mutex::default(), |
| 1012 | 1070 | current: Mutex::default(), |
| 1071 | #[cfg(target_arch = "wasm32")] | |
| 1072 | catalog: Mutex::default(), | |
| 1013 | 1073 | }); |
| 1014 | 1074 | let heard = Arc::clone(&inner); |
| 1015 | 1075 | let live = Live::start(me, &Room::Notebook(secret), reach, relay, move |event| { |
| ... | ... | @@ -1030,9 +1090,9 @@ impl Guest { |
| 1030 | 1090 | host.as_ref().map(|(hello, _)| Arc::clone(hello)) |
| 1031 | 1091 | } |
| 1032 | 1092 | |
| 1033 | /// Whether the host said it stopped sharing. | |
| 1034 | pub fn stopped(&self) -> bool { | |
| 1035 | self.inner.ended.lock().unwrap().is_some() | |
| 1093 | /// Why the host ended this device's access. | |
| 1094 | pub fn ended(&self) -> Option<Ended> { | |
| 1095 | *self.inner.ended.lock().unwrap() | |
| 1036 | 1096 | } |
| 1037 | 1097 | |
| 1038 | 1098 | /// Everyone in the share's room: the host and the other guests. |
| ... | ... | @@ -1077,27 +1137,62 @@ impl Guest { |
| 1077 | 1137 | } |
| 1078 | 1138 | |
| 1079 | 1139 | /// Asks the host `kind` of `request`, waiting for its reply. |
| 1080 | fn request(&self, kind: u16, mut request: Request) -> std::result::Result<Reply, Failed> { | |
| 1140 | #[cfg(not(target_arch = "wasm32"))] | |
| 1141 | fn request(&self, kind: u16, request: Request) -> std::result::Result<Reply, Failed> { | |
| 1142 | crate::task::ready(self.request_async(kind, request)).map_err(Failed::Unsent)? | |
| 1143 | } | |
| 1144 | ||
| 1145 | async fn request_async( | |
| 1146 | &self, | |
| 1147 | kind: u16, | |
| 1148 | mut request: Request, | |
| 1149 | ) -> std::result::Result<Reply, Failed> { | |
| 1081 | 1150 | let Some((_, line)) = self.inner.host.lock().unwrap().clone() else { |
| 1082 | 1151 | return Err(Failed::Unsent(self.offline())); |
| 1083 | 1152 | }; |
| 1084 | 1153 | let chunked = matches!(kind, kind::READ | kind::READ_FILE | kind::PUT); |
| 1085 | 1154 | if chunked { |
| 1086 | let (asked, room) = &self.inner.asked; | |
| 1087 | let mut asked = asked.lock().unwrap(); | |
| 1088 | while *asked + CHUNK > WINDOW { | |
| 1089 | asked = room.wait(asked).unwrap(); | |
| 1155 | #[cfg(not(target_arch = "wasm32"))] | |
| 1156 | { | |
| 1157 | let (asked, _room) = &self.inner.asked; | |
| 1158 | let mut asked = asked.lock().unwrap(); | |
| 1159 | while asked.bytes + CHUNK > WINDOW { | |
| 1160 | asked = _room.wait(asked).unwrap(); | |
| 1161 | } | |
| 1162 | asked.bytes += CHUNK; | |
| 1090 | 1163 | } |
| 1091 | *asked += CHUNK; | |
| 1164 | #[cfg(target_arch = "wasm32")] | |
| 1165 | std::future::poll_fn(|context| { | |
| 1166 | let mut asked = self.inner.asked.0.lock().unwrap(); | |
| 1167 | if asked.bytes + CHUNK > WINDOW { | |
| 1168 | if !asked | |
| 1169 | .waiting | |
| 1170 | .iter() | |
| 1171 | .any(|waker| waker.will_wake(context.waker())) | |
| 1172 | { | |
| 1173 | asked.waiting.push(context.waker().clone()); | |
| 1174 | } | |
| 1175 | std::task::Poll::Pending | |
| 1176 | } else { | |
| 1177 | asked.bytes += CHUNK; | |
| 1178 | std::task::Poll::Ready(()) | |
| 1179 | } | |
| 1180 | }) | |
| 1181 | .await; | |
| 1092 | 1182 | } |
| 1093 | 1183 | let id = self.inner.next.fetch_add(1, Ordering::Relaxed); |
| 1094 | 1184 | request.id = id; |
| 1095 | let (answer, answered) = mpsc::channel(); | |
| 1185 | let (answer, answered) = crate::task::response(); | |
| 1096 | 1186 | self.inner.pending.lock().unwrap().insert(id, answer); |
| 1187 | let _requested = Requested { | |
| 1188 | inner: &self.inner, | |
| 1189 | id, | |
| 1190 | chunked, | |
| 1191 | }; | |
| 1097 | 1192 | let sent = line.send(kind, &request); |
| 1098 | 1193 | let reply = match sent { |
| 1099 | 1194 | Err(error) => Err(Failed::Unsent(error)), |
| 1100 | Ok(()) => match answered.recv_timeout(TIMEOUT) { | |
| 1195 | Ok(()) => match crate::task::answered(&answered, TIMEOUT).await { | |
| 1101 | 1196 | Ok(reply) => Ok(reply), |
| 1102 | 1197 | Err(mpsc::RecvTimeoutError::Timeout) => Err(Failed::Lost(io::Error::new( |
| 1103 | 1198 | io::ErrorKind::TimedOut, |
| ... | ... | @@ -1106,12 +1201,6 @@ impl Guest { |
| 1106 | 1201 | Err(mpsc::RecvTimeoutError::Disconnected) => Err(Failed::Lost(self.offline())), |
| 1107 | 1202 | }, |
| 1108 | 1203 | }; |
| 1109 | self.inner.pending.lock().unwrap().remove(&id); | |
| 1110 | if chunked { | |
| 1111 | let (asked, room) = &self.inner.asked; | |
| 1112 | *asked.lock().unwrap() -= CHUNK; | |
| 1113 | room.notify_all(); | |
| 1114 | } | |
| 1115 | 1204 | let reply = reply?; |
| 1116 | 1205 | match reply.failure { |
| 1117 | 1206 | Some(failure) => Err(Failed::Refused(failure)), |
| ... | ... | @@ -1119,12 +1208,26 @@ impl Guest { |
| 1119 | 1208 | } |
| 1120 | 1209 | } |
| 1121 | 1210 | |
| 1211 | #[cfg(not(target_arch = "wasm32"))] | |
| 1122 | 1212 | fn ask(&self, kind: u16, request: Request) -> io::Result<Reply> { |
| 1123 | 1213 | self.request(kind, request).map_err(Failed::io) |
| 1124 | 1214 | } |
| 1125 | 1215 | |
| 1216 | async fn ask_async(&self, kind: u16, request: Request) -> io::Result<Reply> { | |
| 1217 | self.request_async(kind, request).await.map_err(Failed::io) | |
| 1218 | } | |
| 1219 | ||
| 1126 | 1220 | /// Puts `bytes` in `request`, or uploads them first where they are large. |
| 1221 | #[cfg(not(target_arch = "wasm32"))] | |
| 1127 | 1222 | fn carry(&self, request: &mut Request, bytes: Vec<u8>) -> std::result::Result<(), Failed> { |
| 1223 | crate::task::ready(self.carry_async(request, bytes)).map_err(Failed::Unsent)? | |
| 1224 | } | |
| 1225 | ||
| 1226 | async fn carry_async( | |
| 1227 | &self, | |
| 1228 | request: &mut Request, | |
| 1229 | bytes: Vec<u8>, | |
| 1230 | ) -> std::result::Result<(), Failed> { | |
| 1128 | 1231 | if bytes.len() <= CHUNK { |
| 1129 | 1232 | request.bytes = Some(bytes); |
| 1130 | 1233 | return Ok(()); |
| ... | ... | @@ -1138,7 +1241,8 @@ impl Guest { |
| 1138 | 1241 | ..Request::default() |
| 1139 | 1242 | }; |
| 1140 | 1243 | // Nothing the upload carries happens before the request that uses it. |
| 1141 | self.request(kind::PUT, put) | |
| 1244 | self.request_async(kind::PUT, put) | |
| 1245 | .await | |
| 1142 | 1246 | .map_err(|failed| match failed { |
| 1143 | 1247 | Failed::Lost(error) => Failed::Unsent(error), |
| 1144 | 1248 | failed => failed, |
| ... | ... | @@ -1150,22 +1254,29 @@ impl Guest { |
| 1150 | 1254 | |
| 1151 | 1255 | /// Reads the file at `path` a chunk at a time, as `kind` reads it; a section held, as |
| 1152 | 1256 | /// the changes to it. |
| 1257 | #[cfg(not(target_arch = "wasm32"))] | |
| 1153 | 1258 | fn read(&self, kind: u16, path: &str, limit: usize) -> io::Result<Vec<u8>> { |
| 1259 | crate::task::ready(self.read_async(kind, path, limit))? | |
| 1260 | } | |
| 1261 | ||
| 1262 | async fn read_async(&self, kind: u16, path: &str, limit: usize) -> io::Result<Vec<u8>> { | |
| 1154 | 1263 | let held = (kind == kind::READ) |
| 1155 | 1264 | .then(|| self.inner.image(path)) |
| 1156 | 1265 | .flatten(); |
| 1157 | let first = self.ask( | |
| 1158 | kind, | |
| 1159 | Request { | |
| 1160 | path: path.to_owned(), | |
| 1161 | offset: Some(0), | |
| 1162 | limit: Some(limit as u64), | |
| 1163 | stamp: (held.as_deref()) | |
| 1164 | .and_then(|image| Stamp::of(image).ok()) | |
| 1165 | .map(|stamp| (&stamp).into()), | |
| 1166 | ..Request::default() | |
| 1167 | }, | |
| 1168 | )?; | |
| 1266 | let first = self | |
| 1267 | .ask_async( | |
| 1268 | kind, | |
| 1269 | Request { | |
| 1270 | path: path.to_owned(), | |
| 1271 | offset: Some(0), | |
| 1272 | limit: Some(limit as u64), | |
| 1273 | stamp: (held.as_deref()) | |
| 1274 | .and_then(|image| Stamp::of(image).ok()) | |
| 1275 | .map(|stamp| (&stamp).into()), | |
| 1276 | ..Request::default() | |
| 1277 | }, | |
| 1278 | ) | |
| 1279 | .await?; | |
| 1169 | 1280 | if let (Some(writes), Some(held)) = (&first.writes, held) { |
| 1170 | 1281 | let stamp: Option<Stamp> = first.stamp.as_ref().and_then(|s| s.try_into().ok()); |
| 1171 | 1282 | let image = written(&held, first.length.unwrap_or_default(), writes) |
| ... | ... | @@ -1181,7 +1292,7 @@ impl Guest { |
| 1181 | 1292 | let mut image = first.bytes.unwrap_or_default(); |
| 1182 | 1293 | while image.len() < length { |
| 1183 | 1294 | let chunk = self |
| 1184 | .ask( | |
| 1295 | .ask_async( | |
| 1185 | 1296 | kind, |
| 1186 | 1297 | Request { |
| 1187 | 1298 | path: path.to_owned(), |
| ... | ... | @@ -1189,7 +1300,8 @@ impl Guest { |
| 1189 | 1300 | handle: first.handle, |
| 1190 | 1301 | ..Request::default() |
| 1191 | 1302 | }, |
| 1192 | )? | |
| 1303 | ) | |
| 1304 | .await? | |
| 1193 | 1305 | .bytes |
| 1194 | 1306 | .unwrap_or_default(); |
| 1195 | 1307 | if chunk.is_empty() { |
| ... | ... | @@ -1210,35 +1322,52 @@ impl Guest { |
| 1210 | 1322 | (Stamp::of(&image).ok().as_ref() == Some(stamp)).then(|| image.to_vec()) |
| 1211 | 1323 | } |
| 1212 | 1324 | |
| 1325 | #[cfg(not(target_arch = "wasm32"))] | |
| 1213 | 1326 | pub(crate) fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { |
| 1214 | let reply = self.ask( | |
| 1215 | kind::LIST, | |
| 1216 | Request { | |
| 1217 | path: folder.to_owned(), | |
| 1218 | ..Request::default() | |
| 1219 | }, | |
| 1220 | )?; | |
| 1221 | Ok(reply | |
| 1327 | crate::task::ready(self.entries_async(folder))? | |
| 1328 | } | |
| 1329 | ||
| 1330 | pub(crate) async fn entries_async(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { | |
| 1331 | let reply = self | |
| 1332 | .ask_async( | |
| 1333 | kind::LIST, | |
| 1334 | Request { | |
| 1335 | path: folder.to_owned(), | |
| 1336 | ..Request::default() | |
| 1337 | }, | |
| 1338 | ) | |
| 1339 | .await?; | |
| 1340 | let entries: Vec<_> = reply | |
| 1222 | 1341 | .entries |
| 1223 | 1342 | .unwrap_or_default() |
| 1224 | 1343 | .iter() |
| 1225 | 1344 | .map(entry) |
| 1226 | .collect()) | |
| 1345 | .collect(); | |
| 1346 | #[cfg(target_arch = "wasm32")] | |
| 1347 | self.cache_entries(folder, &entries).await?; | |
| 1348 | Ok(entries) | |
| 1227 | 1349 | } |
| 1228 | 1350 | |
| 1229 | 1351 | /// The stamp of the file at `path`: the host's last report of it where this guest holds |
| 1230 | 1352 | /// that image, else the host's answer. |
| 1353 | #[cfg(not(target_arch = "wasm32"))] | |
| 1231 | 1354 | fn stamp(&self, path: &str) -> io::Result<Stamp> { |
| 1355 | crate::task::ready(self.stamp_async(path))? | |
| 1356 | } | |
| 1357 | ||
| 1358 | async fn stamp_async(&self, path: &str) -> io::Result<Stamp> { | |
| 1232 | 1359 | if let Some(stamp) = self.inner.current.lock().unwrap().get(path) { |
| 1233 | 1360 | return Ok(stamp.clone()); |
| 1234 | 1361 | } |
| 1235 | let reply = self.ask( | |
| 1236 | kind::STAMP, | |
| 1237 | Request { | |
| 1238 | path: path.to_owned(), | |
| 1239 | ..Request::default() | |
| 1240 | }, | |
| 1241 | )?; | |
| 1362 | let reply = self | |
| 1363 | .ask_async( | |
| 1364 | kind::STAMP, | |
| 1365 | Request { | |
| 1366 | path: path.to_owned(), | |
| 1367 | ..Request::default() | |
| 1368 | }, | |
| 1369 | ) | |
| 1370 | .await?; | |
| 1242 | 1371 | reply |
| 1243 | 1372 | .stamp |
| 1244 | 1373 | .as_ref() |
| ... | ... | @@ -1246,34 +1375,114 @@ impl Guest { |
| 1246 | 1375 | .try_into() |
| 1247 | 1376 | } |
| 1248 | 1377 | |
| 1378 | #[cfg(not(target_arch = "wasm32"))] | |
| 1249 | 1379 | fn commit( |
| 1250 | 1380 | &self, |
| 1251 | 1381 | path: &str, |
| 1252 | 1382 | transaction: &Transaction, |
| 1383 | ) -> std::result::Result<(), CommitError> { | |
| 1384 | crate::task::ready(self.commit_async(path, transaction)).map_err(|error| CommitError { | |
| 1385 | state: CommitState::NotCommitted, | |
| 1386 | error, | |
| 1387 | })? | |
| 1388 | } | |
| 1389 | ||
| 1390 | async fn commit_async( | |
| 1391 | &self, | |
| 1392 | path: &str, | |
| 1393 | transaction: &Transaction, | |
| 1253 | 1394 | ) -> std::result::Result<(), CommitError> { |
| 1254 | 1395 | let mut request = Request { |
| 1255 | 1396 | path: path.to_owned(), |
| 1256 | 1397 | ..Request::default() |
| 1257 | 1398 | }; |
| 1258 | self.carry(&mut request, transaction.to_bytes()) | |
| 1399 | self.carry_async(&mut request, transaction.to_bytes()) | |
| 1400 | .await | |
| 1259 | 1401 | .map_err(Failed::commit)?; |
| 1260 | self.request(kind::COMMIT, request) | |
| 1402 | self.request_async(kind::COMMIT, request) | |
| 1403 | .await | |
| 1261 | 1404 | .map(drop) |
| 1262 | 1405 | .map_err(Failed::commit) |
| 1263 | 1406 | } |
| 1264 | 1407 | |
| 1408 | #[cfg(not(target_arch = "wasm32"))] | |
| 1265 | 1409 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> { |
| 1410 | crate::task::ready(self.confirm_async(path, base)).map_err(|error| CommitError { | |
| 1411 | state: CommitState::NotCommitted, | |
| 1412 | error, | |
| 1413 | })? | |
| 1414 | } | |
| 1415 | ||
| 1416 | async fn confirm_async( | |
| 1417 | &self, | |
| 1418 | path: &str, | |
| 1419 | base: &Stamp, | |
| 1420 | ) -> std::result::Result<(), CommitError> { | |
| 1266 | 1421 | let request = Request { |
| 1267 | 1422 | path: path.to_owned(), |
| 1268 | 1423 | stamp: Some(base.into()), |
| 1269 | 1424 | ..Request::default() |
| 1270 | 1425 | }; |
| 1271 | self.request(kind::CONFIRM, request) | |
| 1426 | self.request_async(kind::CONFIRM, request) | |
| 1427 | .await | |
| 1272 | 1428 | .map(drop) |
| 1273 | 1429 | .map_err(Failed::commit) |
| 1274 | 1430 | } |
| 1275 | 1431 | |
| 1432 | async fn edits_async( | |
| 1433 | &self, | |
| 1434 | path: &str, | |
| 1435 | transaction: &Transaction, | |
| 1436 | edits: &[crate::PendingEdit], | |
| 1437 | revisions: &BTreeMap<onestore::ExGuid, onestore::ExGuid>, | |
| 1438 | ) -> std::result::Result<(), CommitError> { | |
| 1439 | let bytes = serde_json::to_vec(&batch::Edits { | |
| 1440 | edits: edits | |
| 1441 | .iter() | |
| 1442 | .map(|edit| (edit.author.clone(), edit.edit.clone())) | |
| 1443 | .collect(), | |
| 1444 | revisions: revisions.clone(), | |
| 1445 | }) | |
| 1446 | .map_err(|error| CommitError { | |
| 1447 | state: CommitState::NotCommitted, | |
| 1448 | error: io::Error::other(error), | |
| 1449 | })?; | |
| 1450 | let mut request = Request { | |
| 1451 | path: path.to_owned(), | |
| 1452 | stamp: Some(transaction.base().into()), | |
| 1453 | ..Request::default() | |
| 1454 | }; | |
| 1455 | self.carry_async(&mut request, bytes) | |
| 1456 | .await | |
| 1457 | .map_err(Failed::commit)?; | |
| 1458 | match self.request_async(kind::EDITS, request).await { | |
| 1459 | Ok(reply) => { | |
| 1460 | if let Some(stamp) = reply.stamp { | |
| 1461 | let stamp = Stamp::try_from(&stamp).map_err(|error| CommitError { | |
| 1462 | state: CommitState::Unknown, | |
| 1463 | error, | |
| 1464 | })?; | |
| 1465 | self.inner | |
| 1466 | .current | |
| 1467 | .lock() | |
| 1468 | .unwrap() | |
| 1469 | .insert(path.to_owned(), stamp); | |
| 1470 | } | |
| 1471 | Ok(()) | |
| 1472 | } | |
| 1473 | Err(Failed::Refused(failure)) | |
| 1474 | if wire::error_kind(failure.kind) == io::ErrorKind::Unsupported => | |
| 1475 | { | |
| 1476 | self.commit_async(path, transaction).await?; | |
| 1477 | self.inner.published(path, transaction); | |
| 1478 | Ok(()) | |
| 1479 | } | |
| 1480 | Err(error) => Err(error.commit()), | |
| 1481 | } | |
| 1482 | } | |
| 1483 | ||
| 1276 | 1484 | /// A request on `path` that answers nothing but whether it happened. |
| 1485 | #[cfg(not(target_arch = "wasm32"))] | |
| 1277 | 1486 | fn verb(&self, kind: u16, path: &str, request: Request) -> Result<()> { |
| 1278 | 1487 | self.ask( |
| 1279 | 1488 | kind, |
| ... | ... | @@ -1338,16 +1547,14 @@ impl Inner { |
| 1338 | 1547 | |
| 1339 | 1548 | fn heard(self: &Arc<Self>, event: Event) { |
| 1340 | 1549 | let serves = |hello: &Hello| { |
| 1341 | hello.serves == Some(self.share) | |
| 1342 | || self | |
| 1343 | .host | |
| 1344 | .lock() | |
| 1345 | .unwrap() | |
| 1346 | .as_ref() | |
| 1347 | .is_some_and(|(host, _)| host.peer == hello.peer) | |
| 1550 | self.host | |
| 1551 | .lock() | |
| 1552 | .unwrap() | |
| 1553 | .as_ref() | |
| 1554 | .is_some_and(|(host, _)| host.peer == hello.peer) | |
| 1348 | 1555 | }; |
| 1349 | 1556 | match event { |
| 1350 | Event::Met(hello, line) if serves(hello) => { | |
| 1557 | Event::Met(hello, line) if hello.serves == Some(self.share) => { | |
| 1351 | 1558 | *self.host.lock().unwrap() = Some((Arc::clone(hello), line.clone())); |
| 1352 | 1559 | } |
| 1353 | 1560 | Event::Left(hello) if serves(hello) => { |
| ... | ... | @@ -1419,7 +1626,13 @@ impl Inner { |
| 1419 | 1626 | self.relay.as_deref(), |
| 1420 | 1627 | move |event| { |
| 1421 | 1628 | if let Some(inner) = inner.upgrade() { |
| 1422 | if matches!(event, Event::Frame { .. }) { | |
| 1629 | if matches!( | |
| 1630 | event, | |
| 1631 | Event::Frame { | |
| 1632 | kind: kind::DELTA | kind::TOUCHED, | |
| 1633 | .. | |
| 1634 | } | |
| 1635 | ) { | |
| 1423 | 1636 | inner.heard(event); |
| 1424 | 1637 | } |
| 1425 | 1638 | (inner.events)(); |
| ... | ... | @@ -1450,6 +1663,8 @@ pub struct HostedRemote { |
| 1450 | 1663 | /// The stamp last asked for, which an image the guest holds may already have. |
| 1451 | 1664 | seen: Option<Stamp>, |
| 1452 | 1665 | rejected: bool, |
| 1666 | #[cfg(target_arch = "wasm32")] | |
| 1667 | pending: Option<web::Pending>, | |
| 1453 | 1668 | } |
| 1454 | 1669 | |
| 1455 | 1670 | impl HostedRemote { |
| ... | ... | @@ -1459,10 +1674,13 @@ impl HostedRemote { |
| 1459 | 1674 | path: path.to_owned(), |
| 1460 | 1675 | seen: None, |
| 1461 | 1676 | rejected: false, |
| 1677 | #[cfg(target_arch = "wasm32")] | |
| 1678 | pending: None, | |
| 1462 | 1679 | } |
| 1463 | 1680 | } |
| 1464 | 1681 | } |
| 1465 | 1682 | |
| 1683 | #[cfg(not(target_arch = "wasm32"))] | |
| 1466 | 1684 | impl crate::Remote for HostedRemote { |
| 1467 | 1685 | fn accepts_edits(&self) -> bool { |
| 1468 | 1686 | !self.rejected |
| ... | ... | @@ -1478,57 +1696,26 @@ impl crate::Remote for HostedRemote { |
| 1478 | 1696 | edits: &[crate::PendingEdit], |
| 1479 | 1697 | revisions: &BTreeMap<onestore::ExGuid, onestore::ExGuid>, |
| 1480 | 1698 | ) -> std::result::Result<(), CommitError> { |
| 1481 | let bytes = serde_json::to_vec(&batch::Edits { | |
| 1482 | edits: edits | |
| 1483 | .iter() | |
| 1484 | .map(|edit| (edit.author.clone(), edit.edit.clone())) | |
| 1485 | .collect(), | |
| 1486 | revisions: revisions.clone(), | |
| 1487 | }) | |
| 1488 | .map_err(|error| CommitError { | |
| 1489 | state: CommitState::NotCommitted, | |
| 1490 | error: io::Error::other(error), | |
| 1491 | })?; | |
| 1492 | let mut request = Request { | |
| 1493 | path: self.path.clone(), | |
| 1494 | stamp: Some(transaction.base().into()), | |
| 1495 | ..Request::default() | |
| 1496 | }; | |
| 1497 | self.guest | |
| 1498 | .carry(&mut request, bytes) | |
| 1499 | .map_err(Failed::commit)?; | |
| 1500 | let result = self.guest.request(kind::EDITS, request); | |
| 1501 | match result { | |
| 1502 | Ok(reply) => { | |
| 1503 | if let Some(stamp) = reply.stamp { | |
| 1504 | let stamp = Stamp::try_from(&stamp).map_err(|error| CommitError { | |
| 1505 | state: CommitState::Unknown, | |
| 1506 | error, | |
| 1507 | })?; | |
| 1508 | self.guest | |
| 1509 | .inner | |
| 1510 | .current | |
| 1511 | .lock() | |
| 1512 | .unwrap() | |
| 1513 | .insert(self.path.clone(), stamp); | |
| 1514 | } | |
| 1515 | self.seen = None; | |
| 1516 | Ok(()) | |
| 1517 | } | |
| 1518 | Err(Failed::Refused(failure)) | |
| 1519 | if wire::error_kind(failure.kind) == io::ErrorKind::Unsupported => | |
| 1520 | { | |
| 1521 | self.publish(transaction) | |
| 1522 | } | |
| 1523 | Err(error) => { | |
| 1524 | let error = error.commit(); | |
| 1525 | if error.state == CommitState::NotCommitted { | |
| 1526 | self.rejected = true; | |
| 1527 | self.guest.inner.current.lock().unwrap().remove(&self.path); | |
| 1528 | } | |
| 1529 | Err(error) | |
| 1530 | } | |
| 1699 | let result = | |
| 1700 | crate::task::ready( | |
| 1701 | self.guest | |
| 1702 | .edits_async(&self.path, transaction, edits, revisions), | |
| 1703 | ) | |
| 1704 | .map_err(|error| CommitError { | |
| 1705 | state: CommitState::NotCommitted, | |
| 1706 | error, | |
| 1707 | })?; | |
| 1708 | if result.is_ok() { | |
| 1709 | self.seen = None; | |
| 1710 | } | |
| 1711 | if result | |
| 1712 | .as_ref() | |
| 1713 | .is_err_and(|error| error.state == CommitState::NotCommitted) | |
| 1714 | { | |
| 1715 | self.rejected = true; | |
| 1716 | self.guest.inner.current.lock().unwrap().remove(&self.path); | |
| 1531 | 1717 | } |
| 1718 | result | |
| 1532 | 1719 | } |
| 1533 | 1720 | |
| 1534 | 1721 | fn read(&mut self) -> io::Result<Vec<u8>> { |
| ... | ... | @@ -1574,17 +1761,23 @@ type Listings = BTreeMap<String, Vec<(String, u8, u64, u64)>>; |
| 1574 | 1761 | |
| 1575 | 1762 | impl Hosted { |
| 1576 | 1763 | pub(crate) fn new(guest: Arc<Guest>, listed: PathBuf) -> Self { |
| 1764 | #[cfg(target_arch = "wasm32")] | |
| 1765 | { | |
| 1766 | *guest.inner.catalog.lock().unwrap() = Some(listed.clone()); | |
| 1767 | } | |
| 1577 | 1768 | Self { guest, listed } |
| 1578 | 1769 | } |
| 1579 | 1770 | } |
| 1580 | 1771 | |
| 1581 | 1772 | /// The host's folders, or as they were last listed while it can't be reached. |
| 1773 | #[cfg(not(target_arch = "wasm32"))] | |
| 1582 | 1774 | struct Source<'a> { |
| 1583 | 1775 | guest: &'a Guest, |
| 1584 | 1776 | kept: Listings, |
| 1585 | 1777 | listed: Listings, |
| 1586 | 1778 | } |
| 1587 | 1779 | |
| 1780 | #[cfg(not(target_arch = "wasm32"))] | |
| 1588 | 1781 | impl discover::Source for Source<'_> { |
| 1589 | 1782 | fn entries(&mut self, path: &str, limit: usize) -> io::Result<Vec<discover::Entry>> { |
| 1590 | 1783 | let entries = match self.guest.entries(path) { |
| ... | ... | @@ -1630,6 +1823,7 @@ impl discover::Source for Source<'_> { |
| 1630 | 1823 | } |
| 1631 | 1824 | } |
| 1632 | 1825 | |
| 1826 | #[cfg(not(target_arch = "wasm32"))] | |
| 1633 | 1827 | impl Storage for Hosted { |
| 1634 | 1828 | fn discover( |
| 1635 | 1829 | &self, |
| ... | ... | @@ -1749,7 +1943,7 @@ impl Storage for Hosted { |
| 1749 | 1943 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { |
| 1750 | 1944 | let request = Request { |
| 1751 | 1945 | to: Some(with.to_owned()), |
| 1752 | stamp: Some(WireStamp::from(base)), | |
| 1946 | stamp: Some(wire::WireStamp::from(base)), | |
| 1753 | 1947 | ..Request::default() |
| 1754 | 1948 | }; |
| 1755 | 1949 | self.guest.verb(kind::SUPERSEDE, path, request) |
crates/notebook/src/live/share/web.rs created+382| ... | ... | @@ -0,0 +1,382 @@ |
| 1 | use super::*; | |
| 2 | use std::{future::Future, pin::Pin}; | |
| 3 | ||
| 4 | pub(super) enum Outcome { | |
| 5 | Read(Vec<u8>), | |
| 6 | Stamp(Box<Stamp>), | |
| 7 | Published, | |
| 8 | } | |
| 9 | ||
| 10 | pub(super) enum Pending { | |
| 11 | Running(Pin<Box<dyn Future<Output = std::result::Result<Outcome, CommitError>>>>), | |
| 12 | Ready(std::result::Result<Outcome, CommitError>), | |
| 13 | } | |
| 14 | ||
| 15 | fn uncommitted(error: io::Error) -> CommitError { | |
| 16 | CommitError { | |
| 17 | state: CommitState::NotCommitted, | |
| 18 | error, | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | impl HostedRemote { | |
| 23 | fn operation( | |
| 24 | &mut self, | |
| 25 | work: impl Future<Output = std::result::Result<Outcome, CommitError>> + 'static, | |
| 26 | ) -> std::result::Result<Outcome, CommitError> { | |
| 27 | match self.pending.take() { | |
| 28 | Some(Pending::Ready(result)) => result, | |
| 29 | pending => { | |
| 30 | self.pending = Some(pending.unwrap_or_else(|| Pending::Running(Box::pin(work)))); | |
| 31 | Err(uncommitted(io::ErrorKind::WouldBlock.into())) | |
| 32 | } | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | fn publication( | |
| 37 | &mut self, | |
| 38 | result: std::result::Result<Outcome, CommitError>, | |
| 39 | ) -> std::result::Result<(), CommitError> { | |
| 40 | match result { | |
| 41 | Ok(Outcome::Published) => { | |
| 42 | self.seen = None; | |
| 43 | Ok(()) | |
| 44 | } | |
| 45 | Ok(_) => Err(uncommitted(io::ErrorKind::InvalidInput.into())), | |
| 46 | Err(error) => { | |
| 47 | if error.error.kind() != io::ErrorKind::WouldBlock | |
| 48 | && error.state == CommitState::NotCommitted | |
| 49 | { | |
| 50 | self.rejected = true; | |
| 51 | self.guest.inner.current.lock().unwrap().remove(&self.path); | |
| 52 | } | |
| 53 | Err(error) | |
| 54 | } | |
| 55 | } | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | impl crate::Remote for HostedRemote { | |
| 60 | fn pending(&mut self) -> Option<Pin<Box<dyn Future<Output = ()> + '_>>> { | |
| 61 | let pending = self.pending.as_mut()?; | |
| 62 | Some(Box::pin(async move { | |
| 63 | if let Pending::Running(work) = pending { | |
| 64 | *pending = Pending::Ready(work.await); | |
| 65 | } | |
| 66 | })) | |
| 67 | } | |
| 68 | ||
| 69 | fn read(&mut self) -> io::Result<Vec<u8>> { | |
| 70 | let (guest, path, seen) = (self.guest.clone(), self.path.clone(), self.seen.clone()); | |
| 71 | let result = self | |
| 72 | .operation(async move { | |
| 73 | let image = match seen.as_ref().and_then(|stamp| guest.held(&path, stamp)) { | |
| 74 | Some(image) => image, | |
| 75 | None => guest | |
| 76 | .read_async(kind::READ, &path, LIMIT) | |
| 77 | .await | |
| 78 | .map_err(uncommitted)?, | |
| 79 | }; | |
| 80 | Ok(Outcome::Read(image)) | |
| 81 | }) | |
| 82 | .map_err(|error| error.error)?; | |
| 83 | match result { | |
| 84 | Outcome::Read(image) => { | |
| 85 | self.rejected = false; | |
| 86 | Ok(image) | |
| 87 | } | |
| 88 | _ => Err(io::ErrorKind::InvalidInput.into()), | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | fn stamp(&mut self) -> io::Result<Stamp> { | |
| 93 | let (guest, path) = (self.guest.clone(), self.path.clone()); | |
| 94 | let result = self | |
| 95 | .operation(async move { | |
| 96 | guest | |
| 97 | .stamp_async(&path) | |
| 98 | .await | |
| 99 | .map(|stamp| Outcome::Stamp(Box::new(stamp))) | |
| 100 | .map_err(uncommitted) | |
| 101 | }) | |
| 102 | .map_err(|error| error.error)?; | |
| 103 | match result { | |
| 104 | Outcome::Stamp(stamp) => { | |
| 105 | self.seen = Some((*stamp).clone()); | |
| 106 | Ok(*stamp) | |
| 107 | } | |
| 108 | _ => Err(io::ErrorKind::InvalidInput.into()), | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | fn accepts_edits(&self) -> bool { | |
| 113 | !self.rejected | |
| 114 | && self | |
| 115 | .guest | |
| 116 | .host() | |
| 117 | .is_some_and(|host| host.ops == Some(1) && host.kinds.contains(&kind::EDITS)) | |
| 118 | } | |
| 119 | ||
| 120 | fn publish_edits( | |
| 121 | &mut self, | |
| 122 | transaction: &Transaction, | |
| 123 | edits: &[crate::PendingEdit], | |
| 124 | revisions: &BTreeMap<onestore::ExGuid, onestore::ExGuid>, | |
| 125 | ) -> std::result::Result<(), CommitError> { | |
| 126 | let (guest, path, transaction, edits, revisions) = ( | |
| 127 | self.guest.clone(), | |
| 128 | self.path.clone(), | |
| 129 | transaction.clone(), | |
| 130 | edits.to_vec(), | |
| 131 | revisions.clone(), | |
| 132 | ); | |
| 133 | let result = self.operation(async move { | |
| 134 | guest | |
| 135 | .edits_async(&path, &transaction, &edits, &revisions) | |
| 136 | .await?; | |
| 137 | Ok(Outcome::Published) | |
| 138 | }); | |
| 139 | self.publication(result) | |
| 140 | } | |
| 141 | ||
| 142 | fn publish(&mut self, transaction: &Transaction) -> std::result::Result<(), CommitError> { | |
| 143 | let (guest, path, transaction) = | |
| 144 | (self.guest.clone(), self.path.clone(), transaction.clone()); | |
| 145 | let result = self.operation(async move { | |
| 146 | guest.commit_async(&path, &transaction).await?; | |
| 147 | guest.inner.published(&path, &transaction); | |
| 148 | Ok(Outcome::Published) | |
| 149 | }); | |
| 150 | self.publication(result) | |
| 151 | } | |
| 152 | ||
| 153 | fn confirm(&mut self, base: &Stamp) -> std::result::Result<(), CommitError> { | |
| 154 | let (guest, path, base) = (self.guest.clone(), self.path.clone(), base.clone()); | |
| 155 | let result = self.operation(async move { | |
| 156 | guest.confirm_async(&path, &base).await?; | |
| 157 | Ok(Outcome::Published) | |
| 158 | }); | |
| 159 | self.publication(result) | |
| 160 | } | |
| 161 | } | |
| 162 | ||
| 163 | struct Cached<'a>(&'a Hosted, Listings); | |
| 164 | ||
| 165 | impl Guest { | |
| 166 | pub(super) async fn cache_entries( | |
| 167 | &self, | |
| 168 | folder: &str, | |
| 169 | entries: &[discover::Entry], | |
| 170 | ) -> io::Result<()> { | |
| 171 | let Some(listed) = self.inner.catalog.lock().unwrap().clone() else { | |
| 172 | return Ok(()); | |
| 173 | }; | |
| 174 | let files = listed.with_extension("files"); | |
| 175 | let mut listings: Listings = crate::fs::read(&listed) | |
| 176 | .ok() | |
| 177 | .and_then(|bytes| serde_json::from_slice(&bytes).ok()) | |
| 178 | .unwrap_or_default(); | |
| 179 | let before = listings.get(folder).cloned().unwrap_or_default(); | |
| 180 | let mut current = Vec::with_capacity(entries.len()); | |
| 181 | crate::fs::create_dir_all(files.join(folder))?; | |
| 182 | for entry in entries { | |
| 183 | if entry.name.contains(['/', '\\', '\0']) || matches!(entry.name.as_str(), "." | "..") { | |
| 184 | return Err(io::ErrorKind::InvalidData.into()); | |
| 185 | } | |
| 186 | let e = wire_entry(entry); | |
| 187 | let listed = (e.name, e.kind, e.size, e.modified); | |
| 188 | let path = if folder.is_empty() { | |
| 189 | entry.name.clone() | |
| 190 | } else { | |
| 191 | format!("{folder}/{}", entry.name) | |
| 192 | }; | |
| 193 | if entry.kind == discover::EntryKind::File | |
| 194 | && (!before.contains(&listed) || crate::fs::metadata(files.join(&path)).is_err()) | |
| 195 | { | |
| 196 | let kind = if path.ends_with(".one") || path.ends_with(".onetoc2") { | |
| 197 | kind::READ | |
| 198 | } else { | |
| 199 | kind::READ_FILE | |
| 200 | }; | |
| 201 | let bytes = self.read_async(kind, &path, LIMIT).await?; | |
| 202 | crate::fs::write(files.join(path), bytes)?; | |
| 203 | } | |
| 204 | current.push(listed); | |
| 205 | } | |
| 206 | // Other folders may have finished listing while these files were being read. | |
| 207 | if let Ok(bytes) = crate::fs::read(&listed) { | |
| 208 | listings = serde_json::from_slice(&bytes).map_err(io::Error::other)?; | |
| 209 | } | |
| 210 | if listings.get(folder) != Some(&current) { | |
| 211 | listings.insert(folder.to_owned(), current); | |
| 212 | crate::fs::write(&listed, serde_json::to_vec(&listings)?)?; | |
| 213 | crate::fs::durable().await?; | |
| 214 | if let Some(reports) = &*self.inner.watch.lock().unwrap() { | |
| 215 | reports.catalog(folder); | |
| 216 | } | |
| 217 | (self.inner.events)(); | |
| 218 | } | |
| 219 | Ok(()) | |
| 220 | } | |
| 221 | } | |
| 222 | ||
| 223 | impl discover::Source for Cached<'_> { | |
| 224 | fn entries(&mut self, path: &str, limit: usize) -> io::Result<Vec<discover::Entry>> { | |
| 225 | let entries = self.1.get(path).ok_or(io::ErrorKind::NotConnected)?; | |
| 226 | if entries.len() > limit { | |
| 227 | return Err(io::ErrorKind::FileTooLarge.into()); | |
| 228 | } | |
| 229 | Ok(entries | |
| 230 | .iter() | |
| 231 | .map(|(name, kind, size, modified)| { | |
| 232 | entry(&WireEntry { | |
| 233 | name: name.clone(), | |
| 234 | kind: *kind, | |
| 235 | size: *size, | |
| 236 | modified: *modified, | |
| 237 | }) | |
| 238 | }) | |
| 239 | .collect()) | |
| 240 | } | |
| 241 | fn read(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { | |
| 242 | let bytes = self | |
| 243 | .0 | |
| 244 | .guest | |
| 245 | .inner | |
| 246 | .image(path) | |
| 247 | .map(|image| image.to_vec()) | |
| 248 | .map(Ok) | |
| 249 | .unwrap_or_else(|| crate::fs::read(self.0.files().join(path)))?; | |
| 250 | if bytes.len() > limit { | |
| 251 | return Err(io::ErrorKind::FileTooLarge.into()); | |
| 252 | } | |
| 253 | Ok(bytes) | |
| 254 | } | |
| 255 | fn read_asset(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { | |
| 256 | self.read(path, limit) | |
| 257 | } | |
| 258 | } | |
| 259 | ||
| 260 | impl Hosted { | |
| 261 | fn files(&self) -> PathBuf { | |
| 262 | self.listed.with_extension("files") | |
| 263 | } | |
| 264 | ||
| 265 | /// Loads a bounded catalog and its files before the synchronous editor opens it. | |
| 266 | pub async fn prepare(guest: Arc<Guest>, cache: &std::path::Path) -> io::Result<()> { | |
| 267 | let listed = | |
| 268 | crate::session::listing(cache, &guest.location()).with_extension("entries.json"); | |
| 269 | let hosted = Self::new(guest, listed); | |
| 270 | let deadline = Instant::now() + Duration::from_secs(20); | |
| 271 | let (_, waiting) = crate::task::channel(); | |
| 272 | while hosted.guest.host().is_none() { | |
| 273 | if Instant::now() >= deadline { | |
| 274 | return Err(hosted.guest.offline()); | |
| 275 | } | |
| 276 | crate::task::wait(&waiting, Some(Duration::from_millis(50))).await; | |
| 277 | } | |
| 278 | let mut folders = vec![String::new()]; | |
| 279 | let mut count = 0; | |
| 280 | while let Some(folder) = folders.pop() { | |
| 281 | if folder.split('/').count() > 32 { | |
| 282 | return Err(io::ErrorKind::FileTooLarge.into()); | |
| 283 | } | |
| 284 | let entries = hosted.guest.entries_async(&folder).await?; | |
| 285 | count += entries.len(); | |
| 286 | if count > 10000 { | |
| 287 | return Err(io::ErrorKind::FileTooLarge.into()); | |
| 288 | } | |
| 289 | crate::fs::create_dir_all(hosted.files().join(&folder))?; | |
| 290 | for entry in &entries { | |
| 291 | if entry.kind == discover::EntryKind::Directory { | |
| 292 | folders.push(if folder.is_empty() { | |
| 293 | entry.name.clone() | |
| 294 | } else { | |
| 295 | format!("{folder}/{}", entry.name) | |
| 296 | }); | |
| 297 | } | |
| 298 | } | |
| 299 | } | |
| 300 | crate::fs::durable().await | |
| 301 | } | |
| 302 | } | |
| 303 | ||
| 304 | fn desktop() -> Error { | |
| 305 | io::Error::new( | |
| 306 | io::ErrorKind::Unsupported, | |
| 307 | "Create and organize sections in desktop Snowbound.", | |
| 308 | ) | |
| 309 | .into() | |
| 310 | } | |
| 311 | ||
| 312 | impl Storage for Hosted { | |
| 313 | fn discover( | |
| 314 | &self, | |
| 315 | cache: &mut discover::Cache, | |
| 316 | limits: discover::Limits, | |
| 317 | ) -> Result<discover::Folder> { | |
| 318 | let listings = | |
| 319 | serde_json::from_slice(&crate::fs::read(&self.listed)?).map_err(io::Error::other)?; | |
| 320 | Ok(cache.discover(&mut Cached(self, listings), limits)?) | |
| 321 | } | |
| 322 | fn location(&self) -> String { | |
| 323 | self.guest.location() | |
| 324 | } | |
| 325 | fn entries(&self, path: &str) -> io::Result<Vec<discover::Entry>> { | |
| 326 | let listings = | |
| 327 | serde_json::from_slice(&crate::fs::read(&self.listed)?).map_err(io::Error::other)?; | |
| 328 | discover::Source::entries(&mut Cached(self, listings), path, 10000) | |
| 329 | } | |
| 330 | fn exists(&self, path: &str) -> bool { | |
| 331 | crate::fs::metadata(self.files().join(path)).is_ok() | |
| 332 | } | |
| 333 | fn stamp(&self, path: &str) -> io::Result<Stamp> { | |
| 334 | Stamp::of(&self.read(path).map_err(io::Error::other)?).map_err(io::Error::other) | |
| 335 | } | |
| 336 | fn read(&self, path: &str) -> Result<Vec<u8>> { | |
| 337 | self.read_file(path, LIMIT) | |
| 338 | } | |
| 339 | fn read_file(&self, path: &str, limit: usize) -> Result<Vec<u8>> { | |
| 340 | Ok(discover::Source::read( | |
| 341 | &mut Cached(self, Listings::new()), | |
| 342 | path, | |
| 343 | limit, | |
| 344 | )?) | |
| 345 | } | |
| 346 | fn create(&self, _: &str, _: &[u8]) -> Result<()> { | |
| 347 | Err(desktop()) | |
| 348 | } | |
| 349 | fn create_directory(&self, _: &str) -> Result<()> { | |
| 350 | Err(desktop()) | |
| 351 | } | |
| 352 | fn hide(&self, _: &str) -> Result<()> { | |
| 353 | Err(desktop()) | |
| 354 | } | |
| 355 | fn rename(&self, _: &str, _: &str) -> Result<()> { | |
| 356 | Err(desktop()) | |
| 357 | } | |
| 358 | fn rename_root(&self, _: &str, _: &[String]) -> Result<String> { | |
| 359 | Err(desktop()) | |
| 360 | } | |
| 361 | fn replace(&self, _: &str, _: &str) -> Result<()> { | |
| 362 | Err(desktop()) | |
| 363 | } | |
| 364 | fn delete(&self, _: &str) -> Result<()> { | |
| 365 | Err(desktop()) | |
| 366 | } | |
| 367 | fn place(&self, _: &str, _: [u8; 16], _: &str) -> Result<()> { | |
| 368 | Err(desktop()) | |
| 369 | } | |
| 370 | fn commit(&self, _: &str, _: &Transaction) -> Result<()> { | |
| 371 | Err(desktop()) | |
| 372 | } | |
| 373 | fn confirm(&self, _: &str, _: &Stamp) -> std::result::Result<(), CommitError> { | |
| 374 | Err(uncommitted(io::Error::new( | |
| 375 | io::ErrorKind::Unsupported, | |
| 376 | "Section management requires desktop Snowbound", | |
| 377 | ))) | |
| 378 | } | |
| 379 | fn supersede(&self, _: &str, _: &Stamp, _: &str) -> Result<()> { | |
| 380 | Err(desktop()) | |
| 381 | } | |
| 382 | } |
crates/notebook/src/live/transport.rs+1-21| ... | ... | @@ -28,27 +28,7 @@ const MOST: usize = 4 << 20; |
| 28 | 28 | /// Set once WebSockets were refused, so later connections go straight to polling. |
| 29 | 29 | static POLLING: AtomicBool = AtomicBool::new(false); |
| 30 | 30 | |
| 31 | /// Why a relay could not be reached, as a person can act on it. | |
| 32 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 33 | pub enum Trouble { | |
| 34 | /// The relay's name, or the proxy's, didn't resolve. | |
| 35 | Dns, | |
| 36 | /// Nothing answered at the relay's address: a firewall, or the relay is down. | |
| 37 | Unreachable, | |
| 38 | TimedOut, | |
| 39 | /// The proxy itself couldn't be reached. | |
| 40 | ProxyUnreachable, | |
| 41 | /// The proxy asks for a name and password (407). | |
| 42 | ProxyAuthentication, | |
| 43 | /// The proxy refused to connect to the relay, with this status. | |
| 44 | ProxyRefused(u16), | |
| 45 | /// The relay's certificate wasn't one the system trusts: something on the way presents its | |
| 46 | /// own. | |
| 47 | Certificate, | |
| 48 | /// Something on the way refused both WebSockets and plain requests to the relay. | |
| 49 | Blocked, | |
| 50 | Other, | |
| 51 | } | |
| 31 | pub use super::model::Trouble; | |
| 52 | 32 | |
| 53 | 33 | pub(super) enum Failure { |
| 54 | 34 | Trouble(Trouble, io::Error), |
crates/notebook/src/live/web.js created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | const sockets = new Map(); | |
| 2 | let next = 1; | |
| 3 | ||
| 4 | export function liveConnect(url, message, closed) { | |
| 5 | const id = next++; | |
| 6 | const socket = new WebSocket(url); | |
| 7 | socket.binaryType = 'arraybuffer'; | |
| 8 | socket.onmessage = event => message(event.data); | |
| 9 | socket.onclose = () => { | |
| 10 | liveClose(id); | |
| 11 | closed(); | |
| 12 | }; | |
| 13 | sockets.set(id, socket); | |
| 14 | return id; | |
| 15 | } | |
| 16 | ||
| 17 | export function liveSend(id, bytes) { | |
| 18 | const socket = sockets.get(id); | |
| 19 | if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error('Relay disconnected'); | |
| 20 | if (socket.bufferedAmount > 1048576) throw new Error('Relay cannot keep up'); | |
| 21 | socket.send(bytes); | |
| 22 | } | |
| 23 | ||
| 24 | export function liveClose(id) { | |
| 25 | const socket = sockets.get(id); | |
| 26 | sockets.delete(id); | |
| 27 | if (!socket) return; | |
| 28 | socket.onmessage = socket.onclose = null; | |
| 29 | socket.close(); | |
| 30 | } |
crates/notebook/src/live/web.rs created+612| ... | ... | @@ -0,0 +1,612 @@ |
| 1 | //! Browser Live Share: event-driven relay connections carrying the same sealed streams. | |
| 2 | ||
| 3 | pub use ::relay::code; | |
| 4 | #[path = "group.rs"] | |
| 5 | mod group; | |
| 6 | #[path = "model.rs"] | |
| 7 | mod model; | |
| 8 | #[path = "share.rs"] | |
| 9 | pub mod share; | |
| 10 | #[path = "wire.rs"] | |
| 11 | pub mod wire; | |
| 12 | use model::hex; | |
| 13 | pub use model::{Event, Peer, Reach, Relayed, Room, Trouble}; | |
| 14 | pub use wire::{Caret, Guid, Hello, Presence, Spot}; | |
| 15 | ||
| 16 | use minicbor::Encode; | |
| 17 | use std::{ | |
| 18 | collections::BTreeMap, | |
| 19 | io, | |
| 20 | sync::{ | |
| 21 | Arc, Mutex, Weak, | |
| 22 | atomic::{AtomicBool, Ordering}, | |
| 23 | }, | |
| 24 | time::Duration, | |
| 25 | }; | |
| 26 | use wasm_bindgen::prelude::*; | |
| 27 | use wire::{Side, kind}; | |
| 28 | ||
| 29 | #[wasm_bindgen(module = "/src/live/web.js")] | |
| 30 | extern "C" { | |
| 31 | #[wasm_bindgen(catch, js_name = liveConnect)] | |
| 32 | fn connect(url: &str, message: &JsValue, closed: &JsValue) -> Result<u32, JsValue>; | |
| 33 | #[wasm_bindgen(catch, js_name = liveSend)] | |
| 34 | fn send(id: u32, bytes: &[u8]) -> Result<(), JsValue>; | |
| 35 | #[wasm_bindgen(js_name = liveClose)] | |
| 36 | fn close(id: u32); | |
| 37 | } | |
| 38 | ||
| 39 | pub struct Live(Arc<Shared>); | |
| 40 | ||
| 41 | struct Shared { | |
| 42 | me: Hello, | |
| 43 | room: Room, | |
| 44 | url: String, | |
| 45 | events: Box<dyn Fn(Event) + Send + Sync>, | |
| 46 | stopped: AtomicBool, | |
| 47 | state: Mutex<State>, | |
| 48 | } | |
| 49 | ||
| 50 | struct State { | |
| 51 | socket: u32, | |
| 52 | slot: u32, | |
| 53 | relayed: Relayed, | |
| 54 | failed: u32, | |
| 55 | outdated: Option<u16>, | |
| 56 | burned: bool, | |
| 57 | streams: BTreeMap<u32, Stream>, | |
| 58 | members: BTreeMap<u32, group::Member>, | |
| 59 | sealer: Option<group::Sealer>, | |
| 60 | presence: Presence, | |
| 61 | scheduled: bool, | |
| 62 | } | |
| 63 | ||
| 64 | struct Stream { | |
| 65 | bytes: Vec<u8>, | |
| 66 | opening: Option<wire::Opening>, | |
| 67 | receive: Option<wire::Sealer>, | |
| 68 | line: Line, | |
| 69 | peer: Option<Peer>, | |
| 70 | } | |
| 71 | ||
| 72 | #[derive(Clone)] | |
| 73 | pub struct Line { | |
| 74 | shared: Weak<Shared>, | |
| 75 | slot: u32, | |
| 76 | sealer: Arc<Mutex<Option<wire::Sealer>>>, | |
| 77 | } | |
| 78 | ||
| 79 | impl Line { | |
| 80 | pub fn send(&self, kind: u16, body: &impl Encode<()>) -> io::Result<()> { | |
| 81 | let shared = self.shared.upgrade().ok_or(io::ErrorKind::NotConnected)?; | |
| 82 | let mut bytes = Vec::new(); | |
| 83 | self.sealer | |
| 84 | .lock() | |
| 85 | .unwrap() | |
| 86 | .as_mut() | |
| 87 | .ok_or(io::ErrorKind::NotConnected)? | |
| 88 | .send(&mut bytes, kind, body)?; | |
| 89 | shared.stream(self.slot, &bytes) | |
| 90 | } | |
| 91 | ||
| 92 | pub fn hang_up(&self, reason: &str) { | |
| 93 | let _ = self.send( | |
| 94 | kind::BYE, | |
| 95 | &wire::Bye { | |
| 96 | reason: reason.into(), | |
| 97 | }, | |
| 98 | ); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | #[derive(Clone)] | |
| 103 | pub struct Sender(Weak<Shared>); | |
| 104 | ||
| 105 | impl Sender { | |
| 106 | pub fn send(&self, kind: u16, body: &impl Encode<()>, to: Option<&[[u8; 16]]>) { | |
| 107 | if let Some(shared) = self.0.upgrade() { | |
| 108 | match to { | |
| 109 | None => { | |
| 110 | let _ = shared.group(kind, body, None); | |
| 111 | } | |
| 112 | Some(to) => { | |
| 113 | let slots: Vec<_> = shared | |
| 114 | .state | |
| 115 | .lock() | |
| 116 | .unwrap() | |
| 117 | .members | |
| 118 | .iter() | |
| 119 | .filter(|(_, m)| { | |
| 120 | m.peer.as_ref().is_some_and(|p| to.contains(&p.hello.peer)) | |
| 121 | }) | |
| 122 | .map(|(slot, _)| *slot) | |
| 123 | .collect(); | |
| 124 | for slot in slots { | |
| 125 | let _ = shared.group(kind, body, Some(slot)); | |
| 126 | } | |
| 127 | } | |
| 128 | } | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | impl Live { | |
| 134 | pub fn start( | |
| 135 | me: Hello, | |
| 136 | room: &Room, | |
| 137 | _: Option<Reach>, | |
| 138 | relay: Option<&str>, | |
| 139 | events: impl Fn(Event) + Send + Sync + 'static, | |
| 140 | ) -> io::Result<Self> { | |
| 141 | let tag = room.tag().ok_or(io::ErrorKind::InvalidInput)?; | |
| 142 | let relay = relay | |
| 143 | .ok_or(io::ErrorKind::NotConnected)? | |
| 144 | .trim_end_matches('/'); | |
| 145 | if !relay.starts_with("wss://") && !relay.starts_with("ws://") { | |
| 146 | return Err(io::ErrorKind::InvalidInput.into()); | |
| 147 | } | |
| 148 | let shared = Arc::new(Shared { | |
| 149 | me, | |
| 150 | room: room.clone(), | |
| 151 | url: format!("{relay}/v1/room/{tag}"), | |
| 152 | events: Box::new(events), | |
| 153 | stopped: AtomicBool::new(false), | |
| 154 | state: Mutex::new(State { | |
| 155 | socket: 0, | |
| 156 | slot: 0, | |
| 157 | relayed: Relayed::Unknown, | |
| 158 | failed: 0, | |
| 159 | outdated: None, | |
| 160 | burned: false, | |
| 161 | streams: BTreeMap::new(), | |
| 162 | members: BTreeMap::new(), | |
| 163 | sealer: None, | |
| 164 | presence: Presence::default(), | |
| 165 | scheduled: false, | |
| 166 | }), | |
| 167 | }); | |
| 168 | shared.connect()?; | |
| 169 | let weak = Arc::downgrade(&shared); | |
| 170 | crate::task::spawn("live ping", move || async move { | |
| 171 | let (_, wait) = crate::task::channel(); | |
| 172 | loop { | |
| 173 | crate::task::wait(&wait, Some(Duration::from_secs(15))).await; | |
| 174 | let Some(shared) = weak | |
| 175 | .upgrade() | |
| 176 | .filter(|s| !s.stopped.load(Ordering::Acquire)) | |
| 177 | else { | |
| 178 | break; | |
| 179 | }; | |
| 180 | let lines: Vec<_> = shared | |
| 181 | .state | |
| 182 | .lock() | |
| 183 | .unwrap() | |
| 184 | .streams | |
| 185 | .values() | |
| 186 | .map(|s| s.line.clone()) | |
| 187 | .collect(); | |
| 188 | for line in lines { | |
| 189 | let _ = line.send(kind::PING, &()); | |
| 190 | } | |
| 191 | if matches!(shared.room, Room::Notebook(_)) { | |
| 192 | let _ = shared.group(kind::PING, &(), None); | |
| 193 | } | |
| 194 | } | |
| 195 | })?; | |
| 196 | Ok(Self(shared)) | |
| 197 | } | |
| 198 | ||
| 199 | pub fn code(&self) -> Option<String> { | |
| 200 | match &self.0.room { | |
| 201 | Room::Code { code, .. } => Some(code.clone()), | |
| 202 | _ => None, | |
| 203 | } | |
| 204 | } | |
| 205 | pub fn relayed(&self) -> Relayed { | |
| 206 | self.0.state.lock().unwrap().relayed.clone() | |
| 207 | } | |
| 208 | pub fn failed(&self) -> u32 { | |
| 209 | self.0.state.lock().unwrap().failed | |
| 210 | } | |
| 211 | pub fn other_version(&self) -> Option<u16> { | |
| 212 | self.0.state.lock().unwrap().outdated | |
| 213 | } | |
| 214 | pub fn burned(&self) -> bool { | |
| 215 | self.0.state.lock().unwrap().burned | |
| 216 | } | |
| 217 | pub fn sender(&self) -> Sender { | |
| 218 | Sender(Arc::downgrade(&self.0)) | |
| 219 | } | |
| 220 | pub fn peers(&self) -> Vec<Peer> { | |
| 221 | let state = self.0.state.lock().unwrap(); | |
| 222 | let mut peers = BTreeMap::new(); | |
| 223 | for peer in state | |
| 224 | .members | |
| 225 | .values() | |
| 226 | .filter_map(|m| m.peer.as_ref()) | |
| 227 | .chain(state.streams.values().filter_map(|s| s.peer.as_ref())) | |
| 228 | { | |
| 229 | peers.insert(peer.hello.peer, peer.clone()); | |
| 230 | } | |
| 231 | peers.into_values().collect() | |
| 232 | } | |
| 233 | pub fn line(&self, peer: &[u8; 16]) -> Option<Line> { | |
| 234 | self.0 | |
| 235 | .state | |
| 236 | .lock() | |
| 237 | .unwrap() | |
| 238 | .streams | |
| 239 | .values() | |
| 240 | .find(|s| s.peer.as_ref().is_some_and(|p| &p.hello.peer == peer)) | |
| 241 | .map(|s| s.line.clone()) | |
| 242 | } | |
| 243 | pub fn set_presence(&self, presence: Presence) { | |
| 244 | let mut state = self.0.state.lock().unwrap(); | |
| 245 | if state.presence == presence { | |
| 246 | return; | |
| 247 | } | |
| 248 | state.presence = presence; | |
| 249 | if std::mem::replace(&mut state.scheduled, true) { | |
| 250 | return; | |
| 251 | } | |
| 252 | drop(state); | |
| 253 | let weak = Arc::downgrade(&self.0); | |
| 254 | let _ = crate::task::spawn("live presence", move || async move { | |
| 255 | let (_, wait) = crate::task::channel(); | |
| 256 | crate::task::wait(&wait, Some(Duration::from_millis(100))).await; | |
| 257 | if let Some(shared) = weak.upgrade() { | |
| 258 | let presence = { | |
| 259 | let mut state = shared.state.lock().unwrap(); | |
| 260 | state.scheduled = false; | |
| 261 | state.presence.clone() | |
| 262 | }; | |
| 263 | let _ = shared.group(kind::PRESENCE, &presence, None); | |
| 264 | } | |
| 265 | }); | |
| 266 | } | |
| 267 | pub fn leave(self, reason: &str) { | |
| 268 | let lines: Vec<_> = self | |
| 269 | .0 | |
| 270 | .state | |
| 271 | .lock() | |
| 272 | .unwrap() | |
| 273 | .streams | |
| 274 | .values() | |
| 275 | .map(|s| s.line.clone()) | |
| 276 | .collect(); | |
| 277 | for line in lines { | |
| 278 | line.hang_up(reason); | |
| 279 | } | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | impl Drop for Live { | |
| 284 | fn drop(&mut self) { | |
| 285 | self.0.stopped.store(true, Ordering::Release); | |
| 286 | close(self.0.state.lock().unwrap().socket); | |
| 287 | } | |
| 288 | } | |
| 289 | ||
| 290 | impl Shared { | |
| 291 | fn connect(self: &Arc<Self>) -> io::Result<()> { | |
| 292 | let weak = Arc::downgrade(self); | |
| 293 | let message = Closure::<dyn FnMut(JsValue)>::new(move |data: JsValue| { | |
| 294 | if let Some(shared) = weak.upgrade() | |
| 295 | && let Err(error) = shared.heard(data) | |
| 296 | { | |
| 297 | if let Some(version) = error | |
| 298 | .get_ref() | |
| 299 | .and_then(|e| e.downcast_ref::<wire::Version>()) | |
| 300 | { | |
| 301 | shared.state.lock().unwrap().outdated = Some(version.0); | |
| 302 | } else if error.kind() == io::ErrorKind::InvalidData { | |
| 303 | shared.state.lock().unwrap().failed += 1; | |
| 304 | } | |
| 305 | shared.disconnected(); | |
| 306 | } | |
| 307 | }) | |
| 308 | .into_js_value(); | |
| 309 | let weak = Arc::downgrade(self); | |
| 310 | let closed = Closure::<dyn FnMut()>::new(move || { | |
| 311 | if let Some(shared) = weak.upgrade() { | |
| 312 | shared.disconnected(); | |
| 313 | } | |
| 314 | }) | |
| 315 | .into_js_value(); | |
| 316 | let socket = connect(&self.url, &message, &closed).map_err(js_error)?; | |
| 317 | self.state.lock().unwrap().socket = socket; | |
| 318 | Ok(()) | |
| 319 | } | |
| 320 | ||
| 321 | fn disconnected(self: &Arc<Self>) { | |
| 322 | let peers = { | |
| 323 | let mut state = self.state.lock().unwrap(); | |
| 324 | close(state.socket); | |
| 325 | let peers: Vec<_> = state | |
| 326 | .streams | |
| 327 | .values() | |
| 328 | .filter_map(|s| s.peer.as_ref().map(|p| p.hello.clone())) | |
| 329 | .collect(); | |
| 330 | state.streams.clear(); | |
| 331 | state.members.clear(); | |
| 332 | state.sealer = None; | |
| 333 | state.relayed = Relayed::Unreachable(Trouble::Other); | |
| 334 | peers | |
| 335 | }; | |
| 336 | for hello in peers { | |
| 337 | (self.events)(Event::Left(&hello)); | |
| 338 | } | |
| 339 | (self.events)(Event::Changed); | |
| 340 | let weak = Arc::downgrade(self); | |
| 341 | let _ = crate::task::spawn("live reconnect", move || async move { | |
| 342 | let (_, wait) = crate::task::channel(); | |
| 343 | crate::task::wait(&wait, Some(Duration::from_secs(2))).await; | |
| 344 | if let Some(shared) = weak | |
| 345 | .upgrade() | |
| 346 | .filter(|s| !s.stopped.load(Ordering::Acquire)) | |
| 347 | { | |
| 348 | let _ = shared.connect(); | |
| 349 | } | |
| 350 | }); | |
| 351 | } | |
| 352 | ||
| 353 | fn stream(&self, slot: u32, bytes: &[u8]) -> io::Result<()> { | |
| 354 | let socket = self.state.lock().unwrap().socket; | |
| 355 | for chunk in bytes.chunks(64 << 10) { | |
| 356 | send(socket, &[&slot.to_be_bytes()[..], chunk].concat()).map_err(js_error)?; | |
| 357 | } | |
| 358 | Ok(()) | |
| 359 | } | |
| 360 | ||
| 361 | fn group(&self, kind: u16, body: &impl Encode<()>, to: Option<u32>) -> io::Result<()> { | |
| 362 | let body = minicbor::to_vec(body).map_err(io::Error::other)?; | |
| 363 | let (socket, sealed) = { | |
| 364 | let mut state = self.state.lock().unwrap(); | |
| 365 | let sealed = state | |
| 366 | .sealer | |
| 367 | .as_mut() | |
| 368 | .ok_or(io::ErrorKind::NotConnected)? | |
| 369 | .seal(kind, &body, to.is_none())?; | |
| 370 | (state.socket, sealed) | |
| 371 | }; | |
| 372 | let mut bytes = match to { | |
| 373 | Some(slot) => [&(::relay::GROUP | 1).to_be_bytes()[..], &slot.to_be_bytes()].concat(), | |
| 374 | None => ::relay::BROADCAST.to_be_bytes().to_vec(), | |
| 375 | }; | |
| 376 | bytes.extend(sealed); | |
| 377 | send(socket, &bytes).map_err(js_error) | |
| 378 | } | |
| 379 | ||
| 380 | fn meet(self: &Arc<Self>, slot: u32) -> io::Result<()> { | |
| 381 | if self.state.lock().unwrap().streams.contains_key(&slot) { | |
| 382 | return Ok(()); | |
| 383 | } | |
| 384 | let tag = self.room.tag().ok_or(io::ErrorKind::InvalidInput)?; | |
| 385 | let (opening, bytes) = wire::Opening::new(Side::Initiator, &tag, &self.room.secret())?; | |
| 386 | let line = Line { | |
| 387 | shared: Arc::downgrade(self), | |
| 388 | slot, | |
| 389 | sealer: Arc::new(Mutex::new(None)), | |
| 390 | }; | |
| 391 | self.state.lock().unwrap().streams.insert( | |
| 392 | slot, | |
| 393 | Stream { | |
| 394 | bytes: Vec::new(), | |
| 395 | opening: Some(opening), | |
| 396 | receive: None, | |
| 397 | line, | |
| 398 | peer: None, | |
| 399 | }, | |
| 400 | ); | |
| 401 | self.stream( | |
| 402 | slot, | |
| 403 | &[&(bytes.len() as u32).to_be_bytes()[..], &bytes].concat(), | |
| 404 | ) | |
| 405 | } | |
| 406 | ||
| 407 | fn heard(self: &Arc<Self>, data: JsValue) -> io::Result<()> { | |
| 408 | if let Some(text) = data.as_string() { | |
| 409 | let notice = text | |
| 410 | .parse::<::relay::Notice>() | |
| 411 | .map_err(|_| io::ErrorKind::InvalidData)?; | |
| 412 | match notice { | |
| 413 | ::relay::Notice::Welcome { you, members } => { | |
| 414 | { | |
| 415 | let mut state = self.state.lock().unwrap(); | |
| 416 | state.slot = you; | |
| 417 | state.relayed = Relayed::Joined; | |
| 418 | if matches!(self.room, Room::Notebook(_)) { | |
| 419 | state.sealer = | |
| 420 | Some(group::Sealer::new(&group::Keys::new(&self.room.secret()))?); | |
| 421 | } | |
| 422 | } | |
| 423 | if matches!(self.room, Room::Code { .. }) { | |
| 424 | for slot in members.into_iter().filter(|slot| *slot < you) { | |
| 425 | self.meet(slot)?; | |
| 426 | } | |
| 427 | } else { | |
| 428 | self.group(kind::HELLO, &self.me, None)?; | |
| 429 | let presence = self.state.lock().unwrap().presence.clone(); | |
| 430 | self.group(kind::PRESENCE, &presence, None)?; | |
| 431 | } | |
| 432 | } | |
| 433 | ::relay::Notice::Left(slot) => { | |
| 434 | let hello = { | |
| 435 | let mut state = self.state.lock().unwrap(); | |
| 436 | state.members.remove(&slot); | |
| 437 | state | |
| 438 | .streams | |
| 439 | .remove(&slot) | |
| 440 | .and_then(|s| s.peer.map(|p| p.hello)) | |
| 441 | }; | |
| 442 | if let Some(hello) = hello { | |
| 443 | (self.events)(Event::Left(&hello)); | |
| 444 | } | |
| 445 | } | |
| 446 | ::relay::Notice::Burned => self.state.lock().unwrap().burned = true, | |
| 447 | _ => {} | |
| 448 | } | |
| 449 | (self.events)(Event::Changed); | |
| 450 | return Ok(()); | |
| 451 | } | |
| 452 | let bytes = js_sys::Uint8Array::new(&data).to_vec(); | |
| 453 | let (slot, bytes) = bytes | |
| 454 | .split_first_chunk::<4>() | |
| 455 | .ok_or(io::ErrorKind::InvalidData)?; | |
| 456 | let slot = u32::from_be_bytes(*slot); | |
| 457 | if slot & ::relay::GROUP != 0 { | |
| 458 | return self.heard_group(slot & !::relay::GROUP, bytes); | |
| 459 | } | |
| 460 | self.heard_stream(slot, bytes) | |
| 461 | } | |
| 462 | ||
| 463 | fn heard_group(self: &Arc<Self>, slot: u32, frame: &[u8]) -> io::Result<()> { | |
| 464 | let (kind, body, hello) = { | |
| 465 | let mut state = self.state.lock().unwrap(); | |
| 466 | let member = match state.members.entry(slot) { | |
| 467 | std::collections::btree_map::Entry::Occupied(e) => e.into_mut(), | |
| 468 | std::collections::btree_map::Entry::Vacant(e) => e.insert(group::Member::new( | |
| 469 | &group::Keys::new(&self.room.secret()), | |
| 470 | frame, | |
| 471 | )?), | |
| 472 | }; | |
| 473 | let (kind, body) = member.open(frame)?; | |
| 474 | (kind, body, member.hello().cloned()) | |
| 475 | }; | |
| 476 | match kind { | |
| 477 | kind::HELLO | kind::HELLO_BACK => { | |
| 478 | let hello: Hello = | |
| 479 | minicbor::decode(&body).map_err(|_| io::ErrorKind::InvalidData)?; | |
| 480 | if hello.peer == self.me.peer { | |
| 481 | return Ok(()); | |
| 482 | } | |
| 483 | let serves = hello.serves.is_some() && self.me.serves.is_none(); | |
| 484 | self.state | |
| 485 | .lock() | |
| 486 | .unwrap() | |
| 487 | .members | |
| 488 | .get_mut(&slot) | |
| 489 | .unwrap() | |
| 490 | .peer = Some(Peer { | |
| 491 | hello: Arc::new(hello), | |
| 492 | presence: None, | |
| 493 | }); | |
| 494 | if kind == kind::HELLO { | |
| 495 | self.group(kind::HELLO_BACK, &self.me, Some(slot))?; | |
| 496 | let presence = self.state.lock().unwrap().presence.clone(); | |
| 497 | self.group(kind::PRESENCE, &presence, Some(slot))?; | |
| 498 | } | |
| 499 | if serves { | |
| 500 | self.meet(slot)?; | |
| 501 | } | |
| 502 | (self.events)(Event::Changed); | |
| 503 | } | |
| 504 | kind::PRESENCE => { | |
| 505 | let presence = minicbor::decode(&body).map_err(|_| io::ErrorKind::InvalidData)?; | |
| 506 | if let Some(peer) = self | |
| 507 | .state | |
| 508 | .lock() | |
| 509 | .unwrap() | |
| 510 | .members | |
| 511 | .get_mut(&slot) | |
| 512 | .and_then(|m| m.peer.as_mut()) | |
| 513 | { | |
| 514 | peer.presence = Some(presence); | |
| 515 | } | |
| 516 | (self.events)(Event::Changed); | |
| 517 | } | |
| 518 | kind if kind < 256 => { | |
| 519 | if let Some(hello) = hello { | |
| 520 | (self.events)(Event::Frame { | |
| 521 | from: &hello, | |
| 522 | kind, | |
| 523 | body: &body, | |
| 524 | }); | |
| 525 | } | |
| 526 | } | |
| 527 | _ => {} | |
| 528 | } | |
| 529 | Ok(()) | |
| 530 | } | |
| 531 | ||
| 532 | fn heard_stream(self: &Arc<Self>, slot: u32, bytes: &[u8]) -> io::Result<()> { | |
| 533 | { | |
| 534 | let mut state = self.state.lock().unwrap(); | |
| 535 | let stream = state | |
| 536 | .streams | |
| 537 | .get_mut(&slot) | |
| 538 | .ok_or(io::ErrorKind::InvalidData)?; | |
| 539 | if stream.bytes.len() + bytes.len() > (16 << 20) + 4 { | |
| 540 | return Err(io::ErrorKind::InvalidData.into()); | |
| 541 | } | |
| 542 | stream.bytes.extend_from_slice(bytes); | |
| 543 | } | |
| 544 | loop { | |
| 545 | let event = { | |
| 546 | let mut state = self.state.lock().unwrap(); | |
| 547 | let stream = state.streams.get_mut(&slot).unwrap(); | |
| 548 | let Some(length) = stream.bytes.get(..4) else { | |
| 549 | break; | |
| 550 | }; | |
| 551 | let length = u32::from_be_bytes(length.try_into().unwrap()) as usize; | |
| 552 | if length > 16 << 20 { | |
| 553 | return Err(io::ErrorKind::InvalidData.into()); | |
| 554 | } | |
| 555 | if stream.bytes.len() < length + 4 { | |
| 556 | break; | |
| 557 | } | |
| 558 | let bytes: Vec<_> = stream.bytes.drain(..length + 4).collect(); | |
| 559 | if let Some(opening) = stream.opening.take() { | |
| 560 | let (send, receive) = opening.finish(&bytes[4..])?; | |
| 561 | *stream.line.sealer.lock().unwrap() = Some(send); | |
| 562 | stream.receive = Some(receive); | |
| 563 | Some((stream.line.clone(), None, kind::HELLO, Vec::new())) | |
| 564 | } else { | |
| 565 | let (kind, body) = stream | |
| 566 | .receive | |
| 567 | .as_mut() | |
| 568 | .ok_or(io::ErrorKind::InvalidData)? | |
| 569 | .receive(&mut io::Cursor::new(bytes))?; | |
| 570 | if stream.peer.is_none() { | |
| 571 | if kind != kind::HELLO { | |
| 572 | return Err(io::ErrorKind::InvalidData.into()); | |
| 573 | } | |
| 574 | let hello = minicbor::decode::<Hello>(&body) | |
| 575 | .map_err(|_| io::ErrorKind::InvalidData)?; | |
| 576 | stream.peer = Some(Peer { | |
| 577 | hello: Arc::new(hello), | |
| 578 | presence: None, | |
| 579 | }); | |
| 580 | } | |
| 581 | Some(( | |
| 582 | stream.line.clone(), | |
| 583 | stream.peer.as_ref().map(|p| p.hello.clone()), | |
| 584 | kind, | |
| 585 | body, | |
| 586 | )) | |
| 587 | } | |
| 588 | }; | |
| 589 | if let Some((line, hello, kind, body)) = event { | |
| 590 | match hello { | |
| 591 | None => line.send(kind::HELLO, &self.me)?, | |
| 592 | Some(hello) if kind == kind::HELLO => (self.events)(Event::Met(&hello, &line)), | |
| 593 | Some(hello) => (self.events)(Event::Frame { | |
| 594 | from: &hello, | |
| 595 | kind, | |
| 596 | body: &body, | |
| 597 | }), | |
| 598 | } | |
| 599 | } | |
| 600 | } | |
| 601 | Ok(()) | |
| 602 | } | |
| 603 | } | |
| 604 | ||
| 605 | fn js_error(error: JsValue) -> io::Error { | |
| 606 | io::Error::new( | |
| 607 | io::ErrorKind::NotConnected, | |
| 608 | error | |
| 609 | .as_string() | |
| 610 | .unwrap_or_else(|| "Relay disconnected".into()), | |
| 611 | ) | |
| 612 | } |
crates/notebook/src/live/wire.rs+60-36| ... | ... | @@ -549,48 +549,72 @@ pub fn open( |
| 549 | 549 | room: &str, |
| 550 | 550 | secret: &[u8], |
| 551 | 551 | ) -> io::Result<(Sealer, Sealer)> { |
| 552 | let password = Password::new(secret); | |
| 553 | let [initiator, responder] = [b"initiator", b"responder"] | |
| 554 | .map(|role| Identity::new(&[&role[..], room.as_bytes()].concat())); | |
| 555 | let (pake, message) = match side { | |
| 556 | Side::Initiator => Spake2::<Ed25519Group>::start_a(&password, &initiator, &responder), | |
| 557 | Side::Responder => Spake2::<Ed25519Group>::start_b(&password, &initiator, &responder), | |
| 558 | }; | |
| 559 | let ours = Open { | |
| 560 | version: VERSION, | |
| 561 | room: room.into(), | |
| 562 | pake: message, | |
| 563 | }; | |
| 552 | let (opening, ours) = Opening::new(side, room, secret)?; | |
| 564 | 553 | if side == Side::Initiator { |
| 565 | write_block(stream, &minicbor::to_vec(&ours).map_err(io::Error::other)?)?; | |
| 554 | write_block(stream, &ours)?; | |
| 566 | 555 | } |
| 567 | let theirs: Open = | |
| 568 | minicbor::decode(&read_block(stream)?).map_err(|_| invalid("A malformed opening"))?; | |
| 569 | // A responder answers even a peer of another version, so that both ends can say which | |
| 570 | // should update. | |
| 556 | let theirs = read_block(stream)?; | |
| 571 | 557 | if side == Side::Responder { |
| 572 | write_block(stream, &minicbor::to_vec(&ours).map_err(io::Error::other)?)?; | |
| 558 | write_block(stream, &ours)?; | |
| 573 | 559 | } |
| 574 | if theirs.version != VERSION { | |
| 575 | return Err(io::Error::new( | |
| 576 | io::ErrorKind::Unsupported, | |
| 577 | Version(theirs.version), | |
| 578 | )); | |
| 560 | opening.finish(&theirs) | |
| 561 | } | |
| 562 | ||
| 563 | pub(super) struct Opening { | |
| 564 | pake: Spake2<Ed25519Group>, | |
| 565 | side: Side, | |
| 566 | room: String, | |
| 567 | } | |
| 568 | ||
| 569 | impl Opening { | |
| 570 | pub(super) fn new(side: Side, room: &str, secret: &[u8]) -> io::Result<(Self, Vec<u8>)> { | |
| 571 | let password = Password::new(secret); | |
| 572 | let [initiator, responder] = [b"initiator", b"responder"] | |
| 573 | .map(|role| Identity::new(&[&role[..], room.as_bytes()].concat())); | |
| 574 | let (pake, message) = match side { | |
| 575 | Side::Initiator => Spake2::<Ed25519Group>::start_a(&password, &initiator, &responder), | |
| 576 | Side::Responder => Spake2::<Ed25519Group>::start_b(&password, &initiator, &responder), | |
| 577 | }; | |
| 578 | let ours = Open { | |
| 579 | version: VERSION, | |
| 580 | room: room.into(), | |
| 581 | pake: message, | |
| 582 | }; | |
| 583 | let message = minicbor::to_vec(&ours).map_err(io::Error::other)?; | |
| 584 | Ok(( | |
| 585 | Self { | |
| 586 | pake, | |
| 587 | side, | |
| 588 | room: room.to_owned(), | |
| 589 | }, | |
| 590 | message, | |
| 591 | )) | |
| 579 | 592 | } |
| 580 | if theirs.room != room { | |
| 581 | return Err(invalid("The peer means another room")); | |
| 593 | ||
| 594 | pub(super) fn finish(self, bytes: &[u8]) -> io::Result<(Sealer, Sealer)> { | |
| 595 | let theirs: Open = minicbor::decode(bytes).map_err(|_| invalid("A malformed opening"))?; | |
| 596 | if theirs.version != VERSION { | |
| 597 | return Err(io::Error::new( | |
| 598 | io::ErrorKind::Unsupported, | |
| 599 | Version(theirs.version), | |
| 600 | )); | |
| 601 | } | |
| 602 | if theirs.room != self.room { | |
| 603 | return Err(invalid("The peer means another room")); | |
| 604 | } | |
| 605 | let key = self | |
| 606 | .pake | |
| 607 | .finish(&theirs.pake) | |
| 608 | .map_err(|_| invalid("A malformed key exchange"))?; | |
| 609 | let [from_initiator, from_responder] = [ | |
| 610 | Sealer::new(&key, b"Snowbound live v1 initiator"), | |
| 611 | Sealer::new(&key, b"Snowbound live v1 responder"), | |
| 612 | ]; | |
| 613 | Ok(match self.side { | |
| 614 | Side::Initiator => (from_initiator, from_responder), | |
| 615 | Side::Responder => (from_responder, from_initiator), | |
| 616 | }) | |
| 582 | 617 | } |
| 583 | let key = pake | |
| 584 | .finish(&theirs.pake) | |
| 585 | .map_err(|_| invalid("A malformed key exchange"))?; | |
| 586 | let [from_initiator, from_responder] = [ | |
| 587 | Sealer::new(&key, b"Snowbound live v1 initiator"), | |
| 588 | Sealer::new(&key, b"Snowbound live v1 responder"), | |
| 589 | ]; | |
| 590 | Ok(match side { | |
| 591 | Side::Initiator => (from_initiator, from_responder), | |
| 592 | Side::Responder => (from_responder, from_initiator), | |
| 593 | }) | |
| 594 | 618 | } |
| 595 | 619 | |
| 596 | 620 | fn write_block(to: &mut impl Write, bytes: &[u8]) -> io::Result<()> { |
crates/notebook/src/session.rs+2-2| ... | ... | @@ -1588,9 +1588,9 @@ impl Notebook { |
| 1588 | 1588 | let (folder, remote) = (root.clone(), remote.clone()); |
| 1589 | 1589 | let bind = move |path: &str| remote(&folder.join(path)); |
| 1590 | 1590 | let mut local = discover::Local::open(&root)?; |
| 1591 | let list = move |folder: &str| { | |
| 1591 | let list = move |folder: String| { | |
| 1592 | 1592 | use discover::Source; |
| 1593 | local.entries(folder, LIMITS.entries) | |
| 1593 | std::future::ready(local.entries(&folder, LIMITS.entries)) | |
| 1594 | 1594 | }; |
| 1595 | 1595 | Ok(((bind, list), watched)) |
| 1596 | 1596 | }, |
crates/notebook/src/sync.rs+109-23| ... | ... | @@ -14,6 +14,11 @@ use std::{ |
| 14 | 14 | /// Errors retain publication state; confirmation checks the stamp, flushes, and notifies |
| 15 | 15 | /// cached readers. |
| 16 | 16 | pub trait Remote { |
| 17 | /// Completes a non-blocking operation that returned `WouldBlock`; retrying that | |
| 18 | /// operation then takes its result. Blocking providers leave this absent. | |
| 19 | fn pending(&mut self) -> Option<std::pin::Pin<Box<dyn Future<Output = ()> + '_>>> { | |
| 20 | None | |
| 21 | } | |
| 17 | 22 | fn read(&mut self) -> io::Result<Vec<u8>>; |
| 18 | 23 | /// The file's stamp without reading its body or coordinating with writers. While it is |
| 19 | 24 | /// the last observed image's, synchronization neither reads nor revalidates the file. |
| ... | ... | @@ -54,6 +59,38 @@ pub trait Remote { |
| 54 | 59 | } |
| 55 | 60 | } |
| 56 | 61 | |
| 62 | pub(crate) trait Waiting { | |
| 63 | fn waiting(&self) -> bool; | |
| 64 | } | |
| 65 | ||
| 66 | impl Waiting for io::Error { | |
| 67 | fn waiting(&self) -> bool { | |
| 68 | self.kind() == io::ErrorKind::WouldBlock | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | impl Waiting for CommitError { | |
| 73 | fn waiting(&self) -> bool { | |
| 74 | self.error.kind() == io::ErrorKind::WouldBlock | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | pub(crate) async fn awaited<R: Remote, T, E: Waiting>( | |
| 79 | remote: &mut R, | |
| 80 | mut operation: impl FnMut(&mut R) -> std::result::Result<T, E>, | |
| 81 | ) -> std::result::Result<T, E> { | |
| 82 | loop { | |
| 83 | let result = operation(remote); | |
| 84 | if result.as_ref().is_err_and(Waiting::waiting) | |
| 85 | && let Some(pending) = remote.pending() | |
| 86 | { | |
| 87 | pending.await; | |
| 88 | continue; | |
| 89 | } | |
| 90 | return result; | |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 57 | 94 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 58 | 95 | pub enum EditStatus { |
| 59 | 96 | Pending, |
| ... | ... | @@ -189,29 +226,53 @@ impl Replica { |
| 189 | 226 | /// Versions the remote keeps beside the file merge into it first, each published as one |
| 190 | 227 | /// more revision and then retired (`resolve.rs`). |
| 191 | 228 | pub fn sync_once(&self, remote: &mut impl Remote) -> Result<Synced> { |
| 229 | crate::task::ready(self.sync_once_async(remote))? | |
| 230 | } | |
| 231 | ||
| 232 | /// The same guarded synchronization step, awaiting non-blocking remote operations. | |
| 233 | pub async fn sync_once_async(&self, remote: &mut impl Remote) -> Result<Synced> { | |
| 234 | let result = self.sync_once_inner(remote).await; | |
| 235 | crate::fs::durable().await?; | |
| 236 | result | |
| 237 | } | |
| 238 | ||
| 239 | /// Ownership uses `try_lock`; the cache mutex is released before every await. | |
| 240 | #[allow(clippy::await_holding_lock)] | |
| 241 | async fn sync_once_inner(&self, remote: &mut impl Remote) -> Result<Synced> { | |
| 192 | 242 | let _owner = self.sync_owner()?; |
| 193 | for version in remote.versions().map_err(Error::RemoteIo)? { | |
| 194 | let image = remote.version(&version.id).map_err(Error::RemoteIo)?; | |
| 195 | let current = remote.read().map_err(Error::RemoteIo)?; | |
| 243 | for version in awaited(remote, Remote::versions) | |
| 244 | .await | |
| 245 | .map_err(Error::RemoteIo)? | |
| 246 | { | |
| 247 | let image = awaited(remote, |remote| remote.version(&version.id)) | |
| 248 | .await | |
| 249 | .map_err(Error::RemoteIo)?; | |
| 250 | let current = awaited(remote, Remote::read) | |
| 251 | .await | |
| 252 | .map_err(Error::RemoteIo)?; | |
| 196 | 253 | let device = version.device.as_deref().unwrap_or("Another device"); |
| 197 | 254 | let keep = match crate::resolve::merge(&current, &image, device) { |
| 198 | 255 | Ok(Merged::Held) => false, |
| 199 | 256 | Ok(Merged::Publish(transaction)) => { |
| 200 | remote.publish(&transaction)?; | |
| 257 | awaited(remote, |remote| remote.publish(&transaction)).await?; | |
| 201 | 258 | false |
| 202 | 259 | } |
| 203 | 260 | // A version this cannot merge is kept whole rather than lost. |
| 204 | 261 | Ok(Merged::Foreign) | Err(Error::Document(_) | Error::Rejected(_)) => true, |
| 205 | 262 | Err(error) => return Err(error), |
| 206 | 263 | }; |
| 207 | remote.retire(&version.id, keep).map_err(Error::RemoteIo)?; | |
| 264 | awaited(remote, |remote| remote.retire(&version.id, keep)) | |
| 265 | .await | |
| 266 | .map_err(Error::RemoteIo)?; | |
| 208 | 267 | } |
| 209 | 268 | let state = state(&*self.lock()?)?; |
| 210 | 269 | let batched = self.section.key.is_none() |
| 211 | 270 | && state.queued |
| 212 | 271 | && state.blocked.is_none() |
| 213 | 272 | && remote.accepts_edits(); |
| 214 | let observed = remote.stamp().map_err(Error::RemoteIo)?; | |
| 273 | let observed = awaited(remote, Remote::stamp) | |
| 274 | .await | |
| 275 | .map_err(Error::RemoteIo)?; | |
| 215 | 276 | if let Some(blocked) = &state.blocked |
| 216 | 277 | && observed == *state.remote.as_ref().unwrap_or(&state.base) |
| 217 | 278 | { |
| ... | ... | @@ -229,7 +290,9 @@ impl Replica { |
| 229 | 290 | let image = match observed { |
| 230 | 291 | observed if observed == state.base || batched => None, |
| 231 | 292 | _ => { |
| 232 | let image = remote.read().map_err(Error::RemoteIo)?; | |
| 293 | let image = awaited(remote, Remote::read) | |
| 294 | .await | |
| 295 | .map_err(Error::RemoteIo)?; | |
| 233 | 296 | // Protected elsewhere: a section written anew, which only its key reads. |
| 234 | 297 | if self.section.key.is_none() && crate::discover::locked(&Store::parse(&image)?) { |
| 235 | 298 | return Err(Error::RemoteIo(io::Error::new( |
| ... | ... | @@ -297,7 +360,8 @@ impl Replica { |
| 297 | 360 | .collect(), |
| 298 | 361 | ) |
| 299 | 362 | }; |
| 300 | if let Err(error) = remote.confirm(&Stamp::of(&image)?) { | |
| 363 | let stamp = Stamp::of(&image)?; | |
| 364 | if let Err(error) = awaited(remote, |remote| remote.confirm(&stamp)).await { | |
| 301 | 365 | if error.state == CommitState::Committed { |
| 302 | 366 | self.acknowledge(*batch, sealed.as_ref(), receipts.as_ref())?; |
| 303 | 367 | } |
| ... | ... | @@ -351,7 +415,7 @@ impl Replica { |
| 351 | 415 | let Some(transaction) = transaction else { |
| 352 | 416 | // Edits that changed nothing are published once the remote's image is durable. |
| 353 | 417 | let base = base::base_stamp(&*self.lock()?)?; |
| 354 | if let Err(error) = remote.confirm(&base) { | |
| 418 | if let Err(error) = awaited(remote, |remote| remote.confirm(&base)).await { | |
| 355 | 419 | if error.state == CommitState::Committed { |
| 356 | 420 | self.acknowledge(batch, None, None)?; |
| 357 | 421 | } |
| ... | ... | @@ -364,18 +428,28 @@ impl Replica { |
| 364 | 428 | changed, |
| 365 | 429 | }); |
| 366 | 430 | }; |
| 431 | if let Err(error) = crate::fs::durable().await { | |
| 432 | self.lock()? | |
| 433 | .execute("UPDATE batches SET attempted=0 WHERE id=?1", [batch])?; | |
| 434 | return Err(error.into()); | |
| 435 | } | |
| 367 | 436 | let published = if self.section.key.is_none() && remote.accepts_edits() { |
| 368 | let connection = self.lock()?; | |
| 369 | let edits = queue::load(&connection, None, Some(batch))?; | |
| 370 | let revisions = decode_revisions(&connection.query_row( | |
| 371 | "SELECT revisions FROM batches WHERE id=?1", | |
| 372 | [batch], | |
| 373 | |row| row.get::<_, String>(0), | |
| 374 | )?)?; | |
| 375 | drop(connection); | |
| 376 | remote.publish_edits(&transaction, &edits, &revisions) | |
| 437 | let (edits, revisions) = { | |
| 438 | let connection = self.lock()?; | |
| 439 | let edits = queue::load(&connection, None, Some(batch))?; | |
| 440 | let revisions = decode_revisions(&connection.query_row( | |
| 441 | "SELECT revisions FROM batches WHERE id=?1", | |
| 442 | [batch], | |
| 443 | |row| row.get::<_, String>(0), | |
| 444 | )?)?; | |
| 445 | (edits, revisions) | |
| 446 | }; | |
| 447 | awaited(remote, |remote| { | |
| 448 | remote.publish_edits(&transaction, &edits, &revisions) | |
| 449 | }) | |
| 450 | .await | |
| 377 | 451 | } else { |
| 378 | remote.publish(&transaction) | |
| 452 | awaited(remote, |remote| remote.publish(&transaction)).await | |
| 379 | 453 | }; |
| 380 | 454 | match published { |
| 381 | 455 | Ok(()) => {} |
| ... | ... | @@ -393,7 +467,13 @@ impl Replica { |
| 393 | 467 | self.acknowledge(batch, Some(&transaction), None)?; |
| 394 | 468 | let revision = self.receipt(id)?; |
| 395 | 469 | if batched { |
| 396 | changed.extend(self.rebase(Some(remote.read().map_err(Error::RemoteIo)?))?); | |
| 470 | changed.extend( | |
| 471 | self.rebase(Some( | |
| 472 | awaited(remote, Remote::read) | |
| 473 | .await | |
| 474 | .map_err(Error::RemoteIo)?, | |
| 475 | ))?, | |
| 476 | ); | |
| 397 | 477 | changed.sort(); |
| 398 | 478 | changed.dedup(); |
| 399 | 479 | } |
| ... | ... | @@ -550,15 +630,21 @@ impl Replica { |
| 550 | 630 | /// Whether nothing is queued and the remote still has the base's stamp, or the queue |
| 551 | 631 | /// is blocked on a remote that has not changed since; reads neither image and does not |
| 552 | 632 | /// lock the cache during remote I/O. |
| 553 | pub(crate) fn settled(&self, remote: &mut impl Remote) -> Result<bool> { | |
| 633 | pub(crate) async fn settled(&self, remote: &mut impl Remote) -> Result<bool> { | |
| 554 | 634 | let state = state(&*self.lock()?)?; |
| 555 | 635 | let expected = match &state.blocked { |
| 556 | 636 | Some(_) => state.remote.unwrap_or(state.base), |
| 557 | 637 | None if !state.queued => state.base, |
| 558 | 638 | None => return Ok(false), |
| 559 | 639 | }; |
| 560 | Ok(remote.stamp().map_err(Error::RemoteIo)? == expected | |
| 561 | && remote.versions().map_err(Error::RemoteIo)?.is_empty()) | |
| 640 | Ok(awaited(remote, Remote::stamp) | |
| 641 | .await | |
| 642 | .map_err(Error::RemoteIo)? | |
| 643 | == expected | |
| 644 | && awaited(remote, Remote::versions) | |
| 645 | .await | |
| 646 | .map_err(Error::RemoteIo)? | |
| 647 | .is_empty()) | |
| 562 | 648 | } |
| 563 | 649 | } |
| 564 | 650 |
crates/notebook/src/task.rs+101-3| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | //! The sync worker's and background's loops: threads natively, and in the browser, which |
| 2 | //! gives a module one thread, tasks on its event loop. A loop is an `async` block whose only | |
| 3 | //! waits are `wait`; natively each wait blocks its thread, so `complete` runs the block to the | |
| 4 | //! end in one poll. | |
| 2 | //! gives a module one thread, tasks on its event loop. Native waits block their thread, so | |
| 3 | //! `complete` runs the block to the end in one poll. | |
| 5 | 4 | |
| 6 | 5 | use std::{io, time::Duration}; |
| 7 | 6 | |
| ... | ... | @@ -11,6 +10,22 @@ pub(crate) use std::{ |
| 11 | 10 | thread::JoinHandle, |
| 12 | 11 | }; |
| 13 | 12 | |
| 13 | #[cfg(all(feature = "live", not(target_arch = "wasm32")))] | |
| 14 | pub(crate) type Answer<T> = std::sync::mpsc::Sender<T>; | |
| 15 | ||
| 16 | #[cfg(all(feature = "live", not(target_arch = "wasm32")))] | |
| 17 | pub(crate) fn response<T>() -> (Answer<T>, std::sync::mpsc::Receiver<T>) { | |
| 18 | std::sync::mpsc::channel() | |
| 19 | } | |
| 20 | ||
| 21 | #[cfg(all(feature = "live", not(target_arch = "wasm32")))] | |
| 22 | pub(crate) async fn answered<T>( | |
| 23 | receiver: &std::sync::mpsc::Receiver<T>, | |
| 24 | timeout: Duration, | |
| 25 | ) -> Result<T, std::sync::mpsc::RecvTimeoutError> { | |
| 26 | receiver.recv_timeout(timeout) | |
| 27 | } | |
| 28 | ||
| 14 | 29 | /// A wake that waits, at most one, as `mpsc::sync_channel(1)` keeps one. |
| 15 | 30 | #[cfg(not(target_arch = "wasm32"))] |
| 16 | 31 | pub(crate) fn channel() -> (Sender<()>, Receiver<()>) { |
| ... | ... | @@ -50,6 +65,18 @@ pub(crate) fn complete<T>(work: impl Future<Output = T>) -> T { |
| 50 | 65 | } |
| 51 | 66 | } |
| 52 | 67 | |
| 68 | /// Polls a synchronous entry point once; browser I/O must use the async entry point. | |
| 69 | pub(crate) fn ready<T>(work: impl Future<Output = T>) -> io::Result<T> { | |
| 70 | let mut work = std::pin::pin!(work); | |
| 71 | match work | |
| 72 | .as_mut() | |
| 73 | .poll(&mut std::task::Context::from_waker(std::task::Waker::noop())) | |
| 74 | { | |
| 75 | std::task::Poll::Ready(output) => Ok(output), | |
| 76 | std::task::Poll::Pending => Err(io::ErrorKind::WouldBlock.into()), | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 53 | 80 | #[cfg(target_arch = "wasm32")] |
| 54 | 81 | pub(crate) use web::*; |
| 55 | 82 | |
| ... | ... | @@ -62,6 +89,77 @@ mod web { |
| 62 | 89 | }; |
| 63 | 90 | use wasm_bindgen::{JsCast, prelude::*}; |
| 64 | 91 | |
| 92 | struct Response<T> { | |
| 93 | value: Option<T>, | |
| 94 | closed: bool, | |
| 95 | waiting: Option<Waker>, | |
| 96 | } | |
| 97 | ||
| 98 | pub(crate) struct Answer<T>(Arc<Mutex<Response<T>>>); | |
| 99 | pub(crate) struct Answered<T>(Arc<Mutex<Response<T>>>); | |
| 100 | ||
| 101 | pub(crate) fn response<T>() -> (Answer<T>, Answered<T>) { | |
| 102 | let response = Arc::new(Mutex::new(Response { | |
| 103 | value: None, | |
| 104 | closed: false, | |
| 105 | waiting: None, | |
| 106 | })); | |
| 107 | (Answer(response.clone()), Answered(response)) | |
| 108 | } | |
| 109 | ||
| 110 | impl<T> Answer<T> { | |
| 111 | pub(crate) fn send(self, value: T) -> Result<(), ()> { | |
| 112 | let wake = { | |
| 113 | let mut response = self.0.lock().map_err(|_| ())?; | |
| 114 | response.value = Some(value); | |
| 115 | response.waiting.take() | |
| 116 | }; | |
| 117 | if let Some(wake) = wake { | |
| 118 | wake.wake(); | |
| 119 | } | |
| 120 | Ok(()) | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | impl<T> Drop for Answer<T> { | |
| 125 | fn drop(&mut self) { | |
| 126 | let wake = { | |
| 127 | let mut response = self.0.lock().unwrap(); | |
| 128 | response.closed = true; | |
| 129 | response.waiting.take() | |
| 130 | }; | |
| 131 | if let Some(wake) = wake { | |
| 132 | wake.wake(); | |
| 133 | } | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | pub(crate) async fn answered<T>( | |
| 138 | receiver: &Answered<T>, | |
| 139 | timeout: Duration, | |
| 140 | ) -> Result<T, std::sync::mpsc::RecvTimeoutError> { | |
| 141 | let deadline = web_time::Instant::now() + timeout; | |
| 142 | let mut armed = false; | |
| 143 | std::future::poll_fn(|context| { | |
| 144 | let mut response = receiver.0.lock().unwrap(); | |
| 145 | if let Some(value) = response.value.take() { | |
| 146 | return Poll::Ready(Ok(value)); | |
| 147 | } | |
| 148 | if response.closed { | |
| 149 | return Poll::Ready(Err(std::sync::mpsc::RecvTimeoutError::Disconnected)); | |
| 150 | } | |
| 151 | if web_time::Instant::now() >= deadline { | |
| 152 | return Poll::Ready(Err(std::sync::mpsc::RecvTimeoutError::Timeout)); | |
| 153 | } | |
| 154 | response.waiting = Some(context.waker().clone()); | |
| 155 | if !std::mem::replace(&mut armed, true) { | |
| 156 | after(timeout, context.waker().clone()); | |
| 157 | } | |
| 158 | Poll::Pending | |
| 159 | }) | |
| 160 | .await | |
| 161 | } | |
| 162 | ||
| 65 | 163 | #[derive(Default)] |
| 66 | 164 | struct Bell { |
| 67 | 165 | rung: bool, |
crates/notebook/src/worker.rs+2-2| ... | ... | @@ -214,13 +214,13 @@ impl Replica { |
| 214 | 214 | } |
| 215 | 215 | let result = match remote.as_mut() { |
| 216 | 216 | Some(remote) => { |
| 217 | if reported && replica.settled(remote).unwrap_or(false) { | |
| 217 | if reported && replica.settled(remote).await.unwrap_or(false) { | |
| 218 | 218 | worker_signal.requested.store(false, Ordering::Release); |
| 219 | 219 | worker_signal.synced.store(crate::now(), Ordering::Release); |
| 220 | 220 | rest().await; |
| 221 | 221 | continue; |
| 222 | 222 | } |
| 223 | let result = replica.sync_once(remote); | |
| 223 | let result = replica.sync_once_async(remote).await; | |
| 224 | 224 | reported = result.is_ok(); |
| 225 | 225 | result |
| 226 | 226 | } |
crates/notebook/tests/live_membership.rs+2-2| ... | ... | @@ -119,7 +119,7 @@ fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart() |
| 119 | 119 | .secret; |
| 120 | 120 | host.remove(&bob_key).unwrap(); |
| 121 | 121 | until("Bob was not removed", || { |
| 122 | bob.stopped() && bob.host().is_none() | |
| 122 | bob.ended() == Some(share::Ended::Removed) && bob.host().is_none() | |
| 123 | 123 | }); |
| 124 | 124 | until("presence did not move to the new room", || { |
| 125 | 125 | host.guests().len() == 1 |
| ... | ... | @@ -129,7 +129,7 @@ fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart() |
| 129 | 129 | assert_eq!(current.members.len(), 1); |
| 130 | 130 | assert_ne!(original_code, code(&host)); |
| 131 | 131 | assert!(alice.host().is_some()); |
| 132 | assert!(!alice.stopped()); | |
| 132 | assert_eq!(alice.ended(), None); | |
| 133 | 133 | let live = Notebook::open_hosted(Arc::clone(&alice), directory.path().join("alice")).unwrap(); |
| 134 | 134 | assert_eq!(live.catalog().sections.len(), 2); |
| 135 | 135 | let restarted: Sharing = |
crates/notebook/tests/live_share.rs+1-1| ... | ... | @@ -398,7 +398,7 @@ fn stopping_lets_every_guest_go_and_retires_the_code() { |
| 398 | 398 | let (guest, _notebook) = guest("Grace", &code, &url, &directory.path().join("grace")); |
| 399 | 399 | host.stop(); |
| 400 | 400 | until("the guest never heard the host stop", || { |
| 401 | guest.stopped() && guest.host().is_none() | |
| 401 | guest.ended() == Some(share::Ended::Stopped) && guest.host().is_none() | |
| 402 | 402 | }); |
| 403 | 403 | assert!(host.code().is_none() && host.guests().is_empty()); |
| 404 | 404 | assert!(matches!( |
crates/notebook/tests/sync.rs+100| ... | ... | @@ -20,6 +20,106 @@ fn save(cache: &Replica, text: ExGuid, range: Range<u32>, replacement: &str) -> |
| 20 | 20 | .unwrap() |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | #[test] | |
| 24 | fn asynchronous_io_keeps_the_attempt_and_recovers_a_lost_reply() { | |
| 25 | use std::{ | |
| 26 | future::Future, | |
| 27 | pin::Pin, | |
| 28 | task::{Context, Poll, Waker}, | |
| 29 | }; | |
| 30 | ||
| 31 | struct Deferred { | |
| 32 | server: Server, | |
| 33 | ready: bool, | |
| 34 | } | |
| 35 | impl Remote for Deferred { | |
| 36 | fn pending(&mut self) -> Option<Pin<Box<dyn Future<Output = ()> + '_>>> { | |
| 37 | let mut yielded = false; | |
| 38 | Some(Box::pin(std::future::poll_fn(move |context| { | |
| 39 | if yielded { | |
| 40 | self.ready = true; | |
| 41 | Poll::Ready(()) | |
| 42 | } else { | |
| 43 | yielded = true; | |
| 44 | context.waker().wake_by_ref(); | |
| 45 | Poll::Pending | |
| 46 | } | |
| 47 | }))) | |
| 48 | } | |
| 49 | fn read(&mut self) -> io::Result<Vec<u8>> { | |
| 50 | if !std::mem::take(&mut self.ready) { | |
| 51 | return Err(io::ErrorKind::WouldBlock.into()); | |
| 52 | } | |
| 53 | self.server.read() | |
| 54 | } | |
| 55 | fn stamp(&mut self) -> io::Result<onestore::Stamp> { | |
| 56 | if !std::mem::take(&mut self.ready) { | |
| 57 | return Err(io::ErrorKind::WouldBlock.into()); | |
| 58 | } | |
| 59 | self.server.stamp() | |
| 60 | } | |
| 61 | fn publish(&mut self, transaction: &Transaction) -> Result<(), CommitError> { | |
| 62 | if !std::mem::take(&mut self.ready) { | |
| 63 | return Err(CommitError { | |
| 64 | state: CommitState::NotCommitted, | |
| 65 | error: io::ErrorKind::WouldBlock.into(), | |
| 66 | }); | |
| 67 | } | |
| 68 | self.server.publish(transaction) | |
| 69 | } | |
| 70 | fn confirm(&mut self, base: &onestore::Stamp) -> Result<(), CommitError> { | |
| 71 | if !std::mem::take(&mut self.ready) { | |
| 72 | return Err(CommitError { | |
| 73 | state: CommitState::NotCommitted, | |
| 74 | error: io::ErrorKind::WouldBlock.into(), | |
| 75 | }); | |
| 76 | } | |
| 77 | self.server.confirm(base) | |
| 78 | } | |
| 79 | } | |
| 80 | fn run<T>(work: impl Future<Output = T>) -> T { | |
| 81 | let mut work = std::pin::pin!(work); | |
| 82 | let mut polls = 0; | |
| 83 | loop { | |
| 84 | polls += 1; | |
| 85 | assert!(polls < 100, "An async operation stopped making progress"); | |
| 86 | if let Poll::Ready(result) = work.as_mut().poll(&mut Context::from_waker(Waker::noop())) | |
| 87 | { | |
| 88 | assert!(polls > 1); | |
| 89 | return result; | |
| 90 | } | |
| 91 | } | |
| 92 | } | |
| 93 | let directory = tempfile::tempdir().unwrap(); | |
| 94 | let path = directory.path().join("cache.sqlite"); | |
| 95 | let source = onestore::create_section("async.one", "Original", "Fixture").unwrap(); | |
| 96 | let (_, object, _) = text(&source); | |
| 97 | let cache = Replica::create(&path, &source).unwrap(); | |
| 98 | let id = save(&cache, object, 0..0, "Browser ").unwrap(); | |
| 99 | let mut remote = Deferred { | |
| 100 | server: Server::new(&source), | |
| 101 | ready: false, | |
| 102 | }; | |
| 103 | remote.server.fault = Fault::UnknownAfter; | |
| 104 | assert!(matches!( | |
| 105 | run(cache.sync_once_async(&mut remote)), | |
| 106 | Err(Error::Remote(CommitError { | |
| 107 | state: CommitState::Unknown, | |
| 108 | .. | |
| 109 | })) | |
| 110 | )); | |
| 111 | assert_eq!(remote.server.publications, 1); | |
| 112 | drop(cache); | |
| 113 | let cache = Replica::open(&path).unwrap(); | |
| 114 | assert!( | |
| 115 | matches!(run(cache.sync_once_async(&mut remote)).unwrap().edit, Some((published, EditStatus::Published { .. })) if published == id) | |
| 116 | ); | |
| 117 | assert_eq!(remote.server.publications, 1); | |
| 118 | assert_eq!(text(&remote.server.visible).2, "Browser Original"); | |
| 119 | assert_eq!(remote.server.visible, remote.server.durable); | |
| 120 | assert!(cache.pending().unwrap().is_empty()); | |
| 121 | } | |
| 122 | ||
| 23 | 123 | #[test] |
| 24 | 124 | fn an_unchanged_stamp_publishes_and_settles_without_reading_the_remote() { |
| 25 | 125 | struct Counted { |
crates/snowbound/src/commands.rs+1| ... | ... | @@ -1163,6 +1163,7 @@ impl State { |
| 1163 | 1163 | #[cfg(feature = "live")] |
| 1164 | 1164 | Id::LiveShare => enabled( |
| 1165 | 1165 | !modal |
| 1166 | && cfg!(not(target_arch = "wasm32")) | |
| 1166 | 1167 | && self.notebook().is_some_and(|library| { |
| 1167 | 1168 | library.catalog().is_some() && library.joined.is_none() |
| 1168 | 1169 | }), |
crates/snowbound/src/live.rs+25-12| ... | ... | @@ -147,7 +147,7 @@ fn keep(file: &Path, value: &impl serde::Serialize) -> io::Result<()> { |
| 147 | 147 | let folder = file.parent().unwrap_or(Path::new(".")); |
| 148 | 148 | notebook::fs::create_dir_all(folder)?; |
| 149 | 149 | let partial = file.with_extension("partial"); |
| 150 | let mut options = std::fs::OpenOptions::new(); | |
| 150 | let mut options = notebook::fs::OpenOptions::new(); | |
| 151 | 151 | options.write(true).create(true).truncate(true); |
| 152 | 152 | #[cfg(unix)] |
| 153 | 153 | std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); |
| ... | ... | @@ -217,8 +217,9 @@ impl Joined { |
| 217 | 217 | ) |
| 218 | 218 | }) |
| 219 | 219 | .map_err(|error| refused(&error))?; |
| 220 | #[cfg(not(target_arch = "wasm32"))] | |
| 220 | 221 | if !share.listed { |
| 221 | let since = std::time::Instant::now(); | |
| 222 | let since = web_time::Instant::now(); | |
| 222 | 223 | while guest.host().is_none() && since.elapsed() < FIRST_LISTING { |
| 223 | 224 | std::thread::sleep(std::time::Duration::from_millis(50)); |
| 224 | 225 | } |
| ... | ... | @@ -257,17 +258,17 @@ impl Joined { |
| 257 | 258 | } |
| 258 | 259 | |
| 259 | 260 | /// Meets whoever shares `code` (and `password`): the share it welcomes this computer to. |
| 260 | pub(crate) fn join( | |
| 261 | pub(crate) async fn join( | |
| 261 | 262 | code: &str, |
| 262 | 263 | password: &str, |
| 263 | 264 | waiting: impl Fn(bool) -> bool, |
| 264 | 265 | ) -> Result<live::wire::Welcome, live::share::Refusal> { |
| 265 | 266 | let me = hello().map_err(|_| live::share::Refusal::Unreachable(live::Trouble::Other))?; |
| 266 | live::share::join_while(me, code, password, reach(), relay().as_deref(), waiting) | |
| 267 | live::share::join_while_async(me, code, password, reach(), relay().as_deref(), waiting).await | |
| 267 | 268 | } |
| 268 | 269 | |
| 269 | 270 | /// Keeps `welcome` as the notebook this computer joined: its location. |
| 270 | pub(crate) fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<String> { | |
| 271 | pub(crate) async fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<String> { | |
| 271 | 272 | let location = live::share::location(&welcome.share); |
| 272 | 273 | let file = kept(cache, JOINED); |
| 273 | 274 | let mut shares: BTreeMap<String, Share> = read_kept(&file); |
| ... | ... | @@ -276,20 +277,32 @@ pub(crate) fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<S |
| 276 | 277 | Share { |
| 277 | 278 | share: welcome.share, |
| 278 | 279 | secret: welcome.secret, |
| 279 | notebook: welcome.notebook, | |
| 280 | host: welcome.host, | |
| 280 | notebook: welcome.notebook.clone(), | |
| 281 | host: welcome.host.clone(), | |
| 281 | 282 | listed: false, |
| 282 | 283 | protocol: live::wire::VERSION, |
| 283 | 284 | }, |
| 284 | 285 | ); |
| 285 | 286 | keep(&file, &shares)?; |
| 287 | #[cfg(target_arch = "wasm32")] | |
| 288 | { | |
| 289 | let guest = Guest::start( | |
| 290 | hello()?, | |
| 291 | welcome.share, | |
| 292 | welcome.secret, | |
| 293 | None, | |
| 294 | relay().as_deref(), | |
| 295 | crate::library::notify_background, | |
| 296 | )?; | |
| 297 | live::share::Hosted::prepare(guest, cache).await?; | |
| 298 | } | |
| 286 | 299 | Ok(location) |
| 287 | 300 | } |
| 288 | 301 | |
| 289 | 302 | /// A peer to go to, since when, and the section and page already asked to open. |
| 290 | 303 | type Going = ( |
| 291 | 304 | [u8; 16], |
| 292 | std::time::Instant, | |
| 305 | web_time::Instant, | |
| 293 | 306 | Option<(Option<[u8; 16]>, Option<live::Guid>)>, |
| 294 | 307 | ); |
| 295 | 308 | |
| ... | ... | @@ -319,9 +332,9 @@ pub(crate) struct Peers { |
| 319 | 332 | /// Each peer's picture as drawn, cut to a circle, decoded once. |
| 320 | 333 | pictures: HashMap<[u8; 16], Option<draw::RasterImage>>, |
| 321 | 334 | /// Where each peer's caret was last seen, and when it got there. |
| 322 | moved: HashMap<[u8; 16], (Caret, std::time::Instant)>, | |
| 335 | moved: HashMap<[u8; 16], (Caret, web_time::Instant)>, | |
| 323 | 336 | /// Where each peer was last seen, and when they last moved, for ordering the avatars. |
| 324 | active: HashMap<[u8; 16], (Option<live::Presence>, std::time::Instant)>, | |
| 337 | active: HashMap<[u8; 16], (Option<live::Presence>, web_time::Instant)>, | |
| 325 | 338 | /// The peer an avatar's click goes to, since when, and the place already asked to open. |
| 326 | 339 | going: Option<Going>, |
| 327 | 340 | pub(crate) share: Option<crate::share::ShareDialog>, |
| ... | ... | @@ -650,7 +663,7 @@ impl State { |
| 650 | 663 | if peers.is_empty() { |
| 651 | 664 | return; |
| 652 | 665 | } |
| 653 | let now = std::time::Instant::now(); | |
| 666 | let now = web_time::Instant::now(); | |
| 654 | 667 | for peer in &peers { |
| 655 | 668 | let id = peer.hello.peer; |
| 656 | 669 | let seen = self |
| ... | ... | @@ -921,7 +934,7 @@ impl State { |
| 921 | 934 | (y * viewport.scale + viewport.origin[1]) / scale, |
| 922 | 935 | ] |
| 923 | 936 | }; |
| 924 | let now = std::time::Instant::now(); | |
| 937 | let now = web_time::Instant::now(); | |
| 925 | 938 | let (mut placed, mut above, mut below) = (Vec::new(), Vec::new(), Vec::new()); |
| 926 | 939 | for peer in self.connected() { |
| 927 | 940 | let Some(caret) = (peer.presence.as_ref()) |
crates/snowbound/src/main.rs+33| ... | ... | @@ -4247,6 +4247,23 @@ impl State { |
| 4247 | 4247 | .filter(|(_, paths)| !paths.is_empty()) |
| 4248 | 4248 | .collect(); |
| 4249 | 4249 | for (library, paths) in &reported { |
| 4250 | #[cfg(all(feature = "live", target_arch = "wasm32"))] | |
| 4251 | if library.joined.is_some() && paths.iter().any(|path| !path.ends_with(".one")) { | |
| 4252 | let refreshed = Arc::new(library.with(library.reopen()?)); | |
| 4253 | let shown = self | |
| 4254 | .session | |
| 4255 | .as_ref() | |
| 4256 | .filter(|session| session.library.location == library.location) | |
| 4257 | .and_then(|session| { | |
| 4258 | let path = &session.tabs[session.tab].path; | |
| 4259 | if refreshed.contains(path) { | |
| 4260 | Some(path.clone()) | |
| 4261 | } else { | |
| 4262 | refreshed.first_section() | |
| 4263 | } | |
| 4264 | }); | |
| 4265 | self.adopt(refreshed, shown.as_deref())?; | |
| 4266 | } | |
| 4250 | 4267 | self.sections_changed(library, paths.clone()); |
| 4251 | 4268 | } |
| 4252 | 4269 | let changed: Vec<String> = (reported.iter()) |
| ... | ... | @@ -6184,6 +6201,22 @@ fn spawn(work: impl FnOnce() + Send + 'static) { |
| 6184 | 6201 | platform::defer(work); |
| 6185 | 6202 | } |
| 6186 | 6203 | |
| 6204 | /// Runs asynchronous work beside the frame, creating the future on its executor. | |
| 6205 | #[cfg(feature = "live")] | |
| 6206 | fn spawn_async<F: Future<Output = ()> + 'static>(work: impl FnOnce() -> F + Send + 'static) { | |
| 6207 | RUNNING.fetch_add(1, Ordering::Relaxed); | |
| 6208 | #[cfg(not(target_arch = "wasm32"))] | |
| 6209 | std::thread::spawn(move || { | |
| 6210 | pollster::block_on(work()); | |
| 6211 | RUNNING.fetch_sub(1, Ordering::Release); | |
| 6212 | }); | |
| 6213 | #[cfg(target_arch = "wasm32")] | |
| 6214 | wasm_bindgen_futures::spawn_local(async move { | |
| 6215 | work().await; | |
| 6216 | RUNNING.fetch_sub(1, Ordering::Release); | |
| 6217 | }); | |
| 6218 | } | |
| 6219 | ||
| 6187 | 6220 | /// FILETIME now: when an edit happened, which its modification times record. |
| 6188 | 6221 | fn filetime() -> u64 { |
| 6189 | 6222 | let unix = web_time::SystemTime::now() |
crates/snowbound/src/menus.rs+1-1| ... | ... | @@ -601,7 +601,7 @@ impl State { |
| 601 | 601 | } |
| 602 | 602 | actions.extend([ |
| 603 | 603 | item(Action::CopyLink, "Copy Link to Notebook", false, true), |
| 604 | #[cfg(feature = "live")] | |
| 604 | #[cfg(all(feature = "live", not(target_arch = "wasm32")))] | |
| 605 | 605 | item( |
| 606 | 606 | Action::LiveShare, |
| 607 | 607 | commands::command(commands::Id::LiveShare).title, |
crates/snowbound/src/share.rs+21-17| ... | ... | @@ -6,7 +6,6 @@ use accesskit::Role; |
| 6 | 6 | use notebook::live::{ |
| 7 | 7 | Relayed, Trouble, |
| 8 | 8 | share::{self, Refusal, Sharing}, |
| 9 | wire::Welcome, | |
| 10 | 9 | }; |
| 11 | 10 | use std::sync::{ |
| 12 | 11 | Arc, |
| ... | ... | @@ -59,7 +58,7 @@ pub(crate) struct JoinDialog { |
| 59 | 58 | code: String, |
| 60 | 59 | password: String, |
| 61 | 60 | status: Status, |
| 62 | replies: crate::live::Channel<Result<Welcome, Refusal>>, | |
| 61 | replies: crate::live::Channel<Result<String, String>>, | |
| 63 | 62 | alive: Arc<AtomicBool>, |
| 64 | 63 | approval: Arc<AtomicBool>, |
| 65 | 64 | } |
| ... | ... | @@ -607,21 +606,14 @@ impl State { |
| 607 | 606 | let mut welcomed = None; |
| 608 | 607 | for reply in dialog.replies.1.try_iter() { |
| 609 | 608 | match reply { |
| 610 | Ok(welcome) => welcomed = Some(welcome), | |
| 611 | Err(refused) => dialog.status = Status::Failed(refusal(&refused)), | |
| 609 | Ok(location) => welcomed = Some(location), | |
| 610 | Err(message) => dialog.status = Status::Failed(message), | |
| 612 | 611 | } |
| 613 | 612 | } |
| 614 | if let Some(welcome) = welcomed { | |
| 615 | match crate::live::joined(&self.cache, welcome) { | |
| 616 | Ok(location) => { | |
| 617 | self.ui.close_popup(join_id()); | |
| 618 | self.open_notebook(location, None); | |
| 619 | return; | |
| 620 | } | |
| 621 | Err(error) => { | |
| 622 | dialog.status = Status::Failed(format!("Couldn’t keep the code: {error}")); | |
| 623 | } | |
| 624 | } | |
| 613 | if let Some(location) = welcomed { | |
| 614 | self.ui.close_popup(join_id()); | |
| 615 | self.open_notebook(location, None); | |
| 616 | return; | |
| 625 | 617 | } |
| 626 | 618 | let ui = &mut self.ui; |
| 627 | 619 | let owners = [join_id(), code_field(), join_password()]; |
| ... | ... | @@ -679,15 +671,27 @@ impl State { |
| 679 | 671 | dialog.status = Status::Waiting; |
| 680 | 672 | let (replies, redraw) = (dialog.replies.0.clone(), self.redraw.clone()); |
| 681 | 673 | let password = dialog.password.clone(); |
| 674 | let cache = self.cache.clone(); | |
| 682 | 675 | let (alive, approval) = |
| 683 | 676 | (Arc::clone(&dialog.alive), Arc::clone(&dialog.approval)); |
| 684 | crate::spawn(move || { | |
| 677 | crate::spawn_async(move || async move { | |
| 685 | 678 | let reply = crate::live::join(&code, &password, |waiting| { |
| 686 | 679 | if approval.swap(waiting, Ordering::AcqRel) != waiting { |
| 687 | 680 | redraw.wake_by_ref(); |
| 688 | 681 | } |
| 689 | 682 | alive.load(Ordering::Acquire) |
| 690 | }); | |
| 683 | }) | |
| 684 | .await; | |
| 685 | approval.store(false, Ordering::Release); | |
| 686 | redraw.wake_by_ref(); | |
| 687 | let reply = match reply { | |
| 688 | Ok(welcome) => { | |
| 689 | crate::live::joined(&cache, welcome).await.map_err(|error| { | |
| 690 | format!("Couldn’t open the shared notebook: {error}") | |
| 691 | }) | |
| 692 | } | |
| 693 | Err(error) => Err(refusal(&error)), | |
| 694 | }; | |
| 691 | 695 | let _ = replies.send(reply); |
| 692 | 696 | redraw.wake(); |
| 693 | 697 | }); |
crates/snowbound/src/sync.rs+15-9| ... | ... | @@ -265,8 +265,7 @@ struct Facts<'a> { |
| 265 | 265 | changes_listed: bool, |
| 266 | 266 | /// Whether the open section's file can be shown in the file manager. |
| 267 | 267 | local: bool, |
| 268 | /// The computer sharing this notebook by Live Share stopped sharing it. | |
| 269 | stopped: bool, | |
| 268 | ended: Option<&'a str>, | |
| 270 | 269 | } |
| 271 | 270 | |
| 272 | 271 | /// What the reader picked in the popup. |
| ... | ... | @@ -541,9 +540,9 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { |
| 541 | 540 | let conflict; |
| 542 | 541 | // A problem's advice stays while working offline, so turning it on moves nothing. |
| 543 | 542 | let advice = match state { |
| 544 | SyncState::NotConnected if facts.stopped => Some(format!( | |
| 545 | "The person sharing this notebook stopped sharing it. Your changes stay on {THIS}." | |
| 546 | )), | |
| 543 | SyncState::NotConnected if facts.ended.is_some() => facts | |
| 544 | .ended | |
| 545 | .map(|ended| format!("{ended} Your changes stay on {THIS}.")), | |
| 547 | 546 | SyncState::NotConnected => Some(match sync.queued { |
| 548 | 547 | 0 => format!("Can’t reach {host}. Sync continues when it’s back."), |
| 549 | 548 | _ => format!("Can’t reach {host}. {waiting} will sync when it’s back."), |
| ... | ... | @@ -1089,12 +1088,19 @@ impl State { |
| 1089 | 1088 | location: &library.location, |
| 1090 | 1089 | place: library.place(), |
| 1091 | 1090 | #[cfg(feature = "live")] |
| 1092 | stopped: library | |
| 1091 | ended: library | |
| 1093 | 1092 | .joined |
| 1094 | 1093 | .as_ref() |
| 1095 | .is_some_and(|joined| joined.guest.stopped()), | |
| 1094 | .and_then(|joined| match joined.guest.ended()? { | |
| 1095 | notebook::live::share::Ended::Stopped => { | |
| 1096 | Some("The person sharing this notebook stopped sharing it.") | |
| 1097 | } | |
| 1098 | notebook::live::share::Ended::Removed => Some( | |
| 1099 | "This device was removed. Ask the person sharing for a new link or code.", | |
| 1100 | ), | |
| 1101 | }), | |
| 1096 | 1102 | #[cfg(not(feature = "live"))] |
| 1097 | stopped: false, | |
| 1103 | ended: None, | |
| 1098 | 1104 | notice: library.notice.as_deref(), |
| 1099 | 1105 | sections: sections(&library, session), |
| 1100 | 1106 | conflicts: session |
| ... | ... | @@ -1212,7 +1218,7 @@ mod tests { |
| 1212 | 1218 | update: &IDLE, |
| 1213 | 1219 | changes_listed: false, |
| 1214 | 1220 | local: true, |
| 1215 | stopped: false, | |
| 1221 | ended: None, | |
| 1216 | 1222 | } |
| 1217 | 1223 | } |
| 1218 | 1224 |
crates/snowbound/src/web.rs+12| ... | ... | @@ -1185,6 +1185,18 @@ async fn open( |
| 1185 | 1185 | None => state.create_notebook(own.clone())?, |
| 1186 | 1186 | } |
| 1187 | 1187 | } |
| 1188 | #[cfg(feature = "live")] | |
| 1189 | if let Some(code) = web_sys::window() | |
| 1190 | .and_then(|window| window.location().search().ok()) | |
| 1191 | .and_then(|query| { | |
| 1192 | query | |
| 1193 | .trim_start_matches('?') | |
| 1194 | .split('&') | |
| 1195 | .find_map(|pair| pair.strip_prefix("join=").map(str::to_owned)) | |
| 1196 | }) | |
| 1197 | { | |
| 1198 | state.join_link(code); | |
| 1199 | } | |
| 1188 | 1200 | Ok(state) |
| 1189 | 1201 | } |
| 1190 | 1202 |
crates/snowbound/web/glue.js+50-21| ... | ... | @@ -23,6 +23,7 @@ function storageWorker() { |
| 23 | 23 | const handles = new Map(); |
| 24 | 24 | let root; |
| 25 | 25 | let queue = Promise.resolve(); |
| 26 | const pending = []; | |
| 26 | 27 | |
| 27 | 28 | const names = (path) => path.split("/").filter(Boolean); |
| 28 | 29 | const folder = async (parts) => { |
| ... | ... | @@ -47,7 +48,14 @@ function storageWorker() { |
| 47 | 48 | }; |
| 48 | 49 | const write = async (path, length, ranges) => { |
| 49 | 50 | const open = await handle(path); |
| 50 | for (const [offset, bytes] of ranges) open.write(bytes, { at: offset }); | |
| 51 | for (const [offset, bytes] of ranges) { | |
| 52 | let written = 0; | |
| 53 | while (written < bytes.length) { | |
| 54 | const count = open.write(bytes.subarray(written), { at: offset + written }); | |
| 55 | if (!count) throw new Error("Browser storage could not finish writing"); | |
| 56 | written += count; | |
| 57 | } | |
| 58 | } | |
| 51 | 59 | open.truncate(length); |
| 52 | 60 | open.flush(); |
| 53 | 61 | }; |
| ... | ... | @@ -103,25 +111,34 @@ function storageWorker() { |
| 103 | 111 | }); |
| 104 | 112 | }; |
| 105 | 113 | |
| 114 | const flush = async () => { | |
| 115 | while (pending.length) { | |
| 116 | for (const [path, length, ranges] of pending[0]) | |
| 117 | if (length === undefined) await remove(path); | |
| 118 | else if (length === null) await folder(names(path)); | |
| 119 | else await write(path, length, ranges); | |
| 120 | pending.shift(); | |
| 121 | } | |
| 122 | }; | |
| 123 | ||
| 106 | 124 | onmessage = ({ data }) => { |
| 107 | queue = queue | |
| 108 | .then(async () => { | |
| 125 | queue = queue.then(async () => { | |
| 126 | try { | |
| 109 | 127 | if (data.kind === "load") { |
| 110 | 128 | root = await navigator.storage.getDirectory(); |
| 111 | 129 | let files = await list(root, "", []); |
| 112 | if (!files.length) { | |
| 113 | await migrate(); | |
| 114 | files = await list(root, "", []); | |
| 115 | } | |
| 116 | postMessage(files, files.flatMap(([, bytes]) => (bytes ? [bytes.buffer] : []))); | |
| 117 | } else if (data.kind === "settle") postMessage(null); | |
| 118 | else | |
| 119 | for (const [path, length, ranges] of data.changes) | |
| 120 | if (length === undefined) await remove(path); | |
| 121 | else if (length === null) await folder(names(path)); | |
| 122 | else await write(path, length, ranges); | |
| 123 | }) | |
| 124 | .catch((error) => console.error("Keeping files", error)); | |
| 130 | if (!files.length) { await migrate(); files = await list(root, "", []); } | |
| 131 | postMessage(files, files.flatMap(([, bytes]) => bytes ? [bytes.buffer] : [])); | |
| 132 | } else { | |
| 133 | if (data.kind === "store") pending.push(data.changes); | |
| 134 | await flush(); | |
| 135 | if (data.kind === "settle") postMessage({ error: null }); | |
| 136 | } | |
| 137 | } catch (error) { | |
| 138 | if (data.kind === "settle" || data.kind === "load") postMessage({ error: String(error) }); | |
| 139 | else console.error("Keeping files", error); | |
| 140 | } | |
| 141 | }); | |
| 125 | 142 | }; |
| 126 | 143 | } |
| 127 | 144 | |
| ... | ... | @@ -132,15 +149,25 @@ export function loadFiles() { |
| 132 | 149 | storage.postMessage({ kind: "load" }); |
| 133 | 150 | return new Promise((resolve, reject) => { |
| 134 | 151 | storage.onmessage = ({ data }) => { |
| 135 | storage.onmessage = () => settling.shift()?.(); | |
| 136 | resolve(data); | |
| 152 | storage.onmessage = ({ data }) => { | |
| 153 | const waiting = settling.shift(); | |
| 154 | if (data.error) waiting?.reject(new Error(data.error)); | |
| 155 | else waiting?.resolve(); | |
| 156 | }; | |
| 157 | if (data.error) reject(new Error(data.error)); | |
| 158 | else resolve(data); | |
| 159 | }; | |
| 160 | storage.onerror = (event) => { | |
| 161 | storageError = new Error(`The storage worker failed: ${event.message}`); | |
| 162 | reject(storageError); | |
| 163 | for (const waiting of settling.splice(0)) waiting.reject(storageError); | |
| 137 | 164 | }; |
| 138 | storage.onerror = (error) => reject(new Error(`The storage worker failed: ${error.message}`)); | |
| 139 | 165 | }); |
| 140 | 166 | } |
| 141 | 167 | |
| 142 | 168 | // Callers waiting for the storage worker to finish what it was given. |
| 143 | 169 | const settling = []; |
| 170 | let storageError; | |
| 144 | 171 | |
| 145 | 172 | /** Writes `[path]` (removed), `[path, null]` (a folder) and `[path, length, ranges]` entries; |
| 146 | 173 | * those under a folder of the user's go there, a committed section only where nothing else |
| ... | ... | @@ -322,8 +349,9 @@ export function fetchDictionary(name) { |
| 322 | 349 | |
| 323 | 350 | /** Resolves once every file handed over so far is written. */ |
| 324 | 351 | function settled() { |
| 325 | const kept = new Promise((resolve) => { | |
| 326 | settling.push(resolve); | |
| 352 | if (storageError) return Promise.reject(storageError); | |
| 353 | const kept = new Promise((resolve, reject) => { | |
| 354 | settling.push({ resolve, reject }); | |
| 327 | 355 | storage.postMessage({ kind: "settle" }); |
| 328 | 356 | }); |
| 329 | 357 | return Promise.all([kept, writing]); |
| ... | ... | @@ -597,6 +625,7 @@ export function adoptCanvas(fresh) { |
| 597 | 625 | /** Wires the page's canvas, text area and file input to `module`'s exports. */ |
| 598 | 626 | export function attach(module) { |
| 599 | 627 | wasm = module; |
| 628 | globalThis.snowboundFlushStorage = () => { wasm.flush(); return settled(); }; | |
| 600 | 629 | canvas = document.getElementById("page"); |
| 601 | 630 | input = document.getElementById("input"); |
| 602 | 631 | picker = document.getElementById("files"); |
crates/snowbound/web/index.html+5| ... | ... | @@ -205,6 +205,11 @@ |
| 205 | 205 | ]); |
| 206 | 206 | status.textContent = "Starting Snowbound"; |
| 207 | 207 | await snowbound.start(snowbound, fonts, dictionaries); |
| 208 | const opened = new URL(location.href); | |
| 209 | if (opened.searchParams.has("join")) { | |
| 210 | opened.searchParams.delete("join"); | |
| 211 | history.replaceState(null, "", opened); | |
| 212 | } | |
| 208 | 213 | sessionStorage.removeItem("snowbound.refetched"); |
| 209 | 214 | } catch (error) { |
| 210 | 215 | // A module and JavaScript from different builds can't link: an old cached copy. Once, |
tools/ci.py+1-1| ... | ... | @@ -80,7 +80,7 @@ def lanes(): |
| 80 | 80 | result.append(dict( |
| 81 | 81 | name='web', minutes=20, packages=['snowbound'], paths=('crates/snowbound/web/', 'tools/release_web.py'), |
| 82 | 82 | # Clippy builds it; the module itself is linked by release_web.py. |
| 83 | commands=[cargo('clippy', '-p', 'snowbound', '--target', 'wasm32-unknown-unknown', '--no-default-features', '--features', 'wgpu', '--', '-D', 'warnings')], | |
| 83 | commands=[cargo('clippy', '-p', 'snowbound', '--target', 'wasm32-unknown-unknown', '--no-default-features', '--features', 'wgpu,live', '--', '-D', 'warnings')], | |
| 84 | 84 | environment={**(wasm or {}), 'CARGO_TARGET_DIR': str(TARGET / 'wasm')}, |
| 85 | 85 | missing='needs the wasm32-unknown-unknown target' if 'wasm32-unknown-unknown' not in targets.split() |
| 86 | 86 | else None if wasm else 'needs nix for a clang that builds for wasm32')) |
tools/release_web.py+1-1| ... | ... | @@ -100,7 +100,7 @@ def build(out): |
| 100 | 100 | """Writes index.html, the module, its JavaScript and the fonts to `out`, the module knowing |
| 101 | 101 | when it was built.""" |
| 102 | 102 | built = str(int(time.time())) |
| 103 | run(['cargo', 'build', '--locked', '-p', 'snowbound', '--release', '--target', 'wasm32-unknown-unknown', '--no-default-features', '--features', 'wgpu', | |
| 103 | run(['cargo', 'build', '--locked', '-p', 'snowbound', '--release', '--target', 'wasm32-unknown-unknown', '--no-default-features', '--features', 'wgpu,live', | |
| 104 | 104 | *PROFILE], env={**environment(), 'SNOWBOUND_WEB_BUILD': built}) |
| 105 | 105 | if out.exists(): |
| 106 | 106 | shutil.rmtree(out) |