prvng_platform/crates/extension-registry/src/events.rs

106 lines
3.2 KiB
Rust
Raw Normal View History

use std::sync::Arc;
use futures::StreamExt;
use platform_nats::NatsBridge;
use serde::Serialize;
use tracing::{error, info, warn};
use crate::cache::ExtensionCache;
use crate::models::ExtensionType;
/// Publishes NATS events for extension lifecycle operations.
pub struct EventPublisher {
bridge: Arc<NatsBridge>,
}
#[derive(Debug, Serialize)]
struct ExtensionEvent<'a> {
name: &'a str,
version: &'a str,
#[serde(rename = "type")]
extension_type: ExtensionType,
}
impl EventPublisher {
pub fn new(bridge: Arc<NatsBridge>) -> Self {
Self { bridge }
}
/// Publish `provisioning.extensions.{type}.installed`.
///
/// Fire-and-forget: logs error on failure but never propagates to handler.
pub async fn publish_installed(
&self,
extension_type: ExtensionType,
name: &str,
version: &str,
) {
let subject = format!("extensions.{}.installed", extension_type);
let payload = ExtensionEvent {
name,
version,
extension_type,
};
match self.bridge.publish_json(&subject, &payload).await {
Ok(_) => {
info!(
subject = %subject,
extension = %name,
version = %version,
"Extension installed event published"
);
}
Err(e) => {
error!(
subject = %subject,
extension = %name,
"Failed to publish extension event: {}", e
);
}
}
}
}
/// Spawn a background task that subscribes to workspace deploy-done events
/// and invalidates the extension cache on each notification.
///
/// Subject: `provisioning.workspace.*.deploy.done` (filter on WORKSPACE stream)
pub fn spawn_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
tokio::spawn(async move {
run_cache_invalidator(bridge, cache).await;
});
}
async fn run_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
const STREAM: &str = "WORKSPACE";
const CONSUMER: &str = "ext-registry-cache-invalidator";
let mut messages = match bridge.subscribe_pull(STREAM, CONSUMER).await {
Ok(m) => m,
Err(e) => {
warn!("Extension registry cache invalidator: subscribe failed — {e}");
return;
}
};
info!("Extension registry cache invalidator running on stream {STREAM}");
while let Some(msg_result) = messages.next().await {
match msg_result {
Ok(msg) => {
// Subject pattern: provisioning.workspace.{ws_id}.deploy.done
// Filter is applied at JetStream level; any message here triggers invalidation.
let subject = msg.subject.as_str();
info!(subject = %subject, "Workspace deploy detected — invalidating extension cache");
cache.invalidate_all();
if let Err(e) = msg.ack().await {
warn!("Cache invalidator: ack failed — {e}");
}
}
Err(e) => {
error!("Cache invalidator: message error — {e}");
}
}
}
}