diff --git a/examples/bridge-demo/index.html b/examples/bridge-demo/index.html index 3478c9b1df5e..06a86d12073a 100644 --- a/examples/bridge-demo/index.html +++ b/examples/bridge-demo/index.html @@ -665,7 +665,7 @@
-requesting chain…
const chainId = await faucet.claimChain(wallet, owner);
document.getElementById('owner').innerText = owner;
document.getElementById('chain-id').innerText = chainId;
- const client = await new linera.Client(wallet, signer);
+ const client = await (await new linera.Client(wallet, signer)).start();
let chain = await client.chain(chainId);
const counter = await chain.application(import.meta.env.LINERA_APPLICATION_ID);
const logs = document.getElementById('logs');
diff --git a/examples/counter/metamask/index.html b/examples/counter/metamask/index.html
index 317c4801dbe0..a43d7363bb6b 100644
--- a/examples/counter/metamask/index.html
+++ b/examples/counter/metamask/index.html
@@ -75,11 +75,16 @@ requesting chain…
const autosigner = linera.signer.PrivateKey.createRandom();
wallet.setOwner(chainId, autosigner.address());
- const client = await new linera.Client(wallet, new linera.signer.Composite(autosigner, signer));
- const chain = await client.chain(chainId, { owner });
+ const deferredClient = await new linera.Client(wallet, new linera.signer.Composite(autosigner, signer));
+
+ // For autosigning: add the in-memory signer as an owner before starting the chain listener,
+ // so the user only needs to sign once.
+ const deferredChain = await deferredClient.chain(chainId, { owner });
+ await deferredChain.addOwner(autosigner.address());
+ deferredChain.free();
- // For autosigning: we then add the in-memory signer as an owner of the chain to allow it to sign transactions.
- await chain.addOwner(autosigner.address());
+ const client = await deferredClient.start();
+ const chain = await client.chain(chainId, { owner });
// Connect to the counter application on the chain.
const counter = await chain.application(import.meta.env.LINERA_APPLICATION_ID);
diff --git a/examples/native-fungible/index.html b/examples/native-fungible/index.html
index 72e8325bb71c..2994bc76a6c7 100644
--- a/examples/native-fungible/index.html
+++ b/examples/native-fungible/index.html
@@ -224,7 +224,7 @@ requesting a new microchai
const wallet = await faucet.createWallet();
const owner = signer.address();
const chainId = await faucet.claimChain(wallet, owner);
- const client = await new linera.Client(wallet, signer);
+ const client = await (await new linera.Client(wallet, signer)).start();
document.querySelector('#chain-id').innerText = chainId;
document.querySelector('#owner').innerText = owner;
diff --git a/examples/native-fungible/metamask/index.html b/examples/native-fungible/metamask/index.html
index 10b7e4f01b85..936b21d99e36 100644
--- a/examples/native-fungible/metamask/index.html
+++ b/examples/native-fungible/metamask/index.html
@@ -221,11 +221,16 @@ Chain history for requesting a new microchai
const autosigner = linera.signer.PrivateKey.createRandom();
wallet.setOwner(chainId, autosigner.address());
- const client = await new linera.Client(wallet, new linera.signer.Composite(autosigner, signer));
- const chain = await client.chain(chainId, { owner });
+ const deferredClient = await new linera.Client(wallet, new linera.signer.Composite(autosigner, signer));
+
+ // For autosigning: add the in-memory signer as an owner before starting the chain listener,
+ // so the user only needs to sign once.
+ const deferredChain = await deferredClient.chain(chainId, { owner });
+ await deferredChain.addOwner(autosigner.address());
+ deferredChain.free();
- // For autosigning: we then add the in-memory signer as an owner of the chain to allow it to sign transactions.
- await chain.addOwner(autosigner.address());
+ const client = await deferredClient.start();
+ const chain = await client.chain(chainId, { owner });
// Connect to the fungible application on the chain.
const application = await chain.application(import.meta.env.LINERA_APPLICATION_ID);
diff --git a/linera-client/src/chain_listener.rs b/linera-client/src/chain_listener.rs
index 4f4a484a1dab..3b54a397c13a 100644
--- a/linera-client/src/chain_listener.rs
+++ b/linera-client/src/chain_listener.rs
@@ -291,7 +291,10 @@ impl ChainListener {
/// Runs the chain listener.
#[instrument(skip(self))]
- pub async fn run(mut self) -> Result>, Error> {
+ pub async fn run(
+ mut self,
+ ) -> Result::Storage, Error>>, Error>
+ {
let chain_ids = {
let guard = self.context.lock().await;
let admin_chain_id = guard.admin_chain_id();
@@ -340,7 +343,7 @@ impl ChainListener {
}
}
future::join_all(self.listening.into_values().map(|client| client.stop())).await;
- Ok(())
+ Ok(self.storage)
})
}
diff --git a/linera-faucet/server/src/lib.rs b/linera-faucet/server/src/lib.rs
index c0d21bfc80e2..0187b0d6d59a 100644
--- a/linera-faucet/server/src/lib.rs
+++ b/linera-faucet/server/src/lib.rs
@@ -1292,7 +1292,7 @@ where
.with_graceful_shutdown(cancellation_token.cancelled_owned())
.into_future();
futures::select! {
- result = Box::pin(chain_listener).fuse() => result?,
+ result = Box::pin(chain_listener).fuse() => { result?; },
_ = Box::pin(batch_processor_task).fuse() => {},
result = Box::pin(server).fuse() => result?,
};
diff --git a/linera-service/src/node_service.rs b/linera-service/src/node_service.rs
index 9150057e3d7c..8d739b1832db 100644
--- a/linera-service/src/node_service.rs
+++ b/linera-service/src/node_service.rs
@@ -1402,7 +1402,7 @@ where
.with_graceful_shutdown(cancellation_token.cancelled_owned())
.into_future();
futures::select! {
- result = chain_listener => result?,
+ result = chain_listener => { result?; },
result = Box::pin(server).fuse() => result?,
};
diff --git a/web/@linera/client/src/chain/application.rs b/web/@linera/client/src/chain/application.rs
index a444bc64512f..f63888c0bb64 100644
--- a/web/@linera/client/src/chain/application.rs
+++ b/web/@linera/client/src/chain/application.rs
@@ -6,11 +6,11 @@ use linera_core::client::ChainClient;
use wasm_bindgen::prelude::*;
use web_sys::wasm_bindgen;
-use crate::{Client, Environment, JsResult};
+use crate::{client::ClientContext, Environment, JsResult};
#[wasm_bindgen]
pub struct Application {
- pub(crate) client: Client,
+ pub(crate) client_context: ClientContext,
pub(crate) chain_client: ChainClient,
pub(crate) id: ApplicationId,
}
@@ -70,7 +70,6 @@ impl Application {
if !operations.is_empty() {
let _hash = self
- .client
.client_context
.lock()
.await
diff --git a/web/@linera/client/src/chain/mod.rs b/web/@linera/client/src/chain/mod.rs
index 2c408de88859..902e52b42206 100644
--- a/web/@linera/client/src/chain/mod.rs
+++ b/web/@linera/client/src/chain/mod.rs
@@ -14,14 +14,14 @@ use serde::ser::Serialize as _;
use wasm_bindgen::prelude::*;
use web_sys::{js_sys, wasm_bindgen};
-use crate::{Client, Environment, JsResult};
+use crate::{client::ClientContext, Environment, JsResult};
pub mod application;
pub use application::Application;
#[wasm_bindgen]
pub struct Chain {
- pub(crate) client: Client,
+ pub(crate) client_context: ClientContext,
pub(crate) chain_client: ChainClient,
}
@@ -80,7 +80,6 @@ impl Chain {
#[wasm_bindgen]
pub async fn transfer(&self, params: TransferParams) -> JsResult<()> {
let _hash = self
- .client
.client_context
.lock()
.await
@@ -124,8 +123,7 @@ impl Chain {
options: Option,
) -> JsResult<()> {
let AddOwnerOptions { weight } = options.unwrap_or_default();
- self.client
- .client_context
+ self.client_context
.lock()
.await
.apply_client_command(&self.chain_client, |_chain_client| {
@@ -143,7 +141,7 @@ impl Chain {
pub async fn validator_version_info(&self) -> JsResult {
self.chain_client.synchronize_from_validators().await?;
let result = self.chain_client.local_committee().await;
- let mut client = self.client.client_context.lock().await;
+ let mut client = self.client_context.lock().await;
client.update_wallet(&self.chain_client).await?;
let committee = result?;
let node_provider = client.make_node_provider();
@@ -187,7 +185,7 @@ impl Chain {
pub async fn application(&self, id: &str) -> JsResult {
web_sys::console::debug_1(&format!("connecting to Linera application {id}").into());
Ok(Application {
- client: self.client.clone(),
+ client_context: self.client_context.clone(),
chain_client: self.chain_client.clone(),
id: id.parse()?,
})
diff --git a/web/@linera/client/src/client.rs b/web/@linera/client/src/client.rs
index a5fe10c97bb2..240227b8db4c 100644
--- a/web/@linera/client/src/client.rs
+++ b/web/@linera/client/src/client.rs
@@ -1,36 +1,23 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
-use std::{rc::Rc, sync::Arc};
+use std::{future::Future, pin::Pin, rc::Rc, sync::Arc};
use futures::{
- future::{self, FutureExt as _},
+ future::{FutureExt as _, RemoteHandle},
lock::Mutex as AsyncMutex,
};
use linera_base::identifiers::{AccountOwner, ChainId};
-use linera_client::chain_listener::{ChainListener, ClientContext as _};
+use linera_client::chain_listener::{ChainListener, ChainListenerConfig, ClientContext as _};
use tokio_util::sync::CancellationToken;
use wasm_bindgen::prelude::*;
use web_sys::wasm_bindgen;
-use crate::{chain::Chain, signer::Signer, storage, wallet::Wallet, Environment, Error, Result};
+use crate::{
+ chain::Chain, signer::Signer, storage, wallet::Wallet, Environment, Error, Result, Storage,
+};
-/// The full client API, exposed to the wallet implementation. Calls
-/// to this API can be trusted to have originated from the user's
-/// request.
-#[wasm_bindgen]
-#[derive(Clone)]
-pub struct Client {
- // This use of `futures::lock::Mutex` is safe because we only
- // expose concurrency to the browser, which must always run all
- // futures on the global task queue.
- // It does nothing here in this single-threaded context, but is
- // hard-coded by `ChainListener`.
- pub(crate) client_context: Arc>>,
- cancellation_token: CancellationToken,
- chain_listener_result:
- future::Shared>>>,
-}
+pub(crate) type ClientContext = Arc>>;
#[derive(Default, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
@@ -41,9 +28,24 @@ pub struct ChainOptions {
owner: Option,
}
+/// A client that has been created but whose chain listener has not yet started.
+///
+/// Use `chain()` to perform pre-start operations (e.g. `addOwner`), then call
+/// `start()` to begin background synchronization and obtain a [`RunningClient`].
+#[wasm_bindgen]
+pub struct Client {
+ pub(crate) client_context: ClientContext,
+ storage: Storage,
+ chain_listener_config: ChainListenerConfig,
+}
+
#[wasm_bindgen]
impl Client {
- /// Creates a new client and connects to the network.
+ /// Creates a new client without starting the chain listener.
+ ///
+ /// After creating the client, you can perform operations like `addOwner`
+ /// via `chain()`. Call `start()` to begin background chain synchronization
+ /// and obtain a [`RunningClient`].
///
/// # Errors
/// On transport or protocol error, if persistent storage is
@@ -65,6 +67,7 @@ impl Client {
let default = wallet.default;
let genesis_config = wallet.genesis_config.clone();
+ let chain_listener_config = options.chain_listener_config.clone();
let client = linera_client::ClientContext::new(
storage.clone(),
@@ -80,38 +83,81 @@ impl Client {
// The `Arc` here is useless, but it is required by the `ChainListener` API.
#[expect(clippy::arc_with_non_send_sync)]
let client = Arc::new(AsyncMutex::new(client));
- let client_clone = client.clone();
- let cancellation_token = tokio_util::sync::CancellationToken::new();
- let chain_listener = ChainListener::new(
- options.chain_listener_config,
- client_clone,
+
+ Ok(Client {
+ client_context: client,
storage,
+ chain_listener_config,
+ })
+ }
+
+ /// Connect to a chain on the Linera network.
+ ///
+ /// # Errors
+ ///
+ /// If the wallet could not be read or chain synchronization fails.
+ #[wasm_bindgen]
+ pub async fn chain(&self, chain: ChainId, options: Option) -> Result {
+ make_chain(&self.client_context, chain, options).await
+ }
+
+ /// Cleanly shut down the client without starting the chain listener.
+ ///
+ /// # Errors
+ ///
+ /// If the context is being referenced by any other objects (chains,
+ /// applications…). Free these with `.free()` before disposing of this
+ /// object.
+ #[wasm_bindgen(js_name = asyncDispose)]
+ pub async fn async_dispose(self) -> Result<()> {
+ Arc::into_inner(self.client_context).ok_or(Error::new(
+ "Client disposed while being referenced elsewhere",
+ ))?;
+ Ok(())
+ }
+
+ /// Starts the chain listener for background synchronization, consuming this
+ /// `Client` and returning a [`RunningClient`].
+ ///
+ /// # Errors
+ /// If the chain listener fails to start.
+ #[wasm_bindgen]
+ pub async fn start(self) -> Result {
+ let cancellation_token = CancellationToken::new();
+ let chain_listener_config = self.chain_listener_config;
+
+ let chain_listener_handle = start_listener(
+ chain_listener_config.clone(),
+ self.client_context.clone(),
+ self.storage,
cancellation_token.clone(),
- tokio::sync::mpsc::unbounded_channel().1,
- true, // Enable background sync
)
- .run()
.await?;
- let (run_chain_listener, chain_listener_result) = async move {
- let result = chain_listener.await.map_err(|error| {
- tracing::error!("ChainListener error: {error:?}");
- Rc::new(error)
- });
- tracing::debug!("chain listener completed");
- result
- }
- .remote_handle();
-
- wasm_bindgen_futures::spawn_local(run_chain_listener);
-
- Ok(Self {
- client_context: client,
+ Ok(RunningClient {
+ client_context: self.client_context,
cancellation_token,
- chain_listener_result: chain_listener_result.shared(),
+ chain_listener_handle: Box::pin(chain_listener_handle),
+ chain_listener_config,
})
}
+}
+
+/// The full client API, exposed to the wallet implementation. Calls
+/// to this API can be trusted to have originated from the user's
+/// request.
+///
+/// Obtained by calling [`Client::start()`].
+#[wasm_bindgen]
+pub struct RunningClient {
+ pub(crate) client_context: ClientContext,
+ cancellation_token: CancellationToken,
+ chain_listener_handle: Pin>>>>,
+ chain_listener_config: ChainListenerConfig,
+}
+#[wasm_bindgen]
+impl RunningClient {
/// Connect to a chain on the Linera network.
///
/// # Errors
@@ -119,20 +165,31 @@ impl Client {
/// If the wallet could not be read or chain synchronization fails.
#[wasm_bindgen]
pub async fn chain(&self, chain: ChainId, options: Option) -> Result {
- let options = options.unwrap_or_default();
- let mut chain_client = self
- .client_context
- .lock()
+ make_chain(&self.client_context, chain, options).await
+ }
+
+ /// Stops the chain listener and returns a [`Client`] that can be reconfigured
+ /// and restarted.
+ ///
+ /// # Errors
+ /// Propagates any errors that occurred during background execution of the
+ /// chain listener.
+ #[wasm_bindgen]
+ pub async fn stop(self) -> Result {
+ self.cancellation_token.cancel();
+
+ let storage = self
+ .chain_listener_handle
.await
- .make_chain_client(chain)
- .await?;
- if let Some(owner) = options.owner {
- chain_client.set_preferred_owner(owner);
- }
+ .map_err(|e| match Rc::into_inner(e) {
+ Some(e) => Error::from(e),
+ None => Error::new("chain listener error (details unavailable)"),
+ })?;
- Ok(Chain {
- chain_client,
- client: self.clone(),
+ Ok(Client {
+ client_context: self.client_context,
+ storage,
+ chain_listener_config: self.chain_listener_config,
})
}
@@ -151,16 +208,67 @@ impl Client {
pub async fn async_dispose(self) -> Result<()> {
self.cancellation_token.cancel();
- if let Err(Some(e)) = self.chain_listener_result.await.map_err(Rc::into_inner) {
- return Err(e.into());
+ if let Err(e) = self.chain_listener_handle.await {
+ if let Some(e) = Rc::into_inner(e) {
+ return Err(e.into());
+ }
}
- let context = Arc::into_inner(self.client_context).ok_or(Error::new(
+ Arc::into_inner(self.client_context).ok_or(Error::new(
"Client disposed while being referenced elsewhere",
))?;
- drop(context);
-
Ok(())
}
}
+
+/// Shared implementation for creating a [`Chain`] from a [`ClientContext`].
+async fn make_chain(
+ client_context: &ClientContext,
+ chain: ChainId,
+ options: Option,
+) -> Result {
+ let options = options.unwrap_or_default();
+ let mut chain_client = client_context.lock().await.make_chain_client(chain).await?;
+ if let Some(owner) = options.owner {
+ chain_client.set_preferred_owner(owner);
+ }
+
+ Ok(Chain {
+ client_context: client_context.clone(),
+ chain_client,
+ })
+}
+
+async fn start_listener(
+ config: ChainListenerConfig,
+ context: ClientContext,
+ storage: Storage,
+ cancellation_token: CancellationToken,
+) -> Result>>> {
+ tracing::debug!("starting chain listener...");
+ let chain_listener = ChainListener::new(
+ config,
+ context,
+ storage,
+ cancellation_token,
+ tokio::sync::mpsc::unbounded_channel().1,
+ true, // Enable background sync
+ )
+ .run()
+ .await?;
+
+ let (run_chain_listener, chain_listener_handle) = async move {
+ let result = chain_listener.await.map_err(|error| {
+ tracing::error!("ChainListener error: {error:?}");
+ Rc::new(error)
+ });
+ tracing::debug!("chain listener completed");
+ result
+ }
+ .remote_handle();
+
+ wasm_bindgen_futures::spawn_local(run_chain_listener);
+
+ Ok(chain_listener_handle)
+}
diff --git a/web/@linera/client/src/lib.rs b/web/@linera/client/src/lib.rs
index b83bc867a223..803fe0f7a6d0 100644
--- a/web/@linera/client/src/lib.rs
+++ b/web/@linera/client/src/lib.rs
@@ -24,7 +24,7 @@ use wasm_bindgen::prelude::*;
use web_sys::wasm_bindgen;
pub mod client;
-pub use client::Client;
+pub use client::{Client, RunningClient};
pub mod chain;
pub use chain::Chain;
pub mod error;