assemblage_view/
bindings.rs

1//! Query and update functions for a wasm frontend backed by an AssemblageDB.
2//!
3//! These bindings expose a DB container that can be used to
4//! [refresh](DbContainer::refresh), [sync](DbContainer::sync),
5//! [broadcast](DbContainer::broadcast) and [fetch](DbContainer::fetch) nodes
6//! from JS. All methods return promises, the resulting
7//! [tiles](crate::model::Tile) are serialized as JS objects using `serde_json`.
8//!
9//! Note that most of the wasm implementations have slightly different function
10//! signatures than their native counterparts, which is caused by the need for
11//! serialization between wasm and JS.
12
13use crate::{
14    markup::{markup_to_node, DeserializationError},
15    model::Tile,
16    DbView,
17};
18use assemblage_db::{
19    broadcast::BroadcastId,
20    data::{Child, Id, Layout, Node},
21    Db,
22};
23use assemblage_kv::storage::{self, PlatformStorage, Storage};
24use log::info;
25use serde::{Deserialize, Serialize};
26use std::{
27    convert::{TryFrom, TryInto},
28    rc::Rc,
29};
30
31#[cfg(target_arch = "wasm32")]
32use wasm_bindgen::prelude::*;
33#[cfg(target_arch = "wasm32")]
34use wasm_bindgen_futures::future_to_promise;
35
36/// An opaque handle to an AssemblageDB that can be used to query and update
37/// nodes.
38#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
39pub struct DbContainer {
40    wrapped: Rc<Db<PlatformStorage>>,
41}
42
43/// Opens and returns the DB with the specified name.
44#[cfg(target_arch = "wasm32")]
45#[wasm_bindgen]
46pub async fn open(name: String) -> Result<DbContainer, JsValue> {
47    #[cfg(feature = "console_error_panic_hook")]
48    console_error_panic_hook::set_once();
49    let _ignored = console_log::init();
50    info!("Opening AssemblageDB \"{}\"", &name);
51    let storage = storage::open(&name).await?;
52    Ok(DbContainer {
53        wrapped: Rc::new(Db::open(storage).await?),
54    })
55}
56
57/// Opens and returns the DB with the specified name.
58#[cfg(not(target_arch = "wasm32"))]
59pub async fn open(name: String) -> crate::Result<DbContainer> {
60    let _ignored = env_logger::try_init();
61    info!("Opening AssemblageDB \"{}\"", &name);
62    let storage = storage::open(&name).await?;
63    Ok(DbContainer {
64        wrapped: Rc::new(Db::open(storage).await?),
65    })
66}
67
68#[cfg(target_arch = "wasm32")]
69#[wasm_bindgen]
70impl DbContainer {
71    /// Looks up the specified id in the DB and returns it rendered as a tile.
72    ///
73    /// Ids prefixed with `broadcast:` will be interpreted as broadcast ids and
74    /// the corresponding broadcast will be fetched and updated (if the
75    /// broadcast does not exist in the DB, a subscription will be created),
76    /// before refreshing and returning the root node of the broadcast as a
77    /// tile.
78    pub fn refresh(&self, id: String) -> js_sys::Promise {
79        let db = Rc::clone(&self.wrapped);
80        future_to_promise(async move {
81            let tile = refresh(db, id).await?;
82            Ok(JsValue::from_serde(&tile).unwrap())
83        })
84    }
85
86    /// Persists a tile in the DB and returns its updated version (which might
87    /// include additional branches for example).
88    pub fn sync(&self, id: Option<String>, tile: JsValue) -> js_sys::Promise {
89        let db = Rc::clone(&self.wrapped);
90        future_to_promise(async move {
91            let tile: Result<Vec<SyncedSection>, serde_json::Error> = tile.into_serde();
92            match tile {
93                Ok(tile) => {
94                    let updated_tile = sync(db, id, tile).await?;
95                    Ok(JsValue::from_serde(&updated_tile).unwrap())
96                }
97                Err(e) => Err(JsValue::from_str(&format!("{}", e))),
98            }
99        })
100    }
101
102    /// Uploads the specified id and all of its descendants as a broadcast that
103    /// can be shared via its url.
104    ///
105    /// If an active broadcast for this id already exists, the broadcast will be
106    /// updated by transmitting only the changes since the last upload.
107    pub fn broadcast(&self, id: String) -> js_sys::Promise {
108        let db = Rc::clone(&self.wrapped);
109        future_to_promise(async move {
110            let updated_tile = broadcast(db, id).await?;
111            Ok(JsValue::from_serde(&updated_tile).unwrap())
112        })
113    }
114
115    /// Updates broadcast nodes by fetching the most recent version of the
116    /// broadcast with the specified id and returning it as a tile.
117    pub fn fetch(&self, id: String) -> js_sys::Promise {
118        let db = Rc::clone(&self.wrapped);
119        future_to_promise(async move {
120            match id.as_str().try_into() {
121                Ok(id) => {
122                    let mut current = db.current().await;
123                    current.fetch_broadcast(&BroadcastId::from(id)).await?;
124                    let tile = current.tile(id).await?;
125                    current.commit().await?;
126                    Ok(JsValue::from_serde(&tile).unwrap())
127                }
128                Err(_) => {
129                    let e = BroadcastError::InvalidId(id);
130                    Err(JsValue::from_str(&format!("{:?}", e)))
131                }
132            }
133        })
134    }
135}
136
137#[cfg(not(target_arch = "wasm32"))]
138impl DbContainer {
139    /// Looks up the specified id in the DB and returns it rendered as a tile.
140    ///
141    /// Ids prefixed with `broadcast:` will be interpreted as broadcast ids and
142    /// the corresponding broadcast will be fetched and updated (if the
143    /// broadcast does not exist in the DB, a subscription will be created),
144    /// before refreshing and returning the root node of the broadcast as a
145    /// tile.
146    pub async fn refresh(&self, id: String) -> Result<Tile, RefreshError> {
147        let db = Rc::clone(&self.wrapped);
148        Ok(refresh(db, id).await?)
149    }
150
151    /// Persists a tile in the DB and returns its updated version (which might
152    /// include additional branches for example).
153    pub async fn sync(
154        &self,
155        id: Option<String>,
156        tile: Vec<SyncedSection>,
157    ) -> Result<Tile, SyncError> {
158        let db = Rc::clone(&self.wrapped);
159        Ok(sync(db, id, tile).await?)
160    }
161
162    /// Uploads the specified id and all of its descendants as a broadcast that
163    /// can be shared via its url.
164    ///
165    /// If an active broadcast for this id already exists, the broadcast will be
166    /// updated by transmitting only the changes since the last upload.
167    pub async fn broadcast(&self, id: String) -> Result<Tile, BroadcastError> {
168        let db = Rc::clone(&self.wrapped);
169        Ok(broadcast(db, id).await?)
170    }
171
172    /// Updates broadcast nodes by fetching the most recent version of the
173    /// broadcast with the specified id and returning it as a tile.
174    pub async fn fetch(&self, id: String) -> Result<Tile, BroadcastError> {
175        match id.as_str().try_into() {
176            Ok(id) => {
177                let db = Rc::clone(&self.wrapped);
178                let mut current = db.current().await;
179                current.fetch_broadcast(&BroadcastId::from(id)).await?;
180                let tile = current.tile(id).await?;
181                current.commit().await?;
182                Ok(tile)
183            }
184            Err(_) => Err(BroadcastError::InvalidId(id)),
185        }
186    }
187}
188
189/// The error type raised if the refreshed id is invalid or the view could not
190/// be refreshed.
191#[derive(Debug)]
192pub enum RefreshError {
193    /// The specified broadcast string is not a valid broadcast UUID.
194    InvalidBroadcastId(String),
195    /// The specified id string is not a valid DB UUID.
196    InvalidId(String),
197    /// The refreshed node could not be rendered as a tile.
198    ViewError(crate::Error),
199}
200
201impl<E: Into<crate::Error>> From<E> for RefreshError {
202    fn from(e: E) -> Self {
203        Self::ViewError(e.into())
204    }
205}
206
207#[cfg(target_arch = "wasm32")]
208impl From<RefreshError> for JsValue {
209    fn from(e: RefreshError) -> Self {
210        JsValue::from_str(&format!("{:?}", e))
211    }
212}
213
214async fn refresh<S: Storage>(db: Rc<Db<S>>, id: String) -> Result<Tile, RefreshError> {
215    if id.starts_with("broadcast:") {
216        let id = id.replace("broadcast:", "");
217        match Id::try_from(id.as_str()) {
218            Ok(id) => {
219                let mut current = db.current().await;
220                let tile = current.tile_from_broadcast(&BroadcastId::from(id)).await?;
221                current.commit().await?;
222                Ok(tile)
223            }
224            Err(_) => Err(RefreshError::InvalidBroadcastId(id)),
225        }
226    } else {
227        match id.as_str().try_into() {
228            Ok(id) => {
229                let current = db.current().await;
230                let tile = current.tile(id).await?;
231                current.commit().await?;
232                Ok(tile)
233            }
234            Err(_) => Err(RefreshError::InvalidId(id)),
235        }
236    }
237}
238
239/// A section of a tile that should be persisted in the DB.
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
241#[serde(tag = "type")]
242pub enum SyncedSection {
243    /// The version in the DB should be reused, as no changes have been made.
244    Existing {
245        /// The id of the section's node in the DB.
246        id: Id,
247    },
248    /// The section should become a new link to an existing node.
249    Linked {
250        /// The id of the linked node in the DB.
251        id: Id,
252    },
253    /// The section should be replaced in the DB with an edited version.
254    Edited {
255        /// The edited blocks.
256        blocks: Vec<SyncedSubsection>,
257    },
258}
259
260/// A subsection of a tile that should be synced with the DB.
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
262#[serde(tag = "type")]
263pub enum SyncedSubsection {
264    /// A block of markup text.
265    Text {
266        /// The markup to construct the node tree of the block.
267        markup: String,
268    },
269}
270
271/// The error type raised if the edited blocks cannot be deserialized or
272/// inserted.
273#[derive(Debug)]
274pub enum SyncError {
275    /// The specified id belongs to an externally imported broadcast and cannot
276    /// be edited.
277    ExternalId(String),
278    /// The specified id string is not a valid DB uuid.
279    InvalidId(String),
280    /// One of the blocks could not be deserialized from markup into a node.
281    DeserializationError(DeserializationError),
282    /// One of the sections nodes could not be found or inserted.
283    DbError(assemblage_db::Error),
284    /// The swapped sections could not be rendered as a tile.
285    ViewError(crate::Error),
286}
287
288impl<E: Into<assemblage_db::Error>> From<E> for SyncError {
289    fn from(e: E) -> Self {
290        Self::DbError(e.into())
291    }
292}
293
294impl From<DeserializationError> for SyncError {
295    fn from(e: DeserializationError) -> Self {
296        Self::DeserializationError(e)
297    }
298}
299
300impl From<crate::Error> for SyncError {
301    fn from(e: crate::Error) -> Self {
302        Self::ViewError(e)
303    }
304}
305
306#[cfg(target_arch = "wasm32")]
307impl From<SyncError> for JsValue {
308    fn from(e: SyncError) -> Self {
309        JsValue::from_str(&format!("{:?}", e))
310    }
311}
312
313async fn sync<S>(
314    db: Rc<Db<S>>,
315    id: Option<String>,
316    s: Vec<SyncedSection>,
317) -> Result<Tile, SyncError>
318where
319    S: Storage,
320{
321    let id = match id {
322        None => None,
323        Some(id) => match id.as_str().try_into() {
324            Ok(id) => Some(id),
325            Err(_) => return Err(SyncError::InvalidId(id)),
326        },
327    };
328    let mut db = db.current().await;
329    let mut children = Vec::with_capacity(s.len());
330    for section in s.iter() {
331        children.push(match section {
332            SyncedSection::Existing { id } => Child::Lazy(*id),
333            SyncedSection::Linked { id } => Child::Eager(Node::list(Layout::Chain, vec![*id])),
334            SyncedSection::Edited { blocks } => {
335                let mut children = Vec::with_capacity(blocks.len());
336                for b in blocks.iter() {
337                    match b {
338                        SyncedSubsection::Text { markup } => {
339                            children.push(markup_to_node(markup)?);
340                        }
341                    }
342                }
343                Child::Eager(Node::list(Layout::Page, children))
344            }
345        })
346    }
347    let replacement = Node::list(Layout::Page, children);
348    let id = match id {
349        None => db.add(replacement).await?,
350        Some(id) => {
351            db.swap(id, replacement).await?;
352            id
353        }
354    };
355    let result = db.tile(id).await?;
356    db.update_broadcasts(id).await?;
357    db.commit().await?;
358    Ok(result)
359}
360
361/// The error type raised if the tile with the specified id could not be
362/// broadcast.
363#[derive(Debug)]
364pub enum BroadcastError {
365    /// The specified id string is not a valid DB uuid.
366    InvalidId(String),
367    /// The broadcast failed due to a DB error.
368    DbError(assemblage_db::Error),
369    /// The broadcast succeeded, but the refreshed tile could not be displayed.
370    ViewError(crate::Error),
371}
372
373impl<E: Into<assemblage_db::Error>> From<E> for BroadcastError {
374    fn from(e: E) -> Self {
375        Self::DbError(e.into())
376    }
377}
378
379impl From<crate::Error> for BroadcastError {
380    fn from(e: crate::Error) -> Self {
381        Self::ViewError(e)
382    }
383}
384
385#[cfg(target_arch = "wasm32")]
386impl From<BroadcastError> for JsValue {
387    fn from(e: BroadcastError) -> Self {
388        JsValue::from_str(&format!("{:?}", e))
389    }
390}
391
392async fn broadcast<S>(db: Rc<Db<S>>, id: String) -> Result<Tile, BroadcastError>
393where
394    S: Storage,
395{
396    let id = match id.as_str().try_into() {
397        Ok(id) => id,
398        Err(_) => return Err(BroadcastError::InvalidId(id)),
399    };
400    let mut db = db.current().await;
401    db.publish_broadcast(id).await?;
402    let result = db.tile(id).await?;
403    db.commit().await?;
404    Ok(result)
405}