Skip to content

Commit c06f0c8

Browse files
committed
[web] separate Client from RunningClient to support early calls to add_owner
1 parent 72b902d commit c06f0c8

5 files changed

Lines changed: 181 additions & 71 deletions

File tree

linera-client/src/chain_listener.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ impl<C: ClientContext + 'static> ChainListener<C> {
291291

292292
/// Runs the chain listener.
293293
#[instrument(skip(self))]
294-
pub async fn run(mut self) -> Result<impl Future<Output = Result<(), Error>>, Error> {
294+
pub async fn run(
295+
mut self,
296+
) -> Result<impl Future<Output = Result<<C::Environment as Environment>::Storage, Error>>, Error>
297+
{
295298
let chain_ids = {
296299
let guard = self.context.lock().await;
297300
let admin_chain_id = guard.admin_chain_id();
@@ -340,7 +343,7 @@ impl<C: ClientContext + 'static> ChainListener<C> {
340343
}
341344
}
342345
future::join_all(self.listening.into_values().map(|client| client.stop())).await;
343-
Ok(())
346+
Ok(self.storage)
344347
})
345348
}
346349

web/@linera/client/src/chain/application.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use linera_core::client::ChainClient;
66
use wasm_bindgen::prelude::*;
77
use web_sys::wasm_bindgen;
88

9-
use crate::{Client, Environment, JsResult};
9+
use crate::{client::ClientContext, Environment, JsResult};
1010

1111
#[wasm_bindgen]
1212
pub struct Application {
13-
pub(crate) client: Client,
13+
pub(crate) client_context: ClientContext,
1414
pub(crate) chain_client: ChainClient<Environment>,
1515
pub(crate) id: ApplicationId,
1616
}
@@ -70,7 +70,6 @@ impl Application {
7070

7171
if !operations.is_empty() {
7272
let _hash = self
73-
.client
7473
.client_context
7574
.lock()
7675
.await

web/@linera/client/src/chain/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ use serde::ser::Serialize as _;
1414
use wasm_bindgen::prelude::*;
1515
use web_sys::{js_sys, wasm_bindgen};
1616

17-
use crate::{Client, Environment, JsResult};
17+
use crate::{client::ClientContext, Environment, JsResult};
1818

1919
pub mod application;
2020
pub use application::Application;
2121

2222
#[wasm_bindgen]
2323
pub struct Chain {
24-
pub(crate) client: Client,
24+
pub(crate) client_context: ClientContext,
2525
pub(crate) chain_client: ChainClient<Environment>,
2626
}
2727

@@ -143,7 +143,7 @@ impl Chain {
143143
pub async fn validator_version_info(&self) -> JsResult<JsValue> {
144144
self.chain_client.synchronize_from_validators().await?;
145145
let result = self.chain_client.local_committee().await;
146-
let mut client = self.client.client_context.lock().await;
146+
let mut client = self.client_context.lock().await;
147147
client.update_wallet(&self.chain_client).await?;
148148
let committee = result?;
149149
let node_provider = client.make_node_provider();
@@ -187,7 +187,7 @@ impl Chain {
187187
pub async fn application(&self, id: &str) -> JsResult<Application> {
188188
web_sys::console::debug_1(&format!("connecting to Linera application {id}").into());
189189
Ok(Application {
190-
client: self.client.clone(),
190+
client_context: self.client_context.clone(),
191191
chain_client: self.chain_client.clone(),
192192
id: id.parse()?,
193193
})

web/@linera/client/src/client.rs

Lines changed: 169 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,23 @@
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

66
use futures::{
7-
future::{self, FutureExt as _},
7+
future::{FutureExt as _, RemoteHandle},
88
lock::Mutex as AsyncMutex,
99
};
1010
use linera_base::identifiers::{AccountOwner, ChainId};
11-
use linera_client::chain_listener::{ChainListener, ClientContext as _};
11+
use linera_client::chain_listener::{ChainListener, ChainListenerConfig, ClientContext as _};
1212
use tokio_util::sync::CancellationToken;
1313
use wasm_bindgen::prelude::*;
1414
use 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]
4543
impl 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+
}

web/@linera/client/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use wasm_bindgen::prelude::*;
2424
use web_sys::wasm_bindgen;
2525

2626
pub mod client;
27-
pub use client::Client;
27+
pub use client::{Client, RunningClient};
2828
pub mod chain;
2929
pub use chain::Chain;
3030
pub mod error;

0 commit comments

Comments
 (0)