pub struct Sync { /* private fields */ }Expand description
Synchronization manager for the database.
The Sync module is a thin frontend that communicates with a background sync engine thread via command channels. All actual sync operations, transport communication, and state management happen in the background thread.
§Multi-Transport Support
Multiple transports can be enabled simultaneously (e.g., HTTP + Iroh P2P), allowing peers to be reachable via different networks. Requests are automatically routed to the appropriate transport based on address type.
// Enable both HTTP and Iroh transports
sync.enable_http_transport().await?;
sync.enable_iroh_transport().await?;
// Start servers on all transports
sync.start_server("127.0.0.1:0").await?;
// Get all server addresses
let addresses = sync.get_all_server_addresses().await?;Implementations§
Source§impl Sync
impl Sync
Sourcepub async fn sync_with_peer_for_bootstrap_with_key(
&self,
address: &Address,
tree_id: &ID,
requesting_public_key: &PublicKey,
requesting_key_name: &str,
requested_permission: Permission,
) -> Result<()>
pub async fn sync_with_peer_for_bootstrap_with_key( &self, address: &Address, tree_id: &ID, requesting_public_key: &PublicKey, requesting_key_name: &str, requested_permission: Permission, ) -> Result<()>
Sync with a peer for bootstrap using a user-provided public key.
This method is specifically designed for bootstrap scenarios where the local device doesn’t have access to the target tree yet and needs to request permission during the initial sync. The public key is provided directly rather than looked up from backend storage, making it compatible with User API managed keys.
§Arguments
address- The transport address of the peer to sync withtree_id- The ID of the tree to syncrequesting_public_key- The formatted public key string (e.g., “ed25519:base64…”)requesting_key_name- The name/ID of the requesting key for audit trailrequested_permission- The permission level being requested
§Returns
A Result indicating success or failure.
§Example
// With User API managed keys:
let public_key = user.get_public_key(user_key_id)?;
sync.sync_with_peer_for_bootstrap_with_key(
&Address::http("127.0.0.1:8080"),
&tree_id,
&public_key,
user_key_id,
Permission::Write(5),
).await?;Sourcepub async fn bootstrap_with_ticket(
&self,
ticket: &DatabaseTicket,
requesting_public_key: &PublicKey,
requesting_key_name: &str,
requested_permission: Permission,
) -> Result<()>
pub async fn bootstrap_with_ticket( &self, ticket: &DatabaseTicket, requesting_public_key: &PublicKey, requesting_key_name: &str, requested_permission: Permission, ) -> Result<()>
Bootstrap with a peer using a DatabaseTicket.
Tries every address hint in the ticket concurrently. Succeeds if at least one address connects and syncs; returns the last error if all fail.
§Arguments
ticket- A ticket containing the database ID and address hints.requesting_public_key- The formatted public key string for authentication.requesting_key_name- The name/ID of the requesting key.requested_permission- The permission level being requested.
§Errors
Returns SyncError::InvalidAddress if the ticket has no address hints.
Returns the last sync error if no address succeeded.
Sourcepub async fn pending_bootstrap_requests(
&self,
) -> Result<Vec<(String, BootstrapRequest)>>
pub async fn pending_bootstrap_requests( &self, ) -> Result<Vec<(String, BootstrapRequest)>>
Get all pending bootstrap requests.
§Returns
A vector of (request_id, bootstrap_request) pairs for pending requests.
Sourcepub async fn approved_bootstrap_requests(
&self,
) -> Result<Vec<(String, BootstrapRequest)>>
pub async fn approved_bootstrap_requests( &self, ) -> Result<Vec<(String, BootstrapRequest)>>
Get all approved bootstrap requests.
§Returns
A vector of (request_id, bootstrap_request) pairs for approved requests.
Sourcepub async fn rejected_bootstrap_requests(
&self,
) -> Result<Vec<(String, BootstrapRequest)>>
pub async fn rejected_bootstrap_requests( &self, ) -> Result<Vec<(String, BootstrapRequest)>>
Get all rejected bootstrap requests.
§Returns
A vector of (request_id, bootstrap_request) pairs for rejected requests.
Sourcepub async fn get_bootstrap_request(
&self,
request_id: &str,
) -> Result<Option<(String, BootstrapRequest)>>
pub async fn get_bootstrap_request( &self, request_id: &str, ) -> Result<Option<(String, BootstrapRequest)>>
Sourcepub async fn approve_bootstrap_request_with_key(
&self,
request_id: &str,
key: &DatabaseKey,
) -> Result<()>
pub async fn approve_bootstrap_request_with_key( &self, request_id: &str, key: &DatabaseKey, ) -> Result<()>
Approve a bootstrap request using a DatabaseKey.
This variant allows approval using keys that are not stored in the backend, such as user keys managed in memory.
§Arguments
request_id- The unique identifier of the request to approvekey- TheDatabaseKeyto use for the transaction and audit trail
§Returns
Result indicating success or failure of the approval operation.
§Errors
Returns SyncError::InsufficientPermission if the approving key does not have
Admin permission on the target database.
Sourcepub async fn reject_bootstrap_request_with_key(
&self,
request_id: &str,
key: &DatabaseKey,
) -> Result<()>
pub async fn reject_bootstrap_request_with_key( &self, request_id: &str, key: &DatabaseKey, ) -> Result<()>
Reject a bootstrap request using a DatabaseKey with Admin permission validation.
This variant allows rejection using keys that are not stored in the backend, such as user keys managed in memory. It validates that the rejecting user has Admin permission on the target database before allowing the rejection.
§Arguments
request_id- The unique identifier of the request to rejectkey- TheDatabaseKeyto use for permission validation and audit trail
§Returns
Result indicating success or failure of the rejection operation.
§Errors
Returns SyncError::InsufficientPermission if the rejecting key does not have
Admin permission on the target database.
Source§impl Sync
impl Sync
Sourcepub async fn sync_tree_with_peer(
&self,
peer_pubkey: &PublicKey,
tree_id: &ID,
) -> Result<()>
pub async fn sync_tree_with_peer( &self, peer_pubkey: &PublicKey, tree_id: &ID, ) -> Result<()>
Synchronize a specific tree with a peer using bidirectional sync.
This is the main synchronization method that implements tip exchange and bidirectional entry transfer to keep trees in sync between peers. It performs both pull (fetch missing entries) and push (send our entries).
§Arguments
peer_pubkey- The public key of the peer to sync withtree_id- The ID of the tree to synchronize
§Returns
A Result indicating success or failure of the sync operation.
Sourcepub async fn send_entries(
&self,
entries: impl AsRef<[Entry]>,
address: &Address,
) -> Result<()>
pub async fn send_entries( &self, entries: impl AsRef<[Entry]>, address: &Address, ) -> Result<()>
Sourcepub async fn send_entries_to_peer(
&self,
peer_id: &PeerId,
entries: Vec<Entry>,
) -> Result<()>
pub async fn send_entries_to_peer( &self, peer_id: &PeerId, entries: Vec<Entry>, ) -> Result<()>
Send specific entries to a peer via the background sync engine.
This method queues entries for direct transmission without duplicate filtering. The caller is responsible for determining which entries should be sent.
§Duplicate Prevention Architecture
Eidetica uses smart duplicate prevention in the background sync engine:
- Database sync (
SyncWithPeercommand): Uses tip comparison for semantic filtering - Direct send (this method): Trusts caller to provide appropriate entries
For automatic duplicate prevention, use tree-based sync relationships instead of calling this method directly.
§Arguments
peer_id- The peer ID to send toentries- The specific entries to send (no filtering applied)
§Returns
A Result indicating whether the command was successfully queued for background processing.
Sourcepub fn queue_entry_for_sync(
&self,
peer_id: &PeerId,
entry_id: &ID,
tree_id: &ID,
) -> Result<()>
pub fn queue_entry_for_sync( &self, peer_id: &PeerId, entry_id: &ID, tree_id: &ID, ) -> Result<()>
Queue an entry for sync to a peer (non-blocking, for use in callbacks).
This method is designed for use in write callbacks where async operations are not possible. It uses try_send to avoid blocking, and logs errors rather than failing the callback.
§Arguments
peer_pubkey- The public key of the peer to sync withentry_id- The ID of the entry to queuetree_id- The tree ID where the entry belongs
§Returns
Ok(()) if the entry was successfully queued. Only returns Err if transport is not enabled.
Sourcepub async fn discover_peer_trees(
&self,
address: &Address,
) -> Result<Vec<TreeInfo>>
pub async fn discover_peer_trees( &self, address: &Address, ) -> Result<Vec<TreeInfo>>
Discover available trees from a peer (simplified API).
This method connects to a peer and retrieves the list of trees they’re willing to sync. This is useful for discovering what can be synced before setting up sync relationships.
§Arguments
address- The transport address of the peer.
§Returns
A vector of TreeInfo describing available trees, or an error.
Sourcepub async fn sync_with_peer(
&self,
address: &Address,
tree_id: Option<&ID>,
) -> Result<()>
pub async fn sync_with_peer( &self, address: &Address, tree_id: Option<&ID>, ) -> Result<()>
Sync with a peer at a given address.
This is a blocking convenience method that:
- Connects to discover the peer’s public key
- Registers the peer and performs immediate sync
- Returns after sync completes
For new code, prefer using register_sync_peer()
directly, which registers intent and lets background sync handle it.
§Arguments
address- The transport address of the peer.tree_id- Optional tree ID to sync (None = discover available trees)
§Returns
Result indicating success or failure.
Sourcepub async fn sync_with_ticket(&self, ticket: &DatabaseTicket) -> Result<()>
pub async fn sync_with_ticket(&self, ticket: &DatabaseTicket) -> Result<()>
Sync with a peer using a DatabaseTicket.
Attempts sync_with_peer for every address
hint in the ticket concurrently. Each address may point to a different
peer, so connections are independent. Succeeds if at least one address
syncs successfully; returns the last error if all fail.
§Arguments
ticket- A ticket containing the database ID and address hints.
§Errors
Returns SyncError::InvalidAddress if the ticket has no address hints.
Returns the last sync error if no address succeeded.
Sourcepub async fn sync_tree_with_peer_auth(
&self,
peer_pubkey: &PublicKey,
tree_id: &ID,
requesting_key: Option<&PublicKey>,
requesting_key_name: Option<&str>,
requested_permission: Option<Permission>,
) -> Result<()>
pub async fn sync_tree_with_peer_auth( &self, peer_pubkey: &PublicKey, tree_id: &ID, requesting_key: Option<&PublicKey>, requesting_key_name: Option<&str>, requested_permission: Option<Permission>, ) -> Result<()>
Sync a specific tree with a peer, with optional authentication for bootstrap.
This is a lower-level method that allows specifying authentication parameters for bootstrap scenarios where access needs to be requested.
§Arguments
peer_pubkey- The public key of the peer to sync withtree_id- The ID of the tree to syncrequesting_key- Optional public key requesting access (for bootstrap)requesting_key_name- Optional name/ID of the requesting keyrequested_permission- Optional permission level being requested
§Returns
A Result indicating success or failure.
Sourcepub async fn flush(&self) -> Result<()>
pub async fn flush(&self) -> Result<()>
Process all queued entries and retry any failed sends.
This method:
- Retries all entries in the retry queue (ignoring backoff timers)
- Processes all entries in the sync queue (batched by peer)
When this method returns, all pending sync work has been attempted. This is useful to eensuree that all pending pushes have completed.
§Returns
Ok(()) if all operations completed successfully, or an error
if the background sync engine is not running or sends failed.
Source§impl Sync
impl Sync
Sourcepub async fn register_peer(
&self,
pubkey: &PublicKey,
display_name: Option<&str>,
) -> Result<()>
pub async fn register_peer( &self, pubkey: &PublicKey, display_name: Option<&str>, ) -> Result<()>
Sourcepub async fn update_peer_status(
&self,
pubkey: &PublicKey,
status: PeerStatus,
) -> Result<()>
pub async fn update_peer_status( &self, pubkey: &PublicKey, status: PeerStatus, ) -> Result<()>
Sourcepub async fn list_peers(&self) -> Result<Vec<PeerInfo>>
pub async fn list_peers(&self) -> Result<Vec<PeerInfo>>
Sourcepub async fn remove_peer(&self, pubkey: &PublicKey) -> Result<()>
pub async fn remove_peer(&self, pubkey: &PublicKey) -> Result<()>
Sourcepub async fn register_sync_peer(&self, info: SyncPeerInfo) -> Result<SyncHandle>
pub async fn register_sync_peer(&self, info: SyncPeerInfo) -> Result<SyncHandle>
Register a peer for syncing (declarative API).
This is the recommended way to set up syncing. It immediately registers the peer and tree/peer relationship, then the background sync engine handles the actual data synchronization.
§Arguments
info- Information about the peer and sync configuration
§Returns
A handle for tracking sync status and adding more address hints.
§Example
// Register peer for syncing
let handle = sync.register_sync_peer(SyncPeerInfo {
peer_pubkey,
tree_id,
addresses: vec![Address {
transport_type: "http".to_string(),
address: "http://localhost:8080".to_string(),
}],
auth: None,
display_name: Some("My Peer".to_string()),
}).await?;
// Optionally wait for initial sync
handle.wait_for_initial_sync().await?;
// Check status anytime
let status = handle.status().await?;
println!("Has local data: {}", status.has_local_data);Sourcepub async fn get_sync_status(
&self,
tree_id: &ID,
_peer_pubkey: &PublicKey,
) -> Result<SyncStatus>
pub async fn get_sync_status( &self, tree_id: &ID, _peer_pubkey: &PublicKey, ) -> Result<SyncStatus>
Sourcepub async fn add_tree_sync(
&self,
peer_pubkey: &PublicKey,
tree_root_id: impl AsRef<str>,
) -> Result<()>
pub async fn add_tree_sync( &self, peer_pubkey: &PublicKey, tree_root_id: impl AsRef<str>, ) -> Result<()>
Sourcepub async fn remove_tree_sync(
&self,
peer_pubkey: &PublicKey,
tree_root_id: impl AsRef<str>,
) -> Result<()>
pub async fn remove_tree_sync( &self, peer_pubkey: &PublicKey, tree_root_id: impl AsRef<str>, ) -> Result<()>
Sourcepub async fn connect_to_peer(&self, address: &Address) -> Result<PublicKey>
pub async fn connect_to_peer(&self, address: &Address) -> Result<PublicKey>
Connect to a remote peer and perform handshake.
This method initiates a connection to a peer, performs the handshake protocol, and automatically registers the peer if successful.
§Arguments
address- The address of the peer to connect to
§Returns
A Result containing the peer’s public key if successful.
Sourcepub async fn update_peer_connection_state(
&self,
pubkey: &PublicKey,
state: ConnectionState,
) -> Result<()>
pub async fn update_peer_connection_state( &self, pubkey: &PublicKey, state: ConnectionState, ) -> Result<()>
Sourcepub async fn is_tree_synced_with_peer(
&self,
peer_pubkey: &PublicKey,
tree_root_id: impl AsRef<str>,
) -> Result<bool>
pub async fn is_tree_synced_with_peer( &self, peer_pubkey: &PublicKey, tree_root_id: impl AsRef<str>, ) -> Result<bool>
Sourcepub async fn add_peer_address(
&self,
peer_pubkey: &PublicKey,
address: Address,
) -> Result<()>
pub async fn add_peer_address( &self, peer_pubkey: &PublicKey, address: Address, ) -> Result<()>
Sourcepub async fn remove_peer_address(
&self,
peer_pubkey: &PublicKey,
address: &Address,
) -> Result<bool>
pub async fn remove_peer_address( &self, peer_pubkey: &PublicKey, address: &Address, ) -> Result<bool>
Source§impl Sync
impl Sync
Sourcepub async fn stop_server(&self) -> Result<()>
pub async fn stop_server(&self) -> Result<()>
Stop the running sync server (async version).
Stops servers on all transports.
§Returns
A Result indicating success or failure of server shutdown.
Sourcepub async fn accept_connections(&self) -> Result<()>
pub async fn accept_connections(&self) -> Result<()>
Start accepting incoming connections on all registered transports.
Must be called each time the instance is created to accept inbound sync requests.
This starts servers on all transports that have been registered via register_transport().
Each transport uses its pre-configured bind address.
§Example
instance.enable_sync().await?;
let sync = instance.sync().unwrap();
// Register transports with their configurations
sync.register_transport("http-local", HttpTransport::builder()
.bind("127.0.0.1:8080")
).await?;
sync.register_transport("p2p", IrohTransport::builder()).await?;
// Start all servers
sync.accept_connections().await?;Sourcepub async fn accept_connections_on(&self, name: impl Into<String>) -> Result<()>
pub async fn accept_connections_on(&self, name: impl Into<String>) -> Result<()>
Start accepting incoming connections on a specific named transport.
Use this when you want fine-grained control over which transports accept connections and when.
§Arguments
name- The name of the transport to start (as used inregister_transport)
§Example
// Register transports
sync.register_transport("http-local", HttpTransport::builder()
.bind("127.0.0.1:8080")
).await?;
sync.register_transport("p2p", IrohTransport::builder()).await?;
// Only start HTTP server (P2P stays inactive)
sync.accept_connections_on("http-local").await?;Sourcepub async fn add_transport(
&self,
name: impl Into<String>,
transport: Box<dyn SyncTransport>,
) -> Result<()>
pub async fn add_transport( &self, name: impl Into<String>, transport: Box<dyn SyncTransport>, ) -> Result<()>
Add a named transport to the sync system.
If a transport with the same name already exists, it will be replaced (the old transport’s server will be stopped if running).
§Arguments
name- Unique name for this transport instance (e.g., “http-local”, “p2p”)transport- The transport to add
Sourcepub async fn register_transport<B: TransportBuilder>(
&self,
name: impl Into<String>,
builder: B,
) -> Result<()>where
B::Transport: 'static,
pub async fn register_transport<B: TransportBuilder>(
&self,
name: impl Into<String>,
builder: B,
) -> Result<()>where
B::Transport: 'static,
Register a named transport instance with persisted state.
This is the recommended way to add transports. The builder’s build() method
receives persisted state for this named instance (may be empty on first run)
and can update it. The updated state is automatically saved.
§Arguments
name- Unique name for this transport instance (e.g., “http-local”, “p2p”)builder- The transport builder that creates the transport
§Example
use eidetica::sync::transports::http::HttpTransport;
use eidetica::sync::transports::iroh::IrohTransport;
// Register an HTTP transport with bind address
sync.register_transport("http-local", HttpTransport::builder()
.bind("127.0.0.1:8080")
).await?;
// Register an Iroh transport (generates and persists node ID on first run)
sync.register_transport("p2p", IrohTransport::builder()).await?;
// Register multiple Iroh transports with different identities
sync.register_transport("p2p-work", IrohTransport::builder()).await?;
sync.register_transport("p2p-personal", IrohTransport::builder()).await?;Sourcepub async fn get_server_address(&self) -> Result<String>
pub async fn get_server_address(&self) -> Result<String>
Get a server address if any transport is running a server.
Returns the first available raw server address string (e.g.,
127.0.0.1:8080). To produce a shareable link, use
create_ticket to generate a full ticket URL,
or construct an Address with Address::new for programmatic use.
Use get_server_address_for for a specific transport.
§Returns
The address of the first running server, or an error if no server is running.
Sourcepub async fn get_server_address_for(&self, name: &str) -> Result<String>
pub async fn get_server_address_for(&self, name: &str) -> Result<String>
Sourcepub async fn get_all_server_addresses(&self) -> Result<Vec<(String, String)>>
pub async fn get_all_server_addresses(&self) -> Result<Vec<(String, String)>>
Get all server addresses for running transports.
§Returns
A vector of (transport_type, address) pairs for all running servers.
Sourcepub async fn create_ticket(&self, database_id: &ID) -> Result<DatabaseTicket>
pub async fn create_ticket(&self, database_id: &ID) -> Result<DatabaseTicket>
Generate a ticket for a database using all running transports’ addresses.
The ticket contains the database ID and address hints from all running transport servers.
§Arguments
database_id- The ID of the database to create a ticket for
§Returns
A DatabaseTicket containing the database ID and transport address hints.
Source§impl Sync
impl Sync
Sourcepub async fn sync_user(
&self,
user_uuid: impl AsRef<str>,
preferences_db_id: &ID,
) -> Result<()>
pub async fn sync_user( &self, user_uuid: impl AsRef<str>, preferences_db_id: &ID, ) -> Result<()>
Synchronize a user’s preferences with the sync system.
This establishes tracking for a user’s preferences database and synchronizes their current preferences to the sync tree. The sync system will monitor the user’s preferences and automatically sync databases according to their settings.
This method ensures the user is tracked and reads their preferences database to update sync configuration. It detects changes via tip comparison and only processes updates when preferences have changed.
This operation is idempotent and can be called multiple times safely.
CRITICAL: All updates to the sync tree happen in a single transaction to ensure atomicity.
§Arguments
user_uuid- The user’s unique identifierpreferences_db_id- The ID of the user’s private database
§Returns
A Result indicating success or an error.
§Example
// After creating or logging in a user
let user = instance.login_user("alice", Some("password"))?;
sync.sync_user(user.user_uuid(), user.user_database().root_id())?;Source§impl Sync
impl Sync
Sourcepub fn sync_tree_root_id(&self) -> &ID
pub fn sync_tree_root_id(&self) -> &ID
Get the root ID of the sync settings tree.
Sourcepub async fn set_setting(
&self,
key: impl Into<String>,
value: impl Into<String>,
) -> Result<()>
pub async fn set_setting( &self, key: impl Into<String>, value: impl Into<String>, ) -> Result<()>
Store a setting in the sync_settings subtree.
§Arguments
key- The setting keyvalue- The setting value
Sourcepub async fn load_transport_config<T: TransportConfig>(
&self,
name: &str,
) -> Result<T>
pub async fn load_transport_config<T: TransportConfig>( &self, name: &str, ) -> Result<T>
Load a transport configuration from the _sync database.
Transport configurations are stored in the transports subtree,
keyed by their name. If no configuration exists for the transport,
returns the default configuration.
§Type Parameters
T- The transport configuration type implementingTransportConfig
§Arguments
name- The name of the transport instance (e.g., “iroh”, “http”)
§Returns
The loaded configuration, or the default if not found.
§Example
use eidetica::sync::transports::iroh::IrohTransportConfig;
let config: IrohTransportConfig = sync.load_transport_config("iroh")?;Sourcepub async fn save_transport_config<T: TransportConfig>(
&self,
name: &str,
config: &T,
) -> Result<()>
pub async fn save_transport_config<T: TransportConfig>( &self, name: &str, config: &T, ) -> Result<()>
Save a transport configuration to the _sync database.
Transport configurations are stored in the transports subtree,
keyed by their name. This persists the configuration so it can
be loaded on subsequent startups.
§Type Parameters
T- The transport configuration type implementingTransportConfig
§Arguments
name- The name of the transport instance (e.g., “iroh”, “http”)config- The configuration to save
§Example
use eidetica::sync::transports::iroh::IrohTransportConfig;
let mut config = IrohTransportConfig::default();
config.get_or_create_secret_key(); // Generate key
sync.save_transport_config("iroh", &config)?;Sourcepub fn get_device_pubkey(&self) -> Result<PublicKey>
pub fn get_device_pubkey(&self) -> Result<PublicKey>
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for Sync
impl !RefUnwindSafe for Sync
impl Send for Sync
impl Sync for Sync
impl Unpin for Sync
impl !UnwindSafe for Sync
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> CompatExt for T
impl<T> CompatExt for T
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more