Skip to main content

feltdb/
materialization.rs

1/// Materialization module for integrating acquired content into the reactive system
2///
3/// When content is acquired from the distributed fabric, it needs to be materialized
4/// into the local reactive state graph so that:
5/// 1. Application queries use the same db.get() API
6/// 2. Reactive subscriptions automatically trigger updates
7/// 3. Content becomes indistinguishable from locally-created content
8use crate::{AcquisitionMetadata, FlowRef, Operation};
9use serde_json::Value;
10use std::time::SystemTime;
11use std::time::UNIX_EPOCH;
12
13/// Handler for materializing acquired content
14#[derive(Clone)]
15pub struct MaterializationHandler {
16    /// Track which references have been materialized to avoid re-materialization
17    materialized: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
18}
19
20impl MaterializationHandler {
21    /// Create a new materialization handler
22    pub fn new() -> Self {
23        Self {
24            materialized: std::sync::Arc::new(std::sync::Mutex::new(
25                std::collections::HashSet::new(),
26            )),
27        }
28    }
29
30    /// Materialize acquired content as a legitimate FeltDB operation
31    /// This makes acquired content participate in the reactive system
32    pub fn materialize_as_operation(
33        &self,
34        flow_ref: &FlowRef,
35        content: Vec<u8>,
36        metadata: &AcquisitionMetadata,
37    ) -> Result<Operation, String> {
38        // Deserialize content to JSON to integrate with FeltDB's operation model
39        let value: Value = serde_json::from_slice(&content).unwrap_or_else(|_| {
40            // If not JSON, wrap raw bytes as a string
41            Value::String(String::from_utf8_lossy(&content).into_owned())
42        });
43
44        let key = format!("{}:{}", flow_ref.namespace, flow_ref.object_id);
45        let now_ms = SystemTime::now()
46            .duration_since(UNIX_EPOCH)
47            .map(|d| d.as_millis())
48            .unwrap_or(0);
49
50        // Create an insert operation for the acquired content
51        // This operation will flow through the reactive system
52        let mut op = Operation::update(
53            now_ms as u64,                   // Use timestamp as operation ID
54            metadata.source_peer.id.clone(), // Mark source peer as originator
55            1,                               // Initial sequence
56            key.clone(),
57            value,
58            flow_ref.namespace.clone(),
59            flow_ref.namespace.clone(),
60        );
61
62        // Attach vector clock from acquisition metadata to preserve causality
63        if let Some(vector_clock) = &metadata.vector_clock {
64            op = op.with_vector_clock(vector_clock.clone());
65        }
66
67        // Mark as materialized
68        let mut materialized = self.materialized.lock().map_err(|e| e.to_string())?;
69        materialized.insert(key);
70
71        Ok(op)
72    }
73
74    /// Check if a reference has already been materialized
75    pub fn is_materialized(&self, flow_ref: &FlowRef) -> Result<bool, String> {
76        let key = format!("{}:{}", flow_ref.namespace, flow_ref.object_id);
77        let materialized = self.materialized.lock().map_err(|e| e.to_string())?;
78        Ok(materialized.contains(&key))
79    }
80
81    /// Get the count of materialized references
82    pub fn materialized_count(&self) -> Result<usize, String> {
83        let materialized = self.materialized.lock().map_err(|e| e.to_string())?;
84        Ok(materialized.len())
85    }
86}
87
88impl Default for MaterializationHandler {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94/// Reactive collection integration
95/// This trait allows acquired content to participate in reactive collections
96pub trait ReactiveCollectionIntegration {
97    /// Notify the collection that new content was acquired and materialized
98    fn on_content_materialized(&self, operation: &Operation) -> Result<(), String>;
99
100    /// Check if this collection would be affected by materialization
101    fn affects_collection(&self, namespace: &str) -> bool;
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::PeerId;
108
109    #[test]
110    fn test_materialization_handler_creation() {
111        let handler = MaterializationHandler::new();
112        assert_eq!(handler.materialized_count().unwrap(), 0);
113    }
114
115    #[test]
116    fn test_materialize_as_operation() {
117        let handler = MaterializationHandler::new();
118        let flow_ref = FlowRef::new("docs", "doc-123");
119        let content = br#"{"title": "Test Document", "content": "Test content"}"#.to_vec();
120        let peer = PeerId::new("peer-a");
121        let metadata = AcquisitionMetadata::new(peer, true);
122
123        let op = handler
124            .materialize_as_operation(&flow_ref, content, &metadata)
125            .unwrap();
126
127        assert_eq!(op.key, "docs:doc-123");
128        assert_eq!(op.capability, "docs");
129        assert!(op.value.is_some());
130    }
131
132    #[test]
133    fn test_materialize_non_json_content() {
134        let handler = MaterializationHandler::new();
135        let flow_ref = FlowRef::new("binary", "file-456");
136        let content = b"raw binary data".to_vec();
137        let peer = PeerId::new("peer-b");
138        let metadata = AcquisitionMetadata::new(peer, true);
139
140        let op = handler
141            .materialize_as_operation(&flow_ref, content, &metadata)
142            .unwrap();
143
144        assert_eq!(op.key, "binary:file-456");
145        // Raw binary should be wrapped as string
146        assert!(op.value.is_some());
147    }
148
149    #[test]
150    fn test_is_materialized() {
151        let handler = MaterializationHandler::new();
152        let flow_ref = FlowRef::new("docs", "doc-789");
153
154        assert!(!handler.is_materialized(&flow_ref).unwrap());
155
156        let content = b"content".to_vec();
157        let peer = PeerId::new("peer-c");
158        let metadata = AcquisitionMetadata::new(peer, true);
159
160        handler
161            .materialize_as_operation(&flow_ref, content, &metadata)
162            .unwrap();
163
164        assert!(handler.is_materialized(&flow_ref).unwrap());
165    }
166
167    #[test]
168    fn test_materialized_count() {
169        let handler = MaterializationHandler::new();
170
171        for i in 0..5 {
172            let flow_ref = FlowRef::new("docs", &format!("doc-{}", i));
173            let content = b"content".to_vec();
174            let peer = PeerId::new("peer-x");
175            let metadata = AcquisitionMetadata::new(peer, true);
176
177            handler
178                .materialize_as_operation(&flow_ref, content, &metadata)
179                .unwrap();
180        }
181
182        assert_eq!(handler.materialized_count().unwrap(), 5);
183    }
184}