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, } #[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) -> 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, cache: Arc) { tokio::spawn(async move { run_cache_invalidator(bridge, cache).await; }); } async fn run_cache_invalidator(bridge: Arc, cache: Arc) { 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}"); } } } }