feat(examples): add native-shell workout host path
Extend the workout exemplar with a native-shell haptic request and acknowledgment path that validates the host manifest and returns through app code without owning workout state. req: host/001 req: host/002 req: host/004 req: host/005 req: examples/001
This commit is contained in:
@@ -49,6 +49,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
||||
- Prefer links or pointers to canonical sources over copied lists.
|
||||
- Avoid project trees, architecture maps, generated inventories, current file sizes, issue lists, TODO inventories, and other snapshots that will rot.
|
||||
- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo check --workspace`, `redgate health --strict`. Use the xtask runner for full verification so jobs are capped from local CPU and memory. req: test/004
|
||||
- Run the workout product exemplar with `cargo run --bin hemx-workout-example` and open `http://127.0.0.1:3028`; set `HEMX_WORKOUT_ADDR=127.0.0.1:3030` if the default port is busy. req: examples/001
|
||||
- hemx core stays small: effects, typed ids, registries, and wire schema only.
|
||||
- Routing, auth, sessions, transport, transitions, sync, and storage belong in integration/user crates.
|
||||
- Public examples and beginner APIs should use generated resources and `IntoEffect`, not raw ids or runtime opcodes.
|
||||
|
||||
+1
-1
@@ -795,7 +795,7 @@ what a valid business email is.
|
||||
## examples
|
||||
|
||||
### req: examples/001
|
||||
001 The repository must contain canonical examples that act as API tests. v0 examples are counter, todo CRUD, form wizard, docs-site page swap, auth action, SSE notifications, and keyed todo list; the workout example is the phone-first local-first product exemplar for commands/events/projections plus host export; local-first kanban is a north-star milestone example. The full techdemo may include an opaque leaf-widget island that communicates through `Effect::event`, without moving island mechanics into hemx core.
|
||||
001 The repository must contain canonical examples that act as API tests. v0 examples are counter, todo CRUD, form wizard, docs-site page swap, auth action, SSE notifications, and keyed todo list; the workout example is the phone-first local-first product exemplar for commands/events/projections plus host export and must be runnable with the documented cargo command; local-first kanban is a north-star milestone example. The full techdemo may include an opaque leaf-widget island that communicates through `Effect::event`, without moving island mechanics into hemx core.
|
||||
|
||||
### req: examples/002
|
||||
002 Each example must have a maximum ceremony budget. The counter example must fit in under 50 lines of user-authored Rust plus one template. Todo CRUD must fit in under 150 lines excluding model definitions.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use hemplate::Hemplate;
|
||||
use hemx::{Html, IntoEffect};
|
||||
use hemx_host::{
|
||||
browser_pwa_host_profile, Capability, CapabilityManifest, CapabilityShape, CapabilityUse,
|
||||
HostCall, HostCallId, HostEvent, SharePayload,
|
||||
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
|
||||
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, SharePayload,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -335,7 +335,44 @@ pub fn record_share_result(app: &AppState) -> impl IntoEffect {
|
||||
completed: true,
|
||||
},
|
||||
);
|
||||
effects(state, "Host result accepted by app code")
|
||||
effects(state, "Browser/PWA host result accepted by app code")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn request_native_haptic(app: &AppState) -> impl IntoEffect {
|
||||
app.update(|state| {
|
||||
// req: host/001 req: host/004
|
||||
let manifest = CapabilityManifest::new([CapabilityUse::new(
|
||||
Capability::Haptics,
|
||||
CapabilityShape::Fire,
|
||||
)]);
|
||||
let call = HostCall::Haptic {
|
||||
id: HostCallId::new("workout-set-haptic"),
|
||||
pattern: HapticPattern::Success,
|
||||
};
|
||||
state.host_status = match manifest.validate_call(
|
||||
&native_shell_host_profile("ios-android-webview-workout"),
|
||||
&call,
|
||||
) {
|
||||
Ok(()) => {
|
||||
"Native-shell haptic request accepted; waiting for host acknowledgment.".into()
|
||||
}
|
||||
Err(error) => format!("Native haptic unavailable: {error}"),
|
||||
};
|
||||
effects(state, "Native-shell host call checked against manifest")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn record_native_haptic_ack(app: &AppState) -> impl IntoEffect {
|
||||
app.update(|state| {
|
||||
// req: host/002 req: host/005
|
||||
apply_host_event(
|
||||
state,
|
||||
HostEvent::Acknowledged {
|
||||
id: HostCallId::new("workout-set-haptic"),
|
||||
},
|
||||
);
|
||||
effects(state, "Native-shell host result accepted by app code")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -353,6 +390,10 @@ fn apply_host_event(state: &mut WorkoutState, event: HostEvent) {
|
||||
} => {
|
||||
state.host_status = "Host share cancelled; local event log unchanged.".into();
|
||||
}
|
||||
HostEvent::Acknowledged { id } if id.0 == "workout-set-haptic" => {
|
||||
state.host_status =
|
||||
"Native-shell haptic acknowledged; workout state stayed app-owned.".into();
|
||||
}
|
||||
_ => {
|
||||
state.host_status = "Host event ignored by app policy.".into();
|
||||
}
|
||||
@@ -416,7 +457,7 @@ mod tests {
|
||||
assert!(app.with_workout(|state| state.projection.exported));
|
||||
assert!(contains_payload_text(
|
||||
&shared,
|
||||
"Host result accepted by app code"
|
||||
"Browser/PWA host result accepted by app code"
|
||||
));
|
||||
assert!(contains_payload_text(
|
||||
&shared,
|
||||
@@ -424,13 +465,40 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_shell_haptic_result_returns_through_app_code() {
|
||||
// req: host/001 req: host/002 req: host/005
|
||||
let app = AppState::demo();
|
||||
|
||||
let request = run(|()| request_native_haptic(&app), ());
|
||||
assert!(contains_payload_text(
|
||||
&request,
|
||||
"Native-shell host call checked against manifest"
|
||||
));
|
||||
assert!(contains_payload_text(
|
||||
&request,
|
||||
"waiting for host acknowledgment"
|
||||
));
|
||||
|
||||
let ack = run(|()| record_native_haptic_ack(&app), ());
|
||||
assert!(contains_payload_text(
|
||||
&ack,
|
||||
"Native-shell host result accepted by app code"
|
||||
));
|
||||
assert!(contains_payload_text(
|
||||
&ack,
|
||||
"workout state stayed app-owned"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_page_has_phone_first_controls_without_user_js() {
|
||||
// req: examples/001 req: examples/005
|
||||
let html = render(&WorkoutState::demo()).to_string();
|
||||
assert!(html.contains("Now-first Workout Copilot"));
|
||||
assert!(html.contains("Complete set"));
|
||||
assert!(html.contains("Export via host share"));
|
||||
assert!(html.contains("Export via browser/PWA share"));
|
||||
assert!(html.contains("Request native haptic"));
|
||||
assert!(!html.contains("<script"));
|
||||
assert!(!html.contains("querySelector"));
|
||||
}
|
||||
|
||||
@@ -70,8 +70,16 @@ fn registry(state: AppState) -> HandlerRegistry {
|
||||
let state = state.clone();
|
||||
move |_| workout_app::export_log(&state)
|
||||
})
|
||||
.on(workout::record_share_result, move |_| {
|
||||
workout_app::record_share_result(&state)
|
||||
.on(workout::record_share_result, {
|
||||
let state = state.clone();
|
||||
move |_| workout_app::record_share_result(&state)
|
||||
})
|
||||
.on(workout::request_native_haptic, {
|
||||
let state = state.clone();
|
||||
move |_| workout_app::request_native_haptic(&state)
|
||||
})
|
||||
.on(workout::record_native_haptic_ack, move |_| {
|
||||
workout_app::record_native_haptic_ack(&state)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
<section class="host">
|
||||
<h2>Export host boundary</h2>
|
||||
<p data-hemx-slot="host_status">{+ self.host_status +}</p>
|
||||
<button type="button" data-hemx-handle="export_log">Export via host share</button>
|
||||
<button type="button" data-hemx-handle="export_log">Export via browser/PWA share</button>
|
||||
<button type="button" data-hemx-handle="record_share_result">Record share completed</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 haptic acknowledged</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user