Skip to content

Commit 558d418

Browse files
committed
Improve wasm repository and docs
1 parent 9540f64 commit 558d418

File tree

2 files changed

+142
-60
lines changed

2 files changed

+142
-60
lines changed

crates/bitwarden-uniffi/src/platform/repository.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ impl From<RepositoryError> for bitwarden_state::repository::RepositoryError {
3838
}
3939
}
4040

41+
/// This macro creates a Uniffi repository trait and its implementation for the
42+
/// [bitwarden_state::repository::Repository] trait
4143
macro_rules! create_uniffi_repository {
4244
($name:ident, $ty:ty) => {
4345
#[uniffi::export(with_foreign)]
Lines changed: 140 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,84 @@
1-
use wasm_bindgen::prelude::wasm_bindgen;
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 we can use
29+
* 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+
*/
33+
34+
use std::{future::Future, marker::PhantomData, rc::Rc};
35+
36+
use bitwarden_state::repository::{Repository, RepositoryError, RepositoryItem};
37+
use bitwarden_threading::ThreadBoundRunner;
38+
use serde::{de::DeserializeOwned, Serialize};
39+
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
40+
41+
/// This trait defines the methods that a [wasm_bindgen] repository must implement.
42+
/// The trait itself exists to provide a generic way of handling the [wasm_bindgen] interface, which
43+
/// is !Send + !Sync, and only deals with [JsValue].
44+
pub(crate) trait WasmRepository<T> {
45+
async fn get(&self, id: String) -> Result<JsValue, JsValue>;
46+
async fn list(&self) -> Result<JsValue, JsValue>;
47+
async fn set(&self, id: String, value: T) -> Result<JsValue, JsValue>;
48+
async fn remove(&self, id: String) -> Result<JsValue, JsValue>;
49+
}
50+
51+
/// This struct wraps a [WasmRepository] in a [ThreadBoundRunner] to allow it to be used as a
52+
/// [Repository] in a thread-safe manner. It implements the [Repository] trait directly, by
53+
/// converting the values as needed with [tsify_next::serde_wasm_bindgen].
54+
pub(crate) struct WasmRepositoryChannel<T, R: WasmRepository<T> + 'static>(
55+
ThreadBoundRunner<R>,
56+
PhantomData<T>,
57+
);
58+
59+
impl<T, R: WasmRepository<T> + 'static> WasmRepositoryChannel<T, R> {
60+
pub(crate) fn new(repository: R) -> Self {
61+
Self(ThreadBoundRunner::new(repository), PhantomData)
62+
}
63+
}
64+
65+
#[async_trait::async_trait]
66+
impl<T: RepositoryItem + Serialize + DeserializeOwned, R: WasmRepository<T> + 'static> Repository<T>
67+
for WasmRepositoryChannel<T, R>
68+
{
69+
async fn get(&self, id: String) -> Result<Option<T>, RepositoryError> {
70+
run_convert(&self.0, |s| async move { s.get(id).await }).await
71+
}
72+
async fn list(&self) -> Result<Vec<T>, RepositoryError> {
73+
run_convert(&self.0, |s| async move { s.list().await }).await
74+
}
75+
async fn set(&self, id: String, value: T) -> Result<(), RepositoryError> {
76+
run_convert(&self.0, |s| async move { s.set(id, value).await.and(UNIT) }).await
77+
}
78+
async fn remove(&self, id: String) -> Result<(), RepositoryError> {
79+
run_convert(&self.0, |s| async move { s.remove(id).await.and(UNIT) }).await
80+
}
81+
}
282

383
#[wasm_bindgen(typescript_custom_section)]
484
const REPOSITORY_CUSTOM_TS_TYPE: &'static str = r#"
@@ -10,6 +90,9 @@ export interface Repository<T> {
1090
}
1191
"#;
1292

