Compare commits
No commits in common. "eef3e1558f109939211728bba04264676947cc46" and "99776a1a617ff84a5560a5bb5f2165d6da4ade17" have entirely different histories.
eef3e1558f
...
99776a1a61
|
|
@ -2,15 +2,6 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
|
|
@ -29,50 +20,6 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allo-isolate"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atomic",
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_log-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||
|
||||
[[package]]
|
||||
name = "android_logger"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f"
|
||||
dependencies = [
|
||||
"android_log-sys",
|
||||
"env_logger",
|
||||
"log",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
|
|
@ -85,12 +32,6 @@ dependencies = [
|
|||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||
|
||||
[[package]]
|
||||
name = "automerge"
|
||||
version = "0.5.12"
|
||||
|
|
@ -114,21 +55,6 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
|
|
@ -168,24 +94,12 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-target"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
|
|
@ -234,31 +148,10 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chronogrid_ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chronogrid_core",
|
||||
"chronogrid_crypto",
|
||||
"flutter_rust_bridge",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chronogrid_net"
|
||||
version = "0.1.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 = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
|
@ -313,25 +206,6 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dart-sys"
|
||||
version = "4.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "4.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.12"
|
||||
|
|
@ -353,17 +227,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "delegate-attr"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a84c9a9c129b98e707ac9a31e204c10c064dd9f949b7da362310ff1a8b137d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
|
@ -381,16 +244,6 @@ version = "1.18.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
|
|
@ -435,113 +288,12 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flutter_rust_bridge"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419323ff3ae8b63ed20b7cd0fd0e3e73fa9234b3b212b87b5586fe76ae52318"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"android_logger",
|
||||
"anyhow",
|
||||
"build-target",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"console_error_panic_hook",
|
||||
"dart-sys",
|
||||
"delegate-attr",
|
||||
"flutter_rust_bridge_macros",
|
||||
"futures",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"oslog",
|
||||
"portable-atomic",
|
||||
"threadpool",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flutter_rust_bridge_macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c6fd06cacc0dfe931d00f3acdc5ac6c5dd9382b33dd78d71cce8f0a62e9458b"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"md-5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
|
|
@ -554,13 +306,8 @@ version = "0.3.34"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
|
@ -606,12 +353,6 @@ dependencies = [
|
|||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
|
|
@ -630,12 +371,6 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
@ -691,12 +426,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128"
|
||||
version = "0.2.7"
|
||||
|
|
@ -742,22 +471,6 @@ version = "0.4.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -768,25 +481,6 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[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 = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
|
|
@ -815,17 +509,6 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oslog"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8343ce955f18e7e68c0207dd0ea776ec453035685395ababd2ea651c569728b3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dashmap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
|
|
@ -849,12 +532,6 @@ version = "0.3.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
|
|
@ -927,35 +604,6 @@ dependencies = [
|
|||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.31.0"
|
||||
|
|
@ -970,12 +618,6 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
|
|
@ -1196,15 +838,6 @@ dependencies = [
|
|||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "threadpool"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
|
||||
dependencies = [
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.12.0"
|
||||
|
|
@ -1220,15 +853,6 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
|
|
@ -1321,16 +945,6 @@ dependencies = [
|
|||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.127"
|
||||
|
|
@ -1363,16 +977,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ resolver = "2"
|
|||
members = [
|
||||
"crates/chronogrid_core",
|
||||
"crates/chronogrid_crypto",
|
||||
"crates/chronogrid_ffi",
|
||||
"crates/chronogrid_net",
|
||||
"crates/chronogrid_ai",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -34,22 +34,6 @@
|
|||
- 45 unit tests (all passing): HLC ordering/parse, all scalar types, save/load/fork/merge round-trips, CRDT doc DB persistence, schema validation with `crdt_docs` table.
|
||||
- Full ADR at `docs/ARCHITECTURE/ADR/0004-core-automerge-crdt-integration.md`.
|
||||
|
||||
## [2026-08-20] - ADL-005: chronogrid_ffi Crate – Flutter Rust Bridge v2 FFI Layer
|
||||
|
||||
- **Status**: Accepted
|
||||
- **Context**: Task 1.5 required generating `flutter_rust_bridge` FFI bindings to bridge all Rust storage, CRDT, and cryptographic methods to the Flutter/Dart presentation layer.
|
||||
- **Decision**:
|
||||
- Created `crates/chronogrid_ffi/` crate with `cdylib` + `staticlib` + `lib` targets, depending on `chronogrid_core`, `chronogrid_crypto`, `flutter_rust_bridge = "=2.7.0"`, and `anyhow = "1"`.
|
||||
- API uses a **serialized-bytes pattern**: CRDT documents cross FFI as `Vec<u8>` (Automerge binary); read functions return DTO structs (`FfiTaskData`, `FfiNoteData`, `FfiDerivedKey`).
|
||||
- Global `Engine` singleton (`Mutex<Option<Engine>>`) holds the `Database` connection + `Hlc` clock. Thread-safe `with_engine()` / `with_engine_mut()` helpers provide access from FRB's isolate thread pool.
|
||||
- 38 public FFI functions across 5 API modules: `engine` (init/close/status), `database` (CRDT doc persistence), `crypto` (key derivation + platform-gated keychain), `task_crdt` (18 create/read/set/clear/merge ops), `note_crdt` (8 create/read/set/clear/merge ops).
|
||||
- All fallible functions return `anyhow::Result<T>`, which FRB v2 maps to Dart exceptions.
|
||||
- Keychain functions compile on all platforms but return an error on Android (delegated to Flutter KeyStore bridge).
|
||||
- `flutter_rust_bridge.yaml` configures codegen: Rust input from `api/`, Dart output to `apps/chronogrid_flutter/lib/src/rust/`.
|
||||
- Flutter `pubspec.yaml` updated with `flutter_rust_bridge: ^2.7.0`, `ffi: ^2.1.0`, `meta: ^1.11.0`.
|
||||
- 25 unit tests (all passing): engine lifecycle, DB CRDT doc persistence, task/note CRDT create/read/mutate/merge round-trips, crypto derivation, stateless read validation, no-engine error paths.
|
||||
- Full ADR at `docs/ARCHITECTURE/ADR/0005-ffi-bridge-flutter-rust-bridge.md`.
|
||||
|
||||
## [2026-08-09] - ADL-002: chronogrid_crypto Crate – Argon2id KDF & OS Keychain
|
||||
|
||||
- **Status**: Accepted
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"configVersion": 2,
|
||||
"packages": [
|
||||
{
|
||||
"name": "args",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/args-2.7.0",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.3"
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/async-2.13.1",
|
||||
|
|
@ -19,12 +13,6 @@
|
|||
"packageUri": "lib/",
|
||||
"languageVersion": "3.1"
|
||||
},
|
||||
{
|
||||
"name": "build_cli_annotations",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/build_cli_annotations-2.1.1",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.8"
|
||||
},
|
||||
{
|
||||
"name": "characters",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/characters-1.4.1",
|
||||
|
|
@ -49,60 +37,24 @@
|
|||
"packageUri": "lib/",
|
||||
"languageVersion": "3.3"
|
||||
},
|
||||
{
|
||||
"name": "ffi",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/ffi-2.2.0",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.7"
|
||||
},
|
||||
{
|
||||
"name": "file",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/file-7.0.1",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.0"
|
||||
},
|
||||
{
|
||||
"name": "flutter",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/flutter/packages/flutter",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.11"
|
||||
},
|
||||
{
|
||||
"name": "flutter_driver",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/flutter/packages/flutter_driver",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.11"
|
||||
},
|
||||
{
|
||||
"name": "flutter_lints",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_lints-5.0.0",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.5"
|
||||
},
|
||||
{
|
||||
"name": "flutter_rust_bridge",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_rust_bridge-2.12.0",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.4"
|
||||
},
|
||||
{
|
||||
"name": "flutter_test",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/flutter/packages/flutter_test",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.11"
|
||||
},
|
||||
{
|
||||
"name": "fuchsia_remote_debug_protocol",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/flutter/packages/fuchsia_remote_debug_protocol",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.11"
|
||||
},
|
||||
{
|
||||
"name": "integration_test",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/flutter/packages/integration_test",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.11"
|
||||
},
|
||||
{
|
||||
"name": "leak_tracker",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker-11.0.2",
|
||||
|
|
@ -151,18 +103,6 @@
|
|||
"packageUri": "lib/",
|
||||
"languageVersion": "3.4"
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/platform-3.1.6",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.2"
|
||||
},
|
||||
{
|
||||
"name": "process",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/process-5.0.5",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.5"
|
||||
},
|
||||
{
|
||||
"name": "sky_engine",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/flutter/bin/cache/pkg/sky_engine",
|
||||
|
|
@ -193,12 +133,6 @@
|
|||
"packageUri": "lib/",
|
||||
"languageVersion": "3.1"
|
||||
},
|
||||
{
|
||||
"name": "sync_http",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/sync_http-0.3.1",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "2.12"
|
||||
},
|
||||
{
|
||||
"name": "term_glyph",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/term_glyph-1.2.2",
|
||||
|
|
@ -223,18 +157,6 @@
|
|||
"packageUri": "lib/",
|
||||
"languageVersion": "3.5"
|
||||
},
|
||||
{
|
||||
"name": "web",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/web-1.1.1",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.4"
|
||||
},
|
||||
{
|
||||
"name": "webdriver",
|
||||
"rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/webdriver-3.1.0",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.1"
|
||||
},
|
||||
{
|
||||
"name": "chronogrid_flutter",
|
||||
"rootUri": "../",
|
||||
|
|
|
|||
|
|
@ -7,26 +7,11 @@
|
|||
"name": "chronogrid_flutter",
|
||||
"version": "0.1.0+1",
|
||||
"dependencies": [
|
||||
"ffi",
|
||||
"flutter",
|
||||
"flutter_rust_bridge",
|
||||
"meta"
|
||||
"flutter"
|
||||
],
|
||||
"devDependencies": [
|
||||
"flutter_lints",
|
||||
"flutter_test",
|
||||
"integration_test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "integration_test",
|
||||
"version": "0.0.0",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"flutter_driver",
|
||||
"flutter_test",
|
||||
"path",
|
||||
"vm_service"
|
||||
"flutter_test"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -54,11 +39,6 @@
|
|||
"vector_math"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "meta",
|
||||
"version": "1.19.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "flutter",
|
||||
"version": "0.0.0",
|
||||
|
|
@ -71,31 +51,6 @@
|
|||
"vector_math"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vm_service",
|
||||
"version": "15.2.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"version": "1.9.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "flutter_driver",
|
||||
"version": "0.0.0",
|
||||
"dependencies": [
|
||||
"file",
|
||||
"flutter",
|
||||
"flutter_test",
|
||||
"fuchsia_remote_debug_protocol",
|
||||
"matcher",
|
||||
"meta",
|
||||
"path",
|
||||
"vm_service",
|
||||
"webdriver"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lints",
|
||||
"version": "5.1.1",
|
||||
|
|
@ -124,11 +79,6 @@
|
|||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vector_math",
|
||||
"version": "2.4.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "stack_trace",
|
||||
"version": "1.12.1",
|
||||
|
|
@ -149,6 +99,11 @@
|
|||
"collection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"version": "1.9.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "matcher",
|
||||
"version": "0.12.20",
|
||||
|
|
@ -192,15 +147,6 @@
|
|||
"version": "1.4.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "fuchsia_remote_debug_protocol",
|
||||
"version": "0.0.0",
|
||||
"dependencies": [
|
||||
"meta",
|
||||
"process",
|
||||
"vm_service"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"version": "2.13.1",
|
||||
|
|
@ -259,75 +205,18 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "ffi",
|
||||
"version": "2.2.0",
|
||||
"name": "vm_service",
|
||||
"version": "15.2.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "flutter_rust_bridge",
|
||||
"version": "2.12.0",
|
||||
"dependencies": [
|
||||
"args",
|
||||
"async",
|
||||
"build_cli_annotations",
|
||||
"meta",
|
||||
"path",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "build_cli_annotations",
|
||||
"version": "2.1.1",
|
||||
"dependencies": [
|
||||
"args",
|
||||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"version": "2.7.0",
|
||||
"name": "meta",
|
||||
"version": "1.19.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.1.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "process",
|
||||
"version": "5.0.5",
|
||||
"dependencies": [
|
||||
"file",
|
||||
"path",
|
||||
"platform"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file",
|
||||
"version": "7.0.1",
|
||||
"dependencies": [
|
||||
"meta",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"version": "3.1.6",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "webdriver",
|
||||
"version": "3.1.0",
|
||||
"dependencies": [
|
||||
"matcher",
|
||||
"path",
|
||||
"stack_trace",
|
||||
"sync_http"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sync_http",
|
||||
"version": "0.3.1",
|
||||
"name": "vector_math",
|
||||
"version": "2.4.2",
|
||||
"dependencies": []
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -25,14 +17,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build_cli_annotations:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_cli_annotations
|
||||
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -65,32 +49,11 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -99,29 +62,11 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_rust_bridge:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_rust_bridge
|
||||
sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -171,7 +116,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||
|
|
@ -186,22 +131,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -239,14 +168,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -279,22 +200,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=3.11.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -9,16 +9,11 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_rust_bridge: ^2.7.0
|
||||
ffi: ^2.1.0
|
||||
meta: ^1.11.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "chronogrid_ffi"
|
||||
description = "Flutter Rust Bridge FFI layer – bridges chronogrid_core and chronogrid_crypto to Dart"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
chronogrid_core = { path = "../chronogrid_core" }
|
||||
chronogrid_crypto = { path = "../chronogrid_crypto" }
|
||||
flutter_rust_bridge = "=2.7.0"
|
||||
anyhow = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
//! Cryptographic operations — Argon2id key derivation and OS keychain access.
|
||||
//!
|
||||
//! These functions wrap [`chronogrid_crypto`] for FFI consumption.
|
||||
//! Key derivation is stateless and does **not** require an active engine.
|
||||
//! Keychain operations are platform-gated: on Android they return an error
|
||||
//! because credential storage is delegated to the Flutter KeyStore bridge.
|
||||
|
||||
use super::types::FfiDerivedKey;
|
||||
|
||||
/// Derives a 256-bit master key from a passphrase using Argon2id.
|
||||
///
|
||||
/// If `salt` is `None`, a fresh 16-byte random salt is generated.
|
||||
/// If `salt` is provided, it **must** be exactly 16 bytes.
|
||||
///
|
||||
/// Returns the derived key and the salt that was used (needed to re-derive
|
||||
/// the same key later).
|
||||
pub fn crypto_derive_master_key(
|
||||
passphrase: String,
|
||||
salt: Option<Vec<u8>>,
|
||||
) -> anyhow::Result<FfiDerivedKey> {
|
||||
let salt_ref = salt.as_deref();
|
||||
|
||||
let derived = chronogrid_crypto::kdf::derive_master_key(&passphrase, salt_ref)
|
||||
.map_err(|e| anyhow::anyhow!("key derivation failed: {e}"))?;
|
||||
|
||||
Ok(FfiDerivedKey {
|
||||
key: derived.key().to_vec(),
|
||||
salt: derived.salt().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a cryptographically random 16-byte salt.
|
||||
///
|
||||
/// This is a convenience wrapper; [`crypto_derive_master_key`] already
|
||||
/// generates a salt internally when `salt` is `None`.
|
||||
pub fn crypto_generate_salt() -> Vec<u8> {
|
||||
chronogrid_crypto::kdf::generate_salt().to_vec()
|
||||
}
|
||||
|
||||
/// Stores raw key material in the OS credential store.
|
||||
///
|
||||
/// The bytes are Base64-encoded internally for credential-manager
|
||||
/// compatibility.
|
||||
///
|
||||
/// # Platform Support
|
||||
///
|
||||
/// * **Windows**: Windows Credential Manager (DPAPI)
|
||||
/// * **Linux**: SecretService D-Bus API (libsecret)
|
||||
/// * **macOS**: macOS Keychain
|
||||
/// * **Android**: Returns an error — use Flutter KeyStore bridge instead.
|
||||
pub fn crypto_store_credential(
|
||||
service: String,
|
||||
account: String,
|
||||
key_material: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
chronogrid_crypto::keychain::store_credential(&service, &account, &key_material)
|
||||
.map_err(|e| anyhow::anyhow!("keychain store failed: {e}"))
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let _ = (service, account, key_material);
|
||||
anyhow::bail!(
|
||||
"OS keychain is not available on Android; \
|
||||
use the Flutter KeyStore bridge instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves raw key material from the OS credential store.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the credential is not found or the keychain
|
||||
/// backend fails. On Android, always returns an error.
|
||||
pub fn crypto_retrieve_credential(
|
||||
service: String,
|
||||
account: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
chronogrid_crypto::keychain::retrieve_credential(&service, &account)
|
||||
.map_err(|e| anyhow::anyhow!("keychain retrieve failed: {e}"))
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let _ = (service, account);
|
||||
anyhow::bail!(
|
||||
"OS keychain is not available on Android; \
|
||||
use the Flutter KeyStore bridge instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a credential from the OS credential store.
|
||||
///
|
||||
/// Idempotent — succeeds even if the credential does not exist.
|
||||
/// On Android, always returns an error.
|
||||
pub fn crypto_delete_credential(
|
||||
service: String,
|
||||
account: String,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
chronogrid_crypto::keychain::delete_credential(&service, &account)
|
||||
.map_err(|e| anyhow::anyhow!("keychain delete failed: {e}"))
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let _ = (service, account);
|
||||
anyhow::bail!(
|
||||
"OS keychain is not available on Android; \
|
||||
use the Flutter KeyStore bridge instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive_key_with_random_salt() {
|
||||
let dk = crypto_derive_master_key("correct horse battery staple".into(), None)
|
||||
.expect("derivation should succeed");
|
||||
assert_eq!(dk.key.len(), 32);
|
||||
assert_eq!(dk.salt.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_key_deterministic_with_same_salt() {
|
||||
let salt = vec![0xABu8; 16];
|
||||
let dk1 =
|
||||
crypto_derive_master_key("hunter2".into(), Some(salt.clone())).unwrap();
|
||||
let dk2 =
|
||||
crypto_derive_master_key("hunter2".into(), Some(salt)).unwrap();
|
||||
assert_eq!(dk1.key, dk2.key, "same passphrase + salt must yield same key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_invalid_salt_length() {
|
||||
let bad_salt = vec![0u8; 10];
|
||||
let result = crypto_derive_master_key("pw".into(), Some(bad_salt));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_salt_returns_16_bytes() {
|
||||
let salt = crypto_generate_salt();
|
||||
assert_eq!(salt.len(), 16);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
//! CRDT document persistence — save / load / delete serialized Automerge
|
||||
//! documents in the encrypted SQLCipher `crdt_docs` table.
|
||||
//!
|
||||
//! All functions require an active engine (see [`super::engine::init_engine`]).
|
||||
|
||||
use super::state::{with_engine, with_engine_mut};
|
||||
|
||||
/// Persists (upserts) a serialized Automerge CRDT document.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `entity_id` — Primary key (typically a UUIDv4 matching a task or note).
|
||||
/// * `entity_type` — `"task"` or `"note"`.
|
||||
/// * `doc_bytes` — Serialized Automerge document bytes.
|
||||
/// * `updated_at` — Timestamp (Unix epoch ms) for the latest mutation.
|
||||
pub fn db_save_crdt_doc(
|
||||
entity_id: String,
|
||||
entity_type: String,
|
||||
doc_bytes: Vec<u8>,
|
||||
updated_at: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
// Use with_engine_mut to ensure exclusive access during write.
|
||||
with_engine_mut(|engine| {
|
||||
engine
|
||||
.db
|
||||
.save_crdt_doc(&entity_id, &entity_type, &doc_bytes, updated_at as u64)
|
||||
.map_err(|e| anyhow::anyhow!("save_crdt_doc failed: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads the serialized Automerge CRDT document for the given entity.
|
||||
///
|
||||
/// Returns `None` if no document has been stored for `entity_id`.
|
||||
pub fn db_load_crdt_doc(entity_id: String) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
with_engine(|engine| {
|
||||
engine
|
||||
.db
|
||||
.load_crdt_doc(&entity_id)
|
||||
.map_err(|e| anyhow::anyhow!("load_crdt_doc failed: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes the CRDT document for the given entity.
|
||||
///
|
||||
/// Returns `true` if a row was deleted, `false` if none existed.
|
||||
pub fn db_delete_crdt_doc(entity_id: String) -> anyhow::Result<bool> {
|
||||
with_engine(|engine| {
|
||||
engine
|
||||
.db
|
||||
.delete_crdt_doc(&entity_id)
|
||||
.map_err(|e| anyhow::anyhow!("delete_crdt_doc failed: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::engine::{close_engine, init_engine_in_memory};
|
||||
use super::*;
|
||||
|
||||
const TEST_KEY: [u8; 32] = [0xAA; 32];
|
||||
const NODE: &str = "test-node";
|
||||
|
||||
fn setup() {
|
||||
let _ = close_engine();
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
let _ = close_engine();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_load_round_trip() {
|
||||
setup();
|
||||
|
||||
let blob = b"fake-automerge-bytes".to_vec();
|
||||
db_save_crdt_doc("t-001".into(), "task".into(), blob.clone(), 1000).unwrap();
|
||||
|
||||
let loaded = db_load_crdt_doc("t-001".into()).unwrap();
|
||||
assert_eq!(loaded, Some(blob));
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_missing_returns_none() {
|
||||
setup();
|
||||
let loaded = db_load_crdt_doc("nonexistent".into()).unwrap();
|
||||
assert_eq!(loaded, None);
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_existing_returns_true() {
|
||||
setup();
|
||||
db_save_crdt_doc("t-002".into(), "task".into(), b"data".to_vec(), 1000).unwrap();
|
||||
assert!(db_delete_crdt_doc("t-002".into()).unwrap());
|
||||
assert_eq!(db_load_crdt_doc("t-002".into()).unwrap(), None);
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_missing_returns_false() {
|
||||
setup();
|
||||
assert!(!db_delete_crdt_doc("nonexistent".into()).unwrap());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operations_without_engine_fail() {
|
||||
let _ = close_engine();
|
||||
assert!(db_load_crdt_doc("x".into()).is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
//! Engine lifecycle — initialize, query, and tear down the core runtime.
|
||||
//!
|
||||
//! The engine must be initialized before any database or CRDT mutation
|
||||
//! functions can be called. Initialization opens (or creates) the encrypted
|
||||
//! SQLCipher database and seeds the node's Hybrid Logical Clock.
|
||||
|
||||
use chronogrid_core::{Database, Hlc};
|
||||
|
||||
use super::state::{Engine, ENGINE};
|
||||
|
||||
/// Initializes the engine with a **file-backed** encrypted database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `db_path` — Filesystem path for the SQLCipher database file.
|
||||
/// * `key` — 32-byte raw encryption key (from Argon2id derivation).
|
||||
/// * `node_id` — Unique identifier for this device / peer (e.g. a UUIDv4).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * If the engine is already initialized.
|
||||
/// * If the key is not exactly 32 bytes.
|
||||
/// * If the database cannot be opened or the schema fails to initialize.
|
||||
pub fn init_engine(db_path: String, key: Vec<u8>, node_id: String) -> anyhow::Result<()> {
|
||||
let mut guard = ENGINE
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("engine lock poisoned: {e}"))?;
|
||||
|
||||
if guard.is_some() {
|
||||
anyhow::bail!("engine is already initialized — call close_engine first");
|
||||
}
|
||||
|
||||
let db = Database::open(&db_path, &key)
|
||||
.map_err(|e| anyhow::anyhow!("failed to open database at '{db_path}': {e}"))?;
|
||||
|
||||
let hlc = Hlc::new(node_id);
|
||||
|
||||
*guard = Some(Engine { db, hlc });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the engine with an **in-memory** database (for testing).
|
||||
///
|
||||
/// Semantics are identical to [`init_engine`] except no file is created.
|
||||
pub fn init_engine_in_memory(key: Vec<u8>, node_id: String) -> anyhow::Result<()> {
|
||||
let mut guard = ENGINE
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("engine lock poisoned: {e}"))?;
|
||||
|
||||
if guard.is_some() {
|
||||
anyhow::bail!("engine is already initialized — call close_engine first");
|
||||
}
|
||||
|
||||
let db = Database::open_in_memory(&key)
|
||||
.map_err(|e| anyhow::anyhow!("failed to open in-memory database: {e}"))?;
|
||||
|
||||
let hlc = Hlc::new(node_id);
|
||||
|
||||
*guard = Some(Engine { db, hlc });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shuts down the engine, closing the database connection.
|
||||
///
|
||||
/// After this call, all database and CRDT mutation functions will return
|
||||
/// errors until [`init_engine`] or [`init_engine_in_memory`] is called again.
|
||||
///
|
||||
/// Calling `close_engine` when no engine is active is a no-op.
|
||||
pub fn close_engine() -> anyhow::Result<()> {
|
||||
let mut guard = ENGINE
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("engine lock poisoned: {e}"))?;
|
||||
|
||||
// Drop the engine (and its database connection) by replacing with None.
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the engine has been initialized and is ready for use.
|
||||
pub fn is_engine_initialized() -> bool {
|
||||
ENGINE
|
||||
.lock()
|
||||
.map(|guard| guard.is_some())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 32-byte test key.
|
||||
const TEST_KEY: [u8; 32] = [0xAA; 32];
|
||||
const NODE: &str = "test-node";
|
||||
|
||||
/// Helper: ensure the engine is closed before / after each test to avoid
|
||||
/// cross-test state leakage via the global singleton.
|
||||
fn reset_engine() {
|
||||
let _ = close_engine();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_and_close_lifecycle() {
|
||||
reset_engine();
|
||||
|
||||
assert!(!is_engine_initialized());
|
||||
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
assert!(is_engine_initialized());
|
||||
|
||||
close_engine().unwrap();
|
||||
assert!(!is_engine_initialized());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_init_is_rejected() {
|
||||
reset_engine();
|
||||
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
let result = init_engine_in_memory(TEST_KEY.to_vec(), NODE.into());
|
||||
assert!(result.is_err());
|
||||
|
||||
reset_engine();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_without_init_is_noop() {
|
||||
reset_engine();
|
||||
// Should not panic or error.
|
||||
close_engine().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_key_length_is_rejected() {
|
||||
reset_engine();
|
||||
let bad_key = vec![0u8; 16];
|
||||
let result = init_engine_in_memory(bad_key, NODE.into());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
//! FFI API surface for `flutter_rust_bridge` code generation.
|
||||
//!
|
||||
//! Every `pub fn` in the submodules below becomes a callable Dart function
|
||||
//! after running `flutter_rust_bridge_codegen generate`.
|
||||
//!
|
||||
//! ## Module Layout
|
||||
//!
|
||||
//! | Module | Responsibility |
|
||||
//! |---------------|-------------------------------------------------------|
|
||||
//! | [`types`] | FFI-safe DTO structs (auto-translated to Dart classes)|
|
||||
//! | [`engine`] | Engine lifecycle (init / close / status) |
|
||||
//! | [`database`] | CRDT document persistence via SQLCipher |
|
||||
//! | [`crypto`] | Argon2id key derivation and OS keychain ops |
|
||||
//! | [`task_crdt`] | Task CRDT create / read / mutate / merge |
|
||||
//! | [`note_crdt`] | Note CRDT create / read / mutate / merge |
|
||||
//!
|
||||
//! ## Internal State
|
||||
//!
|
||||
//! The [`state`] module (crate-internal) holds the global engine singleton.
|
||||
//! API functions acquire the engine lock as needed.
|
||||
|
||||
pub(crate) mod state;
|
||||
|
||||
pub mod crypto;
|
||||
pub mod database;
|
||||
pub mod engine;
|
||||
pub mod note_crdt;
|
||||
pub mod task_crdt;
|
||||
pub mod types;
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
//! Note CRDT operations — create, read, mutate, and merge note documents.
|
||||
//!
|
||||
//! ## Data Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Dart → note_crdt_create(id, ns, title, content) → Vec<u8>
|
||||
//! Dart → db_save_crdt_doc(id, "note", bytes, ts)
|
||||
//! Dart → db_load_crdt_doc(id) → Vec<u8>
|
||||
//! Dart → note_crdt_read(bytes) → FfiNoteData
|
||||
//! Dart → note_crdt_set_title(bytes, title) → Vec<u8>
|
||||
//! ```
|
||||
//!
|
||||
//! Mutation functions require an active engine (HLC). Read and merge are
|
||||
//! stateless.
|
||||
|
||||
use chronogrid_core::NoteCrdt;
|
||||
|
||||
use super::state::with_engine_mut;
|
||||
use super::types::FfiNoteData;
|
||||
|
||||
// ── Construction ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Creates a new note CRDT document and returns its serialized bytes.
|
||||
///
|
||||
/// **Requires active engine** (for HLC).
|
||||
pub fn note_crdt_create(
|
||||
id: String,
|
||||
namespace_id: String,
|
||||
title: String,
|
||||
content_markdown: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut note =
|
||||
NoteCrdt::new(&id, &namespace_id, &title, &content_markdown, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("note_crdt_create failed: {e}"))?;
|
||||
Ok(note.save())
|
||||
})
|
||||
}
|
||||
|
||||
// ── Read ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Deserializes a note CRDT document and returns a structured snapshot.
|
||||
///
|
||||
/// **Stateless** — does not require an active engine.
|
||||
pub fn note_crdt_read(doc_bytes: Vec<u8>) -> anyhow::Result<FfiNoteData> {
|
||||
let note = NoteCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("note_crdt_read: failed to load doc: {e}"))?;
|
||||
|
||||
Ok(FfiNoteData {
|
||||
id: note.entity_id()?.unwrap_or_default(),
|
||||
namespace_id: note.namespace_id()?.unwrap_or_default(),
|
||||
project_id: note.project_id()?,
|
||||
title: note.title()?.unwrap_or_default(),
|
||||
content_markdown: note.content_markdown()?.unwrap_or_default(),
|
||||
hlc_timestamp: note.hlc_timestamp()?.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Field mutations ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Sets the note title. **Requires active engine.**
|
||||
pub fn note_crdt_set_title(doc_bytes: Vec<u8>, title: String) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut note = NoteCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
note.set_title(&title, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(note.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the markdown content body.
|
||||
pub fn note_crdt_set_content(
|
||||
doc_bytes: Vec<u8>,
|
||||
content_markdown: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut note = NoteCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
note.set_content_markdown(&content_markdown, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(note.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the note to a different project.
|
||||
pub fn note_crdt_set_project_id(
|
||||
doc_bytes: Vec<u8>,
|
||||
project_id: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut note = NoteCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
note.set_project_id(&project_id, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(note.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the project association.
|
||||
pub fn note_crdt_clear_project_id(doc_bytes: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut note = NoteCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
note.clear_project_id(&mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(note.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the note to a different namespace.
|
||||
pub fn note_crdt_set_namespace_id(
|
||||
doc_bytes: Vec<u8>,
|
||||
namespace_id: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut note = NoteCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
note.set_namespace_id(&namespace_id, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(note.save())
|
||||
})
|
||||
}
|
||||
|
||||
// ── Merge ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Merges two note CRDT documents and returns the combined document bytes.
|
||||
///
|
||||
/// **Stateless** — does not require an active engine.
|
||||
pub fn note_crdt_merge(doc_a: Vec<u8>, doc_b: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
let mut note_a = NoteCrdt::from_bytes(&doc_a)
|
||||
.map_err(|e| anyhow::anyhow!("note_crdt_merge: failed to load doc_a: {e}"))?;
|
||||
let mut note_b = NoteCrdt::from_bytes(&doc_b)
|
||||
.map_err(|e| anyhow::anyhow!("note_crdt_merge: failed to load doc_b: {e}"))?;
|
||||
|
||||
note_a
|
||||
.merge(&mut note_b)
|
||||
.map_err(|e| anyhow::anyhow!("note_crdt_merge: merge failed: {e}"))?;
|
||||
|
||||
Ok(note_a.save())
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::engine::{close_engine, init_engine_in_memory};
|
||||
use super::*;
|
||||
|
||||
const TEST_KEY: [u8; 32] = [0xAA; 32];
|
||||
const NODE: &str = "test-node";
|
||||
|
||||
fn setup() {
|
||||
let _ = close_engine();
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
let _ = close_engine();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_read_round_trip() {
|
||||
setup();
|
||||
|
||||
let bytes = note_crdt_create(
|
||||
"n-001".into(),
|
||||
"ns-1".into(),
|
||||
"My Note".into(),
|
||||
"# Hello World".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let data = note_crdt_read(bytes).unwrap();
|
||||
|
||||
assert_eq!(data.id, "n-001");
|
||||
assert_eq!(data.namespace_id, "ns-1");
|
||||
assert_eq!(data.title, "My Note");
|
||||
assert_eq!(data.content_markdown, "# Hello World");
|
||||
assert!(!data.hlc_timestamp.is_empty());
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_all_fields_and_read() {
|
||||
setup();
|
||||
|
||||
let mut bytes = note_crdt_create(
|
||||
"n-002".into(),
|
||||
"ns-1".into(),
|
||||
"Title".into(),
|
||||
"Body".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
bytes = note_crdt_set_title(bytes, "New Title".into()).unwrap();
|
||||
bytes = note_crdt_set_content(bytes, "## Updated\nBody text".into()).unwrap();
|
||||
bytes = note_crdt_set_project_id(bytes, "p-001".into()).unwrap();
|
||||
bytes = note_crdt_set_namespace_id(bytes, "ns-2".into()).unwrap();
|
||||
|
||||
let data = note_crdt_read(bytes).unwrap();
|
||||
|
||||
assert_eq!(data.title, "New Title");
|
||||
assert_eq!(data.content_markdown, "## Updated\nBody text");
|
||||
assert_eq!(data.project_id, Some("p-001".into()));
|
||||
assert_eq!(data.namespace_id, "ns-2");
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_optional_fields() {
|
||||
setup();
|
||||
|
||||
let mut bytes = note_crdt_create(
|
||||
"n-003".into(),
|
||||
"ns-1".into(),
|
||||
"Title".into(),
|
||||
"Body".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
bytes = note_crdt_set_project_id(bytes, "p-1".into()).unwrap();
|
||||
bytes = note_crdt_clear_project_id(bytes).unwrap();
|
||||
|
||||
let data = note_crdt_read(bytes).unwrap();
|
||||
assert_eq!(data.project_id, None);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_concurrent_edits() {
|
||||
setup();
|
||||
|
||||
let bytes_a = note_crdt_create(
|
||||
"n-004".into(),
|
||||
"ns-1".into(),
|
||||
"Note".into(),
|
||||
"Original".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes_b =
|
||||
note_crdt_set_content(bytes_a.clone(), "Peer B content".into()).unwrap();
|
||||
let bytes_a =
|
||||
note_crdt_set_content(bytes_a, "Peer A content".into()).unwrap();
|
||||
|
||||
let merged = note_crdt_merge(bytes_a, bytes_b).unwrap();
|
||||
let data = note_crdt_read(merged).unwrap();
|
||||
|
||||
assert!(
|
||||
data.content_markdown == "Peer A content"
|
||||
|| data.content_markdown == "Peer B content",
|
||||
"merge must resolve to one of the concurrent writes, got: {}",
|
||||
data.content_markdown
|
||||
);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_is_stateless() {
|
||||
let _ = close_engine();
|
||||
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
let bytes = note_crdt_create(
|
||||
"n-005".into(),
|
||||
"ns-1".into(),
|
||||
"Stateless".into(),
|
||||
"Content".into(),
|
||||
)
|
||||
.unwrap();
|
||||
close_engine().unwrap();
|
||||
|
||||
let data = note_crdt_read(bytes).unwrap();
|
||||
assert_eq!(data.title, "Stateless");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_without_engine_fails() {
|
||||
let _ = close_engine();
|
||||
let result = note_crdt_create("x".into(), "y".into(), "z".into(), "w".into());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
//! Internal engine state — not part of the public FFI API.
|
||||
//!
|
||||
//! The [`Engine`] struct bundles an open [`Database`] connection and a
|
||||
//! [`Hlc`] (Hybrid Logical Clock) for the local node. A single global
|
||||
//! instance is held behind a [`Mutex`] so that FRB's thread-pool workers
|
||||
//! can safely share access.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use chronogrid_core::{Database, Hlc};
|
||||
|
||||
/// Runtime engine holding the encrypted database and the node's HLC.
|
||||
pub(crate) struct Engine {
|
||||
/// Encrypted SQLCipher database connection with ChronoGrid schema.
|
||||
pub(crate) db: Database,
|
||||
/// Hybrid Logical Clock for causally ordered CRDT mutations.
|
||||
pub(crate) hlc: Hlc,
|
||||
}
|
||||
|
||||
/// Global engine singleton.
|
||||
///
|
||||
/// `None` before [`super::engine::init_engine`] is called, `Some` while the
|
||||
/// engine is active, and `None` again after [`super::engine::close_engine`].
|
||||
pub(crate) static ENGINE: Mutex<Option<Engine>> = Mutex::new(None);
|
||||
|
||||
// ── Convenience helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/// Acquires the engine lock and calls `f` with a shared reference.
|
||||
///
|
||||
/// Returns `Err` if the engine has not been initialized or the lock is
|
||||
/// poisoned.
|
||||
pub(crate) fn with_engine<T>(
|
||||
f: impl FnOnce(&Engine) -> anyhow::Result<T>,
|
||||
) -> anyhow::Result<T> {
|
||||
let guard = ENGINE
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("engine lock poisoned: {e}"))?;
|
||||
let engine = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("engine not initialized — call init_engine first"))?;
|
||||
f(engine)
|
||||
}
|
||||
|
||||
/// Acquires the engine lock and calls `f` with a mutable reference.
|
||||
///
|
||||
/// Returns `Err` if the engine has not been initialized or the lock is
|
||||
/// poisoned.
|
||||
pub(crate) fn with_engine_mut<T>(
|
||||
f: impl FnOnce(&mut Engine) -> anyhow::Result<T>,
|
||||
) -> anyhow::Result<T> {
|
||||
let mut guard = ENGINE
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("engine lock poisoned: {e}"))?;
|
||||
let engine = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("engine not initialized — call init_engine first"))?;
|
||||
f(engine)
|
||||
}
|
||||
|
|
@ -1,445 +0,0 @@
|
|||
//! Task CRDT operations — create, read, mutate, and merge task documents.
|
||||
//!
|
||||
//! ## Data Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Dart → task_crdt_create(id, ns, title) → Vec<u8> (serialized Automerge doc)
|
||||
//! Dart → db_save_crdt_doc(id, "task", bytes, ts) (persist to SQLCipher)
|
||||
//! Dart → db_load_crdt_doc(id) → Vec<u8> (reload from DB)
|
||||
//! Dart → task_crdt_read(bytes) → FfiTaskData (structured snapshot)
|
||||
//! Dart → task_crdt_set_title(bytes, title) → Vec<u8>(mutated doc bytes)
|
||||
//! ```
|
||||
//!
|
||||
//! Mutation functions (create, set_*) require an active engine because they
|
||||
//! advance the node's Hybrid Logical Clock. Read and merge functions are
|
||||
//! stateless.
|
||||
|
||||
use chronogrid_core::TaskCrdt;
|
||||
|
||||
use super::state::with_engine_mut;
|
||||
use super::types::FfiTaskData;
|
||||
|
||||
// ── Construction ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Creates a new task CRDT document and returns its serialized bytes.
|
||||
///
|
||||
/// The document is initialized with `status = "backlog"`,
|
||||
/// `priority_score = 0.0`, and `is_archived = false`.
|
||||
///
|
||||
/// **Requires active engine** (for HLC).
|
||||
pub fn task_crdt_create(
|
||||
id: String,
|
||||
namespace_id: String,
|
||||
title: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::new(&id, &namespace_id, &title, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("task_crdt_create failed: {e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
// ── Read ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Deserializes a task CRDT document and returns a structured snapshot.
|
||||
///
|
||||
/// **Stateless** — does not require an active engine.
|
||||
pub fn task_crdt_read(doc_bytes: Vec<u8>) -> anyhow::Result<FfiTaskData> {
|
||||
let task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("task_crdt_read: failed to load doc: {e}"))?;
|
||||
|
||||
Ok(FfiTaskData {
|
||||
id: task.entity_id()?.unwrap_or_default(),
|
||||
namespace_id: task.namespace_id()?.unwrap_or_default(),
|
||||
project_id: task.project_id()?,
|
||||
title: task.title()?.unwrap_or_default(),
|
||||
description_markdown: task.description_markdown()?,
|
||||
status: task.status()?.unwrap_or_else(|| "backlog".into()),
|
||||
priority_score: task.priority_score()?.unwrap_or(0.0),
|
||||
eisenhower_quadrant: task.eisenhower_quadrant()?,
|
||||
due_date: task.due_date()?.map(|v| v as i64),
|
||||
estimated_duration: task.estimated_duration()?,
|
||||
is_archived: task.is_archived()?.unwrap_or(false),
|
||||
completed_at: task.completed_at()?.map(|v| v as i64),
|
||||
hlc_timestamp: task.hlc_timestamp()?.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Field mutations ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Each setter loads the doc from bytes, applies the mutation with an
|
||||
// advancing HLC, re-serializes, and returns the updated bytes.
|
||||
|
||||
/// Sets the task title. **Requires active engine.**
|
||||
pub fn task_crdt_set_title(doc_bytes: Vec<u8>, title: String) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_title(&title, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the task status (`backlog`, `in_progress`, `review`, `completed`).
|
||||
pub fn task_crdt_set_status(doc_bytes: Vec<u8>, status: String) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_status(&status, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the markdown description.
|
||||
pub fn task_crdt_set_description(
|
||||
doc_bytes: Vec<u8>,
|
||||
description: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_description_markdown(&description, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears the markdown description.
|
||||
pub fn task_crdt_clear_description(doc_bytes: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.clear_description(&mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the computed priority score.
|
||||
pub fn task_crdt_set_priority_score(
|
||||
doc_bytes: Vec<u8>,
|
||||
score: f64,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_priority_score(score, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the Eisenhower quadrant (1–4).
|
||||
pub fn task_crdt_set_eisenhower_quadrant(
|
||||
doc_bytes: Vec<u8>,
|
||||
quadrant: i64,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_eisenhower_quadrant(quadrant, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears the Eisenhower quadrant.
|
||||
pub fn task_crdt_clear_eisenhower_quadrant(doc_bytes: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.clear_eisenhower_quadrant(&mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the due date (Unix epoch milliseconds).
|
||||
pub fn task_crdt_set_due_date(doc_bytes: Vec<u8>, due_ms: i64) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_due_date(due_ms as u64, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears the due date.
|
||||
pub fn task_crdt_clear_due_date(doc_bytes: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.clear_due_date(&mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the estimated duration in minutes.
|
||||
pub fn task_crdt_set_estimated_duration(
|
||||
doc_bytes: Vec<u8>,
|
||||
minutes: i64,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_estimated_duration(minutes, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the archived flag.
|
||||
pub fn task_crdt_set_is_archived(
|
||||
doc_bytes: Vec<u8>,
|
||||
archived: bool,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_is_archived(archived, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the completion timestamp (Unix epoch milliseconds).
|
||||
pub fn task_crdt_set_completed_at(
|
||||
doc_bytes: Vec<u8>,
|
||||
ts_ms: i64,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_completed_at(ts_ms as u64, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears the completion timestamp.
|
||||
pub fn task_crdt_clear_completed_at(doc_bytes: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.clear_completed_at(&mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the task to a different project.
|
||||
pub fn task_crdt_set_project_id(
|
||||
doc_bytes: Vec<u8>,
|
||||
project_id: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_project_id(&project_id, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the project association.
|
||||
pub fn task_crdt_clear_project_id(doc_bytes: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.clear_project_id(&mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the task to a different namespace.
|
||||
pub fn task_crdt_set_namespace_id(
|
||||
doc_bytes: Vec<u8>,
|
||||
namespace_id: String,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
with_engine_mut(|engine| {
|
||||
let mut task = TaskCrdt::from_bytes(&doc_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
task.set_namespace_id(&namespace_id, &mut engine.hlc)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(task.save())
|
||||
})
|
||||
}
|
||||
|
||||
// ── Merge ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Merges two task CRDT documents and returns the combined document bytes.
|
||||
///
|
||||
/// **Stateless** — does not require an active engine. Automerge resolves
|
||||
/// concurrent writes deterministically via its built-in LWW semantics.
|
||||
pub fn task_crdt_merge(doc_a: Vec<u8>, doc_b: Vec<u8>) -> anyhow::Result<Vec<u8>> {
|
||||
let mut task_a = TaskCrdt::from_bytes(&doc_a)
|
||||
.map_err(|e| anyhow::anyhow!("task_crdt_merge: failed to load doc_a: {e}"))?;
|
||||
let mut task_b = TaskCrdt::from_bytes(&doc_b)
|
||||
.map_err(|e| anyhow::anyhow!("task_crdt_merge: failed to load doc_b: {e}"))?;
|
||||
|
||||
task_a
|
||||
.merge(&mut task_b)
|
||||
.map_err(|e| anyhow::anyhow!("task_crdt_merge: merge failed: {e}"))?;
|
||||
|
||||
Ok(task_a.save())
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::engine::{close_engine, init_engine_in_memory};
|
||||
use super::*;
|
||||
|
||||
const TEST_KEY: [u8; 32] = [0xAA; 32];
|
||||
const NODE: &str = "test-node";
|
||||
|
||||
fn setup() {
|
||||
let _ = close_engine();
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
let _ = close_engine();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_read_round_trip() {
|
||||
setup();
|
||||
|
||||
let bytes = task_crdt_create("t-001".into(), "ns-1".into(), "My Task".into())
|
||||
.unwrap();
|
||||
let data = task_crdt_read(bytes).unwrap();
|
||||
|
||||
assert_eq!(data.id, "t-001");
|
||||
assert_eq!(data.namespace_id, "ns-1");
|
||||
assert_eq!(data.title, "My Task");
|
||||
assert_eq!(data.status, "backlog");
|
||||
assert!((data.priority_score - 0.0).abs() < f64::EPSILON);
|
||||
assert!(!data.is_archived);
|
||||
assert!(!data.hlc_timestamp.is_empty());
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_all_fields_and_read() {
|
||||
setup();
|
||||
|
||||
let mut bytes =
|
||||
task_crdt_create("t-002".into(), "ns-1".into(), "Initial".into()).unwrap();
|
||||
|
||||
bytes = task_crdt_set_title(bytes, "Updated".into()).unwrap();
|
||||
bytes = task_crdt_set_status(bytes, "in_progress".into()).unwrap();
|
||||
bytes = task_crdt_set_description(bytes, "# Desc".into()).unwrap();
|
||||
bytes = task_crdt_set_priority_score(bytes, 0.75).unwrap();
|
||||
bytes = task_crdt_set_eisenhower_quadrant(bytes, 2).unwrap();
|
||||
bytes = task_crdt_set_due_date(bytes, 1_700_000_000_000).unwrap();
|
||||
bytes = task_crdt_set_estimated_duration(bytes, 60).unwrap();
|
||||
bytes = task_crdt_set_is_archived(bytes, true).unwrap();
|
||||
bytes = task_crdt_set_completed_at(bytes, 1_700_100_000_000).unwrap();
|
||||
bytes = task_crdt_set_project_id(bytes, "p-001".into()).unwrap();
|
||||
bytes = task_crdt_set_namespace_id(bytes, "ns-2".into()).unwrap();
|
||||
|
||||
let data = task_crdt_read(bytes).unwrap();
|
||||
|
||||
assert_eq!(data.title, "Updated");
|
||||
assert_eq!(data.status, "in_progress");
|
||||
assert_eq!(data.description_markdown, Some("# Desc".into()));
|
||||
assert!((data.priority_score - 0.75).abs() < f64::EPSILON);
|
||||
assert_eq!(data.eisenhower_quadrant, Some(2));
|
||||
assert_eq!(data.due_date, Some(1_700_000_000_000));
|
||||
assert_eq!(data.estimated_duration, Some(60));
|
||||
assert!(data.is_archived);
|
||||
assert_eq!(data.completed_at, Some(1_700_100_000_000));
|
||||
assert_eq!(data.project_id, Some("p-001".into()));
|
||||
assert_eq!(data.namespace_id, "ns-2");
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_optional_fields() {
|
||||
setup();
|
||||
|
||||
let mut bytes =
|
||||
task_crdt_create("t-003".into(), "ns-1".into(), "Task".into()).unwrap();
|
||||
|
||||
bytes = task_crdt_set_description(bytes, "desc".into()).unwrap();
|
||||
bytes = task_crdt_clear_description(bytes).unwrap();
|
||||
|
||||
bytes = task_crdt_set_eisenhower_quadrant(bytes, 3).unwrap();
|
||||
bytes = task_crdt_clear_eisenhower_quadrant(bytes).unwrap();
|
||||
|
||||
bytes = task_crdt_set_due_date(bytes, 123).unwrap();
|
||||
bytes = task_crdt_clear_due_date(bytes).unwrap();
|
||||
|
||||
bytes = task_crdt_set_completed_at(bytes, 456).unwrap();
|
||||
bytes = task_crdt_clear_completed_at(bytes).unwrap();
|
||||
|
||||
bytes = task_crdt_set_project_id(bytes, "p-1".into()).unwrap();
|
||||
bytes = task_crdt_clear_project_id(bytes).unwrap();
|
||||
|
||||
let data = task_crdt_read(bytes).unwrap();
|
||||
assert_eq!(data.description_markdown, None);
|
||||
assert_eq!(data.eisenhower_quadrant, None);
|
||||
assert_eq!(data.due_date, None);
|
||||
assert_eq!(data.completed_at, None);
|
||||
assert_eq!(data.project_id, None);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_concurrent_edits() {
|
||||
setup();
|
||||
|
||||
let bytes_a =
|
||||
task_crdt_create("t-004".into(), "ns-1".into(), "Original".into()).unwrap();
|
||||
|
||||
// Simulate a concurrent peer by creating a second copy.
|
||||
// (In production, forks would come from network sync.)
|
||||
let bytes_b = task_crdt_set_title(bytes_a.clone(), "Peer B Title".into()).unwrap();
|
||||
let bytes_a = task_crdt_set_title(bytes_a, "Peer A Title".into()).unwrap();
|
||||
|
||||
let merged = task_crdt_merge(bytes_a, bytes_b).unwrap();
|
||||
let data = task_crdt_read(merged).unwrap();
|
||||
|
||||
// Automerge picks a deterministic winner.
|
||||
assert!(
|
||||
data.title == "Peer A Title" || data.title == "Peer B Title",
|
||||
"merge must resolve to one of the concurrent writes, got: {}",
|
||||
data.title
|
||||
);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_is_stateless() {
|
||||
// Ensure read works without engine.
|
||||
let _ = close_engine();
|
||||
|
||||
// We need bytes from a previous engine session.
|
||||
init_engine_in_memory(TEST_KEY.to_vec(), NODE.into()).unwrap();
|
||||
let bytes =
|
||||
task_crdt_create("t-005".into(), "ns-1".into(), "Stateless".into()).unwrap();
|
||||
close_engine().unwrap();
|
||||
|
||||
// Read after engine shutdown should still work.
|
||||
let data = task_crdt_read(bytes).unwrap();
|
||||
assert_eq!(data.title, "Stateless");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_without_engine_fails() {
|
||||
let _ = close_engine();
|
||||
let result = task_crdt_create("x".into(), "y".into(), "z".into());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
//! FFI-safe Data Transfer Object (DTO) structs.
|
||||
//!
|
||||
//! These structs use only primitive types that `flutter_rust_bridge` can
|
||||
//! translate directly into Dart classes. They serve as the serialization
|
||||
//! boundary between the Rust CRDT layer and the Flutter presentation layer.
|
||||
|
||||
/// Derived master key + salt returned from Argon2id key derivation.
|
||||
///
|
||||
/// On the Dart side this becomes a class with `Uint8List key` and
|
||||
/// `Uint8List salt` fields.
|
||||
pub struct FfiDerivedKey {
|
||||
/// 32-byte (256-bit) derived master key.
|
||||
pub key: Vec<u8>,
|
||||
/// 16-byte salt used during derivation.
|
||||
pub salt: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Read-only snapshot of a task entity's CRDT state.
|
||||
///
|
||||
/// Returned by [`super::task_crdt::task_crdt_read`] after deserializing an
|
||||
/// Automerge document. All fields mirror the `tasks` table in
|
||||
/// `docs/API_CONTRACTS/schema.sql`.
|
||||
pub struct FfiTaskData {
|
||||
/// UUIDv4 entity identifier.
|
||||
pub id: String,
|
||||
/// Owning namespace ID.
|
||||
pub namespace_id: String,
|
||||
/// Optional project association.
|
||||
pub project_id: Option<String>,
|
||||
/// Task title (required).
|
||||
pub title: String,
|
||||
/// Optional markdown description.
|
||||
pub description_markdown: Option<String>,
|
||||
/// Workflow status: `backlog`, `in_progress`, `review`, `completed`.
|
||||
pub status: String,
|
||||
/// Calculated urgency-importance vector score.
|
||||
pub priority_score: f64,
|
||||
/// Eisenhower matrix quadrant (1–4), if assigned.
|
||||
pub eisenhower_quadrant: Option<i64>,
|
||||
/// Due date as Unix epoch milliseconds, if set.
|
||||
pub due_date: Option<i64>,
|
||||
/// Estimated duration in minutes, if set.
|
||||
pub estimated_duration: Option<i64>,
|
||||
/// Whether the task is archived.
|
||||
pub is_archived: bool,
|
||||
/// Completion timestamp (Unix epoch ms), if completed.
|
||||
pub completed_at: Option<i64>,
|
||||
/// Hybrid Logical Clock timestamp string.
|
||||
pub hlc_timestamp: String,
|
||||
}
|
||||
|
||||
/// Read-only snapshot of a note entity's CRDT state.
|
||||
///
|
||||
/// Returned by [`super::note_crdt::note_crdt_read`] after deserializing an
|
||||
/// Automerge document. All fields mirror the `notes` table in
|
||||
/// `docs/API_CONTRACTS/schema.sql`.
|
||||
pub struct FfiNoteData {
|
||||
/// UUIDv4 entity identifier.
|
||||
pub id: String,
|
||||
/// Owning namespace ID.
|
||||
pub namespace_id: String,
|
||||
/// Optional project association.
|
||||
pub project_id: Option<String>,
|
||||
/// Note title (required).
|
||||
pub title: String,
|
||||
/// Markdown content body.
|
||||
pub content_markdown: String,
|
||||
/// Hybrid Logical Clock timestamp string.
|
||||
pub hlc_timestamp: String,
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
//! # chronogrid_ffi
|
||||
//!
|
||||
//! Flutter Rust Bridge (FRB v2) FFI layer for **ChronoGrid P2P**.
|
||||
//!
|
||||
//! This crate exposes the storage, CRDT, and cryptographic capabilities of
|
||||
//! [`chronogrid_core`] and [`chronogrid_crypto`] through a flat, FFI-friendly
|
||||
//! API surface that `flutter_rust_bridge_codegen` can translate into Dart
|
||||
//! bindings.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌─────────────────────┐
|
||||
//! │ Flutter / Dart UI │
|
||||
//! └────────┬────────────┘
|
||||
//! │ generated Dart bindings
|
||||
//! ┌────────▼────────────┐
|
||||
//! │ chronogrid_ffi │ ← this crate (C-ABI cdylib)
|
||||
//! └────────┬────────────┘
|
||||
//! │ Rust crate deps
|
||||
//! ┌────────▼────────────┐
|
||||
//! │ chronogrid_core │ SQLCipher + Automerge CRDT
|
||||
//! │ chronogrid_crypto │ Argon2id KDF + OS Keychain
|
||||
//! └─────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Engine Pattern
|
||||
//!
|
||||
//! A global [`api::state::Engine`] singleton (behind a `Mutex`) holds the open
|
||||
//! database connection and the node's Hybrid Logical Clock. All mutating CRDT
|
||||
//! and database operations acquire the lock, perform work, and release it.
|
||||
//! This is thread-safe and compatible with FRB's Dart isolate thread pool.
|
||||
//!
|
||||
//! ## Code Generation
|
||||
//!
|
||||
//! After modifying the `api/` module, run:
|
||||
//!
|
||||
//! ```bash
|
||||
//! flutter_rust_bridge_codegen generate
|
||||
//! ```
|
||||
//!
|
||||
//! This produces `src/frb_generated.rs` (Rust glue) and Dart binding files in
|
||||
//! the Flutter project. The generated Rust module is included below only when
|
||||
//! the file exists (i.e., after the first codegen run).
|
||||
|
||||
pub mod api;
|
||||
|
||||
// The FRB codegen produces this module. It does not exist until
|
||||
// `flutter_rust_bridge_codegen generate` has been run at least once.
|
||||
// Uncomment the line below after running codegen:
|
||||
//
|
||||
// mod frb_generated;
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# ADR-0005: FFI Bridge via Flutter Rust Bridge v2
|
||||
|
||||
- **Status**: Accepted
|
||||
- **Date**: 2026-08-20
|
||||
- **Task**: 1.5 — Generate flutter_rust_bridge FFI bindings bridging Rust storage methods to Dart
|
||||
|
||||
## Context
|
||||
|
||||
Tasks 1.1–1.4 established the Rust core engine (`chronogrid_core` with SQLCipher + Automerge CRDT) and the cryptographic primitives (`chronogrid_crypto` with Argon2id KDF + OS keychain). The Flutter presentation layer needs a type-safe, zero-copy FFI bridge to invoke these Rust functions from Dart.
|
||||
|
||||
The project's architectural mandate (`.clinerules`) requires: *"FFI Bridge: Use `flutter_rust_bridge` C-ABI generation ONLY."*
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. New Crate: `chronogrid_ffi`
|
||||
|
||||
A dedicated bridge crate (`crates/chronogrid_ffi/`) was created with:
|
||||
|
||||
- **`[lib] crate-type = ["lib", "cdylib", "staticlib"]`** — produces a shared library for Flutter's FFI loader on all platforms, plus a static library for iOS.
|
||||
- **Dependencies**: `chronogrid_core`, `chronogrid_crypto`, `flutter_rust_bridge = "=2.7.0"`, `anyhow = "1"`.
|
||||
|
||||
### 2. API Surface Design — Serialized CRDT Bytes + Engine Singleton
|
||||
|
||||
Rather than passing opaque Rust pointers across FFI, the API uses a **serialized-bytes pattern**:
|
||||
|
||||
- **CRDT documents** are serialized to/from `Vec<u8>` (Automerge binary format).
|
||||
- **Mutation functions** accept `doc_bytes: Vec<u8>`, apply the edit, and return updated `Vec<u8>`.
|
||||
- **Read functions** (`task_crdt_read`, `note_crdt_read`) deserialize bytes into FFI-safe DTO structs (`FfiTaskData`, `FfiNoteData`).
|
||||
- **Merge functions** are stateless — they accept two document byte arrays and return the merged result.
|
||||
|
||||
This avoids opaque pointer lifetime management, ensures Dart never holds stale Rust references, and leverages Automerge's natural serialization boundary.
|
||||
|
||||
### 3. Engine Singleton (`Mutex<Option<Engine>>`)
|
||||
|
||||
A global `Engine` struct holds:
|
||||
- `db: Database` — the open SQLCipher connection.
|
||||
- `hlc: Hlc` — the node's Hybrid Logical Clock.
|
||||
|
||||
The engine is stored in a `static Mutex<Option<Engine>>` with helper functions `with_engine()` and `with_engine_mut()` for safe concurrent access from FRB's thread pool.
|
||||
|
||||
- `init_engine(path, key, node_id)` / `init_engine_in_memory(key, node_id)` — opens the database and seeds the HLC.
|
||||
- `close_engine()` — drops the connection.
|
||||
- `is_engine_initialized()` — status check.
|
||||
|
||||
### 4. Module Structure
|
||||
|
||||
| Module | Functions | Category |
|
||||
|----------------|-----------|----------|
|
||||
| `engine` | 4 | Lifecycle (init/close/status) |
|
||||
| `database` | 3 | CRDT doc persistence (save/load/delete) |
|
||||
| `crypto` | 5 | Key derivation + keychain (platform-gated) |
|
||||
| `task_crdt` | 18 | Task CRDT create/read/set_*/clear_*/merge |
|
||||
| `note_crdt` | 8 | Note CRDT create/read/set_*/clear_*/merge |
|
||||
| `types` | 3 structs | `FfiDerivedKey`, `FfiTaskData`, `FfiNoteData` |
|
||||
|
||||
**Total**: 38 public FFI functions + 3 DTO structs.
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
All fallible functions return `anyhow::Result<T>`, which FRB v2 automatically maps to Dart exceptions with the error message string. Internal `CoreError` and `CryptoError` variants are converted via their `Display` impls.
|
||||
|
||||
### 6. Platform Gating
|
||||
|
||||
Keychain functions (`crypto_store_credential`, `crypto_retrieve_credential`, `crypto_delete_credential`) compile on all platforms but return an error on Android, where credential storage is delegated to the Flutter KeyStore bridge.
|
||||
|
||||
### 7. Code Generation Config
|
||||
|
||||
`flutter_rust_bridge.yaml` at workspace root configures the codegen:
|
||||
- **Rust input**: `crates/chronogrid_ffi/src/api/`
|
||||
- **Rust output**: `crates/chronogrid_ffi/src/frb_generated.rs`
|
||||
- **Dart output**: `apps/chronogrid_flutter/lib/src/rust/`
|
||||
|
||||
After running `flutter_rust_bridge_codegen generate`, the `frb_generated` module must be uncommented in `lib.rs`.
|
||||
|
||||
### 8. Flutter Side
|
||||
|
||||
`pubspec.yaml` updated with `flutter_rust_bridge: ^2.7.0`, `ffi: ^2.1.0`, and `meta: ^1.11.0`.
|
||||
|
||||
## Testing
|
||||
|
||||
- **25 unit tests** covering: engine lifecycle (init/close/double-init/bad-key), database CRDT doc persistence (save/load/delete/missing), task CRDT (create/read/set-all-fields/clear-optionals/merge/stateless-read/no-engine-error), note CRDT (create/read/set-all-fields/clear-optionals/merge/stateless-read/no-engine-error), and crypto (derive/deterministic/invalid-salt/generate-salt).
|
||||
- All 25 tests pass with `--test-threads=1` (required due to global engine singleton).
|
||||
- Verification pipeline (`scripts/verify_agent_work.ps1`) passes all checks.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The Dart presentation layer can invoke all core Rust functionality via generated type-safe bindings after a single `flutter_rust_bridge_codegen generate` run.
|
||||
- CRDT documents flow as opaque byte arrays across FFI, maintaining the local-first zero-cloud mandate.
|
||||
- The engine singleton pattern means only one database can be open at a time per process, which matches the single-user design.
|
||||
- Future phases can extend the API by adding new `pub fn` entries to the `api/` modules and re-running codegen.
|
||||
|
|
@ -6,4 +6,4 @@
|
|||
- [x] **Task 1.2**: Implement chronogrid_crypto crate with Argon2id master key derivation and OS keychain bindings.
|
||||
- [x] **Task 1.3**: Implement chronogrid_core database initializers using SQLCipher 4.x matching docs/API_CONTRACTS/schema.sql.
|
||||
- [x] **Task 1.4**: Integrate Automerge Rust crate into chronogrid_core for task and note CRDT register mutations.
|
||||
- [x] **Task 1.5**: Generate flutter_rust_bridge FFI bindings bridging Rust storage methods to Dart.
|
||||
- [ ] **Task 1.5**: Generate lutter_rust_bridge FFI bindings bridging Rust storage methods to Dart.
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
# ==============================================================================
|
||||
# Flutter Rust Bridge v2 – Code Generation Configuration
|
||||
# ==============================================================================
|
||||
#
|
||||
# This file configures `flutter_rust_bridge_codegen generate` to scan the
|
||||
# Rust FFI API surface and produce both Dart bindings and Rust glue code.
|
||||
#
|
||||
# Run code generation after modifying any `pub fn` in the api/ module:
|
||||
#
|
||||
# flutter_rust_bridge_codegen generate
|
||||
#
|
||||
# Prerequisites:
|
||||
# cargo install flutter_rust_bridge_codegen@2.7.0
|
||||
#
|
||||
# The generated files are:
|
||||
# Rust: crates/chronogrid_ffi/src/frb_generated.rs
|
||||
# crates/chronogrid_ffi/src/frb_generated.io.rs
|
||||
# Dart: apps/chronogrid_flutter/lib/src/rust/frb_generated.dart
|
||||
# apps/chronogrid_flutter/lib/src/rust/frb_generated.io.dart
|
||||
# apps/chronogrid_flutter/lib/src/rust/api/*.dart
|
||||
# ==============================================================================
|
||||
|
||||
rust_input: crates/chronogrid_ffi/src/api/
|
||||
rust_root: crates/chronogrid_ffi/
|
||||
rust_output: crates/chronogrid_ffi/src/frb_generated.rs
|
||||
|
||||
dart_output: apps/chronogrid_flutter/lib/src/rust/
|
||||
|
||||
# Additional settings
|
||||
c_output: apps/chronogrid_flutter/lib/src/rust/frb_generated.h
|
||||
|
|
@ -7,8 +7,8 @@ $ErrorActionPreference = "Continue"
|
|||
|
||||
Write-Host "--- RUNNING AGENT VERIFICATION PIPELINE ---" -ForegroundColor Cyan
|
||||
|
||||
# 1. Check Rust Compilation for chronogrid_core
|
||||
Write-Host "1. Checking chronogrid_core..." -ForegroundColor Yellow
|
||||
# 1. Check Rust Compilation & Formatting
|
||||
Write-Host "1. Testing Rust Crates..." -ForegroundColor Yellow
|
||||
Set-Location "$PSScriptRoot\..\crates\chronogrid_core"
|
||||
cargo check --quiet
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
|
@ -16,17 +16,8 @@ if ($LASTEXITCODE -ne 0) {
|
|||
Exit 1
|
||||
}
|
||||
|
||||
# 2. Check Rust Compilation for chronogrid_ffi
|
||||
Write-Host "2. Checking chronogrid_ffi..." -ForegroundColor Yellow
|
||||
Set-Location "$PSScriptRoot\..\crates\chronogrid_ffi"
|
||||
cargo check --quiet
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "FAILED: Rust compilation errors detected in chronogrid_ffi." -ForegroundColor Red
|
||||
Exit 1
|
||||
}
|
||||
|
||||
# 3. Check Flutter Code Formatting & Dependencies
|
||||
Write-Host "3. Checking Flutter Shell..." -ForegroundColor Yellow
|
||||
# 2. Check Flutter Code Formatting & Dependencies
|
||||
Write-Host "2. Checking Flutter Shell..." -ForegroundColor Yellow
|
||||
Set-Location "$PSScriptRoot\..\apps\chronogrid_flutter"
|
||||
flutter analyze
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue