11// Copyright (c) Zefchain Labs, Inc.
22// SPDX-License-Identifier: Apache-2.0
33
4- use std:: { rc:: Rc , sync:: Arc } ;
4+ use std:: { future :: Future , pin :: Pin , rc:: Rc , sync:: Arc } ;
55
66use futures:: {
7- future:: { self , FutureExt as _} ,
7+ future:: { FutureExt as _, RemoteHandle } ,
88 lock:: Mutex as AsyncMutex ,
99} ;
1010use linera_base:: identifiers:: { AccountOwner , ChainId } ;
11- use linera_client:: chain_listener:: { ChainListener , ClientContext as _} ;
11+ use linera_client:: chain_listener:: { ChainListener , ChainListenerConfig , ClientContext as _} ;
1212use tokio_util:: sync:: CancellationToken ;
1313use wasm_bindgen:: prelude:: * ;
1414use web_sys:: wasm_bindgen;
1515
16- use crate :: { chain:: Chain , signer:: Signer , storage, wallet:: Wallet , Environment , Error , Result } ;
16+ use crate :: {
17+ chain:: Chain , signer:: Signer , storage, wallet:: Wallet , Environment , Error , Result , Storage ,
18+ } ;
1719
18- /// The full client API, exposed to the wallet implementation. Calls
19- /// to this API can be trusted to have originated from the user's
20- /// request.
21- #[ wasm_bindgen]
22- #[ derive( Clone ) ]
23- pub struct Client {
24- // This use of `futures::lock::Mutex` is safe because we only
25- // expose concurrency to the browser, which must always run all
26- // futures on the global task queue.
27- // It does nothing here in this single-threaded context, but is
28- // hard-coded by `ChainListener`.
29- pub ( crate ) client_context : Arc < AsyncMutex < linera_client:: ClientContext < Environment > > > ,
30- cancellation_token : CancellationToken ,
31- chain_listener_result :
32- future:: Shared < future:: RemoteHandle < Result < ( ) , Rc < linera_client:: Error > > > > ,
33- }
20+ pub ( crate ) type ClientContext = Arc < AsyncMutex < linera_client:: ClientContext < Environment > > > ;
3421
3522#[ derive( Default , serde:: Deserialize , tsify:: Tsify ) ]
3623#[ tsify( from_wasm_abi) ]
@@ -41,9 +28,24 @@ pub struct ChainOptions {
4128 owner : Option < AccountOwner > ,
4229}
4330
31+ /// A client that has been created but whose chain listener has not yet started.
32+ ///
33+ /// Use `chain()` to perform pre-start operations (e.g. `addOwner`), then call
34+ /// `start()` to begin background synchronization and obtain a [`RunningClient`].
35+ #[ wasm_bindgen]
36+ pub struct Client {
37+ pub ( crate ) client_context : ClientContext ,
38+ storage : Storage ,
39+ chain_listener_config : ChainListenerConfig ,
40+ }
41+
4442#[ wasm_bindgen]
4543impl Client {
46- /// Creates a new client and connects to the network.
44+ /// Creates a new client without starting the chain listener.
45+ ///
46+ /// After creating the client, you can perform operations like `addOwner`
47+ /// via `chain()`. Call `start()` to begin background chain synchronization
48+ /// and obtain a [`RunningClient`].
4749 ///
4850 /// # Errors
4951 /// On transport or protocol error, if persistent storage is
@@ -65,6 +67,7 @@ impl Client {
6567
6668 let default = wallet. default ;
6769 let genesis_config = wallet. genesis_config . clone ( ) ;
70+ let chain_listener_config = options. chain_listener_config . clone ( ) ;
6871
6972 let client = linera_client:: ClientContext :: new (
7073 storage. clone ( ) ,
@@ -80,59 +83,113 @@ impl Client {
8083 // The `Arc` here is useless, but it is required by the `ChainListener` API.
8184 #[ expect( clippy:: arc_with_non_send_sync) ]
8285 let client = Arc :: new ( AsyncMutex :: new ( client) ) ;
83- let client_clone = client. clone ( ) ;
84- let cancellation_token = tokio_util:: sync:: CancellationToken :: new ( ) ;
85- let chain_listener = ChainListener :: new (
86- options. chain_listener_config ,
87- client_clone,
86+
87+ Ok ( Client {
88+ client_context : client,
8889 storage,
90+ chain_listener_config,
91+ } )
92+ }
93+
94+ /// Connect to a chain on the Linera network.
95+ ///
96+ /// # Errors
97+ ///
98+ /// If the wallet could not be read or chain synchronization fails.
99+ #[ wasm_bindgen]
100+ pub async fn chain ( & self , chain : ChainId , options : Option < ChainOptions > ) -> Result < Chain > {
101+ make_chain ( & self . client_context , chain, options) . await
102+ }
103+
104+ /// Cleanly shut down the client without starting the chain listener.
105+ ///
106+ /// # Errors
107+ ///
108+ /// If the context is being referenced by any other objects (chains,
109+ /// applications…). Free these with `.free()` before disposing of this
110+ /// object.
111+ #[ wasm_bindgen( js_name = asyncDispose) ]
112+ pub async fn async_dispose ( self ) -> Result < ( ) > {
113+ Arc :: into_inner ( self . client_context ) . ok_or ( Error :: new (
114+ "Client disposed while being referenced elsewhere" ,
115+ ) ) ?;
116+ Ok ( ( ) )
117+ }
118+
119+ /// Starts the chain listener for background synchronization, consuming this
120+ /// `Client` and returning a [`RunningClient`].
121+ ///
122+ /// # Errors
123+ /// If the chain listener fails to start.
124+ #[ wasm_bindgen]
125+ pub async fn start ( self ) -> Result < RunningClient > {
126+ let cancellation_token = CancellationToken :: new ( ) ;
127+ let chain_listener_config = self . chain_listener_config ;
128+
129+ let chain_listener_handle = start_listener (
130+ chain_listener_config. clone ( ) ,
131+ self . client_context . clone ( ) ,
132+ self . storage ,
89133 cancellation_token. clone ( ) ,
90- tokio:: sync:: mpsc:: unbounded_channel ( ) . 1 ,
91- true , // Enable background sync
92134 )
93- . run ( )
94135 . await ?;
95136
96- let ( run_chain_listener, chain_listener_result) = async move {
97- let result = chain_listener. await . map_err ( |error| {
98- tracing:: error!( "ChainListener error: {error:?}" ) ;
99- Rc :: new ( error)
100- } ) ;
101- tracing:: debug!( "chain listener completed" ) ;
102- result
103- }
104- . remote_handle ( ) ;
105-
106- wasm_bindgen_futures:: spawn_local ( run_chain_listener) ;
107-
108- Ok ( Self {
109- client_context : client,
137+ Ok ( RunningClient {
138+ client_context : self . client_context ,
110139 cancellation_token,
111- chain_listener_result : chain_listener_result. shared ( ) ,
140+ chain_listener_handle : Box :: pin ( chain_listener_handle) ,
141+ chain_listener_config,
112142 } )
113143 }
144+ }
145+
146+ /// The full client API, exposed to the wallet implementation. Calls
147+ /// to this API can be trusted to have originated from the user's
148+ /// request.
149+ ///
150+ /// Obtained by calling [`Client::start()`].
151+ #[ wasm_bindgen]
152+ pub struct RunningClient {
153+ pub ( crate ) client_context : ClientContext ,
154+ cancellation_token : CancellationToken ,
155+ chain_listener_handle : Pin < Box < dyn Future < Output = Result < Storage , Rc < linera_client:: Error > > > > > ,
156+ chain_listener_config : ChainListenerConfig ,
157+ }
114158
159+ #[ wasm_bindgen]
160+ impl RunningClient {
115161 /// Connect to a chain on the Linera network.
116162 ///
117163 /// # Errors
118164 ///
119165 /// If the wallet could not be read or chain synchronization fails.
120166 #[ wasm_bindgen]
121167 pub async fn chain ( & self , chain : ChainId , options : Option < ChainOptions > ) -> Result < Chain > {
122- let options = options. unwrap_or_default ( ) ;
123- let mut chain_client = self
124- . client_context
125- . lock ( )
168+ make_chain ( & self . client_context , chain, options) . await
169+ }
170+
171+ /// Stops the chain listener and returns a [`Client`] that can be reconfigured
172+ /// and restarted.
173+ ///
174+ /// # Errors
175+ /// Propagates any errors that occurred during background execution of the
176+ /// chain listener.
177+ #[ wasm_bindgen]
178+ pub async fn stop ( self ) -> Result < Client > {
179+ self . cancellation_token . cancel ( ) ;
180+
181+ let storage = self
182+ . chain_listener_handle
126183 . await
127- . make_chain_client ( chain)
128- . await ?;
129- if let Some ( owner) = options. owner {
130- chain_client. set_preferred_owner ( owner) ;
131- }
184+ . map_err ( |e| match Rc :: into_inner ( e) {
185+ Some ( e) => Error :: from ( e) ,
186+ None => Error :: new ( "chain listener error (details unavailable)" ) ,
187+ } ) ?;
132188
133- Ok ( Chain {
134- chain_client,
135- client : self . clone ( ) ,
189+ Ok ( Client {
190+ client_context : self . client_context ,
191+ storage,
192+ chain_listener_config : self . chain_listener_config ,
136193 } )
137194 }
138195
@@ -151,16 +208,67 @@ impl Client {
151208 pub async fn async_dispose ( self ) -> Result < ( ) > {
152209 self . cancellation_token . cancel ( ) ;
153210
154- if let Err ( Some ( e) ) = self . chain_listener_result . await . map_err ( Rc :: into_inner) {
155- return Err ( e. into ( ) ) ;
211+ if let Err ( e) = self . chain_listener_handle . await {
212+ if let Some ( e) = Rc :: into_inner ( e) {
213+ return Err ( e. into ( ) ) ;
214+ }
156215 }
157216
158- let context = Arc :: into_inner ( self . client_context ) . ok_or ( Error :: new (
217+ Arc :: into_inner ( self . client_context ) . ok_or ( Error :: new (
159218 "Client disposed while being referenced elsewhere" ,
160219 ) ) ?;
161220
162- drop ( context) ;
163-
164221 Ok ( ( ) )
165222 }
166223}
224+
225+ /// Shared implementation for creating a [`Chain`] from a [`ClientContext`].
226+ async fn make_chain (
227+ client_context : & ClientContext ,
228+ chain : ChainId ,
229+ options : Option < ChainOptions > ,
230+ ) -> Result < Chain > {
231+ let options = options. unwrap_or_default ( ) ;
232+ let mut chain_client = client_context. lock ( ) . await . make_chain_client ( chain) . await ?;
233+ if let Some ( owner) = options. owner {
234+ chain_client. set_preferred_owner ( owner) ;
235+ }
236+
237+ Ok ( Chain {
238+ client_context : client_context. clone ( ) ,
239+ chain_client,
240+ } )
241+ }
242+
243+ async fn start_listener (
244+ config : ChainListenerConfig ,
245+ context : ClientContext ,
246+ storage : Storage ,
247+ cancellation_token : CancellationToken ,
248+ ) -> Result < RemoteHandle < Result < Storage , Rc < linera_client:: Error > > > > {
249+ tracing:: debug!( "starting chain listener..." ) ;
250+ let chain_listener = ChainListener :: new (
251+ config,
252+ context,
253+ storage,
254+ cancellation_token,
255+ tokio:: sync:: mpsc:: unbounded_channel ( ) . 1 ,
256+ true , // Enable background sync
257+ )
258+ . run ( )
259+ . await ?;
260+
261+ let ( run_chain_listener, chain_listener_handle) = async move {
262+ let result = chain_listener. await . map_err ( |error| {
263+ tracing:: error!( "ChainListener error: {error:?}" ) ;
264+ Rc :: new ( error)
265+ } ) ;
266+ tracing:: debug!( "chain listener completed" ) ;
267+ result
268+ }
269+ . remote_handle ( ) ;
270+
271+ wasm_bindgen_futures:: spawn_local ( run_chain_listener) ;
272+
273+ Ok ( chain_listener_handle)
274+ }
0 commit comments