feat: separate embedded logos client (#166)

* feat: separate embedded p2p delievery to its own crate

* feat: separate p2p config in its own crate

* chore: split embed module

* chore: refactor registry config

* feat: split logos chat crate

* chore: refactor
This commit is contained in:
kaichao 2026-07-09 15:12:58 +08:00 committed by GitHub
parent b19d0c6e67
commit 939a63e8bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 274 additions and 226 deletions

View File

@ -20,12 +20,12 @@ jobs:
- run: rustup update stable && rustup default stable - run: rustup update stable && rustup default stable
# hashgraph-like-consensus's build.rs shells out to protoc via prost-build. # hashgraph-like-consensus's build.rs shells out to protoc via prost-build.
- run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
# chat-cli pulls in components' embedded_p2p_delivery feature, whose # chat-cli and the embedded-logos-delivery crate link liblogosdelivery
# build.rs links liblogosdelivery (built via Nix or LOGOS_DELIVERY_LIB_DIR). # (built via Nix or LOGOS_DELIVERY_LIB_DIR). The smoketest job builds and
# The smoketest job builds and exercises it under Nix; here we keep the # exercises them under Nix; here we keep the toolchain-only job fast by
# toolchain-only job fast by skipping it. # skipping them.
- run: cargo build --verbose --workspace --exclude chat-cli - run: cargo build --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery
- run: cargo test --verbose --workspace --exclude chat-cli - run: cargo test --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery
clippy: clippy:
name: Clippy name: Clippy

26
Cargo.lock generated
View File

@ -2072,6 +2072,20 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "embedded-logos-delivery"
version = "0.1.0"
dependencies = [
"base64",
"crossbeam-channel",
"libchat",
"logos-generic-chat",
"serde",
"serde_json",
"thiserror",
"tracing",
]
[[package]] [[package]]
name = "enum-ordinalize" name = "enum-ordinalize"
version = "4.3.2" version = "4.3.2"
@ -3599,6 +3613,18 @@ dependencies = [
[[package]] [[package]]
name = "logos-chat" name = "logos-chat"
version = "0.1.0" version = "0.1.0"
dependencies = [
"components",
"crossbeam-channel",
"embedded-logos-delivery",
"libchat",
"logos-account",
"logos-generic-chat",
]
[[package]]
name = "logos-generic-chat"
version = "0.1.0"
dependencies = [ dependencies = [
"chat-sqlite", "chat-sqlite",
"components", "components",

View File

@ -12,8 +12,10 @@ members = [
"core/shared-traits", "core/shared-traits",
"core/sqlite", "core/sqlite",
"core/storage", "core/storage",
"crates/client", "crates/logos-chat",
"crates/generic-chat",
"extensions/components", "extensions/components",
"extensions/embedded-logos-delivery",
] ]
default-members = [ default-members = [
@ -25,7 +27,7 @@ default-members = [
"core/shared-traits", "core/shared-traits",
"core/sqlite", "core/sqlite",
"core/storage", "core/storage",
"crates/client", "crates/generic-chat",
] ]
[workspace.dependencies] [workspace.dependencies]
@ -34,8 +36,10 @@ logos-account = { path = "core/account" }
chat-sqlite = { path = "core/sqlite" } chat-sqlite = { path = "core/sqlite" }
components = { path = "extensions/components" } components = { path = "extensions/components" }
crypto = { path = "core/crypto" } crypto = { path = "core/crypto" }
embedded-logos-delivery = { path = "extensions/embedded-logos-delivery" }
libchat = { path = "core/conversations" } libchat = { path = "core/conversations" }
logos-chat = { path = "crates/client" } logos-chat = { path = "crates/logos-chat" }
logos-generic-chat = { path = "crates/generic-chat" }
shared-traits = { path = "core/shared-traits" } shared-traits = { path = "core/shared-traits" }
storage = { path = "core/storage" } storage = { path = "core/storage" }

View File

@ -11,7 +11,7 @@ path = "src/main.rs"
# Workspace dependencies (sorted) # Workspace dependencies (sorted)
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
logos-account = { workspace = true, features = ["dev"] } logos-account = { workspace = true, features = ["dev"] }
logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] } logos-chat = { workspace = true }
# External dependencies (sorted) # External dependencies (sorted)
anyhow = "1.0" anyhow = "1.0"

View File

@ -8,11 +8,9 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use logos_account::TestLogosAccount;
use logos_chat::{ use logos_chat::{
AccountDirectory, ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event, AccountDirectory, ChatClient, ChatStore, Event, LogosConfig, P2pConfig, RegistrationService,
HttpRegistry, LogosChatClient, LogosConfig, NETWORK_PRESET, REGISTRY_ENDPOINT, Transport,
RegistrationService, StorageConfig, Transport,
}; };
use app::ChatApp; use app::ChatApp;
@ -78,24 +76,27 @@ fn main() -> Result<()> {
let db_str = db_path(&cli)?; let db_str = db_path(&cli)?;
match cli.transport { match cli.transport {
// logos-delivery is the transport baked into `LogosChatClient`, so the
// Logos client opens it from config rather than receiving one.
TransportKind::LogosDelivery => { TransportKind::LogosDelivery => {
let preset = cli.preset.as_deref().unwrap_or(NETWORK_PRESET); let mut p2p_config = P2pConfig::default();
println!("Starting logos-delivery node (preset={preset})..."); if let Some(port) = cli.port {
p2p_config.tcp_port = port;
}
if let Some(preset) = cli.preset.as_deref() {
p2p_config.preset = preset.to_string();
}
println!(
"Starting logos-delivery node (preset={})...",
p2p_config.preset
);
println!("This may take a few seconds while connecting to the network."); println!("This may take a few seconds while connecting to the network.");
let mut config = LogosConfig::new(db_str, "chat-cli"); let mut config = LogosConfig::new(db_str, "chat-cli");
if let Some(port) = cli.port {
config.set_tcp_port(port);
}
if let Some(preset) = cli.preset.as_deref() {
config.set_preset(preset);
}
if let Some(registry_url) = cli.registry_url.as_deref() { if let Some(registry_url) = cli.registry_url.as_deref() {
config.set_registry_url(registry_url); config.set_registry_url(registry_url);
} }
let (client, events) = LogosChatClient::open(config) config.set_p2p_config(p2p_config);
let (client, events) = logos_chat::open(config)
.map_err(|e| anyhow::anyhow!("{e:?}")) .map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to open chat client")?; .context("failed to open chat client")?;
@ -104,32 +105,17 @@ fn main() -> Result<()> {
} }
// The file transport is a local-only path: it reuses the Logos service // The file transport is a local-only path: it reuses the Logos service
// stack (delegate identity, HTTP registry, encrypted storage) but swaps // stack (delegate identity, HTTP registry, encrypted storage) but swaps
// the transport, so it builds a client directly instead of going through // the transport in via `open_with_transport`.
// `LogosChatClient`.
TransportKind::File => { TransportKind::File => {
let transport_dir = cli.data.join("transport"); let transport_dir = cli.data.join("transport");
let transport = transport::file::FileTransport::new(&transport_dir) let transport = transport::file::FileTransport::new(&transport_dir)
.context("failed to create file transport")?; .context("failed to create file transport")?;
let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT); let mut config = LogosConfig::new(db_str, "chat-cli");
// A fresh dev account endorsing a fresh delegate each launch, if let Some(registry_url) = cli.registry_url.as_deref() {
// mirroring `LogosChatClient::open`. config.set_registry_url(registry_url);
let account = TestLogosAccount::new(); }
let delegate = DelegateSigner::random(); let (client, events) = logos_chat::open_with_transport(config, transport)
let mut registry = HttpRegistry::new(endpoint);
account
.add_delegate_signer(&mut registry, delegate.public_key())
.map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to publish the device bundle")?;
let (client, events) = ChatClientBuilder::new(account.address())
.ident(delegate)
.transport(transport)
.registration(registry)
.storage_config(StorageConfig::Encrypted {
path: db_str,
key: "chat-cli".to_string(),
})
.build()
.map_err(|e| anyhow::anyhow!("{e:?}")) .map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to open chat client")?; .context("failed to open chat client")?;

View File

@ -1,10 +0,0 @@
//! Baked-in configuration for the Logos service stack.
/// The endpoint for the account and keypackage registration service.
pub const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co";
/// The logos-delivery network preset the Logos client joins by default.
pub const NETWORK_PRESET: &str = "logos.dev";
/// Default TCP port for the embedded logos-delivery node.
pub const DEFAULT_TCP_PORT: u16 = 60000;

View File

@ -1,123 +0,0 @@
//! The opinionated Logos client.
//!
//! `LogosChatClient` commits to the Logos service stack so independently built
//! clients share the same production services instead of each re-deriving them:
//! a delegate identity, the HTTP keypackage + account registry, encrypted
//! on-disk storage, and — unlike the generic [`ChatClientBuilder`] — the
//! logos-delivery transport itself. logos-delivery *is* the transport for the
//! Logos client, so the caller no longer supplies one; only per-client secrets
//! (the database path and key) and the network config are passed in.
//!
//! The logos-delivery transport carries a native dependency, so this whole
//! module is gated behind the `embedded-p2p-delivery` cargo feature. The registry
//! endpoint lives in [`crate::config`] instead, so it stays available to other
//! transports when the feature is off.
use components::{EmbeddedP2pDeliveryService, HttpRegistry, P2pConfig};
use crossbeam_channel::Receiver;
use libchat::{ChatStorage, StorageConfig};
use logos_account::TestLogosAccount;
use crate::ChatClientBuilder;
use crate::client::{ChatClient, Transport};
use crate::config::{DEFAULT_TCP_PORT, NETWORK_PRESET, REGISTRY_ENDPOINT};
use crate::delegate::DelegateSigner;
use crate::errors::ClientError;
use crate::event::Event;
// logos-delivery already implements `DeliveryService`; teaching it the inbound
// half here (in the crate that owns `Transport`) makes it a full transport, so
// callers need no wrapper newtype.
impl Transport for EmbeddedP2pDeliveryService {
fn inbound(&mut self) -> Receiver<Vec<u8>> {
self.inbound_queue()
}
}
/// Configuration for opening a [`LogosChatClient`].
///
/// `db_path` (a per-client location) and `db_key` (a secret) are required and
/// never baked into the library. The TCP port and the network config default to
/// the baked-in Logos values; override them with the setters (e.g. to point at a
/// local deployment).
pub struct LogosConfig {
db_path: String,
db_key: String,
tcp_port: u16,
preset: String,
registry_url: String,
}
impl LogosConfig {
/// Config for the required per-client `db_path` and `db_key`. The TCP port,
/// network preset, and registry endpoint default to the baked-in Logos
/// values; override them with [`set_tcp_port`](Self::set_tcp_port),
/// [`set_preset`](Self::set_preset), and
/// [`set_registry_url`](Self::set_registry_url).
pub fn new(db_path: impl Into<String>, db_key: impl Into<String>) -> Self {
Self {
db_path: db_path.into(),
db_key: db_key.into(),
tcp_port: DEFAULT_TCP_PORT,
preset: NETWORK_PRESET.to_string(),
registry_url: REGISTRY_ENDPOINT.to_string(),
}
}
/// Override the TCP port for the embedded logos-delivery node.
pub fn set_tcp_port(&mut self, tcp_port: u16) {
self.tcp_port = tcp_port;
}
/// Override the logos-delivery network preset (defaults to the baked-in
/// [`NETWORK_PRESET`]).
pub fn set_preset(&mut self, preset: impl Into<String>) {
self.preset = preset.into();
}
/// Override the registry endpoint (account + keypackage store; defaults to
/// the baked-in [`REGISTRY_ENDPOINT`]).
pub fn set_registry_url(&mut self, registry_url: impl Into<String>) {
self.registry_url = registry_url.into();
}
}
/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`]
/// identity acting for a fresh dev account, the HTTP keypackage + account
/// registry ([`HttpRegistry`], which is both the keypackage store and the
/// account → device directory), encrypted [`ChatStorage`], and the
/// logos-delivery transport.
pub type LogosChatClient = ChatClient<EmbeddedP2pDeliveryService, HttpRegistry, ChatStorage>;
impl LogosChatClient {
/// Open a client on the Logos stack per `config`, starting a logos-delivery
/// node as its transport and persisting to the encrypted database.
pub fn open(config: LogosConfig) -> Result<(Self, Receiver<Event>), ClientError> {
let transport = EmbeddedP2pDeliveryService::start(P2pConfig {
preset: config.preset,
tcp_port: config.tcp_port,
..Default::default()
})
.map_err(|e| ClientError::Transport(e.to_string()))?;
// A fresh account endorsing a fresh delegate each open: the account
// key is dropped after publishing the bundle, so devices cannot be
// added later. A caller-supplied, custody-holding account replaces
// this once the platform provides one.
let account = TestLogosAccount::new();
let delegate = DelegateSigner::random();
let mut registry = HttpRegistry::new(config.registry_url);
account
.add_delegate_signer(&mut registry, delegate.public_key())
.map_err(|e| ClientError::BundlePublish(e.to_string()))?;
ChatClientBuilder::new(account.address())
.ident(delegate)
.transport(transport)
.registration(registry)
.storage_config(StorageConfig::Encrypted {
path: config.db_path,
key: config.db_key,
})
.build()
}
}

View File

@ -1,14 +1,11 @@
[package] [package]
name = "logos-chat" name = "logos-generic-chat"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[lib] [lib]
crate-type = ["rlib"] crate-type = ["rlib"]
[features]
embedded-p2p-delivery = ["components/embedded_p2p_delivery"]
[dependencies] [dependencies]
# Workspace dependencies (sorted) # Workspace dependencies (sorted)
chat-sqlite = { workspace = true } chat-sqlite = { workspace = true }

View File

@ -1,9 +1,9 @@
# message-exchange # message-exchange
An example Rust application built on top of [`crates/client`](../../). An example Rust application built on top of [`crates/generic-chat`](../../).
It demonstrates that creating a working chat client in pure Rust is trivial: depend on It demonstrates that creating a working chat client in pure Rust is trivial: depend on
`crates/client`, pick a `DeliveryService` implementation (here the in-memory `crates/generic-chat`, pick a `DeliveryService` implementation (here the in-memory
`InProcessDelivery` shipped with the crate), and wire up `ChatClient`. No boilerplate, no FFI. `InProcessDelivery` shipped with the crate), and wire up `ChatClient`. No boilerplate, no FFI.
## Running ## Running

View File

@ -1,6 +1,6 @@
use components::EphemeralRegistry; use components::EphemeralRegistry;
use logos_account::TestLogosAccount; use logos_account::TestLogosAccount;
use logos_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus}; use logos_generic_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
use std::time::Duration; use std::time::Duration;
fn main() { fn main() {

View File

@ -1,22 +1,16 @@
mod builder; mod builder;
mod client; mod client;
mod config;
mod delegate; mod delegate;
mod delivery_in_process; mod delivery_in_process;
mod errors; mod errors;
mod event; mod event;
#[cfg(feature = "embedded-p2p-delivery")]
mod logos;
pub use builder::{ChatClientBuilder, Unset}; pub use builder::{ChatClientBuilder, Unset};
pub use client::{ChatClient, Transport}; pub use client::{ChatClient, Transport};
pub use config::{DEFAULT_TCP_PORT, NETWORK_PRESET, REGISTRY_ENDPOINT};
pub use delegate::DelegateSigner; pub use delegate::DelegateSigner;
pub use delivery_in_process::{InProcessDelivery, MessageBus}; pub use delivery_in_process::{InProcessDelivery, MessageBus};
pub use errors::ClientError; pub use errors::ClientError;
pub use event::{Event, MessageSender}; pub use event::{Event, MessageSender};
#[cfg(feature = "embedded-p2p-delivery")]
pub use logos::{LogosChatClient, LogosConfig};
// Re-export types callers need to interact with ChatClient. // Re-export types callers need to interact with ChatClient.
pub use libchat::{ pub use libchat::{

View File

@ -4,7 +4,7 @@ use components::EphemeralRegistry;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use crypto::Ed25519VerifyingKey; use crypto::Ed25519VerifyingKey;
use logos_account::TestLogosAccount; use logos_account::TestLogosAccount;
use logos_chat::{ use logos_generic_chat::{
AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event, AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event,
InProcessDelivery, MessageBus, Transport, InProcessDelivery, MessageBus, Transport,
}; };
@ -30,7 +30,7 @@ fn create_test_client(
ChatClient<InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>, ChatClient<InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>,
Receiver<Event>, Receiver<Event>,
), ),
logos_chat::ClientError, logos_generic_chat::ClientError,
> { > {
let account = TestLogosAccount::new(); let account = TestLogosAccount::new();
let delegate = DelegateSigner::random(); let delegate = DelegateSigner::random();
@ -375,10 +375,16 @@ fn unpublished_account_address_is_an_error() {
let err = saro let err = saro
.create_direct_conversation(&unpublished.address()) .create_direct_conversation(&unpublished.address())
.expect_err("no bundle published for the account"); .expect_err("no bundle published for the account");
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_))); assert!(matches!(
err,
logos_generic_chat::ClientError::AccountResolution(_)
));
let err = saro let err = saro
.create_direct_conversation("not-an-account-address") .create_direct_conversation("not-an-account-address")
.expect_err("not an account key"); .expect_err("not an account key");
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_))); assert!(matches!(
err,
logos_generic_chat::ClientError::AccountResolution(_)
));
} }

View File

@ -0,0 +1,16 @@
[package]
name = "logos-chat"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["rlib"]
[dependencies]
# Workspace dependencies (sorted)
components = { workspace = true }
crossbeam-channel = { workspace = true }
embedded-logos-delivery = { workspace = true }
libchat = { workspace = true }
logos-account = { workspace = true, features = ["dev"] }
logos-generic-chat = { workspace = true }

View File

@ -0,0 +1,12 @@
mod logos;
pub use logos::{LogosChatClient, LogosConfig, REGISTRY_ENDPOINT, open, open_with_transport};
// Facade re-exports so callers need no direct dependency on the transport
// crate.
pub use embedded_logos_delivery::{
DEFAULT_NETWORK_PRESET, DEFAULT_TCP_PORT, EmbeddedLogosDelivery, P2pConfig,
};
// Re-export the transport-generic client surface so callers depend on this
// crate alone.
pub use logos_generic_chat::*;

View File

@ -0,0 +1,117 @@
//! The opinionated Logos client.
//!
//! [`open`] commits to the Logos service stack so independently built clients
//! share the same production services instead of each re-deriving them: a
//! delegate identity, the HTTP keypackage + account registry, and encrypted
//! on-disk storage. The stack is generic over the transport — any
//! [`Transport`] can be injected via [`open_with_transport`] — and the
//! concrete [`LogosChatClient`] commits to the embedded logos-delivery node,
//! whose native `liblogosdelivery` link keeps this crate outside the
//! workspace's default members.
//!
//! [`LogosChatClient`] points at `ChatClient`, which lives in
//! `logos-generic-chat`, so Rust's inherent-impl rule keeps the open
//! constructors off the alias; they are crate-level functions ([`open`],
//! [`open_with_transport`]) taking the all-inclusive [`LogosConfig`] instead.
use components::HttpRegistry;
use crossbeam_channel::Receiver;
use embedded_logos_delivery::{EmbeddedLogosDelivery, P2pConfig};
use libchat::{ChatStorage, StorageConfig};
use logos_account::TestLogosAccount;
use logos_generic_chat::{
ChatClient, ChatClientBuilder, ClientError, DelegateSigner, Event, Transport,
};
/// The endpoint for the account and keypackage registration service.
pub const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co";
/// Configuration for opening a Logos client.
///
/// `db_path` (a per-client location) and `db_key` (a secret) are required and
/// never baked into the library. Everything else defaults: the registry
/// endpoint to the baked-in Logos value and the embedded node's p2p settings
/// to [`P2pConfig::default`]; override them with
/// [`set_registry_url`](Self::set_registry_url) and
/// [`set_p2p_config`](Self::set_p2p_config).
pub struct LogosConfig {
db_path: String,
db_key: String,
registry_url: String,
p2p_config: P2pConfig,
}
impl LogosConfig {
/// Config for the required per-client `db_path` and `db_key`. The registry
/// endpoint defaults to the baked-in Logos value; override it with
/// [`set_registry_url`](Self::set_registry_url).
pub fn new(db_path: impl Into<String>, db_key: impl Into<String>) -> Self {
Self {
db_path: db_path.into(),
db_key: db_key.into(),
registry_url: REGISTRY_ENDPOINT.to_string(),
p2p_config: P2pConfig::default(),
}
}
/// Override the registry endpoint (account + keypackage store; defaults to
/// the baked-in [`REGISTRY_ENDPOINT`]).
pub fn set_registry_url(&mut self, registry_url: impl Into<String>) {
self.registry_url = registry_url.into();
}
/// Override the embedded node's p2p settings (defaults to
/// [`P2pConfig::default`]). Only [`open`] starts an embedded node, so
/// [`open_with_transport`] ignores this.
pub fn set_p2p_config(&mut self, p2p_config: P2pConfig) {
self.p2p_config = p2p_config;
}
}
/// Open a client on the Logos stack per `config`, starting an embedded
/// logos-delivery node per its p2p settings as the transport. A convenience
/// over [`open_with_transport`] that commits to the [`LogosChatClient`]
/// transport.
pub fn open(config: LogosConfig) -> Result<(LogosChatClient, Receiver<Event>), ClientError> {
let transport = EmbeddedLogosDelivery::start(config.p2p_config.clone())
.map_err(|e| ClientError::Transport(e.to_string()))?;
open_with_transport(config, transport)
}
/// Open a client on the Logos stack per `config` with the injected transport,
/// persisting to the encrypted database.
#[allow(clippy::type_complexity)]
pub fn open_with_transport<T: Transport>(
config: LogosConfig,
transport: T,
) -> Result<(ChatClient<T, HttpRegistry, ChatStorage>, Receiver<Event>), ClientError> {
// A fresh account endorsing a fresh delegate each open: the account
// key is dropped after publishing the bundle, so devices cannot be
// added later. A caller-supplied, custody-holding account replaces
// this once the platform provides one.
let account = TestLogosAccount::new();
let delegate = DelegateSigner::random();
let mut registry = HttpRegistry::new(config.registry_url);
account
.add_delegate_signer(&mut registry, delegate.public_key())
.map_err(|e| ClientError::BundlePublish(e.to_string()))?;
ChatClientBuilder::new(account.address())
.ident(delegate)
.transport(transport)
.registration(registry)
.storage_config(StorageConfig::Encrypted {
path: config.db_path,
key: config.db_key,
})
.build()
}
/// The Logos client: a [`ChatClient`] wired to the Logos service stack — a
/// [`DelegateSigner`] identity acting for a fresh dev account, the HTTP
/// keypackage + account registry ([`HttpRegistry`], which is both the
/// keypackage store and the account → device directory), and encrypted
/// [`ChatStorage`] — running an embedded logos-delivery node as its
/// transport. Open one with [`open`], or swap the transport via
/// [`open_with_transport`].
pub type LogosChatClient = ChatClient<EmbeddedLogosDelivery, HttpRegistry, ChatStorage>;

View File

@ -2,10 +2,6 @@
name = "components" name = "components"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
links = "logosdelivery"
[features]
embedded_p2p_delivery = []
[dependencies] [dependencies]
# Workspace dependencies (sorted) # Workspace dependencies (sorted)

View File

@ -1,9 +1,3 @@
mod local_broadcaster; mod local_broadcaster;
pub use local_broadcaster::LocalBroadcaster; pub use local_broadcaster::LocalBroadcaster;
#[cfg(feature = "embedded_p2p_delivery")]
pub mod embedded_p2p_delivery;
#[cfg(feature = "embedded_p2p_delivery")]
pub use embedded_p2p_delivery::{EmbeddedP2pDeliveryService, P2pConfig};

View File

@ -0,0 +1,18 @@
[package]
name = "embedded-logos-delivery"
version = "0.1.0"
edition = "2024"
links = "logosdelivery"
[dependencies]
# Workspace dependencies (sorted)
crossbeam-channel = { workspace = true }
libchat = { workspace = true }
logos-generic-chat = { workspace = true }
# External dependencies (sorted)
base64 = "0.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2"
tracing = "0.1"

View File

@ -5,17 +5,12 @@ use std::process::Command;
fn main() { fn main() {
println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR"); println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR");
if std::env::var_os("CARGO_FEATURE_EMBEDDED_P2P_DELIVERY").is_none() {
return;
}
let Some(lib_dir) = locate_lib_dir() else { let Some(lib_dir) = locate_lib_dir() else {
println!( println!(
"cargo:warning=embedded_p2p_delivery feature is enabled but \ "cargo:warning=liblogosdelivery could not be located; `cargo check`/\
liblogosdelivery could not be located; `cargo check`/`clippy` will \ `clippy` will pass, but building or testing will fail at link. Enter \
pass, but building or testing will fail at link. Enter the dev shell \ the dev shell with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to \
with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to the directory \ the directory containing the library."
containing the library."
); );
return; return;
}; };

View File

@ -1,8 +1,12 @@
//! logos-delivery backed [`client::DeliveryService`] implementation. //! The embedded logos-delivery transport service.
//! //!
//! `LogosDeliveryService` wraps an embedded logos-delivery node running on a //! [`EmbeddedLogosDelivery`] implements [`DeliveryService`] by wrapping an
//! dedicated `std::thread`. All interaction is via synchronous `std::sync::mpsc` //! embedded logos-delivery node running on a dedicated `std::thread`. All
//! channels. //! interaction is via synchronous `std::sync::mpsc` channels.
//!
//! This crate links the native `liblogosdelivery` library, so it lives outside
//! the workspace's default members; depend on it (e.g. via the `logos-chat`
//! crate) only when shipping the embedded node.
//! //!
//! ## Content topic mapping //! ## Content topic mapping
//! //!
@ -51,6 +55,12 @@ type SubscriberList = Arc<Mutex<Vec<Sender<Vec<u8>>>>>;
// ── P2pConfig ─────────────────────────────────────────────────────────────────── // ── P2pConfig ───────────────────────────────────────────────────────────────────
/// The logos-delivery network preset joined by default.
pub const DEFAULT_NETWORK_PRESET: &str = "logos.dev";
/// Default TCP port for the embedded logos-delivery node.
pub const DEFAULT_TCP_PORT: u16 = 60000;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct P2pConfig { pub struct P2pConfig {
pub preset: String, pub preset: String,
@ -61,8 +71,8 @@ pub struct P2pConfig {
impl Default for P2pConfig { impl Default for P2pConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
preset: "logos.dev".into(), preset: DEFAULT_NETWORK_PRESET.into(),
tcp_port: 60000, tcp_port: DEFAULT_TCP_PORT,
log_level: "ERROR".into(), log_level: "ERROR".into(),
} }
} }
@ -115,21 +125,21 @@ impl WakuPayload {
} }
} }
// ── EmbeddedP2pDeliveryService ────────────────────────────────────────────────── // ── EmbeddedLogosDelivery ──────────────────────────────────────────────────
/// logos-delivery backed delivery service. Cheap to clone — all clones share /// logos-delivery backed delivery service. Cheap to clone — all clones share
/// the same background node. /// the same background node.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct EmbeddedP2pDeliveryService { pub struct EmbeddedLogosDelivery {
outbound: mpsc::SyncSender<OutboundCmd>, outbound: mpsc::SyncSender<OutboundCmd>,
#[allow(dead_code)] #[allow(dead_code)]
subscribers: SubscriberList, subscribers: SubscriberList,
inbound_rx: Option<Receiver<Vec<u8>>>, inbound_rx: Option<Receiver<Vec<u8>>>,
} }
impl EmbeddedP2pDeliveryService { impl EmbeddedLogosDelivery {
/// Start the embedded logos-delivery node. The client drains inbound /// Start the embedded logos-delivery node. The client drains inbound
/// payloads via [`Transport::inbound`]. /// payloads via [`Self::inbound_queue`].
pub fn start(cfg: P2pConfig) -> Result<Self, DeliveryError> { pub fn start(cfg: P2pConfig) -> Result<Self, DeliveryError> {
let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCmd>(256); let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCmd>(256);
let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new())); let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new()));
@ -284,7 +294,7 @@ impl EmbeddedP2pDeliveryService {
} }
} }
impl DeliveryService for EmbeddedP2pDeliveryService { impl DeliveryService for EmbeddedLogosDelivery {
type Error = DeliveryError; type Error = DeliveryError;
fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> { fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> {
@ -312,3 +322,13 @@ impl DeliveryService for EmbeddedP2pDeliveryService {
Ok(()) Ok(())
} }
} }
// Teaching the service the inbound half makes it a full client transport, so
// callers need no wrapper newtype. The impl lives here (the crate owning the
// type) because the orphan rule bars it from the `logos-chat` crate, which
// owns neither the trait nor the type.
impl logos_generic_chat::Transport for EmbeddedLogosDelivery {
fn inbound(&mut self) -> Receiver<Vec<u8>> {
self.inbound_queue()
}
}