ixa/entity/
events.rs

1/*!
2
3`EntityCreatedEvent` and `EntityPropertyChangeEvent` types are emitted when an entity is created or an entity's
4property value is changed.
5
6Client code can subscribe to these events with the `Context::subscribe_to_event<IxaEvent>(handler)` method:
7
8```rust,ignore
9// Suppose `InfectionStatus` is a property of the entity `Person`.
10// A type alias for property change events makes code more concise and readable.
11pub type InfectionStatusEvent = PropertyChangeEvent<Person, InfectionStatus>;
12// Suppose we want to execute the following function whenever `InfectionStatus` changes.
13fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent){
14    // ... handle the infection status change event ...
15}
16// We do so by subscribing to this event.
17context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
18```
19
20
21A non-derived property sits on the type-erased side of the boundary of its dependent's `PropertyValueStore`, so it
22needs to somehow trigger the creation of and emit the change events for its dependents in a type-erased way.
23
24Property change events are triggered and collected on the outside of the type-erased `PropertyValueStore` boundary,
25because a non-derived p
26
27*/
28
29use ixa_derive::IxaEvent;
30use smallbox::space::S4;
31use smallbox::SmallBox;
32
33use crate::entity::property::Property;
34use crate::entity::{ContextEntitiesExt, Entity, EntityId};
35use crate::{Context, IxaEvent};
36
37// We choose the size parameter for `PartialPropertyChangeEventBox` based on the assumption that
38// most properties are 64 bits or fewer. The concrete object behind `PartialPropertyChangeEventBox`
39// (the alias for the `SmallBox`) is `PartialPropertyChangeEventCore<E, P>`, which is
40// `#[repr(transparent)]` over `PropertyChangeEvent<E, P>`. That event stores
41//
42// - `EntityId<E>`, one `usize`, so 8 bytes.
43// - `current`: a property value, typically <= 8 bytes
44// - `current`: a property value, typically <= 8 bytes
45//
46// That puts the payload at 24 bytes, with 8-byte alignment. The `S4` size is 4 `usize`s of inline
47// storage, i.e. 32 bytes, and inline storage is used when the payload size and alignment fit. So
48// `S4` comfortably holds the common 24-byte case inline with 8 bytes of slack.
49pub(crate) type PartialPropertyChangeEventBox = SmallBox<dyn PartialPropertyChangeEvent, S4>;
50
51/// Type-erased interface to `PartialPropertyChangeEvent<E, P>`.
52/// Interacts with the index on behalf of the erased type.
53pub(crate) trait PartialPropertyChangeEvent {
54    /// Updates the index with the current property value and emits a change event.
55    fn emit_in_context(&mut self, context: &mut Context);
56}
57
58impl<E: Entity, P: Property<E>> PartialPropertyChangeEvent
59    for PartialPropertyChangeEventCore<E, P>
60{
61    /// Updates the index with the current property value and emits a change event.
62    fn emit_in_context(&mut self, context: &mut Context) {
63        self.0.current = context.get_property(self.0.entity_id);
64
65        {
66            // Update value change counters
67            let property_value_store = context.get_property_value_store::<E, P>();
68            if self.0.current != self.0.previous {
69                for counter in &property_value_store.value_change_counters {
70                    counter
71                        .borrow_mut()
72                        .update(self.0.entity_id, self.0.current, context);
73                }
74            }
75        }
76
77        // Now update the indexes
78        let property_value_store = context.get_property_value_store_mut::<E, P>();
79        // Out with the old
80        property_value_store
81            .index
82            .remove_entity(&self.0.previous.make_canonical(), self.0.entity_id);
83        // In with the new
84        property_value_store
85            .index
86            .add_entity(&self.0.current.make_canonical(), self.0.entity_id);
87
88        // We decided not to do the following check.
89        // See `src/entity/context_extension::ContextEntitiesExt::set_property`.
90        // if self.0.current != self.0.previous {
91        //     context.emit_event(self.to_event());
92        // }
93
94        context.emit_event(self.to_event());
95    }
96}
97
98/// Represents a partially created `PropertyChangeEvent` of a derived property during the computation of property
99/// changes during the update of one of its non-derived property dependencies.
100///
101/// A `PartialPropertyChangeEventCore<E, P>` is layout-compatible with
102/// `PropertyChangeEvent<E, P>`, so converting via `to_event()` does not require an extra heap
103/// allocation.
104#[repr(transparent)]
105pub(crate) struct PartialPropertyChangeEventCore<E: Entity, P: Property<E>>(
106    PropertyChangeEvent<E, P>,
107);
108// We provide blanket impls for these because the compiler isn't smart enough to know
109// `PartialPropertyChangeEvent<E, P>` is always `Copy`/`Clone` if we derive them.
110impl<E: Entity, P: Property<E>> Clone for PartialPropertyChangeEventCore<E, P> {
111    fn clone(&self) -> Self {
112        *self
113    }
114}
115impl<E: Entity, P: Property<E>> Copy for PartialPropertyChangeEventCore<E, P> {}
116
117impl<E: Entity, P: Property<E>> PartialPropertyChangeEventCore<E, P> {
118    pub fn new(entity_id: EntityId<E>, previous_value: P) -> Self {
119        Self(PropertyChangeEvent {
120            entity_id,
121            current: previous_value,
122            previous: previous_value,
123        })
124    }
125
126    pub fn to_event(self) -> PropertyChangeEvent<E, P> {
127        self.0
128    }
129}
130
131/// Emitted when a new entity is created.
132/// These should not be emitted outside this module.
133#[derive(IxaEvent)]
134#[allow(clippy::manual_non_exhaustive)]
135pub struct EntityCreatedEvent<E: Entity> {
136    /// The [`EntityId<E>`] of the new entity.
137    pub entity_id: EntityId<E>,
138}
139// We provide blanket impls for these because the compiler isn't smart enough to know
140// this type is always `Copy`/`Clone` if we derive them.
141impl<E: Entity> Copy for EntityCreatedEvent<E> {}
142impl<E: Entity> Clone for EntityCreatedEvent<E> {
143    fn clone(&self) -> Self {
144        *self
145    }
146}
147
148impl<E: Entity> EntityCreatedEvent<E> {
149    pub fn new(entity_id: EntityId<E>) -> Self {
150        Self { entity_id }
151    }
152}
153
154/// Emitted when a property is updated.
155/// These should not be emitted outside this module.
156#[derive(IxaEvent)]
157#[allow(clippy::manual_non_exhaustive)]
158pub struct PropertyChangeEvent<E: Entity, P: Property<E>> {
159    /// The [`EntityId<E>`] that changed
160    pub entity_id: EntityId<E>,
161    /// The new value
162    pub current: P,
163    /// The old value
164    pub previous: P,
165}
166// We provide blanket impls for these because the compiler isn't smart enough to know
167// this type is always `Copy`/`Clone` if we derive them.
168impl<E: Entity, P: Property<E>> Clone for PropertyChangeEvent<E, P> {
169    fn clone(&self) -> Self {
170        *self
171    }
172}
173impl<E: Entity, P: Property<E>> Copy for PropertyChangeEvent<E, P> {}
174
175#[cfg(test)]
176mod tests {
177    use std::cell::RefCell;
178    use std::rc::Rc;
179
180    use super::*;
181    use crate::{define_derived_property, define_entity, define_property, with, Context};
182
183    define_entity!(Person);
184
185    define_property!(struct Age(u8), Person );
186
187    // define_global_property!(Threshold, u8);
188
189    // An enum
190    define_derived_property!(
191        enum AgeGroup {
192            Child,
193            Adult,
194        },
195        Person,
196        [Age], // Depends only on age
197        [],    // No global dependencies
198        |age| {
199            let age: Age = age;
200            if age.0 < 18 {
201                AgeGroup::Child
202            } else {
203                AgeGroup::Adult
204            }
205        }
206    );
207
208    define_property!(
209        enum RiskCategory {
210            High,
211            Low,
212        },
213        Person
214    );
215
216    define_property!(struct IsRunner(bool), Person, default_const = IsRunner(false));
217
218    define_property!(struct RunningShoes(u8), Person );
219
220    #[test]
221    fn observe_entity_addition() {
222        let mut context = Context::new();
223
224        let flag = Rc::new(RefCell::new(false));
225        let flag_clone = flag.clone();
226        context.subscribe_to_event(move |_context, event: EntityCreatedEvent<Person>| {
227            *flag_clone.borrow_mut() = true;
228            assert_eq!(event.entity_id.0, 0);
229        });
230
231        let _ = context
232            .add_entity::<Person, _>(with!(Person, Age(18), RunningShoes(33), RiskCategory::Low))
233            .unwrap();
234        context.execute();
235        assert!(*flag.borrow());
236    }
237
238    #[test]
239    fn observe_entity_property_change() {
240        let mut context = Context::new();
241
242        let flag = Rc::new(RefCell::new(false));
243        let flag_clone = flag.clone();
244        context.subscribe_to_event(
245            move |_context, event: PropertyChangeEvent<Person, RiskCategory>| {
246                *flag_clone.borrow_mut() = true;
247                assert_eq!(event.entity_id.0, 0, "Entity id is correct");
248                assert_eq!(
249                    event.previous,
250                    RiskCategory::Low,
251                    "Previous value is correct"
252                );
253                assert_eq!(
254                    event.current,
255                    RiskCategory::High,
256                    "Current value is correct"
257                );
258            },
259        );
260
261        let person_id = context
262            .add_entity(with!(Person, Age(9), RunningShoes(33), RiskCategory::Low))
263            .unwrap();
264
265        context.set_property(person_id, RiskCategory::High);
266        context.execute();
267        assert!(*flag.borrow());
268    }
269
270    #[test]
271    fn observe_entity_property_change_with_set() {
272        let mut context = Context::new();
273
274        let flag = Rc::new(RefCell::new(false));
275        let flag_clone = flag.clone();
276        context.subscribe_to_event(
277            move |_context, _event: PropertyChangeEvent<Person, RunningShoes>| {
278                *flag_clone.borrow_mut() = true;
279            },
280        );
281        // Does not emit a change event.
282        let person_id = context
283            .add_entity(with!(Person, Age(9), RunningShoes(33), RiskCategory::Low))
284            .unwrap();
285        // Emits a change event.
286        context.set_property(person_id, RunningShoes(42));
287        context.execute();
288        assert!(*flag.borrow());
289    }
290
291    #[test]
292    fn get_entity_property_change_event() {
293        let mut context = Context::new();
294        let person = context
295            .add_entity(with!(Person, Age(17), RunningShoes(33), RiskCategory::Low))
296            .unwrap();
297
298        let flag = Rc::new(RefCell::new(false));
299
300        let flag_clone = flag.clone();
301        context.subscribe_to_event(
302            move |_context, event: PropertyChangeEvent<Person, AgeGroup>| {
303                assert_eq!(event.entity_id.0, 0);
304                assert_eq!(event.previous, AgeGroup::Child);
305                assert_eq!(event.current, AgeGroup::Adult);
306                *flag_clone.borrow_mut() = true;
307            },
308        );
309        context.set_property(person, Age(18));
310        context.execute();
311        assert!(*flag.borrow());
312    }
313
314    #[test]
315    fn test_person_property_change_event_no_people() {
316        let mut context = Context::new();
317        // Non derived person property -- no problems
318        context.subscribe_to_event(|_context, _event: PropertyChangeEvent<Person, IsRunner>| {
319            unreachable!();
320        });
321
322        // Derived person property -- can't add an event without people being present
323        context.subscribe_to_event(|_context, _event: PropertyChangeEvent<Person, AgeGroup>| {
324            unreachable!();
325        });
326    }
327}