msg type -> wire::push() -> update handle wire into module/service/ect

This commit is contained in:
2026-09-12 15:04:06 +01:00
parent 539c80cda9
commit 27ec2b90a6
24 changed files with 411 additions and 204 deletions
+62 -66
View File
@@ -1,48 +1,33 @@
//! Clock service: emits a `ServiceEvent` whenever a clock kind's displayed
//! value changes (minute or second rollover).
//! Clock service: publishes a `Wire` on every change of the displayed
//! value, and accepts `ClockKind` wires from modules that want it to switch
//! which kind it publishes.
//!
//! The ticker knows nothing about the app's `Message` type; it produces
//! protocol events from `common` that core routes to subscribed modules.
//! protocol wires from `common` that core routes to subscribers.
use std::collections::HashMap;
use std::sync::Arc;
use iced::{futures::SinkExt, Subscription};
use iced::futures::{channel::mpsc, SinkExt, StreamExt};
use iced::Subscription;
use common::{Service, ServiceEvent};
use common::{ClockKind, ClockPayload, Endpoint, Inbox, Wire};
/// Route-key namespace owned by this service; keys are `"clock.<kind>"`.
pub const NAMESPACE: &str = "clock.";
/// What a clock subscription wants: the tick interval and payload selector.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ClockKind {
Mins,
Seconds,
}
impl ClockKind {
/// Route key a module subscribes under to receive this kind's ticks.
pub fn key(self) -> Service {
Service(match self {
Self::Mins => "clock.mins",
Self::Seconds => "clock.seconds",
})
}
}
/// Clock tick payload: the value plus the kind that produced it.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ClockPayload {
pub kind: ClockKind,
pub value: String,
}
/// Emits a payload only when a kind's displayed value changes (minute or
/// second rollover). Drive `tick` once per second.
#[derive(Default)]
/// Drives the clock, publishing only the kind modules last asked for.
pub struct ClockTicker {
last: HashMap<ClockKind, String>,
want: ClockKind,
}
impl Default for ClockTicker {
fn default() -> Self {
Self {
last: HashMap::new(),
want: ClockKind::Seconds,
}
}
}
impl ClockTicker {
@@ -50,24 +35,41 @@ impl ClockTicker {
Self::default()
}
/// Subscription that emits a `ServiceEvent` on every change.
pub fn run() -> Subscription<ServiceEvent> {
Subscription::run_with((), |_| {
/// Subscription that reads inbound wires from `inbox` and publishes
/// changes to every module subscribed to this service's route key.
pub fn run(inbox: Inbox) -> Subscription<Wire> {
Subscription::run_with(inbox, |inbox| {
let key = inbox.key();
// ponytail: the builder runs once (identity is stable, see `Inbox`
// hashing). If a module ever makes `services()` dynamic and a key
// is dropped then recreated, `take()` returns None and this panics
// — make `Bar` rebuild the inbox per start instead.
let mut rx = inbox.take().expect("inbox receiver is taken once");
iced::stream::channel(
0,
|mut sender: iced::futures::channel::mpsc::Sender<ServiceEvent>| async move {
move |mut sender: mpsc::Sender<Wire>| async move {
let mut clock = ClockTicker::new();
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
loop {
interval.tick().await;
for payload in clock.tick() {
let event = ServiceEvent {
key: payload.kind.key(),
payload: Arc::new(payload),
};
if sender.send(event).await.is_err() {
return;
tokio::select! {
_ = interval.tick() => {
if let Some(payload) = clock.tick() {
let wire = Wire::topic(Endpoint::service(key), key, payload);
if sender.send(wire).await.is_err() {
return;
}
}
}
msg = rx.next() => match msg {
Some(wire) => {
// A module poking us: switch published kind.
if let Some(kind) = wire.downcast::<ClockKind>() {
clock.want = *kind;
clock.last.clear();
}
}
None => return,
},
}
}
},
@@ -75,27 +77,21 @@ impl ClockTicker {
})
}
/// Returns a payload per kind whose value changed since the last tick.
/// The second that rolls into a new minute yields `[Seconds, Mins]`.
pub fn tick(&mut self) -> Vec<ClockPayload> {
let now = chrono::Local::now();
[ClockKind::Seconds, ClockKind::Mins]
.into_iter()
.filter_map(|kind| {
let value = now
.format(match kind {
ClockKind::Mins => "%H:%M",
ClockKind::Seconds => "%H:%M:%S",
})
.to_string();
match self.last.get(&kind) {
Some(prev) if *prev == value => None,
_ => {
self.last.insert(kind, value.clone());
Some(ClockPayload { kind, value })
}
}
/// Latest value for the wanted kind, or `None` if unchanged.
pub fn tick(&mut self) -> Option<ClockPayload> {
let value = chrono::Local::now()
.format(match self.want {
ClockKind::Mins => "%H:%M",
ClockKind::Seconds => "%H:%M:%S",
})
.collect()
.to_string();
if self.last.get(&self.want) == Some(&value) {
return None;
}
self.last.insert(self.want, value.clone());
Some(ClockPayload {
kind: self.want,
value,
})
}
}