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_next::serde_wasm_bindgen] to convert between [JsValue] and our types, so
29 *     we can 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_repository] 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 serde::{de::DeserializeOwned, Serialize};
41use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
42
43/// This trait defines the methods that a [::wasm_bindgen] repository must implement.
44/// The trait itself exists to provide a generic way of handling the [::wasm_bindgen] interface,
45/// which is !Send + !Sync, and only deals with [JsValue].
46pub(crate) trait WasmRepository<T> {
47    async fn get(&self, id: String) -> Result<JsValue, JsValue>;
48    async fn list(&self) -> Result<JsValue, JsValue>;
49    async fn set(&self, id: String, value: T) -> Result<JsValue, JsValue>;
50    async fn remove(&self, id: String) -> Result<JsValue, JsValue>;
51}
52
53/// This struct wraps a [WasmRepository] in a [ThreadBoundRunner] to allow it to be used as a
54/// [Repository] in a thread-safe manner. It implements the [Repository] trait directly, by
55/// converting the values as needed with [tsify_next::serde_wasm_bindgen].
56pub(crate) struct WasmRepositoryChannel<T, R: WasmRepository<T> + 'static>(
57    ThreadBoundRunner<R>,
58    PhantomData<T>,
59);
60
61impl<T, R: WasmRepository<T> + 'static> WasmRepositoryChannel<T, R> {
62    pub(crate) fn new(repository: R) -> Self {
63        Self(ThreadBoundRunner::new(repository), PhantomData)
64    }
65}
66
67#[async_trait::async_trait]
68impl<T: RepositoryItem + Serialize + DeserializeOwned, R: WasmRepository<T> + 'static> Repository<T>
69    for WasmRepositoryChannel<T, R>
70{
71    async fn get(&self, id: String) -> Result<Option<T>, RepositoryError> {
72        run_convert(&self.0, |s| async move { s.get(id).await }).await
73    }
74    async fn list(&self) -> Result<Vec<T>, RepositoryError> {
75        run_convert(&self.0, |s| async move { s.list().await }).await
76    }
77    async fn set(&self, id: String, value: T) -> Result<(), RepositoryError> {
78        run_convert(&self.0, |s| async move { s.set(id, value).await.and(UNIT) }).await
79    }
80    async fn remove(&self, id: String) -> Result<(), RepositoryError> {
81        run_convert(&self.0, |s| async move { s.remove(id).await.and(UNIT) }).await
82    }
83}
84
85#[wasm_bindgen(typescript_custom_section)]
86const REPOSITORY_CUSTOM_TS_TYPE: &'static str = r#"
87export interface Repository<T> {
88    get(id: string): Promise<T | null>;
89    list(): Promise<T[]>;
90    set(id: string, value: T): Promise<void>;
91    remove(id: string): Promise<void>;
92}
93"#;
94
95/// This macro generates a [::wasm_bindgen] interface for a repository type, and provides the
96/// implementation of [WasmRepository] and a way to convert it into something that implements
97/// the [Repository] trait.
98macro_rules! create_wasm_repository {
99    ($name:ident, $ty:ty, $typescript_ty:literal) => {
100        #[wasm_bindgen]
101        extern "C" {
102            #[wasm_bindgen(js_name = $name, typescript_type = $typescript_ty)]
103            pub type $name;
104
105            #[wasm_bindgen(method, catch)]
106            async fn get(
107                this: &$name,
108                id: String,
109            ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
110            #[wasm_bindgen(method, catch)]
111            async fn list(this: &$name)
112                -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
113            #[wasm_bindgen(method, catch)]
114            async fn set(
115                this: &$name,
116                id: String,
117                value: $ty,
118            ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
119            #[wasm_bindgen(method, catch)]
120            async fn remove(
121                this: &$name,
122                id: String,
123            ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
124        }
125
126        impl $crate::platform::repository::WasmRepository<$ty> for $name {
127            async fn get(
128                &self,
129                id: String,
130            ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
131                self.get(id).await
132            }
133            async fn list(&self) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
134                self.list().await
135            }
136            async fn set(
137                &self,
138                id: String,
139                value: $ty,
140            ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
141                self.set(id, value).await
142            }
143            async fn remove(
144                &self,
145                id: String,
146            ) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
147                self.remove(id).await
148            }
149        }
150
151        impl $name {
152            pub fn into_channel_impl(
153                self,
154            ) -> ::std::sync::Arc<impl bitwarden_state::repository::Repository<$ty>> {
155                use $crate::platform::repository::WasmRepositoryChannel;
156                ::std::sync::Arc::new(WasmRepositoryChannel::new(self))
157            }
158        }
159    };
160}
161pub(crate) use create_wasm_repository;
162
163const UNIT: Result<JsValue, JsValue> = Ok(JsValue::UNDEFINED);
164
165/// Utility function that runs a closure in a thread-bound manner, and converts the Result from
166/// [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>].
167async fn run_convert<T: 'static, Func, Fut, Ret>(
168    runner: &::bitwarden_threading::ThreadBoundRunner<T>,
169    f: Func,
170) -> Result<Ret, RepositoryError>
171where
172    Func: FnOnce(Rc<T>) -> Fut + Send + 'static,
173    Fut: Future<Output = Result<JsValue, JsValue>>,
174    Ret: serde::de::DeserializeOwned + Send + Sync + 'static,
175{
176    runner
177        .run_in_thread(|state| async move { convert_result(f(state).await) })
178        .await
179        .expect("Task should not panic")
180}
181
182/// Converts a [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>] using
183/// [tsify_next::serde_wasm_bindgen]
184fn convert_result<T: serde::de::DeserializeOwned>(
185    result: Result<JsValue, JsValue>,
186) -> Result<T, RepositoryError> {
187    result
188        .map_err(|e| RepositoryError::Internal(format!("{e:?}")))
189        .and_then(|value| {
190            ::tsify_next::serde_wasm_bindgen::from_value(value)
191                .map_err(|e| RepositoryError::Internal(e.to_string()))
192        })
193}