feat(host): wire techdemo host capability flow
Route browser/PWA share and native-shell haptic host results through techdemo app code before returning normal hemx effects; serve the tiny browser host adapter as an app asset. req: host/001 req: host/002 req: host/004 req: host/005 req: examples/001
This commit is contained in:
Generated
+1
@@ -573,6 +573,7 @@ dependencies = [
|
||||
"hemx",
|
||||
"hemx-axum",
|
||||
"hemx-build",
|
||||
"hemx-host",
|
||||
"hemx-test",
|
||||
"scraper",
|
||||
"thirtyfour",
|
||||
|
||||
@@ -13,6 +13,7 @@ futures-util = "0.3"
|
||||
hemplate = { path = "../../../hemplate/hemplate" }
|
||||
hemx = { path = "../../hemx" }
|
||||
hemx-axum = { path = "../../hemx-axum" }
|
||||
hemx-host = { path = "../../hemx-host" }
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -10,9 +10,16 @@ use hemx_axum::{
|
||||
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||
EffectResponse, InteractionRequest, PageRequest,
|
||||
};
|
||||
use hemx_host::{
|
||||
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
|
||||
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, SharePayload,
|
||||
BROWSER_HOST_JS,
|
||||
};
|
||||
use hemx_techdemo::ui;
|
||||
use hemx_techdemo::ui::control_center::{self as control, classes};
|
||||
use hemx_techdemo::ui::{issue_card as card_control, issue_lane as lane_control};
|
||||
use hemx_techdemo::ui::{
|
||||
host_panel as host_control, issue_card as card_control, issue_lane as lane_control,
|
||||
};
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
@@ -25,6 +32,7 @@ const LANES: [(&str, &str, &str); 3] = [
|
||||
("product", "Product", "Native UX, zero app JS"),
|
||||
];
|
||||
const ISLAND_ORBIT: EventName = EventName::new("hemx:island-orbit");
|
||||
const HOST_CALL: EventName = EventName::new("hemx:host-call");
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WorkItem {
|
||||
@@ -67,6 +75,7 @@ struct DemoState {
|
||||
activity: VecDeque<String>,
|
||||
spotlight: String,
|
||||
selected_id: Option<u64>,
|
||||
host_status: String,
|
||||
}
|
||||
|
||||
impl Default for DemoState {
|
||||
@@ -99,6 +108,7 @@ impl Default for DemoState {
|
||||
activity: VecDeque::new(),
|
||||
spotlight: "No selectors. Generated resources address every target.".into(),
|
||||
selected_id: Some(2),
|
||||
host_status: "Host calls are typed facts until app code accepts a result.".into(),
|
||||
};
|
||||
state.log("Demo booted from server-rendered HTML");
|
||||
state.log("Runtime attached one delegated listener per root");
|
||||
@@ -151,6 +161,7 @@ struct ControlCenter {
|
||||
board: Html,
|
||||
inspector: Html,
|
||||
activity: Html,
|
||||
host: Html,
|
||||
island_snapshot: String,
|
||||
}
|
||||
|
||||
@@ -192,6 +203,13 @@ struct ActivityFeed {
|
||||
items: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct HostPanel {
|
||||
status: String,
|
||||
boundary: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct ArchitectureActivity;
|
||||
@@ -230,6 +248,7 @@ async fn main() {
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.route("/app.css", get(app_css))
|
||||
.route("/control_center.css", get(control_center_css))
|
||||
.route("/hemx-browser-host.js", get(browser_host_js))
|
||||
.route("/island.js", get(island_js))
|
||||
.with_state(state);
|
||||
|
||||
@@ -258,6 +277,7 @@ async fn architecture(request: PageRequest) -> impl IntoResponse {
|
||||
board: architecture_board(),
|
||||
inspector: architecture_inspector(),
|
||||
activity: architecture_activity(),
|
||||
host: render_host_panel(&DemoState::default()),
|
||||
island_snapshot: "2|3|21|architecture route · same opaque island bridge".to_owned(),
|
||||
});
|
||||
request
|
||||
@@ -288,6 +308,14 @@ async fn control_center_css() -> impl IntoResponse {
|
||||
)
|
||||
}
|
||||
|
||||
async fn browser_host_js() -> impl IntoResponse {
|
||||
// req: host/001 req: host/002 req: host/005
|
||||
(
|
||||
[("content-type", "application/javascript; charset=utf-8")],
|
||||
BROWSER_HOST_JS,
|
||||
)
|
||||
}
|
||||
|
||||
async fn island_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
@@ -324,6 +352,72 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
||||
sse(batches)
|
||||
}
|
||||
|
||||
enum HostAppCommand {
|
||||
BrowserShareCompleted,
|
||||
NativeHapticAcknowledged,
|
||||
}
|
||||
|
||||
fn browser_manifest() -> CapabilityManifest {
|
||||
CapabilityManifest::new([CapabilityUse::new(
|
||||
Capability::Share,
|
||||
CapabilityShape::Request,
|
||||
)])
|
||||
}
|
||||
|
||||
fn native_manifest() -> CapabilityManifest {
|
||||
CapabilityManifest::new([CapabilityUse::new(
|
||||
Capability::Haptics,
|
||||
CapabilityShape::Fire,
|
||||
)])
|
||||
}
|
||||
|
||||
fn browser_share_call() -> HostCall {
|
||||
HostCall::Share {
|
||||
id: HostCallId::new("browser-share-1"),
|
||||
payload: SharePayload::text("hemx host capability demo"),
|
||||
}
|
||||
}
|
||||
|
||||
fn native_haptic_call() -> HostCall {
|
||||
HostCall::Haptic {
|
||||
id: HostCallId::new("native-haptic-tap"),
|
||||
pattern: HapticPattern::Success,
|
||||
}
|
||||
}
|
||||
|
||||
fn host_event_to_command(event: HostEvent) -> Option<HostAppCommand> {
|
||||
match event {
|
||||
HostEvent::ShareCompleted {
|
||||
completed: true, ..
|
||||
} => Some(HostAppCommand::BrowserShareCompleted),
|
||||
HostEvent::Acknowledged { id } if id.0 == "native-haptic-tap" => {
|
||||
Some(HostAppCommand::NativeHapticAcknowledged)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_host_event(demo: &mut DemoState, event: HostEvent) {
|
||||
// req: host/002 req: host/005
|
||||
match host_event_to_command(event) {
|
||||
Some(HostAppCommand::BrowserShareCompleted) => {
|
||||
demo.host_status =
|
||||
"Browser/PWA HostEvent became an app command before UI effects.".into();
|
||||
demo.log("Browser share completed through app host pipeline");
|
||||
}
|
||||
Some(HostAppCommand::NativeHapticAcknowledged) => {
|
||||
demo.host_status =
|
||||
"Native-shell HostEvent became an app command before UI effects.".into();
|
||||
demo.log("Native haptic acknowledgment accepted by app code");
|
||||
}
|
||||
None => {
|
||||
demo.host_status =
|
||||
"HostEvent ignored by app policy; no domain change was appended.".into();
|
||||
demo.log("Ignored host event by app policy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
||||
interactions(ui::BUILD_FINGERPRINT)
|
||||
.on(control::launch_work, {
|
||||
@@ -419,6 +513,74 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
||||
demo_effects(&demo, "Inspector focused")
|
||||
}
|
||||
})
|
||||
.on(host_control::request_browser_share, {
|
||||
let shared = shared.clone();
|
||||
move |_| {
|
||||
// req: host/001 req: host/004 req: examples/001
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
let call = browser_share_call();
|
||||
match browser_manifest().validate_call(&browser_pwa_host_profile(), &call) {
|
||||
Ok(()) => {
|
||||
demo.host_status = "Browser/PWA share request accepted; waiting for HostEvent from the host adapter.".into();
|
||||
demo.log("Requested browser/PWA share through hemx-host");
|
||||
(demo_effects(&demo, "Browser host call requested"), HOST_CALL.emit("browser-share-1"))
|
||||
}
|
||||
Err(error) => {
|
||||
demo.host_status = format!("Browser/PWA host check failed: {error}");
|
||||
(demo_effects(&demo, "Browser host check failed"), HOST_CALL.emit("browser-share-failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on(host_control::record_browser_share, {
|
||||
let shared = shared.clone();
|
||||
move |form| {
|
||||
// req: host/002 req: host/005 req: examples/001
|
||||
let completed = form.parse::<bool>("completed").unwrap_or(true);
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
apply_host_event(
|
||||
&mut demo,
|
||||
HostEvent::ShareCompleted {
|
||||
id: HostCallId::new("browser-share-1"),
|
||||
completed,
|
||||
},
|
||||
);
|
||||
demo_effects(&demo, "Browser host result accepted by app code")
|
||||
}
|
||||
})
|
||||
.on(host_control::request_native_haptic, {
|
||||
let shared = shared.clone();
|
||||
move |_| {
|
||||
// req: host/001 req: host/004 req: examples/001
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
let call = native_haptic_call();
|
||||
match native_manifest().validate_call(&native_shell_host_profile("ios-android-webview"), &call) {
|
||||
Ok(()) => {
|
||||
demo.host_status = "Native-shell haptic request accepted; waiting for host acknowledgment.".into();
|
||||
demo.log("Requested native-shell haptic through hemx-host");
|
||||
(demo_effects(&demo, "Native host call requested"), HOST_CALL.emit("native-haptic-tap"))
|
||||
}
|
||||
Err(error) => {
|
||||
demo.host_status = format!("Native host check failed: {error}");
|
||||
(demo_effects(&demo, "Native host check failed"), HOST_CALL.emit("native-haptic-failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on(host_control::record_native_haptic_ack, {
|
||||
let shared = shared.clone();
|
||||
move |_| {
|
||||
// req: host/002 req: host/005 req: examples/001
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
apply_host_event(
|
||||
&mut demo,
|
||||
HostEvent::Acknowledged {
|
||||
id: HostCallId::new("native-haptic-tap"),
|
||||
},
|
||||
);
|
||||
demo_effects(&demo, "Native host result accepted by app code")
|
||||
}
|
||||
})
|
||||
.on(control::simulate_push, {
|
||||
let shared = shared.clone();
|
||||
move |_| {
|
||||
@@ -464,6 +626,7 @@ fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
|
||||
control::board.put(&board_view(demo)),
|
||||
control::activity.put(&activity_view(demo)),
|
||||
control::inspector.put(&inspector_view(demo)),
|
||||
control::host_panel.put(&host_panel(demo)),
|
||||
control::notice.text(notice),
|
||||
control::launch_work_form.clear(),
|
||||
ISLAND_ORBIT.emit(island_snapshot(demo)),
|
||||
@@ -510,6 +673,7 @@ fn page_html(demo: &DemoState) -> Html {
|
||||
board: ui::render(&board_view(demo)),
|
||||
inspector: render_inspector(demo),
|
||||
activity: render_activity(demo),
|
||||
host: render_host_panel(demo),
|
||||
island_snapshot: island_snapshot(demo),
|
||||
})
|
||||
}
|
||||
@@ -536,7 +700,7 @@ fn hero_view(demo: &DemoState) -> HeroMetrics {
|
||||
.count();
|
||||
let impact: u64 = demo.work.iter().map(|item| item.impact as u64).sum();
|
||||
HeroMetrics {
|
||||
resource_count: 13,
|
||||
resource_count: 18,
|
||||
active_count: active,
|
||||
shipped_count: shipped,
|
||||
impact_score: impact,
|
||||
@@ -592,6 +756,19 @@ fn render_activity(demo: &DemoState) -> Html {
|
||||
ui::render(&activity_view(demo))
|
||||
}
|
||||
|
||||
fn host_panel(demo: &DemoState) -> HostPanel {
|
||||
// req: host/001 req: host/002 req: host/005
|
||||
HostPanel {
|
||||
status: demo.host_status.clone(),
|
||||
boundary: "HostCall → HostEvent → app command → EffectBatch",
|
||||
}
|
||||
}
|
||||
|
||||
fn render_host_panel(demo: &DemoState) -> Html {
|
||||
// req: host/001 req: host/002 req: host/005
|
||||
ui::render(&host_panel(demo))
|
||||
}
|
||||
|
||||
fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
||||
// req: html_safety/002 req: view/001
|
||||
let selected = demo
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>hemx Techdemo</title>
|
||||
<script +src="self.runtime_src" defer></script>
|
||||
<script src="/hemx-browser-host.js" defer></script>
|
||||
<script src="/island.js" defer></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<link rel="stylesheet" href="/control_center.css">
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<button type="button" data-hemx-handle="simulate_push">Simulate server push</button>
|
||||
<button type="button" data-hemx-handle="reset_demo">Reset demo</button>
|
||||
</div>
|
||||
<div data-hemx-slot="host_panel">{+= self.host =+}</div>
|
||||
<p data-hemx-slot="notice" class="notice">Every control posts through a generated handle and receives typed updates.</p>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="host-panel">
|
||||
<p><strong>Typed host boundary</strong><br>{+ self.status +}</p>
|
||||
<div class="quick-actions">
|
||||
<button type="button" data-hemx-handle="request_browser_share">Request browser/PWA share</button>
|
||||
<button type="button" data-hemx-handle="record_browser_share" name="completed" value="true">Record browser share result</button>
|
||||
<button type="button" data-hemx-handle="request_native_haptic">Request native haptic</button>
|
||||
<button type="button" data-hemx-handle="record_native_haptic_ack">Record native haptic ack</button>
|
||||
</div>
|
||||
<code>{+ self.boundary +}</code>
|
||||
</div>
|
||||
@@ -1,5 +1,8 @@
|
||||
use hemx_axum::runtime_js_path;
|
||||
use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
|
||||
use hemx_techdemo::ui::host_panel::{
|
||||
record_browser_share, record_native_haptic_ack, request_browser_share, request_native_haptic,
|
||||
};
|
||||
use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
|
||||
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
|
||||
use hemx_techdemo::ui::BUILD_FINGERPRINT;
|
||||
@@ -256,6 +259,53 @@ fn product_is_e2e_working_over_http() {
|
||||
);
|
||||
assert_emit(&push_batch, &island_event_name("orbit"), "activity rows");
|
||||
|
||||
let browser_host_request = post("/", &handle_form_body(request_browser_share, &[]));
|
||||
assert_effect_response(&browser_host_request);
|
||||
let browser_request_batch = browser_host_request.effects();
|
||||
assert_payload_contains(&browser_request_batch, "Browser host call requested");
|
||||
assert_payload_contains(
|
||||
&browser_request_batch,
|
||||
"Browser/PWA share request accepted; waiting for HostEvent",
|
||||
);
|
||||
assert_emit(&browser_request_batch, "hemx:host-call", "browser-share-1");
|
||||
|
||||
let browser_host_result = post(
|
||||
"/",
|
||||
&handle_form_body(record_browser_share, &[("completed", "true")]),
|
||||
);
|
||||
assert_effect_response(&browser_host_result);
|
||||
let browser_result_batch = browser_host_result.effects();
|
||||
assert_payload_contains(
|
||||
&browser_result_batch,
|
||||
"Browser/PWA HostEvent became an app command before UI effects.",
|
||||
);
|
||||
assert_payload_contains(
|
||||
&browser_result_batch,
|
||||
"Browser share completed through app host pipeline",
|
||||
);
|
||||
|
||||
let native_host_request = post("/", &handle_form_body(request_native_haptic, &[]));
|
||||
assert_effect_response(&native_host_request);
|
||||
let native_request_batch = native_host_request.effects();
|
||||
assert_payload_contains(&native_request_batch, "Native host call requested");
|
||||
assert_payload_contains(
|
||||
&native_request_batch,
|
||||
"Native-shell haptic request accepted; waiting for host acknowledgment.",
|
||||
);
|
||||
assert_emit(&native_request_batch, "hemx:host-call", "native-haptic-tap");
|
||||
|
||||
let native_host_result = post("/", &handle_form_body(record_native_haptic_ack, &[]));
|
||||
assert_effect_response(&native_host_result);
|
||||
let native_result_batch = native_host_result.effects();
|
||||
assert_payload_contains(
|
||||
&native_result_batch,
|
||||
"Native-shell HostEvent became an app command before UI effects.",
|
||||
);
|
||||
assert_payload_contains(
|
||||
&native_result_batch,
|
||||
"Native haptic acknowledgment accepted by app code",
|
||||
);
|
||||
|
||||
let delete_missing = post("/", &handle_form_body(delete_work, &[("work_id", "999")]));
|
||||
assert_effect_response(&delete_missing);
|
||||
let delete_missing_batch = delete_missing.effects();
|
||||
|
||||
Reference in New Issue
Block a user