Skip to main content

ankurah_core/
entity.rs

1use crate::event_dag::DEFAULT_BUDGET;
2use crate::retrieval::{GetEvents, GetState};
3use crate::selection::filter::Filterable;
4use crate::{
5    error::{LineageError, MutationError, RetrievalError, StateError},
6    event_dag::AbstractCausalRelation,
7    model::View,
8    property::backend::{backend_from_string, PropertyBackend},
9    reactor::AbstractEntity,
10    value::Value,
11};
12use ankurah_proto::{Clock, CollectionId, EntityId, EntityState, Event, EventId, OperationSet, State};
13use std::collections::BTreeMap;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Weak};
16use tracing::{debug, error, warn};
17
18/// Result of applying a state snapshot to an entity.
19pub enum StateApplyResult {
20    /// StrictDescends — state applied directly
21    Applied,
22    /// DivergedSince — cannot merge without events
23    DivergedRequiresEvents,
24    /// Equal — no-op, state already matches
25    AlreadyApplied,
26    /// StrictAscends — incoming state is older, no-op
27    Older,
28}
29
30/// An entity represents a unique thing within a collection. Entity can only be constructed via a WeakEntitySet
31/// which provides duplication guarantees.
32#[derive(Debug, Clone)]
33pub struct Entity(Arc<EntityInner>);
34
35// TODO optimize this to be faster for scanning over entries in a collection
36/// Used only for reconstituting state to filter database results. No duplication guarantees are provided
37pub struct TemporaryEntity(Arc<EntityInner>);
38
39/// Combined state for atomic updates of head and backends
40#[derive(Debug)]
41struct EntityInnerState {
42    head: Clock,
43    // TODO: remove interior mutability from backends; make mutation methods take &mut self
44    backends: BTreeMap<String, Arc<dyn PropertyBackend>>,
45}
46
47impl EntityInnerState {
48    /// Apply operations from an event, tracking which event set each property.
49    ///
50    /// This enables per-property conflict resolution when concurrent events arrive later.
51    /// For CRDT backends (like Yrs), the event_id tracking is a no-op since CRDTs
52    /// handle concurrency internally. For LWW backends, this stores the event_id
53    /// alongside each property value.
54    fn apply_operations_from_event(
55        &mut self,
56        backend_name: String,
57        operations: &[ankurah_proto::Operation],
58        event_id: EventId,
59    ) -> Result<(), MutationError> {
60        if let Some(backend) = self.backends.get(&backend_name) {
61            backend.apply_operations_with_event(operations, event_id)?;
62        } else {
63            let backend = backend_from_string(&backend_name, None)?;
64            backend.apply_operations_with_event(operations, event_id)?;
65            self.backends.insert(backend_name, backend);
66        }
67        Ok(())
68    }
69}
70
71#[derive(Debug)]
72pub struct EntityInner {
73    pub id: EntityId,
74    pub collection: CollectionId,
75    /// Combined state RwLock for atomic head/backends updates
76    state: std::sync::RwLock<EntityInnerState>,
77    pub(crate) kind: EntityKind,
78    /// Broadcast for notifying Signal subscribers about entity changes
79    pub(crate) broadcast: ankurah_signals::broadcast::Broadcast,
80}
81
82#[derive(Debug)]
83pub enum EntityKind {
84    Primary,                                                     // New or resident entity - TODO delineate these
85    Transacted { trx_alive: Arc<AtomicBool>, upstream: Entity }, // Transaction fork with liveness tracking
86}
87
88impl std::ops::Deref for Entity {
89    type Target = EntityInner;
90
91    fn deref(&self) -> &Self::Target { &self.0 }
92}
93
94impl std::ops::Deref for TemporaryEntity {
95    type Target = EntityInner;
96
97    fn deref(&self) -> &Self::Target { &self.0 }
98}
99
100impl PartialEq for Entity {
101    fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.0, &other.0) }
102}
103
104/// A weak reference to an entity
105pub struct WeakEntity(Weak<EntityInner>);
106
107impl WeakEntity {
108    pub fn upgrade(&self) -> Option<Entity> { self.0.upgrade().map(Entity) }
109}
110
111impl Entity {
112    pub fn id(&self) -> EntityId { self.id }
113
114    // This is intentionally private - only WeakEntitySet should be constructing Entities
115    fn weak(&self) -> WeakEntity { WeakEntity(Arc::downgrade(&self.0)) }
116
117    pub fn collection(&self) -> &CollectionId { &self.collection }
118
119    pub fn head(&self) -> Clock { self.state.read().unwrap().head.clone() }
120
121    /// Check if this entity is writable (i.e., it's a transaction fork that's still alive)
122    pub fn is_writable(&self) -> bool {
123        match &self.kind {
124            EntityKind::Primary => false, // Primary entities are read-only
125            EntityKind::Transacted { trx_alive, .. } => trx_alive.load(Ordering::Acquire),
126        }
127    }
128
129    pub fn to_state(&self) -> Result<State, StateError> {
130        let state = self.state.read().expect("other thread panicked, panic here too");
131        let mut state_buffers = BTreeMap::default();
132        for (name, backend) in &state.backends {
133            let state_buffer = backend.to_state_buffer()?;
134            state_buffers.insert(name.clone(), state_buffer);
135        }
136        let state_buffers = ankurah_proto::StateBuffers(state_buffers);
137        Ok(State { state_buffers, head: state.head.clone() })
138    }
139
140    pub fn to_entity_state(&self) -> Result<EntityState, StateError> {
141        let state = self.to_state()?;
142        Ok(EntityState { entity_id: self.id(), collection: self.collection.clone(), state })
143    }
144
145    // used by the Model macro
146    pub fn create(id: EntityId, collection: CollectionId) -> Self {
147        Self(Arc::new(EntityInner {
148            id,
149            collection,
150            state: std::sync::RwLock::new(EntityInnerState { head: Clock::default(), backends: BTreeMap::default() }),
151            kind: EntityKind::Primary,
152            broadcast: ankurah_signals::broadcast::Broadcast::new(),
153        }))
154    }
155
156    /// This must remain private - ONLY WeakEntitySet should be constructing Entities
157    fn from_state(id: EntityId, collection: CollectionId, state: &State) -> Result<Self, RetrievalError> {
158        let mut backends = BTreeMap::new();
159        for (name, state_buffer) in state.state_buffers.iter() {
160            let backend = backend_from_string(name, Some(state_buffer))?;
161            backends.insert(name.to_owned(), backend);
162        }
163
164        Ok(Self(Arc::new(EntityInner {
165            id,
166            collection,
167            state: std::sync::RwLock::new(EntityInnerState { head: state.head.clone(), backends }),
168            kind: EntityKind::Primary,
169            broadcast: ankurah_signals::broadcast::Broadcast::new(),
170        })))
171    }
172
173    /// Generate an event which contains all operations for all backends since the last time they were collected
174    /// Used for transaction commit. Notably this does not apply the head to the entity, which must be done
175    /// using commit_head
176    pub(crate) fn generate_commit_event(&self) -> Result<Option<Event>, MutationError> {
177        let state = self.state.read().expect("other thread panicked, panic here too");
178        let mut operations = BTreeMap::<String, Vec<ankurah_proto::Operation>>::new();
179        for (name, backend) in &state.backends {
180            if let Some(ops) = backend.to_operations()? {
181                operations.insert(name.clone(), ops);
182            }
183        }
184
185        if operations.is_empty() {
186            Ok(None)
187        } else {
188            let operations = OperationSet(operations);
189            let event = Event { entity_id: self.id, collection: self.collection.clone(), operations, parent: state.head.clone() };
190            Ok(Some(event))
191        }
192    }
193
194    /// Updates the head of the entity to the given clock, which should come exclusively from generate_commit_event
195    pub(crate) fn commit_head(&self, new_head: Clock) {
196        // TODO figure out how to implement CAS with the backend state
197        // probably need an increment for local edits
198        self.state.write().unwrap().head = new_head;
199    }
200
201    /// Attempts to mutate the entity state if the head matches the expected value.
202    ///
203    /// This provides TOCTOU protection: grabs the write lock, checks that `state.head == expected_head`,
204    /// and only then runs the closure. If the head changed, updates `expected_head` to the current value
205    /// and returns `Ok(false)` so the caller can retry with fresh lineage info.
206    ///
207    /// Returns `Ok(true)` if the mutation succeeded, `Ok(false)` if the head moved (retry needed),
208    /// or `Err` if the closure returned an error.
209    fn try_mutate<F, E>(&self, expected_head: &mut Clock, body: F) -> Result<bool, E>
210    where F: FnOnce(&mut EntityInnerState) -> Result<(), E> {
211        let mut state = self.state.write().unwrap();
212        if &state.head != expected_head {
213            *expected_head = state.head.clone();
214            return Ok(false);
215        }
216        body(&mut state)?;
217        Ok(true)
218    }
219
220    pub fn view<V: View>(&self) -> Option<V> {
221        if self.collection() != &V::collection() {
222            None
223        } else {
224            Some(V::from_entity(self.clone()))
225        }
226    }
227
228    /// Attempt to apply an event to the entity
229    #[cfg_attr(feature = "instrument", tracing::instrument(level="debug", skip_all, fields(entity = %self, event = %event)))]
230    pub async fn apply_event<E>(&self, getter: &E, event: &Event) -> Result<bool, MutationError>
231    where E: GetEvents + Send + Sync {
232        debug!("apply_event head: {event} to {self}");
233
234        // Idempotency is handled by the comparison algorithm:
235        // - Event already in head -> Equal -> no-op (Ok(false))
236        // - Event is ancestor of head -> StrictAscends -> no-op (Ok(false))
237        // - Event re-delivered but already integrated -> BFS finds it -> StrictAscends
238        // An explicit event_stored() check is not used here because callers
239        // (node_applier, system.rs) store events to storage BEFORE calling
240        // apply_event (so BFS can find them), which would cause false positives.
241
242        // Creation event on entity with non-empty head: either re-delivery or attack.
243        // On durable nodes (definitive storage), we can cheaply distinguish:
244        //   event_stored() == true  → re-delivery → no-op
245        //   event_stored() == false → different genesis event → reject
246        // On ephemeral nodes, event_stored() may return false for legitimate
247        // re-deliveries (entity arrived via StateSnapshot without event storage),
248        // so we fall through to BFS which correctly identifies:
249        //   StrictAscends → re-delivery → no-op
250        //   Disjoint → different genesis → reject
251        if event.is_entity_create() && !self.head().is_empty() {
252            if getter.event_stored(&event.id()).await? {
253                return Ok(false);
254            }
255            if getter.storage_is_definitive() {
256                return Err(LineageError::Disjoint.into());
257            }
258            // Ephemeral: fall through to comparison
259        }
260
261        // Check for entity creation under the mutex to avoid TOCTOU race
262        if event.is_entity_create() {
263            let mut state = self.state.write().unwrap();
264            // Re-check if head is still empty now that we hold the lock
265            if state.head.is_empty() {
266                // this is the creation event for a new entity, so we simply accept it
267                for (backend_name, operations) in event.operations.iter() {
268                    state.apply_operations_from_event(backend_name.clone(), operations, event.id())?;
269                }
270                state.head = event.id().into();
271                drop(state); // Release lock before broadcast
272                             // Notify Signal subscribers about the change
273                self.broadcast.send(());
274                return Ok(true);
275            }
276            // If head is no longer empty, fall through to normal lineage comparison
277        }
278
279        // Non-creation event on an entity with empty heads means the entity was never created.
280        // Reject early — the DAG comparison would produce DivergedSince(meet=[]) which would
281        // incorrectly apply the update to a non-existent entity.
282        if !event.is_entity_create() && self.head().is_empty() {
283            return Err(MutationError::InvalidEvent);
284        }
285
286        let mut head = self.head();
287        // Retry loop to handle head changes between lineage comparison and mutation
288        const MAX_RETRIES: usize = 5;
289
290        for attempt in 0..MAX_RETRIES {
291            // Stage the event so BFS can discover it, then compare event's clock vs head
292            let subject_clock: Clock = event.id().into();
293            let comparison_result = crate::event_dag::compare(getter, &subject_clock, &head, DEFAULT_BUDGET).await?;
294            match comparison_result.relation {
295                AbstractCausalRelation::Equal => {
296                    debug!("Equal - skip");
297                    return Ok(false);
298                }
299                AbstractCausalRelation::StrictDescends { .. } => {
300                    debug!("Descends - apply (attempt {})", attempt + 1);
301                    let new_head: Clock = event.id().into();
302                    let event_id = event.id();
303                    if self.try_mutate(&mut head, |state| -> Result<(), MutationError> {
304                        for (backend_name, operations) in event.operations.iter() {
305                            state.apply_operations_from_event(backend_name.clone(), operations, event_id.clone())?;
306                        }
307                        state.head = new_head.clone();
308                        Ok(())
309                    })? {
310                        self.broadcast.send(());
311                        return Ok(true);
312                    }
313                    continue;
314                }
315                AbstractCausalRelation::StrictAscends => {
316                    // Incoming event is older than current state - no-op
317                    debug!("StrictAscends - incoming event is older, ignoring");
318                    return Ok(false);
319                }
320                AbstractCausalRelation::DivergedSince { ref meet, .. } => {
321                    debug!("DivergedSince - true concurrency, applying via layers (attempt {})", attempt + 1);
322
323                    let meet = meet.clone();
324
325                    // Decompose the result to get the accumulator.
326                    // The event is already in the accumulated DAG (found via staging in BFS).
327                    let (_relation, accumulator) = comparison_result.into_parts();
328                    let mut layers = accumulator.into_layers(meet.clone(), head.as_slice().to_vec());
329
330                    let mut applied_layers: Vec<crate::event_dag::EventLayer> = Vec::new();
331
332                    // Collect all layers first, then apply under lock
333                    let mut all_layers = Vec::new();
334                    while let Some(layer) = layers.next().await? {
335                        all_layers.push(layer);
336                    }
337
338                    // Atomic update: apply layers and augment head under single lock
339                    {
340                        let mut state = self.state.write().unwrap();
341                        // Re-check that head hasn't changed since lineage comparison
342                        if state.head != head {
343                            warn!("Head changed during lineage comparison, retrying...");
344                            head = state.head.clone();
345                            continue;
346                        }
347
348                        // Apply layers in causal order
349                        for layer in all_layers {
350                            // Check for backends that first appear in this layer's to_apply events
351                            for evt in &layer.to_apply {
352                                for (backend_name, _) in evt.operations.iter() {
353                                    if !state.backends.contains_key(backend_name) {
354                                        let backend = backend_from_string(backend_name, None)?;
355                                        // Replay earlier layers for this newly-created backend
356                                        for earlier in &applied_layers {
357                                            backend.apply_layer(earlier)?;
358                                        }
359                                        state.backends.insert(backend_name.clone(), backend);
360                                    }
361                                }
362                            }
363
364                            // Apply to all backends
365                            for (_backend_name, backend) in state.backends.iter() {
366                                backend.apply_layer(&layer)?;
367                            }
368                            applied_layers.push(layer);
369                        }
370
371                        // Update head: remove superseded tips, add new event
372                        // The incoming event extends tips in its parent clock (meet).
373                        // Any of those that are in the current head are now superseded.
374                        for parent_id in &meet {
375                            state.head.remove(parent_id);
376                        }
377                        state.head.insert(event.id());
378                    }
379                    self.broadcast.send(());
380                    return Ok(true);
381                }
382                AbstractCausalRelation::Disjoint { .. } => {
383                    return Err(LineageError::Disjoint.into());
384                }
385                AbstractCausalRelation::BudgetExceeded { subject, other } => {
386                    return Err(LineageError::BudgetExceeded {
387                        original_budget: DEFAULT_BUDGET,
388                        subject_frontier: subject,
389                        other_frontier: other,
390                    }
391                    .into());
392                }
393            }
394        }
395
396        warn!("apply_event retries exhausted while chasing moving head");
397        Err(MutationError::TOCTOUAttemptsExhausted)
398    }
399
400    /// Apply a state snapshot to this entity.
401    ///
402    /// Returns `StateApplyResult` indicating what happened:
403    /// - `Applied` — state was newer and applied directly (StrictDescends)
404    /// - `AlreadyApplied` — state matches current head (Equal)
405    /// - `Older` — incoming state is older than current (StrictAscends), no-op
406    /// - `DivergedRequiresEvents` — state diverged, events needed for proper merge
407    pub async fn apply_state<E>(&self, getter: &E, state: &State) -> Result<StateApplyResult, MutationError>
408    where E: GetEvents + Send + Sync {
409        let mut head = self.head();
410        let new_head = state.head.clone();
411
412        debug!("{self} apply_state - new head: {new_head}");
413        const MAX_RETRIES: usize = 5;
414
415        for attempt in 0..MAX_RETRIES {
416            let comparison_result = crate::event_dag::compare(getter, &new_head, &head, DEFAULT_BUDGET).await?;
417            match comparison_result.relation {
418                AbstractCausalRelation::Equal => {
419                    debug!("{self} apply_state - heads are equal, skipping");
420                    return Ok(StateApplyResult::AlreadyApplied);
421                }
422                AbstractCausalRelation::StrictDescends { .. } => {
423                    debug!("{self} apply_state - new head descends from current, applying (attempt {})", attempt + 1);
424                    let new_head = state.head.clone();
425                    if self.try_mutate(&mut head, |es| -> Result<(), MutationError> {
426                        for (name, state_buffer) in state.state_buffers.iter() {
427                            let backend = backend_from_string(name, Some(state_buffer))?;
428                            es.backends.insert(name.to_owned(), backend);
429                        }
430                        es.head = new_head;
431                        Ok(())
432                    })? {
433                        self.broadcast.send(());
434                        return Ok(StateApplyResult::Applied);
435                    }
436                    continue;
437                }
438                AbstractCausalRelation::StrictAscends => {
439                    // State is older than current - no-op
440                    debug!("{self} apply_state - new head {new_head} is older than current {head}, ignoring");
441                    return Ok(StateApplyResult::Older);
442                }
443                AbstractCausalRelation::DivergedSince { meet, .. } => {
444                    // State snapshots cannot be merged without the underlying events.
445                    // The caller should either:
446                    // 1. Request the full event history and use apply_event() for each
447                    // 2. Accept this state via policy if the attestation is trusted
448                    // 3. Reject and resync from a known-good state
449                    warn!(
450                        "{self} apply_state - new head {new_head} diverged from {head}, meet: {meet:?}. \
451                        State not applied; events required for proper merge."
452                    );
453                    return Ok(StateApplyResult::DivergedRequiresEvents);
454                }
455                AbstractCausalRelation::Disjoint { .. } => {
456                    error!("{self} apply_state - heads are disjoint (different genesis)");
457                    return Err(LineageError::Disjoint.into());
458                }
459                AbstractCausalRelation::BudgetExceeded { subject, other } => {
460                    tracing::warn!("{self} apply_state - budget exceeded. subject: {subject:?}, other: {other:?}");
461                    return Err(LineageError::BudgetExceeded {
462                        original_budget: DEFAULT_BUDGET,
463                        subject_frontier: subject,
464                        other_frontier: other,
465                    }
466                    .into());
467                }
468            }
469        }
470
471        warn!("apply_state retries exhausted while chasing moving head");
472        Err(MutationError::TOCTOUAttemptsExhausted)
473    }
474
475    /// Create a snapshot of the Entity which is detached from this one, and will not receive the updates this one does
476    /// The trx_alive parameter tracks whether the transaction that owns this snapshot is still alive
477    pub fn snapshot(&self, trx_alive: Arc<AtomicBool>) -> Self {
478        // Inline fork logic
479        let state = self.state.read().expect("other thread panicked, panic here too");
480        let mut forked = BTreeMap::new();
481        for (name, backend) in &state.backends {
482            forked.insert(name.clone(), backend.fork());
483        }
484
485        Self(Arc::new(EntityInner {
486            id: self.id,
487            collection: self.collection.clone(),
488            state: std::sync::RwLock::new(EntityInnerState { head: state.head.clone(), backends: forked }),
489            kind: EntityKind::Transacted { trx_alive, upstream: self.clone() },
490            broadcast: ankurah_signals::broadcast::Broadcast::new(),
491        }))
492    }
493
494    /// Get a reference to the entity's broadcast for Signal implementations
495    pub fn broadcast(&self) -> &ankurah_signals::broadcast::Broadcast { &self.broadcast }
496
497    /// Get a specific backend, creating it if it doesn't exist
498    pub fn get_backend<P: PropertyBackend>(&self) -> Result<Arc<P>, RetrievalError> {
499        let backend_name = P::property_backend_name();
500        let mut state = self.state.write().expect("other thread panicked, panic here too");
501        if let Some(backend) = state.backends.get(backend_name) {
502            let upcasted = backend.clone().as_arc_dyn_any();
503            Ok(upcasted.downcast::<P>().unwrap()) // TODO: handle downcast error
504        } else {
505            let backend = backend_from_string(backend_name, None)?;
506            let upcasted = backend.clone().as_arc_dyn_any();
507            let typed_backend = upcasted.downcast::<P>().unwrap(); // TODO handle downcast error
508            state.backends.insert(backend_name.to_owned(), backend);
509            Ok(typed_backend)
510        }
511    }
512
513    pub fn values(&self) -> Vec<(String, Option<Value>)> {
514        let state = self.state.read().expect("other thread panicked, panic here too");
515        state
516            .backends
517            .values()
518            .flat_map(|backend| {
519                backend
520                    .property_values()
521                    .iter()
522                    .map(|(name, value)| (name.to_string(), value.clone()))
523                    .collect::<Vec<(String, Option<Value>)>>()
524            })
525            .collect()
526    }
527}
528
529// Implement AbstractEntity for Entity (used by reactor)
530impl AbstractEntity for Entity {
531    fn collection(&self) -> ankurah_proto::CollectionId { self.collection.clone() }
532
533    fn id(&self) -> &ankurah_proto::EntityId { &self.id }
534
535    fn value(&self, field: &str) -> Option<crate::value::Value> {
536        if field == "id" {
537            Some(crate::value::Value::EntityId(self.id))
538        } else {
539            // Iterate through backends to find one that has this property
540            let state = self.state.read().expect("other thread panicked, panic here too");
541            state.backends.values().find_map(|backend| backend.property_value(&field.into()))
542        }
543    }
544}
545
546impl std::fmt::Display for Entity {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        write!(f, "Entity({}/{} {:#})", self.collection, self.id.to_base64_short(), self.head())
549    }
550}
551
552impl Filterable for Entity {
553    fn collection(&self) -> &str { self.collection.as_str() }
554
555    fn value(&self, name: &str) -> Option<Value> {
556        if name == "id" {
557            Some(Value::EntityId(self.id))
558        } else {
559            // Iterate through backends to find one that has this property
560            let state = self.state.read().expect("other thread panicked, panic here too");
561            state.backends.values().find_map(|backend| backend.property_value(&name.to_owned()))
562        }
563    }
564}
565
566impl TemporaryEntity {
567    pub fn new(id: EntityId, collection: CollectionId, state: &State) -> Result<Self, RetrievalError> {
568        // Inline from_state_buffers logic
569        let mut backends = BTreeMap::new();
570        for (name, state_buffer) in state.state_buffers.iter() {
571            let backend = backend_from_string(name, Some(state_buffer))?;
572            backends.insert(name.to_owned(), backend);
573        }
574
575        Ok(Self(Arc::new(EntityInner {
576            id,
577            collection,
578            state: std::sync::RwLock::new(EntityInnerState { head: state.head.clone(), backends }),
579            kind: EntityKind::Primary,
580            // slightly annoying that we need to populate this, given that it won't be used
581            broadcast: ankurah_signals::broadcast::Broadcast::new(),
582        })))
583    }
584    pub fn values(&self) -> Vec<(String, Option<Value>)> {
585        let state = self.0.state.read().expect("other thread panicked, panic here too");
586        state.backends.values().flat_map(|backend| backend.property_values()).collect()
587    }
588}
589
590// TODO - clean this up and consolidate with Entity somehow, while still preventing anyone from creating unregistered (non-temporary) Entities
591impl Filterable for TemporaryEntity {
592    fn collection(&self) -> &str { self.0.collection.as_str() }
593
594    fn value(&self, name: &str) -> Option<Value> {
595        if name == "id" {
596            Some(Value::EntityId(self.0.id))
597        } else {
598            // Iterate through backends to find one that has this property
599            let state = self.0.state.read().expect("other thread panicked, panic here too");
600            state.backends.values().find_map(|backend| backend.property_value(&name.to_owned()))
601        }
602    }
603}
604
605impl std::fmt::Display for TemporaryEntity {
606    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607        write!(f, "TemporaryEntity({}/{}) = {}", &self.collection, self.id, self.0.state.read().unwrap().head)
608    }
609}
610
611// TODO - Implement TOCTOU Race condition tests. Require real backend state mutations to be meaningful. punting that for now
612/// A set of entities held weakly
613#[derive(Clone, Default)]
614pub struct WeakEntitySet(Arc<std::sync::RwLock<BTreeMap<EntityId, WeakEntity>>>);
615impl WeakEntitySet {
616    pub fn get(&self, id: &EntityId) -> Option<Entity> {
617        let entities = self.0.read().unwrap();
618        // TODO: call policy agent with cdata
619        if let Some(entity) = entities.get(id) {
620            entity.upgrade()
621        } else {
622            None
623        }
624    }
625
626    pub async fn get_or_retrieve<S, E>(
627        &self,
628        state_getter: &S,
629        event_getter: &E,
630        collection_id: &CollectionId,
631        id: &EntityId,
632    ) -> Result<Option<Entity>, RetrievalError>
633    where
634        S: GetState + Send + Sync,
635        E: GetEvents + Send + Sync,
636    {
637        // do it in two phases to avoid holding the lock while waiting for the collection
638        match self.get(id) {
639            Some(entity) => Ok(Some(entity)),
640            None => match state_getter.get_state(*id).await? {
641                None => Ok(None),
642                Some(state) => {
643                    // technically someone could have added the entity since we last checked, so it's better to use the
644                    // with_state method to re-check
645                    let (_, entity) =
646                        self.with_state(state_getter, event_getter, *id, collection_id.to_owned(), state.payload.state).await?;
647                    Ok(Some(entity))
648                }
649            },
650        }
651    }
652    /// Returns a resident entity, or fetches it from storage, or finally creates if neither of the two are found
653    pub async fn get_retrieve_or_create<S, E>(
654        &self,
655        state_getter: &S,
656        event_getter: &E,
657        collection_id: &CollectionId,
658        id: &EntityId,
659    ) -> Result<Entity, RetrievalError>
660    where
661        S: GetState + Send + Sync,
662        E: GetEvents + Send + Sync,
663    {
664        match self.get_or_retrieve(state_getter, event_getter, collection_id, id).await? {
665            Some(entity) => Ok(entity),
666            None => {
667                let mut entities = self.0.write().unwrap();
668                // TODO: call policy agent with cdata
669                if let Some(entity) = entities.get(id) {
670                    if let Some(entity) = entity.upgrade() {
671                        return Ok(entity);
672                    }
673                }
674                let entity = Entity::create(*id, collection_id.to_owned());
675                entities.insert(*id, entity.weak());
676                Ok(entity)
677            }
678        }
679    }
680    /// Create a brand new entity, and add it to the set
681    pub fn create(&self, collection: CollectionId) -> Entity {
682        let mut entities = self.0.write().unwrap();
683        let id = EntityId::new();
684        let entity = Entity::create(id, collection);
685        entities.insert(id, entity.weak());
686        entity
687    }
688
689    /// Evict an entity from the set only if it is absent from storage-backed
690    /// life: resident with an empty head (or already dead). An empty-head
691    /// resident is a phantom, materialized speculatively for an incoming
692    /// update that then failed to apply; leaving it resident makes the entity
693    /// appear to exist with no state. Returns true if an entry was removed.
694    pub fn remove_if_phantom(&self, id: &EntityId) -> bool {
695        let mut entities = self.0.write().unwrap();
696        if let Some(weak) = entities.get(id) {
697            if let Some(entity) = weak.upgrade() {
698                if !entity.head().is_empty() {
699                    return false;
700                }
701            }
702            entities.remove(id);
703            return true;
704        }
705        false
706    }
707
708    /// TEST ONLY: Create a phantom entity with a specific ID.
709    ///
710    /// This creates an entity that was never properly created via Transaction::create(),
711    /// has no creation event, and has an empty state. Used for adversarial testing to
712    /// verify that commit paths properly reject such entities.
713    ///
714    /// WARNING: This bypasses all normal entity creation validation. Only use in tests
715    /// to verify security properties.
716    ///
717    /// Requires the `test-helpers` feature to be enabled.
718    #[cfg(feature = "test-helpers")]
719    pub fn conjure_evil_phantom(&self, id: EntityId, collection: CollectionId) -> Entity {
720        let mut entities = self.0.write().unwrap();
721        let entity = Entity::create(id, collection);
722        entities.insert(id, entity.weak());
723        entity
724    }
725
726    /// Get or create entity after async operations, checking for race conditions
727    /// Returns (existed, entity) where existed is true if the entity was already present
728    fn private_get_or_create(&self, id: EntityId, collection_id: &CollectionId, state: &State) -> Result<(bool, Entity), RetrievalError> {
729        let mut entities = self.0.write().unwrap();
730        if let Some(existing_weak) = entities.get(&id) {
731            if let Some(existing_entity) = existing_weak.upgrade() {
732                debug!("Entity {id} was created by another thread during async work, using that one");
733                return Ok((true, existing_entity));
734            }
735        }
736        let entity = Entity::from_state(id, collection_id.to_owned(), state)?;
737        entities.insert(id, entity.weak());
738        Ok((false, entity))
739    }
740
741    /// Returns a tuple of (changed, entity)
742    /// changed is Some(true) if the entity was changed, Some(false) if it already exists and the state was not applied
743    /// None if the entity was not previously on the local node (either in the WeakEntitySet or in storage)
744    pub async fn with_state<S, E>(
745        &self,
746        state_getter: &S,
747        event_getter: &E,
748        id: EntityId,
749        collection_id: CollectionId,
750        state: State,
751    ) -> Result<(Option<bool>, Entity), RetrievalError>
752    where
753        S: GetState + Send + Sync,
754        E: GetEvents + Send + Sync,
755    {
756        let entity = match self.get(&id) {
757            Some(entity) => entity, // already resident
758            None => {
759                // not yet resident. We have to retrieve our baseline state before applying the new state
760                if let Some(stored_state) = state_getter.get_state(id).await? {
761                    // get a resident entity for this retrieved state. It's possible somebody frontran us to create it
762                    // but we don't actually care, so we ignore the created flag
763                    self.private_get_or_create(id, &collection_id, &stored_state.payload.state)?.1
764                } else {
765                    // no stored state, so we can use the given state directly
766                    match self.private_get_or_create(id, &collection_id, &state)? {
767                        (true, entity) => entity, // some body frontran us to create it, so we have to apply the new state
768                        (false, entity) => {
769                            // we just created it with the given state, so there's nothing to apply. early return
770                            return Ok((None, entity));
771                        }
772                    }
773                }
774            }
775        };
776
777        // if we're here, we've retrieved the entity from the set and need to apply the state
778        let result = entity.apply_state(event_getter, &state).await?;
779        let changed = matches!(result, StateApplyResult::Applied);
780        Ok((Some(changed), entity))
781    }
782}