Files
barbar/crates/services
2026-09-13 19:47:01 +01:00
..
2026-09-13 19:47:01 +01:00
2026-09-13 19:47:01 +01:00
2026-09-12 21:50:09 +01:00

services

Background services. A service owns a route key, runs as an iced Subscription, receives common::Wires on its Inbox, and publishes payloads back to every module subscribed to its key. Core maps a Service route key to a subscription through IntoSubscription.

Message types are shared, so they live in the common crate, under common/src/messages/services/. That is what lets a service and the modules that consume it name the same payload without either depending on the other.

Getting started: add a service

  1. Declare the service's route key and payload in crates/common/src/messages/services/greeter.rs:
//! Greeter service messages.

use crate::Service;

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum GreeterKind {
    Hello,
}

impl GreeterKind {
    /// Route key a module subscribes under to receive this kind's ticks.
    pub fn key(self) -> Service {
        Service(match self {
            Self::Hello => "greeter.hello",
        })
    }
}

#[derive(Clone)]
pub struct GreeterPayload {
    pub value: String,
}
  1. Re-export them from crates/common/src/messages/services/mod.rs:
mod datetime;
mod greeter;
mod weather;

pub use datetime::{ClockKind, ClockPayload};
pub use greeter::{GreeterKind, GreeterPayload};
pub use weather::*;
  1. Create crates/services/src/greeter.rs:
use std::time::Duration;

use common::{Endpoint, Inbox, Wire, GreeterPayload};
use iced::{futures::channel::mpsc, Subscription};

/// Route-key namespace owned by this service; keys look like `"greeter.hello"`.
pub const NAMESPACE: &str = "greeter.";

pub struct GreeterService;

impl GreeterService {
    pub fn run(inbox: Inbox) -> Subscription<Wire> {
        Subscription::run_with(inbox, |inbox| {
            let key = inbox.key();
            let mut _rx = inbox.take().expect("inbox receiver is taken once");

            iced::stream::channel(0, move |mut sender: mpsc::Sender<Wire>| async move {
                let mut interval = tokio::time::interval(Duration::from_secs(5));
                loop {
                    interval.tick().await;
                    // Fan-out: publish to every module subscribed to `key`.
                    let wire = Wire::topic(Endpoint::service(key), key, GreeterPayload {
                        value: "hello".into(),
                    });
                    if sender.send(wire).await.is_err() {
                        return;
                    }
                }
            })
        })
    }
}
  1. Add one arm to the IntoSubscription match in crates/services/src/lib.rs:
pub mod greeter;

impl IntoSubscription for Service {
    fn into_subscription(self, inbox: Inbox) -> Subscription<Wire> {
        match self.0 {
            key if key.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox),
            key if key.starts_with(weather::NAMESPACE) => weather::WeatherService::run(inbox),
            key if key.starts_with(greeter::NAMESPACE) => greeter::GreeterService::run(inbox),
            _ => Subscription::none(),
        }
    }
}

Then a module subscribes by returning GreeterKind::Hello.key() from its services() and downcasts GreeterPayload in update().

run_with(inbox, ..) keys the subscription by the inbox route key, so the same subscription returned on every rebuild is not restarted. The receiver is taken once inside the closure — don't call take() anywhere else.