feat: prefetch visible observation frontier
This commit is contained in:
@@ -42,7 +42,7 @@ wip-public-api-example (workspace-only executable, integration tests, doctest)
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `wip-protocol` | Transport-independent descriptors, object/call/result types, logical `Value`s, validation, and stable protocol error categories. | Application DTOs, implementations, persistence, or wire encoding. |
|
| `wip-protocol` | Transport-independent descriptors, object/call/result types, logical `Value`s, validation, and stable protocol error categories. | Application DTOs, implementations, persistence, or wire encoding. |
|
||||||
| `wip-https` | Standard HTTPS v1 endpoint/route derivation, UTF-8 JSON codecs, direct success bodies, error/status classification, and retrieval batches. | HTTP I/O, TLS termination, authentication schemes, server frameworks, filesystem projection, or client presentation/cache policy. |
|
| `wip-https` | Standard HTTPS v1 endpoint/route derivation, UTF-8 JSON codecs, direct success bodies, error/status classification, and retrieval batches. | HTTP I/O, TLS termination, authentication schemes, server frameworks, filesystem projection, or client presentation/cache policy. |
|
||||||
| `wip-client` | Endpoint/security-context-isolated Known Space, normalized object and interface observations, lazy entry trees, immutable operation call contexts, stale transitions, and bounded histories. | HTTP I/O, credentials, persistent cache, automatic operation retries, or presentation widgets/events. |
|
| `wip-client` | Endpoint/security-context-isolated Known Space, normalized object and interface observations, intent-driven observation coverage with bounded request deduplication, immutable operation call contexts, stale transitions, and bounded histories. | HTTP I/O, credentials, persistent cache, automatic operation retries, or presentation widgets/events. |
|
||||||
| `wip-tui` | Keyboard-only interactive Worldspace tree, selected Entry and qualified operation panes, descriptor-driven argument editing, bounded result/diagnostic rendering, terminal lifecycle, and HTTPS I/O workers over `wip-client`. | Independent cache/validator semantics, automatic operation retries, Host internals, backend paths, or persistent state. |
|
| `wip-tui` | Keyboard-only interactive Worldspace tree, selected Entry and qualified operation panes, descriptor-driven argument editing, bounded result/diagnostic rendering, terminal lifecycle, and HTTPS I/O workers over `wip-client`. | Independent cache/validator semantics, automatic operation retries, Host internals, backend paths, or persistent state. |
|
||||||
| `wip-interface` | The public `WipType`/`interface` authoring API, Rust-to-protocol codecs, generated descriptors, and typed implementation dispatch. | Object publication, backend lifetime, or transport policy. |
|
| `wip-interface` | The public `WipType`/`interface` authoring API, Rust-to-protocol codecs, generated descriptors, and typed implementation dispatch. | Object publication, backend lifetime, or transport policy. |
|
||||||
| `wip-interface-macros` | Procedural-macro expansion used by `wip-interface`. It is not an application dependency. | A supported direct user API. |
|
| `wip-interface-macros` | Procedural-macro expansion used by `wip-interface`. It is not an application dependency. | A supported direct user API. |
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Presentation-independent stateful runtime for WIP clients.
|
//! Presentation-independent stateful runtime for WIP clients.
|
||||||
//!
|
//!
|
||||||
//! [`Client`] separates observations by canonical HTTPS endpoint and caller-
|
//! [`Client`] separates observations by canonical HTTPS endpoint and caller-
|
||||||
//! supplied security context. It creates requests with the standard
|
//! supplied security context. It reconciles requested observation coverage without
|
||||||
|
//! duplicating fresh or loading work, creates requests with the standard
|
||||||
//! [`wip_https`] adapter, correlates responses by [`RequestId`], and retains
|
//! [`wip_https`] adapter, correlates responses by [`RequestId`], and retains
|
||||||
//! bounded object, tree, interface, diagnostic, and operation state. The crate
|
//! bounded object, tree, interface, diagnostic, and operation state. The crate
|
||||||
//! contains no terminal, widget, or UI event types.
|
//! contains no terminal, widget, or UI event types.
|
||||||
@@ -630,8 +631,64 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts or refreshes an object observation through the requested depth.
|
/// Ensures that an observation is available through `depth` without issuing
|
||||||
pub fn prepare_observe(
|
/// duplicate work.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the requested coverage is already fresh, is currently
|
||||||
|
/// loading, is blocked by a retained failure or stale object state that requires
|
||||||
|
/// an explicit refresh, or when the bounded in-flight capacity is currently
|
||||||
|
/// full. Callers may invoke this method again after applying another completion.
|
||||||
|
pub fn ensure_observed(
|
||||||
|
&mut self,
|
||||||
|
session: &SessionId,
|
||||||
|
path: impl Into<String>,
|
||||||
|
depth: u32,
|
||||||
|
) -> Result<Option<PreparedRequest>, ClientError> {
|
||||||
|
let path = path.into();
|
||||||
|
wip_protocol::validate_path(&path).map_err(|error| {
|
||||||
|
ClientError::new(ClientErrorKind::InvalidRequest, error.to_string())
|
||||||
|
})?;
|
||||||
|
let coverage = observation_coverage(self.session(session)?, &path, depth);
|
||||||
|
if coverage != ObservationCoverage::Missing || self.pending.len() >= self.limits.in_flight {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
self.refresh_observed(session, path, depth).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that an object's direct children have been observed without
|
||||||
|
/// duplicating fresh or loading work.
|
||||||
|
pub fn ensure_children_observed(
|
||||||
|
&mut self,
|
||||||
|
session: &SessionId,
|
||||||
|
path: impl Into<String>,
|
||||||
|
) -> Result<Option<PreparedRequest>, ClientError> {
|
||||||
|
self.ensure_observed(session, path, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly refreshes only one object while preserving previously observed
|
||||||
|
/// children in Known Space.
|
||||||
|
pub fn refresh_object(
|
||||||
|
&mut self,
|
||||||
|
session: &SessionId,
|
||||||
|
path: impl Into<String>,
|
||||||
|
) -> Result<PreparedRequest, ClientError> {
|
||||||
|
self.refresh_observed(session, path, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly refreshes one object and its direct children.
|
||||||
|
pub fn refresh_children(
|
||||||
|
&mut self,
|
||||||
|
session: &SessionId,
|
||||||
|
path: impl Into<String>,
|
||||||
|
) -> Result<PreparedRequest, ClientError> {
|
||||||
|
self.refresh_observed(session, path, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly starts or refreshes an object observation through `depth`.
|
||||||
|
///
|
||||||
|
/// Unlike [`Client::ensure_observed`], this always creates a new request and
|
||||||
|
/// supersedes older requests for the same root path.
|
||||||
|
pub fn refresh_observed(
|
||||||
&mut self,
|
&mut self,
|
||||||
session: &SessionId,
|
session: &SessionId,
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
@@ -1430,6 +1487,64 @@ enum RetrievalSubject {
|
|||||||
Interface(String),
|
Interface(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum ObservationCoverage {
|
||||||
|
Missing,
|
||||||
|
Loading,
|
||||||
|
Covered,
|
||||||
|
Blocked,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observation_coverage(state: &Session, path: &str, depth: u32) -> ObservationCoverage {
|
||||||
|
let Some(entry) = state.entries.get(path) else {
|
||||||
|
return ObservationCoverage::Missing;
|
||||||
|
};
|
||||||
|
match &entry.state {
|
||||||
|
ObservationState::Loading(_) => return ObservationCoverage::Loading,
|
||||||
|
ObservationState::Stale | ObservationState::Error(_) => {
|
||||||
|
return ObservationCoverage::Blocked;
|
||||||
|
}
|
||||||
|
ObservationState::Fresh => {}
|
||||||
|
}
|
||||||
|
if entry
|
||||||
|
.object_key
|
||||||
|
.is_none_or(|key| !state.objects.contains_key(&key))
|
||||||
|
{
|
||||||
|
return ObservationCoverage::Missing;
|
||||||
|
}
|
||||||
|
if depth == 0 {
|
||||||
|
return ObservationCoverage::Covered;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(tree) = state.trees.get(path) else {
|
||||||
|
return ObservationCoverage::Missing;
|
||||||
|
};
|
||||||
|
match &tree.state {
|
||||||
|
ObservationState::Loading(_) => return ObservationCoverage::Loading,
|
||||||
|
ObservationState::Stale => return ObservationCoverage::Missing,
|
||||||
|
ObservationState::Error(_) => return ObservationCoverage::Blocked,
|
||||||
|
ObservationState::Fresh => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut saw_loading = false;
|
||||||
|
let mut saw_missing = false;
|
||||||
|
for child in &tree.children {
|
||||||
|
match observation_coverage(state, child, depth - 1) {
|
||||||
|
ObservationCoverage::Blocked => return ObservationCoverage::Blocked,
|
||||||
|
ObservationCoverage::Loading => saw_loading = true,
|
||||||
|
ObservationCoverage::Missing => saw_missing = true,
|
||||||
|
ObservationCoverage::Covered => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if saw_loading {
|
||||||
|
ObservationCoverage::Loading
|
||||||
|
} else if saw_missing {
|
||||||
|
ObservationCoverage::Missing
|
||||||
|
} else {
|
||||||
|
ObservationCoverage::Covered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn session_endpoint(session: &SessionId) -> Result<Endpoint, ClientError> {
|
fn session_endpoint(session: &SessionId) -> Result<Endpoint, ClientError> {
|
||||||
Endpoint::parse(&session.endpoint)
|
Endpoint::parse(&session.endpoint)
|
||||||
.map_err(|error| ClientError::new(ClientErrorKind::InvalidRequest, error.to_string()))
|
.map_err(|error| ClientError::new(ClientErrorKind::InvalidRequest, error.to_string()))
|
||||||
@@ -1883,6 +1998,16 @@ fn apply_observation(
|
|||||||
.collect();
|
.collect();
|
||||||
reserve_observation_metadata(state, &paths, &observed_children_paths, maximum)?;
|
reserve_observation_metadata(state, &paths, &observed_children_paths, maximum)?;
|
||||||
|
|
||||||
|
for (path, _, children) in &nodes {
|
||||||
|
if children.is_none()
|
||||||
|
&& let Some(tree) = state.trees.get_mut(path)
|
||||||
|
&& tree.revision < request_id
|
||||||
|
&& matches!(tree.state, ObservationState::Loading(_))
|
||||||
|
{
|
||||||
|
tree.state = ObservationState::Stale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut omitted = Vec::new();
|
let mut omitted = Vec::new();
|
||||||
for (path, _, children) in &nodes {
|
for (path, _, children) in &nodes {
|
||||||
let Some(children) = children else {
|
let Some(children) = children else {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ fn malformed_json_unknown_fields_duplicates_and_invalid_success_are_invalid_resp
|
|||||||
];
|
];
|
||||||
for body in cases {
|
for body in cases {
|
||||||
let (mut client, session) = setup();
|
let (mut client, session) = setup();
|
||||||
let request = client.prepare_observe(&session, "/item", 0).unwrap();
|
let request = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
let error = client
|
let error = client
|
||||||
.complete(request.id, response(StatusCode::OK, body))
|
.complete(request.id, response(StatusCode::OK, body))
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
@@ -107,7 +107,7 @@ fn disallowed_code_and_status_success_contradictions_are_invalid_response() {
|
|||||||
];
|
];
|
||||||
for (status, body) in cases {
|
for (status, body) in cases {
|
||||||
let (mut client, session) = setup();
|
let (mut client, session) = setup();
|
||||||
let request = client.prepare_observe(&session, "/item", 0).unwrap();
|
let request = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.complete(request.id, response(status, body))
|
.complete(request.id, response(status, body))
|
||||||
@@ -121,7 +121,7 @@ fn disallowed_code_and_status_success_contradictions_are_invalid_response() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn valid_error_envelope_is_authoritative_and_status_mismatch_is_diagnostic() {
|
fn valid_error_envelope_is_authoritative_and_status_mismatch_is_diagnostic() {
|
||||||
let (mut client, session) = setup();
|
let (mut client, session) = setup();
|
||||||
let request = client.prepare_observe(&session, "/item", 0).unwrap();
|
let request = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
let error = ProtocolError {
|
let error = ProtocolError {
|
||||||
code: ProtocolErrorCode::NotFound,
|
code: ProtocolErrorCode::NotFound,
|
||||||
message: "missing".into(),
|
message: "missing".into(),
|
||||||
@@ -147,7 +147,7 @@ fn valid_error_envelope_is_authoritative_and_status_mismatch_is_diagnostic() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn non_success_without_valid_envelope_remains_transport_failure() {
|
fn non_success_without_valid_envelope_remains_transport_failure() {
|
||||||
let (mut client, session) = setup();
|
let (mut client, session) = setup();
|
||||||
let request = client.prepare_observe(&session, "/item", 0).unwrap();
|
let request = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
let error = client
|
let error = client
|
||||||
.complete(
|
.complete(
|
||||||
request.id,
|
request.id,
|
||||||
@@ -177,7 +177,7 @@ fn unsupported_descriptor_format_is_distinct_from_invalid_response() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn malformed_call_success_after_dispatch_is_outcome_unknown_and_stales_object() {
|
fn malformed_call_success_after_dispatch_is_outcome_unknown_and_stales_object() {
|
||||||
let (mut client, session) = setup();
|
let (mut client, session) = setup();
|
||||||
let observation = client.prepare_observe(&session, "/item", 0).unwrap();
|
let observation = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
observation.id,
|
observation.id,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ fn observation_response(path: &str, value: &Object) -> Response<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn observe_object(client: &mut Client, session: &wip_client::SessionId, path: &str, value: Object) {
|
fn observe_object(client: &mut Client, session: &wip_client::SessionId, path: &str, value: Object) {
|
||||||
let request = client.prepare_observe(session, path, 0).unwrap();
|
let request = client.refresh_observed(session, path, 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(request.id, observation_response(path, &value))
|
.complete(request.id, observation_response(path, &value))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -173,14 +173,129 @@ fn sessions_partition_known_space_by_canonical_endpoint_and_security_context() {
|
|||||||
assert!(client.object(&other, "/item").is_none());
|
assert!(client.object(&other, "/item").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_observed_deduplicates_loading_and_fresh_coverage() {
|
||||||
|
let mut client = client(8, 8, 8, 8);
|
||||||
|
let session = client
|
||||||
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
|
.unwrap();
|
||||||
|
let request = ObserveRequest {
|
||||||
|
path: "/".into(),
|
||||||
|
depth: 1,
|
||||||
|
};
|
||||||
|
let value = TestObservationResponse {
|
||||||
|
root: TestObservation {
|
||||||
|
object: object("", None, b"root"),
|
||||||
|
children: vec![TestObservation {
|
||||||
|
object: object("child", None, b"child"),
|
||||||
|
children: vec![],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let root = client
|
||||||
|
.ensure_observed(&session, "/", 1)
|
||||||
|
.unwrap()
|
||||||
|
.expect("missing coverage starts an observation");
|
||||||
|
assert!(client.ensure_observed(&session, "/", 1).unwrap().is_none());
|
||||||
|
client
|
||||||
|
.complete(
|
||||||
|
root.id,
|
||||||
|
encode_test_observation_response(&request, &value, wire_limits()).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(client.ensure_observed(&session, "/", 1).unwrap().is_none());
|
||||||
|
|
||||||
|
let child = client
|
||||||
|
.ensure_observed(&session, "/child", 1)
|
||||||
|
.unwrap()
|
||||||
|
.expect("depth-boundary children remain missing coverage");
|
||||||
|
assert!(
|
||||||
|
client
|
||||||
|
.ensure_observed(&session, "/child", 1)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.complete(
|
||||||
|
child.id,
|
||||||
|
encode_test_observation_response(
|
||||||
|
&ObserveRequest {
|
||||||
|
path: "/child".into(),
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
&TestObservationResponse {
|
||||||
|
root: TestObservation {
|
||||||
|
object: object("child", None, b"child"),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wire_limits(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
client
|
||||||
|
.ensure_observed(&session, "/child", 1)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_observed_respects_capacity_and_does_not_loop_on_failure() {
|
||||||
|
let mut client = client(8, 8, 1, 8);
|
||||||
|
let session = client
|
||||||
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
|
.unwrap();
|
||||||
|
let first = client
|
||||||
|
.ensure_observed(&session, "/one", 0)
|
||||||
|
.unwrap()
|
||||||
|
.expect("first observation fits");
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.ensure_observed(&session, "relative", 0)
|
||||||
|
.unwrap_err()
|
||||||
|
.kind,
|
||||||
|
ClientErrorKind::InvalidRequest
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
client
|
||||||
|
.ensure_observed(&session, "/two", 0)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.fail_transport(first.id, "scripted failure")
|
||||||
|
.unwrap_err()
|
||||||
|
.kind,
|
||||||
|
ClientErrorKind::Transport
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
client
|
||||||
|
.ensure_observed(&session, "/one", 0)
|
||||||
|
.unwrap()
|
||||||
|
.is_none(),
|
||||||
|
"retained failures require an explicit refresh"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
client
|
||||||
|
.ensure_observed(&session, "/two", 0)
|
||||||
|
.unwrap()
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn newer_refresh_rejects_out_of_order_response_for_the_same_subject() {
|
fn newer_refresh_rejects_out_of_order_response_for_the_same_subject() {
|
||||||
let mut client = client(8, 8, 8, 8);
|
let mut client = client(8, 8, 8, 8);
|
||||||
let session = client
|
let session = client
|
||||||
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let old = client.prepare_observe(&session, "/item", 0).unwrap();
|
let old = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
let new = client.prepare_observe(&session, "/item", 0).unwrap();
|
let new = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
@@ -227,7 +342,7 @@ fn normalized_object_cache_rejects_conflicts_and_evicts_to_its_bound() {
|
|||||||
object("shared", Some("shared-ref"), b"v1"),
|
object("shared", Some("shared-ref"), b"v1"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let conflict = client.prepare_observe(&session, "/b/shared", 0).unwrap();
|
let conflict = client.refresh_observed(&session, "/b/shared", 0).unwrap();
|
||||||
let mut changed = object("shared", Some("shared-ref"), b"v1");
|
let mut changed = object("shared", Some("shared-ref"), b"v1");
|
||||||
changed.description = Some("contradiction".into());
|
changed.description = Some("contradiction".into());
|
||||||
let error = client
|
let error = client
|
||||||
@@ -279,8 +394,8 @@ fn normalized_aliases_preserve_newer_request_ownership() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let older_tree = client.prepare_observe(&session, "/a", 1).unwrap();
|
let older_tree = client.refresh_observed(&session, "/a", 1).unwrap();
|
||||||
let newer_alias = client.prepare_observe(&session, "/b/shared", 0).unwrap();
|
let newer_alias = client.refresh_observed(&session, "/b/shared", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
newer_alias.id,
|
newer_alias.id,
|
||||||
@@ -303,8 +418,8 @@ fn normalized_aliases_preserve_newer_request_ownership() {
|
|||||||
Some(b"newer".to_vec())
|
Some(b"newer".to_vec())
|
||||||
);
|
);
|
||||||
|
|
||||||
let older_tree = client.prepare_observe(&session, "/a", 1).unwrap();
|
let older_tree = client.refresh_observed(&session, "/a", 1).unwrap();
|
||||||
let newer_alias = client.prepare_observe(&session, "/b/shared", 0).unwrap();
|
let newer_alias = client.refresh_observed(&session, "/b/shared", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
older_tree.id,
|
older_tree.id,
|
||||||
@@ -334,8 +449,8 @@ fn normalized_aliases_preserve_newer_request_ownership() {
|
|||||||
Some(b"newest".to_vec())
|
Some(b"newest".to_vec())
|
||||||
);
|
);
|
||||||
|
|
||||||
let older_tree = client.prepare_observe(&session, "/a", 1).unwrap();
|
let older_tree = client.refresh_observed(&session, "/a", 1).unwrap();
|
||||||
let newer_alias = client.prepare_observe(&session, "/b/shared", 0).unwrap();
|
let newer_alias = client.refresh_observed(&session, "/b/shared", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
newer_alias.id,
|
newer_alias.id,
|
||||||
@@ -378,8 +493,8 @@ fn normalized_aliases_preserve_newer_request_ownership() {
|
|||||||
"/child",
|
"/child",
|
||||||
object("child", Some("child-ref"), b"stable"),
|
object("child", Some("child-ref"), b"stable"),
|
||||||
);
|
);
|
||||||
let older_tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let older_tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let newer_child = client.prepare_observe(&session, "/child", 0).unwrap();
|
let newer_child = client.refresh_observed(&session, "/child", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
newer_child.id,
|
newer_child.id,
|
||||||
@@ -426,7 +541,7 @@ fn observation_expansion_is_complete_and_refreshes_edges() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(client.tree(&session, "/").is_none());
|
assert!(client.tree(&session, "/").is_none());
|
||||||
|
|
||||||
let request = client.prepare_observe(&session, "/", 1).unwrap();
|
let request = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let logical_request = ObserveRequest {
|
let logical_request = ObserveRequest {
|
||||||
path: "/".into(),
|
path: "/".into(),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
@@ -449,7 +564,7 @@ fn observation_expansion_is_complete_and_refreshes_edges() {
|
|||||||
assert_eq!(client.tree(&session, "/").unwrap().children, ["/items"]);
|
assert_eq!(client.tree(&session, "/").unwrap().children, ["/items"]);
|
||||||
assert!(client.object(&session, "/items").is_some());
|
assert!(client.object(&session, "/items").is_some());
|
||||||
|
|
||||||
let refresh = client.prepare_observe(&session, "/", 1).unwrap();
|
let refresh = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let refreshed = TestObservationResponse {
|
let refreshed = TestObservationResponse {
|
||||||
root: TestObservation {
|
root: TestObservation {
|
||||||
object: object("", None, b"root-2"),
|
object: object("", None, b"root-2"),
|
||||||
@@ -479,7 +594,7 @@ fn depth_zero_observation_preserves_previously_observed_children() {
|
|||||||
path: "/".into(),
|
path: "/".into(),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
};
|
};
|
||||||
let deep = client.prepare_observe(&session, "/", 1).unwrap();
|
let deep = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
deep.id,
|
deep.id,
|
||||||
@@ -500,7 +615,7 @@ fn depth_zero_observation_preserves_previously_observed_children() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let shallow = client.prepare_observe(&session, "/", 0).unwrap();
|
let shallow = client.refresh_observed(&session, "/", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
shallow.id,
|
shallow.id,
|
||||||
@@ -536,8 +651,8 @@ fn shallow_observation_supersedes_loading_deep_observation_without_leaking_loadi
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let deep = client.prepare_observe(&session, "/", 1).unwrap();
|
let deep = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let shallow = client.prepare_observe(&session, "/", 0).unwrap();
|
let shallow = client.refresh_observed(&session, "/", 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client.tree(&session, "/").unwrap().state,
|
client.tree(&session, "/").unwrap().state,
|
||||||
ObservationState::Stale
|
ObservationState::Stale
|
||||||
@@ -564,6 +679,80 @@ fn shallow_observation_supersedes_loading_deep_observation_without_leaking_loadi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newer_parent_boundary_observation_replans_superseded_child_lookahead() {
|
||||||
|
let mut client = client(16, 8, 8, 8);
|
||||||
|
let session = client
|
||||||
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
|
.unwrap();
|
||||||
|
let root_request = ObserveRequest {
|
||||||
|
path: "/".into(),
|
||||||
|
depth: 1,
|
||||||
|
};
|
||||||
|
let root_value = |validator: &'static [u8]| TestObservationResponse {
|
||||||
|
root: TestObservation {
|
||||||
|
object: object("", None, validator),
|
||||||
|
children: vec![TestObservation {
|
||||||
|
object: object("child", None, validator),
|
||||||
|
children: vec![],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let initial = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
|
client
|
||||||
|
.complete(
|
||||||
|
initial.id,
|
||||||
|
encode_test_observation_response(&root_request, &root_value(b"initial"), wire_limits())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let older_child = client.refresh_observed(&session, "/child", 1).unwrap();
|
||||||
|
let newer_parent = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
|
client
|
||||||
|
.complete(
|
||||||
|
newer_parent.id,
|
||||||
|
encode_test_observation_response(&root_request, &root_value(b"newer"), wire_limits())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.tree(&session, "/child").unwrap().state,
|
||||||
|
ObservationState::Stale
|
||||||
|
);
|
||||||
|
|
||||||
|
let replanned = client
|
||||||
|
.ensure_observed(&session, "/child", 1)
|
||||||
|
.unwrap()
|
||||||
|
.expect("superseded lookahead is missing rather than permanently loading");
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.complete(
|
||||||
|
older_child.id,
|
||||||
|
encode_test_observation_response(
|
||||||
|
&ObserveRequest {
|
||||||
|
path: "/child".into(),
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
&TestObservationResponse {
|
||||||
|
root: TestObservation {
|
||||||
|
object: object("child", None, b"older"),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wire_limits(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Completion::StaleResponseRejected(older_child.id)
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
client.object(&session, "/child").unwrap().state,
|
||||||
|
ObservationState::Loading(id) if id == replanned.id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
|
fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
|
||||||
let mut client = client(8, 8, 8, 8);
|
let mut client = client(8, 8, 8, 8);
|
||||||
@@ -818,7 +1007,7 @@ fn older_retrieval_cannot_overwrite_success_validator() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let older_alias = client.prepare_observe(&session, "/alias/item", 0).unwrap();
|
let older_alias = client.refresh_observed(&session, "/alias/item", 0).unwrap();
|
||||||
let call = client
|
let call = client
|
||||||
.prepare_call(&session, "/item", "math", "calculate", args(7))
|
.prepare_call(&session, "/item", "math", "calculate", args(7))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -878,7 +1067,9 @@ fn older_retrieval_cannot_overwrite_success_validator() {
|
|||||||
"/item",
|
"/item",
|
||||||
object("item", Some("shared-ref"), b"object-v2"),
|
object("item", Some("shared-ref"), b"object-v2"),
|
||||||
);
|
);
|
||||||
let older_alias = client.prepare_observe(&session, "/second/item", 0).unwrap();
|
let older_alias = client
|
||||||
|
.refresh_observed(&session, "/second/item", 0)
|
||||||
|
.unwrap();
|
||||||
let call = client
|
let call = client
|
||||||
.prepare_call(&session, "/item", "math", "calculate", args(8))
|
.prepare_call(&session, "/item", "math", "calculate", args(8))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -941,7 +1132,7 @@ fn in_flight_calls_pin_target_identity_until_staleness_is_applied() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.prepare_observe(&session, "/other", 0)
|
.refresh_observed(&session, "/other", 0)
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
.kind,
|
.kind,
|
||||||
ClientErrorKind::Capacity
|
ClientErrorKind::Capacity
|
||||||
@@ -1129,8 +1320,8 @@ fn overlapping_tree_work_preserves_newer_subject_request_ownership() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let old_tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let newer_observation = client.prepare_observe(&session, "/child", 0).unwrap();
|
let newer_observation = client.refresh_observed(&session, "/child", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
old_tree.id,
|
old_tree.id,
|
||||||
@@ -1152,8 +1343,8 @@ fn overlapping_tree_work_preserves_newer_subject_request_ownership() {
|
|||||||
Some(b"newer-observation".to_vec())
|
Some(b"newer-observation".to_vec())
|
||||||
);
|
);
|
||||||
|
|
||||||
let older_tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let older_tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let completed_newer_observation = client.prepare_observe(&session, "/child", 0).unwrap();
|
let completed_newer_observation = client.refresh_observed(&session, "/child", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
completed_newer_observation.id,
|
completed_newer_observation.id,
|
||||||
@@ -1171,8 +1362,8 @@ fn overlapping_tree_work_preserves_newer_subject_request_ownership() {
|
|||||||
Some(b"completed-newer".to_vec())
|
Some(b"completed-newer".to_vec())
|
||||||
);
|
);
|
||||||
|
|
||||||
let older_omitting_tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let older_omitting_tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let newer_observation = client.prepare_observe(&session, "/child", 0).unwrap();
|
let newer_observation = client.refresh_observed(&session, "/child", 0).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
newer_observation.id,
|
newer_observation.id,
|
||||||
@@ -1199,8 +1390,8 @@ fn overlapping_tree_work_preserves_newer_subject_request_ownership() {
|
|||||||
assert_eq!(child.state, ObservationState::Fresh);
|
assert_eq!(child.state, ObservationState::Fresh);
|
||||||
assert_eq!(child.validator, Some(b"newer-than-omission".to_vec()));
|
assert_eq!(child.validator, Some(b"newer-than-omission".to_vec()));
|
||||||
|
|
||||||
let old_tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let old_tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let newer_child_tree = client.prepare_observe(&session, "/child", 1).unwrap();
|
let newer_child_tree = client.refresh_observed(&session, "/child", 1).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
old_tree.id,
|
old_tree.id,
|
||||||
@@ -1241,8 +1432,8 @@ fn overlapping_tree_work_preserves_newer_subject_request_ownership() {
|
|||||||
ObservationState::Fresh
|
ObservationState::Fresh
|
||||||
);
|
);
|
||||||
|
|
||||||
let older_parent = client.prepare_observe(&session, "/", 1).unwrap();
|
let older_parent = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let newer_child = client.prepare_observe(&session, "/child", 1).unwrap();
|
let newer_child = client.refresh_observed(&session, "/child", 1).unwrap();
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
newer_child.id,
|
newer_child.id,
|
||||||
@@ -1281,8 +1472,8 @@ fn superseded_transport_failure_does_not_poison_newer_refresh() {
|
|||||||
let session = client
|
let session = client
|
||||||
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let old = client.prepare_observe(&session, "/item", 0).unwrap();
|
let old = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
let newer = client.prepare_observe(&session, "/item", 0).unwrap();
|
let newer = client.refresh_observed(&session, "/item", 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.fail_transport(old.id, "old connection failed")
|
.fail_transport(old.id, "old connection failed")
|
||||||
@@ -1300,8 +1491,8 @@ fn superseded_transport_failure_does_not_poison_newer_refresh() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let old = client.prepare_observe(&session, "/", 1).unwrap();
|
let old = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
let newer_tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let newer_tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.fail_transport(old.id, "old tree connection failed")
|
.fail_transport(old.id, "old tree connection failed")
|
||||||
@@ -1410,10 +1601,10 @@ fn entry_eviction_never_discards_a_loading_tree_subject() {
|
|||||||
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
observe_object(&mut client, &session, "/", object("", None, b"root"));
|
observe_object(&mut client, &session, "/", object("", None, b"root"));
|
||||||
let tree = client.prepare_observe(&session, "/", 1).unwrap();
|
let tree = client.refresh_observed(&session, "/", 1).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.prepare_observe(&session, "/other", 0)
|
.refresh_observed(&session, "/other", 0)
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
.kind,
|
.kind,
|
||||||
ClientErrorKind::Capacity
|
ClientErrorKind::Capacity
|
||||||
@@ -1454,7 +1645,7 @@ fn failed_entry_and_tree_observation_metadata_is_bounded() {
|
|||||||
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for path in ["/one", "/two", "/three"] {
|
for path in ["/one", "/two", "/three"] {
|
||||||
let request = client.prepare_observe(&session, path, 0).unwrap();
|
let request = client.refresh_observed(&session, path, 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.fail_transport(request.id, "retrieval failed")
|
.fail_transport(request.id, "retrieval failed")
|
||||||
@@ -1468,7 +1659,7 @@ fn failed_entry_and_tree_observation_metadata_is_bounded() {
|
|||||||
assert!(client.object(&session, "/three").is_some());
|
assert!(client.object(&session, "/three").is_some());
|
||||||
|
|
||||||
for path in ["/alpha", "/beta", "/gamma"] {
|
for path in ["/alpha", "/beta", "/gamma"] {
|
||||||
let request = client.prepare_observe(&session, path, 1).unwrap();
|
let request = client.refresh_observed(&session, path, 1).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client
|
client
|
||||||
.fail_transport(request.id, "tree failed")
|
.fail_transport(request.id, "tree failed")
|
||||||
@@ -1487,8 +1678,8 @@ fn in_flight_and_history_are_bounded() {
|
|||||||
let session = client
|
let session = client
|
||||||
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
.open_session("https://example.test/wip", SecurityContext::new("subject"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let first = client.prepare_observe(&session, "/one", 0).unwrap();
|
let first = client.refresh_observed(&session, "/one", 0).unwrap();
|
||||||
let error = client.prepare_observe(&session, "/two", 0).unwrap_err();
|
let error = client.refresh_observed(&session, "/two", 0).unwrap_err();
|
||||||
assert_eq!(error.kind, ClientErrorKind::Capacity);
|
assert_eq!(error.kind, ClientErrorKind::Capacity);
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
@@ -1498,8 +1689,8 @@ fn in_flight_and_history_are_bounded() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Diagnostic history evicts oldest entries at the configured bound.
|
// Diagnostic history evicts oldest entries at the configured bound.
|
||||||
let stale_old = client.prepare_observe(&session, "/one", 0).unwrap();
|
let stale_old = client.refresh_observed(&session, "/one", 0).unwrap();
|
||||||
let stale_new = client.prepare_observe(&session, "/one", 0).unwrap_err();
|
let stale_new = client.refresh_observed(&session, "/one", 0).unwrap_err();
|
||||||
assert_eq!(stale_new.kind, ClientErrorKind::Capacity);
|
assert_eq!(stale_new.kind, ClientErrorKind::Capacity);
|
||||||
client
|
client
|
||||||
.complete(
|
.complete(
|
||||||
|
|||||||
@@ -92,10 +92,11 @@ a second explicit Enter (or `e`) to execute once. There is no operation auto-ret
|
|||||||
|
|
||||||
## Screen model
|
## Screen model
|
||||||
|
|
||||||
- **Worldspace tree** starts at `/` and materializes direct children only when a
|
- **Worldspace tree** starts at `/`, lazily materializes expanded nodes, and
|
||||||
node is expanded. Text labels distinguish unloaded, loading, empty, stale, and
|
prefetches children for each visible Object so emptiness and the next expansion
|
||||||
failed state; color is supplemental. Every successful expansion is complete for
|
frontier are known without selecting that Object first. Text labels distinguish
|
||||||
its requested depth.
|
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
|
- **Selected Entry** shows only public WIP path, object name, description, ordered
|
||||||
interfaces, and freshness. Validators, credentials, Host internals, and backend
|
interfaces, and freshness. Validators, credentials, Host internals, and backend
|
||||||
paths are deliberately hidden. If current metadata disappears, old text is not
|
paths are deliberately hidden. If current metadata disappears, old text is not
|
||||||
|
|||||||
@@ -299,7 +299,9 @@ impl App {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(endpoint: impl Into<String>) -> Self {
|
pub fn new(endpoint: impl Into<String>) -> Self {
|
||||||
let mut entries = BTreeMap::new();
|
let mut entries = BTreeMap::new();
|
||||||
entries.insert("/".into(), EntryView::placeholder("/"));
|
let mut root = EntryView::placeholder("/");
|
||||||
|
root.expanded = true;
|
||||||
|
entries.insert("/".into(), root);
|
||||||
Self {
|
Self {
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
connection: ConnectionState::Disconnected,
|
connection: ConnectionState::Disconnected,
|
||||||
@@ -556,7 +558,6 @@ impl App {
|
|||||||
let entry = self.ensure_entry(&observation.path);
|
let entry = self.ensure_entry(&observation.path);
|
||||||
entry.children = children.clone();
|
entry.children = children.clone();
|
||||||
entry.tree_state = ViewState::from(&observation.state);
|
entry.tree_state = ViewState::from(&observation.state);
|
||||||
entry.expanded = true;
|
|
||||||
for path in children {
|
for path in children {
|
||||||
self.ensure_entry(&path);
|
self.ensure_entry(&path);
|
||||||
}
|
}
|
||||||
@@ -876,6 +877,6 @@ mod tests {
|
|||||||
wip_client::SecurityContext::new("test"),
|
wip_client::SecurityContext::new("test"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
client.prepare_observe(&session, "/", 0).unwrap().id
|
client.refresh_object(&session, "/").unwrap().id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -608,6 +608,36 @@ mod tests {
|
|||||||
.join("\n")
|
.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]
|
#[test]
|
||||||
fn snapshot_contains_non_color_state_markers_and_hides_validators() {
|
fn snapshot_contains_non_color_state_markers_and_hides_validators() {
|
||||||
let mut app = App::new("https://example.test/wip/");
|
let mut app = App::new("https://example.test/wip/");
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ impl<T: Transport> Runtime<T> {
|
|||||||
/// Starts or restarts observation at Worldspace `/`.
|
/// Starts or restarts observation at Worldspace `/`.
|
||||||
pub fn connect(&mut self) {
|
pub fn connect(&mut self) {
|
||||||
self.app.connection = ConnectionState::Connecting;
|
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),
|
Ok(request) => self.dispatch(request, InFlightKind::Tree("/".into()), false),
|
||||||
Err(error) => self.local_error("connect", error),
|
Err(error) => self.local_error("connect", error),
|
||||||
}
|
}
|
||||||
@@ -317,6 +317,9 @@ impl<T: Transport> Runtime<T> {
|
|||||||
for action in actions {
|
for action in actions {
|
||||||
quit |= self.perform(action);
|
quit |= self.perform(action);
|
||||||
}
|
}
|
||||||
|
if !quit {
|
||||||
|
self.reconcile_visible_lookahead();
|
||||||
|
}
|
||||||
quit
|
quit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,16 +350,14 @@ impl<T: Transport> Runtime<T> {
|
|||||||
|
|
||||||
fn perform(&mut self, action: Action) -> bool {
|
fn perform(&mut self, action: Action) -> bool {
|
||||||
match action {
|
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),
|
Ok(request) => self.dispatch(request, InFlightKind::Tree(path), false),
|
||||||
Err(error) => self.local_error("tree expansion", error),
|
Err(error) => self.local_error("tree expansion", error),
|
||||||
},
|
},
|
||||||
Action::Refresh { path } => {
|
Action::Refresh { path } => match self.client.refresh_children(&self.session, &path) {
|
||||||
match self.client.prepare_observe(&self.session, &path, 1) {
|
Ok(request) => self.dispatch(request, InFlightKind::Tree(path), false),
|
||||||
Ok(request) => self.dispatch(request, InFlightKind::Tree(path), false),
|
Err(error) => self.local_error("subtree refresh", error),
|
||||||
Err(error) => self.local_error("subtree refresh", error),
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
Action::FetchInterface(reference) => {
|
Action::FetchInterface(reference) => {
|
||||||
match self.client.prepare_interface(&self.session, &reference) {
|
match self.client.prepare_interface(&self.session, &reference) {
|
||||||
Ok(request) => {
|
Ok(request) => {
|
||||||
@@ -393,7 +394,7 @@ impl<T: Transport> Runtime<T> {
|
|||||||
Action::Reconnect => self.connect(),
|
Action::Reconnect => self.connect(),
|
||||||
Action::NavigateEntry(path) => {
|
Action::NavigateEntry(path) => {
|
||||||
self.app.navigate_entry(path.clone());
|
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),
|
Ok(request) => self.dispatch(request, InFlightKind::Object(path), false),
|
||||||
Err(error) => self.local_error("Entry navigation", error),
|
Err(error) => self.local_error("Entry navigation", error),
|
||||||
}
|
}
|
||||||
@@ -403,6 +404,24 @@ impl<T: Transport> Runtime<T> {
|
|||||||
false
|
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) {
|
fn dispatch(&mut self, request: PreparedRequest, kind: InFlightKind, operation: bool) {
|
||||||
let id = request.id;
|
let id = request.id;
|
||||||
self.app.request_started(id.get(), kind.clone());
|
self.app.request_started(id.get(), kind.clone());
|
||||||
@@ -567,6 +586,7 @@ impl<T: Transport> Runtime<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.reconcile_authoritative();
|
self.reconcile_authoritative();
|
||||||
|
self.reconcile_visible_lookahead();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_completion(&mut self, completion: Completion, kind: Option<&InFlightKind>) {
|
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("/hello.txt"));
|
||||||
assert!(runtime.app().entries.contains_key("/nested"));
|
assert!(runtime.app().entries.contains_key("/nested"));
|
||||||
assert!(runtime.app().entries.contains_key("/outside-link"));
|
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.
|
// Keyboard-select the file and load its read interface and descriptor.
|
||||||
runtime.app_mut().selected_path = "/hello.txt".into();
|
runtime.app_mut().selected_path = "/hello.txt".into();
|
||||||
|
|||||||
Reference in New Issue
Block a user