Skip to main content

ankurah_core/
context.rs

1use crate::retrieval::SuspenseEvents;
2use crate::{
3    changes::EntityChange,
4    entity::Entity,
5    error::{MutationError, RetrievalError},
6    livequery::{EntityLiveQuery, LiveQuery},
7    model::View,
8    node::{MatchArgs, Node},
9    policy::{AccessDenied, PolicyAgent},
10    storage::{StorageCollectionWrapper, StorageEngine},
11    transaction::Transaction,
12};
13use ankurah_proto::{self as proto, Attested, Clock, CollectionId, EntityState, Event};
14use async_trait::async_trait;
15use std::sync::{atomic::AtomicBool, Arc};
16use tracing::debug;
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::*;
19
20/// Context is used to provide a local interface to fetch and subscribe to entities
21/// with a specific ContextData. Generally this means your auth token for a specific user,
22/// but ContextData is abstracted so you can use what you want.
23#[cfg_attr(feature = "wasm", wasm_bindgen)]
24#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
25pub struct Context(Arc<dyn TContext + Send + Sync + 'static>);
26impl Clone for Context {
27    fn clone(&self) -> Self { Self(self.0.clone()) }
28}
29
30pub struct NodeAndContext<SE, PA: PolicyAgent>
31where
32    SE: StorageEngine + Send + Sync + 'static,
33    PA: PolicyAgent + Send + Sync + 'static,
34{
35    pub node: Node<SE, PA>,
36    pub cdata: PA::ContextData,
37}
38
39#[async_trait]
40pub trait TContext {
41    fn node_id(&self) -> proto::EntityId;
42    /// Create a brand new entity for a transaction, and add it to the WeakEntitySet
43    /// Note that this does not actually persist the entity to the storage engine
44    /// It merely ensures that there are no duplicate entities with the same ID (except forked entities)
45    fn create_entity(&self, collection: proto::CollectionId, trx_alive: Arc<AtomicBool>) -> Entity;
46    fn check_write(&self, entity: &Entity) -> Result<(), AccessDenied>;
47    async fn get_entity(&self, id: proto::EntityId, collection: &proto::CollectionId, cached: bool) -> Result<Entity, RetrievalError>;
48    fn get_resident_entity(&self, id: proto::EntityId) -> Option<Entity>;
49    async fn fetch_entities(&self, collection: &proto::CollectionId, args: MatchArgs) -> Result<Vec<Entity>, RetrievalError>;
50    async fn commit_local_trx(&self, trx: &Transaction) -> Result<Vec<Event>, MutationError>;
51    fn query(&self, collection_id: proto::CollectionId, args: MatchArgs) -> Result<EntityLiveQuery, RetrievalError>;
52    async fn collection(&self, id: &proto::CollectionId) -> Result<StorageCollectionWrapper, RetrievalError>;
53}
54
55#[async_trait]
56impl<SE: StorageEngine + Send + Sync + 'static, PA: PolicyAgent + Send + Sync + 'static> TContext for NodeAndContext<SE, PA> {
57    fn node_id(&self) -> proto::EntityId { self.node.id }
58    fn create_entity(&self, collection: proto::CollectionId, trx_alive: Arc<AtomicBool>) -> Entity {
59        let primary_entity = self.node.entities.create(collection);
60        primary_entity.snapshot(trx_alive)
61    }
62    fn check_write(&self, entity: &Entity) -> Result<(), AccessDenied> { self.node.policy_agent.check_write(&self.cdata, entity, None) }
63    async fn get_entity(&self, id: proto::EntityId, collection: &proto::CollectionId, cached: bool) -> Result<Entity, RetrievalError> {
64        self.get_entity(collection, id, cached).await
65    }
66    fn get_resident_entity(&self, id: proto::EntityId) -> Option<Entity> { self.node.entities.get(&id) }
67    async fn fetch_entities(&self, collection: &proto::CollectionId, args: MatchArgs) -> Result<Vec<Entity>, RetrievalError> {
68        self.fetch_entities(collection, args).await
69    }
70    async fn commit_local_trx(&self, trx: &Transaction) -> Result<Vec<Event>, MutationError> {
71        use std::sync::atomic::Ordering;
72
73        // Atomically mark transaction as no longer alive, preventing double-commit.
74        // compare_exchange returns Err if the value was already false (already committed/rolled back).
75        if trx.alive.compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire).is_err() {
76            return Err(MutationError::General("Transaction already committed or rolled back".into()));
77        }
78
79        // Generate events from the transaction entities
80        let trx_id = trx.id.clone();
81        let mut entity_events = Vec::new();
82        for entity in trx.entities.iter() {
83            if let Some(event) = entity.generate_commit_event()? {
84                // Validate creation events: if parent is empty, this is a creation event
85                // and the entity must have been created in this transaction via create()
86                if event.is_entity_create() {
87                    let created_ids = trx.created_entity_ids.read().unwrap();
88                    if !created_ids.contains(&entity.id) {
89                        return Err(MutationError::General(
90                            format!(
91                                "Cannot commit phantom entity {}: entity has empty parent (creation event) \
92                             but was not created in this transaction via create()",
93                                entity.id
94                            )
95                            .into(),
96                        ));
97                    }
98                }
99                entity_events.push((entity.clone(), event));
100            }
101        }
102
103        // Now commit the events
104        let mut attested_events = Vec::new();
105        let mut entity_attested_events = Vec::new();
106
107        // Phase 1: check policy and collect attestations for EVERY event
108        // before persisting ANY of them, so a later denial leaves nothing
109        // durable (failure atomicity, V7).
110        for (entity, event) in entity_events {
111            // Create a temporary fork to apply the event for validation
112            use std::sync::atomic::AtomicBool;
113            let trx_alive = Arc::new(AtomicBool::new(true));
114            let forked = entity.snapshot(trx_alive);
115
116            // Get the canonical (upstream) entity for before state
117            let entity_before = match &entity.kind {
118                crate::entity::EntityKind::Transacted { upstream, .. } => upstream.clone(),
119                crate::entity::EntityKind::Primary => entity.clone(),
120            };
121
122            // Stage event and apply to fork for after state (no commit_event call here)
123            let collection_id = &event.collection;
124            let collection = self.node.collections.get(collection_id).await?;
125            let event_getter = crate::retrieval::LocalEventGetter::new(collection, self.node.durable);
126            event_getter.stage_event(event.clone());
127            forked.apply_event(&event_getter, &event).await?;
128
129            let attestation = self.node.policy_agent.check_event(&self.node, &self.cdata, &entity_before, &forked, &event)?;
130            let attested = Attested::opt(event.clone(), attestation);
131
132            attested_events.push(attested.clone());
133            entity_attested_events.push((entity, attested));
134        }
135
136        // Phase 2: all events attested; persist them.
137        for (_, attested) in &entity_attested_events {
138            let collection = self.node.collections.get(&attested.payload.collection).await?;
139            let event_getter = crate::retrieval::LocalEventGetter::new(collection, self.node.durable);
140            event_getter.commit_event(attested).await?;
141        }
142
143        // Update heads BEFORE relaying (makes entities visible to server echo)
144        for (entity, attested_event) in &entity_attested_events {
145            entity.commit_head(Clock::new([attested_event.payload.id()]));
146        }
147        // Relay to peers and wait for confirmation
148        self.node.relay_to_required_peers(&self.cdata, trx_id, &attested_events).await?;
149
150        // All peers confirmed, persist state to storage
151        let mut changes: Vec<EntityChange> = Vec::new();
152        for (entity, attested_event) in entity_attested_events {
153            let collection_id = &attested_event.payload.collection;
154            let collection = self.node.collections.get(collection_id).await?;
155
156            // Persist canonical entity (upstream for transactional forks, entity itself for primary)
157            let canonical_entity = match &entity.kind {
158                crate::entity::EntityKind::Transacted { upstream, .. } => {
159                    // Event is now in storage, construct fresh getter for upstream apply
160                    let event_getter = crate::retrieval::LocalEventGetter::new(collection.clone(), self.node.durable);
161                    upstream.apply_event(&event_getter, &attested_event.payload).await?;
162                    upstream.clone()
163                }
164                crate::entity::EntityKind::Primary => entity,
165            };
166
167            let state = canonical_entity.to_state()?;
168
169            let entity_state = EntityState { entity_id: canonical_entity.id(), collection: canonical_entity.collection().clone(), state };
170            let attestation = self.node.policy_agent.attest_state(&self.node, &entity_state);
171            let attested = Attested::opt(entity_state, attestation);
172            collection.set_state(attested).await?;
173
174            changes.push(EntityChange::new(canonical_entity, vec![attested_event])?);
175        }
176
177        // Notify reactor of ALL changes
178        self.node.reactor.notify_change(changes).await;
179
180        Ok(attested_events.into_iter().map(|a| a.payload).collect())
181    }
182    fn query(&self, collection_id: proto::CollectionId, args: MatchArgs) -> Result<EntityLiveQuery, RetrievalError> {
183        EntityLiveQuery::new(&self.node, collection_id, args, self.cdata.clone())
184    }
185    async fn collection(&self, id: &proto::CollectionId) -> Result<StorageCollectionWrapper, RetrievalError> {
186        self.node.system.collection(id).await
187    }
188}
189
190// This whole impl is conditionalized by the wasm feature flag
191#[cfg(feature = "wasm")]
192#[wasm_bindgen]
193impl Context {
194    #[wasm_bindgen(js_name = "node_id")]
195    pub fn js_node_id(&self) -> proto::EntityId { self.0.node_id() }
196}
197
198// This impl may or may not have the wasm_bindgen attribute but the functions will always be defined
199#[cfg_attr(feature = "wasm", wasm_bindgen)]
200#[cfg_attr(feature = "uniffi", uniffi::export)]
201impl Context {
202    /// Begin a transaction.
203    pub fn begin(&self) -> Transaction { Transaction::new(self.0.clone()) }
204}
205
206impl Context {
207    pub fn new<SE: StorageEngine + Send + Sync + 'static, PA: PolicyAgent + Send + Sync + 'static>(
208        node: Node<SE, PA>,
209        data: PA::ContextData,
210    ) -> Self {
211        Self(Arc::new(NodeAndContext { node, cdata: data }))
212    }
213
214    pub fn node_id(&self) -> proto::EntityId { self.0.node_id() }
215
216    // TODO: Fix this - arghhh async lifetimes
217    // pub async fn trx<T, F, Fut>(self: &Arc<Self>, f: F) -> anyhow::Result<T>
218    // where
219    //     F: for<'a> FnOnce(&'a Transaction) -> Fut,
220    //     Fut: std::future::Future<Output = anyhow::Result<T>>,
221    // {
222    //     let trx = self.begin();
223    //     let result = f(&trx).await?;
224    //     trx.commit().await?;
225    //     Ok(result)
226    // }
227
228    pub async fn get<R: View>(&self, id: proto::EntityId) -> Result<R, RetrievalError> {
229        let entity = self.0.get_entity(id, &R::collection(), false).await?;
230        Ok(R::from_entity(entity))
231    }
232
233    /// Get an entity, but its ok to return early if the entity is already in the local node storage
234    pub async fn get_cached<R: View>(&self, id: proto::EntityId) -> Result<R, RetrievalError> {
235        let entity = self.0.get_entity(id, &R::collection(), true).await?;
236        Ok(R::from_entity(entity))
237    }
238
239    pub async fn fetch<R: View>(&self, args: impl TryInto<MatchArgs, Error = impl Into<RetrievalError>>) -> Result<Vec<R>, RetrievalError> {
240        let args: MatchArgs = args.try_into().map_err(|e| e.into())?;
241        use crate::model::Model;
242        let collection_id = R::Model::collection();
243
244        let entities = self.0.fetch_entities(&collection_id, args).await?;
245
246        Ok(entities.into_iter().map(|e| R::from_entity(e)).collect())
247    }
248
249    pub async fn fetch_one<R: View + Clone + 'static>(
250        &self,
251        args: impl TryInto<MatchArgs, Error = impl Into<RetrievalError>>,
252    ) -> Result<Option<R>, RetrievalError> {
253        let views = self.fetch::<R>(args).await?;
254        Ok(views.into_iter().next())
255    }
256    /// Subscribe to changes in entities matching a selection
257    pub fn query<R>(&self, args: impl TryInto<MatchArgs, Error = impl Into<RetrievalError>>) -> Result<LiveQuery<R>, RetrievalError>
258    where R: View {
259        let args: MatchArgs = args.try_into().map_err(|e| e.into())?;
260        use crate::model::Model;
261        Ok(self.0.query(R::Model::collection(), args)?.map::<R>())
262    }
263
264    /// Subscribe to changes in entities matching a selection and wait for initialization
265    pub async fn query_wait<R>(
266        &self,
267        args: impl TryInto<MatchArgs, Error = impl Into<RetrievalError>>,
268    ) -> Result<LiveQuery<R>, RetrievalError>
269    where
270        R: View,
271    {
272        let livequery = self.query::<R>(args)?;
273        livequery.wait_initialized().await;
274        Ok(livequery)
275    }
276    pub async fn collection(&self, id: &proto::CollectionId) -> Result<StorageCollectionWrapper, RetrievalError> {
277        self.0.collection(id).await
278    }
279}
280
281impl<SE, PA> NodeAndContext<SE, PA>
282where
283    SE: StorageEngine + Send + Sync + 'static,
284    PA: PolicyAgent + Send + Sync + 'static,
285{
286    /// Retrieve a single entity, either by cloning the resident Entity from the Node's WeakEntitySet or fetching from storage
287    pub(crate) async fn get_entity(
288        &self,
289        collection_id: &CollectionId,
290        id: proto::EntityId,
291        cached: bool,
292    ) -> Result<Entity, RetrievalError> {
293        debug!("Node({}).get_entity {:?}-{:?}", self.node.id, id, collection_id);
294
295        if !self.node.durable {
296            // Fetch from peers and commit first response
297            match self.node.get_from_peer(collection_id, vec![id], &self.cdata).await {
298                Ok(_) => (),
299                Err(RetrievalError::NoDurablePeers) if cached => (),
300                Err(e) => {
301                    return Err(e);
302                }
303            }
304        }
305
306        if let Some(local) = self.node.entities.get(&id) {
307            debug!("Node({}).get_entity found local entity - returning", self.node.id);
308            let state = local.to_state()?;
309            let entity_id = local.id();
310            self.node.policy_agent.check_read(&self.cdata, &entity_id, collection_id, &state)?;
311            return Ok(local);
312        }
313        debug!("{}.get_entity fetching from storage", self.node);
314
315        let collection = self.node.collections.get(collection_id).await?;
316        match collection.get_state(id).await {
317            Ok(entity_state) => {
318                self.node.policy_agent.check_read(
319                    &self.cdata,
320                    &entity_state.payload.entity_id,
321                    collection_id,
322                    &entity_state.payload.state,
323                )?;
324                let state_getter = crate::retrieval::LocalStateGetter::new(collection.clone());
325                let event_getter = crate::retrieval::CachedEventGetter::new(collection_id.clone(), collection, &self.node, &self.cdata);
326                let (_changed, entity) = self
327                    .node
328                    .entities
329                    .with_state(&state_getter, &event_getter, id, collection_id.clone(), entity_state.payload.state)
330                    .await?;
331                Ok(entity)
332            }
333            Err(e) => Err(e),
334        }
335    }
336    /// Fetch a list of entities based on a selection
337    pub async fn fetch_entities(&self, collection_id: &CollectionId, mut args: MatchArgs) -> Result<Vec<Entity>, RetrievalError> {
338        self.node.policy_agent.can_access_collection(&self.cdata, collection_id)?;
339        // Fetch raw states from storage
340
341        args.selection.predicate = self.node.policy_agent.filter_predicate(&self.cdata, collection_id, args.selection.predicate)?;
342
343        // Resolve types in the AST (converts literals for JSON path comparisons)
344        args.selection = self.node.type_resolver.resolve_selection_types(args.selection);
345
346        // TODO implement cached: true
347        if !self.node.durable {
348            // Fetch from peers and commit first response
349            Ok(self.fetch_from_peer(collection_id, args.selection).await?)
350        } else {
351            let storage_collection = self.node.collections.get(collection_id).await?;
352            let states = storage_collection.fetch_states(&args.selection).await?;
353
354            // Convert states to entities
355            let mut entities = Vec::new();
356            let state_getter = crate::retrieval::LocalStateGetter::new(storage_collection.clone());
357            let event_getter = crate::retrieval::CachedEventGetter::new(collection_id.clone(), storage_collection, &self.node, &self.cdata);
358            for state in states {
359                let (_, entity) = self
360                    .node
361                    .entities
362                    .with_state(&state_getter, &event_getter, state.payload.entity_id, collection_id.clone(), state.payload.state)
363                    .await?;
364                entities.push(entity);
365            }
366            Ok(entities)
367        }
368    }
369
370    /// Fetch entities from the first available durable peer with known_matches support
371    async fn fetch_from_peer(
372        &self,
373        collection_id: &proto::CollectionId,
374        selection: ankql::ast::Selection,
375    ) -> Result<Vec<crate::entity::Entity>, RetrievalError> {
376        let peer_id = self.node.get_durable_peer_random().ok_or(RetrievalError::NoDurablePeers)?;
377
378        // 1. Pre-fetch known_matches from local storage
379        let known_matched_entities = self.node.fetch_entities_from_local(collection_id, &selection).await?;
380
381        let known_matches = known_matched_entities
382            .iter()
383            .map(|entity| proto::KnownEntity { entity_id: entity.id(), head: entity.head().clone() })
384            .collect();
385
386        // 2. Send fetch request with known_matches
387        let selection_clone = selection.clone();
388        match self
389            .node
390            .request(peer_id, &self.cdata, proto::NodeRequestBody::Fetch { collection: collection_id.clone(), selection, known_matches })
391            .await?
392        {
393            proto::NodeResponseBody::Fetch(deltas) => {
394                let collection = self.node.collections.get(collection_id).await?;
395                let event_getter =
396                    crate::retrieval::CachedEventGetter::new(collection_id.clone(), collection.clone(), &self.node, &self.cdata);
397                let state_getter = crate::retrieval::LocalStateGetter::new(collection);
398
399                // 3. Apply deltas to local storage using NodeApplier
400                crate::node_applier::NodeApplier::apply_deltas(&self.node, &peer_id, deltas, &event_getter, &state_getter).await?;
401                // ARCHITECTURAL QUESTION: Optimize in-place mutation vs re-fetching for remote-peer-assisted operations https://github.com/ankurah/ankurah/issues/145
402
403                // 4. Re-fetch entities from local storage after applying deltas
404                self.node.fetch_entities_from_local(collection_id, &selection_clone).await
405            }
406            proto::NodeResponseBody::Error(e) => {
407                tracing::debug!("Error from peer fetch: {}", e);
408                Err(RetrievalError::Other(format!("{:?}", e)))
409            }
410            _ => {
411                tracing::debug!("Unexpected response type from peer fetch");
412                Err(RetrievalError::Other("Unexpected response type".to_string()))
413            }
414        }
415    }
416}