ankurah_core/
model.rs

1pub mod tsify;
2
3use std::sync::Arc;
4
5use ankurah_proto::{CollectionId, EntityId, State};
6
7use crate::entity::Entity;
8use crate::error::StateError;
9
10use crate::property::PropertyError;
11
12use anyhow::Result;
13
14#[cfg(feature = "wasm")]
15use js_sys;
16#[cfg(feature = "wasm")]
17use wasm_bindgen;
18
19/// A model is a struct that represents the present values for a given entity
20/// Schema is defined primarily by the Model object, and the View is derived from that via macro.
21pub trait Model {
22    type View: View;
23    type Mutable: Mutable;
24    fn collection() -> CollectionId;
25    // TODO - this seems to be necessary, but I don't understand why
26    // Backend fields should be getting initialized on demand when the values are set
27    fn initialize_new_entity(&self, entity: &Entity);
28}
29
30/// A read only view of an Entity which offers typed accessors
31pub trait View {
32    type Model: Model;
33    type Mutable: Mutable;
34    fn id(&self) -> EntityId { self.entity().id() }
35
36    fn collection() -> CollectionId { <Self::Model as Model>::collection() }
37    fn entity(&self) -> &Entity;
38    fn from_entity(inner: Entity) -> Self;
39    fn to_model(&self) -> Result<Self::Model, PropertyError>;
40}
41
42/// A lifetime-constrained wrapper around a Mutable for compile-time transaction safety
43#[derive(Debug)]
44pub struct MutableBorrow<'rec, T: Mutable> {
45    mutable: T,
46    _entity_ref: &'rec Entity,
47}
48
49impl<'rec, T: Mutable> MutableBorrow<'rec, T> {
50    pub fn new(entity_ref: &'rec Entity) -> Self { Self { mutable: T::new(entity_ref.clone()), _entity_ref: entity_ref } }
51
52    /// Extract the core mutable (for WASM usage)
53    pub fn into_core(self) -> T { self.mutable }
54}
55
56impl<'rec, T: Mutable> std::ops::Deref for MutableBorrow<'rec, T> {
57    type Target = T;
58    fn deref(&self) -> &Self::Target { &self.mutable }
59}
60
61impl<'rec, T: Mutable> std::ops::DerefMut for MutableBorrow<'rec, T> {
62    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.mutable }
63}
64
65/// A mutable Model instance for an Entity with typed accessors.
66/// It is associated with a transaction, and may not outlive said transaction.
67pub trait Mutable {
68    type Model: Model;
69    type View: View;
70    fn id(&self) -> EntityId { self.entity().id() }
71    fn collection() -> CollectionId { <Self::Model as Model>::collection() }
72
73    fn entity(&self) -> &Entity;
74    fn new(entity: Entity) -> Self
75    where Self: Sized;
76
77    fn state(&self) -> Result<State, StateError> { self.entity().to_state() }
78
79    fn read(&self) -> Self::View {
80        let inner = self.entity();
81
82        let new_inner = match &inner.kind {
83            // If there is an upstream, use it
84            crate::entity::EntityKind::Transacted { upstream, .. } => upstream.clone(),
85            // Else we're a new Entity, and we have to rely on the commit to add this to the node
86            crate::entity::EntityKind::Primary => inner.clone(),
87        };
88
89        Self::View::from_entity(new_inner)
90    }
91}
92
93// Helper function for Subscribe implementations in generated Views
94// don't document this
95#[doc(hidden)]
96pub fn view_subscribe<V, F>(view: &V, listener: F) -> ankurah_signals::SubscriptionGuard
97where
98    V: ankurah_signals::Signal + View + Clone + Send + Sync + 'static,
99    F: ankurah_signals::subscribe::IntoSubscribeListener<V>,
100{
101    let listener = listener.into_subscribe_listener();
102    let view_clone = view.clone();
103    let subscription = view.listen(Arc::new(move |_| {
104        // Call the listener with the current view when the broadcast fires
105        listener(view_clone.clone());
106    }));
107    ankurah_signals::SubscriptionGuard::new(subscription)
108}
109
110#[doc(hidden)]
111pub fn view_subscribe_no_clone<V, F>(view: &V, listener: F) -> ankurah_signals::SubscriptionGuard
112where
113    V: ankurah_signals::Signal + View + Send + Sync + 'static,
114    F: ankurah_signals::subscribe::IntoSubscribeListener<()>,
115{
116    let listener = listener.into_subscribe_listener();
117    let subscription = view.listen(Arc::new(move |_| {
118        listener(());
119    }));
120    ankurah_signals::SubscriptionGuard::new(subscription)
121}
122
123// Helper function for map implementations in generated WASM ResultSet wrappers
124// don't document this
125#[doc(hidden)]
126#[cfg(feature = "wasm")]
127pub fn js_resultset_map<V>(resultset: &crate::resultset::ResultSet<V>, callback: &js_sys::Function) -> js_sys::Array
128where V: View + Clone + 'static + Into<wasm_bindgen::JsValue> {
129    use ankurah_signals::Get;
130    let items = resultset.get();
131    let result_array = js_sys::Array::new();
132
133    for item in items {
134        let js_item = item.into();
135        if let Ok(mapped_value) = callback.call1(&wasm_bindgen::JsValue::NULL, &js_item) {
136            result_array.push(&mapped_value);
137        }
138    }
139
140    result_array
141}
142
143// Helper function for subscribe implementations in generated WASM LiveQuery wrappers
144#[doc(hidden)]
145#[cfg(feature = "wasm")]
146pub fn js_livequery_subscribe<V, W, F>(
147    livequery: &crate::livequery::LiveQuery<V>,
148    callback: js_sys::Function,
149    immediate: bool,
150    wrap_changeset: F,
151) -> ankurah_signals::SubscriptionGuard
152where
153    V: View + Clone + Send + Sync + 'static,
154    W: Into<wasm_bindgen::JsValue>,
155    F: Fn(crate::changes::ChangeSet<V>) -> W + Send + Sync + 'static,
156{
157    use ankurah_signals::{Peek, Subscribe};
158
159    // If immediate, call the callback with current state first
160    if immediate {
161        let current_items = livequery.peek();
162        let changes = current_items.into_iter().map(|item| crate::changes::ItemChange::Add { item, events: vec![] }).collect();
163        let initial_changeset = crate::changes::ChangeSet { resultset: livequery.resultset(), changes };
164        let wrapped = wrap_changeset(initial_changeset);
165        let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &wrapped.into());
166    }
167
168    // Set up the subscription for future changes
169    let callback = ::send_wrapper::SendWrapper::new(callback);
170    livequery.subscribe(move |changeset: crate::changes::ChangeSet<V>| {
171        let wrapped_changeset = wrap_changeset(changeset);
172        let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &wrapped_changeset.into());
173    })
174}