feat(local): model local command log boundary
Define local/offline truth as commands, domain events, and projections rather than stored DOM patches or EffectBatch payloads, and wire the techdemo through a local command-to-projection-to-effect flow. req: local/001 req: local/002 req: local/003 req: local/004 req: examples/001
This commit is contained in:
@@ -19,6 +19,7 @@ use hemx_techdemo::ui;
|
||||
use hemx_techdemo::ui::control_center::{self as control, classes};
|
||||
use hemx_techdemo::ui::{
|
||||
host_panel as host_control, issue_card as card_control, issue_lane as lane_control,
|
||||
local_panel as local_control,
|
||||
};
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::convert::Infallible;
|
||||
@@ -68,6 +69,78 @@ impl Stage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum LocalCommand {
|
||||
CompleteSet { set_id: u64, reps: u8 },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum LocalEvent {
|
||||
SetCompleted { set_id: u64, reps: u8 },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LocalProjection {
|
||||
completed_sets: usize,
|
||||
summary: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LocalJournal {
|
||||
commands: Vec<LocalCommand>,
|
||||
events: Vec<LocalEvent>,
|
||||
projection: LocalProjection,
|
||||
}
|
||||
|
||||
impl Default for LocalJournal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commands: Vec::new(),
|
||||
events: Vec::new(),
|
||||
projection: LocalProjection {
|
||||
completed_sets: 0,
|
||||
summary: "No local commands queued".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalJournal {
|
||||
fn accept(&mut self, command: LocalCommand) {
|
||||
// req: local/001 req: local/004
|
||||
let event = match &command {
|
||||
LocalCommand::CompleteSet { set_id, reps } => LocalEvent::SetCompleted {
|
||||
set_id: *set_id,
|
||||
reps: *reps,
|
||||
},
|
||||
};
|
||||
self.commands.push(command);
|
||||
self.events.push(event.clone());
|
||||
self.project(&event);
|
||||
}
|
||||
|
||||
fn project(&mut self, event: &LocalEvent) {
|
||||
// req: local/001 req: local/004
|
||||
match event {
|
||||
LocalEvent::SetCompleted { set_id, reps } => {
|
||||
self.projection.completed_sets += 1;
|
||||
self.projection.summary = format!(
|
||||
"Projected set {set_id} with {reps} reps from commands/events; no DOM patch or EffectBatch was stored"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self) -> String {
|
||||
format!(
|
||||
"{} commands, {} events, {} projected sets",
|
||||
self.commands.len(),
|
||||
self.events.len(),
|
||||
self.projection.completed_sets
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DemoState {
|
||||
next_id: u64,
|
||||
@@ -76,6 +149,7 @@ struct DemoState {
|
||||
spotlight: String,
|
||||
selected_id: Option<u64>,
|
||||
host_status: String,
|
||||
local_journal: LocalJournal,
|
||||
}
|
||||
|
||||
impl Default for DemoState {
|
||||
@@ -109,6 +183,7 @@ impl Default for DemoState {
|
||||
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(),
|
||||
local_journal: LocalJournal::default(),
|
||||
};
|
||||
state.log("Demo booted from server-rendered HTML");
|
||||
state.log("Runtime attached one delegated listener per root");
|
||||
@@ -162,6 +237,7 @@ struct ControlCenter {
|
||||
inspector: Html,
|
||||
activity: Html,
|
||||
host: Html,
|
||||
local: Html,
|
||||
island_snapshot: String,
|
||||
}
|
||||
|
||||
@@ -210,6 +286,14 @@ struct HostPanel {
|
||||
boundary: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct LocalPanel {
|
||||
status: String,
|
||||
projection: String,
|
||||
boundary: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct ArchitectureActivity;
|
||||
@@ -278,6 +362,7 @@ async fn architecture(request: PageRequest) -> impl IntoResponse {
|
||||
inspector: architecture_inspector(),
|
||||
activity: architecture_activity(),
|
||||
host: render_host_panel(&DemoState::default()),
|
||||
local: render_local_panel(&DemoState::default()),
|
||||
island_snapshot: "2|3|21|architecture route · same opaque island bridge".to_owned(),
|
||||
});
|
||||
request
|
||||
@@ -581,6 +666,20 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
||||
demo_effects(&demo, "Native host result accepted by app code")
|
||||
}
|
||||
})
|
||||
.on(local_control::queue_local_set, {
|
||||
let shared = shared.clone();
|
||||
move |_| {
|
||||
// req: local/001 req: local/004 req: examples/001
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
let set_id = demo.local_journal.commands.len() as u64 + 1;
|
||||
demo.local_journal
|
||||
.accept(LocalCommand::CompleteSet { set_id, reps: 8 });
|
||||
demo.log(format!(
|
||||
"Local command #{set_id} became event and projection before UI effects"
|
||||
));
|
||||
demo_effects(&demo, "Local command accepted · projection rendered")
|
||||
}
|
||||
})
|
||||
.on(control::simulate_push, {
|
||||
let shared = shared.clone();
|
||||
move |_| {
|
||||
@@ -627,6 +726,7 @@ fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
|
||||
control::activity.put(&activity_view(demo)),
|
||||
control::inspector.put(&inspector_view(demo)),
|
||||
control::host_panel.put(&host_panel(demo)),
|
||||
control::local_panel.put(&local_panel(demo)),
|
||||
control::notice.text(notice),
|
||||
control::launch_work_form.clear(),
|
||||
ISLAND_ORBIT.emit(island_snapshot(demo)),
|
||||
@@ -674,6 +774,7 @@ fn page_html(demo: &DemoState) -> Html {
|
||||
inspector: render_inspector(demo),
|
||||
activity: render_activity(demo),
|
||||
host: render_host_panel(demo),
|
||||
local: render_local_panel(demo),
|
||||
island_snapshot: island_snapshot(demo),
|
||||
})
|
||||
}
|
||||
@@ -700,7 +801,7 @@ fn hero_view(demo: &DemoState) -> HeroMetrics {
|
||||
.count();
|
||||
let impact: u64 = demo.work.iter().map(|item| item.impact as u64).sum();
|
||||
HeroMetrics {
|
||||
resource_count: 18,
|
||||
resource_count: 20,
|
||||
active_count: active,
|
||||
shipped_count: shipped,
|
||||
impact_score: impact,
|
||||
@@ -769,6 +870,20 @@ fn render_host_panel(demo: &DemoState) -> Html {
|
||||
ui::render(&host_panel(demo))
|
||||
}
|
||||
|
||||
fn local_panel(demo: &DemoState) -> LocalPanel {
|
||||
// req: local/001 req: local/004
|
||||
LocalPanel {
|
||||
status: demo.local_journal.status(),
|
||||
projection: demo.local_journal.projection.summary.clone(),
|
||||
boundary: "LocalCommand → LocalEvent → Projection → EffectBatch",
|
||||
}
|
||||
}
|
||||
|
||||
fn render_local_panel(demo: &DemoState) -> Html {
|
||||
// req: local/001 req: local/004
|
||||
ui::render(&local_panel(demo))
|
||||
}
|
||||
|
||||
fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
||||
// req: html_safety/002 req: view/001
|
||||
let selected = demo
|
||||
|
||||
Reference in New Issue
Block a user