fraiseql_core/runtime/subscription/mod.rs
1//! Subscription runtime for event-driven GraphQL subscriptions.
2//!
3//! FraiseQL subscriptions are **compiled projections of database events**, not
4//! traditional resolver-based subscriptions. Events originate from database
5//! transactions (via LISTEN/NOTIFY or CDC) and are delivered through transport
6//! adapters.
7//!
8//! # Architecture
9//!
10//! ```text
11//! Database Transaction (INSERT/UPDATE/DELETE)
12//! ↓ (commits)
13//! LISTEN/NOTIFY (PostgreSQL)
14//! ↓
15//! SubscriptionManager (event routing)
16//! ↓
17//! SubscriptionMatcher (filter evaluation)
18//! ↓ (parallel delivery)
19//! ├─ graphql-ws Adapter (WebSocket)
20//! ├─ Webhook Adapter (HTTP POST)
21//! └─ Kafka Adapter (event streaming)
22//! ```
23//!
24//! # Example
25//!
26//! ```ignore
27//! use fraiseql_core::runtime::subscription::{
28//! SubscriptionManager, SubscriptionEvent, SubscriptionId,
29//! };
30//! use tokio::sync::broadcast;
31//!
32//! // Create subscription manager
33//! let manager = SubscriptionManager::new(schema);
34//!
35//! // Subscribe to events
36//! let subscription_id = manager.subscribe(
37//! "OrderCreated",
38//! user_context,
39//! variables,
40//! ).await?;
41//!
42//! // Receive events
43//! let mut receiver = manager.receiver();
44//! while let Ok(event) = receiver.recv().await {
45//! if event.matches_subscription(subscription_id) {
46//! // Deliver to client
47//! }
48//! }
49//!
50//! // Unsubscribe
51//! manager.unsubscribe(subscription_id).await?;
52//! ```
53
54use thiserror::Error;
55
56mod kafka;
57mod manager;
58pub mod protocol;
59#[cfg(test)]
60mod tests;
61mod transport;
62mod types;
63mod webhook;
64
65pub use kafka::{KafkaAdapter, KafkaConfig, KafkaMessage};
66pub use manager::SubscriptionManager;
67pub use transport::{DeliveryResult, TransportAdapter, TransportManager};
68pub use types::{
69 ActiveSubscription, SubscriptionEvent, SubscriptionId, SubscriptionOperation,
70 SubscriptionPayload,
71};
72pub use webhook::{WebhookAdapter, WebhookConfig, WebhookPayload};
73
74// =============================================================================
75// Error Types
76// =============================================================================
77
78/// Errors that can occur during subscription operations.
79#[derive(Debug, Error)]
80pub enum SubscriptionError {
81 /// Subscription type not found in schema.
82 #[error("Subscription not found: {0}")]
83 SubscriptionNotFound(String),
84
85 /// Authentication required for subscription.
86 #[error("Authentication required for subscription: {0}")]
87 AuthenticationRequired(String),
88
89 /// User not authorized for subscription.
90 #[error("Not authorized for subscription: {0}")]
91 Forbidden(String),
92
93 /// Invalid subscription variables.
94 #[error("Invalid subscription variables: {0}")]
95 InvalidVariables(String),
96
97 /// Subscription already exists.
98 #[error("Subscription already exists: {0}")]
99 AlreadyExists(String),
100
101 /// Subscription not active.
102 #[error("Subscription not active: {0}")]
103 NotActive(String),
104
105 /// Internal subscription error.
106 #[error("Subscription error: {0}")]
107 Internal(String),
108
109 /// Channel send error.
110 #[error("Failed to send event: {0}")]
111 SendError(String),
112
113 /// Database connection error.
114 #[error("Database connection error: {0}")]
115 DatabaseConnection(String),
116
117 /// Listener already running.
118 #[error("Listener already running")]
119 ListenerAlreadyRunning,
120
121 /// Listener not running.
122 #[error("Listener not running")]
123 ListenerNotRunning,
124
125 /// Failed to parse notification payload.
126 #[error("Failed to parse notification: {0}")]
127 InvalidNotification(String),
128
129 /// Failed to deliver event to transport.
130 #[error("Failed to deliver to {transport}: {reason}")]
131 DeliveryFailed {
132 /// Transport that failed.
133 transport: String,
134 /// Reason for failure.
135 reason: String,
136 },
137}