mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-19 23:52:23 -05:00
Add the crate skeleton
This commit is contained in:
parent
65a3ebdbe2
commit
088331a933
17 changed files with 2181 additions and 2 deletions
196
Cargo.lock
generated
196
Cargo.lock
generated
|
|
@ -750,6 +750,29 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "automerge"
|
||||||
|
version = "0.5.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f0dae93622d3c6850d196503480004576249e0e391bddb3f54600974d92a790"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"flate2",
|
||||||
|
"fxhash",
|
||||||
|
"hex",
|
||||||
|
"im",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"leb128",
|
||||||
|
"serde",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"smol_str",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
|
|
@ -1178,6 +1201,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitmaps"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "bitvec"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -2889,6 +2921,34 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eigensync"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"automerge",
|
||||||
|
"chrono",
|
||||||
|
"clap 4.5.41",
|
||||||
|
"futures",
|
||||||
|
"libp2p",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-prometheus",
|
||||||
|
"prometheus",
|
||||||
|
"rusqlite",
|
||||||
|
"rusqlite_migration",
|
||||||
|
"serde",
|
||||||
|
"serde_cbor",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time 0.3.41",
|
||||||
|
"tokio",
|
||||||
|
"toml 0.8.23",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
|
|
@ -4582,6 +4642,20 @@ dependencies = [
|
||||||
"xmltree",
|
"xmltree",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "im"
|
||||||
|
version = "15.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9"
|
||||||
|
dependencies = [
|
||||||
|
"bitmaps",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"rand_xoshiro",
|
||||||
|
"sized-chunks",
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.6"
|
version = "0.25.6"
|
||||||
|
|
@ -4977,6 +5051,12 @@ dependencies = [
|
||||||
"spin 0.9.8",
|
"spin 0.9.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libappindicator"
|
name = "libappindicator"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -6759,6 +6839,51 @@ version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project-lite",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-prometheus"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e1a24eafe47b693cb938f8505f240dc26c71db60df9aca376b4f857e9653ec7"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"prometheus",
|
||||||
|
"protobuf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry_sdk"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-util",
|
||||||
|
"glob",
|
||||||
|
"lazy_static",
|
||||||
|
"once_cell",
|
||||||
|
"opentelemetry",
|
||||||
|
"ordered-float 4.6.0",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -6774,6 +6899,15 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -7463,6 +7597,21 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prometheus"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"fnv",
|
||||||
|
"lazy_static",
|
||||||
|
"memchr",
|
||||||
|
"parking_lot 0.12.4",
|
||||||
|
"protobuf",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometheus-client"
|
name = "prometheus-client"
|
||||||
version = "0.22.3"
|
version = "0.22.3"
|
||||||
|
|
@ -7506,6 +7655,12 @@ dependencies = [
|
||||||
"unarray",
|
"unarray",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "2.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ptr_meta"
|
name = "ptr_meta"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
@ -7815,6 +7970,15 @@ dependencies = [
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xoshiro"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
|
@ -8149,6 +8313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
|
"chrono",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink 0.9.1",
|
"hashlink 0.9.1",
|
||||||
|
|
@ -8157,6 +8322,16 @@ dependencies = [
|
||||||
"time 0.3.41",
|
"time 0.3.41",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite_migration"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923b42e802f7dc20a0a6b5e097ba7c83fe4289da07e49156fecf6af08aa9cd1c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"rusqlite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.37.2"
|
version = "1.37.2"
|
||||||
|
|
@ -8755,7 +8930,7 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ordered-float",
|
"ordered-float 2.10.1",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -9174,6 +9349,16 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sized-chunks"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
|
||||||
|
dependencies = [
|
||||||
|
"bitmaps",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
|
|
@ -9228,6 +9413,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smol_str"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snow"
|
name = "snow"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [ "eigensync",
|
||||||
"electrum-pool",
|
"electrum-pool",
|
||||||
"monero-rpc",
|
"monero-rpc",
|
||||||
"monero-rpc-pool",
|
"monero-rpc-pool",
|
||||||
|
|
|
||||||
59
eigensync/Cargo.toml
Normal file
59
eigensync/Cargo.toml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
[package]
|
||||||
|
name = "eigensync"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "eigensync-server"
|
||||||
|
path = "src/bin/server.rs"
|
||||||
|
required-features = ["server"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["client"]
|
||||||
|
server = ["dep:clap", "dep:toml", "dep:tracing-subscriber"]
|
||||||
|
client = []
|
||||||
|
metrics = ["dep:opentelemetry", "dep:opentelemetry-prometheus", "dep:prometheus"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Core dependencies
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "time", "fs", "signal"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
||||||
|
# Automerge
|
||||||
|
automerge = "0.5"
|
||||||
|
|
||||||
|
# Networking
|
||||||
|
libp2p = { workspace = true, features = ["request-response", "tcp", "noise", "yamux", "identify", "ping", "mdns"] }
|
||||||
|
futures = { workspace = true }
|
||||||
|
|
||||||
|
# Database
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled", "chrono"] }
|
||||||
|
rusqlite_migration = "1.2"
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde_cbor = "0.11"
|
||||||
|
|
||||||
|
# Time handling
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
time = { version = "0.3", features = ["serde", "formatting"] }
|
||||||
|
|
||||||
|
# Server-only dependencies
|
||||||
|
clap = { version = "4.0", features = ["derive"], optional = true }
|
||||||
|
toml = { version = "0.8", optional = true }
|
||||||
|
tracing-subscriber = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
# Metrics (optional)
|
||||||
|
opentelemetry = { version = "0.23", optional = true }
|
||||||
|
opentelemetry-prometheus = { version = "0.16", optional = true }
|
||||||
|
prometheus = { version = "0.13", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
171
eigensync/src/bin/server.rs
Normal file
171
eigensync/src/bin/server.rs
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
//! Eigensync server binary
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use eigensync::{Server, ServerConfig};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "eigensync-server")]
|
||||||
|
#[command(about = "Eigensync distributed state synchronization server")]
|
||||||
|
#[command(version)]
|
||||||
|
struct Cli {
|
||||||
|
/// Configuration file path
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Database path
|
||||||
|
#[arg(long)]
|
||||||
|
database: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Listen address
|
||||||
|
#[arg(short, long, default_value = "0.0.0.0")]
|
||||||
|
listen_address: String,
|
||||||
|
|
||||||
|
/// Listen port
|
||||||
|
#[arg(short, long, default_value = "9944")]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
/// Maximum number of peers
|
||||||
|
#[arg(long, default_value = "100")]
|
||||||
|
max_peers: u32,
|
||||||
|
|
||||||
|
/// Enable debug logging
|
||||||
|
#[arg(short, long)]
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
|
/// Enable JSON logging
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Run the server
|
||||||
|
Run,
|
||||||
|
/// Generate default configuration
|
||||||
|
GenerateConfig {
|
||||||
|
/// Output file path
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
let filter = if cli.debug {
|
||||||
|
"debug,eigensync=trace"
|
||||||
|
} else {
|
||||||
|
"info,eigensync=debug"
|
||||||
|
};
|
||||||
|
|
||||||
|
if cli.json {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::EnvFilter::new(filter))
|
||||||
|
.with(tracing_subscriber::fmt::layer().json())
|
||||||
|
.init();
|
||||||
|
} else {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::EnvFilter::new(filter))
|
||||||
|
.with(tracing_subscriber::fmt::layer().pretty())
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Starting eigensync server v{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
// Handle subcommands
|
||||||
|
let command = cli.command.clone().unwrap_or(Commands::Run);
|
||||||
|
match command {
|
||||||
|
Commands::Run => {
|
||||||
|
run_server(cli).await?;
|
||||||
|
}
|
||||||
|
Commands::GenerateConfig { output } => {
|
||||||
|
generate_config(output)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_server(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Load or create configuration
|
||||||
|
let mut config = if let Some(config_path) = cli.config {
|
||||||
|
if config_path.exists() {
|
||||||
|
info!("Loading configuration from {:?}", config_path);
|
||||||
|
// TODO: Implement config file loading
|
||||||
|
ServerConfig::default()
|
||||||
|
} else {
|
||||||
|
warn!("Configuration file {:?} not found, using defaults", config_path);
|
||||||
|
ServerConfig::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("No configuration file specified, using defaults");
|
||||||
|
ServerConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override config with CLI arguments
|
||||||
|
if let Some(database_path) = cli.database {
|
||||||
|
config.database_path = database_path;
|
||||||
|
}
|
||||||
|
config.listen_address = cli.listen_address;
|
||||||
|
config.listen_port = cli.port;
|
||||||
|
config.max_peers = cli.max_peers;
|
||||||
|
|
||||||
|
info!("Server configuration:");
|
||||||
|
info!(" Database: {:?}", config.database_path);
|
||||||
|
info!(" Listen: {}:{}", config.listen_address, config.listen_port);
|
||||||
|
info!(" Max peers: {}", config.max_peers);
|
||||||
|
|
||||||
|
// Create and run server
|
||||||
|
let server = Server::new(config).await?;
|
||||||
|
|
||||||
|
// Set up signal handling for graceful shutdown
|
||||||
|
let shutdown_signal = async {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("Failed to install CTRL+C signal handler");
|
||||||
|
info!("Received shutdown signal");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run server with graceful shutdown
|
||||||
|
tokio::select! {
|
||||||
|
result = server.run() => {
|
||||||
|
match result {
|
||||||
|
Ok(_) => info!("Server completed successfully"),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Server error: {}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = shutdown_signal => {
|
||||||
|
info!("Shutting down server gracefully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_config(output: Option<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = ServerConfig::default();
|
||||||
|
let config_toml = toml::to_string_pretty(&config)?;
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Some(path) => {
|
||||||
|
std::fs::write(&path, config_toml)?;
|
||||||
|
info!("Generated configuration file: {:?}", path);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("{}", config_toml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
72
eigensync/src/client/behaviour.rs
Normal file
72
eigensync/src/client/behaviour.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
//! libp2p networking behaviour for eigensync client
|
||||||
|
|
||||||
|
use crate::protocol::{EigensyncRequest, EigensyncResponse};
|
||||||
|
use crate::types::{Result, PeerId};
|
||||||
|
|
||||||
|
/// libp2p behaviour for eigensync client (placeholder)
|
||||||
|
pub struct ClientBehaviour {
|
||||||
|
// TODO: Add actual behaviour components
|
||||||
|
// request_response: RequestResponse<EigensyncCodec>,
|
||||||
|
// identify: Identify,
|
||||||
|
// ping: Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientBehaviour {
|
||||||
|
/// Create a new client behaviour
|
||||||
|
pub fn new() -> Self {
|
||||||
|
tracing::debug!("Creating client behaviour");
|
||||||
|
|
||||||
|
// TODO: Initialize behaviour components
|
||||||
|
Self {
|
||||||
|
// request_response: RequestResponse::new(...),
|
||||||
|
// identify: Identify::new(...),
|
||||||
|
// ping: Ping::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a request to the server
|
||||||
|
pub async fn send_request(
|
||||||
|
&mut self,
|
||||||
|
server_peer_id: PeerId,
|
||||||
|
request: EigensyncRequest,
|
||||||
|
) -> Result<EigensyncResponse> {
|
||||||
|
tracing::debug!("Sending request to server {}: {:?}", server_peer_id, request);
|
||||||
|
|
||||||
|
// TODO: Implement request sending
|
||||||
|
match request {
|
||||||
|
EigensyncRequest::GetChanges(_params) => {
|
||||||
|
// TODO: Handle GetChanges request
|
||||||
|
todo!("GetChanges request not implemented")
|
||||||
|
},
|
||||||
|
EigensyncRequest::SubmitChanges(_params) => {
|
||||||
|
// TODO: Handle SubmitChanges request
|
||||||
|
todo!("SubmitChanges request not implemented")
|
||||||
|
},
|
||||||
|
EigensyncRequest::Ping(_params) => {
|
||||||
|
// TODO: Handle Ping request
|
||||||
|
todo!("Ping request not implemented")
|
||||||
|
},
|
||||||
|
EigensyncRequest::GetStatus(_params) => {
|
||||||
|
// TODO: Handle GetStatus request
|
||||||
|
todo!("GetStatus request not implemented")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ClientBehaviour {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_behaviour_creation() {
|
||||||
|
let _behaviour = ClientBehaviour::new();
|
||||||
|
// Behaviour creation should not panic
|
||||||
|
}
|
||||||
|
}
|
||||||
81
eigensync/src/client/database.rs
Normal file
81
eigensync/src/client/database.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
//! Client database layer for caching documents and metadata
|
||||||
|
|
||||||
|
use crate::types::{Result, ActorId};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Client database for caching Automerge documents and metadata
|
||||||
|
pub struct ClientDatabase {
|
||||||
|
connection: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientDatabase {
|
||||||
|
/// Open or create a client cache database
|
||||||
|
pub async fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
tracing::info!("Opening client cache database at {:?}", path.as_ref());
|
||||||
|
|
||||||
|
// TODO: Implement actual database opening and migration
|
||||||
|
let connection = Connection::open(path)?;
|
||||||
|
|
||||||
|
Ok(Self { connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store an Automerge document
|
||||||
|
pub async fn store_document(
|
||||||
|
&self,
|
||||||
|
document_id: &str,
|
||||||
|
document_data: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::debug!("Storing document {}", document_id);
|
||||||
|
|
||||||
|
// TODO: Implement document storage
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load an Automerge document
|
||||||
|
pub async fn load_document(&self, document_id: &str) -> Result<Option<Vec<u8>>> {
|
||||||
|
tracing::debug!("Loading document {}", document_id);
|
||||||
|
|
||||||
|
// TODO: Implement document loading
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store document metadata
|
||||||
|
pub async fn store_metadata(
|
||||||
|
&self,
|
||||||
|
document_id: &str,
|
||||||
|
last_sync: chrono::DateTime<chrono::Utc>,
|
||||||
|
heads: &[automerge::ChangeHash],
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::debug!("Storing metadata for document {}", document_id);
|
||||||
|
|
||||||
|
// TODO: Implement metadata storage
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load document metadata
|
||||||
|
pub async fn load_metadata(
|
||||||
|
&self,
|
||||||
|
document_id: &str,
|
||||||
|
) -> Result<Option<(chrono::DateTime<chrono::Utc>, Vec<automerge::ChangeHash>)>> {
|
||||||
|
tracing::debug!("Loading metadata for document {}", document_id);
|
||||||
|
|
||||||
|
// TODO: Implement metadata loading
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_client_database_creation() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let db_path = temp_dir.path().join("test_cache.db");
|
||||||
|
|
||||||
|
let db = ClientDatabase::open(db_path).await;
|
||||||
|
assert!(db.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
156
eigensync/src/client/document.rs
Normal file
156
eigensync/src/client/document.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
//! Automerge document management for client
|
||||||
|
|
||||||
|
use crate::types::{Result, ActorId};
|
||||||
|
use automerge::{AutoCommit, ChangeHash};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Manager for Automerge documents
|
||||||
|
pub struct DocumentManager {
|
||||||
|
documents: HashMap<String, AutoCommit>,
|
||||||
|
actor_id: ActorId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentManager {
|
||||||
|
/// Create a new document manager
|
||||||
|
pub fn new(actor_id: ActorId) -> Self {
|
||||||
|
tracing::debug!("Creating document manager for actor {}", actor_id);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
documents: HashMap::new(),
|
||||||
|
actor_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or create a document for a swap
|
||||||
|
pub fn get_or_create_document(&mut self, document_id: &str) -> Result<&mut AutoCommit> {
|
||||||
|
tracing::debug!("Getting or creating document {}", document_id);
|
||||||
|
|
||||||
|
if !self.documents.contains_key(document_id) {
|
||||||
|
// TODO: Try to load from cache first
|
||||||
|
let mut doc = AutoCommit::new();
|
||||||
|
doc.set_actor(self.actor_id.0.clone());
|
||||||
|
self.documents.insert(document_id.to_string(), doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.documents.get_mut(document_id).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a swap state to a document
|
||||||
|
pub fn append_swap_state(
|
||||||
|
&mut self,
|
||||||
|
document_id: &str,
|
||||||
|
state_json: Value,
|
||||||
|
timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
) -> Result<Vec<automerge::Change>> {
|
||||||
|
tracing::debug!("Appending swap state to document {}", document_id);
|
||||||
|
|
||||||
|
let doc = self.get_or_create_document(document_id)?;
|
||||||
|
|
||||||
|
// TODO: Implement state appending to Automerge document
|
||||||
|
// Structure: { "states": [ { "timestamp": "...", "state": {...} } ] }
|
||||||
|
|
||||||
|
// For now, return empty changes
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest state from a document
|
||||||
|
pub fn get_latest_state(&self, document_id: &str) -> Result<Option<Value>> {
|
||||||
|
tracing::debug!("Getting latest state from document {}", document_id);
|
||||||
|
|
||||||
|
if let Some(doc) = self.documents.get(document_id) {
|
||||||
|
// TODO: Implement state retrieval from Automerge document
|
||||||
|
// Find entry with latest timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply changes to a document
|
||||||
|
pub fn apply_changes(
|
||||||
|
&mut self,
|
||||||
|
document_id: &str,
|
||||||
|
changes: &[automerge::Change],
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::debug!("Applying {} changes to document {}", changes.len(), document_id);
|
||||||
|
|
||||||
|
let doc = self.get_or_create_document(document_id)?;
|
||||||
|
|
||||||
|
for change in changes {
|
||||||
|
doc.load_incremental(change.raw_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get document heads
|
||||||
|
pub fn get_heads(&mut self, document_id: &str) -> Vec<ChangeHash> {
|
||||||
|
if let Some(doc) = self.documents.get_mut(document_id) {
|
||||||
|
doc.get_heads()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate changes since given heads
|
||||||
|
pub fn changes_since(
|
||||||
|
&mut self,
|
||||||
|
document_id: &str,
|
||||||
|
heads: &[ChangeHash],
|
||||||
|
) -> Result<Vec<automerge::Change>> {
|
||||||
|
if let Some(doc) = self.documents.get_mut(document_id) {
|
||||||
|
let changes = doc.get_changes(heads);
|
||||||
|
Ok(changes.into_iter().cloned().collect())
|
||||||
|
} else {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a document for storage
|
||||||
|
pub fn serialize_document(&mut self, document_id: &str) -> Result<Option<Vec<u8>>> {
|
||||||
|
if let Some(doc) = self.documents.get_mut(document_id) {
|
||||||
|
let bytes = doc.save();
|
||||||
|
Ok(Some(bytes))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a document from serialized data
|
||||||
|
pub fn load_document(&mut self, document_id: &str, data: &[u8]) -> Result<()> {
|
||||||
|
tracing::debug!("Loading document {} from {} bytes", document_id, data.len());
|
||||||
|
|
||||||
|
let mut doc = AutoCommit::load(data)?;
|
||||||
|
doc.set_actor(self.actor_id.0.clone());
|
||||||
|
self.documents.insert(document_id.to_string(), doc);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_document_manager_creation() {
|
||||||
|
let actor_id = ActorId(automerge::ActorId::random());
|
||||||
|
let _manager = DocumentManager::new(actor_id);
|
||||||
|
// Manager creation should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_or_create_document() {
|
||||||
|
let actor_id = ActorId(automerge::ActorId::random());
|
||||||
|
let mut manager = DocumentManager::new(actor_id);
|
||||||
|
|
||||||
|
// First call should create the document
|
||||||
|
let _doc1 = manager.get_or_create_document("test-doc").unwrap();
|
||||||
|
|
||||||
|
// Second call should return the existing document (test that no panic occurs)
|
||||||
|
let _doc2 = manager.get_or_create_document("test-doc").unwrap();
|
||||||
|
|
||||||
|
// Document should exist in the manager
|
||||||
|
assert!(manager.documents.contains_key("test-doc"));
|
||||||
|
}
|
||||||
|
}
|
||||||
124
eigensync/src/client/mod.rs
Normal file
124
eigensync/src/client/mod.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
//! Client-side components for eigensync
|
||||||
|
|
||||||
|
use crate::types::{Result, PeerId, ActorId};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub mod database;
|
||||||
|
pub mod behaviour;
|
||||||
|
pub mod sync_loop;
|
||||||
|
pub mod document;
|
||||||
|
|
||||||
|
/// Configuration for the eigensync client
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ClientConfig {
|
||||||
|
/// Path to client cache database
|
||||||
|
pub cache_database_path: PathBuf,
|
||||||
|
/// Server address to connect to
|
||||||
|
pub server_address: String,
|
||||||
|
/// Server port to connect to
|
||||||
|
pub server_port: u16,
|
||||||
|
/// Sync interval
|
||||||
|
pub sync_interval: Duration,
|
||||||
|
/// Connection timeout
|
||||||
|
pub connection_timeout: Duration,
|
||||||
|
/// Actor ID for this client
|
||||||
|
pub actor_id: ActorId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ClientConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
cache_database_path: PathBuf::from("eigensync_cache.sqlite"),
|
||||||
|
server_address: "127.0.0.1".to_string(),
|
||||||
|
server_port: 9944,
|
||||||
|
sync_interval: Duration::from_secs(30),
|
||||||
|
connection_timeout: Duration::from_secs(10),
|
||||||
|
actor_id: ActorId(automerge::ActorId::random()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main client struct (placeholder implementation)
|
||||||
|
pub struct Client {
|
||||||
|
config: ClientConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Create a new client with the given configuration
|
||||||
|
pub async fn new(config: ClientConfig) -> Result<Self> {
|
||||||
|
tracing::info!("Creating eigensync client with config: {:?}", config);
|
||||||
|
|
||||||
|
// TODO: Initialize database, networking, etc.
|
||||||
|
|
||||||
|
Ok(Self { config })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the client sync loop
|
||||||
|
pub async fn start_sync(&mut self) -> Result<()> {
|
||||||
|
tracing::info!("Starting eigensync client sync");
|
||||||
|
|
||||||
|
// TODO: Implement client sync loop
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a swap state to the local document
|
||||||
|
pub async fn append_swap_state(
|
||||||
|
&mut self,
|
||||||
|
swap_id: uuid::Uuid,
|
||||||
|
state_json: serde_json::Value,
|
||||||
|
timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::debug!("Appending swap state for {}: {:?}", swap_id, state_json);
|
||||||
|
|
||||||
|
// TODO: Implement state appending
|
||||||
|
// 1. Load Automerge document for swap_id
|
||||||
|
// 2. Add new state entry with timestamp
|
||||||
|
// 3. Generate patch
|
||||||
|
// 4. Store locally and mark for sync
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest state for a swap
|
||||||
|
pub async fn get_latest_swap_state(
|
||||||
|
&self,
|
||||||
|
swap_id: uuid::Uuid,
|
||||||
|
) -> Result<Option<serde_json::Value>> {
|
||||||
|
tracing::debug!("Getting latest swap state for {}", swap_id);
|
||||||
|
|
||||||
|
// TODO: Implement state retrieval
|
||||||
|
// 1. Load Automerge document for swap_id
|
||||||
|
// 2. Find entry with latest timestamp
|
||||||
|
// 3. Return state JSON
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get client configuration
|
||||||
|
pub fn config(&self) -> &ClientConfig {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_config_default() {
|
||||||
|
let config = ClientConfig::default();
|
||||||
|
assert_eq!(config.server_address, "127.0.0.1");
|
||||||
|
assert_eq!(config.server_port, 9944);
|
||||||
|
assert_eq!(config.sync_interval, Duration::from_secs(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_client_creation() {
|
||||||
|
let config = ClientConfig::default();
|
||||||
|
let client = Client::new(config).await;
|
||||||
|
assert!(client.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
104
eigensync/src/client/sync_loop.rs
Normal file
104
eigensync/src/client/sync_loop.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
//! Client sync loop for periodic synchronization with server
|
||||||
|
|
||||||
|
use crate::client::behaviour::ClientBehaviour;
|
||||||
|
use crate::client::database::ClientDatabase;
|
||||||
|
use crate::types::{Result, PeerId};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Client sync loop for periodic synchronization
|
||||||
|
pub struct ClientSyncLoop {
|
||||||
|
behaviour: ClientBehaviour,
|
||||||
|
database: ClientDatabase,
|
||||||
|
server_peer_id: PeerId,
|
||||||
|
sync_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientSyncLoop {
|
||||||
|
/// Create a new client sync loop
|
||||||
|
pub fn new(
|
||||||
|
behaviour: ClientBehaviour,
|
||||||
|
database: ClientDatabase,
|
||||||
|
server_peer_id: PeerId,
|
||||||
|
sync_interval: Duration,
|
||||||
|
) -> Self {
|
||||||
|
tracing::debug!("Creating client sync loop with interval {:?}", sync_interval);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
behaviour,
|
||||||
|
database,
|
||||||
|
server_peer_id,
|
||||||
|
sync_interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the sync loop
|
||||||
|
pub async fn run(mut self) -> Result<()> {
|
||||||
|
tracing::info!("Starting client sync loop");
|
||||||
|
|
||||||
|
let mut interval = tokio::time::interval(self.sync_interval);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
match self.perform_sync().await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::debug!("Sync completed successfully");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Sync failed: {}", e);
|
||||||
|
// Continue syncing despite errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a single sync operation
|
||||||
|
async fn perform_sync(&mut self) -> Result<()> {
|
||||||
|
tracing::debug!("Performing sync with server {}", self.server_peer_id);
|
||||||
|
|
||||||
|
// TODO: Implement sync logic:
|
||||||
|
// 1. Get list of documents to sync
|
||||||
|
// 2. For each document:
|
||||||
|
// - Get local heads/metadata
|
||||||
|
// - Request changes since last sync
|
||||||
|
// - Apply received changes
|
||||||
|
// - Submit any local changes
|
||||||
|
// - Update local metadata
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync a specific document
|
||||||
|
pub async fn sync_document(&mut self, document_id: &str) -> Result<()> {
|
||||||
|
tracing::debug!("Syncing document {}", document_id);
|
||||||
|
|
||||||
|
// TODO: Implement document-specific sync
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force immediate sync (outside of normal interval)
|
||||||
|
pub async fn sync_now(&mut self) -> Result<()> {
|
||||||
|
tracing::info!("Forcing immediate sync");
|
||||||
|
|
||||||
|
self.perform_sync().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sync_loop_creation() {
|
||||||
|
let behaviour = ClientBehaviour::new();
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let db_path = temp_dir.path().join("test_cache.db");
|
||||||
|
let database = crate::client::database::ClientDatabase::open(db_path).await.unwrap();
|
||||||
|
let server_peer_id = PeerId::random();
|
||||||
|
let sync_interval = Duration::from_secs(1);
|
||||||
|
|
||||||
|
let _sync_loop = ClientSyncLoop::new(behaviour, database, server_peer_id, sync_interval);
|
||||||
|
// Sync loop creation should not panic
|
||||||
|
}
|
||||||
|
}
|
||||||
70
eigensync/src/lib.rs
Normal file
70
eigensync/src/lib.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
//! Eigensync: Distributed State Synchronization using Automerge CRDTs
|
||||||
|
//!
|
||||||
|
//! This crate provides a distributed state synchronization system built on top of
|
||||||
|
//! Automerge CRDTs and libp2p networking. It enables synchronizing append-only state
|
||||||
|
//! machines across multiple devices.
|
||||||
|
//!
|
||||||
|
//! # Features
|
||||||
|
//!
|
||||||
|
//! - **Append-only state synchronization**: Designed for state machines that only add states
|
||||||
|
//! - **Conflict-free replication**: Uses Automerge CRDTs to handle concurrent updates
|
||||||
|
//! - **Peer-to-peer networking**: Built on libp2p for reliable P2P communication
|
||||||
|
//! - **Persistent storage**: SQLite-based persistence for both server and client
|
||||||
|
//! - **Authentication**: PeerId-based authentication with ActorId mapping
|
||||||
|
//!
|
||||||
|
//! # Architecture
|
||||||
|
//!
|
||||||
|
//! The system consists of:
|
||||||
|
//! - **Server**: Stores and distributes patches per PeerId
|
||||||
|
//! - **Client**: Maintains local Automerge document and syncs with server
|
||||||
|
//! - **Protocol**: Request/response protocol for patch exchange
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
|
pub mod protocol;
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
pub mod client;
|
||||||
|
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
pub mod metrics;
|
||||||
|
|
||||||
|
// Re-export commonly used types
|
||||||
|
pub use types::{Error, Result, ActorId, PeerId, DocumentState, PatchInfo};
|
||||||
|
pub use protocol::{EigensyncMessage, EigensyncRequest, EigensyncResponse};
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
pub use client::{Client, ClientConfig};
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub use server::{Server, ServerConfig};
|
||||||
|
|
||||||
|
/// Version of the eigensync protocol
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
/// Protocol name for libp2p
|
||||||
|
pub const PROTOCOL_NAME: &str = "/eigensync/1.0.0";
|
||||||
|
|
||||||
|
/// Main entry point for integrating eigensync with swap state machine
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
pub async fn append_state(
|
||||||
|
client: &mut Client,
|
||||||
|
swap_id: uuid::Uuid,
|
||||||
|
state_json: serde_json::Value,
|
||||||
|
timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
) -> Result<()> {
|
||||||
|
client.append_swap_state(swap_id, state_json, timestamp).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_protocol_constants() {
|
||||||
|
assert_eq!(PROTOCOL_VERSION, 1);
|
||||||
|
assert_eq!(PROTOCOL_NAME, "/eigensync/1.0.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
190
eigensync/src/metrics.rs
Normal file
190
eigensync/src/metrics.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
//! Metrics and observability for eigensync
|
||||||
|
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
use prometheus::{Counter, Histogram, Registry};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Metrics collector for eigensync operations
|
||||||
|
pub struct EigensyncMetrics {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
registry: Registry,
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
changes_sent: Counter,
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
changes_received: Counter,
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
sync_duration: Histogram,
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
rtt_histogram: Histogram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EigensyncMetrics {
|
||||||
|
/// Create a new metrics collector
|
||||||
|
pub fn new() -> Self {
|
||||||
|
tracing::debug!("Creating eigensync metrics collector");
|
||||||
|
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
{
|
||||||
|
let registry = Registry::new();
|
||||||
|
|
||||||
|
let changes_sent = Counter::new(
|
||||||
|
"eigensync_changes_sent_total",
|
||||||
|
"Total number of changes sent"
|
||||||
|
).expect("Failed to create changes_sent counter");
|
||||||
|
|
||||||
|
let changes_received = Counter::new(
|
||||||
|
"eigensync_changes_received_total",
|
||||||
|
"Total number of changes received"
|
||||||
|
).expect("Failed to create changes_received counter");
|
||||||
|
|
||||||
|
let sync_duration = Histogram::with_opts(
|
||||||
|
prometheus::HistogramOpts::new(
|
||||||
|
"eigensync_sync_duration_seconds",
|
||||||
|
"Duration of sync operations in seconds"
|
||||||
|
)
|
||||||
|
).expect("Failed to create sync_duration histogram");
|
||||||
|
|
||||||
|
let rtt_histogram = Histogram::with_opts(
|
||||||
|
prometheus::HistogramOpts::new(
|
||||||
|
"eigensync_request_rtt_seconds",
|
||||||
|
"Round-trip time for requests in seconds"
|
||||||
|
)
|
||||||
|
).expect("Failed to create rtt histogram");
|
||||||
|
|
||||||
|
registry.register(Box::new(changes_sent.clone())).unwrap();
|
||||||
|
registry.register(Box::new(changes_received.clone())).unwrap();
|
||||||
|
registry.register(Box::new(sync_duration.clone())).unwrap();
|
||||||
|
registry.register(Box::new(rtt_histogram.clone())).unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
registry,
|
||||||
|
changes_sent,
|
||||||
|
changes_received,
|
||||||
|
sync_duration,
|
||||||
|
rtt_histogram,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "metrics"))]
|
||||||
|
{
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record changes sent
|
||||||
|
pub fn record_changes_sent(&self, count: u64) {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
{
|
||||||
|
self.changes_sent.inc_by(count as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Recorded {} changes sent", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record changes received
|
||||||
|
pub fn record_changes_received(&self, count: u64) {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
{
|
||||||
|
self.changes_received.inc_by(count as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Recorded {} changes received", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record sync operation duration
|
||||||
|
pub fn record_sync_duration(&self, duration: std::time::Duration) {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
{
|
||||||
|
self.sync_duration.observe(duration.as_secs_f64());
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Recorded sync duration: {:?}", duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record request round-trip time
|
||||||
|
pub fn record_rtt(&self, rtt: std::time::Duration) {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
{
|
||||||
|
self.rtt_histogram.observe(rtt.as_secs_f64());
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Recorded RTT: {:?}", rtt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get metrics registry (for Prometheus export)
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
pub fn registry(&self) -> &Registry {
|
||||||
|
&self.registry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EigensyncMetrics {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timer for measuring operation duration
|
||||||
|
pub struct Timer {
|
||||||
|
start: Instant,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer {
|
||||||
|
/// Start a new timer
|
||||||
|
pub fn new(label: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
start: Instant::now(),
|
||||||
|
label: label.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the timer and return the elapsed duration
|
||||||
|
pub fn stop(self) -> std::time::Duration {
|
||||||
|
let duration = self.start.elapsed();
|
||||||
|
tracing::debug!("Timer '{}' finished in {:?}", self.label, duration);
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro for timing operations
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! time_operation {
|
||||||
|
($metrics:expr, $operation:expr, $block:block) => {{
|
||||||
|
let timer = crate::metrics::Timer::new($operation);
|
||||||
|
let result = $block;
|
||||||
|
let duration = timer.stop();
|
||||||
|
$metrics.record_sync_duration(duration);
|
||||||
|
result
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metrics_creation() {
|
||||||
|
let _metrics = EigensyncMetrics::new();
|
||||||
|
// Metrics creation should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timer() {
|
||||||
|
let timer = Timer::new("test");
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||||
|
let duration = timer.stop();
|
||||||
|
assert!(duration.as_millis() >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metrics_recording() {
|
||||||
|
let metrics = EigensyncMetrics::new();
|
||||||
|
|
||||||
|
// These should not panic
|
||||||
|
metrics.record_changes_sent(5);
|
||||||
|
metrics.record_changes_received(3);
|
||||||
|
metrics.record_sync_duration(std::time::Duration::from_millis(100));
|
||||||
|
metrics.record_rtt(std::time::Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
}
|
||||||
395
eigensync/src/protocol.rs
Normal file
395
eigensync/src/protocol.rs
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
//! Protocol definitions for eigensync communication
|
||||||
|
//!
|
||||||
|
//! This module defines the wire protocol used for communication between
|
||||||
|
//! eigensync clients and servers. The protocol is versioned and uses
|
||||||
|
//! serde_cbor for serialization with length-prefixed frames.
|
||||||
|
|
||||||
|
use crate::types::{ActorId, PeerId, Result, Error};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Protocol version for version negotiation
|
||||||
|
pub const CURRENT_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
/// Maximum message size to prevent DoS attacks (10 MB)
|
||||||
|
pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Default request timeout
|
||||||
|
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
/// Main message envelope for all eigensync communications
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EigensyncMessage {
|
||||||
|
/// Protocol version
|
||||||
|
pub version: u32,
|
||||||
|
/// Unique request identifier for matching responses
|
||||||
|
pub request_id: uuid::Uuid,
|
||||||
|
/// Message payload
|
||||||
|
pub payload: EigensyncPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Union type for all message payloads
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", content = "data")]
|
||||||
|
pub enum EigensyncPayload {
|
||||||
|
Request(EigensyncRequest),
|
||||||
|
Response(EigensyncResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All possible request types
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", content = "params")]
|
||||||
|
pub enum EigensyncRequest {
|
||||||
|
/// Get changes from server since a given point
|
||||||
|
GetChanges(GetChangesParams),
|
||||||
|
/// Submit new changes to server
|
||||||
|
SubmitChanges(SubmitChangesParams),
|
||||||
|
/// Ping for connectivity testing
|
||||||
|
Ping(PingParams),
|
||||||
|
/// Get server status/info
|
||||||
|
GetStatus(GetStatusParams),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All possible response types
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", content = "result")]
|
||||||
|
pub enum EigensyncResponse {
|
||||||
|
/// Response to GetChanges request
|
||||||
|
GetChanges(GetChangesResult),
|
||||||
|
/// Response to SubmitChanges request
|
||||||
|
SubmitChanges(SubmitChangesResult),
|
||||||
|
/// Response to Ping request
|
||||||
|
Ping(PingResult),
|
||||||
|
/// Response to GetStatus request
|
||||||
|
GetStatus(GetStatusResult),
|
||||||
|
/// Error response for any request
|
||||||
|
Error(ErrorResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request parameters
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetChangesParams {
|
||||||
|
/// Document to get changes for (typically swap_id)
|
||||||
|
pub document_id: String,
|
||||||
|
/// Only return changes after this sequence number
|
||||||
|
pub since_sequence: Option<u64>,
|
||||||
|
/// Only return changes after this timestamp
|
||||||
|
pub since_timestamp: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
/// Maximum number of changes to return (for pagination)
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
/// Automerge heads we already have (to optimize sync)
|
||||||
|
pub have_heads: Vec<automerge::ChangeHash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SubmitChangesParams {
|
||||||
|
/// Document these changes apply to
|
||||||
|
pub document_id: String,
|
||||||
|
/// Serialized Automerge changes
|
||||||
|
pub changes: Vec<Vec<u8>>,
|
||||||
|
/// Actor ID that created these changes
|
||||||
|
pub actor_id: ActorId,
|
||||||
|
/// Expected sequence number for optimistic concurrency control
|
||||||
|
pub expected_sequence: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PingParams {
|
||||||
|
/// Timestamp when ping was sent
|
||||||
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Optional payload for bandwidth testing
|
||||||
|
pub payload: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetStatusParams {
|
||||||
|
/// Include detailed statistics
|
||||||
|
pub include_stats: bool,
|
||||||
|
/// Include information about other peers
|
||||||
|
pub include_peers: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response results
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetChangesResult {
|
||||||
|
/// Document ID these changes apply to
|
||||||
|
pub document_id: String,
|
||||||
|
/// Serialized Automerge changes
|
||||||
|
pub changes: Vec<Vec<u8>>,
|
||||||
|
/// Sequence numbers for each change
|
||||||
|
pub sequences: Vec<u64>,
|
||||||
|
/// Whether there are more changes available
|
||||||
|
pub has_more: bool,
|
||||||
|
/// Current document heads after applying these changes
|
||||||
|
pub new_heads: Vec<automerge::ChangeHash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SubmitChangesResult {
|
||||||
|
/// Document ID changes were applied to
|
||||||
|
pub document_id: String,
|
||||||
|
/// Sequence numbers assigned to the submitted changes
|
||||||
|
pub assigned_sequences: Vec<u64>,
|
||||||
|
/// Number of changes that were actually new (not duplicates)
|
||||||
|
pub new_changes_count: u32,
|
||||||
|
/// Current document heads after applying changes
|
||||||
|
pub new_heads: Vec<automerge::ChangeHash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PingResult {
|
||||||
|
/// Timestamp from the request
|
||||||
|
pub request_timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Timestamp when server processed the request
|
||||||
|
pub response_timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Echo back any payload that was sent
|
||||||
|
pub payload: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetStatusResult {
|
||||||
|
/// Server version/build info
|
||||||
|
pub server_version: String,
|
||||||
|
/// Protocol versions supported
|
||||||
|
pub supported_versions: Vec<u32>,
|
||||||
|
/// Server uptime in seconds
|
||||||
|
pub uptime_seconds: u64,
|
||||||
|
/// Number of connected peers
|
||||||
|
pub connected_peers: u32,
|
||||||
|
/// Number of documents being tracked
|
||||||
|
pub document_count: u64,
|
||||||
|
/// Total number of changes stored
|
||||||
|
pub total_changes: u64,
|
||||||
|
/// Optional detailed statistics
|
||||||
|
pub stats: Option<ServerStats>,
|
||||||
|
/// Optional peer information
|
||||||
|
pub peers: Option<Vec<PeerStatus>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ErrorResult {
|
||||||
|
/// Error code for programmatic handling
|
||||||
|
pub code: ErrorCode,
|
||||||
|
/// Human-readable error message
|
||||||
|
pub message: String,
|
||||||
|
/// Optional additional details
|
||||||
|
pub details: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServerStats {
|
||||||
|
/// Total bytes stored
|
||||||
|
pub total_bytes: u64,
|
||||||
|
/// Bytes sent since startup
|
||||||
|
pub bytes_sent: u64,
|
||||||
|
/// Bytes received since startup
|
||||||
|
pub bytes_received: u64,
|
||||||
|
/// Number of requests processed
|
||||||
|
pub requests_processed: u64,
|
||||||
|
/// Average request processing time in milliseconds
|
||||||
|
pub avg_request_time_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PeerStatus {
|
||||||
|
/// Peer ID
|
||||||
|
pub peer_id: String,
|
||||||
|
/// Associated actor ID if authenticated
|
||||||
|
pub actor_id: Option<String>,
|
||||||
|
/// When peer connected
|
||||||
|
pub connected_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Last activity timestamp
|
||||||
|
pub last_activity: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Number of documents this peer is syncing
|
||||||
|
pub document_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error codes for programmatic error handling
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
/// Unknown or internal server error
|
||||||
|
InternalError = 1000,
|
||||||
|
/// Invalid request format or parameters
|
||||||
|
InvalidRequest = 1001,
|
||||||
|
/// Authentication failed
|
||||||
|
AuthenticationFailed = 1002,
|
||||||
|
/// Requested resource not found
|
||||||
|
NotFound = 1003,
|
||||||
|
/// Rate limit exceeded
|
||||||
|
RateLimitExceeded = 1004,
|
||||||
|
/// Storage quota exceeded
|
||||||
|
QuotaExceeded = 1005,
|
||||||
|
/// Version not supported
|
||||||
|
UnsupportedVersion = 1006,
|
||||||
|
/// Request timeout
|
||||||
|
Timeout = 1007,
|
||||||
|
/// Conflict in optimistic concurrency control
|
||||||
|
Conflict = 1008,
|
||||||
|
/// Invalid actor/peer mapping
|
||||||
|
InvalidActorMapping = 1009,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorCode {
|
||||||
|
/// Convert error code to human-readable string
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ErrorCode::InternalError => "internal_error",
|
||||||
|
ErrorCode::InvalidRequest => "invalid_request",
|
||||||
|
ErrorCode::AuthenticationFailed => "authentication_failed",
|
||||||
|
ErrorCode::NotFound => "not_found",
|
||||||
|
ErrorCode::RateLimitExceeded => "rate_limit_exceeded",
|
||||||
|
ErrorCode::QuotaExceeded => "quota_exceeded",
|
||||||
|
ErrorCode::UnsupportedVersion => "unsupported_version",
|
||||||
|
ErrorCode::Timeout => "timeout",
|
||||||
|
ErrorCode::Conflict => "conflict",
|
||||||
|
ErrorCode::InvalidActorMapping => "invalid_actor_mapping",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ErrorCode> for Error {
|
||||||
|
fn from(code: ErrorCode) -> Self {
|
||||||
|
Error::Protocol {
|
||||||
|
message: format!("Protocol error: {}", code.as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec for serializing/deserializing eigensync messages
|
||||||
|
pub struct EigensyncCodec;
|
||||||
|
|
||||||
|
impl EigensyncCodec {
|
||||||
|
/// Serialize message to bytes with length prefix
|
||||||
|
pub fn encode(message: &EigensyncMessage) -> Result<Vec<u8>> {
|
||||||
|
let payload = serde_cbor::to_vec(message)?;
|
||||||
|
|
||||||
|
if payload.len() > MAX_MESSAGE_SIZE {
|
||||||
|
return Err(Error::Protocol {
|
||||||
|
message: format!(
|
||||||
|
"Message too large: {} bytes > {} max",
|
||||||
|
payload.len(),
|
||||||
|
MAX_MESSAGE_SIZE
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Vec::with_capacity(4 + payload.len());
|
||||||
|
result.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||||
|
result.extend_from_slice(&payload);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize message from bytes (assumes length prefix already read)
|
||||||
|
pub fn decode(data: &[u8]) -> Result<EigensyncMessage> {
|
||||||
|
if data.len() > MAX_MESSAGE_SIZE {
|
||||||
|
return Err(Error::Protocol {
|
||||||
|
message: format!(
|
||||||
|
"Message too large: {} bytes > {} max",
|
||||||
|
data.len(),
|
||||||
|
MAX_MESSAGE_SIZE
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: EigensyncMessage = serde_cbor::from_slice(data)?;
|
||||||
|
|
||||||
|
// Validate version
|
||||||
|
if message.version > CURRENT_VERSION {
|
||||||
|
return Err(Error::Protocol {
|
||||||
|
message: format!(
|
||||||
|
"Unsupported protocol version: {} > {}",
|
||||||
|
message.version,
|
||||||
|
CURRENT_VERSION
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a request message
|
||||||
|
pub fn create_request(request: EigensyncRequest) -> EigensyncMessage {
|
||||||
|
EigensyncMessage {
|
||||||
|
version: CURRENT_VERSION,
|
||||||
|
request_id: uuid::Uuid::new_v4(),
|
||||||
|
payload: EigensyncPayload::Request(request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a response message
|
||||||
|
pub fn create_response(
|
||||||
|
request_id: uuid::Uuid,
|
||||||
|
response: EigensyncResponse,
|
||||||
|
) -> EigensyncMessage {
|
||||||
|
EigensyncMessage {
|
||||||
|
version: CURRENT_VERSION,
|
||||||
|
request_id,
|
||||||
|
payload: EigensyncPayload::Response(response),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an error response
|
||||||
|
pub fn create_error_response(
|
||||||
|
request_id: uuid::Uuid,
|
||||||
|
code: ErrorCode,
|
||||||
|
message: String,
|
||||||
|
) -> EigensyncMessage {
|
||||||
|
Self::create_response(
|
||||||
|
request_id,
|
||||||
|
EigensyncResponse::Error(ErrorResult {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
details: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_codec_roundtrip() {
|
||||||
|
let request = EigensyncRequest::Ping(PingParams {
|
||||||
|
timestamp: chrono::Utc::now(),
|
||||||
|
payload: Some(b"test".to_vec()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = EigensyncCodec::create_request(request);
|
||||||
|
let encoded = EigensyncCodec::encode(&message).unwrap();
|
||||||
|
|
||||||
|
// Skip the length prefix for decoding
|
||||||
|
let decoded = EigensyncCodec::decode(&encoded[4..]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(message.version, decoded.version);
|
||||||
|
assert_eq!(message.request_id, decoded.request_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_size_limit() {
|
||||||
|
let large_payload = vec![0u8; MAX_MESSAGE_SIZE + 1];
|
||||||
|
let request = EigensyncRequest::SubmitChanges(SubmitChangesParams {
|
||||||
|
document_id: "test".to_string(),
|
||||||
|
changes: vec![large_payload],
|
||||||
|
actor_id: ActorId(automerge::ActorId::random()),
|
||||||
|
expected_sequence: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = EigensyncCodec::create_request(request);
|
||||||
|
let result = EigensyncCodec::encode(&message);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains("Message too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_codes() {
|
||||||
|
assert_eq!(ErrorCode::InternalError.as_str(), "internal_error");
|
||||||
|
assert_eq!(ErrorCode::AuthenticationFailed.as_str(), "authentication_failed");
|
||||||
|
assert_eq!(ErrorCode::NotFound.as_str(), "not_found");
|
||||||
|
}
|
||||||
|
}
|
||||||
72
eigensync/src/server/behaviour.rs
Normal file
72
eigensync/src/server/behaviour.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
//! libp2p networking behaviour for eigensync server
|
||||||
|
|
||||||
|
use crate::protocol::{EigensyncMessage, EigensyncRequest, EigensyncResponse};
|
||||||
|
use crate::types::{Result, PeerId};
|
||||||
|
|
||||||
|
/// libp2p behaviour for eigensync server (placeholder)
|
||||||
|
pub struct ServerBehaviour {
|
||||||
|
// TODO: Add actual behaviour components
|
||||||
|
// request_response: RequestResponse<EigensyncCodec>,
|
||||||
|
// identify: Identify,
|
||||||
|
// ping: Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerBehaviour {
|
||||||
|
/// Create a new server behaviour
|
||||||
|
pub fn new() -> Self {
|
||||||
|
tracing::debug!("Creating server behaviour");
|
||||||
|
|
||||||
|
// TODO: Initialize behaviour components
|
||||||
|
Self {
|
||||||
|
// request_response: RequestResponse::new(...),
|
||||||
|
// identify: Identify::new(...),
|
||||||
|
// ping: Ping::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle incoming request
|
||||||
|
pub async fn handle_request(
|
||||||
|
&mut self,
|
||||||
|
peer_id: PeerId,
|
||||||
|
request: EigensyncRequest,
|
||||||
|
) -> Result<EigensyncResponse> {
|
||||||
|
tracing::debug!("Handling request from peer {}: {:?}", peer_id, request);
|
||||||
|
|
||||||
|
// TODO: Implement request handling
|
||||||
|
match request {
|
||||||
|
EigensyncRequest::GetChanges(_params) => {
|
||||||
|
// TODO: Handle GetChanges
|
||||||
|
todo!("GetChanges not implemented")
|
||||||
|
},
|
||||||
|
EigensyncRequest::SubmitChanges(_params) => {
|
||||||
|
// TODO: Handle SubmitChanges
|
||||||
|
todo!("SubmitChanges not implemented")
|
||||||
|
},
|
||||||
|
EigensyncRequest::Ping(_params) => {
|
||||||
|
// TODO: Handle Ping
|
||||||
|
todo!("Ping not implemented")
|
||||||
|
},
|
||||||
|
EigensyncRequest::GetStatus(_params) => {
|
||||||
|
// TODO: Handle GetStatus
|
||||||
|
todo!("GetStatus not implemented")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerBehaviour {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_behaviour_creation() {
|
||||||
|
let _behaviour = ServerBehaviour::new();
|
||||||
|
// Behaviour creation should not panic
|
||||||
|
}
|
||||||
|
}
|
||||||
81
eigensync/src/server/database.rs
Normal file
81
eigensync/src/server/database.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
//! Server database layer for patch storage and peer management
|
||||||
|
|
||||||
|
use crate::types::{Result, PeerId, ActorId};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Server database for storing patches and peer information
|
||||||
|
pub struct ServerDatabase {
|
||||||
|
connection: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerDatabase {
|
||||||
|
/// Open or create a server database
|
||||||
|
pub async fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
tracing::info!("Opening server database at {:?}", path.as_ref());
|
||||||
|
|
||||||
|
// TODO: Implement actual database opening and migration
|
||||||
|
let connection = Connection::open(path)?;
|
||||||
|
|
||||||
|
Ok(Self { connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a patch for a peer
|
||||||
|
pub async fn store_patch(
|
||||||
|
&self,
|
||||||
|
peer_id: PeerId,
|
||||||
|
actor_id: ActorId,
|
||||||
|
document_id: &str,
|
||||||
|
patch_data: &[u8],
|
||||||
|
) -> Result<u64> {
|
||||||
|
tracing::debug!("Storing patch for peer {} in document {}", peer_id, document_id);
|
||||||
|
|
||||||
|
// TODO: Implement patch storage
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get patches for a peer since a given sequence number
|
||||||
|
pub async fn get_patches(
|
||||||
|
&self,
|
||||||
|
peer_id: PeerId,
|
||||||
|
document_id: &str,
|
||||||
|
since_sequence: Option<u64>,
|
||||||
|
) -> Result<Vec<(u64, Vec<u8>)>> {
|
||||||
|
tracing::debug!("Getting patches for peer {} in document {} since {:?}",
|
||||||
|
peer_id, document_id, since_sequence);
|
||||||
|
|
||||||
|
// TODO: Implement patch retrieval
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind a peer ID to an actor ID
|
||||||
|
pub async fn bind_peer_actor(&self, peer_id: PeerId, actor_id: ActorId) -> Result<()> {
|
||||||
|
tracing::debug!("Binding peer {} to actor {}", peer_id, actor_id);
|
||||||
|
|
||||||
|
// TODO: Implement peer-actor binding
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get actor ID for a peer
|
||||||
|
pub async fn get_actor_for_peer(&self, peer_id: PeerId) -> Result<Option<ActorId>> {
|
||||||
|
tracing::debug!("Getting actor for peer {}", peer_id);
|
||||||
|
|
||||||
|
// TODO: Implement actor lookup
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_creation() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let db_path = temp_dir.path().join("test.db");
|
||||||
|
|
||||||
|
let db = ServerDatabase::open(db_path).await;
|
||||||
|
assert!(db.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
74
eigensync/src/server/event_loop.rs
Normal file
74
eigensync/src/server/event_loop.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
//! Server event loop for handling network events
|
||||||
|
|
||||||
|
use crate::server::behaviour::ServerBehaviour;
|
||||||
|
use crate::server::database::ServerDatabase;
|
||||||
|
use crate::types::{Result, PeerId};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Server event loop for handling network events and database operations
|
||||||
|
pub struct ServerEventLoop {
|
||||||
|
behaviour: ServerBehaviour,
|
||||||
|
database: ServerDatabase,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerEventLoop {
|
||||||
|
/// Create a new server event loop
|
||||||
|
pub fn new(behaviour: ServerBehaviour, database: ServerDatabase) -> Self {
|
||||||
|
tracing::debug!("Creating server event loop");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
behaviour,
|
||||||
|
database,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the server event loop
|
||||||
|
pub async fn run(mut self) -> Result<()> {
|
||||||
|
tracing::info!("Starting server event loop");
|
||||||
|
|
||||||
|
// TODO: Implement actual event loop with libp2p swarm
|
||||||
|
loop {
|
||||||
|
// Placeholder: just sleep for now
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Poll swarm for events
|
||||||
|
// - Handle incoming requests
|
||||||
|
// - Manage peer connections
|
||||||
|
// - Perform periodic maintenance tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle peer connection
|
||||||
|
pub async fn handle_peer_connected(&mut self, peer_id: PeerId) -> Result<()> {
|
||||||
|
tracing::info!("Peer connected: {}", peer_id);
|
||||||
|
|
||||||
|
// TODO: Implement peer connection handling
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle peer disconnection
|
||||||
|
pub async fn handle_peer_disconnected(&mut self, peer_id: PeerId) -> Result<()> {
|
||||||
|
tracing::info!("Peer disconnected: {}", peer_id);
|
||||||
|
|
||||||
|
// TODO: Implement peer disconnection handling
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_event_loop_creation() {
|
||||||
|
let behaviour = ServerBehaviour::new();
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let db_path = temp_dir.path().join("test.db");
|
||||||
|
let database = ServerDatabase::open(db_path).await.unwrap();
|
||||||
|
|
||||||
|
let _event_loop = ServerEventLoop::new(behaviour, database);
|
||||||
|
// Event loop creation should not panic
|
||||||
|
}
|
||||||
|
}
|
||||||
92
eigensync/src/server/mod.rs
Normal file
92
eigensync/src/server/mod.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
//! Server-side components for eigensync
|
||||||
|
|
||||||
|
use crate::types::{Result, PeerId, ActorId};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub mod database;
|
||||||
|
pub mod behaviour;
|
||||||
|
pub mod event_loop;
|
||||||
|
|
||||||
|
/// Configuration for the eigensync server
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
/// Path to server database
|
||||||
|
pub database_path: PathBuf,
|
||||||
|
/// Address to listen on
|
||||||
|
pub listen_address: String,
|
||||||
|
/// Port to listen on
|
||||||
|
pub listen_port: u16,
|
||||||
|
/// Maximum number of connected peers
|
||||||
|
pub max_peers: u32,
|
||||||
|
/// Rate limiting configuration
|
||||||
|
pub rate_limit: crate::types::RateLimitConfig,
|
||||||
|
/// Snapshot configuration
|
||||||
|
pub snapshot_config: crate::types::SnapshotConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
database_path: PathBuf::from("server_patches.sqlite"),
|
||||||
|
listen_address: "0.0.0.0".to_string(),
|
||||||
|
listen_port: 9944,
|
||||||
|
max_peers: 100,
|
||||||
|
rate_limit: crate::types::RateLimitConfig::default(),
|
||||||
|
snapshot_config: crate::types::SnapshotConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main server struct (placeholder implementation)
|
||||||
|
pub struct Server {
|
||||||
|
config: ServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Create a new server with the given configuration
|
||||||
|
pub async fn new(config: ServerConfig) -> Result<Self> {
|
||||||
|
tracing::info!("Creating eigensync server with config: {:?}", config);
|
||||||
|
|
||||||
|
// TODO: Initialize database, networking, etc.
|
||||||
|
|
||||||
|
Ok(Self { config })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the server
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
tracing::info!("Starting eigensync server on {}:{}",
|
||||||
|
self.config.listen_address, self.config.listen_port);
|
||||||
|
|
||||||
|
// TODO: Implement server event loop
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get server configuration
|
||||||
|
pub fn config(&self) -> &ServerConfig {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_server_config_default() {
|
||||||
|
let config = ServerConfig::default();
|
||||||
|
assert_eq!(config.listen_address, "0.0.0.0");
|
||||||
|
assert_eq!(config.listen_port, 9944);
|
||||||
|
assert_eq!(config.max_peers, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_server_creation() {
|
||||||
|
let config = ServerConfig::default();
|
||||||
|
let server = Server::new(config).await;
|
||||||
|
assert!(server.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
244
eigensync/src/types.rs
Normal file
244
eigensync/src/types.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
//! Common types and error definitions for eigensync
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Result type alias for eigensync operations
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// PeerId type alias
|
||||||
|
pub type PeerId = libp2p::PeerId;
|
||||||
|
|
||||||
|
/// ActorId uniquely identifies an actor in the Automerge document
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct ActorId(pub automerge::ActorId);
|
||||||
|
|
||||||
|
impl From<automerge::ActorId> for ActorId {
|
||||||
|
fn from(actor_id: automerge::ActorId) -> Self {
|
||||||
|
Self(actor_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ActorId> for automerge::ActorId {
|
||||||
|
fn from(actor_id: ActorId) -> Self {
|
||||||
|
actor_id.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ActorId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Comprehensive error types for eigensync operations
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] rusqlite::Error),
|
||||||
|
|
||||||
|
#[error("Migration error: {0}")]
|
||||||
|
Migration(#[from] rusqlite_migration::Error),
|
||||||
|
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serialization(#[from] serde_cbor::Error),
|
||||||
|
|
||||||
|
#[error("JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Automerge error: {0}")]
|
||||||
|
Automerge(#[from] automerge::AutomergeError),
|
||||||
|
|
||||||
|
#[error("Network error: {0}")]
|
||||||
|
Network(#[from] libp2p::swarm::DialError),
|
||||||
|
|
||||||
|
#[error("Protocol error: {message}")]
|
||||||
|
Protocol { message: String },
|
||||||
|
|
||||||
|
#[error("Authentication failed for peer {peer_id}: {reason}")]
|
||||||
|
Authentication { peer_id: PeerId, reason: String },
|
||||||
|
|
||||||
|
#[error("Document not found: {document_id}")]
|
||||||
|
DocumentNotFound { document_id: String },
|
||||||
|
|
||||||
|
#[error("Invalid configuration: {message}")]
|
||||||
|
InvalidConfig { message: String },
|
||||||
|
|
||||||
|
#[error("Timeout: {operation}")]
|
||||||
|
Timeout { operation: String },
|
||||||
|
|
||||||
|
#[error("Actor mapping conflict: peer {peer_id} tried to use actor {actor_id} already mapped to different peer")]
|
||||||
|
ActorMappingConflict { peer_id: PeerId, actor_id: ActorId },
|
||||||
|
|
||||||
|
#[error("Storage quota exceeded: {current_size} bytes")]
|
||||||
|
StorageQuotaExceeded { current_size: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a patch/change in the system
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PatchInfo {
|
||||||
|
/// Unique identifier for this patch
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
/// Actor that created this patch
|
||||||
|
pub actor_id: ActorId,
|
||||||
|
/// Timestamp when patch was created
|
||||||
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Size of the patch data in bytes
|
||||||
|
pub size_bytes: u64,
|
||||||
|
/// Hash of the patch content for integrity checking
|
||||||
|
pub content_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current state of a document
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DocumentState {
|
||||||
|
/// Document identifier (typically swap_id)
|
||||||
|
pub document_id: String,
|
||||||
|
/// Current number of patches applied
|
||||||
|
pub patch_count: u64,
|
||||||
|
/// Total size of all patches in bytes
|
||||||
|
pub total_size_bytes: u64,
|
||||||
|
/// Timestamp of last update
|
||||||
|
pub last_updated: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Current document heads
|
||||||
|
pub heads: Vec<automerge::ChangeHash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for snapshot and garbage collection
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SnapshotConfig {
|
||||||
|
/// Trigger snapshot after this many changes
|
||||||
|
pub max_changes: u64,
|
||||||
|
/// Trigger snapshot after this many bytes
|
||||||
|
pub max_size_bytes: u64,
|
||||||
|
/// Whether to compress snapshots above this size
|
||||||
|
pub compress_threshold_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SnapshotConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_changes: 10_000,
|
||||||
|
max_size_bytes: 10 * 1024 * 1024, // 10 MB
|
||||||
|
compress_threshold_bytes: 1024 * 1024, // 1 MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metrics for monitoring sync operations
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SyncMetrics {
|
||||||
|
/// Total number of changes sent
|
||||||
|
pub changes_sent: u64,
|
||||||
|
/// Total number of changes received
|
||||||
|
pub changes_received: u64,
|
||||||
|
/// Total number of bytes sent
|
||||||
|
pub bytes_sent: u64,
|
||||||
|
/// Total number of bytes received
|
||||||
|
pub bytes_received: u64,
|
||||||
|
/// Number of sync operations performed
|
||||||
|
pub sync_operations: u64,
|
||||||
|
/// Number of failed sync operations
|
||||||
|
pub sync_failures: u64,
|
||||||
|
/// Average round-trip time in milliseconds
|
||||||
|
pub avg_rtt_ms: f64,
|
||||||
|
/// Number of conflicts resolved
|
||||||
|
pub conflicts_resolved: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of a peer connection
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum PeerState {
|
||||||
|
/// Peer is disconnected
|
||||||
|
Disconnected,
|
||||||
|
/// Peer is connecting
|
||||||
|
Connecting,
|
||||||
|
/// Peer is connected and authenticated
|
||||||
|
Connected,
|
||||||
|
/// Peer authentication failed
|
||||||
|
AuthenticationFailed,
|
||||||
|
/// Peer connection failed
|
||||||
|
ConnectionFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a connected peer
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PeerInfo {
|
||||||
|
/// Peer ID (as string)
|
||||||
|
pub peer_id: String,
|
||||||
|
/// Associated actor ID
|
||||||
|
pub actor_id: Option<ActorId>,
|
||||||
|
/// Current connection state
|
||||||
|
pub state: PeerState,
|
||||||
|
/// When the peer was first seen
|
||||||
|
pub first_seen: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// When the peer was last seen
|
||||||
|
pub last_seen: chrono::DateTime<chrono::Utc>,
|
||||||
|
/// Sync metrics for this peer
|
||||||
|
pub metrics: SyncMetrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A batch of changes/patches for efficient transmission
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ChangeBatch {
|
||||||
|
/// Unique identifier for this batch
|
||||||
|
pub batch_id: uuid::Uuid,
|
||||||
|
/// Document this batch applies to
|
||||||
|
pub document_id: String,
|
||||||
|
/// The actual changes (serialized)
|
||||||
|
pub changes: Vec<Vec<u8>>,
|
||||||
|
/// Metadata about each change
|
||||||
|
pub patch_info: Vec<PatchInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for rate limiting
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RateLimitConfig {
|
||||||
|
/// Maximum requests per second per peer
|
||||||
|
pub max_requests_per_second: u32,
|
||||||
|
/// Maximum bytes per second per peer
|
||||||
|
pub max_bytes_per_second: u64,
|
||||||
|
/// Burst allowance
|
||||||
|
pub burst_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RateLimitConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_requests_per_second: 10,
|
||||||
|
max_bytes_per_second: 1024 * 1024, // 1 MB/s
|
||||||
|
burst_size: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_actor_id_conversion() {
|
||||||
|
let automerge_actor = automerge::ActorId::random();
|
||||||
|
let actor_id = ActorId::from(automerge_actor.clone());
|
||||||
|
let converted_back: automerge::ActorId = actor_id.into();
|
||||||
|
assert_eq!(automerge_actor, converted_back);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_snapshot_config_defaults() {
|
||||||
|
let config = SnapshotConfig::default();
|
||||||
|
assert_eq!(config.max_changes, 10_000);
|
||||||
|
assert_eq!(config.max_size_bytes, 10 * 1024 * 1024);
|
||||||
|
assert_eq!(config.compress_threshold_bytes, 1024 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rate_limit_config_defaults() {
|
||||||
|
let config = RateLimitConfig::default();
|
||||||
|
assert_eq!(config.max_requests_per_second, 10);
|
||||||
|
assert_eq!(config.max_bytes_per_second, 1024 * 1024);
|
||||||
|
assert_eq!(config.burst_size, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue