arch: split ConfigRepository, extract polling, consolidate conversions, decouple protocol

- Value↔JSON: From impls on domain Value behind `json` feature, delete 4 duplicate converters
- ConfigRepository split into ConfigRepository (12), UserRepository (3), WidgetStateCache (2)
- polling orchestration moved from bootstrap to application::polling_service
- WidgetRenderer in client-domain owns scroll/cache, both clients use it
- network loop consolidated into client-application::run_connection_loop
- protocol crate drops domain dep, Wire↔Domain conversions move to adapters
This commit is contained in:
2026-06-19 18:12:50 +02:00
parent 1c854d127f
commit 7001b5e911
46 changed files with 1063 additions and 951 deletions

View File

@@ -1,5 +1,5 @@
use crate::conversions::wire_to_layout;
use client_domain::{BoundingBox, Color, LayoutEngine, RenderTree, ThemeConfig};
use domain::LayoutNode;
use protocol::{
ServerMessage, WidgetDescriptor, WireColor, WireDisplayHint, WireLayoutNode, WireWidgetState,
};
@@ -73,7 +73,7 @@ impl ClientApp {
wire_layout: WireLayoutNode,
widgets: Vec<WidgetDescriptor>,
) -> Vec<RepaintCommand> {
let layout: LayoutNode = wire_layout.into();
let layout = wire_to_layout(wire_layout);
let new_tree = LayoutEngine::compute(&layout, self.screen);
self.widget_states.clear();

View File

@@ -0,0 +1,42 @@
use client_domain::NetworkPort;
use protocol::{ServerMessage, decode_server_message};
use std::thread;
use std::time::Duration;
pub fn run_connection_loop<N: NetworkPort>(
net: &mut N,
server_addr: &str,
poll_interval: Duration,
reconnect_delay: Duration,
mut on_message: impl FnMut(ServerMessage),
mut on_connection_change: impl FnMut(bool),
) {
loop {
if !net.is_connected() {
match net.connect(server_addr) {
Ok(()) => on_connection_change(true),
Err(_) => {
on_connection_change(false);
thread::sleep(reconnect_delay);
continue;
}
}
}
match net.receive() {
Ok(Some(payload)) => {
if let Ok(msg) = decode_server_message(&payload) {
on_message(msg);
}
}
Ok(None) => {
thread::sleep(poll_interval);
}
Err(_) => {
let _ = net.disconnect();
on_connection_change(false);
thread::sleep(reconnect_delay);
}
}
}
}

View File

@@ -0,0 +1,100 @@
use domain::value_objects::{
AlignItems, ContainerNode, Direction, DisplayHint, DisplayHintKind, HAlign, JustifyContent,
LayoutChild, LayoutNode, Sizing, VAlign, Value, WidgetError, WidgetState,
};
use protocol::{
WireAlignItems, WireDirection, WireDisplayHint, WireDisplayHintKind, WireHAlign,
WireJustifyContent, WireLayoutNode, WireSizing, WireVAlign, WireValue, WireWidgetError,
WireWidgetState,
};
pub fn wire_to_value(w: WireValue) -> Value {
match w {
WireValue::Null => Value::Null,
WireValue::Bool(b) => Value::Bool(b),
WireValue::Number(n) => Value::Number(n),
WireValue::String(s) => Value::String(s),
WireValue::Array(arr) => Value::Array(arr.into_iter().map(wire_to_value).collect()),
WireValue::Object(map) => Value::Object(
map.into_iter()
.map(|(k, v)| (k, wire_to_value(v)))
.collect(),
),
}
}
pub fn wire_to_widget_error(w: WireWidgetError) -> WidgetError {
match w {
WireWidgetError::SourceUnavailable => WidgetError::SourceUnavailable,
WireWidgetError::ExtractionFailed => WidgetError::ExtractionFailed,
}
}
pub fn wire_to_widget_state(w: WireWidgetState) -> WidgetState {
WidgetState {
data: w
.data
.into_iter()
.map(|kv| (kv.key, wire_to_value(kv.value)))
.collect(),
error: w.error.map(wire_to_widget_error),
}
}
pub fn wire_to_display_hint(w: WireDisplayHint) -> DisplayHint {
DisplayHint {
kind: match w.kind {
WireDisplayHintKind::IconValue => DisplayHintKind::IconValue,
WireDisplayHintKind::TextBlock => DisplayHintKind::TextBlock,
WireDisplayHintKind::KeyValue => DisplayHintKind::KeyValue,
},
h_align: match w.h_align {
WireHAlign::Left => HAlign::Left,
WireHAlign::Center => HAlign::Center,
WireHAlign::Right => HAlign::Right,
},
v_align: match w.v_align {
WireVAlign::Top => VAlign::Top,
WireVAlign::Middle => VAlign::Middle,
WireVAlign::Bottom => VAlign::Bottom,
},
}
}
pub fn wire_to_layout(w: WireLayoutNode) -> LayoutNode {
match w {
WireLayoutNode::Leaf(id) => LayoutNode::Leaf(id),
WireLayoutNode::Container(c) => LayoutNode::Container(ContainerNode {
direction: match c.direction {
WireDirection::Row => Direction::Row,
WireDirection::Column => Direction::Column,
},
gap: c.gap,
padding: c.padding,
justify_content: match c.justify_content {
WireJustifyContent::Start => JustifyContent::Start,
WireJustifyContent::Center => JustifyContent::Center,
WireJustifyContent::End => JustifyContent::End,
WireJustifyContent::SpaceBetween => JustifyContent::SpaceBetween,
WireJustifyContent::SpaceEvenly => JustifyContent::SpaceEvenly,
},
align_items: match c.align_items {
WireAlignItems::Start => AlignItems::Start,
WireAlignItems::Center => AlignItems::Center,
WireAlignItems::End => AlignItems::End,
WireAlignItems::Stretch => AlignItems::Stretch,
},
children: c
.children
.into_iter()
.map(|ch| LayoutChild {
sizing: match ch.sizing {
WireSizing::Fixed(px) => Sizing::Fixed(px),
WireSizing::Flex(weight) => Sizing::Flex(weight),
},
node: wire_to_layout(ch.node),
})
.collect(),
}),
}
}

View File

@@ -1,3 +1,6 @@
mod client_app;
mod connection_loop;
pub mod conversions;
pub use client_app::{ClientApp, RepaintCommand};
pub use connection_loop::run_connection_loop;

View File

@@ -0,0 +1,151 @@
use client_application::conversions::{
wire_to_display_hint, wire_to_layout, wire_to_value, wire_to_widget_state,
};
use domain::{
AlignItems, ContainerNode, Direction, DisplayHint, DisplayHintKind, JustifyContent,
LayoutChild, LayoutNode, Sizing, Value, WidgetError, WidgetState,
};
use protocol::{
WireContainerNode, WireDirection, WireDisplayHint, WireKeyValue, WireLayoutChild,
WireLayoutNode, WireSizing, WireValue, WireWidgetError, WireWidgetState,
};
use std::collections::BTreeMap;
fn value_to_wire(v: &Value) -> WireValue {
match v {
Value::Null => WireValue::Null,
Value::Bool(b) => WireValue::Bool(*b),
Value::Number(n) => WireValue::Number(*n),
Value::String(s) => WireValue::String(s.clone()),
Value::Array(arr) => WireValue::Array(arr.iter().map(value_to_wire).collect()),
Value::Object(map) => WireValue::Object(
map.iter()
.map(|(k, v)| (k.clone(), value_to_wire(v)))
.collect(),
),
}
}
#[test]
fn value_converts_to_wire_and_back() {
let original = Value::Object(BTreeMap::from([(
"items".into(),
Value::Array(vec![
Value::String("hello".into()),
Value::Number(42.0),
Value::Bool(true),
Value::Null,
]),
)]));
let wire = value_to_wire(&original);
let roundtripped = wire_to_value(wire);
assert_eq!(original, roundtripped);
}
#[test]
fn widget_state_with_error_converts_to_wire_and_back() {
let original = WidgetState {
data: BTreeMap::from([("temp".into(), Value::Number(5.4))]),
error: Some(WidgetError::SourceUnavailable),
};
let wire = WireWidgetState {
data: original
.data
.iter()
.map(|(k, v)| WireKeyValue {
key: k.clone(),
value: value_to_wire(v),
})
.collect(),
error: Some(WireWidgetError::SourceUnavailable),
};
let roundtripped = wire_to_widget_state(wire);
assert_eq!(original, roundtripped);
}
#[test]
fn layout_tree_converts_to_wire_and_back() {
let original = LayoutNode::Container(ContainerNode {
direction: Direction::Row,
gap: 4,
padding: 2,
justify_content: JustifyContent::Start,
align_items: AlignItems::Stretch,
children: vec![
LayoutChild {
sizing: Sizing::Flex(1),
node: LayoutNode::Leaf(1),
},
LayoutChild {
sizing: Sizing::Fixed(100),
node: LayoutNode::Container(ContainerNode {
direction: Direction::Column,
gap: 2,
padding: 0,
justify_content: JustifyContent::Start,
align_items: AlignItems::Stretch,
children: vec![LayoutChild {
sizing: Sizing::Flex(1),
node: LayoutNode::Leaf(2),
}],
}),
},
],
});
let wire = WireLayoutNode::Container(WireContainerNode {
direction: WireDirection::Row,
gap: 4,
padding: 2,
justify_content: protocol::WireJustifyContent::Start,
align_items: protocol::WireAlignItems::Stretch,
children: vec![
WireLayoutChild {
sizing: WireSizing::Flex(1),
node: WireLayoutNode::Leaf(1),
},
WireLayoutChild {
sizing: WireSizing::Fixed(100),
node: WireLayoutNode::Container(WireContainerNode {
direction: WireDirection::Column,
gap: 2,
padding: 0,
justify_content: protocol::WireJustifyContent::Start,
align_items: protocol::WireAlignItems::Stretch,
children: vec![WireLayoutChild {
sizing: WireSizing::Flex(1),
node: WireLayoutNode::Leaf(2),
}],
}),
},
],
});
let roundtripped = wire_to_layout(wire);
assert_eq!(original, roundtripped);
}
#[test]
fn display_hint_converts_to_wire_and_back() {
for (hint, wire_kind) in [
(
DisplayHintKind::IconValue,
protocol::WireDisplayHintKind::IconValue,
),
(
DisplayHintKind::TextBlock,
protocol::WireDisplayHintKind::TextBlock,
),
(
DisplayHintKind::KeyValue,
protocol::WireDisplayHintKind::KeyValue,
),
] {
let original = DisplayHint::new(hint);
let wire = WireDisplayHint::new(wire_kind);
let roundtripped = wire_to_display_hint(wire);
assert_eq!(original, roundtripped);
}
}