//! The wake-routing substrate: in-process channels carrying wakes at //! the commit boundary (engine-mem.md §Architecture). //! //! There is no batching boundary — wakes deliver at the commit //! boundary directly to each open receiver, per-subscriber FIFO by //! commit order; coalescing is not applicable. The substrate realizes //! the shape structurally: //! //! - **Commit-ordered source**: a mechanism (notify, stream publishes, //! the tx commit-merge's staged notifies and publishes) fires its //! commit's wakes //! through [`WakeBus::fire_commit_wakes`], under one guard: the //! commit sequence bumps and the per-channel fanout completes as a //! single operation, so every subscriber's receipt order is exactly //! commit order. Two concurrent commits serialize on the guard; a //! subscriber's wake stream is FIFO by commit order. //! - **Per-channel attach**: [`WakeBus::attach`] registers under the //! same guard and returns an unbounded [`tokio::sync::mpsc`] feed — //! attach never blocks, and a receiver attached after a commit never //! sees it (no replay; the attach either happens before that commit's //! fanout or after it — the guard admits no mid-fanout attach). //! - **Subscriber registry**: unbounded senders live only in the bus, //! so the sole close path is the engine's drop. [`WakeBus::close`] //! empties the registry under the same guard — every live feed's //! sender drops and each receiver drains to `Disconnected`; the //! consumer-visible `recv() -> None` terminal arm arrives with //! `mem-engine-notify-listen`'s [`alkstore::WakeReceiver`] bridge //! over this substrate. A dropped feed detaches itself //! ([`WakeFeed`]'s `Drop`). //! //! The `Wake` value crosses exactly as the contract pins it — channel //! only, never payloads or ids (ADR-008 §3). The fanout is per-notify: //! one commit's notify on a channel is one wake per subscribed feed, //! never coalesced (no batching boundary exists to coalesce across). // The substrate routes every wave-6 mechanism's commit-boundary // wakes; until the mechanism tasks wire it, the foundations-only // build sees the attach/fire surface as dead. #![allow(dead_code)] use std::sync::Arc; use std::sync::Mutex; use alkstore::Wake; use crate::state::Guard; /// A subscriber's registry row: its id (the detach key), the channel /// it attached on (exact-match deliverance), and its feed's sender. /// Senders live only here — a subscriber holds no bus reference of its /// own beyond its [`WakeFeed`] (which carries the bus Arc for /// detach-on-drop, never for delivery), so the engine's close empties /// the senders and the feeds disconnect regardless of who still holds /// a feed. #[derive(Debug)] struct Subscription { id: u64, channel: String, tx: tokio::sync::mpsc::UnboundedSender, } /// The internal guarded registry: next-subscriber id, next commit /// sequence, and the live subscriptions in first-attached order. #[derive(Debug, Default)] struct Registry { next_subscriber_id: u64, next_commit_seq: u64, subscriptions: Vec, } /// The in-process wake bus: the commit-ordered wake source's entry /// point ([`WakeBus::fire_commit_wakes`]) and the per-channel receiver /// attach ([`WakeBus::attach`]) over one guarded registry. pub(crate) struct WakeBus { registry: Mutex, closed: std::sync::atomic::AtomicBool, } impl Default for WakeBus { fn default() -> Self { Self::new() } } impl std::fmt::Debug for WakeBus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WakeBus").finish_non_exhaustive() } } impl WakeBus { #[allow(dead_code)] pub(crate) fn new() -> Self { Self { registry: Mutex::new(Registry::default()), closed: std::sync::atomic::AtomicBool::new(false), } } /// Attach a feed for `channel` — starts from "now": the attach /// lands under the registry guard, so commits that fired before it /// are never replayed into it. Returns the feed plus its detach /// handle (drop the [`WakeFeed`] to detach). pub(crate) fn attach(self: &Arc, channel: &str) -> WakeFeed { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let mut registry = self.registry.guard(); if self.closed.load(std::sync::atomic::Ordering::Acquire) { return WakeFeed { id: 0, bus: Arc::clone(self), rx: dead_feed_receiver(), }; } registry.next_subscriber_id += 1; let id = registry.next_subscriber_id; registry.subscriptions.push(Subscription { id, channel: channel.to_string(), tx, }); WakeFeed { id, bus: Arc::clone(self), rx, } } /// Fire the commit boundary's wakes — the mechanism's wake source: /// under one guard, the commit sequence bumps once and every feed /// attached on a fired channel receives one [`Wake`] per commit-notify, /// in commit order. A channel with no subscribers delivers nothing. /// A closed bus is inert. pub(crate) fn fire_commit_wakes(&self, channels: impl IntoIterator) { if self.closed.load(std::sync::atomic::Ordering::Acquire) { return; } let mut registry = self.registry.guard(); registry.next_commit_seq += 1; let fired: Vec = channels.into_iter().collect(); for channel in &fired { for subscription in ®istry.subscriptions { if subscription.channel == *channel { let wake = Wake::new(channel.clone()); let _ = subscription.tx.send(wake); } } } } /// The commit watermark: how many commit boundaries have fired /// through this bus. The substrate's ordering observable — the /// consumer contract never surfaces it (wakes carry the channel /// only); unit tests pin FIFO-by-commit-order against it. #[cfg(test)] pub(crate) fn commit_watermark(&self) -> u64 { self.registry.guard().next_commit_seq } /// Close the bus — the engine's drop path. Empties the registry /// under one guard: every feed's sender drops, receivers drain to /// disconnected, and attach/fire after it are no-ops. Idempotent. pub(crate) fn close(&self) { self.closed .store(true, std::sync::atomic::Ordering::Release); let mut registry = self.registry.guard(); registry.subscriptions.clear(); } fn detach(&self, id: u64) { let mut registry = self.registry.guard(); registry.subscriptions.retain(|sub| sub.id != id); } } /// A feed whose sender never existed — the attach-on-closed shape (the /// engine is gone; the feed reads as closed immediately, the same /// terminal arm an engine drop produces). fn dead_feed_receiver() -> tokio::sync::mpsc::UnboundedReceiver { let (_tx, rx) = tokio::sync::mpsc::unbounded_channel::(); rx } /// A receiver's handle into the substrate: the raw unbounded feed of /// [`Wake`] values (per-subscriber FIFO by commit order — see the /// module docs) plus its own-detach-on-drop. This is the substrate /// primitive; the consumer-visible `WakeReceiver` bridge (the /// `recv`/`try_recv`/`recv_timeout` forms and the terminal `None` /// arm) is `mem-engine-notify-listen`'s surface over it. pub(crate) struct WakeFeed { id: u64, bus: Arc, rx: tokio::sync::mpsc::UnboundedReceiver, } impl std::fmt::Debug for WakeFeed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WakeFeed") .field("id", &self.id) .finish_non_exhaustive() } } impl WakeFeed { pub(crate) fn rx(&mut self) -> &mut tokio::sync::mpsc::UnboundedReceiver { &mut self.rx } } impl Drop for WakeFeed { fn drop(&mut self) { if self.id != 0 { self.bus.detach(self.id); } } } #[cfg(test)] mod tests { use super::*; use tokio::sync::mpsc::error::TryRecvError; fn delivered(feed: &mut WakeFeed) -> Vec { let mut wakes = Vec::new(); loop { match feed.rx().try_recv() { Ok(wake) => wakes.push(wake), Err(TryRecvError::Empty) => break, Err(TryRecvError::Disconnected) => { panic!("the feed disconnected unexpectedly") } } } wakes } #[test] fn delivery_is_channel_scoped_and_never_coalesced() { let bus = Arc::new(WakeBus::new()); let mut a = bus.attach("orders"); let mut b = bus.attach("orders"); let mut foreign = bus.attach("payments"); bus.fire_commit_wakes(vec!["orders".into(), "orders".into(), "payments".into()]); let a_wakes = delivered(&mut a); let b_wakes = delivered(&mut b); let foreign_wakes = delivered(&mut foreign); assert_eq!( a_wakes, b_wakes, "subscribers on the same channel receive the same wakes" ); assert!( a_wakes.iter().all(|w| w.channel == "orders"), "each wake carries its channel" ); assert_eq!( foreign_wakes[0].channel, "payments", "a foreign-channel subscriber receives only its own channel" ); assert_eq!( a_wakes.len(), 2, "per-notify delivery — one wake per commit, never coalesced" ); } #[test] fn per_subscriber_fifo_by_commit_order() { let bus = Arc::new(WakeBus::new()); let mut feed = bus.attach("orders"); for seq in 0..8 { assert_eq!( bus.commit_watermark(), seq, "the watermark advances one per fired commit boundary" ); bus.fire_commit_wakes(vec![format!("orders-{seq}"), "orders".into()]); } assert_eq!(bus.commit_watermark(), 8); let wakes = delivered(&mut feed); assert_eq!( wakes, vec![Wake::new("orders".into()); 8], "each commit's wake delivered, in commit order" ); } #[test] fn attach_after_commit_sees_nothing_before_it() { let bus = Arc::new(WakeBus::new()); bus.fire_commit_wakes(vec!["orders".into()]); let mut late = bus.attach("orders"); assert!( late.rx().try_recv().is_err(), "a listener attached after a commit never sees it (no replay)" ); bus.fire_commit_wakes(vec!["orders".into()]); assert_eq!( delivered(&mut late), vec![Wake::new("orders".into())], "post-attach commits deliver" ); } #[tokio::test(flavor = "current_thread")] async fn close_disconnects_every_live_feed() { let bus = Arc::new(WakeBus::new()); let mut a = bus.attach("orders"); let mut b = bus.attach("payments"); bus.close(); bus.fire_commit_wakes(vec!["orders".into()]); let mut late = bus.attach("orders"); for feed in [&mut a, &mut b, &mut late] { assert!( feed.rx().recv().await.is_none(), "engine close is the terminal None arm for every open receiver" ); } assert_eq!(bus.commit_watermark(), 0, "a fire on a closed bus is inert"); } #[test] fn dropped_feed_detaches() { let bus = Arc::new(WakeBus::new()); let feed = bus.attach("orders"); let mut kept = bus.attach("orders"); std::mem::drop(feed); bus.fire_commit_wakes(vec!["orders".into()]); let wakes = delivered(&mut kept); assert_eq!( wakes.len(), 1, "the detached feed received nothing, the live one the wake" ); } }