ywasm/
map.rs

1use crate::collection::SharedCollection;
2use crate::js::{Callback, Js};
3use crate::transaction::YTransaction;
4use crate::weak::YWeakLink;
5use crate::{js, ImplicitTransaction};
6use gloo_utils::format::JsValueSerdeExt;
7use std::collections::HashMap;
8use wasm_bindgen::prelude::wasm_bindgen;
9use wasm_bindgen::JsValue;
10use yrs::types::map::MapEvent;
11use yrs::types::{ToJson, TYPE_REFS_MAP};
12use yrs::{DeepObservable, Map, MapRef, Observable, TransactionMut};
13
14/// Collection used to store key-value entries in an unordered manner. Keys are always represented
15/// as UTF-8 strings. Values can be any value type supported by Yrs: JSON-like primitives as well as
16/// shared data types.
17///
18/// In terms of conflict resolution, [Map] uses logical last-write-wins principle, meaning the past
19/// updates are automatically overridden and discarded by newer ones, while concurrent updates made
20/// by different peers are resolved into a single value using document id seniority to establish
21/// order.
22#[wasm_bindgen]
23pub struct YMap(pub(crate) SharedCollection<HashMap<String, JsValue>, MapRef>);
24
25#[wasm_bindgen]
26impl YMap {
27    /// Creates a new preliminary instance of a `YMap` shared data type, with its state
28    /// initialized to provided parameter.
29    ///
30    /// Preliminary instances can be nested into other shared data types such as `YArray` and `YMap`.
31    /// Once a preliminary instance has been inserted this way, it becomes integrated into ywasm
32    /// document store and cannot be nested again: attempt to do so will result in an exception.
33    #[wasm_bindgen(constructor)]
34    pub fn new(init: Option<js_sys::Object>) -> Self {
35        let map = if let Some(object) = init {
36            let mut map = HashMap::new();
37            let entries = js_sys::Object::entries(&object);
38            for tuple in entries.iter() {
39                let tuple = js_sys::Array::from(&tuple);
40                let key = tuple.get(0).as_string().unwrap();
41                let value = tuple.get(1);
42                map.insert(key, value);
43            }
44            map
45        } else {
46            HashMap::new()
47        };
48        YMap(SharedCollection::prelim(map))
49    }
50
51    #[wasm_bindgen(getter, js_name = type)]
52    #[inline]
53    pub fn get_type(&self) -> u8 {
54        TYPE_REFS_MAP
55    }
56
57    /// Gets unique logical identifier of this type, shared across peers collaborating on the same
58    /// document.
59    #[wasm_bindgen(getter, js_name = id)]
60    #[inline]
61    pub fn id(&self) -> crate::Result<JsValue> {
62        self.0.id()
63    }
64
65    /// Returns true if this is a preliminary instance of `YMap`.
66    ///
67    /// Preliminary instances can be nested into other shared data types such as `YArray` and `YMap`.
68    /// Once a preliminary instance has been inserted this way, it becomes integrated into ywasm
69    /// document store and cannot be nested again: attempt to do so will result in an exception.
70    #[wasm_bindgen(getter)]
71    pub fn prelim(&self) -> bool {
72        self.0.is_prelim()
73    }
74
75    /// Checks if current YMap reference is alive and has not been deleted by its parent collection.
76    /// This method only works on already integrated shared types and will return false is current
77    /// type is preliminary (has not been integrated into document).
78    #[wasm_bindgen(js_name = alive)]
79    pub fn alive(&self, txn: &YTransaction) -> bool {
80        self.0.is_alive(txn)
81    }
82
83    /// Returns a number of entries stored within this instance of `YMap`.
84    #[wasm_bindgen(js_name = length)]
85    pub fn length(&self, txn: &ImplicitTransaction) -> crate::Result<u32> {
86        match &self.0 {
87            SharedCollection::Prelim(c) => Ok(c.len() as u32),
88            SharedCollection::Integrated(c) => c.readonly(txn, |c, txn| Ok(c.len(txn))),
89        }
90    }
91
92    /// Converts contents of this `YMap` instance into a JSON representation.
93    #[wasm_bindgen(js_name = toJson)]
94    pub fn to_json(&self, txn: &ImplicitTransaction) -> crate::Result<JsValue> {
95        match &self.0 {
96            SharedCollection::Prelim(c) => {
97                let map = js_sys::Object::new();
98                for (k, v) in c.iter() {
99                    js_sys::Reflect::set(&map, &k.into(), v).unwrap();
100                }
101                Ok(map.into())
102            }
103            SharedCollection::Integrated(c) => c.readonly(txn, |c, txn| {
104                let any = c.to_json(txn);
105                JsValue::from_serde(&any).map_err(|e| JsValue::from_str(&e.to_string()))
106            }),
107        }
108    }
109
110    /// Sets a given `key`-`value` entry within this instance of `YMap`. If another entry was
111    /// already stored under given `key`, it will be overridden with new `value`.
112    #[wasm_bindgen(js_name = set)]
113    pub fn set(
114        &mut self,
115        key: &str,
116        value: JsValue,
117        txn: ImplicitTransaction,
118    ) -> crate::Result<()> {
119        match &mut self.0 {
120            SharedCollection::Prelim(c) => {
121                c.insert(key.to_string(), value);
122                Ok(())
123            }
124            SharedCollection::Integrated(c) => c.mutably(txn, |c, txn| {
125                c.insert(txn, key.to_string(), Js::new(value));
126                Ok(())
127            }),
128        }
129    }
130
131    /// Removes an entry identified by a given `key` from this instance of `YMap`, if such exists.
132    #[wasm_bindgen(method, js_name = delete)]
133    pub fn delete(&mut self, key: &str, txn: ImplicitTransaction) -> crate::Result<()> {
134        match &mut self.0 {
135            SharedCollection::Prelim(c) => {
136                c.remove(key);
137                Ok(())
138            }
139            SharedCollection::Integrated(c) => c.mutably(txn, |c, txn| {
140                c.remove(txn, key);
141                Ok(())
142            }),
143        }
144    }
145
146    /// Returns value of an entry stored under given `key` within this instance of `YMap`,
147    /// or `undefined` if no such entry existed.
148    #[wasm_bindgen(js_name = get)]
149    pub fn get(&self, key: &str, txn: &ImplicitTransaction) -> crate::Result<JsValue> {
150        match &self.0 {
151            SharedCollection::Prelim(c) => {
152                let value = c.get(key);
153                Ok(value.cloned().unwrap_or(JsValue::UNDEFINED))
154            }
155            SharedCollection::Integrated(c) => c.readonly(txn, |c, txn| {
156                let value = c.get(txn, key);
157                match value {
158                    None => Ok(JsValue::UNDEFINED),
159                    Some(value) => Ok(Js::from_value(&value, txn.doc()).into()),
160                }
161            }),
162        }
163    }
164
165    #[wasm_bindgen(js_name = link)]
166    pub fn link(&self, key: &str, txn: &ImplicitTransaction) -> crate::Result<JsValue> {
167        match &self.0 {
168            SharedCollection::Prelim(_) => Err(JsValue::from_str(js::errors::INVALID_PRELIM_OP)),
169            SharedCollection::Integrated(c) => c.readonly(txn, |c, txn| {
170                let link = c.link(txn, key);
171                match link {
172                    Some(link) => Ok(YWeakLink::from_prelim(link, txn.doc().clone()).into()),
173                    None => Err(JsValue::from_str(js::errors::KEY_NOT_FOUND)),
174                }
175            }),
176        }
177    }
178
179    /// Returns an iterator that can be used to traverse over all entries stored within this
180    /// instance of `YMap`. Order of entry is not specified.
181    ///
182    /// Example:
183    ///
184    /// ```javascript
185    /// import YDoc from 'ywasm'
186    ///
187    /// /// document on machine A
188    /// const doc = new YDoc()
189    /// const map = doc.getMap('name')
190    /// const txn = doc.beginTransaction()
191    /// try {
192    ///     map.set(txn, 'key1', 'value1')
193    ///     map.set(txn, 'key2', true)
194    ///
195    ///     for (let [key, value] of map.entries(txn)) {
196    ///         console.log(key, value)
197    ///     }
198    /// } finally {
199    ///     txn.free()
200    /// }
201    /// ```
202    #[wasm_bindgen(js_name = entries)]
203    pub fn entries(&self, txn: &ImplicitTransaction) -> crate::Result<JsValue> {
204        match &self.0 {
205            SharedCollection::Prelim(c) => {
206                let map = js_sys::Object::new();
207                for (k, v) in c.iter() {
208                    js_sys::Reflect::set(&map, &k.into(), v)?;
209                }
210                Ok(map.into())
211            }
212            SharedCollection::Integrated(c) => c.readonly(txn, |c, txn| {
213                let map = js_sys::Object::new();
214                let doc = txn.doc();
215                for (k, v) in c.iter(txn) {
216                    let value = Js::from_value(&v, doc);
217                    js_sys::Reflect::set(&map, &k.into(), &value.into())?;
218                }
219                Ok(map.into())
220            }),
221        }
222    }
223
224    /// Subscribes to all operations happening over this instance of `YMap`. All changes are
225    /// batched and eventually triggered during transaction commit phase.
226    #[wasm_bindgen(js_name = observe)]
227    pub fn observe(&mut self, callback: js_sys::Function) -> crate::Result<()> {
228        match &self.0 {
229            SharedCollection::Prelim(_) => {
230                Err(JsValue::from_str(crate::js::errors::INVALID_PRELIM_OP))
231            }
232            SharedCollection::Integrated(c) => {
233                let txn = c.transact()?;
234                let array = c.resolve(&txn)?;
235                let abi = callback.subscription_key();
236                array.observe_with(abi, move |txn, e| {
237                    let e = YMapEvent::new(e, txn);
238                    let txn = YTransaction::from_ref(txn);
239                    callback
240                        .call2(&JsValue::UNDEFINED, &e.into(), &txn.into())
241                        .unwrap();
242                });
243                Ok(())
244            }
245        }
246    }
247
248    /// Unsubscribes a callback previously subscribed with `observe` method.
249    #[wasm_bindgen(js_name = unobserve)]
250    pub fn unobserve(&mut self, callback: js_sys::Function) -> crate::Result<bool> {
251        match &self.0 {
252            SharedCollection::Prelim(_) => {
253                Err(JsValue::from_str(crate::js::errors::INVALID_PRELIM_OP))
254            }
255            SharedCollection::Integrated(c) => {
256                let txn = c.transact()?;
257                let shared_ref = c.resolve(&txn)?;
258                let abi = callback.subscription_key();
259                Ok(shared_ref.unobserve(abi))
260            }
261        }
262    }
263
264    /// Subscribes to all operations happening over this Y shared type, as well as events in
265    /// shared types stored within this one. All changes are batched and eventually triggered
266    /// during transaction commit phase.
267    #[wasm_bindgen(js_name = observeDeep)]
268    pub fn observe_deep(&mut self, callback: js_sys::Function) -> crate::Result<()> {
269        match &self.0 {
270            SharedCollection::Prelim(_) => {
271                Err(JsValue::from_str(crate::js::errors::INVALID_PRELIM_OP))
272            }
273            SharedCollection::Integrated(c) => {
274                let txn = c.transact()?;
275                let shared_ref = c.resolve(&txn)?;
276                let abi = callback.subscription_key();
277                shared_ref.observe_deep_with(abi, move |txn, e| {
278                    let e = crate::js::convert::events_into_js(txn, e);
279                    let txn = YTransaction::from_ref(txn);
280                    callback
281                        .call2(&JsValue::UNDEFINED, &e, &txn.into())
282                        .unwrap();
283                });
284                Ok(())
285            }
286        }
287    }
288
289    /// Unsubscribes a callback previously subscribed with `observeDeep` method.
290    #[wasm_bindgen(js_name = unobserveDeep)]
291    pub fn unobserve_deep(&mut self, callback: js_sys::Function) -> crate::Result<bool> {
292        match &self.0 {
293            SharedCollection::Prelim(_) => {
294                Err(JsValue::from_str(crate::js::errors::INVALID_PRELIM_OP))
295            }
296            SharedCollection::Integrated(c) => {
297                let txn = c.transact()?;
298                let shared_ref = c.resolve(&txn)?;
299                let abi = callback.subscription_key();
300                Ok(shared_ref.unobserve_deep(abi))
301            }
302        }
303    }
304}
305
306/// Event generated by `YMap.observe` method. Emitted during transaction commit phase.
307#[wasm_bindgen]
308pub struct YMapEvent {
309    inner: &'static MapEvent,
310    txn: &'static TransactionMut<'static>,
311    target: Option<JsValue>,
312    keys: Option<JsValue>,
313}
314
315#[wasm_bindgen]
316impl YMapEvent {
317    pub(crate) fn new<'doc>(event: &MapEvent, txn: &TransactionMut<'doc>) -> Self {
318        let inner: &'static MapEvent = unsafe { std::mem::transmute(event) };
319        let txn: &'static TransactionMut<'static> = unsafe { std::mem::transmute(txn) };
320        YMapEvent {
321            inner,
322            txn,
323            target: None,
324            keys: None,
325        }
326    }
327
328    #[wasm_bindgen(getter)]
329    pub fn origin(&mut self) -> JsValue {
330        let origin = self.txn.origin();
331        if let Some(origin) = origin {
332            Js::from(origin).into()
333        } else {
334            JsValue::UNDEFINED
335        }
336    }
337
338    /// Returns an array of keys and indexes creating a path from root type down to current instance
339    /// of shared type (accessible via `target` getter).
340    #[wasm_bindgen]
341    pub fn path(&self) -> JsValue {
342        crate::js::convert::path_into_js(self.inner.path())
343    }
344
345    /// Returns a current shared type instance, that current event changes refer to.
346    #[wasm_bindgen(getter)]
347    pub fn target(&mut self) -> JsValue {
348        let target = self.inner.target();
349        let doc = self.txn.doc();
350        let js = self.target.get_or_insert_with(|| {
351            YMap(SharedCollection::integrated(target.clone(), doc.clone())).into()
352        });
353        js.clone()
354    }
355
356    /// Returns a list of key-value changes made over corresponding `YMap` collection within
357    /// bounds of current transaction. These changes follow a format:
358    ///
359    /// - { action: 'add'|'update'|'delete', oldValue: any|undefined, newValue: any|undefined }
360    #[wasm_bindgen(getter)]
361    pub fn keys(&mut self) -> crate::Result<JsValue> {
362        if let Some(keys) = &self.keys {
363            Ok(keys.clone())
364        } else {
365            let txn = self.txn;
366            let keys = self.inner.keys(txn);
367            let result = js_sys::Object::new();
368            for (key, value) in keys.iter() {
369                let key = JsValue::from(key.as_ref());
370                let value = crate::js::convert::entry_change_into_js(value, txn.doc())?;
371                js_sys::Reflect::set(&result, &key, &value).unwrap();
372            }
373            let keys: JsValue = result.into();
374            self.keys = Some(keys.clone());
375            Ok(keys)
376        }
377    }
378}