Remove unit change platform event plumbing
This commit is contained in:
parent
a49f6adf9b
commit
dabcde1fca
|
|
@ -37,11 +37,7 @@ pub async fn run() {
|
||||||
.expect("Failed to bootstrap platform");
|
.expect("Failed to bootstrap platform");
|
||||||
|
|
||||||
let control_runtime = Arc::new(control::runtime::ControlRuntimeStore::new());
|
let control_runtime = Arc::new(control::runtime::ControlRuntimeStore::new());
|
||||||
|
let platform = builder.build();
|
||||||
let event_sink = Arc::new(crate::event::FeederPlatformEventSink::new(
|
|
||||||
control_runtime.clone(),
|
|
||||||
));
|
|
||||||
let platform = builder.build().with_event_sink(event_sink);
|
|
||||||
|
|
||||||
let event_manager = Arc::new(EventManager::new(
|
let event_manager = Arc::new(EventManager::new(
|
||||||
platform.pool.clone(),
|
platform.pool.clone(),
|
||||||
|
|
|
||||||
|
|
@ -88,38 +88,6 @@ impl EventManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bridges platform events to feeder-specific side effects.
|
|
||||||
/// Connection management and telemetry are handled by core automatically.
|
|
||||||
pub struct FeederPlatformEventSink {
|
|
||||||
control_runtime: std::sync::Arc<crate::control::runtime::ControlRuntimeStore>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FeederPlatformEventSink {
|
|
||||||
pub fn new(
|
|
||||||
control_runtime: std::sync::Arc<crate::control::runtime::ControlRuntimeStore>,
|
|
||||||
) -> Self {
|
|
||||||
Self { control_runtime }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl plc_platform_core::platform_context::PlatformEventSink for FeederPlatformEventSink {
|
|
||||||
fn on_event(&self, event: &plc_platform_core::event::PlatformEvent) {
|
|
||||||
match event {
|
|
||||||
plc_platform_core::event::PlatformEvent::UnitsChanged { unit_ids } => {
|
|
||||||
let runtime = self.control_runtime.clone();
|
|
||||||
let ids = unit_ids.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
for unit_id in ids {
|
|
||||||
runtime.notify_unit(unit_id).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Other platform events: connection management handled by core.
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_control_event(
|
async fn handle_control_event(
|
||||||
event: AppEvent,
|
event: AppEvent,
|
||||||
pool: &sqlx::PgPool,
|
pool: &sqlx::PgPool,
|
||||||
|
|
|
||||||
|
|
@ -21,21 +21,30 @@ impl EventEnvelope {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Platform-level events emitted by core handlers.
|
/// Platform-level events emitted by core handlers.
|
||||||
/// Each variant carries enough context for persistence + app-specific side effects.
|
/// Each variant carries enough context for persistence and platform side effects.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PlatformEvent {
|
pub enum PlatformEvent {
|
||||||
SourceCreated { source_id: Uuid },
|
SourceCreated {
|
||||||
SourceUpdated { source_id: Uuid },
|
source_id: Uuid,
|
||||||
SourceDeleted { source_id: Uuid, source_name: String },
|
},
|
||||||
PointsCreated { source_id: Uuid, point_ids: Vec<Uuid> },
|
SourceUpdated {
|
||||||
PointsDeleted { source_id: Uuid, point_ids: Vec<Uuid> },
|
source_id: Uuid,
|
||||||
UnitsChanged { unit_ids: Vec<Uuid> },
|
},
|
||||||
|
SourceDeleted {
|
||||||
|
source_id: Uuid,
|
||||||
|
source_name: String,
|
||||||
|
},
|
||||||
|
PointsCreated {
|
||||||
|
source_id: Uuid,
|
||||||
|
point_ids: Vec<Uuid>,
|
||||||
|
},
|
||||||
|
PointsDeleted {
|
||||||
|
source_id: Uuid,
|
||||||
|
point_ids: Vec<Uuid>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists platform events to the `event` table and broadcasts via WebSocket.
|
/// Persists platform events to the `event` table and broadcasts via WebSocket.
|
||||||
///
|
|
||||||
/// Apps get notified via `PlatformEventSink` for their own side effects
|
|
||||||
/// (connection management, control runtime, etc.).
|
|
||||||
pub async fn persist_and_broadcast(
|
pub async fn persist_and_broadcast(
|
||||||
event: &PlatformEvent,
|
event: &PlatformEvent,
|
||||||
pool: &sqlx::PgPool,
|
pool: &sqlx::PgPool,
|
||||||
|
|
@ -45,8 +54,11 @@ pub async fn persist_and_broadcast(
|
||||||
PlatformEvent::SourceCreated { source_id } => {
|
PlatformEvent::SourceCreated { source_id } => {
|
||||||
let name = fetch_source_name(pool, *source_id).await;
|
let name = fetch_source_name(pool, *source_id).await;
|
||||||
Some((
|
Some((
|
||||||
"platform.source.created", "info",
|
"platform.source.created",
|
||||||
None, None, Some(*source_id),
|
"info",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(*source_id),
|
||||||
format!("Source {} created", name),
|
format!("Source {} created", name),
|
||||||
serde_json::json!({ "source_id": source_id }),
|
serde_json::json!({ "source_id": source_id }),
|
||||||
))
|
))
|
||||||
|
|
@ -54,37 +66,57 @@ pub async fn persist_and_broadcast(
|
||||||
PlatformEvent::SourceUpdated { source_id } => {
|
PlatformEvent::SourceUpdated { source_id } => {
|
||||||
let name = fetch_source_name(pool, *source_id).await;
|
let name = fetch_source_name(pool, *source_id).await;
|
||||||
Some((
|
Some((
|
||||||
"platform.source.updated", "info",
|
"platform.source.updated",
|
||||||
None, None, Some(*source_id),
|
"info",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(*source_id),
|
||||||
format!("Source {} updated", name),
|
format!("Source {} updated", name),
|
||||||
serde_json::json!({ "source_id": source_id }),
|
serde_json::json!({ "source_id": source_id }),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
PlatformEvent::SourceDeleted { source_id, source_name } => Some((
|
PlatformEvent::SourceDeleted {
|
||||||
"platform.source.deleted", "warn",
|
source_id,
|
||||||
None, None, None,
|
source_name,
|
||||||
|
} => Some((
|
||||||
|
"platform.source.deleted",
|
||||||
|
"warn",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
format!("Source {} deleted", source_name),
|
format!("Source {} deleted", source_name),
|
||||||
serde_json::json!({ "source_id": source_id }),
|
serde_json::json!({ "source_id": source_id }),
|
||||||
)),
|
)),
|
||||||
PlatformEvent::PointsCreated { source_id, point_ids } => {
|
PlatformEvent::PointsCreated {
|
||||||
|
source_id,
|
||||||
|
point_ids,
|
||||||
|
} => {
|
||||||
let name = fetch_source_name(pool, *source_id).await;
|
let name = fetch_source_name(pool, *source_id).await;
|
||||||
Some((
|
Some((
|
||||||
"platform.point.batch_created", "info",
|
"platform.point.batch_created",
|
||||||
None, None, Some(*source_id),
|
"info",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(*source_id),
|
||||||
format!("Created {} points for source {}", point_ids.len(), name),
|
format!("Created {} points for source {}", point_ids.len(), name),
|
||||||
serde_json::json!({ "source_id": source_id, "point_ids": point_ids }),
|
serde_json::json!({ "source_id": source_id, "point_ids": point_ids }),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
PlatformEvent::PointsDeleted { source_id, point_ids } => {
|
PlatformEvent::PointsDeleted {
|
||||||
|
source_id,
|
||||||
|
point_ids,
|
||||||
|
} => {
|
||||||
let name = fetch_source_name(pool, *source_id).await;
|
let name = fetch_source_name(pool, *source_id).await;
|
||||||
Some((
|
Some((
|
||||||
"platform.point.batch_deleted", "warn",
|
"platform.point.batch_deleted",
|
||||||
None, None, Some(*source_id),
|
"warn",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(*source_id),
|
||||||
format!("Deleted {} points for source {}", point_ids.len(), name),
|
format!("Deleted {} points for source {}", point_ids.len(), name),
|
||||||
serde_json::json!({ "source_id": source_id, "point_ids": point_ids }),
|
serde_json::json!({ "source_id": source_id, "point_ids": point_ids }),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
PlatformEvent::UnitsChanged { .. } => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((event_type, level, unit_id, equipment_id, source_id, message, payload)) = record
|
let Some((event_type, level, unit_id, equipment_id, source_id, message, payload)) = record
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
|
@ -8,21 +8,11 @@ use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::platform_context::PlatformContext;
|
||||||
use crate::util::{
|
use crate::util::{
|
||||||
pagination::{PaginatedResponse, PaginationParams},
|
pagination::{PaginatedResponse, PaginationParams},
|
||||||
response::ApiErr,
|
response::ApiErr,
|
||||||
};
|
};
|
||||||
use crate::platform_context::PlatformContext;
|
|
||||||
|
|
||||||
fn notify_units(state: &PlatformContext, unit_ids: impl IntoIterator<Item = Uuid>) {
|
|
||||||
let ids: Vec<Uuid> = {
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
|
||||||
unit_ids.into_iter().filter(|id| seen.insert(*id)).collect()
|
|
||||||
};
|
|
||||||
if !ids.is_empty() {
|
|
||||||
state.emit_event(crate::event::PlatformEvent::UnitsChanged { unit_ids: ids });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
pub struct GetEquipmentListQuery {
|
pub struct GetEquipmentListQuery {
|
||||||
|
|
@ -188,10 +178,6 @@ pub async fn create_equipment(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(unit_id) = payload.unit_id {
|
|
||||||
notify_units(&state, [unit_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
|
|
@ -217,12 +203,12 @@ pub async fn update_equipment(
|
||||||
return Ok(Json(serde_json::json!({"ok_msg": "No fields to update"})));
|
return Ok(Json(serde_json::json!({"ok_msg": "No fields to update"})));
|
||||||
}
|
}
|
||||||
|
|
||||||
let exists = crate::service::get_equipment_by_id(&state.pool, equipment_id).await?;
|
if crate::service::get_equipment_by_id(&state.pool, equipment_id)
|
||||||
let existing_equipment = if let Some(equipment) = exists {
|
.await?
|
||||||
equipment
|
.is_none()
|
||||||
} else {
|
{
|
||||||
return Err(ApiErr::NotFound("Equipment not found".to_string(), None));
|
return Err(ApiErr::NotFound("Equipment not found".to_string(), None));
|
||||||
};
|
}
|
||||||
|
|
||||||
if let Some(Some(unit_id)) = payload.unit_id {
|
if let Some(Some(unit_id)) = payload.unit_id {
|
||||||
let unit_exists = crate::service::get_unit_by_id(&state.pool, unit_id).await?;
|
let unit_exists = crate::service::get_unit_by_id(&state.pool, unit_id).await?;
|
||||||
|
|
@ -255,19 +241,6 @@ pub async fn update_equipment(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut unit_ids = Vec::new();
|
|
||||||
if let Some(unit_id) = existing_equipment.unit_id {
|
|
||||||
unit_ids.push(unit_id);
|
|
||||||
}
|
|
||||||
let next_unit_id = match payload.unit_id {
|
|
||||||
Some(next) => next,
|
|
||||||
None => existing_equipment.unit_id,
|
|
||||||
};
|
|
||||||
if let Some(unit_id) = next_unit_id {
|
|
||||||
unit_ids.push(unit_id);
|
|
||||||
}
|
|
||||||
notify_units(&state, unit_ids);
|
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"ok_msg": "Equipment updated successfully"
|
"ok_msg": "Equipment updated successfully"
|
||||||
})))
|
})))
|
||||||
|
|
@ -293,9 +266,6 @@ pub async fn batch_set_equipment_unit(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let before_unit_ids =
|
|
||||||
crate::service::get_unit_ids_by_equipment_ids(&state.pool, &payload.equipment_ids).await?;
|
|
||||||
|
|
||||||
let updated_count = crate::service::batch_set_equipment_unit(
|
let updated_count = crate::service::batch_set_equipment_unit(
|
||||||
&state.pool,
|
&state.pool,
|
||||||
&payload.equipment_ids,
|
&payload.equipment_ids,
|
||||||
|
|
@ -303,12 +273,6 @@ pub async fn batch_set_equipment_unit(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut unit_ids = before_unit_ids;
|
|
||||||
if let Some(unit_id) = payload.unit_id {
|
|
||||||
unit_ids.push(unit_id);
|
|
||||||
}
|
|
||||||
notify_units(&state, unit_ids);
|
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"ok_msg": "Equipment unit updated successfully",
|
"ok_msg": "Equipment unit updated successfully",
|
||||||
"updated_count": updated_count
|
"updated_count": updated_count
|
||||||
|
|
@ -319,15 +283,10 @@ pub async fn delete_equipment(
|
||||||
State(state): State<PlatformContext>,
|
State(state): State<PlatformContext>,
|
||||||
Path(equipment_id): Path<Uuid>,
|
Path(equipment_id): Path<Uuid>,
|
||||||
) -> Result<impl IntoResponse, ApiErr> {
|
) -> Result<impl IntoResponse, ApiErr> {
|
||||||
let unit_ids = crate::service::get_unit_ids_by_equipment_ids(&state.pool, &[equipment_id]).await?;
|
|
||||||
let deleted = crate::service::delete_equipment(&state.pool, equipment_id).await?;
|
let deleted = crate::service::delete_equipment(&state.pool, equipment_id).await?;
|
||||||
if !deleted {
|
if !deleted {
|
||||||
return Err(ApiErr::NotFound("Equipment not found".to_string(), None));
|
return Err(ApiErr::NotFound("Equipment not found".to_string(), None));
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_units(&state, unit_ids);
|
|
||||||
|
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,6 @@ use crate::util::{
|
||||||
response::ApiErr,
|
response::ApiErr,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn notify_units(state: &PlatformContext, unit_ids: impl IntoIterator<Item = Uuid>) {
|
|
||||||
let ids: Vec<Uuid> = {
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
|
||||||
unit_ids.into_iter().filter(|id| seen.insert(*id)).collect()
|
|
||||||
};
|
|
||||||
if !ids.is_empty() {
|
|
||||||
state.emit_event(crate::event::PlatformEvent::UnitsChanged { unit_ids: ids });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all points.
|
/// List all points.
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
pub struct GetPointListQuery {
|
pub struct GetPointListQuery {
|
||||||
|
|
@ -245,8 +235,6 @@ pub async fn update_point(
|
||||||
if existing_point.is_none() {
|
if existing_point.is_none() {
|
||||||
return Err(ApiErr::NotFound("Point not found".to_string(), None));
|
return Err(ApiErr::NotFound("Point not found".to_string(), None));
|
||||||
}
|
}
|
||||||
let before_unit_ids = crate::service::get_unit_ids_by_point_ids(pool, &[point_id]).await?;
|
|
||||||
|
|
||||||
let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new("UPDATE point SET ");
|
let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new("UPDATE point SET ");
|
||||||
let mut wrote_field = false;
|
let mut wrote_field = false;
|
||||||
|
|
||||||
|
|
@ -301,9 +289,6 @@ pub async fn update_point(
|
||||||
qb.push(" WHERE id = ").push_bind(point_id);
|
qb.push(" WHERE id = ").push_bind(point_id);
|
||||||
qb.build().execute(pool).await?;
|
qb.build().execute(pool).await?;
|
||||||
|
|
||||||
let after_unit_ids = crate::service::get_unit_ids_by_point_ids(pool, &[point_id]).await?;
|
|
||||||
notify_units(&state, before_unit_ids.into_iter().chain(after_unit_ids));
|
|
||||||
|
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
serde_json::json!({"ok_msg": "Point updated successfully"}),
|
serde_json::json!({"ok_msg": "Point updated successfully"}),
|
||||||
))
|
))
|
||||||
|
|
@ -404,8 +389,6 @@ pub async fn batch_set_point_equipment(
|
||||||
return Err(ApiErr::NotFound("No valid points found".to_string(), None));
|
return Err(ApiErr::NotFound("No valid points found".to_string(), None));
|
||||||
}
|
}
|
||||||
|
|
||||||
let before_unit_ids = crate::service::get_unit_ids_by_point_ids(pool, &existing_points).await?;
|
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
UPDATE point
|
UPDATE point
|
||||||
|
|
@ -421,9 +404,6 @@ pub async fn batch_set_point_equipment(
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let after_unit_ids = crate::service::get_unit_ids_by_point_ids(pool, &existing_points).await?;
|
|
||||||
notify_units(&state, before_unit_ids.into_iter().chain(after_unit_ids));
|
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"ok_msg": "Point equipment updated successfully",
|
"ok_msg": "Point equipment updated successfully",
|
||||||
"updated_count": result.rows_affected()
|
"updated_count": result.rows_affected()
|
||||||
|
|
@ -436,8 +416,6 @@ pub async fn delete_point(
|
||||||
Path(point_id): Path<Uuid>,
|
Path(point_id): Path<Uuid>,
|
||||||
) -> Result<impl IntoResponse, ApiErr> {
|
) -> Result<impl IntoResponse, ApiErr> {
|
||||||
let pool = &state.pool;
|
let pool = &state.pool;
|
||||||
let affected_unit_ids = crate::service::get_unit_ids_by_point_ids(pool, &[point_id]).await?;
|
|
||||||
|
|
||||||
let source_id = {
|
let source_id = {
|
||||||
let grouped = crate::service::get_points_grouped_by_source(pool, &[point_id]).await?;
|
let grouped = crate::service::get_points_grouped_by_source(pool, &[point_id]).await?;
|
||||||
grouped.keys().next().copied()
|
grouped.keys().next().copied()
|
||||||
|
|
@ -465,8 +443,6 @@ pub async fn delete_point(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_units(&state, affected_unit_ids);
|
|
||||||
|
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
serde_json::json!({"ok_msg": "Point deleted successfully"}),
|
serde_json::json!({"ok_msg": "Point deleted successfully"}),
|
||||||
))
|
))
|
||||||
|
|
@ -614,7 +590,6 @@ pub async fn batch_delete_points(
|
||||||
let point_ids = payload.point_ids;
|
let point_ids = payload.point_ids;
|
||||||
|
|
||||||
let grouped = crate::service::get_points_grouped_by_source(pool, &point_ids).await?;
|
let grouped = crate::service::get_points_grouped_by_source(pool, &point_ids).await?;
|
||||||
let affected_unit_ids = crate::service::get_unit_ids_by_point_ids(pool, &point_ids).await?;
|
|
||||||
let existing_point_ids: Vec<Uuid> = grouped
|
let existing_point_ids: Vec<Uuid> = grouped
|
||||||
.values()
|
.values()
|
||||||
.flat_map(|points| points.iter().map(|p| p.point_id))
|
.flat_map(|points| points.iter().map(|p| p.point_id))
|
||||||
|
|
@ -637,8 +612,6 @@ pub async fn batch_delete_points(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_units(&state, affected_unit_ids);
|
|
||||||
|
|
||||||
Ok(Json(BatchDeletePointsRes {
|
Ok(Json(BatchDeletePointsRes {
|
||||||
deleted_count: result.rows_affected(),
|
deleted_count: result.rows_affected(),
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,13 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::connection::ConnectionManager;
|
use crate::connection::ConnectionManager;
|
||||||
use crate::event::PlatformEvent;
|
use crate::event::PlatformEvent;
|
||||||
use crate::websocket::WebSocketManager;
|
use crate::websocket::WebSocketManager;
|
||||||
|
use std::sync::Arc;
|
||||||
/// Callback interface for app-specific side effects on platform events.
|
|
||||||
///
|
|
||||||
/// Platform-level concerns (event persistence, WebSocket broadcast, connection
|
|
||||||
/// management) are handled automatically by `PlatformContext::emit_event()`.
|
|
||||||
/// Implementations only need to handle truly app-specific behavior
|
|
||||||
/// (e.g. feeder's control runtime notifications).
|
|
||||||
pub trait PlatformEventSink: Send + Sync {
|
|
||||||
/// Called for every platform event. Override to add app-specific side effects.
|
|
||||||
fn on_event(&self, event: &PlatformEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlatformContext {
|
pub struct PlatformContext {
|
||||||
pub pool: sqlx::PgPool,
|
pub pool: sqlx::PgPool,
|
||||||
pub connection_manager: Arc<ConnectionManager>,
|
pub connection_manager: Arc<ConnectionManager>,
|
||||||
pub ws_manager: Arc<WebSocketManager>,
|
pub ws_manager: Arc<WebSocketManager>,
|
||||||
pub event_sink: Option<Arc<dyn PlatformEventSink>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformContext {
|
impl PlatformContext {
|
||||||
|
|
@ -33,27 +20,14 @@ impl PlatformContext {
|
||||||
pool,
|
pool,
|
||||||
connection_manager,
|
connection_manager,
|
||||||
ws_manager,
|
ws_manager,
|
||||||
event_sink: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_event_sink(mut self, sink: Arc<dyn PlatformEventSink>) -> Self {
|
|
||||||
self.event_sink = Some(sink);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emit a platform event.
|
/// Emit a platform event.
|
||||||
///
|
///
|
||||||
/// Synchronously notifies the app-specific sink, then spawns async work for:
|
/// Spawns async work for event persistence, WebSocket broadcast, and
|
||||||
/// - Event persistence to DB + WebSocket broadcast
|
/// connection management side effects (connect, subscribe, etc.).
|
||||||
/// - Connection management side effects (connect, subscribe, etc.)
|
|
||||||
pub fn emit_event(&self, event: PlatformEvent) {
|
pub fn emit_event(&self, event: PlatformEvent) {
|
||||||
// 1. Notify app-specific sink synchronously (fire-and-forget via channels).
|
|
||||||
if let Some(sink) = &self.event_sink {
|
|
||||||
sink.on_event(&event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Spawn async: persistence + WS broadcast + connection management.
|
|
||||||
let pool = self.pool.clone();
|
let pool = self.pool.clone();
|
||||||
let ws_manager = self.ws_manager.clone();
|
let ws_manager = self.ws_manager.clone();
|
||||||
let cm = self.connection_manager.clone();
|
let cm = self.connection_manager.clone();
|
||||||
|
|
@ -120,9 +94,6 @@ impl PlatformContext {
|
||||||
tracing::error!("Failed to unsubscribe points: {}", e);
|
tracing::error!("Failed to unsubscribe points: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlatformEvent::UnitsChanged { .. } => {
|
|
||||||
// No platform-level side effect; app handles via on_event().
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::model::{ControlUnit, EventRecord};
|
use crate::model::{ControlUnit, EventRecord};
|
||||||
use sqlx::{PgPool, QueryBuilder, Row};
|
use sqlx::{PgPool, QueryBuilder, Row};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -31,9 +31,11 @@ pub async fn get_units_count(pool: &PgPool, keyword: Option<&str>) -> Result<i64
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
None => sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM unit"#)
|
None => {
|
||||||
|
sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM unit"#)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await,
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,9 +85,7 @@ pub async fn get_units_paginated(
|
||||||
None => {
|
None => {
|
||||||
if page_size == -1 {
|
if page_size == -1 {
|
||||||
let sql = format!("SELECT * FROM unit ORDER BY {}", unit_order);
|
let sql = format!("SELECT * FROM unit ORDER BY {}", unit_order);
|
||||||
sqlx::query_as::<_, ControlUnit>(&sql)
|
sqlx::query_as::<_, ControlUnit>(&sql).fetch_all(pool).await
|
||||||
.fetch_all(pool)
|
|
||||||
.await
|
|
||||||
} else {
|
} else {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -138,10 +138,7 @@ pub struct CreateUnitParams<'a> {
|
||||||
pub require_manual_ack_after_fault: bool,
|
pub require_manual_ack_after_fault: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_unit(
|
pub async fn create_unit(pool: &PgPool, params: CreateUnitParams<'_>) -> Result<Uuid, sqlx::Error> {
|
||||||
pool: &PgPool,
|
|
||||||
params: CreateUnitParams<'_>,
|
|
||||||
) -> Result<Uuid, sqlx::Error> {
|
|
||||||
let unit_id = Uuid::new_v4();
|
let unit_id = Uuid::new_v4();
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -222,10 +219,7 @@ pub async fn update_unit(
|
||||||
param_count += 1;
|
param_count += 1;
|
||||||
}
|
}
|
||||||
if params.require_manual_ack_after_fault.is_some() {
|
if params.require_manual_ack_after_fault.is_some() {
|
||||||
updates.push(format!(
|
updates.push(format!("require_manual_ack_after_fault = ${}", param_count));
|
||||||
"require_manual_ack_after_fault = ${}",
|
|
||||||
param_count
|
|
||||||
));
|
|
||||||
param_count += 1;
|
param_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,8 +279,7 @@ pub async fn get_events_count(
|
||||||
unit_id: Option<Uuid>,
|
unit_id: Option<Uuid>,
|
||||||
event_type: Option<&str>,
|
event_type: Option<&str>,
|
||||||
) -> Result<i64, sqlx::Error> {
|
) -> Result<i64, sqlx::Error> {
|
||||||
let mut qb =
|
let mut qb = QueryBuilder::new("SELECT COUNT(*)::BIGINT FROM event WHERE 1 = 1");
|
||||||
QueryBuilder::new("SELECT COUNT(*)::BIGINT FROM event WHERE 1 = 1");
|
|
||||||
|
|
||||||
if let Some(unit_id) = unit_id {
|
if let Some(unit_id) = unit_id {
|
||||||
qb.push(" AND unit_id = ").push_bind(unit_id);
|
qb.push(" AND unit_id = ").push_bind(unit_id);
|
||||||
|
|
@ -329,9 +322,7 @@ pub async fn get_all_enabled_units(pool: &PgPool) -> Result<Vec<ControlUnit>, sq
|
||||||
"SELECT * FROM unit WHERE enabled = TRUE ORDER BY {}",
|
"SELECT * FROM unit WHERE enabled = TRUE ORDER BY {}",
|
||||||
unit_order_clause()
|
unit_order_clause()
|
||||||
);
|
);
|
||||||
sqlx::query_as::<_, ControlUnit>(&sql)
|
sqlx::query_as::<_, ControlUnit>(&sql).fetch_all(pool).await
|
||||||
.fetch_all(pool)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_equipment_by_unit_ids(
|
pub async fn get_equipment_by_unit_ids(
|
||||||
|
|
@ -403,30 +394,6 @@ pub async fn get_unit_ids_by_equipment_ids(
|
||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_unit_ids_by_point_ids(
|
|
||||||
pool: &PgPool,
|
|
||||||
point_ids: &[Uuid],
|
|
||||||
) -> Result<Vec<Uuid>, sqlx::Error> {
|
|
||||||
if point_ids.is_empty() {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rows = sqlx::query_scalar::<_, Uuid>(
|
|
||||||
r#"
|
|
||||||
SELECT DISTINCT e.unit_id
|
|
||||||
FROM point p
|
|
||||||
INNER JOIN equipment e ON e.id = p.equipment_id
|
|
||||||
WHERE p.id = ANY($1)
|
|
||||||
AND e.unit_id IS NOT NULL
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(point_ids)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EquipmentSignalRole {
|
pub struct EquipmentSignalRole {
|
||||||
pub equipment_id: Uuid,
|
pub equipment_id: Uuid,
|
||||||
pub point_id: Uuid,
|
pub point_id: Uuid,
|
||||||
|
|
@ -507,4 +474,3 @@ pub async fn get_equipment_role_points(
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue