workflow_chrome/
storage.rs

1use crate::error::Error;
2use cfg_if::cfg_if;
3use chrome_sys::storage;
4use js_sys::{Array, Object};
5use wasm_bindgen::prelude::*;
6use workflow_core::task::call_async_no_send;
7
8pub struct LocalStorage;
9
10impl LocalStorage {
11    pub async fn set_item(key: &str, value: &str) -> Result<JsValue, JsValue> {
12        let key = key.to_string();
13        let value = value.to_string();
14        call_async_no_send!(async move {
15            let data = Object::new();
16            js_sys::Reflect::set(&data, &key.into(), &value.into())?;
17            storage::set(data.into()).await
18        })
19    }
20
21    pub async fn get_item(key: &str) -> Result<Option<String>, JsValue> {
22        let _key = key.to_string();
23        let obj = call_async_no_send!(storage::get(_key).await)?;
24        Ok(js_sys::Reflect::get(&obj, &key.into())?.as_string())
25    }
26
27    pub async fn get_items(keys: Vec<&str>) -> Result<StorageData, JsValue> {
28        let keys = keys.iter().map(|k| k.to_string()).collect::<Vec<_>>();
29        Ok(call_async_no_send!(async move {
30            let query = Array::new();
31            for key in keys {
32                query.push(&key.into());
33            }
34            storage::get_items(query).await
35        })?
36        .try_into()?)
37    }
38
39    pub async fn get_all() -> Result<StorageData, JsValue> {
40        Ok(call_async_no_send!(storage::get_all().await)?.try_into()?)
41    }
42
43    pub async fn keys() -> Result<Vec<String>, JsValue> {
44        Ok(Self::get_all().await?.keys())
45    }
46
47    pub async fn remove_item(key: &str) -> Result<(), JsValue> {
48        let key = key.to_string();
49        call_async_no_send!(storage::remove(key).await)
50    }
51
52    pub async fn rename_item(from_key: &str, to_key: &str) -> Result<(), Error> {
53        let from_key = from_key.to_string();
54        let to_key = to_key.to_string();
55
56        if Self::get_item(&to_key).await?.is_some() {
57            return Err(Error::KeyExists(to_key));
58        }
59        if let Some(existing) = Self::get_item(&from_key).await? {
60            Self::set_item(&to_key, &existing).await?;
61            Self::remove_item(&from_key).await?;
62            Ok(())
63        } else {
64            Err(Error::MissingKey(from_key))
65        }
66    }
67
68    pub async fn remove_items(keys: Vec<&str>) -> Result<(), JsValue> {
69        let keys = keys.iter().map(|k| k.to_string()).collect::<Vec<_>>();
70        call_async_no_send!(async move {
71            let query = Array::new();
72            for key in keys {
73                query.push(&key.into());
74            }
75            storage::remove_items(query).await
76        })
77    }
78
79    pub async fn clear() -> Result<(), JsValue> {
80        call_async_no_send!(storage::clear().await)
81    }
82
83    #[cfg(debug_assertions)]
84    pub async fn unit_tests() -> Result<(), String> {
85        use workflow_core::sendable::Sendable;
86
87        let old_data = Sendable(Self::get_all().await.unwrap());
88        let error = Sendable(test_impl().await.err());
89        let old_data_clone = old_data.clone();
90        call_async_no_send!(storage::set(old_data.unwrap().inner.into()).await).unwrap();
91
92        let new_data = Self::get_all().await.unwrap();
93        for key in new_data.keys() {
94            let new_value = new_data.get(&key).unwrap();
95            let old_value = old_data_clone.get(&key).unwrap();
96            if new_value != old_value {
97                return Err(format!(
98                    "[WARNING] Data restore failed: {key} => {old_value:?} != {new_value:?}"
99                ));
100            }
101        }
102
103        if let Some(err) = error.unwrap() {
104            return Err(err.as_string().unwrap_or(format!("{err:?}")));
105        }
106        Ok(())
107    }
108}
109
110#[derive(Debug, Clone)]
111pub struct StorageData {
112    pub inner: Object,
113}
114
115impl TryFrom<JsValue> for StorageData {
116    type Error = JsError;
117    fn try_from(inner: JsValue) -> Result<Self, Self::Error> {
118        if !inner.is_object() {
119            return Err(JsError::new(&format!(
120                "Invalid JsValue: cant convert JsValue ({inner:?}) to StorageData."
121            )));
122        }
123        let inner = Object::from(inner);
124        Ok(Self { inner })
125    }
126}
127
128impl StorageData {
129    pub fn keys(&self) -> Vec<String> {
130        let mut keys = vec![];
131        for key in Object::keys(&self.inner) {
132            keys.push(key.as_string().unwrap());
133        }
134
135        keys
136    }
137
138    pub fn has(&self, key: &str) -> bool {
139        self.inner.has_own_property(&key.into())
140    }
141
142    pub fn get_value(&self, key: &str) -> Result<Option<JsValue>, JsValue> {
143        let value = js_sys::Reflect::get(&self.inner, &key.into())?;
144        if value.eq(&JsValue::UNDEFINED) {
145            Ok(None)
146        } else {
147            Ok(Some(value))
148        }
149    }
150
151    pub fn get(&self, key: &str) -> Result<Option<String>, JsValue> {
152        let value = js_sys::Reflect::get(&self.inner, &key.into())?;
153        if value.eq(&JsValue::UNDEFINED) {
154            Ok(None)
155        } else {
156            Ok(value.as_string())
157        }
158    }
159}
160
161#[cfg(debug_assertions)]
162macro_rules! assert_test {
163    ($name:literal, $cond1:expr, $cond2:expr) => {{
164        if $cond1 != $cond2 {
165            return Result::<(), JsValue>::Err(
166                format!(
167                    "{} => {}, {:?} != {:?}",
168                    $name,
169                    stringify!($cond1),
170                    $cond1,
171                    $cond2
172                )
173                .into(),
174            );
175        }
176    }};
177}
178
179#[cfg(debug_assertions)]
180async fn test_impl() -> Result<(), JsValue> {
181    LocalStorage::clear().await?;
182    {
183        let empty_data = LocalStorage::get_all().await?;
184        assert_test!("Key length should be 0", empty_data.keys().len(), 0);
185    }
186    {
187        LocalStorage::set_item("key1", "value-1").await?;
188        let data = LocalStorage::get_all().await?;
189        assert_test!("Key length should be 1", data.keys().len(), 1);
190        assert_test!("'key1' key should be there", data.has("key1"), true);
191        assert_test!(
192            "value for 'key1' key should be 'value-1'",
193            data.get("key1")?.unwrap(),
194            "value-1"
195        );
196    }
197    {
198        let item = LocalStorage::get_item("key1").await?.unwrap();
199        assert_test!("value for 'key1' key should be 'value-1'", item, "value-1");
200    }
201    {
202        LocalStorage::set_item("key2", "value-2").await?;
203        let data = LocalStorage::get_all().await?;
204        assert_test!("Key length should be 2", data.keys().len(), 2);
205        assert_test!("'key2' key should be there", data.has("key2"), true);
206        assert_test!(
207            "value for 'key2' key should be 'value-2'",
208            data.get("key2")?.unwrap(),
209            "value-2"
210        );
211    }
212    {
213        let item = LocalStorage::get_item("key2").await?.unwrap();
214        assert_test!("value for 'key2' key should be 'value-2'", item, "value-2");
215    }
216    {
217        LocalStorage::set_item("key3", "value-3").await?;
218        let data = LocalStorage::get_all().await?;
219        assert_test!("Key length should be 3", data.keys().len(), 3);
220        assert_test!("'key3' key should be there", data.has("key3"), true);
221        assert_test!(
222            "value for 'key3' key should be 'value-3'",
223            data.get("key3")?.unwrap(),
224            "value-3"
225        );
226    }
227    {
228        let data = LocalStorage::get_items(vec!["key2", "key3"]).await?;
229        assert_test!("Key length should be 2", data.keys().len(), 2);
230        assert_test!("'key2' key should be there", data.has("key2"), true);
231        assert_test!("'key3' key should be there", data.has("key3"), true);
232        assert_test!(
233            "value for 'key2' key should be 'value-2'",
234            data.get("key2")?.unwrap(),
235            "value-2"
236        );
237        assert_test!(
238            "value for 'key3' key should be 'value-3'",
239            data.get("key3")?.unwrap(),
240            "value-3"
241        );
242    }
243    {
244        LocalStorage::remove_item("key2").await?;
245        let data = LocalStorage::get_all().await?;
246        assert_test!(
247            "After remove_item, Key length should be 2",
248            data.keys().len(),
249            2
250        );
251        assert_test!("'key2' key should not be there", data.has("key2"), false);
252        assert_test!(
253            "value for 'key2' key should be None",
254            data.get("key2")?,
255            Option::<String>::None
256        );
257    }
258    {
259        LocalStorage::clear().await?;
260        let data = LocalStorage::get_all().await?;
261        assert_test!("After clear, Key length should be 0", data.keys().len(), 0);
262        assert_test!("'key1' key should not be there", data.has("key1"), false);
263        assert_test!(
264            "value for 'key2' key should be None",
265            data.get("key2")?,
266            Option::<String>::None
267        );
268    }
269
270    Ok(())
271}
272
273pub async fn __chrome_storage_unit_test() {
274    cfg_if! {
275        if #[cfg(all(target_arch = "wasm32", debug_assertions))] {
276            if !workflow_core::runtime::is_chrome_extension() {
277                workflow_log::log_info!("ChromeStorage::test() FAILED: these are unit tests for chrome extension storage api.");
278                return
279            }
280            use LocalStorage as ChromeStorage;
281            match ChromeStorage::unit_tests().await{
282                Ok(_)=>workflow_log::log_info!("ChromeStorage::test() PASSED"),
283                Err(err)=>workflow_log::log_error!("ChromeStorage::test() FAILED: {err:?}")
284            };
285        }
286    }
287}