From 28c56defe7e19aa2f6f7a02043a5d6969f583d62 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Thu, 11 Dec 2025 22:10:06 +0000 Subject: [PATCH] initial working demo sans networking Signed-off-by: Sienna Meridian Satterwhite --- Cargo.lock | 1377 ++++------------- Cargo.toml | 2 +- crates/app/Cargo.toml | 50 + crates/app/src/camera.rs | 27 + crates/app/src/cube.rs | 65 + crates/app/src/debug_ui.rs | 89 ++ crates/app/src/input/mouse.rs | 79 + crates/app/src/lib.rs | 12 + crates/app/src/main.rs | 101 ++ crates/app/src/rendering.rs | 53 + crates/app/src/setup.rs | 279 ++++ crates/app/tests/cube_sync_headless.rs | 471 ++++++ crates/client/.gitignore | 1 - crates/client/Cargo.toml | 43 - crates/client/src/lib.rs | 14 - crates/client/src/main.rs | 24 - crates/server/Cargo.toml | 58 - crates/server/src/assets/mod.rs | 1 - crates/server/src/components/database.rs | 15 - crates/server/src/components/gossip.rs | 95 -- crates/server/src/components/mod.rs | 5 - crates/server/src/config.rs | 89 -- crates/server/src/db/mod.rs | 5 - crates/server/src/db/operations.rs | 339 ---- crates/server/src/db/schema.rs | 210 --- crates/server/src/entities/mod.rs | 1 - crates/server/src/iroh_sync.rs | 49 - crates/server/src/main.rs | 99 -- crates/server/src/models.rs | 66 - crates/server/src/proto/emotions.proto | 72 - crates/server/src/services/chat_poller.rs | 143 -- .../server/src/services/embedding_service.rs | 119 -- crates/server/src/services/emotion_service.rs | 134 -- crates/server/src/services/grpc_server.rs | 232 --- crates/server/src/services/mod.rs | 7 - crates/server/src/sync_plugin.rs | 114 -- crates/server/src/systems/database.rs | 9 - crates/server/src/systems/gossip.rs | 117 -- crates/server/src/systems/mod.rs | 7 - crates/server/src/systems/setup.rs | 24 - 40 files changed, 1552 insertions(+), 3145 deletions(-) create mode 100644 crates/app/Cargo.toml create mode 100644 crates/app/src/camera.rs create mode 100644 crates/app/src/cube.rs create mode 100644 crates/app/src/debug_ui.rs create mode 100644 crates/app/src/input/mouse.rs create mode 100644 crates/app/src/lib.rs create mode 100644 crates/app/src/main.rs create mode 100644 crates/app/src/rendering.rs create mode 100644 crates/app/src/setup.rs create mode 100644 crates/app/tests/cube_sync_headless.rs delete mode 100644 crates/client/.gitignore delete mode 100644 crates/client/Cargo.toml delete mode 100644 crates/client/src/lib.rs delete mode 100644 crates/client/src/main.rs delete mode 100644 crates/server/Cargo.toml delete mode 100644 crates/server/src/assets/mod.rs delete mode 100644 crates/server/src/components/database.rs delete mode 100644 crates/server/src/components/gossip.rs delete mode 100644 crates/server/src/components/mod.rs delete mode 100644 crates/server/src/config.rs delete mode 100644 crates/server/src/db/mod.rs delete mode 100644 crates/server/src/db/operations.rs delete mode 100644 crates/server/src/db/schema.rs delete mode 100644 crates/server/src/entities/mod.rs delete mode 100644 crates/server/src/iroh_sync.rs delete mode 100644 crates/server/src/main.rs delete mode 100644 crates/server/src/models.rs delete mode 100644 crates/server/src/proto/emotions.proto delete mode 100644 crates/server/src/services/chat_poller.rs delete mode 100644 crates/server/src/services/embedding_service.rs delete mode 100644 crates/server/src/services/emotion_service.rs delete mode 100644 crates/server/src/services/grpc_server.rs delete mode 100644 crates/server/src/services/mod.rs delete mode 100644 crates/server/src/sync_plugin.rs delete mode 100644 crates/server/src/systems/database.rs delete mode 100644 crates/server/src/systems/gossip.rs delete mode 100644 crates/server/src/systems/mod.rs delete mode 100644 crates/server/src/systems/setup.rs diff --git a/Cargo.lock b/Cargo.lock index c8585c6..24be171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,8 +44,8 @@ dependencies = [ "accesskit_consumer", "hashbrown 0.15.5", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -216,6 +216,29 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "anyhow", + "bevy", + "bevy_egui", + "bincode", + "bytes", + "crossbeam-channel", + "futures-lite", + "iroh", + "iroh-gossip", + "lib", + "objc", + "serde", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "approx" version = "0.5.1" @@ -226,12 +249,23 @@ dependencies = [ ] [[package]] -name = "arbitrary" -version = "1.4.2" +name = "arboard" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ - "derive_arbitrary", + "clipboard-win", + "image", + "log", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", ] [[package]] @@ -410,7 +444,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "base64 0.22.1", + "base64", "http", "log", "url", @@ -445,12 +479,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.22.1" @@ -793,6 +821,53 @@ dependencies = [ "syn", ] +[[package]] +name = "bevy_egui" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c20416343c6d24eedad9db93c4c42c6571b15d14bac4f6f41b993ec413243f9" +dependencies = [ + "arboard", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_time", + "bevy_transform", + "bevy_ui_render", + "bevy_utils", + "bevy_window", + "bevy_winit", + "bytemuck", + "crossbeam-channel", + "egui", + "encase", + "getrandom 0.3.4", + "image", + "itertools 0.14.0", + "js-sys", + "thread_local", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webbrowser", + "wgpu-types", + "winit", +] + [[package]] name = "bevy_encase_derive" version = "0.17.2" @@ -865,7 +940,7 @@ version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d67e954b20551818f7cdb33f169ab4db64506ada66eb4d60d3cb8861103411" dependencies = [ - "base64 0.22.1", + "base64", "bevy_animation", "bevy_app", "bevy_asset", @@ -1677,7 +1752,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -1686,30 +1761,15 @@ dependencies = [ "syn", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec 0.8.0", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bit-vec" version = "0.8.0" @@ -1874,62 +1934,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "candle-core" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ccf5ee3532e66868516d9b315f73aec9f34ea1a37ae98514534d458915dbf1" -dependencies = [ - "byteorder", - "gemm 0.17.1", - "half", - "memmap2", - "num-traits", - "num_cpus", - "rand 0.9.2", - "rand_distr", - "rayon", - "safetensors", - "thiserror 1.0.69", - "ug", - "yoke 0.7.5", - "zip", -] - -[[package]] -name = "candle-nn" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1160c3b63f47d40d91110a3e1e1e566ae38edddbbf492a60b40ffc3bc1ff38" -dependencies = [ - "candle-core", - "half", - "num-traits", - "rayon", - "safetensors", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "candle-transformers" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a0900d49f8605e0e7e6693a1f560e6271279de98e5fa369e7abf3aac245020" -dependencies = [ - "byteorder", - "candle-core", - "candle-nn", - "fancy-regex", - "num-traits", - "rand 0.9.2", - "rayon", - "serde", - "serde_json", - "serde_plain", - "tracing", -] - [[package]] name = "cast" version = "0.3.0" @@ -2077,20 +2081,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] -name = "client" -version = "0.1.0" +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ - "anyhow", - "bevy", - "iroh", - "iroh-gossip", - "lib", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tracing", - "tracing-subscriber", + "error-code", ] [[package]] @@ -2133,19 +2129,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -2243,7 +2226,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -2554,41 +2537,6 @@ dependencies = [ "syn", ] -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -2621,48 +2569,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - [[package]] name = "derive_more" version = "1.0.0" @@ -2732,27 +2638,6 @@ dependencies = [ "crypto-common 0.2.0-rc.4", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -2842,31 +2727,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] -name = "dyn-stack" -version = "0.10.0" +name = "ecolor" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" dependencies = [ "bytemuck", - "reborrow", + "emath", ] -[[package]] -name = "dyn-stack" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" -dependencies = [ - "bytemuck", - "dyn-stack-macros", -] - -[[package]] -name = "dyn-stack-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" - [[package]] name = "ed25519" version = "3.0.0-rc.2" @@ -2894,12 +2763,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "egui" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" +dependencies = [ + "ahash", + "bitflags 2.10.0", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "emath" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" +dependencies = [ + "bytemuck", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -2944,12 +2839,6 @@ dependencies = [ "syn", ] -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -2972,6 +2861,30 @@ dependencies = [ "regex", ] +[[package]] +name = "epaint" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" + [[package]] name = "equivalent" version = "1.0.2" @@ -3000,13 +2913,10 @@ dependencies = [ ] [[package]] -name = "esaxx-rs" -version = "0.1.10" +name = "error-code" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" -dependencies = [ - "cc", -] +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "euclid" @@ -3050,23 +2960,32 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fancy-regex" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" -dependencies = [ - "bit-set 0.5.3", - "regex-automata", - "regex-syntax", -] - [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -3154,15 +3073,6 @@ dependencies = [ "ttf-parser 0.20.0", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -3170,7 +3080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -3184,12 +3094,6 @@ dependencies = [ "syn", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -3335,243 +3239,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gemm" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-c32 0.17.1", - "gemm-c64 0.17.1", - "gemm-common 0.17.1", - "gemm-f16 0.17.1", - "gemm-f32 0.17.1", - "gemm-f64 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-c32 0.18.2", - "gemm-c64 0.18.2", - "gemm-common 0.18.2", - "gemm-f16 0.18.2", - "gemm-f32 0.18.2", - "gemm-f64 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-c32" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-c32" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-c64" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-c64" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-common" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" -dependencies = [ - "bytemuck", - "dyn-stack 0.10.0", - "half", - "num-complex", - "num-traits", - "once_cell", - "paste", - "pulp 0.18.22", - "raw-cpuid 10.7.0", - "rayon", - "seq-macro", - "sysctl 0.5.5", -] - -[[package]] -name = "gemm-common" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" -dependencies = [ - "bytemuck", - "dyn-stack 0.13.2", - "half", - "libm", - "num-complex", - "num-traits", - "once_cell", - "paste", - "pulp 0.21.5", - "raw-cpuid 11.6.0", - "rayon", - "seq-macro", - "sysctl 0.6.0", -] - -[[package]] -name = "gemm-f16" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "gemm-f32 0.17.1", - "half", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "rayon", - "seq-macro", -] - -[[package]] -name = "gemm-f16" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "gemm-f32 0.18.2", - "half", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "rayon", - "seq-macro", -] - -[[package]] -name = "gemm-f32" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-f32" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-f64" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-f64" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - [[package]] name = "generator" version = "0.8.7" @@ -3857,12 +3524,9 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "bytemuck", "cfg-if", "crunchy", "num-traits", - "rand 0.9.2", - "rand_distr", "zerocopy", ] @@ -3974,23 +3638,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" -[[package]] -name = "hf-hub" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b780635574b3d92f036890d8373433d6f9fc7abb320ee42a5c25897fc8ed732" -dependencies = [ - "dirs", - "indicatif", - "log", - "native-tls", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror 1.0.69", - "ureq", -] - [[package]] name = "hickory-proto" version = "0.25.2" @@ -4137,7 +3784,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots", ] [[package]] @@ -4146,7 +3793,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -4196,7 +3843,7 @@ checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.1", + "yoke", "zerofrom", "zerovec", ] @@ -4263,18 +3910,12 @@ dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke 0.8.1", + "yoke", "zerofrom", "zerotrie", "zerovec", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -4328,6 +3969,7 @@ dependencies = [ "moxcms", "num-traits", "png", + "tiff", ] [[package]] @@ -4342,19 +3984,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console", - "number_prefix", - "portable-atomic", - "unicode-width", - "web-time", -] - [[package]] name = "inflections" version = "1.1.1" @@ -4501,7 +4130,7 @@ dependencies = [ "tracing", "url", "wasm-bindgen-futures", - "webpki-roots 1.0.4", + "webpki-roots", "z32", ] @@ -4676,7 +4305,7 @@ dependencies = [ "tokio-websockets", "tracing", "url", - "webpki-roots 1.0.4", + "webpki-roots", "ws_stream_wasm", "z32", ] @@ -4730,18 +4359,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -5010,22 +4630,6 @@ dependencies = [ "libc", ] -[[package]] -name = "macro_rules_attribute" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" -dependencies = [ - "macro_rules_attribute-proc_macro", - "paste", -] - -[[package]] -name = "macro_rules_attribute-proc_macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" - [[package]] name = "malloc_buf" version = "0.0.6" @@ -5057,7 +4661,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", - "stable_deref_trait", ] [[package]] @@ -5069,7 +4672,7 @@ dependencies = [ "bitflags 2.10.0", "block", "core-graphics-types 0.2.0", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", @@ -5120,28 +4723,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "monostate" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" -dependencies = [ - "monostate-impl", - "serde", - "serde_core", -] - -[[package]] -name = "monostate-impl" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "moxcms" version = "0.7.9" @@ -5213,7 +4794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" dependencies = [ "arrayvec", - "bit-set 0.8.0", + "bit-set", "bitflags 2.10.0", "cfg-if", "cfg_aliases", @@ -5250,23 +4831,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.8.0" @@ -5431,6 +4995,12 @@ dependencies = [ "libc", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -5511,7 +5081,6 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "bytemuck", "num-traits", "serde", ] @@ -5575,16 +5144,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_enum" version = "0.7.5" @@ -5607,12 +5166,6 @@ dependencies = [ "syn", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "objc" version = "0.2.7" @@ -5659,10 +5212,22 @@ dependencies = [ "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -5673,7 +5238,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5684,7 +5249,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5696,7 +5261,7 @@ dependencies = [ "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5706,6 +5271,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -5716,7 +5296,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5729,7 +5309,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5751,6 +5331,17 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + [[package]] name = "objc2-io-kit" version = "0.3.2" @@ -5761,6 +5352,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -5769,8 +5371,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -5782,7 +5384,7 @@ dependencies = [ "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5794,7 +5396,7 @@ dependencies = [ "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5805,7 +5407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5821,7 +5423,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -5837,7 +5439,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5850,7 +5452,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5905,84 +5507,18 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "onig" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" -dependencies = [ - "bitflags 2.10.0", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "orbclient" version = "0.3.49" @@ -6259,7 +5795,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b575f975dcf03e258b0c7ab3f81497d7124f508884c37da66a7314aa2a8d467" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "derive_more 2.0.1", "futures-lite", @@ -6377,8 +5913,8 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", + "bit-set", + "bit-vec", "bitflags 2.10.0", "num-traits", "rand 0.9.2", @@ -6390,32 +5926,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "pulp" -version = "0.18.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6" -dependencies = [ - "bytemuck", - "libm", - "num-complex", - "reborrow", -] - -[[package]] -name = "pulp" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" -dependencies = [ - "bytemuck", - "cfg-if", - "libm", - "num-complex", - "reborrow", - "version_check", -] - [[package]] name = "pxfm" version = "0.1.25" @@ -6431,6 +5941,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -6617,24 +6133,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c" -[[package]] -name = "raw-cpuid" -version = "10.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.10.0", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -6651,17 +6149,6 @@ dependencies = [ "rayon-core", ] -[[package]] -name = "rayon-cond" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9" -dependencies = [ - "either", - "itertools 0.11.0", - "rayon", -] - [[package]] name = "rayon-core" version = "1.13.0" @@ -6682,12 +6169,6 @@ dependencies = [ "font-types", ] -[[package]] -name = "reborrow" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" - [[package]] name = "rectangle-pack" version = "0.4.2" @@ -6712,17 +6193,6 @@ dependencies = [ "bitflags 2.10.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "regex" version = "1.12.2" @@ -6764,7 +6234,7 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "futures-util", @@ -6796,7 +6266,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots", ] [[package]] @@ -6835,7 +6305,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ - "base64 0.22.1", + "base64", "bitflags 2.10.0", "serde", "serde_derive", @@ -6933,7 +6403,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -6961,7 +6431,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", @@ -6997,7 +6467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", - "quick-error", + "quick-error 1.2.3", "tempfile", "wait-timeout", ] @@ -7034,16 +6504,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "safetensors" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "salsa20" version = "0.11.0-rc.1" @@ -7097,19 +6557,6 @@ dependencies = [ "tiny-skia", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -7151,12 +6598,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" -[[package]] -name = "seq-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" - [[package]] name = "serde" version = "1.0.228" @@ -7210,15 +6651,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_plain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.3" @@ -7250,35 +6682,6 @@ dependencies = [ "serde", ] -[[package]] -name = "server" -version = "0.1.0" -dependencies = [ - "anyhow", - "bevy", - "candle-core", - "candle-nn", - "candle-transformers", - "chrono", - "futures-lite", - "hf-hub", - "iroh", - "iroh-gossip", - "lib", - "parking_lot", - "rand 0.8.5", - "rusqlite", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokenizers", - "tokio", - "tokio-stream", - "toml", - "tracing", - "tracing-subscriber", -] - [[package]] name = "sha1" version = "0.11.0-rc.2" @@ -7508,18 +6911,6 @@ dependencies = [ "der", ] -[[package]] -name = "spm_precompiled" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" -dependencies = [ - "base64 0.13.1", - "nom", - "serde", - "unicode-segmentation", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7544,12 +6935,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.27.2" @@ -7664,34 +7049,6 @@ dependencies = [ "libc", ] -[[package]] -name = "sysctl" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" -dependencies = [ - "bitflags 2.10.0", - "byteorder", - "enum-as-inner", - "libc", - "thiserror 1.0.69", - "walkdir", -] - -[[package]] -name = "sysctl" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" -dependencies = [ - "bitflags 2.10.0", - "byteorder", - "enum-as-inner", - "libc", - "thiserror 1.0.69", - "walkdir", -] - [[package]] name = "sysinfo" version = "0.37.2" @@ -7816,6 +7173,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.44" @@ -7905,38 +7276,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokenizers" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b08cc37428a476fc9e20ac850132a513a2e1ce32b6a31addf2b74fa7033b905" -dependencies = [ - "aho-corasick", - "derive_builder", - "esaxx-rs", - "getrandom 0.2.16", - "indicatif", - "itertools 0.12.1", - "lazy_static", - "log", - "macro_rules_attribute", - "monostate", - "onig", - "paste", - "rand 0.8.5", - "rayon", - "rayon-cond", - "regex", - "regex-syntax", - "serde", - "serde_json", - "spm_precompiled", - "thiserror 1.0.69", - "unicode-normalization-alignments", - "unicode-segmentation", - "unicode_categories", -] - [[package]] name = "tokio" version = "1.48.0" @@ -8007,7 +7346,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "futures-sink", @@ -8252,27 +7591,6 @@ version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" -[[package]] -name = "ug" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03719c61a91b51541f076dfdba45caacf750b230cefaa4b32d6f5411c3f7f437" -dependencies = [ - "gemm 0.18.2", - "half", - "libloading", - "memmap2", - "num", - "num-traits", - "num_cpus", - "rayon", - "safetensors", - "serde", - "thiserror 1.0.69", - "tracing", - "yoke 0.7.5", -] - [[package]] name = "unarray" version = "0.1.4" @@ -8309,15 +7627,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization-alignments" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" -dependencies = [ - "smallvec", -] - [[package]] name = "unicode-properties" version = "0.1.4" @@ -8348,12 +7657,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "universal-hash" version = "0.6.0-rc.2" @@ -8370,25 +7673,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64 0.22.1", - "flate2", - "log", - "native-tls", - "once_cell", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "url", - "webpki-roots 0.26.11", -] - [[package]] name = "url" version = "2.5.7" @@ -8696,6 +7980,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + [[package]] name = "webpki-root-certs" version = "0.26.11" @@ -8714,15 +8014,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.4", -] - [[package]] name = "webpki-roots" version = "1.0.4" @@ -8732,6 +8023,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wgpu" version = "26.0.1" @@ -8766,8 +8063,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f62f1053bd28c2268f42916f31588f81f64796e2ff91b81293515017ca8bd9" dependencies = [ "arrayvec", - "bit-set 0.8.0", - "bit-vec 0.8.0", + "bit-set", + "bit-vec", "bitflags 2.10.0", "cfg_aliases", "document-features", @@ -8826,7 +8123,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set 0.8.0", + "bit-set", "bitflags 2.10.0", "block", "bytemuck", @@ -9530,8 +8827,8 @@ dependencies = [ "memmap2", "ndk 0.9.0", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -9701,18 +8998,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - [[package]] name = "yoke" version = "0.8.1" @@ -9720,22 +9005,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.1", + "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "yoke-derive" version = "0.8.1" @@ -9828,7 +9101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", - "yoke 0.8.1", + "yoke", "zerofrom", ] @@ -9838,7 +9111,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ - "yoke 0.8.1", + "yoke", "zerofrom", "zerovec-derive", ] @@ -9855,16 +9128,16 @@ dependencies = [ ] [[package]] -name = "zip" -version = "1.1.4" +name = "zune-core" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "displaydoc", - "indexmap", - "num_enum", - "thiserror 1.0.69", + "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 3743093..d1c21a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/lib", "crates/server", "crates/client", "crates/sync-macros"] +members = ["crates/lib", "crates/sync-macros", "crates/app"] resolver = "2" [workspace.package] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml new file mode 100644 index 0000000..126140f --- /dev/null +++ b/crates/app/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +[features] +default = ["desktop"] +desktop = [] # macOS only +ios = [] +headless = [] + +[dependencies] +lib = { path = "../lib" } +bevy = { version = "0.17", default-features = false, features = [ + "bevy_winit", + "bevy_render", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_ui", + "bevy_text", + "png", +] } +bevy_egui = "0.38" +uuid = { version = "1.0", features = ["v4", "serde"] } +anyhow = "1.0" +tokio = { version = "1", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = { version = "1.0", features = ["derive"] } +iroh = { version = "0.95", features = ["discovery-local-network"] } +iroh-gossip = "0.95" +futures-lite = "2.0" +bincode = "1.3" +bytes = "1.0" +crossbeam-channel = "0.5.15" + +[target.'cfg(target_os = "ios")'.dependencies] +objc = "0.2" + +[dev-dependencies] +iroh = { version = "0.95", features = ["discovery-local-network"] } +iroh-gossip = "0.95" +tempfile = "3" +futures-lite = "2.0" +bincode = "1.3" +bytes = "1.0" + +[lib] +name = "app" +crate-type = ["staticlib", "cdylib", "lib"] diff --git a/crates/app/src/camera.rs b/crates/app/src/camera.rs new file mode 100644 index 0000000..18c76bc --- /dev/null +++ b/crates/app/src/camera.rs @@ -0,0 +1,27 @@ +//! Camera configuration +//! +//! This module handles the 3D camera setup for the cube demo. + +use bevy::prelude::*; + +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, setup_camera); + } +} + +/// Set up the 3D camera +/// +/// Camera is positioned at (4, 3, 6) looking at the cube's initial position (0, 0.5, 0). +/// This provides a good viewing angle to see the cube, ground plane, and any movements. +fn setup_camera(mut commands: Commands) { + info!("Setting up camera"); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(4.0, 3.0, 6.0) + .looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y), + )); +} diff --git a/crates/app/src/cube.rs b/crates/app/src/cube.rs new file mode 100644 index 0000000..ce51aaf --- /dev/null +++ b/crates/app/src/cube.rs @@ -0,0 +1,65 @@ +//! Cube entity management + +use bevy::prelude::*; +use lib::{ + networking::{NetworkedEntity, NetworkedTransform, Synced}, + persistence::Persisted, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Marker component for the replicated cube +#[derive(Component, Reflect, Debug, Clone, Copy, Default, Serialize, Deserialize)] +#[reflect(Component)] +pub struct CubeMarker; + +pub struct CubePlugin; + +impl Plugin for CubePlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .add_systems(Startup, spawn_cube); + } +} + +/// Spawn the synced cube on startup +fn spawn_cube( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + node_clock: Option>, +) { + // Wait until NodeVectorClock is available (after networking plugin initializes) + let Some(clock) = node_clock else { + warn!("NodeVectorClock not ready, deferring cube spawn"); + return; + }; + + let entity_id = Uuid::new_v4(); + let node_id = clock.node_id; + + info!("Spawning cube with network ID: {}", entity_id); + + commands.spawn(( + CubeMarker, + // Bevy 3D components + Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::srgb(0.8, 0.3, 0.6), + perceptual_roughness: 0.7, + metallic: 0.3, + ..default() + })), + Transform::from_xyz(0.0, 0.5, 0.0), + GlobalTransform::default(), + // Networking + NetworkedEntity::with_id(entity_id, node_id), + NetworkedTransform, + // Persistence + Persisted::with_id(entity_id), + // Sync marker + Synced, + )); + + info!("Cube spawned successfully"); +} diff --git a/crates/app/src/debug_ui.rs b/crates/app/src/debug_ui.rs new file mode 100644 index 0000000..cc38434 --- /dev/null +++ b/crates/app/src/debug_ui.rs @@ -0,0 +1,89 @@ +//! Debug UI overlay using egui + +use bevy::prelude::*; +use bevy_egui::{egui, EguiContexts, EguiPrimaryContextPass}; +use lib::networking::{GossipBridge, NodeVectorClock}; + +pub struct DebugUiPlugin; + +impl Plugin for DebugUiPlugin { + fn build(&self, app: &mut App) { + app.add_systems(EguiPrimaryContextPass, render_debug_ui); + } +} + +/// Render the debug UI panel +fn render_debug_ui( + mut contexts: EguiContexts, + node_clock: Option>, + gossip_bridge: Option>, + cube_query: Query<(&Transform, &lib::networking::NetworkedEntity), With>, +) { + let Ok(ctx) = contexts.ctx_mut() else { + return; + }; + + egui::Window::new("Debug Info") + .default_pos([10.0, 10.0]) + .default_width(300.0) + .resizable(true) + .show(ctx, |ui| { + ui.heading("Network Status"); + ui.separator(); + + // Node information + if let Some(clock) = &node_clock { + ui.label(format!("Node ID: {}", &clock.node_id.to_string()[..8])); + // Show the current node's clock value (timestamp) + let current_timestamp = clock.clock.clocks.get(&clock.node_id).copied().unwrap_or(0); + ui.label(format!("Clock: {}", current_timestamp)); + ui.label(format!("Known nodes: {}", clock.clock.clocks.len())); + } else { + ui.label("Node: Not initialized"); + } + + ui.add_space(5.0); + + // Gossip bridge status + if let Some(bridge) = &gossip_bridge { + ui.label(format!("Bridge Node: {}", &bridge.node_id().to_string()[..8])); + ui.label("Status: Connected"); + } else { + ui.label("Gossip: Not ready"); + } + + ui.add_space(10.0); + ui.heading("Cube State"); + ui.separator(); + + // Cube information + match cube_query.iter().next() { + Some((transform, networked)) => { + let pos = transform.translation; + ui.label(format!("Position: ({:.2}, {:.2}, {:.2})", pos.x, pos.y, pos.z)); + + let (axis, angle) = transform.rotation.to_axis_angle(); + let angle_deg: f32 = angle.to_degrees(); + ui.label(format!("Rotation: {:.2}° around ({:.2}, {:.2}, {:.2})", + angle_deg, axis.x, axis.y, axis.z)); + + ui.label(format!("Scale: ({:.2}, {:.2}, {:.2})", + transform.scale.x, transform.scale.y, transform.scale.z)); + + ui.add_space(5.0); + ui.label(format!("Network ID: {}", &networked.network_id.to_string()[..8])); + ui.label(format!("Owner: {}", &networked.owner_node_id.to_string()[..8])); + } + None => { + ui.label("Cube: Not spawned yet"); + } + } + + ui.add_space(10.0); + ui.heading("Controls"); + ui.separator(); + ui.label("Left drag: Move cube (XY)"); + ui.label("Right drag: Rotate cube"); + ui.label("Scroll: Move cube (Z)"); + }); +} diff --git a/crates/app/src/input/mouse.rs b/crates/app/src/input/mouse.rs new file mode 100644 index 0000000..226973f --- /dev/null +++ b/crates/app/src/input/mouse.rs @@ -0,0 +1,79 @@ +//! Mouse input handling for macOS + +use bevy::prelude::*; +use bevy::input::mouse::{MouseMotion, MouseWheel}; + +pub struct MouseInputPlugin; + +impl Plugin for MouseInputPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, handle_mouse_input); + } +} + +/// Mouse interaction state +#[derive(Resource, Default)] +struct MouseState { + /// Whether the left mouse button is currently pressed + left_pressed: bool, + /// Whether the right mouse button is currently pressed + right_pressed: bool, +} + +/// Handle mouse input to move and rotate the cube +fn handle_mouse_input( + mouse_buttons: Res>, + mut mouse_motion: EventReader, + mut mouse_wheel: EventReader, + mut mouse_state: Local>, + mut cube_query: Query<&mut Transform, With>, +) { + // Initialize mouse state if needed + if mouse_state.is_none() { + *mouse_state = Some(MouseState::default()); + } + let state = mouse_state.as_mut().unwrap(); + + // Update button states + state.left_pressed = mouse_buttons.pressed(MouseButton::Left); + state.right_pressed = mouse_buttons.pressed(MouseButton::Right); + + // Get total mouse delta this frame + let mut total_delta = Vec2::ZERO; + for motion in mouse_motion.read() { + total_delta += motion.delta; + } + + // Process mouse motion + if total_delta != Vec2::ZERO { + for mut transform in cube_query.iter_mut() { + if state.left_pressed { + // Left drag: Move cube in XY plane + // Scale factor for sensitivity + let sensitivity = 0.01; + transform.translation.x += total_delta.x * sensitivity; + transform.translation.y -= total_delta.y * sensitivity; // Invert Y + } else if state.right_pressed { + // Right drag: Rotate cube + let sensitivity = 0.01; + let rotation_x = Quat::from_rotation_y(total_delta.x * sensitivity); + let rotation_y = Quat::from_rotation_x(-total_delta.y * sensitivity); + transform.rotation = rotation_x * transform.rotation * rotation_y; + } + } + } + + // Process mouse wheel for Z-axis movement + let mut total_scroll = 0.0; + for wheel in mouse_wheel.read() { + total_scroll += wheel.y; + } + + if total_scroll != 0.0 { + for mut transform in cube_query.iter_mut() { + // Scroll: Move in Z axis + let sensitivity = 0.1; + transform.translation.z += total_scroll * sensitivity; + } + } +} diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs new file mode 100644 index 0000000..16fd111 --- /dev/null +++ b/crates/app/src/lib.rs @@ -0,0 +1,12 @@ +//! Replicated cube demo app +//! +//! This app demonstrates real-time CRDT synchronization between iPad and Mac +//! with Apple Pencil input controlling a 3D cube. + +pub mod camera; +pub mod cube; +pub mod debug_ui; +pub mod rendering; +pub mod setup; + +pub use cube::CubeMarker; diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs new file mode 100644 index 0000000..8ea3590 --- /dev/null +++ b/crates/app/src/main.rs @@ -0,0 +1,101 @@ +//! Replicated cube demo - macOS and iPad +//! +//! This demonstrates real-time CRDT synchronization with Apple Pencil input. + +use bevy::prelude::*; +use bevy_egui::EguiPlugin; +use lib::{ + networking::{NetworkingConfig, NetworkingPlugin}, + persistence::{PersistenceConfig, PersistencePlugin}, +}; +use std::path::PathBuf; +use uuid::Uuid; + +mod camera; +mod cube; +mod debug_ui; +mod rendering; +mod setup; + +#[cfg(not(target_os = "ios"))] +mod input { + pub mod mouse; + pub use mouse::MouseInputPlugin; +} + +#[cfg(target_os = "ios")] +mod input { + pub mod pencil; + pub use pencil::PencilInputPlugin; +} + +use camera::*; +use cube::*; +use debug_ui::*; +use input::*; +use rendering::*; +use setup::*; + +fn main() { + // Initialize logging + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("wgpu=error".parse().unwrap()) + .add_directive("naga=warn".parse().unwrap()), + ) + .init(); + + // Create node ID (in production, load from config or generate once) + let node_id = Uuid::new_v4(); + info!("Starting app with node ID: {}", node_id); + + // Database path + let db_path = PathBuf::from("cube_demo.db"); + + // Create Bevy app + App::new() + .add_plugins(DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + title: format!("Replicated Cube Demo - Node {}", &node_id.to_string()[..8]), + resolution: (1280, 720).into(), + ..default() + }), + ..default() + }) + .disable::() // Disable Bevy's logger, using tracing-subscriber instead + ) + .add_plugins(EguiPlugin::default()) + // Networking (bridge will be set up in startup) + .add_plugins(NetworkingPlugin::new(NetworkingConfig { + node_id, + sync_interval_secs: 1.0, + prune_interval_secs: 60.0, + tombstone_gc_interval_secs: 300.0, + })) + // Persistence + .add_plugins(PersistencePlugin::with_config( + db_path, + PersistenceConfig { + flush_interval_secs: 2, + checkpoint_interval_secs: 30, + battery_adaptive: true, + ..Default::default() + }, + )) + // Camera + .add_plugins(CameraPlugin) + // Rendering + .add_plugins(RenderingPlugin) + // Input + .add_plugins(MouseInputPlugin) + // Cube management + .add_plugins(CubePlugin) + // Debug UI + .add_plugins(DebugUiPlugin) + // Gossip networking setup + .add_systems(Startup, setup_gossip_networking) + .add_systems(Update, poll_gossip_bridge) + .run(); +} diff --git a/crates/app/src/rendering.rs b/crates/app/src/rendering.rs new file mode 100644 index 0000000..94090ab --- /dev/null +++ b/crates/app/src/rendering.rs @@ -0,0 +1,53 @@ +//! Lighting and ground plane setup + +use bevy::prelude::*; + +pub struct RenderingPlugin; + +impl Plugin for RenderingPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, setup_lighting_and_ground); + } +} + +/// Set up lighting and ground plane +/// +/// Creates a directional light (sun), ambient light, and a green ground plane. +/// Camera setup is handled separately in the camera module. +fn setup_lighting_and_ground( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + info!("Setting up lighting and ground plane"); + + // Directional light (sun) + commands.spawn(( + DirectionalLight { + illuminance: 10000.0, + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Ambient light + commands.insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 150.0, + affects_lightmapped_meshes: false, + }); + + // Ground plane + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::srgb(0.3, 0.5, 0.3), + perceptual_roughness: 0.9, + ..default() + })), + Transform::from_xyz(0.0, 0.0, 0.0), + )); + + info!("Lighting and ground setup complete"); +} diff --git a/crates/app/src/setup.rs b/crates/app/src/setup.rs new file mode 100644 index 0000000..a6ddcd7 --- /dev/null +++ b/crates/app/src/setup.rs @@ -0,0 +1,279 @@ +//! Gossip networking setup with dedicated tokio runtime +//! +//! This module manages iroh-gossip networking with a tokio runtime running as a sidecar to Bevy. +//! The tokio runtime runs in a dedicated background thread, separate from Bevy's ECS loop. +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────┐ +//! │ Bevy Main Thread │ +//! │ ┌────────────────────────────────┐ │ +//! │ │ setup_gossip_networking() │ │ Startup System +//! │ │ - Creates channel │ │ +//! │ │ - Spawns background thread │ │ +//! │ └────────────────────────────────┘ │ +//! │ ┌────────────────────────────────┐ │ +//! │ │ poll_gossip_bridge() │ │ Update System +//! │ │ - Receives GossipBridge │ │ (runs every frame) +//! │ │ - Inserts as resource │ │ +//! │ └────────────────────────────────┘ │ +//! └─────────────────────────────────────┘ +//! ↕ (crossbeam channel) +//! ┌─────────────────────────────────────┐ +//! │ Background Thread (macOS only) │ +//! │ ┌────────────────────────────────┐ │ +//! │ │ Tokio Runtime │ │ +//! │ │ ┌────────────────────────────┐ │ │ +//! │ │ │ init_gossip() │ │ │ +//! │ │ │ - Creates iroh endpoint │ │ │ +//! │ │ │ - Sets up mDNS discovery │ │ │ +//! │ │ │ - Subscribes to topic │ │ │ +//! │ │ │ - Creates GossipBridge │ │ │ +//! │ │ └────────────────────────────┘ │ │ +//! │ │ ┌────────────────────────────┐ │ │ +//! │ │ │ spawn_bridge_tasks() │ │ │ +//! │ │ │ - Task 1: Forward outgoing │ │ │ +//! │ │ │ - Task 2: Forward incoming │ │ │ +//! │ │ └────────────────────────────┘ │ │ +//! │ └────────────────────────────────┘ │ +//! └─────────────────────────────────────┘ +//! ``` +//! +//! # Communication Pattern +//! +//! 1. **Bevy → Tokio**: GossipBridge's internal queue (try_recv_outgoing) +//! 2. **Tokio → Bevy**: GossipBridge's internal queue (push_incoming) +//! 3. **Thread handoff**: crossbeam_channel (one-time GossipBridge transfer) + +use anyhow::Result; +use bevy::prelude::*; +use lib::networking::GossipBridge; +use uuid::Uuid; + +/// Channel for receiving the GossipBridge from the background thread +/// +/// This resource exists temporarily during startup. Once the GossipBridge +/// is received and inserted, this channel resource is removed. +#[cfg(not(target_os = "ios"))] +#[derive(Resource)] +pub struct GossipBridgeChannel(crossbeam_channel::Receiver); + +/// Set up gossip networking with iroh +/// +/// This is a Bevy startup system that initializes the gossip networking stack. +/// On macOS, it spawns a dedicated thread with a tokio runtime. On iOS, it logs +/// a warning (iOS networking not yet implemented). +/// +/// # Platform Support +/// +/// - **macOS**: Full support with mDNS discovery +/// - **iOS**: Not yet implemented +pub fn setup_gossip_networking(mut commands: Commands) { + info!("Setting up gossip networking..."); + + // Spawn dedicated thread with Tokio runtime for gossip initialization + #[cfg(not(target_os = "ios"))] + { + let (sender, receiver) = crossbeam_channel::unbounded(); + commands.insert_resource(GossipBridgeChannel(receiver)); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + match init_gossip().await { + Ok(bridge) => { + info!("Gossip bridge initialized successfully"); + if let Err(e) = sender.send(bridge) { + error!("Failed to send bridge to main thread: {}", e); + } + } + Err(e) => { + error!("Failed to initialize gossip: {}", e); + } + } + }); + }); + } + + #[cfg(target_os = "ios")] + { + warn!("iOS networking not yet implemented - gossip networking disabled"); + } +} + +/// Poll the channel for the GossipBridge and insert it when ready +/// +/// This is a Bevy update system that runs every frame. It checks the channel +/// for the GossipBridge created in the background thread. Once received, it +/// inserts the bridge as a resource and removes the channel. +/// +/// # Platform Support +/// +/// - **macOS**: Polls the channel and inserts GossipBridge +/// - **iOS**: No-op (networking not implemented) +pub fn poll_gossip_bridge( + mut commands: Commands, + #[cfg(not(target_os = "ios"))] + channel: Option>, +) { + #[cfg(not(target_os = "ios"))] + if let Some(channel) = channel { + if let Ok(bridge) = channel.0.try_recv() { + info!("Inserting GossipBridge resource into Bevy world"); + commands.insert_resource(bridge); + commands.remove_resource::(); + } + } +} + +/// Initialize iroh-gossip networking stack +/// +/// This async function runs in the background tokio runtime and: +/// 1. Creates an iroh endpoint with mDNS discovery +/// 2. Spawns the gossip protocol +/// 3. Sets up the router to accept gossip connections +/// 4. Subscribes to a shared topic (ID: [42; 32]) +/// 5. Waits for join with a 2-second timeout +/// 6. Creates and configures the GossipBridge +/// 7. Spawns forwarding tasks to bridge messages +/// +/// # Returns +/// +/// - `Ok(GossipBridge)`: Successfully initialized networking +/// - `Err(anyhow::Error)`: Initialization failed +/// +/// # Platform Support +/// +/// This function is only compiled on non-iOS platforms. +#[cfg(not(target_os = "ios"))] +async fn init_gossip() -> Result { + use iroh::discovery::mdns::MdnsDiscovery; + use iroh::protocol::Router; + use iroh::Endpoint; + use iroh_gossip::net::Gossip; + use iroh_gossip::proto::TopicId; + + info!("Creating endpoint with mDNS discovery..."); + let endpoint = Endpoint::builder() + .discovery(MdnsDiscovery::builder()) + .bind() + .await?; + + let endpoint_id = endpoint.addr().id; + info!("Endpoint created: {}", endpoint_id); + + // Convert endpoint ID to UUID + let id_bytes = endpoint_id.as_bytes(); + let mut uuid_bytes = [0u8; 16]; + uuid_bytes.copy_from_slice(&id_bytes[..16]); + let node_id = Uuid::from_bytes(uuid_bytes); + + info!("Spawning gossip protocol..."); + let gossip = Gossip::builder().spawn(endpoint.clone()); + + info!("Setting up router..."); + let router = Router::builder(endpoint.clone()) + .accept(iroh_gossip::ALPN, gossip.clone()) + .spawn(); + + // Subscribe to shared topic + let topic_id = TopicId::from_bytes([42; 32]); + info!("Subscribing to topic..."); + let subscribe_handle = gossip.subscribe(topic_id, vec![]).await?; + + let (sender, mut receiver) = subscribe_handle.split(); + + // Wait for join (with timeout since we might be the first node) + info!("Waiting for gossip join..."); + match tokio::time::timeout(std::time::Duration::from_secs(2), receiver.joined()).await { + Ok(Ok(())) => info!("Joined gossip swarm"), + Ok(Err(e)) => warn!("Join error: {} (proceeding anyway)", e), + Err(_) => info!("Join timeout (first node in swarm)"), + } + + // Create bridge + let bridge = GossipBridge::new(node_id); + info!("GossipBridge created with node ID: {}", node_id); + + // Spawn forwarding tasks - pass endpoint, router, gossip to keep them alive + spawn_bridge_tasks(sender, receiver, bridge.clone(), endpoint, router, gossip); + + Ok(bridge) +} + +/// Spawn tokio tasks to forward messages between iroh-gossip and GossipBridge +/// +/// This function spawns two concurrent tokio tasks that run for the lifetime of the application: +/// +/// 1. **Outgoing Task**: Polls GossipBridge for outgoing messages and broadcasts them via gossip +/// 2. **Incoming Task**: Receives messages from gossip and pushes them into GossipBridge +/// +/// # Lifetime Management +/// +/// The iroh resources (endpoint, router, gossip) are moved into the first task to keep them +/// alive for the application lifetime. Without this, they would be dropped immediately and +/// the gossip connection would close. +/// +/// # Platform Support +/// +/// This function is only compiled on non-iOS platforms. +#[cfg(not(target_os = "ios"))] +fn spawn_bridge_tasks( + sender: iroh_gossip::api::GossipSender, + mut receiver: iroh_gossip::api::GossipReceiver, + bridge: GossipBridge, + _endpoint: iroh::Endpoint, + _router: iroh::protocol::Router, + _gossip: iroh_gossip::net::Gossip, +) { + use bytes::Bytes; + use futures_lite::StreamExt; + use lib::networking::VersionedMessage; + use std::time::Duration; + + let node_id = bridge.node_id(); + + // Task 1: Forward outgoing messages from GossipBridge → iroh-gossip + // Keep endpoint, router, gossip alive by moving them into this task + let bridge_out = bridge.clone(); + tokio::spawn(async move { + let _endpoint = _endpoint; // Keep alive for app lifetime + let _router = _router; // Keep alive for app lifetime + let _gossip = _gossip; // Keep alive for app lifetime + + loop { + if let Some(msg) = bridge_out.try_recv_outgoing() { + if let Ok(bytes) = bincode::serialize(&msg) { + if let Err(e) = sender.broadcast(Bytes::from(bytes)).await { + error!("[Node {}] Broadcast failed: {}", node_id, e); + } + } + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }); + + // Task 2: Forward incoming messages from iroh-gossip → GossipBridge + let bridge_in = bridge.clone(); + tokio::spawn(async move { + loop { + match tokio::time::timeout(Duration::from_millis(100), receiver.next()).await { + Ok(Some(Ok(event))) => { + if let iroh_gossip::api::Event::Received(msg) = event { + if let Ok(versioned_msg) = + bincode::deserialize::(&msg.content) + { + if let Err(e) = bridge_in.push_incoming(versioned_msg) { + error!("[Node {}] Push incoming failed: {}", node_id, e); + } + } + } + } + Ok(Some(Err(e))) => error!("[Node {}] Receiver error: {}", node_id, e), + Ok(None) => break, + Err(_) => {} // Timeout + } + } + }); +} diff --git a/crates/app/tests/cube_sync_headless.rs b/crates/app/tests/cube_sync_headless.rs new file mode 100644 index 0000000..dd7eb27 --- /dev/null +++ b/crates/app/tests/cube_sync_headless.rs @@ -0,0 +1,471 @@ +//! Headless integration tests for cube synchronization +//! +//! These tests validate end-to-end CRDT synchronization of the cube entity +//! using multiple headless Bevy apps with real iroh-gossip networking. + +use std::{ + path::PathBuf, + time::{Duration, Instant}, +}; + +use anyhow::Result; +use app::CubeMarker; +use bevy::{ + app::{App, ScheduleRunnerPlugin}, + ecs::world::World, + prelude::*, + MinimalPlugins, +}; +use bytes::Bytes; +use futures_lite::StreamExt; +use iroh::{protocol::Router, Endpoint}; +use iroh_gossip::{ + api::{GossipReceiver, GossipSender}, + net::Gossip, + proto::TopicId, +}; +use lib::{ + networking::{ + GossipBridge, NetworkedEntity, NetworkedTransform, NetworkingConfig, NetworkingPlugin, + Synced, VersionedMessage, + }, + persistence::{Persisted, PersistenceConfig, PersistencePlugin}, +}; +use tempfile::TempDir; +use uuid::Uuid; + +// ============================================================================ +// Test Utilities +// ============================================================================ + +mod test_utils { + use super::*; + + /// Test context that manages temporary directories with RAII cleanup + pub struct TestContext { + _temp_dir: TempDir, + db_path: PathBuf, + } + + impl TestContext { + pub fn new() -> Self { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let db_path = temp_dir.path().join("test.db"); + Self { + _temp_dir: temp_dir, + db_path, + } + } + + pub fn db_path(&self) -> PathBuf { + self.db_path.clone() + } + } + + /// Create a headless Bevy app configured for cube testing + pub fn create_test_app(node_id: Uuid, db_path: PathBuf, bridge: GossipBridge) -> App { + let mut app = App::new(); + + app.add_plugins( + MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( + 1.0 / 60.0, + ))), + ) + .insert_resource(bridge) + .add_plugins(NetworkingPlugin::new(NetworkingConfig { + node_id, + sync_interval_secs: 0.5, // Fast for testing + prune_interval_secs: 10.0, + tombstone_gc_interval_secs: 30.0, + })) + .add_plugins(PersistencePlugin::with_config( + db_path, + PersistenceConfig { + flush_interval_secs: 1, + checkpoint_interval_secs: 5, + battery_adaptive: false, + ..Default::default() + }, + )); + + // Register cube component types for reflection + app.register_type::(); + + app + } + + /// Count entities with CubeMarker component + pub fn count_cubes(world: &mut World) -> usize { + let mut query = world.query::<&CubeMarker>(); + query.iter(world).count() + } + + /// Count entities with a specific network ID + pub fn count_entities_with_id(world: &mut World, network_id: Uuid) -> usize { + let mut query = world.query::<&NetworkedEntity>(); + query + .iter(world) + .filter(|entity| entity.network_id == network_id) + .count() + } + + /// Wait for sync condition to be met, polling both apps + pub async fn wait_for_sync( + app1: &mut App, + app2: &mut App, + timeout: Duration, + check_fn: F, + ) -> Result<()> + where + F: Fn(&mut World, &mut World) -> bool, + { + let start = Instant::now(); + let mut tick_count = 0; + + while start.elapsed() < timeout { + // Tick both apps + app1.update(); + app2.update(); + tick_count += 1; + + if tick_count % 50 == 0 { + println!( + "Waiting for sync... tick {} ({:.1}s elapsed)", + tick_count, + start.elapsed().as_secs_f32() + ); + } + + // Check condition + if check_fn(app1.world_mut(), app2.world_mut()) { + println!( + "Sync completed after {} ticks ({:.3}s)", + tick_count, + start.elapsed().as_secs_f32() + ); + return Ok(()); + } + + // Small delay to avoid spinning + tokio::time::sleep(Duration::from_millis(16)).await; + } + + println!("Sync timeout after {} ticks", tick_count); + anyhow::bail!("Sync timeout after {:?}. Condition not met.", timeout) + } + + /// Initialize a single iroh-gossip node + async fn init_gossip_node( + topic_id: TopicId, + bootstrap_addrs: Vec, + ) -> Result<(Endpoint, Gossip, Router, GossipBridge)> { + println!(" Creating endpoint with mDNS discovery..."); + let endpoint = Endpoint::builder() + .discovery(iroh::discovery::mdns::MdnsDiscovery::builder()) + .bind() + .await?; + let endpoint_id = endpoint.addr().id; + println!(" Endpoint created: {}", endpoint_id); + + // Convert 32-byte endpoint ID to 16-byte UUID + let id_bytes = endpoint_id.as_bytes(); + let mut uuid_bytes = [0u8; 16]; + uuid_bytes.copy_from_slice(&id_bytes[..16]); + let node_id = Uuid::from_bytes(uuid_bytes); + + println!(" Spawning gossip protocol..."); + let gossip = Gossip::builder().spawn(endpoint.clone()); + + println!(" Setting up router..."); + let router = Router::builder(endpoint.clone()) + .accept(iroh_gossip::ALPN, gossip.clone()) + .spawn(); + + let bootstrap_count = bootstrap_addrs.len(); + let has_bootstrap_peers = !bootstrap_addrs.is_empty(); + + let bootstrap_ids: Vec<_> = bootstrap_addrs.iter().map(|a| a.id).collect(); + + if has_bootstrap_peers { + let static_provider = iroh::discovery::static_provider::StaticProvider::default(); + for addr in &bootstrap_addrs { + static_provider.add_endpoint_info(addr.clone()); + } + endpoint.discovery().add(static_provider); + println!( + " Added {} bootstrap peers to static discovery", + bootstrap_count + ); + + println!(" Connecting to bootstrap peers..."); + for addr in &bootstrap_addrs { + match endpoint.connect(addr.clone(), iroh_gossip::ALPN).await { + Ok(_conn) => println!(" ✓ Connected to bootstrap peer: {}", addr.id), + Err(e) => { + println!(" ✗ Failed to connect to bootstrap peer {}: {}", addr.id, e) + } + } + } + } + + println!( + " Subscribing to topic with {} bootstrap peers...", + bootstrap_count + ); + let subscribe_handle = gossip.subscribe(topic_id, bootstrap_ids).await?; + + println!(" Splitting sender/receiver..."); + let (sender, mut receiver) = subscribe_handle.split(); + + if has_bootstrap_peers { + println!(" Waiting for join to complete (with timeout)..."); + match tokio::time::timeout(Duration::from_secs(3), receiver.joined()).await { + Ok(Ok(())) => println!(" Join completed!"), + Ok(Err(e)) => println!(" Join error: {}", e), + Err(_) => { + println!(" Join timeout - proceeding anyway (mDNS may still connect later)") + } + } + } else { + println!(" No bootstrap peers - skipping join wait (first node in swarm)"); + } + + let bridge = GossipBridge::new(node_id); + println!(" Spawning bridge tasks..."); + + spawn_gossip_bridge_tasks(sender, receiver, bridge.clone()); + + println!(" Node initialization complete"); + Ok((endpoint, gossip, router, bridge)) + } + + /// Setup a pair of iroh-gossip nodes connected to the same topic + pub async fn setup_gossip_pair() -> Result<( + Endpoint, + Endpoint, + Router, + Router, + GossipBridge, + GossipBridge, + )> { + let topic_id = TopicId::from_bytes([42; 32]); + println!("Using topic ID: {:?}", topic_id); + + println!("Initializing node 1..."); + let (ep1, _gossip1, router1, bridge1) = init_gossip_node(topic_id, vec![]).await?; + println!("Node 1 initialized with ID: {}", ep1.addr().id); + + let node1_addr = ep1.addr().clone(); + println!("Node 1 full address: {:?}", node1_addr); + + println!("Initializing node 2 with bootstrap peer: {}", node1_addr.id); + let (ep2, _gossip2, router2, bridge2) = + init_gossip_node(topic_id, vec![node1_addr]).await?; + println!("Node 2 initialized with ID: {}", ep2.addr().id); + + println!("Waiting for mDNS/gossip peer discovery..."); + tokio::time::sleep(Duration::from_secs(2)).await; + println!("Peer discovery wait complete"); + + Ok((ep1, ep2, router1, router2, bridge1, bridge2)) + } + + /// Spawn background tasks to forward messages between iroh-gossip and GossipBridge + fn spawn_gossip_bridge_tasks( + sender: GossipSender, + mut receiver: GossipReceiver, + bridge: GossipBridge, + ) { + let node_id = bridge.node_id(); + + // Task 1: Forward from bridge.outgoing → gossip sender + let bridge_out = bridge.clone(); + tokio::spawn(async move { + let mut msg_count = 0; + loop { + if let Some(versioned_msg) = bridge_out.try_recv_outgoing() { + msg_count += 1; + println!( + "[Node {}] Sending message #{} via gossip", + node_id, msg_count + ); + match bincode::serialize(&versioned_msg) { + Ok(bytes) => { + if let Err(e) = sender.broadcast(Bytes::from(bytes)).await { + eprintln!("[Node {}] Failed to broadcast message: {}", node_id, e); + } else { + println!( + "[Node {}] Message #{} broadcasted successfully", + node_id, msg_count + ); + } + } + Err(e) => eprintln!( + "[Node {}] Failed to serialize message for broadcast: {}", + node_id, e + ), + } + } + + tokio::time::sleep(Duration::from_millis(10)).await; + } + }); + + // Task 2: Forward from gossip receiver → bridge.incoming + let bridge_in = bridge.clone(); + tokio::spawn(async move { + let mut msg_count = 0; + println!("[Node {}] Gossip receiver task started", node_id); + loop { + match tokio::time::timeout(Duration::from_millis(100), receiver.next()).await { + Ok(Some(Ok(event))) => { + println!( + "[Node {}] Received gossip event: {:?}", + node_id, + std::mem::discriminant(&event) + ); + if let iroh_gossip::api::Event::Received(msg) = event { + msg_count += 1; + println!( + "[Node {}] Received message #{} from gossip", + node_id, msg_count + ); + match bincode::deserialize::(&msg.content) { + Ok(versioned_msg) => { + if let Err(e) = bridge_in.push_incoming(versioned_msg) { + eprintln!( + "[Node {}] Failed to push to bridge incoming: {}", + node_id, e + ); + } else { + println!( + "[Node {}] Message #{} pushed to bridge incoming", + node_id, msg_count + ); + } + } + Err(e) => eprintln!( + "[Node {}] Failed to deserialize gossip message: {}", + node_id, e + ), + } + } + } + Ok(Some(Err(e))) => { + eprintln!("[Node {}] Gossip receiver error: {}", node_id, e) + } + Ok(None) => { + println!("[Node {}] Gossip stream ended", node_id); + break; + } + Err(_) => { + // Timeout, no message available + } + } + } + }); + } +} + +// ============================================================================ +// Integration Tests +// ============================================================================ + +/// Test: Basic cube spawn and sync (Node A spawns → Node B receives) +#[tokio::test(flavor = "multi_thread")] +async fn test_cube_spawn_and_sync() -> Result<()> { + use test_utils::*; + + println!("=== Starting test_cube_spawn_and_sync ==="); + + // Setup contexts + println!("Creating test contexts..."); + let ctx1 = TestContext::new(); + let ctx2 = TestContext::new(); + + // Setup gossip networking + println!("Setting up gossip pair..."); + let (ep1, ep2, router1, router2, bridge1, bridge2) = setup_gossip_pair().await?; + + let node1_id = bridge1.node_id(); + let node2_id = bridge2.node_id(); + + // Create headless apps + println!("Creating Bevy apps..."); + let mut app1 = create_test_app(node1_id, ctx1.db_path(), bridge1); + let mut app2 = create_test_app(node2_id, ctx2.db_path(), bridge2); + println!("Apps created successfully"); + + println!("Node 1 ID: {}", node1_id); + println!("Node 2 ID: {}", node2_id); + + // Node 1 spawns cube + let entity_id = Uuid::new_v4(); + println!("Spawning cube {} on node 1", entity_id); + let spawned_entity = app1 + .world_mut() + .spawn(( + CubeMarker, + Transform::from_xyz(1.0, 2.0, 3.0), + GlobalTransform::default(), + NetworkedEntity::with_id(entity_id, node1_id), + NetworkedTransform, + Persisted::with_id(entity_id), + Synced, + )) + .id(); + + // IMPORTANT: Trigger change detection for persistence + // Bevy only marks components as "changed" when mutated, not on spawn + { + let world = app1.world_mut(); + if let Ok(mut entity_mut) = world.get_entity_mut(spawned_entity) { + if let Some(mut persisted) = entity_mut.get_mut::() { + // Dereferencing the mutable borrow triggers change detection + let _ = &mut *persisted; + } + } + } + println!("Cube spawned, triggered persistence"); + + println!("Cube spawned, starting sync wait..."); + + // Wait for cube to sync to node 2 + wait_for_sync(&mut app1, &mut app2, Duration::from_secs(10), |_, w2| { + count_entities_with_id(w2, entity_id) > 0 + }) + .await?; + + println!("Cube synced to node 2!"); + + // Verify cube exists on node 2 with correct Transform + let cube_transform = app2 + .world_mut() + .query_filtered::<&Transform, With>() + .single(app2.world()) + .expect("Cube should exist on node 2"); + + assert!( + (cube_transform.translation.x - 1.0).abs() < 0.01, + "Transform.x should be 1.0" + ); + assert!( + (cube_transform.translation.y - 2.0).abs() < 0.01, + "Transform.y should be 2.0" + ); + assert!( + (cube_transform.translation.z - 3.0).abs() < 0.01, + "Transform.z should be 3.0" + ); + + println!("Transform verified!"); + + // Cleanup + router1.shutdown().await?; + router2.shutdown().await?; + drop(ep1); + drop(ep2); + + println!("=== Test completed successfully ==="); + Ok(()) +} diff --git a/crates/client/.gitignore b/crates/client/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/crates/client/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml deleted file mode 100644 index 3d0fd81..0000000 --- a/crates/client/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "client" -version = "0.1.0" -edition.workspace = true - -[[bin]] -name = "client" -path = "src/main.rs" - -[dependencies] -# Bevy -bevy = { version = "0.17", default-features = false, features = [ - "bevy_winit", - "bevy_render", - "bevy_core_pipeline", - "bevy_sprite", - "bevy_ui", - "bevy_text", - "png", - "x11", -] } - -# Iroh - P2P networking and gossip -iroh = { workspace = true } -iroh-gossip = { workspace = true } - -# Async runtime -tokio = { version = "1", features = ["full"] } - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# Error handling -thiserror = "2.0" -anyhow = "1.0" - -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# Local dependencies -lib = { path = "../lib" } diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs deleted file mode 100644 index b93cf3f..0000000 --- a/crates/client/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs deleted file mode 100644 index 925112e..0000000 --- a/crates/client/src/main.rs +++ /dev/null @@ -1,24 +0,0 @@ -use bevy::prelude::*; -use tracing::info; - -fn main() { - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); - - // Start Bevy app - App::new() - .add_plugins(DefaultPlugins) - .add_systems(Startup, setup) - .add_systems(Update, sync_system) - .run(); -} - -fn setup(mut commands: Commands) { - commands.spawn(Camera2d); - info!("Client started"); -} - -fn sync_system() { - // TODO: Implement gossip sync for client -} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml deleted file mode 100644 index 55787eb..0000000 --- a/crates/server/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "server" -version = "0.1.0" -edition.workspace = true - -[[bin]] -name = "server" -path = "src/main.rs" - -[dependencies] -# Bevy (headless) -bevy = { version = "0.17", default-features = false, features = [ - "bevy_state", -] } - -# Iroh - P2P networking and gossip -iroh = { workspace = true } -iroh-gossip = { workspace = true } - -# Async runtime -tokio = { version = "1", features = ["full"] } -tokio-stream = "0.1" -futures-lite = "2.5" - -# Database -rusqlite = { version = "0.37.0", features = ["bundled", "column_decltype", "load_extension"] } - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.9" - -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# Error handling -thiserror = "2.0" -anyhow = "1.0" - -# Date/time -chrono = { version = "0.4", features = ["serde"] } - -# Random number generation -rand = "0.8" - -# ML/AI - Candle for inference (using newer versions with better compatibility) -candle-core = "0.8" -candle-nn = "0.8" -candle-transformers = "0.8" -tokenizers = "0.20" -hf-hub = "0.3" - -# Synchronization -parking_lot = { workspace = true } - -# Local dependencies -lib = { path = "../lib" } diff --git a/crates/server/src/assets/mod.rs b/crates/server/src/assets/mod.rs deleted file mode 100644 index a44d83c..0000000 --- a/crates/server/src/assets/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Asset loading and management will go here diff --git a/crates/server/src/components/database.rs b/crates/server/src/components/database.rs deleted file mode 100644 index e4913a6..0000000 --- a/crates/server/src/components/database.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::sync::Arc; - -use bevy::prelude::*; -use parking_lot::Mutex; -use rusqlite::Connection; - -use crate::config::Config; - -/// Bevy resource wrapping application configuration -#[derive(Resource)] -pub struct AppConfig(pub Config); - -/// Bevy resource wrapping database connection -#[derive(Resource)] -pub struct Database(pub Arc>); diff --git a/crates/server/src/components/gossip.rs b/crates/server/src/components/gossip.rs deleted file mode 100644 index 32e0475..0000000 --- a/crates/server/src/components/gossip.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::sync::Arc; - -use bevy::prelude::*; -use iroh::{ - Endpoint, - protocol::Router, -}; -use iroh_gossip::{ - api::{ - GossipReceiver, - GossipSender, - }, - net::Gossip, - proto::TopicId, -}; -use parking_lot::Mutex; -use serde::{ - Deserialize, - Serialize, -}; - -/// Message envelope for gossip sync -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SyncMessage { - /// The actual message from iMessage - pub message: lib::Message, - /// Timestamp when this was published to gossip - pub sync_timestamp: i64, - /// ID of the node that published this - pub publisher_node_id: String, -} - -/// Bevy resource wrapping the gossip handle -#[derive(Resource, Clone)] -pub struct IrohGossipHandle { - pub gossip: Gossip, -} - -/// Bevy resource wrapping the gossip sender -#[derive(Resource)] -pub struct IrohGossipSender { - pub sender: Arc>, -} - -/// Bevy resource wrapping the gossip receiver -#[derive(Resource)] -pub struct IrohGossipReceiver { - pub receiver: Arc>, -} - -/// Bevy resource with Iroh router -#[derive(Resource)] -pub struct IrohRouter { - pub router: Router, -} - -/// Bevy resource with Iroh endpoint -#[derive(Resource, Clone)] -pub struct IrohEndpoint { - pub endpoint: Endpoint, - pub node_id: String, -} - -/// Bevy resource for gossip topic ID -#[derive(Resource)] -pub struct GossipTopic(pub TopicId); - -/// Bevy resource for tracking gossip initialization task -#[derive(Resource)] -pub struct GossipInitTask( - pub bevy::tasks::Task>, -); - -/// Bevy message: a new message that needs to be published to gossip -#[derive(Message, Clone, Debug)] -pub struct PublishMessageEvent { - pub message: lib::Message, -} - -/// Bevy message: a message received from gossip that needs to be saved to -/// SQLite -#[derive(Message, Clone, Debug)] -pub struct GossipMessageReceived { - pub sync_message: SyncMessage, -} - -/// Helper to serialize a sync message -pub fn serialize_sync_message(msg: &SyncMessage) -> anyhow::Result> { - Ok(serde_json::to_vec(msg)?) -} - -/// Helper to deserialize a sync message -pub fn deserialize_sync_message(data: &[u8]) -> anyhow::Result { - Ok(serde_json::from_slice(data)?) -} diff --git a/crates/server/src/components/mod.rs b/crates/server/src/components/mod.rs deleted file mode 100644 index 16be00a..0000000 --- a/crates/server/src/components/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod database; -pub mod gossip; - -pub use database::*; -pub use gossip::*; diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs deleted file mode 100644 index 0a5ee2a..0000000 --- a/crates/server/src/config.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::{ - fs, - path::Path, -}; - -use anyhow::{ - Context, - Result, -}; -use serde::{ - Deserialize, - Serialize, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - pub database: DatabaseConfig, - pub services: ServicesConfig, - pub models: ModelsConfig, - pub tailscale: TailscaleConfig, - pub grpc: GrpcConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DatabaseConfig { - pub path: String, - pub chat_db_path: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ServicesConfig { - pub poll_interval_ms: u64, - pub training_set_sample_rate: f64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelsConfig { - pub embedding_model: String, - pub emotion_model: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TailscaleConfig { - pub hostname: String, - pub state_dir: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GrpcConfig { - pub port: u16, -} - -impl Config { - pub fn from_file>(path: P) -> Result { - let content = fs::read_to_string(path.as_ref()) - .context(format!("Failed to read config file: {:?}", path.as_ref()))?; - let config: Config = toml::from_str(&content).context("Failed to parse config file")?; - Ok(config) - } - - pub fn default_config() -> Self { - Self { - database: DatabaseConfig { - path: "./us.db".to_string(), - chat_db_path: "./crates/lib/chat.db".to_string(), - }, - services: ServicesConfig { - poll_interval_ms: 1000, - training_set_sample_rate: 0.05, - }, - models: ModelsConfig { - embedding_model: "Qwen/Qwen3-Embedding-0.6B".to_string(), - emotion_model: "SamLowe/roberta-base-go_emotions".to_string(), - }, - tailscale: TailscaleConfig { - hostname: "lonni-daemon".to_string(), - state_dir: "./tailscale-state".to_string(), - }, - grpc: GrpcConfig { port: 50051 }, - } - } - - pub fn save>(&self, path: P) -> Result<()> { - let content = toml::to_string_pretty(self).context("Failed to serialize config")?; - fs::write(path.as_ref(), content) - .context(format!("Failed to write config file: {:?}", path.as_ref()))?; - Ok(()) - } -} diff --git a/crates/server/src/db/mod.rs b/crates/server/src/db/mod.rs deleted file mode 100644 index be92c6e..0000000 --- a/crates/server/src/db/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod operations; -pub mod schema; - -pub use operations::*; -pub use schema::*; diff --git a/crates/server/src/db/operations.rs b/crates/server/src/db/operations.rs deleted file mode 100644 index 97ddf2c..0000000 --- a/crates/server/src/db/operations.rs +++ /dev/null @@ -1,339 +0,0 @@ -use chrono::{ - TimeZone, - Utc, -}; -use rusqlite::{ - Connection, - OptionalExtension, - Result, - Row, - params, -}; - -use crate::{ - db::schema::{ - deserialize_embedding, - serialize_embedding, - }, - models::*, -}; - -/// Insert a new message into the database -pub fn insert_message(conn: &Connection, msg: &lib::Message) -> Result { - let timestamp = msg.date.map(|dt| dt.timestamp()); - let created_at = Utc::now().timestamp(); - - conn.execute( - "INSERT INTO messages (chat_db_rowid, text, timestamp, is_from_me, created_at) - VALUES (?1, ?2, ?3, ?4, ?5) - ON CONFLICT(chat_db_rowid) DO NOTHING", - params![msg.rowid, msg.text, timestamp, msg.is_from_me, created_at], - )?; - - Ok(conn.last_insert_rowid()) -} - -/// Get message ID by chat.db rowid -pub fn get_message_id_by_chat_rowid(conn: &Connection, chat_db_rowid: i64) -> Result> { - conn.query_row( - "SELECT id FROM messages WHERE chat_db_rowid = ?1", - params![chat_db_rowid], - |row| row.get(0), - ) - .optional() -} - -/// Get message by ID -pub fn get_message(conn: &Connection, id: i64) -> Result { - conn.query_row( - "SELECT id, chat_db_rowid, text, timestamp, is_from_me, created_at FROM messages WHERE id = ?1", - params![id], - map_message_row, - ) -} - -fn map_message_row(row: &Row) -> Result { - let timestamp: Option = row.get(3)?; - let created_at: i64 = row.get(5)?; - - Ok(Message { - id: row.get(0)?, - chat_db_rowid: row.get(1)?, - text: row.get(2)?, - timestamp: timestamp.map(|ts| Utc.timestamp_opt(ts, 0).unwrap()), - is_from_me: row.get(4)?, - created_at: Utc.timestamp_opt(created_at, 0).unwrap(), - }) -} - -/// Insert message embedding -pub fn insert_message_embedding( - conn: &Connection, - message_id: i64, - embedding: &[f32], - model_name: &str, -) -> Result { - let embedding_bytes = serialize_embedding(embedding); - let created_at = Utc::now().timestamp(); - - conn.execute( - "INSERT INTO message_embeddings (message_id, embedding, model_name, created_at) - VALUES (?1, ?2, ?3, ?4)", - params![message_id, embedding_bytes, model_name, created_at], - )?; - - Ok(conn.last_insert_rowid()) -} - -/// Get message embedding -pub fn get_message_embedding( - conn: &Connection, - message_id: i64, -) -> Result> { - conn.query_row( - "SELECT id, message_id, embedding, model_name, created_at - FROM message_embeddings WHERE message_id = ?1", - params![message_id], - |row| { - let embedding_bytes: Vec = row.get(2)?; - let created_at: i64 = row.get(4)?; - - Ok(MessageEmbedding { - id: row.get(0)?, - message_id: row.get(1)?, - embedding: deserialize_embedding(&embedding_bytes), - model_name: row.get(3)?, - created_at: Utc.timestamp_opt(created_at, 0).unwrap(), - }) - }, - ) - .optional() -} - -/// Insert or get word embedding -pub fn insert_word_embedding( - conn: &Connection, - word: &str, - embedding: &[f32], - model_name: &str, -) -> Result { - let embedding_bytes = serialize_embedding(embedding); - let created_at = Utc::now().timestamp(); - - conn.execute( - "INSERT INTO word_embeddings (word, embedding, model_name, created_at) - VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(word) DO NOTHING", - params![word, embedding_bytes, model_name, created_at], - )?; - - Ok(conn.last_insert_rowid()) -} - -/// Get word embedding -pub fn get_word_embedding(conn: &Connection, word: &str) -> Result> { - conn.query_row( - "SELECT id, word, embedding, model_name, created_at - FROM word_embeddings WHERE word = ?1", - params![word], - |row| { - let embedding_bytes: Vec = row.get(2)?; - let created_at: i64 = row.get(4)?; - - Ok(WordEmbedding { - id: row.get(0)?, - word: row.get(1)?, - embedding: deserialize_embedding(&embedding_bytes), - model_name: row.get(3)?, - created_at: Utc.timestamp_opt(created_at, 0).unwrap(), - }) - }, - ) - .optional() -} - -/// Insert emotion classification -pub fn insert_emotion( - conn: &Connection, - message_id: i64, - emotion: &str, - confidence: f64, - model_version: &str, -) -> Result { - let now = Utc::now().timestamp(); - - conn.execute( - "INSERT INTO emotions (message_id, emotion, confidence, model_version, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![message_id, emotion, confidence, model_version, now, now], - )?; - - Ok(conn.last_insert_rowid()) -} - -/// Update emotion classification -pub fn update_emotion( - conn: &Connection, - message_id: i64, - emotion: &str, - confidence: f64, -) -> Result<()> { - let updated_at = Utc::now().timestamp(); - - conn.execute( - "UPDATE emotions SET emotion = ?1, confidence = ?2, updated_at = ?3 - WHERE message_id = ?4", - params![emotion, confidence, updated_at, message_id], - )?; - - Ok(()) -} - -/// Get emotion by message ID -pub fn get_emotion_by_message_id(conn: &Connection, message_id: i64) -> Result> { - conn.query_row( - "SELECT id, message_id, emotion, confidence, model_version, created_at, updated_at - FROM emotions WHERE message_id = ?1", - params![message_id], - map_emotion_row, - ) - .optional() -} - -/// Get emotion by ID -pub fn get_emotion_by_id(conn: &Connection, id: i64) -> Result> { - conn.query_row( - "SELECT id, message_id, emotion, confidence, model_version, created_at, updated_at - FROM emotions WHERE id = ?1", - params![id], - map_emotion_row, - ) - .optional() -} - -/// List all emotions with optional filters -pub fn list_emotions( - conn: &Connection, - emotion_filter: Option<&str>, - min_confidence: Option, - limit: Option, - offset: Option, -) -> Result> { - let mut query = String::from( - "SELECT id, message_id, emotion, confidence, model_version, created_at, updated_at - FROM emotions WHERE 1=1", - ); - - if emotion_filter.is_some() { - query.push_str(" AND emotion = ?1"); - } - - if min_confidence.is_some() { - query.push_str(" AND confidence >= ?2"); - } - - query.push_str(" ORDER BY created_at DESC"); - - if limit.is_some() { - query.push_str(" LIMIT ?3"); - } - - if offset.is_some() { - query.push_str(" OFFSET ?4"); - } - - let mut stmt = conn.prepare(&query)?; - let emotions = stmt - .query_map( - params![ - emotion_filter.unwrap_or(""), - min_confidence.unwrap_or(0.0), - limit.unwrap_or(1000), - offset.unwrap_or(0), - ], - map_emotion_row, - )? - .collect::>>()?; - - Ok(emotions) -} - -/// Delete emotion by ID -pub fn delete_emotion(conn: &Connection, id: i64) -> Result<()> { - conn.execute("DELETE FROM emotions WHERE id = ?1", params![id])?; - Ok(()) -} - -/// Count total emotions -pub fn count_emotions(conn: &Connection) -> Result { - conn.query_row("SELECT COUNT(*) FROM emotions", [], |row| row.get(0)) -} - -fn map_emotion_row(row: &Row) -> Result { - let created_at: i64 = row.get(5)?; - let updated_at: i64 = row.get(6)?; - - Ok(Emotion { - id: row.get(0)?, - message_id: row.get(1)?, - emotion: row.get(2)?, - confidence: row.get(3)?, - model_version: row.get(4)?, - created_at: Utc.timestamp_opt(created_at, 0).unwrap(), - updated_at: Utc.timestamp_opt(updated_at, 0).unwrap(), - }) -} - -/// Insert emotion training sample -pub fn insert_training_sample( - conn: &Connection, - message_id: Option, - text: &str, - expected_emotion: &str, -) -> Result { - let now = Utc::now().timestamp(); - - conn.execute( - "INSERT INTO emotions_training_set (message_id, text, expected_emotion, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![message_id, text, expected_emotion, now, now], - )?; - - Ok(conn.last_insert_rowid()) -} - -/// Get state value from daemon_state table -pub fn get_state(conn: &Connection, key: &str) -> Result> { - conn.query_row( - "SELECT value FROM daemon_state WHERE key = ?1", - params![key], - |row| row.get(0), - ) - .optional() -} - -/// Set state value in daemon_state table -pub fn set_state(conn: &Connection, key: &str, value: &str) -> Result<()> { - let updated_at = Utc::now().timestamp(); - - conn.execute( - "INSERT INTO daemon_state (key, value, updated_at) - VALUES (?1, ?2, ?3) - ON CONFLICT(key) DO UPDATE SET value = ?2, updated_at = ?3", - params![key, value, updated_at], - )?; - - Ok(()) -} - -/// Get last processed chat.db rowid from database or return 0 -pub fn get_last_processed_rowid(conn: &Connection) -> Result { - Ok(get_state(conn, "last_processed_rowid")? - .and_then(|s| s.parse().ok()) - .unwrap_or(0)) -} - -/// Save last processed chat.db rowid to database -pub fn save_last_processed_rowid(conn: &Connection, rowid: i64) -> Result<()> { - set_state(conn, "last_processed_rowid", &rowid.to_string()) -} diff --git a/crates/server/src/db/schema.rs b/crates/server/src/db/schema.rs deleted file mode 100644 index f73025d..0000000 --- a/crates/server/src/db/schema.rs +++ /dev/null @@ -1,210 +0,0 @@ -use rusqlite::{ - Connection, - Result, -}; -use tracing::info; - -pub fn initialize_database(conn: &Connection) -> Result<()> { - info!("Initializing database schema"); - - // Load sqlite-vec extension (macOS only) - let vec_path = "./extensions/vec0.dylib"; - - // Try to load the vector extension (non-fatal if it fails for now) - match unsafe { conn.load_extension_enable() } { - | Ok(_) => { - match unsafe { conn.load_extension(vec_path, None::<&str>) } { - | Ok(_) => info!("Loaded sqlite-vec extension"), - | Err(e) => info!( - "Could not load sqlite-vec extension: {}. Vector operations will not be available.", - e - ), - } - let _ = unsafe { conn.load_extension_disable() }; - }, - | Err(e) => info!("Extension loading not enabled: {}", e), - } - - // Create messages table - conn.execute( - "CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_db_rowid INTEGER UNIQUE NOT NULL, - text TEXT, - timestamp INTEGER, - is_from_me BOOLEAN NOT NULL, - created_at INTEGER NOT NULL - )", - [], - )?; - - // Create index on chat_db_rowid for fast lookups - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_messages_chat_db_rowid ON messages(chat_db_rowid)", - [], - )?; - - // Create message_embeddings table - conn.execute( - "CREATE TABLE IF NOT EXISTS message_embeddings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - message_id INTEGER NOT NULL, - embedding BLOB NOT NULL, - model_name TEXT NOT NULL, - created_at INTEGER NOT NULL, - FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE - )", - [], - )?; - - // Create index on message_id - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_message_embeddings_message_id ON message_embeddings(message_id)", - [], - )?; - - // Create word_embeddings table - conn.execute( - "CREATE TABLE IF NOT EXISTS word_embeddings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - word TEXT UNIQUE NOT NULL, - embedding BLOB NOT NULL, - model_name TEXT NOT NULL, - created_at INTEGER NOT NULL - )", - [], - )?; - - // Create index on word - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_word_embeddings_word ON word_embeddings(word)", - [], - )?; - - // Create emotions table - conn.execute( - "CREATE TABLE IF NOT EXISTS emotions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - message_id INTEGER NOT NULL, - emotion TEXT NOT NULL, - confidence REAL NOT NULL, - model_version TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE - )", - [], - )?; - - // Create indexes for emotions - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_emotions_message_id ON emotions(message_id)", - [], - )?; - - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_emotions_emotion ON emotions(emotion)", - [], - )?; - - // Create emotions_training_set table - conn.execute( - "CREATE TABLE IF NOT EXISTS emotions_training_set ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - message_id INTEGER, - text TEXT NOT NULL, - expected_emotion TEXT NOT NULL, - actual_emotion TEXT, - confidence REAL, - is_validated BOOLEAN NOT NULL DEFAULT 0, - notes TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL - )", - [], - )?; - - // Create index on emotions_training_set - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_emotions_training_set_message_id ON emotions_training_set(message_id)", - [], - )?; - - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_emotions_training_set_validated ON emotions_training_set(is_validated)", - [], - )?; - - // Create state table for daemon state persistence - conn.execute( - "CREATE TABLE IF NOT EXISTS daemon_state ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at INTEGER NOT NULL - )", - [], - )?; - - // Create models table for storing ML model files - conn.execute( - "CREATE TABLE IF NOT EXISTS models ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - model_type TEXT NOT NULL, - version TEXT NOT NULL, - file_data BLOB NOT NULL, - metadata TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - )", - [], - )?; - - // Create index on model name and type - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_models_name ON models(name)", - [], - )?; - - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_models_type ON models(model_type)", - [], - )?; - - info!("Database schema initialized successfully"); - Ok(()) -} - -/// Helper function to serialize f32 vector to bytes for storage -pub fn serialize_embedding(embedding: &[f32]) -> Vec { - embedding.iter().flat_map(|f| f.to_le_bytes()).collect() -} - -/// Helper function to deserialize bytes back to f32 vector -pub fn deserialize_embedding(bytes: &[u8]) -> Vec { - bytes - .chunks_exact(4) - .map(|chunk| { - let array: [u8; 4] = chunk.try_into().unwrap(); - f32::from_le_bytes(array) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_embedding_serialization() { - let original = vec![1.0f32, 2.5, -3.7, 0.0, 100.5]; - let serialized = serialize_embedding(&original); - let deserialized = deserialize_embedding(&serialized); - - assert_eq!(original.len(), deserialized.len()); - for (a, b) in original.iter().zip(deserialized.iter()) { - assert!((a - b).abs() < 1e-6); - } - } -} diff --git a/crates/server/src/entities/mod.rs b/crates/server/src/entities/mod.rs deleted file mode 100644 index 01d81c2..0000000 --- a/crates/server/src/entities/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Entity builders and spawners will go here diff --git a/crates/server/src/iroh_sync.rs b/crates/server/src/iroh_sync.rs deleted file mode 100644 index aa0aeaa..0000000 --- a/crates/server/src/iroh_sync.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::Result; -use iroh::{ - Endpoint, - protocol::Router, -}; -use iroh_gossip::{ - api::{ - GossipReceiver, - GossipSender, - }, - net::Gossip, - proto::TopicId, -}; - -/// Initialize Iroh endpoint and gossip for the given topic -pub async fn init_iroh_gossip( - topic_id: TopicId, -) -> Result<(Endpoint, Gossip, Router, GossipSender, GossipReceiver)> { - println!("Initializing Iroh endpoint..."); - - // Create the Iroh endpoint - let endpoint = Endpoint::bind().await?; - println!("Endpoint created"); - - // Build the gossip protocol - println!("Building gossip protocol..."); - let gossip = Gossip::builder().spawn(endpoint.clone()); - - // Setup the router to handle incoming connections - println!("Setting up router..."); - let router = Router::builder(endpoint.clone()) - .accept(iroh_gossip::ALPN, gossip.clone()) - .spawn(); - - // Subscribe to the topic (no bootstrap peers for now) - println!("Subscribing to topic: {:?}", topic_id); - let bootstrap_peers = vec![]; - let subscribe_handle = gossip.subscribe(topic_id, bootstrap_peers).await?; - - // Split into sender and receiver - let (sender, mut receiver) = subscribe_handle.split(); - - // Wait for join to complete - println!("Waiting for gossip join..."); - receiver.joined().await?; - println!("Gossip initialized successfully"); - - Ok((endpoint, gossip, router, sender, receiver)) -} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs deleted file mode 100644 index 43179a5..0000000 --- a/crates/server/src/main.rs +++ /dev/null @@ -1,99 +0,0 @@ -mod assets; -mod components; -mod config; -mod db; -mod entities; -mod iroh_sync; -mod models; -mod services; -mod systems; - -use std::{ - path::Path, - sync::Arc, -}; - -use anyhow::{ - Context, - Result, -}; -use bevy::prelude::*; -// Import components and systems -use components::*; -use config::Config; -use iroh_gossip::proto::TopicId; -// Re-export init function -pub use iroh_sync::init_iroh_gossip; -use parking_lot::Mutex; -use rusqlite::Connection; -use systems::*; - -fn main() { - println!("Starting server"); - - // Load configuration and initialize database - let (config, us_db) = match initialize_app() { - | Ok(data) => data, - | Err(e) => { - eprintln!("Failed to initialize app: {}", e); - return; - }, - }; - - // Create a topic ID for gossip (use a fixed topic for now) - let mut topic_bytes = [0u8; 32]; - topic_bytes[..10].copy_from_slice(b"us-sync-v1"); - let topic_id = TopicId::from_bytes(topic_bytes); - - // Start Bevy app (headless) - App::new() - .add_plugins(MinimalPlugins) - .add_message::() - .add_message::() - .insert_resource(AppConfig(config)) - .insert_resource(Database(us_db)) - .insert_resource(GossipTopic(topic_id)) - .add_systems(Startup, (setup_database, setup_gossip)) - .add_systems( - Update, - ( - poll_gossip_init, - poll_chat_db, - detect_new_messages, - publish_to_gossip, - receive_from_gossip, - save_gossip_messages, - ), - ) - .run(); -} - -/// Initialize configuration and database -fn initialize_app() -> Result<(Config, Arc>)> { - let config = if Path::new("config.toml").exists() { - println!("Loading config from config.toml"); - Config::from_file("config.toml")? - } else { - println!("No config.toml found, using default configuration"); - let config = Config::default_config(); - config - .save("config.toml") - .context("Failed to save default config")?; - println!("Saved default configuration to config.toml"); - config - }; - - println!("Configuration loaded"); - println!(" Database: {}", config.database.path); - println!(" Chat DB: {}", config.database.chat_db_path); - - // Initialize database - println!("Initializing database at {}", config.database.path); - let conn = Connection::open(&config.database.path).context("Failed to open database")?; - - db::initialize_database(&conn).context("Failed to initialize database schema")?; - - let us_db = Arc::new(Mutex::new(conn)); - - Ok((config, us_db)) -} diff --git a/crates/server/src/models.rs b/crates/server/src/models.rs deleted file mode 100644 index a158398..0000000 --- a/crates/server/src/models.rs +++ /dev/null @@ -1,66 +0,0 @@ -use chrono::{ - DateTime, - Utc, -}; -use serde::{ - Deserialize, - Serialize, -}; - -/// Represents a message stored in our database -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Message { - pub id: i64, - pub chat_db_rowid: i64, - pub text: Option, - pub timestamp: Option>, - pub is_from_me: bool, - pub created_at: DateTime, -} - -/// Represents a message embedding (full message vector) -#[derive(Debug, Clone)] -pub struct MessageEmbedding { - pub id: i64, - pub message_id: i64, - pub embedding: Vec, - pub model_name: String, - pub created_at: DateTime, -} - -/// Represents a word embedding -#[derive(Debug, Clone)] -pub struct WordEmbedding { - pub id: i64, - pub word: String, - pub embedding: Vec, - pub model_name: String, - pub created_at: DateTime, -} - -/// Represents an emotion classification for a message -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Emotion { - pub id: i64, - pub message_id: i64, - pub emotion: String, - pub confidence: f64, - pub model_version: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// Represents an emotion training sample -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EmotionTrainingSample { - pub id: i64, - pub message_id: Option, - pub text: String, - pub expected_emotion: String, - pub actual_emotion: Option, - pub confidence: Option, - pub is_validated: bool, - pub notes: Option, - pub created_at: DateTime, - pub updated_at: DateTime, -} diff --git a/crates/server/src/proto/emotions.proto b/crates/server/src/proto/emotions.proto deleted file mode 100644 index 6298375..0000000 --- a/crates/server/src/proto/emotions.proto +++ /dev/null @@ -1,72 +0,0 @@ -syntax = "proto3"; - -package emotions; - -// Emotion classification for a message -message Emotion { - int64 id = 1; - int64 message_id = 2; - string emotion = 3; - double confidence = 4; - string model_version = 5; - int64 created_at = 6; - int64 updated_at = 7; -} - -// Request to get a single emotion by message ID -message GetEmotionRequest { - int64 message_id = 1; -} - -// Request to get multiple emotions with optional filters -message GetEmotionsRequest { - repeated int64 message_ids = 1; - optional string emotion_filter = 2; - optional double min_confidence = 3; - optional int32 limit = 4; - optional int32 offset = 5; -} - -// Response containing multiple emotions -message EmotionsResponse { - repeated Emotion emotions = 1; - int32 total_count = 2; -} - -// Request to update an emotion (for corrections/fine-tuning) -message UpdateEmotionRequest { - int64 message_id = 1; - string emotion = 2; - double confidence = 3; - optional string notes = 4; -} - -// Request to delete an emotion -message DeleteEmotionRequest { - int64 id = 1; -} - -// Generic response for mutations -message EmotionResponse { - bool success = 1; - string message = 2; - optional Emotion emotion = 3; -} - -// Empty message for list all -message Empty {} - -// The emotion service with full CRUD operations -service EmotionService { - // Read operations - rpc GetEmotion(GetEmotionRequest) returns (Emotion); - rpc GetEmotions(GetEmotionsRequest) returns (EmotionsResponse); - rpc ListAllEmotions(Empty) returns (EmotionsResponse); - - // Update operations (for classification corrections and fine-tuning) - rpc UpdateEmotion(UpdateEmotionRequest) returns (EmotionResponse); - rpc BatchUpdateEmotions(stream UpdateEmotionRequest) returns (EmotionResponse); - - // Delete operation - rpc DeleteEmotion(DeleteEmotionRequest) returns (EmotionResponse); -} diff --git a/crates/server/src/services/chat_poller.rs b/crates/server/src/services/chat_poller.rs deleted file mode 100644 index 626da04..0000000 --- a/crates/server/src/services/chat_poller.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::{ - path::Path, - sync::Arc, - time::Duration, -}; - -use anyhow::{ - Context, - Result, -}; -use chrono::Utc; -use rusqlite::Connection; -use tokio::{ - sync::{ - Mutex, - mpsc, - }, - time, -}; -use tracing::{ - debug, - error, - info, - warn, -}; - -use crate::db; - -pub struct ChatPollerService { - chat_db_path: String, - us_db: Arc>, - tx: mpsc::Sender, - poll_interval: Duration, -} - -impl ChatPollerService { - pub fn new( - chat_db_path: String, - us_db: Arc>, - tx: mpsc::Sender, - poll_interval_ms: u64, - ) -> Self { - Self { - chat_db_path, - us_db, - tx, - poll_interval: Duration::from_millis(poll_interval_ms), - } - } - - pub async fn run(&self) -> Result<()> { - info!("Starting chat poller service"); - info!( - "Polling {} every {:?}", - self.chat_db_path, self.poll_interval - ); - - // Get last processed rowid from database - let us_db = self.us_db.lock().await; - let mut last_rowid = - db::get_last_processed_rowid(&us_db).context("Failed to get last processed rowid")?; - drop(us_db); - - info!("Starting from rowid: {}", last_rowid); - - let mut interval = time::interval(self.poll_interval); - - loop { - interval.tick().await; - - match self.poll_messages(last_rowid).await { - | Ok(new_messages) => { - if !new_messages.is_empty() { - info!("Found {} new messages", new_messages.len()); - - for msg in new_messages { - // Update last_rowid - if msg.rowid > last_rowid { - last_rowid = msg.rowid; - } - - // Send message to processing pipeline - if let Err(e) = self.tx.send(msg).await { - error!("Failed to send message to processing pipeline: {}", e); - } - } - - // Save state to database - let us_db = self.us_db.lock().await; - if let Err(e) = db::save_last_processed_rowid(&us_db, last_rowid) { - warn!("Failed to save last processed rowid: {}", e); - } - drop(us_db); - } else { - debug!("No new messages"); - } - }, - | Err(e) => { - error!("Error polling messages: {}", e); - }, - } - } - } - - async fn poll_messages(&self, last_rowid: i64) -> Result> { - // Check if chat.db exists - if !Path::new(&self.chat_db_path).exists() { - return Err(anyhow::anyhow!( - "chat.db not found at {}", - self.chat_db_path - )); - } - - // Open chat.db (read-only) - let chat_db = lib::ChatDb::open(&self.chat_db_path).context("Failed to open chat.db")?; - - // Get messages with rowid > last_rowid - // We'll use the existing get_our_messages but need to filter by rowid - // For now, let's get recent messages and filter in-memory - let start_date = Some(Utc::now() - chrono::Duration::days(7)); - let end_date = Some(Utc::now()); - - let messages = chat_db - .get_our_messages(start_date, end_date) - .context("Failed to get messages from chat.db")?; - - // Filter messages with rowid > last_rowid and ensure they're not duplicates - let new_messages: Vec = messages - .into_iter() - .filter(|msg| msg.rowid > last_rowid) - .collect(); - - // Insert new messages into our database - let us_db = self.us_db.lock().await; - for msg in &new_messages { - if let Err(e) = db::insert_message(&us_db, msg) { - warn!("Failed to insert message {}: {}", msg.rowid, e); - } - } - - Ok(new_messages) - } -} diff --git a/crates/server/src/services/embedding_service.rs b/crates/server/src/services/embedding_service.rs deleted file mode 100644 index bf549c1..0000000 --- a/crates/server/src/services/embedding_service.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use rusqlite::Connection; -use tokio::sync::{ - Mutex, - mpsc, -}; -use tracing::{ - error, - info, - warn, -}; - -use crate::db; - -/// Service responsible for generating embeddings for messages and words -pub struct EmbeddingService { - us_db: Arc>, - rx: mpsc::Receiver, - model_name: String, -} - -impl EmbeddingService { - pub fn new( - us_db: Arc>, - rx: mpsc::Receiver, - model_name: String, - ) -> Self { - Self { - us_db, - rx, - model_name, - } - } - - pub async fn run(mut self) -> Result<()> { - info!("Starting embedding service with model: {}", self.model_name); - - // TODO: Load the embedding model here - // For now, we'll create a placeholder implementation - info!("Loading embedding model..."); - // let model = load_embedding_model(&self.model_name)?; - info!("Embedding model loaded (placeholder)"); - - while let Some(msg) = self.rx.recv().await { - if let Err(e) = self.process_message(&msg).await { - error!("Error processing message {}: {}", msg.rowid, e); - } - } - - Ok(()) - } - - async fn process_message(&self, msg: &lib::Message) -> Result<()> { - // Get message ID from our database - let us_db = self.us_db.lock().await; - let message_id = match db::get_message_id_by_chat_rowid(&us_db, msg.rowid)? { - | Some(id) => id, - | None => { - warn!("Message {} not found in database, skipping", msg.rowid); - return Ok(()); - }, - }; - - // Check if embedding already exists - if db::get_message_embedding(&us_db, message_id)?.is_some() { - return Ok(()); - } - - // Skip if message has no text - let text = match &msg.text { - | Some(t) if !t.is_empty() => t, - | _ => return Ok(()), - }; - - drop(us_db); - - // Generate embedding for the full message - // TODO: Replace with actual model inference - let message_embedding = self.generate_embedding(text)?; - - // Store message embedding - let us_db = self.us_db.lock().await; - db::insert_message_embedding(&us_db, message_id, &message_embedding, &self.model_name)?; - - // Tokenize and generate word embeddings - let words = self.tokenize(text); - for word in words { - // Check if word embedding exists - if db::get_word_embedding(&us_db, &word)?.is_none() { - // Generate embedding for word - let word_embedding = self.generate_embedding(&word)?; - db::insert_word_embedding(&us_db, &word, &word_embedding, &self.model_name)?; - } - } - - drop(us_db); - info!("Generated embeddings for message {}", msg.rowid); - - Ok(()) - } - - fn generate_embedding(&self, text: &str) -> Result> { - // TODO: Replace with actual model inference using Candle - // For now, return a placeholder embedding of dimension 1024 - let embedding = vec![0.0f32; 1024]; - Ok(embedding) - } - - fn tokenize(&self, text: &str) -> Vec { - // Simple word tokenization (split on whitespace and punctuation) - // TODO: Replace with proper tokenizer - text.split(|c: char| c.is_whitespace() || c.is_ascii_punctuation()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_lowercase()) - .collect() - } -} diff --git a/crates/server/src/services/emotion_service.rs b/crates/server/src/services/emotion_service.rs deleted file mode 100644 index 3b28f1d..0000000 --- a/crates/server/src/services/emotion_service.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use rusqlite::Connection; -use tokio::sync::{ - Mutex, - mpsc, -}; -use tracing::{ - error, - info, - warn, -}; - -use crate::db; - -/// Service responsible for classifying emotions in messages -pub struct EmotionService { - us_db: Arc>, - rx: mpsc::Receiver, - model_version: String, - training_sample_rate: f64, -} - -impl EmotionService { - pub fn new( - us_db: Arc>, - rx: mpsc::Receiver, - model_version: String, - training_sample_rate: f64, - ) -> Self { - Self { - us_db, - rx, - model_version, - training_sample_rate, - } - } - - pub async fn run(mut self) -> Result<()> { - info!( - "Starting emotion classification service with model: {}", - self.model_version - ); - info!( - "Training sample rate: {:.2}%", - self.training_sample_rate * 100.0 - ); - - // TODO: Load the RoBERTa emotion classification model here - info!("Loading RoBERTa-base-go_emotions model..."); - // let model = load_emotion_model(&self.model_version)?; - info!("Emotion model loaded (placeholder)"); - - while let Some(msg) = self.rx.recv().await { - if let Err(e) = self.process_message(&msg).await { - error!("Error processing message {}: {}", msg.rowid, e); - } - } - - Ok(()) - } - - async fn process_message(&self, msg: &lib::Message) -> Result<()> { - // Get message ID from our database - let us_db = self.us_db.lock().await; - let message_id = match db::get_message_id_by_chat_rowid(&us_db, msg.rowid)? { - | Some(id) => id, - | None => { - warn!("Message {} not found in database, skipping", msg.rowid); - return Ok(()); - }, - }; - - // Check if emotion classification already exists - if db::get_emotion_by_message_id(&us_db, message_id)?.is_some() { - return Ok(()); - } - - // Skip if message has no text - let text = match &msg.text { - | Some(t) if !t.is_empty() => t, - | _ => return Ok(()), - }; - - drop(us_db); - - // Classify emotion - // TODO: Replace with actual model inference - let (emotion, confidence) = self.classify_emotion(text)?; - - // Store emotion classification - let us_db = self.us_db.lock().await; - db::insert_emotion( - &us_db, - message_id, - &emotion, - confidence, - &self.model_version, - )?; - - // Randomly add to training set based on sample rate - if rand::random::() < self.training_sample_rate { - db::insert_training_sample(&us_db, Some(message_id), text, &emotion)?; - info!( - "Added message {} to training set (emotion: {})", - msg.rowid, emotion - ); - } - - drop(us_db); - info!( - "Classified message {} as {} (confidence: {:.2})", - msg.rowid, emotion, confidence - ); - - Ok(()) - } - - fn classify_emotion(&self, text: &str) -> Result<(String, f64)> { - // TODO: Replace with actual RoBERTa-base-go_emotions inference using Candle - // The model outputs probabilities for 28 emotions: - // admiration, amusement, anger, annoyance, approval, caring, confusion, - // curiosity, desire, disappointment, disapproval, disgust, embarrassment, - // excitement, fear, gratitude, grief, joy, love, nervousness, optimism, - // pride, realization, relief, remorse, sadness, surprise, neutral - - // For now, return a placeholder - let emotion = "neutral".to_string(); - let confidence = 0.85; - - Ok((emotion, confidence)) - } -} diff --git a/crates/server/src/services/grpc_server.rs b/crates/server/src/services/grpc_server.rs deleted file mode 100644 index b17f7eb..0000000 --- a/crates/server/src/services/grpc_server.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::db; -use anyhow::Result; -use rusqlite::Connection; -use std::sync::Arc; -use tokio::sync::Mutex; -use tonic::{Request, Response, Status}; -use tracing::{error, info}; - -// Include the generated protobuf code -pub mod emotions { - tonic::include_proto!("emotions"); -} - -use emotions::emotion_service_server::{EmotionService as EmotionServiceTrait, EmotionServiceServer}; -use emotions::*; - -pub struct GrpcServer { - us_db: Arc>, - address: String, -} - -impl GrpcServer { - pub fn new(us_db: Arc>, address: String) -> Self { - Self { us_db, address } - } - - pub async fn run(self) -> Result<()> { - let addr = self.address.parse()?; - info!("Starting gRPC server on {}", self.address); - - let service = EmotionServiceImpl { - us_db: self.us_db.clone(), - }; - - tonic::transport::Server::builder() - .add_service(EmotionServiceServer::new(service)) - .serve(addr) - .await?; - - Ok(()) - } -} - -struct EmotionServiceImpl { - us_db: Arc>, -} - -#[tonic::async_trait] -impl EmotionServiceTrait for EmotionServiceImpl { - async fn get_emotion( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let conn = self.us_db.lock().await; - - match db::get_emotion_by_message_id(&conn, req.message_id) { - Ok(Some(emotion)) => Ok(Response::new(emotion_to_proto(emotion))), - Ok(None) => Err(Status::not_found(format!( - "Emotion not found for message_id: {}", - req.message_id - ))), - Err(e) => { - error!("Database error: {}", e); - Err(Status::internal("Database error")) - } - } - } - - async fn get_emotions( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let conn = self.us_db.lock().await; - - let emotion_filter = req.emotion_filter.as_deref(); - let min_confidence = req.min_confidence; - let limit = req.limit.map(|l| l as i32); - let offset = req.offset.map(|o| o as i32); - - match db::list_emotions(&conn, emotion_filter, min_confidence, limit, offset) { - Ok(emotions) => { - let total_count = db::count_emotions(&conn).unwrap_or(0); - Ok(Response::new(EmotionsResponse { - emotions: emotions.into_iter().map(emotion_to_proto).collect(), - total_count, - })) - } - Err(e) => { - error!("Database error: {}", e); - Err(Status::internal("Database error")) - } - } - } - - async fn list_all_emotions( - &self, - _request: Request, - ) -> Result, Status> { - let conn = self.us_db.lock().await; - - match db::list_emotions(&conn, None, None, None, None) { - Ok(emotions) => { - let total_count = emotions.len() as i32; - Ok(Response::new(EmotionsResponse { - emotions: emotions.into_iter().map(emotion_to_proto).collect(), - total_count, - })) - } - Err(e) => { - error!("Database error: {}", e); - Err(Status::internal("Database error")) - } - } - } - - async fn update_emotion( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let conn = self.us_db.lock().await; - - match db::update_emotion(&conn, req.message_id, &req.emotion, req.confidence) { - Ok(_) => { - // If notes are provided, add to training set - if let Some(notes) = req.notes { - if let Ok(Some(msg)) = db::get_message(&conn, req.message_id) { - if let Some(text) = msg.text { - let _ = db::insert_training_sample( - &conn, - Some(req.message_id), - &text, - &req.emotion, - ); - } - } - } - - // Fetch the updated emotion - match db::get_emotion_by_message_id(&conn, req.message_id) { - Ok(Some(emotion)) => Ok(Response::new(EmotionResponse { - success: true, - message: "Emotion updated successfully".to_string(), - emotion: Some(emotion_to_proto(emotion)), - })), - _ => Ok(Response::new(EmotionResponse { - success: true, - message: "Emotion updated successfully".to_string(), - emotion: None, - })), - } - } - Err(e) => { - error!("Database error: {}", e); - Err(Status::internal("Database error")) - } - } - } - - async fn batch_update_emotions( - &self, - request: Request>, - ) -> Result, Status> { - let mut stream = request.into_inner(); - let mut count = 0; - - while let Some(req) = stream.message().await? { - let conn = self.us_db.lock().await; - match db::update_emotion(&conn, req.message_id, &req.emotion, req.confidence) { - Ok(_) => { - count += 1; - if let Some(notes) = req.notes { - if let Ok(Some(msg)) = db::get_message(&conn, req.message_id) { - if let Some(text) = msg.text { - let _ = db::insert_training_sample( - &conn, - Some(req.message_id), - &text, - &req.emotion, - ); - } - } - } - } - Err(e) => { - error!("Failed to update emotion for message {}: {}", req.message_id, e); - } - } - drop(conn); - } - - Ok(Response::new(EmotionResponse { - success: true, - message: format!("Updated {} emotions", count), - emotion: None, - })) - } - - async fn delete_emotion( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let conn = self.us_db.lock().await; - - match db::delete_emotion(&conn, req.id) { - Ok(_) => Ok(Response::new(EmotionResponse { - success: true, - message: format!("Emotion {} deleted successfully", req.id), - emotion: None, - })), - Err(e) => { - error!("Database error: {}", e); - Err(Status::internal("Database error")) - } - } - } -} - -fn emotion_to_proto(emotion: crate::models::Emotion) -> Emotion { - Emotion { - id: emotion.id, - message_id: emotion.message_id, - emotion: emotion.emotion, - confidence: emotion.confidence, - model_version: emotion.model_version, - created_at: emotion.created_at.timestamp(), - updated_at: emotion.updated_at.timestamp(), - } -} diff --git a/crates/server/src/services/mod.rs b/crates/server/src/services/mod.rs deleted file mode 100644 index e9c812f..0000000 --- a/crates/server/src/services/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod chat_poller; -pub mod embedding_service; -pub mod emotion_service; - -pub use chat_poller::ChatPollerService; -pub use embedding_service::EmbeddingService; -pub use emotion_service::EmotionService; diff --git a/crates/server/src/sync_plugin.rs b/crates/server/src/sync_plugin.rs deleted file mode 100644 index 5ca3185..0000000 --- a/crates/server/src/sync_plugin.rs +++ /dev/null @@ -1,114 +0,0 @@ -use bevy::prelude::*; -use lib::sync::{Syncable, SyncMessage}; -use crate::components::*; - -/// Bevy plugin for transparent CRDT sync via gossip -pub struct SyncPlugin; - -impl Plugin for SyncPlugin { - fn build(&self, app: &mut App) { - app.add_systems(Update, ( - publish_sync_ops, - receive_sync_ops, - )); - } -} - -/// Trait for Bevy resources that can be synced -pub trait SyncedResource: Resource + Syncable + Clone + Send + Sync + 'static {} - -/// Queue of sync operations to publish -#[derive(Resource, Default)] -pub struct SyncOpQueue { - pub ops: Vec, -} - -impl SyncOpQueue { - pub fn push(&mut self, op: T::Operation) { - self.ops.push(op); - } -} - -/// System to publish sync operations to gossip -fn publish_sync_ops( - mut queue: ResMut>, - resource: Res, - sender: Option>, -) { - if sender.is_none() || queue.ops.is_empty() { - return; - } - - let sender = sender.unwrap(); - let sender_guard = sender.sender.lock(); - - for op in queue.ops.drain(..) { - let sync_msg = resource.create_sync_message(op); - - match sync_msg.to_bytes() { - Ok(bytes) => { - println!("Publishing sync operation: {} bytes", bytes.len()); - // TODO: Actually send via gossip - // sender_guard.broadcast(bytes)?; - } - Err(e) => { - eprintln!("Failed to serialize sync operation: {}", e); - } - } - } -} - -/// System to receive and apply sync operations from gossip -fn receive_sync_ops( - mut resource: ResMut, - receiver: Option>, -) { - if receiver.is_none() { - return; - } - - // TODO: Poll receiver for messages - // For each message: - // 1. Deserialize SyncMessage - // 2. Apply to resource with resource.apply_sync_op(&op) -} - -/// Helper to register a synced resource -pub trait SyncedResourceExt { - fn add_synced_resource(&mut self) -> &mut Self; -} - -impl SyncedResourceExt for App { - fn add_synced_resource(&mut self) -> &mut Self { - self.init_resource::>(); - self - } -} - -/// Example synced resource -#[cfg(test)] -mod tests { - use super::*; - use lib::sync::synced; - - #[synced] - pub struct TestConfig { - pub value: i32, - - #[sync(skip)] - node_id: String, - } - - impl Resource for TestConfig {} - impl SyncedResource for TestConfig {} - - #[test] - fn test_sync_plugin() { - let mut app = App::new(); - app.add_plugins(MinimalPlugins); - app.add_plugins(SyncPlugin); - app.add_synced_resource::(); - - // TODO: Test that operations are queued and published - } -} diff --git a/crates/server/src/systems/database.rs b/crates/server/src/systems/database.rs deleted file mode 100644 index 59eb520..0000000 --- a/crates/server/src/systems/database.rs +++ /dev/null @@ -1,9 +0,0 @@ -use bevy::prelude::*; - -use crate::components::*; - -/// System: Poll chat.db for new messages using Bevy's task system -pub fn poll_chat_db(_config: Res, _db: Res) { - // TODO: Use Bevy's AsyncComputeTaskPool to poll chat.db - // This will replace the tokio::spawn chat poller -} diff --git a/crates/server/src/systems/gossip.rs b/crates/server/src/systems/gossip.rs deleted file mode 100644 index 66ee8e7..0000000 --- a/crates/server/src/systems/gossip.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::sync::Arc; - -use bevy::prelude::*; -use parking_lot::Mutex; - -use crate::components::*; - -/// System: Poll the gossip init task and insert resources when complete -pub fn poll_gossip_init(mut commands: Commands, mut init_task: Option>) { - if let Some(mut task) = init_task { - // Check if the task is finished (non-blocking) - if let Some(result) = - bevy::tasks::block_on(bevy::tasks::futures_lite::future::poll_once(&mut task.0)) - { - if let Some((endpoint, gossip, router, sender, receiver)) = result { - println!("Inserting gossip resources"); - - // Insert all the resources - commands.insert_resource(IrohEndpoint { - endpoint, - node_id: "TODO".to_string(), // TODO: Figure out how to get node_id in iroh 0.95 - }); - commands.insert_resource(IrohGossipHandle { gossip }); - commands.insert_resource(IrohRouter { router }); - commands.insert_resource(IrohGossipSender { - sender: Arc::new(Mutex::new(sender)), - }); - commands.insert_resource(IrohGossipReceiver { - receiver: Arc::new(Mutex::new(receiver)), - }); - - // Remove the init task - commands.remove_resource::(); - } - } - } -} - -/// System: Detect new messages in SQLite that need to be published to gossip -pub fn detect_new_messages( - _db: Res, - _last_synced: Local, - _publish_events: MessageWriter, -) { - // TODO: Query SQLite for messages with rowid > last_synced - // When we detect new messages, we'll send PublishMessageEvent -} - -/// System: Publish messages to gossip when PublishMessageEvent is triggered -pub fn publish_to_gossip( - mut events: MessageReader, - sender: Option>, - endpoint: Option>, -) { - if sender.is_none() || endpoint.is_none() { - // Gossip not initialized yet, skip - return; - } - - let sender = sender.unwrap(); - let endpoint = endpoint.unwrap(); - - for event in events.read() { - println!("Publishing message {} to gossip", event.message.rowid); - - // Create sync message - let sync_message = SyncMessage { - message: event.message.clone(), - sync_timestamp: chrono::Utc::now().timestamp(), - publisher_node_id: endpoint.node_id.clone(), - }; - - // Serialize the message - match serialize_sync_message(&sync_message) { - | Ok(bytes) => { - // TODO: Publish to gossip - // For now, just log that we would publish - println!("Would publish {} bytes to gossip", bytes.len()); - - // Note: Direct async broadcasting from Bevy systems is tricky - // due to Sync requirements We'll need to use a - // different approach, possibly with channels or a dedicated - // task - }, - | Err(e) => { - eprintln!("Failed to serialize sync message: {}", e); - }, - } - } -} - -/// System: Receive messages from gossip -pub fn receive_from_gossip( - mut _gossip_events: MessageWriter, - receiver: Option>, -) { - if receiver.is_none() { - // Gossip not initialized yet, skip - return; - } - - // TODO: Implement proper async message reception - // This will require spawning a long-running task that listens for gossip - // events and sends them as Bevy messages. For now, this is a - // placeholder. -} - -/// System: Save received gossip messages to SQLite -pub fn save_gossip_messages(mut events: MessageReader, _db: Res) { - for event in events.read() { - println!( - "Received message {} from gossip (published by {})", - event.sync_message.message.rowid, event.sync_message.publisher_node_id - ); - // TODO: Save to SQLite if we don't already have it - } -} diff --git a/crates/server/src/systems/mod.rs b/crates/server/src/systems/mod.rs deleted file mode 100644 index 4cefcce..0000000 --- a/crates/server/src/systems/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod database; -pub mod gossip; -pub mod setup; - -pub use database::*; -pub use gossip::*; -pub use setup::*; diff --git a/crates/server/src/systems/setup.rs b/crates/server/src/systems/setup.rs deleted file mode 100644 index 7e303f7..0000000 --- a/crates/server/src/systems/setup.rs +++ /dev/null @@ -1,24 +0,0 @@ -use bevy::{ - prelude::*, - tasks::AsyncComputeTaskPool, -}; - -use crate::components::*; - -/// Startup system: Initialize database -pub fn setup_database(_db: Res) { - println!("Database resource initialized"); -} - -/// Startup system: Initialize Iroh gossip -pub fn setup_gossip(mut commands: Commands, topic: Res) { - println!("Setting up Iroh gossip for topic: {:?}", topic.0); - - let topic_id = topic.0; - - // TODO: Initialize gossip properly - // For now, skip async initialization due to Sync requirements in Bevy tasks - // We'll need to use a different initialization strategy - - println!("Gossip initialization skipped (TODO: implement proper async init)"); -}