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
+ }
2
82
3
83
#[ wasm_bindgen( typescript_custom_section) ]
4
84
const REPOSITORY_CUSTOM_TS_TYPE : & ' static str = r#"
@@ -10,6 +90,9 @@ export interface Repository<T> {
10
90
}
11
91
"# ;
12
92
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.
13
96
macro_rules! create_wasm_repository {
14
97
( $name: ident, $ty: ty, $typescript_ty: literal) => {
15
98
#[ wasm_bindgen]
@@ -38,74 +121,71 @@ macro_rules! create_wasm_repository {
38
121
) -> Result <:: wasm_bindgen:: JsValue , :: wasm_bindgen:: JsValue >;
39
122
}
40
123
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
+
41
149
impl $name {
42
150
pub fn into_channel_impl(
43
151
self ,
44
152
) -> :: 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 ) )
69
155
}
70
156
}
71
157
} ;
72
158
}
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;
80
160
81
- use bitwarden_state:: repository:: RepositoryError ;
82
- use wasm_bindgen:: JsValue ;
161
+ const UNIT : Result < JsValue , JsValue > = Ok ( JsValue :: UNDEFINED ) ;
83
162
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
+ }
100
179
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
+ } )
111
191
}
0 commit comments