mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-04 06:03:12 +00:00
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:
parent
b19d0c6e67
commit
939a63e8bc
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -20,12 +20,12 @@ jobs:
|
||||
- run: rustup update stable && rustup default stable
|
||||
# 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
|
||||
# chat-cli pulls in components' embedded_p2p_delivery feature, whose
|
||||
# build.rs links liblogosdelivery (built via Nix or LOGOS_DELIVERY_LIB_DIR).
|
||||
# The smoketest job builds and exercises it under Nix; here we keep the
|
||||
# toolchain-only job fast by skipping it.
|
||||
- run: cargo build --verbose --workspace --exclude chat-cli
|
||||
- run: cargo test --verbose --workspace --exclude chat-cli
|
||||
# chat-cli and the embedded-logos-delivery crate link liblogosdelivery
|
||||
# (built via Nix or LOGOS_DELIVERY_LIB_DIR). The smoketest job builds and
|
||||
# exercises them under Nix; here we keep the toolchain-only job fast by
|
||||
# skipping them.
|
||||
- run: cargo build --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery
|
||||
- run: cargo test --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
|
||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -2072,6 +2072,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-logos-delivery"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"crossbeam-channel",
|
||||
"libchat",
|
||||
"logos-generic-chat",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-ordinalize"
|
||||
version = "4.3.2"
|
||||
@ -3599,6 +3613,18 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "logos-chat"
|
||||
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 = [
|
||||
"chat-sqlite",
|
||||
"components",
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@ -12,8 +12,10 @@ members = [
|
||||
"core/shared-traits",
|
||||
"core/sqlite",
|
||||
"core/storage",
|
||||
"crates/client",
|
||||
"crates/logos-chat",
|
||||
"crates/generic-chat",
|
||||
"extensions/components",
|
||||
"extensions/embedded-logos-delivery",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@ -25,7 +27,7 @@ default-members = [
|
||||
"core/shared-traits",
|
||||
"core/sqlite",
|
||||
"core/storage",
|
||||
"crates/client",
|
||||
"crates/generic-chat",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -34,8 +36,10 @@ logos-account = { path = "core/account" }
|
||||
chat-sqlite = { path = "core/sqlite" }
|
||||
components = { path = "extensions/components" }
|
||||
crypto = { path = "core/crypto" }
|
||||
embedded-logos-delivery = { path = "extensions/embedded-logos-delivery" }
|
||||
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" }
|
||||
storage = { path = "core/storage" }
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ path = "src/main.rs"
|
||||
# Workspace dependencies (sorted)
|
||||
crossbeam-channel = { workspace = true }
|
||||
logos-account = { workspace = true, features = ["dev"] }
|
||||
logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] }
|
||||
logos-chat = { workspace = true }
|
||||
|
||||
# External dependencies (sorted)
|
||||
anyhow = "1.0"
|
||||
|
||||
@ -8,11 +8,9 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use crossbeam_channel::Receiver;
|
||||
use logos_account::TestLogosAccount;
|
||||
use logos_chat::{
|
||||
AccountDirectory, ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event,
|
||||
HttpRegistry, LogosChatClient, LogosConfig, NETWORK_PRESET, REGISTRY_ENDPOINT,
|
||||
RegistrationService, StorageConfig, Transport,
|
||||
AccountDirectory, ChatClient, ChatStore, Event, LogosConfig, P2pConfig, RegistrationService,
|
||||
Transport,
|
||||
};
|
||||
|
||||
use app::ChatApp;
|
||||
@ -78,24 +76,27 @@ fn main() -> Result<()> {
|
||||
let db_str = db_path(&cli)?;
|
||||
|
||||
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 => {
|
||||
let preset = cli.preset.as_deref().unwrap_or(NETWORK_PRESET);
|
||||
println!("Starting logos-delivery node (preset={preset})...");
|
||||
let mut p2p_config = P2pConfig::default();
|
||||
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.");
|
||||
|
||||
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() {
|
||||
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:?}"))
|
||||
.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
|
||||
// stack (delegate identity, HTTP registry, encrypted storage) but swaps
|
||||
// the transport, so it builds a client directly instead of going through
|
||||
// `LogosChatClient`.
|
||||
// the transport in via `open_with_transport`.
|
||||
TransportKind::File => {
|
||||
let transport_dir = cli.data.join("transport");
|
||||
let transport = transport::file::FileTransport::new(&transport_dir)
|
||||
.context("failed to create file transport")?;
|
||||
|
||||
let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT);
|
||||
// A fresh dev account endorsing a fresh delegate each launch,
|
||||
// mirroring `LogosChatClient::open`.
|
||||
let account = TestLogosAccount::new();
|
||||
let delegate = DelegateSigner::random();
|
||||
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()
|
||||
let mut config = LogosConfig::new(db_str, "chat-cli");
|
||||
if let Some(registry_url) = cli.registry_url.as_deref() {
|
||||
config.set_registry_url(registry_url);
|
||||
}
|
||||
let (client, events) = logos_chat::open_with_transport(config, transport)
|
||||
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
||||
.context("failed to open chat client")?;
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,11 @@
|
||||
[package]
|
||||
name = "logos-chat"
|
||||
name = "logos-generic-chat"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
embedded-p2p-delivery = ["components/embedded_p2p_delivery"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace dependencies (sorted)
|
||||
chat-sqlite = { workspace = true }
|
||||
@ -1,9 +1,9 @@
|
||||
# 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
|
||||
`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.
|
||||
|
||||
## Running
|
||||
@ -1,6 +1,6 @@
|
||||
use components::EphemeralRegistry;
|
||||
use logos_account::TestLogosAccount;
|
||||
use logos_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
|
||||
use logos_generic_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
@ -1,22 +1,16 @@
|
||||
mod builder;
|
||||
mod client;
|
||||
mod config;
|
||||
mod delegate;
|
||||
mod delivery_in_process;
|
||||
mod errors;
|
||||
mod event;
|
||||
#[cfg(feature = "embedded-p2p-delivery")]
|
||||
mod logos;
|
||||
|
||||
pub use builder::{ChatClientBuilder, Unset};
|
||||
pub use client::{ChatClient, Transport};
|
||||
pub use config::{DEFAULT_TCP_PORT, NETWORK_PRESET, REGISTRY_ENDPOINT};
|
||||
pub use delegate::DelegateSigner;
|
||||
pub use delivery_in_process::{InProcessDelivery, MessageBus};
|
||||
pub use errors::ClientError;
|
||||
pub use event::{Event, MessageSender};
|
||||
#[cfg(feature = "embedded-p2p-delivery")]
|
||||
pub use logos::{LogosChatClient, LogosConfig};
|
||||
|
||||
// Re-export types callers need to interact with ChatClient.
|
||||
pub use libchat::{
|
||||
@ -4,7 +4,7 @@ use components::EphemeralRegistry;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use crypto::Ed25519VerifyingKey;
|
||||
use logos_account::TestLogosAccount;
|
||||
use logos_chat::{
|
||||
use logos_generic_chat::{
|
||||
AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event,
|
||||
InProcessDelivery, MessageBus, Transport,
|
||||
};
|
||||
@ -30,7 +30,7 @@ fn create_test_client(
|
||||
ChatClient<InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>,
|
||||
Receiver<Event>,
|
||||
),
|
||||
logos_chat::ClientError,
|
||||
logos_generic_chat::ClientError,
|
||||
> {
|
||||
let account = TestLogosAccount::new();
|
||||
let delegate = DelegateSigner::random();
|
||||
@ -375,10 +375,16 @@ fn unpublished_account_address_is_an_error() {
|
||||
let err = saro
|
||||
.create_direct_conversation(&unpublished.address())
|
||||
.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
|
||||
.create_direct_conversation("not-an-account-address")
|
||||
.expect_err("not an account key");
|
||||
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
logos_generic_chat::ClientError::AccountResolution(_)
|
||||
));
|
||||
}
|
||||
16
crates/logos-chat/Cargo.toml
Normal file
16
crates/logos-chat/Cargo.toml
Normal 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 }
|
||||
12
crates/logos-chat/src/lib.rs
Normal file
12
crates/logos-chat/src/lib.rs
Normal 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::*;
|
||||
117
crates/logos-chat/src/logos.rs
Normal file
117
crates/logos-chat/src/logos.rs
Normal 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>;
|
||||
@ -2,10 +2,6 @@
|
||||
name = "components"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
links = "logosdelivery"
|
||||
|
||||
[features]
|
||||
embedded_p2p_delivery = []
|
||||
|
||||
[dependencies]
|
||||
# Workspace dependencies (sorted)
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
mod local_broadcaster;
|
||||
|
||||
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};
|
||||
|
||||
18
extensions/embedded-logos-delivery/Cargo.toml
Normal file
18
extensions/embedded-logos-delivery/Cargo.toml
Normal 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"
|
||||
@ -5,17 +5,12 @@ use std::process::Command;
|
||||
fn main() {
|
||||
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 {
|
||||
println!(
|
||||
"cargo:warning=embedded_p2p_delivery feature is enabled but \
|
||||
liblogosdelivery could not be located; `cargo check`/`clippy` will \
|
||||
pass, but building or testing will fail at link. Enter the dev shell \
|
||||
with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to the directory \
|
||||
containing the library."
|
||||
"cargo:warning=liblogosdelivery could not be located; `cargo check`/\
|
||||
`clippy` will pass, but building or testing will fail at link. Enter \
|
||||
the dev shell with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to \
|
||||
the directory containing the library."
|
||||
);
|
||||
return;
|
||||
};
|
||||
@ -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
|
||||
//! dedicated `std::thread`. All interaction is via synchronous `std::sync::mpsc`
|
||||
//! channels.
|
||||
//! [`EmbeddedLogosDelivery`] implements [`DeliveryService`] by wrapping an
|
||||
//! embedded logos-delivery node running on a dedicated `std::thread`. All
|
||||
//! 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
|
||||
//!
|
||||
@ -51,6 +55,12 @@ type SubscriberList = Arc<Mutex<Vec<Sender<Vec<u8>>>>>;
|
||||
|
||||
// ── 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)]
|
||||
pub struct P2pConfig {
|
||||
pub preset: String,
|
||||
@ -61,8 +71,8 @@ pub struct P2pConfig {
|
||||
impl Default for P2pConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
preset: "logos.dev".into(),
|
||||
tcp_port: 60000,
|
||||
preset: DEFAULT_NETWORK_PRESET.into(),
|
||||
tcp_port: DEFAULT_TCP_PORT,
|
||||
log_level: "ERROR".into(),
|
||||
}
|
||||
}
|
||||
@ -115,21 +125,21 @@ impl WakuPayload {
|
||||
}
|
||||
}
|
||||
|
||||
// ── EmbeddedP2pDeliveryService ──────────────────────────────────────────────────
|
||||
// ── EmbeddedLogosDelivery ──────────────────────────────────────────────────
|
||||
|
||||
/// logos-delivery backed delivery service. Cheap to clone — all clones share
|
||||
/// the same background node.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EmbeddedP2pDeliveryService {
|
||||
pub struct EmbeddedLogosDelivery {
|
||||
outbound: mpsc::SyncSender<OutboundCmd>,
|
||||
#[allow(dead_code)]
|
||||
subscribers: SubscriberList,
|
||||
inbound_rx: Option<Receiver<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl EmbeddedP2pDeliveryService {
|
||||
impl EmbeddedLogosDelivery {
|
||||
/// 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> {
|
||||
let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCmd>(256);
|
||||
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;
|
||||
|
||||
fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> {
|
||||
@ -312,3 +322,13 @@ impl DeliveryService for EmbeddedP2pDeliveryService {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user