feat(host): unify typed failure results
Represent denied, timeout, unavailable, and error host outcomes with one HostFailure result shape, update the browser adapter, and keep Workout host recovery flowing through app code before UI effects. req: host/002 req: host/005 req: local/003
This commit is contained in:
+71
-7
@@ -381,6 +381,47 @@ impl HostCall {
|
||||
}
|
||||
}
|
||||
|
||||
/// Boring, typed host failure classes that app code can handle before it emits UI.
|
||||
/// req: host/002 req: host/005
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostFailureKind {
|
||||
PermissionDenied,
|
||||
Timeout,
|
||||
Unavailable,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// One host failure result shape for denied, timeout, unavailable, and error cases.
|
||||
/// req: host/002 req: host/005
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HostFailure {
|
||||
pub id: Option<HostCallId>,
|
||||
pub capability: Option<Capability>,
|
||||
pub kind: HostFailureKind,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl HostFailure {
|
||||
pub fn new(kind: HostFailureKind, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
capability: None,
|
||||
kind,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: HostCallId) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_capability(mut self, capability: Capability) -> Self {
|
||||
self.capability = Some(capability);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Facts and results produced by a host adapter.
|
||||
///
|
||||
/// App code decides what a host event means for the product/domain before any
|
||||
@@ -390,17 +431,11 @@ pub enum HostEvent {
|
||||
Acknowledged {
|
||||
id: HostCallId,
|
||||
},
|
||||
Failed {
|
||||
id: Option<HostCallId>,
|
||||
message: String,
|
||||
},
|
||||
Failed(HostFailure),
|
||||
ShareCompleted {
|
||||
id: HostCallId,
|
||||
completed: bool,
|
||||
},
|
||||
PermissionDenied {
|
||||
capability: Capability,
|
||||
},
|
||||
StreamChunk {
|
||||
stream: HostStreamId,
|
||||
bytes: Vec<u8>,
|
||||
@@ -442,6 +477,8 @@ mod tests {
|
||||
assert!(
|
||||
BROWSER_HOST_JS.contains("return { ShareCompleted: { id: data.id, completed: true } }")
|
||||
);
|
||||
assert!(BROWSER_HOST_JS.contains("kind, message"));
|
||||
assert!(BROWSER_HOST_JS.contains("\"Unavailable\""));
|
||||
assert!(!BROWSER_HOST_JS.contains("querySelector"));
|
||||
assert!(!BROWSER_HOST_JS.contains("innerHTML"));
|
||||
assert!(!BROWSER_HOST_JS.contains("classList"));
|
||||
@@ -520,6 +557,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_failures_use_one_typed_result_shape() {
|
||||
// req: host/002 req: host/005
|
||||
let denied = HostEvent::Failed(
|
||||
HostFailure::new(HostFailureKind::PermissionDenied, "share denied")
|
||||
.with_id(HostCallId::new("share-1"))
|
||||
.with_capability(Capability::Share),
|
||||
);
|
||||
let timeout = HostEvent::Failed(
|
||||
HostFailure::new(HostFailureKind::Timeout, "share timed out")
|
||||
.with_id(HostCallId::new("share-1"))
|
||||
.with_capability(Capability::Share),
|
||||
);
|
||||
let unavailable = HostEvent::Failed(
|
||||
HostFailure::new(HostFailureKind::Unavailable, "share unsupported")
|
||||
.with_capability(Capability::Share),
|
||||
);
|
||||
let error = HostEvent::Failed(HostFailure::new(HostFailureKind::Error, "host crashed"));
|
||||
|
||||
for event in [denied, timeout, unavailable, error] {
|
||||
match event {
|
||||
HostEvent::Failed(failure) => assert!(!failure.message.is_empty()),
|
||||
other => panic!("unexpected host event shape: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_pwa_host_result_routes_through_app_code_before_hemx_effect() {
|
||||
// req: host/001 req: host/002 req: host/005
|
||||
|
||||
Reference in New Issue
Block a user