Skip to main content

bitwarden_wasm_internal/platform/
repository.rs

1/*!
2 * To support clients implementing the [Repository] trait in a [::wasm_bindgen] environment,
3 * we need to deal with an `extern "C"` interface, as that is what [::wasm_bindgen] supports:
4 *
5 * This looks something like this:
6 *
7 * ```rust,ignore
8 * #[wasm_bindgen]
9 * extern "C" {
10 *     pub type CipherRepository;
11 *
12 *     #[wasm_bindgen(method, catch)]
13 *     async fn get(this: &CipherRepository, id: String) -> Result<JsValue, JsValue>;
14 * }
15 * ```
16 *
17 * As you can see, this has a few limitations:
18 * - The type must be known at compile time, so we cannot use generics directly, which means we
19 *   can't use the existing [Repository] trait directly.
20 * - The return type must be [JsValue], so we need to convert our types to and from [JsValue].
21 *
22 * To facilitate this, we provide some utilities:
23 * - [WasmRepository] trait, which defines the methods as we expect them to come from
24 *   [::wasm_bindgen], using [JsValue]. This is generic and should be implemented for each
25 *   concrete repository we define, but the implementation should be very straightforward.
26 * - [WasmRepositoryChannel] struct, which wraps a [WasmRepository] in a [ThreadBoundRunner] and
27 *   implements the [Repository] trait. This has a few special considerations:
28 *   - It uses [tsify::serde_wasm_bindgen] to convert between [JsValue] and our types, so we can
29 *     use the existing [Repository] trait.
30 *   - It runs the calls in a thread-bound manner, so we can safely call the [WasmRepository]
31 *     methods from any thread.
32 * - The [create_wasm_repositories] macro, defines the [::wasm_bindgen] interface and implements
33 *   the [WasmRepository] trait for you.
34 */
35
36use std::{future::Future, marker::PhantomData, rc::Rc};
37
38use bitwarden_state::repository::{Repository, RepositoryError, RepositoryItem};
39use bitwarden_threading::ThreadBoundRunner;
40use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
41
42/// This trait defines the methods that a [::wasm_bindgen] repository must implement.
43/// The trait itself exists to provide a generic way of handling the [::wasm_bindgen] interface,
44/// which is !Send + !Sync, and only deals with [JsValue].
45pub(crate) trait WasmRepository<T> {
46    async fn get(&self, id: String) -> Result<JsValue, JsValue>;
47    async fn list(&self) -> Result<JsValue, JsValue>;
48    async fn set(&self, id: String, value: T) -> Result<JsValue, JsValue>;
49    async fn set_bulk(&self, values: Vec<(String, T)>) -> Result<JsValue, JsValue>;
50    async fn remove(&self, id: String) -> Result<JsValue, JsValue>;
51    async fn remove_bulk(&self, keys: Vec<String>) -> Result<JsValue, JsValue>;
52    async fn remove_all(&self) -> Result<JsValue, JsValue>;
53}
54
55/// This struct wraps a [WasmRepository] in a [ThreadBoundRunner] to allow it to be used as a
56/// [Repository] in a thread-safe manner. It implements the [Repository] trait directly, by
57/// converting the values as needed with [tsify::serde_wasm_bindgen].
58pub(crate) struct WasmRepositoryChannel<T, R: WasmRepository<T> + 'static>(
59    ThreadBoundRunner<R>,
60    PhantomData<T>,
61);
62
63impl<T, R: WasmRepository<T> + 'static> WasmRepositoryChannel<T, R> {
64    pub(crate) fn new(repository: R) -> Self {
65        Self(ThreadBoundRunner::new(repository), PhantomData)
66    }
67}
68
69#[async_trait::async_trait]
70impl<T: RepositoryItem, R: WasmRepository<T> + 'static> Repository<T>
71    for WasmRepositoryChannel<T, R>
72{
73    async fn get(&self, key: T::Key) -> Result<Option<T>, RepositoryError> {
74        let key = key.to_string();
75        run_convert(&self.0, |s| async move { s.get(key).await }).await
76    }
77    async fn list(&self) -> Result<Vec<T>, RepositoryError> {
78        run_convert(&self.0, |s| async move { s.list().await }).await
79    }
80    async fn set(&self, key: T::Key, value: T) -> Result<(), RepositoryError> {
81        let key = key.to_string();
82        run_convert(
83            &self.0,
84            |s| async move { s.set(key, value).await.and(UNIT) },
85        )
86        .await
87    }
88    async fn set_bulk(&self, values: Vec<(T::Key, T)>) -> Result<(), RepositoryError> {
89        let values: Vec<(String, T)> = values
90            .into_iter()
91            .map(|(k, v)| (k.to_string(), v))
92            .collect();
93        run_convert(
94            &self.0,
95            |s| async move { s.set_bulk(values).await.and(UNIT) },
96        )
97        .await
98    }
99    async fn remove(&self, key: T::Key) -> Result<(), RepositoryError> {
100        let key = key.to_string();
101        run_convert(&self.0, |s| async move { s.remove(key).await.and(UNIT) }).await
102    }
103    async fn remove_bulk(&self, keys: Vec<T::Key>) -> Result<(), RepositoryError> {
104        let keys: Vec<String> = keys.into_iter().map(|k| k.to_string()).collect();
105        run_convert(
106            &self.0,
107            |s| async move { s.remove_bulk(keys).await.and(UNIT) },
108        )
109        .await
110    }
111    async fn remove_all(&self) -> Result<(), RepositoryError> {
112        run_convert(&self.0, |s| async move { s.remove_all().await.and(UNIT) }).await
113    }
114}
115
116#[wasm_bindgen(typescript_custom_section)]
117const REPOSITORY_CUSTOM_TS_TYPE: &'static str = r#"
118export interface Repository<T> {
119    get(id: string): Promise<T | null>;
120    list(): Promise<T[]>;
121    set(id: string, value: T): Promise<void>;
122    setBulk(values: [string, T][]): Promise<void>;
123    remove(id: string): Promise<void>;
124    removeBulk(keys: string[]): Promise<void>;
125    removeAll(): Promise<void>;
126}
127"#;
128
129/// This macro generates a [::wasm_bindgen] interface for a repository type, and provides the
130/// implementation of [WasmRepository] and a way to convert it into something that implements
131/// the [Repository] trait.
132macro_rules! create_wasm_repositories {
133    ( $container_name:ident ; $( $qualified_type_name:ty, $type_name:ident, $field_name:ident, $repo_name:ident );+ $(;)? ) => {
134
135        const _: () = {
136            #[wasm_bindgen(typescript_custom_section)]
137            const REPOSITORIES_CUSTOM_TS_TYPE: &'static str = concat!(
138                "export interface ",
139                stringify!($container_name),
140                "{\n",
141                $( stringify!($field_name), ": Repository<", stringify!($type_name), "> | null;\n", )+
142                "}\n"
143            );
144        };
145
146        #[wasm_bindgen]
147        extern "C" {
148            #[wasm_bindgen(typescript_type = $container_name)]
149            pub type $container_name;
150
151            $(
152                #[wasm_bindgen(method, getter)]
153                pub fn $field_name(this: &$container_name) -> Option<$repo_name>;
154            )+
155        }
156
157        impl $container_name {
158            pub fn register_all(self, client: &bitwarden_core::platform::StateClient) {
159                $(
160                    if let Some(repo) = self.$field_name() {
161                        let repo = repo.into_channel_impl();
162                        client.register_client_managed(repo);
163                    }
164                )+
165            }
166        }
167
168        $(
169            #[wasm_bindgen]
170            extern "C" {
171                #[wasm_bindgen]
172                pub type $repo_name;
173
174                #[wasm_bindgen(method, catch)]
175                async fn get(
176                    this: &$repo_name,
177                    id: String,
178                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
179                #[wasm_bindgen(method, catch)]
180                async fn list(this: &$repo_name)
181                    -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
182                #[wasm_bindgen(method, catch)]
183                async fn set(
184                    this: &$repo_name,
185                    id: String,
186                    value: $qualified_type_name,
187                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
188                #[wasm_bindgen(method, catch, js_name = "setBulk")]
189                async fn set_bulk(
190                    this: &$repo_name,
191                    values: ::wasm_bindgen::JsValue,
192                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
193                #[wasm_bindgen(method, catch)]
194                async fn remove(
195                    this: &$repo_name,
196                    id: String,
197                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
198                #[wasm_bindgen(method, catch, js_name = "removeBulk")]
199                async fn remove_bulk(
200                    this: &$repo_name,
201                    keys: ::wasm_bindgen::JsValue,
202                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
203                #[wasm_bindgen(method, catch, js_name = "removeAll")]
204                async fn remove_all(
205                    this: &$repo_name,
206                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
207            }
208
209            impl $crate::platform::repository::WasmRepository<$qualified_type_name> for $repo_name {
210                async fn get(
211                    &self,
212                    id: String,
213                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
214                    self.get(id).await
215                }
216                async fn list(&self) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
217                    self.list().await
218                }
219                async fn set(
220                    &self,
221                    id: String,
222                    value: $qualified_type_name,
223                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
224                    self.set(id, value).await
225                }
226                async fn set_bulk(
227                    &self,
228                    values: Vec<(String, $qualified_type_name)>,
229                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
230                    let js_val = ::tsify::serde_wasm_bindgen::to_value(&values)
231                        .map_err(|e| ::wasm_bindgen::JsValue::from_str(&e.to_string()))?;
232                    self.set_bulk(js_val).await
233                }
234                async fn remove(
235                    &self,
236                    id: String,
237                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
238                    self.remove(id).await
239                }
240                async fn remove_bulk(
241                    &self,
242                    keys: Vec<String>,
243                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
244                    let js_val = ::tsify::serde_wasm_bindgen::to_value(&keys)
245                        .map_err(|e| ::wasm_bindgen::JsValue::from_str(&e.to_string()))?;
246                    self.remove_bulk(js_val).await
247                }
248                async fn remove_all(&self) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
249                    self.remove_all().await
250                }
251            }
252
253            impl $repo_name {
254                pub fn into_channel_impl(
255                    self,
256                ) -> ::std::sync::Arc<impl bitwarden_state::repository::Repository<$qualified_type_name>> {
257                    use $crate::platform::repository::WasmRepositoryChannel;
258                    ::std::sync::Arc::new(WasmRepositoryChannel::new(self))
259                }
260            }
261        )+
262    };
263}
264pub(crate) use create_wasm_repositories;
265
266const UNIT: Result<JsValue, JsValue> = Ok(JsValue::UNDEFINED);
267
268/// Utility function that runs a closure in a thread-bound manner, and converts the Result from
269/// [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>].
270async fn run_convert<T: 'static, Func, Fut, Ret>(
271    runner: &::bitwarden_threading::ThreadBoundRunner<T>,
272    f: Func,
273) -> Result<Ret, RepositoryError>
274where
275    Func: FnOnce(Rc<T>) -> Fut + Send + 'static,
276    Fut: Future<Output = Result<JsValue, JsValue>>,
277    Ret: serde::de::DeserializeOwned + Send + Sync + 'static,
278{
279    runner
280        .run_in_thread(|state| async move { convert_result(f(state).await) })
281        .await
282        .expect("Task should not panic")
283}
284
285/// Converts a [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>] using
286/// [tsify::serde_wasm_bindgen]
287fn convert_result<T: serde::de::DeserializeOwned>(
288    result: Result<JsValue, JsValue>,
289) -> Result<T, RepositoryError> {
290    result
291        .map_err(|e| RepositoryError::Internal(format!("{e:?}")))
292        .and_then(|value| {
293            ::tsify::serde_wasm_bindgen::from_value(value)
294                .map_err(|e| RepositoryError::Internal(e.to_string()))
295        })
296}