diff --git a/Cargo.toml b/Cargo.toml index 9c68904..de65336 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "examples/weather-mesh-demo/weather-station-alpha", "examples/weather-mesh-demo/weather-station-beta", "examples/weather-mesh-demo/weather-station-gamma", + "examples/hello-mailbox", ] exclude = ["_external"] resolver = "2" diff --git a/Makefile b/Makefile index bedf6f5..22d6f75 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,7 @@ test: fmt: @printf "$(GREEN)Formatting code (workspace members only)...$(NC)\n" - @for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta; do \ + @for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox; do \ printf "$(YELLOW) → Formatting $$pkg$(NC)\n"; \ cargo fmt -p $$pkg 2>/dev/null || true; \ done @@ -128,7 +128,7 @@ fmt: fmt-check: @printf "$(GREEN)Checking code formatting (workspace members only)...$(NC)\n" @FAILED=0; \ - for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta; do \ + for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox; do \ printf "$(YELLOW) → Checking $$pkg$(NC)\n"; \ if ! cargo fmt -p $$pkg -- --check 2>&1; then \ printf "$(RED)❌ Formatting check failed for $$pkg$(NC)\n"; \ @@ -275,6 +275,8 @@ examples: cargo build --package weather-station-beta @printf "$(YELLOW) → Building weather-station-gamma (embedded, embassy runtime)$(NC)\n" cargo build --package weather-station-gamma --target thumbv7em-none-eabihf + @printf "$(YELLOW) → Building hello-mailbox (sync)$(NC)\n" + cargo build --package hello-mailbox @printf "$(GREEN)All examples built successfully!$(NC)\n" ## Security & Quality commands @@ -414,7 +416,7 @@ check: fmt-check clippy test test-embedded test-wasm deny @printf "$(BLUE)✓ Embedded target compatibility verified$(NC)\n" @printf "$(BLUE)✓ WASM target compatibility verified$(NC)\n" @printf "$(BLUE)✓ Dependencies verified (deny)$(NC)\n" - + ## WASM commands wasm: @printf "$(GREEN)Building WASM adapter with wasm-pack...$(NC)\n" diff --git a/README.md b/README.md index 18dde4d..4ee5215 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ docker compose up | --- | --- | --- | | **SPMC Ring** | Bounded stream with independent consumers | Sensor telemetry, event logs | | **SingleLatest** | Only the current value matters | Feature flags, config, UI state | -| **Mailbox** | Latest instruction wins | Device commands, actuation, RPC | +| [**Mailbox**](https://github.com/aimdb-dev/aimdb/tree/main/examples/hello-mailbox) | Latest instruction wins | Device commands, actuation, RPC | - **Capabilities are unlocked by traits.** Implement `Streamable` to cross WASM/WebSocket/CLI boundaries, `Migratable` for typed schema evolution, `Observable` for monitoring, `Linkable` for wire-format connectors. Without the trait, the type system says no. - **Connectors that ship today:** MQTT, KNX, WebSocket. Writing your own is one trait impl. diff --git a/examples/hello-mailbox/Cargo.toml b/examples/hello-mailbox/Cargo.toml new file mode 100644 index 0000000..25e95e7 --- /dev/null +++ b/examples/hello-mailbox/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "hello-mailbox" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "AimDB minimal example demonstrating Mailbox buffer (latest-wins) semantics" +publish = false + +[dependencies] +aimdb-core = { path = "../../aimdb-core", features = ["std"] } +aimdb-tokio-adapter = { path = "../../aimdb-tokio-adapter", features = ["tokio-runtime"] } +aimdb-sync = { path = "../../aimdb-sync" } +tokio = { workspace = true, features = ["full"] } diff --git a/examples/hello-mailbox/README.md b/examples/hello-mailbox/README.md new file mode 100644 index 0000000..12f1f32 --- /dev/null +++ b/examples/hello-mailbox/README.md @@ -0,0 +1,35 @@ +# hello-mailbox: Mailbox buffer demo +The Mailbox buffer keeps only the last message written. It's like a real mailbox where only the most recent letter is visible. + +## When to use +This Mailbox buffer demo is useful in a variety of scenarios where you want to retain only the last message, for example: +- When you have a robotic arm that needs to execute a sequence of commands, and you only want to execute the last command, or even if the robot gets too much commands in a short period of time. + +## How it works +The Mailbox buffer demo works by simulating a mailbox buffer that retains only the last message. +it runs 2 rounds: + - the first round sends 3 Colors and the MailBox only gets the last message. + - the second round sends 2 Colors and the MailBox only gets the last message. + +## How to run +From the workspace root, run: +``` +cargo run -p hello-mailbox +``` +**Expected output** +``` +=== hello-mailbox: Mailbox buffer demo === + + Round 1 +1. Firing three rapid commands BEFORE consumer exists: Red → Green → Blue +2. Consumer created AFTER the burst — reads once: + ✓ Got: Blue ← only the latest survived + (Red and Green were overwritten before anyone could read them) + + Round 2 +1. Firing two rapid commands BEFORE consumer exists: Red → Green +2. Consumer created AFTER the burst — reads once: + ✓ Got: Green ← only the latest survived (Green) +3. Shutting down... + ✓ Done. +``` diff --git a/examples/hello-mailbox/src/main.rs b/examples/hello-mailbox/src/main.rs new file mode 100644 index 0000000..e4d62ae --- /dev/null +++ b/examples/hello-mailbox/src/main.rs @@ -0,0 +1,86 @@ +use aimdb_core::{buffer::BufferCfg, AimDbBuilder}; +use aimdb_sync::AimDbBuilderSyncExt; +use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +// Enum of colors for the LED +#[derive(Debug, Clone, PartialEq, Eq)] +enum Color { + Red, + Green, + Blue, +} + +// Struct representing the LED state +#[derive(Debug, Clone, PartialEq, Eq)] +struct Led { + color: Color, +} + +// Main function +fn main() -> Result<(), Box> { + println!("=== hello-mailbox: Mailbox buffer demo ===\n"); + + // configuration + let adapter = Arc::new(TokioAdapter); + let mut builder = AimDbBuilder::new().runtime(adapter); + + builder.configure::("actuator.led", |reg| { + reg.buffer(BufferCfg::Mailbox); + }); + + let handle = builder.attach()?; + let producer = handle.producer::("actuator.led")?; + + { + // Produce quickly BEFORE creating the consumer + println!(" Round 1 "); + println!("1. Firing three rapid commands BEFORE consumer exists: Red → Green → Blue"); + producer.set(Led { color: Color::Red })?; + producer.set(Led { + color: Color::Green, + })?; + producer.set(Led { color: Color::Blue })?; + thread::sleep(Duration::from_millis(100)); + + // Now we create the consumer — it will only see the last value in the Mailbox + println!("2. Consumer created AFTER the burst — reads once:"); + let consumer = handle.consumer::("actuator.led")?; + thread::sleep(Duration::from_millis(100)); + + match consumer.try_get() { + Ok(msg) => println!(" ✓ Got: {:?} ← only the latest survived", msg.color), + Err(_) => println!(" (mailbox was already empty)"), + } + + println!(" (Red and Green were overwritten before anyone could read them)\n"); + } + + println!(" Round 2 "); + println!("1. Firing two rapid commands BEFORE consumer exists: Red → Green"); + // Create the color burst again + producer.set(Led { color: Color::Red })?; + producer.set(Led { + color: Color::Green, + })?; + thread::sleep(Duration::from_millis(100)); + + println!("2. Consumer created AFTER the burst — reads once:"); + let consumer2 = handle.consumer::("actuator.led")?; + thread::sleep(Duration::from_millis(100)); + + match consumer2.try_get() { + Ok(msg) => println!( + " ✓ Got: {:?} ← only the latest survived (Green)", + msg.color + ), + Err(_) => println!(" (mailbox was already empty)"), + } + + println!("3. Shutting down..."); + handle.detach()?; + println!(" ✓ Done."); + Ok(()) +}