old backend core lib

This commit is contained in:
2026-07-18 11:44:58 -07:00
parent 994854d104
commit f599672a30
36 changed files with 3776 additions and 276 deletions
+76
View File
@@ -21,6 +21,7 @@ use uuid::Uuid;
use crate::{
domain::{ComponentMessage, LiveEvent, LiveEventKind},
overlay::OverlaySettings,
song_request::{SongRequestDefinition, SongRequestProjection},
};
pub const DANMAKU_OVERLAY_KIND: &str = "danmaku_overlay";
@@ -180,6 +181,19 @@ pub trait EventProjection: Send + Sync {
/// `async-trait` dependency.
pub type HandlerFuture<'a> = Pin<Box<dyn Future<Output = Result<(), ComponentError>> + Send + 'a>>;
/// Future used by component-specific durable state snapshots during WebSocket
/// authentication. Snapshot messages share the normal component envelope.
pub type SnapshotFuture<'a> =
Pin<Box<dyn Future<Output = Result<Vec<ComponentMessage>, ComponentError>> + Send + 'a>>;
pub trait ComponentSnapshotProvider: Send + Sync {
fn snapshot<'a>(
&'a self,
component: &'a ComponentInstance,
room_id: &'a str,
) -> SnapshotFuture<'a>;
}
/// An active handler may perform durable side effects (for example recording a
/// song request). It runs independently of WebSocket receiver count and should
/// implement idempotency in its persistence layer.
@@ -280,6 +294,7 @@ pub struct ComponentRuntime {
definition: Arc<dyn ComponentDefinition>,
projection: Arc<dyn EventProjection>,
handlers: Vec<Arc<dyn EventHandler>>,
snapshot_provider: Option<Arc<dyn ComponentSnapshotProvider>>,
}
impl ComponentRuntime {
@@ -320,6 +335,17 @@ impl ComponentRuntime {
pub fn handlers(&self) -> Vec<Arc<dyn EventHandler>> {
self.handlers.clone()
}
pub async fn snapshot(
&self,
component: &ComponentInstance,
room_id: &str,
) -> Result<Vec<ComponentMessage>, ComponentError> {
match &self.snapshot_provider {
Some(provider) => provider.snapshot(component, room_id).await,
None => Ok(Vec::new()),
}
}
}
#[derive(Clone)]
@@ -347,6 +373,12 @@ impl ComponentRegistry {
)
.expect("built-in component kinds are unique");
registry
.register(
Arc::new(SongRequestDefinition),
Arc::new(SongRequestProjection),
)
.expect("built-in component kinds are unique");
registry
}
pub fn register(
@@ -371,6 +403,7 @@ impl ComponentRegistry {
definition,
projection,
handlers: Vec::new(),
snapshot_provider: None,
},
);
Ok(())
@@ -392,6 +425,22 @@ impl ComponentRegistry {
Ok(())
}
pub fn register_snapshot_provider(
&self,
kind: &str,
provider: Arc<dyn ComponentSnapshotProvider>,
) -> Result<(), ComponentError> {
let mut entries = self
.entries
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let runtime = entries
.get_mut(kind)
.ok_or_else(|| ComponentError::NotRegistered(kind.to_owned()))?;
runtime.snapshot_provider = Some(provider);
Ok(())
}
pub fn runtime(&self, kind: &str) -> Result<ComponentRuntime, ComponentError> {
self.entries
.read()
@@ -469,6 +518,33 @@ mod tests {
assert!(!subscriptions.contains(LiveEventKind::GiftCombo));
}
#[test]
fn builtin_song_request_is_registered_with_bounded_unlimited_defaults() {
let registry = ComponentRegistry::default();
let runtime = registry.runtime("song_request").unwrap();
let mut settings = runtime.definition().default_settings();
settings["scrollSpeedPixelsPerSecond"] = Value::from(999);
let validated = registry
.validate_settings("song_request", 1, settings)
.unwrap();
assert_eq!(validated["scrollSpeedPixelsPerSecond"], 200);
assert_eq!(validated["maxQueueSize"], 0);
assert_eq!(validated["maxRequestsPerViewer"], 0);
assert_eq!(validated["requestCooldownSeconds"], 0);
let instance = ComponentInstance::new(
Uuid::new_v4(),
Uuid::new_v4(),
"song_request",
"点歌姬",
1,
validated,
);
let subscriptions = runtime.subscriptions(&instance).unwrap();
assert!(subscriptions.contains(LiveEventKind::Danmaku));
assert!(!subscriptions.contains(LiveEventKind::Gift));
}
#[test]
fn duplicate_component_kinds_are_rejected() {
let registry = ComponentRegistry::default();