diff --git a/Cargo.lock b/Cargo.lock index 23ba539..3281539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,20 +37,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.5" @@ -60,12 +46,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.6" @@ -208,21 +188,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "2.13.1" @@ -238,24 +203,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytes" version = "1.12.1" @@ -306,7 +259,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -388,18 +341,17 @@ dependencies = [ [[package]] name = "cultcache-rs" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "anyhow", "chrono", "cultcache-rs-derive", "fs2", - "pretty_assertions", "redb", "rmp-serde", "serde", "serde_bytes", "serde_json", - "tempfile", "uuid", "windows-sys 0.59.0", ] @@ -407,6 +359,7 @@ dependencies = [ [[package]] name = "cultcache-rs-derive" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "proc-macro2", "quote", @@ -416,19 +369,18 @@ dependencies = [ [[package]] name = "cultmesh-rs" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "anyhow", "cultcache-rs", "cultnet-rs", - "pretty_assertions", - "rmp-serde", "serde", - "tempfile", ] [[package]] name = "cultnet-rs" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "aes-gcm", "anyhow", @@ -437,7 +389,6 @@ dependencies = [ "cultcache-rs", "ed25519-dalek", "hmac", - "pretty_assertions", "rand 0.9.5", "rmp-serde", "rmpv", @@ -446,9 +397,9 @@ dependencies = [ "serde_json", "sha2", "socket2 0.5.10", - "tempfile", "uuid", "windows-sys 0.61.2", + "zeroize", ] [[package]] @@ -478,12 +429,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - [[package]] name = "der" version = "0.7.10" @@ -500,12 +445,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.10.7" @@ -528,12 +467,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "ed25519" version = "2.2.3" @@ -558,21 +491,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.3.14" @@ -583,17 +501,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "fancy-regex" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -612,23 +519,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" -[[package]] -name = "fluent-uri" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -638,16 +528,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fraction" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" -dependencies = [ - "lazy_static", - "num", -] - [[package]] name = "fs2" version = "0.4.3" @@ -748,11 +628,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -796,13 +674,11 @@ dependencies = [ "cultnet-rs", "ghostlight-persona-projection", "hmac", - "jsonschema", "jsonwebtoken", "libc", "rand 0.9.5", "reqwest", "rmp-serde", - "schemars", "serde", "serde_bytes", "serde_json", @@ -815,7 +691,6 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", - "windows", "zeroize", ] @@ -827,23 +702,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hmac" version = "0.12.1" @@ -976,7 +834,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1124,58 +982,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonschema" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ec8a241beed129f06114aa68007e905ca350e7baeb6e17a7631bb7978d91b2" -dependencies = [ - "ahash", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "jsonschema-regex", - "jsonschema-value", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "serde", - "serde_json", - "strum", - "unicode-general-category", - "uuid-simd", -] - -[[package]] -name = "jsonschema-regex" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91994f45017ed5e66aa8e59b8415f4cb033a6380d7200387b7cf117595fbdf85" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "jsonschema-value" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec7637f83e510868ae6ed625f7ebfbbde4554ee8ce49854caa5126a8b9b9ecb" -dependencies = [ - "ahash", - "bytecount", - "fraction", - "num-cmp", - "num-traits", - "serde_json", -] - [[package]] name = "jsonwebtoken" version = "9.3.1" @@ -1257,12 +1063,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "micromap" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" - [[package]] name = "mime" version = "0.3.17" @@ -1299,20 +1099,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.8" @@ -1323,21 +1109,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.2" @@ -1353,27 +1124,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1395,12 +1145,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1421,7 +1165,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1492,16 +1236,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1670,55 +1404,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "ref-cast" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "referencing" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6efa2154ea6f5ce0fdecdd2a8d18f2fa1a39a8fbba91564f555a592e4dce8278" -dependencies = [ - "ahash", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown", - "itoa", - "micromap", - "parking_lot", - "percent-encoding", - "serde_json", -] - -[[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" @@ -1896,33 +1581,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schemars" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" -dependencies = [ - "chrono", - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", - "uuid", -] - -[[package]] -name = "schemars_derive" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 3.0.3", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -1975,17 +1633,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "serde_derive_internals" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - [[package]] name = "serde_json" version = "1.0.151" @@ -2127,27 +1774,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "subtle" version = "2.6.1" @@ -2480,12 +2106,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -2538,16 +2158,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "valuable" version = "0.1.1" @@ -2560,12 +2170,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "want" version = "0.3.1" @@ -2709,41 +2313,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -2752,20 +2321,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -2790,53 +2348,19 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -2845,7 +2369,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2872,7 +2396,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2891,15 +2415,6 @@ dependencies = [ "windows_x86_64_msvc", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2960,12 +2475,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index ce04448..cdaf955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,6 @@ members = [ "crates/ghostlight-dungeon", "crates/ghostlight-persona-projection", - "vendor/cultcache-rs", - "vendor/cultcache-rs/cultcache-rs-derive", - "vendor/cultnet-rs", - "vendor/cultmesh-rs", ] resolver = "2" @@ -23,9 +19,9 @@ axum = { version = "0.8", features = ["macros"] } chrono = { version = "0.4", features = ["serde"] } base64 = "0.22" codex-connector = { git = "https://github.com/GameCult/CodexConnector.git", rev = "68fe94b9475823de92ed766a97ea962d48723708" } -cultcache-rs = { path = "vendor/cultcache-rs" } -cultnet-rs = { path = "vendor/cultnet-rs" } -cultmesh-rs = { path = "vendor/cultmesh-rs" } +cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "85f7024a35ea42cf9f0a0199a064526427150191", package = "cultcache-rs" } +cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "85f7024a35ea42cf9f0a0199a064526427150191", package = "cultnet-rs" } +cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "85f7024a35ea42cf9f0a0199a064526427150191", package = "cultmesh-rs" } jsonwebtoken = "9" hmac = "0.12" rand = "0.9" diff --git a/vendor/README.md b/vendor/README.md deleted file mode 100644 index 5b397de..0000000 --- a/vendor/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Vendored GameCult Rust substrate - -`cultcache-rs`, `cultnet-rs`, and `cultmesh-rs` are a locally owned compatible -protocol generation. Ghostlight vendors the set so an immutable release can be -rebuilt from its exact Git commit. - -The vendored CultCache crate owns both the snapshot store and the pinned redb -store used by application sessions, the world journal, and controller-work -custody. Its redb table, key, and envelope encoding remain compatible with the -existing Ghostlight stores. There is no second CultCache package or persistence -authority in the workspace. diff --git a/vendor/cultcache-rs/.gitignore b/vendor/cultcache-rs/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/vendor/cultcache-rs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/vendor/cultcache-rs/Cargo.toml b/vendor/cultcache-rs/Cargo.toml deleted file mode 100644 index ef221d1..0000000 --- a/vendor/cultcache-rs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "cultcache-rs" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -name = "cultcache_rs" -path = "src/lib.rs" - -[dependencies] -anyhow = "1" -chrono = { version = "0.4", features = ["serde"] } -cultcache-rs-derive = { path = "cultcache-rs-derive" } -fs2 = "0.4" -redb = "4.1.0" -rmp-serde = "1.3" -serde = { version = "1", features = ["derive"] } -serde_bytes = "0.11" -serde_json = "1" -uuid = { version = "1", features = ["v4"] } - -[dev-dependencies] -pretty_assertions = "1.4" -tempfile = "3" - -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage_FileSystem"] } diff --git a/vendor/cultcache-rs/README.md b/vendor/cultcache-rs/README.md deleted file mode 100644 index b21c324..0000000 --- a/vendor/cultcache-rs/README.md +++ /dev/null @@ -1,332 +0,0 @@ -# CultCache Rust - -`cultcache-rs` is the Rust port of the useful part of GameCult's CultCache: -a polymorphism-aware in-memory cache that remembers through pluggable backing -stores. - -Consumer code should think in domain types: - -```rust -let player = cache.get_required::("player:meta")?; -cache.put("player:meta", &player)?; -``` - -The persistence layer should deal with envelopes, routing, MessagePack payload -bytes, and schema identity. Application code should not paw at loose JSON files -like a sad little bureaucrat with a clipboard. - -## What It Is - -CultCache is a domain cache with persistence adapters. - -- `CultCache` is the query and mutation surface. -- Domain structs implement `DatabaseEntry`. -- Entries are stored behind a `type::key` identity, so multiple entry types - can share a logical key without colliding. -- Each payload is encoded directly from the known `DatabaseEntry` type into - MessagePack bytes. -- Backing stores are adapters, not the public data model. -- Writes persist to the resolved backing store before the in-memory cache is - updated. -- Type-specific backing stores beat generic backing stores. -- `SingleFileMessagePackBackingStore` is the first concrete store and guards - file access with a sidecar lock file. - -This is not an ORM, not a database, and not distributed consensus in a novelty -hat. If multiple processes write the same backing file, use an external lock or -a coordinator. - -## Current API - -```rust -use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore}; - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "settings")] -struct Settings { - #[cultcache(key = 0)] - theme: String, - - #[cultcache(key = 1, default)] - retries: u32, -} - -let mut cache = CultCache::new(); -cache.register_entry_type::()?; -cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new("cache.cc")); -cache.pull_all_backing_stores()?; - -cache.put("app", &Settings { - theme: "ash".to_string(), - retries: 3, -})?; - -let settings = cache.get_required::("app")?; -# Ok::<(), anyhow::Error>(()) -``` - -For a closed set of entries, generate a registry instead of repeating yourself: - -```rust -use cultcache_rs::cultcache_registry; - -cultcache_registry!(GameCultEntries { - Settings, - PlayerData, -}); - -let mut cache = CultCache::new(); -cache.register_registry(GameCultEntries)?; -``` - -The important part is that callers retrieve domain values directly: - -```rust -let settings: Settings = cache.get_required("app")?; -let all_settings: Vec = cache.get_all()?; -``` - -The cache is the ergonomic surface. The backing store is the memory prosthetic. - -## How This Maps From Original CultCache - -The original C# CultCache relies on runtime reflection: - -- cacheable models inherit from `DatabaseEntry` -- the cache scans child classes at startup -- `Get(id)` returns a `T` -- optional `INamedEntry` enables name lookup -- optional field/property indexes are registered by member name -- backing stores pull/push/delete `DatabaseEntry` values - -Rust does not have C#-style assembly scanning or runtime subclass discovery. -The closest honest Rust translation is: - -- domain structs implement a marker trait, `DatabaseEntry` -- the derive macro generates a positional array formatter from explicit integer - field slots -- the envelope carries the polymorphic type discriminator -- unknown persisted type ids fail closed instead of constructing arbitrary - runtime types -- a generated registry should eventually replace repetitive manual - registration - -Manual `register_entry_type::()` is still accepted, but entry identity no -longer needs a hand-written impl: `#[derive(DatabaseEntry)]` declares it beside -the domain type. `cultcache_registry!` provides the current generated resolver -surface for a closed entry set. - -## Intended Rust Ergonomics - -The target user-facing shape should be: - -```rust -#[derive(Clone, DatabaseEntry)] -#[cultcache(type = "player")] -pub struct PlayerData { - #[cultcache(key = 0)] - pub id: String, - - #[cultcache(key = 1)] - pub name: String, - - #[cultcache(key = 2)] - pub faction: String, -} - -let mut cache = CultCache::builder() - .with_generated_entries(game_entries()) - .with_generic_store(SingleFileMessagePackBackingStore::new("cache.cc")) - .build()?; - -cache.pull_all_backing_stores()?; -let player = cache.get_required::("player:ari")?; -``` - -The current `cultcache_registry!` macro is deliberately simple. The remaining -ergonomic endpoint is automatic inventory or build-script generation so users -do not have to maintain the registry list by hand. - -The registry is the Rust equivalent of C# reflection and MessagePack resolver -setup. It should know: - -- entry type id -- generated MessagePack/serde formatter path -- Rust type name / schema name -- optional store domain route -- optional name key extractor -- optional secondary indexes -- optional global singleton marker - -In other words: code generation should restore the original CultCache feeling -without pretending Rust has runtime reflection hiding under the floorboards. - -## Current Surface - -- `CultCache::new` -- `register_entry_type::` -- `add_generic_backing_store` -- `add_backing_store` -- `pull_all_backing_stores` -- `get::` -- `get_required::` -- `get_envelope::` -- `get_required_envelope::` -- `get_all::` -- `put::` -- `put_envelope::` -- `update::` -- `delete::` -- `snapshot` -- `SingleFileMessagePackBackingStore` - -## Backing Store Routing - -Generic store: - -```rust -cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new("cache.cc")); -``` - -Type-specific store: - -```rust -cache.add_backing_store( - SingleFileMessagePackBackingStore::new("players.cc"), - ["player"], -); -``` - -When writing a `PlayerData`, the cache checks type-specific stores first. If none -match, it writes to the first generic store. Later matching stores are mirrors. - -This mirrors the C# behavior: - -- specific domain stores own their domain -- the first generic store is the primary generic write target -- later generic stores mirror writes -- this is not multi-master - -## Persistence Semantics - -`put` persists before mutating the in-memory cache. If persistence fails, the -cache does not pretend the write succeeded. - -The envelope is MessagePack, and the payload inside each envelope is also raw -MessagePack bytes encoded from the registered concrete `DatabaseEntry` type. -That avoids the old bootstrap path where payloads were normalized through -`serde_json::Value`. - -For bit-compatible neighbors, that also enables a real fast lane: - -- `get_envelope::()` exports the canonical persisted bytes for a typed entry -- `put_envelope::()` ingests the same envelope into another cache instance - without re-encoding the payload first - -It still decodes once for validation and typed reads. We are not pretending one -`Vec` became metaphysically zero-copy because we believed in it harder. But -the stupid decode/re-encode loop is gone. - -The `DatabaseEntry` derive does not trust Rust source field order as the durable -schema. Every persisted member must declare a stable integer slot: - -```rust -#[derive(Clone, DatabaseEntry)] -#[cultcache(type = "player")] -pub struct PlayerData { - #[cultcache(key = 0)] - pub id: String, - - #[cultcache(key = 1)] - pub name: String, - - #[cultcache(key = 2, default)] - pub level: u32, -} -``` - -The derive emits a tuple/array formatter. Gaps serialize as nil. Missing fields -marked `default` use `Default::default()` when older payloads are read. Deleted -field slots should stay reserved until an explicit store migration rewrites the -data. - -The single-file MessagePack store rewrites an atomic snapshot. That is a sane -starting point for small typed state surfaces, settings, Epiphany agent memory, -heartbeat state, and other compact control-plane data. Large corpora should use -a sharded store or a real database instead of asking one file to become a -warehouse and then acting wounded when physics invoices us. - -The single-file store uses a sidecar lock file for shared reads and exclusive -writes. That protects ordinary multi-process access to the same file. It is -still not a multi-master replication protocol, and it is not a substitute for a -coordinator when higher-level write ordering matters. - -## Near-Term Ergonomic Improvements - -1. **Derive macro** - - `#[derive(DatabaseEntry)]` - - `#[cultcache(type = "...")]` - - `#[cultcache(key = N)]` on every persisted field - - tuple/array formatter generation landed - - optional `#[cultcache(name)]`, `#[cultcache(index)]`, and - `#[cultcache(global)]` are still future work - -2. **Generated registry** - - a `CultCacheRegistry` trait - - `cache.register_registry(GameCultEntries)` - - basic macro registry landed - - automatic inventory/build-script generation and store routes are future work - -3. **Name and index lookups** - - `cache.get_by_name::("Potion")` - - `cache.get_by_index::("faction", "Lucent")` - - generated extractors instead of reflection strings - -4. **Schema/version metadata** - - schema version in the envelope or payload metadata - - explicit migration hooks - - refusal on unknown persisted entry types unless a migration/resolver is - installed - -5. **Projection helpers** - - JSON projection for review and git diffs - - vector stripping / large-field elision for agent memory surfaces - -## Why Not Skip Registration Immediately? - -Rust can only deserialize polymorphic data into concrete types if something -maps the persisted `type` discriminator to the Rust type. In C#, reflection and -MessagePack resolvers can discover a closed `DatabaseEntry` inheritance tree at -runtime and route it through known formatters. In Rust, that map has to come from -somewhere: - -- explicit registration -- a generated registry -- a macro inventory crate -- a hand-written resolver - -Explicit registration is the simplest honest implementation. Generated registry -is the ergonomic destination. Hand-written resolver tables are the punishment -we give ourselves if we get lazy. - -## Security Model - -CultCache should never parse arbitrary structured data into arbitrary runtime -types. It only accepts persisted envelopes whose `type` discriminator resolves -to a known `DatabaseEntry` type registered in the cache or generated registry. - -That is the Rust version of the original trick: - -- C# refuses to store objects outside the `DatabaseEntry` root type. -- Runtime-visible `DatabaseEntry` subclasses get dedicated formatters. -- Unknown or unregistered types are rejected. -- MessagePack payload bytes deserialize through known concrete formatters, not - an open-world type loader or generic structured-value bridge. - -The derive/registry work should preserve that shape. Its job is to remove -manual registration ceremony, not to loosen the closed-world boundary. - -## License - -Private GameCult infrastructure for now. Public packaging can wait until the -API grows enough teeth to deserve strangers. diff --git a/vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml b/vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml deleted file mode 100644 index 74d52a4..0000000 --- a/vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cultcache-rs-derive" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["derive", "parsing"] } diff --git a/vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs b/vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs deleted file mode 100644 index f05a23f..0000000 --- a/vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs +++ /dev/null @@ -1,243 +0,0 @@ -use proc_macro::TokenStream; -use quote::format_ident; -use quote::quote; -use syn::Data; -use syn::DeriveInput; -use syn::Fields; -use syn::LitInt; -use syn::LitStr; -use syn::Type; -use syn::parse_macro_input; - -#[proc_macro_derive(DatabaseEntry, attributes(cultcache))] -pub fn derive_database_entry(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let ident = input.ident.clone(); - let mut entry_type: Option = None; - let mut schema_name: Option = None; - - for attribute in input - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cultcache")) - { - let parse_result = attribute.parse_nested_meta(|meta| { - if meta.path.is_ident("type") { - entry_type = Some(meta.value()?.parse()?); - Ok(()) - } else if meta.path.is_ident("schema") { - schema_name = Some(meta.value()?.parse()?); - Ok(()) - } else { - Err(meta.error("unsupported cultcache attribute")) - } - }); - if let Err(error) = parse_result { - return error.to_compile_error().into(); - } - } - - let Some(entry_type) = entry_type else { - return syn::Error::new_spanned( - ident, - "DatabaseEntry derive requires #[cultcache(type = \"...\")]", - ) - .to_compile_error() - .into(); - }; - let schema_name = schema_name.unwrap_or_else(|| LitStr::new(&ident.to_string(), ident.span())); - - let fields = match input.data { - Data::Struct(data) => match data.fields { - Fields::Named(fields) => fields.named, - _ => { - return syn::Error::new_spanned( - ident, - "DatabaseEntry derive requires a named struct", - ) - .to_compile_error() - .into(); - } - }, - _ => { - return syn::Error::new_spanned(ident, "DatabaseEntry derive requires a struct") - .to_compile_error() - .into(); - } - }; - - let mut slots: Vec = Vec::new(); - for field in fields { - let Some(field_ident) = field.ident else { - return syn::Error::new_spanned(field.ty, "DatabaseEntry derive requires named fields") - .to_compile_error() - .into(); - }; - let mut key: Option = None; - let mut default = false; - for attribute in field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cultcache")) - { - let parse_result = attribute.parse_nested_meta(|meta| { - if meta.path.is_ident("key") { - let value: LitInt = meta.value()?.parse()?; - key = Some(value.base10_parse()?); - Ok(()) - } else if meta.path.is_ident("default") { - default = true; - Ok(()) - } else { - Err(meta.error("unsupported field cultcache attribute")) - } - }); - if let Err(error) = parse_result { - return error.to_compile_error().into(); - } - } - let Some(key) = key else { - return syn::Error::new_spanned( - field_ident, - "DatabaseEntry fields require #[cultcache(key = N)]", - ) - .to_compile_error() - .into(); - }; - if slots.iter().any(|slot| slot.key == key) { - return syn::Error::new_spanned( - field_ident, - format!("duplicate DatabaseEntry slot {key}"), - ) - .to_compile_error() - .into(); - } - slots.push(CultCacheSlot { - key, - ident: field_ident, - ty: field.ty, - default, - }); - } - slots.sort_by_key(|slot| slot.key); - let max_slot = slots.iter().map(|slot| slot.key).max().unwrap_or(0); - - let serialize_elements = (0..=max_slot).map(|slot| { - if let Some(field) = slots.iter().find(|field| field.key == slot) { - let field_ident = &field.ident; - quote! { - tuple.serialize_element(&self.#field_ident)?; - } - } else { - quote! { - tuple.serialize_element(&())?; - } - } - }); - - let option_declarations = slots.iter().map(|field| { - let variable = format_ident!("slot_{}", field.key); - let ty = &field.ty; - quote! { - let mut #variable: ::std::option::Option<#ty> = ::std::option::Option::None; - } - }); - - let deserialize_match_arms = (0..=max_slot).map(|slot| { - if let Some(field) = slots.iter().find(|field| field.key == slot) { - let variable = format_ident!("slot_{}", field.key); - let ty = &field.ty; - quote! { - #slot => { - #variable = seq.next_element::<#ty>()?; - } - } - } else { - quote! { - #slot => { - let _ = seq.next_element::<::serde::de::IgnoredAny>()?; - } - } - } - }); - - let build_fields = slots.iter().map(|field| { - let field_ident = &field.ident; - let variable = format_ident!("slot_{}", field.key); - if field.default { - quote! { - #field_ident: #variable.unwrap_or_default() - } - } else { - let field_name = field_ident.to_string(); - quote! { - #field_ident: #variable.ok_or_else(|| ::serde::de::Error::missing_field(#field_name))? - } - } - }); - - quote! { - impl ::cultcache_rs::DatabaseEntry for #ident { - const TYPE: &'static str = #entry_type; - const SCHEMA_NAME: &'static str = #schema_name; - } - - impl ::serde::Serialize for #ident { - fn serialize(&self, serializer: S) -> ::std::result::Result - where - S: ::serde::Serializer, - { - use ::serde::ser::SerializeTuple; - let mut tuple = serializer.serialize_tuple(#max_slot + 1)?; - #(#serialize_elements)* - tuple.end() - } - } - - impl<'de> ::serde::Deserialize<'de> for #ident { - fn deserialize(deserializer: D) -> ::std::result::Result - where - D: ::serde::Deserializer<'de>, - { - struct CultCacheVisitor; - - impl<'de> ::serde::de::Visitor<'de> for CultCacheVisitor { - type Value = #ident; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - formatter.write_str(concat!("a CultCache array for ", stringify!(#ident))) - } - - fn visit_seq(self, mut seq: A) -> ::std::result::Result - where - A: ::serde::de::SeqAccess<'de>, - { - #(#option_declarations)* - let mut index = 0usize; - while index <= #max_slot { - match index { - #(#deserialize_match_arms)* - _ => unreachable!(), - } - index += 1; - } - while let ::std::option::Option::Some(_) = seq.next_element::<::serde::de::IgnoredAny>()? {} - Ok(#ident { - #(#build_fields),* - }) - } - } - - deserializer.deserialize_tuple(#max_slot + 1, CultCacheVisitor) - } - } - } - .into() -} - -struct CultCacheSlot { - key: usize, - ident: syn::Ident, - ty: Type, - default: bool, -} diff --git a/vendor/cultcache-rs/examples/cultcache_interop.rs b/vendor/cultcache-rs/examples/cultcache_interop.rs deleted file mode 100644 index 4049acf..0000000 --- a/vendor/cultcache-rs/examples/cultcache_interop.rs +++ /dev/null @@ -1,121 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::CultCache; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use serde_json::json; -use std::collections::BTreeMap; - -const INTEROP_TYPE: &str = "cultcache.interop-note"; -const INTEROP_SCHEMA_VERSION: &str = "cultcache.interop_note.v1"; - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "cultcache.interop-note", schema = "CultCacheInteropNote")] -struct CultCacheInteropNote { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - document_id: String, - #[cultcache(key = 2)] - author_runtime_id: String, - #[cultcache(key = 3)] - title: String, - #[cultcache(key = 4)] - body: String, - #[cultcache(key = 5, default)] - tags: Vec, -} - -fn main() -> Result<()> { - let mut args = std::env::args().skip(1); - let mode = args - .next() - .ok_or_else(|| anyhow!("expected mode: write | read"))?; - let options = parse_args(args.collect()); - let file = require_arg(&options, "file")?; - - match mode.as_str() { - "write" => write_note(file, require_arg(&options, "runtime-id")?)?, - "read" => read_note(file)?, - _ => return Err(anyhow!("unknown mode {mode}")), - } - Ok(()) -} - -fn write_note(file: &str, runtime_id: &str) -> Result<()> { - let mut cache = build_cache(file)?; - cache.pull_all_backing_stores()?; - let note = CultCacheInteropNote { - schema_version: INTEROP_SCHEMA_VERSION.to_string(), - document_id: format!("note:{runtime_id}"), - author_runtime_id: runtime_id.to_string(), - title: format!("{runtime_id} wrote a CultCache note"), - body: "The v1 store format is the contract.".to_string(), - tags: vec![ - runtime_id.to_string(), - "rust".to_string(), - "interop".to_string(), - ], - }; - cache.put(¬e.document_id, ¬e)?; - print_note(¬e) -} - -fn read_note(file: &str) -> Result<()> { - let mut cache = build_cache(file)?; - cache.pull_all_backing_stores()?; - let notes = cache.get_all::()?; - let note = notes - .first() - .ok_or_else(|| anyhow!("no {INTEROP_TYPE} records found"))?; - print_note(note) -} - -fn build_cache(file: &str) -> Result { - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file)); - Ok(cache) -} - -fn print_note(note: &CultCacheInteropNote) -> Result<()> { - println!( - "{}", - serde_json::to_string(&json!({ - "schemaVersion": note.schema_version, - "documentId": note.document_id, - "authorRuntimeId": note.author_runtime_id, - "title": note.title, - "body": note.body, - "tags": note.tags, - }))? - ); - Ok(()) -} - -fn parse_args(args: Vec) -> BTreeMap { - let mut parsed = BTreeMap::new(); - let mut index = 0; - while index < args.len() { - let token = &args[index]; - if !token.starts_with("--") { - index += 1; - continue; - } - let name = token.trim_start_matches("--").to_string(); - let value = args - .get(index + 1) - .cloned() - .unwrap_or_else(|| panic!("missing value for --{name}")); - parsed.insert(name, value); - index += 2; - } - parsed -} - -fn require_arg<'a>(options: &'a BTreeMap, name: &str) -> Result<&'a str> { - options - .get(name) - .map(String::as_str) - .ok_or_else(|| anyhow!("missing required argument --{name}")) -} diff --git a/vendor/cultcache-rs/src/lib.rs b/vendor/cultcache-rs/src/lib.rs deleted file mode 100644 index 0532ca6..0000000 --- a/vendor/cultcache-rs/src/lib.rs +++ /dev/null @@ -1,1080 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::fs; -use std::fs::File; -use std::fs::OpenOptions; -use std::path::Path; -use std::path::PathBuf; - -mod owned_redb; - -extern crate self as cultcache_rs; - -pub use cultcache_rs_derive::DatabaseEntry; -pub use owned_redb::OwnedRedbMessagePackBackingStore; - -pub trait DatabaseEntry: Serialize + DeserializeOwned + Clone + Send + 'static { - const TYPE: &'static str; - const SCHEMA_NAME: &'static str = "DatabaseEntry"; -} - -pub trait CultCacheRegistry { - fn register_entries(&self, cache: &mut CultCache) -> Result<()>; -} - -#[macro_export] -macro_rules! cultcache_registry { - ($name:ident { $($entry:ty),* $(,)? }) => { - #[derive(Clone, Copy, Debug, Default)] - pub struct $name; - - impl $crate::CultCacheRegistry for $name { - fn register_entries(&self, cache: &mut $crate::CultCache) -> ::anyhow::Result<()> { - $( - cache.register_entry_type::<$entry>()?; - )* - Ok(()) - } - } - }; -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultCacheEnvelope { - pub key: String, - #[serde(rename = "type")] - pub r#type: String, - #[serde(with = "serde_bytes")] - pub payload: Vec, - pub stored_at: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema_id: Option, -} - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedStoreSnapshot( - String, - Vec, - Vec, -); - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedSchemaCatalogEntry( - String, - String, - String, - String, - String, - Vec, - Vec, -); - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedSchemaCatalogMember( - u32, - String, - String, - bool, - bool, - Option, - bool, - Option, -); - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedRecord( - String, - String, - String, - #[serde(with = "serde_bytes")] Vec, -); - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct PushAllOptions { - pub soft: bool, -} - -pub trait CacheBackingStore: Send { - fn pull_all(&self) -> Result>; - fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()>; - fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()>; - - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - let existing = self.pull_all()?; - for entry in existing { - self.delete(&entry)?; - } - for entry in entries { - self.push(entry)?; - } - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SingleFileMessagePackBackingStore { - path: PathBuf, -} - -impl SingleFileMessagePackBackingStore { - pub fn new(path: impl Into) -> Self { - Self { path: path.into() } - } - - pub fn path(&self) -> &Path { - &self.path - } - - /// Holds the pre-created sibling lock shared while a read-only consumer - /// evaluates and acts on one snapshot. The reader never creates or writes - /// either file, so an authority owner can grant only read access. Writers - /// cannot replace the snapshot until the action returns. - pub fn with_read_only_shared_snapshot( - &self, - action: impl FnOnce(Vec) -> Result, - ) -> Result { - self.with_read_only_shared_snapshot_and_unlock_diagnostic(action, |_| {}) - } - - pub fn with_read_only_shared_snapshot_and_unlock_diagnostic( - &self, - action: impl FnOnce(Vec) -> Result, - on_unlock_failure: impl FnOnce(anyhow::Error), - ) -> Result { - self.with_read_only_shared_snapshot_using( - action, - |lock| { - fs2::FileExt::unlock(lock) - .with_context(|| format!("failed to unlock {}", self.lock_path().display())) - }, - on_unlock_failure, - ) - } - - fn with_read_only_shared_snapshot_using( - &self, - action: impl FnOnce(Vec) -> Result, - unlock: U, - on_unlock_failure: D, - ) -> Result - where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), - { - let lock_path = self.lock_path(); - let lock = OpenOptions::new() - .read(true) - .open(&lock_path) - .with_context(|| format!("failed to open pre-created {}", lock_path.display()))?; - fs2::FileExt::lock_shared(&lock) - .with_context(|| format!("failed to lock {}", lock_path.display()))?; - let guard = ReadOnlySharedLockGuard::new(lock, unlock, on_unlock_failure); - let result = self.read_all_unlocked().and_then(action); - drop(guard); - result - } - - fn read_all_unlocked(&self) -> Result> { - if !self.path.exists() { - return Ok(Vec::new()); - } - let bytes = fs::read(&self.path) - .with_context(|| format!("failed to read {}", self.path.display()))?; - if bytes.is_empty() { - return Ok(Vec::new()); - } - decode_store_snapshot(&bytes) - .or_else(|_| rmp_serde::from_slice(&bytes)) - .with_context(|| format!("failed to decode MessagePack {}", self.path.display())) - } - - fn write_all_unlocked(&self, entries: &[CultCacheEnvelope]) -> Result<()> { - if let Some(parent) = self.path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - let bytes = rmp_serde::to_vec(&encode_store_snapshot(entries)) - .context("failed to encode MessagePack")?; - let tmp_path = temporary_path_for(&self.path); - fs::write(&tmp_path, bytes) - .with_context(|| format!("failed to write {}", tmp_path.display()))?; - if self.path.exists() { - fs::remove_file(&self.path) - .with_context(|| format!("failed to replace {}", self.path.display()))?; - } - fs::rename(&tmp_path, &self.path).with_context(|| { - format!( - "failed to move {} to {}", - tmp_path.display(), - self.path.display() - ) - })?; - Ok(()) - } - - fn with_shared_lock(&self, action: impl FnOnce() -> Result) -> Result { - let lock = self.open_lock_file()?; - fs2::FileExt::lock_shared(&lock) - .with_context(|| format!("failed to lock {}", self.lock_path().display()))?; - let result = action(); - fs2::FileExt::unlock(&lock) - .with_context(|| format!("failed to unlock {}", self.lock_path().display()))?; - result - } - - fn with_exclusive_lock(&self, action: impl FnOnce() -> Result) -> Result { - let lock = self.open_lock_file()?; - fs2::FileExt::lock_exclusive(&lock) - .with_context(|| format!("failed to lock {}", self.lock_path().display()))?; - let result = action(); - fs2::FileExt::unlock(&lock) - .with_context(|| format!("failed to unlock {}", self.lock_path().display()))?; - result - } - - fn open_lock_file(&self) -> Result { - let lock_path = self.lock_path(); - if let Some(parent) = lock_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(&lock_path) - .with_context(|| format!("failed to open {}", lock_path.display())) - } - - fn lock_path(&self) -> PathBuf { - let mut lock_name = self - .path - .file_name() - .map(|value| value.to_os_string()) - .unwrap_or_else(|| "cultcache.cc".into()); - lock_name.push(".lock"); - self.path.with_file_name(lock_name) - } -} - -struct ReadOnlySharedLockGuard -where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), -{ - lock: Option, - unlock: Option, - on_unlock_failure: Option, -} - -impl ReadOnlySharedLockGuard -where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), -{ - fn new(lock: File, unlock: U, on_unlock_failure: D) -> Self { - Self { - lock: Some(lock), - unlock: Some(unlock), - on_unlock_failure: Some(on_unlock_failure), - } - } -} - -impl Drop for ReadOnlySharedLockGuard -where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), -{ - fn drop(&mut self) { - let (Some(lock), Some(unlock)) = (self.lock.as_ref(), self.unlock.take()) else { - return; - }; - if let Err(error) = unlock(lock) - && let Some(on_unlock_failure) = self.on_unlock_failure.take() - { - on_unlock_failure(error); - } - } -} - -impl CacheBackingStore for SingleFileMessagePackBackingStore { - fn pull_all(&self) -> Result> { - self.with_shared_lock(|| self.read_all_unlocked()) - } - - fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - self.with_exclusive_lock(|| { - let mut entries = self.read_all_unlocked()?; - entries.retain(|candidate| entry_id(candidate) != entry_id(entry)); - entries.push(entry.clone()); - entries.sort_by_key(entry_id); - self.write_all_unlocked(&entries) - }) - } - - fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - self.with_exclusive_lock(|| { - let mut entries = self.read_all_unlocked()?; - entries.retain(|candidate| entry_id(candidate) != entry_id(entry)); - self.write_all_unlocked(&entries) - }) - } - - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - self.with_exclusive_lock(|| { - let mut entries = entries.to_vec(); - entries.sort_by_key(entry_id); - self.write_all_unlocked(&entries) - }) - } -} - -struct CultCacheStoreRegistration { - store: Box, - types: BTreeSet, -} - -pub struct CultCache { - definitions: BTreeMap, - entries: BTreeMap, - stores: Vec, -} - -impl CultCache { - pub fn new() -> Self { - Self { - definitions: BTreeMap::new(), - entries: BTreeMap::new(), - stores: Vec::new(), - } - } - - pub fn register_entry_type(&mut self) -> Result<()> { - if T::TYPE.trim().is_empty() { - return Err(anyhow!( - "CultCache entry types must declare a non-empty type" - )); - } - if let Some(existing_schema) = self.definitions.get(T::TYPE) - && *existing_schema != T::SCHEMA_NAME - { - return Err(anyhow!( - "CultCache already has a different definition registered for type {:?}", - T::TYPE - )); - } - self.definitions.insert(T::TYPE.to_string(), T::SCHEMA_NAME); - Ok(()) - } - - pub fn register_document_type(&mut self) -> Result<()> { - self.register_entry_type::() - } - - pub fn register_registry(&mut self, registry: R) -> Result<&mut Self> { - registry.register_entries(self)?; - Ok(self) - } - - pub fn add_backing_store( - &mut self, - store: impl CacheBackingStore + 'static, - types: impl IntoIterator>, - ) { - self.stores.push(CultCacheStoreRegistration { - store: Box::new(store), - types: types.into_iter().map(Into::into).collect(), - }); - } - - pub fn add_generic_backing_store(&mut self, store: impl CacheBackingStore + 'static) { - self.add_backing_store(store, Vec::::new()); - } - - pub fn pull_all_backing_stores(&mut self) -> Result<()> { - self.entries.clear(); - let known_types: BTreeSet = self.definitions.keys().cloned().collect(); - for registration in &mut self.stores { - for entry in registration.store.pull_all()? { - if !known_types.contains(&entry.r#type) { - return Err(anyhow!( - "No schema is registered for persisted entry type {:?}", - entry.r#type - )); - } - self.entries.insert(entry_id(&entry), entry); - } - } - Ok(()) - } - - pub fn get(&self, key: &str) -> Result> { - self.require_entry_type::()?; - let Some(entry) = self.entries.get(&entry_id_parts(T::TYPE, key)) else { - return Ok(None); - }; - let payload = rmp_serde::from_slice(&entry.payload).with_context(|| { - format!( - "failed to decode CultCache entry {:?} at key {:?} as {}", - T::TYPE, - key, - T::SCHEMA_NAME - ) - })?; - Ok(Some(payload)) - } - - pub fn get_required(&self, key: &str) -> Result { - self.get::(key)? - .ok_or_else(|| anyhow!("CultCache has no {:?} entry at key {:?}", T::TYPE, key)) - } - - pub fn get_envelope(&self, key: &str) -> Result> { - self.require_entry_type::()?; - Ok(self.entries.get(&entry_id_parts(T::TYPE, key)).cloned()) - } - - pub fn get_required_envelope(&self, key: &str) -> Result { - self.get_envelope::(key)? - .ok_or_else(|| anyhow!("CultCache has no {:?} envelope at key {:?}", T::TYPE, key)) - } - - pub fn get_all(&self) -> Result> { - self.require_entry_type::()?; - let mut values = Vec::new(); - for entry in self.entries.values() { - if entry.r#type != T::TYPE { - continue; - } - values.push(rmp_serde::from_slice(&entry.payload).with_context(|| { - format!( - "failed to decode CultCache entry {:?} at key {:?} as {}", - T::TYPE, - entry.key, - T::SCHEMA_NAME - ) - })?); - } - Ok(values) - } - - pub fn put(&mut self, key: impl Into, value: &T) -> Result { - self.require_entry_type::()?; - let key = key.into(); - let payload = rmp_serde::to_vec(value).with_context(|| { - format!( - "failed to encode CultCache entry {:?} at key {:?} as {}", - T::TYPE, - key, - T::SCHEMA_NAME - ) - })?; - let parsed: T = rmp_serde::from_slice(&payload).with_context(|| { - format!( - "failed to validate CultCache entry {:?} at key {:?} as {}", - T::TYPE, - key, - T::SCHEMA_NAME - ) - })?; - let entry = CultCacheEnvelope { - key: key.clone(), - r#type: T::TYPE.to_string(), - payload, - stored_at: now_utc_second(), - schema_id: Some(T::TYPE.to_string()), - }; - let route = self.resolve_route_indices(T::TYPE); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - T::TYPE - )); - }; - self.stores[primary_index].store.push(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.push(&entry)?; - } - self.entries.insert(entry_id(&entry), entry); - Ok(parsed) - } - - pub fn put_envelope(&mut self, entry: CultCacheEnvelope) -> Result { - self.require_entry_type::()?; - if entry.r#type != T::TYPE { - return Err(anyhow!( - "CultCache envelope type {:?} does not match registered Rust type {:?}", - entry.r#type, - T::TYPE - )); - } - if entry.key.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope keys for type {:?} must be non-empty", - T::TYPE - )); - } - if entry.stored_at.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope stored_at for type {:?} must be non-empty", - T::TYPE - )); - } - - let parsed: T = rmp_serde::from_slice(&entry.payload).with_context(|| { - format!( - "failed to validate CultCache envelope {:?} at key {:?} as {}", - T::TYPE, - entry.key, - T::SCHEMA_NAME - ) - })?; - let route = self.resolve_route_indices(T::TYPE); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - T::TYPE - )); - }; - self.stores[primary_index].store.push(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.push(&entry)?; - } - self.entries.insert(entry_id(&entry), entry); - Ok(parsed) - } - - pub fn put_raw_envelope(&mut self, entry: CultCacheEnvelope) -> Result<()> { - if !self.definitions.contains_key(&entry.r#type) { - return Err(anyhow!( - "No entry type registered for CultCache envelope type {:?}", - entry.r#type - )); - } - if entry.key.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope keys for type {:?} must be non-empty", - entry.r#type - )); - } - if entry.stored_at.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope stored_at for type {:?} must be non-empty", - entry.r#type - )); - } - let route = self.resolve_route_indices(&entry.r#type); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - entry.r#type - )); - }; - self.stores[primary_index].store.push(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.push(&entry)?; - } - self.entries.insert(entry_id(&entry), entry); - Ok(()) - } - - pub fn update(&mut self, key: &str, updater: F) -> Result - where - T: DatabaseEntry, - F: FnOnce(Option) -> T, - { - let current = self.get::(key)?; - self.put::(key.to_string(), &updater(current)) - } - - pub fn delete(&mut self, key: &str) -> Result { - self.require_entry_type::()?; - let id = entry_id_parts(T::TYPE, key); - let Some(entry) = self.entries.get(&id).cloned() else { - return Ok(false); - }; - let route = self.resolve_route_indices(T::TYPE); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - T::TYPE - )); - }; - self.stores[primary_index].store.delete(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.delete(&entry)?; - } - self.entries.remove(&id); - Ok(true) - } - - pub fn snapshot(&self) -> Vec { - self.entries.values().cloned().collect() - } - - fn require_entry_type(&self) -> Result<()> { - match self.definitions.get(T::TYPE) { - Some(schema_name) if *schema_name == T::SCHEMA_NAME => Ok(()), - _ => Err(anyhow!( - "CultCache entry type {:?} is not registered on this cache instance", - T::TYPE - )), - } - } - - fn resolve_route_indices(&self, type_id: &str) -> Vec { - let type_specific: Vec = self - .stores - .iter() - .enumerate() - .filter_map(|(index, registration)| { - registration.types.contains(type_id).then_some(index) - }) - .collect(); - if !type_specific.is_empty() { - return type_specific; - } - self.stores - .iter() - .enumerate() - .filter_map(|(index, registration)| registration.types.is_empty().then_some(index)) - .collect() - } -} - -impl Default for CultCache { - fn default() -> Self { - Self::new() - } -} - -fn entry_id(entry: &CultCacheEnvelope) -> String { - entry_id_parts(&entry.r#type, &entry.key) -} - -fn entry_id_parts(r#type: &str, key: &str) -> String { - format!("{type}::{key}", type = r#type) -} - -fn now_utc_second() -> String { - chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) -} - -fn encode_store_snapshot(entries: &[CultCacheEnvelope]) -> PersistedStoreSnapshot { - let mut schema_names = BTreeMap::::new(); - for entry in entries { - schema_names.insert( - entry - .schema_id - .clone() - .unwrap_or_else(|| entry.r#type.clone()), - entry.r#type.clone(), - ); - } - - let catalog = schema_names - .into_iter() - .map(|(schema_id, document_type)| { - PersistedSchemaCatalogEntry( - schema_id.clone(), - document_type, - format!("{schema_id}.v1"), - schema_id.clone(), - format!( - "{{\"schemaName\":\"{}\",\"schemaVersion\":\"{}.v1\",\"members\":[]}}", - escape_json_string(&schema_id), - escape_json_string(&schema_id) - ), - vec![schema_id], - Vec::new(), - ) - }) - .collect(); - let records = entries - .iter() - .map(|entry| { - PersistedRecord( - entry.key.clone(), - entry - .schema_id - .clone() - .unwrap_or_else(|| entry.r#type.clone()), - entry.stored_at.clone(), - entry.payload.clone(), - ) - }) - .collect(); - - PersistedStoreSnapshot("cultcache.store.v1".to_string(), catalog, records) -} - -fn decode_store_snapshot(bytes: &[u8]) -> Result> { - let snapshot: PersistedStoreSnapshot = - rmp_serde::from_slice(bytes).context("failed to decode CultCache v1 snapshot")?; - if snapshot.0 != "cultcache.store.v1" { - return Err(anyhow!("unsupported CultCache snapshot {}", snapshot.0)); - } - - let catalog = snapshot - .1 - .into_iter() - .map(|entry| (entry.0, entry.1)) - .collect::>(); - snapshot - .2 - .into_iter() - .map(|record| { - let r#type = catalog.get(&record.1).cloned().ok_or_else(|| { - anyhow!( - "CultCache record {:?} references missing schema {:?}", - record.0, - record.1 - ) - })?; - Ok(CultCacheEnvelope { - key: record.0, - r#type, - stored_at: record.2, - payload: record.3, - schema_id: Some(record.1), - }) - }) - .collect() -} - -fn escape_json_string(value: &str) -> String { - value.replace('\\', "\\\\").replace('"', "\\\"") -} - -fn temporary_path_for(path: &Path) -> PathBuf { - let mut file_name = path - .file_name() - .map(|value| value.to_os_string()) - .unwrap_or_else(|| "cultcache.cc".into()); - file_name.push(format!(".{}.tmp", uuid::Uuid::new_v4())); - path.with_file_name(file_name) -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - use std::{sync::mpsc, time::Duration}; - - #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] - #[cultcache(type = "settings")] - struct Settings { - #[cultcache(key = 0)] - theme: String, - #[cultcache(key = 1, default)] - retries: u32, - } - - #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] - #[cultcache(type = "note")] - struct Note { - #[cultcache(key = 0)] - title: String, - #[cultcache(key = 1)] - body: String, - } - - cultcache_registry!(TestEntries { Settings, Note }); - - #[test] - fn read_only_snapshot_holds_the_precreated_writer_lock() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("authority.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - let released = CultCacheEnvelope { - key: "traffic".into(), - r#type: "admission".into(), - payload: b"released".to_vec(), - stored_at: "2026-09-02T00:00:00Z".into(), - schema_id: Some("admission.v1".into()), - }; - store.push(&released)?; - - let writer_path = store_path.clone(); - let (started_tx, started_rx) = mpsc::channel(); - let (finished_tx, finished_rx) = mpsc::channel(); - store.with_read_only_shared_snapshot(|captured| { - assert_eq!(captured, vec![released.clone()]); - std::thread::spawn(move || { - started_tx.send(()).unwrap(); - let engaged = CultCacheEnvelope { - payload: b"engaged".to_vec(), - ..released - }; - let mut writer = SingleFileMessagePackBackingStore::new(writer_path); - writer.push(&engaged).unwrap(); - finished_tx.send(()).unwrap(); - }); - started_rx.recv_timeout(Duration::from_secs(1))?; - assert!( - finished_rx - .recv_timeout(Duration::from_millis(100)) - .is_err() - ); - Ok(()) - })?; - finished_rx.recv_timeout(Duration::from_secs(1))?; - store.with_read_only_shared_snapshot(|captured| { - assert_eq!(captured[0].payload, b"engaged"); - Ok(()) - })?; - Ok(()) - } - - #[test] - fn read_only_snapshot_never_creates_a_missing_lock_or_rewrites_the_store() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("authority.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - store.push(&CultCacheEnvelope { - key: "traffic".into(), - r#type: "admission".into(), - payload: b"released".to_vec(), - stored_at: "2026-09-02T00:00:00Z".into(), - schema_id: Some("admission.v1".into()), - })?; - let before = fs::read(&store_path)?; - let lock_path = store.lock_path(); - fs::remove_file(&lock_path)?; - - assert!(store.with_read_only_shared_snapshot(|_| Ok(())).is_err()); - assert!(!lock_path.exists()); - assert_eq!(fs::read(&store_path)?, before); - Ok(()) - } - - #[test] - fn familiar_cultcache_flow_persists_and_reloads_typed_documents() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let settings = Settings { - theme: "ash".to_string(), - retries: 3, - }; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - cache.pull_all_backing_stores()?; - cache.put("app", &settings)?; - assert_eq!(cache.get_required::("app")?, settings); - - let mut reloaded = CultCache::new(); - reloaded.register_entry_type::()?; - reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - reloaded.pull_all_backing_stores()?; - assert_eq!(reloaded.get_required::("app")?, settings); - Ok(()) - } - - #[test] - fn entry_identity_is_polymorphic_by_type_and_key() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut cache = CultCache::new(); - cache.register_registry(TestEntries)?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - - cache.put( - "shared", - &Settings { - theme: "green".to_string(), - retries: 1, - }, - )?; - cache.put( - "shared", - &Note { - title: "same key".to_string(), - body: "different type".to_string(), - }, - )?; - - assert_eq!(cache.snapshot().len(), 2); - assert_eq!(cache.get_required::("shared")?.title, "same key"); - assert_eq!(cache.get_required::("shared")?.theme, "green"); - Ok(()) - } - - #[test] - fn type_specific_store_routes_before_generic_store() -> Result<()> { - let temp = tempfile::tempdir()?; - let generic_path = temp.path().join("generic.cc"); - let settings_path = temp.path().join("settings.cc"); - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&generic_path)); - cache.add_backing_store( - SingleFileMessagePackBackingStore::new(&settings_path), - ["settings"], - ); - - cache.put( - "app", - &Settings { - theme: "ash".to_string(), - retries: 3, - }, - )?; - cache.put( - "memo", - &Note { - title: "hello".to_string(), - body: "world".to_string(), - }, - )?; - - let generic_entries = SingleFileMessagePackBackingStore::new(&generic_path).pull_all()?; - let settings_entries = SingleFileMessagePackBackingStore::new(&settings_path).pull_all()?; - assert_eq!(generic_entries[0].r#type, "note"); - assert_eq!(settings_entries[0].r#type, "settings"); - Ok(()) - } - - #[test] - fn update_and_delete_follow_the_cache_api() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - - let updated = cache.update::("app", |current| { - let mut current = current.unwrap_or(Settings { - theme: "ash".to_string(), - retries: 0, - }); - current.retries += 1; - current - })?; - assert_eq!(updated.retries, 1); - assert!(cache.delete::("app")?); - assert!(cache.get::("app")?.is_none()); - Ok(()) - } - - #[test] - fn pull_rejects_unregistered_persisted_entry_type() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - store.push(&CultCacheEnvelope { - key: "unknown".to_string(), - r#type: "unregistered".to_string(), - payload: rmp_serde::to_vec(&1_u8)?, - stored_at: now_utc_second(), - schema_id: Some("unregistered".to_string()), - })?; - - let mut cache = CultCache::new(); - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - let error = cache.pull_all_backing_stores().unwrap_err(); - assert!( - error - .to_string() - .contains("No schema is registered for persisted entry type") - ); - Ok(()) - } - - #[test] - fn payload_is_binary_messagepack_not_json_value() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - cache.put( - "app", - &Settings { - theme: "ash".to_string(), - retries: 3, - }, - )?; - - let entry = cache.snapshot().remove(0); - let decoded: Settings = rmp_serde::from_slice(&entry.payload)?; - assert_eq!(decoded.theme, "ash"); - assert!(!entry.payload.is_empty()); - Ok(()) - } - - #[test] - fn corrupted_payload_fails_during_typed_retrieval() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - store.push(&CultCacheEnvelope { - key: "app".to_string(), - r#type: "settings".to_string(), - payload: vec![0xc1], - stored_at: now_utc_second(), - schema_id: Some("settings".to_string()), - })?; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - cache.pull_all_backing_stores()?; - let error = cache.get_required::("app").unwrap_err(); - assert!( - error - .to_string() - .contains("failed to decode CultCache entry") - ); - Ok(()) - } - - #[test] - fn put_envelope_reuses_existing_messagepack_payload() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin.cc"); - let target_store = temp.path().join("target.cc"); - - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.put( - "app", - &Settings { - theme: "ash".to_string(), - retries: 3, - }, - )?; - - let envelope = origin.get_required_envelope::("app")?; - - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - let applied = target.put_envelope::(envelope.clone())?; - - assert_eq!( - applied, - Settings { - theme: "ash".to_string(), - retries: 3, - } - ); - assert_eq!(target.get_required::("app")?, applied); - assert_eq!( - target.get_required_envelope::("app")?.payload, - envelope.payload - ); - Ok(()) - } -} diff --git a/vendor/cultcache-rs/src/owned_redb.rs b/vendor/cultcache-rs/src/owned_redb.rs deleted file mode 100644 index 0bca612..0000000 --- a/vendor/cultcache-rs/src/owned_redb.rs +++ /dev/null @@ -1,564 +0,0 @@ -use crate::{CacheBackingStore, CultCacheEnvelope, PushAllOptions}; -use anyhow::{Context, Result, anyhow, bail}; -use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition}; -use std::collections::BTreeSet; -use std::fs::{self, File, OpenOptions}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -const REDB_ENVELOPES: TableDefinition<&[u8], &[u8]> = TableDefinition::new("cultcache_envelopes"); - -struct OwnedRedbInner { - database: Database, - _owner_lock: File, - file_identity: String, -} - -/// A pinned redb store for one long-lived service owner. This handle holds the -/// CultCache external lock, an open file handle, and the redb database for its -/// entire lifetime. Clones share that exact ownership authority. -#[derive(Clone)] -pub struct OwnedRedbMessagePackBackingStore { - path: PathBuf, - inner: Arc, -} - -impl std::fmt::Debug for OwnedRedbMessagePackBackingStore { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("OwnedRedbMessagePackBackingStore") - .field("path", &self.path) - .field("file_identity", &self.inner.file_identity) - .finish_non_exhaustive() - } -} - -impl OwnedRedbMessagePackBackingStore { - pub fn new(path: impl Into) -> Result { - let path = path.into(); - if let Some(parent) = path - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - let lock_path = redb_lock_path(&path); - let owner_lock = OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(&lock_path) - .with_context(|| format!("failed to open {}", lock_path.display()))?; - fs2::FileExt::try_lock_exclusive(&owner_lock).with_context(|| { - format!( - "redb CultCache {} already has an active owner", - path.display() - ) - })?; - - // Give redb ownership of the already pinned file, structurally closing - // path substitution during database creation. The post-open identity - // comparison separately verifies that the pathname still names it. - let database_file = OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(&path) - .with_context(|| format!("failed to pin redb CultCache {}", path.display()))?; - let held_identity = file_identity_from_file(&database_file)?; - let database = Database::builder() - .create_file(database_file) - .with_context(|| format!("failed to open redb CultCache {}", path.display()))?; - let path_identity_file = File::open(&path)?; - let path_identity = file_identity_from_file(&path_identity_file)?; - if held_identity != path_identity { - bail!( - "redb CultCache path identity changed while opening: held {held_identity}, path {path_identity}" - ); - } - let write = database.begin_write()?; - { - write.open_table(REDB_ENVELOPES)?; - } - write.commit()?; - Ok(Self { - path, - inner: Arc::new(OwnedRedbInner { - database, - _owner_lock: owner_lock, - file_identity: held_identity, - }), - }) - } - - pub fn path(&self) -> &Path { - &self.path - } - - pub fn file_identity(&self) -> &str { - &self.inner.file_identity - } - - pub fn require_file_identity(&self, expected: &str) -> Result<()> { - if expected != self.file_identity() { - bail!( - "redb CultCache file identity mismatch: expected {expected}, owned {}", - self.file_identity() - ); - } - Ok(()) - } - - pub fn validate_path_identity(&self) -> Result<()> { - let current_file = File::open(&self.path) - .with_context(|| format!("owned redb path {} is missing", self.path.display()))?; - let current = file_identity_from_file(¤t_file)?; - self.require_file_identity(¤t).with_context(|| { - format!( - "owned redb path {} no longer names the pinned file", - self.path.display() - ) - }) - } - - pub fn compare_and_swap_batch( - &self, - expected: &[CultCacheEnvelope], - replacements: Vec, - ) -> Result { - if replacements.is_empty() { - return Err(anyhow!( - "conditional batch requires a non-empty replacement set" - )); - } - let expected_ids = unique_batch_ids(expected, "expected")?; - let replacement_ids = unique_batch_ids(&replacements, "replacement")?; - if !expected_ids.is_subset(&replacement_ids) { - return Err(anyhow!( - "conditional batch must replace every expected identity" - )); - } - let write = self.inner.database.begin_write()?; - let matched = { - let mut table = write.open_table(REDB_ENVELOPES)?; - let mut valid = true; - for row in expected { - if read_redb_entry(&table, row)? != Some(row.clone()) { - valid = false; - break; - } - } - if valid { - for row in &replacements { - if !expected_ids.contains(&entry_id(row)) - && read_redb_entry(&table, row)?.is_some() - { - valid = false; - break; - } - } - } - if valid { - for row in &replacements { - insert_redb_entry(&mut table, row)?; - } - } - valid - }; - if matched { - write.commit()?; - } else { - write.abort()?; - } - Ok(matched) - } - - pub fn append_if_snapshot_unchanged( - &self, - expected_snapshot: &[CultCacheEnvelope], - additions: Vec, - ) -> Result { - if additions.is_empty() { - return Err(anyhow!("conditional snapshot append requires additions")); - } - unique_batch_ids(expected_snapshot, "expected snapshot")?; - let addition_ids = unique_batch_ids(&additions, "snapshot additions")?; - let write = self.inner.database.begin_write()?; - let matched = { - let mut table = write.open_table(REDB_ENVELOPES)?; - let mut current = read_all_redb(&table)?; - let mut expected = expected_snapshot.to_vec(); - current.sort_by_key(entry_id); - expected.sort_by_key(entry_id); - let valid = current == expected - && !current - .iter() - .any(|row| addition_ids.contains(&entry_id(row))); - if valid { - for row in &additions { - insert_redb_entry(&mut table, row)?; - } - } - valid - }; - if matched { - write.commit()?; - } else { - write.abort()?; - } - Ok(matched) - } - - pub fn replace_and_append_if_snapshot_unchanged( - &self, - expected_snapshot: &[CultCacheEnvelope], - replacements: Vec, - ) -> Result { - if replacements.is_empty() { - return Err(anyhow!( - "conditional snapshot replacement requires replacements" - )); - } - unique_batch_ids(expected_snapshot, "expected snapshot")?; - unique_batch_ids(&replacements, "snapshot replacements")?; - let write = self.inner.database.begin_write()?; - let matched = { - let mut table = write.open_table(REDB_ENVELOPES)?; - let mut current = read_all_redb(&table)?; - let mut expected = expected_snapshot.to_vec(); - current.sort_by_key(entry_id); - expected.sort_by_key(entry_id); - let valid = current == expected; - if valid { - for row in &replacements { - insert_redb_entry(&mut table, row)?; - } - } - valid - }; - if matched { - write.commit()?; - } else { - write.abort()?; - } - Ok(matched) - } -} - -impl CacheBackingStore for OwnedRedbMessagePackBackingStore { - fn pull_all(&self) -> Result> { - let read = self.inner.database.begin_read()?; - let table = read.open_table(REDB_ENVELOPES)?; - read_all_redb(&table) - } - - fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - let write = self.inner.database.begin_write()?; - { - let mut table = write.open_table(REDB_ENVELOPES)?; - insert_redb_entry(&mut table, entry)?; - } - write.commit()?; - Ok(()) - } - - fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - let write = self.inner.database.begin_write()?; - { - let mut table = write.open_table(REDB_ENVELOPES)?; - let key = redb_identity(entry)?; - table.remove(key.as_slice())?; - } - write.commit()?; - Ok(()) - } - - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - unique_batch_ids(entries, "push all")?; - let write = self.inner.database.begin_write()?; - { - let mut table = write.open_table(REDB_ENVELOPES)?; - let keys = table - .iter()? - .map(|row| Ok(row?.0.value().to_vec())) - .collect::>>()?; - for key in keys { - table.remove(key.as_slice())?; - } - for entry in entries { - insert_redb_entry(&mut table, entry)?; - } - } - write.commit()?; - Ok(()) - } -} - -fn unique_batch_ids( - entries: &[CultCacheEnvelope], - label: &str, -) -> Result> { - let ids = entries.iter().map(entry_id).collect::>(); - if ids.len() != entries.len() { - return Err(anyhow!( - "conditional batch {label} set contains duplicate identities" - )); - } - Ok(ids) -} - -fn entry_id(entry: &CultCacheEnvelope) -> (String, String) { - (entry.r#type.clone(), entry.key.clone()) -} - -fn redb_lock_path(path: &Path) -> PathBuf { - let mut lock_name = path - .file_name() - .map(|value| value.to_os_string()) - .unwrap_or_else(|| "cultcache.cc".into()); - lock_name.push(".lock"); - path.with_file_name(lock_name) -} - -#[cfg(unix)] -fn file_identity_from_file(file: &File) -> Result { - use std::os::unix::fs::MetadataExt; - let metadata = file.metadata()?; - Ok(format!( - "unix:{:016x}:{:016x}", - metadata.dev(), - metadata.ino() - )) -} - -#[cfg(windows)] -fn file_identity_from_file(file: &File) -> Result { - use std::mem::zeroed; - use std::os::windows::io::AsRawHandle; - use windows_sys::Win32::Storage::FileSystem::{ - BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle, - }; - let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { zeroed() }; - let success = - unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut information) }; - if success == 0 { - return Err(std::io::Error::last_os_error()) - .context("failed to read Windows file identity"); - } - let index = ((information.nFileIndexHigh as u64) << 32) | information.nFileIndexLow as u64; - Ok(format!( - "windows:{:08x}:{index:016x}", - information.dwVolumeSerialNumber - )) -} - -fn redb_identity(entry: &CultCacheEnvelope) -> Result> { - rmp_serde::to_vec(&entry_id(entry)).context("failed to encode CultCache identity") -} - -fn read_redb_entry( - table: &impl ReadableTable<&'static [u8], &'static [u8]>, - identity: &CultCacheEnvelope, -) -> Result> { - let key = redb_identity(identity)?; - table - .get(key.as_slice())? - .map(|value| { - rmp_serde::from_slice(value.value()).context("failed to decode redb CultCache envelope") - }) - .transpose() -} - -fn insert_redb_entry( - table: &mut redb::Table<&[u8], &[u8]>, - entry: &CultCacheEnvelope, -) -> Result<()> { - let key = redb_identity(entry)?; - let value = rmp_serde::to_vec(entry).context("failed to encode redb CultCache envelope")?; - table.insert(key.as_slice(), value.as_slice())?; - Ok(()) -} - -fn read_all_redb( - table: &impl ReadableTable<&'static [u8], &'static [u8]>, -) -> Result> { - let mut entries = table - .iter()? - .map(|row| { - let (_, value) = row?; - rmp_serde::from_slice(value.value()).context("failed to decode redb CultCache envelope") - }) - .collect::>>()?; - entries.sort_by_key(entry_id); - Ok(entries) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn test_envelope(r#type: &str, key: &str, payload: &[u8]) -> CultCacheEnvelope { - CultCacheEnvelope { - key: key.to_string(), - r#type: r#type.to_string(), - payload: payload.to_vec(), - stored_at: "2026-07-13T00:00:00Z".to_string(), - schema_id: Some(r#type.to_string()), - } - } - - #[test] - fn legacy_redb_identity_remains_tuple_encoded() -> Result<()> { - let entry = test_envelope("model", "current", b"one"); - assert_eq!( - redb_identity(&entry)?, - vec![ - 0x92, 0xa5, b'm', b'o', b'd', b'e', b'l', 0xa7, b'c', b'u', b'r', b'r', b'e', b'n', - b't', - ] - ); - Ok(()) - } - - #[test] - fn owned_redb_clones_share_authority_and_fresh_owner_is_refused() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("owned.cc"); - let owner = OwnedRedbMessagePackBackingStore::new(&path)?; - let clone = owner.clone(); - assert_eq!(owner.file_identity(), clone.file_identity()); - assert!(OwnedRedbMessagePackBackingStore::new(&path).is_err()); - drop(owner); - assert!(OwnedRedbMessagePackBackingStore::new(&path).is_err()); - drop(clone); - assert!(OwnedRedbMessagePackBackingStore::new(&path).is_ok()); - Ok(()) - } - - #[test] - fn owned_redb_file_identity_is_stable_across_writes_and_reopen() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("identity.cc"); - let identity = { - let mut owner = OwnedRedbMessagePackBackingStore::new(&path)?; - let identity = owner.file_identity().to_string(); - owner.push(&test_envelope("model", "current", b"one"))?; - owner.validate_path_identity()?; - owner.require_file_identity(&identity)?; - identity - }; - let reopened = OwnedRedbMessagePackBackingStore::new(&path)?; - assert_eq!(reopened.file_identity(), identity); - assert_eq!(reopened.pull_all()?.len(), 1); - Ok(()) - } - - #[test] - fn owned_redb_identity_helper_refuses_mismatch() -> Result<()> { - let temp = tempfile::tempdir()?; - let owner = OwnedRedbMessagePackBackingStore::new(temp.path().join("mismatch.cc"))?; - assert!(owner.require_file_identity("not-the-owned-file").is_err()); - owner.require_file_identity(owner.file_identity())?; - Ok(()) - } - - #[test] - fn owned_redb_batch_refuses_stale_member_and_commits_success_atomically() -> Result<()> { - let temp = tempfile::tempdir()?; - let mut owner = OwnedRedbMessagePackBackingStore::new(temp.path().join("batch-owned.cc"))?; - let first = test_envelope("model", "first", b"one"); - let second = test_envelope("model", "second", b"one"); - owner.push(&first)?; - owner.push(&second)?; - let first_two = test_envelope("model", "first", b"two"); - let second_two = test_envelope("model", "second", b"two"); - let companion = test_envelope("receipt", "batch", b"committed"); - let stale_second = test_envelope("model", "second", b"stale"); - assert!(!owner.compare_and_swap_batch( - &[first.clone(), stale_second], - vec![first_two.clone(), second_two.clone(), companion.clone()], - )?); - assert_eq!(owner.pull_all()?, vec![first.clone(), second.clone()]); - assert!(owner.compare_and_swap_batch( - &[first, second], - vec![first_two.clone(), second_two.clone(), companion.clone()], - )?); - assert_eq!(owner.pull_all()?, vec![first_two, second_two, companion]); - Ok(()) - } - - #[test] - fn owned_redb_snapshot_append_refuses_stale_or_colliding_state() -> Result<()> { - let temp = tempfile::tempdir()?; - let mut owner = - OwnedRedbMessagePackBackingStore::new(temp.path().join("snapshot-append.cc"))?; - let model = test_envelope("model", "current", b"revision-1"); - owner.push(&model)?; - let stale = owner.pull_all()?; - owner.push(&test_envelope("event", "concurrent", b"live"))?; - assert!(!owner.append_if_snapshot_unchanged( - &stale, - vec![test_envelope("readiness", "proof", b"ready")], - )?); - let exact = owner.pull_all()?; - assert!(owner.append_if_snapshot_unchanged( - &exact, - vec![test_envelope("readiness", "proof", b"ready")], - )?); - let current = owner.pull_all()?; - assert!(!owner.append_if_snapshot_unchanged( - ¤t, - vec![test_envelope("model", "current", b"collision")], - )?); - Ok(()) - } - - #[test] - fn owned_redb_snapshot_replacement_and_append_refuses_concurrent_rows() -> Result<()> { - let temp = tempfile::tempdir()?; - let mut owner = - OwnedRedbMessagePackBackingStore::new(temp.path().join("snapshot-replace.cc"))?; - let current = test_envelope("session", "current", b"active"); - owner.push(¤t)?; - let stale = owner.pull_all()?; - let concurrent = test_envelope("event", "concurrent", b"live"); - owner.push(&concurrent)?; - let completed = test_envelope("session", "current", b"completed"); - let receipt = test_envelope("receipt", "terminal", b"done"); - assert!(!owner.replace_and_append_if_snapshot_unchanged( - &stale, - vec![completed.clone(), receipt.clone()], - )?); - let exact = owner.pull_all()?; - assert!(owner.replace_and_append_if_snapshot_unchanged( - &exact, - vec![completed.clone(), receipt.clone()], - )?); - assert_eq!(owner.pull_all()?, vec![concurrent, receipt, completed]); - Ok(()) - } - - #[test] - fn owned_redb_path_replacement_cannot_redirect_pinned_authority() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("pinned.cc"); - let displaced = temp.path().join("displaced.cc"); - let mut owner = OwnedRedbMessagePackBackingStore::new(&path)?; - let identity = owner.file_identity().to_string(); - owner.push(&test_envelope("model", "before", b"one"))?; - fs::rename(&path, &displaced)?; - File::create(&path)?; - assert!(owner.validate_path_identity().is_err()); - assert_eq!(owner.file_identity(), identity); - owner.push(&test_envelope("model", "after", b"two"))?; - assert_eq!(owner.pull_all()?.len(), 2); - assert_ne!( - file_identity_from_file(&File::open(&path)?)?, - owner.file_identity() - ); - Ok(()) - } -} diff --git a/vendor/cultmesh-rs/Cargo.lock b/vendor/cultmesh-rs/Cargo.lock deleted file mode 100644 index 328295c..0000000 --- a/vendor/cultmesh-rs/Cargo.lock +++ /dev/null @@ -1,1233 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "cultcache-rs" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "cultcache-rs-derive", - "fs2", - "rmp-serde", - "serde", - "serde_bytes", - "serde_json", - "uuid", -] - -[[package]] -name = "cultcache-rs-derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cultmesh-rs" -version = "0.1.0" -dependencies = [ - "anyhow", - "cultcache-rs", - "cultnet-rs", - "pretty_assertions", - "rmp-serde", - "serde", - "tempfile", -] - -[[package]] -name = "cultnet-rs" -version = "0.1.0" -dependencies = [ - "aes-gcm", - "anyhow", - "base64", - "chrono", - "cultcache-rs", - "hmac", - "rand", - "rmp-serde", - "rmpv", - "serde", - "serde_bytes", - "serde_json", - "sha2", - "socket2", - "uuid", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" - -[[package]] -name = "memchr" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rmpv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" -dependencies = [ - "rmp", - "serde", - "serde_bytes", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/cultmesh-rs/Cargo.toml b/vendor/cultmesh-rs/Cargo.toml deleted file mode 100644 index 9a719d3..0000000 --- a/vendor/cultmesh-rs/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "cultmesh-rs" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -name = "cultmesh_rs" -path = "src/lib.rs" - -[dependencies] -anyhow = "1" -cultcache-rs = { path = "../cultcache-rs" } -cultnet-rs = { path = "../cultnet-rs" } -serde = { version = "1", features = ["derive"] } - -[dev-dependencies] -pretty_assertions = "1.4.1" -rmp-serde = "1" -tempfile = "3" diff --git a/vendor/cultmesh-rs/src/lib.rs b/vendor/cultmesh-rs/src/lib.rs deleted file mode 100644 index 5adbb53..0000000 --- a/vendor/cultmesh-rs/src/lib.rs +++ /dev/null @@ -1,1363 +0,0 @@ -use anyhow::{Context, Result}; -use cultcache_rs::CultCache; -use cultcache_rs::CultCacheEnvelope; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultnet_rs::CultNetDocumentPutOptions; -use cultnet_rs::CultNetDocumentRegistry; -use cultnet_rs::CultNetRudpSocketTransportConnection; -use cultnet_rs::CultNetRudpSocketTransportOptions; -use cultnet_rs::CultNetWireContract; -use cultnet_rs::decode_cultnet_message_from_slice; -use cultnet_rs::encode_cultnet_message_to_vec; -use serde::Serialize; -use std::collections::BTreeMap; -use std::env; -use std::net::SocketAddr; -use std::net::UdpSocket; -use std::path::Path; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -pub const CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID: u32 = 0x0d1d_0002; - -pub trait CultMeshDocumentSet: Clone + Send + Sync + 'static { - fn register_cache(&self, cache: &mut CultCache) -> Result<()>; - fn register_documents(&self, registry: &mut CultNetDocumentRegistry) -> Result<()>; -} - -#[macro_export] -macro_rules! cultmesh_documents { - ($name:ident { $($entry:ty => $schema_version:expr),* $(,)? }) => { - #[derive(Clone, Copy, Debug, Default)] - pub struct $name; - - impl $crate::CultMeshDocumentSet for $name { - fn register_cache( - &self, - cache: &mut cultcache_rs::CultCache, - ) -> ::anyhow::Result<()> { - $( - cache.register_entry_type::<$entry>()?; - )* - Ok(()) - } - - fn register_documents( - &self, - registry: &mut cultnet_rs::CultNetDocumentRegistry, - ) -> ::anyhow::Result<()> { - $( - registry.register( - cultnet_rs::CultNetDocumentBinding::for_entry_with_schema_id::<$entry>( - $schema_version.to_string(), - $schema_version.to_string(), - ), - ); - )* - Ok(()) - } - } - }; -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshNodeOptions { - pub runtime_id: String, - pub pull_on_start: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshRudpDocumentPublishOptions { - pub target: SocketAddr, - pub runtime_id: String, - pub connection_id: u32, - pub connect_timeout: Duration, - pub flush_timeout: Duration, - pub poll_interval: Duration, - pub resend_delay_ms: u64, - pub source_agent_id: Option, - pub source_role: Option, - pub tags: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshRudpSnapshotOptions { - pub target: SocketAddr, - pub runtime_id: String, - pub connection_id: u32, - pub connect_timeout: Duration, - pub response_timeout: Duration, - pub poll_interval: Duration, - pub resend_delay_ms: u64, - pub schema_ids: Option>, - pub record_keys: Option>, -} - -impl CultMeshRudpSnapshotOptions { - pub fn odin(target: SocketAddr, runtime_id: impl Into) -> Self { - Self { - target, - runtime_id: runtime_id.into(), - ..Self::default() - } - } -} - -impl Default for CultMeshRudpSnapshotOptions { - fn default() -> Self { - Self { - target: SocketAddr::from(([0, 0, 0, 0], 0)), - runtime_id: "cultmesh-rudp-snapshot-client".to_string(), - connection_id: CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - connect_timeout: Duration::from_secs(3), - response_timeout: Duration::from_secs(3), - poll_interval: Duration::from_millis(10), - resend_delay_ms: 50, - schema_ids: None, - record_keys: None, - } - } -} - -impl CultMeshRudpDocumentPublishOptions { - pub fn odin(target: SocketAddr, runtime_id: impl Into) -> Self { - Self { - target, - runtime_id: runtime_id.into(), - ..Self::default() - } - } -} - -impl Default for CultMeshRudpDocumentPublishOptions { - fn default() -> Self { - Self { - target: SocketAddr::from(([0, 0, 0, 0], 0)), - runtime_id: "cultmesh-rudp-document-publisher".to_string(), - connection_id: CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - connect_timeout: Duration::from_secs(3), - flush_timeout: Duration::from_secs(30), - poll_interval: Duration::from_millis(10), - resend_delay_ms: 50, - source_agent_id: None, - source_role: None, - tags: Vec::new(), - } - } -} - -impl Default for CultMeshNodeOptions { - fn default() -> Self { - Self { - runtime_id: "cultmesh-local".to_string(), - pull_on_start: true, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshRudpEndpoint { - pub host: String, - pub port: u16, - pub addr: SocketAddr, -} - -pub struct CultMeshNode { - runtime_id: String, - cache: CultCache, - documents: CultNetDocumentRegistry, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamKind { - Audio, - Video, - Tensor, - Bytes, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamBodyTransport { - SharedMemory, - SharedD3d12Texture, - SharedD3d11Texture, - DmaBuf, - IoSurface, - AHardwareBuffer, - CultCachePage, - InlineBytes, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamAccess { - Read, - Write, - ReadWrite, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamCopyBudget { - ZeroCopyTarget, - OneCopyFallback, - OpaqueRuntime, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CultMeshStreamClock { - pub clock_domain_id: String, - pub source_id: Option, - pub sample_rate: i32, - pub offset_to_verse_time_ns: i64, - pub confidence: f64, - pub evidence_kind: Option, -} - -impl CultMeshStreamClock { - pub fn new(clock_domain_id: impl Into) -> Result { - let clock_domain_id = require_non_empty(clock_domain_id.into(), "clock_domain_id")?; - Ok(Self { - clock_domain_id, - source_id: None, - sample_rate: 0, - offset_to_verse_time_ns: 0, - confidence: 0.0, - evidence_kind: None, - }) - } - - pub fn source_id(mut self, source_id: impl Into) -> Result { - self.source_id = Some(require_non_empty(source_id.into(), "source_id")?); - Ok(self) - } - - pub fn confidence(mut self, confidence: f64) -> Self { - self.confidence = confidence; - self - } - - pub fn evidence_kind(mut self, evidence_kind: impl Into) -> Result { - self.evidence_kind = Some(require_non_empty(evidence_kind.into(), "evidence_kind")?); - Ok(self) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CultMeshStreamDescriptor { - pub stream_id: String, - pub verse_id: String, - pub owner_peer_id: String, - pub kind: CultMeshStreamKind, - pub clock: CultMeshStreamClock, - pub preferred_transports: Vec, - pub label: Option, - pub required_access: CultMeshStreamAccess, - pub max_in_flight_frames: u32, - pub metadata_schema_id: Option, -} - -impl CultMeshStreamDescriptor { - pub fn new( - stream_id: impl Into, - verse_id: impl Into, - owner_peer_id: impl Into, - kind: CultMeshStreamKind, - clock: CultMeshStreamClock, - preferred_transports: Vec, - ) -> Result { - if preferred_transports.is_empty() { - anyhow::bail!("preferred_transports must not be empty"); - } - Ok(Self { - stream_id: require_non_empty(stream_id.into(), "stream_id")?, - verse_id: require_non_empty(verse_id.into(), "verse_id")?, - owner_peer_id: require_non_empty(owner_peer_id.into(), "owner_peer_id")?, - kind, - clock, - preferred_transports, - label: None, - required_access: CultMeshStreamAccess::Read, - max_in_flight_frames: 0, - metadata_schema_id: None, - }) - } - - pub fn label(mut self, label: impl Into) -> Result { - self.label = Some(require_non_empty(label.into(), "label")?); - Ok(self) - } - - pub fn max_in_flight_frames(mut self, max_in_flight_frames: u32) -> Self { - self.max_in_flight_frames = max_in_flight_frames; - self - } - - pub fn metadata_schema_id(mut self, metadata_schema_id: impl Into) -> Result { - self.metadata_schema_id = Some(require_non_empty( - metadata_schema_id.into(), - "metadata_schema_id", - )?); - Ok(self) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshStreamConsumerProfile { - pub peer_id: String, - pub verse_id: String, - pub supported_transports: Vec, - pub accepted_kinds: Vec, - pub max_in_flight_frames: u32, -} - -impl CultMeshStreamConsumerProfile { - pub fn new( - peer_id: impl Into, - verse_id: impl Into, - supported_transports: Vec, - ) -> Result { - if supported_transports.is_empty() { - anyhow::bail!("supported_transports must not be empty"); - } - Ok(Self { - peer_id: require_non_empty(peer_id.into(), "peer_id")?, - verse_id: require_non_empty(verse_id.into(), "verse_id")?, - supported_transports, - accepted_kinds: Vec::new(), - max_in_flight_frames: 0, - }) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshStreamNegotiation { - pub stream_id: String, - pub producer_peer_id: String, - pub consumer_peer_id: String, - pub transport: CultMeshStreamBodyTransport, - pub access: CultMeshStreamAccess, - pub max_in_flight_frames: u32, - pub copy_budget: CultMeshStreamCopyBudget, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshStreamFrameHandle { - pub stream_id: String, - pub sequence: u64, - pub timestamp_ns: i64, - pub duration_ns: i64, - pub transport: CultMeshStreamBodyTransport, - pub byte_length: usize, - pub resource_key: Option, - pub unavoidable_copy_count: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshSharedMemoryFrameRingStats { - pub stream_id: String, - pub slot_count: usize, - pub slot_byte_length: usize, - pub published_frames: u64, - pub dropped_frames: u64, - pub blocked_writes: u64, - pub latest_sequence: u64, - pub unavoidable_copy_count: u64, -} - -pub struct CultMeshFrameReadLease<'a> { - pub handle: CultMeshStreamFrameHandle, - bytes: &'a [u8], -} - -impl CultMeshFrameReadLease<'_> { - pub fn bytes(&self) -> &[u8] { - self.bytes - } -} - -#[derive(Clone, Debug)] -pub struct CultMeshSharedMemoryFrameRing { - stream_id: String, - slot_byte_length: usize, - slots: Vec)>>, - write_cursor: usize, - latest_slot: Option, - next_sequence: u64, - published_frames: u64, - dropped_frames: u64, - blocked_writes: u64, - unavoidable_copy_count: u64, -} - -impl CultMeshSharedMemoryFrameRing { - pub fn new( - stream_id: impl Into, - slot_count: usize, - slot_byte_length: usize, - ) -> Result { - let stream_id = require_non_empty(stream_id.into(), "stream_id")?; - if slot_count == 0 { - anyhow::bail!("slot_count must be greater than zero"); - } - if slot_byte_length == 0 { - anyhow::bail!("slot_byte_length must be greater than zero"); - } - Ok(Self { - stream_id, - slot_byte_length, - slots: vec![None; slot_count], - write_cursor: 0, - latest_slot: None, - next_sequence: 0, - published_frames: 0, - dropped_frames: 0, - blocked_writes: 0, - unavoidable_copy_count: 0, - }) - } - - pub fn try_publish_copy( - &mut self, - bytes: &[u8], - timestamp_ns: i64, - duration_ns: i64, - ) -> Result> { - if bytes.len() > self.slot_byte_length { - anyhow::bail!("frame byte length exceeds slot size"); - } - if self.slots.is_empty() { - self.blocked_writes += 1; - return Ok(None); - } - - let slot_index = self.write_cursor; - if self.slots[slot_index].is_some() { - self.dropped_frames += 1; - } - let handle = CultMeshStreamFrameHandle { - stream_id: self.stream_id.clone(), - sequence: self.next_sequence, - timestamp_ns, - duration_ns, - transport: CultMeshStreamBodyTransport::SharedMemory, - byte_length: bytes.len(), - resource_key: Some(format!("{}:slot:{slot_index}", self.stream_id)), - unavoidable_copy_count: 1, - }; - self.slots[slot_index] = Some((handle.clone(), bytes.to_vec())); - self.latest_slot = Some(slot_index); - self.write_cursor = (slot_index + 1) % self.slots.len(); - self.next_sequence = self.next_sequence.saturating_add(1); - self.published_frames = self.published_frames.saturating_add(1); - self.unavoidable_copy_count = self.unavoidable_copy_count.saturating_add(1); - Ok(Some(handle)) - } - - pub fn try_acquire_latest_read(&self) -> Option> { - let slot_index = self.latest_slot?; - let (handle, bytes) = self.slots[slot_index].as_ref()?; - Some(CultMeshFrameReadLease { - handle: handle.clone(), - bytes, - }) - } - - pub fn stats(&self) -> CultMeshSharedMemoryFrameRingStats { - let latest_sequence = self - .latest_slot - .and_then(|slot| self.slots[slot].as_ref().map(|(handle, _)| handle.sequence)) - .unwrap_or(0); - CultMeshSharedMemoryFrameRingStats { - stream_id: self.stream_id.clone(), - slot_count: self.slots.len(), - slot_byte_length: self.slot_byte_length, - published_frames: self.published_frames, - dropped_frames: self.dropped_frames, - blocked_writes: self.blocked_writes, - latest_sequence, - unavoidable_copy_count: self.unavoidable_copy_count, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultMeshStreamCatalog { - streams: BTreeMap, - latest_frames: BTreeMap, - rings: BTreeMap, -} - -impl CultMeshStreamCatalog { - pub fn declare(&mut self, stream: CultMeshStreamDescriptor) { - self.streams.insert(stream.stream_id.clone(), stream); - } - - pub fn get(&self, stream_id: &str) -> Option<&CultMeshStreamDescriptor> { - self.streams.get(stream_id) - } - - pub fn streams(&self) -> Vec<&CultMeshStreamDescriptor> { - self.streams.values().collect() - } - - pub fn find( - &self, - verse_id: &str, - kind: Option, - ) -> Vec<&CultMeshStreamDescriptor> { - self.streams - .values() - .filter(|stream| stream.verse_id == verse_id && kind.is_none_or(|k| stream.kind == k)) - .collect() - } - - pub fn negotiate( - &self, - stream_id: &str, - consumer: &CultMeshStreamConsumerProfile, - ) -> Result { - let stream = self - .streams - .get(stream_id) - .ok_or_else(|| anyhow::anyhow!("unknown CultMesh stream '{stream_id}'"))?; - if stream.verse_id != consumer.verse_id { - anyhow::bail!("stream and consumer must belong to the same Verse"); - } - if !consumer.accepted_kinds.is_empty() && !consumer.accepted_kinds.contains(&stream.kind) { - anyhow::bail!("consumer does not accept stream kind"); - } - let transport = stream - .preferred_transports - .iter() - .copied() - .find(|candidate| consumer.supported_transports.contains(candidate)) - .ok_or_else(|| { - anyhow::anyhow!("stream and consumer have no compatible body transport") - })?; - let producer_max = if stream.max_in_flight_frames == 0 { - u32::MAX - } else { - stream.max_in_flight_frames - }; - let consumer_max = if consumer.max_in_flight_frames == 0 { - u32::MAX - } else { - consumer.max_in_flight_frames - }; - Ok(CultMeshStreamNegotiation { - stream_id: stream.stream_id.clone(), - producer_peer_id: stream.owner_peer_id.clone(), - consumer_peer_id: consumer.peer_id.clone(), - transport, - access: stream.required_access, - max_in_flight_frames: producer_max.min(consumer_max), - copy_budget: copy_budget_for(transport), - }) - } - - pub fn create_shared_memory_ring( - &mut self, - stream_id: &str, - slot_count: usize, - slot_byte_length: usize, - ) -> Result<&mut CultMeshSharedMemoryFrameRing> { - if !self.streams.contains_key(stream_id) { - anyhow::bail!("unknown CultMesh stream '{stream_id}'"); - } - self.rings.insert( - stream_id.to_string(), - CultMeshSharedMemoryFrameRing::new(stream_id, slot_count, slot_byte_length)?, - ); - Ok(self - .rings - .get_mut(stream_id) - .expect("ring was inserted for stream")) - } - - pub fn ring(&self, stream_id: &str) -> Option<&CultMeshSharedMemoryFrameRing> { - self.rings.get(stream_id) - } - - pub fn ring_mut(&mut self, stream_id: &str) -> Option<&mut CultMeshSharedMemoryFrameRing> { - self.rings.get_mut(stream_id) - } - - pub fn publish_frame(&mut self, handle: CultMeshStreamFrameHandle) -> Result<()> { - if !self.streams.contains_key(&handle.stream_id) { - anyhow::bail!("unknown CultMesh stream '{}'", handle.stream_id); - } - self.latest_frames.insert(handle.stream_id.clone(), handle); - Ok(()) - } - - pub fn latest_frame(&self, stream_id: &str) -> Option<&CultMeshStreamFrameHandle> { - self.latest_frames.get(stream_id) - } -} - -fn copy_budget_for(transport: CultMeshStreamBodyTransport) -> CultMeshStreamCopyBudget { - match transport { - CultMeshStreamBodyTransport::SharedMemory - | CultMeshStreamBodyTransport::SharedD3d12Texture - | CultMeshStreamBodyTransport::SharedD3d11Texture - | CultMeshStreamBodyTransport::DmaBuf - | CultMeshStreamBodyTransport::IoSurface - | CultMeshStreamBodyTransport::AHardwareBuffer => CultMeshStreamCopyBudget::ZeroCopyTarget, - CultMeshStreamBodyTransport::CultCachePage => CultMeshStreamCopyBudget::OneCopyFallback, - CultMeshStreamBodyTransport::InlineBytes => CultMeshStreamCopyBudget::OpaqueRuntime, - } -} - -fn require_non_empty(value: String, name: &str) -> Result { - if value.trim().is_empty() { - anyhow::bail!("{name} must be non-empty"); - } - Ok(value) -} - -impl CultMeshNode { - pub fn runtime_id(&self) -> &str { - &self.runtime_id - } - - pub fn cache(&self) -> &CultCache { - &self.cache - } - - pub fn documents(&self) -> &CultNetDocumentRegistry { - &self.documents - } - - pub fn get(&self, key: &str) -> Result> { - self.cache.get::(key) - } - - pub fn get_required(&self, key: &str) -> Result { - self.cache.get_required::(key) - } - - pub fn put(&mut self, key: impl Into, value: &T) -> Result { - self.cache.put(key, value) - } - - pub fn pull_all_backing_stores(&mut self) -> Result<()> { - self.cache.pull_all_backing_stores() - } - - pub fn delete(&mut self, key: &str) -> Result { - self.cache.delete::(key) - } - - pub fn flush(&mut self) -> Result<()> { - Ok(()) - } - - pub fn publish_document_to_rudp_catalog( - &self, - key: impl Into, - value: &T, - options: CultMeshRudpDocumentPublishOptions, - ) -> Result<()> - where - T: DatabaseEntry + Serialize, - { - let message = self.create_rudp_document_message(key, value, &options)?; - publish_cultnet_messages_to_rudp_catalog(std::slice::from_ref(&message), options) - } - - pub fn create_rudp_document_message( - &self, - key: impl Into, - value: &T, - options: &CultMeshRudpDocumentPublishOptions, - ) -> Result - where - T: DatabaseEntry + Serialize, - { - let key = key.into(); - self.documents.create_raw_document_put_message( - format!("{}:{}:{}", options.runtime_id, T::TYPE, key), - key, - value, - CultNetDocumentPutOptions { - source_runtime_id: Some(options.runtime_id.clone()), - source_agent_id: options.source_agent_id.clone(), - source_role: options.source_role.clone(), - tags: if options.tags.is_empty() { - None - } else { - Some(options.tags.clone()) - }, - ..CultNetDocumentPutOptions::default() - }, - ) - } - - pub fn pull_rudp_catalog_snapshot( - &mut self, - options: CultMeshRudpSnapshotOptions, - ) -> Result { - let response = request_raw_snapshot_from_rudp_catalog(options)?; - let cultnet_rs::CultNetMessage::SnapshotResponseRaw { documents, .. } = response else { - anyhow::bail!("expected cultnet.snapshot_response_raw.v0"); - }; - let mut applied = 0usize; - for document in documents { - let Some(document_type) = - registered_document_type_for_schema(&self.documents, &document.schema_id) - else { - continue; - }; - self.cache.put_raw_envelope(CultCacheEnvelope { - key: document.record_key, - r#type: document_type, - payload: document.payload, - stored_at: document.stored_at, - schema_id: Some(document.schema_id), - })?; - applied += 1; - } - Ok(applied) - } -} - -fn registered_document_type_for_schema( - documents: &CultNetDocumentRegistry, - schema_id: &str, -) -> Option { - let normalized = strip_schema_version_suffix(schema_id); - if normalized != schema_id { - return Some(normalized.to_string()); - } - documents - .binding_by_schema_id(schema_id) - .map(|binding| binding.document_type.clone()) - .or_else(|| documents.binding(schema_id).map(|_| schema_id.to_string())) -} - -fn strip_schema_version_suffix(value: &str) -> &str { - let Some((prefix, suffix)) = value.rsplit_once(".v") else { - return value; - }; - if suffix.chars().all(|character| character.is_ascii_digit()) { - prefix - } else { - value - } -} - -fn request_raw_snapshot_from_rudp_catalog( - options: CultMeshRudpSnapshotOptions, -) -> Result { - if options.runtime_id.trim().is_empty() { - anyhow::bail!("runtime_id must be non-empty"); - } - let bind_addr = if options.target.is_ipv4() { - SocketAddr::from(([0, 0, 0, 0], 0)) - } else { - SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 0)) - }; - let socket = UdpSocket::bind(bind_addr)?; - socket.set_read_timeout(Some(options.poll_interval))?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: options.runtime_id.clone(), - socket, - mode: cultnet_rs::CultNetRudpSocketMode::Client, - remote_addr: Some(options.target), - connection_id: options.connection_id, - initial_sequence: 1, - resend_delay_ms: options.resend_delay_ms, - transport_id: Some("cultmesh-rudp-snapshot-client".to_string()), - max_payload_bytes: None, - max_fragment_bytes: Some(1200), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - client.connect(Vec::new())?; - let connect_deadline = Instant::now() + options.connect_timeout; - while !client.connected() && Instant::now() < connect_deadline { - let _ = client.receive_once()?; - client.poll_resends()?; - thread::sleep(options.poll_interval); - } - if !client.connected() { - anyhow::bail!( - "timed out connecting CultMesh RUDP snapshot client {} to {}", - options.runtime_id, - options.target - ); - } - - let message_id = format!("{}:snapshot:{}", options.runtime_id, unix_millis()); - let request = cultnet_rs::CultNetMessage::SnapshotRequest { - message_id: message_id.clone(), - schema_ids: options.schema_ids, - record_keys: options.record_keys, - }; - let payload = encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)?; - client.send("schema", payload)?; - let response_deadline = Instant::now() + options.response_timeout; - let mut snapshot_response = None; - while Instant::now() < response_deadline { - if let Some(frame) = client.receive_once()? { - if frame.channel_id == "schema" { - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - match &message { - cultnet_rs::CultNetMessage::SnapshotResponseRaw { - message_id: received, - .. - } if received == &message_id => { - snapshot_response = Some(message); - break; - } - cultnet_rs::CultNetMessage::Error { error } => { - let _ = client.disconnect(b"snapshot-error".to_vec()); - anyhow::bail!("CultMesh RUDP snapshot failed: {error}"); - } - _ => {} - } - } - } - client.poll_resends()?; - thread::sleep(options.poll_interval); - } - if let Some(message) = snapshot_response { - let _ = client.disconnect(b"snapshot-complete".to_vec()); - return Ok(message); - } - let _ = client.disconnect(b"snapshot-timeout".to_vec()); - anyhow::bail!( - "timed out waiting for CultMesh RUDP snapshot from {}", - options.target - ) -} - -fn unix_millis() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() -} - -pub fn publish_cultnet_messages_to_rudp_catalog( - messages: &[cultnet_rs::CultNetMessage], - options: CultMeshRudpDocumentPublishOptions, -) -> Result<()> { - if options.runtime_id.trim().is_empty() { - anyhow::bail!("runtime_id must be non-empty"); - } - let bind_addr = if options.target.is_ipv4() { - SocketAddr::from(([0, 0, 0, 0], 0)) - } else { - SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 0)) - }; - let socket = UdpSocket::bind(bind_addr)?; - socket.set_read_timeout(Some(options.poll_interval))?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: options.runtime_id.clone(), - socket, - mode: cultnet_rs::CultNetRudpSocketMode::Client, - remote_addr: Some(options.target), - connection_id: options.connection_id, - initial_sequence: 1, - resend_delay_ms: options.resend_delay_ms, - transport_id: Some("cultmesh-rudp-document-publisher".to_string()), - max_payload_bytes: None, - max_fragment_bytes: Some(1200), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - client.connect(Vec::new())?; - let connect_deadline = Instant::now() + options.connect_timeout; - while !client.connected() && Instant::now() < connect_deadline { - let _ = client.receive_once()?; - client.poll_resends()?; - thread::sleep(options.poll_interval); - } - if !client.connected() { - anyhow::bail!( - "timed out connecting CultMesh RUDP document publisher {} to {}", - options.runtime_id, - options.target - ); - } - - for message in messages { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - client.send("schema", payload)?; - } - client.flush_reliable(options.flush_timeout) -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct CultMesh; - -impl CultMesh { - pub fn resolve_rudp_endpoint(endpoint: &str) -> Result { - let text = require_non_empty(endpoint.trim().to_string(), "endpoint")?; - if text.starts_with("rudp://") { - return parse_rudp_socket_addr(&text); - } - if text.starts_with("cultmesh://") { - let resolved = default_cultmesh_rudp_endpoint_resolver(&text)?.ok_or_else(|| { - anyhow::anyhow!("CultMesh URI {text} did not resolve to a RUDP endpoint") - })?; - return parse_rudp_socket_addr(&resolved); - } - parse_socket_addr(&text) - } - - pub fn create_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, - ) -> Result - where - D: CultMeshDocumentSet, - { - let mut cache = CultCache::new(); - documents.register_cache(&mut cache)?; - cache - .add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref())); - if options.pull_on_start { - cache.pull_all_backing_stores()?; - } - - let mut registry = CultNetDocumentRegistry::new(); - documents.register_documents(&mut registry)?; - - Ok(CultMeshNode { - runtime_id: options.runtime_id, - cache, - documents: registry, - }) - } - - pub fn start_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, - ) -> Result - where - D: CultMeshDocumentSet, - { - Self::create_node(store_path, documents, options) - } - - pub fn create_stream_catalog() -> CultMeshStreamCatalog { - CultMeshStreamCatalog::default() - } -} - -pub fn create_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, -) -> Result -where - D: CultMeshDocumentSet, -{ - CultMesh::create_node(store_path, documents, options) -} - -pub fn start_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, -) -> Result -where - D: CultMeshDocumentSet, -{ - CultMesh::start_node(store_path, documents, options) -} - -pub fn create_stream_catalog() -> CultMeshStreamCatalog { - CultMesh::create_stream_catalog() -} - -pub fn resolve_rudp_endpoint(endpoint: &str) -> Result { - CultMesh::resolve_rudp_endpoint(endpoint) -} - -fn parse_rudp_socket_addr(value: &str) -> Result { - let text = value.trim().strip_prefix("rudp://").unwrap_or(value.trim()); - parse_socket_addr(text) -} - -fn parse_socket_addr(value: &str) -> Result { - value - .parse() - .with_context(|| format!("RUDP endpoint must be a socket address, got {value:?}")) -} - -fn default_cultmesh_rudp_endpoint_resolver(uri: &str) -> Result> { - let authority = cultmesh_uri_authority(uri)?; - let slug = authority - .chars() - .map(|ch| { - if ch.is_ascii_alphanumeric() { - ch.to_ascii_uppercase() - } else { - '_' - } - }) - .collect::() - .trim_matches('_') - .to_string(); - if slug.is_empty() { - return Ok(None); - } - - for key in [ - format!("CULTMESH_URI_{slug}_RUDP"), - format!("{slug}_CULTMESH_RUDP_ENDPOINT"), - ] { - if let Ok(value) = env::var(&key) { - let text = value.trim(); - if !text.is_empty() { - return Ok(Some(if text.starts_with("rudp://") { - text.to_string() - } else { - format!("rudp://{text}") - })); - } - } - } - Ok(None) -} - -fn cultmesh_uri_authority(uri: &str) -> Result { - let rest = uri - .strip_prefix("cultmesh://") - .ok_or_else(|| anyhow::anyhow!("CultMesh URI must start with cultmesh://"))?; - let authority = rest.split(['/', '?', '#']).next().unwrap_or("").trim(); - if authority.is_empty() { - anyhow::bail!("CultMesh URI must include an authority"); - } - Ok(authority.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[derive(Clone, Debug, PartialEq, Eq, cultcache_rs::DatabaseEntry)] - #[cultcache(type = "cultmesh.test.note", schema = "CultMeshTestNote")] - struct Note { - #[cultcache(key = 0)] - body: String, - } - - cultmesh_documents!(TestDocuments { - Note => "cultmesh.test.note.v0", - }); - - #[test] - fn node_round_trips_registered_documents() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cultmesh.cc"); - let note = Note { - body: "blessed circuit".to_string(), - }; - - let mut node = CultMesh::create_node( - &store_path, - TestDocuments, - CultMeshNodeOptions { - runtime_id: "test-runtime".to_string(), - ..CultMeshNodeOptions::default() - }, - )?; - assert_eq!(node.runtime_id(), "test-runtime"); - node.put("note", ¬e)?; - node.flush()?; - - let reloaded = CultMesh::create_node(&store_path, TestDocuments, Default::default())?; - assert_eq!(reloaded.get_required::("note")?, note); - assert!( - reloaded - .documents() - .binding_by_schema_id("cultmesh.test.note.v0") - .is_some() - ); - Ok(()) - } - - #[test] - fn node_publishes_registered_document_to_rudp_catalog() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cultmesh.cc"); - let note = Note { - body: "respect odin once".to_string(), - }; - let node = CultMesh::create_node( - &store_path, - TestDocuments, - CultMeshNodeOptions { - runtime_id: "muninn-test".to_string(), - ..CultMeshNodeOptions::default() - }, - )?; - - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(Duration::from_millis(10)))?; - let target = socket.local_addr()?; - let (sender, receiver) = std::sync::mpsc::channel(); - let server = thread::spawn(move || -> Result<()> { - let mut server = CultNetRudpSocketTransportConnection::new( - CultNetRudpSocketTransportOptions::server( - "odin-test-catalog", - socket, - CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - ), - )?; - let deadline = Instant::now() + Duration::from_secs(2); - while Instant::now() < deadline { - if let Some(frame) = server.receive_once()? { - let message = cultnet_rs::decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - if let cultnet_rs::CultNetMessage::DocumentPutRaw { document, .. } = message { - sender - .send(( - document.schema_id, - document.record_key, - document.source_runtime_id, - )) - .ok(); - return Ok(()); - } - } - server.poll_resends()?; - } - anyhow::bail!("timed out waiting for RUDP document put") - }); - - node.publish_document_to_rudp_catalog( - "note", - ¬e, - CultMeshRudpDocumentPublishOptions::odin(target, "muninn-test"), - )?; - let received = receiver.recv_timeout(Duration::from_secs(2))?; - server.join().expect("server thread should not panic")?; - - assert_eq!(received.0, "cultmesh.test.note.v0"); - assert_eq!(received.1, "note"); - assert_eq!(received.2.as_deref(), Some("muninn-test")); - Ok(()) - } - - #[test] - fn large_rudp_publication_waits_for_acknowledged_delivery() -> Result<()> { - let temp = tempfile::tempdir()?; - let node = CultMesh::create_node( - temp.path().join("cultmesh.cc"), - TestDocuments, - CultMeshNodeOptions { - runtime_id: "large-publisher-test".into(), - ..CultMeshNodeOptions::default() - }, - )?; - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(Duration::from_millis(10)))?; - let target = socket.local_addr()?; - let options = CultMeshRudpDocumentPublishOptions::odin(target, "large-publisher-test"); - let messages = [ - node.create_rudp_document_message( - "large-a", - &Note { - body: "a".repeat(256 * 1024), - }, - &options, - )?, - node.create_rudp_document_message( - "large-b", - &Note { - body: "b".repeat(256 * 1024), - }, - &options, - )?, - ]; - - let server = thread::spawn(move || -> Result> { - thread::sleep(Duration::from_millis(500)); - let mut server = CultNetRudpSocketTransportConnection::new( - CultNetRudpSocketTransportOptions::server( - "delayed-odin-test", - socket, - CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - ), - )?; - let deadline = Instant::now() + Duration::from_secs(5); - let mut record_keys = Vec::new(); - while Instant::now() < deadline && record_keys.len() < 2 { - if let Some(frame) = server.receive_once()? { - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - if let cultnet_rs::CultNetMessage::DocumentPutRaw { document, .. } = message { - record_keys.push(document.record_key); - } - } - server.poll_resends()?; - } - let linger_deadline = Instant::now() + Duration::from_millis(250); - while Instant::now() < linger_deadline { - let _ = server.receive_once()?; - server.poll_resends()?; - } - if record_keys.len() != 2 { - anyhow::bail!("timed out waiting for both large RUDP documents"); - } - Ok(record_keys) - }); - - publish_cultnet_messages_to_rudp_catalog(&messages, options)?; - assert_eq!( - server.join().expect("server thread should not panic")?, - vec!["large-a", "large-b"] - ); - Ok(()) - } - - #[test] - fn resolves_rudp_endpoint_from_cultmesh_uri_bootstrap_env() -> Result<()> { - let previous = env::var("CULTMESH_URI_ODIN_RUDP").ok(); - unsafe { - env::set_var("CULTMESH_URI_ODIN_RUDP", "127.0.0.1:17871"); - } - let result = CultMesh::resolve_rudp_endpoint("cultmesh://odin/rendezvous/provider-catalog"); - unsafe { - if let Some(value) = previous { - env::set_var("CULTMESH_URI_ODIN_RUDP", value); - } else { - env::remove_var("CULTMESH_URI_ODIN_RUDP"); - } - } - - assert_eq!(result?, "127.0.0.1:17871".parse()?); - Ok(()) - } - - #[test] - fn rejects_unresolved_cultmesh_rudp_endpoint() { - let previous = env::var("CULTMESH_URI_MISSING_ODIN_RUDP").ok(); - unsafe { - env::remove_var("CULTMESH_URI_MISSING_ODIN_RUDP"); - } - let error = - CultMesh::resolve_rudp_endpoint("cultmesh://missing-odin/rendezvous/provider-catalog") - .expect_err("unresolved CultMesh URI should fail"); - unsafe { - if let Some(value) = previous { - env::set_var("CULTMESH_URI_MISSING_ODIN_RUDP", value); - } - } - assert!(error.to_string().contains("did not resolve")); - } - - #[test] - fn raw_snapshot_application_uses_registered_document_type() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cultmesh.cc"); - let mut node = CultMesh::create_node( - &store_path, - TestDocuments, - CultMeshNodeOptions { - runtime_id: "snapshot-apply-test".to_string(), - ..CultMeshNodeOptions::default() - }, - )?; - let payload = rmp_serde::to_vec(&Note { - body: "catalog truth".to_string(), - })?; - - node.cache - .put_raw_envelope(cultcache_rs::CultCacheEnvelope { - key: "note".to_string(), - r#type: node - .documents - .binding_by_schema_id("cultmesh.test.note.v0") - .expect("test binding") - .document_type - .clone(), - payload, - stored_at: "2026-07-03T00:00:00Z".to_string(), - schema_id: Some("cultmesh.test.note.v0".to_string()), - })?; - node.flush()?; - - let reloaded = CultMesh::create_node(&store_path, TestDocuments, Default::default())?; - assert_eq!(reloaded.get_required::("note")?.body, "catalog truth"); - assert_eq!(reloaded.cache().snapshot()[0].r#type, Note::TYPE); - Ok(()) - } - - #[test] - fn stream_catalog_negotiates_and_publishes_latest_shared_memory_frame() -> Result<()> { - let mut catalog = CultMesh::create_stream_catalog(); - let stream = CultMeshStreamDescriptor::new( - "muninn:nightwing:move-evidence", - "mimir-live", - "muninn:nightwing", - CultMeshStreamKind::Bytes, - CultMeshStreamClock::new("muninn:nightwing:clock")? - .source_id("muninn:nightwing:move-evidence")? - .confidence(1.0) - .evidence_kind("muninn-move-evidence")?, - vec![ - CultMeshStreamBodyTransport::SharedMemory, - CultMeshStreamBodyTransport::CultCachePage, - ], - )? - .label("Muninn Move evidence")? - .max_in_flight_frames(4) - .metadata_schema_id("mimir.muninn_move_evidence_stream_frame.v1")?; - catalog.declare(stream); - - let consumer = CultMeshStreamConsumerProfile::new( - "mimir:starfire", - "mimir-live", - vec![CultMeshStreamBodyTransport::SharedMemory], - )?; - let negotiation = catalog.negotiate("muninn:nightwing:move-evidence", &consumer)?; - - assert_eq!( - negotiation.transport, - CultMeshStreamBodyTransport::SharedMemory - ); - assert_eq!( - negotiation.copy_budget, - CultMeshStreamCopyBudget::ZeroCopyTarget - ); - - let handle = { - let ring = - catalog.create_shared_memory_ring("muninn:nightwing:move-evidence", 4, 128)?; - ring.try_publish_copy(&[1, 2, 3], 42, 0)? - .expect("frame should publish") - }; - catalog.publish_frame(handle.clone())?; - - assert_eq!( - catalog.latest_frame("muninn:nightwing:move-evidence"), - Some(&handle) - ); - let lease = catalog - .ring("muninn:nightwing:move-evidence") - .and_then(CultMeshSharedMemoryFrameRing::try_acquire_latest_read) - .expect("latest frame should be readable"); - assert_eq!(lease.handle.sequence, 0); - assert_eq!(lease.bytes(), &[1, 2, 3]); - Ok(()) - } -} diff --git a/vendor/cultnet-rs/.gitignore b/vendor/cultnet-rs/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/vendor/cultnet-rs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/vendor/cultnet-rs/Cargo.toml b/vendor/cultnet-rs/Cargo.toml deleted file mode 100644 index 1334626..0000000 --- a/vendor/cultnet-rs/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "cultnet-rs" -version = "0.1.0" -edition = "2024" -publish = false -license = "MIT" -description = "CultNet-style typed MessagePack messaging, direct-pipe framing, security helpers, and CultCache replication for Rust runtimes." - -[lib] -name = "cultnet_rs" -path = "src/lib.rs" - -[dependencies] -aes-gcm = "0.10" -anyhow = "1" -base64 = "0.22" -chrono = { version = "0.4", features = ["serde"] } -cultcache-rs = { path = "../cultcache-rs" } -ed25519-dalek = "2.2" -hmac = "0.12" -rand = "0.9" -rmp-serde = "1.3" -rmpv = { version = "1.3", features = ["with-serde"] } -serde = { version = "1", features = ["derive"] } -serde_bytes = "0.11" -serde_json = "1" -sha2 = "0.10" -socket2 = "0.5" -uuid = { version = "1", features = ["v4"] } - -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security_Cryptography"] } - -[dev-dependencies] -pretty_assertions = "1.4" -tempfile = "3" diff --git a/vendor/cultnet-rs/README.md b/vendor/cultnet-rs/README.md deleted file mode 100644 index 7ade81d..0000000 --- a/vendor/cultnet-rs/README.md +++ /dev/null @@ -1,279 +0,0 @@ -# CultNet RS - -`cultnet-rs` is the Rust sibling of `cultnet-ts`: typed MessagePack messages, -4-byte length-prefixed direct-pipe framing, CultLib-shaped security helpers, and -CultCache replication without making callers paw through raw envelopes like we -lost a bet. - -The contract is intentionally boring: - -- `cultnet.schema.v0` sends schema-versioned MessagePack objects. -- `gamecult.networking.v0` maps the legacy C# union shape explicitly. -- schema discovery uses explicit catalog request/response messages; no inbound - auto-detect sludge. -- document put/delete/snapshot messages move typed CultCache entries. -- raw document put/snapshot messages preserve canonical MessagePack payload - bytes for bit-compatible neighbors. -- hello messages advertise `supportedMutationContracts`, so callers discover - which document types are read-only, which accept typed intents, which - authority owns the mutation, and which receipt documents prove the outcome. -- payloads are decoded through registered Rust types before entering the cache. - -This crate is not an HTTP wrapper. It is the wire vocabulary Epiphany, -Ghostlight, VoidBot, and the rest of the swarm can share. - -The public API surface is the schema plus its mutation contracts. A runtime does -not expose a pile of bespoke verbs and hope everyone remembers the ritual; it -advertises typed documents, typed intents, and typed receipts. Polite machines -knock on the contract before touching the furniture. - -## Receipts - -```powershell -cargo test -``` - -The initial tests prove: - -- CultLib-compatible AES-GCM string encryption and HMAC session tokens -- 4-byte big-endian MessagePack framing -- schema-versioned message round trips -- legacy `gamecult.networking.v0` login mapping -- schema discovery catalog responses with canonical JSON schema hashes -- shard catalog request/response filtering and response application -- CultCache snapshot replication through registered typed documents -- raw snapshot replication that preserves the original payload bytes -- raw snapshot replication into a local same-schema Rust alias type -- document mutation contract advertisement through hello frames and registries - -## Schema Discovery - -`cultnet-rs` now ships a built-in schema registry for the shared swarm contract -surface: - -- core wire messages -- legacy `gamecult.networking.v0` auth/sample payloads -- schema catalog request/response messages -- shard catalog request/response messages -- the canonical `ghostlight.agent-state` document payload schema - -Use `builtin_schema_registry()` or `CultMesh::create_builtin_schema_registry()` -when you want the standard catalog, or register your own closed-world schema set -with `CultNetSchemaRegistry` / `CultMesh::create_schema_registry()`. Discovery -stays explicit on purpose: peers advertise only the contracts they were compiled -to understand, the same way CultCache only consumes the document types you -registered instead of pretending polymorphism is a public park. - -## Shard Catalogs - -Rust now exposes the same shard topology vocabulary as the other runtimes -without claiming to be the full C# service body: - -```rust -use cultnet_rs::{CultMesh, CultNetMessage, CultNetShardDescriptor}; - -let mut catalog = CultMesh::create_shard_catalog(); -catalog.upsert(CultNetShardDescriptor { - shard_id: "notes-a".to_string(), - owner_runtime_id: "rust-primary".to_string(), - epoch: 3, - is_primary: Some(true), - schema_ids: vec!["note.v0".to_string()], - key_prefix: Some("note:".to_string()), - primary_endpoints: vec!["rudp://127.0.0.1:4100".to_string()], - replica_endpoints: vec![], - read_replica_endpoints: vec![], - region: None, - authority_lease_id: None, -})?; - -let response = catalog.create_catalog_response(&CultNetMessage::ShardCatalogRequest { - message_id: "discover-shards".to_string(), - schema_ids: Some(vec!["note.v0".to_string()]), - record_keys: Some(vec!["note:1".to_string()]), -})?; -``` - -`CultNetShardCatalog::apply_response(...)` imports remote -`cultnet.shard_catalog_response.v0` descriptors into the local catalog. The -catalog owns topology inspection only; shard-log serving, daemon policy, and -game-session authority remain separate responsibilities. - -## Local Fast Lane - -`cultnet-rs` now mirrors the raw replication seam from `cultnet-ts`: - -- `cultnet.document_put_raw.v0` -- `cultnet.snapshot_response_raw.v0` - -Those messages carry the exact persisted MessagePack payload bytes from -CultCache along with the typed envelope metadata. Combined with -`CultCache::put_envelope::()`, that lets a bit-compatible neighbor ingest the -document without re-encoding the payload first. - -The receiving Rust type does not have to be the sender's concrete Rust type. If -the local registry binds a different `DatabaseEntry` to the same schema id, -`sync_raw_document_from_snapshot_response::(...)` hydrates one local -alias type directly: - -```rust -let mut target_registry = CultNetDocumentRegistry::new(); -target_registry.register(CultNetDocumentBinding::for_entry_with_schema_id::( - "aetheria.note.v1".to_string(), - "aetheria.note.v1".to_string(), -)); - -let note = target_registry.sync_raw_document_from_snapshot_response::( - &mut target_cache, - &snapshot_response, - "note:bridge", -)?; -``` - -That still is not zero-copy in the religious sense. Frames allocate, bytes move, -and the receiving cache still decodes once to keep typed reads and validation -honest. The win is narrower and realer: identical payload bytes stop getting -decoded into generic sludge and then encoded right back into the same bytes for -no reason. - -## Reactive Documents - -For local state that should feel owned by the caller, Rust can wrap a registered -CultCache record as a managed reactive document: - -```rust -use std::sync::{Arc, Mutex}; -use cultnet_rs::{CultNetReactiveDocumentOptions, CultNetDocumentRegistry}; - -let cache = Arc::new(Mutex::new(target_cache)); -let note = registry.reactive_document::( - Arc::clone(&cache), - "note:bridge", - CultNetReactiveDocumentOptions::default(), -)?; - -{ - let current = note.current(); - current.lock().unwrap().body = "local prediction".to_string(); -} -``` - -The handle polls the serialized `current` value on the configured debounce -cadence and flushes changed state back through the typed cache. `update(...)`, -`set_current(...)`, `mark_dirty()`, and `flush()` are available when the caller -wants a more explicit Rust shape. - -When a canonical document put arrives while local state is dirty, -`apply_document_put_message(...)` or `apply_raw_document_put_message(...)` -records a `CultNetReactiveDocumentReconciliation`: the canonical value, the -predicted local value, and a delta that can be smoothed by the caller. CultCache -payloads serialize by numeric field slot, so Rust tuple-backed deltas use slot -keys such as `"0"` and `"1"` when field names are not present on the wire. - -## CultMesh Rust Facade - -Rust is still the low-level substrate runtime, not a clone of the C# game -server. It does now expose the small CultMesh entrypoint needed for practical -contact and authority ergonomics: - -```rust -use cultnet_rs::{ - CultMesh, CultMeshAuthorityLease, CultMeshPeerCard, CultMeshRudpClientOptions, - CultMeshRudpSocketOptions, CultNetMessage, CultNetSchemaKind, -}; -use chrono::{Duration, Utc}; - -let schemas = CultMesh::create_builtin_schema_registry()?; -let shared_contracts = schemas.list(&cultnet_rs::CultNetSchemaCatalogOptions { - include_schema_json: false, - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::SharedContract]), -}); - -let mut server = CultMesh::create_rudp_server( - "rust-server", - 0x1020_3040, - CultMeshRudpSocketOptions::default(), -)?; -let port = server.profile.transports[0].port.unwrap(); - -let peer = CultMeshPeerCard::new( - "rust-server", - "local", - [format!("rudp://127.0.0.1:{port}")], -) -.with_roles(["schema"]) -.with_authority_lease_id("lease:rust-server"); - -let now = Utc::now(); -let mut peers = CultMesh::create_peer_catalog(); -let mut leases = CultMesh::create_authority_lease_catalog(); -peers.upsert(peer.clone())?; -leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-server".to_string(), - verse_id: "local".to_string(), - peer_id: "rust-server".to_string(), - roles: vec!["schema".to_string()], - shard_ids: Vec::new(), - issuer_runtime_id: None, - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), -})?; - -let mut client = CultMesh::connect_rudp_client_for_authorized_peer( - "rust-client", - 0x1020_3040, - &peers, - &leases, - "local", - "schema", - None, - now, - CultMeshRudpClientOptions { - socket_options: CultMeshRudpSocketOptions::default(), - connect_payload: b"join".to_vec(), - ..CultMeshRudpClientOptions::default() - }, -)?; -client.send_schema_message(&CultNetMessage::SchemaCatalogRequest { - message_id: "rust-schema-catalog".to_string(), - include_schema_json: Some(false), - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::DocumentPayload]), -})?; -``` - -`create_rudp_client...` remains available when the caller intentionally owns the -handshake and polling loop. `connect_rudp_client...` performs the client -handshake before returning the same schema-message-capable RUDP transport. - -For caller-owned reconnect loops, `CultNetRudpReconnectLoop` keeps transport -construction outside the library while sharing the portable retry controller: - -```rust -use cultnet_rs::{ - CultNetReconnectPolicyOptions, CultNetRudpReconnectLoop, create_reconnect_policy, -}; - -let policy = create_reconnect_policy(CultNetReconnectPolicyOptions::default()); -let mut reconnect = CultNetRudpReconnectLoop::new(policy, b"join".to_vec(), || { - CultMesh::create_rudp_client_for_peer( - "rust-client", - 0x1020_3040, - &peer, - CultMeshRudpSocketOptions::default(), - ) -}); - -reconnect.start()?; -let decision = reconnect.handle_closed(now_ms, 0); -if decision.is_some() { - reconnect.reconnect_if_due(now_ms)?; -} -``` - -`CultMesh::parse_rudp_endpoint(...)` handles advertised `rudp://host:port` -contact hints, and `CultMeshAuthorityLeaseCatalog` keeps trust separate from -discovery. A peer card can say where to dial; `peers.first_authorized(...)` and -`CultMesh::create_rudp_client_for_authorized_peer(...)` only use that contact -hint after a lease says the peer may own the requested role or shard. diff --git a/vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json deleted file mode 100644 index 265bcaf..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-delete.schema.json", - "title": "CultNet Document Delete Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "schemaId", - "recordKey" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.document_delete.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemaId": { "type": "string", "minLength": 1 }, - "recordKey": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json deleted file mode 100644 index e444fef..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-mutation-contract.schema.json", - "title": "CultNet Document Mutation Contract", - "type": "object", - "required": [ - "documentType", - "operations", - "authority" - ], - "additionalProperties": false, - "properties": { - "documentType": { "type": "string", "minLength": 1 }, - "payloadSchemaVersion": { "type": "string", "minLength": 1 }, - "operations": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "enum": ["snapshot", "documentPut", "documentDelete", "intentSubmit", "receiptWatch"] - } - }, - "authority": { - "type": "string", - "enum": ["readOnly", "localUser", "coordinator", "runtime", "denied"] - }, - "intentDocumentTypes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "receiptDocumentTypes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "notes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json deleted file mode 100644 index 3c178d0..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-put-raw.schema.json", - "title": "CultNet Raw Document Put Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "document" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.document_put_raw.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "document": { - "$ref": "./cultnet.raw-document-record.schema.json" - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-put.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-put.schema.json deleted file mode 100644 index a04f5fc..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-put.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-put.schema.json", - "title": "CultNet Document Put Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "document" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.document_put.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "document": { - "$ref": "./cultnet.document-record.schema.json" - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-record.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-record.schema.json deleted file mode 100644 index 94301dc..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-record.schema.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-record.schema.json", - "title": "CultNet Document Record", - "type": "object", - "required": [ - "schemaId", - "recordKey", - "storedAt", - "payload" - ], - "additionalProperties": false, - "properties": { - "schemaId": { "type": "string", "minLength": 1 }, - "recordKey": { "type": "string", "minLength": 1 }, - "storedAt": { "type": "string", "minLength": 1 }, - "payload": {}, - "sourceRuntimeId": { "type": "string", "minLength": 1 }, - "sourceAgentId": { "type": "string", "minLength": 1 }, - "sourceRole": { "type": "string", "minLength": 1 }, - "tags": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.error.schema.json b/vendor/cultnet-rs/contracts/cultnet.error.schema.json deleted file mode 100644 index af6da9c..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.error.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.error.schema.json", - "title": "CultNet Error Message", - "type": "object", - "required": [ - "schemaVersion", - "error" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.error.v0" }, - "error": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.hello.schema.json b/vendor/cultnet-rs/contracts/cultnet.hello.schema.json deleted file mode 100644 index a557dc3..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.hello.schema.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.hello.schema.json", - "title": "CultNet Hello Message", - "type": "object", - "required": [ - "schemaVersion", - "runtimeId", - "runtimeKind" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.hello.v0" }, - "runtimeId": { "type": "string", "minLength": 1 }, - "runtimeKind": { "type": "string", "minLength": 1 }, - "agentId": { "type": "string", "minLength": 1 }, - "role": { "type": "string", "minLength": 1 }, - "displayName": { "type": "string", "minLength": 1 }, - "supportedDocumentTypes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "supportedMutationContracts": { - "type": "array", - "items": { - "$ref": "./cultnet.document-mutation-contract.schema.json" - } - }, - "supportedMessageVersions": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "transportProfiles": { - "type": "array", - "items": { - "$ref": "./cultnet.transport-profile.schema.json" - } - }, - "supportsSchemaCatalog": { "type": "boolean" } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.login-success.schema.json b/vendor/cultnet-rs/contracts/cultnet.login-success.schema.json deleted file mode 100644 index 3d8a459..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.login-success.schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.login-success.schema.json", - "title": "CultNet Login Success Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "session" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.login_success.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "session": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.login.schema.json b/vendor/cultnet-rs/contracts/cultnet.login.schema.json deleted file mode 100644 index 62f4959..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.login.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.login.schema.json", - "title": "CultNet Login Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "auth", - "password" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.login.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "auth": { "type": "string", "minLength": 1 }, - "password": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json b/vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json deleted file mode 100644 index 3348a5f..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.raw-document-record.schema.json", - "title": "CultNet Raw Document Record", - "type": "object", - "required": [ - "schemaId", - "recordKey", - "storedAt", - "payloadEncoding", - "payload" - ], - "additionalProperties": false, - "properties": { - "schemaId": { "type": "string", "minLength": 1 }, - "recordKey": { "type": "string", "minLength": 1 }, - "storedAt": { "type": "string", "minLength": 1 }, - "payloadEncoding": { "const": "messagepack" }, - "payload": { - "type": "string", - "contentEncoding": "base64", - "x-cult-runtime-type": "bytes" - }, - "sourceRuntimeId": { "type": "string", "minLength": 1 }, - "sourceAgentId": { "type": "string", "minLength": 1 }, - "sourceRole": { "type": "string", "minLength": 1 }, - "tags": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.register.schema.json b/vendor/cultnet-rs/contracts/cultnet.register.schema.json deleted file mode 100644 index 77286f3..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.register.schema.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.register.schema.json", - "title": "CultNet Register Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "email", - "password", - "name" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.register.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "email": { "type": "string", "minLength": 1 }, - "password": { "type": "string", "minLength": 1 }, - "name": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json b/vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json deleted file mode 100644 index fc3fe05..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.sample-change-name.schema.json", - "title": "CultNet Sample Change Name Message", - "type": "object", - "required": [ - "schemaVersion", - "name" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.sample.change_name.v0" }, - "name": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json b/vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json deleted file mode 100644 index 8426fb5..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.sample-chat.schema.json", - "title": "CultNet Sample Chat Message", - "type": "object", - "required": [ - "schemaVersion", - "text" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.sample.chat.v0" }, - "text": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json b/vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json deleted file mode 100644 index 611733d..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-catalog-request.schema.json", - "title": "CultNet Schema Catalog Request Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.schema_catalog_request.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "includeSchemaJson": { "type": "boolean" }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "kinds": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "wire_message", - "document_payload", - "shared_contract" - ] - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json b/vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json deleted file mode 100644 index fa371e4..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-catalog-response.schema.json", - "title": "CultNet Schema Catalog Response Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "schemas" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.schema_catalog_response.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemas": { - "type": "array", - "items": { - "$ref": "./cultnet.schema-descriptor.schema.json" - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json b/vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json deleted file mode 100644 index 2379685..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-descriptor.schema.json", - "title": "CultNet Schema Descriptor", - "type": "object", - "required": [ - "schemaId", - "kind", - "wireContracts", - "contentHash" - ], - "additionalProperties": false, - "properties": { - "schemaId": { "type": "string", "minLength": 1 }, - "kind": { - "type": "string", - "enum": [ - "wire_message", - "document_payload", - "shared_contract" - ] - }, - "schemaVersion": { "type": "string", "minLength": 1 }, - "documentType": { "type": "string", "minLength": 1 }, - "title": { "type": "string", "minLength": 1 }, - "wireContracts": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "cultnet.schema.v0", - "gamecult.networking.v0" - ] - } - }, - "contentHash": { "type": "string", "minLength": 1 }, - "schemaJson": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json b/vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json deleted file mode 100644 index c91ee09..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "cultnet.shard_catalog_request.v0", - "type": "object", - "required": ["schemaVersion", "messageId"], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.shard_catalog_request.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "recordKeys": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json b/vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json deleted file mode 100644 index b266178..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "cultnet.shard_catalog_response.v0", - "type": "object", - "required": ["schemaVersion", "messageId", "shards"], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.shard_catalog_response.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "shards": { - "type": "array", - "items": { "$ref": "#/$defs/shardDescriptor" } - } - }, - "$defs": { - "shardDescriptor": { - "type": "object", - "required": ["shardId", "ownerRuntimeId", "epoch"], - "additionalProperties": false, - "properties": { - "shardId": { "type": "string", "minLength": 1 }, - "ownerRuntimeId": { "type": "string", "minLength": 1 }, - "epoch": { "type": "integer", "minimum": 0 }, - "isPrimary": { "type": "boolean" }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "keyPrefix": { "type": "string", "minLength": 1 }, - "primaryEndpoints": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "replicaEndpoints": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "readReplicaEndpoints": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "region": { "type": "string", "minLength": 1 }, - "authorityLeaseId": { "type": "string", "minLength": 1 } - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json b/vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json deleted file mode 100644 index 712d705..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-request.schema.json", - "title": "CultNet Snapshot Request Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.snapshot_request.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "recordKeys": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json b/vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json deleted file mode 100644 index a84bc50..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-response-raw.schema.json", - "title": "CultNet Raw Snapshot Response Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "documents" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.snapshot_response_raw.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "documents": { - "type": "array", - "items": { - "$ref": "./cultnet.raw-document-record.schema.json" - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json b/vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json deleted file mode 100644 index 940822c..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-response.schema.json", - "title": "CultNet Snapshot Response Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "documents" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.snapshot_response.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "documents": { - "type": "array", - "items": { - "$ref": "./cultnet.document-record.schema.json" - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json b/vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json deleted file mode 100644 index 90261c7..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json", - "title": "CultNet Transport Profile", - "type": "object", - "required": [ - "schemaVersion", - "runtimeId", - "transports" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.transport_profile.v0" }, - "runtimeId": { "type": "string", "minLength": 1 }, - "transports": { - "type": "array", - "items": { - "type": "object", - "required": [ - "transportId", - "protocol", - "channels" - ], - "additionalProperties": false, - "properties": { - "transportId": { "type": "string", "minLength": 1 }, - "protocol": { - "type": "string", - "enum": [ - "tcp_framed", - "litenetlib", - "websocket", - "rudp" - ] - }, - "host": { "type": "string", "minLength": 1 }, - "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, - "path": { "type": "string", "minLength": 1 }, - "discoveryGroup": { "type": "string", "minLength": 1 }, - "wireContracts": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "reconnectPolicy": { - "type": "object", - "required": [ - "schemaVersion", - "policyId", - "baseDelayMs", - "maxDelayMs", - "maxJitterMs" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.reconnect_policy.v0" }, - "policyId": { "type": "string", "minLength": 1 }, - "baseDelayMs": { "type": "integer", "minimum": 0 }, - "maxDelayMs": { "type": "integer", "minimum": 0 }, - "maxJitterMs": { "type": "integer", "minimum": 0 }, - "maxAttempts": { "type": "integer", "minimum": 1 } - } - }, - "channels": { - "type": "array", - "items": { - "type": "object", - "required": [ - "channelId", - "delivery", - "ordering" - ], - "additionalProperties": false, - "properties": { - "channelId": { "type": "string", "minLength": 1 }, - "delivery": { - "type": "string", - "enum": [ - "reliable", - "unreliable" - ] - }, - "ordering": { - "type": "string", - "enum": [ - "ordered", - "unordered", - "sequenced" - ] - }, - "maxPayloadBytes": { "type": "integer", "minimum": 1 }, - "maxFragmentBytes": { "type": "integer", "minimum": 1 }, - "maxPendingReliablePackets": { "type": "integer", "minimum": 1 } - } - } - } - } - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.verify.schema.json b/vendor/cultnet-rs/contracts/cultnet.verify.schema.json deleted file mode 100644 index 1f992aa..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.verify.schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.verify.schema.json", - "title": "CultNet Verify Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "session" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.verify.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "session": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json b/vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json deleted file mode 100644 index 6437cd0..0000000 --- a/vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json +++ /dev/null @@ -1,845 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/Ghostlight/schemas/agent-state.schema.json", - "title": "Ghostlight Agent State Schema", - "description": "First canonical contract for Ghostlight agent, perception, relationship, event, scene, and dialogue-context state.", - "type": "object", - "required": [ - "schema_version", - "world", - "agents", - "relationships", - "events", - "scenes" - ], - "additionalProperties": false, - "properties": { - "schema_version": { - "const": "ghostlight.agent_state.v0" - }, - "world": { - "$ref": "#/$defs/world_state" - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/$defs/agent" - } - }, - "relationships": { - "type": "array", - "items": { - "$ref": "#/$defs/relationship" - } - }, - "events": { - "type": "array", - "items": { - "$ref": "#/$defs/event" - } - }, - "scenes": { - "type": "array", - "items": { - "$ref": "#/$defs/scene" - } - } - }, - "$defs": { - "world_state": { - "type": "object", - "required": [ - "world_id", - "setting", - "time", - "canon_context" - ], - "additionalProperties": false, - "properties": { - "world_id": { - "type": "string" - }, - "setting": { - "type": "string" - }, - "time": { - "type": "object", - "required": [ - "label" - ], - "additionalProperties": true, - "properties": { - "label": { - "type": "string" - } - } - }, - "canon_context": { - "type": "array", - "items": { - "type": "string" - } - }, - "ambient_pressures": { - "type": "array", - "items": { - "$ref": "#/$defs/pressure" - } - } - } - }, - "agent": { - "type": "object", - "required": [ - "agent_id", - "identity", - "canonical_state", - "goals", - "memories", - "perceived_state_overlays" - ], - "additionalProperties": false, - "properties": { - "agent_id": { - "type": "string" - }, - "identity": { - "$ref": "#/$defs/identity" - }, - "canonical_state": { - "$ref": "#/$defs/canonical_state" - }, - "goals": { - "type": "array", - "items": { - "$ref": "#/$defs/goal" - } - }, - "memories": { - "$ref": "#/$defs/memory_bundle" - }, - "perceived_state_overlays": { - "type": "array", - "items": { - "$ref": "#/$defs/perceived_state_overlay" - } - } - } - }, - "identity": { - "type": "object", - "required": [ - "name", - "roles", - "origin", - "public_description" - ], - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "roles": { - "type": "array", - "items": { - "type": "string" - } - }, - "origin": { - "type": "string" - }, - "public_description": { - "type": "string" - }, - "private_notes": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "canonical_state": { - "type": "object", - "required": [ - "underlying_organization", - "stable_dispositions", - "behavioral_dimensions", - "presentation_strategy", - "voice_style", - "situational_state", - "values" - ], - "additionalProperties": false, - "properties": { - "underlying_organization": { - "$ref": "#/$defs/variable_map" - }, - "stable_dispositions": { - "$ref": "#/$defs/variable_map" - }, - "behavioral_dimensions": { - "$ref": "#/$defs/variable_map" - }, - "presentation_strategy": { - "$ref": "#/$defs/variable_map" - }, - "situational_state": { - "$ref": "#/$defs/variable_map" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/$defs/value_commitment" - } - }, - "voice_style": { - "$ref": "#/$defs/variable_map" - } - } - }, - "variable_map": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/state_variable" - } - }, - "state_variable": { - "type": "object", - "required": [ - "mean", - "plasticity", - "current_activation" - ], - "additionalProperties": false, - "properties": { - "mean": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "plasticity": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "current_activation": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "evidence": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "value_commitment": { - "type": "object", - "required": [ - "value_id", - "label", - "priority", - "unforgivable_if_betrayed" - ], - "additionalProperties": false, - "properties": { - "value_id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "unforgivable_if_betrayed": { - "type": "boolean" - } - } - }, - "goal": { - "type": "object", - "required": [ - "goal_id", - "description", - "scope", - "priority", - "emotional_stake", - "status" - ], - "additionalProperties": false, - "properties": { - "goal_id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "scope": { - "enum": [ - "immediate", - "scene", - "case", - "arc", - "life" - ] - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "emotional_stake": { - "type": "string" - }, - "blockers": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "enum": [ - "active", - "blocked", - "dormant", - "resolved", - "abandoned" - ] - } - } - }, - "memory_bundle": { - "type": "object", - "required": [ - "episodic", - "semantic", - "relationship_summaries" - ], - "additionalProperties": false, - "properties": { - "episodic": { - "type": "array", - "items": { - "$ref": "#/$defs/memory" - } - }, - "semantic": { - "type": "array", - "items": { - "$ref": "#/$defs/memory" - } - }, - "relationship_summaries": { - "type": "array", - "items": { - "$ref": "#/$defs/memory" - } - } - } - }, - "memory": { - "type": "object", - "required": [ - "memory_id", - "summary", - "salience", - "confidence" - ], - "additionalProperties": false, - "properties": { - "memory_id": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "salience": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "linked_event_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "linked_relationship_id": { - "type": "string" - } - } - }, - "perceived_state_overlay": { - "type": "object", - "required": [ - "observer_agent_id", - "target_agent_id", - "perceived_dimensions", - "beliefs", - "distortions" - ], - "additionalProperties": false, - "properties": { - "observer_agent_id": { - "type": "string" - }, - "target_agent_id": { - "type": "string" - }, - "perceived_dimensions": { - "$ref": "#/$defs/inferred_variable_map" - }, - "beliefs": { - "type": "array", - "items": { - "$ref": "#/$defs/belief" - } - }, - "distortions": { - "type": "array", - "items": { - "enum": [ - "hostile_attribution", - "false_reassurance", - "overread_warmth", - "underread_threat", - "false_mask_detection", - "missed_mask_detection", - "status_injury", - "threat_amplification", - "institutional_suspicion_transfer", - "care_underread", - "mask_overfocus" - ] - } - } - } - }, - "belief": { - "type": "object", - "required": [ - "belief_id", - "claim", - "confidence" - ], - "additionalProperties": false, - "properties": { - "belief_id": { - "type": "string" - }, - "claim": { - "type": "string" - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "subject_id": { - "type": "string" - }, - "claim_type": { - "enum": [ - "fact", - "identity_inference", - "motive_inference", - "prediction", - "norm", - "value_read", - "relationship_read" - ] - }, - "evidence_event_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "visibility": { - "enum": [ - "private", - "shared", - "public", - "rumor" - ] - }, - "emotional_charge": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "relationship": { - "type": "object", - "required": [ - "relationship_id", - "source_id", - "target_id", - "stance", - "summary" - ], - "additionalProperties": false, - "properties": { - "relationship_id": { - "type": "string" - }, - "source_id": { - "type": "string" - }, - "target_id": { - "type": "string" - }, - "stance": { - "$ref": "#/$defs/variable_map" - }, - "summary": { - "type": "string" - }, - "unresolved_incident_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "event": { - "type": "object", - "required": [ - "event_id", - "kind", - "summary", - "participants", - "pressure_tags" - ], - "additionalProperties": false, - "properties": { - "event_id": { - "type": "string" - }, - "kind": { - "enum": [ - "dialogue", - "action", - "memory", - "relationship_shift", - "scene_outcome", - "author_note" - ] - }, - "summary": { - "type": "string" - }, - "participants": { - "type": "array", - "items": { - "type": "string" - } - }, - "pressure_tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "observed_exchange": { - "type": "array", - "items": { - "$ref": "#/$defs/event_exchange_turn" - } - }, - "private_interpretations": { - "type": "array", - "items": { - "$ref": "#/$defs/event_private_interpretation" - } - }, - "event_effects": { - "type": "array", - "items": { - "$ref": "#/$defs/event_effect" - } - } - } - }, - "scene": { - "type": "object", - "required": [ - "scene_id", - "location", - "participants", - "public_stakes", - "hidden_stakes", - "dialogue_context_packs" - ], - "additionalProperties": false, - "properties": { - "scene_id": { - "type": "string" - }, - "location": { - "type": "string" - }, - "participants": { - "type": "array", - "items": { - "type": "string" - } - }, - "public_stakes": { - "type": "array", - "items": { - "type": "string" - } - }, - "hidden_stakes": { - "type": "array", - "items": { - "type": "string" - } - }, - "dialogue_context_packs": { - "type": "array", - "items": { - "$ref": "#/$defs/dialogue_context_pack" - } - } - } - }, - "dialogue_context_pack": { - "type": "object", - "required": [ - "speaker_agent_id", - "listener_ids", - "speaker_local_truth", - "speaker_beliefs", - "active_memories", - "active_goals", - "presentation_constraints" - ], - "additionalProperties": false, - "properties": { - "speaker_agent_id": { - "type": "string" - }, - "listener_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "speaker_local_truth": { - "type": "array", - "items": { - "type": "string" - } - }, - "speaker_beliefs": { - "type": "array", - "items": { - "type": "string" - } - }, - "active_memories": { - "type": "array", - "items": { - "type": "string" - } - }, - "active_goals": { - "type": "array", - "items": { - "type": "string" - } - }, - "presentation_constraints": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "pressure": { - "type": "object", - "required": [ - "pressure_id", - "label", - "intensity" - ], - "additionalProperties": false, - "properties": { - "pressure_id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "intensity": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "inferred_variable_map": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/inferred_state_variable" - } - }, - "inferred_state_variable": { - "type": "object", - "required": [ - "observed_activation", - "attributed_disposition", - "confidence" - ], - "additionalProperties": false, - "properties": { - "observed_activation": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "attributed_disposition": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "evidence": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "event_exchange_turn": { - "type": "object", - "required": [ - "speaker_id", - "utterance_summary", - "dialogue_function" - ], - "additionalProperties": false, - "properties": { - "speaker_id": { - "type": "string" - }, - "utterance_summary": { - "type": "string" - }, - "dialogue_function": { - "type": "string" - }, - "observable_cues": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "event_private_interpretation": { - "type": "object", - "required": [ - "agent_id", - "interpretation", - "confidence" - ], - "additionalProperties": false, - "properties": { - "agent_id": { - "type": "string" - }, - "interpretation": { - "type": "string" - }, - "attributed_motive": { - "type": "string" - }, - "appraisal_tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "event_effect": { - "type": "object", - "required": [ - "agent_id", - "summary" - ], - "additionalProperties": false, - "properties": { - "agent_id": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "state_deltas": { - "type": "array", - "items": { - "$ref": "#/$defs/state_delta" - } - }, - "new_belief_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "memory_update_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "state_delta": { - "type": "object", - "required": [ - "path", - "delta" - ], - "additionalProperties": false, - "properties": { - "path": { - "type": "string" - }, - "delta": { - "type": "number", - "minimum": -1, - "maximum": 1 - } - } - } - } -} diff --git a/vendor/cultnet-rs/examples/cultnet_interop_peer.rs b/vendor/cultnet-rs/examples/cultnet_interop_peer.rs deleted file mode 100644 index ab00c6c..0000000 --- a/vendor/cultnet-rs/examples/cultnet_interop_peer.rs +++ /dev/null @@ -1,1628 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::CultCache; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultnet_rs::CultNetDocumentBinding; -use cultnet_rs::CultNetDocumentMutationContract; -use cultnet_rs::CultNetDocumentOperation; -use cultnet_rs::CultNetDocumentPutOptions; -use cultnet_rs::CultNetDocumentRegistry; -use cultnet_rs::CultNetMessage; -use cultnet_rs::CultNetMutationAuthority; -use cultnet_rs::CultNetRudpPacketType; -use cultnet_rs::CultNetRudpSendOptions; -use cultnet_rs::CultNetRudpSession; -use cultnet_rs::CultNetRudpSessionOptions; -use cultnet_rs::CultNetRudpSocketTransportConnection; -use cultnet_rs::CultNetRudpSocketTransportOptions; -use cultnet_rs::CultNetSchemaKind; -use cultnet_rs::CultNetSchemaRegistration; -use cultnet_rs::CultNetSchemaRegistry; -use cultnet_rs::CultNetTransportProfile; -use cultnet_rs::CultNetWireContract; -use cultnet_rs::RudpTransportProfileOptions; -use cultnet_rs::TcpFramedTransportConnection; -use cultnet_rs::TcpFramedTransportProfileOptions; -use cultnet_rs::builtin_schema_registry; -use cultnet_rs::create_rudp_transport_profile; -use cultnet_rs::create_tcp_framed_transport_profile; -use cultnet_rs::decode_cultnet_message_from_slice; -use cultnet_rs::decode_rudp_packet; -use cultnet_rs::encode_cultnet_message_to_vec; -use cultnet_rs::encode_rudp_packet; -use serde::Deserialize; -use serde::Serialize; -use socket2::Domain; -use socket2::Protocol; -use socket2::Socket; -use socket2::Type; -use std::collections::BTreeMap; -use std::fs; -use std::net::Ipv4Addr; -use std::net::SocketAddr; -use std::net::SocketAddrV4; -use std::net::TcpListener; -use std::net::TcpStream; -use std::net::UdpSocket; -use std::sync::Arc; -use std::sync::Mutex; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -const INTEROP_DOCUMENT_TYPE: &str = "cultnet.interop-note"; -const INTEROP_SCHEMA_VERSION: &str = "cultnet.interop_note.v0"; -const MUTATION_INTENT_TYPE: &str = "cultnet.interop-note-mutation-intent"; -const MUTATION_INTENT_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note-mutation-intent.schema.json"; -const MUTATION_INTENT_SCHEMA_VERSION: &str = "cultnet.interop_note_mutation_intent.v0"; -const MUTATION_RECEIPT_TYPE: &str = "cultnet.interop-note-mutation-receipt"; -const MUTATION_RECEIPT_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note-mutation-receipt.schema.json"; -const MUTATION_RECEIPT_SCHEMA_VERSION: &str = "cultnet.interop_note_mutation_receipt.v0"; -const FIRE_COMMAND_TYPE: &str = "cultnet.interop-fire-weapon-command"; -const FIRE_COMMAND_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-fire-weapon-command.schema.json"; -const FIRE_COMMAND_SCHEMA_VERSION: &str = "cultnet.interop_fire_weapon_command.v0"; -const FIRE_RECEIPT_TYPE: &str = "cultnet.interop-fire-weapon-receipt"; -const FIRE_RECEIPT_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-fire-weapon-receipt.schema.json"; -const FIRE_RECEIPT_SCHEMA_VERSION: &str = "cultnet.interop_fire_weapon_receipt.v0"; -const DISCOVERY_ANNOUNCE_SCHEMA_VERSION: &str = "cultnet.discovery_announce.v0"; -const RUDP_INTEROP_CONNECTION_ID: u32 = 0x43554C54; -const RUDP_INTEROP_MAX_FRAGMENT_BYTES: usize = 1024; -const RUDP_INTEROP_RESEND_DELAY_MS: u64 = 25; -const RUDP_INTEROP_READ_TIMEOUT: Duration = Duration::from_secs(15); - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "cultnet.interop-note", schema = "CultNetInteropNote")] -struct CultNetInteropNote { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - document_id: String, - #[cultcache(key = 2)] - author_runtime_id: String, - #[cultcache(key = 3)] - title: String, - #[cultcache(key = 4)] - body: String, - #[cultcache(key = 5)] - tags: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-note-mutation-intent", - schema = "CultNetInteropMutationIntent" -)] -struct CultNetInteropMutationIntent { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - intent_id: String, - #[cultcache(key = 2)] - target_document_id: String, - #[cultcache(key = 3)] - append_body: String, - #[cultcache(key = 4)] - append_tag: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-note-mutation-receipt", - schema = "CultNetInteropMutationReceipt" -)] -struct CultNetInteropMutationReceipt { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - intent_id: String, - #[cultcache(key = 2)] - accepted: bool, - #[cultcache(key = 3)] - document_id: String, - #[cultcache(key = 4)] - body: String, - #[cultcache(key = 5)] - tags: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-fire-weapon-command", - schema = "CultNetInteropFireCommand" -)] -struct CultNetInteropFireCommand { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - command_id: String, - #[cultcache(key = 2)] - character_id: String, - #[cultcache(key = 3)] - weapon_id: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-fire-weapon-receipt", - schema = "CultNetInteropFireReceipt" -)] -struct CultNetInteropFireReceipt { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - command_id: String, - #[cultcache(key = 2)] - accepted: bool, - #[cultcache(key = 3)] - character_id: String, - #[cultcache(key = 4)] - weapon_id: String, - #[cultcache(key = 5)] - shots_fired: u32, - #[cultcache(key = 6)] - ammo_remaining: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(tag = "schemaVersion", rename_all = "camelCase")] -enum DiscoveryMessage { - #[serde(rename = "cultnet.discovery_probe.v0", rename_all = "camelCase")] - Probe { - message_id: String, - requester_runtime_id: String, - }, - #[serde(rename = "cultnet.discovery_announce.v0", rename_all = "camelCase")] - Announce { - message_id: String, - runtime_id: String, - runtime_kind: String, - display_name: String, - agent_id: Option, - tcp_host: String, - tcp_port: u16, - wire_contract: String, - supported_document_types: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - transport_profiles: Option>, - supports_schema_catalog: bool, - }, -} - -#[derive(Clone, Debug)] -struct PeerConfig { - runtime_id: String, - runtime_kind: String, - display_name: String, - agent_id: String, - bind_host: String, - advertise_host: String, - tcp_port: u16, - rudp_port: u16, - discovery_port: u16, - discovery_group: Ipv4Addr, - schema_path: String, -} - -#[derive(Clone, Debug)] -struct DialConfig { - runtime_id: String, - runtime_kind: String, - display_name: String, - agent_id: String, - target_host: String, - target_port: Option, - target_rudp_port: Option, - schema_path: String, -} - -trait SchemaMessageSender { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()>; -} - -trait SchemaMessageTransport: SchemaMessageSender { - fn read_schema_message(&mut self) -> Result; -} - -fn main() -> Result<()> { - let mut args = std::env::args().skip(1); - let mode = args.next().ok_or_else(|| { - anyhow!("expected mode: serve | probe | dial | rudp-serve-once | rudp-dial-once | rudp-serve-message-once | rudp-dial-message-once") - })?; - let options = parse_args(args.collect()); - - match mode.as_str() { - "serve" => serve(parse_peer_config(&options)?)?, - "probe" => probe(&options)?, - "dial" => dial(parse_dial_config(&options)?)?, - "rudp-serve-once" => rudp_serve_once(&options)?, - "rudp-dial-once" => rudp_dial_once(&options)?, - "rudp-serve-message-once" => rudp_serve_message_once(&options)?, - "rudp-dial-message-once" => rudp_dial_message_once(&options)?, - _ => return Err(anyhow!("unknown mode {mode}")), - } - - Ok(()) -} - -fn serve(config: PeerConfig) -> Result<()> { - let schema_registration = load_schema_registration(&config.schema_path)?; - let mut schema_registry = builtin_schema_registry()?; - schema_registry.register(schema_registration.clone())?; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - register_capability_entry_types(&mut cache)?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(runtime_store_path( - &config.runtime_id, - ))); - cache.pull_all_backing_stores()?; - let note = build_note(&config.runtime_id, &config.display_name); - cache.put(¬e.document_id, ¬e)?; - - let mut document_registry = CultNetDocumentRegistry::new(); - register_capability_bindings(&mut document_registry, &schema_registration.schema_id); - - let cache = Arc::new(Mutex::new(cache)); - let document_registry = Arc::new(document_registry); - let schema_registry = Arc::new(schema_registry); - let config = Arc::new(config); - - start_udp_discovery_server(config.clone())?; - start_tcp_server( - config.clone(), - cache.clone(), - document_registry.clone(), - schema_registry.clone(), - )?; - start_rudp_server(config.clone(), cache, document_registry, schema_registry)?; - - print_json(&serde_json::json!({ - "status": "ready", - "mode": "serve", - "runtimeId": config.runtime_id, - "runtimeKind": config.runtime_kind, - "tcpPort": config.tcp_port, - "rudpPort": config.rudp_port, - "discoveryPort": config.discovery_port, - "discoveryGroup": config.discovery_group.to_string(), - }))?; - - loop { - thread::sleep(Duration::from_secs(3600)); - } -} - -fn probe(options: &BTreeMap) -> Result<()> { - let runtime_id = require_arg(options, "runtime-id")?.to_string(); - let discovery_port = parse_u16_arg(options, "discovery-port")?; - let discovery_group = parse_ipv4_arg(options, "discovery-group")?; - let timeout_ms = parse_u64_arg(options, "timeout-ms").unwrap_or(1_500); - let message_id = format!("{runtime_id}-{}", chrono::Utc::now().timestamp_millis()); - - let socket = create_discovery_socket(0, false)?; - socket.set_read_timeout(Some(Duration::from_millis(timeout_ms)))?; - - let probe_message = DiscoveryMessage::Probe { - message_id: message_id.clone(), - requester_runtime_id: runtime_id.clone(), - }; - let payload = rmp_serde::to_vec_named(&probe_message)?; - socket.send_to( - &payload, - SocketAddr::V4(SocketAddrV4::new(discovery_group, discovery_port)), - )?; - - let mut buffer = vec![0_u8; 4096]; - let mut found = BTreeMap::::new(); - loop { - match socket.recv_from(&mut buffer) { - Ok((len, _)) => { - if let Ok(DiscoveryMessage::Announce { - message_id: response_message_id, - runtime_id, - runtime_kind, - display_name, - agent_id, - tcp_host, - tcp_port, - wire_contract, - supported_document_types, - transport_profiles, - supports_schema_catalog, - }) = rmp_serde::from_slice::(&buffer[..len]) - { - if response_message_id == message_id { - found.insert( - runtime_id.clone(), - serde_json::json!({ - "schemaVersion": DISCOVERY_ANNOUNCE_SCHEMA_VERSION, - "messageId": response_message_id, - "runtimeId": runtime_id, - "runtimeKind": runtime_kind, - "displayName": display_name, - "agentId": agent_id, - "tcpHost": tcp_host, - "tcpPort": tcp_port, - "wireContract": wire_contract, - "supportedDocumentTypes": supported_document_types, - "transportProfiles": transport_profiles, - "supportsSchemaCatalog": supports_schema_catalog, - }), - ); - } - } - } - Err(error) - if error.kind() == std::io::ErrorKind::WouldBlock - || error.kind() == std::io::ErrorKind::TimedOut => - { - break; - } - Err(error) => return Err(error.into()), - } - } - - print_json(&serde_json::json!({ - "mode": "probe", - "runtimeId": runtime_id, - "peers": found.into_values().collect::>(), - }))?; - Ok(()) -} - -fn dial(config: DialConfig) -> Result<()> { - let schema_registration = load_schema_registration(&config.schema_path)?; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - register_capability_entry_types(&mut cache)?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(runtime_store_path( - &format!("{}-dial", config.runtime_id), - ))); - cache.pull_all_backing_stores()?; - - let mut document_registry = CultNetDocumentRegistry::new(); - register_capability_bindings(&mut document_registry, &schema_registration.schema_id); - - let transport_name = if config.target_rudp_port.is_some() { - "rudp" - } else { - "tcp" - }; - let mut transport = open_dial_transport(&config)?; - - transport.send_schema_message(&CultNetMessage::Hello { - runtime_id: config.runtime_id.clone(), - runtime_kind: config.runtime_kind.clone(), - agent_id: Some(config.agent_id.clone()), - role: None, - display_name: Some(config.display_name.clone()), - supported_document_types: Some(vec![INTEROP_DOCUMENT_TYPE.to_string()]), - supported_mutation_contracts: Some(interaction_contracts()), - supported_message_versions: Some(vec![INTEROP_SCHEMA_VERSION.to_string()]), - transport_profiles: Some(dial_transport_profiles(&config)), - supports_schema_catalog: Some(true), - })?; - - let remote_hello = transport - .read_schema_message() - .context("waiting for remote hello")?; - let remote_runtime_id = match &remote_hello { - CultNetMessage::Hello { runtime_id, .. } => runtime_id.clone(), - other => return Err(anyhow!("expected hello response, got {other:?}")), - }; - - transport.send_schema_message(&CultNetMessage::SchemaCatalogRequest { - message_id: format!("{}-catalog", config.runtime_id), - include_schema_json: Some(true), - schema_ids: None, - kinds: None, - })?; - let catalog_response = transport - .read_schema_message() - .context("waiting for schema catalog response")?; - let has_interop_schema = match &catalog_response { - CultNetMessage::SchemaCatalogResponse { schemas, .. } => schemas.iter().any(|schema| { - schema.schema_id == schema_registration.schema_id - && schema.document_type.as_deref() == Some(INTEROP_DOCUMENT_TYPE) - }), - other => return Err(anyhow!("expected catalog response, got {other:?}")), - }; - - transport.send_schema_message(&CultNetMessage::SnapshotRequest { - message_id: format!("{}-snapshot", config.runtime_id), - schema_ids: Some(vec![schema_registration.schema_id.clone()]), - record_keys: None, - })?; - let snapshot_response = transport - .read_schema_message() - .context("waiting for snapshot response")?; - let applied = document_registry - .apply_raw_snapshot_response::(&mut cache, &snapshot_response)?; - let note = applied - .into_iter() - .find(|candidate| candidate.author_runtime_id == remote_runtime_id) - .ok_or_else(|| anyhow!("did not receive an interop note from {remote_runtime_id}"))?; - - print_json(&serde_json::json!({ - "mode": "dial", - "transport": transport_name, - "runtimeId": config.runtime_id, - "targetHost": config.target_host, - "targetPort": config.target_rudp_port.or(config.target_port), - "remoteHello": { - "schemaVersion": "cultnet.hello.v0", - "runtimeId": remote_runtime_id, - }, - "hasInteropSchema": has_interop_schema, - "retrievedNote": { - "schemaVersion": note.schema_version, - "documentId": note.document_id, - "authorRuntimeId": note.author_runtime_id, - "title": note.title, - "body": note.body, - "tags": note.tags, - }, - "mutatedNote": mutate_remote_note(&mut *transport, &mut cache, &document_registry, &schema_registration.schema_id, &config.runtime_id, ¬e)?, - "fireReceipt": fire_remote_weapon(&mut *transport, &mut cache, &document_registry, &config.runtime_id, &remote_runtime_id)?, - }))?; - Ok(()) -} - -fn rudp_serve_once(options: &BTreeMap) -> Result<()> { - let bind_host = options - .get("bind-host") - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()); - let bind_port = options - .get("bind-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --bind-port must be a u16")? - .unwrap_or(0); - let expected_client_payload = options - .get("client-payload") - .map(|value| value.as_bytes().to_vec()) - .unwrap_or_else(|| b"ts-rust-client-state".to_vec()); - let server_payload = options - .get("server-payload") - .map(|value| value.as_bytes().to_vec()) - .unwrap_or_else(|| b"rust-server-state".to_vec()); - let server_extra_payload = options - .get("server-extra-payload") - .map(|value| value.as_bytes().to_vec()); - let disconnect_reason = options - .get("disconnect-reason") - .map(|value| value.as_bytes().to_vec()); - let max_fragment_bytes = options - .get("max-fragment-bytes") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --max-fragment-bytes must be a u32")?; - let socket = UdpSocket::bind((bind_host.as_str(), bind_port)) - .with_context(|| format!("failed to bind RUDP server on {bind_host}:{bind_port}"))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let local_port = socket.local_addr()?.port(); - let mut transport_options = - CultNetRudpSocketTransportOptions::server("rust-rudp-interop", socket, 0x22446688); - transport_options.initial_sequence = 100; - transport_options.resend_delay_ms = 25; - transport_options.max_fragment_bytes = max_fragment_bytes; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - - print_json(&serde_json::json!({ - "status": "ready", - "port": local_port, - }))?; - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" || frame.payload != expected_client_payload { - return Err(anyhow!( - "unexpected RUDP frame: channel={} payload={:?}", - frame.channel_id, - frame.payload - )); - } - transport.send("schema", server_payload.clone())?; - if let Some(extra_payload) = &server_extra_payload { - transport.send("schema", extra_payload.clone())?; - } - if let Some(reason) = &disconnect_reason { - transport.disconnect(reason.clone())?; - } - poll_rudp_after_send(&mut transport, Duration::from_millis(250))?; - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!("timed out waiting for TypeScript RUDP frame")) -} - -fn rudp_dial_once(options: &BTreeMap) -> Result<()> { - let target_host = require_arg(options, "target-host")?; - let target_port = parse_u16_arg(options, "target-port")?; - let remote_addr: SocketAddr = format!("{target_host}:{target_port}") - .parse() - .with_context(|| { - format!("failed to parse RUDP remote endpoint {target_host}:{target_port}") - })?; - let socket = UdpSocket::bind(("127.0.0.1", 0))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let mut transport_options = CultNetRudpSocketTransportOptions::client( - "rust-rudp-client-interop", - socket, - remote_addr, - 0x88664422, - ); - transport_options.resend_delay_ms = 25; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - transport.connect(b"rust-join".to_vec())?; - - let mut sent = false; - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" || frame.payload != b"ts-rust-server-state" { - return Err(anyhow!( - "unexpected RUDP frame: channel={} payload={:?}", - frame.channel_id, - frame.payload - )); - } - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - transport.poll_resends()?; - if transport.connected() && !sent { - transport.send("schema", b"rust-client-state".to_vec())?; - sent = true; - } - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!("timed out waiting for TypeScript RUDP response")) -} - -fn rudp_serve_message_once(options: &BTreeMap) -> Result<()> { - let bind_host = options - .get("bind-host") - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()); - let bind_port = options - .get("bind-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --bind-port must be a u16")? - .unwrap_or(0); - let socket = UdpSocket::bind((bind_host.as_str(), bind_port)).with_context(|| { - format!("failed to bind RUDP message server on {bind_host}:{bind_port}") - })?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let local_port = socket.local_addr()?.port(); - let mut transport_options = - CultNetRudpSocketTransportOptions::server("rust-rudp-message-interop", socket, 0x22446689); - transport_options.initial_sequence = 100; - transport_options.resend_delay_ms = 25; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - - print_json(&serde_json::json!({ - "status": "ready", - "port": local_port, - }))?; - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" { - return Err(anyhow!("unexpected RUDP channel: {}", frame.channel_id)); - } - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - let CultNetMessage::SchemaCatalogRequest { .. } = message else { - return Err(anyhow!("unexpected schema message: {message:?}")); - }; - let response = CultNetMessage::Hello { - runtime_id: "rust-rudp-message-interop".to_string(), - runtime_kind: "rust".to_string(), - agent_id: None, - role: None, - display_name: Some("Rust RUDP Message Interop".to_string()), - supported_document_types: Some(vec![]), - supported_mutation_contracts: Some(vec![]), - supported_message_versions: Some(vec![ - "cultnet.hello.v0".to_string(), - "cultnet.schema_catalog_request.v0".to_string(), - ]), - transport_profiles: Some(vec![create_rudp_transport_profile( - "rust-rudp-message-interop", - RudpTransportProfileOptions { - host: Some("127.0.0.1".to_string()), - port: Some(local_port), - ..RudpTransportProfileOptions::default() - }, - )]), - supports_schema_catalog: Some(true), - }; - transport.send( - "schema", - encode_cultnet_message_to_vec(&response, CultNetWireContract::CultNetSchemaV0)?, - )?; - poll_rudp_after_send(&mut transport, Duration::from_millis(250))?; - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!( - "timed out waiting for TypeScript RUDP schema-v0 message" - )) -} - -fn rudp_dial_message_once(options: &BTreeMap) -> Result<()> { - let target_host = require_arg(options, "target-host")?; - let target_port = parse_u16_arg(options, "target-port")?; - let remote_addr: SocketAddr = format!("{target_host}:{target_port}") - .parse() - .with_context(|| { - format!("failed to parse RUDP remote endpoint {target_host}:{target_port}") - })?; - let socket = UdpSocket::bind(("127.0.0.1", 0))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let mut transport_options = CultNetRudpSocketTransportOptions::client( - "rust-rudp-message-client-interop", - socket, - remote_addr, - 0x88664423, - ); - transport_options.resend_delay_ms = 25; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - transport.connect(b"rust-message-join".to_vec())?; - - let request = CultNetMessage::SchemaCatalogRequest { - message_id: "rust-ts-schema-message".to_string(), - include_schema_json: Some(false), - schema_ids: Some(vec![]), - kinds: Some(vec![CultNetSchemaKind::WireMessage]), - }; - - let mut sent = false; - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" { - return Err(anyhow!("unexpected RUDP channel: {}", frame.channel_id)); - } - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - match message { - CultNetMessage::Hello { runtime_id, .. } - if runtime_id == "ts-rust-rudp-message-server" => - { - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - other => return Err(anyhow!("unexpected schema message: {other:?}")), - } - } - transport.poll_resends()?; - if transport.connected() && !sent { - transport.send( - "schema", - encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)?, - )?; - sent = true; - } - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!( - "timed out waiting for TypeScript RUDP schema-v0 response" - )) -} - -fn poll_rudp_after_send( - transport: &mut CultNetRudpSocketTransportConnection, - duration: Duration, -) -> Result<()> { - let deadline = Instant::now() + duration; - while Instant::now() < deadline { - let _ = transport.receive_once()?; - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - Ok(()) -} - -fn mutate_remote_note( - transport: &mut dyn SchemaMessageTransport, - cache: &mut CultCache, - document_registry: &CultNetDocumentRegistry, - note_schema_id: &str, - runtime_id: &str, - note: &CultNetInteropNote, -) -> Result { - let intent = CultNetInteropMutationIntent { - schema_version: MUTATION_INTENT_SCHEMA_VERSION.to_string(), - intent_id: format!("{runtime_id}-decorate"), - target_document_id: note.document_id.clone(), - append_body: format!(" Decorated by {runtime_id}."), - append_tag: format!("decorated:{runtime_id}"), - }; - let message = document_registry.create_raw_document_put_message( - format!("{runtime_id}-decorate-put"), - intent.intent_id.clone(), - &intent, - CultNetDocumentPutOptions::default(), - )?; - transport.send_schema_message(&message)?; - - let receipt_message = transport - .read_schema_message() - .context("waiting for mutation receipt")?; - let _receipt = document_registry - .apply_raw_document_put_message::(cache, &receipt_message)?; - let mutated_message = transport - .read_schema_message() - .context("waiting for mutated note")?; - let mutated = document_registry - .apply_raw_document_put_message::(cache, &mutated_message)?; - if mutated_message_schema_id(&mutated_message) != Some(note_schema_id) { - return Err(anyhow!("mutation response used the wrong schema id")); - } - Ok(serde_json::json!({ - "schemaVersion": mutated.schema_version, - "documentId": mutated.document_id, - "authorRuntimeId": mutated.author_runtime_id, - "title": mutated.title, - "body": mutated.body, - "tags": mutated.tags, - })) -} - -fn fire_remote_weapon( - transport: &mut dyn SchemaMessageTransport, - cache: &mut CultCache, - document_registry: &CultNetDocumentRegistry, - runtime_id: &str, - remote_runtime_id: &str, -) -> Result { - let command = CultNetInteropFireCommand { - schema_version: FIRE_COMMAND_SCHEMA_VERSION.to_string(), - command_id: format!("{runtime_id}-fire"), - character_id: remote_runtime_id.to_string(), - weapon_id: "interop-rifle".to_string(), - }; - let message = document_registry.create_raw_document_put_message( - format!("{runtime_id}-fire-put"), - command.command_id.clone(), - &command, - CultNetDocumentPutOptions::default(), - )?; - transport.send_schema_message(&message)?; - let receipt_message = transport - .read_schema_message() - .context("waiting for fire receipt")?; - let receipt = document_registry - .apply_raw_document_put_message::(cache, &receipt_message)?; - Ok(serde_json::json!({ - "schemaVersion": receipt.schema_version, - "commandId": receipt.command_id, - "accepted": receipt.accepted, - "characterId": receipt.character_id, - "weaponId": receipt.weapon_id, - "shotsFired": receipt.shots_fired, - "ammoRemaining": receipt.ammo_remaining, - })) -} - -fn start_udp_discovery_server(config: Arc) -> Result<()> { - let socket = create_discovery_socket(config.discovery_port, true)?; - socket.join_multicast_v4(&config.discovery_group, &Ipv4Addr::UNSPECIFIED)?; - socket.set_read_timeout(Some(Duration::from_millis(250)))?; - - thread::spawn(move || { - let mut buffer = vec![0_u8; 4096]; - loop { - match socket.recv_from(&mut buffer) { - Ok((len, remote)) => { - if let Ok(DiscoveryMessage::Probe { - message_id, - requester_runtime_id: _, - }) = rmp_serde::from_slice::(&buffer[..len]) - { - let announce = DiscoveryMessage::Announce { - message_id, - runtime_id: config.runtime_id.clone(), - runtime_kind: config.runtime_kind.clone(), - display_name: config.display_name.clone(), - agent_id: Some(config.agent_id.clone()), - tcp_host: config.advertise_host.clone(), - tcp_port: config.tcp_port, - wire_contract: "cultnet.schema.v0".to_string(), - supported_document_types: vec![INTEROP_DOCUMENT_TYPE.to_string()], - transport_profiles: Some(interop_transport_profiles( - &config.runtime_id, - &config.advertise_host, - config.tcp_port, - config.rudp_port, - )), - supports_schema_catalog: true, - }; - if let Ok(payload) = rmp_serde::to_vec_named(&announce) { - let _ = socket.send_to(&payload, remote); - } - } - } - Err(error) - if error.kind() == std::io::ErrorKind::WouldBlock - || error.kind() == std::io::ErrorKind::TimedOut => {} - Err(_) => break, - } - } - }); - - Ok(()) -} - -fn start_tcp_server( - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let listener = - TcpListener::bind((config.bind_host.as_str(), config.tcp_port)).with_context(|| { - format!( - "failed to bind TCP listener on {}:{}", - config.bind_host, config.tcp_port - ) - })?; - listener.set_nonblocking(false)?; - - thread::spawn(move || { - for stream in listener.incoming() { - let Ok(stream) = stream else { - continue; - }; - let config = config.clone(); - let cache = cache.clone(); - let document_registry = document_registry.clone(); - let schema_registry = schema_registry.clone(); - thread::spawn(move || { - if let Err(error) = - handle_connection(stream, config, cache, document_registry, schema_registry) - { - eprintln!("{error:#}"); - } - }); - } - }); - - Ok(()) -} - -fn start_rudp_server( - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let socket = - UdpSocket::bind((config.bind_host.as_str(), config.rudp_port)).with_context(|| { - format!( - "failed to bind RUDP listener on {}:{}", - config.bind_host, config.rudp_port - ) - })?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - - thread::spawn(move || { - if let Err(error) = - run_rudp_server(socket, config, cache, document_registry, schema_registry) - { - eprintln!("{error:#}"); - } - }); - - Ok(()) -} - -struct RudpPeerSession { - session: CultNetRudpSession, -} - -fn run_rudp_server( - socket: UdpSocket, - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let mut peers = BTreeMap::::new(); - let mut buffer = vec![0_u8; 65_535]; - loop { - match socket.recv_from(&mut buffer) { - Ok((len, remote_addr)) => { - let packet = match decode_rudp_packet(&buffer[..len]) { - Ok(packet) if packet.connection_id == RUDP_INTEROP_CONNECTION_ID => packet, - Ok(_) => continue, - Err(error) => { - eprintln!("{error:#}"); - continue; - } - }; - let peer = peers.entry(remote_addr).or_insert_with(|| RudpPeerSession { - session: CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: RUDP_INTEROP_CONNECTION_ID, - initial_sequence: 100, - resend_delay_ms: RUDP_INTEROP_RESEND_DELAY_MS, - max_pending_reliable_packets: None, - }), - }); - if packet.packet_type == CultNetRudpPacketType::Connect { - let accept = peer.session.accept_connect( - &packet, - rudp_now_ms(), - b"cultnet-interop-rudp".to_vec(), - )?; - send_rudp_packet(&socket, remote_addr, &accept)?; - continue; - } - - let result = peer.session.receive(&packet, rudp_now_ms())?; - if let Some(reply) = result.reply { - send_rudp_packet(&socket, remote_addr, &reply)?; - } - if packet.packet_type == CultNetRudpPacketType::Data { - let ack = peer.session.create_ack(); - send_rudp_packet(&socket, remote_addr, &ack)?; - } - for frame in result.delivered { - if frame.channel_id != "schema" { - continue; - } - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - let mut transport = RudpSessionSender { - socket: &socket, - remote_addr, - session: &mut peer.session, - }; - handle_server_message( - &mut transport, - message, - &config, - &cache, - &document_registry, - &schema_registry, - )?; - } - } - Err(error) - if error.kind() == std::io::ErrorKind::WouldBlock - || error.kind() == std::io::ErrorKind::TimedOut - || error.kind() == std::io::ErrorKind::ConnectionReset => - { - let now = rudp_now_ms(); - for (remote_addr, peer) in peers.iter_mut() { - for packet in peer.session.due_resends(now) { - send_rudp_packet(&socket, *remote_addr, &packet)?; - } - } - } - Err(error) => return Err(error.into()), - } - } -} - -fn handle_connection( - stream: TcpStream, - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let mut transport = TcpFramedTransportConnection::new( - stream, - tcp_transport_profile(&config.runtime_id, &config.advertise_host, config.tcp_port), - ); - loop { - let message = match transport.read_schema_message() { - Ok(message) => message, - Err(error) if is_eof_like(&error) => break, - Err(error) => return Err(error), - }; - - handle_server_message( - &mut transport, - message, - &config, - &cache, - &document_registry, - &schema_registry, - )?; - } - - Ok(()) -} - -fn handle_server_message( - transport: &mut dyn SchemaMessageSender, - message: CultNetMessage, - config: &PeerConfig, - cache: &Arc>, - document_registry: &CultNetDocumentRegistry, - schema_registry: &CultNetSchemaRegistry, -) -> Result<()> { - match message { - CultNetMessage::Hello { .. } => { - transport.send_schema_message(&CultNetMessage::Hello { - runtime_id: config.runtime_id.clone(), - runtime_kind: config.runtime_kind.clone(), - agent_id: Some(config.agent_id.clone()), - role: None, - display_name: Some(config.display_name.clone()), - supported_document_types: Some(vec![INTEROP_DOCUMENT_TYPE.to_string()]), - supported_mutation_contracts: Some(interaction_contracts()), - supported_message_versions: Some(vec![INTEROP_SCHEMA_VERSION.to_string()]), - transport_profiles: Some(interop_transport_profiles( - &config.runtime_id, - &config.advertise_host, - config.tcp_port, - config.rudp_port, - )), - supports_schema_catalog: Some(true), - })?; - } - request @ CultNetMessage::SchemaCatalogRequest { .. } => { - let response = schema_registry.create_catalog_response(&request)?; - transport.send_schema_message(&response)?; - } - CultNetMessage::SnapshotRequest { - message_id, - schema_ids, - record_keys, - } => { - let mut response = document_registry.create_raw_snapshot_response( - &cache.lock().expect("cache poisoned"), - message_id, - schema_ids.as_deref(), - record_keys.as_deref(), - )?; - if let CultNetMessage::SnapshotResponseRaw { documents, .. } = &mut response { - for document in documents.iter_mut() { - document.source_runtime_id = Some(config.runtime_id.clone()); - document.source_agent_id = Some(config.agent_id.clone()); - document.source_role = Some("peer".to_string()); - document.tags = Some(vec!["interop".to_string(), config.runtime_id.clone()]); - } - } - transport.send_schema_message(&response)?; - } - message @ CultNetMessage::DocumentPutRaw { .. } => { - handle_raw_put(transport, config, cache, document_registry, &message)?; - } - _ => {} - } - Ok(()) -} - -fn handle_raw_put( - transport: &mut dyn SchemaMessageSender, - config: &PeerConfig, - cache: &Arc>, - document_registry: &CultNetDocumentRegistry, - message: &CultNetMessage, -) -> Result<()> { - let CultNetMessage::DocumentPutRaw { document, .. } = message else { - return Ok(()); - }; - if document.schema_id == MUTATION_INTENT_SCHEMA_ID { - let mut cache = cache.lock().expect("cache poisoned"); - let intent = document_registry - .apply_raw_document_put_message::(&mut cache, message)?; - let mut note = cache.get_required::(&intent.target_document_id)?; - note.body = format!("{}{}", note.body, intent.append_body); - note.tags.push(intent.append_tag); - cache.put(¬e.document_id, ¬e)?; - let receipt = CultNetInteropMutationReceipt { - schema_version: MUTATION_RECEIPT_SCHEMA_VERSION.to_string(), - intent_id: intent.intent_id.clone(), - accepted: true, - document_id: note.document_id.clone(), - body: note.body.clone(), - tags: note.tags.clone(), - }; - let options = response_options(config, "mutation"); - let receipt_message = document_registry.create_raw_document_put_message( - format!("{}-mutation-receipt", config.runtime_id), - receipt.intent_id.clone(), - &receipt, - options.clone(), - )?; - let note_message = document_registry.create_raw_document_put_message( - format!("{}-mutated-note", config.runtime_id), - note.document_id.clone(), - ¬e, - options, - )?; - transport.send_schema_message(&receipt_message)?; - transport.send_schema_message(¬e_message)?; - } else if document.schema_id == FIRE_COMMAND_SCHEMA_ID { - let mut cache = cache.lock().expect("cache poisoned"); - let command = document_registry - .apply_raw_document_put_message::(&mut cache, message)?; - let receipt = CultNetInteropFireReceipt { - schema_version: FIRE_RECEIPT_SCHEMA_VERSION.to_string(), - command_id: command.command_id, - accepted: true, - character_id: command.character_id, - weapon_id: command.weapon_id, - shots_fired: 1, - ammo_remaining: 29, - }; - let receipt_message = document_registry.create_raw_document_put_message( - format!("{}-fire-receipt", config.runtime_id), - receipt.command_id.clone(), - &receipt, - response_options(config, "side-effect"), - )?; - transport.send_schema_message(&receipt_message)?; - } - Ok(()) -} - -fn load_schema_registration(schema_path: &str) -> Result { - let schema_json = fs::read_to_string(schema_path) - .with_context(|| format!("failed to read schema {}", schema_path))?; - let parsed: serde_json::Value = serde_json::from_str(&schema_json) - .with_context(|| format!("failed to parse schema {}", schema_path))?; - let schema_id = parsed - .get("$id") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| anyhow!("schema {} is missing $id", schema_path))? - .to_string(); - let title = parsed - .get("title") - .and_then(serde_json::Value::as_str) - .map(ToString::to_string); - - Ok(CultNetSchemaRegistration { - schema_id, - kind: CultNetSchemaKind::DocumentPayload, - wire_contracts: vec![CultNetWireContract::CultNetSchemaV0], - schema_version: Some(INTEROP_SCHEMA_VERSION.to_string()), - document_type: Some(INTEROP_DOCUMENT_TYPE.to_string()), - title, - schema_json: Some(schema_json), - }) -} - -fn register_capability_entry_types(cache: &mut CultCache) -> Result<()> { - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; - Ok(()) -} - -fn register_capability_bindings( - document_registry: &mut CultNetDocumentRegistry, - note_schema_id: &str, -) { - document_registry - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropNote, - >( - note_schema_id.to_string(), - INTEROP_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropMutationIntent, - >( - MUTATION_INTENT_SCHEMA_ID.to_string(), - MUTATION_INTENT_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropMutationReceipt, - >( - MUTATION_RECEIPT_SCHEMA_ID.to_string(), - MUTATION_RECEIPT_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropFireCommand, - >( - FIRE_COMMAND_SCHEMA_ID.to_string(), - FIRE_COMMAND_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropFireReceipt, - >( - FIRE_RECEIPT_SCHEMA_ID.to_string(), - FIRE_RECEIPT_SCHEMA_VERSION.to_string(), - )); -} - -fn interaction_contracts() -> Vec { - vec![CultNetDocumentMutationContract { - document_type: INTEROP_DOCUMENT_TYPE.to_string(), - payload_schema_version: Some(INTEROP_SCHEMA_VERSION.to_string()), - operations: vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::DocumentPut, - CultNetDocumentOperation::IntentSubmit, - CultNetDocumentOperation::ReceiptWatch, - ], - authority: CultNetMutationAuthority::Runtime, - intent_document_types: Some(vec![ - MUTATION_INTENT_TYPE.to_string(), - FIRE_COMMAND_TYPE.to_string(), - ]), - receipt_document_types: Some(vec![ - MUTATION_RECEIPT_TYPE.to_string(), - FIRE_RECEIPT_TYPE.to_string(), - ]), - notes: None, - }] -} - -fn tcp_transport_profile(runtime_id: &str, host: &str, port: u16) -> CultNetTransportProfile { - create_tcp_framed_transport_profile( - runtime_id, - TcpFramedTransportProfileOptions { - transport_id: Some("interop-tcp".to_string()), - host: Some(host.to_string()), - port: Some(port), - ..TcpFramedTransportProfileOptions::default() - }, - ) -} - -fn rudp_transport_profile(runtime_id: &str, host: &str, port: u16) -> CultNetTransportProfile { - create_rudp_transport_profile( - runtime_id, - RudpTransportProfileOptions { - transport_id: Some("interop-rudp".to_string()), - host: Some(host.to_string()), - port: Some(port), - max_fragment_bytes: Some(RUDP_INTEROP_MAX_FRAGMENT_BYTES as u32), - ..RudpTransportProfileOptions::default() - }, - ) -} - -fn interop_transport_profiles( - runtime_id: &str, - host: &str, - tcp_port: u16, - rudp_port: u16, -) -> Vec { - vec![ - tcp_transport_profile(runtime_id, host, tcp_port), - rudp_transport_profile(runtime_id, host, rudp_port), - ] -} - -fn dial_transport_profiles(config: &DialConfig) -> Vec { - let mut profiles = Vec::new(); - if let Some(target_port) = config.target_port { - profiles.push(tcp_transport_profile( - &config.runtime_id, - &config.target_host, - target_port, - )); - } - if let Some(target_rudp_port) = config.target_rudp_port { - profiles.push(rudp_transport_profile( - &config.runtime_id, - &config.target_host, - target_rudp_port, - )); - } - profiles -} - -fn open_dial_transport(config: &DialConfig) -> Result> { - if let Some(target_rudp_port) = config.target_rudp_port { - let remote_addr: SocketAddr = format!("{}:{}", config.target_host, target_rudp_port) - .parse() - .with_context(|| { - format!( - "failed to parse RUDP remote endpoint {}:{}", - config.target_host, target_rudp_port - ) - })?; - let socket = UdpSocket::bind(("127.0.0.1", 0))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let mut options = CultNetRudpSocketTransportOptions::client( - format!("{}-interop-rudp-dial", config.runtime_id), - socket, - remote_addr, - RUDP_INTEROP_CONNECTION_ID, - ); - options.transport_id = Some("interop-rudp".to_string()); - options.resend_delay_ms = RUDP_INTEROP_RESEND_DELAY_MS; - options.max_fragment_bytes = Some(RUDP_INTEROP_MAX_FRAGMENT_BYTES as u32); - let mut transport = CultNetRudpSocketTransportConnection::new(options)?; - transport.connect(b"cultnet-interop-rudp".to_vec())?; - wait_for_rudp_connected(&mut transport, Duration::from_secs(5))?; - return Ok(Box::new(transport)); - } - - let target_port = config - .target_port - .ok_or_else(|| anyhow!("dial requires --target-port or --target-rudp-port"))?; - let stream = - TcpStream::connect((config.target_host.as_str(), target_port)).with_context(|| { - format!( - "failed to connect to {}:{}", - config.target_host, target_port - ) - })?; - Ok(Box::new(TcpFramedTransportConnection::new( - stream, - tcp_transport_profile(&config.runtime_id, &config.target_host, target_port), - ))) -} - -fn response_options(config: &PeerConfig, tag: &str) -> CultNetDocumentPutOptions { - CultNetDocumentPutOptions { - source_runtime_id: Some(config.runtime_id.clone()), - source_agent_id: Some(config.agent_id.clone()), - source_role: Some("peer".to_string()), - tags: Some(vec![tag.to_string(), config.runtime_id.clone()]), - ..CultNetDocumentPutOptions::default() - } -} - -fn mutated_message_schema_id(message: &CultNetMessage) -> Option<&str> { - match message { - CultNetMessage::DocumentPutRaw { document, .. } => Some(document.schema_id.as_str()), - _ => None, - } -} - -fn build_note(runtime_id: &str, display_name: &str) -> CultNetInteropNote { - CultNetInteropNote { - schema_version: INTEROP_SCHEMA_VERSION.to_string(), - document_id: format!("note:{runtime_id}"), - author_runtime_id: runtime_id.to_string(), - title: format!("{display_name} keeps a little note"), - body: format!( - "{runtime_id} can move CultNet state without begging the gods for translation." - ), - tags: vec![ - runtime_id.to_string(), - "interop".to_string(), - "cultnet".to_string(), - ], - } -} - -fn is_eof_like(error: &anyhow::Error) -> bool { - error - .downcast_ref::() - .is_some_and(|io| io.kind() == std::io::ErrorKind::UnexpectedEof) -} - -impl SchemaMessageSender for TcpFramedTransportConnection { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - self.send("schema", &payload) - } -} - -impl SchemaMessageTransport for TcpFramedTransportConnection { - fn read_schema_message(&mut self) -> Result { - let frame = self.receive()?; - decode_cultnet_message_from_slice(&frame.payload, CultNetWireContract::CultNetSchemaV0) - } -} - -impl SchemaMessageSender for CultNetRudpSocketTransportConnection { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - CultNetRudpSocketTransportConnection::send_schema_message(self, message) - } -} - -impl SchemaMessageTransport for CultNetRudpSocketTransportConnection { - fn read_schema_message(&mut self) -> Result { - let deadline = Instant::now() + RUDP_INTEROP_READ_TIMEOUT; - while Instant::now() < deadline { - if let Some(message) = self.receive_schema_message_once()? { - return Ok(message); - } - self.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - Err(anyhow!("timed out waiting for RUDP schema message")) - } -} - -struct RudpSessionSender<'a> { - socket: &'a UdpSocket, - remote_addr: SocketAddr, - session: &'a mut CultNetRudpSession, -} - -impl SchemaMessageSender for RudpSessionSender<'_> { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - let packets = self.session.send_many( - "schema", - payload, - CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: rudp_now_ms(), - }, - Some(RUDP_INTEROP_MAX_FRAGMENT_BYTES), - )?; - for packet in packets { - send_rudp_packet(self.socket, self.remote_addr, &packet)?; - } - Ok(()) - } -} - -fn send_rudp_packet( - socket: &UdpSocket, - remote_addr: SocketAddr, - packet: &cultnet_rs::CultNetRudpPacket, -) -> Result<()> { - socket.send_to(&encode_rudp_packet(packet)?, remote_addr)?; - Ok(()) -} - -fn wait_for_rudp_connected( - transport: &mut CultNetRudpSocketTransportConnection, - timeout: Duration, -) -> Result<()> { - let deadline = Instant::now() + timeout; - while Instant::now() < deadline { - if transport.connected() { - return Ok(()); - } - let _ = transport.receive_once()?; - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - Err(anyhow!("timed out waiting for RUDP connect")) -} - -fn rudp_now_ms() -> u64 { - use std::time::SystemTime; - use std::time::UNIX_EPOCH; - - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64 -} - -fn create_discovery_socket(port: u16, _join_group: bool) -> Result { - let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?; - socket.set_reuse_address(true)?; - socket.bind(&SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port).into())?; - let socket = UdpSocket::from(socket); - socket.set_multicast_loop_v4(true)?; - socket.set_multicast_ttl_v4(1)?; - socket.set_nonblocking(false)?; - Ok(socket) -} - -fn parse_peer_config(options: &BTreeMap) -> Result { - let tcp_port = parse_u16_arg(options, "tcp-port")?; - Ok(PeerConfig { - runtime_id: require_arg(options, "runtime-id")?.to_string(), - runtime_kind: require_arg(options, "runtime-kind")?.to_string(), - display_name: require_arg(options, "display-name")?.to_string(), - agent_id: require_arg(options, "agent-id")?.to_string(), - bind_host: options - .get("bind-host") - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()), - advertise_host: require_arg(options, "advertise-host")?.to_string(), - tcp_port, - rudp_port: options - .get("rudp-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --rudp-port must be a u16")? - .unwrap_or(tcp_port), - discovery_port: parse_u16_arg(options, "discovery-port")?, - discovery_group: parse_ipv4_arg(options, "discovery-group")?, - schema_path: require_arg(options, "schema-path")?.to_string(), - }) -} - -fn parse_dial_config(options: &BTreeMap) -> Result { - Ok(DialConfig { - runtime_id: require_arg(options, "runtime-id")?.to_string(), - runtime_kind: require_arg(options, "runtime-kind")?.to_string(), - display_name: require_arg(options, "display-name")?.to_string(), - agent_id: require_arg(options, "agent-id")?.to_string(), - target_host: require_arg(options, "target-host")?.to_string(), - target_port: options - .get("target-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --target-port must be a u16")?, - target_rudp_port: options - .get("target-rudp-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --target-rudp-port must be a u16")?, - schema_path: require_arg(options, "schema-path")?.to_string(), - }) -} - -fn parse_args(args: Vec) -> BTreeMap { - let mut parsed = BTreeMap::new(); - let mut index = 0; - while index < args.len() { - let token = &args[index]; - if !token.starts_with("--") { - index += 1; - continue; - } - let name = token.trim_start_matches("--").to_string(); - let value = args - .get(index + 1) - .cloned() - .unwrap_or_else(|| panic!("missing value for --{name}")); - parsed.insert(name, value); - index += 2; - } - parsed -} - -fn require_arg<'a>(options: &'a BTreeMap, name: &str) -> Result<&'a str> { - options - .get(name) - .map(String::as_str) - .ok_or_else(|| anyhow!("missing required argument --{name}")) -} - -fn parse_u16_arg(options: &BTreeMap, name: &str) -> Result { - require_arg(options, name)? - .parse::() - .with_context(|| format!("argument --{name} must be a u16")) -} - -fn parse_u64_arg(options: &BTreeMap, name: &str) -> Option { - options - .get(name) - .map(|value| value.parse::().expect("u64 arg")) -} - -fn parse_ipv4_arg(options: &BTreeMap, name: &str) -> Result { - require_arg(options, name)? - .parse::() - .with_context(|| format!("argument --{name} must be an IPv4 address")) -} - -fn runtime_store_path(runtime_id: &str) -> std::path::PathBuf { - std::env::temp_dir().join(format!("cultnet-rs-interop-{runtime_id}.msgpack")) -} - -fn print_json(value: &serde_json::Value) -> Result<()> { - println!("{}", serde_json::to_string(value)?); - Ok(()) -} diff --git a/vendor/cultnet-rs/src/contracts.rs b/vendor/cultnet-rs/src/contracts.rs deleted file mode 100644 index e637883..0000000 --- a/vendor/cultnet-rs/src/contracts.rs +++ /dev/null @@ -1,1406 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CultNetWireContract { - #[serde(rename = "cultnet.schema.v0")] - CultNetSchemaV0, - #[serde(rename = "gamecult.networking.v0")] - GameCultNetworkingV0, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetSchemaKind { - WireMessage, - DocumentPayload, - SharedContract, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetRawPayloadEncoding { - Messagepack, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetDocumentRecord { - pub schema_id: String, - pub record_key: String, - pub stored_at: String, - pub payload: TPayload, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_agent_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tags: Option>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum CultNetDocumentOperation { - Snapshot, - DocumentPut, - DocumentDelete, - IntentSubmit, - ReceiptWatch, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum CultNetMutationAuthority { - ReadOnly, - LocalUser, - Coordinator, - Runtime, - Denied, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetDocumentMutationContract { - pub document_type: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub payload_schema_version: Option, - pub operations: Vec, - pub authority: CultNetMutationAuthority, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub intent_document_types: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub receipt_document_types: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub notes: Option>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetTransportProfile { - pub schema_version: String, - pub runtime_id: String, - pub transports: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetTransportDescriptor { - pub transport_id: String, - pub protocol: CultNetTransportProtocol, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub port: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub discovery_group: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub wire_contracts: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reconnect_policy: Option, - pub channels: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetReconnectPolicy { - pub schema_version: String, - pub policy_id: String, - pub base_delay_ms: u64, - pub max_delay_ms: u64, - pub max_jitter_ms: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_attempts: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetTransportProtocol { - TcpFramed, - Litenetlib, - Websocket, - Rudp, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetTransportChannel { - pub channel_id: String, - pub delivery: CultNetTransportDelivery, - pub ordering: CultNetTransportOrdering, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_payload_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_fragment_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_pending_reliable_packets: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetTransportDelivery { - Reliable, - Unreliable, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetTransportOrdering { - Ordered, - Unordered, - Sequenced, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetRawDocumentRecord { - pub schema_id: String, - pub record_key: String, - pub stored_at: String, - pub payload_encoding: CultNetRawPayloadEncoding, - #[serde(with = "serde_bytes")] - pub payload: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_agent_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tags: Option>, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetSchemaDescriptor { - pub schema_id: String, - pub kind: CultNetSchemaKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema_version: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub document_type: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub title: Option, - pub wire_contracts: Vec, - pub content_hash: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema_json: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetShardDescriptor { - pub shard_id: String, - pub owner_runtime_id: String, - pub epoch: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub is_primary: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub schema_ids: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub key_prefix: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub primary_endpoints: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub replica_endpoints: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub read_replica_endpoints: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub authority_lease_id: Option, -} - -impl CultNetShardDescriptor { - pub fn serves(&self, schema_id: Option<&str>, record_key: Option<&str>) -> bool { - if let Some(schema_id) = schema_id - && !self.schema_ids.is_empty() - && !self - .schema_ids - .iter() - .any(|candidate| candidate == schema_id) - { - return false; - } - - if let Some(record_key) = record_key - && let Some(prefix) = self.key_prefix.as_deref() - && !record_key.starts_with(prefix) - { - return false; - } - - true - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "schemaVersion")] -pub enum CultNetMessage { - #[serde(rename = "cultnet.hello.v0", rename_all = "camelCase")] - Hello { - runtime_id: String, - runtime_kind: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - agent_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - display_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - supported_document_types: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - supported_mutation_contracts: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - supported_message_versions: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - transport_profiles: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - supports_schema_catalog: Option, - }, - #[serde(rename = "cultnet.login.v0", rename_all = "camelCase")] - Login { - nonce: String, - auth: String, - password: String, - }, - #[serde(rename = "cultnet.register.v0", rename_all = "camelCase")] - Register { - nonce: String, - email: String, - password: String, - name: String, - }, - #[serde(rename = "cultnet.verify.v0", rename_all = "camelCase")] - Verify { nonce: String, session: String }, - #[serde(rename = "cultnet.login_success.v0", rename_all = "camelCase")] - LoginSuccess { nonce: String, session: String }, - #[serde(rename = "cultnet.error.v0", rename_all = "camelCase")] - Error { error: String }, - #[serde(rename = "cultnet.sample.change_name.v0", rename_all = "camelCase")] - SampleChangeName { name: String }, - #[serde(rename = "cultnet.sample.chat.v0", rename_all = "camelCase")] - SampleChat { text: String }, - #[serde(rename = "cultnet.document_put.v0", rename_all = "camelCase")] - DocumentPut { - message_id: String, - document: CultNetDocumentRecord, - }, - #[serde(rename = "cultnet.document_delete.v0", rename_all = "camelCase")] - DocumentDelete { - message_id: String, - schema_id: String, - record_key: String, - }, - #[serde(rename = "cultnet.document_put_raw.v0", rename_all = "camelCase")] - DocumentPutRaw { - message_id: String, - document: CultNetRawDocumentRecord, - }, - #[serde(rename = "cultnet.snapshot_request.v0", rename_all = "camelCase")] - SnapshotRequest { - message_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - schema_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - record_keys: Option>, - }, - #[serde(rename = "cultnet.snapshot_response.v0", rename_all = "camelCase")] - SnapshotResponse { - message_id: String, - documents: Vec>, - }, - #[serde(rename = "cultnet.snapshot_response_raw.v0", rename_all = "camelCase")] - SnapshotResponseRaw { - message_id: String, - documents: Vec, - }, - #[serde(rename = "cultnet.schema_catalog_request.v0", rename_all = "camelCase")] - SchemaCatalogRequest { - message_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - include_schema_json: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - schema_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - kinds: Option>, - }, - #[serde( - rename = "cultnet.schema_catalog_response.v0", - rename_all = "camelCase" - )] - SchemaCatalogResponse { - message_id: String, - schemas: Vec, - }, - #[serde(rename = "cultnet.shard_catalog_request.v0", rename_all = "camelCase")] - ShardCatalogRequest { - message_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - schema_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - record_keys: Option>, - }, - #[serde(rename = "cultnet.shard_catalog_response.v0", rename_all = "camelCase")] - ShardCatalogResponse { - message_id: String, - shards: Vec, - }, - #[serde(rename = "cultnet.operation_request.v0", rename_all = "camelCase")] - OperationRequest { - message_id: String, - service_id: String, - operation: String, - payload_schema: String, - #[serde(default = "messagepack_base64_encoding")] - payload_encoding: String, - payload: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - target_runtime_id: Option, - }, - #[serde(rename = "cultnet.operation_response.v0", rename_all = "camelCase")] - OperationResponse { - message_id: String, - service_id: String, - operation: String, - status: String, - payload_schema: String, - #[serde(default = "messagepack_base64_encoding")] - payload_encoding: String, - payload: String, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - diagnostics: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_runtime_id: Option, - }, -} - -fn messagepack_base64_encoding() -> String { - "messagepack-base64".to_string() -} - -pub fn parse_cultnet_message( - wire_value: &rmpv::Value, - contract: CultNetWireContract, -) -> Result { - match contract { - CultNetWireContract::CultNetSchemaV0 => { - let message = match schema_version_from_wire_value(wire_value)? { - Some("cultnet.document_put_raw.v0" | "cultnet.snapshot_response_raw.v0") => { - parse_raw_cultnet_schema_message(wire_value)? - } - _ => { - let json_value: Value = rmp_serde::from_slice(&rmp_serde::to_vec(wire_value)?)?; - serde_json::from_value(json_value)? - } - }; - validate_message(&message)?; - Ok(message) - } - CultNetWireContract::GameCultNetworkingV0 => parse_gamecult_networking_message(wire_value), - } -} - -pub fn encode_cultnet_message_for_wire( - message: &CultNetMessage, - contract: CultNetWireContract, -) -> Result { - validate_message(message)?; - match contract { - CultNetWireContract::CultNetSchemaV0 => match message { - CultNetMessage::DocumentPutRaw { .. } | CultNetMessage::SnapshotResponseRaw { .. } => { - encode_raw_cultnet_schema_message(message) - } - _ => Ok(rmp_serde::from_slice(&rmp_serde::to_vec( - &serde_json::to_value(message)?, - )?)?), - }, - CultNetWireContract::GameCultNetworkingV0 => encode_gamecult_networking_message(message), - } -} - -pub fn encode_cultnet_message_to_vec( - message: &CultNetMessage, - contract: CultNetWireContract, -) -> Result> { - let wire_value = encode_cultnet_message_for_wire(message, contract)?; - Ok(rmp_serde::to_vec(&wire_value)?) -} - -pub fn decode_cultnet_message_from_slice( - bytes: &[u8], - contract: CultNetWireContract, -) -> Result { - if contract == CultNetWireContract::CultNetSchemaV0 { - let wire_value: rmpv::Value = rmp_serde::from_slice(bytes)?; - return parse_cultnet_message(&wire_value, contract); - } - let wire_value: rmpv::Value = rmp_serde::from_slice(bytes)?; - parse_cultnet_message(&wire_value, contract) -} - -fn validate_message(message: &CultNetMessage) -> Result<()> { - match message { - CultNetMessage::Hello { - runtime_id, - runtime_kind, - agent_id, - role, - display_name, - supported_document_types, - supported_mutation_contracts, - supported_message_versions, - transport_profiles: _, - supports_schema_catalog: _, - } => { - require_non_empty(runtime_id, "runtimeId")?; - require_non_empty(runtime_kind, "runtimeKind")?; - require_optional_non_empty(agent_id.as_deref(), "agentId")?; - require_optional_non_empty(role.as_deref(), "role")?; - require_optional_non_empty(display_name.as_deref(), "displayName")?; - require_optional_string_vec( - supported_document_types.as_deref(), - "supportedDocumentTypes", - )?; - if let Some(contracts) = supported_mutation_contracts { - for contract in contracts { - validate_mutation_contract(contract)?; - } - } - require_optional_string_vec( - supported_message_versions.as_deref(), - "supportedMessageVersions", - )?; - } - CultNetMessage::Login { - nonce, - auth, - password, - } => { - require_non_empty(nonce, "nonce")?; - require_non_empty(auth, "auth")?; - require_non_empty(password, "password")?; - } - CultNetMessage::Register { - nonce, - email, - password, - name, - } => { - require_non_empty(nonce, "nonce")?; - require_non_empty(email, "email")?; - require_non_empty(password, "password")?; - require_non_empty(name, "name")?; - } - CultNetMessage::Verify { nonce, session } - | CultNetMessage::LoginSuccess { nonce, session } => { - require_non_empty(nonce, "nonce")?; - require_non_empty(session, "session")?; - } - CultNetMessage::Error { error } => require_non_empty(error, "error")?, - CultNetMessage::SampleChangeName { name } => require_non_empty(name, "name")?, - CultNetMessage::SampleChat { text } => require_non_empty(text, "text")?, - CultNetMessage::DocumentPut { - message_id, - document, - } => { - require_non_empty(message_id, "messageId")?; - validate_document_record(document)?; - } - CultNetMessage::DocumentDelete { - message_id, - schema_id, - record_key, - } => { - require_non_empty(message_id, "messageId")?; - require_non_empty(schema_id, "schemaId")?; - require_non_empty(record_key, "recordKey")?; - } - CultNetMessage::DocumentPutRaw { - message_id, - document, - } => { - require_non_empty(message_id, "messageId")?; - validate_raw_document_record(document)?; - } - CultNetMessage::SnapshotRequest { - message_id, - schema_ids, - record_keys, - } => { - require_non_empty(message_id, "messageId")?; - require_optional_string_vec(schema_ids.as_deref(), "schemaIds")?; - require_optional_string_vec(record_keys.as_deref(), "recordKeys")?; - } - CultNetMessage::SnapshotResponse { - message_id, - documents, - } => { - require_non_empty(message_id, "messageId")?; - for document in documents { - validate_document_record(document)?; - } - } - CultNetMessage::SnapshotResponseRaw { - message_id, - documents, - } => { - require_non_empty(message_id, "messageId")?; - for document in documents { - validate_raw_document_record(document)?; - } - } - CultNetMessage::SchemaCatalogRequest { - message_id, - include_schema_json: _, - schema_ids, - kinds: _, - } => { - require_non_empty(message_id, "messageId")?; - require_optional_string_vec(schema_ids.as_deref(), "schemaIds")?; - } - CultNetMessage::SchemaCatalogResponse { - message_id, - schemas, - } => { - require_non_empty(message_id, "messageId")?; - for schema in schemas { - validate_schema_descriptor(schema)?; - } - } - CultNetMessage::ShardCatalogRequest { - message_id, - schema_ids, - record_keys, - } => { - require_non_empty(message_id, "messageId")?; - require_optional_string_vec(schema_ids.as_deref(), "schemaIds")?; - require_optional_string_vec(record_keys.as_deref(), "recordKeys")?; - } - CultNetMessage::ShardCatalogResponse { message_id, shards } => { - require_non_empty(message_id, "messageId")?; - for shard in shards { - validate_shard_descriptor(shard)?; - } - } - CultNetMessage::OperationRequest { - message_id, - service_id, - operation, - payload_schema, - payload_encoding, - payload, - source_runtime_id, - target_runtime_id, - } => { - require_non_empty(message_id, "messageId")?; - require_non_empty(service_id, "serviceId")?; - require_non_empty(operation, "operation")?; - require_non_empty(payload_schema, "payloadSchema")?; - require_exact(payload_encoding, "messagepack-base64", "payloadEncoding")?; - require_non_empty(payload, "payload")?; - require_optional_non_empty(source_runtime_id.as_deref(), "sourceRuntimeId")?; - require_optional_non_empty(target_runtime_id.as_deref(), "targetRuntimeId")?; - } - CultNetMessage::OperationResponse { - message_id, - service_id, - operation, - status, - payload_schema, - payload_encoding, - payload, - diagnostics: _, - source_runtime_id, - } => { - require_non_empty(message_id, "messageId")?; - require_non_empty(service_id, "serviceId")?; - require_non_empty(operation, "operation")?; - require_non_empty(status, "status")?; - require_non_empty(payload_schema, "payloadSchema")?; - require_exact(payload_encoding, "messagepack-base64", "payloadEncoding")?; - require_non_empty(payload, "payload")?; - require_optional_non_empty(source_runtime_id.as_deref(), "sourceRuntimeId")?; - } - } - Ok(()) -} - -fn validate_mutation_contract(contract: &CultNetDocumentMutationContract) -> Result<()> { - require_non_empty(&contract.document_type, "documentType")?; - require_optional_non_empty( - contract.payload_schema_version.as_deref(), - "payloadSchemaVersion", - )?; - if contract.operations.is_empty() { - return Err(anyhow!( - "CultNet mutation contract for {:?} must advertise at least one operation", - contract.document_type - )); - } - require_optional_string_vec( - contract.intent_document_types.as_deref(), - "intentDocumentTypes", - )?; - require_optional_string_vec( - contract.receipt_document_types.as_deref(), - "receiptDocumentTypes", - )?; - require_optional_string_vec(contract.notes.as_deref(), "notes")?; - Ok(()) -} - -fn validate_document_record(document: &CultNetDocumentRecord) -> Result<()> { - require_non_empty(&document.schema_id, "schemaId")?; - require_non_empty(&document.record_key, "recordKey")?; - require_non_empty(&document.stored_at, "storedAt")?; - require_optional_non_empty(document.source_runtime_id.as_deref(), "sourceRuntimeId")?; - require_optional_non_empty(document.source_agent_id.as_deref(), "sourceAgentId")?; - require_optional_non_empty(document.source_role.as_deref(), "sourceRole")?; - require_optional_string_vec(document.tags.as_deref(), "tags")?; - Ok(()) -} - -fn validate_raw_document_record(document: &CultNetRawDocumentRecord) -> Result<()> { - require_non_empty(&document.schema_id, "schemaId")?; - require_non_empty(&document.record_key, "recordKey")?; - require_non_empty(&document.stored_at, "storedAt")?; - if document.payload.is_empty() { - return Err(anyhow!( - "CultNet field payload must contain non-empty MessagePack bytes" - )); - } - require_optional_non_empty(document.source_runtime_id.as_deref(), "sourceRuntimeId")?; - require_optional_non_empty(document.source_agent_id.as_deref(), "sourceAgentId")?; - require_optional_non_empty(document.source_role.as_deref(), "sourceRole")?; - require_optional_string_vec(document.tags.as_deref(), "tags")?; - Ok(()) -} - -fn validate_schema_descriptor(schema: &CultNetSchemaDescriptor) -> Result<()> { - require_non_empty(&schema.schema_id, "schemaId")?; - require_optional_non_empty(schema.schema_version.as_deref(), "schemaVersion")?; - require_optional_non_empty(schema.document_type.as_deref(), "documentType")?; - require_optional_non_empty(schema.title.as_deref(), "title")?; - require_non_empty(&schema.content_hash, "contentHash")?; - require_optional_non_empty(schema.schema_json.as_deref(), "schemaJson")?; - - if schema.wire_contracts.is_empty() { - return Err(anyhow!( - "CultNet field wireContracts must contain at least one supported contract" - )); - } - - Ok(()) -} - -fn validate_shard_descriptor(shard: &CultNetShardDescriptor) -> Result<()> { - require_non_empty(&shard.shard_id, "shardId")?; - require_non_empty(&shard.owner_runtime_id, "ownerRuntimeId")?; - require_optional_string_vec(Some(shard.schema_ids.as_slice()), "schemaIds")?; - require_optional_non_empty(shard.key_prefix.as_deref(), "keyPrefix")?; - require_optional_string_vec(Some(shard.primary_endpoints.as_slice()), "primaryEndpoints")?; - require_optional_string_vec(Some(shard.replica_endpoints.as_slice()), "replicaEndpoints")?; - require_optional_string_vec( - Some(shard.read_replica_endpoints.as_slice()), - "readReplicaEndpoints", - )?; - require_optional_non_empty(shard.region.as_deref(), "region")?; - require_optional_non_empty(shard.authority_lease_id.as_deref(), "authorityLeaseId")?; - Ok(()) -} - -fn require_non_empty(value: &str, field: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("CultNet field {field} must be non-empty")); - } - Ok(()) -} - -fn require_exact(value: &str, expected: &str, field: &str) -> Result<()> { - if value != expected { - return Err(anyhow!("{field} must be {expected}")); - } - Ok(()) -} - -fn require_optional_non_empty(value: Option<&str>, field: &str) -> Result<()> { - if let Some(value) = value { - require_non_empty(value, field)?; - } - Ok(()) -} - -fn require_optional_string_vec(value: Option<&[String]>, field: &str) -> Result<()> { - if let Some(values) = value { - for item in values { - require_non_empty(item, field)?; - } - } - Ok(()) -} - -fn schema_version_from_wire_value(input: &rmpv::Value) -> Result> { - let Some(object) = input.as_map() else { - return Ok(None); - }; - - Ok(object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == "schemaVersion") - .and_then(|_| value.as_str()) - })) -} - -fn parse_raw_cultnet_schema_message(input: &rmpv::Value) -> Result { - let object = input - .as_map() - .ok_or_else(|| anyhow!("cultnet.schema.v0 raw messages must be objects"))?; - - let get = |name: &str| -> Option<&rmpv::Value> { - object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == name) - .map(|_| value) - }) - }; - - let schema_version = require_legacy_string(get("schemaVersion"), "schemaVersion")?; - match schema_version.as_str() { - "cultnet.document_put_raw.v0" => Ok(CultNetMessage::DocumentPutRaw { - message_id: require_legacy_string(get("messageId"), "messageId")?, - document: require_raw_document_record(get("document"), "document")?, - }), - "cultnet.snapshot_response_raw.v0" => { - let documents = get("documents") - .and_then(rmpv::Value::as_array) - .ok_or_else(|| anyhow!("documents must be an array"))? - .iter() - .enumerate() - .map(|(index, value)| { - require_raw_document_record(value.into(), &format!("documents[{index}]")) - }) - .collect::>>()?; - - Ok(CultNetMessage::SnapshotResponseRaw { - message_id: require_legacy_string(get("messageId"), "messageId")?, - documents, - }) - } - _ => Err(anyhow!( - "Unsupported raw cultnet.schema.v0 schemaVersion {schema_version}" - )), - } -} - -fn encode_raw_cultnet_schema_message(message: &CultNetMessage) -> Result { - Ok(rmpv::Value::Map(match message { - CultNetMessage::DocumentPutRaw { - message_id, - document, - } => vec![ - ( - rmpv::Value::from("schemaVersion"), - rmpv::Value::from("cultnet.document_put_raw.v0"), - ), - ( - rmpv::Value::from("messageId"), - rmpv::Value::from(message_id.as_str()), - ), - ( - rmpv::Value::from("document"), - encode_raw_document_record(document), - ), - ], - CultNetMessage::SnapshotResponseRaw { - message_id, - documents, - } => vec![ - ( - rmpv::Value::from("schemaVersion"), - rmpv::Value::from("cultnet.snapshot_response_raw.v0"), - ), - ( - rmpv::Value::from("messageId"), - rmpv::Value::from(message_id.as_str()), - ), - ( - rmpv::Value::from("documents"), - rmpv::Value::Array(documents.iter().map(encode_raw_document_record).collect()), - ), - ], - _ => { - return Err(anyhow!( - "Message is not a raw cultnet.schema.v0 binary replication message" - )); - } - })) -} - -fn require_raw_document_record( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result { - let object = value - .and_then(rmpv::Value::as_map) - .ok_or_else(|| anyhow!("{field_name} must be an object"))?; - - let get = |name: &str| -> Option<&rmpv::Value> { - object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == name) - .map(|_| value) - }) - }; - - let payload_encoding = require_legacy_string( - get("payloadEncoding"), - &format!("{field_name}.payloadEncoding"), - )?; - - Ok(CultNetRawDocumentRecord { - schema_id: require_legacy_string(get("schemaId"), &format!("{field_name}.schemaId"))?, - record_key: require_legacy_string(get("recordKey"), &format!("{field_name}.recordKey"))?, - stored_at: require_legacy_string(get("storedAt"), &format!("{field_name}.storedAt"))?, - payload_encoding: match payload_encoding.as_str() { - "messagepack" => CultNetRawPayloadEncoding::Messagepack, - _ => { - return Err(anyhow!( - "{field_name}.payloadEncoding has unsupported value {payload_encoding}" - )); - } - }, - payload: get("payload") - .and_then(rmpv::Value::as_slice) - .map(|bytes| bytes.to_vec()) - .ok_or_else(|| anyhow!("{field_name}.payload must be binary MessagePack bytes"))?, - source_runtime_id: require_legacy_optional_string( - get("sourceRuntimeId"), - &format!("{field_name}.sourceRuntimeId"), - )?, - source_agent_id: require_legacy_optional_string( - get("sourceAgentId"), - &format!("{field_name}.sourceAgentId"), - )?, - source_role: require_legacy_optional_string( - get("sourceRole"), - &format!("{field_name}.sourceRole"), - )?, - tags: require_legacy_optional_string_array(get("tags"), &format!("{field_name}.tags"))?, - }) -} - -fn encode_raw_document_record(document: &CultNetRawDocumentRecord) -> rmpv::Value { - rmpv::Value::Map(vec![ - ( - rmpv::Value::from("schemaId"), - rmpv::Value::from(document.schema_id.as_str()), - ), - ( - rmpv::Value::from("recordKey"), - rmpv::Value::from(document.record_key.as_str()), - ), - ( - rmpv::Value::from("storedAt"), - rmpv::Value::from(document.stored_at.as_str()), - ), - ( - rmpv::Value::from("payloadEncoding"), - rmpv::Value::from(match document.payload_encoding { - CultNetRawPayloadEncoding::Messagepack => "messagepack", - }), - ), - ( - rmpv::Value::from("payload"), - rmpv::Value::Binary(document.payload.clone()), - ), - ( - rmpv::Value::from("sourceRuntimeId"), - document - .source_runtime_id - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("sourceAgentId"), - document - .source_agent_id - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("sourceRole"), - document - .source_role - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("tags"), - legacy_optional_string_array(document.tags.as_deref()), - ), - ]) -} - -fn parse_gamecult_networking_message(input: &rmpv::Value) -> Result { - let items = input.as_array().ok_or_else(|| { - anyhow!("gamecult.networking.v0 messages must be a 2-element union array") - })?; - if items.len() != 2 { - return Err(anyhow!( - "gamecult.networking.v0 messages must be a 2-element union array" - )); - } - let tag = items[0] - .as_i64() - .ok_or_else(|| anyhow!("gamecult.networking.v0 message tag must be an integer"))?; - let payload = items[1] - .as_array() - .ok_or_else(|| anyhow!("gamecult.networking.v0 message payload must be an array"))?; - match tag { - 0 => Ok(CultNetMessage::Login { - nonce: require_legacy_bytes(payload.first(), "LoginMessage.Nonce")?, - auth: require_legacy_bytes(payload.get(1), "LoginMessage.Auth")?, - password: require_legacy_bytes(payload.get(2), "LoginMessage.Password")?, - }), - 1 => Ok(CultNetMessage::Register { - nonce: require_legacy_bytes(payload.first(), "RegisterMessage.Nonce")?, - email: require_legacy_bytes(payload.get(1), "RegisterMessage.Email")?, - password: require_legacy_bytes(payload.get(2), "RegisterMessage.Password")?, - name: require_legacy_bytes(payload.get(3), "RegisterMessage.Name")?, - }), - 2 => Ok(CultNetMessage::Verify { - nonce: require_legacy_bytes(payload.first(), "VerifyMessage.Nonce")?, - session: require_legacy_bytes(payload.get(1), "VerifyMessage.Session")?, - }), - 3 => Ok(CultNetMessage::LoginSuccess { - nonce: require_legacy_bytes(payload.first(), "LoginSuccessMessage.Nonce")?, - session: require_legacy_bytes(payload.get(1), "LoginSuccessMessage.Session")?, - }), - 4 => Ok(CultNetMessage::Error { - error: require_legacy_string(payload.first(), "ErrorMessage.Error")?, - }), - 5 => Ok(CultNetMessage::SampleChangeName { - name: require_legacy_string(payload.first(), "ChangeNameMessage.Name")?, - }), - 6 => Ok(CultNetMessage::SampleChat { - text: require_legacy_string(payload.first(), "ChatMessage.Text")?, - }), - 7 => Ok(CultNetMessage::SchemaCatalogRequest { - message_id: require_legacy_string( - payload.first(), - "SchemaCatalogRequestMessage.MessageId", - )?, - include_schema_json: Some(require_legacy_bool( - payload.get(1), - "SchemaCatalogRequestMessage.IncludeSchemaJson", - )?), - schema_ids: require_legacy_optional_string_array( - payload.get(2), - "SchemaCatalogRequestMessage.SchemaIds", - )?, - kinds: require_legacy_optional_schema_kind_array( - payload.get(3), - "SchemaCatalogRequestMessage.Kinds", - )?, - }), - 8 => Ok(CultNetMessage::SchemaCatalogResponse { - message_id: require_legacy_string( - payload.first(), - "SchemaCatalogResponseMessage.MessageId", - )?, - schemas: require_legacy_schema_descriptor_array( - payload.get(1), - "SchemaCatalogResponseMessage.Schemas", - )?, - }), - _ => Err(anyhow!( - "Unsupported gamecult.networking.v0 union tag {tag}" - )), - } -} - -fn encode_gamecult_networking_message(message: &CultNetMessage) -> Result { - let pair = match message { - CultNetMessage::Login { - nonce, - auth, - password, - } => vec![ - rmpv::Value::from(0), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "LoginMessage.Nonce")?, - legacy_bytes(auth, "LoginMessage.Auth")?, - legacy_bytes(password, "LoginMessage.Password")?, - ]), - ], - CultNetMessage::Register { - nonce, - email, - password, - name, - } => vec![ - rmpv::Value::from(1), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "RegisterMessage.Nonce")?, - legacy_bytes(email, "RegisterMessage.Email")?, - legacy_bytes(password, "RegisterMessage.Password")?, - legacy_bytes(name, "RegisterMessage.Name")?, - ]), - ], - CultNetMessage::Verify { nonce, session } => vec![ - rmpv::Value::from(2), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "VerifyMessage.Nonce")?, - legacy_bytes(session, "VerifyMessage.Session")?, - ]), - ], - CultNetMessage::LoginSuccess { nonce, session } => vec![ - rmpv::Value::from(3), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "LoginSuccessMessage.Nonce")?, - legacy_bytes(session, "LoginSuccessMessage.Session")?, - ]), - ], - CultNetMessage::Error { error } => { - vec![ - rmpv::Value::from(4), - rmpv::Value::Array(vec![error.as_str().into()]), - ] - } - CultNetMessage::SampleChangeName { name } => { - vec![ - rmpv::Value::from(5), - rmpv::Value::Array(vec![name.as_str().into()]), - ] - } - CultNetMessage::SampleChat { text } => { - vec![ - rmpv::Value::from(6), - rmpv::Value::Array(vec![text.as_str().into()]), - ] - } - CultNetMessage::SchemaCatalogRequest { - message_id, - include_schema_json, - schema_ids, - kinds, - } => vec![ - rmpv::Value::from(7), - rmpv::Value::Array(vec![ - message_id.as_str().into(), - rmpv::Value::from(include_schema_json.unwrap_or(false)), - legacy_optional_string_array(schema_ids.as_deref()), - legacy_optional_schema_kind_array(kinds.as_deref()), - ]), - ], - CultNetMessage::SchemaCatalogResponse { - message_id, - schemas, - } => vec![ - rmpv::Value::from(8), - rmpv::Value::Array(vec![ - message_id.as_str().into(), - rmpv::Value::Array( - schemas - .iter() - .map(legacy_schema_descriptor) - .collect::>>()?, - ), - ]), - ], - _ => { - return Err(anyhow!( - "Message is not defined in the gamecult.networking.v0 contract" - )); - } - }; - Ok(rmpv::Value::Array(pair)) -} - -fn require_legacy_bytes(value: Option<&rmpv::Value>, field_name: &str) -> Result { - let bytes = value - .and_then(rmpv::Value::as_slice) - .ok_or_else(|| anyhow!("{field_name} must be binary data in gamecult.networking.v0"))?; - Ok(URL_SAFE_NO_PAD.encode(bytes)) -} - -fn require_legacy_string(value: Option<&rmpv::Value>, field_name: &str) -> Result { - let value = value - .and_then(rmpv::Value::as_str) - .ok_or_else(|| anyhow!("{field_name} must be a string in gamecult.networking.v0"))?; - Ok(value.to_string()) -} - -fn require_legacy_bool(value: Option<&rmpv::Value>, field_name: &str) -> Result { - value - .and_then(rmpv::Value::as_bool) - .ok_or_else(|| anyhow!("{field_name} must be a boolean in gamecult.networking.v0")) -} - -fn require_legacy_optional_string_array( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result>> { - let Some(value) = value else { - return Ok(None); - }; - if value.is_nil() { - return Ok(None); - } - let items = value - .as_array() - .ok_or_else(|| anyhow!("{field_name} must be an array in gamecult.networking.v0"))?; - let values = items - .iter() - .map(|item| { - item.as_str() - .map(ToString::to_string) - .ok_or_else(|| anyhow!("{field_name} must contain only strings")) - }) - .collect::>>()?; - Ok(Some(values)) -} - -fn require_legacy_optional_schema_kind_array( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result>> { - let Some(values) = require_legacy_optional_string_array(value, field_name)? else { - return Ok(None); - }; - values - .into_iter() - .map(|value| match value.as_str() { - "wire_message" => Ok(CultNetSchemaKind::WireMessage), - "document_payload" => Ok(CultNetSchemaKind::DocumentPayload), - "shared_contract" => Ok(CultNetSchemaKind::SharedContract), - _ => Err(anyhow!("{field_name} has unsupported schema kind {value}")), - }) - .collect::>>() - .map(Some) -} - -fn require_legacy_schema_descriptor_array( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result> { - let items = value - .and_then(rmpv::Value::as_array) - .ok_or_else(|| anyhow!("{field_name} must be an array in gamecult.networking.v0"))?; - items - .iter() - .enumerate() - .map(|(index, item)| { - require_legacy_schema_descriptor(item, &format!("{field_name}[{index}]")) - }) - .collect() -} - -fn require_legacy_schema_descriptor( - value: &rmpv::Value, - field_name: &str, -) -> Result { - let object = value - .as_map() - .ok_or_else(|| anyhow!("{field_name} must be an object in gamecult.networking.v0"))?; - - let get = |name: &str| -> Option<&rmpv::Value> { - object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == name) - .map(|_| value) - }) - }; - - let schema_id = require_legacy_string(get("schemaId"), &format!("{field_name}.SchemaId"))?; - let kind = require_legacy_string(get("kind"), &format!("{field_name}.Kind"))?; - let schema_version = require_legacy_optional_string( - get("schemaVersion"), - &format!("{field_name}.SchemaVersion"), - )?; - let document_type = - require_legacy_optional_string(get("documentType"), &format!("{field_name}.DocumentType"))?; - let title = require_legacy_optional_string(get("title"), &format!("{field_name}.Title"))?; - let wire_contracts = require_legacy_optional_wire_contracts( - get("wireContracts"), - &format!("{field_name}.WireContracts"), - )?; - let content_hash = - require_legacy_string(get("contentHash"), &format!("{field_name}.ContentHash"))?; - let schema_json = - require_legacy_optional_string(get("schemaJson"), &format!("{field_name}.SchemaJson"))?; - - let kind = match kind.as_str() { - "wire_message" => CultNetSchemaKind::WireMessage, - "document_payload" => CultNetSchemaKind::DocumentPayload, - "shared_contract" => CultNetSchemaKind::SharedContract, - _ => { - return Err(anyhow!( - "{field_name}.Kind has unsupported schema kind {kind}" - )); - } - }; - - Ok(CultNetSchemaDescriptor { - schema_id, - kind, - schema_version, - document_type, - title, - wire_contracts, - content_hash, - schema_json, - }) -} - -fn require_legacy_optional_string( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result> { - let Some(value) = value else { - return Ok(None); - }; - if value.is_nil() { - return Ok(None); - } - Ok(Some(require_legacy_string(Some(value), field_name)?)) -} - -fn require_legacy_optional_wire_contracts( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result> { - let Some(values) = require_legacy_optional_string_array(value, field_name)? else { - return Ok(Vec::new()); - }; - - values - .into_iter() - .map(|value| match value.as_str() { - "cultnet.schema.v0" => Ok(CultNetWireContract::CultNetSchemaV0), - "gamecult.networking.v0" => Ok(CultNetWireContract::GameCultNetworkingV0), - _ => Err(anyhow!( - "{field_name} contains unsupported wire contract {value}" - )), - }) - .collect() -} - -fn legacy_bytes(input: &str, field_name: &str) -> Result { - if input.trim().is_empty() { - return Err(anyhow!("{field_name} must be a non-empty base64url string")); - } - Ok(rmpv::Value::Binary(URL_SAFE_NO_PAD.decode(input)?)) -} - -fn legacy_optional_string_array(input: Option<&[String]>) -> rmpv::Value { - match input { - Some(values) => rmpv::Value::Array( - values - .iter() - .map(|value| rmpv::Value::from(value.as_str())) - .collect(), - ), - None => rmpv::Value::Nil, - } -} - -fn legacy_optional_schema_kind_array(input: Option<&[CultNetSchemaKind]>) -> rmpv::Value { - match input { - Some(values) => rmpv::Value::Array( - values - .iter() - .map(|value| { - rmpv::Value::from(match value { - CultNetSchemaKind::WireMessage => "wire_message", - CultNetSchemaKind::DocumentPayload => "document_payload", - CultNetSchemaKind::SharedContract => "shared_contract", - }) - }) - .collect(), - ), - None => rmpv::Value::Nil, - } -} - -fn legacy_schema_descriptor(schema: &CultNetSchemaDescriptor) -> Result { - Ok(rmpv::Value::Map(vec![ - ( - rmpv::Value::from("schemaId"), - rmpv::Value::from(schema.schema_id.as_str()), - ), - ( - rmpv::Value::from("kind"), - rmpv::Value::from(match schema.kind { - CultNetSchemaKind::WireMessage => "wire_message", - CultNetSchemaKind::DocumentPayload => "document_payload", - CultNetSchemaKind::SharedContract => "shared_contract", - }), - ), - ( - rmpv::Value::from("schemaVersion"), - schema - .schema_version - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("documentType"), - schema - .document_type - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("title"), - schema - .title - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("wireContracts"), - rmpv::Value::Array( - schema - .wire_contracts - .iter() - .map(|value| { - rmpv::Value::from(match value { - CultNetWireContract::CultNetSchemaV0 => "cultnet.schema.v0", - CultNetWireContract::GameCultNetworkingV0 => "gamecult.networking.v0", - }) - }) - .collect(), - ), - ), - ( - rmpv::Value::from("contentHash"), - rmpv::Value::from(schema.content_hash.as_str()), - ), - ( - rmpv::Value::from("schemaJson"), - schema - .schema_json - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ])) -} diff --git a/vendor/cultnet-rs/src/cultmesh.rs b/vendor/cultnet-rs/src/cultmesh.rs deleted file mode 100644 index 20d2123..0000000 --- a/vendor/cultnet-rs/src/cultmesh.rs +++ /dev/null @@ -1,525 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use chrono::DateTime; -use chrono::Utc; -use std::collections::BTreeMap; -use std::net::SocketAddr; -use std::net::ToSocketAddrs; -use std::net::UdpSocket; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -use crate::CultNetRudpSocketMode; -use crate::CultNetRudpSocketTransportConnection; -use crate::CultNetRudpSocketTransportOptions; -use crate::CultNetSchemaRegistry; -use crate::CultNetShardCatalog; -use crate::builtin_schema_registry; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshPeerCard { - pub peer_id: String, - pub verse_id: String, - pub endpoints: Vec, - pub roles: Vec, - pub shard_ids: Vec, - pub authority_lease_id: Option, -} - -impl CultMeshPeerCard { - pub fn new( - peer_id: impl Into, - verse_id: impl Into, - endpoints: impl IntoIterator>, - ) -> Self { - Self { - peer_id: peer_id.into(), - verse_id: verse_id.into(), - endpoints: endpoints.into_iter().map(Into::into).collect(), - roles: Vec::new(), - shard_ids: Vec::new(), - authority_lease_id: None, - } - } - - pub fn with_roles(mut self, roles: impl IntoIterator>) -> Self { - self.roles = roles.into_iter().map(Into::into).collect(); - self - } - - pub fn with_shard_ids( - mut self, - shard_ids: impl IntoIterator>, - ) -> Self { - self.shard_ids = shard_ids.into_iter().map(Into::into).collect(); - self - } - - pub fn with_authority_lease_id(mut self, authority_lease_id: impl Into) -> Self { - self.authority_lease_id = Some(authority_lease_id.into()); - self - } - - pub fn rudp_endpoint(&self) -> Result { - let endpoint = self - .endpoints - .iter() - .find(|endpoint| endpoint.to_ascii_lowercase().starts_with("rudp://")) - .ok_or_else(|| anyhow!("Peer {} does not advertise a RUDP endpoint", self.peer_id))?; - CultMesh::parse_rudp_endpoint(endpoint) - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultMeshPeerCatalog { - peers: BTreeMap, -} - -impl CultMeshPeerCatalog { - pub fn new() -> Self { - Self::default() - } - - pub fn upsert(&mut self, peer: CultMeshPeerCard) -> Result<()> { - require_non_empty(&peer.peer_id, "peer.peer_id")?; - require_non_empty(&peer.verse_id, "peer.verse_id")?; - self.peers.insert(peer.peer_id.clone(), peer); - Ok(()) - } - - pub fn get(&self, peer_id: &str) -> Option<&CultMeshPeerCard> { - self.peers.get(peer_id) - } - - pub fn peers(&self) -> Vec { - self.peers.values().cloned().collect() - } - - pub fn find(&self, verse_id: &str, role: Option<&str>) -> Vec { - self.peers - .values() - .filter(|peer| { - peer.verse_id == verse_id - && role.is_none_or(|role| peer.roles.iter().any(|value| value == role)) - }) - .cloned() - .collect() - } - - pub fn find_authorized( - &self, - verse_id: &str, - role: &str, - leases: &CultMeshAuthorityLeaseCatalog, - shard_id: Option<&str>, - at: DateTime, - ) -> Vec { - self.find(verse_id, Some(role)) - .into_iter() - .filter(|peer| leases.is_authorized(peer, role, shard_id, at)) - .collect() - } - - pub fn first_authorized( - &self, - verse_id: &str, - role: &str, - leases: &CultMeshAuthorityLeaseCatalog, - shard_id: Option<&str>, - at: DateTime, - ) -> Option { - self.find_authorized(verse_id, role, leases, shard_id, at) - .into_iter() - .next() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshAuthorityLease { - pub lease_id: String, - pub verse_id: String, - pub peer_id: String, - pub roles: Vec, - pub shard_ids: Vec, - pub issuer_runtime_id: Option, - pub valid_from: DateTime, - pub expires_at: DateTime, -} - -impl CultMeshAuthorityLease { - pub fn covers( - &self, - peer: &CultMeshPeerCard, - role: &str, - shard_id: Option<&str>, - at: DateTime, - ) -> bool { - at >= self.valid_from - && at < self.expires_at - && self.verse_id == peer.verse_id - && self.peer_id == peer.peer_id - && self.roles.iter().any(|value| value == role) - && peer.roles.iter().any(|value| value == role) - && shard_id.is_none_or(|shard_id| { - self.shard_ids.is_empty() || self.shard_ids.iter().any(|value| value == shard_id) - }) - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultMeshAuthorityLeaseCatalog { - leases: BTreeMap, -} - -impl CultMeshAuthorityLeaseCatalog { - pub fn new() -> Self { - Self::default() - } - - pub fn upsert(&mut self, lease: CultMeshAuthorityLease) -> Result<()> { - require_non_empty(&lease.lease_id, "lease.lease_id")?; - require_non_empty(&lease.verse_id, "lease.verse_id")?; - require_non_empty(&lease.peer_id, "lease.peer_id")?; - if lease.expires_at <= lease.valid_from { - return Err(anyhow!( - "CultMesh authority lease expiry must be after valid_from" - )); - } - self.leases.insert(lease.lease_id.clone(), lease); - Ok(()) - } - - pub fn get(&self, lease_id: &str) -> Option<&CultMeshAuthorityLease> { - self.leases.get(lease_id) - } - - pub fn leases(&self) -> Vec { - self.leases.values().cloned().collect() - } - - pub fn is_authorized( - &self, - peer: &CultMeshPeerCard, - role: &str, - shard_id: Option<&str>, - at: DateTime, - ) -> bool { - let Some(lease_id) = peer.authority_lease_id.as_deref() else { - return false; - }; - self.leases - .get(lease_id) - .is_some_and(|lease| lease.covers(peer, role, shard_id, at)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpEndpoint { - pub host: String, - pub port: u16, -} - -impl CultNetRudpEndpoint { - pub fn uri(&self) -> String { - if self.host.contains(':') && !self.host.starts_with('[') { - format!("rudp://[{}]:{}", self.host, self.port) - } else { - format!("rudp://{}:{}", self.host, self.port) - } - } - - pub fn socket_addr(&self) -> Result { - format!("{}:{}", socket_host(&self.host), self.port) - .to_socket_addrs() - .map_err(|error| anyhow!("Invalid RUDP socket endpoint {}: {error}", self.uri()))? - .next() - .ok_or_else(|| anyhow!("RUDP socket endpoint {} did not resolve", self.uri())) - } -} - -#[derive(Clone, Debug)] -pub struct CultMeshRudpSocketOptions { - pub bind_host: String, - pub bind_port: u16, - pub read_timeout: Option, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, -} - -impl Default for CultMeshRudpSocketOptions { - fn default() -> Self { - Self { - bind_host: "127.0.0.1".to_string(), - bind_port: 0, - read_timeout: Some(Duration::from_millis(20)), - initial_sequence: 1, - resend_delay_ms: 250, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - } - } -} - -#[derive(Clone, Debug)] -pub struct CultMeshRudpClientOptions { - pub socket_options: CultMeshRudpSocketOptions, - pub connect_payload: Vec, - pub connect_timeout: Duration, - pub poll_interval: Duration, -} - -impl Default for CultMeshRudpClientOptions { - fn default() -> Self { - Self { - socket_options: CultMeshRudpSocketOptions::default(), - connect_payload: Vec::new(), - connect_timeout: Duration::from_secs(1), - poll_interval: Duration::from_millis(5), - } - } -} - -pub struct CultMesh; - -impl CultMesh { - pub fn create_schema_registry() -> CultNetSchemaRegistry { - CultNetSchemaRegistry::new() - } - - pub fn create_builtin_schema_registry() -> Result { - builtin_schema_registry() - } - - pub fn create_shard_catalog() -> CultNetShardCatalog { - CultNetShardCatalog::new() - } - - pub fn create_peer_catalog() -> CultMeshPeerCatalog { - CultMeshPeerCatalog::new() - } - - pub fn create_authority_lease_catalog() -> CultMeshAuthorityLeaseCatalog { - CultMeshAuthorityLeaseCatalog::new() - } - - pub fn parse_rudp_endpoint(endpoint: &str) -> Result { - cultnet_rudp_endpoint(endpoint) - } - - pub fn create_rudp_server( - runtime_id: impl Into, - connection_id: u32, - options: CultMeshRudpSocketOptions, - ) -> Result { - let socket = bind_rudp_socket(&options)?; - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - transport_id: None, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: None, - }) - } - - pub fn create_rudp_client( - runtime_id: impl Into, - connection_id: u32, - endpoint: &CultNetRudpEndpoint, - options: CultMeshRudpSocketOptions, - ) -> Result { - let socket = bind_rudp_socket(&options)?; - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(endpoint.socket_addr()?), - connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - transport_id: None, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: None, - }) - } - - pub fn create_rudp_client_for_endpoint( - runtime_id: impl Into, - connection_id: u32, - endpoint: &str, - options: CultMeshRudpSocketOptions, - ) -> Result { - let endpoint = Self::parse_rudp_endpoint(endpoint)?; - Self::create_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn create_rudp_client_for_peer( - runtime_id: impl Into, - connection_id: u32, - peer: &CultMeshPeerCard, - options: CultMeshRudpSocketOptions, - ) -> Result { - let endpoint = peer.rudp_endpoint()?; - Self::create_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn create_rudp_client_for_authorized_peer( - runtime_id: impl Into, - connection_id: u32, - peers: &CultMeshPeerCatalog, - leases: &CultMeshAuthorityLeaseCatalog, - verse_id: &str, - role: &str, - shard_id: Option<&str>, - at: DateTime, - options: CultMeshRudpSocketOptions, - ) -> Result { - let peer = peers - .first_authorized(verse_id, role, leases, shard_id, at) - .ok_or_else(|| { - anyhow!("No authorized RUDP peer for role {role} in Verse {verse_id}") - })?; - Self::create_rudp_client_for_peer(runtime_id, connection_id, &peer, options) - } - - pub fn connect_rudp_client( - runtime_id: impl Into, - connection_id: u32, - endpoint: &CultNetRudpEndpoint, - options: CultMeshRudpClientOptions, - ) -> Result { - let runtime_id = runtime_id.into(); - let mut client = Self::create_rudp_client( - runtime_id.clone(), - connection_id, - endpoint, - options.socket_options, - )?; - client.connect(options.connect_payload)?; - let deadline = Instant::now() + options.connect_timeout; - while Instant::now() < deadline { - let _ = client.receive_once()?; - client.poll_resends()?; - if client.connected() { - return Ok(client); - } - thread::sleep(options.poll_interval); - } - if client.connected() { - return Ok(client); - } - Err(anyhow!( - "Timed out waiting for RUDP client {runtime_id} to connect" - )) - } - - pub fn connect_rudp_client_for_endpoint( - runtime_id: impl Into, - connection_id: u32, - endpoint: &str, - options: CultMeshRudpClientOptions, - ) -> Result { - let endpoint = Self::parse_rudp_endpoint(endpoint)?; - Self::connect_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn connect_rudp_client_for_peer( - runtime_id: impl Into, - connection_id: u32, - peer: &CultMeshPeerCard, - options: CultMeshRudpClientOptions, - ) -> Result { - let endpoint = peer.rudp_endpoint()?; - Self::connect_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn connect_rudp_client_for_authorized_peer( - runtime_id: impl Into, - connection_id: u32, - peers: &CultMeshPeerCatalog, - leases: &CultMeshAuthorityLeaseCatalog, - verse_id: &str, - role: &str, - shard_id: Option<&str>, - at: DateTime, - options: CultMeshRudpClientOptions, - ) -> Result { - let peer = peers - .first_authorized(verse_id, role, leases, shard_id, at) - .ok_or_else(|| { - anyhow!("No authorized RUDP peer for role {role} in Verse {verse_id}") - })?; - Self::connect_rudp_client_for_peer(runtime_id, connection_id, &peer, options) - } -} - -pub fn cultnet_rudp_endpoint(endpoint: &str) -> Result { - let scheme = "rudp://"; - if endpoint.len() < scheme.len() || !endpoint[..scheme.len()].eq_ignore_ascii_case(scheme) { - return Err(anyhow!("RUDP endpoint must use the rudp scheme")); - } - let rest = &endpoint[scheme.len()..]; - let (host, port_text) = if let Some(rest) = rest.strip_prefix('[') { - let (host, port_rest) = rest - .split_once(']') - .ok_or_else(|| anyhow!("RUDP IPv6 endpoint must close the host bracket"))?; - let port_text = port_rest - .strip_prefix(':') - .ok_or_else(|| anyhow!("RUDP endpoint must include a valid port"))?; - (host.to_string(), port_text) - } else { - let (host, port_text) = rest - .rsplit_once(':') - .ok_or_else(|| anyhow!("RUDP endpoint must include a host and port"))?; - if host.contains(':') { - return Err(anyhow!("RUDP IPv6 endpoint host must be bracketed")); - } - (host.to_string(), port_text) - }; - require_non_empty(&host, "rudp host")?; - let port = port_text - .parse::() - .map_err(|error| anyhow!("RUDP endpoint must include a valid port: {error}"))?; - if port == 0 { - return Err(anyhow!("RUDP endpoint port must be greater than zero")); - } - Ok(CultNetRudpEndpoint { host, port }) -} - -fn bind_rudp_socket(options: &CultMeshRudpSocketOptions) -> Result { - let socket = UdpSocket::bind(format!( - "{}:{}", - socket_host(&options.bind_host), - options.bind_port - ))?; - socket.set_read_timeout(options.read_timeout)?; - Ok(socket) -} - -fn socket_host(host: &str) -> String { - if host.contains(':') && !host.starts_with('[') { - format!("[{host}]") - } else { - host.to_string() - } -} - -fn require_non_empty(value: &str, field: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("CultMesh {field} must not be empty")); - } - Ok(()) -} diff --git a/vendor/cultnet-rs/src/framing.rs b/vendor/cultnet-rs/src/framing.rs deleted file mode 100644 index 0157881..0000000 --- a/vendor/cultnet-rs/src/framing.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; - -pub const FRAME_HEADER_BYTES: usize = 4; - -pub fn encode_frame(payload: &[u8]) -> Result> { - let len: u32 = payload - .len() - .try_into() - .map_err(|_| anyhow!("CultNet frame payload is too large"))?; - let mut frame = Vec::with_capacity(FRAME_HEADER_BYTES + payload.len()); - frame.extend_from_slice(&len.to_be_bytes()); - frame.extend_from_slice(payload); - Ok(frame) -} - -#[derive(Clone, Debug, Default)] -pub struct LengthPrefixedMessageFramer { - buffer: Vec, -} - -impl LengthPrefixedMessageFramer { - pub fn new() -> Self { - Self::default() - } - - pub fn push(&mut self, chunk: impl AsRef<[u8]>) -> Vec> { - self.buffer.extend_from_slice(chunk.as_ref()); - let mut frames = Vec::new(); - loop { - if self.buffer.len() < FRAME_HEADER_BYTES { - break; - } - let payload_len = u32::from_be_bytes([ - self.buffer[0], - self.buffer[1], - self.buffer[2], - self.buffer[3], - ]) as usize; - let total_len = FRAME_HEADER_BYTES + payload_len; - if self.buffer.len() < total_len { - break; - } - frames.push(self.buffer[FRAME_HEADER_BYTES..total_len].to_vec()); - self.buffer.drain(0..total_len); - } - frames - } -} diff --git a/vendor/cultnet-rs/src/lib.rs b/vendor/cultnet-rs/src/lib.rs deleted file mode 100644 index c13c4e8..0000000 --- a/vendor/cultnet-rs/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -mod contracts; -mod cultmesh; -mod framing; -mod operation_service; -mod provider_session; -mod replication; -mod rudp; -mod schema_discovery; -mod security; -mod service_identity; -mod shard_catalog; -mod transport; - -pub use contracts::*; -pub use cultmesh::*; -pub use framing::*; -pub use operation_service::*; -pub use provider_session::*; -pub use replication::*; -pub use rudp::*; -pub use schema_discovery::*; -pub use security::*; -pub use service_identity::*; -pub use shard_catalog::*; -pub use transport::*; diff --git a/vendor/cultnet-rs/src/operation_service.rs b/vendor/cultnet-rs/src/operation_service.rs deleted file mode 100644 index 84d60cf..0000000 --- a/vendor/cultnet-rs/src/operation_service.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::net::UdpSocket; -use std::time::Duration; - -use anyhow::{Result, anyhow}; - -use crate::{ - CultNetMessage, CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions, -}; - -pub const CULTNET_OPERATION_CONNECTION_ID: u32 = 0x4355_4c54; - -pub struct CultNetOperationServer { - transport: CultNetRudpSocketTransportConnection, - endpoint: String, -} - -impl CultNetOperationServer { - pub fn bind(runtime_id: impl Into, host: &str, port: u16) -> Result { - let socket = UdpSocket::bind((host, port))?; - socket.set_read_timeout(Some(Duration::from_millis(25)))?; - let address = socket.local_addr()?; - let mut options = CultNetRudpSocketTransportOptions::server( - runtime_id, - socket, - CULTNET_OPERATION_CONNECTION_ID, - ); - options.max_fragment_bytes = Some(2048); - Ok(Self { - transport: CultNetRudpSocketTransportConnection::new(options)?, - endpoint: format!("rudp://{}:{}", address.ip(), address.port()), - }) - } - - pub fn endpoint(&self) -> &str { - &self.endpoint - } - - pub fn serve_once( - &mut self, - mut handler: impl FnMut(CultNetMessage) -> Result, - ) -> Result { - let Some(request) = self.transport.receive_schema_message_once()? else { - self.transport.poll_resends()?; - return Ok(false); - }; - if !matches!(request, CultNetMessage::OperationRequest { .. }) { - return Err(anyhow!( - "CultNet operation service received a non-operation message" - )); - } - let response = handler(request)?; - if !matches!(response, CultNetMessage::OperationResponse { .. }) { - return Err(anyhow!( - "CultNet operation handler returned a non-operation response" - )); - } - self.transport.send_schema_message(&response)?; - Ok(true) - } -} diff --git a/vendor/cultnet-rs/src/provider_session.rs b/vendor/cultnet-rs/src/provider_session.rs deleted file mode 100644 index 9965b0e..0000000 --- a/vendor/cultnet-rs/src/provider_session.rs +++ /dev/null @@ -1,625 +0,0 @@ -use anyhow::{Result, anyhow}; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use chrono::DateTime; -use rmpv::Value; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; - -use crate::{CultNetMessage, CultNetRawDocumentRecord}; - -pub const CULTMESH_PROVIDER_SESSION_SERVICE_ID: &str = "gamecult.mesh.provider_session"; -pub const CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING: &str = "messagepack-base64"; - -pub const CULTMESH_PROVIDER_REGISTER_OPERATION: &str = "provider.register"; -pub const CULTMESH_PROVIDER_RENEW_OPERATION: &str = "provider.renew"; -pub const CULTMESH_PROVIDER_PUBLICATION_PUT_OPERATION: &str = "provider.publication.put"; -pub const CULTMESH_PROVIDER_PUBLICATION_DELETE_OPERATION: &str = "provider.publication.delete"; -pub const CULTMESH_PROVIDER_RECEIPT_PUT_OPERATION: &str = "provider.receipt.put"; -pub const CULTMESH_PROVIDER_WITHDRAW_OPERATION: &str = "provider.withdraw"; - -pub const CULTMESH_PROVIDER_REGISTRATION_SCHEMA: &str = "gamecult.mesh.provider_registration.v1"; -pub const CULTMESH_PROVIDER_LEASE_SCHEMA: &str = "gamecult.mesh.provider_lease.v1"; -pub const CULTMESH_PROVIDER_LEASE_RENEWAL_SCHEMA: &str = "gamecult.mesh.provider_lease_renewal.v1"; -pub const CULTMESH_PROVIDER_PUBLICATION_PUT_SCHEMA: &str = - "gamecult.mesh.provider_publication_put.v1"; -pub const CULTMESH_PROVIDER_PUBLICATION_DELETE_SCHEMA: &str = - "gamecult.mesh.provider_publication_delete.v1"; -pub const CULTMESH_PROVIDER_COMMAND_SCHEMA: &str = "gamecult.mesh.provider_command.v1"; -pub const CULTMESH_PROVIDER_RECEIPT_PUT_SCHEMA: &str = "gamecult.mesh.provider_receipt_put.v1"; -pub const CULTMESH_PROVIDER_WITHDRAWAL_SCHEMA: &str = "gamecult.mesh.provider_withdrawal.v1"; -pub const CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA: &str = - "gamecult.mesh.provider_mutation_acceptance.v1"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshProviderOperationStatus { - Ok, - Conflict, - Expired, - Denied, - Invalid, -} - -impl CultMeshProviderOperationStatus { - pub fn as_str(self) -> &'static str { - match self { - Self::Ok => "ok", - Self::Conflict => "conflict", - Self::Expired => "expired", - Self::Denied => "denied", - Self::Invalid => "invalid", - } - } - - pub fn parse(value: &str) -> Result { - match value { - "ok" => Ok(Self::Ok), - "conflict" => Ok(Self::Conflict), - "expired" => Ok(Self::Expired), - "denied" => Ok(Self::Denied), - "invalid" => Ok(Self::Invalid), - _ => Err(anyhow!("Unsupported provider-session status {value:?}")), - } - } -} - -pub trait CultMeshProviderSessionPayload { - fn validate(&self) -> Result<()>; -} - -/// Provider-owned evidence carried verbatim in the RUDP Connect payload. -/// `client_session_id` distinguishes process/session generations; the optional -/// token remains opaque authorization evidence for the receiving service. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderConnectEvidenceWire { - pub client_session_id: String, - #[serde(default)] - pub session_token: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderConnectEvidenceWire { - fn validate(&self) -> Result<()> { - require_text(&self.client_session_id, "clientSessionId")?; - require_optional_text(self.session_token.as_deref(), "sessionToken") - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderRegistrationWire { - pub provider_id: String, - pub service_instance_id: String, - pub endpoint_id: String, - pub verse_id: String, - pub requested_lease_duration_ms: i32, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub authority_lease_id: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderRegistrationWire { - fn validate(&self) -> Result<()> { - require_identity( - &self.provider_id, - &self.service_instance_id, - &self.endpoint_id, - &self.verse_id, - )?; - require_positive(self.requested_lease_duration_ms, "requestedLeaseDurationMs")?; - require_optional_text(self.authority_lease_id.as_deref(), "authorityLeaseId") - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderLeaseWire { - pub provider_id: String, - pub service_instance_id: String, - pub endpoint_id: String, - pub verse_id: String, - pub lease_id: String, - pub valid_from_utc: String, - pub expires_at_utc: String, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderLeaseWire { - fn validate(&self) -> Result<()> { - require_identity( - &self.provider_id, - &self.service_instance_id, - &self.endpoint_id, - &self.verse_id, - )?; - require_text(&self.lease_id, "leaseId")?; - let valid_from = require_timestamp(&self.valid_from_utc, "validFromUtc")?; - let expires_at = require_timestamp(&self.expires_at_utc, "expiresAtUtc")?; - if expires_at <= valid_from { - return Err(anyhow!("Provider lease must expire after validFromUtc")); - } - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderLeaseRenewalWire { - pub lease_id: String, - pub requested_lease_duration_ms: i32, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderLeaseRenewalWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - require_positive(self.requested_lease_duration_ms, "requestedLeaseDurationMs") - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderPublicationPutWire { - pub lease_id: String, - pub publication_id: String, - pub document: CultNetRawDocumentRecord, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderPublicationPutWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - require_text(&self.publication_id, "publicationId")?; - validate_raw_document(&self.document) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderPublicationDeleteWire { - pub lease_id: String, - pub publication_id: String, - pub schema_id: String, - pub record_key: String, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderPublicationDeleteWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - require_text(&self.publication_id, "publicationId")?; - require_text(&self.schema_id, "schemaId")?; - require_text(&self.record_key, "recordKey") - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderCommandWire { - pub command_id: String, - pub command_kind: String, - pub provider_id: String, - pub service_instance_id: String, - pub payload: Value, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderCommandWire { - fn validate(&self) -> Result<()> { - require_text(&self.command_id, "commandId")?; - require_text(&self.command_kind, "commandKind")?; - require_text(&self.provider_id, "providerId")?; - require_text(&self.service_instance_id, "serviceInstanceId") - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum CultMeshProviderReceiptStateWire { - Applied, - Rejected, - Failed, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderCommandReceiptWire { - pub receipt_id: String, - pub command_id: String, - pub command_kind: String, - pub provider_id: String, - pub service_instance_id: String, - pub state: CultMeshProviderReceiptStateWire, - pub completed_at_utc: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub result: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderCommandReceiptWire { - fn validate(&self) -> Result<()> { - require_text(&self.receipt_id, "receiptId")?; - require_text(&self.command_id, "commandId")?; - require_text(&self.command_kind, "commandKind")?; - require_text(&self.provider_id, "providerId")?; - require_text(&self.service_instance_id, "serviceInstanceId")?; - require_timestamp(&self.completed_at_utc, "completedAtUtc")?; - require_optional_text(self.error.as_deref(), "error") - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderReceiptPutWire { - pub lease_id: String, - pub receipt: CultMeshProviderCommandReceiptWire, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderReceiptPutWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - self.receipt.validate() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderWithdrawalWire { - pub lease_id: String, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderWithdrawalWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId") - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderMutationAcceptanceWire { - pub accepted_at_utc: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lease_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub publication_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub command_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub receipt_id: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderMutationAcceptanceWire { - fn validate(&self) -> Result<()> { - require_timestamp(&self.accepted_at_utc, "acceptedAtUtc")?; - require_optional_text(self.lease_id.as_deref(), "leaseId")?; - require_optional_text(self.publication_id.as_deref(), "publicationId")?; - require_optional_text(self.command_id.as_deref(), "commandId")?; - require_optional_text(self.receipt_id.as_deref(), "receiptId") - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CultMeshProviderErrorWire {} - -impl CultMeshProviderSessionPayload for CultMeshProviderErrorWire { - fn validate(&self) -> Result<()> { - Ok(()) - } -} - -pub fn encode_provider_session_payload(value: &T) -> Result -where - T: CultMeshProviderSessionPayload + Serialize, -{ - value.validate()?; - Ok(STANDARD.encode(rmp_serde::to_vec_named(value)?)) -} - -pub fn encode_provider_connect_evidence( - evidence: &CultMeshProviderConnectEvidenceWire, -) -> Result> { - evidence.validate()?; - Ok(rmp_serde::to_vec_named(evidence)?) -} - -pub fn decode_provider_connect_evidence( - payload: &[u8], -) -> Result { - if payload.is_empty() { - return Err(anyhow!( - "Provider Connect evidence must be a non-empty MessagePack map" - )); - } - let evidence: CultMeshProviderConnectEvidenceWire = rmp_serde::from_slice(payload)?; - evidence.validate()?; - Ok(evidence) -} - -pub fn decode_provider_session_payload(payload: &str) -> Result -where - T: CultMeshProviderSessionPayload + DeserializeOwned, -{ - require_text(payload, "payload")?; - let bytes = STANDARD - .decode(payload) - .map_err(|error| anyhow!("Provider-session payload must be MessagePack base64: {error}"))?; - let value: T = rmp_serde::from_slice(&bytes)?; - value.validate()?; - Ok(value) -} - -pub fn decode_provider_command_document( - document: &CultNetRawDocumentRecord, -) -> Result { - if document.schema_id != CULTMESH_PROVIDER_COMMAND_SCHEMA { - return Err(anyhow!( - "Expected provider command schema {:?}, received {:?}", - CULTMESH_PROVIDER_COMMAND_SCHEMA, - document.schema_id - )); - } - let command: CultMeshProviderCommandWire = rmp_serde::from_slice(&document.payload)?; - command.validate()?; - Ok(command) -} - -pub fn create_provider_session_request( - message_id: impl Into, - operation: impl Into, - payload_schema: impl Into, - payload: &T, - source_runtime_id: Option, - target_runtime_id: Option, -) -> Result -where - T: CultMeshProviderSessionPayload + Serialize, -{ - let message_id = message_id.into(); - let operation = operation.into(); - let payload_schema = payload_schema.into(); - require_text(&message_id, "messageId")?; - validate_operation_schema(&operation, &payload_schema)?; - Ok(CultNetMessage::OperationRequest { - message_id, - service_id: CULTMESH_PROVIDER_SESSION_SERVICE_ID.to_string(), - operation, - payload_schema, - payload_encoding: CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING.to_string(), - payload: encode_provider_session_payload(payload)?, - source_runtime_id, - target_runtime_id, - }) -} - -pub fn decode_provider_session_request( - request: &CultNetMessage, - expected_operation: &str, - expected_payload_schema: &str, -) -> Result -where - T: CultMeshProviderSessionPayload + DeserializeOwned, -{ - let CultNetMessage::OperationRequest { - service_id, - operation, - payload_schema, - payload_encoding, - payload, - .. - } = request - else { - return Err(anyhow!("Expected a CultNet operation request")); - }; - require_envelope( - service_id, - operation, - payload_schema, - payload_encoding, - expected_operation, - expected_payload_schema, - )?; - decode_provider_session_payload(payload) -} - -pub fn create_provider_session_response( - request: &CultNetMessage, - status: CultMeshProviderOperationStatus, - payload_schema: impl Into, - payload: &T, - source_runtime_id: Option, -) -> Result -where - T: CultMeshProviderSessionPayload + Serialize, -{ - let CultNetMessage::OperationRequest { - message_id, - operation, - .. - } = request - else { - return Err(anyhow!("Expected a CultNet operation request")); - }; - require_text(message_id, "messageId")?; - require_text(operation, "operation")?; - let payload_schema = payload_schema.into(); - require_text(&payload_schema, "payloadSchema")?; - Ok(CultNetMessage::OperationResponse { - message_id: message_id.clone(), - service_id: CULTMESH_PROVIDER_SESSION_SERVICE_ID.to_string(), - operation: operation.clone(), - status: status.as_str().to_string(), - payload_schema, - payload_encoding: CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING.to_string(), - payload: encode_provider_session_payload(payload)?, - diagnostics: Vec::new(), - source_runtime_id, - }) -} - -pub fn create_provider_session_error_response( - request: &CultNetMessage, - status: CultMeshProviderOperationStatus, - diagnostics: Vec, - source_runtime_id: Option, -) -> Result { - if status == CultMeshProviderOperationStatus::Ok { - return Err(anyhow!( - "Provider-session error responses require a non-ok status" - )); - } - if diagnostics.is_empty() { - return Err(anyhow!( - "Provider-session error responses require diagnostics" - )); - } - for diagnostic in &diagnostics { - require_text(diagnostic, "diagnostics[]")?; - } - let mut response = create_provider_session_response( - request, - status, - CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA, - &CultMeshProviderErrorWire {}, - source_runtime_id, - )?; - let CultNetMessage::OperationResponse { - diagnostics: response_diagnostics, - .. - } = &mut response - else { - unreachable!("provider-session response helper returned a request") - }; - *response_diagnostics = diagnostics; - Ok(response) -} - -pub fn decode_provider_session_response( - response: &CultNetMessage, - expected_operation: &str, - expected_payload_schema: &str, -) -> Result<(CultMeshProviderOperationStatus, T)> -where - T: CultMeshProviderSessionPayload + DeserializeOwned, -{ - let CultNetMessage::OperationResponse { - service_id, - operation, - status, - payload_schema, - payload_encoding, - payload, - .. - } = response - else { - return Err(anyhow!("Expected a CultNet operation response")); - }; - require_envelope( - service_id, - operation, - payload_schema, - payload_encoding, - expected_operation, - expected_payload_schema, - )?; - Ok(( - CultMeshProviderOperationStatus::parse(status)?, - decode_provider_session_payload(payload)?, - )) -} - -fn validate_operation_schema(operation: &str, payload_schema: &str) -> Result<()> { - let expected = match operation { - CULTMESH_PROVIDER_REGISTER_OPERATION => CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - CULTMESH_PROVIDER_RENEW_OPERATION => CULTMESH_PROVIDER_LEASE_RENEWAL_SCHEMA, - CULTMESH_PROVIDER_PUBLICATION_PUT_OPERATION => CULTMESH_PROVIDER_PUBLICATION_PUT_SCHEMA, - CULTMESH_PROVIDER_PUBLICATION_DELETE_OPERATION => { - CULTMESH_PROVIDER_PUBLICATION_DELETE_SCHEMA - } - CULTMESH_PROVIDER_RECEIPT_PUT_OPERATION => CULTMESH_PROVIDER_RECEIPT_PUT_SCHEMA, - CULTMESH_PROVIDER_WITHDRAW_OPERATION => CULTMESH_PROVIDER_WITHDRAWAL_SCHEMA, - _ => { - return Err(anyhow!( - "Unsupported provider-session operation {operation:?}" - )); - } - }; - if payload_schema != expected { - return Err(anyhow!( - "Provider-session operation {operation:?} requires payload schema {expected:?}" - )); - } - Ok(()) -} - -fn require_envelope( - service_id: &str, - operation: &str, - payload_schema: &str, - payload_encoding: &str, - expected_operation: &str, - expected_payload_schema: &str, -) -> Result<()> { - if service_id != CULTMESH_PROVIDER_SESSION_SERVICE_ID { - return Err(anyhow!( - "Unexpected provider-session service id {service_id:?}" - )); - } - if operation != expected_operation { - return Err(anyhow!( - "Unexpected provider-session operation {operation:?}" - )); - } - if payload_schema != expected_payload_schema { - return Err(anyhow!( - "Unexpected provider-session payload schema {payload_schema:?}" - )); - } - if payload_encoding != CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING { - return Err(anyhow!( - "Unexpected provider-session payload encoding {payload_encoding:?}" - )); - } - Ok(()) -} - -fn validate_raw_document(document: &CultNetRawDocumentRecord) -> Result<()> { - require_text(&document.schema_id, "document.schemaId")?; - require_text(&document.record_key, "document.recordKey")?; - Ok(()) -} - -fn require_identity( - provider_id: &str, - service_instance_id: &str, - endpoint_id: &str, - verse_id: &str, -) -> Result<()> { - require_text(provider_id, "providerId")?; - require_text(service_instance_id, "serviceInstanceId")?; - require_text(endpoint_id, "endpointId")?; - require_text(verse_id, "verseId") -} - -fn require_text(value: &str, field: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("Provider-session {field} must not be empty")); - } - Ok(()) -} - -fn require_optional_text(value: Option<&str>, field: &str) -> Result<()> { - if let Some(value) = value { - require_text(value, field)?; - } - Ok(()) -} - -fn require_positive(value: i32, field: &str) -> Result<()> { - if value <= 0 { - return Err(anyhow!( - "Provider-session {field} must be greater than zero" - )); - } - Ok(()) -} - -fn require_timestamp(value: &str, field: &str) -> Result> { - require_text(value, field)?; - DateTime::parse_from_rfc3339(value) - .map_err(|error| anyhow!("Provider-session {field} must be RFC3339: {error}")) -} diff --git a/vendor/cultnet-rs/src/replication.rs b/vendor/cultnet-rs/src/replication.rs deleted file mode 100644 index d02a141..0000000 --- a/vendor/cultnet-rs/src/replication.rs +++ /dev/null @@ -1,1014 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::CultCache; -use cultcache_rs::CultCacheEnvelope; -use cultcache_rs::DatabaseEntry; -use serde::Serialize; -use serde::de::DeserializeOwned; -use serde_json::Value; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::sync::Arc; -use std::sync::Mutex; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::thread; -use std::thread::JoinHandle; -use std::time::Duration; - -use crate::CultNetDocumentMutationContract; -use crate::CultNetDocumentRecord; -use crate::CultNetMessage; -use crate::CultNetRawDocumentRecord; -use crate::CultNetRawPayloadEncoding; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetDocumentPutOptions { - pub stored_at: Option, - pub source_runtime_id: Option, - pub source_agent_id: Option, - pub source_role: Option, - pub tags: Option>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetDocumentBinding { - pub schema_id: String, - pub document_type: String, - pub mutation_contract: Option, - pub payload_schema_version: Option, -} - -impl CultNetDocumentBinding { - pub fn for_entry(payload_schema_version: impl Into>) -> Self { - Self { - document_type: T::TYPE.to_string(), - schema_id: T::TYPE.to_string(), - mutation_contract: None, - payload_schema_version: payload_schema_version.into(), - } - } - - pub fn for_entry_with_schema_id( - schema_id: impl Into, - payload_schema_version: impl Into>, - ) -> Self { - Self { - document_type: T::TYPE.to_string(), - schema_id: schema_id.into(), - mutation_contract: None, - payload_schema_version: payload_schema_version.into(), - } - } - - pub fn with_mutation_contract(mut self, contract: CultNetDocumentMutationContract) -> Self { - self.mutation_contract = Some(contract); - self - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultNetDocumentRegistry { - bindings_by_type: BTreeMap, - bindings_by_schema_id: BTreeMap, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetReactiveDocumentOptions { - pub flush_delay: Duration, - pub detect_local_changes: bool, - pub replace_dirty_current_on_canonical_snapshot: bool, -} - -impl Default for CultNetReactiveDocumentOptions { - fn default() -> Self { - Self { - flush_delay: Duration::from_millis(16), - detect_local_changes: true, - replace_dirty_current_on_canonical_snapshot: false, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CultNetReactiveDocumentReconciliation { - pub canonical: T, - pub predicted: T, - pub delta: BTreeMap, - pub version: u64, - pub received_at: String, -} - -struct CultNetReactiveDocumentState { - dirty: bool, - flushing: bool, - flush_queued: bool, - last_clean_payload: Vec, - reconciliation: Option>, - reconciliation_version: u64, - last_error: Option, -} - -pub struct CultNetReactiveDocument -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - registry: CultNetDocumentRegistry, - cache: Arc>, - record_key: String, - current: Arc>, - state: Arc>>, - options: CultNetReactiveDocumentOptions, - disposed: Arc, - detect_worker: Option>, -} - -impl CultNetDocumentRegistry { - pub fn new() -> Self { - Self::default() - } - - pub fn register(&mut self, binding: CultNetDocumentBinding) -> &mut Self { - self.bindings_by_schema_id - .insert(binding.schema_id.clone(), binding.clone()); - self.bindings_by_type - .insert(binding.document_type.clone(), binding); - self - } - - pub fn binding(&self, document_type: &str) -> Option<&CultNetDocumentBinding> { - self.bindings_by_type.get(document_type) - } - - pub fn binding_by_schema_id(&self, schema_id: &str) -> Option<&CultNetDocumentBinding> { - self.bindings_by_schema_id.get(schema_id) - } - - pub fn mutation_contracts(&self) -> Vec { - self.bindings_by_type - .values() - .filter_map(|binding| binding.mutation_contract.clone()) - .collect() - } - - pub fn create_document_put_message( - &self, - message_id: impl Into, - record_key: impl Into, - value: &T, - options: CultNetDocumentPutOptions, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let binding = self.require_binding(T::TYPE)?; - let payload = serde_json::to_value(round_trip_typed(value)?)?; - Ok(CultNetMessage::DocumentPut { - message_id: message_id.into(), - document: CultNetDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: record_key.into(), - stored_at: options.stored_at.unwrap_or_else(now_utc_second), - payload, - source_runtime_id: options.source_runtime_id, - source_agent_id: options.source_agent_id, - source_role: options.source_role, - tags: options.tags, - }, - }) - } - - pub fn create_document_delete_message( - &self, - message_id: impl Into, - schema_id: impl Into, - record_key: impl Into, - ) -> CultNetMessage { - CultNetMessage::DocumentDelete { - message_id: message_id.into(), - schema_id: schema_id.into(), - record_key: record_key.into(), - } - } - - pub fn create_raw_document_put_message_from_envelope( - &self, - message_id: impl Into, - envelope: &CultCacheEnvelope, - ) -> Result { - Ok(CultNetMessage::DocumentPutRaw { - message_id: message_id.into(), - document: self.raw_document_record_from_envelope(envelope)?, - }) - } - - pub fn create_raw_document_put_message( - &self, - message_id: impl Into, - record_key: impl Into, - value: &T, - options: CultNetDocumentPutOptions, - ) -> Result - where - T: DatabaseEntry + Serialize, - { - let binding = self.require_binding(T::TYPE)?; - Ok(CultNetMessage::DocumentPutRaw { - message_id: message_id.into(), - document: CultNetRawDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: record_key.into(), - stored_at: options.stored_at.unwrap_or_else(now_utc_second), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: rmp_serde::to_vec(value)?, - source_runtime_id: options.source_runtime_id, - source_agent_id: options.source_agent_id, - source_role: options.source_role, - tags: options.tags, - }, - }) - } - - pub fn create_snapshot_response( - &self, - cache: &CultCache, - message_id: impl Into, - schema_ids: Option<&[String]>, - record_keys: Option<&[String]>, - ) -> Result { - let requested_schema_ids = schema_ids.map(|items| items.iter().collect::>()); - let requested_record_keys = record_keys.map(|items| items.iter().collect::>()); - let mut documents = Vec::new(); - for envelope in cache.snapshot() { - let binding = self.require_binding(&envelope.r#type)?; - if requested_schema_ids - .as_ref() - .is_some_and(|ids| !ids.contains(&binding.schema_id)) - { - continue; - } - if requested_record_keys - .as_ref() - .is_some_and(|keys| !keys.contains(&envelope.key)) - { - continue; - } - documents.push(self.document_record_from_envelope(&envelope)?); - } - Ok(CultNetMessage::SnapshotResponse { - message_id: message_id.into(), - documents, - }) - } - - pub fn create_raw_snapshot_response( - &self, - cache: &CultCache, - message_id: impl Into, - schema_ids: Option<&[String]>, - record_keys: Option<&[String]>, - ) -> Result { - let requested_schema_ids = schema_ids.map(|items| items.iter().collect::>()); - let requested_record_keys = record_keys.map(|items| items.iter().collect::>()); - let mut documents = Vec::new(); - for envelope in cache.snapshot() { - let binding = self.require_binding(&envelope.r#type)?; - if requested_schema_ids - .as_ref() - .is_some_and(|ids| !ids.contains(&binding.schema_id)) - { - continue; - } - if requested_record_keys - .as_ref() - .is_some_and(|keys| !keys.contains(&envelope.key)) - { - continue; - } - documents.push(self.raw_document_record_from_envelope(&envelope)?); - } - Ok(CultNetMessage::SnapshotResponseRaw { - message_id: message_id.into(), - documents, - }) - } - - pub fn apply_document_put_message( - &self, - cache: &mut CultCache, - message: &CultNetMessage, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::DocumentPut { document, .. } = message else { - return Err(anyhow!("expected cultnet.document_put.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - if document.schema_id != binding.schema_id { - return Err(anyhow!( - "schema id {:?} does not match registered Rust type {:?} schema {:?}", - document.schema_id, - T::TYPE, - binding.schema_id - )); - } - let value: T = serde_json::from_value(document.payload.clone()).with_context(|| { - format!( - "failed to decode CultNet payload schema {:?} as {}", - binding.schema_id, - T::SCHEMA_NAME - ) - })?; - cache.put(&document.record_key, &value) - } - - pub fn apply_document_delete_message( - &self, - cache: &mut CultCache, - message: &CultNetMessage, - ) -> Result - where - T: DatabaseEntry, - { - let CultNetMessage::DocumentDelete { - schema_id, - record_key, - .. - } = message - else { - return Err(anyhow!("expected cultnet.document_delete.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - if schema_id != &binding.schema_id { - return Err(anyhow!( - "schema id {:?} does not match registered Rust type {:?} schema {:?}", - schema_id, - T::TYPE, - binding.schema_id - )); - } - cache.delete::(record_key) - } - - pub fn apply_raw_document_put_message( - &self, - cache: &mut CultCache, - message: &CultNetMessage, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::DocumentPutRaw { document, .. } = message else { - return Err(anyhow!("expected cultnet.document_put_raw.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - if document.schema_id != binding.schema_id { - return Err(anyhow!( - "schema id {:?} does not match registered Rust type {:?} schema {:?}", - document.schema_id, - T::TYPE, - binding.schema_id - )); - } - cache.put_envelope::(CultCacheEnvelope { - key: document.record_key.clone(), - r#type: binding.document_type.clone(), - payload: document.payload.clone(), - stored_at: document.stored_at.clone(), - schema_id: Some(binding.schema_id.clone()), - }) - } - - pub fn apply_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - ) -> Result> - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponse { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response.v0")); - }; - let mut applied = Vec::new(); - let binding = self.require_binding(T::TYPE)?; - for document in documents { - if document.schema_id != binding.schema_id { - continue; - } - applied.push(self.apply_document_put_message::( - cache, - &CultNetMessage::DocumentPut { - message_id: "snapshot-apply".to_string(), - document: document.clone(), - }, - )?); - } - Ok(applied) - } - - pub fn sync_document_from_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - record_key: &str, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponse { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - let document = documents - .iter() - .find(|document| { - document.schema_id == binding.schema_id && document.record_key == record_key - }) - .ok_or_else(|| { - anyhow!( - "No CultNet snapshot document for schema {:?} and record key {:?}", - binding.schema_id, - record_key - ) - })?; - self.apply_document_put_message::( - cache, - &CultNetMessage::DocumentPut { - message_id: "snapshot-sync".to_string(), - document: document.clone(), - }, - ) - } - - pub fn apply_raw_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - ) -> Result> - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponseRaw { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response_raw.v0")); - }; - let mut applied = Vec::new(); - let binding = self.require_binding(T::TYPE)?; - for document in documents { - if document.schema_id != binding.schema_id { - continue; - } - applied.push(self.apply_raw_document_put_message::( - cache, - &CultNetMessage::DocumentPutRaw { - message_id: "snapshot-apply-raw".to_string(), - document: document.clone(), - }, - )?); - } - Ok(applied) - } - - pub fn sync_raw_document_from_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - record_key: &str, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponseRaw { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response_raw.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - let document = documents - .iter() - .find(|document| { - document.schema_id == binding.schema_id && document.record_key == record_key - }) - .ok_or_else(|| { - anyhow!( - "No raw CultNet snapshot document for schema {:?} and record key {:?}", - binding.schema_id, - record_key - ) - })?; - self.apply_raw_document_put_message::( - cache, - &CultNetMessage::DocumentPutRaw { - message_id: "snapshot-sync-raw".to_string(), - document: document.clone(), - }, - ) - } - - pub fn reactive_document( - &self, - cache: Arc>, - record_key: impl Into, - options: CultNetReactiveDocumentOptions, - ) -> Result> - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - CultNetReactiveDocument::new(self.clone(), cache, record_key, options) - } - - fn document_record_from_envelope( - &self, - envelope: &CultCacheEnvelope, - ) -> Result> { - let binding = self.require_binding(&envelope.r#type)?; - let payload: Value = rmp_serde::from_slice(&envelope.payload).with_context(|| { - format!( - "failed to decode CultCache envelope {:?} at {:?} as generic CultNet payload", - envelope.r#type, envelope.key - ) - })?; - Ok(CultNetDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: envelope.key.clone(), - stored_at: envelope.stored_at.clone(), - payload, - source_runtime_id: None, - source_agent_id: None, - source_role: None, - tags: None, - }) - } - - fn raw_document_record_from_envelope( - &self, - envelope: &CultCacheEnvelope, - ) -> Result { - let binding = self.require_binding(&envelope.r#type)?; - Ok(CultNetRawDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: envelope.key.clone(), - stored_at: envelope.stored_at.clone(), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: envelope.payload.clone(), - source_runtime_id: None, - source_agent_id: None, - source_role: None, - tags: None, - }) - } - - fn require_binding(&self, document_type: &str) -> Result<&CultNetDocumentBinding> { - self.binding(document_type).ok_or_else(|| { - anyhow!("No CultNet document binding is registered for {document_type:?}") - }) - } -} - -impl CultNetReactiveDocument -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - pub fn new( - registry: CultNetDocumentRegistry, - cache: Arc>, - record_key: impl Into, - options: CultNetReactiveDocumentOptions, - ) -> Result { - let record_key = record_key.into(); - if record_key.trim().is_empty() { - return Err(anyhow!("record_key must be non-empty")); - } - registry.require_binding(T::TYPE)?; - let current = { - let cache = cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - cache.get_required::(&record_key)? - }; - let last_clean_payload = serialize_typed(¤t)?; - let current = Arc::new(Mutex::new(current)); - let state = Arc::new(Mutex::new(CultNetReactiveDocumentState { - dirty: false, - flushing: false, - flush_queued: false, - last_clean_payload, - reconciliation: None, - reconciliation_version: 0, - last_error: None, - })); - let disposed = Arc::new(AtomicBool::new(false)); - let detect_worker = if options.detect_local_changes { - Some(start_reactive_detection_worker::( - Arc::clone(&cache), - Arc::clone(¤t), - Arc::clone(&state), - Arc::clone(&disposed), - options.flush_delay, - record_key.clone(), - )) - } else { - None - }; - Ok(Self { - registry, - cache, - record_key, - current, - state, - options, - disposed, - detect_worker, - }) - } - - pub fn current(&self) -> Arc> { - Arc::clone(&self.current) - } - - pub fn record_key(&self) -> &str { - &self.record_key - } - - pub fn is_dirty(&self) -> bool { - self.state.lock().map(|state| state.dirty).unwrap_or(false) - } - - pub fn reconciliation(&self) -> Option> { - self.state - .lock() - .ok() - .and_then(|state| state.reconciliation.clone()) - } - - pub fn last_error(&self) -> Option { - self.state - .lock() - .ok() - .and_then(|state| state.last_error.clone()) - } - - pub fn update(&self, update: impl FnOnce(&mut T)) -> Result { - let predicted = { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - update(&mut current); - current.clone() - }; - { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.dirty = true; - } - if self.options.flush_delay.is_zero() { - self.flush()?; - } - Ok(predicted) - } - - pub fn set_current(&self, value: T) -> Result { - { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - *current = value.clone(); - } - { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.dirty = true; - } - if self.options.flush_delay.is_zero() { - self.flush()?; - } - Ok(value) - } - - pub fn mark_dirty(&self) -> Result<()> { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.dirty = true; - Ok(()) - } - - pub fn refresh(&self) -> Result { - let canonical = { - let cache = self - .cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - cache.get_required::(&self.record_key)? - }; - { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - *current = canonical.clone(); - } - { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.last_clean_payload = serialize_typed(&canonical)?; - state.dirty = false; - state.flush_queued = false; - state.reconciliation = None; - } - Ok(canonical) - } - - pub fn flush(&self) -> Result<()> { - flush_reactive_document::(&self.cache, &self.current, &self.state, &self.record_key) - } - - pub fn apply_document_put_message(&self, message: &CultNetMessage) -> Result { - let canonical = { - let mut cache = self - .cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - self.registry - .apply_document_put_message::(&mut cache, message)? - }; - self.apply_canonical_value(canonical) - } - - pub fn apply_raw_document_put_message(&self, message: &CultNetMessage) -> Result { - let canonical = { - let mut cache = self - .cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - self.registry - .apply_raw_document_put_message::(&mut cache, message)? - }; - self.apply_canonical_value(canonical) - } - - pub fn clear_reconciliation(&self) -> Result<()> { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.reconciliation = None; - Ok(()) - } - - fn apply_canonical_value(&self, canonical: T) -> Result { - let predicted = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))? - .clone(); - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - if state.dirty || state.flushing { - let delta = create_reconciliation_delta(&predicted, &canonical)?; - if delta.is_empty() { - state.reconciliation = None; - } else { - state.reconciliation_version += 1; - state.reconciliation = Some(CultNetReactiveDocumentReconciliation { - canonical: canonical.clone(), - predicted, - delta, - version: state.reconciliation_version, - received_at: now_utc_second(), - }); - } - if !self.options.replace_dirty_current_on_canonical_snapshot { - return Ok(canonical); - } - } - drop(state); - { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - *current = canonical.clone(); - } - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.last_clean_payload = serialize_typed(&canonical)?; - state.reconciliation = None; - Ok(canonical) - } -} - -impl Drop for CultNetReactiveDocument -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - fn drop(&mut self) { - self.disposed.store(true, Ordering::SeqCst); - let _ = self.detect_worker.take(); - } -} - -fn start_reactive_detection_worker( - cache: Arc>, - current: Arc>, - state: Arc>>, - disposed: Arc, - flush_delay: Duration, - record_key: String, -) -> JoinHandle<()> -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - thread::spawn(move || { - let delay = if flush_delay.is_zero() { - Duration::from_millis(1) - } else { - flush_delay - }; - while !disposed.load(Ordering::SeqCst) { - thread::sleep(delay); - match detect_reactive_document_change::(¤t, &state).and_then(|dirty| { - if dirty { - flush_reactive_document::(&cache, ¤t, &state, &record_key)?; - } - Ok(()) - }) { - Ok(()) => {} - Err(error) => { - if let Ok(mut state) = state.lock() { - state.last_error = Some(error.to_string()); - } - } - } - } - }) -} - -fn detect_reactive_document_change( - current: &Arc>, - state: &Arc>>, -) -> Result -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - let payload = { - let current = current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - serialize_typed(&*current)? - }; - let mut state = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - if !state.dirty && !state.flushing && payload != state.last_clean_payload { - state.dirty = true; - } - Ok(state.dirty && !state.flushing) -} - -fn flush_reactive_document( - cache: &Arc>, - current: &Arc>, - state: &Arc>>, - record_key: &str, -) -> Result<()> -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - let predicted = { - let mut state_guard = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - if !state_guard.dirty { - drop(state_guard); - if !detect_reactive_document_change::(current, state)? { - return Ok(()); - } - state_guard = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - } - if state_guard.flushing { - state_guard.flush_queued = true; - return Ok(()); - } - state_guard.flushing = true; - state_guard.dirty = false; - current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))? - .clone() - }; - let payload = serialize_typed(&predicted)?; - { - let mut cache = cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - cache.put::(record_key.to_string(), &predicted)?; - } - let should_flush_again = { - let mut state = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.flushing = false; - state.last_clean_payload = payload; - state.last_error = None; - state.reconciliation = None; - let changed_after_flush = { - let current = current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - serialize_typed(&*current)? != state.last_clean_payload - }; - if changed_after_flush { - state.dirty = true; - } - let should_flush_again = state.flush_queued || state.dirty; - state.flush_queued = false; - should_flush_again - }; - if should_flush_again { - flush_reactive_document::(cache, current, state, record_key)?; - } - Ok(()) -} - -fn serialize_typed(value: &T) -> Result> -where - T: Serialize, -{ - Ok(rmp_serde::to_vec(value)?) -} - -fn create_reconciliation_delta(predicted: &T, canonical: &T) -> Result> -where - T: Serialize, -{ - let predicted = serde_json::to_value(predicted)?; - let canonical = serde_json::to_value(canonical)?; - let mut delta = BTreeMap::new(); - match (predicted, canonical) { - (Value::Object(predicted), Value::Object(canonical)) => { - for (key, predicted_value) in predicted { - let canonical_value = canonical.get(&key).cloned().unwrap_or(Value::Null); - if predicted_value == canonical_value { - continue; - } - delta.insert( - key, - numeric_delta(&predicted_value, &canonical_value).unwrap_or(predicted_value), - ); - } - } - (Value::Array(predicted), Value::Array(canonical)) => { - for (index, predicted_value) in predicted.into_iter().enumerate() { - let canonical_value = canonical.get(index).cloned().unwrap_or(Value::Null); - if predicted_value == canonical_value { - continue; - } - delta.insert( - index.to_string(), - numeric_delta(&predicted_value, &canonical_value).unwrap_or(predicted_value), - ); - } - } - (predicted, canonical) if predicted != canonical => { - delta.insert( - "value".to_string(), - numeric_delta(&predicted, &canonical).unwrap_or(predicted), - ); - } - _ => {} - } - Ok(delta) -} - -fn numeric_delta(predicted: &Value, canonical: &Value) -> Option { - let predicted = predicted.as_f64()?; - let canonical = canonical.as_f64()?; - serde_json::Number::from_f64(predicted - canonical).map(Value::Number) -} - -fn round_trip_typed(value: &T) -> Result -where - T: Serialize + DeserializeOwned, -{ - Ok(rmp_serde::from_slice(&rmp_serde::to_vec(value)?)?) -} - -fn now_utc_second() -> String { - chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) -} diff --git a/vendor/cultnet-rs/src/rudp.rs b/vendor/cultnet-rs/src/rudp.rs deleted file mode 100644 index f487d14..0000000 --- a/vendor/cultnet-rs/src/rudp.rs +++ /dev/null @@ -1,2038 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::collections::VecDeque; -use std::io::ErrorKind; -use std::net::SocketAddr; -use std::net::UdpSocket; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - -use crate::CultNetMessage; -use crate::CultNetReconnectController; -use crate::CultNetReconnectDecision; -use crate::CultNetReconnectPolicy; -use crate::CultNetTransportChannel; -use crate::CultNetTransportDelivery; -use crate::CultNetTransportDescriptor; -use crate::CultNetTransportFrame; -use crate::CultNetTransportOrdering; -use crate::CultNetTransportProfile; -use crate::CultNetTransportProtocol; -use crate::CultNetTransportStats; -use crate::CultNetWireContract; -use crate::create_reconnect_policy; -use crate::decode_cultnet_message_from_slice; -use crate::encode_cultnet_message_to_vec; - -const RUDP_MAGIC: [u8; 4] = [0x43, 0x4e, 0x52, 0x30]; -const RUDP_VERSION: u8 = 0; -const RUDP_FIXED_HEADER_BYTES: usize = 36; -pub const CULTNET_RUDP_DEFAULT_MAX_PAYLOAD_BYTES: usize = 16 * 1024 * 1024; -pub const CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS: usize = 32; -const RUDP_RECEIVED_SEQUENCE_WINDOW: usize = 4_096; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultNetRudpPacketType { - Connect, - Accept, - Data, - Ack, - Ping, - Pong, - Disconnect, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpPacket { - pub packet_type: CultNetRudpPacketType, - pub connection_id: u32, - pub sequence: u32, - pub ack: u32, - pub ack_mask: u32, - pub channel_id: String, - pub reliable: bool, - pub ordered: bool, - pub sequenced: bool, - pub fragment_id: u16, - pub fragment_index: u16, - pub fragment_count: u16, - pub payload: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpDeliveredFrame { - pub channel_id: String, - pub payload: Vec, - pub sequence: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpReceiveResult { - pub delivered: Vec, - pub ready_to_send: Vec, - pub reply: Option, - pub pong: bool, - pub pong_payload: Vec, - pub disconnected: bool, - pub disconnect_reason: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpSessionOptions { - pub connection_id: u32, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub max_pending_reliable_packets: Option, -} - -impl Default for CultNetRudpSessionOptions { - fn default() -> Self { - Self { - connection_id: 0, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetRudpSendOptions { - pub reliable: bool, - pub ordered: bool, - pub sequenced: bool, - pub now_ms: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct PendingReliablePacket { - packet: CultNetRudpPacket, - last_sent_at_ms: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct PendingOrderedFrame { - frame: CultNetRudpDeliveredFrame, - next_sequence: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct FragmentBuffer { - channel_id: String, - ordered: bool, - fragment_count: u16, - payloads: BTreeMap>, - sequences: BTreeMap, -} - -pub struct CultNetRudpSession { - connection_id: u32, - resend_delay_ms: u64, - max_pending_reliable_packets: Option, - max_payload_bytes: Option, - max_pending_fragment_sets: usize, - initial_sequence: u32, - next_sequence: u32, - next_sequenced_by_channel: BTreeMap, - next_fragment_id: u16, - connected: bool, - last_received_at_ms: Option, - highest_received_sequence: Option, - received_sequences: BTreeSet, - latest_sequenced_by_channel: BTreeMap, - pending_reliable: BTreeMap, - queued_reliable: VecDeque, - ordered_next_sequence_by_channel: BTreeMap, - ordered_buffers: BTreeMap>, - fragment_buffers: BTreeMap<(String, u16), FragmentBuffer>, -} - -impl CultNetRudpSession { - pub fn new(options: CultNetRudpSessionOptions) -> Self { - Self { - connection_id: options.connection_id, - resend_delay_ms: options.resend_delay_ms, - max_pending_reliable_packets: options.max_pending_reliable_packets, - max_payload_bytes: None, - max_pending_fragment_sets: 64, - initial_sequence: options.initial_sequence, - next_sequence: options.initial_sequence, - next_sequenced_by_channel: BTreeMap::new(), - next_fragment_id: 1, - connected: false, - last_received_at_ms: None, - highest_received_sequence: None, - received_sequences: BTreeSet::new(), - latest_sequenced_by_channel: BTreeMap::new(), - pending_reliable: BTreeMap::new(), - queued_reliable: VecDeque::new(), - ordered_next_sequence_by_channel: BTreeMap::new(), - ordered_buffers: BTreeMap::new(), - fragment_buffers: BTreeMap::new(), - } - } - - pub fn connection_id(&self) -> u32 { - self.connection_id - } - - pub fn resend_delay_ms(&self) -> u64 { - self.resend_delay_ms - } - - pub fn connected(&self) -> bool { - self.connected - } - - pub fn pending_reliable_sequences(&self) -> Vec { - self.pending_reliable.keys().copied().collect() - } - - pub fn queued_reliable_packet_count(&self) -> usize { - self.queued_reliable.len() - } - - pub fn outstanding_reliable_packet_count(&self) -> usize { - self.pending_reliable.len() + self.queued_reliable.len() - } - - fn pending_accept_for_resend(&mut self, now_ms: u64) -> Option { - let pending = self - .pending_reliable - .values_mut() - .find(|pending| pending.packet.packet_type == CultNetRudpPacketType::Accept)?; - pending.last_sent_at_ms = now_ms; - Some(pending.packet.clone()) - } - - pub fn last_received_at_ms(&self) -> Option { - self.last_received_at_ms - } - - pub fn set_max_payload_bytes(&mut self, max_payload_bytes: Option) { - self.max_payload_bytes = max_payload_bytes; - } - - pub fn set_max_pending_fragment_sets( - &mut self, - max_pending_fragment_sets: usize, - ) -> Result<()> { - if max_pending_fragment_sets == 0 { - return Err(anyhow!( - "RUDP max_pending_fragment_sets must be greater than zero" - )); - } - self.max_pending_fragment_sets = max_pending_fragment_sets; - Ok(()) - } - - pub fn reset_peer_state(&mut self) { - self.next_sequence = self.initial_sequence; - self.next_sequenced_by_channel.clear(); - self.next_fragment_id = 1; - self.connected = false; - self.last_received_at_ms = None; - self.highest_received_sequence = None; - self.received_sequences.clear(); - self.latest_sequenced_by_channel.clear(); - self.pending_reliable.clear(); - self.queued_reliable.clear(); - self.ordered_next_sequence_by_channel.clear(); - self.ordered_buffers.clear(); - self.fragment_buffers.clear(); - } - - pub fn create_connect(&mut self, now_ms: u64, payload: Vec) -> Result { - self.ensure_reliable_capacity(1)?; - let packet = self.create_packet( - CultNetRudpPacketType::Connect, - "control", - payload, - true, - true, - false, - ); - self.track_reliable(packet.clone(), now_ms); - Ok(packet) - } - - pub fn accept_connect( - &mut self, - packet: &CultNetRudpPacket, - now_ms: u64, - payload: Vec, - ) -> Result { - self.require_connection(packet)?; - if packet.packet_type != CultNetRudpPacketType::Connect { - return Err(anyhow!( - "Expected RUDP connect packet, got {:?}", - packet.packet_type - )); - } - - self.ensure_reliable_capacity(1)?; - self.remember_received(packet.sequence); - self.last_received_at_ms = Some(now_ms); - self.connected = true; - let response = self.create_packet( - CultNetRudpPacketType::Accept, - "control", - payload, - true, - true, - false, - ); - self.track_reliable(response.clone(), now_ms); - Ok(response) - } - - pub fn send( - &mut self, - channel_id: &str, - payload: Vec, - options: CultNetRudpSendOptions, - ) -> Result { - if options.reliable - && self.pending_reliable.len() >= CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS - { - return Err(anyhow!( - "RUDP reliable send window is full; receive acknowledgements before sending" - )); - } - self.send_many(channel_id, payload, options, None)? - .into_iter() - .next() - .ok_or_else(|| anyhow!("RUDP send produced no packets")) - } - - pub fn send_many( - &mut self, - channel_id: &str, - payload: Vec, - options: CultNetRudpSendOptions, - max_fragment_bytes: Option, - ) -> Result> { - if !self.connected { - return Err(anyhow!( - "Cannot send RUDP data before the session is connected" - )); - } - if options.ordered && !options.reliable { - return Err(anyhow!("RUDP ordered delivery requires reliability")); - } - self.require_payload_size(payload.len())?; - - if let Some(max_fragment_bytes) = max_fragment_bytes { - if max_fragment_bytes == 0 { - return Err(anyhow!("RUDP max_fragment_bytes must be greater than zero")); - } - if payload.len() > max_fragment_bytes { - let fragment_count = payload.len().div_ceil(max_fragment_bytes); - if fragment_count > u16::MAX as usize { - return Err(anyhow!("RUDP payload requires more than 65535 fragments")); - } - self.ensure_reliable_capacity(if options.reliable { fragment_count } else { 0 })?; - let fragment_id = self.allocate_fragment_id(); - let mut packets = Vec::new(); - for index in 0..fragment_count { - let start = index * max_fragment_bytes; - let end = (start + max_fragment_bytes).min(payload.len()); - let packet = self.create_packet_with_fragments( - CultNetRudpPacketType::Data, - channel_id, - payload[start..end].to_vec(), - options.reliable, - options.ordered, - options.sequenced, - fragment_id, - index as u16, - fragment_count as u16, - ); - packets.push(packet); - } - return Ok(if options.reliable { - self.admit_reliable_packets(packets, options.now_ms) - } else { - packets - }); - } - } - - self.ensure_reliable_capacity(if options.reliable { 1 } else { 0 })?; - let packet = self.create_packet( - CultNetRudpPacketType::Data, - channel_id, - payload, - options.reliable, - options.ordered, - options.sequenced, - ); - if packet.reliable { - return Ok(self.admit_reliable_packets(vec![packet], options.now_ms)); - } - Ok(vec![packet]) - } - - pub fn receive( - &mut self, - packet: &CultNetRudpPacket, - now_ms: u64, - ) -> Result { - self.require_connection(packet)?; - self.apply_acknowledgements(packet); - let ready_to_send = self.promote_queued_reliable(now_ms); - self.last_received_at_ms = Some(now_ms); - let expected_sequence_if_uninitialized = self - .highest_received_sequence - .map(|sequence| sequence + 1) - .unwrap_or(packet.sequence); - - if packet.packet_type == CultNetRudpPacketType::Accept { - self.remember_received(packet.sequence); - self.connected = true; - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - if packet.packet_type == CultNetRudpPacketType::Ping { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: Some(self.create_packet( - CultNetRudpPacketType::Pong, - "control", - packet.payload.clone(), - false, - false, - false, - )), - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - if packet.packet_type == CultNetRudpPacketType::Ack - || packet.packet_type == CultNetRudpPacketType::Pong - { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: packet.packet_type == CultNetRudpPacketType::Pong, - pong_payload: if packet.packet_type == CultNetRudpPacketType::Pong { - packet.payload.clone() - } else { - Vec::new() - }, - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - if packet.packet_type == CultNetRudpPacketType::Disconnect { - self.connected = false; - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: true, - disconnect_reason: packet.payload.clone(), - }); - } - - if packet.packet_type != CultNetRudpPacketType::Data { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - let duplicate = packet.reliable - && (self.received_sequences.contains(&packet.sequence) - || self.highest_received_sequence.is_some_and(|highest| { - packet.sequence < highest - && highest - packet.sequence >= RUDP_RECEIVED_SEQUENCE_WINDOW as u32 - })); - if packet.reliable { - self.remember_received(packet.sequence); - } - if duplicate { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - let Some((frame, ordered, next_sequence)) = self.reassemble(packet)? else { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - }; - let delivered = if ordered { - self.deliver_ordered(frame, next_sequence, expected_sequence_if_uninitialized) - } else if packet.sequenced { - let latest = self - .latest_sequenced_by_channel - .get(&frame.channel_id) - .copied(); - let newest_sequence = next_sequence.saturating_sub(1); - if latest.is_some_and(|latest| newest_sequence <= latest) { - Vec::new() - } else { - self.latest_sequenced_by_channel - .insert(frame.channel_id.clone(), newest_sequence); - vec![frame] - } - } else { - vec![frame] - }; - Ok(CultNetRudpReceiveResult { - delivered, - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }) - } - - pub fn create_ack(&mut self) -> CultNetRudpPacket { - let (ack, ack_mask) = self.ack_state(); - CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Ack, - connection_id: self.connection_id, - sequence: 0, - ack, - ack_mask, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - } - } - - pub fn create_ack_for(&self, sequence: u32) -> CultNetRudpPacket { - CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Ack, - connection_id: self.connection_id, - sequence: 0, - ack: sequence, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - } - } - - pub fn create_ack_for_received(&mut self, sequence: u32) -> CultNetRudpPacket { - let (ack, _) = self.ack_state(); - if ack >= sequence && ack - sequence <= 32 { - self.create_ack() - } else { - self.create_ack_for(sequence) - } - } - - pub fn create_ping(&mut self, payload: Vec) -> CultNetRudpPacket { - self.create_packet( - CultNetRudpPacketType::Ping, - "control", - payload, - false, - false, - false, - ) - } - - pub fn create_disconnect(&mut self, reason: Vec) -> CultNetRudpPacket { - self.connected = false; - self.create_packet( - CultNetRudpPacketType::Disconnect, - "control", - reason, - false, - false, - false, - ) - } - - pub fn check_timeout(&mut self, now_ms: u64, timeout_ms: u64) -> bool { - if !self.connected { - return false; - } - let Some(last_received_at_ms) = self.last_received_at_ms else { - return false; - }; - if now_ms.saturating_sub(last_received_at_ms) <= timeout_ms { - return false; - } - self.connected = false; - true - } - - pub fn due_resends(&mut self, now_ms: u64) -> Vec { - let mut due = Vec::new(); - for pending in self.pending_reliable.values_mut() { - if now_ms.saturating_sub(pending.last_sent_at_ms) >= self.resend_delay_ms { - pending.last_sent_at_ms = now_ms; - due.push(pending.packet.clone()); - } - } - due.sort_by_key(|packet| packet.sequence); - due - } - - fn create_packet( - &mut self, - packet_type: CultNetRudpPacketType, - channel_id: &str, - payload: Vec, - reliable: bool, - ordered: bool, - sequenced: bool, - ) -> CultNetRudpPacket { - self.create_packet_with_fragments( - packet_type, - channel_id, - payload, - reliable, - ordered, - sequenced, - 0, - 0, - 0, - ) - } - - #[allow(clippy::too_many_arguments)] - fn create_packet_with_fragments( - &mut self, - packet_type: CultNetRudpPacketType, - channel_id: &str, - payload: Vec, - reliable: bool, - ordered: bool, - sequenced: bool, - fragment_id: u16, - fragment_index: u16, - fragment_count: u16, - ) -> CultNetRudpPacket { - let sequence = if reliable { - let sequence = self.next_sequence; - self.next_sequence = self - .next_sequence - .checked_add(1) - .expect("reliable sequence overflow"); - sequence - } else if sequenced { - let next = self - .next_sequenced_by_channel - .entry(channel_id.to_string()) - .or_insert(1); - let sequence = *next; - *next = next.checked_add(1).expect("sequenced channel overflow"); - sequence - } else { - 0 - }; - let (ack, ack_mask) = self.ack_state(); - CultNetRudpPacket { - packet_type, - connection_id: self.connection_id, - sequence, - ack, - ack_mask, - channel_id: channel_id.to_string(), - reliable, - ordered, - sequenced, - fragment_id, - fragment_index, - fragment_count, - payload, - } - } - - fn track_reliable(&mut self, packet: CultNetRudpPacket, now_ms: u64) { - self.pending_reliable.insert( - packet.sequence, - PendingReliablePacket { - packet, - last_sent_at_ms: now_ms, - }, - ); - } - - fn admit_reliable_packets( - &mut self, - packets: Vec, - now_ms: u64, - ) -> Vec { - let available = - CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS.saturating_sub(self.pending_reliable.len()); - let mut ready = Vec::with_capacity(available.min(packets.len())); - for packet in packets { - if ready.len() < available { - self.track_reliable(packet.clone(), now_ms); - ready.push(packet); - } else { - self.queued_reliable.push_back(packet); - } - } - ready - } - - fn promote_queued_reliable(&mut self, now_ms: u64) -> Vec { - let available = - CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS.saturating_sub(self.pending_reliable.len()); - let mut ready = Vec::with_capacity(available.min(self.queued_reliable.len())); - for _ in 0..available { - let Some(packet) = self.queued_reliable.pop_front() else { - break; - }; - self.track_reliable(packet.clone(), now_ms); - ready.push(packet); - } - ready - } - - fn ensure_reliable_capacity(&self, packet_count: usize) -> Result<()> { - if packet_count == 0 { - return Ok(()); - } - if let Some(limit) = self.max_pending_reliable_packets { - if self.outstanding_reliable_packet_count() + packet_count > limit { - return Err(anyhow!("RUDP reliable send queue is full")); - } - } - Ok(()) - } - - fn apply_acknowledgements(&mut self, packet: &CultNetRudpPacket) { - self.pending_reliable.remove(&packet.ack); - for bit in 0..32 { - if (packet.ack_mask & (1_u32 << bit)) != 0 && packet.ack > bit { - self.pending_reliable.remove(&(packet.ack - bit - 1)); - } - } - } - - fn remember_received(&mut self, sequence: u32) { - self.received_sequences.insert(sequence); - if self - .highest_received_sequence - .is_none_or(|highest| sequence > highest) - { - self.highest_received_sequence = Some(sequence); - } - if self.received_sequences.len() > RUDP_RECEIVED_SEQUENCE_WINDOW { - let keep_from = self - .highest_received_sequence - .unwrap_or(sequence) - .saturating_sub(RUDP_RECEIVED_SEQUENCE_WINDOW as u32 - 1); - self.received_sequences = self.received_sequences.split_off(&keep_from); - } - } - - fn ack_state(&self) -> (u32, u32) { - let ack = self.highest_received_sequence.unwrap_or(0); - let mut ack_mask = 0_u32; - for bit in 0..32 { - if ack > bit && self.received_sequences.contains(&(ack - bit - 1)) { - ack_mask |= 1_u32 << bit; - } - } - (ack, ack_mask) - } - - fn reassemble( - &mut self, - packet: &CultNetRudpPacket, - ) -> Result> { - if packet.fragment_count == 0 { - self.require_payload_size(packet.payload.len())?; - return Ok(Some(( - CultNetRudpDeliveredFrame { - channel_id: packet.channel_id.clone(), - payload: packet.payload.clone(), - sequence: packet.sequence, - }, - packet.ordered, - packet.sequence + 1, - ))); - } - if packet.fragment_id == 0 { - return Err(anyhow!( - "RUDP fragmented packet must have a non-zero fragment id" - )); - } - if packet.fragment_index >= packet.fragment_count { - return Err(anyhow!( - "RUDP fragment index must be lower than fragment count" - )); - } - - let key = (packet.channel_id.clone(), packet.fragment_id); - if !self.fragment_buffers.contains_key(&key) - && self.fragment_buffers.len() >= self.max_pending_fragment_sets - { - return Err(anyhow!("RUDP pending fragment-set limit reached")); - } - let buffered_bytes = self - .fragment_buffers - .get(&key) - .map(|buffer| { - buffer - .payloads - .iter() - .filter(|(index, _)| **index != packet.fragment_index) - .map(|(_, payload)| payload.len()) - .sum::() - }) - .unwrap_or(0); - if let Err(error) = self.require_payload_size(buffered_bytes + packet.payload.len()) { - self.fragment_buffers.remove(&key); - return Err(error); - } - let buffer = self - .fragment_buffers - .entry(key.clone()) - .or_insert_with(|| FragmentBuffer { - channel_id: packet.channel_id.clone(), - ordered: packet.ordered, - fragment_count: packet.fragment_count, - payloads: BTreeMap::new(), - sequences: BTreeMap::new(), - }); - if buffer.fragment_count != packet.fragment_count || buffer.ordered != packet.ordered { - return Err(anyhow!( - "RUDP fragment metadata changed within a fragment set" - )); - } - buffer - .payloads - .insert(packet.fragment_index, packet.payload.clone()); - buffer - .sequences - .insert(packet.fragment_index, packet.sequence); - if buffer.payloads.len() < packet.fragment_count as usize { - return Ok(None); - } - - let mut payload = Vec::new(); - let mut sequences = Vec::new(); - for index in 0..packet.fragment_count { - let Some(chunk) = buffer.payloads.get(&index) else { - return Ok(None); - }; - let Some(sequence) = buffer.sequences.get(&index) else { - return Ok(None); - }; - payload.extend_from_slice(chunk); - sequences.push(*sequence); - } - let channel_id = buffer.channel_id.clone(); - let ordered = buffer.ordered; - self.fragment_buffers.remove(&key); - Ok(Some(( - CultNetRudpDeliveredFrame { - channel_id, - payload, - sequence: *sequences.iter().min().unwrap(), - }, - ordered, - sequences.iter().max().unwrap() + 1, - ))) - } - - fn require_payload_size(&self, payload_bytes: usize) -> Result<()> { - if self - .max_payload_bytes - .is_some_and(|max_payload_bytes| payload_bytes > max_payload_bytes) - { - return Err(anyhow!("RUDP payload exceeds max_payload_bytes")); - } - Ok(()) - } - - fn deliver_ordered( - &mut self, - frame: CultNetRudpDeliveredFrame, - next_sequence_after_frame: u32, - expected_sequence_if_uninitialized: u32, - ) -> Vec { - let channel_id = frame.channel_id.clone(); - let mut next = if let Some(next) = self - .ordered_next_sequence_by_channel - .get(&channel_id) - .copied() - { - next - } else { - self.ordered_next_sequence_by_channel.insert( - channel_id.clone(), - expected_sequence_if_uninitialized.min(frame.sequence), - ); - expected_sequence_if_uninitialized.min(frame.sequence) - }; - - while frame.sequence > next - && self.received_sequences.contains(&next) - && !self - .ordered_buffers - .get(&channel_id) - .is_some_and(|buffer| buffer.contains_key(&next)) - { - next = next.saturating_add(1); - self.ordered_next_sequence_by_channel - .insert(channel_id.clone(), next); - } - - if frame.sequence < next { - return Vec::new(); - } - - if frame.sequence > next { - self.ordered_buffers.entry(channel_id).or_default().insert( - frame.sequence, - PendingOrderedFrame { - frame, - next_sequence: next_sequence_after_frame, - }, - ); - return Vec::new(); - } - - self.ordered_next_sequence_by_channel - .insert(channel_id.clone(), next_sequence_after_frame); - let mut delivered = vec![frame]; - delivered.extend(self.drain_ordered(&channel_id)); - delivered - } - - fn drain_ordered(&mut self, channel_id: &str) -> Vec { - let mut delivered = Vec::new(); - loop { - let Some(next) = self - .ordered_next_sequence_by_channel - .get(channel_id) - .copied() - else { - break; - }; - let Some(buffer) = self.ordered_buffers.get_mut(channel_id) else { - break; - }; - let Some(pending) = buffer.remove(&next) else { - break; - }; - delivered.push(pending.frame); - self.ordered_next_sequence_by_channel - .insert(channel_id.to_string(), pending.next_sequence); - self.skip_received_non_channel_sequences(channel_id); - } - delivered - } - - fn skip_received_non_channel_sequences(&mut self, channel_id: &str) { - let Some(mut next) = self - .ordered_next_sequence_by_channel - .get(channel_id) - .copied() - else { - return; - }; - while self.received_sequences.contains(&next) - && !self - .ordered_buffers - .get(channel_id) - .is_some_and(|buffer| buffer.contains_key(&next)) - { - next = next.saturating_add(1); - self.ordered_next_sequence_by_channel - .insert(channel_id.to_string(), next); - } - } - - fn allocate_fragment_id(&mut self) -> u16 { - let fragment_id = self.next_fragment_id; - self.next_fragment_id = self.next_fragment_id.saturating_add(1); - if self.next_fragment_id == 0 { - self.next_fragment_id = 1; - } - fragment_id - } - - fn require_connection(&self, packet: &CultNetRudpPacket) -> Result<()> { - if packet.connection_id != self.connection_id { - return Err(anyhow!( - "RUDP packet connection id {} does not match {}", - packet.connection_id, - self.connection_id - )); - } - Ok(()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultNetRudpSocketMode { - Client, - Server, -} - -pub struct CultNetRudpSocketTransportOptions { - pub runtime_id: String, - pub socket: UdpSocket, - pub mode: CultNetRudpSocketMode, - pub remote_addr: Option, - pub connection_id: u32, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub transport_id: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, - pub reconnect_policy: Option, -} - -impl CultNetRudpSocketTransportOptions { - pub fn client( - runtime_id: impl Into, - socket: UdpSocket, - remote_addr: SocketAddr, - connection_id: u32, - ) -> Self { - Self { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(remote_addr), - connection_id, - initial_sequence: 1, - resend_delay_ms: 250, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - } - } - - pub fn server(runtime_id: impl Into, socket: UdpSocket, connection_id: u32) -> Self { - Self { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: 1, - resend_delay_ms: 250, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - } - } -} - -pub struct CultNetRudpSocketTransportConnection { - socket: UdpSocket, - session: CultNetRudpSession, - mode: CultNetRudpSocketMode, - remote_addr: Option, - pub profile: CultNetTransportProfile, - stats: CultNetTransportStats, - delivered_frames: VecDeque, - max_fragment_bytes: Option, - disconnect_reason: Option>, - pong_payloads: VecDeque>, -} - -pub struct CultNetRudpServerHubOptions { - pub runtime_id: String, - pub socket: UdpSocket, - pub connection_id: u32, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub transport_id: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, - pub max_peers: usize, -} - -impl CultNetRudpServerHubOptions { - pub fn new(runtime_id: impl Into, socket: UdpSocket, connection_id: u32) -> Self { - Self { - runtime_id: runtime_id.into(), - socket, - connection_id, - initial_sequence: 1, - resend_delay_ms: 250, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - max_peers: 256, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpServerSessionContext { - pub remote_addr: SocketAddr, - pub connection_id: u32, - /// Server-minted generation used to fence work from an older session at - /// the same UDP endpoint. - pub session_generation: u64, - /// Exact bytes supplied by the peer's most recently accepted Connect packet. - /// CultNet does not interpret these bytes; higher layers may use them as - /// transport evidence when making their own authorization decision. - pub connect_payload: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum CultNetRudpServerEvent { - Connected { - session: CultNetRudpServerSessionContext, - }, - Frame { - session: CultNetRudpServerSessionContext, - frame: CultNetTransportFrame, - }, - Pong { - session: CultNetRudpServerSessionContext, - payload: Vec, - }, - Disconnected { - session: CultNetRudpServerSessionContext, - reason: Vec, - }, -} - -struct CultNetRudpServerPeer { - session: CultNetRudpSession, - context: CultNetRudpServerSessionContext, -} - -/// One UDP listener with an independent reliable session for every remote peer. -/// -/// The hub owns packet/session mechanics only. Registration, leases, commands, -/// and all other application policy belong to the service using the hub. -pub struct CultNetRudpServerHub { - socket: UdpSocket, - connection_id: u32, - initial_sequence: u32, - resend_delay_ms: u64, - max_pending_reliable_packets: Option, - max_payload_bytes: usize, - max_fragment_bytes: Option, - max_peers: usize, - next_session_generation: u64, - peers: BTreeMap, - pending_events: VecDeque, - pub profile: CultNetTransportProfile, - stats: CultNetTransportStats, -} - -impl CultNetRudpServerHub { - pub fn new(options: CultNetRudpServerHubOptions) -> Result { - let local_addr = options.socket.local_addr()?; - if options.max_peers == 0 { - return Err(anyhow!( - "RUDP server hub max_peers must be greater than zero" - )); - } - let max_payload_bytes = options - .max_payload_bytes - .or(Some(CULTNET_RUDP_DEFAULT_MAX_PAYLOAD_BYTES as u32)); - let profile = create_rudp_transport_profile( - options.runtime_id, - RudpTransportProfileOptions { - transport_id: options.transport_id, - host: Some(local_addr.ip().to_string()), - port: Some(local_addr.port()), - max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: None, - }, - ); - Ok(Self { - socket: options.socket, - connection_id: options.connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - max_pending_reliable_packets: options - .max_pending_reliable_packets - .map(|value| value as usize), - max_payload_bytes: max_payload_bytes.expect("RUDP hub has an effective payload limit") - as usize, - max_fragment_bytes: options.max_fragment_bytes.map(|value| value as usize), - max_peers: options.max_peers, - next_session_generation: 1, - peers: BTreeMap::new(), - pending_events: VecDeque::new(), - profile, - stats: CultNetTransportStats::default(), - }) - } - - pub fn local_addr(&self) -> Result { - Ok(self.socket.local_addr()?) - } - - pub fn stats(&self) -> CultNetTransportStats { - self.stats.clone() - } - - pub fn sessions(&self) -> Vec { - self.peers - .values() - .map(|peer| peer.context.clone()) - .collect() - } - - pub fn session(&self, remote_addr: SocketAddr) -> Option<&CultNetRudpServerSessionContext> { - self.peers.get(&remote_addr).map(|peer| &peer.context) - } - - pub fn send( - &mut self, - session: &CultNetRudpServerSessionContext, - channel_id: &str, - payload: Vec, - ) -> Result<()> { - let remote_addr = session.remote_addr; - let packets = { - let peer = self - .peers - .get_mut(&remote_addr) - .ok_or_else(|| anyhow!("Unknown RUDP server peer {remote_addr}"))?; - if peer.context.session_generation != session.session_generation { - return Err(anyhow!( - "RUDP server session generation {} is no longer active", - session.session_generation - )); - } - peer.session.send_many( - channel_id, - payload, - channel_send_options(channel_id, now_ms()), - self.max_fragment_bytes, - )? - }; - for packet in packets { - self.send_packet(remote_addr, &packet)?; - } - self.stats.frames_sent += 1; - Ok(()) - } - - pub fn send_schema_message( - &mut self, - session: &CultNetRudpServerSessionContext, - message: &CultNetMessage, - ) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - self.send(session, "schema", payload) - } - - pub fn disconnect( - &mut self, - session: &CultNetRudpServerSessionContext, - reason: Vec, - ) -> Result { - let Some(peer) = self.peers.get(&session.remote_addr) else { - return Ok(false); - }; - if peer.context.session_generation != session.session_generation { - return Ok(false); - } - let mut peer = self - .peers - .remove(&session.remote_addr) - .expect("validated RUDP peer exists"); - let packet = peer.session.create_disconnect(reason); - self.send_packet(session.remote_addr, &packet)?; - Ok(true) - } - - pub fn receive_event_once(&mut self) -> Result> { - if let Some(event) = self.pending_events.pop_front() { - return Ok(Some(event)); - } - - let mut wire = vec![0_u8; 65_535]; - let (received, remote_addr) = match self.socket.recv_from(&mut wire) { - Ok(value) => value, - Err(error) - if error.kind() == ErrorKind::WouldBlock - || error.kind() == ErrorKind::TimedOut - || error.kind() == ErrorKind::ConnectionReset => - { - return Ok(None); - } - Err(error) => return Err(error.into()), - }; - wire.truncate(received); - self.stats.bytes_received += received as u64; - let packet = decode_rudp_packet(&wire)?; - if packet.connection_id != self.connection_id { - return Ok(None); - } - - if packet.packet_type == CultNetRudpPacketType::Connect { - if let Some(peer) = self.peers.get_mut(&remote_addr) - && peer.context.connect_payload == packet.payload - { - let _ = peer.session.receive(&packet, now_ms())?; - let reply = peer - .session - .pending_accept_for_resend(now_ms()) - .unwrap_or_else(|| peer.session.create_ack()); - self.send_packet(remote_addr, &reply)?; - return Ok(None); - } - if !self.peers.contains_key(&remote_addr) && self.peers.len() >= self.max_peers { - return Err(anyhow!("RUDP server hub peer limit reached")); - } - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: self.connection_id, - initial_sequence: self.initial_sequence, - resend_delay_ms: self.resend_delay_ms, - max_pending_reliable_packets: self.max_pending_reliable_packets, - }); - session.set_max_payload_bytes(Some(self.max_payload_bytes)); - let generation = self.next_session_generation; - self.next_session_generation = self - .next_session_generation - .checked_add(1) - .ok_or_else(|| anyhow!("RUDP server session generation exhausted"))?; - let context = CultNetRudpServerSessionContext { - remote_addr, - connection_id: packet.connection_id, - session_generation: generation, - connect_payload: packet.payload.clone(), - }; - let accept = session.accept_connect(&packet, now_ms(), Vec::new())?; - self.send_packet(remote_addr, &accept)?; - let replaced = self.peers.insert( - remote_addr, - CultNetRudpServerPeer { - session, - context: context.clone(), - }, - ); - if let Some(replaced) = replaced { - self.pending_events - .push_back(CultNetRudpServerEvent::Disconnected { - session: replaced.context, - reason: b"replaced by a new Connect generation".to_vec(), - }); - } - self.pending_events - .push_back(CultNetRudpServerEvent::Connected { session: context }); - return Ok(self.pending_events.pop_front()); - } - - let Some(peer) = self.peers.get_mut(&remote_addr) else { - return Ok(None); - }; - let result = peer.session.receive(&packet, now_ms())?; - let context = peer.context.clone(); - let ack = if packet.reliable { - Some(peer.session.create_ack_for_received(packet.sequence)) - } else { - None - }; - if let Some(reply) = result.reply { - self.send_packet(remote_addr, &reply)?; - } - for ready in result.ready_to_send { - self.send_packet(remote_addr, &ready)?; - } - if let Some(ack) = ack { - self.send_packet(remote_addr, &ack)?; - } - if result.pong { - self.pending_events.push_back(CultNetRudpServerEvent::Pong { - session: context.clone(), - payload: result.pong_payload, - }); - } - for delivered in result.delivered { - self.stats.frames_received += 1; - self.pending_events - .push_back(CultNetRudpServerEvent::Frame { - session: context.clone(), - frame: CultNetTransportFrame { - channel_id: delivered.channel_id, - payload: delivered.payload, - }, - }); - } - if result.disconnected { - self.pending_events - .push_back(CultNetRudpServerEvent::Disconnected { - session: context, - reason: result.disconnect_reason, - }); - self.peers.remove(&remote_addr); - } - Ok(self.pending_events.pop_front()) - } - - pub fn poll_resends(&mut self) -> Result<()> { - let now = now_ms(); - let mut packets = Vec::new(); - for (remote_addr, peer) in &mut self.peers { - packets.extend( - peer.session - .due_resends(now) - .into_iter() - .map(|packet| (*remote_addr, packet)), - ); - } - for (remote_addr, packet) in packets { - self.send_packet(remote_addr, &packet)?; - } - Ok(()) - } - - pub fn remove_timed_out_sessions( - &mut self, - timeout_ms: u64, - ) -> Vec { - let now = now_ms(); - let timed_out = self - .peers - .iter_mut() - .filter_map(|(remote_addr, peer)| { - peer.session - .check_timeout(now, timeout_ms) - .then_some((*remote_addr, peer.context.clone())) - }) - .collect::>(); - for (remote_addr, _) in &timed_out { - self.peers.remove(remote_addr); - } - timed_out.into_iter().map(|(_, context)| context).collect() - } - - fn send_packet(&mut self, remote_addr: SocketAddr, packet: &CultNetRudpPacket) -> Result<()> { - let wire = encode_rudp_packet(packet)?; - let sent = self.socket.send_to(&wire, remote_addr)?; - self.stats.bytes_sent += sent as u64; - Ok(()) - } -} - -impl CultNetRudpSocketTransportConnection { - pub fn new(options: CultNetRudpSocketTransportOptions) -> Result { - let local_addr = options.socket.local_addr()?; - let max_payload_bytes = options - .max_payload_bytes - .or(Some(CULTNET_RUDP_DEFAULT_MAX_PAYLOAD_BYTES as u32)); - let profile = create_rudp_transport_profile( - options.runtime_id, - RudpTransportProfileOptions { - transport_id: options.transport_id, - host: Some(local_addr.ip().to_string()), - port: Some(local_addr.port()), - max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: options.reconnect_policy, - }, - ); - let max_pending_reliable_packets = options - .max_pending_reliable_packets - .map(|value| value as usize); - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: options.connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - max_pending_reliable_packets, - }); - session.set_max_payload_bytes(max_payload_bytes.map(|value| value as usize)); - Ok(Self { - socket: options.socket, - session, - mode: options.mode, - remote_addr: options.remote_addr, - profile, - stats: CultNetTransportStats::default(), - delivered_frames: VecDeque::new(), - max_fragment_bytes: options.max_fragment_bytes.map(|value| value as usize), - disconnect_reason: None, - pong_payloads: VecDeque::new(), - }) - } - - pub fn connected(&self) -> bool { - self.session.connected() - } - - pub fn stats(&self) -> CultNetTransportStats { - self.stats.clone() - } - - pub fn disconnect_reason(&self) -> Option<&[u8]> { - self.disconnect_reason.as_deref() - } - - pub fn pop_pong_payload(&mut self) -> Option> { - self.pong_payloads.pop_front() - } - - pub fn connect(&mut self, payload: Vec) -> Result<()> { - if self.mode != CultNetRudpSocketMode::Client { - return Err(anyhow!( - "Only a client RUDP socket transport can initiate connect" - )); - } - let packet = self.session.create_connect(now_ms(), payload)?; - self.send_packet(&packet) - } - - pub fn send(&mut self, channel_id: &str, payload: Vec) -> Result<()> { - let options = channel_send_options(channel_id, now_ms()); - let packets = - self.session - .send_many(channel_id, payload, options, self.max_fragment_bytes)?; - for packet in packets { - self.send_packet(&packet)?; - } - self.stats.frames_sent += 1; - Ok(()) - } - - pub fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - self.send("schema", payload) - } - - pub fn receive_schema_message_once(&mut self) -> Result> { - let Some(frame) = self.receive_once()? else { - return Ok(None); - }; - if frame.channel_id != "schema" { - return Err(anyhow!( - "Expected RUDP schema frame, received channel {}", - frame.channel_id - )); - } - Ok(Some(decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?)) - } - - pub fn disconnect(&mut self, reason: Vec) -> Result<()> { - let packet = self.session.create_disconnect(reason); - self.send_packet(&packet) - } - - pub fn ping(&mut self, payload: Vec) -> Result<()> { - let packet = self.session.create_ping(payload); - self.send_packet(&packet) - } - - pub fn check_timeout(&mut self, timeout_ms: u64) -> bool { - self.session.check_timeout(now_ms(), timeout_ms) - } - - pub fn pending_reliable_packet_count(&self) -> usize { - self.session.pending_reliable_sequences().len() - } - - pub fn outstanding_reliable_packet_count(&self) -> usize { - self.session.outstanding_reliable_packet_count() - } - - pub fn flush_reliable(&mut self, timeout: Duration) -> Result<()> { - let original_timeout = self.socket.read_timeout()?; - let poll_timeout = original_timeout - .map(|configured| configured.min(Duration::from_millis(10))) - .unwrap_or(Duration::from_millis(10)); - self.socket.set_read_timeout(Some(poll_timeout))?; - - let deadline = Instant::now() + timeout; - let mut preserved_frames = VecDeque::new(); - let result = (|| { - while self.outstanding_reliable_packet_count() > 0 { - if Instant::now() >= deadline { - return Err(anyhow!( - "RUDP reliable flush timed out with {} packets outstanding", - self.outstanding_reliable_packet_count() - )); - } - if let Some(frame) = self.receive_once()? { - preserved_frames.push_back(frame); - } - self.poll_resends()?; - } - Ok(()) - })(); - - preserved_frames.append(&mut self.delivered_frames); - self.delivered_frames = preserved_frames; - let restore = self.socket.set_read_timeout(original_timeout); - match (result, restore) { - (Err(error), _) => Err(error), - (Ok(()), Err(error)) => Err(error.into()), - (Ok(()), Ok(())) => Ok(()), - } - } - - pub fn receive_once(&mut self) -> Result> { - if let Some(frame) = self.delivered_frames.pop_front() { - return Ok(Some(frame)); - } - - let mut wire = vec![0_u8; 65_535]; - let (received, remote_addr) = match self.socket.recv_from(&mut wire) { - Ok(value) => value, - Err(error) - if error.kind() == ErrorKind::WouldBlock - || error.kind() == ErrorKind::TimedOut - || error.kind() == ErrorKind::ConnectionReset => - { - return Ok(None); - } - Err(error) => return Err(error.into()), - }; - wire.truncate(received); - self.stats.bytes_received += received as u64; - - let packet = decode_rudp_packet(&wire)?; - if let Some(expected) = self.remote_addr { - if expected != remote_addr { - if self.mode == CultNetRudpSocketMode::Server - && packet.packet_type == CultNetRudpPacketType::Connect - { - self.remote_addr = Some(remote_addr); - } else { - return Ok(None); - } - } - } else { - if self.mode == CultNetRudpSocketMode::Server - && packet.packet_type != CultNetRudpPacketType::Connect - { - return Ok(None); - } - self.remote_addr = Some(remote_addr); - } - if self.mode == CultNetRudpSocketMode::Server - && packet.packet_type == CultNetRudpPacketType::Connect - { - self.session.reset_peer_state(); - let accept = self.session.accept_connect(&packet, now_ms(), Vec::new())?; - self.send_packet(&accept)?; - return Ok(None); - } - - let result = self.session.receive(&packet, now_ms())?; - if let Some(reply) = result.reply { - self.send_packet(&reply)?; - } - for ready in result.ready_to_send { - self.send_packet(&ready)?; - } - if result.pong { - self.pong_payloads.push_back(result.pong_payload); - } - if result.disconnected { - self.disconnect_reason = Some(result.disconnect_reason); - return Ok(None); - } - - for frame in result.delivered { - self.delivered_frames.push_back(CultNetTransportFrame { - channel_id: frame.channel_id, - payload: frame.payload, - }); - self.stats.frames_received += 1; - } - let frame = self.delivered_frames.pop_front(); - if packet.reliable || packet.packet_type == CultNetRudpPacketType::Accept || frame.is_some() - { - let ack = self.session.create_ack_for_received(packet.sequence); - self.send_packet(&ack)?; - } - Ok(frame) - } - - pub fn poll_resends(&mut self) -> Result<()> { - for packet in self.session.due_resends(now_ms()) { - self.send_packet(&packet)?; - } - Ok(()) - } - - fn send_packet(&mut self, packet: &CultNetRudpPacket) -> Result<()> { - let Some(remote_addr) = self.remote_addr else { - return Err(anyhow!( - "RUDP socket transport does not have a remote endpoint" - )); - }; - let wire = encode_rudp_packet(packet)?; - let sent = self.socket.send_to(&wire, remote_addr)?; - self.stats.bytes_sent += sent as u64; - Ok(()) - } -} - -pub struct CultNetRudpReconnectLoop -where - F: FnMut() -> Result, -{ - pub reconnect_controller: CultNetReconnectController, - create_transport: F, - connect_payload: Vec, - transport: Option, - stopped: bool, -} - -impl CultNetRudpReconnectLoop -where - F: FnMut() -> Result, -{ - pub fn new( - reconnect_policy: CultNetReconnectPolicy, - connect_payload: Vec, - create_transport: F, - ) -> Self { - Self { - reconnect_controller: CultNetReconnectController::new(reconnect_policy), - create_transport, - connect_payload, - transport: None, - stopped: true, - } - } - - pub fn with_default_policy(connect_payload: Vec, create_transport: F) -> Self { - Self::new( - create_reconnect_policy(Default::default()), - connect_payload, - create_transport, - ) - } - - pub fn transport(&self) -> Option<&CultNetRudpSocketTransportConnection> { - self.transport.as_ref() - } - - pub fn transport_mut(&mut self) -> Option<&mut CultNetRudpSocketTransportConnection> { - self.transport.as_mut() - } - - pub fn start(&mut self) -> Result<&mut CultNetRudpSocketTransportConnection> { - self.stopped = false; - self.reconnect_controller.reset(); - self.open_transport() - } - - pub fn stop(&mut self) { - self.stopped = true; - self.transport = None; - self.reconnect_controller.reset(); - } - - pub fn mark_connected(&mut self) { - self.reconnect_controller.reset(); - } - - pub fn handle_closed( - &mut self, - now_ms: u64, - jitter_ms: u64, - ) -> Option { - self.transport = None; - if self.stopped { - return None; - } - Some(self.reconnect_controller.record_failure(now_ms, jitter_ms)) - } - - pub fn reconnect_if_due(&mut self, now_ms: u64) -> Result { - if self.stopped || !self.reconnect_controller.can_attempt(now_ms) { - return Ok(false); - } - self.open_transport()?; - Ok(true) - } - - fn open_transport(&mut self) -> Result<&mut CultNetRudpSocketTransportConnection> { - let mut transport = (self.create_transport)()?; - transport.connect(self.connect_payload.clone())?; - self.transport = Some(transport); - Ok(self - .transport - .as_mut() - .expect("RUDP reconnect loop opened a transport")) - } -} - -#[derive(Clone, Debug, Default)] -pub struct RudpTransportProfileOptions { - pub transport_id: Option, - pub host: Option, - pub port: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, - pub reconnect_policy: Option, -} - -pub fn create_rudp_transport_profile( - runtime_id: impl Into, - options: RudpTransportProfileOptions, -) -> CultNetTransportProfile { - CultNetTransportProfile { - schema_version: "cultnet.transport_profile.v0".to_string(), - runtime_id: runtime_id.into(), - transports: vec![CultNetTransportDescriptor { - transport_id: options.transport_id.unwrap_or_else(|| "rudp".to_string()), - protocol: CultNetTransportProtocol::Rudp, - host: options.host, - port: options.port, - path: None, - discovery_group: None, - wire_contracts: Some(vec!["cultnet.schema.v0".to_string()]), - reconnect_policy: Some( - options - .reconnect_policy - .unwrap_or_else(|| create_reconnect_policy(Default::default())), - ), - channels: vec![ - CultNetTransportChannel { - channel_id: "schema".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Ordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - CultNetTransportChannel { - channel_id: "latest".to_string(), - delivery: CultNetTransportDelivery::Unreliable, - ordering: CultNetTransportOrdering::Sequenced, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - CultNetTransportChannel { - channel_id: "realtime".to_string(), - delivery: CultNetTransportDelivery::Unreliable, - ordering: CultNetTransportOrdering::Unordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - CultNetTransportChannel { - channel_id: "media".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Unordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - ], - }], - } -} - -pub fn encode_rudp_packet(packet: &CultNetRudpPacket) -> Result> { - let channel_id = packet.channel_id.as_bytes(); - if channel_id.len() > u8::MAX as usize { - return Err(anyhow!( - "CultNet RUDP channel id cannot exceed 255 UTF-8 bytes" - )); - } - - let header_bytes = RUDP_FIXED_HEADER_BYTES + channel_id.len(); - let mut wire = vec![0_u8; header_bytes + packet.payload.len()]; - wire[..4].copy_from_slice(&RUDP_MAGIC); - wire[4] = RUDP_VERSION; - wire[5] = packet_type_to_code(packet.packet_type); - wire[6] = encode_flags(packet); - wire[7] = header_bytes as u8; - wire[8..12].copy_from_slice(&packet.connection_id.to_be_bytes()); - wire[12..16].copy_from_slice(&packet.sequence.to_be_bytes()); - wire[16..20].copy_from_slice(&packet.ack.to_be_bytes()); - wire[20..24].copy_from_slice(&packet.ack_mask.to_be_bytes()); - wire[24..26].copy_from_slice(&packet.fragment_id.to_be_bytes()); - wire[26..28].copy_from_slice(&packet.fragment_index.to_be_bytes()); - wire[28..30].copy_from_slice(&packet.fragment_count.to_be_bytes()); - wire[30..34].copy_from_slice(&(packet.payload.len() as u32).to_be_bytes()); - wire[34] = channel_id.len() as u8; - wire[35] = 0; - wire[RUDP_FIXED_HEADER_BYTES..header_bytes].copy_from_slice(channel_id); - wire[header_bytes..].copy_from_slice(&packet.payload); - Ok(wire) -} - -pub fn decode_rudp_packet(wire: &[u8]) -> Result { - if wire.len() < RUDP_FIXED_HEADER_BYTES { - return Err(anyhow!( - "CultNet RUDP packet is shorter than the fixed header" - )); - } - if wire[..4] != RUDP_MAGIC { - return Err(anyhow!("CultNet RUDP packet has the wrong magic")); - } - if wire[4] != RUDP_VERSION { - return Err(anyhow!( - "Unsupported CultNet RUDP packet version {}", - wire[4] - )); - } - - let packet_type = packet_type_from_code(wire[5])?; - let header_bytes = wire[7] as usize; - let channel_id_len = wire[34] as usize; - if header_bytes != RUDP_FIXED_HEADER_BYTES + channel_id_len { - return Err(anyhow!( - "CultNet RUDP packet header length does not match the channel id length" - )); - } - let payload_len = u32::from_be_bytes(wire[30..34].try_into()?) as usize; - if wire.len() != header_bytes + payload_len { - return Err(anyhow!( - "CultNet RUDP packet payload length does not match the packet size" - )); - } - - let flags = wire[6]; - Ok(CultNetRudpPacket { - packet_type, - reliable: (flags & 0b0000_0001) != 0, - ordered: (flags & 0b0000_0010) != 0, - sequenced: (flags & 0b0000_0100) != 0, - connection_id: u32::from_be_bytes(wire[8..12].try_into()?), - sequence: u32::from_be_bytes(wire[12..16].try_into()?), - ack: u32::from_be_bytes(wire[16..20].try_into()?), - ack_mask: u32::from_be_bytes(wire[20..24].try_into()?), - fragment_id: u16::from_be_bytes(wire[24..26].try_into()?), - fragment_index: u16::from_be_bytes(wire[26..28].try_into()?), - fragment_count: u16::from_be_bytes(wire[28..30].try_into()?), - channel_id: String::from_utf8(wire[RUDP_FIXED_HEADER_BYTES..header_bytes].to_vec())?, - payload: wire[header_bytes..].to_vec(), - }) -} - -fn encode_flags(packet: &CultNetRudpPacket) -> u8 { - (if packet.reliable { 0b0000_0001 } else { 0 }) - | (if packet.ordered { 0b0000_0010 } else { 0 }) - | (if packet.sequenced { 0b0000_0100 } else { 0 }) - | (if packet.fragment_count > 0 { - 0b0000_1000 - } else { - 0 - }) -} - -fn packet_type_to_code(packet_type: CultNetRudpPacketType) -> u8 { - match packet_type { - CultNetRudpPacketType::Connect => 1, - CultNetRudpPacketType::Accept => 2, - CultNetRudpPacketType::Data => 3, - CultNetRudpPacketType::Ack => 4, - CultNetRudpPacketType::Ping => 5, - CultNetRudpPacketType::Pong => 6, - CultNetRudpPacketType::Disconnect => 7, - } -} - -fn channel_send_options(channel_id: &str, now_ms: u64) -> CultNetRudpSendOptions { - match channel_id { - "schema" => CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms, - }, - "latest" => CultNetRudpSendOptions { - reliable: false, - ordered: false, - sequenced: true, - now_ms, - }, - "media" => CultNetRudpSendOptions { - reliable: true, - ordered: false, - sequenced: false, - now_ms, - }, - _ => CultNetRudpSendOptions { - reliable: false, - ordered: false, - sequenced: false, - now_ms, - }, - } -} - -fn now_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis() as u64) - .unwrap_or(0) -} - -fn packet_type_from_code(code: u8) -> Result { - match code { - 1 => Ok(CultNetRudpPacketType::Connect), - 2 => Ok(CultNetRudpPacketType::Accept), - 3 => Ok(CultNetRudpPacketType::Data), - 4 => Ok(CultNetRudpPacketType::Ack), - 5 => Ok(CultNetRudpPacketType::Ping), - 6 => Ok(CultNetRudpPacketType::Pong), - 7 => Ok(CultNetRudpPacketType::Disconnect), - _ => Err(anyhow!("Unsupported CultNet RUDP packet type {code}")), - } -} diff --git a/vendor/cultnet-rs/src/schema_discovery.rs b/vendor/cultnet-rs/src/schema_discovery.rs deleted file mode 100644 index 272323b..0000000 --- a/vendor/cultnet-rs/src/schema_discovery.rs +++ /dev/null @@ -1,410 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use serde_json::Value; -use sha2::Digest; -use sha2::Sha256; -use std::collections::BTreeMap; -use std::collections::BTreeSet; - -use crate::CultNetMessage; -use crate::CultNetSchemaDescriptor; -use crate::CultNetSchemaKind; -use crate::CultNetWireContract; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetSchemaRegistration { - pub schema_id: String, - pub kind: CultNetSchemaKind, - pub wire_contracts: Vec, - pub schema_version: Option, - pub document_type: Option, - pub title: Option, - pub schema_json: Option, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetSchemaCatalogOptions { - pub include_schema_json: bool, - pub schema_ids: Option>, - pub kinds: Option>, -} - -#[derive(Clone, Debug, Default)] -pub struct CultNetSchemaRegistry { - entries: BTreeMap, -} - -impl CultNetSchemaRegistry { - pub fn new() -> Self { - Self::default() - } - - pub fn register(&mut self, registration: CultNetSchemaRegistration) -> Result<()> { - if registration.schema_id.trim().is_empty() { - return Err(anyhow!("schema_id must be non-empty")); - } - if registration.wire_contracts.is_empty() { - return Err(anyhow!("wire_contracts must contain at least one entry")); - } - - let content_hash = if let Some(schema_json) = registration.schema_json.as_deref() { - canonical_schema_hash(schema_json)? - } else { - return Err(anyhow!( - "schema_json is required so discovery can advertise canonical hashes" - )); - }; - - self.entries.insert( - registration.schema_id.clone(), - CultNetSchemaDescriptor { - schema_id: registration.schema_id, - kind: registration.kind, - schema_version: registration.schema_version, - document_type: registration.document_type, - title: registration.title, - wire_contracts: registration.wire_contracts, - content_hash, - schema_json: registration.schema_json, - }, - ); - - Ok(()) - } - - pub fn get( - &self, - schema_id: &str, - include_schema_json: bool, - ) -> Option { - self.entries - .get(schema_id) - .map(|entry| sanitize_descriptor(entry, include_schema_json)) - } - - pub fn list(&self, options: &CultNetSchemaCatalogOptions) -> Vec { - let requested_ids = options - .schema_ids - .as_ref() - .map(|values| values.iter().cloned().collect::>()); - let requested_kinds = options - .kinds - .as_ref() - .map(|values| values.iter().copied().collect::>()); - - self.entries - .values() - .filter(|entry| { - if let Some(requested_ids) = &requested_ids - && !requested_ids.contains(&entry.schema_id) - { - return false; - } - - if let Some(requested_kinds) = &requested_kinds - && !requested_kinds.contains(&entry.kind) - { - return false; - } - - true - }) - .map(|entry| sanitize_descriptor(entry, options.include_schema_json)) - .collect() - } - - pub fn create_catalog_response(&self, request: &CultNetMessage) -> Result { - let CultNetMessage::SchemaCatalogRequest { - message_id, - include_schema_json, - schema_ids, - kinds, - } = request - else { - return Err(anyhow!( - "expected cultnet.schema_catalog_request.v0 for schema discovery" - )); - }; - - Ok(CultNetMessage::SchemaCatalogResponse { - message_id: message_id.clone(), - schemas: self.list(&CultNetSchemaCatalogOptions { - include_schema_json: include_schema_json.unwrap_or(false), - schema_ids: schema_ids.clone(), - kinds: kinds.clone(), - }), - }) - } -} - -fn sanitize_descriptor( - descriptor: &CultNetSchemaDescriptor, - include_schema_json: bool, -) -> CultNetSchemaDescriptor { - let mut clone = descriptor.clone(); - if !include_schema_json { - clone.schema_json = None; - } - clone -} - -fn canonical_schema_hash(schema_json: &str) -> Result { - let value: Value = serde_json::from_str(schema_json)?; - let canonical = stable_stringify(&value); - Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) -} - -fn stable_stringify(value: &Value) -> String { - match value { - Value::Null => "null".to_string(), - Value::Bool(value) => value.to_string(), - Value::Number(value) => value.to_string(), - Value::String(value) => serde_json::to_string(value).expect("string serialization"), - Value::Array(values) => format!( - "[{}]", - values - .iter() - .map(stable_stringify) - .collect::>() - .join(",") - ), - Value::Object(values) => { - let mut entries = values.iter().collect::>(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); - format!( - "{{{}}}", - entries - .into_iter() - .map(|(key, value)| { - format!( - "{}:{}", - serde_json::to_string(key).expect("key serialization"), - stable_stringify(value) - ) - }) - .collect::>() - .join(",") - ) - } - } -} - -pub fn builtin_schema_registry() -> Result { - let mut registry = CultNetSchemaRegistry::new(); - - for registration in [ - schema_registration( - include_str!("../contracts/cultnet.hello.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.hello.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-mutation-contract.schema.json"), - CultNetSchemaKind::SharedContract, - vec![CultNetWireContract::CultNetSchemaV0], - None, - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.transport-profile.schema.json"), - CultNetSchemaKind::SharedContract, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.transport_profile.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.login.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.login.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.register.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.register.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.verify.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.verify.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.login-success.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.login_success.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.error.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.error.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.sample-change-name.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.sample.change_name.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.sample-chat.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.sample.chat.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-put.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.document_put.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-delete.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.document_delete.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.raw-document-record.schema.json"), - CultNetSchemaKind::SharedContract, - vec![CultNetWireContract::CultNetSchemaV0], - None, - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-put-raw.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.document_put_raw.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.snapshot-request.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.snapshot_request.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.snapshot-response.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.snapshot_response.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.snapshot-response-raw.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.snapshot_response_raw.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.schema-catalog-request.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.schema_catalog_request.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.schema-catalog-response.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.schema_catalog_response.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.shard-catalog-request.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.shard_catalog_request.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.shard-catalog-response.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.shard_catalog_response.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/ghostlight.agent-state.schema.json"), - CultNetSchemaKind::DocumentPayload, - vec![CultNetWireContract::CultNetSchemaV0], - Some("ghostlight.agent_state.v0"), - Some("ghostlight.agent-state"), - )?, - ] { - registry.register(registration)?; - } - - Ok(registry) -} - -fn schema_registration( - schema_json: &str, - kind: CultNetSchemaKind, - wire_contracts: Vec, - schema_version: Option<&str>, - document_type: Option<&str>, -) -> Result { - let value: Value = serde_json::from_str(schema_json)?; - let schema_id = value - .get("$id") - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("schema is missing $id"))?; - let title = value - .get("title") - .and_then(Value::as_str) - .map(str::to_string); - - Ok(CultNetSchemaRegistration { - schema_id: schema_id.to_string(), - kind, - wire_contracts, - schema_version: schema_version.map(str::to_string), - document_type: document_type.map(str::to_string), - title, - schema_json: Some(schema_json.to_string()), - }) -} diff --git a/vendor/cultnet-rs/src/security.rs b/vendor/cultnet-rs/src/security.rs deleted file mode 100644 index 30aa841..0000000 --- a/vendor/cultnet-rs/src/security.rs +++ /dev/null @@ -1,270 +0,0 @@ -use aes_gcm::Aes256Gcm; -use aes_gcm::KeyInit; -use aes_gcm::Nonce; -use aes_gcm::aead::Aead; -use aes_gcm::aead::Payload; -use anyhow::Result; -use anyhow::anyhow; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use chrono::DateTime; -use chrono::TimeZone; -use chrono::Utc; -use hmac::Hmac; -use hmac::Mac; -use rand::RngCore; -use sha2::Digest; -use sha2::Sha256; - -const NONCE_LENGTH: usize = 12; -type HmacSha256 = Hmac; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetClientSecurityOptions { - pub connection_key: String, - encryption_key: [u8; 32], -} - -impl CultNetClientSecurityOptions { - pub fn new(connection_key: impl Into) -> Result { - let connection_key = connection_key.into(); - if connection_key.trim().is_empty() { - return Err(anyhow!("Connection key must be provided")); - } - let encryption_key = sha256(connection_key.as_bytes()); - Ok(Self { - connection_key, - encryption_key, - }) - } - - pub fn development() -> Self { - Self::new("gamecult-dev-connection-key").expect("development key is valid") - } - - pub fn encryption_key(&self) -> [u8; 32] { - self.encryption_key - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetServerSecurityOptions { - client: CultNetClientSecurityOptions, - pub session_signing_secret: String, - session_signing_key: [u8; 32], - pub is_development: bool, -} - -impl CultNetServerSecurityOptions { - pub fn new( - connection_key: impl Into, - session_signing_secret: impl Into, - is_development: bool, - ) -> Result { - let session_signing_secret = session_signing_secret.into(); - if session_signing_secret.trim().is_empty() { - return Err(anyhow!("Session signing secret must be provided")); - } - Ok(Self { - client: CultNetClientSecurityOptions::new(connection_key)?, - session_signing_key: sha256(session_signing_secret.as_bytes()), - session_signing_secret, - is_development, - }) - } - - pub fn development() -> Self { - Self::new( - "gamecult-dev-connection-key", - "gamecult-dev-session-signing-secret", - true, - ) - .expect("development security config is valid") - } - - pub fn to_client_options(&self) -> CultNetClientSecurityOptions { - self.client.clone() - } - - pub fn encryption_key(&self) -> [u8; 32] { - self.client.encryption_key() - } - - pub fn session_signing_key(&self) -> [u8; 32] { - self.session_signing_key - } -} - -pub trait CultNetEncryptionOptions { - fn encryption_key(&self) -> [u8; 32]; -} - -impl CultNetEncryptionOptions for CultNetClientSecurityOptions { - fn encryption_key(&self) -> [u8; 32] { - self.encryption_key() - } -} - -impl CultNetEncryptionOptions for CultNetServerSecurityOptions { - fn encryption_key(&self) -> [u8; 32] { - self.encryption_key() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ValidatedCultNetSessionToken { - pub user_id: String, - pub expires_at_utc: DateTime, -} - -pub struct CultNetSecret; - -impl CultNetSecret { - pub fn new_nonce() -> [u8; NONCE_LENGTH] { - let mut nonce = [0_u8; NONCE_LENGTH]; - rand::rng().fill_bytes(&mut nonce); - nonce - } - - pub fn encrypt_string( - input: Option<&str>, - nonce: &[u8], - options: &impl CultNetEncryptionOptions, - ) -> Result>> { - let Some(input) = input.filter(|value| !value.is_empty()) else { - return Ok(None); - }; - Ok(Some(Self::encrypt_bytes(input.as_bytes(), nonce, options)?)) - } - - pub fn decrypt_string( - encrypted: Option<&[u8]>, - nonce: Option<&[u8]>, - options: &impl CultNetEncryptionOptions, - ) -> Result> { - let (Some(encrypted), Some(nonce)) = (encrypted, nonce) else { - return Ok(None); - }; - let bytes = Self::decrypt_bytes(encrypted, nonce, options)?; - Ok(Some(String::from_utf8(bytes)?)) - } - - pub fn encrypt_bytes( - input: &[u8], - nonce: &[u8], - options: &impl CultNetEncryptionOptions, - ) -> Result> { - let nonce = validate_nonce(nonce)?; - let cipher = Aes256Gcm::new_from_slice(&options.encryption_key())?; - let encrypted = cipher - .encrypt( - Nonce::from_slice(nonce), - Payload { - msg: input, - aad: &[], - }, - ) - .map_err(|_| anyhow!("AES-GCM encryption failed"))?; - Ok(encrypted) - } - - pub fn decrypt_bytes( - encrypted: &[u8], - nonce: &[u8], - options: &impl CultNetEncryptionOptions, - ) -> Result> { - let nonce = validate_nonce(nonce)?; - let cipher = Aes256Gcm::new_from_slice(&options.encryption_key())?; - cipher - .decrypt( - Nonce::from_slice(nonce), - Payload { - msg: encrypted, - aad: &[], - }, - ) - .map_err(|_| anyhow!("AES-GCM decryption failed")) - } - - pub fn create_session_token( - user_id: &str, - expires_at_utc: DateTime, - options: &CultNetServerSecurityOptions, - ) -> Result { - let payload = format!("{}|{}", user_id, expires_at_utc.timestamp()); - let signature = hmac_sha256(&options.session_signing_key(), payload.as_bytes())?; - Ok(format!( - "{}.{}", - URL_SAFE_NO_PAD.encode(payload.as_bytes()), - URL_SAFE_NO_PAD.encode(signature) - )) - } - - pub fn try_validate_session_token( - token: Option<&str>, - options: &CultNetServerSecurityOptions, - ) -> Result> { - let Some(token) = token.filter(|value| !value.trim().is_empty()) else { - return Ok(None); - }; - let Some((payload_part, signature_part)) = token.split_once('.') else { - return Ok(None); - }; - let payload = URL_SAFE_NO_PAD.decode(payload_part)?; - let signature = URL_SAFE_NO_PAD.decode(signature_part)?; - let expected = hmac_sha256(&options.session_signing_key(), &payload)?; - if !constant_time_eq(&signature, &expected) { - return Ok(None); - } - let payload = String::from_utf8(payload)?; - let Some((user_id, expires_at)) = payload.split_once('|') else { - return Ok(None); - }; - let expires_at_seconds: i64 = expires_at.parse()?; - let Some(expires_at_utc) = Utc.timestamp_opt(expires_at_seconds, 0).single() else { - return Ok(None); - }; - if expires_at_utc <= Utc::now() { - return Ok(None); - } - Ok(Some(ValidatedCultNetSessionToken { - user_id: user_id.to_string(), - expires_at_utc, - })) - } - - pub fn to_base64_url(input: &[u8]) -> String { - URL_SAFE_NO_PAD.encode(input) - } - - pub fn from_base64_url(input: &str) -> Result> { - Ok(URL_SAFE_NO_PAD.decode(input)?) - } -} - -fn validate_nonce(nonce: &[u8]) -> Result<&[u8]> { - if nonce.len() != NONCE_LENGTH { - return Err(anyhow!("Invalid nonce")); - } - Ok(nonce) -} - -fn sha256(input: &[u8]) -> [u8; 32] { - Sha256::digest(input).into() -} - -fn hmac_sha256(key: &[u8], input: &[u8]) -> Result> { - let mut mac = ::new_from_slice(key)?; - mac.update(input); - Ok(mac.finalize().into_bytes().to_vec()) -} - -fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { - if left.len() != right.len() { - return false; - } - left.iter() - .zip(right) - .fold(0_u8, |acc, (left, right)| acc | (left ^ right)) - == 0 -} diff --git a/vendor/cultnet-rs/src/service_identity.rs b/vendor/cultnet-rs/src/service_identity.rs deleted file mode 100644 index 9160b43..0000000 --- a/vendor/cultnet-rs/src/service_identity.rs +++ /dev/null @@ -1,705 +0,0 @@ -use anyhow::{Context, Result, anyhow, bail}; -use cultcache_rs::{CacheBackingStore, CultCacheEnvelope, SingleFileMessagePackBackingStore}; -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; -use rand::RngCore; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::io::Write; -use std::marker::PhantomData; -use std::path::Path; - -pub const WINDOWS_SERVICE_IDENTITY_ASSURANCE: &str = "os_user_installation_bound_best_effort"; -pub const LINUX_SERVICE_IDENTITY_ASSURANCE: &str = "os_installation_file_bound_cloneable_baseline"; - -/// Fixes every authority-bearing name at the call site. A service cannot open -/// another service's identity by supplying a convenient runtime string. -pub trait ServiceIdentityProfile: Send + Sync + 'static { - const PRIVATE_TYPE: &'static str; - const PRIVATE_SCHEMA: &'static str; - const PRIVATE_KEY: &'static str; - const TRUST_ANCHOR_TYPE: &'static str; - const TRUST_ANCHOR_SCHEMA: &'static str; - const TRUST_ANCHOR_KEY: &'static str; - const ID_DOMAIN: &'static [u8]; - const SIGNATURE_DOMAIN: &'static [u8]; - const PROTECTOR_CONTEXT: &'static str; -} - -/// A purpose is a type, not caller-provided prose. Protocol implementations -/// define one zero-sized type per signed statement kind. -pub trait ServiceSignaturePurpose: Send + Sync + 'static { - const PURPOSE: &'static [u8]; -} - -pub enum IdunnServiceIdentity {} - -/// Root-operated identity dedicated to deployment-brake release grants. The -/// Idunn daemon receives only this profile's public trust anchor; opening the -/// private store is a provisioning operation, never daemon startup work. -pub enum IdunnDeploymentBrakeOperatorIdentity {} - -pub struct IdunnDeploymentBrakeReleasePurpose; - -impl ServiceSignaturePurpose - for IdunnDeploymentBrakeReleasePurpose -{ - const PURPOSE: &'static [u8] = b"idunn.deployment-release.v1"; -} - -impl ServiceIdentityProfile for IdunnDeploymentBrakeOperatorIdentity { - const PRIVATE_TYPE: &'static str = "idunn.deployment_brake_operator.private.v1"; - const PRIVATE_SCHEMA: &'static str = "idunn.deployment_brake_operator.private.v1"; - const PRIVATE_KEY: &'static str = "idunn-deployment-brake-operator"; - const TRUST_ANCHOR_TYPE: &'static str = "idunn.deployment_brake_operator.trust_anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "idunn.deployment_brake_operator.trust_anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "idunn-deployment-brake-operator-public"; - const ID_DOMAIN: &'static [u8] = b"idunn.deployment-brake-operator.id.v1\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"idunn.deployment-brake-operator.signature.v1\0"; - const PROTECTOR_CONTEXT: &'static str = "idunn-deployment-brake-operator-v1"; -} - -/// Dedicated provider-owned identity for generic daemon-health statements. -/// It is intentionally independent of any one provider runtime or repository. -pub enum GameCultProviderHealthIdentity {} - -/// The only generic health statement purpose accepted from provider-health -/// identities by Idunn admission. -pub struct IdunnSignedDaemonHealthPurpose; - -impl ServiceSignaturePurpose for IdunnSignedDaemonHealthPurpose { - const PURPOSE: &'static [u8] = b"idunn.signed_daemon_health.v1"; -} - -impl ServiceIdentityProfile for GameCultProviderHealthIdentity { - const PRIVATE_TYPE: &'static str = "gamecult.provider_health_identity.private.v1"; - const PRIVATE_SCHEMA: &'static str = "gamecult.provider_health_identity.private.v1"; - const PRIVATE_KEY: &'static str = "gamecult-provider-health-identity"; - const TRUST_ANCHOR_TYPE: &'static str = "gamecult.provider_health_identity.trust_anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "gamecult.provider_health_identity.trust_anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "gamecult-provider-health-identity-public"; - const ID_DOMAIN: &'static [u8] = b"gamecult.provider-health.identity.v1\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"gamecult.provider-health.signature.v1\0"; - const PROTECTOR_CONTEXT: &'static str = "gamecult-provider-health-identity-v1"; -} - -/// The only signing purpose accepted for Idunn's public projection of a -/// provider-authenticated health admission. Keeping this profile here makes -/// purpose selection a compile-time protocol choice rather than caller text. -pub struct IdunnAuthenticatedProviderHealthProjectionPurpose; - -impl ServiceSignaturePurpose - for IdunnAuthenticatedProviderHealthProjectionPurpose -{ - const PURPOSE: &'static [u8] = b"idunn.authenticated-provider-health-projection.v1"; -} - -impl ServiceIdentityProfile for IdunnServiceIdentity { - const PRIVATE_TYPE: &'static str = "idunn.service_identity.private.v1"; - const PRIVATE_SCHEMA: &'static str = "idunn.service_identity.private.v1"; - const PRIVATE_KEY: &'static str = "idunn-service-identity"; - const TRUST_ANCHOR_TYPE: &'static str = "idunn.service_identity.trust_anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "idunn.service_identity.trust_anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "idunn-service-identity-public"; - const ID_DOMAIN: &'static [u8] = b"idunn.service-identity.id.v1\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"idunn.service-identity.signature.v1\0"; - const PROTECTOR_CONTEXT: &'static str = "idunn-service-identity-v1"; -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ServiceIdentityPrivateEntry { - pub schema_version: String, - pub identity_id: String, - pub public_key: Vec, - pub protected_private_seed: Vec, - pub protector_kind: String, - pub protector_binding: String, - pub protector_version: String, - pub assurance: String, - pub created_at: String, - pub enrollment_nonce: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ServiceIdentityTrustAnchor { - pub schema_version: String, - pub identity_id: String, - pub public_key: Vec, - pub assurance: String, - pub identity_created_at: String, - pub source_identity_record_sha256: String, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ServiceIdentitySignature { - pub identity_id: String, - pub signature: Vec, -} - -/// The only value holding unprotected key material. It exposes signing, never -/// seed export or access to the underlying signing key. -pub struct ServiceIdentitySigner { - entry: ServiceIdentityPrivateEntry, - signing_key: SigningKey, - profile: PhantomData

, -} - -impl ServiceIdentitySigner

{ - pub fn entry(&self) -> &ServiceIdentityPrivateEntry { - &self.entry - } - - pub fn trust_anchor(&self) -> Result { - Ok(ServiceIdentityTrustAnchor { - schema_version: P::TRUST_ANCHOR_SCHEMA.into(), - identity_id: self.entry.identity_id.clone(), - public_key: self.entry.public_key.clone(), - assurance: self.entry.assurance.clone(), - identity_created_at: self.entry.created_at.clone(), - source_identity_record_sha256: format!( - "sha256-{}", - hex(&Sha256::digest(rmp_serde::to_vec(&self.entry)?)) - ), - }) - } - - pub fn sign>(&self, payload: &[u8]) -> ServiceIdentitySignature { - ServiceIdentitySignature { - identity_id: self.entry.identity_id.clone(), - signature: self - .signing_key - .sign(&signing_message::(payload)) - .to_bytes() - .to_vec(), - } - } -} - -pub fn enroll_service_identity_at( - path: &Path, -) -> Result> { - if path.exists() { - bail!( - "service identity store {} already exists; enrollment is immutable", - path.display() - ); - } - prepare_parent(path)?; - let mut seed = [0u8; 32]; - rand::rng().fill_bytes(&mut seed); - let signing_key = SigningKey::from_bytes(&seed); - let mut nonce = [0u8; 32]; - rand::rng().fill_bytes(&mut nonce); - let public_key = signing_key.verifying_key().to_bytes(); - let binding = platform_binding::

()?; - let entry = ServiceIdentityPrivateEntry { - schema_version: P::PRIVATE_SCHEMA.into(), - identity_id: identity_id::

(&public_key), - public_key: public_key.to_vec(), - protected_private_seed: protect_seed::

(&seed, &binding)?, - protector_kind: platform_protector_kind().into(), - protector_binding: binding, - protector_version: "v1".into(), - assurance: platform_assurance().into(), - created_at: chrono::Utc::now().to_rfc3339(), - enrollment_nonce: nonce.to_vec(), - }; - validate_private::

(&entry)?; - let envelope = private_envelope::

(&entry)?; - atomic_create_private_store(path, &envelope)?; - harden_store_permissions(path)?; - open_service_identity_at::

(path) -} - -pub fn open_service_identity_at( - path: &Path, -) -> Result> { - if !path.is_file() { - bail!("service identity store {} does not exist", path.display()); - } - let entries = SingleFileMessagePackBackingStore::new(path).pull_all()?; - if entries.len() != 1 { - bail!("service identity store must contain exactly one immutable envelope"); - } - let envelope = &entries[0]; - if envelope.r#type != P::PRIVATE_TYPE - || envelope.key != P::PRIVATE_KEY - || envelope.schema_id.as_deref() != Some(P::PRIVATE_SCHEMA) - { - bail!("service identity store belongs to a different profile or schema"); - } - let entry: ServiceIdentityPrivateEntry = rmp_serde::from_slice(&envelope.payload) - .context("service identity private payload is malformed MessagePack")?; - validate_private::

(&entry)?; - if entry.protector_kind != platform_protector_kind() || entry.assurance != platform_assurance() - { - bail!("service identity protector does not belong to this platform implementation"); - } - let binding = platform_binding::

()?; - if entry.protector_binding != binding { - bail!("service identity protector binding does not match this OS installation or profile"); - } - let seed: [u8; 32] = unprotect_seed::

(&entry.protected_private_seed, &binding)? - .try_into() - .map_err(|_| anyhow!("unprotected service identity seed has invalid length"))?; - let signing_key = SigningKey::from_bytes(&seed); - if signing_key.verifying_key().to_bytes().as_slice() != entry.public_key.as_slice() { - bail!("service identity private seed does not match enrolled public key"); - } - Ok(ServiceIdentitySigner { - entry, - signing_key, - profile: PhantomData, - }) -} - -pub fn export_service_identity_trust_anchor( - signer: &ServiceIdentitySigner

, - path: &Path, -) -> Result { - let anchor = signer.trust_anchor()?; - prepare_parent(path)?; - let envelope = CultCacheEnvelope { - key: P::TRUST_ANCHOR_KEY.into(), - r#type: P::TRUST_ANCHOR_TYPE.into(), - payload: rmp_serde::to_vec(&anchor)?, - stored_at: anchor.identity_created_at.clone(), - schema_id: Some(P::TRUST_ANCHOR_SCHEMA.into()), - }; - let mut backing = SingleFileMessagePackBackingStore::new(path); - match backing.pull_all()?.as_slice() { - [] => backing.push(&envelope)?, - [current] if current == &envelope => {} - _ => bail!("service identity trust anchor output already contains different state"), - } - Ok(anchor) -} - -pub fn verify_service_identity_signature( - anchor: &ServiceIdentityTrustAnchor, - payload: &[u8], - proof: &ServiceIdentitySignature, -) -> Result<()> -where - P: ServiceIdentityProfile, - S: ServiceSignaturePurpose

, -{ - validate_anchor::

(anchor)?; - if proof.identity_id != anchor.identity_id { - bail!("service identity signature names a different identity"); - } - let key: [u8; 32] = anchor - .public_key - .as_slice() - .try_into() - .map_err(|_| anyhow!("service identity public key has invalid length"))?; - let signature = Signature::from_slice(&proof.signature) - .map_err(|_| anyhow!("service identity signature has invalid length"))?; - VerifyingKey::from_bytes(&key)? - .verify(&signing_message::(payload), &signature) - .map_err(|_| anyhow!("service identity signature verification failed")) -} - -/// Verify against a public key already admitted by an owning trust store. -/// The profile derives the identity, so callers cannot substitute a convenient -/// identity string while retaining the same signature bytes. -pub fn verify_service_identity_signature_with_public_key( - public_key: &[u8], - payload: &[u8], - proof: &ServiceIdentitySignature, -) -> Result<()> -where - P: ServiceIdentityProfile, - S: ServiceSignaturePurpose

, -{ - if proof.identity_id != derive_service_identity_id::

(public_key)? { - bail!("service identity signature names a different identity"); - } - let key: [u8; 32] = public_key - .try_into() - .map_err(|_| anyhow!("service identity public key has invalid length"))?; - let signature = Signature::from_slice(&proof.signature) - .map_err(|_| anyhow!("service identity signature has invalid length"))?; - VerifyingKey::from_bytes(&key)? - .verify(&signing_message::(payload), &signature) - .map_err(|_| anyhow!("service identity signature verification failed")) -} - -fn validate_private(entry: &ServiceIdentityPrivateEntry) -> Result<()> { - if entry.schema_version != P::PRIVATE_SCHEMA - || entry.public_key.len() != 32 - || entry.enrollment_nonce.len() != 32 - || entry.protected_private_seed.is_empty() - || entry.protector_version != "v1" - { - bail!("service identity private entry violates its profile schema"); - } - chrono::DateTime::parse_from_rfc3339(&entry.created_at) - .map_err(|_| anyhow!("service identity created_at is not RFC3339"))?; - if identity_id::

(&entry.public_key) != entry.identity_id { - bail!("service identity id does not match public key"); - } - Ok(()) -} - -fn validate_anchor(anchor: &ServiceIdentityTrustAnchor) -> Result<()> { - if anchor.schema_version != P::TRUST_ANCHOR_SCHEMA - || anchor.public_key.len() != 32 - || derive_service_identity_id::

(&anchor.public_key)? != anchor.identity_id - { - bail!("service identity trust anchor violates its profile schema"); - } - Ok(()) -} - -/// Derives the profile-bound identity id from an exact Ed25519 public key. -/// Consumers use this instead of accepting a caller-supplied identity string. -pub fn derive_service_identity_id(public_key: &[u8]) -> Result { - if public_key.len() != 32 { - bail!("service identity public key has invalid length"); - } - Ok(hex(&Sha256::digest([P::ID_DOMAIN, public_key].concat()))) -} - -fn identity_id(key: &[u8]) -> String { - derive_service_identity_id::

(key).expect("internal service identity key is Ed25519") -} - -fn signing_message>( - payload: &[u8], -) -> Vec { - let mut out = - Vec::with_capacity(P::SIGNATURE_DOMAIN.len() + S::PURPOSE.len() + payload.len() + 16); - out.extend_from_slice(P::SIGNATURE_DOMAIN); - out.extend_from_slice(&(S::PURPOSE.len() as u64).to_be_bytes()); - out.extend_from_slice(S::PURPOSE); - out.extend_from_slice(&(payload.len() as u64).to_be_bytes()); - out.extend_from_slice(payload); - out -} - -fn private_envelope( - entry: &ServiceIdentityPrivateEntry, -) -> Result { - Ok(CultCacheEnvelope { - key: P::PRIVATE_KEY.into(), - r#type: P::PRIVATE_TYPE.into(), - payload: rmp_serde::to_vec(entry)?, - stored_at: entry.created_at.clone(), - schema_id: Some(P::PRIVATE_SCHEMA.into()), - }) -} - -fn prepare_parent(path: &Path) -> Result<()> { - let parent = path - .parent() - .ok_or_else(|| anyhow!("service identity path has no parent"))?; - std::fs::create_dir_all(parent)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?; - } - Ok(()) -} - -/// Enrollment is a one-shot authority transition. `create_new` makes two -/// simultaneous enrollments unable to overwrite one another, while the encoded -/// envelope vector remains a supported CultCache single-file representation. -fn atomic_create_private_store(path: &Path, envelope: &CultCacheEnvelope) -> Result<()> { - let bytes = rmp_serde::to_vec(&vec![envelope.clone()]) - .context("failed to encode immutable service identity store")?; - let mut file = std::fs::OpenOptions::new() - .create_new(true) - .write(true) - .open(path) - .with_context(|| { - format!( - "service identity store {} already exists or cannot be created; enrollment is immutable", - path.display() - ) - })?; - if let Err(error) = file.write_all(&bytes).and_then(|()| file.sync_all()) { - drop(file); - let _ = std::fs::remove_file(path); - return Err(error).context("failed to persist immutable service identity store"); - } - Ok(()) -} -fn harden_store_permissions(_path: &Path) -> Result<()> { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(_path, std::fs::Permissions::from_mode(0o600))?; - } - Ok(()) -} - -#[cfg(windows)] -fn platform_protector_kind() -> &'static str { - "windows_dpapi_current_user" -} -#[cfg(target_os = "linux")] -fn platform_protector_kind() -> &'static str { - "linux_file_mode_machine_id_binding" -} -#[cfg(windows)] -fn platform_assurance() -> &'static str { - WINDOWS_SERVICE_IDENTITY_ASSURANCE -} -#[cfg(target_os = "linux")] -fn platform_assurance() -> &'static str { - LINUX_SERVICE_IDENTITY_ASSURANCE -} - -#[cfg(windows)] -fn platform_binding() -> Result { - Ok(format!("dpapi-current-user:{}", P::PROTECTOR_CONTEXT)) -} -#[cfg(target_os = "linux")] -fn platform_binding() -> Result { - let raw = std::fs::read_to_string("/etc/machine-id") - .or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id")) - .context("Linux machine-id is unavailable")?; - let id = raw.trim(); - if id.is_empty() { - bail!("Linux machine-id is empty"); - } - Ok(format!( - "{}:machine-id-sha256:{}", - P::PROTECTOR_CONTEXT, - hex(&Sha256::digest(id.as_bytes())) - )) -} - -#[cfg(windows)] -fn protect_seed(seed: &[u8; 32], binding: &str) -> Result> { - dpapi::

(seed, binding, true) -} -#[cfg(windows)] -fn unprotect_seed(seed: &[u8], binding: &str) -> Result> { - dpapi::

(seed, binding, false) -} -#[cfg(windows)] -fn dpapi(input: &[u8], binding: &str, protect: bool) -> Result> { - use windows_sys::Win32::Foundation::LocalFree; - use windows_sys::Win32::Security::Cryptography::{ - CRYPT_INTEGER_BLOB, CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData, CryptUnprotectData, - }; - let mut input = input.to_vec(); - let mut entropy = Sha256::digest( - [ - b"gamecult-service-identity-dpapi-v1\0".as_slice(), - P::PROTECTOR_CONTEXT.as_bytes(), - binding.as_bytes(), - ] - .concat(), - ) - .to_vec(); - let ib = CRYPT_INTEGER_BLOB { - cbData: input.len() as u32, - pbData: input.as_mut_ptr(), - }; - let eb = CRYPT_INTEGER_BLOB { - cbData: entropy.len() as u32, - pbData: entropy.as_mut_ptr(), - }; - let mut output = CRYPT_INTEGER_BLOB { - cbData: 0, - pbData: std::ptr::null_mut(), - }; - let ok = unsafe { - if protect { - CryptProtectData( - &ib, - std::ptr::null(), - &eb, - std::ptr::null(), - std::ptr::null(), - CRYPTPROTECT_UI_FORBIDDEN, - &mut output, - ) - } else { - CryptUnprotectData( - &ib, - std::ptr::null_mut(), - &eb, - std::ptr::null(), - std::ptr::null(), - CRYPTPROTECT_UI_FORBIDDEN, - &mut output, - ) - } - }; - if ok == 0 { - return Err(std::io::Error::last_os_error()) - .context("DPAPI service identity operation failed"); - } - let bytes = - unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec() }; - unsafe { - LocalFree(output.pbData.cast()); - } - Ok(bytes) -} - -#[cfg(target_os = "linux")] -fn protect_seed(seed: &[u8; 32], binding: &str) -> Result> { - xor_linux::

(seed, binding) -} -#[cfg(target_os = "linux")] -fn unprotect_seed(seed: &[u8], binding: &str) -> Result> { - if seed.len() != 32 { - bail!("protected Linux service seed has invalid length"); - } - xor_linux::

(seed, binding) -} -#[cfg(target_os = "linux")] -fn xor_linux(seed: &[u8], binding: &str) -> Result> { - let mask = Sha256::digest( - [ - b"gamecult-linux-service-seed-v1\0".as_slice(), - P::PROTECTOR_CONTEXT.as_bytes(), - binding.as_bytes(), - ] - .concat(), - ); - Ok(seed.iter().zip(mask).map(|(a, b)| a ^ b).collect()) -} - -fn hex(bytes: &[u8]) -> String { - const D: &[u8; 16] = b"0123456789abcdef"; - let mut s = String::with_capacity(bytes.len() * 2); - for b in bytes { - s.push(D[(b >> 4) as usize] as char); - s.push(D[(b & 15) as usize] as char); - } - s -} - -#[cfg(test)] -mod tests { - use super::*; - enum OtherPurpose {} - impl ServiceSignaturePurpose for OtherPurpose { - const PURPOSE: &'static [u8] = b"idunn.other.v1"; - } - - #[test] - fn immutable_enrollment_reopens_and_is_purpose_and_payload_bound() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("idunn.ccmp"); - let signer = enroll_service_identity_at::(&path)?; - let proof = signer.sign::(b"healthy"); - let anchor = signer.trust_anchor()?; - verify_service_identity_signature::< - IdunnServiceIdentity, - IdunnAuthenticatedProviderHealthProjectionPurpose, - >(&anchor, b"healthy", &proof)?; - assert!( - verify_service_identity_signature::< - IdunnServiceIdentity, - IdunnAuthenticatedProviderHealthProjectionPurpose, - >(&anchor, b"unhealthy", &proof) - .is_err() - ); - assert!( - verify_service_identity_signature::( - &anchor, b"healthy", &proof - ) - .is_err() - ); - assert!(enroll_service_identity_at::(&path).is_err()); - assert_eq!( - derive_service_identity_id::(&anchor.public_key)?, - anchor.identity_id - ); - assert!(derive_service_identity_id::(&[0; 31]).is_err()); - assert_eq!( - open_service_identity_at::(&path)?.entry(), - signer.entry() - ); - Ok(()) - } - - #[test] - fn malformed_or_profile_substituted_state_fails_closed_without_replacement() -> Result<()> { - enum Alien {} - impl ServiceIdentityProfile for Alien { - const PRIVATE_TYPE: &'static str = "alien.private.v1"; - const PRIVATE_SCHEMA: &'static str = "alien.private.v1"; - const PRIVATE_KEY: &'static str = "alien"; - const TRUST_ANCHOR_TYPE: &'static str = "alien.anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "alien.anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "alien-public"; - const ID_DOMAIN: &'static [u8] = b"alien.id\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"alien.sig\0"; - const PROTECTOR_CONTEXT: &'static str = "alien-v1"; - } - let temp = tempfile::tempdir()?; - let malformed = temp.path().join("bad.ccmp"); - std::fs::write(&malformed, b"rot")?; - let before = std::fs::read(&malformed)?; - assert!(open_service_identity_at::(&malformed).is_err()); - assert!(enroll_service_identity_at::(&malformed).is_err()); - assert_eq!(before, std::fs::read(&malformed)?); - let good = temp.path().join("good.ccmp"); - enroll_service_identity_at::(&good)?; - assert!(open_service_identity_at::(&good).is_err()); - Ok(()) - } - - #[test] - fn exported_anchor_is_public_only_immutable_and_rejects_identity_substitution() -> Result<()> { - let temp = tempfile::tempdir()?; - let signer = - enroll_service_identity_at::(&temp.path().join("private.ccmp"))?; - let output = temp.path().join("public.ccmp"); - let anchor = export_service_identity_trust_anchor(&signer, &output)?; - let bytes = std::fs::read(&output)?; - assert!( - !bytes - .windows(signer.entry().protected_private_seed.len()) - .any(|w| w == signer.entry().protected_private_seed) - ); - export_service_identity_trust_anchor(&signer, &output)?; - assert_eq!(bytes, std::fs::read(&output)?); - let other = - enroll_service_identity_at::(&temp.path().join("other.ccmp"))?; - let proof = other.sign::(b"healthy"); - assert!( - verify_service_identity_signature::< - IdunnServiceIdentity, - IdunnAuthenticatedProviderHealthProjectionPurpose, - >(&anchor, b"healthy", &proof) - .is_err() - ); - Ok(()) - } - - #[test] - fn provider_health_profile_is_domain_separated_and_purpose_bound() -> Result<()> { - let temp = tempfile::tempdir()?; - let signer = enroll_service_identity_at::( - &temp.path().join("provider-health.cc"), - )?; - let proof = signer.sign::(b"signed-health-record"); - let anchor = signer.trust_anchor()?; - verify_service_identity_signature::< - GameCultProviderHealthIdentity, - IdunnSignedDaemonHealthPurpose, - >(&anchor, b"signed-health-record", &proof)?; - verify_service_identity_signature_with_public_key::< - GameCultProviderHealthIdentity, - IdunnSignedDaemonHealthPurpose, - >(&anchor.public_key, b"signed-health-record", &proof)?; - assert_eq!( - anchor.schema_version, - "gamecult.provider_health_identity.trust_anchor.v1" - ); - assert_ne!( - derive_service_identity_id::(&anchor.public_key)?, - derive_service_identity_id::(&anchor.public_key)? - ); - Ok(()) - } -} diff --git a/vendor/cultnet-rs/src/shard_catalog.rs b/vendor/cultnet-rs/src/shard_catalog.rs deleted file mode 100644 index a112246..0000000 --- a/vendor/cultnet-rs/src/shard_catalog.rs +++ /dev/null @@ -1,134 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use std::collections::BTreeMap; -use std::collections::BTreeSet; - -use crate::CultNetMessage; -use crate::CultNetShardDescriptor; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetShardCatalogOptions { - pub schema_ids: Option>, - pub record_keys: Option>, -} - -#[derive(Clone, Debug, Default)] -pub struct CultNetShardCatalog { - shards: BTreeMap, -} - -impl CultNetShardCatalog { - pub fn new() -> Self { - Self::default() - } - - pub fn upsert(&mut self, descriptor: CultNetShardDescriptor) -> Result<()> { - validate_descriptor(&descriptor)?; - self.shards.insert(descriptor.shard_id.clone(), descriptor); - Ok(()) - } - - pub fn get(&self, shard_id: &str) -> Option<&CultNetShardDescriptor> { - self.shards.get(shard_id) - } - - pub fn list(&self, options: &CultNetShardCatalogOptions) -> Vec { - let schema_ids = options - .schema_ids - .as_ref() - .map(|values| values.iter().cloned().collect::>()); - let record_keys = options - .record_keys - .as_ref() - .map(|values| values.iter().cloned().collect::>()); - - self.shards - .values() - .filter(|shard| { - let schema_match = schema_ids.as_ref().is_none_or(|requested| { - requested - .iter() - .any(|schema_id| shard.serves(Some(schema_id), None)) - }); - let key_match = record_keys.as_ref().is_none_or(|requested| { - requested - .iter() - .any(|record_key| shard.serves(None, Some(record_key))) - }); - schema_match && key_match - }) - .cloned() - .collect() - } - - pub fn create_catalog_response(&self, request: &CultNetMessage) -> Result { - let CultNetMessage::ShardCatalogRequest { - message_id, - schema_ids, - record_keys, - } = request - else { - return Err(anyhow!( - "expected cultnet.shard_catalog_request.v0 for shard discovery" - )); - }; - - Ok(CultNetMessage::ShardCatalogResponse { - message_id: message_id.clone(), - shards: self.list(&CultNetShardCatalogOptions { - schema_ids: schema_ids.clone(), - record_keys: record_keys.clone(), - }), - }) - } - - pub fn apply_response( - &mut self, - response: &CultNetMessage, - ) -> Result> { - let CultNetMessage::ShardCatalogResponse { shards, .. } = response else { - return Err(anyhow!( - "expected cultnet.shard_catalog_response.v0 for shard discovery" - )); - }; - - let mut applied = Vec::with_capacity(shards.len()); - for shard in shards { - self.upsert(shard.clone())?; - applied.push(shard.clone()); - } - Ok(applied) - } -} - -fn validate_descriptor(descriptor: &CultNetShardDescriptor) -> Result<()> { - if descriptor.shard_id.trim().is_empty() { - return Err(anyhow!("shard_id must be non-empty")); - } - if descriptor.owner_runtime_id.trim().is_empty() { - return Err(anyhow!("owner_runtime_id must be non-empty")); - } - for schema_id in &descriptor.schema_ids { - if schema_id.trim().is_empty() { - return Err(anyhow!("schema_ids must not contain empty values")); - } - } - if descriptor - .key_prefix - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(anyhow!("key_prefix must be non-empty when present")); - } - for endpoint in descriptor - .primary_endpoints - .iter() - .chain(descriptor.replica_endpoints.iter()) - .chain(descriptor.read_replica_endpoints.iter()) - { - if endpoint.trim().is_empty() { - return Err(anyhow!("shard endpoints must not contain empty values")); - } - } - Ok(()) -} diff --git a/vendor/cultnet-rs/src/transport.rs b/vendor/cultnet-rs/src/transport.rs deleted file mode 100644 index 7bb203c..0000000 --- a/vendor/cultnet-rs/src/transport.rs +++ /dev/null @@ -1,288 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use std::io::Read; -use std::io::Write; - -use crate::CultNetReconnectPolicy; -use crate::CultNetTransportChannel; -use crate::CultNetTransportDelivery; -use crate::CultNetTransportDescriptor; -use crate::CultNetTransportOrdering; -use crate::CultNetTransportProfile; -use crate::CultNetTransportProtocol; -use crate::FRAME_HEADER_BYTES; -use crate::encode_frame; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetTransportStats { - pub bytes_received: u64, - pub bytes_sent: u64, - pub frames_received: u64, - pub frames_sent: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetTransportFrame { - pub channel_id: String, - pub payload: Vec, -} - -#[derive(Clone, Debug)] -pub struct CultNetReconnectPolicyOptions { - pub policy_id: String, - pub base_delay_ms: u64, - pub max_delay_ms: u64, - pub max_jitter_ms: u64, - pub max_attempts: Option, -} - -impl Default for CultNetReconnectPolicyOptions { - fn default() -> Self { - Self { - policy_id: "default".to_string(), - base_delay_ms: 1_000, - max_delay_ms: 30_000, - max_jitter_ms: 250, - max_attempts: None, - } - } -} - -pub fn create_reconnect_policy(options: CultNetReconnectPolicyOptions) -> CultNetReconnectPolicy { - CultNetReconnectPolicy { - schema_version: "cultnet.reconnect_policy.v0".to_string(), - policy_id: if options.policy_id.trim().is_empty() { - "default".to_string() - } else { - options.policy_id - }, - base_delay_ms: options.base_delay_ms, - max_delay_ms: options.max_delay_ms, - max_jitter_ms: options.max_jitter_ms, - max_attempts: options.max_attempts, - } -} - -pub fn compute_reconnect_delay_ms( - policy: &CultNetReconnectPolicy, - attempt: u32, - jitter_ms: u64, -) -> u64 { - let normalized_attempt = attempt.max(1); - let multiplier = 2_u64.saturating_pow(normalized_attempt.saturating_sub(1)); - let capped_base_delay = policy - .base_delay_ms - .saturating_mul(multiplier) - .min(policy.max_delay_ms); - capped_base_delay + jitter_ms.min(policy.max_jitter_ms) -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetReconnectDecision { - pub attempt: u32, - pub should_retry: bool, - pub delay_ms: u64, - pub next_attempt_at_ms: Option, - pub exhausted: bool, -} - -#[derive(Clone, Debug)] -pub struct CultNetReconnectController { - pub policy: CultNetReconnectPolicy, - attempt: u32, - next_attempt_at_ms: Option, - exhausted: bool, -} - -impl Default for CultNetReconnectController { - fn default() -> Self { - Self::new(create_reconnect_policy(Default::default())) - } -} - -impl CultNetReconnectController { - pub fn new(policy: CultNetReconnectPolicy) -> Self { - Self { - policy, - attempt: 0, - next_attempt_at_ms: None, - exhausted: false, - } - } - - pub fn attempt(&self) -> u32 { - self.attempt - } - - pub fn next_attempt_at_ms(&self) -> Option { - self.next_attempt_at_ms - } - - pub fn exhausted(&self) -> bool { - self.exhausted - } - - pub fn reset(&mut self) { - self.attempt = 0; - self.next_attempt_at_ms = None; - self.exhausted = false; - } - - pub fn can_attempt(&self, now_ms: u64) -> bool { - !self.exhausted - && self - .next_attempt_at_ms - .is_none_or(|next_attempt_at_ms| now_ms >= next_attempt_at_ms) - } - - pub fn record_failure(&mut self, now_ms: u64, jitter_ms: u64) -> CultNetReconnectDecision { - let next_attempt = self.attempt.saturating_add(1); - if self - .policy - .max_attempts - .is_some_and(|max_attempts| next_attempt > max_attempts) - { - self.exhausted = true; - self.next_attempt_at_ms = None; - return CultNetReconnectDecision { - attempt: self.attempt, - should_retry: false, - delay_ms: 0, - next_attempt_at_ms: None, - exhausted: true, - }; - } - - self.attempt = next_attempt; - let delay_ms = compute_reconnect_delay_ms(&self.policy, self.attempt, jitter_ms); - let next_attempt_at_ms = now_ms.saturating_add(delay_ms); - self.next_attempt_at_ms = Some(next_attempt_at_ms); - CultNetReconnectDecision { - attempt: self.attempt, - should_retry: true, - delay_ms, - next_attempt_at_ms: Some(next_attempt_at_ms), - exhausted: false, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct TcpFramedTransportProfileOptions { - pub transport_id: Option, - pub host: Option, - pub port: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, -} - -pub fn create_tcp_framed_transport_profile( - runtime_id: impl Into, - options: TcpFramedTransportProfileOptions, -) -> CultNetTransportProfile { - CultNetTransportProfile { - schema_version: "cultnet.transport_profile.v0".to_string(), - runtime_id: runtime_id.into(), - transports: vec![CultNetTransportDescriptor { - transport_id: options - .transport_id - .unwrap_or_else(|| "tcp-framed".to_string()), - protocol: CultNetTransportProtocol::TcpFramed, - host: options.host, - port: options.port, - path: None, - discovery_group: None, - wire_contracts: Some(vec!["cultnet.schema.v0".to_string()]), - reconnect_policy: None, - channels: vec![CultNetTransportChannel { - channel_id: "schema".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Ordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: None, - }], - }], - } -} - -pub struct TcpFramedTransportConnection { - stream: TStream, - pub profile: CultNetTransportProfile, - stats: CultNetTransportStats, -} - -impl TcpFramedTransportConnection { - pub fn new(stream: TStream, profile: CultNetTransportProfile) -> Self { - Self { - stream, - profile, - stats: CultNetTransportStats::default(), - } - } - - pub fn stats(&self) -> CultNetTransportStats { - self.stats.clone() - } - - pub fn into_inner(self) -> TStream { - self.stream - } -} - -impl TcpFramedTransportConnection -where - TStream: Write, -{ - pub fn send(&mut self, channel_id: &str, payload: &[u8]) -> Result<()> { - if channel_id != "schema" { - return Err(anyhow!( - "tcp_framed transport only supports the schema channel, got {channel_id:?}" - )); - } - - let frame = encode_frame(payload)?; - self.stream.write_all(&frame)?; - self.stream.flush()?; - self.stats.bytes_sent += frame.len() as u64; - self.stats.frames_sent += 1; - Ok(()) - } -} - -impl TcpFramedTransportConnection -where - TStream: Read, -{ - pub fn receive(&mut self) -> Result { - let mut header = [0_u8; FRAME_HEADER_BYTES]; - self.stream.read_exact(&mut header)?; - let payload_len = u32::from_be_bytes(header) as usize; - if let Some(max_payload_bytes) = self - .profile - .transports - .iter() - .find(|transport| transport.protocol == CultNetTransportProtocol::TcpFramed) - .and_then(|transport| { - transport - .channels - .iter() - .find(|channel| channel.channel_id == "schema") - }) - .and_then(|channel| channel.max_payload_bytes) - && payload_len > max_payload_bytes as usize - { - return Err(anyhow!( - "tcp_framed payload length {payload_len} exceeds advertised maximum {max_payload_bytes}" - )); - } - let mut payload = vec![0_u8; payload_len]; - self.stream.read_exact(&mut payload)?; - self.stats.bytes_received += (FRAME_HEADER_BYTES + payload_len) as u64; - self.stats.frames_received += 1; - Ok(CultNetTransportFrame { - channel_id: "schema".to_string(), - payload, - }) - } -} diff --git a/vendor/cultnet-rs/tests/cultnet.rs b/vendor/cultnet-rs/tests/cultnet.rs deleted file mode 100644 index 598c383..0000000 --- a/vendor/cultnet-rs/tests/cultnet.rs +++ /dev/null @@ -1,2279 +0,0 @@ -use anyhow::Result; -use chrono::Duration; -use chrono::Utc; -use cultcache_rs::CultCache; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultnet_rs::CultMesh; -use cultnet_rs::CultMeshAuthorityLease; -use cultnet_rs::CultMeshPeerCard; -use cultnet_rs::CultMeshRudpClientOptions; -use cultnet_rs::CultMeshRudpSocketOptions; -use cultnet_rs::CultNetClientSecurityOptions; -use cultnet_rs::CultNetDocumentBinding; -use cultnet_rs::CultNetDocumentMutationContract; -use cultnet_rs::CultNetDocumentOperation; -use cultnet_rs::CultNetDocumentPutOptions; -use cultnet_rs::CultNetDocumentRegistry; -use cultnet_rs::CultNetMessage; -use cultnet_rs::CultNetMutationAuthority; -use cultnet_rs::CultNetReactiveDocumentOptions; -use cultnet_rs::CultNetReconnectController; -use cultnet_rs::CultNetReconnectPolicyOptions; -use cultnet_rs::CultNetRudpPacket; -use cultnet_rs::CultNetRudpPacketType; -use cultnet_rs::CultNetRudpReconnectLoop; -use cultnet_rs::CultNetRudpSendOptions; -use cultnet_rs::CultNetRudpSession; -use cultnet_rs::CultNetRudpSessionOptions; -use cultnet_rs::CultNetRudpSocketMode; -use cultnet_rs::CultNetRudpSocketTransportConnection; -use cultnet_rs::CultNetRudpSocketTransportOptions; -use cultnet_rs::CultNetSchemaKind; -use cultnet_rs::CultNetSchemaRegistration; -use cultnet_rs::CultNetSchemaRegistry; -use cultnet_rs::CultNetSecret; -use cultnet_rs::CultNetServerSecurityOptions; -use cultnet_rs::CultNetShardCatalog; -use cultnet_rs::CultNetShardDescriptor; -use cultnet_rs::CultNetTransportChannel; -use cultnet_rs::CultNetTransportDelivery; -use cultnet_rs::CultNetTransportDescriptor; -use cultnet_rs::CultNetTransportFrame; -use cultnet_rs::CultNetTransportOrdering; -use cultnet_rs::CultNetTransportProfile; -use cultnet_rs::CultNetTransportProtocol; -use cultnet_rs::CultNetWireContract; -use cultnet_rs::LengthPrefixedMessageFramer; -use cultnet_rs::TcpFramedTransportConnection; -use cultnet_rs::TcpFramedTransportProfileOptions; -use cultnet_rs::builtin_schema_registry; -use cultnet_rs::compute_reconnect_delay_ms; -use cultnet_rs::create_reconnect_policy; -use cultnet_rs::create_rudp_transport_profile; -use cultnet_rs::create_tcp_framed_transport_profile; -use cultnet_rs::decode_cultnet_message_from_slice; -use cultnet_rs::decode_rudp_packet; -use cultnet_rs::encode_cultnet_message_for_wire; -use cultnet_rs::encode_cultnet_message_to_vec; -use cultnet_rs::encode_frame; -use cultnet_rs::encode_rudp_packet; -use pretty_assertions::assert_eq; -use std::cell::RefCell; -use std::net::UdpSocket; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration as StdDuration; - -const TS_HELLO_FRAME: &[u8] = include_bytes!("fixtures/cultnet-ts-hello.frame"); -const TS_LEGACY_LOGIN_FRAME: &[u8] = include_bytes!("fixtures/cultnet-ts-legacy-login.frame"); - -fn bind_udp_socket() -> Result { - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(StdDuration::from_millis(20)))?; - Ok(socket) -} - -fn pump_rudp_handshake( - client: &mut CultNetRudpSocketTransportConnection, - server: &mut CultNetRudpSocketTransportConnection, -) -> Result<()> { - for _ in 0..20 { - let _ = server.receive_once()?; - let _ = client.receive_once()?; - let _ = server.receive_once()?; - if client.connected() && server.connected() { - return Ok(()); - } - thread::sleep(StdDuration::from_millis(5)); - } - anyhow::bail!("RUDP socket handshake did not complete"); -} - -fn receive_rudp_frame( - transport: &mut CultNetRudpSocketTransportConnection, -) -> Result { - for _ in 0..20 { - if let Some(frame) = transport.receive_once()? { - return Ok(frame); - } - thread::sleep(StdDuration::from_millis(5)); - } - anyhow::bail!("RUDP socket frame was not delivered") -} - -fn receive_rudp_schema_message( - transport: &mut CultNetRudpSocketTransportConnection, -) -> Result { - for _ in 0..20 { - if let Some(message) = transport.receive_schema_message_once()? { - return Ok(message); - } - thread::sleep(StdDuration::from_millis(5)); - } - anyhow::bail!("RUDP schema message was not delivered") -} - -fn pump_rudp_server_until_connected( - server: Arc>, - done: Arc, -) -> thread::JoinHandle> { - thread::spawn(move || { - while !done.load(Ordering::SeqCst) { - { - let mut server = server.lock().expect("RUDP server mutex poisoned"); - let _ = server.receive_once()?; - server.poll_resends()?; - if server.connected() { - return Ok(()); - } - } - thread::sleep(StdDuration::from_millis(5)); - } - Ok(()) - }) -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.agent-state", - schema = "GhostlightAgentStateFixture" -)] -struct GhostlightAgentStateFixture { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - agent_id: String, - #[cultcache(key = 2)] - display_name: String, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.agent-state.ui", - schema = "GhostlightAgentStateUiFixture" -)] -struct GhostlightAgentStateUiFixture { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - agent_id: String, - #[cultcache(key = 2)] - display_name: String, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.reactive-note", - schema = "GhostlightReactiveNoteFixture" -)] -struct GhostlightReactiveNoteFixture { - #[cultcache(key = 0)] - body: String, - #[cultcache(key = 1)] - revision: i32, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.reactive-note.ui", - schema = "GhostlightReactiveNoteUiFixture" -)] -struct GhostlightReactiveNoteUiFixture { - #[cultcache(key = 0)] - body: String, - #[cultcache(key = 1)] - revision: i32, -} - -#[test] -fn security_helpers_round_trip_encrypted_strings_and_validate_sessions() -> Result<()> { - let server_security = CultNetServerSecurityOptions::development(); - let client_security = server_security.to_client_options(); - let nonce = CultNetSecret::new_nonce(); - let encrypted = CultNetSecret::encrypt_string(Some("hello"), &nonce, &client_security)? - .expect("non-empty input encrypts"); - assert_eq!( - CultNetSecret::decrypt_string(Some(&encrypted), Some(&nonce), &server_security)?, - Some("hello".to_string()) - ); - - let token = CultNetSecret::create_session_token( - "runtime-face", - chrono::Utc::now() + Duration::minutes(1), - &server_security, - )?; - let validated = CultNetSecret::try_validate_session_token(Some(&token), &server_security)? - .expect("token validates before expiry"); - assert_eq!(validated.user_id, "runtime-face"); - Ok(()) -} - -#[test] -fn cultnet_schema_messages_round_trip_through_messagepack_frames() -> Result<()> { - let message = CultNetMessage::Hello { - runtime_id: "voidbot-main".to_string(), - runtime_kind: "rust-worker".to_string(), - agent_id: Some("void".to_string()), - role: None, - display_name: Some("Void".to_string()), - supported_document_types: Some(vec!["ghostlight.agent-state".to_string()]), - supported_mutation_contracts: Some(vec![CultNetDocumentMutationContract { - document_type: "ghostlight.agent-state".to_string(), - payload_schema_version: Some("ghostlight.agent_state.v0".to_string()), - operations: vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::DocumentPut, - ], - authority: CultNetMutationAuthority::Coordinator, - intent_document_types: Some(vec!["ghostlight.agent-state.intent.v0".to_string()]), - receipt_document_types: Some(vec!["ghostlight.agent-state.receipt.v0".to_string()]), - notes: Some(vec![ - "Persona may request bounded memory mutation; coordinator reviews authority." - .to_string(), - ]), - }]), - supported_message_versions: None, - transport_profiles: Some(vec![CultNetTransportProfile { - schema_version: "cultnet.transport_profile.v0".to_string(), - runtime_id: "voidbot-main".to_string(), - transports: vec![CultNetTransportDescriptor { - transport_id: "direct-pipe".to_string(), - protocol: CultNetTransportProtocol::TcpFramed, - host: None, - port: None, - path: None, - discovery_group: None, - wire_contracts: Some(vec!["cultnet.schema.v0".to_string()]), - reconnect_policy: None, - channels: vec![CultNetTransportChannel { - channel_id: "schema".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Ordered, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - }], - }], - }]), - supports_schema_catalog: Some(true), - }; - let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?; - let frame = encode_frame(&payload)?; - assert_eq!(&frame[..4], &(payload.len() as u32).to_be_bytes()); - - let mut framer = LengthPrefixedMessageFramer::new(); - assert!(framer.push(&frame[..2]).is_empty()); - let frames = framer.push(&frame[2..]); - assert_eq!(frames.len(), 1); - let decoded = - decode_cultnet_message_from_slice(&frames[0], CultNetWireContract::CultNetSchemaV0)?; - assert_eq!(decoded, message); - Ok(()) -} - -#[test] -fn tcp_framed_transport_carries_schema_payloads_with_stats() -> Result<()> { - let payload = b"cultnet-payload".to_vec(); - let profile = create_tcp_framed_transport_profile( - "rust-transport", - TcpFramedTransportProfileOptions { - transport_id: Some("test-tcp".to_string()), - ..TcpFramedTransportProfileOptions::default() - }, - ); - let mut sender = TcpFramedTransportConnection::new(Vec::::new(), profile.clone()); - sender.send("schema", &payload)?; - assert_eq!(sender.stats().frames_sent, 1); - assert_eq!(sender.stats().bytes_sent, (payload.len() + 4) as u64); - assert!(sender.send("unreliable", &[]).is_err()); - - let bytes = sender.into_inner(); - let mut receiver = TcpFramedTransportConnection::new(std::io::Cursor::new(bytes), profile); - let frame = receiver.receive()?; - assert_eq!(frame.channel_id, "schema"); - assert_eq!(frame.payload, payload); - assert_eq!(receiver.stats().frames_received, 1); - assert_eq!( - receiver.stats().bytes_received, - (frame.payload.len() + 4) as u64 - ); - assert_eq!( - receiver.profile.transports[0].protocol, - CultNetTransportProtocol::TcpFramed - ); - Ok(()) -} - -#[test] -fn tcp_framed_transport_rejects_oversized_payload_before_reading_it() { - let profile = create_tcp_framed_transport_profile( - "bounded-rust-transport", - TcpFramedTransportProfileOptions { - max_payload_bytes: Some(8), - ..TcpFramedTransportProfileOptions::default() - }, - ); - let mut bytes = Vec::new(); - bytes.extend_from_slice(&9_u32.to_be_bytes()); - let mut receiver = TcpFramedTransportConnection::new(std::io::Cursor::new(bytes), profile); - let error = receiver.receive().expect_err("oversized frame must fail"); - assert!(error.to_string().contains("exceeds advertised maximum 8")); - assert_eq!(receiver.stats().bytes_received, 0); -} - -#[test] -fn rudp_packet_codec_uses_deterministic_reliable_ordered_fixture() -> Result<()> { - let encoded = encode_rudp_packet(&CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Data, - connection_id: 0x01020304, - sequence: 0x0000002a, - ack: 0x00000029, - ack_mask: 0x80000001, - channel_id: "schema".to_string(), - reliable: true, - ordered: true, - sequenced: false, - fragment_id: 7, - fragment_index: 1, - fragment_count: 3, - payload: b"hello".to_vec(), - })?; - - assert_eq!( - encoded, - vec![ - 67, 78, 82, 48, 0, 3, 11, 42, 1, 2, 3, 4, 0, 0, 0, 42, 0, 0, 0, 41, 128, 0, 0, 1, 0, 7, - 0, 1, 0, 3, 0, 0, 0, 5, 6, 0, 115, 99, 104, 101, 109, 97, 104, 101, 108, 108, 111, - ] - ); - - let decoded = decode_rudp_packet(&encoded)?; - assert_eq!(decoded.packet_type, CultNetRudpPacketType::Data); - assert_eq!(decoded.connection_id, 0x01020304); - assert_eq!(decoded.sequence, 0x0000002a); - assert_eq!(decoded.ack, 0x00000029); - assert_eq!(decoded.ack_mask, 0x80000001); - assert_eq!(decoded.channel_id, "schema"); - assert!(decoded.reliable); - assert!(decoded.ordered); - assert!(!decoded.sequenced); - assert_eq!(decoded.fragment_id, 7); - assert_eq!(decoded.fragment_index, 1); - assert_eq!(decoded.fragment_count, 3); - assert_eq!(decoded.payload, b"hello"); - Ok(()) -} - -#[test] -fn rudp_transport_profile_advertises_state_and_realtime_channels() { - let profile = create_rudp_transport_profile( - "rust-rudp", - cultnet_rs::RudpTransportProfileOptions { - transport_id: Some("public-rudp".to_string()), - host: Some("127.0.0.1".to_string()), - port: Some(7777), - max_payload_bytes: Some(1200), - max_fragment_bytes: Some(1000), - max_pending_reliable_packets: Some(64), - reconnect_policy: None, - }, - ); - - assert_eq!( - profile.transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - assert_eq!( - profile.transports[0] - .reconnect_policy - .as_ref() - .map(|policy| policy.schema_version.as_str()), - Some("cultnet.reconnect_policy.v0") - ); - assert_eq!( - profile.transports[0] - .reconnect_policy - .as_ref() - .map(|policy| policy.base_delay_ms), - Some(1_000) - ); - let channels = profile.transports[0] - .channels - .iter() - .map(|channel| { - ( - channel.channel_id.as_str(), - channel.delivery, - channel.ordering, - channel.max_pending_reliable_packets, - ) - }) - .collect::>(); - assert_eq!( - channels, - vec![ - ( - "schema", - CultNetTransportDelivery::Reliable, - CultNetTransportOrdering::Ordered, - Some(64) - ), - ( - "latest", - CultNetTransportDelivery::Unreliable, - CultNetTransportOrdering::Sequenced, - Some(64) - ), - ( - "realtime", - CultNetTransportDelivery::Unreliable, - CultNetTransportOrdering::Unordered, - Some(64) - ), - ( - "media", - CultNetTransportDelivery::Reliable, - CultNetTransportOrdering::Unordered, - Some(64) - ), - ] - ); -} - -#[test] -fn reconnect_policy_exposes_portable_delay_contract() { - let policy = create_reconnect_policy(CultNetReconnectPolicyOptions { - policy_id: "rudp-default".to_string(), - max_attempts: Some(8), - ..CultNetReconnectPolicyOptions::default() - }); - - assert_eq!(policy.schema_version, "cultnet.reconnect_policy.v0"); - assert_eq!(policy.policy_id, "rudp-default"); - assert_eq!(policy.max_attempts, Some(8)); - assert_eq!(compute_reconnect_delay_ms(&policy, 1, 0), 1_000); - assert_eq!(compute_reconnect_delay_ms(&policy, 3, 17), 4_017); - assert_eq!(compute_reconnect_delay_ms(&policy, 9, 999), 30_250); - assert_eq!(compute_reconnect_delay_ms(&policy, 0, 0), 1_000); -} - -#[test] -fn reconnect_controller_schedules_attempts_and_reset() { - let policy = create_reconnect_policy(CultNetReconnectPolicyOptions { - max_attempts: Some(2), - ..CultNetReconnectPolicyOptions::default() - }); - let mut controller = CultNetReconnectController::new(policy); - - let first = controller.record_failure(10_000, 0); - assert_eq!(first.attempt, 1); - assert!(first.should_retry); - assert_eq!(first.delay_ms, 1_000); - assert_eq!(first.next_attempt_at_ms, Some(11_000)); - assert!(!first.exhausted); - assert!(!controller.can_attempt(10_999)); - assert!(controller.can_attempt(11_000)); - - let second = controller.record_failure(11_000, 17); - assert_eq!(second.attempt, 2); - assert_eq!(second.delay_ms, 2_017); - assert_eq!(second.next_attempt_at_ms, Some(13_017)); - assert!(second.should_retry); - - let exhausted = controller.record_failure(13_017, 0); - assert_eq!(exhausted.attempt, 2); - assert!(!exhausted.should_retry); - assert_eq!(exhausted.delay_ms, 0); - assert_eq!(exhausted.next_attempt_at_ms, None); - assert!(exhausted.exhausted); - assert!(!controller.can_attempt(99_000)); - - controller.reset(); - assert_eq!(controller.attempt(), 0); - assert_eq!(controller.next_attempt_at_ms(), None); - assert!(!controller.exhausted()); - assert!(controller.can_attempt(99_000)); -} - -#[test] -fn rudp_reconnect_loop_consumes_shared_controller() -> Result<()> { - let server_socket = bind_udp_socket()?; - let remote_addr = server_socket.local_addr()?; - let opened_local_addrs = Rc::new(RefCell::new(Vec::new())); - let opened_for_factory = Rc::clone(&opened_local_addrs); - let connection_id = 0x2233_4455; - let policy = create_reconnect_policy(CultNetReconnectPolicyOptions { - max_attempts: Some(2), - ..CultNetReconnectPolicyOptions::default() - }); - - let mut loop_ = CultNetRudpReconnectLoop::new(policy, b"join".to_vec(), move || { - let socket = bind_udp_socket()?; - opened_for_factory.borrow_mut().push(socket.local_addr()?); - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions::client( - "rust-rudp-reconnect", - socket, - remote_addr, - connection_id, - )) - }); - - { - let first = loop_.start()?; - assert!(first.stats().bytes_sent > 0); - } - assert!(loop_.transport().is_some()); - assert_eq!(opened_local_addrs.borrow().len(), 1); - - let decision = loop_.handle_closed(10_000, 17).expect("retry decision"); - assert_eq!(decision.attempt, 1); - assert!(decision.should_retry); - assert_eq!(decision.delay_ms, 1_017); - assert_eq!(decision.next_attempt_at_ms, Some(11_017)); - assert_eq!(loop_.reconnect_controller.attempt(), 1); - assert_eq!( - loop_.reconnect_controller.next_attempt_at_ms(), - Some(11_017) - ); - - assert!(!loop_.reconnect_if_due(11_016)?); - assert_eq!(opened_local_addrs.borrow().len(), 1); - assert!(loop_.reconnect_if_due(11_017)?); - assert_eq!(opened_local_addrs.borrow().len(), 2); - assert!(loop_.transport().is_some()); - - loop_.mark_connected(); - assert_eq!(loop_.reconnect_controller.attempt(), 0); - - loop_.stop(); - assert!(loop_.transport().is_none()); - assert_eq!(loop_.reconnect_controller.attempt(), 0); - - Ok(()) -} - -#[test] -fn rudp_session_handshake_acks_reliable_connect_and_accept_packets() -> Result<()> { - let mut client = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 0x0a0b0c0d, - initial_sequence: 1, - resend_delay_ms: 50, - ..CultNetRudpSessionOptions::default() - }); - let mut server = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 0x0a0b0c0d, - initial_sequence: 100, - resend_delay_ms: 50, - ..CultNetRudpSessionOptions::default() - }); - - let connect = client.create_connect(0, b"join".to_vec())?; - assert_eq!(connect.packet_type, CultNetRudpPacketType::Connect); - assert_eq!(connect.sequence, 1); - assert_eq!(client.pending_reliable_sequences(), vec![1]); - - let accept = server.accept_connect(&connect, 10, b"ok".to_vec())?; - assert_eq!(accept.packet_type, CultNetRudpPacketType::Accept); - assert_eq!(accept.ack, 1); - assert!(server.connected()); - assert_eq!(server.pending_reliable_sequences(), vec![100]); - - client.receive(&accept, 20)?; - assert!(client.connected()); - assert!(client.pending_reliable_sequences().is_empty()); - - let ack = client.create_ack(); - assert_eq!(ack.ack, 100); - server.receive(&ack, 30)?; - assert!(server.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_computes_ack_masks_and_clears_pending_reliable_packets() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 7, - initial_sequence: 10, - resend_delay_ms: 100, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 7, - initial_sequence: 200, - resend_delay_ms: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 7, - sequence: 1, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 7, - sequence: 2, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }; - let first = sender.send("schema", b"first".to_vec(), options.clone())?; - let second = sender.send("schema", b"second".to_vec(), options.clone())?; - let third = sender.send("schema", b"third".to_vec(), options)?; - assert_eq!(sender.pending_reliable_sequences(), vec![10, 11, 12]); - - receiver.receive(&first, 0)?; - receiver.receive(&third, 0)?; - let ack_with_gap = receiver.create_ack(); - assert_eq!(ack_with_gap.ack, 12); - assert_eq!(ack_with_gap.ack_mask, 0b10 | (1 << 9)); - sender.receive(&ack_with_gap, 0)?; - assert_eq!(sender.pending_reliable_sequences(), vec![11]); - - receiver.receive(&second, 0)?; - let full_ack = receiver.create_ack(); - assert_eq!(full_ack.ack, 12); - assert_eq!(full_ack.ack_mask, 0b11 | (1 << 9)); - sender.receive(&full_ack, 0)?; - assert!(sender.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_schedules_reliable_resends_until_acked() -> Result<()> { - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 99, - initial_sequence: 1, - resend_delay_ms: 100, - ..CultNetRudpSessionOptions::default() - }); - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 99, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - let sent = session.send( - "schema", - b"payload".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - now_ms: 10, - ..CultNetRudpSendOptions::default() - }, - )?; - - assert!(session.due_resends(90).is_empty()); - assert_eq!( - session - .due_resends(110) - .iter() - .map(|packet| packet.sequence) - .collect::>(), - vec![sent.sequence] - ); - assert!(session.due_resends(150).is_empty()); - - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Ack, - connection_id: 99, - sequence: 51, - ack: sent.sequence, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - assert!(session.due_resends(250).is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_pings_and_detects_receive_timeout() -> Result<()> { - let mut client = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 101, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut server = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 101, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - let connect = client.create_connect(0, b"join".to_vec())?; - let accept = server.accept_connect(&connect, 10, Vec::new())?; - client.receive(&accept, 20)?; - - let ping = client.create_ping(b"pulse".to_vec()); - let ping_result = server.receive(&ping, 30)?; - let pong = ping_result.reply.expect("ping should produce pong"); - assert_eq!(pong.packet_type, CultNetRudpPacketType::Pong); - assert_eq!(pong.payload, b"pulse"); - - let pong_result = client.receive(&pong, 40)?; - assert!(pong_result.pong); - assert_eq!(pong_result.pong_payload, b"pulse"); - assert!(!client.check_timeout(90, 50)); - assert!(client.check_timeout(91, 50)); - assert!(!client.connected()); - Ok(()) -} - -#[test] -fn rudp_lossy_packets_cannot_create_reliable_ordered_gaps() -> Result<()> { - let mut client = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 198, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut server = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 198, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - let connect = client.create_connect(0, Vec::new())?; - let accept = server.accept_connect(&connect, 1, Vec::new())?; - client.receive(&accept, 2)?; - - let ping = client.create_ping(b"pulse".to_vec()); - let pong = server - .receive(&ping, 3)? - .reply - .expect("ping should produce pong"); - let realtime = server.send( - "realtime", - b"discarded realtime".to_vec(), - CultNetRudpSendOptions::default(), - )?; - let latest = server.send( - "latest", - b"discarded latest state".to_vec(), - CultNetRudpSendOptions { - sequenced: true, - ..CultNetRudpSendOptions::default() - }, - )?; - let schema = server.send( - "schema", - b"committed response".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - )?; - - assert_eq!(ping.sequence, 0); - assert_eq!(pong.sequence, 0); - assert_eq!(realtime.sequence, 0); - assert_eq!(latest.sequence, 1); - assert_eq!(schema.sequence, accept.sequence + 1); - assert_eq!( - client.receive(&schema, 4)?.delivered[0].payload, - b"committed response" - ); - Ok(()) -} - -#[test] -fn rudp_unreliable_sequenced_delivery_is_scoped_to_its_channel() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 197, - initial_sequence: 50, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 197, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - let connect = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect, 1, Vec::new())?; - sender.receive(&accept, 2)?; - let options = CultNetRudpSendOptions { - sequenced: true, - ..CultNetRudpSendOptions::default() - }; - let older = sender.send("latest", b"older".to_vec(), options.clone())?; - let newer = sender.send("latest", b"newer".to_vec(), options)?; - - assert_eq!(older.sequence, 1); - assert_eq!(newer.sequence, 2); - assert_eq!(receiver.receive(&newer, 3)?.delivered[0].payload, b"newer"); - assert!(receiver.receive(&older, 4)?.delivered.is_empty()); - Ok(()) -} - -#[test] -fn rudp_rejects_unreliable_ordered_delivery() -> Result<()> { - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 196, - ..CultNetRudpSessionOptions::default() - }); - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 196, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - let error = session - .send( - "schema", - b"cannot order what will not retransmit".to_vec(), - CultNetRudpSendOptions { - ordered: true, - ..CultNetRudpSendOptions::default() - }, - ) - .unwrap_err(); - assert!( - error - .to_string() - .contains("ordered delivery requires reliability") - ); - Ok(()) -} - -#[test] -fn rudp_session_bounds_pending_reliable_packets_before_enqueue() -> Result<()> { - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 102, - initial_sequence: 1, - max_pending_reliable_packets: Some(2), - ..CultNetRudpSessionOptions::default() - }); - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 102, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - session.send( - "schema", - b"first".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - )?; - session.send( - "schema", - b"second".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - )?; - let error = session - .send( - "schema", - b"third".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("reliable send queue is full")); - assert_eq!(session.pending_reliable_sequences(), vec![1, 2]); - - let mut fragmented = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 103, - initial_sequence: 1, - max_pending_reliable_packets: Some(3), - ..CultNetRudpSessionOptions::default() - }); - fragmented.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 103, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - let error = fragmented - .send_many( - "schema", - b"fragment-me".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - Some(3), - ) - .unwrap_err(); - assert!(error.to_string().contains("reliable send queue is full")); - assert!(fragmented.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_suppresses_duplicates_and_delivers_reliable_ordered_payloads_in_sequence() --> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 123, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 123, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 123, - sequence: 90, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 123, - sequence: 91, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }; - let first = sender.send("schema", b"first".to_vec(), options.clone())?; - let second = sender.send("schema", b"second".to_vec(), options.clone())?; - let third = sender.send("schema", b"third".to_vec(), options)?; - - assert_eq!( - receiver - .receive(&first, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["first"] - ); - assert!(receiver.receive(&third, 0)?.delivered.is_empty()); - assert!(receiver.receive(&first, 0)?.delivered.is_empty()); - assert_eq!( - receiver - .receive(&second, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["second", "third"] - ); - Ok(()) -} - -#[test] -fn rudp_session_skips_control_packets_while_ordering_schema_payloads() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 124, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 124, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 124, - sequence: 90, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 124, - sequence: 91, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }; - let first = sender.send("schema", b"first".to_vec(), options.clone())?; - let control = sender.create_ack(); - let second = sender.send("schema", b"second".to_vec(), options)?; - assert_eq!(control.sequence, 0); - assert_eq!(second.sequence, first.sequence + 1); - - assert_eq!( - receiver - .receive(&first, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["first"] - ); - assert!(receiver.receive(&control, 0)?.delivered.is_empty()); - assert_eq!( - receiver - .receive(&second, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["second"] - ); - Ok(()) -} - -#[test] -fn rudp_session_fragments_and_reassembles_reliable_ordered_payloads() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 456, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 456, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 456, - sequence: 90, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 456, - sequence: 91, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let packets = sender.send_many( - "schema", - b"fragment-me-please".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - now_ms: 10, - ..CultNetRudpSendOptions::default() - }, - Some(5), - )?; - assert_eq!(packets.len(), 4); - assert_eq!( - packets - .iter() - .map(|packet| packet.fragment_count) - .collect::>(), - vec![4, 4, 4, 4] - ); - assert_eq!( - packets - .iter() - .map(|packet| packet.fragment_index) - .collect::>(), - vec![0, 1, 2, 3] - ); - assert!( - packets - .iter() - .all(|packet| packet.fragment_id == packets[0].fragment_id) - ); - - assert!(receiver.receive(&packets[0], 0)?.delivered.is_empty()); - assert!(receiver.receive(&packets[1], 0)?.delivered.is_empty()); - assert!(receiver.receive(&packets[2], 0)?.delivered.is_empty()); - let delivered = receiver.receive(&packets[3], 0)?.delivered; - assert_eq!(delivered.len(), 1); - assert_eq!(delivered[0].payload, b"fragment-me-please"); - assert_eq!(delivered[0].sequence, packets[0].sequence); - Ok(()) -} - -#[test] -fn rudp_socket_transport_handshakes_and_carries_reliable_ordered_schema_frames() -> Result<()> { - let server_socket = bind_udp_socket()?; - let client_socket = bind_udp_socket()?; - let server_addr = server_socket.local_addr()?; - let connection_id = 0x1020_3040; - let mut server = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-server".to_string(), - socket: server_socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: 100, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-client".to_string(), - socket: client_socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(server_addr), - connection_id, - initial_sequence: 1, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - - client.connect(b"join".to_vec())?; - pump_rudp_handshake(&mut client, &mut server)?; - assert!(client.connected()); - assert!(server.connected()); - - client.send("schema", b"client-state".to_vec())?; - let server_frame = receive_rudp_frame(&mut server)?; - assert_eq!(server_frame.channel_id, "schema"); - assert_eq!(server_frame.payload, b"client-state"); - - server.send("schema", b"server-state".to_vec())?; - let client_frame = receive_rudp_frame(&mut client)?; - assert_eq!(client_frame.channel_id, "schema"); - assert_eq!(client_frame.payload, b"server-state"); - - client.send_schema_message(&CultNetMessage::Hello { - runtime_id: "rust-rudp-client".to_string(), - runtime_kind: "rust-worker".to_string(), - agent_id: None, - role: Some("schema".to_string()), - display_name: None, - supported_document_types: None, - supported_mutation_contracts: None, - supported_message_versions: None, - transport_profiles: Some(vec![client.profile.clone()]), - supports_schema_catalog: Some(true), - })?; - let message = receive_rudp_schema_message(&mut server)?; - let CultNetMessage::Hello { - runtime_id, - transport_profiles, - supports_schema_catalog, - .. - } = message - else { - anyhow::bail!("RUDP schema message did not decode as hello"); - }; - assert_eq!(runtime_id, "rust-rudp-client"); - assert_eq!(supports_schema_catalog, Some(true)); - assert_eq!( - transport_profiles.expect("hello advertises transport")[0].transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - assert_eq!( - server.profile.transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - assert_eq!(client.stats().frames_sent, 2); - assert_eq!(server.stats().frames_received, 2); - - Ok(()) -} - -#[test] -fn rudp_socket_transport_carries_fragmented_reliable_ordered_schema_frames() -> Result<()> { - let server_socket = bind_udp_socket()?; - let client_socket = bind_udp_socket()?; - let server_addr = server_socket.local_addr()?; - let connection_id = 0x1020_3041; - let mut server = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-fragment-server".to_string(), - socket: server_socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: 100, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: Some(8), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-fragment-client".to_string(), - socket: client_socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(server_addr), - connection_id, - initial_sequence: 1, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: Some(8), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - - client.connect(b"join".to_vec())?; - pump_rudp_handshake(&mut client, &mut server)?; - - let payload = b"this-schema-frame-is-larger-than-one-rudp-fragment".to_vec(); - client.send("schema", payload.clone())?; - let server_frame = receive_rudp_frame(&mut server)?; - assert_eq!(server_frame.channel_id, "schema"); - assert_eq!(server_frame.payload, payload); - assert_eq!(client.stats().frames_sent, 1); - assert_eq!(server.stats().frames_received, 1); - Ok(()) -} - -#[test] -fn cultmesh_facade_creates_rudp_client_from_peer_endpoint() -> Result<()> { - let connection_id = 0x2030_4050; - let options = CultMeshRudpSocketOptions { - resend_delay_ms: 25, - max_fragment_bytes: Some(8), - max_pending_reliable_packets: Some(16), - ..CultMeshRudpSocketOptions::default() - }; - let mut server = - CultMesh::create_rudp_server("rust-cultmesh-server", connection_id, options.clone())?; - let server_port = server.profile.transports[0] - .port - .expect("RUDP server profile advertises its local port"); - let endpoint = CultMesh::parse_rudp_endpoint(&format!("rudp://127.0.0.1:{server_port}"))?; - assert_eq!(endpoint.host, "127.0.0.1"); - assert_eq!(endpoint.port, server_port); - assert_eq!( - CultMesh::parse_rudp_endpoint("RuDp://localhost:4100")?.host, - "localhost" - ); - assert_eq!( - CultMesh::parse_rudp_endpoint("rudp://[::1]:4100")?.uri(), - "rudp://[::1]:4100" - ); - - let peer = CultMeshPeerCard::new("rust-cultmesh-server", "local", [endpoint.uri()]) - .with_roles(["schema"]) - .with_authority_lease_id("lease:rust-cultmesh-server"); - let mut peers = CultMesh::create_peer_catalog(); - peers.upsert(peer.clone())?; - assert_eq!(peers.find("local", Some("schema")), vec![peer.clone()]); - let now = Utc::now(); - let mut leases = CultMesh::create_authority_lease_catalog(); - assert!( - CultMesh::create_rudp_client_for_authorized_peer( - "rust-cultmesh-client", - connection_id, - &peers, - &leases, - "local", - "schema", - None, - now, - options.clone(), - ) - .is_err() - ); - leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-cultmesh-server".to_string(), - verse_id: "local".to_string(), - peer_id: "rust-cultmesh-server".to_string(), - roles: vec!["schema".to_string()], - shard_ids: Vec::new(), - issuer_runtime_id: Some("odin".to_string()), - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), - })?; - - let mut client = CultMesh::create_rudp_client_for_authorized_peer( - "rust-cultmesh-client", - connection_id, - &peers, - &leases, - "local", - "schema", - None, - now, - options, - )?; - client.connect(b"join".to_vec())?; - pump_rudp_handshake(&mut client, &mut server)?; - - client.send("schema", b"client-state".to_vec())?; - let server_frame = receive_rudp_frame(&mut server)?; - assert_eq!(server_frame.channel_id, "schema"); - assert_eq!(server_frame.payload, b"client-state"); - Ok(()) -} - -#[test] -fn cultmesh_facade_connects_authorized_rudp_client_for_schema_messages() -> Result<()> { - let connection_id = 0x2030_4051; - let options = CultMeshRudpSocketOptions { - resend_delay_ms: 25, - max_fragment_bytes: Some(1024), - max_pending_reliable_packets: Some(16), - ..CultMeshRudpSocketOptions::default() - }; - let server = Arc::new(Mutex::new(CultMesh::create_rudp_server( - "rust-cultmesh-connected-server", - connection_id, - options.clone(), - )?)); - let server_port = server - .lock() - .expect("RUDP server mutex poisoned") - .profile - .transports[0] - .port - .expect("RUDP server profile advertises its local port"); - let endpoint = CultMesh::parse_rudp_endpoint(&format!("rudp://127.0.0.1:{server_port}"))?; - let peer = CultMeshPeerCard::new("rust-cultmesh-connected-server", "local", [endpoint.uri()]) - .with_roles(["schema"]) - .with_authority_lease_id("lease:rust-cultmesh-connected-server"); - let mut peers = CultMesh::create_peer_catalog(); - peers.upsert(peer)?; - let now = Utc::now(); - let mut leases = CultMesh::create_authority_lease_catalog(); - leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-cultmesh-connected-server".to_string(), - verse_id: "local".to_string(), - peer_id: "rust-cultmesh-connected-server".to_string(), - roles: vec!["schema".to_string()], - shard_ids: Vec::new(), - issuer_runtime_id: Some("odin".to_string()), - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), - })?; - - let done = Arc::new(AtomicBool::new(false)); - let pump = pump_rudp_server_until_connected(server.clone(), done.clone()); - let mut client = CultMesh::connect_rudp_client_for_authorized_peer( - "rust-cultmesh-connected-client", - connection_id, - &peers, - &leases, - "local", - "schema", - None, - now, - CultMeshRudpClientOptions { - socket_options: options, - connect_payload: b"join".to_vec(), - connect_timeout: StdDuration::from_secs(1), - poll_interval: StdDuration::from_millis(5), - }, - )?; - done.store(true, Ordering::SeqCst); - pump.join() - .map_err(|_| anyhow::anyhow!("RUDP server pump panicked"))??; - - assert!(client.connected()); - assert!( - server - .lock() - .expect("RUDP server mutex poisoned") - .connected() - ); - client.send_schema_message(&CultNetMessage::SchemaCatalogRequest { - message_id: "rust-cultmesh-rudp-schema-catalog".to_string(), - include_schema_json: Some(true), - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::DocumentPayload]), - })?; - let request = { - let mut server = server.lock().expect("RUDP server mutex poisoned"); - receive_rudp_schema_message(&mut server)? - }; - let CultNetMessage::SchemaCatalogRequest { - message_id, kinds, .. - } = request - else { - anyhow::bail!("RUDP schema message did not decode as schema catalog request"); - }; - assert_eq!(message_id, "rust-cultmesh-rudp-schema-catalog"); - assert_eq!(kinds, Some(vec![CultNetSchemaKind::DocumentPayload])); - assert_eq!( - client.profile.transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - Ok(()) -} - -#[test] -fn cultmesh_facade_exposes_schema_registry_and_shard_catalog_owners() -> Result<()> { - let mut schemas = CultMesh::create_schema_registry(); - schemas.register(CultNetSchemaRegistration { - schema_id: "rust.cultmesh.note.v1".to_string(), - kind: CultNetSchemaKind::DocumentPayload, - wire_contracts: vec![CultNetWireContract::CultNetSchemaV0], - schema_version: Some("rust.cultmesh.note.v1".to_string()), - document_type: Some("rust.cultmesh.note".to_string()), - title: Some("Rust CultMesh Note".to_string()), - schema_json: Some( - r#"{"$id":"rust.cultmesh.note.v1","type":"object","properties":{"body":{"type":"string"}}}"# - .to_string(), - ), - })?; - assert_eq!( - schemas - .get("rust.cultmesh.note.v1", false) - .expect("registered schema is discoverable") - .document_type - .as_deref(), - Some("rust.cultmesh.note") - ); - - let builtins = CultMesh::create_builtin_schema_registry()?; - let shared_contracts = builtins.list(&cultnet_rs::CultNetSchemaCatalogOptions { - include_schema_json: false, - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::SharedContract]), - }); - assert!(shared_contracts.iter().any(|schema| { - schema.schema_id - == "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json" - && schema.schema_version.as_deref() == Some("cultnet.transport_profile.v0") - })); - - let mut shards = CultMesh::create_shard_catalog(); - shards.upsert(CultNetShardDescriptor { - shard_id: "rust-notes-a".to_string(), - owner_runtime_id: "rust-cultmesh".to_string(), - epoch: 7, - is_primary: Some(true), - schema_ids: vec!["rust.cultmesh.note.v1".to_string()], - key_prefix: Some("note:".to_string()), - primary_endpoints: vec!["rudp://127.0.0.1:4100".to_string()], - replica_endpoints: Vec::new(), - read_replica_endpoints: Vec::new(), - region: None, - authority_lease_id: Some("lease:rust-notes-a".to_string()), - })?; - let matching = shards.list(&cultnet_rs::CultNetShardCatalogOptions { - schema_ids: Some(vec!["rust.cultmesh.note.v1".to_string()]), - record_keys: Some(vec!["note:1".to_string()]), - }); - assert_eq!(matching.len(), 1); - assert_eq!(matching[0].shard_id, "rust-notes-a"); - assert!(shards.get("rust-notes-a").is_some()); - Ok(()) -} - -#[test] -fn cultmesh_authority_leases_gate_peer_contact_hints() -> Result<()> { - let now = Utc::now(); - let peer = CultMeshPeerCard::new("rust-peer", "public", ["rudp://127.0.0.1:4100"]) - .with_roles(["shard-primary"]) - .with_authority_lease_id("lease:rust-peer"); - let mut leases = CultMesh::create_authority_lease_catalog(); - let mut peers = CultMesh::create_peer_catalog(); - peers.upsert(peer.clone())?; - - assert!(!leases.is_authorized(&peer, "shard-primary", Some("players"), now)); - assert!( - peers - .find_authorized("public", "shard-primary", &leases, Some("players"), now) - .is_empty() - ); - leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-peer".to_string(), - verse_id: "public".to_string(), - peer_id: "rust-peer".to_string(), - roles: vec!["shard-primary".to_string()], - shard_ids: vec!["players".to_string()], - issuer_runtime_id: Some("odin".to_string()), - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), - })?; - - assert!(leases.is_authorized(&peer, "shard-primary", Some("players"), now)); - assert_eq!( - peers.find_authorized("public", "shard-primary", &leases, Some("players"), now), - vec![peer.clone()] - ); - assert_eq!( - peers.first_authorized("public", "shard-primary", &leases, Some("players"), now), - Some(peer.clone()) - ); - assert!(!leases.is_authorized(&peer, "schema", Some("players"), now)); - assert!( - peers - .first_authorized("public", "schema", &leases, Some("players"), now) - .is_none() - ); - assert!(!leases.is_authorized(&peer, "shard-primary", Some("inventory"), now)); - assert!(!leases.is_authorized( - &peer, - "shard-primary", - Some("players"), - now + Duration::minutes(1) - )); - Ok(()) -} - -#[test] -fn legacy_gamecult_networking_contract_round_trips_login_union() -> Result<()> { - let message = CultNetMessage::Login { - nonce: CultNetSecret::to_base64_url(b"nonce"), - auth: CultNetSecret::to_base64_url(b"auth"), - password: CultNetSecret::to_base64_url(b"password"), - }; - let wire = - encode_cultnet_message_for_wire(&message, CultNetWireContract::GameCultNetworkingV0)?; - let items = wire.as_array().expect("legacy union is an array"); - assert_eq!(items[0].as_i64(), Some(0)); - assert_eq!( - items[1].as_array().unwrap()[0].as_slice(), - Some(&b"nonce"[..]) - ); - - let bytes = rmp_serde::to_vec(&wire)?; - let decoded = - decode_cultnet_message_from_slice(&bytes, CultNetWireContract::GameCultNetworkingV0)?; - assert_eq!(decoded, message); - Ok(()) -} - -#[test] -fn rust_decodes_typescript_generated_cultnet_frames() -> Result<()> { - let mut framer = LengthPrefixedMessageFramer::new(); - let hello_frames = framer.push(TS_HELLO_FRAME); - assert_eq!(hello_frames.len(), 1); - let hello = - decode_cultnet_message_from_slice(&hello_frames[0], CultNetWireContract::CultNetSchemaV0)?; - assert_eq!( - hello, - CultNetMessage::Hello { - runtime_id: "voidbot-main".to_string(), - runtime_kind: "node-worker".to_string(), - agent_id: Some("void".to_string()), - role: None, - display_name: Some("Void".to_string()), - supported_document_types: Some(vec!["ghostlight.agent-state".to_string()]), - supported_mutation_contracts: None, - supported_message_versions: None, - transport_profiles: None, - supports_schema_catalog: None, - } - ); - - let mut framer = LengthPrefixedMessageFramer::new(); - let login_frames = framer.push(TS_LEGACY_LOGIN_FRAME); - assert_eq!(login_frames.len(), 1); - let login = decode_cultnet_message_from_slice( - &login_frames[0], - CultNetWireContract::GameCultNetworkingV0, - )?; - assert_eq!( - login, - CultNetMessage::Login { - nonce: "bm9uY2U".to_string(), - auth: "YXV0aA".to_string(), - password: "cGFzc3dvcmQ".to_string(), - } - ); - Ok(()) -} - -#[test] -fn document_registry_advertises_mutation_contracts_with_bindings() -> Result<()> { - let mut registry = CultNetDocumentRegistry::new(); - let contract = CultNetDocumentMutationContract { - document_type: "ghostlight.agent-state".to_string(), - payload_schema_version: Some("ghostlight.agent_state.v0".to_string()), - operations: vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::IntentSubmit, - CultNetDocumentOperation::ReceiptWatch, - ], - authority: CultNetMutationAuthority::Coordinator, - intent_document_types: Some(vec!["ghostlight.agent-state.intent.v0".to_string()]), - receipt_document_types: Some(vec!["ghostlight.agent-state.receipt.v0".to_string()]), - notes: Some(vec![ - "Intent documents are reviewed before cache mutation.".to_string(), - ]), - }; - registry.register( - CultNetDocumentBinding::for_entry::( - "ghostlight.agent_state.v0".to_string(), - ) - .with_mutation_contract(contract.clone()), - ); - - assert_eq!(registry.mutation_contracts(), vec![contract]); - Ok(()) -} - -#[test] -fn document_registry_replicates_typed_cultcache_state() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin.msgpack"); - let target_store = temp.path().join("target.msgpack"); - let payload = GhostlightAgentStateFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - }; - - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry::< - GhostlightAgentStateFixture, - >("ghostlight.agent_state.v0".to_string())); - - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.pull_all_backing_stores()?; - origin.put("epiphany.persona", &payload)?; - - let snapshot = registry.create_snapshot_response(&origin, "snapshot-1", None, None)?; - - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - target.pull_all_backing_stores()?; - let applied = - registry.apply_snapshot_response::(&mut target, &snapshot)?; - let synced = registry.sync_document_from_snapshot_response::( - &mut target, - &snapshot, - "epiphany.persona", - )?; - assert_eq!(applied, vec![payload.clone()]); - assert_eq!(synced, payload); - assert_eq!( - target.get_required::("epiphany.persona")?, - payload - ); - - let direct_put = registry.create_document_put_message( - "put-1", - "epiphany.persona", - &GhostlightAgentStateFixture { - display_name: "Persona Prime".to_string(), - ..payload - }, - CultNetDocumentPutOptions::default(), - )?; - let updated = registry - .apply_document_put_message::(&mut target, &direct_put)?; - assert_eq!(updated.display_name, "Persona Prime"); - Ok(()) -} - -#[test] -fn raw_snapshot_replication_preserves_messagepack_payload_bytes() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin-raw.msgpack"); - let target_store = temp.path().join("target-raw.msgpack"); - let payload = GhostlightAgentStateFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - }; - - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry::< - GhostlightAgentStateFixture, - >("ghostlight.agent_state.v0".to_string())); - - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.pull_all_backing_stores()?; - origin.put("epiphany.persona", &payload)?; - - let raw_snapshot = - registry.create_raw_snapshot_response(&origin, "raw-snapshot-1", None, None)?; - let source_envelope = - origin.get_required_envelope::("epiphany.persona")?; - - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - target.pull_all_backing_stores()?; - let applied = registry - .apply_raw_snapshot_response::(&mut target, &raw_snapshot)?; - let target_envelope = - target.get_required_envelope::("epiphany.persona")?; - - assert_eq!(applied, vec![payload.clone()]); - assert_eq!(target_envelope.payload, source_envelope.payload); - assert_eq!( - target.get_required::("epiphany.persona")?, - payload - ); - Ok(()) -} - -#[test] -fn raw_snapshot_replication_hydrates_same_schema_rust_aliases() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin-alias.msgpack"); - let target_store = temp.path().join("target-alias.msgpack"); - let canonical = GhostlightAgentStateFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - }; - - let mut origin_registry = CultNetDocumentRegistry::new(); - origin_registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightAgentStateFixture, - >( - "ghostlight.agent_state.v0".to_string(), - "ghostlight.agent_state.v0".to_string(), - )); - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.pull_all_backing_stores()?; - origin.put("epiphany.persona", &canonical)?; - let raw_snapshot = origin_registry.create_raw_snapshot_response( - &origin, - "raw-snapshot-alias", - Some(&["ghostlight.agent_state.v0".to_string()]), - Some(&["epiphany.persona".to_string()]), - )?; - - let mut alias_registry = CultNetDocumentRegistry::new(); - alias_registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightAgentStateUiFixture, - >( - "ghostlight.agent_state.v0".to_string(), - "ghostlight.agent_state.v0".to_string(), - )); - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - target.pull_all_backing_stores()?; - - let applied = alias_registry - .sync_raw_document_from_snapshot_response::( - &mut target, - &raw_snapshot, - "epiphany.persona", - )?; - - assert_eq!( - applied, - GhostlightAgentStateUiFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - } - ); - assert_eq!( - target.get_required::("epiphany.persona")?, - applied - ); - Ok(()) -} - -#[test] -fn reactive_document_coalesces_direct_same_schema_alias_member_writes() -> Result<()> { - let temp = tempfile::tempdir()?; - let target_store = temp.path().join("target-reactive-alias.msgpack"); - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightReactiveNoteUiFixture, - >( - "ghostlight.reactive_note.v0".to_string(), - "ghostlight.reactive_note.v0".to_string(), - )); - let cache = Arc::new(Mutex::new(CultCache::new())); - { - let mut cache = cache.lock().expect("cache mutex"); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - cache.pull_all_backing_stores()?; - cache.put( - "note:reactive", - &GhostlightReactiveNoteUiFixture { - body: "initial".to_string(), - revision: 1, - }, - )?; - } - - let reactive = registry.reactive_document::( - Arc::clone(&cache), - "note:reactive", - CultNetReactiveDocumentOptions { - flush_delay: StdDuration::from_millis(5), - ..CultNetReactiveDocumentOptions::default() - }, - )?; - { - let current = reactive.current(); - let mut current = current.lock().expect("reactive current mutex"); - current.body = "first-local-edit".to_string(); - current.body = "second-local-edit".to_string(); - current.revision = 2; - } - - for _ in 0..30 { - let body = cache - .lock() - .expect("cache mutex") - .get_required::("note:reactive")? - .body; - if body == "second-local-edit" { - break; - } - thread::sleep(StdDuration::from_millis(5)); - } - - let stored = cache - .lock() - .expect("cache mutex") - .get_required::("note:reactive")?; - assert_eq!(stored.body, "second-local-edit"); - assert_eq!(stored.revision, 2); - assert!(!reactive.is_dirty()); - assert!(reactive.last_error().is_none()); - Ok(()) -} - -#[test] -fn reactive_document_tracks_canonical_reconciliation_delta() -> Result<()> { - let temp = tempfile::tempdir()?; - let target_store = temp.path().join("target-reactive-reconcile.msgpack"); - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightReactiveNoteFixture, - >( - "ghostlight.reactive_note.v0".to_string(), - "ghostlight.reactive_note.v0".to_string(), - )); - let cache = Arc::new(Mutex::new(CultCache::new())); - { - let mut cache = cache.lock().expect("cache mutex"); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - cache.pull_all_backing_stores()?; - cache.put( - "note:reconcile", - &GhostlightReactiveNoteFixture { - body: "initial".to_string(), - revision: 1, - }, - )?; - } - - let reactive = registry.reactive_document::( - Arc::clone(&cache), - "note:reconcile", - CultNetReactiveDocumentOptions { - flush_delay: StdDuration::from_secs(60), - detect_local_changes: false, - ..CultNetReactiveDocumentOptions::default() - }, - )?; - reactive.update(|note| { - note.body = "local-prediction".to_string(); - note.revision = 2; - })?; - let authoritative = registry.create_document_put_message( - "put-authoritative", - "note:reconcile", - &GhostlightReactiveNoteFixture { - body: "authoritative".to_string(), - revision: 7, - }, - CultNetDocumentPutOptions::default(), - )?; - - reactive.apply_document_put_message(&authoritative)?; - - let current = reactive.current(); - let current = current.lock().expect("reactive current mutex").clone(); - assert_eq!(current.body, "local-prediction"); - let reconciliation = reactive - .reconciliation() - .expect("dirty canonical apply records reconciliation"); - assert_eq!(reconciliation.canonical.body, "authoritative"); - assert_eq!(reconciliation.predicted.body, "local-prediction"); - assert_eq!( - reconciliation.delta.get("0"), - Some(&serde_json::json!("local-prediction")) - ); - assert_eq!( - reconciliation.delta.get("1"), - Some(&serde_json::json!(-5.0)) - ); - - reactive.flush()?; - let stored = cache - .lock() - .expect("cache mutex") - .get_required::("note:reconcile")?; - assert_eq!(stored.body, "local-prediction"); - assert!(reactive.reconciliation().is_none()); - Ok(()) -} - -#[test] -fn client_security_keeps_the_connection_key_visible_without_exposing_cipher_logic() -> Result<()> { - let options = CultNetClientSecurityOptions::development(); - assert_eq!(options.connection_key, "gamecult-dev-connection-key"); - assert_ne!(options.encryption_key(), [0_u8; 32]); - Ok(()) -} - -#[test] -fn builtin_schema_registry_advertises_canonical_ghostlight_schema_without_inline_body_by_default() --> Result<()> { - let registry = builtin_schema_registry()?; - let response = registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest { - message_id: "catalog-1".to_string(), - include_schema_json: None, - schema_ids: None, - kinds: None, - })?; - - let CultNetMessage::SchemaCatalogResponse { schemas, .. } = response else { - panic!("expected catalog response"); - }; - - let ghostlight = schemas - .iter() - .find(|schema| schema.document_type.as_deref() == Some("ghostlight.agent-state")) - .expect("ghostlight agent-state schema is advertised"); - - assert_eq!(ghostlight.kind, CultNetSchemaKind::DocumentPayload); - assert_eq!( - ghostlight.schema_version.as_deref(), - Some("ghostlight.agent_state.v0") - ); - assert_eq!(ghostlight.schema_json, None); - assert!(!ghostlight.content_hash.is_empty()); - - let transport_profile = schemas - .iter() - .find(|schema| schema.schema_version.as_deref() == Some("cultnet.transport_profile.v0")) - .expect("transport profile schema is advertised"); - - assert_eq!(transport_profile.kind, CultNetSchemaKind::SharedContract); - assert_eq!( - transport_profile.schema_id, - "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json" - ); - assert_eq!( - transport_profile.wire_contracts, - vec![CultNetWireContract::CultNetSchemaV0] - ); - Ok(()) -} - -#[test] -fn builtin_schema_registry_advertises_shard_catalog_wire_messages() -> Result<()> { - let registry = builtin_schema_registry()?; - let response = registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest { - message_id: "catalog-shards".to_string(), - include_schema_json: None, - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::WireMessage]), - })?; - - let CultNetMessage::SchemaCatalogResponse { schemas, .. } = response else { - panic!("expected catalog response"); - }; - - assert!(schemas.iter().any(|schema| { - schema.schema_version.as_deref() == Some("cultnet.shard_catalog_request.v0") - })); - assert!(schemas.iter().any(|schema| { - schema.schema_version.as_deref() == Some("cultnet.shard_catalog_response.v0") - })); - Ok(()) -} - -#[test] -fn shard_catalog_filters_descriptors_and_applies_remote_responses() -> Result<()> { - let mut catalog = CultNetShardCatalog::new(); - catalog.upsert(CultNetShardDescriptor { - shard_id: "notes-a".to_string(), - owner_runtime_id: "rust-primary".to_string(), - epoch: 3, - is_primary: Some(true), - schema_ids: vec!["note.v0".to_string()], - key_prefix: Some("note:".to_string()), - primary_endpoints: vec!["rudp://127.0.0.1:4100".to_string()], - replica_endpoints: vec![], - read_replica_endpoints: vec![], - region: Some("local".to_string()), - authority_lease_id: Some("lease:notes-a".to_string()), - })?; - catalog.upsert(CultNetShardDescriptor { - shard_id: "players-a".to_string(), - owner_runtime_id: "rust-primary".to_string(), - epoch: 1, - is_primary: Some(false), - schema_ids: vec!["player.v0".to_string()], - key_prefix: Some("player:".to_string()), - primary_endpoints: vec!["cultnet://127.0.0.1:3075".to_string()], - replica_endpoints: vec![], - read_replica_endpoints: vec![], - region: None, - authority_lease_id: None, - })?; - - let response = catalog.create_catalog_response(&CultNetMessage::ShardCatalogRequest { - message_id: "shards".to_string(), - schema_ids: Some(vec!["note.v0".to_string()]), - record_keys: Some(vec!["note:1".to_string()]), - })?; - - let CultNetMessage::ShardCatalogResponse { message_id, shards } = &response else { - panic!("expected shard catalog response"); - }; - assert_eq!(message_id, "shards"); - assert_eq!(shards.len(), 1); - assert_eq!(shards[0].shard_id, "notes-a"); - - let bytes = encode_cultnet_message_to_vec(&response, CultNetWireContract::CultNetSchemaV0)?; - let decoded = decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)?; - assert_eq!(decoded, response); - - let mut applied = CultNetShardCatalog::new(); - let applied_shards = applied.apply_response(&decoded)?; - assert_eq!(applied_shards.len(), 1); - assert!(applied.get("notes-a").is_some_and(|shard| { - shard.serves(Some("note.v0"), Some("note:2")) - && !shard.serves(Some("note.v0"), Some("player:2")) - })); - Ok(()) -} - -#[test] -fn schema_discovery_round_trips_over_legacy_gamecult_contract_when_inline_schemas_are_requested() --> Result<()> { - let registry = { - let mut registry = CultNetSchemaRegistry::new(); - registry.register(cultnet_rs::CultNetSchemaRegistration { - schema_id: "https://example.test/contracts/example.schema.json".to_string(), - kind: CultNetSchemaKind::SharedContract, - wire_contracts: vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - schema_version: None, - document_type: None, - title: Some("Example Schema".to_string()), - schema_json: Some( - r#"{ - "$schema":"https://json-schema.org/draft/2020-12/schema", - "$id":"https://example.test/contracts/example.schema.json", - "title":"Example Schema", - "type":"object", - "properties":{"value":{"type":"string"}}, - "required":["value"], - "additionalProperties":false - }"# - .to_string(), - ), - })?; - registry - }; - - let response = registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest { - message_id: "catalog-legacy".to_string(), - include_schema_json: Some(true), - schema_ids: None, - kinds: None, - })?; - let wire = - encode_cultnet_message_for_wire(&response, CultNetWireContract::GameCultNetworkingV0)?; - let bytes = rmp_serde::to_vec(&wire)?; - let decoded = - decode_cultnet_message_from_slice(&bytes, CultNetWireContract::GameCultNetworkingV0)?; - - let CultNetMessage::SchemaCatalogResponse { - message_id, - schemas, - } = decoded - else { - panic!("expected legacy schema catalog response"); - }; - - assert_eq!(message_id, "catalog-legacy"); - assert_eq!( - schemas[0].schema_id, - "https://example.test/contracts/example.schema.json" - ); - assert!( - schemas[0] - .schema_json - .as_deref() - .is_some_and(|schema| schema.contains("\"value\"")) - ); - Ok(()) -} diff --git a/vendor/cultnet-rs/tests/fixtures/cultnet-ts-hello.frame b/vendor/cultnet-rs/tests/fixtures/cultnet-ts-hello.frame deleted file mode 100644 index e2b22d5..0000000 Binary files a/vendor/cultnet-rs/tests/fixtures/cultnet-ts-hello.frame and /dev/null differ diff --git a/vendor/cultnet-rs/tests/fixtures/cultnet-ts-legacy-login.frame b/vendor/cultnet-rs/tests/fixtures/cultnet-ts-legacy-login.frame deleted file mode 100644 index c6f335d..0000000 Binary files a/vendor/cultnet-rs/tests/fixtures/cultnet-ts-legacy-login.frame and /dev/null differ diff --git a/vendor/cultnet-rs/tests/provider_session.rs b/vendor/cultnet-rs/tests/provider_session.rs deleted file mode 100644 index 4ad7c23..0000000 --- a/vendor/cultnet-rs/tests/provider_session.rs +++ /dev/null @@ -1,365 +0,0 @@ -use anyhow::Result; -use cultnet_rs::*; - -const REGISTRATION_GOLDEN: &str = "hqpwcm92aWRlcklkqGFldGhlcmlhsXNlcnZpY2VJbnN0YW5jZUlkq2FldGhlcmlhLTQyqmVuZHBvaW50SWSvYWV0aGVyaWEtcHVibGljp3ZlcnNlSWSmcHVibGljuHJlcXVlc3RlZExlYXNlRHVyYXRpb25Nc811MLBhdXRob3JpdHlMZWFzZUlkq2F1dGhvcml0eS03"; -const CONNECT_EVIDENCE_GOLDEN: &str = - "gq9jbGllbnRTZXNzaW9uSWSyYWV0aGVyaWEtY2xpZW50LTQyrHNlc3Npb25Ub2tlbrJvZGluLXNlc3Npb24tdG9rZW4="; -const TOKENLESS_CONNECT_EVIDENCE_GOLDEN: &str = - "gq9jbGllbnRTZXNzaW9uSWSwYW5vbnltb3VzLWNsaWVudKxzZXNzaW9uVG9rZW7A"; - -fn registration() -> CultMeshProviderRegistrationWire { - CultMeshProviderRegistrationWire { - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - endpoint_id: "aetheria-public".to_string(), - verse_id: "public".to_string(), - requested_lease_duration_ms: 30_000, - authority_lease_id: Some("authority-7".to_string()), - } -} - -#[test] -fn registration_matches_csharp_and_typescript_golden_payload() -> Result<()> { - let encoded = encode_provider_session_payload(®istration())?; - assert_eq!(encoded, REGISTRATION_GOLDEN); - assert_eq!( - decode_provider_session_payload::(&encoded)?, - registration() - ); - Ok(()) -} - -#[test] -fn connect_evidence_is_a_validated_named_messagepack_map() -> Result<()> { - let evidence = CultMeshProviderConnectEvidenceWire { - client_session_id: "aetheria-client-42".to_string(), - session_token: Some("odin-session-token".to_string()), - }; - let encoded = encode_provider_connect_evidence(&evidence)?; - assert_eq!( - base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &encoded), - CONNECT_EVIDENCE_GOLDEN - ); - assert_eq!(decode_provider_connect_evidence(&encoded)?, evidence); - let value: rmpv::Value = rmp_serde::from_slice(&encoded)?; - let map = value.as_map().expect("Connect evidence is a named map"); - assert_eq!( - map.iter() - .find_map(|(key, value)| (key.as_str() == Some("clientSessionId")).then_some(value)) - .and_then(rmpv::Value::as_str), - Some("aetheria-client-42") - ); - assert_eq!( - map.iter() - .find_map(|(key, value)| (key.as_str() == Some("sessionToken")).then_some(value)) - .and_then(rmpv::Value::as_str), - Some("odin-session-token") - ); - - assert!( - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: String::new(), - session_token: None, - }) - .is_err() - ); - assert!( - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "client".to_string(), - session_token: Some(" ".to_string()), - }) - .is_err() - ); - assert!(decode_provider_connect_evidence(&[]).is_err()); - let tokenless = CultMeshProviderConnectEvidenceWire { - client_session_id: "anonymous-client".to_string(), - session_token: None, - }; - let tokenless_encoded = encode_provider_connect_evidence(&tokenless)?; - assert_eq!( - base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - &tokenless_encoded - ), - TOKENLESS_CONNECT_EVIDENCE_GOLDEN - ); - assert_eq!( - decode_provider_connect_evidence(&tokenless_encoded)?, - tokenless - ); - Ok(()) -} - -#[test] -fn registration_round_trips_inside_canonical_operation_envelope() -> Result<()> { - let request = create_provider_session_request( - "register-1", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - ®istration(), - Some("aetheria-42".to_string()), - Some("odin".to_string()), - )?; - let bytes = encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)?; - let decoded_request = - decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)?; - let decoded: CultMeshProviderRegistrationWire = decode_provider_session_request( - &decoded_request, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - )?; - assert_eq!(decoded, registration()); - - let lease = CultMeshProviderLeaseWire { - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - endpoint_id: "aetheria-public".to_string(), - verse_id: "public".to_string(), - lease_id: "lease-2".to_string(), - valid_from_utc: "2026-07-14T12:00:00Z".to_string(), - expires_at_utc: "2026-07-14T12:00:10Z".to_string(), - }; - let response = create_provider_session_response( - &decoded_request, - CultMeshProviderOperationStatus::Ok, - CULTMESH_PROVIDER_LEASE_SCHEMA, - &lease, - Some("odin".to_string()), - )?; - let (status, decoded_lease) = decode_provider_session_response::( - &response, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_LEASE_SCHEMA, - )?; - assert_eq!(status, CultMeshProviderOperationStatus::Ok); - assert_eq!(decoded_lease, lease); - Ok(()) -} - -#[test] -fn publication_and_command_preserve_typed_messagepack_values() -> Result<()> { - let publication = CultMeshProviderPublicationPutWire { - lease_id: "lease-1".to_string(), - publication_id: "surface:pilot".to_string(), - document: CultNetRawDocumentRecord { - schema_id: "gamecult.eve.surface.v1".to_string(), - record_key: "eve:surface:pilot".to_string(), - stored_at: String::new(), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: vec![0x81, 0xa2, 0x69, 0x64, 0x01], - source_runtime_id: Some("aetheria-42".to_string()), - source_agent_id: None, - source_role: None, - tags: None, - }, - }; - let publication_encoded = encode_provider_session_payload(&publication)?; - assert_eq!( - decode_provider_session_payload::( - &publication_encoded - )?, - publication - ); - - let command = CultMeshProviderCommandWire { - command_id: "cmd-7".to_string(), - command_kind: "pilot.thrust".to_string(), - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - payload: rmpv::Value::Map(vec![(rmpv::Value::from("axis"), rmpv::Value::F64(0.75))]), - }; - let command_bytes = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - encode_provider_session_payload(&command)?, - )?; - let decoded_command = decode_provider_command_document(&CultNetRawDocumentRecord { - schema_id: CULTMESH_PROVIDER_COMMAND_SCHEMA.to_string(), - record_key: "provider-command:aetheria:aetheria-42:cmd-7".to_string(), - stored_at: String::new(), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: command_bytes, - source_runtime_id: Some("odin".to_string()), - source_agent_id: None, - source_role: Some("provider-session-broker".to_string()), - tags: None, - })?; - assert_eq!(decoded_command, command); - Ok(()) -} - -#[test] -fn lifecycle_validation_rejects_split_or_malformed_authority() { - let mismatched = create_provider_session_request( - "bad-schema", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_PUBLICATION_PUT_SCHEMA, - ®istration(), - None, - None, - ); - assert!(mismatched.is_err()); - - let malformed_lease = CultMeshProviderLeaseWire { - provider_id: "provider".to_string(), - service_instance_id: "instance".to_string(), - endpoint_id: "endpoint".to_string(), - verse_id: "verse".to_string(), - lease_id: "lease".to_string(), - valid_from_utc: "2026-07-14T12:00:10Z".to_string(), - expires_at_utc: "2026-07-14T12:00:00Z".to_string(), - }; - assert!(encode_provider_session_payload(&malformed_lease).is_err()); - - let malformed_request = CultNetMessage::OperationRequest { - message_id: String::new(), - service_id: CULTMESH_PROVIDER_SESSION_SERVICE_ID.to_string(), - operation: String::new(), - payload_schema: CULTMESH_PROVIDER_REGISTRATION_SCHEMA.to_string(), - payload_encoding: CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING.to_string(), - payload: REGISTRATION_GOLDEN.to_string(), - source_runtime_id: None, - target_runtime_id: None, - }; - assert!( - create_provider_session_response( - &malformed_request, - CultMeshProviderOperationStatus::Invalid, - CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA, - &CultMeshProviderMutationAcceptanceWire { - accepted_at_utc: "2026-07-14T12:00:00Z".to_string(), - lease_id: None, - publication_id: None, - command_id: None, - receipt_id: None, - }, - None, - ) - .is_err() - ); -} - -#[test] -fn remaining_lifecycle_payloads_round_trip_as_named_maps() -> Result<()> { - let renewal = CultMeshProviderLeaseRenewalWire { - lease_id: "lease-1".to_string(), - requested_lease_duration_ms: 30_000, - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&renewal)? - )?, - renewal - ); - - let deletion = CultMeshProviderPublicationDeleteWire { - lease_id: "lease-1".to_string(), - publication_id: "surface:pilot".to_string(), - schema_id: "gamecult.eve.surface.v1".to_string(), - record_key: "eve:surface:pilot".to_string(), - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&deletion)? - )?, - deletion - ); - - let receipt = CultMeshProviderReceiptPutWire { - lease_id: "lease-1".to_string(), - receipt: CultMeshProviderCommandReceiptWire { - receipt_id: "receipt-7".to_string(), - command_id: "cmd-7".to_string(), - command_kind: "pilot.thrust".to_string(), - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - state: CultMeshProviderReceiptStateWire::Applied, - completed_at_utc: "2026-07-14T12:01:02.345Z".to_string(), - result: Some(rmpv::Value::Map(vec![( - rmpv::Value::from("accepted"), - rmpv::Value::Boolean(true), - )])), - error: None, - }, - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&receipt)? - )?, - receipt - ); - - let withdrawal = CultMeshProviderWithdrawalWire { - lease_id: "lease-1".to_string(), - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&withdrawal)? - )?, - withdrawal - ); - - let acceptance = CultMeshProviderMutationAcceptanceWire { - accepted_at_utc: "2026-07-14T12:01:03Z".to_string(), - lease_id: Some("lease-1".to_string()), - publication_id: Some("surface:pilot".to_string()), - command_id: None, - receipt_id: None, - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&acceptance)? - )?, - acceptance - ); - Ok(()) -} - -#[test] -fn non_ok_response_uses_correlated_empty_map_and_diagnostics() -> Result<()> { - let request = create_provider_session_request( - "register-denied", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - ®istration(), - Some("aetheria-42".to_string()), - Some("odin".to_string()), - )?; - let response = create_provider_session_error_response( - &request, - CultMeshProviderOperationStatus::Denied, - vec!["registration authority rejected the session".to_string()], - Some("odin".to_string()), - )?; - let CultNetMessage::OperationResponse { - message_id, - operation, - status, - payload_schema, - payload, - diagnostics, - .. - } = response - else { - panic!("expected operation response"); - }; - assert_eq!(message_id, "register-denied"); - assert_eq!(operation, CULTMESH_PROVIDER_REGISTER_OPERATION); - assert_eq!(status, "denied"); - assert_eq!(payload_schema, CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA); - assert_eq!(diagnostics.len(), 1); - let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, payload)?; - assert_eq!( - rmp_serde::from_slice::(&bytes)?, - rmpv::Value::Map(Vec::new()) - ); - assert!( - create_provider_session_error_response( - &request, - CultMeshProviderOperationStatus::Ok, - vec!["not an error".to_string()], - None, - ) - .is_err() - ); - Ok(()) -} diff --git a/vendor/cultnet-rs/tests/rudp_server_hub.rs b/vendor/cultnet-rs/tests/rudp_server_hub.rs deleted file mode 100644 index a8a2c11..0000000 --- a/vendor/cultnet-rs/tests/rudp_server_hub.rs +++ /dev/null @@ -1,459 +0,0 @@ -use std::net::UdpSocket; -use std::time::Duration; - -use anyhow::{Result, anyhow}; -use cultnet_rs::*; - -const CONNECTION_ID: u32 = 0x4355_4c54; - -fn socket() -> Result { - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - Ok(socket) -} - -fn client(remote: std::net::SocketAddr) -> Result { - let mut options = - CultNetRudpSocketTransportOptions::client("provider", socket()?, remote, CONNECTION_ID); - options.max_fragment_bytes = Some(2_048); - CultNetRudpSocketTransportConnection::new(options) -} - -fn connect( - hub: &mut CultNetRudpServerHub, - client: &mut CultNetRudpSocketTransportConnection, - evidence: &[u8], -) -> Result { - client.connect(evidence.to_vec())?; - let mut connected = None; - for _ in 0..20 { - if let Some(CultNetRudpServerEvent::Connected { session }) = hub.receive_event_once()? { - connected = Some(session); - break; - } - } - let session = connected.ok_or_else(|| anyhow!("hub did not receive Connect"))?; - let _ = client.receive_once()?; - assert!(client.connected()); - Ok(session) -} - -fn receive_frame(hub: &mut CultNetRudpServerHub) -> Result { - for _ in 0..20 { - if let Some(event @ CultNetRudpServerEvent::Frame { .. }) = hub.receive_event_once()? { - return Ok(event); - } - } - Err(anyhow!("hub did not receive a frame")) -} - -fn receive_message(client: &mut CultNetRudpSocketTransportConnection) -> Result { - for _ in 0..20 { - if let Some(message) = client.receive_schema_message_once()? { - return Ok(message); - } - } - Err(anyhow!("client did not receive a schema message")) -} - -#[test] -fn hub_keeps_independent_peer_sessions_and_exposes_connect_evidence() -> Result<()> { - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let mut first = client(server_addr)?; - let mut second = client(server_addr)?; - - let first_session = connect(&mut hub, &mut first, b"session-token-one")?; - let second_session = connect(&mut hub, &mut second, b"session-token-two")?; - assert_ne!(first_session.remote_addr, second_session.remote_addr); - assert_eq!(first_session.connect_payload, b"session-token-one"); - assert_eq!(second_session.connect_payload, b"session-token-two"); - assert_eq!(hub.sessions().len(), 2); - - let first_request = create_provider_session_request( - "register-first", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - &CultMeshProviderRegistrationWire { - provider_id: "first".to_string(), - service_instance_id: "first-1".to_string(), - endpoint_id: "first-public".to_string(), - verse_id: "public".to_string(), - requested_lease_duration_ms: 30_000, - authority_lease_id: None, - }, - Some("first-1".to_string()), - Some("odin".to_string()), - )?; - let second_request = create_provider_session_request( - "register-second", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - &CultMeshProviderRegistrationWire { - provider_id: "second".to_string(), - service_instance_id: "second-1".to_string(), - endpoint_id: "second-public".to_string(), - verse_id: "public".to_string(), - requested_lease_duration_ms: 30_000, - authority_lease_id: None, - }, - Some("second-1".to_string()), - Some("odin".to_string()), - )?; - first.send_schema_message(&first_request)?; - second.send_schema_message(&second_request)?; - - for _ in 0..2 { - let CultNetRudpServerEvent::Frame { session, frame } = receive_frame(&mut hub)? else { - unreachable!(); - }; - assert_eq!(frame.channel_id, "schema"); - let request = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - let CultNetMessage::OperationRequest { message_id, .. } = &request else { - return Err(anyhow!("expected operation request")); - }; - let provider_id = if message_id == "register-first" { - "first" - } else { - "second" - }; - let lease = CultMeshProviderLeaseWire { - provider_id: provider_id.to_string(), - service_instance_id: format!("{provider_id}-1"), - endpoint_id: format!("{provider_id}-public"), - verse_id: "public".to_string(), - lease_id: format!("lease-{provider_id}"), - valid_from_utc: "2026-07-14T12:00:00Z".to_string(), - expires_at_utc: "2026-07-14T12:00:30Z".to_string(), - }; - let response = create_provider_session_response( - &request, - CultMeshProviderOperationStatus::Ok, - CULTMESH_PROVIDER_LEASE_SCHEMA, - &lease, - Some("odin".to_string()), - )?; - hub.send_schema_message(&session, &response)?; - } - - let first_response = receive_message(&mut first)?; - let second_response = receive_message(&mut second)?; - let (_, first_lease) = decode_provider_session_response::( - &first_response, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_LEASE_SCHEMA, - )?; - let (_, second_lease) = decode_provider_session_response::( - &second_response, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_LEASE_SCHEMA, - )?; - assert_eq!(first_lease.provider_id, "first"); - assert_eq!(second_lease.provider_id, "second"); - assert_eq!(hub.session(first_session.remote_addr), Some(&first_session)); - assert_eq!( - hub.session(second_session.remote_addr), - Some(&second_session) - ); - Ok(()) -} - -#[test] -fn hub_fences_replaced_generations_and_does_not_replace_connect_retransmits() -> Result<()> { - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let mut provider = client(server_addr)?; - let original_evidence = - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "provider-client-1".to_string(), - session_token: Some("shared-session-token".to_string()), - })?; - let replacement_evidence = - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "provider-client-2".to_string(), - session_token: Some("shared-session-token".to_string()), - })?; - let original = connect(&mut hub, &mut provider, &original_evidence)?; - - provider.connect(original_evidence.clone())?; - for _ in 0..3 { - assert!(hub.receive_event_once()?.is_none()); - } - assert_eq!(hub.session(original.remote_addr), Some(&original)); - - provider.connect(replacement_evidence.clone())?; - let mut disconnected = None; - for _ in 0..20 { - if let Some(event @ CultNetRudpServerEvent::Disconnected { .. }) = - hub.receive_event_once()? - { - disconnected = Some(event); - break; - } - } - let disconnected = - disconnected.ok_or_else(|| anyhow!("replacement did not disconnect old generation"))?; - let CultNetRudpServerEvent::Disconnected { session, .. } = disconnected else { - return Err(anyhow!("expected old generation Disconnected event")); - }; - assert_eq!(session, original); - let connected = hub - .receive_event_once()? - .ok_or_else(|| anyhow!("replacement did not connect new generation"))?; - let CultNetRudpServerEvent::Connected { - session: replacement, - } = connected - else { - return Err(anyhow!("expected replacement Connected event")); - }; - assert_ne!(replacement.session_generation, original.session_generation); - let original_decoded = decode_provider_connect_evidence(&original.connect_payload)?; - let replacement_decoded = decode_provider_connect_evidence(&replacement.connect_payload)?; - assert_eq!( - original_decoded.session_token, - replacement_decoded.session_token - ); - assert_ne!( - original_decoded.client_session_id, - replacement_decoded.client_session_id - ); - assert!( - hub.send(&original, "schema", vec![0x80]) - .unwrap_err() - .to_string() - .contains("no longer active") - ); - assert_eq!(hub.session(replacement.remote_addr), Some(&replacement)); - Ok(()) -} - -#[test] -fn identical_connect_retransmit_reuses_pending_accept_after_loss() -> Result<()> { - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let client_socket = socket()?; - let mut client_session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let evidence = encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "provider-client-lost-accept".to_string(), - session_token: Some("shared-session-token".to_string()), - })?; - let connect_packet = client_session.create_connect(0, evidence)?; - let connect_wire = encode_rudp_packet(&connect_packet)?; - - client_socket.send_to(&connect_wire, server_addr)?; - let CultNetRudpServerEvent::Connected { session } = hub - .receive_event_once()? - .ok_or_else(|| anyhow!("hub did not accept first Connect"))? - else { - return Err(anyhow!("hub did not emit Connected")); - }; - let mut buffer = vec![0_u8; 65_535]; - let (first_len, _) = client_socket.recv_from(&mut buffer)?; - let lost_accept = decode_rudp_packet(&buffer[..first_len])?; - assert_eq!(lost_accept.packet_type, CultNetRudpPacketType::Accept); - - client_socket.send_to(&connect_wire, server_addr)?; - assert!(hub.receive_event_once()?.is_none()); - let (resent_len, _) = client_socket.recv_from(&mut buffer)?; - let resent_accept = decode_rudp_packet(&buffer[..resent_len])?; - assert_eq!(resent_accept, lost_accept); - assert_eq!(hub.session(session.remote_addr), Some(&session)); - - client_session.receive(&resent_accept, 1)?; - assert!(client_session.connected()); - Ok(()) -} - -#[test] -fn exact_reliable_acks_clear_more_than_one_ack_window_of_fragments() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let connect_packet = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?; - sender.receive(&accept, 0)?; - receiver.receive(&sender.create_ack_for(accept.sequence), 0)?; - - let packets = sender.send_many( - "schema", - vec![0x5a; 40], - CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: 1, - }, - Some(1), - )?; - assert_eq!(packets.len(), 40); - let mut delivered = Vec::new(); - for packet in &packets { - let result = receiver.receive(packet, 2)?; - delivered.extend(result.delivered); - sender.receive(&receiver.create_ack_for(packet.sequence), 3)?; - } - assert_eq!(delivered.len(), 1); - assert_eq!(delivered[0].payload, vec![0x5a; 40]); - assert!(sender.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn session_and_hub_reject_configured_memory_bounds() -> Result<()> { - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - receiver.set_max_payload_bytes(Some(4)); - receiver.set_max_pending_fragment_sets(1)?; - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let connect_packet = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?; - sender.receive(&accept, 0)?; - - let oversized = sender.send_many( - "schema", - vec![1, 2, 3, 4, 5], - CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: 1, - }, - Some(2), - )?; - receiver.receive(&oversized[0], 1)?; - receiver.receive(&oversized[1], 1)?; - assert!(receiver.receive(&oversized[2], 1).is_err()); - - let fragment_options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: 2, - }; - let first_set = sender.send_many( - "schema", - vec![1, 2, 3, 4], - fragment_options.clone(), - Some(2), - )?; - let second_set = sender.send_many("schema", vec![5, 6, 7, 8], fragment_options, Some(2))?; - receiver.receive(&first_set[0], 2)?; - assert!(receiver.receive(&second_set[0], 2).is_err()); - - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut options = CultNetRudpServerHubOptions::new("odin", server_socket, CONNECTION_ID); - options.max_peers = 1; - let mut hub = CultNetRudpServerHub::new(options)?; - let mut first = client(server_addr)?; - let mut second = client(server_addr)?; - connect(&mut hub, &mut first, b"first")?; - second.connect(b"second".to_vec())?; - for _ in 0..20 { - match hub.receive_event_once() { - Err(error) => { - assert!(error.to_string().contains("peer limit")); - return Ok(()); - } - Ok(_) => continue, - } - } - Err(anyhow!("hub did not enforce peer limit")) -} - -#[test] -fn replay_history_is_bounded_and_idle_hub_sessions_expire() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let connect_packet = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?; - sender.receive(&accept, 0)?; - let mut oldest = None; - for sequence in 0..4_100 { - let packet = sender.send( - "realtime", - vec![(sequence % 251) as u8], - CultNetRudpSendOptions { - reliable: false, - ordered: false, - sequenced: false, - now_ms: sequence, - }, - )?; - if oldest.is_none() { - oldest = Some(packet.clone()); - } - assert_eq!(receiver.receive(&packet, sequence)?.delivered.len(), 1); - } - assert!( - receiver - .receive(&oldest.expect("oldest packet"), 4_101)? - .delivered - .is_empty() - ); - - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let mut provider = client(server_addr)?; - let context = connect(&mut hub, &mut provider, b"expiring-session")?; - std::thread::sleep(Duration::from_millis(2)); - assert_eq!(hub.remove_timed_out_sessions(0), vec![context]); - assert!(hub.sessions().is_empty()); - Ok(()) -}