From 2bad250a04908055da17826a79f2872b5cea83a4 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Sat, 15 Nov 2025 23:42:12 +0000 Subject: [PATCH] initial commit Signed-off-by: Sienna Meridian Satterwhite --- .gitignore | 77 + Cargo.lock | 8506 +++++++++++++++++ Cargo.toml | 51 + config.toml | 18 + crates/client/.gitignore | 1 + crates/client/Cargo.toml | 43 + crates/client/src/lib.rs | 14 + crates/client/src/main.rs | 24 + crates/lib/.gitignore | 4 + crates/lib/Cargo.toml | 20 + crates/lib/src/db.rs | 139 + crates/lib/src/error.rs | 15 + crates/lib/src/lib.rs | 30 + crates/lib/src/models.rs | 112 + crates/lib/src/sync.rs | 165 + crates/lib/tests/our_messages_test.rs | 98 + crates/lib/tests/sync_integration.rs | 157 + crates/server/Cargo.toml | 58 + crates/server/src/assets/mod.rs | 1 + crates/server/src/components/database.rs | 14 + crates/server/src/components/gossip.rs | 87 + crates/server/src/components/mod.rs | 5 + crates/server/src/config.rs | 84 + crates/server/src/db/mod.rs | 5 + crates/server/src/db/operations.rs | 321 + crates/server/src/db/schema.rs | 207 + crates/server/src/entities/mod.rs | 1 + crates/server/src/iroh_sync.rs | 42 + crates/server/src/main.rs | 96 + crates/server/src/models.rs | 60 + crates/server/src/proto/emotions.proto | 72 + crates/server/src/services/chat_poller.rs | 121 + .../server/src/services/embedding_service.rs | 110 + crates/server/src/services/emotion_service.rs | 119 + 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 | 12 + crates/server/src/systems/gossip.rs | 116 + crates/server/src/systems/mod.rs | 7 + crates/server/src/systems/setup.rs | 22 + crates/sync-macros/Cargo.toml | 15 + crates/sync-macros/src/lib.rs | 345 + docs/rfcs/0001-crdt-gossip-sync.md | 1653 ++++ docs/rfcs/0002-persistence-strategy.md | 566 ++ docs/rfcs/README.md | 39 + index.html | 640 ++ 47 files changed, 14645 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 config.toml create mode 100644 crates/client/.gitignore create mode 100644 crates/client/Cargo.toml create mode 100644 crates/client/src/lib.rs create mode 100644 crates/client/src/main.rs create mode 100644 crates/lib/.gitignore create mode 100644 crates/lib/Cargo.toml create mode 100644 crates/lib/src/db.rs create mode 100644 crates/lib/src/error.rs create mode 100644 crates/lib/src/lib.rs create mode 100644 crates/lib/src/models.rs create mode 100644 crates/lib/src/sync.rs create mode 100644 crates/lib/tests/our_messages_test.rs create mode 100644 crates/lib/tests/sync_integration.rs create mode 100644 crates/server/Cargo.toml create mode 100644 crates/server/src/assets/mod.rs create mode 100644 crates/server/src/components/database.rs create mode 100644 crates/server/src/components/gossip.rs create mode 100644 crates/server/src/components/mod.rs create mode 100644 crates/server/src/config.rs create mode 100644 crates/server/src/db/mod.rs create mode 100644 crates/server/src/db/operations.rs create mode 100644 crates/server/src/db/schema.rs create mode 100644 crates/server/src/entities/mod.rs create mode 100644 crates/server/src/iroh_sync.rs create mode 100644 crates/server/src/main.rs create mode 100644 crates/server/src/models.rs create mode 100644 crates/server/src/proto/emotions.proto create mode 100644 crates/server/src/services/chat_poller.rs create mode 100644 crates/server/src/services/embedding_service.rs create mode 100644 crates/server/src/services/emotion_service.rs create mode 100644 crates/server/src/services/grpc_server.rs create mode 100644 crates/server/src/services/mod.rs create mode 100644 crates/server/src/sync_plugin.rs create mode 100644 crates/server/src/systems/database.rs create mode 100644 crates/server/src/systems/gossip.rs create mode 100644 crates/server/src/systems/mod.rs create mode 100644 crates/server/src/systems/setup.rs create mode 100644 crates/sync-macros/Cargo.toml create mode 100644 crates/sync-macros/src/lib.rs create mode 100644 docs/rfcs/0001-crdt-gossip-sync.md create mode 100644 docs/rfcs/0002-persistence-strategy.md create mode 100644 docs/rfcs/README.md create mode 100644 index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a96ee5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# SQLite databases +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 + +# Compressed files +*.zip +*.tar.gz +*.7z +*.rar + +# Rust/Cargo +target/ +Cargo.lock # Remove this line if this is a binary crate, keep for libraries +**/*.rs.bk +*.pdb + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +Desktop.ini + +# Linux +*~ +.directory + +# IDEs and editors +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ +*.sublime-project +*.sublime-workspace + +# Environment and config files that may contain secrets +.env +.env.local +.env.*.local +config.toml # Remove this if you want to track config +*.local.toml + +# Logs +*.log +logs/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Generated documentation +docs/book/ +target/doc/ + +# OS-specific network storage +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.fseventsd +.Spotlight-V100 +.TemporaryItems + +# Project-specific (based on your untracked files) +emotion-gradient-config-*.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..60e303f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,8506 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" + +[[package]] +name = "accesskit_consumer" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db81010a6895d8707f9072e6ce98070579b43b717193d2614014abd5cb17dd43" +dependencies = [ + "accesskit", + "hashbrown 0.15.5", +] + +[[package]] +name = "accesskit_macos" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0089e5c0ac0ca281e13ea374773898d9354cc28d15af9f0f7394d44a495b575" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.5", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "accesskit_windows" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d63dd5041e49c363d83f5419a896ecb074d309c414036f616dc0b04faca971" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.5", + "static_assertions", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "accesskit_winit" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cfabe59d0eaca7412bfb1f70198dd31e3b0496fee7e15b066f9c36a1a140a0" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "acto" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a026259da4f1a13b4af60cda453c392de64c58c12d239c560923e0382f42f2b9" +dependencies = [ + "parking_lot", + "pin-project-lite", + "rustc_version", + "smol_str 0.1.24", + "tokio", + "tracing", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8202ab55fcbf46ca829833f347a82a2a4ce0596f0304ac322c2d100030cd56" +dependencies = [ + "bytes", + "crypto-common", + "inout", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.10.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomicow" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52e8890bb9844440d0c412fa74b67fd2f14e85248b6e00708059b6da9e5f8bf" +dependencies = [ + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base16ct" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" + +[[package]] +name = "base32" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bevy" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342f7e9335416dc98642d5747c4ed8a6ad9f7244a36d5b2b7a1b7910e4d8f524" +dependencies = [ + "bevy_internal", +] + +[[package]] +name = "bevy_a11y" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3917cd35096fb2fe176632740b68a4b53cb61006cfff13d66ef47ee2c2478d53" +dependencies = [ + "accesskit", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_reflect", +] + +[[package]] +name = "bevy_android" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a9dd9488c77fa2ea31b5da2f978aab7f1cc82e6d2c3be0adf637d9fd7cb6c8" +dependencies = [ + "android-activity", +] + +[[package]] +name = "bevy_app" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f582409b4ed3850d9b66ee94e71a0e2c20e7068121d372530060c4dfcba66fa" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "cfg-if", + "console_error_panic_hook", + "ctrlc", + "downcast-rs", + "log", + "thiserror 2.0.17", + "variadics_please", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "bevy_asset" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6ee42e74a64a46ab91bd1c0155f8abe5b732bdb948a9b26e541456cc7940e5" +dependencies = [ + "async-broadcast", + "async-fs", + "async-lock", + "atomicow", + "bevy_android", + "bevy_app", + "bevy_asset_macros", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.10.0", + "blake3", + "crossbeam-channel", + "derive_more 2.0.1", + "disqualified", + "downcast-rs", + "either", + "futures-io", + "futures-lite", + "js-sys", + "parking_lot", + "ron", + "serde", + "stackfuture", + "thiserror 2.0.17", + "tracing", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bevy_asset_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03711d2c087227f64ba85dd38a99d4d6893f80d2475c2e77fb90a883760a055" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_camera" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b70d79ccbd8bfefc79f33a104dfd82ae2f5276ce04d6df75787bfa3edc4c4c1a" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more 2.0.1", + "downcast-rs", + "serde", + "smallvec", + "thiserror 2.0.17", + "wgpu-types", +] + +[[package]] +name = "bevy_color" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94dc78477c1c208c0cd221c64e907aba8ba165f39bebb72adc6180e1a13e8938" +dependencies = [ + "bevy_math", + "bevy_reflect", + "bytemuck", + "derive_more 2.0.1", + "encase", + "serde", + "thiserror 2.0.17", + "wgpu-types", +] + +[[package]] +name = "bevy_core_pipeline" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c866a2fe33ec27a612d883223d30f1857aa852766b21a9603628735dace632f" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.10.0", + "nonmax", + "radsort", + "smallvec", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "bevy_derive" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c733807158f8fcac68e23222e69ed91a6492ae9410fc2c145b9bb182cfd63e" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_diagnostic" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12fa32312818c08aa4035bebe9fb3f62aaf7efae33688e718dd6ee6c0147493" +dependencies = [ + "atomic-waker", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_tasks", + "bevy_time", + "const-fnv1a-hash", + "log", + "serde", +] + +[[package]] +name = "bevy_ecs" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d929d32190cfcde6efd2df493601c4dbc18a691fd9775a544c951c3c112e1a" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.10.0", + "bumpalo", + "concurrent-queue", + "derive_more 2.0.1", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror 2.0.17", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eeddfb80a2e000663e87be9229c26b4da92bddbc06c8776bc0d1f4a7f679079" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_encase_derive" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449e5903594a00f007732ba232af0c527ad4e6e3d29bc3e195ec78dbd20c8b2" +dependencies = [ + "bevy_macro_utils", + "encase_derive_impl", +] + +[[package]] +name = "bevy_gizmos" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3f174faa13041634060dd99f6f59c29997fd62f40252f0466c2ebea8603d4d" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_ecs", + "bevy_gizmos_macros", + "bevy_image", + "bevy_light", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bytemuck", + "tracing", +] + +[[package]] +name = "bevy_gizmos_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714273aa7f285c0aaa874b7fbe37fe4e6e45355e3e6f3321aefa1b78cda259e0" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_image" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168de8239b2aedd2eeef9f76ae1909b2fdf859b11dcdb4d4d01b93f5f2c771be" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "bitflags 2.10.0", + "bytemuck", + "futures-lite", + "guillotiere", + "half", + "image", + "rectangle-pack", + "serde", + "thiserror 2.0.17", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_input" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4074b2d0d6680b4deb308ded7b4e8b1b99181c0502e2632e78af815b26f01" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "derive_more 2.0.1", + "log", + "smol_str 0.2.2", + "thiserror 2.0.17", +] + +[[package]] +name = "bevy_input_focus" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70761eba0f616a1caa761457bff2b8ae80c9916f39d167fab8c2d5c98d2b8951" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_window", + "log", + "thiserror 2.0.17", +] + +[[package]] +name = "bevy_internal" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43985739584f3a5d43026aa1edd772f064830be46c497518f05f7dfbc886bba" +dependencies = [ + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_gizmos", + "bevy_image", + "bevy_input", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_sprite_render", + "bevy_state", + "bevy_tasks", + "bevy_text", + "bevy_time", + "bevy_transform", + "bevy_ui", + "bevy_utils", + "bevy_window", + "bevy_winit", +] + +[[package]] +name = "bevy_light" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad00ab66d1e93edb928be66606a71066f3b1cbc9f414720e290ef5361eb6237" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "tracing", +] + +[[package]] +name = "bevy_log" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae217a035714a37b779487f82edc4c7c1223f7088d7ad94054f29f524d61c51" +dependencies = [ + "android_log-sys", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_utils", + "tracing", + "tracing-log", + "tracing-oslog", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17dbc3f8948da58b3c17767d20fd3cd35fe4721ed19a9a3204a6f1d6c9951bdd" +dependencies = [ + "parking_lot", + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "bevy_math" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a41e368ffa95ae2a353197d1ae3993f4d3d471444d80b65c932db667ea7b9e" +dependencies = [ + "approx", + "bevy_reflect", + "derive_more 2.0.1", + "glam", + "itertools 0.14.0", + "libm", + "rand 0.9.2", + "rand_distr", + "serde", + "smallvec", + "thiserror 2.0.17", + "variadics_please", +] + +[[package]] +name = "bevy_mesh" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6255244b71153b305fddb4e6f827cb97ed51f276b6e632f5fc46538647948f6" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mikktspace", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bitflags 2.10.0", + "bytemuck", + "derive_more 2.0.1", + "hexasphere", + "thiserror 2.0.17", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_mikktspace" +version = "0.17.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef8e4b7e61dfe7719bb03c884dc270cd46a82efb40f93e9933b990c5c190c59" + +[[package]] +name = "bevy_picking" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a232a8ea4dc9b83c08226f56b868acb1ead06946a95d8b9c8cbbcc860cd8090" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_window", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_platform" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cf8cda162688c95250e74cffaa1c3a04597f105d4ca35554106f107308ea57" +dependencies = [ + "critical-section", + "foldhash 0.2.0", + "futures-channel", + "getrandom 0.3.4", + "hashbrown 0.16.0", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin 0.10.0", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "bevy_ptr" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28ab4074e7b781bab84e9b0a41ede245d673d1f75646ce0db27643aedcfb3a85" + +[[package]] +name = "bevy_reflect" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333df3f5947b7e62728eb5c0b51d679716b16c7c5283118fed4563f13230954e" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more 2.0.1", + "disqualified", + "downcast-rs", + "erased-serde", + "foldhash 0.2.0", + "glam", + "inventory", + "serde", + "smallvec", + "smol_str 0.2.2", + "thiserror 2.0.17", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0205dce9c5a4d8d041b263bcfd96e9d9d6f3d49416e12db347ab5778b3071fe1" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_render" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d6a5d47ebb247e4ecaaf4a3b0310b7c518728ff2362c69f4220d0d3228e17d" +dependencies = [ + "async-channel", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_encase_derive", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render_macros", + "bevy_shader", + "bevy_tasks", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.10.0", + "bytemuck", + "derive_more 2.0.1", + "downcast-rs", + "encase", + "fixedbitset", + "image", + "indexmap", + "js-sys", + "naga", + "nonmax", + "offset-allocator", + "send_wrapper", + "smallvec", + "thiserror 2.0.17", + "tracing", + "variadics_please", + "wasm-bindgen", + "web-sys", + "wgpu", +] + +[[package]] +name = "bevy_render_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e8b553adf0a4f9f059c5c2dcb52d9ac09abede1c322a92b43b9f4bb11c3843" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_shader" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cef8f8e53776d286eb62bb60164f30671f07005ff407e94ec1176e9426d1477" +dependencies = [ + "bevy_asset", + "bevy_platform", + "bevy_reflect", + "naga", + "naga_oil", + "serde", + "thiserror 2.0.17", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_sprite" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bb52fa52caa1cc8d95acf45e52efc0c72b59755c2f0801a30fdab367921db0" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_text", + "bevy_transform", + "bevy_window", + "radsort", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_sprite_render" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31bb90a9139b04568bd30b2492ba61234092d95a7f7e3c84b55369b16d7e261b" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_text", + "bevy_transform", + "bevy_utils", + "bitflags 2.10.0", + "bytemuck", + "derive_more 2.0.1", + "fixedbitset", + "nonmax", + "tracing", +] + +[[package]] +name = "bevy_state" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4e955f36cdc7b31556e4619a653dcf65d46967d90d36fb788f746c8e89257e" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_state_macros", + "bevy_utils", + "log", + "variadics_please", +] + +[[package]] +name = "bevy_state_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3e4e32b1b96585740a2b447661af7db1b9d688db5e4d96da50461cd8f5ce63" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_tasks" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18839182775f30d26f0f84d9de85d25361bb593c99517a80b64ede6cbaf41adc" +dependencies = [ + "async-channel", + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "crossbeam-queue", + "derive_more 2.0.1", + "futures-lite", + "heapless 0.8.0", + "pin-project", +] + +[[package]] +name = "bevy_text" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1b759cf2ed8992132bd541ebb9ffcfa777d2faf3596d418fb25984bc6677d8" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "cosmic-text", + "serde", + "smallvec", + "sys-locale", + "thiserror 2.0.17", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_time" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52edd3d30ed94074f646ba1c9914e407af9abe5b6fb7a4322c855341a536cc" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "crossbeam-channel", + "log", + "serde", +] + +[[package]] +name = "bevy_transform" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7995ae14430b1a268d1e4f098ab770e8af880d2df5e4e37161b47d8d9e9625bd" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_log", + "bevy_math", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "derive_more 2.0.1", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "bevy_ui" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc999815a67a6b2fc911df9eea27af703ff656aed6fd31d8606dced701f07fd6" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_sprite", + "bevy_text", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more 2.0.1", + "smallvec", + "taffy", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "bevy_utils" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080254083c74d5f6eb0649d7cd6181bda277e8fe3c509ec68990a5d56ec23f24" +dependencies = [ + "bevy_platform", + "disqualified", + "thread_local", +] + +[[package]] +name = "bevy_window" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f582478606d6b6e5c53befbe7612f038fdfb73f8a27f7aae644406637347acd4" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "log", + "raw-window-handle", + "serde", +] + +[[package]] +name = "bevy_winit" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0ccf2faca4b4c156a26284d1bbf90a8cac8568a273adcd6c1a270c1342f3df" +dependencies = [ + "accesskit", + "accesskit_winit", + "approx", + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_window", + "cfg-if", + "tracing", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[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", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[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 = "cc" +version = "1.2.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd162f2b8af3e0639d83f28a637e4e55657b7a74508dba5a9bf4da523d5c9e9" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e12a13eb01ded5d32ee9658d94f553a19e804204f2dc811df69ab4d9e0cb8c7" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "client" +version = "0.1.0" +dependencies = [ + "anyhow", + "bevy", + "iroh", + "iroh-gossip", + "lib", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + +[[package]] +name = "const-oid" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "const_soft_float" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "constgebra" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aaf9b65849a68662ac6c0810c8893a765c960b907dd7cfab9c4a50bf764fbc" +dependencies = [ + "const_soft_float", +] + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cosmic-text" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" +dependencies = [ + "bitflags 2.10.0", + "fontdb", + "log", + "rangemap", + "rustc-hash 1.1.0", + "rustybuzz", + "self_cell", + "smol_str 0.2.2", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crdts" +version = "7.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387808c885b79055facbd4b2e806a683fe1bc37abc7dfa5fea1974ad2d4137b0" +dependencies = [ + "num", + "quickcheck", + "serde", + "tiny-keccak", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.2.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" +dependencies = [ + "hybrid-array", + "rand_core 0.9.3", +] + +[[package]] +name = "crypto_box" +version = "0.10.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bda4de3e070830cf3a27a394de135b6709aefcc54d1e16f2f029271254a6ed9" +dependencies = [ + "aead", + "chacha20", + "crypto_secretbox", + "curve25519-dalek", + "salsa20", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto_secretbox" +version = "0.2.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54532aae6546084a52cef855593daf9555945719eeeda9974150e0def854873e" +dependencies = [ + "aead", + "chacha20", + "cipher", + "hybrid-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "ctrlc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core 0.9.3", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "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 = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.8.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac89f8a64533a9b0eaa73a68e424db0fb1fd6271c74cc0125336a05f090568d" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "dyn-stack" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b" +dependencies = [ + "bytemuck", + "reborrow", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "594435fe09e345ee388e4e8422072ff7dfeca8729389fbd997b3f5504c44cd47" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.9.3", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encase" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02ba239319a4f60905966390f5e52799d868103a533bb7e27822792332504ddd" +dependencies = [ + "const_panic", + "encase_derive", + "glam", + "thiserror 2.0.17", +] + +[[package]] +name = "encase_derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5223d6c647f09870553224f6e37261fe5567bc5a4f4cf13ed337476e79990f2f" +dependencies = [ + "encase_derive_impl", +] + +[[package]] +name = "encase_derive_impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796db3d892515842ca2dfb11124c4bb4a9e58d9f2c5c1072e5bca1b2334507b" +dependencies = [ + "proc-macro2", + "quote", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +dependencies = [ + "cc", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +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 = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb68017df91f2e477ed4bea586c59eaecaa47ed885a770d0444e21e62572cd2" +dependencies = [ + "fixedbitset", + "futures-buffered", + "futures-core", + "futures-lite", + "pin-project", + "slab", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.61.3", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "glam" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" +dependencies = [ + "bytemuck", + "libm", + "rand 0.9.2", + "serde_core", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.10.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.10.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "grid" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36119f3a540b086b4e436bb2b588cf98a68863470e0e880f4d0842f112a3183a" + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +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", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "portable-atomic", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexasphere" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a164ceff4500f2a72b1d21beaa8aa8ad83aec2b641844c659b190cb3ea2e0b" +dependencies = [ + "constgebra", + "glam", + "tinyvec", +] + +[[package]] +name = "hexf-parse" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "http", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "rustls", + "thiserror 2.0.17", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +dependencies = [ + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.4", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke 0.8.1", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke 0.8.1", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.2", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[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 = "inout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7357b6e7aa75618c7864ebd0634b115a7218b0615f4cb1df33ac3eca23943d4" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "iroh" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2374ba3cdaac152dc6ada92d971f7328e6408286faab3b7350842b2ebbed4789" +dependencies = [ + "aead", + "backon", + "bytes", + "cfg_aliases", + "crypto_box", + "data-encoding", + "derive_more 2.0.1", + "ed25519-dalek", + "futures-util", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "igd-next", + "instant", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "iroh-quinn-udp", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netwatch", + "pin-project", + "pkarr", + "pkcs8", + "portmapper", + "rand 0.9.2", + "reqwest", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "rustls-webpki", + "serde", + "smallvec", + "strum", + "swarm-discovery", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots 1.0.4", + "z32", +] + +[[package]] +name = "iroh-base" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a8c5fb1cc65589f0d7ab44269a76f615a8c4458356952c9b0ef1c93ea45ff8" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "derive_more 2.0.1", + "ed25519-dalek", + "n0-error", + "rand_core 0.9.3", + "serde", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-gossip" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "026dd31b487ec5e80ac0240f4eb70cd6c0a2800f6ef44beca5329443c194bb22" +dependencies = [ + "blake3", + "bytes", + "data-encoding", + "derive_more 2.0.1", + "ed25519-dalek", + "futures-concurrency", + "futures-lite", + "futures-util", + "hex", + "indexmap", + "iroh", + "iroh-base", + "iroh-metrics", + "irpc", + "n0-error", + "n0-future", + "postcard", + "rand 0.9.2", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-metrics" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e3381da7c93c12d353230c74bba26131d1c8bf3a4d8af0fec041546454582e" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "postcard", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e12bd0763fd16062f5cc5e8db15dd52d26e75a8af4c7fb57ccee3589b344b8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iroh-quinn" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde160ebee7aabede6ae887460cd303c8b809054224815addf1469d54a6fcf7" +dependencies = [ + "bytes", + "cfg_aliases", + "iroh-quinn-proto", + "iroh-quinn-udp", + "pin-project-lite", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-proto" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" +dependencies = [ + "bytes", + "getrandom 0.2.16", + "rand 0.8.5", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "iroh-relay" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fbdf2aeffa7d6ede1a31f6570866c2199b1cee96a0b563994623795d1bac2c" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more 2.0.1", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "lru 0.16.2", + "n0-error", + "n0-future", + "num_enum", + "pin-project", + "pkarr", + "postcard", + "rand 0.9.2", + "reqwest", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "sha1", + "strum", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "webpki-roots 1.0.4", + "ws_stream_wasm", + "z32", +] + +[[package]] +name = "irpc" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bee97aaa18387c4f0aae61058195dc9f9dea3e41c0e272973fe3e9bf611563d" +dependencies = [ + "futures-util", + "irpc-derive", + "n0-error", + "n0-future", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "irpc-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58148196d2230183c9679431ac99b57e172000326d664e8456fa2cd27af6505a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.11.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" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lib" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "crdts", + "futures-lite", + "iroh", + "iroh-gossip", + "rusqlite", + "serde", + "serde_json", + "sync-macros", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.5.18", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" + +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.0", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", + "stable_deref_trait", +] + +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "n0-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5969a2f40e9d9ed121a789c415f4114ac2b28e5731c080bdefee217d3b3fb" +dependencies = [ + "anyhow", + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a6908df844696d9af91c7c3950d50e52d67df327d02a95367f95bbf177d6556" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "n0-future" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c0709ac8235ce13b82bc4d180ee3c42364b90c1a8a628c3422d991d75a728b5" +dependencies = [ + "cfg_aliases", + "derive_more 1.0.0", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38acf13c1ddafc60eb7316d52213467f8ccb70b6f02b65e7d97f7799b1f50be4" +dependencies = [ + "derive_more 2.0.1", + "n0-error", + "n0-future", +] + +[[package]] +name = "naga" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" +dependencies = [ + "arrayvec", + "bit-set 0.8.0", + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.15.5", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "pp-rs", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.17", + "unicode-ident", +] + +[[package]] +name = "naga_oil" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b586d3cf5c9b7e13fe2af6e114406ff70773fd80881960378933b63e76f37dd" +dependencies = [ + "codespan-reporting", + "data-encoding", + "indexmap", + "naga", + "regex", + "rustc-hash 1.1.0", + "thiserror 2.0.17", + "tracing", + "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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "netdev" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ab878b4c90faf36dab10ea51d48c69ae9019bcca47c048a7c9b273d5d7a823" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +dependencies = [ + "bitflags 2.10.0", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.17", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f2acd376ef48b6c326abf3ba23c449e0cb8aa5c2511d189dd8a8a3bfac889b" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more 2.0.1", + "iroh-quinn-udp", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "pin-project-lite", + "serde", + "socket2 0.6.1", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result 0.4.1", + "wmi", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.16", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", + "serde", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "offset-allocator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e234d535da3521eb95106f40f0b73483d80bfb3aacf27c40d7e2b72f1a3e00a2" +dependencies = [ + "log", + "nonmax", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkarr" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "792c1328860f6874e90e3b387b4929819cc7783a6bd5a4728e918706eb436a48" +dependencies = [ + "async-compat", + "base32", + "bytes", + "cfg_aliases", + "document-features", + "dyn-clone", + "ed25519-dalek", + "futures-buffered", + "futures-lite", + "getrandom 0.3.4", + "log", + "lru 0.13.0", + "ntimestamp", + "reqwest", + "self_cell", + "serde", + "sha1_smol", + "simple-dns", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77089aec8290d0b7bb01b671b091095cf1937670725af4fd73d47249f03b12c0" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.9.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb78a635f75d76d856374961deecf61031c0b6f928c83dc9c0924ab6c019c298" +dependencies = [ + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portmapper" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b575f975dcf03e258b0c7ab3f81497d7124f508884c37da66a7314aa2a8d467" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more 2.0.1", + "futures-lite", + "futures-util", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "netwatch", + "num_enum", + "rand 0.9.2", + "serde", + "smallvec", + "socket2 0.6.1", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless 0.7.17", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "pp-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand 0.8.5", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radsort" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.2", +] + +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + +[[package]] +name = "rangemap" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "resolv-conf" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" +dependencies = [ + "base64 0.22.1", + "bitflags 2.10.0", + "serde", + "serde_derive", + "unicode-ident", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.10.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3ff3b81c8a6e381bc1673768141383f9328048a60edddcfc752a8291a138443" +dependencies = [ + "cfg-if", + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3ef0e35b322ddfaecbc60f34ab448e157e48531288ee49fafbb053696b8ffe2" +dependencies = [ + "base16ct", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e046edf639aa2e7afb285589e5405de2ef7e61d4b0ac1e30256e3eab911af9" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stackfuture" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eae92052b72ef70dafa16eddbabffc77e5ca3574be2f7bc1127b36f0a7ad7f2" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swarm-discovery" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790d8444f7db1e88f70aed3234cab8e42c48e05360bfc86ca7dce0d9a5d95d26" +dependencies = [ + "acto", + "hickory-proto", + "rand 0.9.2", + "socket2 0.5.10", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "swash" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync-macros" +version = "0.1.0" +dependencies = [ + "lib", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +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 = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4f4d046dd956a47a7e1a2947083d7ac3e6aa3cfaaead36173ceaa5ab11878c" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.3.4", + "http", + "httparse", + "rand 0.9.2", + "ring", + "rustls-pki-types", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-oslog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" +dependencies = [ + "cc", + "cfg-if", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "typewit" +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 = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-linebreak" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55be643b40a21558f44806b53ee9319595bc7ca6896372e4e08e5d7d83c9cd6" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.4", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wgpu" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" +dependencies = [ + "arrayvec", + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.15.5", + "log", + "naga", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f62f1053bd28c2268f42916f31588f81f64796e2ff91b81293515017ca8bd9" +dependencies = [ + "arrayvec", + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.10.0", + "cfg_aliases", + "document-features", + "hashbrown 0.15.5", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.17", + "wgpu-core-deps-apple", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18ae5fbde6a4cbebae38358aa73fcd6e0f15c6144b67ef5dc91ded0db125dbdf" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720a5cb9d12b3d337c15ff0e24d3e97ed11490ff3f7506e7f3d98c68fa5d6f14" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "26.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d0e67224cc7305b3b4eb2cc57ca4c4c3afc665c1d1bee162ea806e19c47bdd" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.8.0", + "bitflags 2.10.0", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types 0.2.0", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.15.5", + "libc", + "libloading", + "log", + "metal", + "naga", + "objc", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.17", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "js-sys", + "log", + "serde", + "thiserror 2.0.17", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +dependencies = [ + "android-activity", + "atomic-waker", + "bitflags 2.10.0", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "ndk", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smol_str 0.2.2", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wmi" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120d8c2b6a7c96c27bf4a7947fd7f02d73ca7f5958b8bd72a696e46cb5521ee6" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.17", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.2", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.10.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yazi" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive 0.8.1", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "z32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke 0.8.1", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke 0.8.1", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "indexmap", + "num_enum", + "thiserror 1.0.69", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3743093 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,51 @@ +[workspace] +members = ["crates/lib", "crates/server", "crates/client", "crates/sync-macros"] +resolver = "2" + +[workspace.package] +edition = "2024" + +[workspace.dependencies] +# Async runtime +tokio = { version = "1", features = ["full"] } +tokio-stream = "0.1" + +# Iroh - P2P networking and gossip +iroh = { version = "0.95.0",features = ["discovery-local-network"] } +iroh-gossip = "0.95.0" + +# Database +rusqlite = "0.37.0" + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.9" + +# Error handling +thiserror = "2.0" +anyhow = "1.0" + +# Date/time +chrono = { version = "0.4", features = ["serde"] } + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Random +rand = "0.8" + +# ML/AI +candle-core = "0.8" +candle-nn = "0.8" +candle-transformers = "0.8" +tokenizers = "0.20" +hf-hub = "0.3" + +# Bevy +bevy = "0.17" + +# Synchronization +parking_lot = "0.12" +crdts = "7.3" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..0e506a9 --- /dev/null +++ b/config.toml @@ -0,0 +1,18 @@ +[database] +path = "./us.db" +chat_db_path = "./crates/lib/chat.db" + +[services] +poll_interval_ms = 1000 +training_set_sample_rate = 0.05 + +[models] +embedding_model = "Qwen/Qwen3-Embedding-0.6B" +emotion_model = "SamLowe/roberta-base-go_emotions" + +[tailscale] +hostname = "lonni-daemon" +state_dir = "./tailscale-state" + +[grpc] +port = 50051 diff --git a/crates/client/.gitignore b/crates/client/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/crates/client/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml new file mode 100644 index 0000000..3d0fd81 --- /dev/null +++ b/crates/client/Cargo.toml @@ -0,0 +1,43 @@ +[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 new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crates/client/src/lib.rs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..925112e --- /dev/null +++ b/crates/client/src/main.rs @@ -0,0 +1,24 @@ +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/lib/.gitignore b/crates/lib/.gitignore new file mode 100644 index 0000000..64866cc --- /dev/null +++ b/crates/lib/.gitignore @@ -0,0 +1,4 @@ +/target +chat.db +*.db-shm +*.db-wal diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml new file mode 100644 index 0000000..f2a471b --- /dev/null +++ b/crates/lib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lib" +version = "0.1.0" +edition.workspace = true + +[dependencies] +rusqlite = { version = "0.37.0", features = ["bundled"] } +chrono = { version = "0.4", features = ["serde"] } +thiserror = "2.0" +serde = { version = "1.0", features = ["derive"] } +serde_json.workspace = true +crdts.workspace = true +anyhow.workspace = true +sync-macros = { path = "../sync-macros" } + +[dev-dependencies] +tokio.workspace = true +iroh.workspace = true +iroh-gossip.workspace = true +futures-lite = "2.0" diff --git a/crates/lib/src/db.rs b/crates/lib/src/db.rs new file mode 100644 index 0000000..fd19b99 --- /dev/null +++ b/crates/lib/src/db.rs @@ -0,0 +1,139 @@ +use crate::error::Result; +use crate::models::*; +use rusqlite::{Connection, OpenFlags, Row, params}; + +pub struct ChatDb { + conn: Connection, +} + +impl ChatDb { + /// Open a connection to the chat database in read-only mode + pub fn open(path: &str) -> Result { + let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?; + Ok(Self { conn }) + } + + /// Get messages from the conversation with +31 6 39 13 29 13 + /// + /// Returns messages from January 1, 2024 to present from the conversation + /// with the specified Dutch phone number. + /// + /// # Arguments + /// + /// * `start_date` - Start date (defaults to January 1, 2024 if None) + /// * `end_date` - End date (defaults to current time if None) + pub fn get_our_messages( + &self, + start_date: Option>, + end_date: Option>, + ) -> Result> { + use chrono::{TimeZone, Utc}; + + // Default date range: January 1, 2024 to now + let start = + start_date.unwrap_or_else(|| Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()); + let end = end_date.unwrap_or_else(|| Utc::now()); + + // Convert to Apple timestamps (nanoseconds since 2001-01-01) + let start_timestamp = datetime_to_apple_timestamp(start); + let end_timestamp = datetime_to_apple_timestamp(end); + + // The phone number might be stored with or without spaces + let phone_with_spaces = "+31 6 39 13 29 13"; + let phone_without_spaces = "+31639132913"; + + // Find the chat with this phone number (try both formats) + let chat = self + .get_chat_for_phone_number(phone_with_spaces) + .or_else(|_| self.get_chat_for_phone_number(phone_without_spaces))?; + + // Get messages from this chat within the date range + let mut stmt = self.conn.prepare( + "SELECT m.ROWID, m.guid, m.text, m.service, m.handle_id, m.date, m.date_read, m.date_delivered, + m.is_from_me, m.is_read, m.is_delivered, m.is_sent, m.is_emote, m.is_audio_message, + m.cache_has_attachments, m.associated_message_guid, m.associated_message_type, + m.thread_originator_guid, m.reply_to_guid, m.is_spam + FROM message m + INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id + WHERE cmj.chat_id = ? + AND m.date >= ? + AND m.date <= ? + ORDER BY m.date ASC" + )?; + + let messages = stmt + .query_map( + params![chat.rowid, start_timestamp, end_timestamp], + map_message_row, + )? + .collect::, _>>()?; + + Ok(messages) + } + + /// Helper function to find the largest chat with a specific phone number + fn get_chat_for_phone_number(&self, phone_number: &str) -> Result { + let mut stmt = self.conn.prepare( + "SELECT c.ROWID, c.guid, c.chat_identifier, c.service_name, c.display_name, + c.group_id, c.room_name, c.is_archived, c.is_filtered, + c.last_read_message_timestamp, COUNT(cmj.message_id) as msg_count + FROM chat c + INNER JOIN chat_handle_join chj ON c.ROWID = chj.chat_id + INNER JOIN handle h ON chj.handle_id = h.ROWID + INNER JOIN chat_message_join cmj ON c.ROWID = cmj.chat_id + WHERE h.id = ? + GROUP BY c.ROWID + ORDER BY msg_count DESC + LIMIT 1" + )?; + + let chat = stmt.query_row(params![phone_number], |row| { + Ok(Chat { + rowid: row.get(0)?, + guid: row.get(1)?, + chat_identifier: row.get(2)?, + service_name: row.get(3)?, + display_name: row.get(4)?, + group_id: row.get(5)?, + room_name: row.get(6)?, + is_archived: row.get::<_, i64>(7)? != 0, + is_filtered: row.get::<_, i64>(8)? != 0, + last_read_message_timestamp: row.get::<_, Option>(9)?.map(apple_timestamp_to_datetime), + }) + })?; + + Ok(chat) + } +} + +// Helper function to map database rows to structs +fn map_message_row(row: &Row) -> rusqlite::Result { + Ok(Message { + rowid: row.get(0)?, + guid: row.get(1)?, + text: row.get(2)?, + service: row.get(3)?, + handle_id: row.get(4)?, + date: row + .get::<_, Option>(5)? + .map(apple_timestamp_to_datetime), + date_read: row + .get::<_, Option>(6)? + .map(apple_timestamp_to_datetime), + date_delivered: row + .get::<_, Option>(7)? + .map(apple_timestamp_to_datetime), + is_from_me: row.get::<_, i64>(8)? != 0, + is_read: row.get::<_, i64>(9)? != 0, + is_delivered: row.get::<_, i64>(10)? != 0, + is_sent: row.get::<_, i64>(11)? != 0, + is_emote: row.get::<_, i64>(12)? != 0, + is_audio_message: row.get::<_, i64>(13)? != 0, + cache_has_attachments: row.get::<_, i64>(14)? != 0, + associated_message_guid: row.get(15)?, + associated_message_type: row.get(16)?, + thread_originator_guid: row.get(17)?, + reply_to_guid: row.get(18)?, + is_spam: row.get::<_, i64>(19)? != 0, + }) +} diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs new file mode 100644 index 0000000..37521ed --- /dev/null +++ b/crates/lib/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ChatDbError { + #[error("Database error: {0}")] + Database(#[from] rusqlite::Error), + + #[error("Not found: {0}")] + NotFound(String), + + #[error("Invalid data: {0}")] + InvalidData(String), +} + +pub type Result = std::result::Result; diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs new file mode 100644 index 0000000..3daaab2 --- /dev/null +++ b/crates/lib/src/lib.rs @@ -0,0 +1,30 @@ +//! Data access layer for iMessage chat.db +//! +//! This library provides a read-only interface to query messages from a specific conversation. +//! +//! # Safety +//! +//! All database connections are opened in read-only mode to prevent any +//! accidental modifications to your iMessage database. +//! +//! # Example +//! +//! ```no_run +//! use lib::ChatDb; +//! +//! let db = ChatDb::open("chat.db")?; +//! +//! // Get all messages from January 2024 to now +//! let messages = db.get_our_messages(None, None)?; +//! println!("Found {} messages", messages.len()); +//! # Ok::<(), lib::ChatDbError>(()) +//! ``` + +mod error; +mod models; +mod db; +pub mod sync; + +pub use error::{ChatDbError, Result}; +pub use models::{Message, Chat}; +pub use db::ChatDb; diff --git a/crates/lib/src/models.rs b/crates/lib/src/models.rs new file mode 100644 index 0000000..69af7fd --- /dev/null +++ b/crates/lib/src/models.rs @@ -0,0 +1,112 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Represents a message in the iMessage database +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub rowid: i64, + pub guid: String, + pub text: Option, + pub service: Option, + pub handle_id: i64, + pub date: Option>, + pub date_read: Option>, + pub date_delivered: Option>, + pub is_from_me: bool, + pub is_read: bool, + pub is_delivered: bool, + pub is_sent: bool, + pub is_emote: bool, + pub is_audio_message: bool, + pub cache_has_attachments: bool, + pub associated_message_guid: Option, + pub associated_message_type: i64, + pub thread_originator_guid: Option, + pub reply_to_guid: Option, + pub is_spam: bool, +} + +/// Represents a chat/conversation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Chat { + pub rowid: i64, + pub guid: String, + pub chat_identifier: Option, + pub service_name: Option, + pub display_name: Option, + pub group_id: Option, + pub room_name: Option, + pub is_archived: bool, + pub is_filtered: bool, + pub last_read_message_timestamp: Option>, +} + +/// Helper function to convert Apple's Cocoa timestamp (seconds since 2001-01-01) to DateTime +pub fn apple_timestamp_to_datetime(timestamp: i64) -> DateTime { + // Apple's Cocoa timestamps are in nanoseconds since 2001-01-01 00:00:00 UTC + // Convert to Unix timestamp (seconds since 1970-01-01 00:00:00 UTC) + const APPLE_EPOCH_OFFSET: i64 = 978307200; // Seconds between 1970-01-01 and 2001-01-01 + + let seconds = timestamp / 1_000_000_000 + APPLE_EPOCH_OFFSET; + let nanos = (timestamp % 1_000_000_000) as u32; + + DateTime::from_timestamp(seconds, nanos).unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap()) +} + +/// Helper function to convert DateTime to Apple's Cocoa timestamp +pub fn datetime_to_apple_timestamp(dt: DateTime) -> i64 { + const APPLE_EPOCH_OFFSET: i64 = 978307200; + + let unix_timestamp = dt.timestamp(); + let nanos = dt.timestamp_subsec_nanos() as i64; + + (unix_timestamp - APPLE_EPOCH_OFFSET) * 1_000_000_000 + nanos +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Datelike, TimeZone, Timelike}; + + #[test] + fn test_apple_timestamp_to_datetime_zero() { + let dt = apple_timestamp_to_datetime(0); + assert_eq!(dt.year(), 2001); + assert_eq!(dt.month(), 1); + assert_eq!(dt.day(), 1); + assert_eq!(dt.hour(), 0); + assert_eq!(dt.minute(), 0); + assert_eq!(dt.second(), 0); + } + + #[test] + fn test_apple_timestamp_to_datetime_known_value() { + let timestamp = 694224000000000000i64; + let dt = apple_timestamp_to_datetime(timestamp); + assert_eq!(dt.year(), 2023); + assert_eq!(dt.month(), 1); + assert_eq!(dt.day(), 1); + } + + #[test] + fn test_apple_timestamp_roundtrip() { + let original = 694224000000000000i64; + let dt = apple_timestamp_to_datetime(original); + let converted_back = datetime_to_apple_timestamp(dt); + assert_eq!(original, converted_back); + } + + #[test] + fn test_datetime_to_apple_timestamp_epoch() { + let dt = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).unwrap(); + let timestamp = datetime_to_apple_timestamp(dt); + assert_eq!(timestamp, 0); + } + + #[test] + fn test_negative_apple_timestamp() { + let timestamp = -31536000000000000i64; + let dt = apple_timestamp_to_datetime(timestamp); + assert_eq!(dt.year(), 2000); + } +} diff --git a/crates/lib/src/sync.rs b/crates/lib/src/sync.rs new file mode 100644 index 0000000..ae76b9a --- /dev/null +++ b/crates/lib/src/sync.rs @@ -0,0 +1,165 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +// Re-export the macros +pub use sync_macros::{synced, Synced}; + +// Re-export common CRDT types from the crdts library +pub use crdts::{ + ctx::ReadCtx, + lwwreg::LWWReg, + map::Map, + orswot::Orswot, + CmRDT, CvRDT, +}; + +pub type NodeId = String; + +/// Transparent wrapper for synced values +/// +/// This wraps any value with LWW semantics but allows you to use it like a normal value +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyncedValue { + value: T, + timestamp: DateTime, + node_id: NodeId, +} + +impl SyncedValue { + pub fn new(value: T, node_id: NodeId) -> Self { + Self { + value, + timestamp: Utc::now(), + node_id, + } + } + + pub fn get(&self) -> &T { + &self.value + } + + pub fn set(&mut self, value: T, node_id: NodeId) { + self.value = value; + self.timestamp = Utc::now(); + self.node_id = node_id; + } + + pub fn apply_lww(&mut self, value: T, timestamp: DateTime, node_id: NodeId) { + if timestamp > self.timestamp || (timestamp == self.timestamp && node_id > self.node_id) { + self.value = value; + self.timestamp = timestamp; + self.node_id = node_id; + } + } + + pub fn merge(&mut self, other: &Self) { + self.apply_lww(other.value.clone(), other.timestamp, other.node_id.clone()); + } +} + +// Allow transparent access to the inner value +impl Deref for SyncedValue { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl DerefMut for SyncedValue { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +/// Wrapper for a sync message that goes over gossip +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyncMessage { + /// Unique message ID + pub message_id: String, + /// Node that sent this + pub node_id: NodeId, + /// When it was sent + pub timestamp: DateTime, + /// The actual sync operation + pub operation: T, +} + +impl SyncMessage { + pub fn new(node_id: NodeId, operation: T) -> Self { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let seq = COUNTER.fetch_add(1, Ordering::SeqCst); + + Self { + message_id: format!("{}-{}-{}", node_id, Utc::now().timestamp_millis(), seq), + node_id, + timestamp: Utc::now(), + operation, + } + } + + pub fn to_bytes(&self) -> anyhow::Result> { + Ok(serde_json::to_vec(self)?) + } +} + +impl Deserialize<'de>> SyncMessage { + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + Ok(serde_json::from_slice(bytes)?) + } +} + +/// Helper trait for types that can be synced +pub trait Syncable: Sized { + type Operation: Serialize + for<'de> Deserialize<'de> + Clone; + + /// Apply a sync operation to this value + fn apply_sync_op(&mut self, op: &Self::Operation); + + /// Get the node ID for this instance + fn node_id(&self) -> &NodeId; + + /// Create a sync message for an operation + fn create_sync_message(&self, op: Self::Operation) -> SyncMessage { + SyncMessage::new(self.node_id().clone(), op) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_synced_value() { + let mut val = SyncedValue::new(42, "node1".to_string()); + assert_eq!(*val.get(), 42); + + val.set(100, "node1".to_string()); + assert_eq!(*val.get(), 100); + + // Test LWW semantics + let old_time = Utc::now() - chrono::Duration::seconds(10); + val.apply_lww(50, old_time, "node2".to_string()); + assert_eq!(*val.get(), 100); // Should not update with older timestamp + } + + #[test] + fn test_sync_message() { + #[derive(Debug, Clone, Serialize, Deserialize)] + struct TestOp { + value: i32, + } + + let op = TestOp { value: 42 }; + let msg = SyncMessage::new("node1".to_string(), op); + + let bytes = msg.to_bytes().unwrap(); + let decoded = SyncMessage::::from_bytes(&bytes).unwrap(); + + assert_eq!(decoded.node_id, "node1"); + assert_eq!(decoded.operation.value, 42); + } +} diff --git a/crates/lib/tests/our_messages_test.rs b/crates/lib/tests/our_messages_test.rs new file mode 100644 index 0000000..d92a367 --- /dev/null +++ b/crates/lib/tests/our_messages_test.rs @@ -0,0 +1,98 @@ +use lib::{ChatDb, Result}; +use chrono::Datelike; + +/// Test that we can get messages from the Dutch phone number conversation +#[test] +fn test_get_our_messages_default_range() -> Result<()> { + let db = ChatDb::open("chat.db")?; + + // Get messages from January 2024 to now (default) + let messages = db.get_our_messages(None, None)?; + + println!("Found {} messages from January 2024 to now", messages.len()); + + // Verify we got some messages + assert!(messages.len() > 0, "Should find messages in the conversation"); + + // Verify messages are in chronological order (ASC) + for i in 1..messages.len().min(10) { + if let (Some(prev_date), Some(curr_date)) = (messages[i-1].date, messages[i].date) { + assert!( + prev_date <= curr_date, + "Messages should be in ascending date order" + ); + } + } + + // Verify all messages are from 2024 or later + for msg in messages.iter().take(10) { + if let Some(date) = msg.date { + assert!(date.year() >= 2024, "Messages should be from 2024 or later"); + println!("Message date: {}, from_me: {}, text: {:?}", + date, msg.is_from_me, msg.text.as_ref().map(|s| &s[..s.len().min(50)])); + } + } + + Ok(()) +} + +/// Test that we can get messages with a custom date range +#[test] +fn test_get_our_messages_custom_range() -> Result<()> { + use chrono::{TimeZone, Utc}; + + let db = ChatDb::open("chat.db")?; + + // Get messages from March 2024 to June 2024 + let start = Utc.with_ymd_and_hms(2024, 3, 1, 0, 0, 0).unwrap(); + let end = Utc.with_ymd_and_hms(2024, 6, 1, 0, 0, 0).unwrap(); + + let messages = db.get_our_messages(Some(start), Some(end))?; + + println!("Found {} messages from March to June 2024", messages.len()); + + // Verify all messages are within the date range + for msg in &messages { + if let Some(date) = msg.date { + assert!( + date >= start && date <= end, + "Message date {} should be between {} and {}", + date, start, end + ); + } + } + + Ok(()) +} + +/// Test displaying a summary of the conversation +#[test] +fn test_conversation_summary() -> Result<()> { + let db = ChatDb::open("chat.db")?; + + let messages = db.get_our_messages(None, None)?; + + println!("\n=== Conversation Summary ==="); + println!("Total messages: {}", messages.len()); + + let from_me = messages.iter().filter(|m| m.is_from_me).count(); + let from_them = messages.len() - from_me; + + println!("From me: {}", from_me); + println!("From them: {}", from_them); + + // Show first few messages + println!("\nFirst 5 messages:"); + for (i, msg) in messages.iter().take(5).enumerate() { + if let Some(date) = msg.date { + let sender = if msg.is_from_me { "Me" } else { "Them" }; + let text = msg.text.as_ref() + .map(|t| if t.len() > 60 { format!("{}...", &t[..60]) } else { t.clone() }) + .unwrap_or_else(|| "[No text]".to_string()); + + println!("{}. {} ({}): {}", i + 1, date.format("%Y-%m-%d %H:%M"), sender, text); + } + } + + Ok(()) +} diff --git a/crates/lib/tests/sync_integration.rs b/crates/lib/tests/sync_integration.rs new file mode 100644 index 0000000..317cefa --- /dev/null +++ b/crates/lib/tests/sync_integration.rs @@ -0,0 +1,157 @@ +use lib::sync::{synced, SyncMessage, Syncable}; +use iroh::{Endpoint, protocol::{Router, ProtocolHandler, AcceptError}}; +use anyhow::Result; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Test configuration that can be synced +#[synced] +struct TestConfig { + value: i32, + name: String, + + #[sync(skip)] + node_id: String, +} + +/// ALPN identifier for our sync protocol +const SYNC_ALPN: &[u8] = b"/lonni/sync/1"; + +/// Protocol handler for receiving sync messages +#[derive(Debug, Clone)] +struct SyncProtocol { + config: Arc>, +} + +impl ProtocolHandler for SyncProtocol { + async fn accept(&self, connection: iroh::endpoint::Connection) -> Result<(), AcceptError> { + println!("Accepting connection from: {}", connection.remote_id()); + + // Accept the bidirectional stream + let (mut send, mut recv) = connection.accept_bi().await + .map_err(AcceptError::from_err)?; + + println!("Stream accepted, reading message..."); + + // Read the sync message + let bytes = recv.read_to_end(1024 * 1024).await + .map_err(AcceptError::from_err)?; + + println!("Received {} bytes", bytes.len()); + + // Deserialize and apply + let msg = SyncMessage::::from_bytes(&bytes) + .map_err(|e| AcceptError::from_err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?; + + println!("Applying operation from node: {}", msg.node_id); + + let mut config = self.config.lock().await; + config.apply_op(&msg.operation); + + println!("Operation applied successfully"); + + // Close the stream + send.finish() + .map_err(AcceptError::from_err)?; + + Ok(()) + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_sync_between_two_nodes() -> Result<()> { + println!("\n=== Testing Sync Between Two Nodes ===\n"); + + // Create two endpoints + let node1 = Endpoint::builder().bind().await?; + let node2 = Endpoint::builder().bind().await?; + + let node1_addr = node1.addr(); + let node2_addr = node2.addr(); + + let node1_id = node1_addr.id.to_string(); + let node2_id = node2_addr.id.to_string(); + + println!("Node 1: {}", node1_id); + println!("Node 2: {}", node2_id); + + // Create synced configs on both nodes + let mut config1 = TestConfig::new( + 42, + "initial".to_string(), + node1_id.clone(), + ); + + let config2 = TestConfig::new( + 42, + "initial".to_string(), + node2_id.clone(), + ); + let config2_shared = Arc::new(Mutex::new(config2)); + + println!("\nInitial state:"); + println!(" Node 1: value={}, name={}", config1.value(), config1.name()); + { + let config2 = config2_shared.lock().await; + println!(" Node 2: value={}, name={}", config2.value(), config2.name()); + } + + // Set up router on node2 to accept incoming connections + println!("\nSetting up node2 router..."); + let protocol = SyncProtocol { + config: config2_shared.clone(), + }; + let router = Router::builder(node2) + .accept(SYNC_ALPN, protocol) + .spawn(); + + router.endpoint().online().await; + println!("✓ Node2 router ready"); + + // Node 1 changes the value + println!("\nNode 1 changing value to 100..."); + let op = config1.set_value(100); + + // Serialize the operation + let sync_msg = SyncMessage::new(node1_id.clone(), op); + let bytes = sync_msg.to_bytes()?; + println!("Serialized to {} bytes", bytes.len()); + + // Establish QUIC connection from node1 to node2 + println!("\nEstablishing QUIC connection..."); + let conn = node1.connect(node2_addr.clone(), SYNC_ALPN).await?; + println!("✓ Connection established"); + + // Open a bidirectional stream + let (mut send, _recv) = conn.open_bi().await?; + + // Send the sync message + println!("Sending sync message..."); + send.write_all(&bytes).await?; + send.finish()?; + println!("✓ Message sent"); + + // Wait a bit for the message to be processed + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + // Verify both configs have the same value + println!("\nFinal state:"); + println!(" Node 1: value={}, name={}", config1.value(), config1.name()); + { + let config2 = config2_shared.lock().await; + println!(" Node 2: value={}, name={}", config2.value(), config2.name()); + + assert_eq!(*config1.value(), 100); + assert_eq!(*config2.value(), 100); + assert_eq!(config1.name(), "initial"); + assert_eq!(config2.name(), "initial"); + } + + println!("\n✓ Sync successful!"); + + // Cleanup + router.shutdown().await?; + node1.close().await; + + Ok(()) +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml new file mode 100644 index 0000000..55787eb --- /dev/null +++ b/crates/server/Cargo.toml @@ -0,0 +1,58 @@ +[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 new file mode 100644 index 0000000..a44d83c --- /dev/null +++ b/crates/server/src/assets/mod.rs @@ -0,0 +1 @@ +// Asset loading and management will go here diff --git a/crates/server/src/components/database.rs b/crates/server/src/components/database.rs new file mode 100644 index 0000000..eeca181 --- /dev/null +++ b/crates/server/src/components/database.rs @@ -0,0 +1,14 @@ +use bevy::prelude::*; +use parking_lot::Mutex; +use rusqlite::Connection; +use std::sync::Arc; + +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 new file mode 100644 index 0000000..e460888 --- /dev/null +++ b/crates/server/src/components/gossip.rs @@ -0,0 +1,87 @@ +use bevy::prelude::*; +use iroh::protocol::Router; +use iroh::Endpoint; +use iroh_gossip::api::{GossipReceiver, GossipSender}; +use iroh_gossip::net::Gossip; +use iroh_gossip::proto::TopicId; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// 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 new file mode 100644 index 0000000..16be00a --- /dev/null +++ b/crates/server/src/components/mod.rs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..aeb295d --- /dev/null +++ b/crates/server/src/config.rs @@ -0,0 +1,84 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; + +#[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 new file mode 100644 index 0000000..be92c6e --- /dev/null +++ b/crates/server/src/db/mod.rs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..8427121 --- /dev/null +++ b/crates/server/src/db/operations.rs @@ -0,0 +1,321 @@ +use crate::db::schema::{deserialize_embedding, serialize_embedding}; +use crate::models::*; +use chrono::{TimeZone, Utc}; +use rusqlite::{params, Connection, OptionalExtension, Result, Row}; + +/// 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 new file mode 100644 index 0000000..7b79067 --- /dev/null +++ b/crates/server/src/db/schema.rs @@ -0,0 +1,207 @@ +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 new file mode 100644 index 0000000..01d81c2 --- /dev/null +++ b/crates/server/src/entities/mod.rs @@ -0,0 +1 @@ +// Entity builders and spawners will go here diff --git a/crates/server/src/iroh_sync.rs b/crates/server/src/iroh_sync.rs new file mode 100644 index 0000000..2af93f4 --- /dev/null +++ b/crates/server/src/iroh_sync.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use iroh::protocol::Router; +use iroh::Endpoint; +use iroh_gossip::api::{GossipReceiver, GossipSender}; +use iroh_gossip::net::Gossip; +use iroh_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 new file mode 100644 index 0000000..162ca2d --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,96 @@ +mod assets; +mod components; +mod config; +mod db; +mod entities; +mod iroh_sync; +mod models; +mod services; +mod systems; + +use anyhow::{Context, Result}; +use bevy::prelude::*; +use config::Config; +use iroh_gossip::proto::TopicId; +use parking_lot::Mutex; +use rusqlite::Connection; +use std::path::Path; +use std::sync::Arc; + +// Re-export init function +pub use iroh_sync::init_iroh_gossip; + +// Import components and systems +use components::*; +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 new file mode 100644 index 0000000..6f6df77 --- /dev/null +++ b/crates/server/src/models.rs @@ -0,0 +1,60 @@ +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 new file mode 100644 index 0000000..6298375 --- /dev/null +++ b/crates/server/src/proto/emotions.proto @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000..4e0a15b --- /dev/null +++ b/crates/server/src/services/chat_poller.rs @@ -0,0 +1,121 @@ +use crate::db; +use anyhow::{Context, Result}; +use chrono::Utc; +use rusqlite::Connection; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{mpsc, Mutex}; +use tokio::time; +use tracing::{debug, error, info, warn}; + +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 new file mode 100644 index 0000000..8371502 --- /dev/null +++ b/crates/server/src/services/embedding_service.rs @@ -0,0 +1,110 @@ +use crate::db; +use anyhow::Result; +use rusqlite::Connection; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use tracing::{error, info, warn}; + +/// 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 new file mode 100644 index 0000000..6831b05 --- /dev/null +++ b/crates/server/src/services/emotion_service.rs @@ -0,0 +1,119 @@ +use crate::db; +use anyhow::Result; +use rusqlite::Connection; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use tracing::{error, info, warn}; + +/// 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 new file mode 100644 index 0000000..b17f7eb --- /dev/null +++ b/crates/server/src/services/grpc_server.rs @@ -0,0 +1,232 @@ +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 new file mode 100644 index 0000000..15ea300 --- /dev/null +++ b/crates/server/src/services/mod.rs @@ -0,0 +1,7 @@ +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; \ No newline at end of file diff --git a/crates/server/src/sync_plugin.rs b/crates/server/src/sync_plugin.rs new file mode 100644 index 0000000..5ca3185 --- /dev/null +++ b/crates/server/src/sync_plugin.rs @@ -0,0 +1,114 @@ +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 new file mode 100644 index 0000000..71ff22e --- /dev/null +++ b/crates/server/src/systems/database.rs @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..3828b81 --- /dev/null +++ b/crates/server/src/systems/gossip.rs @@ -0,0 +1,116 @@ +use bevy::prelude::*; +use parking_lot::Mutex; +use std::sync::Arc; + +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 new file mode 100644 index 0000000..4cefcce --- /dev/null +++ b/crates/server/src/systems/mod.rs @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..abeb34f --- /dev/null +++ b/crates/server/src/systems/setup.rs @@ -0,0 +1,22 @@ +use bevy::prelude::*; +use bevy::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)"); +} diff --git a/crates/sync-macros/Cargo.toml b/crates/sync-macros/Cargo.toml new file mode 100644 index 0000000..e65a2d3 --- /dev/null +++ b/crates/sync-macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sync-macros" +version = "0.1.0" +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" + +[dev-dependencies] +lib = { path = "../lib" } diff --git a/crates/sync-macros/src/lib.rs b/crates/sync-macros/src/lib.rs new file mode 100644 index 0000000..820af4d --- /dev/null +++ b/crates/sync-macros/src/lib.rs @@ -0,0 +1,345 @@ +use proc_macro::TokenStream; +use quote::{quote, format_ident}; +use syn::{parse_macro_input, DeriveInput, Data, Fields, Type, ItemStruct}; + +/// Attribute macro for transparent CRDT sync +/// +/// Transforms your struct to use CRDTs internally while keeping the API simple. +/// +/// # Example +/// ``` +/// #[synced] +/// struct EmotionGradientConfig { +/// canvas_width: f32, // Becomes SyncedValue internally +/// canvas_height: f32, // Auto-generates getters/setters +/// +/// #[sync(skip)] +/// node_id: String, // Not synced +/// } +/// +/// // Use it like a normal struct: +/// let mut config = EmotionGradientConfig::new("node1".into()); +/// config.set_canvas_width(1024.0); // Auto-generates sync operation +/// println!("Width: {}", config.canvas_width()); // Transparent access +/// ``` +#[proc_macro_attribute] +pub fn synced(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemStruct); + let name = &input.ident; + let vis = &input.vis; + let op_enum_name = format_ident!("{}Op", name); + + let fields = match &input.fields { + Fields::Named(fields) => &fields.named, + _ => panic!("synced only supports structs with named fields"), + }; + + let mut internal_fields = Vec::new(); + let mut field_getters = Vec::new(); + let mut field_setters = Vec::new(); + let mut op_variants = Vec::new(); + let mut apply_arms = Vec::new(); + let mut merge_code = Vec::new(); + let mut new_params = Vec::new(); + let mut new_init = Vec::new(); + + for field in fields { + let field_name = field.ident.as_ref().unwrap(); + let field_vis = &field.vis; + let field_type = &field.ty; + + // Check if field should be skipped + let should_skip = field.attrs.iter().any(|attr| { + attr.path().is_ident("sync") + && attr + .parse_args::() + .map(|i| i == "skip") + .unwrap_or(false) + }); + + if should_skip { + // Keep as-is, no wrapping + internal_fields.push(quote! { + #field_vis #field_name: #field_type + }); + new_params.push(quote! { #field_name: #field_type }); + new_init.push(quote! { #field_name }); + continue; + } + + // Wrap in SyncedValue + internal_fields.push(quote! { + #field_name: lib::sync::SyncedValue<#field_type> + }); + + // Generate getter + field_getters.push(quote! { + #field_vis fn #field_name(&self) -> &#field_type { + self.#field_name.get() + } + }); + + // Generate setter that returns operation + let setter_name = format_ident!("set_{}", field_name); + let op_variant = format_ident!( + "Set{}", + field_name + .to_string() + .chars() + .enumerate() + .map(|(i, c)| if i == 0 { + c.to_ascii_uppercase() + } else { + c + }) + .collect::() + ); + + field_setters.push(quote! { + #field_vis fn #setter_name(&mut self, value: #field_type) -> #op_enum_name { + let op = #op_enum_name::#op_variant { + value: value.clone(), + timestamp: chrono::Utc::now(), + node_id: self.node_id().clone(), + }; + self.#field_name.set(value, self.node_id().clone()); + op + } + }); + + // Generate operation variant + op_variants.push(quote! { + #op_variant { + value: #field_type, + timestamp: chrono::DateTime, + node_id: String, + } + }); + + // Generate apply arm + apply_arms.push(quote! { + #op_enum_name::#op_variant { value, timestamp, node_id } => { + self.#field_name.apply_lww(value.clone(), timestamp.clone(), node_id.clone()); + } + }); + + // Generate merge code + merge_code.push(quote! { + self.#field_name.merge(&other.#field_name); + }); + + // Add to new() parameters + new_params.push(quote! { #field_name: #field_type }); + new_init.push(quote! { + #field_name: lib::sync::SyncedValue::new(#field_name, node_id.clone()) + }); + } + + let expanded = quote! { + /// Sync operations enum + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + #[serde(tag = "type")] + #vis enum #op_enum_name { + #(#op_variants),* + } + + impl #op_enum_name { + pub fn to_bytes(&self) -> anyhow::Result> { + Ok(serde_json::to_vec(self)?) + } + + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + Ok(serde_json::from_slice(bytes)?) + } + } + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + #vis struct #name { + #(#internal_fields),* + } + + impl #name { + #vis fn new(#(#new_params),*) -> Self { + Self { + #(#new_init),* + } + } + + /// Transparent field accessors + #(#field_getters)* + + /// Field setters that generate sync operations + #(#field_setters)* + + /// Apply a sync operation from another node + #vis fn apply_op(&mut self, op: &#op_enum_name) { + match op { + #(#apply_arms),* + } + } + + /// Merge state from another instance + #vis fn merge(&mut self, other: &Self) { + #(#merge_code)* + } + } + + impl lib::sync::Syncable for #name { + type Operation = #op_enum_name; + + fn apply_sync_op(&mut self, op: &Self::Operation) { + self.apply_op(op); + } + + fn node_id(&self) -> &lib::sync::NodeId { + // Assume there's a node_id field marked with #[sync(skip)] + &self.node_id + } + } + }; + + TokenStream::from(expanded) +} + +/// Old derive macro - kept for backwards compatibility +#[proc_macro_derive(Synced, attributes(sync))] +pub fn derive_synced(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let op_enum_name = format_ident!("{}Op", name); + + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => panic!("Synced only supports structs with named fields"), + }, + _ => panic!("Synced only supports structs"), + }; + + let mut field_ops = Vec::new(); + let mut apply_arms = Vec::new(); + let mut setter_methods = Vec::new(); + let mut merge_code = Vec::new(); + + for field in fields { + let field_name = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + // Check if field should be skipped + let should_skip = field.attrs.iter() + .any(|attr| { + attr.path().is_ident("sync") && + attr.parse_args::() + .map(|i| i == "skip") + .unwrap_or(false) + }); + + if should_skip { + continue; + } + + let op_variant = format_ident!("Set{}", + field_name.to_string() + .chars() + .enumerate() + .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c }) + .collect::() + ); + + let setter_name = format_ident!("set_{}", field_name); + + // Determine CRDT strategy based on type + let crdt_strategy = get_crdt_strategy(field_type); + + match crdt_strategy.as_str() { + "lww" => { + // LWW for simple types + field_ops.push(quote! { + #op_variant { + value: #field_type, + timestamp: chrono::DateTime, + node_id: String, + } + }); + + apply_arms.push(quote! { + #op_enum_name::#op_variant { value, timestamp, node_id } => { + self.#field_name.apply_lww(value.clone(), timestamp.clone(), node_id.clone()); + } + }); + + setter_methods.push(quote! { + pub fn #setter_name(&mut self, value: #field_type) -> #op_enum_name { + let op = #op_enum_name::#op_variant { + value: value.clone(), + timestamp: chrono::Utc::now(), + node_id: self.node_id().clone(), + }; + self.#field_name = lib::sync::SyncedValue::new(value, self.node_id().clone()); + op + } + }); + + merge_code.push(quote! { + self.#field_name.merge(&other.#field_name); + }); + } + _ => { + // Default to LWW + } + } + } + + let expanded = quote! { + /// Auto-generated sync operations enum + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + #[serde(tag = "type")] + pub enum #op_enum_name { + #(#field_ops),* + } + + impl #op_enum_name { + pub fn to_bytes(&self) -> anyhow::Result> { + Ok(serde_json::to_vec(self)?) + } + + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + Ok(serde_json::from_slice(bytes)?) + } + } + + impl #name { + /// Apply a sync operation from another node + pub fn apply_op(&mut self, op: &#op_enum_name) { + match op { + #(#apply_arms),* + } + } + + /// Merge state from another instance + pub fn merge(&mut self, other: &Self) { + #(#merge_code)* + } + + /// Auto-generated setter methods that create sync ops + #(#setter_methods)* + } + + impl lib::sync::Syncable for #name { + type Operation = #op_enum_name; + + fn apply_sync_op(&mut self, op: &Self::Operation) { + self.apply_op(op); + } + } + }; + + TokenStream::from(expanded) +} + +/// Determine CRDT strategy based on field type +fn get_crdt_strategy(_ty: &Type) -> String { + // For now, default everything to LWW + // TODO: Detect HashMap -> use Map, Vec -> use ORSet, etc. + "lww".to_string() +} diff --git a/docs/rfcs/0001-crdt-gossip-sync.md b/docs/rfcs/0001-crdt-gossip-sync.md new file mode 100644 index 0000000..445258e --- /dev/null +++ b/docs/rfcs/0001-crdt-gossip-sync.md @@ -0,0 +1,1653 @@ +# RFC 0001: CRDT Synchronization Protocol over iroh-gossip + +**Status:** Approved +**Authors:** Sienna +**Created:** 2025-11-15 +**Updated:** 2025-11-15 + +## Abstract + +This RFC proposes a gossip-based CRDT synchronization protocol for building eventually-consistent multiplayer collaborative applications. The protocol enables 2-5 concurrent users to collaborate in real-time on shared entities and resources with support for presence, cursors, selections, and drawing operations. + +## Motivation + +We want to build a multiplayer collaborative system where: +- Two people can draw/edit together and see each other's changes in real-time +- Changes work offline and sync automatically when reconnected +- There's no "server" - all peers are equal +- State is eventually consistent across all peers +- We can see cursors, presence, selections, etc. + +### Why iroh-gossip? + +**Message bus > point-to-point**: In a 3-person session, gossip naturally handles the "multicast" pattern better than explicit peer-to-peer connections. Each message gets epidemic-broadcast to all peers efficiently. + +**Built-in mesh networking**: iroh-gossip handles peer discovery and maintains the mesh topology automatically. We just subscribe to a topic. + +**QUIC underneath**: Low latency, multiplexed, encrypted by default. + +### Why CRDTs? + +**The core problem**: When two people edit the same thing simultaneously, how do we merge the changes without coordination? + +**CRDT solution**: Operations are designed to be commutative (order doesn't matter), associative (grouping doesn't matter), and idempotent (re-applying is safe). This means: +- No locks needed +- No coordination needed +- All peers converge to same state eventually +- Works offline naturally + +## High-Level Architecture + +```mermaid +graph TB + User[User Actions] --> Bevy[Bevy ECS] + Bevy --> Detect[Change Detection] + Detect --> CRDT[CRDT Operations] + CRDT --> OpLog[Operation Log] + CRDT --> VClock[Vector Clock] + CRDT --> Bincode[bincode Serialization] + Bincode --> Gossip[iroh-gossip] + + Gossip --> Receive[Receive Messages] + Receive --> Deserialize[bincode Deserialize] + Deserialize --> Validate[Validate Vector Clock] + Validate --> Apply[Apply CRDT Ops] + Apply --> Bevy + + OpLog -.-> AntiEntropy[Anti-Entropy] + AntiEntropy --> Gossip + + SQLite[(SQLite)] -.-> Persist[Persistence Layer] + Persist -.-> Bevy + Bevy -.-> Persist + + style Gossip fill:#f9f,stroke:#333,stroke-width:2px + style CRDT fill:#bbf,stroke:#333,stroke-width:2px + style OpLog fill:#bfb,stroke:#333,stroke-width:2px +``` + +**The flow:** +1. User does something (moves object, draws stroke, selects item) +2. Bevy system detects the change (using `Changed` queries) +3. Generate CRDT operation describing the change +4. Log operation to operation log (for anti-entropy) +5. Update vector clock (increment our sequence number) +6. Serialize with bincode +7. Broadcast via gossip +8. All peers (including us) receive and apply the operation +9. State converges + +### System States + +```mermaid +stateDiagram-v2 + [*] --> Initializing + Initializing --> LoadingPersistence: Load SQLite + LoadingPersistence --> Joining: Connect to gossip + Joining --> RequestingState: Broadcast JoinRequest + RequestingState --> Syncing: Receive FullState + Syncing --> Synchronized: Apply complete + Synchronized --> Synchronized: Normal operations + Synchronized --> Partitioned: Network disconnect + Partitioned --> Rejoining: Network restored + Rejoining --> AntiEntropy: Compare clocks + AntiEntropy --> Synchronized: Catchup complete + Synchronized --> [*]: Shutdown +``` + +## Entity-Component-System Model + +We're building on Bevy's ECS, but networked entities need special handling: + +**The UUID problem**: Bevy's `Entity` is just an incrementing ID local to that instance. If two peers create entities, they'll have conflicting IDs. + +**Our solution**: Separate network identity from ECS identity: + +```rust +#[derive(Component)] +struct NetworkedEntity { + network_id: Uuid, // Stable across all peers + is_local: bool, // Did we create this? + owner_node_id: NodeId, // Who created it originally? +} + +// Maintained by sync system +struct NetworkEntityMap { + map: HashMap, // network UUID → Bevy Entity +} +``` + +When we receive an update for UUID `abc-123`: +- Look it up in the map +- If it exists: update the existing Entity +- If not: spawn a new Entity and add to map + +## CRDT Types We Need + +Different types of data need different CRDT semantics: + +### Last-Write-Wins (LWW) +**For**: Simple fields where newest value should win (position, color, size) + +**Concept**: Attach timestamp to each write. When merging, keep the newer one. If timestamps equal, use node ID as tiebreaker. + +```rust +struct SyncedValue { + value: T, + timestamp: DateTime, + node_id: NodeId, +} + +fn merge(&mut self, other: &Self) { + if other.timestamp > self.timestamp + || (other.timestamp == self.timestamp && other.node_id > self.node_id) { + self.value = other.value.clone(); + self.timestamp = other.timestamp; + self.node_id = other.node_id.clone(); + } +} +``` + +### OR-Set (Observed-Remove Set) +**For**: Collections where items can be added/removed (selections, tags, participants) + +**Concept**: Each add gets a unique token. Removes specify which add tokens to remove. This prevents the "add-remove" problem where concurrent operations conflict. + +**Example**: If Alice adds "red" and Bob removes "red" concurrently, we need to know if Bob saw Alice's add or not. + +```rust +// From crdts library +let mut set: Orswot = Default::default(); + +// Adding +let add_ctx = set.read_ctx().derive_add_ctx("alice"); +set.apply(set.add("red".to_string(), add_ctx)); + +// Removing +let rm_ctx = set.read_ctx().derive_rm_ctx("bob"); +set.apply(set.rm("red".to_string(), rm_ctx)); +``` + +### Map CRDT +**For**: Nested key-value data (metadata, properties, nested objects) + +**Concept**: Each key can have its own CRDT semantics. The map handles add/remove of keys. + +```rust +// From crdts library +let mut map: Map, NodeId> = Map::new(); + +// Update a key +let ctx = map.read_ctx().derive_add_ctx("alice"); +map.apply(map.update("score".to_string(), ctx, |reg, actor| { + reg.write(100, actor) +})); +``` + +### Sequence CRDT (List/RGA) +**For**: Ordered collections (drawing paths, text, ordered lists) + +**Concept**: Each insertion gets a unique ID that includes ordering information. Maintains causal order even with concurrent edits. + +**Use case**: Collaborative drawing paths where two people add points simultaneously. + +### Counter +**For**: Increment/decrement operations (likes, votes, reference counts) + +**Concept**: Each node tracks its own increment/decrement deltas. Total is the sum of all deltas. + +```rust +// PN-Counter (positive-negative) +struct Counter { + increments: HashMap, + decrements: HashMap, +} + +fn value(&self) -> i64 { + let pos: u64 = self.increments.values().sum(); + let neg: u64 = self.decrements.values().sum(); + pos as i64 - neg as i64 +} +``` + +## Message Protocol + +We need a few types of messages: + +### 1. JoinRequest +When a new peer joins, they need the current state. + +```rust +enum SyncMessage { + JoinRequest { + node_id: NodeId, + vector_clock: VectorClock, // Usually empty on first join + }, + // ... +} +``` + +### 2. FullState +Response to join - here's everything you need: + +```rust +FullState { + entities: HashMap, // All entities + components + resources: HashMap, // Global singletons + vector_clock: VectorClock, // Current causality state +} +``` + +### 3. EntityDelta +Incremental update when something changes: + +```rust +EntityDelta { + entity_id: Uuid, + component_ops: Vec, // Operations to apply + vector_clock: VectorClock, +} +``` + +### 4. Presence & Cursor (Ephemeral) +High-frequency, not persisted: + +```rust +Presence { + node_id: NodeId, + user_info: UserInfo, // Name, color, avatar + ttl_ms: u64, // Auto-expire after 5 seconds +} + +Cursor { + node_id: NodeId, + position: (f32, f32), +} +``` + +### Serialization: bincode + +**Why bincode**: Fast, compact, type-safe with serde. + +**Caveat**: No schema evolution. If we change the message format, old and new clients can't talk. + +**Solution**: Version envelope: + +```rust +#[derive(Serialize, Deserialize)] +struct VersionedMessage { + version: u32, + payload: Vec, // bincode-serialized SyncMessage +} +``` + +If we see version 2 and we're version 1, we can gracefully reject or upgrade. + +## Vector Clocks: Tracking Causality + +**The problem**: How do we know if we're missing messages? How do we detect conflicts? + +**Solution**: Vector clocks - each node tracks "what operations has each peer seen?" + +```rust +struct VectorClock { + versions: HashMap, // node → sequence number +} +``` + +**Example**: +- Alice's clock: `{alice: 5, bob: 3}` +- Bob's clock: `{alice: 4, bob: 4}` + +This tells us: +- Alice has seen her own ops 1-5 +- Alice has seen Bob's ops 1-3 +- Bob has seen Alice's ops 1-4 (missing Alice's op 5!) +- Bob has seen his own ops 1-4 + +So we know Bob is missing Alice's operation #5. + +**Key operations**: + +```rust +// Increment when we create an operation +clock.increment(&local_node_id); + +// Merge when we receive a message +clock.merge(&received_clock); + +// Check causality +if their_clock.happened_before(&our_clock) { + // They're behind, we have newer info +} + +if our_clock.is_concurrent_with(&their_clock) { + // Conflict! Need CRDT merge semantics +} +``` + +## Synchronization Flow + +### On Join (New Peer) + +```mermaid +sequenceDiagram + participant NewPeer + participant Gossip + participant ExistingPeer + participant Database + + NewPeer->>Gossip: JoinRequest
{node_id, vector_clock: {}} + Gossip-->>ExistingPeer: Broadcast + + ExistingPeer->>Database: Query all entities + Database-->>ExistingPeer: Return entities + + ExistingPeer->>ExistingPeer: Collect resources + ExistingPeer->>ExistingPeer: Build FullState + + ExistingPeer->>Gossip: FullState
{entities, resources, vector_clock} + Gossip-->>NewPeer: Receive + + NewPeer->>NewPeer: Spawn entities in ECS + NewPeer->>NewPeer: Apply resources + NewPeer->>NewPeer: Update vector clock + NewPeer->>NewPeer: Mark as Synchronized + + Note over NewPeer: Now ready for delta updates +``` + +**Key insights**: +- Multiple peers may respond to a JoinRequest +- The joining peer should pick the FullState with the most advanced vector clock +- After applying FullState, immediately broadcast SyncRequest to catch any deltas that happened during transfer +- Anti-entropy will repair any remaining inconsistencies + +**Improved join algorithm**: +```rust +async fn join_session(gossip: &GossipHandle) -> Result<()> { + // Broadcast join request + gossip.broadcast(SyncMessage::JoinRequest { + node_id: local_node_id(), + vector_clock: VectorClock::new(), + }).await?; + + // Collect FullState responses for a short window (500ms) + let mut responses = Vec::new(); + let deadline = Instant::now() + Duration::from_millis(500); + + while Instant::now() < deadline { + if let Ok(msg) = timeout_until(deadline, gossip.recv()).await { + if let SyncMessage::FullState { .. } = msg { + responses.push(msg); + } + } + } + + // Pick the most up-to-date response (highest vector clock) + let best_state = responses.into_iter() + .max_by_key(|r| r.vector_clock.total_operations()) + .ok_or("No FullState received")?; + + // Apply the state + apply_full_state(best_state)?; + + // Immediately request any deltas that happened during the transfer + gossip.broadcast(SyncMessage::SyncRequest { + vector_clock: our_current_clock(), + }).await?; + + Ok(()) +} +``` + +**Why this works**: +- We don't rely on "first response wins" (which could be stale) +- We select the peer with the most complete state +- We immediately catch up on any operations that happened during the state transfer +- Anti-entropy continues to fill any remaining gaps + +### During Operation (Real-time) + +```mermaid +sequenceDiagram + participant User + participant Bevy + participant OpLog + participant VClock + participant Gossip + participant RemotePeer + + User->>Bevy: Move entity + Bevy->>Bevy: Changed query fires + + Bevy->>VClock: Increment local counter + VClock-->>Bevy: New sequence number + + Bevy->>Bevy: Create ComponentOp::Set + + Bevy->>OpLog: Log operation + OpLog-->>Bevy: Stored at (node_id, seq) + + Bevy->>Gossip: EntityDelta
{entity_id, ops, vector_clock} + + Gossip-->>Bevy: Echo back (local receive) + Bevy->>Bevy: Skip (own operation) + + Gossip-->>RemotePeer: Broadcast + RemotePeer->>RemotePeer: Deserialize + RemotePeer->>VClock: Merge vector clocks + RemotePeer->>RemotePeer: Apply CRDT operation + RemotePeer->>Bevy: Update ECS component + + Note over Bevy,RemotePeer: Both peers now have same state +``` + +**Performance note**: We echo our own operations back to maintain consistency in the async/sync boundary. + +### Anti-Entropy (Periodic Catchup) + +```mermaid +sequenceDiagram + participant PeerA + participant OpLog_A + participant Gossip + participant PeerB + participant OpLog_B + + Note over PeerA,PeerB: Every 5 seconds + + PeerA->>Gossip: SyncRequest
{vector_clock: {A:10, B:5}} + Gossip-->>PeerB: Receive + + PeerB->>PeerB: Compare clocks
{A:10, B:5} vs {A:8, B:7} + Note over PeerB: PeerA missing: B[6,7]
PeerB missing: A[9,10] + + PeerB->>OpLog_B: Query operations
where node_id=B AND seq IN (6,7) + OpLog_B-->>PeerB: Return ops + + PeerB->>Gossip: MissingDeltas
[B:6, B:7] + Gossip-->>PeerA: Receive + + PeerA->>PeerA: Apply missing operations + PeerA->>OpLog_A: Store in log + + Note over PeerA: Now has B:6, B:7 + + PeerA->>OpLog_A: Query operations
where node_id=A AND seq IN (9,10) + OpLog_A-->>PeerA: Return ops + + PeerA->>Gossip: MissingDeltas
[A:9, A:10] + Gossip-->>PeerB: Receive + + PeerB->>PeerB: Apply missing operations + + Note over PeerA,PeerB: Clocks now match: {A:10, B:7} +``` + +**Why this works**: +- Vector clocks tell us exactly what's missing +- Operation log preserves operations for catchup +- Anti-entropy repairs any message loss +- Eventually all peers converge + +## Operation Log: The Anti-Entropy Engine + +The operation log is critical for ensuring eventual consistency. It's an append-only log of all CRDT operations that allows peers to catch up after network partitions or disconnections. + +### Structure + +```rust +struct OperationLogEntry { + // Primary key: (node_id, sequence_number) + node_id: NodeId, + sequence_number: u64, + + // The actual operation + operation: Delta, // EntityDelta or ResourceDelta + + // When this operation was created + timestamp: DateTime, + + // How many bytes (for storage metrics) + size_bytes: u32, +} +``` + +### Storage Schema (SQLite) + +```sql +CREATE TABLE operation_log ( + -- Composite primary key ensures uniqueness per node + node_id TEXT NOT NULL, + sequence_number INTEGER NOT NULL, + + -- Operation type for filtering + operation_type TEXT NOT NULL, -- 'EntityDelta' | 'ResourceDelta' + + -- The serialized Delta (bincode) + operation_blob BLOB NOT NULL, + + -- Timestamp for pruning + created_at INTEGER NOT NULL, + + -- Size tracking + size_bytes INTEGER NOT NULL, + + PRIMARY KEY (node_id, sequence_number) +); + +-- Index for time-based queries (pruning old operations) +CREATE INDEX idx_operation_log_timestamp +ON operation_log(created_at); + +-- Index for node lookups (common query pattern) +CREATE INDEX idx_operation_log_node +ON operation_log(node_id, sequence_number); +``` + +### Write Path + +Every time we generate an operation: + +```mermaid +graph LR + A[Generate Op] --> B[Increment Vector Clock] + B --> C[Serialize Operation] + C --> D[Write to OpLog Table] + D --> E[Broadcast via Gossip] + + style D fill:#bfb,stroke:#333,stroke-width:2px +``` + +**Atomicity**: The vector clock increment and operation log write should be atomic. If we fail after incrementing the clock but before logging, we'll have a gap in the sequence. + +**Solution**: Use SQLite transaction: +```sql +BEGIN TRANSACTION; +UPDATE vector_clock SET counter = counter + 1 WHERE node_id = ?; +INSERT INTO operation_log (...) VALUES (...); +COMMIT; +``` + +### Query Path (Anti-Entropy) + +When we receive a SyncRequest with a vector clock that's behind ours: + +```mermaid +graph TB + A[Receive SyncRequest] --> B{Compare Clocks} + B -->|Behind| C[Calculate Missing Ranges] + C --> D[Query OpLog] + D --> E[Serialize MissingDeltas] + E --> F[Broadcast to Peer] + + style D fill:#bfb,stroke:#333,stroke-width:2px +``` + +**Query example**: +```sql +-- Peer is missing operations from node 'alice' between 5-10 +SELECT operation_blob +FROM operation_log +WHERE node_id = 'alice' + AND sequence_number BETWEEN 5 AND 10 +ORDER BY sequence_number ASC; +``` + +**Batch limits**: If the gap is too large (>1000 operations?), might be faster to send FullState instead. + +### Pruning Strategy + +We can't keep operations forever. Three pruning strategies: + +#### 1. Time-based Pruning (Simple) + +Delete operations older than 24 hours: + +```sql +DELETE FROM operation_log +WHERE created_at < (strftime('%s', 'now') - 86400) * 1000; +``` + +**Pros**: Simple, predictable storage +**Cons**: Peers offline for >24 hours need full resync + +#### 2. Vector Clock-based Pruning (Smart) + +Only delete operations that **all known peers have seen**: + +```mermaid +graph TB + A[Collect All Peer Clocks] --> B[Find Minimum Seq Per Node] + B --> C[Delete Below Minimum] + + style C fill:#fbb,stroke:#333,stroke-width:2px +``` + +**Example**: +- Alice's clock: `{alice: 100, bob: 50}` +- Bob's clock: `{alice: 80, bob: 60}` +- Minimum: `{alice: 80, bob: 50}` +- Can delete: Alice's ops 1-79, Bob's ops 1-49 + +**Pros**: Maximally efficient storage +**Cons**: Complex, requires tracking peer clocks + +#### 3. Hybrid (Recommended) + +- Delete ops older than 24 hours AND seen by all peers +- Keep at least last 1000 operations per node (safety buffer) + +```sql +WITH peer_min AS ( + SELECT node_id, MIN(counter) as min_seq + FROM vector_clock + GROUP BY node_id +) +DELETE FROM operation_log +WHERE (node_id, sequence_number) IN ( + SELECT ol.node_id, ol.sequence_number + FROM operation_log ol + JOIN peer_min pm ON ol.node_id = pm.node_id + WHERE ol.sequence_number < pm.min_seq + AND ol.created_at < (strftime('%s', 'now') - 86400) * 1000 + AND ol.sequence_number < ( + SELECT MAX(sequence_number) - 1000 + FROM operation_log ol2 + WHERE ol2.node_id = ol.node_id + ) +); +``` + +### Operation Log Cleanup Methodology + +The cleanup methodology defines **when**, **how**, and **what** to delete while maintaining correctness. + +#### Triggering Conditions + +Cleanup triggers (whichever comes first): +- **Time-based**: Every 1 hour +- **Storage threshold**: When log exceeds 50 MB +- **Operation count**: When exceeding 100,000 ops +- **Manual trigger**: Via admin API + +**Why multiple triggers?**: Different workloads have different patterns. Active sessions hit operation count first, idle sessions hit time first. + +#### Cleanup Algorithm + +Five-phase process ensures safety: + +```mermaid +graph TB + A[Cleanup Triggered] --> B[Phase 1: Collect Metadata] + B --> C[Phase 2: Calculate Safety Bounds] + C --> D[Phase 3: Identify Candidates] + D --> E[Phase 4: Delete Operations] + E --> F[Phase 5: Verify & Log] + F --> G[Cleanup Complete] + + style B fill:#bbf,stroke:#333,stroke-width:2px + style C fill:#fbb,stroke:#333,stroke-width:2px + style D fill:#bfb,stroke:#333,stroke-width:2px + style E fill:#fbb,stroke:#333,stroke-width:2px +``` + +**Phase 1: Collect Metadata** - Query vector clocks, operation stats, peer activity + +**Phase 2: Calculate Safety Bounds** - For each node, compute the safe deletion watermark by taking the minimum of: +1. Min sequence number all peers have seen (from vector clock) +2. Operations older than 24 hours +3. Safety buffer (keep last 1000 operations) +4. Active peer protection (keep last 5000 if peer active in last hour) + +**Phase 3: Identify Candidates** - Generate deletion query per node based on watermarks + +**Phase 4: Delete Operations** - Execute in SQLite transaction with pre/post audit trail + +**Phase 5: Verify & Log** - Validate no gaps in sequences, emit metrics + +#### Safety Mechanisms + +**Dry-run mode**: Preview deletions without executing + +**Maximum deletion limit**: Never delete >50% of operations in one run (prevents catastrophic bugs) + +**Active peer protection**: Keep more operations for recently active peers (5000 vs 1000) + +**Cleanup lock**: Prevent concurrent cleanup runs + +**Emergency stop**: Can kill running cleanup mid-execution + +#### Scheduling + +**Background task**: Low-priority, runs hourly by default + +**Adaptive scheduling**: Adjust frequency based on activity rate (30min for high activity, 4 hours for low) + +**Time-of-day awareness**: Prefer 2-4 AM for cleanup (low activity period) + +#### Key Edge Cases + +**Peer rejoins after long absence**: If requested operations were pruned, send FullState instead of deltas + +**Cleanup during partition**: Detect partition (no peer contact for >1 hour) and increase safety buffer to 10,000 operations + +**Corrupted vector clock**: Validate clock before cleanup; skip if invalid and trigger anti-entropy + +**Out of disk space**: Check available space before running (require 100MB minimum) + +### Recovery Scenarios + +#### Scenario 1: Clean Restart + +```mermaid +sequenceDiagram + participant App + participant Database + participant Gossip + + App->>Database: Load vector clock + Database-->>App: {alice: 100, bob: 50} + + App->>Database: Load entities/resources + Database-->>App: Current state + + App->>Gossip: Connect and join + App->>Gossip: Broadcast SyncRequest
{clock: {alice:100, bob:50}} + + Note over App: Catchup via anti-entropy +``` + +**Key**: We persist the vector clock, so we know exactly where we left off. + +#### Scenario 2: Corrupted State + +If the database is corrupted but operation log is intact: + +```mermaid +graph TB + A[Detect Corruption] --> B[Clear entity tables] + B --> C[Keep operation_log table] + C --> D[Reset vector clock to local: 0] + D --> E[Broadcast JoinRequest] + E --> F[Receive FullState] + F --> G[Replay ALL operations from log] + G --> H[Rebuild vector clock] + + style G fill:#fbb,stroke:#333,stroke-width:2px +``` + +**Replay**: +```sql +SELECT operation_blob +FROM operation_log +ORDER BY node_id, sequence_number ASC; +``` + +Apply each operation in order. This rebuilds the entire state deterministically. + +#### Scenario 3: Network Partition + +```mermaid +sequenceDiagram + participant PeerA + participant Network + participant PeerB + + Note over PeerA,PeerB: Working normally + + Network->>Network: Partition! + + PeerA->>PeerA: Continue working
(operations logged locally) + PeerB->>PeerB: Continue working
(operations logged locally) + + Note over PeerA,PeerB: Hours pass... + + Network->>Network: Partition healed + + PeerA->>PeerB: SyncRequest
{A:150, B:50} + Note over PeerB: B is at {A:100, B:120} + + PeerB->>PeerA: MissingDeltas [A:101-150] + PeerA->>PeerB: MissingDeltas [B:51-120] + + PeerA->>PeerA: Apply B's operations + PeerB->>PeerB: Apply A's operations + + Note over PeerA,PeerB: CRDTs merge conflicting changes + Note over PeerA,PeerB: Both converge to same state +``` + +**CRDTs save us**: Even though both peers made conflicting changes, the CRDT merge semantics ensure they converge. + +### Storage Estimation + +For capacity planning: + +**Average operation size**: +- LWW Set: ~100 bytes (component type + value + metadata) +- OR-Set Add: ~80 bytes +- Sequence Insert: ~60 bytes + +**Rate estimation** (2 users actively collaborating): +- User actions: ~10 ops/minute +- Total: ~20 ops/minute +- Per hour: 1,200 operations +- 24 hours: 28,800 operations +- Storage: ~2.88 MB per day + +**With pruning** (24 hour window): Steady state ~3 MB + +**Worst case** (5 users, very active): ~15 MB per day, prune to ~15 MB steady state + +### Monitoring + +Track these metrics: + +```rust +struct OperationLogMetrics { + total_operations: u64, + operations_by_node: HashMap, + oldest_operation_age: Duration, + total_size_bytes: u64, + operations_pruned_last_hour: u64, +} +``` + +**Alerts**: +- Operation log >100 MB (pruning not working?) +- Gap in sequence numbers (missed operations?) +- Oldest operation >48 hours (peer not catching up?) + +## Persistence Strategy + +Not everything needs to be saved to disk: + +**Persistent (SQLite)**: +- Entities and components marked `#[persist(true)]` +- Resources (workspace config, document metadata) +- **Operation log** (detailed above - critical for anti-entropy) +- Vector clock state + +**Ephemeral (in-memory only)**: +- Presence updates (who's online) +- Cursor positions +- Selections/highlights +- Components marked `#[persist(false)]` + +**Why hybrid?**: Cursors at 60Hz would blow up the database. They're only relevant during the session. + +### Database Schema Summary + +```sql +-- Entities +CREATE TABLE entities ( + network_id TEXT PRIMARY KEY, + owner_node_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Components (one row per component per entity) +CREATE TABLE components ( + entity_id TEXT NOT NULL, + component_type TEXT NOT NULL, + data BLOB NOT NULL, -- bincode ComponentData + updated_at INTEGER NOT NULL, + PRIMARY KEY (entity_id, component_type), + FOREIGN KEY (entity_id) REFERENCES entities(network_id) +); + +-- Resources (global singletons) +CREATE TABLE resources ( + resource_id TEXT PRIMARY KEY, + resource_type TEXT NOT NULL, + data BLOB NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Operation log (see detailed schema above) +CREATE TABLE operation_log (...); + +-- Vector clock (persisted state) +CREATE TABLE vector_clock ( + node_id TEXT PRIMARY KEY, + counter INTEGER NOT NULL +); +``` + +## iroh-gossip Integration + +The actual gossip API is simple: + +### Setup +```rust +use iroh_gossip::{net::Gossip, proto::TopicId}; + +// Create gossip instance +let gossip = Gossip::builder().spawn(endpoint); + +// Subscribe to topic +let topic = TopicId::from_bytes(*b"global-sync-topic!!!!!"); +let handle = gossip.subscribe(topic, bootstrap_peers).await?; +let (sender, receiver) = handle.split(); +``` + +### Broadcast +```rust +let msg = SyncMessage::EntityDelta { /* ... */ }; +let bytes = bincode::serialize(&VersionedMessage { version: 1, payload: bincode::serialize(&msg)? })?; + +sender.broadcast(bytes.into()).await?; +``` + +### Receive +```rust +use futures_lite::StreamExt; + +while let Some(event) = receiver.next().await { + match event? { + Event::Received(msg) => { + let versioned: VersionedMessage = bincode::deserialize(&msg.content)?; + let sync_msg: SyncMessage = bincode::deserialize(&versioned.payload)?; + // Apply to ECS... + } + _ => {} + } +} +``` + +## Bevy Integration Strategy + +**The async problem**: Bevy is sync, gossip is async. + +**Solution**: Channel bridge: + +```rust +// In async task +let (tx, rx) = mpsc::unbounded_channel(); + +tokio::spawn(async move { + while let Some(event) = receiver.next().await { + // Send to Bevy via channel + tx.send(event).ok(); + } +}); + +// In Bevy system +fn poll_gossip_system( + mut channel: ResMut, + mut events: EventWriter, +) { + while let Ok(msg) = channel.rx.try_recv() { + events.send(GossipReceived(msg)); + } +} +``` + +**Change detection**: + +```rust +fn broadcast_changes_system( + query: Query<(&NetworkedEntity, &Transform), Changed>, + gossip_tx: Res, + mut vector_clock: ResMut, +) { + for (net_entity, transform) in query.iter() { + vector_clock.increment(&local_node_id); + + let op = ComponentOp::Set { + component_type: "Transform", + value: bincode::serialize(transform)?, + timestamp: Utc::now().timestamp_micros(), + node_id: local_node_id.clone(), + }; + + let msg = SyncMessage::EntityDelta { + entity_id: net_entity.network_id, + component_ops: vec![op], + vector_clock: vector_clock.clone(), + }; + + gossip_tx.send(msg).ok(); + } +} +``` + +## Multiplayer Features + +### Presence (Who's Online) + +**Concept**: Heartbeat every 1 second, 5 second TTL. If we don't hear from someone for 5 seconds, they're gone. + +**Why ephemeral**: Presence is only meaningful during the session. No need to persist. + +### Cursors (60Hz) + +**Challenge**: High frequency, low importance. Can't flood gossip. + +**Solution**: Rate limit to 60Hz, send position only (tiny message), ephemeral. + +**Rendering**: Spawn a sprite entity per remote peer, update position from cursor messages. + +### Selections (What's Selected) + +**Challenge**: Multiple people selecting overlapping sets of items. + +**Solution**: OR-Set CRDT. Each selection is a set of UUIDs. Merge sets using OR-Set semantics. + +**Visual**: Highlight selected items with user's color. + +### Drawing (Collaborative Strokes) + +**Challenge**: Two people drawing simultaneously, paths interleave. + +**Solution**: Each stroke is an entity. Path is a Sequence CRDT. Points get unique IDs with ordering info. + +**Result**: Both strokes appear correctly, points stay in order per-stroke. + +## Performance Considerations + +### Message Batching +Collect changes over 16ms (one frame), send one batched message instead of many tiny ones. + +### Rate Limiting +Cursors: max 60Hz +Presence: max 1Hz +Changes: best-effort (as fast as user actions) + +### Message Size +iroh-gossip has practical limits (~1MB per message). For large states, chunk them. + +## Trade-offs and Decisions + +### Why Global Topic, Not Per-Entity? + +**Global**: Simpler, works great for 2-5 users, less topic management +**Per-entity**: Scales better, more complex, probably overkill for our use case + +**Decision**: Start with global, can shard later if needed. + +### Why bincode, Not JSON? + +**bincode**: Faster, smaller, type-safe +**JSON**: Human-readable, easier debugging, schema evolution + +**Decision**: bincode with version envelope. We can switch encoding later without changing wire protocol (just bump version). + +### Why LWW for Simple Fields? + +**Alternative**: Operational transforms, per-field CRDTs + +**Decision**: LWW is simple and works for most use cases. Timestamp conflicts are rare. For critical data (where "last write wins" isn't good enough), use proper CRDTs. + +### Why Operation Log? + +**Alternative**: State-based sync only (send full state periodically) + +**Decision**: Hybrid - operations for real-time (small), full state for joins (comprehensive), operation log for anti-entropy (reliable). + +## Security and Identity + +Even for a private, controlled deployment, the protocol must define security boundaries to prevent accidental data corruption and ensure system integrity. + +### Authentication: Cryptographic Node Identity + +**Problem**: As written, NodeId could be any string. A buggy client or malicious peer could forge messages with another node's ID, corrupting vector clocks and CRDT state. + +**Solution**: Use cryptographic identities built into iroh. + +```rust +use iroh::NodeId; // iroh's NodeId is already a cryptographic public key + +// Each peer has a keypair +// NodeId is derived from the public key +// All gossip messages are implicitly authenticated by QUIC's transport layer +``` + +**Key properties**: +- NodeId = public key (or hash thereof) +- iroh-gossip over QUIC already provides message authenticity (peer signatures) +- No additional signing needed - the transport guarantees messages come from the claimed NodeId + +**Implementation**: Just use iroh's `NodeId` type consistently instead of `String`. + +### Authorization: Session Membership + +**Problem**: How do we ensure only authorized peers can join a session and access the data? + +**Solution**: Pre-shared topic secret (simplest for 2-5 users). + +```rust +// Generate a session secret when creating a new document/workspace +use iroh_gossip::proto::TopicId; +use blake3; + +let session_secret = generate_random_bytes(32); // Share out-of-band +let topic = TopicId::from_bytes(blake3::hash(&session_secret).as_bytes()); + +// To join, a peer must know the session_secret +// They derive the same TopicId and subscribe +``` + +**Access control**: +- Secret is shared via invite link, QR code, or manual entry +- Without the secret, you can't derive the TopicId, so you can't join the gossip +- For better UX, encrypt the secret into a shareable URL: `lonni://join/` + +**Revocation**: If someone leaves the session and shouldn't have access: +- Generate a new secret and topic +- Broadcast a "migration" message on the old topic +- All peers move to the new topic +- The revoked peer is left on the old topic (no data flow) + +### Encryption: Application-Level Data Protection + +**Question**: Is the gossip topic itself private, or could passive observers see it? + +**Answer**: iroh-gossip over QUIC is transport-encrypted, but: +- Any peer in the gossip network can see message content +- If you don't trust all peers (or future peers after revocation), add application-level encryption + +**Proposal** (for future consideration): +```rust +// Encrypt FullState and EntityDelta messages with session key derived from session_secret +let encrypted_payload = encrypt_aead( + &session_secret, + &bincode::serialize(&sync_msg)? +)?; +``` + +**For v1**: Skip application-level encryption. Transport encryption + topic secret is sufficient for a private, trusted peer group. + +### Resource Limits: Preventing Abuse + +**Problem**: What if a buggy client creates 100,000 entities or floods with operations? + +**Solution**: Rate limiting and quotas. + +```rust +struct RateLimits { + max_entities_per_node: usize, // Default: 10,000 + max_ops_per_second: usize, // Default: 100 + max_entity_size_bytes: usize, // Default: 1 MB + max_component_count_per_entity: usize, // Default: 50 +} + +// When receiving an EntityDelta: +fn apply_delta(&mut self, delta: EntityDelta) -> Result<(), RateLimitError> { + // Check if this node has exceeded entity quota + let node_entity_count = self.entities_by_owner(&delta.source_node).count(); + if node_entity_count >= self.limits.max_entities_per_node { + return Err(RateLimitError::TooManyEntities); + } + + // Check operation rate (requires tracking ops/sec per node) + if self.operation_rate_exceeds_limit(&delta.source_node) { + return Err(RateLimitError::TooManyOperations); + } + + // Apply the operation... +} +``` + +**Handling violations**: +- Log a warning +- Ignore the operation (don't apply) +- Optionally: display a warning to the user ("Peer X is sending too many operations") + +**Why this works**: CRDTs allow us to safely ignore operations. Other peers will still converge correctly. + +## Entity Deletion and Garbage Collection + +**Problem**: Without deletion, the dataset grows indefinitely. Users can't remove unwanted entities. + +**Solution**: Tombstones (the standard CRDT deletion pattern). + +### Deletion Protocol + +**Strategy**: Despawn from ECS, keep tombstone in database only. + +When an entity is deleted: +1. **Despawn from Bevy's ECS** - The entity is removed from the world entirely using `despawn_recursive()` +2. **Store tombstone in SQLite** - Record the deletion (entity ID, timestamp, node ID) in a `deleted_entities` table +3. **Remove from network map** - Clean up the UUID → Entity mapping +4. **Broadcast deletion operation** - Send a "Deleted" component operation via gossip so other peers delete it too + +**Why this works:** +- **Zero footguns**: Deleted entities don't exist in ECS, so queries can't accidentally find them. No need to remember `Without` on every query. +- **Better performance**: ECS doesn't waste time iterating over tombstones during queries. +- **Tombstones live where they belong**: In the persistent database, not cluttering the real-time ECS. + +**Receiving deletions from peers:** + +When we receive a deletion operation: +1. Check if the entity exists in our ECS (via the network map) +2. If yes: despawn it, record tombstone, remove from map +3. If no: just record the tombstone (peer is catching us up on something we never spawned) + +**Receiving operations for deleted entities:** + +When we receive an operation for an entity UUID that's not in our ECS map: +1. Check the database for a tombstone +2. If tombstone exists and operation timestamp < deletion timestamp: **Ignore** (stale operation) +3. If tombstone exists and operation timestamp > deletion timestamp: **Resurrection conflict** - log warning, potentially notify user +4. If no tombstone: Spawn the entity normally (we're just learning about it for the first time) + +### Garbage Collection: Operation Log Only + +**Critical rule**: Never delete entity rows from the database. Only garbage collect their operation log entries. + +Tombstones (the `Deleted` component and the entity row) must be kept **forever**. They're tiny (just metadata) and prevent the resurrection problem. + +**What gets garbage collected**: +- Operation log entries for deleted entities (once all peers have seen the deletion) +- Blob data referenced by deleted entities (see Blob Garbage Collection section) + +**What never gets deleted**: +- Entity rows (even with `Deleted` component) +- The `Deleted` component itself + +**Operation log cleanup for deleted entities**: +```sql +-- During operation log cleanup, we CAN delete operations for deleted entities +-- once all peers have acknowledged the deletion +WITH peer_min AS ( + SELECT node_id, MIN(counter) as min_seq + FROM vector_clock + GROUP BY node_id +), +deleted_entities AS ( + SELECT e.network_id, e.owner_node_id, c.updated_at as deleted_at + FROM entities e + JOIN components c ON e.network_id = c.entity_id + WHERE c.component_type = 'Deleted' + AND c.updated_at < (strftime('%s', 'now') - 86400) * 1000 -- Deleted >24h ago +) +DELETE FROM operation_log +WHERE (node_id, sequence_number) IN ( + SELECT ol.node_id, ol.sequence_number + FROM operation_log ol + JOIN deleted_entities de ON ol.entity_id = de.network_id + JOIN peer_min pm ON ol.node_id = pm.node_id + WHERE ol.sequence_number < pm.min_seq -- All peers have seen this operation +); +``` + +**Why keep entity rows forever?**: +- Prevents resurrection conflicts (see next section) +- Tombstone size is negligible (~100 bytes per entity) +- Allows us to detect late operations and handle them correctly + +### Edge Case: Resurrection + +What if a peer deletes an entity, but a partitioned peer still has the old version? + +```mermaid +sequenceDiagram + participant Alice + participant Bob + + Note over Alice,Bob: Both have entity E + + Alice->>Alice: Delete E (add Deleted component) + Note over Bob: Network partition (offline) + + Alice->>Alice: Garbage collect E + Note over Alice: E is gone from database + + Bob->>Bob: Edit E (change position) + Note over Bob: Reconnects + + Bob->>Alice: EntityDelta for E + + Alice->>Alice: Entity E doesn't exist!
Recreate from delta? Or ignore? +``` + +**Solution**: Never garbage collect entities. Only garbage collect their operation log entries. Keep the entity row and the `Deleted` component forever (they're tiny - just metadata). + +This way, if a late operation arrives for a deleted entity, we can detect the conflict: +- If `Deleted` timestamp > operation timestamp: Ignore (operation predates deletion) +- If operation timestamp > `Deleted` timestamp: Resurrection! Re-add the entity (or warn the user) + +## Large Binary Data + +**Problem**: iroh-gossip has practical message size limits (~1MB). Syncing a 5MB image via gossip would saturate the network and fail. + +**Solution**: Use iroh-blobs for large binary data, gossip for metadata. + +### Two-Tier Architecture + +```mermaid +graph TB + User[User adds image] --> Upload[Upload to iroh-blobs] + Upload --> Hash[Get blob hash] + Hash --> Metadata[Create component with hash] + Metadata --> Gossip[Broadcast metadata via gossip] + + Gossip --> Peer[Remote peer] + Peer --> Check{Has blob?} + Check -->|No| Fetch[Fetch from iroh-blobs] + Check -->|Yes| Render[Render image] + Fetch --> Render +``` + +### Protocol + +**Adding a large file**: +```rust +// 1. Add blob to iroh-blobs +let hash = blobs.add_bytes(image_data).await?; + +// 2. Create a component that references the blob +let image_component = ImageComponent { + blob_hash: hash.to_string(), + width: 1024, + height: 768, + mime_type: "image/png".to_string(), +}; + +// 3. Broadcast a tiny metadata operation via gossip +let op = ComponentOp::Set { + component_type: "Image", + value: bincode::serialize(&image_component)?, + timestamp: Utc::now().timestamp_micros(), + node_id: local_node_id.clone(), +}; +``` + +**Receiving the metadata**: +```rust +// Peer receives the EntityDelta with Image component +async fn apply_image_component(component: ImageComponent) { + let hash = Hash::from_str(&component.blob_hash)?; + + // Check if we already have the blob + if !blobs.has(hash).await? { + // Fetch it from any peer that has it + blobs.download(hash).await?; + } + + // Now we can load and render the image + let image_data = blobs.read_to_bytes(hash).await?; + // ... render in Bevy +} +``` + +### Blob Discovery + +iroh-blobs has built-in peer discovery and transfer. As long as peers are connected (via gossip or direct connections), they can find and fetch blobs from each other. + +**Optimization**: When broadcasting the metadata, include a "providers" list: +```rust +struct ImageComponent { + blob_hash: String, + providers: Vec, // Which peers currently have this blob + width: u32, + height: u32, +} +``` + +This helps peers find the blob faster. + +### Threshold + +Define a threshold for what goes through blobs vs gossip: + +```rust +const MAX_INLINE_SIZE: usize = 64 * 1024; // 64 KB + +fn serialize_component(component: &Component) -> ComponentData { + let bytes = bincode::serialize(component)?; + + if bytes.len() <= MAX_INLINE_SIZE { + // Small enough - send inline via gossip + ComponentData::Inline(bytes) + } else { + // Too large - upload to blobs and send hash + let hash = blobs.add_bytes(&bytes).await?; + ComponentData::BlobRef(hash) + } +} +``` + +### Blob Garbage Collection + +**Problem**: When an entity with a blob reference is deleted, the blob becomes orphaned. Without GC, blobs accumulate indefinitely. + +**Solution**: Mark-and-sweep garbage collection using vector clock synchronization. + +#### How It Works + +**Mark Phase** - Scan database for all blob hashes currently referenced by non-deleted entities. Build a `HashSet` of active blobs. + +**Sweep Phase** - List all blobs in iroh-blobs. For each blob not in the active set: +- Check if all peers have seen the deletion (using vector clock + blob_references table) +- Apply 24-hour safety window +- If safe, delete the blob and record bytes freed + +**Safety coordination**: The critical challenge is knowing when all peers have seen a blob deletion. We solve this by: +1. Recording blob add/remove events in a `blob_references` table with (node_id, sequence_number) +2. Using vector clock logic: only delete if all peers' sequence numbers exceed the removal operation's sequence +3. Adding a 24-hour time window for additional safety + +#### Database Schema + +Track blob lifecycle events: + +```sql +CREATE TABLE blob_references ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + blob_hash TEXT NOT NULL, + entity_id TEXT NOT NULL, + event_type TEXT NOT NULL, -- 'added' | 'removed' + node_id TEXT NOT NULL, + sequence_number INTEGER NOT NULL, + timestamp INTEGER NOT NULL +); +``` + +**Optimization**: Maintain reference counts for efficiency: + +```sql +CREATE TABLE blob_refcount ( + blob_hash TEXT PRIMARY KEY, + ref_count INTEGER NOT NULL DEFAULT 0, + last_accessed INTEGER NOT NULL, + total_size_bytes INTEGER NOT NULL +); +``` + +This avoids full component scans - GC only checks blobs where `ref_count = 0`. + +#### Scheduling + +Run every 4 hours (less frequent than operation log cleanup, since it's more expensive). Process max 100 blobs per run to avoid blocking. + +#### Key Edge Cases + +**Offline peer**: Bob is offline. Alice adds and deletes a 50MB blob. GC runs and deletes it. Bob reconnects and gets FullState - no blob reference, no problem. + +**Concurrent add/delete**: Alice adds entity E with blob B. Bob deletes E. CRDT resolves to deleted. Both peers have orphaned blob B, which gets cleaned up after the 24-hour safety period. + +**Why it works**: Deletion tombstones win (LWW timestamp), and vector clock coordination ensures both peers converge and safely GC the blob. + +## Schema Evolution and Data Migration + +**Problem**: The version envelope only handles message format changes, not data semantics changes. + +### The Challenge + +Consider these scenarios: + +**Scenario 1: Component Type Change** +```rust +// Version 1 +struct Position { x: f32, y: f32 } + +// Version 2 +struct Transform { + pos: Vec2, // Changed from x,y to Vec2 + rot: f32, // Added rotation +} +``` + +- v2 client receives v1 `Position` component - can't deserialize +- v1 client receives v2 `Transform` component - can't deserialize + +**Scenario 2: Component Removed** +- v2 removes the `Health` component entirely +- v1 clients still send `Health` operations +- v2 clients don't know what to do with them + +### Solution: Component Schema Versioning + +**Approach 1: Graceful Degradation (Recommended for v1)** + +```rust +// Tag each component type with a version +#[derive(Serialize, Deserialize)] +struct ComponentData { + component_type: String, + component_version: u32, // NEW + data: Vec, +} + +// When receiving a component: +fn deserialize_component(data: ComponentData) -> Result { + match (data.component_type.as_str(), data.component_version) { + ("Transform", 1) => { + // Deserialize v1 + let pos: Position = bincode::deserialize(&data.data)?; + // Migrate to v2 + Ok(Component::Transform(Transform { + pos: Vec2::new(pos.x, pos.y), + rot: 0.0, // Default value + })) + } + ("Transform", 2) => { + // Deserialize v2 directly + Ok(Component::Transform(bincode::deserialize(&data.data)?)) + } + (unknown_type, _) => { + // Component type we don't understand - just skip it + warn!("Unknown component type: {}", unknown_type); + Ok(Component::Unknown) + } + } +} +``` + +**Rules**: +- Unknown component types are ignored (not an error) +- Older versions can be migrated forward +- Newer versions send the latest schema +- If an old client receives a new schema it can't understand, it ignores it (graceful degradation) + +**Approach 2: Coordinated Upgrades (For Breaking Changes)** + +For major changes where compatibility can't be maintained: + +1. **Announce migration**: All peers must update within a time window +2. **Migration phase**: New version can read old + new formats +3. **Cutover**: After all peers upgraded, stop supporting old format +4. **Garbage collect**: Clean up old-format data + +**For a 2-5 person private app**: Coordinated upgrades are manageable. Just message everyone: "Hey, please update the app to the latest version." + +### Handling Unknown Data + +```rust +fn apply_component_op(&mut self, op: ComponentOp) { + match self.component_registry.get(&op.component_type) { + Some(schema) => { + // We understand this component - deserialize and apply + let component = schema.deserialize(&op.value)?; + self.set_component(component); + } + None => { + // Unknown component - store it opaquely + // This preserves it for anti-entropy and persistence + // but we don't render/use it + self.unknown_components.insert( + op.component_type.clone(), + op.value.clone() + ); + } + } +} +``` + +This ensures that even if a peer doesn't understand a component, it still: +- Stores it in the database +- Includes it in FullState responses +- Preserves it in anti-entropy + +This prevents data loss when old and new clients coexist. + +## Open Questions + +1. **Network partitions**: How to handle very long-term partitions (weeks)? Should we have a "reset and resync" UX? +2. **Compaction efficiency**: What's the best heuristic for detecting compactable operations? (e.g., squashing 1000 LWW ops into one) +3. **Conflict UI**: Should users see when CRDTs merged conflicting changes, or is silent convergence better UX? + +## Success Criteria + +We'll know this is working when: +- [ ] Two peers can draw simultaneously and see each other's strokes +- [ ] Adding/removing items shows correct merged state +- [ ] Presence/cursors update in real-time +- [ ] Disconnecting and reconnecting syncs correctly +- [ ] State persists across restarts +- [ ] No conflicts, no lost data + +## Migration Path + +1. **Phase 1**: Basic sync with LWW only, in-memory (no persistence) +2. **Phase 2**: Add persistence layer (SQLite) +3. **Phase 3**: Add OR-Set for selections +4. **Phase 4**: Add Sequence CRDT for drawing paths +5. **Phase 5**: Anti-entropy and robustness +6. **Phase 6**: Optimize performance + +Each phase builds on the previous, maintains backward compatibility. + +## References + +- [CRDTs: Consistency without concurrency control](https://arxiv.org/abs/0907.0929) - Shapiro et al. +- [A comprehensive study of CRDTs](https://hal.inria.fr/hal-00932836) - Shapiro et al. +- [iroh documentation](https://docs.rs/iroh) +- [iroh-gossip documentation](https://docs.rs/iroh-gossip) +- [crdts crate](https://docs.rs/crdts) - Rust CRDT implementations +- [Automerge](https://automerge.org/) - JSON CRDT implementation +- [Yjs](https://docs.yjs.dev/) - Shared editing CRDT + +## Appendix: Component Operation Types + +Just for reference, the operations we'll need: + +```rust +enum ComponentOp { + // LWW: simple set + Set { component_type: String, value: Vec, timestamp: i64, node_id: NodeId }, + + // OR-Set: add/remove + SetAdd { component_type: String, element: Vec, ctx: AddContext }, + SetRemove { component_type: String, element: Vec, ctx: Vec }, + + // Map: update/remove keys + MapUpdate { component_type: String, key: String, operation: Box }, + MapRemove { component_type: String, key: String }, + + // Sequence: insert/delete + SequenceInsert { component_type: String, id: SequenceId, value: Vec }, + SequenceDelete { component_type: String, id: SequenceId }, + + // Counter: increment/decrement + CounterIncrement { component_type: String, delta: u64, node_id: NodeId }, + CounterDecrement { component_type: String, delta: u64, node_id: NodeId }, +} +``` + +These map directly to the CRDT primitives from the `crdts` library. \ No newline at end of file diff --git a/docs/rfcs/0002-persistence-strategy.md b/docs/rfcs/0002-persistence-strategy.md new file mode 100644 index 0000000..003a7a5 --- /dev/null +++ b/docs/rfcs/0002-persistence-strategy.md @@ -0,0 +1,566 @@ +# RFC 0002: Persistence Strategy for Battery-Efficient State Management + +**Status:** Draft +**Authors:** Sienna +**Created:** 2025-11-15 +**Related:** RFC 0001 (CRDT Sync Protocol) + +## Abstract + +This RFC defines a persistence strategy that balances data durability with battery efficiency for mobile platforms (iPad). The core challenge: Bevy runs at 60fps and generates continuous state changes, but we can't write to SQLite on every frame without destroying battery life and flash storage. + +## The Problem + +**Naive approach (bad)**: +```rust +fn sync_to_db_system(query: Query<&NetworkedEntity, Changed>) { + for entity in query.iter() { + db.execute("UPDATE components SET data = ? WHERE entity_id = ?", ...)?; + // This runs 60 times per second! + // iPad battery: 💀 + } +} +``` + +**Why this is terrible**: +- SQLite writes trigger `fsync()` syscalls (flush to physical storage) +- Each `fsync()` on iOS can take 5-20ms and drains battery significantly +- At 60fps with multiple entities, we'd be doing hundreds of disk writes per second +- Flash wear: mobile devices have limited write cycles +- User moves object around → hundreds of unnecessary writes of intermediate positions + +## Requirements + +1. **Survive crashes**: If the app crashes, user shouldn't lose more than a few seconds of work +2. **Battery efficient**: Minimize disk I/O, especially `fsync()` calls +3. **Flash-friendly**: Reduce write amplification on mobile storage +4. **Low latency**: Persistence shouldn't block rendering or input +5. **Recoverable**: On startup, we should be able to reconstruct recent state + +## Categorizing Data by Persistence Needs + +Not all data is equal. We need to categorize by how critical immediate persistence is: + +### Tier 1: Critical State (Persist Immediately) + +**What**: State that's hard or impossible to reconstruct if lost +- User-created entities (the fact that they exist) +- Operation log entries (for CRDT sync) +- Vector clock state (for causality tracking) +- Document metadata (name, creation time, etc.) + +**Why**: These are the "source of truth" - if we lose them, data is gone + +**Strategy**: Write to database within ~1 second of creation, but still batched + +### Tier 2: Derived State (Defer and Batch) + +**What**: State that can be reconstructed or is constantly changing +- Entity positions during drag operations +- Transform components (position, rotation, scale) +- UI state (selected items, viewport position) +- Temporary drawing strokes in progress + +**Why**: These change rapidly and the intermediate states aren't valuable + +**Strategy**: Batch writes, flush every 5-10 seconds or on specific events + +### Tier 3: Ephemeral State (Never Persist) + +**What**: State that only matters during current session +- Remote peer cursors +- Presence indicators (who's online) +- Network connection status +- Frame-rate metrics + +**Why**: These are meaningless after restart + +**Strategy**: Keep in-memory only (Bevy resources, not components) + +## Write Strategy: The Three-Buffer System + +We use a three-tier approach to minimize disk writes while maintaining durability: + +### Layer 1: In-Memory Dirty Tracking (0ms latency) + +Bevy change detection marks components as dirty, but we don't write immediately. Instead, we maintain a dirty set: + +```rust +#[derive(Resource)] +struct DirtyEntities { + // Entities with changes not yet in write buffer + entities: HashSet, + components: HashMap>, // entity → dirty component types + last_modified: HashMap, // when was it last changed +} +``` + +**Update frequency**: Every frame (cheap - just memory operations) + +### Layer 2: Write Buffer (100ms-1s batching) + +Periodically (every 100ms-1s), we collect dirty entities and prepare a write batch: + +```rust +#[derive(Resource)] +struct WriteBuffer { + // Pending writes not yet committed to SQLite + pending_operations: Vec, + last_flush: Instant, +} + +enum PersistenceOp { + UpsertEntity { id: Uuid, data: EntityData }, + UpsertComponent { entity_id: Uuid, component_type: String, data: Vec }, + LogOperation { node_id: NodeId, seq: u64, op: Vec }, + UpdateVectorClock { node_id: NodeId, counter: u64 }, +} +``` + +**Update frequency**: Every 100ms-1s (configurable based on battery level) + +**Strategy**: Accumulate operations in memory, then batch-write them + +### Layer 3: SQLite with WAL Mode (5-10s commit interval) + +Write buffer is flushed to SQLite, but we don't call `fsync()` immediately. Instead, we use WAL mode and control checkpoint timing: + +```sql +-- Enable Write-Ahead Logging +PRAGMA journal_mode = WAL; + +-- Don't auto-checkpoint on every transaction +PRAGMA wal_autocheckpoint = 0; + +-- Synchronous = NORMAL (fsync WAL on commit, but not every write) +PRAGMA synchronous = NORMAL; +``` + +**Update frequency**: Manual checkpoints every 5-10 seconds (or on specific events) + +## Flush Events: When to Force Persistence + +Certain events require immediate persistence (within 1 second): + +### 1. Entity Creation +When user creates a new entity, we need to persist its existence quickly: +- Add to write buffer immediately +- Trigger flush within 1 second + +### 2. Major User Actions +Actions that represent "savepoints" in user's mental model: +- Finishing a drawing stroke (stroke start → immediate, intermediate points → batched, stroke end → flush) +- Deleting entities +- Changing document metadata +- Undo/redo operations + +### 3. Application State Transitions +State changes that might precede app termination: +- App going to background (iOS `applicationWillResignActive`) +- Low memory warning +- User explicitly saving (if we have a save button) +- Switching documents/workspaces + +### 4. Network Events +Sync protocol events that need persistence: +- Receiving operation log entries from peers +- Vector clock updates (every 5 operations or 5 seconds, whichever comes first) + +### 5. Periodic Background Flush +Even if no major events happen: +- Flush every 10 seconds during active use +- Flush every 30 seconds when idle (no user input for >1 minute) + +## Battery-Adaptive Flushing + +Different flush strategies based on battery level: + +```rust +fn get_flush_interval(battery_level: f32, is_charging: bool) -> Duration { + if is_charging { + Duration::from_secs(5) // Aggressive - power available + } else if battery_level > 0.5 { + Duration::from_secs(10) // Normal + } else if battery_level > 0.2 { + Duration::from_secs(30) // Conservative + } else { + Duration::from_secs(60) // Very conservative - low battery + } +} +``` + +**On iOS**: Use `UIDevice.current.batteryLevel` and `UIDevice.current.batteryState` + +## SQLite Optimizations for Mobile + +### Transaction Batching + +Group multiple writes into a single transaction: + +```rust +async fn flush_write_buffer(buffer: &WriteBuffer, db: &Connection) -> Result<()> { + let tx = db.transaction()?; + + // All writes in one transaction + for op in &buffer.pending_operations { + match op { + PersistenceOp::UpsertEntity { id, data } => { + tx.execute("INSERT OR REPLACE INTO entities (...) VALUES (...)", ...)?; + } + PersistenceOp::UpsertComponent { entity_id, component_type, data } => { + tx.execute("INSERT OR REPLACE INTO components (...) VALUES (...)", ...)?; + } + // ... + } + } + + tx.commit()?; // Single fsync for entire batch +} +``` + +**Impact**: 100 individual writes = 100 fsyncs. 1 transaction with 100 writes = 1 fsync. + +### WAL Mode Checkpoint Control + +```rust +async fn checkpoint_wal(db: &Connection) -> Result<()> { + // Manually checkpoint WAL to database file + db.execute("PRAGMA wal_checkpoint(PASSIVE)", [])?; +} +``` + +**PASSIVE checkpoint**: Doesn't block readers, syncs when possible +**When to checkpoint**: Every 10 seconds, or when WAL exceeds 1MB + +### Index Strategy + +Be selective about indexes - they increase write cost: + +```sql +-- Only index what we actually query frequently +CREATE INDEX idx_components_entity ON components(entity_id); +CREATE INDEX idx_oplog_node_seq ON operation_log(node_id, sequence_number); + +-- DON'T index everything just because we can +-- Every index = extra writes on every INSERT/UPDATE +``` + +### Page Size Optimization + +```sql +-- Larger page size = fewer I/O operations for sequential writes +-- Default is 4KB, but 8KB or 16KB can be better for mobile +PRAGMA page_size = 8192; +``` + +**Caveat**: Must be set before database is created (or VACUUM to rebuild) + +## Recovery Strategy + +What happens if app crashes before flush? + +### What We Lose + +**Worst case**: Up to 10 seconds of component updates (positions, transforms) + +**What we DON'T lose**: +- Entity existence (flushed within 1 second of creation) +- Operation log entries (flushed with vector clock updates) +- Any data from before the last checkpoint + +### Recovery on Startup + +```mermaid +graph TB + A[App Starts] --> B[Open SQLite] + B --> C{Check WAL file} + C -->|WAL exists| D[Recover from WAL] + C -->|No WAL| E[Load from main DB] + D --> F[Load entities from DB] + E --> F + F --> G[Load operation log] + G --> H[Rebuild vector clock] + H --> I[Connect to gossip] + I --> J[Request sync from peers] + J --> K[Fill any gaps via anti-entropy] + K --> L[Fully recovered] +``` + +**Key insight**: Even if we lose local state, gossip sync repairs it. Peers send us missing operations. + +### Crash Detection + +On startup, detect if previous session crashed: + +```sql +CREATE TABLE session_state ( + key TEXT PRIMARY KEY, + value TEXT +); + +-- On startup, check if previous session closed cleanly +SELECT value FROM session_state WHERE key = 'clean_shutdown'; + +-- If not found or 'false', we crashed +-- Trigger recovery procedures +``` + +## Platform-Specific Concerns + +### iOS / iPadOS + +**Background app suspension**: iOS aggressively suspends apps. We have ~5 seconds when moving to background: + +```rust +// When app moves to background: +fn handle_background_event() { + // Force immediate flush + flush_write_buffer().await?; + checkpoint_wal().await?; + + // Mark clean shutdown + db.execute("INSERT OR REPLACE INTO session_state VALUES ('clean_shutdown', 'true')", [])?; +} +``` + +**Low Power Mode**: Detect and reduce flush frequency: +```swift +// iOS-specific detection +if ProcessInfo.processInfo.isLowPowerModeEnabled { + set_flush_interval(Duration::from_secs(60)); +} +``` + +### Desktop (macOS/Linux/Windows) + +More relaxed constraints: +- Battery life less critical on plugged-in desktops +- Can use more aggressive flush intervals (every 5 seconds) +- Larger WAL sizes acceptable (up to 10MB before checkpoint) + +## Monitoring & Metrics + +Track these metrics to tune persistence: + +```rust +struct PersistenceMetrics { + // Write volume + total_writes: u64, + bytes_written: u64, + + // Timing + flush_count: u64, + avg_flush_duration: Duration, + checkpoint_count: u64, + avg_checkpoint_duration: Duration, + + // WAL health + wal_size_bytes: u64, + max_wal_size_bytes: u64, + + // Recovery + crash_recovery_count: u64, + clean_shutdown_count: u64, +} +``` + +**Alerts**: +- Flush duration >50ms (disk might be slow or overloaded) +- WAL size >5MB (checkpoint more frequently) +- Crash recovery rate >10% (need more aggressive flushing) + +## Write Coalescing: Deduplication + +When the same entity is modified multiple times before flush, we only keep the latest: + +```rust +fn add_to_write_buffer(op: PersistenceOp, buffer: &mut WriteBuffer) { + match op { + PersistenceOp::UpsertComponent { entity_id, component_type, data } => { + // Remove any existing pending write for this entity+component + buffer.pending_operations.retain(|existing_op| { + !matches!(existing_op, + PersistenceOp::UpsertComponent { + entity_id: e_id, + component_type: c_type, + .. + } if e_id == &entity_id && c_type == &component_type + ) + }); + + // Add the new one (latest state) + buffer.pending_operations.push(op); + } + // ... + } +} +``` + +**Impact**: User drags object for 5 seconds @ 60fps = 300 transform updates → coalesced to 1 write + +## Persistence vs Sync: Division of Responsibility + +Important distinction: + +**Persistence layer** (this RFC): +- Writes to local SQLite +- Optimized for durability and battery life +- Only cares about local state survival + +**Sync layer** (RFC 0001): +- Broadcasts operations via gossip +- Maintains operation log for anti-entropy +- Ensures eventual consistency across peers + +**Key insight**: These operate independently. An operation can be: +1. Logged to operation log (for sync) - happens immediately +2. Applied to ECS (for rendering) - happens immediately +3. Persisted to SQLite (for durability) - happens on flush schedule + +If local state is lost due to delayed flush, sync layer repairs it from peers. + +## Configuration Schema + +Expose configuration for tuning: + +```toml +[persistence] +# Base flush interval (may be adjusted by battery level) +flush_interval_secs = 10 + +# Max time to defer critical writes (entity creation, etc.) +critical_flush_delay_ms = 1000 + +# WAL checkpoint interval +checkpoint_interval_secs = 30 + +# Max WAL size before forced checkpoint +max_wal_size_mb = 5 + +# Adaptive flushing based on battery +battery_adaptive = true + +# Flush intervals per battery tier +[persistence.battery_tiers] +charging = 5 +high = 10 # >50% +medium = 30 # 20-50% +low = 60 # <20% + +# Platform overrides +[persistence.ios] +background_flush_timeout_secs = 5 +low_power_mode_interval_secs = 60 +``` + +## Example System Implementation + +```rust +fn persistence_system( + dirty: Res, + mut write_buffer: ResMut, + db: Res, + time: Res