From 67cbb4ea36b1c9952e7e3408e1bbb57267f5325f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 3 Sep 2026 23:09:45 +0200 Subject: [PATCH] Give the Rust CultMesh runtime a home The only Rust CultMesh implementation in existence was 1,363 lines inside Ghostlight's vendor directory. It was in no library repo and no standalone repo, so it had never been checked against the browser, Kotlin, and TypeScript CultMesh bodies it must interoperate with. It is not a duplicate of the facade in cultnet-rs. That one is the small surface the parity table claims for Rust: peer cards, authority leases, endpoint parsing. This is the stream and session layer -- node, document sets, RUDP document publication, stream descriptors, negotiation, stream catalog, and shared-memory frame rings -- which the same table lists under Rust's "Not claimed". A consumer grew the unclaimed surface because there was nowhere else to put it. Its path dependencies already matched this layout and it builds against CultLib's cultnet-rs unchanged, so it does not depend on the forked RUDP extras. Seven runtime-local tests pass, and CI now runs them. The parity table gains no claim from this. The Rust row says plainly that no interop lane exercises the stream surface, and expansion work records that runtime-local tests are not a parity claim. Landing code is not evidence. packages/cultmesh-rs needed an explicit .gitignore re-include; the NuGet packages rule hides every language package until one is added by name. Co-Authored-By: Claude Opus 5 --- .github/workflows/cultnet-interop.yml | 3 + .gitignore | 2 + docs/runtime-parity-scope.md | 6 +- packages/cultmesh-rs/Cargo.lock | 1183 +++++++++++++++++++++ packages/cultmesh-rs/Cargo.toml | 20 + packages/cultmesh-rs/src/lib.rs | 1363 +++++++++++++++++++++++++ 6 files changed, 2576 insertions(+), 1 deletion(-) create mode 100644 packages/cultmesh-rs/Cargo.lock create mode 100644 packages/cultmesh-rs/Cargo.toml create mode 100644 packages/cultmesh-rs/src/lib.rs diff --git a/.github/workflows/cultnet-interop.yml b/.github/workflows/cultnet-interop.yml index 33699446..3437add3 100644 --- a/.github/workflows/cultnet-interop.yml +++ b/.github/workflows/cultnet-interop.yml @@ -92,6 +92,9 @@ jobs: - name: Verify Rust RUDP reliability run: cargo test --manifest-path packages/cultnet-rs/Cargo.toml rudp + - name: Verify Rust CultMesh runtime-local surface + run: cargo test --manifest-path packages/cultmesh-rs/Cargo.toml + - name: Verify C# RUDP reliability run: dotnet test tests/GameCult.Networking.Tests/GameCult.Networking.Tests.csproj --filter "FullyQualifiedName~Rudp" diff --git a/.gitignore b/.gitignore index d639c709..ce48c2dc 100644 --- a/.gitignore +++ b/.gitignore @@ -307,6 +307,8 @@ node_modules/ !packages/cultcache-rs/** !packages/cultnet-rs/ !packages/cultnet-rs/** +!packages/cultmesh-rs/ +!packages/cultmesh-rs/** packages/**/node_modules/ packages/**/dist/ packages/**/dist-test/ diff --git a/docs/runtime-parity-scope.md b/docs/runtime-parity-scope.md index cd3761d7..681f1ad3 100644 --- a/docs/runtime-parity-scope.md +++ b/docs/runtime-parity-scope.md @@ -33,7 +33,7 @@ CultMesh. | C# | Reference implementation and production game/server substrate | CultCache typed documents and SoA, CultNet schema-v0, LiteNetLib production path with adapter profile, shared transport profiles, shared RUDP socket transport, C# multi-peer RUDP listener and schema service host, C# interop RUDP serve/dial path, connected RUDP CultMesh client helpers, RUDP-backed schema-client selection for `rudp://` service endpoints, RUDP reconnect policy/controller and reconnect-loop helper, LiteNetLib client reconnect-controller adoption, schema-client transport port for service request/response clients, CultMesh node/session/catalog/stream/simulation/RUDP surfaces, authorized peer lookup | No need to abandon LiteNetLib before the CultNet adapter fully owns production service adoption | | Python | Package runtime, local daemon endpoint, raw interop peer, typed state participant | CultCache/CultNet/CultMesh package surface, branded CultMesh node/schema/shard/Verse/peer/authority/stream/RUDP ergonomics, authorized peer lookup, shard catalogs/logs, schema-transport factory for raw client request/subscription flows, local TCP and RUDP CultMesh server listeners, Python interop RUDP serve/dial path, session/prediction/simulation fact helpers, RUDP codec/session/socket transport, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, benchmark evidence against C# cache paths | Universal C# throughput, full C# daemon/server stack, and RUDP as the only public service pipe | | TypeScript | Node/browser-adjacent projection and interop runtime | CultCache parity tests, CultNet schema-v0 peer, advertised schema/shard catalog wire contracts plus request/sync helpers, transport profiles, TCP-framed transport port and peer helper, shared RUDP socket transport, RUDP interop serve/dial path, RUDP reconnect policy/controller, RUDP reconnect-loop helper, branded CultMesh node/schema/shard/Verse/peer/authority/stream/RUDP ergonomics, authorized peer lookup | Full C# game session/server stack | -| Rust | Low-level cache/transport/runtime substrate | CultCache/CultNet interop lane, schema and shard catalog helpers, typed same-schema alias hydration from raw snapshots, transport profiles, TCP-framed port, shared RUDP codec/session/socket transport, Rust interop RUDP serve/dial path, connected RUDP CultMesh client helpers, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, small CultMesh facade for schema registry, shard catalog, peer cards, authority leases, authorized peer lookup, RUDP endpoint parsing, and peer-card/authorized-peer RUDP client construction | Full CultMesh game-session/server facade, stream catalog, or production game-session server | +| Rust | Low-level cache/transport/runtime substrate | CultCache/CultNet interop lane, schema and shard catalog helpers, typed same-schema alias hydration from raw snapshots, transport profiles, TCP-framed port, shared RUDP codec/session/socket transport, Rust interop RUDP serve/dial path, connected RUDP CultMesh client helpers, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, small CultMesh facade for schema registry, shard catalog, peer cards, authority leases, authorized peer lookup, RUDP endpoint parsing, and peer-card/authorized-peer RUDP client construction | Full CultMesh game-session/server facade or production game-session server. `packages/cultmesh-rs` now holds a Rust stream and session surface -- node, document sets, RUDP document publication, stream descriptors, negotiation, stream catalog, and shared-memory frame rings -- but no interop lane exercises it, so it carries no cross-runtime parity claim | | Kotlin | JVM/Android client substrate | Typed MessagePack cache, one-call typed snapshot document sync through local same-schema aliases, built-in schema catalog advertisement, schema/shard catalogs with shared schema-message transport fetch/sync helpers, authority leases, authorized peer lookup, stream catalog, branded `CultMesh` facade, WebSocket transport profile/connection helper, RUDP client/server sugar, connected RUDP CultMesh client helpers, Kotlin interop RUDP serve/dial path, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence | C# SoA store, full C# server daemon, or Unity hot-loop memory layout | ## Evidence Gates @@ -85,6 +85,10 @@ server body, is deliberately not made. backfilling C# server responsibilities it should not own. Kotlin WebSocket and RUDP adapters now share one `CultNetSchemaMessageTransport` helper shape for schema/shard catalog request and sync flows. +- Bring `packages/cultmesh-rs` under the wire claim. It arrived from a consumer repo where it grew + outside any conformance harness, and its stream, negotiation, and shared-memory frame-ring + surfaces have never been checked against the browser, Kotlin, and TypeScript CultMesh bodies + they must interoperate with. Runtime-local tests pass; that is not a parity claim. - Keep performance claims evidence-bound: C# owns SoA; Python has benchmark comparison gates; other runtimes should advertise measured native hot paths only when those paths exist. diff --git a/packages/cultmesh-rs/Cargo.lock b/packages/cultmesh-rs/Cargo.lock new file mode 100644 index 00000000..c515ded2 --- /dev/null +++ b/packages/cultmesh-rs/Cargo.lock @@ -0,0 +1,1183 @@ +# 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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[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 = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[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.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +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 = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[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", + "redb", + "rmp-serde", + "serde", + "serde_bytes", + "serde_json", + "uuid", + "windows-sys 0.59.0", +] + +[[package]] +name = "cultcache-rs-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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", + "ed25519-dalek", + "hmac", + "rand", + "rmp-serde", + "rmpv", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "socket2", + "uuid", + "windows-sys 0.61.2", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[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 = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[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 = "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 = "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.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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 = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[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 = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +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 = "redb" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6c3b63e007e90ce536ec2ae4690826136a20ec8dbbbb400daef1bb999d2e36" +dependencies = [ + "libc", +] + +[[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 = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +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 = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[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 = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +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.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "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.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[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 2.0.119", +] + +[[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 2.0.119", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +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.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[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.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/cultmesh-rs/Cargo.toml b/packages/cultmesh-rs/Cargo.toml new file mode 100644 index 00000000..9a719d32 --- /dev/null +++ b/packages/cultmesh-rs/Cargo.toml @@ -0,0 +1,20 @@ +[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/packages/cultmesh-rs/src/lib.rs b/packages/cultmesh-rs/src/lib.rs new file mode 100644 index 00000000..5adbb53f --- /dev/null +++ b/packages/cultmesh-rs/src/lib.rs @@ -0,0 +1,1363 @@ +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(()) + } +}