93+
/// This macro generates a [wasm_bindgen] interface for a repository type, and provides the
94+
/// implementation of [WasmRepository] and a way to convert it into something that implements
95+
/// the [Repository] trait.
1396
macro_rules! create_wasm_repository {
1497
($name:ident, $ty:ty, $typescript_ty:literal) => {
1598
#[wasm_bindgen]
@@ -38,74 +121,71 @@ macro_rules! create_wasm_repository {
38121
) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue>;
39122
}
40123

124+
impl $crate::platform::repository::WasmRepository<$ty> for $name {
125+
async fn get(
126+
&self,
127+
id: String,
128+
) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
129+
self.get(id).await
130+
}
131+
async fn list(&self) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
132+
self.list().await
133+
}
134+
async fn set(
135+
&self,
136+
id: String,
137+
value: $ty,
138+
) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
139+
self.set(id, value).await
140+
}
141+
async fn remove(
142+
&self,
143+
id: String,
144+
) -> Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue> {
145+
self.remove(id).await
146+
}
147+
}
148+
41149
impl $name {
42150
pub fn into_channel_impl(
43151
self,
44152
) -> ::std::sync::Arc<impl bitwarden_state::repository::Repository<$ty>> {
45-
use ::bitwarden_state::repository::*;
46-
use $crate::platform::repository::__macro_internal::*;
47-
48-
struct Store(::bitwarden_threading::ThreadBoundRunner<$name>);
49-
let store = Store(::bitwarden_threading::ThreadBoundRunner::new(self));
50-
51-
#[async_trait::async_trait]
52-
impl Repository<$ty> for Store {
53-
async fn get(&self, id: String) -> Result<Option<$ty>, RepositoryError> {
54-
run_convert(&self.0, |s| async move { s.get(id).await }).await
55-
}
56-
async fn list(&self) -> Result<Vec<$ty>, RepositoryError> {
57-
run_convert(&self.0, |s| async move { s.list().await }).await
58-
}
59-
async fn set(&self, id: String, value: $ty) -> Result<(), RepositoryError> {
60-
run_convert(&self.0, |s| async move { s.set(id, value).await.and(UNIT) })
61-
.await
62-
}
63-
async fn remove(&self, id: String) -> Result<(), RepositoryError> {
64-
run_convert(&self.0, |s| async move { s.remove(id).await.and(UNIT) }).await
65-
}
66-
}
67-
68-
::std::sync::Arc::new(store)
153+
use $crate::platform::repository::WasmRepositoryChannel;
154+
::std::sync::Arc::new(WasmRepositoryChannel::new(self))
69155
}
70156
}
71157
};
72158
}
73-
pub(super) use create_wasm_repository;
74-
75-
/// Some utilities to handle the conversion of JsValue to Rust types.
76-
/// They exist outside the macro to try to reduce code bloat in the generated code.
77-
#[doc(hidden)]
78-
pub mod __macro_internal {
79-
use std::{future::Future, rc::Rc};
159+
pub(crate) use create_wasm_repository;
80160

81-
use bitwarden_state::repository::RepositoryError;
82-
use wasm_bindgen::JsValue;
161+
const UNIT: Result<JsValue, JsValue> = Ok(JsValue::UNDEFINED);
83162

84-
pub const UNIT: Result<JsValue, JsValue> = Ok(JsValue::UNDEFINED);
85-
86-
pub async fn run_convert<T: 'static, Func, Fut, Ret>(
87-
runner: &::bitwarden_threading::ThreadBoundRunner<T>,
88-
f: Func,
89-
) -> Result<Ret, RepositoryError>
90-
where
91-
Func: FnOnce(Rc<T>) -> Fut + Send + 'static,
92-
Fut: Future<Output = Result<JsValue, JsValue>>,
93-
Ret: serde::de::DeserializeOwned + Send + Sync + 'static,
94-
{
95-
runner
96-
.run_in_thread(|state| async move { convert_result(f(state).await) })
97-
.await
98-
.expect("Task should not panic")
99-
}
163+
/// Utility function that runs a closure in a thread-bound manner, and converts the Result from
164+
/// [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>].
165+
async fn run_convert<T: 'static, Func, Fut, Ret>(
166+
runner: &::bitwarden_threading::ThreadBoundRunner<T>,
167+
f: Func,
168+
) -> Result<Ret, RepositoryError>
169+
where
170+
Func: FnOnce(Rc<T>) -> Fut + Send + 'static,
171+
Fut: Future<Output = Result<JsValue, JsValue>>,
172+
Ret: serde::de::DeserializeOwned + Send + Sync + 'static,
173+
{
174+
runner
175+
.run_in_thread(|state| async move { convert_result(f(state).await) })
176+
.await
177+
.expect("Task should not panic")
178+
}
100179

101-
fn convert_result<T: serde::de::DeserializeOwned>(
102-
result: Result<JsValue, JsValue>,
103-
) -> Result<T, RepositoryError> {
104-
result
105-
.map_err(|e| RepositoryError::Internal(format!("{e:?}")))
106-
.and_then(|value| {
107-
::tsify_next::serde_wasm_bindgen::from_value(value)
108-
.map_err(|e| RepositoryError::Internal(e.to_string()))
109-
})
110-
}
180+
/// Converts a [Result<JsValue, JsValue>] to a typed [Result<T, RepositoryError>] using
181+
/// [tsify_next::serde_wasm_bindgen]
182+
fn convert_result<T: serde::de::DeserializeOwned>(
183+
result: Result<JsValue, JsValue>,
184+
) -> Result<T, RepositoryError> {
185+
result
186+
.map_err(|e| RepositoryError::Internal(format!("{e:?}")))
187+
.and_then(|value| {
188+
::tsify_next::serde_wasm_bindgen::from_value(value)
189+
.map_err(|e| RepositoryError::Internal(e.to_string()))
190+
})
111191
}

0 commit comments

Comments
 (0)