From 44103d7cb1cd039de204f6925179a3621467d6cd Mon Sep 17 00:00:00 2001 From: Dominik Strebel Date: Fri, 23 Oct 2026 22:58:25 +0200 Subject: [PATCH 1/2] fix(map): Load All Features waits for a web source still opening --- crates/engine/src/tests.rs | 28 ++++++++++++++++++++++++++++ crates/map/src/lib.rs | 3 +++ 2 files changed, 31 insertions(+) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 158f5f66e..45cd5d8fc 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9406,6 +9406,34 @@ fn streamed_layer_outside_its_zoom_range_reads_nothing() { assert!(sent.load(std::sync::atomic::Ordering::Relaxed) > before); } +/// Load All Features on a layer whose web source is still opening waits for it to open, rather +/// than fail with "the layer's source isn't open" (the server here holds its answers a while). +#[test] +fn load_all_features_waits_for_a_source_still_opening() { + use rustgis_data::{Dataset, Feature, Geometry, GeometryType}; + let mut ds = Dataset::new("pts", GeometryType::Point, vec![]); + for i in 0..100u32 { + let c = rustgis_geom::Coord::new(f64::from(i) * 0.1, 1.0); + ds.features.push(Feature { id: u64::from(i) + 1, geometry: Some(Geometry::Point(c)), attrs: vec![] }); + } + let fgb = rustgis_data::source::fgb::write(&ds).unwrap(); + let (base, _) = serve_ranges(fgb.clone()); + let mut s = Session::new(Project::default()); + s.run("layer.addUrl", &json!({"url": format!("{base}/pts.fgb")})).unwrap(); + let text = serde_json::to_string(&s.project).unwrap(); + let gate = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let (slow, _) = serve_gated(fgb, true, Some(gate.clone())); + let mut t = Session::new(Project::default()); + t.run("project.openText", &json!({"text": text.replace(&base, &slow)})).unwrap(); + let open = gate.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(300)); + open.store(true, std::sync::atomic::Ordering::Relaxed); + }); + let all = t.map().layers.iter().find(|l| l.is_streamed()).unwrap().load_all().unwrap(); + assert_eq!(all.features.len(), 100); +} + /// A FlatGeobuf and a GeoParquet by their address are read in place, by range: a view fetches a /// small part of the file; the project keeps the address and reads it again; asked to, a file is /// downloaded whole instead. diff --git a/crates/map/src/lib.rs b/crates/map/src/lib.rs index 9bb76f2e6..94df237a3 100644 --- a/crates/map/src/lib.rs +++ b/crates/map/src/lib.rs @@ -392,6 +392,9 @@ impl Layer { let mut copy = src.clone(); if !copy.is_open() { copy.open()?; + // A source by its web address may still be opening in the background (the view or the + // project started it): wait for it, as an export does, rather than read nothing. + copy.wait_open(std::time::Duration::from_secs(30)); } let (mut ds, _) = copy.read_all()?; // A sample isn't all of them: refuse rather than run a tool on part of the data. From 53cb50da6669b58de09c0eb9782aacab34d787fa Mon Sep 17 00:00:00 2001 From: Dominik Strebel Date: Sat, 24 Oct 2026 07:06:34 +0200 Subject: [PATCH 2/2] fix(map): Load All Features names a source still opening after the wait; the test holds the server until the load waits --- crates/engine/src/tests.rs | 18 +++++++++++------- crates/map/src/lib.rs | 6 ++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 45cd5d8fc..43df0c098 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9425,13 +9425,17 @@ fn load_all_features_waits_for_a_source_still_opening() { let (slow, _) = serve_gated(fgb, true, Some(gate.clone())); let mut t = Session::new(Project::default()); t.run("project.openText", &json!({"text": text.replace(&base, &slow)})).unwrap(); - let open = gate.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(300)); - open.store(true, std::sync::atomic::Ordering::Relaxed); - }); - let all = t.map().layers.iter().find(|l| l.is_streamed()).unwrap().load_all().unwrap(); - assert_eq!(all.features.len(), 100); + let layer = t.map().layers.iter().find(|l| l.is_streamed()).unwrap().clone(); + // Started as the view starts it, and held by the server: still opening when the load begins. + let mut started = layer.source.clone().unwrap(); + started.open().unwrap(); + assert!(!started.is_open() && started.is_pending()); + let load = std::thread::spawn(move || layer.load_all()); + // Still waiting while the server holds its answers, rather than failed at once. + std::thread::sleep(std::time::Duration::from_millis(300)); + assert!(!load.is_finished(), "the load didn't wait for the source: {:?}", load.join().unwrap().err()); + gate.store(true, std::sync::atomic::Ordering::Relaxed); + assert_eq!(load.join().unwrap().unwrap().features.len(), 100); } /// A FlatGeobuf and a GeoParquet by their address are read in place, by range: a view fetches a diff --git a/crates/map/src/lib.rs b/crates/map/src/lib.rs index 94df237a3..a95ac42b4 100644 --- a/crates/map/src/lib.rs +++ b/crates/map/src/lib.rs @@ -395,6 +395,12 @@ impl Layer { // A source by its web address may still be opening in the background (the view or the // project started it): wait for it, as an export does, rather than read nothing. copy.wait_open(std::time::Duration::from_secs(30)); + if !copy.is_open() { + return Err(copy + .error + .clone() + .unwrap_or_else(|| "the layer's source is still opening: try again once it has opened".into())); + } } let (mut ds, _) = copy.read_all()?; // A sample isn't all of them: refuse rather than run a tool on part of the data.