feat: prefetch visible observation frontier

This commit is contained in:
2026-09-30 19:26:50 +09:00
parent ea7b0f00ea
commit 06edf62d4e
9 changed files with 440 additions and 68 deletions
+5 -4
View File
@@ -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
+4 -3
View File
@@ -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
}
}
+30
View File
@@ -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/");
+29 -9
View File
@@ -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>) {
+4
View File
@@ -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();