use crate::middleware::{EventBus, EventSubscriber, StoreEvent};
use crate::store::{Store, StoreId};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use thiserror::Error;
struct CoordinationRule {
source_store_id: StoreId,
source_mutation: Option<String>,
handler: Arc<dyn Fn(&StoreEvent) + Send + Sync>,
}
struct CoordinationSubscriber {
source_store_id: StoreId,
source_mutation: Option<String>,
handler: Arc<dyn Fn(&StoreEvent) + Send + Sync>,
}
impl EventSubscriber for CoordinationSubscriber {
fn on_event(&self, event: &StoreEvent) {
(self.handler)(event);
}
fn name(&self) -> &'static str {
"StoreCoordinator"
}
fn filter(&self, event: &StoreEvent) -> bool {
match event {
StoreEvent::MutationCompleted {
store_id,
name,
success,
..
} => {
if *store_id != self.source_store_id || !success {
return false;
}
match &self.source_mutation {
Some(expected) => *name == expected.as_str(),
None => true,
}
}
StoreEvent::StateChanged { store_id, .. } => {
*store_id == self.source_store_id && self.source_mutation.is_none()
}
_ => false,
}
}
}
pub struct StoreCoordinator {
rules: Vec<CoordinationRule>,
event_bus: Arc<EventBus>,
dependency_graph: Option<StoreDependencyGraph>,
}
impl Default for StoreCoordinator {
fn default() -> Self {
Self::new()
}
}
impl StoreCoordinator {
pub fn new() -> Self {
Self {
rules: Vec::new(),
event_bus: Arc::new(EventBus::new()),
dependency_graph: None,
}
}
pub fn with_event_bus(event_bus: Arc<EventBus>) -> Self {
Self {
rules: Vec::new(),
event_bus,
dependency_graph: None,
}
}
pub fn on_change<Source: Store, Target: Store>(
&mut self,
source: &Source,
target: &Target,
handler: impl Fn(&Target, &StoreEvent) + Send + Sync + 'static,
) -> &mut Self {
let target = target.clone();
self.rules.push(CoordinationRule {
source_store_id: source.id(),
source_mutation: None,
handler: Arc::new(move |event| {
handler(&target, event);
}),
});
self
}
pub fn on_mutation<Source: Store, Target: Store>(
&mut self,
source: &Source,
mutation_name: &str,
target: &Target,
handler: impl Fn(&Target) + Send + Sync + 'static,
) -> &mut Self {
let target = target.clone();
self.rules.push(CoordinationRule {
source_store_id: source.id(),
source_mutation: Some(mutation_name.to_string()),
handler: Arc::new(move |_event| {
handler(&target);
}),
});
self
}
pub fn activate(&self) {
for rule in &self.rules {
let subscriber = CoordinationSubscriber {
source_store_id: rule.source_store_id,
source_mutation: rule.source_mutation.clone(),
handler: Arc::clone(&rule.handler),
};
self.event_bus.subscribe(subscriber);
}
}
pub fn event_bus(&self) -> &Arc<EventBus> {
&self.event_bus
}
pub fn invalidate_on_change<Source: Store>(
&mut self,
source: &Source,
scope: Option<&'static str>,
) -> &mut Self {
let source_id = source.id();
let bus = Arc::clone(&self.event_bus);
self.rules.push(CoordinationRule {
source_store_id: source_id,
source_mutation: None,
handler: Arc::new(move |_event| {
bus.emit(StoreEvent::CacheInvalidated {
source_store_id: source_id,
scope,
timestamp: coordination_timestamp_ms(),
});
}),
});
self
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
}
fn coordination_timestamp_ms() -> u64 {
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() as u64
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::SystemTime;
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
}
#[derive(Debug, Error, Clone)]
pub enum CoordinationError {
#[error("Circular dependency detected: {0}")]
CircularDependency(String),
#[error("Store not found in graph: {0}")]
StoreNotFound(String),
}
pub struct StoreDependencyGraph {
edges: HashMap<StoreId, Vec<StoreId>>,
names: HashMap<StoreId, &'static str>,
}
impl Default for StoreDependencyGraph {
fn default() -> Self {
Self::new()
}
}
impl StoreDependencyGraph {
pub fn new() -> Self {
Self {
edges: HashMap::new(),
names: HashMap::new(),
}
}
pub fn depends_on<Source: Store, Dep: Store>(
&mut self,
source: &Source,
dependency: &Dep,
) -> &mut Self {
let source_id = source.id();
let dep_id = dependency.id();
self.names.entry(source_id).or_insert_with(|| source.name());
self.names
.entry(dep_id)
.or_insert_with(|| dependency.name());
self.edges.entry(dep_id).or_default();
self.edges.entry(source_id).or_default().push(dep_id);
self
}
pub fn validate(&self) -> Result<(), CoordinationError> {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
let mut path = Vec::new();
for &node in self.edges.keys() {
if !visited.contains(&node) {
self.dfs_cycle_check(node, &mut visited, &mut rec_stack, &mut path)?;
}
}
Ok(())
}
pub fn topological_order(&self) -> Result<Vec<StoreId>, CoordinationError> {
self.validate()?;
let mut reverse_adj: HashMap<StoreId, Vec<StoreId>> = HashMap::new();
for &node in self.edges.keys() {
reverse_adj.entry(node).or_default();
}
for (&source, deps) in &self.edges {
for &dep in deps {
reverse_adj.entry(dep).or_default().push(source);
}
}
let mut in_deg: HashMap<StoreId, usize> = self
.edges
.iter()
.map(|(&id, deps)| (id, deps.len()))
.collect();
let mut queue: VecDeque<StoreId> = in_deg
.iter()
.filter(|(_, deg)| **deg == 0)
.map(|(id, _)| *id)
.collect();
let mut result = Vec::new();
while let Some(node) = queue.pop_front() {
result.push(node);
if let Some(dependents) = reverse_adj.get(&node) {
for &dependent in dependents {
if let Some(deg) = in_deg.get_mut(&dependent) {
*deg -= 1;
if *deg == 0 {
queue.push_back(dependent);
}
}
}
}
}
if result.len() != self.edges.len() {
return Err(CoordinationError::CircularDependency(
"cycle detected during topological sort".to_string(),
));
}
Ok(result)
}
pub fn dependents_of(&self, store_id: StoreId) -> Vec<StoreId> {
self.edges
.iter()
.filter_map(|(&source, deps)| {
if deps.contains(&store_id) {
Some(source)
} else {
None
}
})
.collect()
}
pub fn store_name(&self, store_id: StoreId) -> Option<&'static str> {
self.names.get(&store_id).copied()
}
pub fn len(&self) -> usize {
self.edges.len()
}
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
fn dfs_cycle_check(
&self,
node: StoreId,
visited: &mut HashSet<StoreId>,
rec_stack: &mut HashSet<StoreId>,
path: &mut Vec<StoreId>,
) -> Result<(), CoordinationError> {
visited.insert(node);
rec_stack.insert(node);
path.push(node);
if let Some(deps) = self.edges.get(&node) {
for &dep in deps {
if !visited.contains(&dep) {
self.dfs_cycle_check(dep, visited, rec_stack, path)?;
} else if rec_stack.contains(&dep) {
let cycle_names: Vec<&str> = path
.iter()
.skip_while(|&&id| id != dep)
.filter_map(|id| self.names.get(id).copied())
.collect();
let dep_name = self.names.get(&dep).copied().unwrap_or("unknown");
return Err(CoordinationError::CircularDependency(format!(
"{} → {}",
cycle_names.join(" → "),
dep_name
)));
}
}
}
path.pop();
rec_stack.remove(&node);
Ok(())
}
}
impl std::fmt::Debug for StoreDependencyGraph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoreDependencyGraph")
.field("store_count", &self.edges.len())
.field("names", &self.names.values().collect::<Vec<_>>())
.finish()
}
}
impl StoreCoordinator {
pub fn with_dependency_graph(mut self, graph: StoreDependencyGraph) -> Self {
self.dependency_graph = Some(graph);
self
}
pub fn dependency_graph(&self) -> Option<&StoreDependencyGraph> {
self.dependency_graph.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use leptos::prelude::*;
#[derive(Clone, Debug, Default)]
struct SourceStore {
state: RwSignal<i32>,
}
impl Store for SourceStore {
type State = i32;
fn state(&self) -> ReadSignal<Self::State> {
self.state.read_only()
}
}
#[derive(Clone, Debug, Default)]
struct TargetStore {
state: RwSignal<i32>,
}
impl Store for TargetStore {
type State = i32;
fn state(&self) -> ReadSignal<Self::State> {
self.state.read_only()
}
}
fn with_owner<F: FnOnce()>(f: F) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let owner = Owner::new();
owner.with(f);
});
}
#[test]
fn test_coordinator_rule_count() {
with_owner(|| {
let source = SourceStore::default();
let target = TargetStore::default();
let mut coord = StoreCoordinator::new();
assert_eq!(coord.rule_count(), 0);
coord.on_change(&source, &target, |_t, _e| {});
assert_eq!(coord.rule_count(), 1);
coord.on_mutation(&source, "increment", &target, |_t| {});
assert_eq!(coord.rule_count(), 2);
});
}
#[test]
fn test_coordinator_event_filtering() {
with_owner(|| {
let source = SourceStore::default();
let source_id = source.id();
let subscriber = CoordinationSubscriber {
source_store_id: source_id,
source_mutation: Some("increment".to_string()),
handler: Arc::new(|_| {}),
};
assert!(subscriber.filter(&StoreEvent::MutationCompleted {
store_id: source_id,
name: "increment",
duration_ms: 0,
success: true,
}));
assert!(!subscriber.filter(&StoreEvent::MutationCompleted {
store_id: source_id,
name: "decrement",
duration_ms: 0,
success: true,
}));
assert!(!subscriber.filter(&StoreEvent::MutationCompleted {
store_id: source_id,
name: "increment",
duration_ms: 0,
success: false,
}));
let other_id = StoreId::with_instance::<TargetStore>(999);
assert!(!subscriber.filter(&StoreEvent::MutationCompleted {
store_id: other_id,
name: "increment",
duration_ms: 0,
success: true,
}));
assert!(!subscriber.filter(&StoreEvent::StateChanged {
store_id: source_id,
store_name: "SourceStore",
timestamp: 0,
}));
});
}
#[test]
fn test_coordinator_wildcard_filtering() {
with_owner(|| {
let source = SourceStore::default();
let source_id = source.id();
let subscriber = CoordinationSubscriber {
source_store_id: source_id,
source_mutation: None,
handler: Arc::new(|_| {}),
};
assert!(subscriber.filter(&StoreEvent::MutationCompleted {
store_id: source_id,
name: "increment",
duration_ms: 0,
success: true,
}));
assert!(subscriber.filter(&StoreEvent::MutationCompleted {
store_id: source_id,
name: "decrement",
duration_ms: 0,
success: true,
}));
assert!(!subscriber.filter(&StoreEvent::MutationCompleted {
store_id: source_id,
name: "increment",
duration_ms: 0,
success: false,
}));
assert!(subscriber.filter(&StoreEvent::StateChanged {
store_id: source_id,
store_name: "SourceStore",
timestamp: 0,
}));
let other_id = StoreId::with_instance::<TargetStore>(999);
assert!(!subscriber.filter(&StoreEvent::StateChanged {
store_id: other_id,
store_name: "TargetStore",
timestamp: 0,
}));
assert!(!subscriber.filter(&StoreEvent::MutationStarted {
store_id: source_id,
name: "increment",
timestamp: 0,
}));
});
}
#[test]
fn test_coordinator_activate_registers_subscribers() {
with_owner(|| {
let source = SourceStore::default();
let target = TargetStore::default();
let mut coord = StoreCoordinator::new();
coord.on_change(&source, &target, |_t, _e| {});
coord.on_mutation(&source, "increment", &target, |_t| {});
assert_eq!(coord.event_bus().subscriber_count(), 0);
coord.activate();
assert_eq!(coord.event_bus().subscriber_count(), 2);
});
}
#[test]
fn test_cache_invalidated_not_matched_by_coordination_subscriber() {
with_owner(|| {
let source = SourceStore::default();
let source_id = source.id();
let wildcard_sub = CoordinationSubscriber {
source_store_id: source_id,
source_mutation: None,
handler: Arc::new(|_| {}),
};
assert!(!wildcard_sub.filter(&StoreEvent::CacheInvalidated {
source_store_id: source_id,
scope: None,
timestamp: 0,
}));
let specific_sub = CoordinationSubscriber {
source_store_id: source_id,
source_mutation: Some("increment".to_string()),
handler: Arc::new(|_| {}),
};
assert!(!specific_sub.filter(&StoreEvent::CacheInvalidated {
source_store_id: source_id,
scope: Some("pricing"),
timestamp: 0,
}));
});
}
#[test]
fn test_invalidate_on_change_rule_count() {
with_owner(|| {
let source = SourceStore::default();
let mut coord = StoreCoordinator::new();
coord.invalidate_on_change(&source, Some("pricing"));
assert_eq!(coord.rule_count(), 1);
coord.invalidate_on_change(&source, None);
assert_eq!(coord.rule_count(), 2);
});
}
#[test]
fn test_invalidate_on_change_emits_event() {
use std::sync::atomic::{AtomicU32, Ordering};
with_owner(|| {
let source = SourceStore::default();
let source_id = source.id();
let mut coord = StoreCoordinator::new();
coord.invalidate_on_change(&source, Some("test-scope"));
coord.activate();
let count = Arc::new(AtomicU32::new(0));
let count_clone = count.clone();
struct CountInvalidations {
count: Arc<AtomicU32>,
}
impl EventSubscriber for CountInvalidations {
fn on_event(&self, _event: &StoreEvent) {
self.count.fetch_add(1, Ordering::SeqCst);
}
fn filter(&self, event: &StoreEvent) -> bool {
matches!(event, StoreEvent::CacheInvalidated { .. })
}
}
coord
.event_bus()
.subscribe(CountInvalidations { count: count_clone });
coord.event_bus().emit(StoreEvent::MutationCompleted {
store_id: source_id,
name: "update",
duration_ms: 1,
success: true,
});
assert_eq!(count.load(Ordering::SeqCst), 1);
});
}
#[derive(Clone, Debug, Default)]
struct StoreA {
state: RwSignal<i32>,
}
impl Store for StoreA {
type State = i32;
fn state(&self) -> ReadSignal<Self::State> {
self.state.read_only()
}
}
#[derive(Clone, Debug, Default)]
struct StoreB {
state: RwSignal<i32>,
}
impl Store for StoreB {
type State = i32;
fn state(&self) -> ReadSignal<Self::State> {
self.state.read_only()
}
}
#[derive(Clone, Debug, Default)]
struct StoreC {
state: RwSignal<i32>,
}
impl Store for StoreC {
type State = i32;
fn state(&self) -> ReadSignal<Self::State> {
self.state.read_only()
}
}
#[test]
fn test_dependency_graph_empty() {
let graph = StoreDependencyGraph::new();
assert!(graph.is_empty());
assert_eq!(graph.len(), 0);
assert!(graph.validate().is_ok());
assert!(graph.topological_order().unwrap().is_empty());
}
#[test]
fn test_dependency_graph_linear() {
with_owner(|| {
let a = StoreA::default();
let b = StoreB::default();
let c = StoreC::default();
let mut graph = StoreDependencyGraph::new();
graph.depends_on(&b, &a); graph.depends_on(&c, &b);
assert_eq!(graph.len(), 3);
assert!(graph.validate().is_ok());
let order = graph.topological_order().unwrap();
assert_eq!(order.len(), 3);
let pos_a = order.iter().position(|id| *id == a.id()).unwrap();
let pos_b = order.iter().position(|id| *id == b.id()).unwrap();
let pos_c = order.iter().position(|id| *id == c.id()).unwrap();
assert!(pos_a < pos_b);
assert!(pos_b < pos_c);
});
}
#[test]
fn test_dependency_graph_cycle_detection() {
with_owner(|| {
let a = StoreA::default();
let b = StoreB::default();
let mut graph = StoreDependencyGraph::new();
graph.depends_on(&b, &a); graph.depends_on(&a, &b);
let result = graph.validate();
assert!(result.is_err());
match result {
Err(CoordinationError::CircularDependency(msg)) => {
assert!(msg.contains("→"), "Error should contain cycle path: {msg}");
}
_ => panic!("Expected CircularDependency error"),
}
});
}
#[test]
fn test_dependency_graph_dependents_of() {
with_owner(|| {
let a = StoreA::default();
let b = StoreB::default();
let c = StoreC::default();
let mut graph = StoreDependencyGraph::new();
graph.depends_on(&b, &a); graph.depends_on(&c, &a);
let dependents = graph.dependents_of(a.id());
assert_eq!(dependents.len(), 2);
assert!(dependents.contains(&b.id()));
assert!(dependents.contains(&c.id()));
assert!(graph.dependents_of(b.id()).is_empty());
});
}
#[test]
fn test_dependency_graph_store_name() {
with_owner(|| {
let a = StoreA::default();
let b = StoreB::default();
let mut graph = StoreDependencyGraph::new();
graph.depends_on(&b, &a);
assert!(graph.store_name(a.id()).is_some());
assert!(graph.store_name(b.id()).is_some());
});
}
#[test]
fn test_coordinator_with_dependency_graph() {
with_owner(|| {
let a = StoreA::default();
let b = StoreB::default();
let mut graph = StoreDependencyGraph::new();
graph.depends_on(&b, &a);
let coord = StoreCoordinator::new().with_dependency_graph(graph);
assert!(coord.dependency_graph().is_some());
assert_eq!(coord.dependency_graph().unwrap().len(), 2);
});
}
}