feat: prefetch visible observation frontier
This commit is contained in:
@@ -92,10 +92,11 @@ a second explicit Enter (or `e`) to execute once. There is no operation auto-ret
|
||||
|
||||
## Screen model
|
||||
|
||||
- **Worldspace tree** starts at `/` and materializes direct children only when a
|
||||
node is expanded. Text labels distinguish unloaded, loading, empty, stale, and
|
||||
failed state; color is supplemental. Every successful expansion is complete for
|
||||
its requested depth.
|
||||
- **Worldspace tree** starts at `/`, lazily materializes expanded nodes, and
|
||||
prefetches children for each visible Object so emptiness and the next expansion
|
||||
frontier are known without selecting that Object first. Text labels distinguish
|
||||
unloaded, loading, empty, stale, and failed state; color is supplemental. Every
|
||||
successful expansion is complete for its requested depth.
|
||||
- **Selected Entry** shows only public WIP path, object name, description, ordered
|
||||
interfaces, and freshness. Validators, credentials, Host internals, and backend
|
||||
paths are deliberately hidden. If current metadata disappears, old text is not
|
||||
|
||||
@@ -299,7 +299,9 @@ impl App {
|
||||
#[must_use]
|
||||
pub fn new(endpoint: impl Into<String>) -> Self {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("/".into(), EntryView::placeholder("/"));
|
||||
let mut root = EntryView::placeholder("/");
|
||||
root.expanded = true;
|
||||
entries.insert("/".into(), root);
|
||||
Self {
|
||||
endpoint: endpoint.into(),
|
||||
connection: ConnectionState::Disconnected,
|
||||
@@ -556,7 +558,6 @@ impl App {
|
||||
let entry = self.ensure_entry(&observation.path);
|
||||
entry.children = children.clone();
|
||||
entry.tree_state = ViewState::from(&observation.state);
|
||||
entry.expanded = true;
|
||||
for path in children {
|
||||
self.ensure_entry(&path);
|
||||
}
|
||||
@@ -876,6 +877,6 @@ mod tests {
|
||||
wip_client::SecurityContext::new("test"),
|
||||
)
|
||||
.unwrap();
|
||||
client.prepare_observe(&session, "/", 0).unwrap().id
|
||||
client.refresh_object(&session, "/").unwrap().id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +608,36 @@ mod tests {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observed_empty_visible_entry_is_labeled_without_expansion() {
|
||||
let mut app = App::new("https://example.test/wip/");
|
||||
{
|
||||
let root = app.entries.get_mut("/").unwrap();
|
||||
root.children = vec!["/empty".into()];
|
||||
root.tree_state = ViewState::Fresh;
|
||||
}
|
||||
app.entries.insert(
|
||||
"/empty".into(),
|
||||
EntryView {
|
||||
path: "/empty".into(),
|
||||
object: Some(Object {
|
||||
name: "empty".into(),
|
||||
description: None,
|
||||
interfaces: vec![],
|
||||
r#ref: None,
|
||||
validator: None,
|
||||
}),
|
||||
state: ViewState::Fresh,
|
||||
children: vec![],
|
||||
tree_state: ViewState::Fresh,
|
||||
expanded: false,
|
||||
},
|
||||
);
|
||||
|
||||
let snapshot = rendered(&app, 80, 20);
|
||||
assert!(snapshot.contains("empty [empty]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_contains_non_color_state_markers_and_hides_validators() {
|
||||
let mut app = App::new("https://example.test/wip/");
|
||||
|
||||
@@ -304,7 +304,7 @@ impl<T: Transport> Runtime<T> {
|
||||
/// Starts or restarts observation at Worldspace `/`.
|
||||
pub fn connect(&mut self) {
|
||||
self.app.connection = ConnectionState::Connecting;
|
||||
match self.client.prepare_observe(&self.session, "/", 1) {
|
||||
match self.client.refresh_children(&self.session, "/") {
|
||||
Ok(request) => self.dispatch(request, InFlightKind::Tree("/".into()), false),
|
||||
Err(error) => self.local_error("connect", error),
|
||||
}
|
||||
@@ -317,6 +317,9 @@ impl<T: Transport> Runtime<T> {
|
||||
for action in actions {
|
||||
quit |= self.perform(action);
|
||||
}
|
||||
if !quit {
|
||||
self.reconcile_visible_lookahead();
|
||||
}
|
||||
quit
|
||||
}
|
||||
|
||||
@@ -347,16 +350,14 @@ impl<T: Transport> Runtime<T> {
|
||||
|
||||
fn perform(&mut self, action: Action) -> bool {
|
||||
match action {
|
||||
Action::Expand { path } => match self.client.prepare_observe(&self.session, &path, 1) {
|
||||
Action::Expand { path } => match self.client.refresh_children(&self.session, &path) {
|
||||
Ok(request) => self.dispatch(request, InFlightKind::Tree(path), false),
|
||||
Err(error) => self.local_error("tree expansion", error),
|
||||
},
|
||||
Action::Refresh { path } => {
|
||||
match self.client.prepare_observe(&self.session, &path, 1) {
|
||||
Ok(request) => self.dispatch(request, InFlightKind::Tree(path), false),
|
||||
Err(error) => self.local_error("subtree refresh", error),
|
||||
}
|
||||
}
|
||||
Action::Refresh { path } => match self.client.refresh_children(&self.session, &path) {
|
||||
Ok(request) => self.dispatch(request, InFlightKind::Tree(path), false),
|
||||
Err(error) => self.local_error("subtree refresh", error),
|
||||
},
|
||||
Action::FetchInterface(reference) => {
|
||||
match self.client.prepare_interface(&self.session, &reference) {
|
||||
Ok(request) => {
|
||||
@@ -393,7 +394,7 @@ impl<T: Transport> Runtime<T> {
|
||||
Action::Reconnect => self.connect(),
|
||||
Action::NavigateEntry(path) => {
|
||||
self.app.navigate_entry(path.clone());
|
||||
match self.client.prepare_observe(&self.session, &path, 0) {
|
||||
match self.client.refresh_object(&self.session, &path) {
|
||||
Ok(request) => self.dispatch(request, InFlightKind::Object(path), false),
|
||||
Err(error) => self.local_error("Entry navigation", error),
|
||||
}
|
||||
@@ -403,6 +404,24 @@ impl<T: Transport> Runtime<T> {
|
||||
false
|
||||
}
|
||||
|
||||
fn reconcile_visible_lookahead(&mut self) {
|
||||
if self.app.connection != ConnectionState::Connected {
|
||||
return;
|
||||
}
|
||||
for path in self.app.visible_paths() {
|
||||
match self.client.ensure_children_observed(&self.session, &path) {
|
||||
Ok(Some(request)) => {
|
||||
self.dispatch(request, InFlightKind::Tree(path), false);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
self.local_error("observation lookahead", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch(&mut self, request: PreparedRequest, kind: InFlightKind, operation: bool) {
|
||||
let id = request.id;
|
||||
self.app.request_started(id.get(), kind.clone());
|
||||
@@ -567,6 +586,7 @@ impl<T: Transport> Runtime<T> {
|
||||
}
|
||||
}
|
||||
self.reconcile_authoritative();
|
||||
self.reconcile_visible_lookahead();
|
||||
}
|
||||
|
||||
fn apply_completion(&mut self, completion: Completion, kind: Option<&InFlightKind>) {
|
||||
|
||||
@@ -53,6 +53,10 @@ fn real_http_filesystem_workflow_is_lazy_validator_safe_and_symlink_confined() {
|
||||
assert!(runtime.app().entries.contains_key("/hello.txt"));
|
||||
assert!(runtime.app().entries.contains_key("/nested"));
|
||||
assert!(runtime.app().entries.contains_key("/outside-link"));
|
||||
let empty_directory = &runtime.app().entries["/nested"];
|
||||
assert_eq!(empty_directory.tree_state, ViewState::Fresh);
|
||||
assert!(empty_directory.children.is_empty());
|
||||
assert!(!empty_directory.expanded);
|
||||
|
||||
// Keyboard-select the file and load its read interface and descriptor.
|
||||
runtime.app_mut().selected_path = "/hello.txt".into();
|
||||
|
||||
Reference in New Issue
Block a user