Skip to main content

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
8/// Async accessor for the Chrome extension `storage.local` area.
9pub struct LocalStorage;
10
11impl LocalStorage {
12    /// Stores `value` under `key`, overwriting any existing value.
13    pub async fn set_item(key: &str, value: &str) -> Result<JsValue, JsValue> {
14        let key = key.to_string();
15        let value = value.to_string();
16        call_async_no_send!(async move {
17            let data = Object::new();
18            js_sys::Reflect::set(&data, &key.into(), &value.into())?;
19            storage::set(data.into()).await
20        })
21    }
22
23    /// Retrieves the value stored under `key`, or `None` if it is not present.
24    pub async fn get_item(key: &str) -> Result<Option<String>, JsValue> {
25        let _key = key.to_string();
26        let obj = call_async_no_send!(storage::get(_key).await)?;
27        Ok(js_sys::Reflect::get(&obj, &key.into())?.as_string())
28    }
29
30    /// Retrieves the items stored under the given `keys` as [`StorageData`].
31    pub async fn get_items(keys: Vec<&str>) -> Result<StorageData, JsValue> {
32        let keys = keys.iter().map(|k| k.to_string()).collect::<Vec<_>>();
33        Ok(call_async_no_send!(async move {
34            let query = Array::new();
35            for key in keys {
36                query.push(&key.into());
37            }
38            storage::get_items(query).await
39        })?
40        .try_into()?)
41    }
42
43    /// Retrieves all items from extension storage as [`StorageData`].
44    pub async fn get_all() -> Result<StorageData, JsValue> {
45        Ok(call_async_no_send!(storage::get_all().await)?.try_into()?)
46    }
47
48    /// Returns the keys of all items currently in extension storage.
49    pub async fn keys() -> Result<Vec<String>, JsValue> {
50        Ok(Self::get_all().await?.keys())
51    }
52
53    /// Removes the item stored under `key`.
54    pub async fn remove_item(key: &str) -> Result<(), JsValue> {
55        let key = key.to_string();
56        call_async_no_send!(storage::remove(key).await)
57    }
58
59    /// Renames a stored item from `from_key` to `to_key`. Errors with
60    /// [`Error::KeyExists`] if `to_key` is already present, or
61    /// [`Error::MissingKey`] if `from_key` does not exist.
62    pub async fn rename_item(from_key: &str, to_key: &str) -> Result<(), Error> {
63        let from_key = from_key.to_string();
64        let to_key = to_key.to_string();
65
66        if Self::get_item(&to_key).await?.is_some() {
67            return Err(Error::KeyExists(to_key));
68        }
69        if let Some(existing) = Self::get_item(&from_key).await? {
70            Self::set_item(&to_key, &existing).await?;
71            Self::remove_item(&from_key).await?;
72            Ok(())
73        } else {
74            Err(Error::MissingKey(from_key))
75        }
76    }
77
78    /// Removes the items stored under each of the given `keys`.
79    pub async fn remove_items(keys: Vec<&str>) -> Result<(), JsValue> {
80        let keys = keys.iter().map(|k| k.to_string()).collect::<Vec<_>>();
81        call_async_no_send!(async move {
82            let query = Array::new();
83            for key in keys {
84                query.push(&key.into());
85            }
86            storage::remove_items(query).await
87        })
88    }
89
90    /// Removes all items from extension storage.
91    pub async fn clear() -> Result<(), JsValue> {
92        call_async_no_send!(storage::clear().await)
93    }
94
95    /// Runs the storage unit test suite, preserving and restoring any
96    /// pre-existing storage contents around the test and reporting any failure
97    /// (including a failed restore) as an error string.
98    #[cfg(debug_assertions)]
99    pub async fn unit_tests() -> Result<(), String> {
100        use workflow_core::sendable::Sendable;
101
102        let old_data = Sendable(Self::get_all().await.unwrap());
103        let error = Sendable(test_impl().await.err());
104        let old_data_clone = old_data.clone();
105        call_async_no_send!(storage::set(old_data.unwrap().inner.into()).await).unwrap();
106
107        let new_data = Self::get_all().await.unwrap();
108        for key in new_data.keys() {
109            let new_value = new_data.get(&key).unwrap();
110            let old_value = old_data_clone.get(&key).unwrap();
111            if new_value != old_value {
112                return Err(format!(
113                    "[WARNING] Data restore failed: {key} => {old_value:?} != {new_value:?}"
114                ));
115            }
116        }
117
118        if let Some(err) = error.unwrap() {
119            return Err(err.as_string().unwrap_or(format!("{err:?}")));
120        }
121        Ok(())
122    }
123}
124
125/// A set of key/value pairs retrieved from Chrome extension storage, wrapping
126/// the underlying JavaScript [`Object`].
127#[derive(Debug, Clone)]
128pub struct StorageData {
129    /// The underlying JavaScript object holding the stored key/value pairs.
130    pub inner: Object,
131}
132
133impl TryFrom<JsValue> for StorageData {
134    type Error = JsError;
135    fn try_from(inner: JsValue) -> Result<Self, Self::Error> {
136        if !inner.is_object() {
137            return Err(JsError::new(&format!(
138                "Invalid JsValue: cant convert JsValue ({inner:?}) to StorageData."
139            )));
140        }
141        let inner = Object::from(inner);
142        Ok(Self { inner })
143    }
144}
145
146impl StorageData {
147    /// Returns the list of keys present in the storage data.
148    pub fn keys(&self) -> Vec<String> {
149        let mut keys = vec![];
150        for key in Object::keys(&self.inner) {
151            keys.push(key.as_string().unwrap());
152        }
153
154        keys
155    }
156
157    /// Returns `true` if the data contains an own property named `key`.
158    pub fn has(&self, key: &str) -> bool {
159        js_sys::Object::has_own(&self.inner, &key.into())
160    }
161
162    /// Returns the raw [`JsValue`] stored for `key`, or `None` if the key is
163    /// absent.
164    pub fn get_value(&self, key: &str) -> Result<Option<JsValue>, JsValue> {
165        let value = js_sys::Reflect::get(&self.inner, &key.into())?;
166        if value.eq(&JsValue::UNDEFINED) {
167            Ok(None)
168        } else {
169            Ok(Some(value))
170        }
171    }
172
173    /// Returns the value for `key` as a `String`, or `None` if the key is
174    /// absent (or its value is not a string).
175    pub fn get(&self, key: &str) -> Result<Option<String>, JsValue> {
176        let value = js_sys::Reflect::get(&self.inner, &key.into())?;
177        if value.eq(&JsValue::UNDEFINED) {
178            Ok(None)
179        } else {
180            Ok(value.as_string())
181        }
182    }
183}
184
185#[cfg(debug_assertions)]
186macro_rules! assert_test {
187    ($name:literal, $cond1:expr_2021, $cond2:expr_2021) => {{
188        if $cond1 != $cond2 {
189            return Result::<(), JsValue>::Err(
190                format!(
191                    "{} => {}, {:?} != {:?}",
192                    $name,
193                    stringify!($cond1),
194                    $cond1,
195                    $cond2
196                )
197                .into(),
198            );
199        }
200    }};
201}
202
203#[cfg(debug_assertions)]
204async fn test_impl() -> Result<(), JsValue> {
205    LocalStorage::clear().await?;
206    {
207        let empty_data = LocalStorage::get_all().await?;
208        assert_test!("Key length should be 0", empty_data.keys().len(), 0);
209    }
210    {
211        LocalStorage::set_item("key1", "value-1").await?;
212        let data = LocalStorage::get_all().await?;
213        assert_test!("Key length should be 1", data.keys().len(), 1);
214        assert_test!("'key1' key should be there", data.has("key1"), true);
215        assert_test!(
216            "value for 'key1' key should be 'value-1'",
217            data.get("key1")?.unwrap(),
218            "value-1"
219        );
220    }
221    {
222        let item = LocalStorage::get_item("key1").await?.unwrap();
223        assert_test!("value for 'key1' key should be 'value-1'", item, "value-1");
224    }
225    {
226        LocalStorage::set_item("key2", "value-2").await?;
227        let data = LocalStorage::get_all().await?;
228        assert_test!("Key length should be 2", data.keys().len(), 2);
229        assert_test!("'key2' key should be there", data.has("key2"), true);
230        assert_test!(
231            "value for 'key2' key should be 'value-2'",
232            data.get("key2")?.unwrap(),
233            "value-2"
234        );
235    }
236    {
237        let item = LocalStorage::get_item("key2").await?.unwrap();
238        assert_test!("value for 'key2' key should be 'value-2'", item, "value-2");
239    }
240    {
241        LocalStorage::set_item("key3", "value-3").await?;
242        let data = LocalStorage::get_all().await?;
243        assert_test!("Key length should be 3", data.keys().len(), 3);
244        assert_test!("'key3' key should be there", data.has("key3"), true);
245        assert_test!(
246            "value for 'key3' key should be 'value-3'",
247            data.get("key3")?.unwrap(),
248            "value-3"
249        );
250    }
251    {
252        let data = LocalStorage::get_items(vec!["key2", "key3"]).await?;
253        assert_test!("Key length should be 2", data.keys().len(), 2);
254        assert_test!("'key2' key should be there", data.has("key2"), true);
255        assert_test!("'key3' key should be there", data.has("key3"), true);
256        assert_test!(
257            "value for 'key2' key should be 'value-2'",
258            data.get("key2")?.unwrap(),
259            "value-2"
260        );
261        assert_test!(
262            "value for 'key3' key should be 'value-3'",
263            data.get("key3")?.unwrap(),
264            "value-3"
265        );
266    }
267    {
268        LocalStorage::remove_item("key2").await?;
269        let data = LocalStorage::get_all().await?;
270        assert_test!(
271            "After remove_item, Key length should be 2",
272            data.keys().len(),
273            2
274        );
275        assert_test!("'key2' key should not be there", data.has("key2"), false);
276        assert_test!(
277            "value for 'key2' key should be None",
278            data.get("key2")?,
279            Option::<String>::None
280        );
281    }
282    {
283        LocalStorage::clear().await?;
284        let data = LocalStorage::get_all().await?;
285        assert_test!("After clear, Key length should be 0", data.keys().len(), 0);
286        assert_test!("'key1' key should not be there", data.has("key1"), false);
287        assert_test!(
288            "value for 'key2' key should be None",
289            data.get("key2")?,
290            Option::<String>::None
291        );
292    }
293
294    Ok(())
295}
296
297/// Entry point that runs the Chrome extension storage unit tests when built
298/// for `wasm32` in debug mode within a Chrome extension context; otherwise a
299/// no-op that logs the reason it was skipped.
300pub async fn __chrome_storage_unit_test() {
301    cfg_if! {
302        if #[cfg(all(target_arch = "wasm32", debug_assertions))] {
303            if !workflow_core::runtime::is_chrome_extension() {
304                workflow_log::log_info!("ChromeStorage::test() FAILED: these are unit tests for chrome extension storage api.");
305                return
306            }
307            use LocalStorage as ChromeStorage;
308            match ChromeStorage::unit_tests().await{
309                Ok(_)=>workflow_log::log_info!("ChromeStorage::test() PASSED"),
310                Err(err)=>workflow_log::log_error!("ChromeStorage::test() FAILED: {err:?}")
311            };
312        }
313    }
314}