mirror of
https://github.com/ruvnet/RuView
synced 2026-08-08 20:11:43 +00:00
feat(homecore): complete migration and startup restore
This commit is contained in:
@@ -47,6 +47,9 @@ pub struct Context {
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Marker stored on snapshots loaded from durable state at startup.
|
||||
pub const RESTORE_USER_ID: &'static str = "homecore.restore";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -66,6 +69,20 @@ impl Context {
|
||||
parent_id: Some(parent.id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a fresh context that identifies a startup restoration. The
|
||||
/// persisted context, when valid, is retained as the causal parent.
|
||||
pub fn restoration(parent_id: Option<Uuid>) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
user_id: Some(Self::RESTORE_USER_ID.to_owned()),
|
||||
parent_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_restoration(&self) -> bool {
|
||||
self.user_id.as_deref() == Some(Self::RESTORE_USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bus::EventBus;
|
||||
use crate::registry::EntityRegistry;
|
||||
use crate::registry::{DeviceRegistry, EntityRegistry};
|
||||
use crate::service::ServiceRegistry;
|
||||
use crate::state::StateMachine;
|
||||
|
||||
@@ -20,6 +20,7 @@ struct HomeCoreInner {
|
||||
pub states: StateMachine,
|
||||
pub services: ServiceRegistry,
|
||||
pub entities: EntityRegistry,
|
||||
pub devices: DeviceRegistry,
|
||||
}
|
||||
|
||||
impl HomeCore {
|
||||
@@ -31,6 +32,7 @@ impl HomeCore {
|
||||
services: ServiceRegistry::with_event_bus(bus.clone()),
|
||||
bus,
|
||||
entities: EntityRegistry::new(),
|
||||
devices: DeviceRegistry::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -50,6 +52,10 @@ impl HomeCore {
|
||||
pub fn entities(&self) -> &EntityRegistry {
|
||||
&self.inner.entities
|
||||
}
|
||||
|
||||
pub fn devices(&self) -> &DeviceRegistry {
|
||||
&self.inner.devices
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HomeCore {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! - [`state`] — `StateMachine`: DashMap-backed concurrent state store
|
||||
//! - [`bus`] — `EventBus`: tokio broadcast wiring for system + domain events
|
||||
//! - [`service`] — `ServiceRegistry` (stub; full mpsc dispatch lands in P2)
|
||||
//! - [`registry`] — `EntityRegistry` (in-memory P1; persistence lands in P2)
|
||||
//! - [`registry`] — in-memory entity and device registries, restored by the server
|
||||
//! - [`homecore`] — `HomeCore` runtime coordinator: holds bus + states + services
|
||||
//!
|
||||
//! ## Threading model
|
||||
@@ -23,31 +23,30 @@
|
||||
//!
|
||||
//! ## What's NOT here yet (deferred to P2+)
|
||||
//!
|
||||
//! - Persistence of entity registry to `.homecore/storage/core.entity_registry`
|
||||
//! - Automatic persistence of registry mutations (startup restoration exists)
|
||||
//! - Schema validation (`schemas` module from §3 stub)
|
||||
//! - Service handler mpsc dispatch (`service::ServiceRegistry::call`)
|
||||
//! - Device registry (mirror of HA's `core.device_registry`)
|
||||
//! - Witness chain integration (ADR-028)
|
||||
//!
|
||||
//! Each is marked `// TODO P2:` at the relevant call site.
|
||||
|
||||
pub mod bus;
|
||||
pub mod entity;
|
||||
pub mod event;
|
||||
pub mod state;
|
||||
pub mod bus;
|
||||
pub mod service;
|
||||
pub mod registry;
|
||||
pub mod service;
|
||||
pub mod state;
|
||||
|
||||
mod homecore;
|
||||
|
||||
pub use homecore::HomeCore;
|
||||
|
||||
pub use bus::EventBus;
|
||||
pub use entity::{EntityId, EntityIdError, State};
|
||||
pub use event::{Context, DomainEvent, EventType, StateChangedEvent, SystemEvent};
|
||||
pub use state::StateMachine;
|
||||
pub use bus::EventBus;
|
||||
pub use registry::{DeviceEntry, DeviceRegistry, EntityCategory, EntityEntry, EntityRegistry};
|
||||
pub use service::{ServiceCall, ServiceError, ServiceName, ServiceRegistry};
|
||||
pub use registry::{EntityCategory, EntityEntry, EntityRegistry};
|
||||
pub use state::StateMachine;
|
||||
|
||||
/// HOMECORE protocol/data-model version. Bumped when the public surface
|
||||
/// or on-disk persistence schema changes in a backwards-incompatible way.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! In-memory entity registry (P1). Persistence to
|
||||
//! `.homecore/storage/core.entity_registry` lands in P2.
|
||||
//! In-memory entity and device registries. Durable files are loaded by
|
||||
//! `homecore-server` during bounded startup restoration.
|
||||
//!
|
||||
//! Schema fields mirror HA `core.entity_registry` v13 per ADR-127 §2.4.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -43,6 +43,41 @@ pub struct EntityEntry {
|
||||
pub config_entry_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Physical-device metadata persisted in `core.device_registry`.
|
||||
///
|
||||
/// The fields track the HA v13 registry surface used by HOMECORE. Identifier
|
||||
/// and connection pairs are sets because their order is not semantically
|
||||
/// meaningful in HA.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DeviceEntry {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub config_entries: HashSet<String>,
|
||||
#[serde(default)]
|
||||
pub identifiers: HashSet<(String, String)>,
|
||||
#[serde(default)]
|
||||
pub connections: HashSet<(String, String)>,
|
||||
pub manufacturer: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub model_id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub name_by_user: Option<String>,
|
||||
pub sw_version: Option<String>,
|
||||
pub hw_version: Option<String>,
|
||||
pub serial_number: Option<String>,
|
||||
pub via_device_id: Option<String>,
|
||||
pub area_id: Option<String>,
|
||||
pub entry_type: Option<String>,
|
||||
pub disabled_by: Option<String>,
|
||||
pub configuration_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub labels: HashSet<String>,
|
||||
pub primary_config_entry: Option<String>,
|
||||
/// Forward-compatible device fields from newer HA v13-compatible rows.
|
||||
#[serde(default, flatten)]
|
||||
pub extra: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EntityRegistry {
|
||||
entries: Arc<RwLock<HashMap<EntityId, EntityEntry>>>,
|
||||
@@ -89,6 +124,45 @@ impl Default for EntityRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceRegistry {
|
||||
entries: Arc<RwLock<HashMap<String, DeviceEntry>>>,
|
||||
}
|
||||
|
||||
impl DeviceRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(&self, entry: DeviceEntry) {
|
||||
self.entries.write().await.insert(entry.id.clone(), entry);
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: &str) -> Option<DeviceEntry> {
|
||||
self.entries.read().await.get(id).cloned()
|
||||
}
|
||||
|
||||
pub async fn all(&self) -> Vec<DeviceEntry> {
|
||||
self.entries.read().await.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub async fn len(&self) -> usize {
|
||||
self.entries.read().await.len()
|
||||
}
|
||||
|
||||
pub async fn is_empty(&self) -> bool {
|
||||
self.entries.read().await.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DeviceRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
//!
|
||||
//! - `async_set_internal` schema validation
|
||||
//! - Bulk delete of an entire domain (`async_remove_domain`)
|
||||
//! - Restore-state on startup from the recorder (ADR-132)
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -158,6 +157,19 @@ impl StateMachine {
|
||||
next
|
||||
}
|
||||
|
||||
/// Install a durable snapshot during startup without emitting a new
|
||||
/// state-change event. Callers must mark the snapshot context as a
|
||||
/// restoration; this prevents accidental use as a silent runtime write.
|
||||
pub fn restore(&self, snapshot: State) -> Result<Arc<State>, RestoreStateError> {
|
||||
if !snapshot.context.is_restoration() {
|
||||
return Err(RestoreStateError::UnmarkedContext);
|
||||
}
|
||||
let entity_id = snapshot.entity_id.clone();
|
||||
let snapshot = Arc::new(snapshot);
|
||||
self.inner.states.insert(entity_id, Arc::clone(&snapshot));
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
/// Remove a state. Fires `state_changed` with `new_state = None`.
|
||||
pub fn remove(&self, entity_id: &EntityId) -> Option<Arc<State>> {
|
||||
let removed = self.inner.states.remove(entity_id).map(|(_, s)| s);
|
||||
@@ -213,6 +225,12 @@ impl Default for StateMachine {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum RestoreStateError {
|
||||
#[error("restored state context is not marked as a restoration")]
|
||||
UnmarkedContext,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user