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#[cfg(feature = "wasm")]
19use wasm_bindgen::JsCast;
20
21pub trait Model: Sized {
24 type View: View;
25 type Mutable: Mutable;
26
27 #[cfg(feature = "wasm")]
31 type RefWrapper: From<crate::property::Ref<Self>> + Into<crate::property::Ref<Self>>;
32
33 fn collection() -> CollectionId;
34 fn initialize_new_entity(&self, entity: &Entity);
37}
38
39pub trait View {
41 type Model: Model;
42 type Mutable: Mutable;
43 fn id(&self) -> EntityId { self.entity().id() }
44
45 fn collection() -> CollectionId { <Self::Model as Model>::collection() }
46 fn entity(&self) -> &Entity;
47 fn from_entity(inner: Entity) -> Self;
48 fn to_model(&self) -> Result<Self::Model, PropertyError>;
49}
50
51#[derive(Debug)]
53pub struct MutableBorrow<'rec, T: Mutable> {
54 mutable: T,
55 _entity_ref: &'rec Entity,
56}
57
58impl<'rec, T: Mutable> MutableBorrow<'rec, T> {
59 pub fn new(entity_ref: &'rec Entity) -> Self { Self { mutable: T::new(entity_ref.clone()), _entity_ref: entity_ref } }
60
61 pub fn into_core(self) -> T { self.mutable }
63}
64
65impl<'rec, T: Mutable> std::ops::Deref for MutableBorrow<'rec, T> {
66 type Target = T;
67 fn deref(&self) -> &Self::Target { &self.mutable }
68}
69
70impl<'rec, T: Mutable> std::ops::DerefMut for MutableBorrow<'rec, T> {
71 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.mutable }
72}
73
74pub trait Mutable {
77 type Model: Model;
78 type View: View;
79 fn id(&self) -> EntityId { self.entity().id() }
80 fn collection() -> CollectionId { <Self::Model as Model>::collection() }
81
82 fn entity(&self) -> &Entity;
83 fn new(entity: Entity) -> Self
84 where Self: Sized;
85
86 fn state(&self) -> Result<State, StateError> { self.entity().to_state() }
87
88 fn read(&self) -> Self::View {
89 let inner = self.entity();
90
91 let new_inner = match &inner.kind {
92 crate::entity::EntityKind::Transacted { upstream, .. } => upstream.clone(),
94 crate::entity::EntityKind::Primary => inner.clone(),
96 };
97
98 Self::View::from_entity(new_inner)
99 }
100}
101
102#[doc(hidden)]
104#[cfg(feature = "wasm")]
105pub fn wasm_prop<T>(result: Result<T, PropertyError>, property: &'static str, model: &'static str) -> Result<T, wasm_bindgen::JsValue> {
106 result.map_err(|err| match err {
107 PropertyError::Missing => wasm_bindgen::JsValue::from_str(&format!("property '{}' is missing in model '{}'", property, model)),
108 _ => wasm_bindgen::JsValue::from_str(&err.to_string()),
109 })
110}
111
112#[doc(hidden)]
115pub fn view_subscribe<V, F>(view: &V, listener: F) -> ankurah_signals::SubscriptionGuard
116where
117 V: ankurah_signals::Signal + View + Clone + Send + Sync + 'static,
118 F: ankurah_signals::subscribe::IntoSubscribeListener<V>,
119{
120 let listener = listener.into_subscribe_listener();
121 let view_clone = view.clone();
122 let subscription = view.listen(Arc::new(move |_| {
123 listener(view_clone.clone());
125 }));
126 ankurah_signals::SubscriptionGuard::new(subscription)
127}
128
129#[doc(hidden)]
130pub fn view_subscribe_no_clone<V, F>(view: &V, listener: F) -> ankurah_signals::SubscriptionGuard
131where
132 V: ankurah_signals::Signal + View + Send + Sync + 'static,
133 F: ankurah_signals::subscribe::IntoSubscribeListener<()>,
134{
135 let listener = listener.into_subscribe_listener();
136 let subscription = view.listen(Arc::new(move |_| {
137 listener(());
138 }));
139 ankurah_signals::SubscriptionGuard::new(subscription)
140}
141
142#[doc(hidden)]
146#[cfg(feature = "wasm")]
147pub fn js_preprocess_ref_field(obj: &wasm_bindgen::JsValue, field_name: &str) -> Result<(), wasm_bindgen::JsValue> {
148 let field_key = wasm_bindgen::JsValue::from_str(field_name);
149 if let Ok(v) = js_sys::Reflect::get(obj, &field_key) {
150 if v.as_string().is_some() {
152 return Ok(());
153 }
154
155 let id_key = wasm_bindgen::JsValue::from_str("id");
157 if let Ok(id_value) = js_sys::Reflect::get(&v, &id_key) {
158 let base64_key = wasm_bindgen::JsValue::from_str("to_base64");
160 if let Ok(to_base64_fn) = js_sys::Reflect::get(&id_value, &base64_key) {
161 if let Some(func) = to_base64_fn.dyn_ref::<js_sys::Function>() {
162 if let Ok(result) = func.call0(&id_value) {
163 if let Some(id_str) = result.as_string() {
164 js_sys::Reflect::set(obj, &field_key, &wasm_bindgen::JsValue::from_str(&id_str))?;
165 return Ok(());
166 }
167 }
168 }
169 }
170 }
171
172 if !v.is_undefined() && !v.is_null() {
174 return Err(wasm_bindgen::JsValue::from_str(&format!("Field '{}' must be a View, Ref, or base64 string", field_name)));
175 }
176 }
177 Ok(())
178}
179
180#[doc(hidden)]
183#[cfg(feature = "wasm")]
184pub fn js_resultset_map<V>(resultset: &crate::resultset::ResultSet<V>, callback: &js_sys::Function) -> js_sys::Array
185where V: View + Clone + 'static + Into<wasm_bindgen::JsValue> {
186 use ankurah_signals::Get;
187 let items = resultset.get();
188 let result_array = js_sys::Array::new();
189
190 for item in items {
191 let js_item = item.into();
192 if let Ok(mapped_value) = callback.call1(&wasm_bindgen::JsValue::NULL, &js_item) {
193 result_array.push(&mapped_value);
194 }
195 }
196
197 result_array
198}
199
200#[doc(hidden)]
202#[cfg(feature = "wasm")]
203pub fn js_livequery_subscribe<V, W, F>(
204 livequery: &crate::livequery::LiveQuery<V>,
205 callback: js_sys::Function,
206 immediate: bool,
207 wrap_changeset: F,
208) -> ankurah_signals::SubscriptionGuard
209where
210 V: View + Clone + Send + Sync + 'static,
211 W: Into<wasm_bindgen::JsValue>,
212 F: Fn(crate::changes::ChangeSet<V>) -> W + Send + Sync + 'static,
213{
214 use ankurah_signals::{Peek, Subscribe};
215
216 if immediate {
218 let current_items = livequery.peek();
219 let changes = current_items.into_iter().map(|item| crate::changes::ItemChange::Add { item, events: vec![] }).collect();
220 let initial_changeset = crate::changes::ChangeSet { resultset: livequery.resultset(), changes };
221 let wrapped = wrap_changeset(initial_changeset);
222 let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &wrapped.into());
223 }
224
225 let callback = ::send_wrapper::SendWrapper::new(callback);
227 livequery.subscribe(move |changeset: crate::changes::ChangeSet<V>| {
228 let wrapped_changeset = wrap_changeset(changeset);
229 let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &wrapped_changeset.into());
230 })
231}