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 remove(&self, id: String) -> Result<JsValue, JsValue>;
50}
51
52/// This struct wraps a [WasmRepository] in a [ThreadBoundRunner] to allow it to be used as a
53/// [Repository] in a thread-safe manner. It implements the [Repository] trait directly, by
54/// converting the values as needed with [tsify::serde_wasm_bindgen].
55pub(crate) struct WasmRepositoryChannel<T, R: WasmRepository<T> + 'static>(
56    ThreadBoundRunner<R>,
57    PhantomData<T>,
58);
59
60impl<T, R: WasmRepository<T> + 'static> WasmRepositoryChannel<T, R> {
61    pub(crate) fn new(repository: R) -> Self {
62        Self(ThreadBoundRunner::new(repository), PhantomData)
63    }
64}
65
66#[async_trait::async_trait]
67impl<T: RepositoryItem, R: WasmRepository<T> + 'static> Repository<T>
68    for WasmRepositoryChannel<T, R>
69{
70    async fn get(&self, id: String) -> Result<Option<T>, RepositoryError> {
71        run_convert(&self.0, |s| async move { s.get(id).await }).await
72    }
73    async fn list(&self) -> Result<Vec<T>, RepositoryError> {
74        run_convert(&self.0, |s| async move { s.list().await }).await
75    }
76    async fn set(&self, id: String, value: T) -> Result<(), RepositoryError> {
77        run_convert(&self.0, |s| async move { s.set(id, value).await.and(UNIT) }).await
78    }
79    async fn remove(&self, id: String) -> Result<(), RepositoryError> {
80        run_convert(&self.0, |s| async move { s.remove(id).await.and(UNIT) }).await
81    }
82}
83
84#[wasm_bindgen(typescript_custom_section)]
85const REPOSITORY_CUSTOM_TS_TYPE: &'static str = r#"
86export interface Repository<T> {
87    get(id: string): Promise<T | null>;
88    list(): Promise<T[]>;
89    set(id: string, value: T): Promise<void>;
90    remove(id: string): Promise<void>;
91}
92"#;
93
94/// This macro generates a [::wasm_bindgen] interface for a repository type, and provides the
95/// implementation of [WasmRepository] and a way to convert it into something that implements
96/// the [Repository] trait.
97macro_rules! create_wasm_repositories {
98    ( $container_name:ident ; $( $qualified_type_name:ty, $type_name:ident, $field_name:ident, $repo_name:ident );+ $(;)? ) => {
99
100        const _: () = {
101            #[wasm_bindgen(typescript_custom_section)]
102            const REPOSITORIES_CUSTOM_TS_TYPE: &'static str = concat!(
103                "export interface ",
104                stringify!($container_name),
105                "{\n",
106                $( stringify!($field_name), ": Repository<", stringify!($type_name), "> | null;\n", )+
107                "}\n"
108            );
109        };
110
111        #[wasm_bindgen]
112        extern "C" {
113            #[wasm_bindgen(typescript_type = $container_name)]
114            pub type $container_name;
115
116            $(
117                #[wasm_bindgen(method, getter)]
118                pub fn $field_name(this: &$container_name) -> Option<$repo_name>;
119            )+
120        }
121
122        impl $container_name {
123            pub fn register_all(self, client: &bitwarden_core::platform::StateClient) {
124                $(
125                    if let Some(repo) = self.$field_name() {
126                        let repo = repo.into_channel_impl();
127                        client.register_client_managed(repo);
128                    }
129                )+
130            }
131        }
132
133        $(
134            #[wasm_bindgen]
135            extern "C" {
136                #[wasm_bindgen]
137                pub type $repo_name;
138
139                #[wasm_bindgen(method, catch)]
140                async fn get(
141                    this: &$repo_name,
142                    id: String,
143                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
144                #[wasm_bindgen(method, catch)]
145                async fn list(this: &$repo_name)
146                    -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
147                #[wasm_bindgen(method, catch)]
148                async fn set(
149                    this: &$repo_name,
150                    id: String,
151                    value: $qualified_type_name,
152                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
153                #[wasm_bindgen(method, catch)]
154                async fn remove(
155                    this: &$repo_name,
156                    id: String,
157                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
158            }
159
160            impl $crate::platform::repository::WasmRepository<$qualified_type_name> for $repo_name {
161                async fn get(
162                    &self,
163                    id: String,
164                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
165                    self.get(id).await
166                }
167                async fn list(&self) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
168                    self.list().await
169                }
170                async fn set(
171                    &self,
172                    id: String,
173                    value: $qualified_type_name,
174                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
175                    self.set(id, value).await
176                }
177                async fn remove(
178                    &self,
179                    id: String,
180                ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
181                    self.remove(id).await
182                }
183            }
184
185            impl $repo_name {
186                pub fn into_channel_impl(
187                    self,
188                ) -> ::std::sync::Arc<impl bitwarden_state::repository::Repository<$qualified_type_name>> {
189                    use $crate::platform::repository::WasmRepositoryChannel;
190                    ::std::sync::Arc::new(WasmRepositoryChannel::new(self))
191                }
192            }
193        )+
194    };
195}
196pub(crate) use create_wasm_repositories;
197
198const UNIT: Result<JsValue, JsValue> = Ok(JsValue::UNDEFINED);
199
200/// Utility function that runs a closure in a thread-bound manner, and converts the Result from
201/// [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>].
202async fn run_convert<T: 'static, Func, Fut, Ret>(
203    runner: &::bitwarden_threading::ThreadBoundRunner<T>,
204    f: Func,
205) -> Result<Ret, RepositoryError>
206where
207    Func: FnOnce(Rc<T>) -> Fut + Send + 'static,
208    Fut: Future<Output = Result<JsValue, JsValue>>,
209    Ret: serde::de::DeserializeOwned + Send + Sync + 'static,
210{
211    runner
212        .run_in_thread(|state| async move { convert_result(f(state).await) })
213        .await
214        .expect("Task should not panic")
215}
216
217/// Converts a [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>] using
218/// [tsify::serde_wasm_bindgen]
219fn convert_result<T: serde::de::DeserializeOwned>(
220    result: Result<JsValue, JsValue>,
221) -> Result<T, RepositoryError> {
222    result
223        .map_err(|e| RepositoryError::Internal(format!("{e:?}")))
224        .and_then(|value| {
225            ::tsify::serde_wasm_bindgen::from_value(value)
226                .map_err(|e| RepositoryError::Internal(e.to_string()))
227        })
228}