{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[285b64d4] Offload Reddit payloads to RocksDB and stream event log replay.\n\nVendor durable as a workspace crate, store entity JSON in entity_db instead\nof GlobalTree, and replay events.jsonl one line at a time to cut startup RAM.\n\nCo-authored-by: Cursor \n\nSide A — unified diff (full patch):\ndiff --git a/Cargo.lock b/Cargo.lock\nindex e55d87f32ab32064686431c7082ef8c9ca872d63..8fc09f9ac978bd7ccf57f177989057b606677db8 100644\n--- a/Cargo.lock\n+++ b/Cargo.lock\n@@ -11,6 +11,18 @@ dependencies = [\n \"memchr\",\n ]\n \n+[[package]]\n+name = \"anes\"\n+version = \"0.1.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299\"\n+\n+[[package]]\n+name = \"anstyle\"\n+version = \"1.0.14\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000\"\n+\n [[package]]\n name = \"anyhow\"\n version = \"1.0.102\"\n@@ -56,6 +68,12 @@ version = \"1.1.2\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0\"\n \n+[[package]]\n+name = \"autocfg\"\n+version = \"1.5.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53\"\n+\n [[package]]\n name = \"axum\"\n version = \"0.7.9\"\n@@ -153,6 +171,75 @@ version = \"0.22.1\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6\"\n \n+[[package]]\n+name = \"bincode\"\n+version = \"1.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad\"\n+dependencies = [\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.65.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5\"\n+dependencies = [\n+ \"bitflags 1.3.2\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"lazy_static\",\n+ \"lazycell\",\n+ \"peeking_take_while\",\n+ \"prettyplease\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 1.1.0\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.72.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895\"\n+dependencies = [\n+ \"bitflags 2.11.1\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"itertools 0.13.0\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 2.1.2\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bit-set\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3\"\n+dependencies = [\n+ \"bit-vec\",\n+]\n+\n+[[package]]\n+name = \"bit-vec\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"1.3.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\"\n+\n [[package]]\n name = \"bitflags\"\n version = \"2.11.1\"\n@@ -171,6 +258,22 @@ version = \"1.11.1\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33\"\n \n+[[package]]\n+name = \"bzip2-sys\"\n+version = \"0.1.13+1.0.8\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+]\n+\n+[[package]]\n+name = \"cast\"\n+version = \"0.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5\"\n+\n [[package]]\n name = \"cc\"\n version = \"1.2.62\"\n@@ -178,15 +281,89 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98\"\n dependencies = [\n \"find-msvc-tools\",\n+ \"jobserver\",\n+ \"libc\",\n \"shlex\",\n ]\n \n+[[package]]\n+name = \"cexpr\"\n+version = \"0.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766\"\n+dependencies = [\n+ \"nom\",\n+]\n+\n [[package]]\n name = \"cfg-if\"\n version = \"1.0.4\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801\"\n \n+[[package]]\n+name = \"ciborium\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"ciborium-ll\",\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"ciborium-io\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757\"\n+\n+[[package]]\n+name = \"ciborium-ll\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"half\",\n+]\n+\n+[[package]]\n+name = \"clang-sys\"\n+version = \"1.8.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4\"\n+dependencies = [\n+ \"glob\",\n+ \"libc\",\n+ \"libloading\",\n+]\n+\n+[[package]]\n+name = \"clap\"\n+version = \"4.6.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51\"\n+dependencies = [\n+ \"clap_builder\",\n+]\n+\n+[[package]]\n+name = \"clap_builder\"\n+version = \"4.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f\"\n+dependencies = [\n+ \"anstyle\",\n+ \"clap_lex\",\n+]\n+\n+[[package]]\n+name = \"clap_lex\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9\"\n+\n [[package]]\n name = \"cookie\"\n version = \"0.18.1\"\n@@ -224,6 +401,73 @@ version = \"0.8.7\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b\"\n \n+[[package]]\n+name = \"criterion\"\n+version = \"0.5.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f\"\n+dependencies = [\n+ \"anes\",\n+ \"cast\",\n+ \"ciborium\",\n+ \"clap\",\n+ \"criterion-plot\",\n+ \"is-terminal\",\n+ \"itertools 0.10.5\",\n+ \"num-traits\",\n+ \"once_cell\",\n+ \"oorandom\",\n+ \"plotters\",\n+ \"rayon\",\n+ \"regex\",\n+ \"serde\",\n+ \"serde_derive\",\n+ \"serde_json\",\n+ \"tinytemplate\",\n+ \"walkdir\",\n+]\n+\n+[[package]]\n+name = \"criterion-plot\"\n+version = \"0.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1\"\n+dependencies = [\n+ \"cast\",\n+ \"itertools 0.10.5\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-deque\"\n+version = \"0.8.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51\"\n+dependencies = [\n+ \"crossbeam-epoch\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-epoch\"\n+version = \"0.9.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e\"\n+dependencies = [\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-utils\"\n+version = \"0.8.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28\"\n+\n+[[package]]\n+name = \"crunchy\"\n+version = \"0.2.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5\"\n+\n [[package]]\n name = \"deranged\"\n version = \"0.5.8\"\n@@ -250,6 +494,25 @@ version = \"0.15.7\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b\"\n \n+[[package]]\n+name = \"durable\"\n+version = \"0.1.0\"\n+dependencies = [\n+ \"bincode\",\n+ \"criterion\",\n+ \"proptest\",\n+ \"rocksdb\",\n+ \"serde\",\n+ \"tempfile\",\n+ \"thiserror\",\n+]\n+\n+[[package]]\n+name = \"either\"\n+version = \"1.16.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e\"\n+\n [[package]]\n name = \"encoding_rs\"\n version = \"0.8.35\"\n@@ -373,6 +636,18 @@ dependencies = [\n \"wasi\",\n ]\n \n+[[package]]\n+name = \"getrandom\"\n+version = \"0.3.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"libc\",\n+ \"r-efi 5.3.0\",\n+ \"wasip2\",\n+]\n+\n [[package]]\n name = \"getrandom\"\n version = \"0.4.2\"\n@@ -381,11 +656,17 @@ checksum = \"0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555\"\n dependencies = [\n \"cfg-if\",\n \"libc\",\n- \"r-efi\",\n+ \"r-efi 6.0.0\",\n \"wasip2\",\n \"wasip3\",\n ]\n \n+[[package]]\n+name = \"glob\"\n+version = \"0.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280\"\n+\n [[package]]\n name = \"h2\"\n version = \"0.4.14\"\n@@ -405,6 +686,17 @@ dependencies = [\n \"tracing\",\n ]\n \n+[[package]]\n+name = \"half\"\n+version = \"2.7.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"crunchy\",\n+ \"zerocopy\",\n+]\n+\n [[package]]\n name = \"hashbrown\"\n version = \"0.15.5\"\n@@ -426,6 +718,12 @@ version = \"0.5.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\"\n \n+[[package]]\n+name = \"hermit-abi\"\n+version = \"0.5.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c\"\n+\n [[package]]\n name = \"http\"\n version = \"1.4.1\"\n@@ -676,12 +974,51 @@ version = \"2.12.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2\"\n \n+[[package]]\n+name = \"is-terminal\"\n+version = \"0.4.17\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46\"\n+dependencies = [\n+ \"hermit-abi\",\n+ \"libc\",\n+ \"windows-sys 0.61.2\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.10.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473\"\n+dependencies = [\n+ \"either\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186\"\n+dependencies = [\n+ \"either\",\n+]\n+\n [[package]]\n name = \"itoa\"\n version = \"1.0.18\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682\"\n \n+[[package]]\n+name = \"jobserver\"\n+version = \"0.1.34\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33\"\n+dependencies = [\n+ \"getrandom 0.3.4\",\n+ \"libc\",\n+]\n+\n [[package]]\n name = \"js-sys\"\n version = \"0.3.99\"\n@@ -700,6 +1037,12 @@ version = \"1.5.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe\"\n \n+[[package]]\n+name = \"lazycell\"\n+version = \"1.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55\"\n+\n [[package]]\n name = \"leb128fmt\"\n version = \"0.1.0\"\n@@ -712,6 +1055,43 @@ version = \"0.2.186\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66\"\n \n+[[package]]\n+name = \"libloading\"\n+version = \"0.8.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"windows-link\",\n+]\n+\n+[[package]]\n+name = \"librocksdb-sys\"\n+version = \"0.11.0+8.1.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e\"\n+dependencies = [\n+ \"bindgen 0.65.1\",\n+ \"bzip2-sys\",\n+ \"cc\",\n+ \"glob\",\n+ \"libc\",\n+ \"libz-sys\",\n+ \"lz4-sys\",\n+ \"zstd-sys\",\n+]\n+\n+[[package]]\n+name = \"libz-sys\"\n+version = \"1.1.28\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+ \"vcpkg\",\n+]\n+\n [[package]]\n name = \"linux-raw-sys\"\n version = \"0.12.1\"\n@@ -730,6 +1110,16 @@ version = \"0.4.30\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5\"\n \n+[[package]]\n+name = \"lz4-sys\"\n+version = \"1.11.1+lz4-1.10.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6\"\n+dependencies = [\n+ \"cc\",\n+ \"libc\",\n+]\n+\n [[package]]\n name = \"matchers\"\n version = \"0.2.0\"\n@@ -781,6 +1171,12 @@ version = \"0.3.17\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a\"\n \n+[[package]]\n+name = \"minimal-lexical\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\"\n+\n [[package]]\n name = \"mio\"\n version = \"1.2.0\"\n@@ -826,6 +1222,16 @@ dependencies = [\n \"tempfile\",\n ]\n \n+[[package]]\n+name = \"nom\"\n+version = \"7.1.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\"\n+dependencies = [\n+ \"memchr\",\n+ \"minimal-lexical\",\n+]\n+\n [[package]]\n name = \"nu-ansi-term\"\n version = \"0.50.3\"\n@@ -841,19 +1247,34 @@ version = \"0.2.2\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441\"\n \n+[[package]]\n+name = \"num-traits\"\n+version = \"0.2.19\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\"\n+dependencies = [\n+ \"autocfg\",\n+]\n+\n [[package]]\n name = \"once_cell\"\n version = \"1.21.4\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50\"\n \n+[[package]]\n+name = \"oorandom\"\n+version = \"11.1.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e\"\n+\n [[package]]\n name = \"openssl\"\n version = \"0.10.80\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"cfg-if\",\n \"foreign-types\",\n \"libc\",\n@@ -890,6 +1311,12 @@ dependencies = [\n \"vcpkg\",\n ]\n \n+[[package]]\n+name = \"peeking_take_while\"\n+version = \"0.1.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099\"\n+\n [[package]]\n name = \"percent-encoding\"\n version = \"2.3.2\"\n@@ -908,6 +1335,34 @@ version = \"0.3.33\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e\"\n \n+[[package]]\n+name = \"plotters\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747\"\n+dependencies = [\n+ \"num-traits\",\n+ \"plotters-backend\",\n+ \"plotters-svg\",\n+ \"wasm-bindgen\",\n+ \"web-sys\",\n+]\n+\n+[[package]]\n+name = \"plotters-backend\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a\"\n+\n+[[package]]\n+name = \"plotters-svg\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670\"\n+dependencies = [\n+ \"plotters-backend\",\n+]\n+\n [[package]]\n name = \"potential_utf\"\n version = \"0.1.5\"\n@@ -974,6 +1429,31 @@ dependencies = [\n \"unicode-ident\",\n ]\n \n+[[package]]\n+name = \"proptest\"\n+version = \"1.11.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744\"\n+dependencies = [\n+ \"bit-set\",\n+ \"bit-vec\",\n+ \"bitflags 2.11.1\",\n+ \"num-traits\",\n+ \"rand 0.9.4\",\n+ \"rand_chacha 0.9.0\",\n+ \"rand_xorshift\",\n+ \"regex-syntax\",\n+ \"rusty-fork\",\n+ \"tempfile\",\n+ \"unarray\",\n+]\n+\n+[[package]]\n+name = \"quick-error\"\n+version = \"1.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0\"\n+\n [[package]]\n name = \"quote\"\n version = \"1.0.45\"\n@@ -983,6 +1463,12 @@ dependencies = [\n \"proc-macro2\",\n ]\n \n+[[package]]\n+name = \"r-efi\"\n+version = \"5.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f\"\n+\n [[package]]\n name = \"r-efi\"\n version = \"6.0.0\"\n@@ -996,8 +1482,18 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a\"\n dependencies = [\n \"libc\",\n- \"rand_chacha\",\n- \"rand_core\",\n+ \"rand_chacha 0.3.1\",\n+ \"rand_core 0.6.4\",\n+]\n+\n+[[package]]\n+name = \"rand\"\n+version = \"0.9.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea\"\n+dependencies = [\n+ \"rand_chacha 0.9.0\",\n+ \"rand_core 0.9.5\",\n ]\n \n [[package]]\n@@ -1007,7 +1503,17 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\"\n dependencies = [\n \"ppv-lite86\",\n- \"rand_core\",\n+ \"rand_core 0.6.4\",\n+]\n+\n+[[package]]\n+name = \"rand_chacha\"\n+version = \"0.9.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb\"\n+dependencies = [\n+ \"ppv-lite86\",\n+ \"rand_core 0.9.5\",\n ]\n \n [[package]]\n@@ -1019,6 +1525,56 @@ dependencies = [\n \"getrandom 0.2.17\",\n ]\n \n+[[package]]\n+name = \"rand_core\"\n+version = \"0.9.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c\"\n+dependencies = [\n+ \"getrandom 0.3.4\",\n+]\n+\n+[[package]]\n+name = \"rand_xorshift\"\n+version = \"0.4.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a\"\n+dependencies = [\n+ \"rand_core 0.9.5\",\n+]\n+\n+[[package]]\n+name = \"rayon\"\n+version = \"1.12.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d\"\n+dependencies = [\n+ \"either\",\n+ \"rayon-core\",\n+]\n+\n+[[package]]\n+name = \"rayon-core\"\n+version = \"1.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91\"\n+dependencies = [\n+ \"crossbeam-deque\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"regex\"\n+version = \"1.12.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-automata\",\n+ \"regex-syntax\",\n+]\n+\n [[package]]\n name = \"regex-automata\"\n version = \"0.4.14\"\n@@ -1090,13 +1646,35 @@ dependencies = [\n \"windows-sys 0.52.0\",\n ]\n \n+[[package]]\n+name = \"rocksdb\"\n+version = \"0.21.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe\"\n+dependencies = [\n+ \"libc\",\n+ \"librocksdb-sys\",\n+]\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2\"\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"2.1.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe\"\n+\n [[package]]\n name = \"rustix\"\n version = \"1.1.4\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"errno\",\n \"libc\",\n \"linux-raw-sys\",\n@@ -1142,12 +1720,33 @@ version = \"1.0.22\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d\"\n \n+[[package]]\n+name = \"rusty-fork\"\n+version = \"0.3.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2\"\n+dependencies = [\n+ \"fnv\",\n+ \"quick-error\",\n+ \"tempfile\",\n+ \"wait-timeout\",\n+]\n+\n [[package]]\n name = \"ryu\"\n version = \"1.0.23\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f\"\n \n+[[package]]\n+name = \"same-file\"\n+version = \"1.0.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\"\n+dependencies = [\n+ \"winapi-util\",\n+]\n+\n [[package]]\n name = \"schannel\"\n version = \"0.1.29\"\n@@ -1163,7 +1762,7 @@ version = \"3.7.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"core-foundation 0.10.1\",\n \"core-foundation-sys\",\n \"libc\",\n@@ -1307,9 +1906,10 @@ dependencies = [\n \"axum\",\n \"axum-extra\",\n \"dotenvy\",\n+ \"durable\",\n \"futures-util\",\n \"maud\",\n- \"rand\",\n+ \"rand 0.8.6\",\n \"reqwest\",\n \"serde\",\n \"serde_json\",\n@@ -1378,7 +1978,7 @@ version = \"0.7.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"core-foundation 0.9.4\",\n \"system-configuration-sys\",\n ]\n@@ -1476,6 +2076,16 @@ dependencies = [\n \"zerovec\",\n ]\n \n+[[package]]\n+name = \"tinytemplate\"\n+version = \"1.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc\"\n+dependencies = [\n+ \"serde\",\n+ \"serde_json\",\n+]\n+\n [[package]]\n name = \"tokio\"\n version = \"1.52.3\"\n@@ -1558,7 +2168,7 @@ version = \"0.5.2\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"bytes\",\n \"http\",\n \"http-body\",\n@@ -1575,7 +2185,7 @@ version = \"0.6.11\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"bytes\",\n \"futures-util\",\n \"http\",\n@@ -1667,6 +2277,12 @@ version = \"0.2.5\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b\"\n \n+[[package]]\n+name = \"unarray\"\n+version = \"0.1.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94\"\n+\n [[package]]\n name = \"unicode-ident\"\n version = \"1.0.24\"\n@@ -1727,6 +2343,25 @@ version = \"0.9.5\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a\"\n \n+[[package]]\n+name = \"wait-timeout\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11\"\n+dependencies = [\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"walkdir\"\n+version = \"2.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\"\n+dependencies = [\n+ \"same-file\",\n+ \"winapi-util\",\n+]\n+\n [[package]]\n name = \"want\"\n version = \"0.3.1\"\n@@ -1843,7 +2478,7 @@ version = \"0.244.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"hashbrown 0.15.5\",\n \"indexmap\",\n \"semver\",\n@@ -1859,6 +2494,15 @@ dependencies = [\n \"wasm-bindgen\",\n ]\n \n+[[package]]\n+name = \"winapi-util\"\n+version = \"0.1.11\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22\"\n+dependencies = [\n+ \"windows-sys 0.61.2\",\n+]\n+\n [[package]]\n name = \"windows-link\"\n version = \"0.2.1\"\n@@ -2040,7 +2684,7 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2\"\n dependencies = [\n \"anyhow\",\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n \"indexmap\",\n \"log\",\n \"serde\",\n@@ -2184,3 +2828,14 @@ name = \"zmij\"\n version = \"1.0.21\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa\"\n+\n+[[package]]\n+name = \"zstd-sys\"\n+version = \"2.0.16+zstd.1.5.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748\"\n+dependencies = [\n+ \"bindgen 0.72.1\",\n+ \"cc\",\n+ \"pkg-config\",\n+]\ndiff --git a/Cargo.toml b/Cargo.toml\nindex 156820ec5151b709a254dafc177da10488fee566..89f57d8943de978ef6c2dc038a5dfb89420c4283 100644\n--- a/Cargo.toml\n+++ b/Cargo.toml\n@@ -1,3 +1,3 @@\n [workspace]\n-members = [\"server\"]\n+members = [\"server\", \"durable\"]\n resolver = \"2\"\ndiff --git a/Dockerfile b/Dockerfile\nindex 38e4d595f2366b6a6e9405f1e12373b70fac79a1..517aa49ea2c01ba0d06a4647259ed917a0f6665c 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -3,7 +3,7 @@ FROM rust:1.88-slim AS builder\n WORKDIR /build\n \n RUN apt-get update && \\\n- apt-get install -y pkg-config libssl-dev && \\\n+ apt-get install -y pkg-config libssl-dev clang && \\\n rm -rf /var/lib/apt/lists/*\n \n COPY . .\ndiff --git a/durable/.gitignore b/durable/.gitignore\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..90c273ef12d6313594dde541c3465f2a47739729\n--- /dev/null\n+++ b/durable/.gitignore\n@@ -0,0 +1,35 @@\n+all.txt\n+\n+# Generated by Cargo\n+# will have compiled files and executables\n+debug/\n+target/\n+\n+# These are backup files generated by rustfmt\n+**/*.rs.bk\n+\n+# MSVC Windows builds of rustc generate these, which store debugging information\n+*.pdb\n+\n+# Generated by cargo mutants\n+# Contains mutation testing data\n+**/mutants.out*/\n+\n+# RustRover\n+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n+# and can be added to the global gitignore or merged into this file. For a more nuclear\n+# option (not recommended) you can uncomment the following to ignore the entire idea folder.\n+#.idea/\n+\n+\n+# Added by cargo\n+\n+/target\n+\n+\n+# Added by cargo\n+#\n+# already existing elements were commented out\n+\n+#/target\ndiff --git a/durable/Cargo.lock b/durable/Cargo.lock\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..99d59ec2166a1e88dc4867d316b3d5fe9bb0a1b6\n--- /dev/null\n+++ b/durable/Cargo.lock\n@@ -0,0 +1,1198 @@\n+# This file is automatically @generated by Cargo.\n+# It is not intended for manual editing.\n+version = 4\n+\n+[[package]]\n+name = \"aho-corasick\"\n+version = \"1.1.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916\"\n+dependencies = [\n+ \"memchr\",\n+]\n+\n+[[package]]\n+name = \"anes\"\n+version = \"0.1.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299\"\n+\n+[[package]]\n+name = \"anstyle\"\n+version = \"1.0.11\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd\"\n+\n+[[package]]\n+name = \"autocfg\"\n+version = \"1.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8\"\n+\n+[[package]]\n+name = \"bincode\"\n+version = \"1.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad\"\n+dependencies = [\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.65.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5\"\n+dependencies = [\n+ \"bitflags 1.3.2\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"lazy_static\",\n+ \"lazycell\",\n+ \"peeking_take_while\",\n+ \"prettyplease\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 1.1.0\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.71.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3\"\n+dependencies = [\n+ \"bitflags 2.9.1\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"itertools 0.13.0\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 2.1.1\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bit-set\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3\"\n+dependencies = [\n+ \"bit-vec\",\n+]\n+\n+[[package]]\n+name = \"bit-vec\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"1.3.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"2.9.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967\"\n+\n+[[package]]\n+name = \"bumpalo\"\n+version = \"3.18.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee\"\n+\n+[[package]]\n+name = \"bzip2-sys\"\n+version = \"0.1.13+1.0.8\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+]\n+\n+[[package]]\n+name = \"cast\"\n+version = \"0.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5\"\n+\n+[[package]]\n+name = \"cc\"\n+version = \"1.2.27\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc\"\n+dependencies = [\n+ \"jobserver\",\n+ \"libc\",\n+ \"shlex\",\n+]\n+\n+[[package]]\n+name = \"cexpr\"\n+version = \"0.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766\"\n+dependencies = [\n+ \"nom\",\n+]\n+\n+[[package]]\n+name = \"cfg-if\"\n+version = \"1.0.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268\"\n+\n+[[package]]\n+name = \"ciborium\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"ciborium-ll\",\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"ciborium-io\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757\"\n+\n+[[package]]\n+name = \"ciborium-ll\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"half\",\n+]\n+\n+[[package]]\n+name = \"clang-sys\"\n+version = \"1.8.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4\"\n+dependencies = [\n+ \"glob\",\n+ \"libc\",\n+ \"libloading\",\n+]\n+\n+[[package]]\n+name = \"clap\"\n+version = \"4.5.40\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f\"\n+dependencies = [\n+ \"clap_builder\",\n+]\n+\n+[[package]]\n+name = \"clap_builder\"\n+version = \"4.5.40\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e\"\n+dependencies = [\n+ \"anstyle\",\n+ \"clap_lex\",\n+]\n+\n+[[package]]\n+name = \"clap_lex\"\n+version = \"0.7.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675\"\n+\n+[[package]]\n+name = \"criterion\"\n+version = \"0.5.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f\"\n+dependencies = [\n+ \"anes\",\n+ \"cast\",\n+ \"ciborium\",\n+ \"clap\",\n+ \"criterion-plot\",\n+ \"is-terminal\",\n+ \"itertools 0.10.5\",\n+ \"num-traits\",\n+ \"once_cell\",\n+ \"oorandom\",\n+ \"plotters\",\n+ \"rayon\",\n+ \"regex\",\n+ \"serde\",\n+ \"serde_derive\",\n+ \"serde_json\",\n+ \"tinytemplate\",\n+ \"walkdir\",\n+]\n+\n+[[package]]\n+name = \"criterion-plot\"\n+version = \"0.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1\"\n+dependencies = [\n+ \"cast\",\n+ \"itertools 0.10.5\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-deque\"\n+version = \"0.8.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51\"\n+dependencies = [\n+ \"crossbeam-epoch\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-epoch\"\n+version = \"0.9.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e\"\n+dependencies = [\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-utils\"\n+version = \"0.8.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28\"\n+\n+[[package]]\n+name = \"crunchy\"\n+version = \"0.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929\"\n+\n+[[package]]\n+name = \"durable\"\n+version = \"0.1.0\"\n+dependencies = [\n+ \"bincode\",\n+ \"criterion\",\n+ \"proptest\",\n+ \"rocksdb\",\n+ \"serde\",\n+ \"tempfile\",\n+ \"thiserror\",\n+]\n+\n+[[package]]\n+name = \"either\"\n+version = \"1.15.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719\"\n+\n+[[package]]\n+name = \"errno\"\n+version = \"0.3.13\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad\"\n+dependencies = [\n+ \"libc\",\n+ \"windows-sys 0.60.2\",\n+]\n+\n+[[package]]\n+name = \"fastrand\"\n+version = \"2.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be\"\n+\n+[[package]]\n+name = \"fnv\"\n+version = \"1.0.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\"\n+\n+[[package]]\n+name = \"getrandom\"\n+version = \"0.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"libc\",\n+ \"r-efi\",\n+ \"wasi\",\n+]\n+\n+[[package]]\n+name = \"glob\"\n+version = \"0.3.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2\"\n+\n+[[package]]\n+name = \"half\"\n+version = \"2.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"crunchy\",\n+]\n+\n+[[package]]\n+name = \"hermit-abi\"\n+version = \"0.5.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c\"\n+\n+[[package]]\n+name = \"is-terminal\"\n+version = \"0.4.16\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9\"\n+dependencies = [\n+ \"hermit-abi\",\n+ \"libc\",\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.10.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473\"\n+dependencies = [\n+ \"either\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186\"\n+dependencies = [\n+ \"either\",\n+]\n+\n+[[package]]\n+name = \"itoa\"\n+version = \"1.0.15\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c\"\n+\n+[[package]]\n+name = \"jobserver\"\n+version = \"0.1.33\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a\"\n+dependencies = [\n+ \"getrandom\",\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"js-sys\"\n+version = \"0.3.77\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f\"\n+dependencies = [\n+ \"once_cell\",\n+ \"wasm-bindgen\",\n+]\n+\n+[[package]]\n+name = \"lazy_static\"\n+version = \"1.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe\"\n+\n+[[package]]\n+name = \"lazycell\"\n+version = \"1.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55\"\n+\n+[[package]]\n+name = \"libc\"\n+version = \"0.2.174\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776\"\n+\n+[[package]]\n+name = \"libloading\"\n+version = \"0.8.8\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"windows-targets 0.53.2\",\n+]\n+\n+[[package]]\n+name = \"librocksdb-sys\"\n+version = \"0.11.0+8.1.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e\"\n+dependencies = [\n+ \"bindgen 0.65.1\",\n+ \"bzip2-sys\",\n+ \"cc\",\n+ \"glob\",\n+ \"libc\",\n+ \"libz-sys\",\n+ \"lz4-sys\",\n+ \"zstd-sys\",\n+]\n+\n+[[package]]\n+name = \"libz-sys\"\n+version = \"1.1.22\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+ \"vcpkg\",\n+]\n+\n+[[package]]\n+name = \"linux-raw-sys\"\n+version = \"0.9.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12\"\n+\n+[[package]]\n+name = \"log\"\n+version = \"0.4.27\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94\"\n+\n+[[package]]\n+name = \"lz4-sys\"\n+version = \"1.11.1+lz4-1.10.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6\"\n+dependencies = [\n+ \"cc\",\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"memchr\"\n+version = \"2.7.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0\"\n+\n+[[package]]\n+name = \"minimal-lexical\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\"\n+\n+[[package]]\n+name = \"nom\"\n+version = \"7.1.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\"\n+dependencies = [\n+ \"memchr\",\n+ \"minimal-lexical\",\n+]\n+\n+[[package]]\n+name = \"num-traits\"\n+version = \"0.2.19\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\"\n+dependencies = [\n+ \"autocfg\",\n+]\n+\n+[[package]]\n+name = \"once_cell\"\n+version = \"1.21.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d\"\n+\n+[[package]]\n+name = \"oorandom\"\n+version = \"11.1.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e\"\n+\n+[[package]]\n+name = \"peeking_take_while\"\n+version = \"0.1.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099\"\n+\n+[[package]]\n+name = \"pkg-config\"\n+version = \"0.3.32\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c\"\n+\n+[[package]]\n+name = \"plotters\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747\"\n+dependencies = [\n+ \"num-traits\",\n+ \"plotters-backend\",\n+ \"plotters-svg\",\n+ \"wasm-bindgen\",\n+ \"web-sys\",\n+]\n+\n+[[package]]\n+name = \"plotters-backend\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a\"\n+\n+[[package]]\n+name = \"plotters-svg\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670\"\n+dependencies = [\n+ \"plotters-backend\",\n+]\n+\n+[[package]]\n+name = \"ppv-lite86\"\n+version = \"0.2.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9\"\n+dependencies = [\n+ \"zerocopy\",\n+]\n+\n+[[package]]\n+name = \"prettyplease\"\n+version = \"0.2.35\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"proc-macro2\"\n+version = \"1.0.95\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778\"\n+dependencies = [\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"proptest\"\n+version = \"1.7.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f\"\n+dependencies = [\n+ \"bit-set\",\n+ \"bit-vec\",\n+ \"bitflags 2.9.1\",\n+ \"lazy_static\",\n+ \"num-traits\",\n+ \"rand\",\n+ \"rand_chacha\",\n+ \"rand_xorshift\",\n+ \"regex-syntax\",\n+ \"rusty-fork\",\n+ \"tempfile\",\n+ \"unarray\",\n+]\n+\n+[[package]]\n+name = \"quick-error\"\n+version = \"1.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0\"\n+\n+[[package]]\n+name = \"quote\"\n+version = \"1.0.40\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d\"\n+dependencies = [\n+ \"proc-macro2\",\n+]\n+\n+[[package]]\n+name = \"r-efi\"\n+version = \"5.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f\"\n+\n+[[package]]\n+name = \"rand\"\n+version = \"0.9.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97\"\n+dependencies = [\n+ \"rand_chacha\",\n+ \"rand_core\",\n+]\n+\n+[[package]]\n+name = \"rand_chacha\"\n+version = \"0.9.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb\"\n+dependencies = [\n+ \"ppv-lite86\",\n+ \"rand_core\",\n+]\n+\n+[[package]]\n+name = \"rand_core\"\n+version = \"0.9.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38\"\n+dependencies = [\n+ \"getrandom\",\n+]\n+\n+[[package]]\n+name = \"rand_xorshift\"\n+version = \"0.4.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a\"\n+dependencies = [\n+ \"rand_core\",\n+]\n+\n+[[package]]\n+name = \"rayon\"\n+version = \"1.10.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa\"\n+dependencies = [\n+ \"either\",\n+ \"rayon-core\",\n+]\n+\n+[[package]]\n+name = \"rayon-core\"\n+version = \"1.12.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2\"\n+dependencies = [\n+ \"crossbeam-deque\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"regex\"\n+version = \"1.11.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-automata\",\n+ \"regex-syntax\",\n+]\n+\n+[[package]]\n+name = \"regex-automata\"\n+version = \"0.4.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-syntax\",\n+]\n+\n+[[package]]\n+name = \"regex-syntax\"\n+version = \"0.8.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c\"\n+\n+[[package]]\n+name = \"rocksdb\"\n+version = \"0.21.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe\"\n+dependencies = [\n+ \"libc\",\n+ \"librocksdb-sys\",\n+]\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2\"\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"2.1.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d\"\n+\n+[[package]]\n+name = \"rustix\"\n+version = \"1.0.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266\"\n+dependencies = [\n+ \"bitflags 2.9.1\",\n+ \"errno\",\n+ \"libc\",\n+ \"linux-raw-sys\",\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"rustversion\"\n+version = \"1.0.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d\"\n+\n+[[package]]\n+name = \"rusty-fork\"\n+version = \"0.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f\"\n+dependencies = [\n+ \"fnv\",\n+ \"quick-error\",\n+ \"tempfile\",\n+ \"wait-timeout\",\n+]\n+\n+[[package]]\n+name = \"ryu\"\n+version = \"1.0.20\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f\"\n+\n+[[package]]\n+name = \"same-file\"\n+version = \"1.0.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\"\n+dependencies = [\n+ \"winapi-util\",\n+]\n+\n+[[package]]\n+name = \"serde\"\n+version = \"1.0.219\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6\"\n+dependencies = [\n+ \"serde_derive\",\n+]\n+\n+[[package]]\n+name = \"serde_derive\"\n+version = \"1.0.219\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"serde_json\"\n+version = \"1.0.140\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373\"\n+dependencies = [\n+ \"itoa\",\n+ \"memchr\",\n+ \"ryu\",\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"shlex\"\n+version = \"1.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64\"\n+\n+[[package]]\n+name = \"syn\"\n+version = \"2.0.104\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"tempfile\"\n+version = \"3.20.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1\"\n+dependencies = [\n+ \"fastrand\",\n+ \"getrandom\",\n+ \"once_cell\",\n+ \"rustix\",\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"thiserror\"\n+version = \"1.0.69\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52\"\n+dependencies = [\n+ \"thiserror-impl\",\n+]\n+\n+[[package]]\n+name = \"thiserror-impl\"\n+version = \"1.0.69\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"tinytemplate\"\n+version = \"1.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc\"\n+dependencies = [\n+ \"serde\",\n+ \"serde_json\",\n+]\n+\n+[[package]]\n+name = \"unarray\"\n+version = \"0.1.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94\"\n+\n+[[package]]\n+name = \"unicode-ident\"\n+version = \"1.0.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512\"\n+\n+[[package]]\n+name = \"vcpkg\"\n+version = \"0.2.15\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426\"\n+\n+[[package]]\n+name = \"wait-timeout\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11\"\n+dependencies = [\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"walkdir\"\n+version = \"2.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\"\n+dependencies = [\n+ \"same-file\",\n+ \"winapi-util\",\n+]\n+\n+[[package]]\n+name = \"wasi\"\n+version = \"0.14.2+wasi-0.2.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3\"\n+dependencies = [\n+ \"wit-bindgen-rt\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"once_cell\",\n+ \"rustversion\",\n+ \"wasm-bindgen-macro\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-backend\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6\"\n+dependencies = [\n+ \"bumpalo\",\n+ \"log\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+ \"wasm-bindgen-shared\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-macro\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407\"\n+dependencies = [\n+ \"quote\",\n+ \"wasm-bindgen-macro-support\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-macro-support\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+ \"wasm-bindgen-backend\",\n+ \"wasm-bindgen-shared\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-shared\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d\"\n+dependencies = [\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"web-sys\"\n+version = \"0.3.77\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2\"\n+dependencies = [\n+ \"js-sys\",\n+ \"wasm-bindgen\",\n+]\n+\n+[[package]]\n+name = \"winapi-util\"\n+version = \"0.1.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb\"\n+dependencies = [\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"windows-sys\"\n+version = \"0.59.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b\"\n+dependencies = [\n+ \"windows-targets 0.52.6\",\n+]\n+\n+[[package]]\n+name = \"windows-sys\"\n+version = \"0.60.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb\"\n+dependencies = [\n+ \"windows-targets 0.53.2\",\n+]\n+\n+[[package]]\n+name = \"windows-targets\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973\"\n+dependencies = [\n+ \"windows_aarch64_gnullvm 0.52.6\",\n+ \"windows_aarch64_msvc 0.52.6\",\n+ \"windows_i686_gnu 0.52.6\",\n+ \"windows_i686_gnullvm 0.52.6\",\n+ \"windows_i686_msvc 0.52.6\",\n+ \"windows_x86_64_gnu 0.52.6\",\n+ \"windows_x86_64_gnullvm 0.52.6\",\n+ \"windows_x86_64_msvc 0.52.6\",\n+]\n+\n+[[package]]\n+name = \"windows-targets\"\n+version = \"0.53.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef\"\n+dependencies = [\n+ \"windows_aarch64_gnullvm 0.53.0\",\n+ \"windows_aarch64_msvc 0.53.0\",\n+ \"windows_i686_gnu 0.53.0\",\n+ \"windows_i686_gnullvm 0.53.0\",\n+ \"windows_i686_msvc 0.53.0\",\n+ \"windows_x86_64_gnu 0.53.0\",\n+ \"windows_x86_64_gnullvm 0.53.0\",\n+ \"windows_x86_64_msvc 0.53.0\",\n+]\n+\n+[[package]]\n+name = \"windows_aarch64_gnullvm\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3\"\n+\n+[[package]]\n+name = \"windows_aarch64_gnullvm\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764\"\n+\n+[[package]]\n+name = \"windows_aarch64_msvc\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469\"\n+\n+[[package]]\n+name = \"windows_aarch64_msvc\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c\"\n+\n+[[package]]\n+name = \"windows_i686_gnu\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b\"\n+\n+[[package]]\n+name = \"windows_i686_gnu\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3\"\n+\n+[[package]]\n+name = \"windows_i686_gnullvm\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66\"\n+\n+[[package]]\n+name = \"windows_i686_gnullvm\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11\"\n+\n+[[package]]\n+name = \"windows_i686_msvc\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66\"\n+\n+[[package]]\n+name = \"windows_i686_msvc\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnu\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnu\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnullvm\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnullvm\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57\"\n+\n+[[package]]\n+name = \"windows_x86_64_msvc\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec\"\n+\n+[[package]]\n+name = \"windows_x86_64_msvc\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486\"\n+\n+[[package]]\n+name = \"wit-bindgen-rt\"\n+version = \"0.39.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1\"\n+dependencies = [\n+ \"bitflags 2.9.1\",\n+]\n+\n+[[package]]\n+name = \"zerocopy\"\n+version = \"0.8.26\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f\"\n+dependencies = [\n+ \"zerocopy-derive\",\n+]\n+\n+[[package]]\n+name = \"zerocopy-derive\"\n+version = \"0.8.26\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"zstd-sys\"\n+version = \"2.0.15+zstd.1.5.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237\"\n+dependencies = [\n+ \"bindgen 0.71.1\",\n+ \"cc\",\n+ \"pkg-config\",\n+]\ndiff --git a/durable/Cargo.toml b/durable/Cargo.toml\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..907e79cd894446e2ac76c8240b86c2927103727b\n--- /dev/null\n+++ b/durable/Cargo.toml\n@@ -0,0 +1,18 @@\n+[package]\n+name = \"durable\"\n+version = \"0.1.0\"\n+edition = \"2021\"\n+authors = [\"Durable Contributors\"]\n+description = \"RocksDB-backed persistent data structures for Rust\"\n+license = \"MIT OR Apache-2.0\"\n+\n+[dependencies]\n+rocksdb = \"0.21\"\n+serde = { version = \"1.0\", features = [\"derive\"] }\n+bincode = \"1.3\"\n+thiserror = \"1.0\"\n+\n+[dev-dependencies]\n+tempfile = \"3.8\"\n+criterion = \"0.5\"\n+proptest = \"1.4\"\ndiff --git a/durable/LICENSE b/durable/LICENSE\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64\n--- /dev/null\n+++ b/durable/LICENSE\n@@ -0,0 +1,201 @@\n+ Apache License\n+ Version 2.0, January 2004\n+ http://www.apache.org/licenses/\n+\n+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n+\n+ 1. Definitions.\n+\n+ \"License\" shall mean the terms and conditions for use, reproduction,\n+ and distribution as defined by Sections 1 through 9 of this document.\n+\n+ \"Licensor\" shall mean the copyright owner or entity authorized by\n+ the copyright owner that is granting the License.\n+\n+ \"Legal Entity\" shall mean the union of the acting entity and all\n+ other entities that control, are controlled by, or are under common\n+ control with that entity. For the purposes of this definition,\n+ \"control\" means (i) the power, direct or indirect, to cause the\n+ direction or management of such entity, whether by contract or\n+ otherwise, or (ii) ownership of fifty percent (50%) or more of the\n+ outstanding shares, or (iii) beneficial ownership of such entity.\n+\n+ \"You\" (or \"Your\") shall mean an individual or Legal Entity\n+ exercising permissions granted by this License.\n+\n+ \"Source\" form shall mean the preferred form for making modifications,\n+ including but not limited to software source code, documentation\n+ source, and configuration files.\n+\n+ \"Object\" form shall mean any form resulting from mechanical\n+ transformation or translation of a Source form, including but\n+ not limited to compiled object code, generated documentation,\n+ and conversions to other media types.\n+\n+ \"Work\" shall mean the work of authorship, whether in Source or\n+ Object form, made available under the License, as indicated by a\n+ copyright notice that is included in or attached to the work\n+ (an example is provided in the Appendix below).\n+\n+ \"Derivative Works\" shall mean any work, whether in Source or Object\n+ form, that is based on (or derived from) the Work and for which the\n+ editorial revisions, annotations, elaborations, or other modifications\n+ represent, as a whole, an original work of authorship. For the purposes\n+ of this License, Derivative Works shall not include works that remain\n+ separable from, or merely link (or bind by name) to the interfaces of,\n+ the Work and Derivative Works thereof.\n+\n+ \"Contribution\" shall mean any work of authorship, including\n+ the original version of the Work and any modifications or additions\n+ to that Work or Derivative Works thereof, that is intentionally\n+ submitted to Licensor for inclusion in the Work by the copyright owner\n+ or by an individual or Legal Entity authorized to submit on behalf of\n+ the copyright owner. For the purposes of this definition, \"submitted\"\n+ means any form of electronic, verbal, or written communication sent\n+ to the Licensor or its representatives, including but not limited to\n+ communication on electronic mailing lists, source code control systems,\n+ and issue tracking systems that are managed by, or on behalf of, the\n+ Licensor for the purpose of discussing and improving the Work, but\n+ excluding communication that is conspicuously marked or otherwise\n+ designated in writing by the copyright owner as \"Not a Contribution.\"\n+\n+ \"Contributor\" shall mean Licensor and any individual or Legal Entity\n+ on behalf of whom a Contribution has been received by Licensor and\n+ subsequently incorporated within the Work.\n+\n+ 2. Grant of Copyright License. Subject to the terms and conditions of\n+ this License, each Contributor hereby grants to You a perpetual,\n+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n+ copyright license to reproduce, prepare Derivative Works of,\n+ publicly display, publicly perform, sublicense, and distribute the\n+ Work and such Derivative Works in Source or Object form.\n+\n+ 3. Grant of Patent License. Subject to the terms and conditions of\n+ this License, each Contributor hereby grants to You a perpetual,\n+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n+ (except as stated in this section) patent license to make, have made,\n+ use, offer to sell, sell, import, and otherwise transfer the Work,\n+ where such license applies only to those patent claims licensable\n+ by such Contributor that are necessarily infringed by their\n+ Contribution(s) alone or by combination of their Contribution(s)\n+ with the Work to which such Contribution(s) was submitted. If You\n+ institute patent litigation against any entity (including a\n+ cross-claim or counterclaim in a lawsuit) alleging that the Work\n+ or a Contribution incorporated within the Work constitutes direct\n+ or contributory patent infringement, then any patent licenses\n+ granted to You under this License for that Work shall terminate\n+ as of the date such litigation is filed.\n+\n+ 4. Redistribution. You may reproduce and distribute copies of the\n+ Work or Derivative Works thereof in any medium, with or without\n+ modifications, and in Source or Object form, provided that You\n+ meet the following conditions:\n+\n+ (a) You must give any other recipients of the Work or\n+ Derivative Works a copy of this License; and\n+\n+ (b) You must cause any modified files to carry prominent notices\n+ stating that You changed the files; and\n+\n+ (c) You must retain, in the Source form of any Derivative Works\n+ that You distribute, all copyright, patent, trademark, and\n+ attribution notices from the Source form of the Work,\n+ excluding those notices that do not pertain to any part of\n+ the Derivative Works; and\n+\n+ (d) If the Work includes a \"NOTICE\" text file as part of its\n+ distribution, then any Derivative Works that You distribute must\n+ include a readable copy of the attribution notices contained\n+ within such NOTICE file, excluding those notices that do not\n+ pertain to any part of the Derivative Works, in at least one\n+ of the following places: within a NOTICE text file distributed\n+ as part of the Derivative Works; within the Source form or\n+ documentation, if provided along with the Derivative Works; or,\n+ within a display generated by the Derivative Works, if and\n+ wherever such third-party notices normally appear. The contents\n+ of the NOTICE file are for informational purposes only and\n+ do not modify the License. You may add Your own attribution\n+ notices within Derivative Works that You distribute, alongside\n+ or as an addendum to the NOTICE text from the Work, provided\n+ that such additional attribution notices cannot be construed\n+ as modifying the License.\n+\n+ You may add Your own copyright statement to Your modifications and\n+ may provide additional or different license terms and conditions\n+ for use, reproduction, or distribution of Your modifications, or\n+ for any such Derivative Works as a whole, provided Your use,\n+ reproduction, and distribution of the Work otherwise complies with\n+ the conditions stated in this License.\n+\n+ 5. Submission of Contributions. Unless You explicitly state otherwise,\n+ any Contribution intentionally submitted for inclusion in the Work\n+ by You to the Licensor shall be under the terms and conditions of\n+ this License, without any additional terms or conditions.\n+ Notwithstanding the above, nothing herein shall supersede or modify\n+ the terms of any separate license agreement you may have executed\n+ with Licensor regarding such Contributions.\n+\n+ 6. Trademarks. This License does not grant permission to use the trade\n+ names, trademarks, service marks, or product names of the Licensor,\n+ except as required for reasonable and customary use in describing the\n+ origin of the Work and reproducing the content of the NOTICE file.\n+\n+ 7. Disclaimer of Warranty. Unless required by applicable law or\n+ agreed to in writing, Licensor provides the Work (and each\n+ Contributor provides its Contributions) on an \"AS IS\" BASIS,\n+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n+ implied, including, without limitation, any warranties or conditions\n+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n+ PARTICULAR PURPOSE. You are solely responsible for determining the\n+ appropriateness of using or redistributing the Work and assume any\n+ risks associated with Your exercise of permissions under this License.\n+\n+ 8. Limitation of Liability. In no event and under no legal theory,\n+ whether in tort (including negligence), contract, or otherwise,\n+ unless required by applicable law (such as deliberate and grossly\n+ negligent acts) or agreed to in writing, shall any Contributor be\n+ liable to You for damages, including any direct, indirect, special,\n+ incidental, or consequential damages of any character arising as a\n+ result of this License or out of the use or inability to use the\n+ Work (including but not limited to damages for loss of goodwill,\n+ work stoppage, computer failure or malfunction, or any and all\n+ other commercial damages or losses), even if such Contributor\n+ has been advised of the possibility of such damages.\n+\n+ 9. Accepting Warranty or Additional Liability. While redistributing\n+ the Work or Derivative Works thereof, You may choose to offer,\n+ and charge a fee for, acceptance of support, warranty, indemnity,\n+ or other liability obligations and/or rights consistent with this\n+ License. However, in accepting such obligations, You may act only\n+ on Your own behalf and on Your sole responsibility, not on behalf\n+ of any other Contributor, and only if You agree to indemnify,\n+ defend, and hold each Contributor harmless for any liability\n+ incurred by, or claims asserted against, such Contributor by reason\n+ of your accepting any such warranty or additional liability.\n+\n+ END OF TERMS AND CONDITIONS\n+\n+ APPENDIX: How to apply the Apache License to your work.\n+\n+ To apply the Apache License to your work, attach the following\n+ boilerplate notice, with the fields enclosed by brackets \"[]\"\n+ replaced with your own identifying information. (Don't include\n+ the brackets!) The text should be enclosed in the appropriate\n+ comment syntax for the file format. We also recommend that a\n+ file or class name and description of purpose be included on the\n+ same \"printed page\" as the copyright notice for easier\n+ identification within third-party archives.\n+\n+ Copyright [yyyy] [name of copyright owner]\n+\n+ Licensed under the Apache License, Version 2.0 (the \"License\");\n+ you may not use this file except in compliance with the License.\n+ You may obtain a copy of the License at\n+\n+ http://www.apache.org/licenses/LICENSE-2.0\n+\n+ Unless required by applicable law or agreed to in writing, software\n+ distributed under the License is distributed on an \"AS IS\" BASIS,\n+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ See the License for the specific language governing permissions and\n+ limitations under the License.\ndiff --git a/durable/README.md b/durable/README.md\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..46f3b163a41d1094334ea5dc4e8add7b627f16e2\n--- /dev/null\n+++ b/durable/README.md\n@@ -0,0 +1,207 @@\n+# Durable\n+\n+RocksDB-backed persistent data structures for Rust. Think `std::collections` but on disk!\n+\n+## Features\n+\n+- **Persistent Collections**: `DurableVec`, `DurableMap`, `DurableSet` (coming soon)\n+- **Type-Safe**: Full Rust type safety with serde serialization\n+- **ACID Guarantees**: All operations are atomic and crash-safe\n+- **Zero-Copy Capable**: Efficient iteration without loading entire collections\n+- **Embedded**: No external services required - just a directory on disk\n+\n+## Quick Start\n+\n+Add to your `Cargo.toml`:\n+\n+```toml\n+[dependencies]\n+durable = \"0.1.0\"\n+```\n+\n+## Example\n+\n+### DurableVec\n+```rust\n+use durable::{Db, DurableVec};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Serialize, Deserialize)]\n+struct Task {\n+ id: u64,\n+ title: String,\n+ completed: bool,\n+}\n+\n+fn main() -> Result<(), Box> {\n+ // Open or create a database\n+ let db = Db::open(\"my_db\")?;\n+ \n+ // Create a persistent vector\n+ let mut tasks = DurableVec::::new(&db, \"tasks\")?;\n+ \n+ // Use it like a normal Vec!\n+ tasks.push(Task {\n+ id: 1,\n+ title: \"Build something amazing\".to_string(),\n+ completed: false,\n+ })?;\n+ \n+ // Data persists across program restarts\n+ println!(\"Total tasks: {}\", tasks.len()?);\n+ \n+ Ok(())\n+}\n+```\n+\n+### DurableMap\n+```rust\n+use durable::{Db, DurableMap};\n+\n+fn main() -> Result<(), Box> {\n+ let db = Db::open(\"my_db\")?;\n+ \n+ // Create a persistent map\n+ let mut scores = DurableMap::::new(&db, \"scores\")?;\n+ \n+ // Use it like a HashMap!\n+ // Use put() when you don't need the old value (more efficient)\n+ scores.put(\"Alice\".to_string(), 100)?;\n+ scores.put(\"Bob\".to_string(), 85)?;\n+ \n+ // Use insert() when you need to know the old value\n+ if let Some(old_score) = scores.insert(\"Alice\".to_string(), 120)? {\n+ println!(\"Alice's previous score was: {}\", old_score);\n+ }\n+ \n+ // Get values\n+ if let Some(score) = scores.get(&\"Alice\".to_string())? {\n+ println!(\"Alice's score: {}\", score);\n+ }\n+ \n+ // Iterate over entries\n+ for (name, score) in scores.iter()? {\n+ println!(\"{}: {}\", name, score);\n+ }\n+ \n+ Ok(())\n+}\n+```\n+\n+### Nested Collections\n+\n+Durable supports nesting collections within each other for complex data structures:\n+\n+```rust\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+ let db = Db::open(\"my_db\")?;\n+ \n+ // Create a map where each user has a list of posts\n+ let user_posts: DurableMap> = \n+ DurableMap::new_nested(&db, \"user_posts\");\n+ \n+ // Add posts for a user\n+ let mut alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+ alice_posts.push(\"Hello, world!\".to_string())?;\n+ alice_posts.push(\"Rust is awesome!\".to_string())?;\n+ \n+ // Or use chained calls for convenience\n+ user_posts.entry(\"bob\".to_string())?.or_default()?.push(\"First post!\".to_string())?;\n+ \n+ // Access nested data\n+ let alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+ println!(\"Alice has {} posts\", alice_posts.len()?);\n+ \n+ Ok(())\n+}\n+```\n+\n+The entry API automatically creates nested collections when they don't exist, providing ergonomic access patterns similar to `std::collections::HashMap::entry().or_default()`.\n+\n+## Current Status\n+\n+### Implemented\n+\n+- ✅ `DurableVec` with full test coverage including:\n+ - Basic operations: `push`, `pop`, `get`, `len`, `clear`\n+ - Batch operations: `extend`\n+ - Iteration: `iter()` returns a streaming iterator, `to_vec()` loads into memory\n+ - Property-based testing with proptest\n+ - Unicode string support\n+ - Complex type support\n+\n+- ✅ `DurableMap` with full test coverage including:\n+ - Basic operations: `insert`, `put`, `get`, `remove`, `contains_key`, `len`, `clear`\n+ - Batch operations: `extend`\n+ - Iteration: `iter()`, `keys()`, `values()` return streaming iterators\n+ - Memory loading: `to_vec()`, `keys_vec()`, `values_vec()` for convenience\n+ - Complex key and value types\n+ - Property-based testing with proptest\n+\n+- ✅ **Nested Collections** with entry API:\n+ - `DurableMap>` - Maps to vectors\n+ - `entry()` method with `or_default()` for ergonomic access\n+ - Automatic collection creation and management\n+ - Full persistence and isolation between nested collections\n+ - Type-safe compile-time enforcement\n+\n+### Coming Soon\n+\n+- 🚧 `DurableSet` - Persistent HashSet \n+- 🚧 Deep nesting (e.g., `DurableMap>>`)\n+- 🚧 Schema migration support\n+- 🚧 Batch operations across multiple collections\n+\n+## Performance\n+\n+All operations are designed to be efficient:\n+\n+- **DurableVec**:\n+ - `push`: Single atomic write with WAL flush\n+ - `get`: Direct key lookup, O(1) \n+ - `len`: Metadata lookup, O(1)\n+ - `extend`: Batched writes for efficiency\n+ - `clear`: Atomic batch deletion\n+\n+- **DurableMap**:\n+ - `insert`: Returns old value (2 ops: get + put), O(1) average\n+ - `put`: No return value (1 op: existence check + put), O(1) average\n+ - `get`: Direct key lookup, O(1) average\n+ - `remove`: Single delete with WAL flush\n+ - `len`: Metadata lookup, O(1)\n+ - `extend`: Batched writes for efficiency\n+\n+## Testing\n+\n+Run the test suite:\n+\n+```bash\n+cargo test\n+```\n+\n+Run the examples:\n+\n+```bash\n+cargo run --example vec_example\n+cargo run --example map_example\n+cargo run --example combined_example # Shows both collections working together\n+cargo run --example streaming_demo # Demonstrates efficient streaming iteration\n+cargo run --example nested_example # Shows nested collections (Map -> Vec)\n+cargo run --example simple_ranking # Gaming leaderboard from docs/motivation.md\n+cargo run --example ranking_history # Complex ranking system with persistence\n+```\n+\n+## License\n+\n+Licensed under either of:\n+\n+- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n+- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n+\n+at your option.\n+\n+## Contributing\n+\n+Contributions are welcome! Please feel free to submit a Pull Request.\ndiff --git a/durable/docs/001.md b/durable/docs/001.md\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3ad7cf9d93572de9ded367f9ce6dcc28608bba39\n--- /dev/null\n+++ b/durable/docs/001.md\n@@ -0,0 +1,272 @@\n+## Durable — RocksDB-backed Persistent Data Structures for Rust\n+\n+### *draft RFC v0.1*\n+\n+---\n+\n+### 1  Purpose\n+\n+Provide ergonomic, std-like collections (`DurableMap`, `DurableVec`, `DurableSet`, …) whose contents are **durably stored in RocksDB** yet feel in-memory:\n+\n+```rust\n+let db = durable::open(\"db\"); // single call opens RocksDB\n+let mut posts = DurableMap::::new(&db, \"posts\")?;\n+\n+posts.insert(id, post)?; // ACID write\n+let p = posts.get(&id)?; // typed read\n+```\n+\n+Target use cases:\n+\n+| Domain | Why durable? |\n+| ------------------------------ | -------------------------------- |\n+| Local-first apps / CRDT caches | crash-safe, embedded |\n+| High-write time-series blobs | append-only keys, fast iteration |\n+| ML / ranking histories | vector-within-vector patterns |\n+| Game state / async board games | snapshot + rollback |\n+| Sorter (tag → bucket → item) | exactly the motivating structure |\n+\n+---\n+\n+### 2  Design Goals\n+\n+1. **Ergonomic** – Feel like `std::collections`; no manual key mangling.\n+2. **Typed** – Keys & values are generic; (de)serialization pluggable (default = `bincode`).\n+3. **Atomic** – Multi-op `batch.commit()` gives RocksDB write-batch semantics.\n+4. **Crash-safe** – Every public write path `fsync`s RocksDB WAL first.\n+5. **Composable** – Any collection may nest another via *prefix subspaces*.\n+6. **Zero external services** – Single embedded `.sst` directory.\n+7. **Opt-in features** – Reactive diffs, LRU cache, metrics behind feature flags.\n+\n+Non-goals (v0 line):\n+\n+* Distributed replication\n+* Multi-process concurrency (one writer process is assumed; readers may open secondary Rocks instances)\n+* SQL-style ad-hoc queries – you iterate, not query-plan.\n+\n+---\n+\n+### 3  Key–Value Layout\n+\n+*Each collection owns a **prefix** inside a single Column Family (`default` by default).*\n+\n+```\n+ 0x00 0x00 [ 0x00 …]\n+```\n+\n+* `user-prefix` = crate-level namespace (allows multi-tenant apps)\n+* `col_id` = 8-byte, little-endian numeric ID assigned on `DurableMap::new(&db,\"posts\")`.\n+* `logical-key` = `serde`-encoded key (or ordinal for Vec).\n+* `subindex` = extra path segments used by nested collections (e.g., `Vec` elements in a map value).\n+\n+Because RocksDB stores keys lexicographically, all elements of a collection (and its descendants) live contiguously → range scans & prefix deletes are cheap.\n+\n+---\n+\n+### 4  Public API (surface)\n+\n+```rust\n+/// Opens (or creates) a durable database at `path`.\n+pub fn open>(path: P) -> Result;\n+\n+/// A transactional write batch\n+pub struct Batch<'db> { /* .. */ }\n+\n+impl<'db> Batch<'db> {\n+ pub fn put(&mut self, col: &impl WriteCollection, key: &K, val: &V) -> Result<()>;\n+ pub fn delete(&mut self, col: &impl WriteCollection, key: &K) -> Result<()>;\n+ pub fn commit(self) -> Result<()>;\n+}\n+\n+/// Collections ------------------------------------------------------------\n+\n+pub struct DurableMap<'db, K, V> { /* .. */ }\n+pub struct DurableVec<'db, T> { /* .. */ }\n+pub struct DurableSet<'db, T> { /* .. */ }\n+pub struct DurableIndex<'db, K, V> { /* sorted-map, range queries */ }\n+\n+/// Common read API\n+pub trait ReadCollection {\n+ fn get(&self, k: &K) -> Result>;\n+ fn contains_key(&self, k: &K) -> Result;\n+ fn len(&self) -> Result;\n+ fn iter(&self) -> Iter<'_, (K,V)>; // owns snapshot\n+}\n+\n+/// Common write API (auto batch or explicit)\n+pub trait WriteCollection: ReadCollection {\n+ fn insert(&mut self, k: K, v: V) -> Result>;\n+ fn remove(&mut self, k: &K) -> Result>;\n+ fn clear(&mut self) -> Result<()>;\n+}\n+```\n+\n+*All writes go through an internal `rocksdb::WriteBatch`* so a single logical op remains atomic even if it touches subkeys (e.g., pushing into a `DurableVec` updates `len` meta key + appends element).\n+\n+---\n+\n+### 5  Collection Semantics\n+\n+#### 5.1 `DurableVec`\n+\n+* Meta-key `__len` stores current length (`u64`).\n+* Element key = `|`\n+* `push(elem)` = `batch.put(key(len), elem); batch.put(__len, len+1)`\n+ – O(1) write, O(log N) read via prefix seek.\n+* `iter()` performs a prefix range; snapshot guarantees repeat-read.\n+\n+#### 5.2 `DurableMap`\n+\n+* Key = `|`\n+* `len` optional (feature `\"size_tracking\"`); otherwise O(prefix-scan).\n+\n+#### 5.3 Nested Collections\n+\n+```rust\n+let users = DurableMap::>::new(&db,\"users\")?;\n+users.entry(\"alice\")?.or_default()?.push(order)?;\n+```\n+\n+Internally `Entry::or_default` creates a *child prefix* off the parent’s key:\n+`|\"alice\"|0x00||…`\n+\n+Child collections store their own metadata keys beneath that path.\n+\n+---\n+\n+### 6  Transactions & Consistency\n+\n+* **Auto-batch**: default mutator methods create a WriteBatch, commit, and flush WAL.\n+* **Explicit batch**: user opens `let mut wb = db.batch();`, issues puts/deletes via collection adapters, then `wb.commit()` for cross-collection atomicity.\n+* **Crash guarantee**: after `commit` returns, updates survive power loss (`rocksdb::DB::flush_wal(true)`).\n+\n+Read operations take a **consistent snapshot** by default; advanced users can opt-out for max throughput.\n+\n+---\n+\n+### 7  Migrations (v0.2 roadmap)\n+\n+* Each collection stores a `u32 schema_version` meta key.\n+* `durable::open` accepts an optional `Schema` describing:\n+\n+ ```rust\n+ struct Schema { collections: Vec, version: u32 }\n+ ```\n+\n+ If version mismatch ⇒ run user-supplied `migrate(old, new, &db)` which gets a mutable view and may batch-rewrite keys.\n+\n+---\n+\n+### 8  Reactive Diffs (feature `\"watch\"`)\n+\n+* Behind a Tokio-aware feature; uses RocksDB’s `get_updates_since(seq)` API.\n+* `watch_prefix(prefix) -> impl Stream`\n+ – `Diff` = key, old val (Option), new val (Option).\n+* Back-pressure handled with an in-process ring buffer; user chooses lag policy.\n+\n+---\n+\n+### 9  Caching Layer (feature `\"cache\"`)\n+\n+* Probabilistic LRU over deserialized values.\n+* Configurable per-collection: `with_cache(cap_entries, ttl_ms)`.\n+* Coherent: write path invalidates cache keys on commit.\n+\n+---\n+\n+### 10  Error Model\n+\n+```rust\n+#[non_exhaustive]\n+pub enum Error {\n+ Rocks(rocksdb::Error),\n+ Serde(bincode::Error),\n+ Corruption(String),\n+ TransactionAborted,\n+ // feature-gated variants e.g. WatchLagged\n+}\n+```\n+\n+`Result = std::result::Result` everywhere.\n+\n+---\n+\n+### 11  Performance Budget (baseline targets)\n+\n+| Operation | Goal |\n+| -------------------- | -------------------------------------- |\n+| `map.insert` | < 30 µs (including WAL fsync) |\n+| `vec.push` | < 25 µs |\n+| Prefix iteration 1 M | > 120 MB/s read BW on NVMe |\n+| Concurrent readers | Linear scaling up to RocksDB read-IOPS |\n+| Watch latency | < 5 ms p50 on local SSD |\n+\n+(bench harness lives in `benches/` via Criterion.)\n+\n+---\n+\n+### 12  Dependency Footprint\n+\n+* `rocksdb` (–> FB fork) ⟹ builds C++11 static lib (\\~10 MB)\n+* `bincode` (default), with `serde` feature.\n+* `tokio-stream` only if `watch` feature enabled.\n+\n+MSRV = 1.76.\n+\n+---\n+\n+### 13  Minimum Deliverable for **v0.1-alpha**\n+\n+* [ ] `Db::open`, `Db::batch`\n+* [ ] `DurableMap`, `DurableVec`\n+* [ ] automatic serialization via `serde`\n+* [ ] atomic commit + WAL flush\n+* [ ] snapshot reads\n+* [ ] unit tests (insert/get/iter/crash-recovery using `tempdir`)\n+* [ ] criterion bench\n+\n+---\n+\n+### 14  Future Work\n+\n+* **Replicated mode** (Raft or FoundationDB layer)\n+* **CRDT merge semantics** for offline edits\n+* **DurableGraph** (adjacency lists + index)\n+* WebAssembly key-value adapters (edge workers)\n+* `tracing` instrumentation & Prometheus metrics\n+\n+---\n+\n+### 15  Licensing & Governance\n+\n+* License: **Apache-2.0 OR MIT** (standard Rust dual license)\n+* Code-of-conduct: Rust CoC template\n+* Contribution model: PR + mandatory CI (fmt, clippy, test, bench)\n+* Early roadmap guided by original author(s); transfer to an org when ≥3 maintainers.\n+\n+---\n+\n+## Appendix A — Sorter Use-Case Sketch\n+\n+```rust\n+type TagId = String;\n+type Bucket = u64; // logical Unix day or version #\n+type ItemId = String;\n+type Elo = i32;\n+\n+let hist = DurableMap::>>::new(&db,\"hist\")?;\n+\n+// update elo\n+hist.entry(\"tf2\")?\n+ .or_default()?\n+ .entry(today_bucket)?\n+ .or_default()?\n+ .push((item_id, new_elo))?;\n+\n+// stream bucket\n+let items = hist.get(\"tf2\")?.unwrap()\n+ .get(&today_bucket)?.unwrap()\n+ .iter().collect::>();\n+```\n+\n+All three layers share one RocksDB instance; you pay one WAL flush per ranking update, but reads are prefix-scans with zero allocations.\ndiff --git a/durable/docs/motivation.md b/durable/docs/motivation.md\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..50d8ee5ef9f384d908e3a86f6945e3f4ced58292\n--- /dev/null\n+++ b/durable/docs/motivation.md\n@@ -0,0 +1,248 @@\n+# Why Durable? The Missing Abstraction Layer for Persistent Storage\n+\n+## The Problem: The Abstraction Gap\n+\n+Every database forces developers to translate between how they **think** about data and how they **store** it. This translation layer is where bugs hide, performance suffers, and development slows down.\n+\n+### Example: Building a Multiplayer Game Leaderboard\n+\n+Let's say you need to store player rankings by game mode, with history by day. Here's the data model in your head:\n+\n+```\n+Game Mode → Day → List of (Player, Score)\n+```\n+\n+#### With Raw Key-Value Stores (sled, RocksDB)\n+\n+```rust\n+// Storing a score requires manual key construction\n+let key = format!(\"leaderboard:{}:{}:player:{}\", game_mode, day, player_id);\n+db.insert(key.as_bytes(), score.to_le_bytes())?;\n+\n+// Getting today's leaderboard? Manual prefix scan and deserialization\n+let prefix = format!(\"leaderboard:{}:{}:\", game_mode, day);\n+let mut scores = Vec::new();\n+for item in db.scan_prefix(prefix.as_bytes()) {\n+ let (key, value) = item?;\n+ // Parse player_id from key string... hope the format is right\n+ // Deserialize score... hope it's the right type\n+ scores.push((player_id, score));\n+}\n+scores.sort_by_key(|(_, s)| *s);\n+\n+// Want to know how many players played today? Another scan!\n+// Want to atomically update multiple scores? Write a transaction wrapper!\n+// Want to clean up old days? Manual prefix iteration and deletion!\n+```\n+\n+**Problems:**\n+- String manipulation for every operation\n+- No type safety (everything is bytes)\n+- Manual implementation of collection semantics\n+- No atomicity across related keys\n+- Performance overhead from string parsing\n+\n+#### With SQL Databases (SQLite, PostgreSQL)\n+\n+```sql\n+CREATE TABLE leaderboards (\n+ game_mode VARCHAR(50),\n+ day DATE,\n+ player_id UUID,\n+ score INTEGER,\n+ PRIMARY KEY (game_mode, day, player_id)\n+);\n+\n+-- Getting a leaderboard requires SQL\n+SELECT player_id, score \n+FROM leaderboards \n+WHERE game_mode = ? AND day = ?\n+ORDER BY score DESC;\n+```\n+\n+```rust\n+// In Rust, you need an ORM or manual query building\n+let scores: Vec<(Uuid, i32)> = sqlx::query_as(\n+ \"SELECT player_id, score FROM leaderboards WHERE game_mode = $1 AND day = $2 ORDER BY score DESC\"\n+)\n+.bind(&game_mode)\n+.bind(&day)\n+.fetch_all(&pool)\n+.await?;\n+```\n+\n+**Problems:**\n+- Impedance mismatch (relational model vs nested structures)\n+- SQL complexity for simple operations\n+- ORMs add abstraction layers and performance overhead\n+- Async runtime required even for local storage\n+- Schema migrations for every structural change\n+\n+#### With Document Stores (MongoDB)\n+\n+```javascript\n+// Document structure\n+{\n+ game_mode: \"ranked\",\n+ day: \"2024-01-15\",\n+ scores: [\n+ { player_id: \"abc\", score: 1500 },\n+ { player_id: \"def\", score: 1400 }\n+ ]\n+}\n+\n+// But now you have a different problem: updating a single score\n+// requires loading and saving the entire document!\n+```\n+\n+**Problems:**\n+- Not embedded (requires separate process)\n+- Document size limits\n+- Inefficient for partial updates\n+- Complex setup for local-first apps\n+\n+## The Solution: Native Data Structures\n+\n+With Durable, you express your data model directly:\n+\n+```rust\n+let leaderboard = DurableMap::>>::new(&db, \"leaderboard\")?;\n+\n+// Store a score - reads like natural Rust code\n+leaderboard\n+ .entry(game_mode)?\n+ .or_default()?\n+ .entry(day)?\n+ .or_default()?\n+ .push((player_id, score))?;\n+\n+// Get today's leaderboard - it's just a Vec\n+let mut today_scores = leaderboard\n+ .get(&game_mode)?\n+ .and_then(|mode| mode.get(&day).ok())\n+ .unwrap_or_default();\n+today_scores.sort_by_key(|(_, s)| *s);\n+\n+// All operations are atomic, typed, and efficient\n+```\n+\n+## Why This Matters\n+\n+### 1. **Zero Translation Overhead**\n+\n+Your mental model **is** the storage model. No more:\n+- String concatenation for keys\n+- Manual serialization/deserialization \n+- SQL query construction\n+- Document structure mapping\n+\n+### 2. **Composition Without Complexity**\n+\n+Nested data structures \"just work\":\n+\n+```rust\n+// A real-world example: user notifications by app by priority\n+let notifications = DurableMap::>>>::new(&db, \"notifs\")?;\n+\n+// Natural access patterns\n+notifications\n+ .get(&user_id)?\n+ .get(&app_id)?\n+ .get(&Priority::High)?\n+ .iter()\n+ .take(10) // Latest 10 high-priority notifications\n+```\n+\n+Try implementing this with SQL joins or KV prefixes!\n+\n+### 3. **Type Safety Throughout**\n+\n+```rust\n+// This won't compile - type safety at every level\n+let score: String = leaderboard.get(&\"chess\")?.get(&20240115)?.get(0)?;\n+// ^^^^^^ expected Score, found String\n+\n+// With raw KV stores, this is a runtime error after deserialization\n+```\n+\n+### 4. **Atomicity By Design**\n+\n+```rust\n+// Multiple operations in one atomic batch\n+let mut batch = db.batch();\n+batch.vec_push(&game.players, new_player)?;\n+batch.map_insert(&game.scores, player_id, 0)?;\n+batch.map_increment(&game.stats, \"player_count\", 1)?;\n+batch.commit()?; // All or nothing\n+```\n+\n+### 5. **Performance Without Compromise**\n+\n+- **Locality**: Related data stored contiguously (prefix design)\n+- **Zero-copy possible**: Direct memory mapping for read-heavy workloads\n+- **Streaming iteration**: No need to load entire collections\n+- **Bulk operations**: Native batch support\n+\n+## Comparison Matrix\n+\n+| Feature | Durable | sled/RocksDB | SQLite | MongoDB |\n+|---------|---------|--------------|---------|----------|\n+| **Native collections** | ✅ Built-in | ❌ DIY | ❌ Tables only | ⚠️ Documents |\n+| **Type safety** | ✅ Full | ❌ Bytes | ⚠️ ORM-dependent | ⚠️ Schema validation |\n+| **Nested structures** | ✅ Natural | ❌ Manual prefixes | ❌ Joins/JSON | ✅ Embedded docs |\n+| **Atomic operations** | ✅ Automatic | ⚠️ Manual batching | ✅ Transactions | ⚠️ Document-level |\n+| **Local/embedded** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Separate process |\n+| **Schema evolution** | ✅ Per-collection | ❌ DIY | ⚠️ Migrations | ✅ Flexible |\n+| **Memory efficiency** | ✅ Scan & stream | ✅ Manual | ⚠️ Query-dependent | ❌ Doc loading |\n+\n+## Real-World Use Cases Where Durable Shines\n+\n+### Local-First Sync Engine\n+\n+```rust\n+// Sync state with conflict tracking\n+let sync_state = DurableMap::>::new(&db, \"sync\")?;\n+\n+// Natural conflict detection\n+let versions = sync_state.get(&record_id)?;\n+if versions.values().unique().count() > 1 {\n+ // Conflict detected - handle naturally\n+}\n+```\n+\n+### Time-Series Analytics Cache\n+\n+```rust\n+// Metrics by source by minute\n+let metrics = DurableMap::>>::new(&db, \"metrics\")?;\n+\n+// Natural windowing\n+let last_hour: Vec = metrics\n+ .get(&source)?\n+ .range(now - 3600..=now)?\n+ .flat_map(|(_, minute_metrics)| minute_metrics.iter())\n+ .collect();\n+```\n+\n+### Feature Flag System with History\n+\n+```rust\n+// Flags by environment with change history\n+let flags = DurableMap::>>::new(&db, \"flags\")?;\n+\n+// Natural audit trail\n+let history = flags.get(&Env::Prod)?.get(\"new-feature\")?.iter().collect();\n+```\n+\n+## The Bottom Line\n+\n+**Durable isn't a better database - it's the missing abstraction layer that lets you use persistent storage like in-memory collections.**\n+\n+Stop translating. Start building.\n+\n+---\n+\n+*Next: Read the [RFC](001.md) for implementation details, or jump to the [Quick Start Guide](quickstart.md).* \n\\ No newline at end of file\ndiff --git a/durable/examples/combined_example.rs b/durable/examples/combined_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..626a6e1cf7e3c9c26f9f2edc58950d9bc31ec67e\n--- /dev/null\n+++ b/durable/examples/combined_example.rs\n@@ -0,0 +1,160 @@\n+use durable::{Db, DurableMap, DurableVec};\n+use serde::{Serialize, Deserialize};\n+use std::time::{SystemTime, UNIX_EPOCH};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize)]\n+struct Message {\n+ id: u64,\n+ from: String,\n+ to: String,\n+ content: String,\n+ timestamp: u64,\n+}\n+\n+#[derive(Debug, Clone, Serialize, Deserialize)]\n+struct User {\n+ username: String,\n+ display_name: String,\n+ message_count: u32,\n+}\n+\n+fn get_timestamp() -> u64 {\n+ SystemTime::now()\n+ .duration_since(UNIX_EPOCH)\n+ .unwrap()\n+ .as_secs()\n+}\n+\n+fn main() -> Result<(), Box> {\n+ // Open or create a database\n+ let db = Db::open(\"chat_db\")?;\n+ \n+ // Create our collections\n+ let mut users = DurableMap::::new(&db, \"users\")?;\n+ let mut messages = DurableVec::::new(&db, \"messages\")?;\n+ let mut user_message_indices = DurableMap::>::new(&db, \"user_messages\")?;\n+ \n+ // Create some users\n+ users.insert(\"alice\".to_string(), User {\n+ username: \"alice\".to_string(),\n+ display_name: \"Alice Smith\".to_string(),\n+ message_count: 0,\n+ })?;\n+ \n+ users.insert(\"bob\".to_string(), User {\n+ username: \"bob\".to_string(),\n+ display_name: \"Bob Johnson\".to_string(),\n+ message_count: 0,\n+ })?;\n+ \n+ users.insert(\"charlie\".to_string(), User {\n+ username: \"charlie\".to_string(),\n+ display_name: \"Charlie Brown\".to_string(),\n+ message_count: 0,\n+ })?;\n+ \n+ // Helper to send a message\n+ let send_message = |from: &str, to: &str, content: &str, \n+ messages: &mut DurableVec,\n+ users: &mut DurableMap,\n+ indices: &mut DurableMap>| -> Result<(), Box> {\n+ // Create message\n+ let msg_id = messages.len()? as u64;\n+ let message = Message {\n+ id: msg_id,\n+ from: from.to_string(),\n+ to: to.to_string(),\n+ content: content.to_string(),\n+ timestamp: get_timestamp(),\n+ };\n+ \n+ // Store message\n+ messages.push(message)?;\n+ let msg_index = messages.len()? - 1;\n+ \n+ // Update sender's message count\n+ if let Some(mut sender) = users.get(&from.to_string())? {\n+ sender.message_count += 1;\n+ users.insert(from.to_string(), sender)?;\n+ }\n+ \n+ // Track message indices for recipient\n+ let mut recipient_indices = indices.get(&to.to_string())?.unwrap_or_default();\n+ recipient_indices.push(msg_index);\n+ indices.insert(to.to_string(), recipient_indices)?;\n+ \n+ Ok(())\n+ };\n+ \n+ // Send some messages\n+ println!(\"💬 Chat Application Demo\\n\");\n+ println!(\"Sending messages...\");\n+ \n+ send_message(\"alice\", \"bob\", \"Hey Bob, how's the Durable library coming along?\", \n+ &mut messages, &mut users, &mut user_message_indices)?;\n+ \n+ send_message(\"bob\", \"alice\", \"It's going great! We have DurableVec and DurableMap working!\", \n+ &mut messages, &mut users, &mut user_message_indices)?;\n+ \n+ send_message(\"charlie\", \"alice\", \"That sounds awesome! Can I help with testing?\", \n+ &mut messages, &mut users, &mut user_message_indices)?;\n+ \n+ send_message(\"alice\", \"charlie\", \"Absolutely! The more testing the better!\", \n+ &mut messages, &mut users, &mut user_message_indices)?;\n+ \n+ send_message(\"bob\", \"charlie\", \"Check out the examples directory for usage patterns\", \n+ &mut messages, &mut users, &mut user_message_indices)?;\n+ \n+ // Display all users and their message counts\n+ println!(\"\\n👥 Users:\");\n+ let mut all_users = users.to_vec()?;\n+ all_users.sort_by_key(|(username, _)| username.clone());\n+ \n+ for (username, user) in all_users {\n+ println!(\" {} ({}) - {} messages sent\", \n+ user.display_name, username, user.message_count);\n+ }\n+ \n+ // Display all messages\n+ println!(\"\\n📨 All messages:\");\n+ for (i, msg) in messages.iter()?.enumerate() {\n+ let msg = msg?;\n+ println!(\" [{}] {} → {}: {}\", i, msg.from, msg.to, msg.content);\n+ }\n+ \n+ // Show inbox for each user\n+ println!(\"\\n📥 User inboxes:\");\n+ for item in users.iter() {\n+ let (username, _) = item?;\n+ if let Some(indices) = user_message_indices.get(&username)? {\n+ println!(\"\\n {}'s inbox ({} messages):\", username, indices.len());\n+ for &idx in &indices {\n+ if let Some(msg) = messages.get(idx)? {\n+ println!(\" From {}: {}\", msg.from, msg.content);\n+ }\n+ }\n+ }\n+ }\n+ \n+ // Statistics\n+ println!(\"\\n📊 Statistics:\");\n+ println!(\" Total users: {}\", users.len()?);\n+ println!(\" Total messages: {}\", messages.len()?);\n+ \n+ // Demonstrate persistence\n+ println!(\"\\n💾 Data has been persisted to disk!\");\n+ println!(\" Database location: ./chat_db\");\n+ \n+ // Clean up\n+ drop(messages);\n+ drop(users);\n+ drop(user_message_indices);\n+ drop(db);\n+ \n+ // Remove the database for this example\n+ std::fs::remove_dir_all(\"chat_db\").ok();\n+ \n+ println!(\"\\n✅ Example completed!\");\n+ \n+ Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/examples/map_example.rs b/durable/examples/map_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..08b8f2c8826caf53c4c20b422a540c92f6624029\n--- /dev/null\n+++ b/durable/examples/map_example.rs\n@@ -0,0 +1,99 @@\n+use durable::{Db, DurableMap};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize)]\n+struct UserProfile {\n+ name: String,\n+ email: String,\n+ score: u32,\n+}\n+\n+fn main() -> Result<(), Box> {\n+ // Open or create a database\n+ let db = Db::open(\"example_db\")?;\n+ \n+ // Create a persistent map of user profiles\n+ let mut users = DurableMap::::new(&db, \"users\")?;\n+ \n+ // Insert some users\n+ // Using put() when we don't need the old value - more efficient!\n+ users.put(\n+ \"alice\".to_string(),\n+ UserProfile {\n+ name: \"Alice Smith\".to_string(),\n+ email: \"alice@example.com\".to_string(),\n+ score: 1500,\n+ },\n+ )?;\n+ \n+ users.put(\n+ \"bob\".to_string(),\n+ UserProfile {\n+ name: \"Bob Johnson\".to_string(),\n+ email: \"bob@example.com\".to_string(),\n+ score: 1200,\n+ },\n+ )?;\n+ \n+ // Using insert() when we might need the old value\n+ let old_charlie = users.insert(\n+ \"charlie\".to_string(),\n+ UserProfile {\n+ name: \"Charlie Brown\".to_string(),\n+ email: \"charlie@example.com\".to_string(),\n+ score: 1800,\n+ },\n+ )?;\n+ \n+ if old_charlie.is_some() {\n+ println!(\"Replaced existing charlie entry\");\n+ }\n+ \n+ println!(\"Total users: {}\", users.len()?);\n+ \n+ // Look up a specific user\n+ if let Some(alice) = users.get(&\"alice\".to_string())? {\n+ println!(\"\\nAlice's profile: {:?}\", alice);\n+ }\n+ \n+ // Check if a user exists\n+ println!(\"\\nDoes 'david' exist? {}\", users.contains_key(&\"david\".to_string())?);\n+ \n+ // Update a user's score\n+ if let Some(mut bob) = users.get(&\"bob\".to_string())? {\n+ bob.score += 100;\n+ // Use put() here since we don't need the old value back\n+ users.put(\"bob\".to_string(), bob)?;\n+ println!(\"Updated Bob's score!\");\n+ }\n+ \n+ // Iterate over all users\n+ println!(\"\\nAll users (sorted by username):\");\n+ let mut all_users = users.to_vec()?;\n+ all_users.sort_by_key(|(username, _)| username.clone());\n+ \n+ for (username, profile) in all_users {\n+ println!(\" {} ({}) - Score: {}\", username, profile.email, profile.score);\n+ }\n+ \n+ // Get just the usernames\n+ let mut usernames = users.keys_vec()?;\n+ usernames.sort();\n+ println!(\"\\nAll usernames: {:?}\", usernames);\n+ \n+ // Find the highest scoring user\n+ let profiles = users.values_vec()?;\n+ if let Some(top_user) = profiles.iter().max_by_key(|p| p.score) {\n+ println!(\"\\nTop scorer: {} with {} points\", top_user.name, top_user.score);\n+ }\n+ \n+ // Remove a user\n+ if let Some(removed) = users.remove(&\"charlie\".to_string())? {\n+ println!(\"\\nRemoved user: {}\", removed.name);\n+ println!(\"Users remaining: {}\", users.len()?);\n+ }\n+ \n+ println!(\"\\nData has been persisted to disk.\");\n+ \n+ Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/examples/nested_example.rs b/durable/examples/nested_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3b880f2c8b8ad2633d4b5fcf2016652216c9de8e\n--- /dev/null\n+++ b/durable/examples/nested_example.rs\n@@ -0,0 +1,64 @@\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+ // Open a database\n+ let db = Db::open(\"nested_example_db\")?;\n+ \n+ // Create a map where each user has a list of posts\n+ let user_posts: DurableMap> = DurableMap::new_nested(&db, \"user_posts\");\n+ \n+ // Add posts for Alice\n+ println!(\"Adding posts for Alice...\");\n+ let mut alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+ alice_posts.push(\"Hello, world!\".to_string())?;\n+ alice_posts.push(\"Rust is awesome!\".to_string())?;\n+ alice_posts.push(\"Loving persistent data structures!\".to_string())?;\n+ \n+ // Add posts for Bob\n+ println!(\"Adding posts for Bob...\");\n+ let mut bob_posts = user_posts.entry(\"bob\".to_string())?.or_default()?;\n+ bob_posts.push(\"First post\".to_string())?;\n+ bob_posts.push(\"Learning Rust\".to_string())?;\n+ \n+ // Add a post for Charlie in a chained call\n+ println!(\"Adding post for Charlie...\");\n+ user_posts.entry(\"charlie\".to_string())?.or_default()?.push(\"One-liner post!\".to_string())?;\n+ \n+ // Read back Alice's posts\n+ println!(\"\\nAlice's posts:\");\n+ let alice_posts_read = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+ for i in 0..alice_posts_read.len()? {\n+ if let Some(post) = alice_posts_read.get(i)? {\n+ println!(\" {}: {}\", i + 1, post);\n+ }\n+ }\n+ \n+ // Read back Bob's posts\n+ println!(\"\\nBob's posts:\");\n+ let bob_posts_read = user_posts.entry(\"bob\".to_string())?.or_default()?;\n+ for i in 0..bob_posts_read.len()? {\n+ if let Some(post) = bob_posts_read.get(i)? {\n+ println!(\" {}: {}\", i + 1, post);\n+ }\n+ }\n+ \n+ // Read back Charlie's posts\n+ println!(\"\\nCharlie's posts:\");\n+ let charlie_posts_read = user_posts.entry(\"charlie\".to_string())?.or_default()?;\n+ for i in 0..charlie_posts_read.len()? {\n+ if let Some(post) = charlie_posts_read.get(i)? {\n+ println!(\" {}: {}\", i + 1, post);\n+ }\n+ }\n+ \n+ println!(\"\\nDemonstration of persistence...\");\n+ println!(\"Data is now persisted to disk. You can stop and restart this program,\");\n+ println!(\"and all the posts will still be there!\");\n+ \n+ println!(\"\\nTotal users with posts: 3\");\n+ println!(\"Alice has {} posts\", alice_posts_read.len()?);\n+ println!(\"Bob has {} posts\", bob_posts_read.len()?);\n+ println!(\"Charlie has {} posts\", charlie_posts_read.len()?);\n+ \n+ Ok(())\n+}\n\\ No newline at end of file\ndiff --git a/durable/examples/ranking_history.rs b/durable/examples/ranking_history.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..4da8592d240318318ae20c834d616210edb16c8d\n--- /dev/null\n+++ b/durable/examples/ranking_history.rs\n@@ -0,0 +1,179 @@\n+use durable::{Db, DurableMap, DurableVec};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]\n+struct RankingEntry {\n+ player_id: String,\n+ score: i32,\n+ timestamp: u64,\n+}\n+\n+impl RankingEntry {\n+ fn new(player_id: &str, score: i32) -> Self {\n+ Self {\n+ player_id: player_id.to_string(),\n+ score,\n+ timestamp: std::time::SystemTime::now()\n+ .duration_since(std::time::UNIX_EPOCH)\n+ .unwrap()\n+ .as_secs(),\n+ }\n+ }\n+}\n+\n+fn main() -> Result<(), Box> {\n+ println!(\"🎮 Gaming Ranking History System (Rewritten with Nested Entry API)\");\n+ println!(\"================================================================\");\n+ \n+ let db = Db::open(\"ranking_history_db\")?;\n+ \n+ // THE CORE CHANGE: Define the truly nested data structure.\n+ // Instead of a composite key, we nest a Map within a Map.\n+ // This represents the ideal, ergonomic API.\n+ type DailyRankings = DurableVec;\n+ type GameHistory = DurableMap;\n+ type Rankings = DurableMap;\n+\n+ let rankings: Rankings = DurableMap::new_nested(&db, \"game_rankings_v2\");\n+ \n+ // Simulate some game days\n+ let today = 20241215u32;\n+ let yesterday = 20241214u32;\n+ let last_week = 20241208u32;\n+ \n+ // No more `make_key` helper function!\n+ \n+ println!(\"\\n📊 Adding ranking data using chained entry().or_default()...\");\n+ \n+ // Add rankings for CS2 today. This demonstrates the new, clean access pattern.\n+ println!(\"Adding CS2 rankings for today ({})\", today);\n+ let mut cs2_today = rankings\n+ .entry(\"cs2\".to_string())?\n+ .or_default()? // Returns GameHistory (DurableMap) for \"cs2\"\n+ .entry(today)?\n+ .or_default()?; // Returns DailyRankings (DurableVec<...>) for `today`\n+\n+ cs2_today.push(RankingEntry::new(\"player1\", 2450))?;\n+ cs2_today.push(RankingEntry::new(\"player2\", 2380))?;\n+ cs2_today.push(RankingEntry::new(\"player3\", 2320))?;\n+ cs2_today.push(RankingEntry::new(\"player4\", 2280))?;\n+ \n+ // Add rankings for CS2 yesterday\n+ println!(\"Adding CS2 rankings for yesterday ({})\", yesterday);\n+ rankings\n+ .entry(\"cs2\".to_string())?\n+ .or_default()?\n+ .entry(yesterday)?\n+ .or_default()?\n+ .push(RankingEntry::new(\"player1\", 2420))?;\n+ rankings\n+ .entry(\"cs2\".to_string())?\n+ .or_default()?\n+ .entry(yesterday)?\n+ .or_default()?\n+ .push(RankingEntry::new(\"player2\", 2350))?;\n+ rankings\n+ .entry(\"cs2\".to_string())?\n+ .or_default()?\n+ .entry(yesterday)?\n+ .or_default()?\n+ .push(RankingEntry::new(\"player5\", 2300))?;\n+\n+ // Add rankings for Valorant today\n+ println!(\"Adding Valorant rankings for today ({})\", today);\n+ let mut valorant_today = rankings\n+ .entry(\"valorant\".to_string())?\n+ .or_default()?\n+ .entry(today)?\n+ .or_default()?;\n+\n+ valorant_today.push(RankingEntry::new(\"player6\", 1850))?;\n+ valorant_today.push(RankingEntry::new(\"player7\", 1820))?;\n+ valorant_today.push(RankingEntry::new(\"player1\", 1800))?; // Same player, different game\n+ \n+ // Add TF2 rankings (matching the docs example)\n+ println!(\"Adding TF2 rankings for last week ({})\", last_week);\n+ rankings\n+ .entry(\"tf2\".to_string())?\n+ .or_default()?\n+ .entry(last_week)?\n+ .or_default()?\n+ .push(RankingEntry::new(\"veteran_player\", 3200))?;\n+ rankings\n+ .entry(\"tf2\".to_string())?\n+ .or_default()?\n+ .entry(last_week)?\n+ .or_default()?\n+ .push(RankingEntry::new(\"old_school_gamer\", 3150))?;\n+ \n+ println!(\"\\n🏆 Reading back ranking data with the same natural API...\");\n+ \n+ // Get today's CS2 leaderboard\n+ println!(\"\\n🎯 CS2 Leaderboard for {} (today):\", today);\n+ let mut today_rankings = rankings\n+ .entry(\"cs2\".to_string())?\n+ .or_default()?\n+ .entry(today)?\n+ .or_default()?\n+ .to_vec()?;\n+\n+ // Sort by score descending\n+ today_rankings.sort_by(|a, b| b.score.cmp(&a.score));\n+ \n+ for (rank, entry) in today_rankings.iter().enumerate() {\n+ println!(\" {}. {} - {} points\", rank + 1, entry.player_id, entry.score);\n+ }\n+ \n+ // Show cross-game analysis is still easy\n+ println!(\"\\n🎮 Multi-game player analysis for player1 on {}:\", today);\n+ let cs2_player1_score = today_rankings.iter()\n+ .find(|e| e.player_id == \"player1\")\n+ .map(|e| e.score);\n+\n+ let valorant_player1_score = valorant_today.to_vec()?.iter()\n+ .find(|e| e.player_id == \"player1\")\n+ .map(|e| e.score);\n+\n+ if let Some(score) = cs2_player1_score { println!(\" CS2 Score: {}\", score); }\n+ if let Some(score) = valorant_player1_score { println!(\" Valorant Score: {}\", score); }\n+ \n+ // Showcase the power of the nested structure for stats\n+ // For nested collections, we use the keys API instead of iter()\n+ println!(\"\\n📊 Dynamic Database Statistics (discovered games):\");\n+ \n+ // Note: For nested collections, we iterate over known keys or use a different approach\n+ // since the values (nested DurableMaps) cannot be directly deserialized\n+ let games = vec![\"cs2\", \"valorant\", \"tf2\"]; // In a real app, you might track these separately\n+ \n+ for game in games {\n+ let game_history = rankings.entry(game.to_string())?.or_default()?;\n+ let active_days = game_history.len()?;\n+ \n+ if active_days > 0 {\n+ // For demonstration, let's count entries from known days\n+ let mut total_entries = 0;\n+ let days = [today, yesterday, last_week];\n+ \n+ for day in days {\n+ if let Ok(daily_rankings) = game_history.entry(day) {\n+ if let Ok(rankings_vec) = daily_rankings.or_default() {\n+ total_entries += rankings_vec.len()?;\n+ }\n+ }\n+ }\n+ \n+ if total_entries > 0 {\n+ println!(\" • {}: {} total entries across {} active day(s)\", \n+ game.to_uppercase(), total_entries, active_days);\n+ }\n+ }\n+ }\n+ \n+ println!(\"\\n✨ Key Benefits of This Rewritten Approach:\");\n+ println!(\" • No more manual key construction (`format!`) - the core goal is met!\");\n+ println!(\" • The code's structure now mirrors the mental model: `rankings[game][day]`\");\n+ println!(\" • Truly compositional API, unlocking more powerful dynamic queries (like the stats section)\");\n+ println!(\" • Demonstrates the full power of the `DurableCollection` and `entry()` design.\");\n+\n+ Ok(())\n+}\n\\ No newline at end of file\ndiff --git a/durable/examples/simple_ranking.rs b/durable/examples/simple_ranking.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3cc873c41f3d93c7fa9e6e2dfc80e815f456b1f9\n--- /dev/null\n+++ b/durable/examples/simple_ranking.rs\n@@ -0,0 +1,68 @@\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+ println!(\"🏆 Simple Game Ranking Example\");\n+ println!(\"Demonstrating the pattern from docs/motivation.md\");\n+ println!(\"===============================================\");\n+ \n+ let db = Db::open(\"simple_ranking_db\")?;\n+ \n+ // This is the exact pattern from the docs: Game Mode → List of (Player, Score)\n+ // For simplicity, we're showing one day's data per game mode\n+ let rankings: DurableMap> = DurableMap::new_nested(&db, \"rankings\");\n+ \n+ println!(\"\\n📊 Adding TF2 rankings (from the docs example)...\");\n+ \n+ // This is the exact code pattern shown in docs/motivation.md\n+ let mut tf2_rankings = rankings.entry(\"tf2\".to_string())?.or_default()?;\n+ tf2_rankings.push((\"player1\".to_string(), 1500))?;\n+ tf2_rankings.push((\"player2\".to_string(), 1400))?;\n+ tf2_rankings.push((\"player3\".to_string(), 1300))?;\n+ \n+ println!(\"✅ Added TF2 rankings using the docs pattern!\");\n+ \n+ // Add some other games for comparison\n+ println!(\"\\n📊 Adding CS2 rankings...\");\n+ let mut cs2_rankings = rankings.entry(\"cs2\".to_string())?.or_default()?;\n+ cs2_rankings.push((\"pro_player\".to_string(), 2500))?;\n+ cs2_rankings.push((\"skilled_gamer\".to_string(), 2200))?;\n+ \n+ println!(\"✅ Added CS2 rankings!\");\n+ \n+ // Now read back the data\n+ println!(\"\\n🏆 Current TF2 Leaderboard:\");\n+ let tf2_data = rankings.entry(\"tf2\".to_string())?.or_default()?;\n+ \n+ // Convert to vec and sort for display\n+ let mut tf2_leaderboard = tf2_data.to_vec()?;\n+ tf2_leaderboard.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by score descending\n+ \n+ for (rank, (player, score)) in tf2_leaderboard.iter().enumerate() {\n+ println!(\" {}. {} - {} points\", rank + 1, player, score);\n+ }\n+ \n+ println!(\"\\n🏆 Current CS2 Leaderboard:\");\n+ let cs2_data = rankings.entry(\"cs2\".to_string())?.or_default()?;\n+ \n+ let mut cs2_leaderboard = cs2_data.to_vec()?;\n+ cs2_leaderboard.sort_by(|a, b| b.1.cmp(&a.1));\n+ \n+ for (rank, (player, score)) in cs2_leaderboard.iter().enumerate() {\n+ println!(\" {}. {} - {} points\", rank + 1, player, score);\n+ }\n+ \n+ println!(\"\\n📈 Database Statistics:\");\n+ println!(\" TF2 has {} players\", tf2_data.len()?);\n+ println!(\" CS2 has {} players\", cs2_data.len()?);\n+ \n+ println!(\"\\n✨ This demonstrates the exact pattern from docs/motivation.md:\");\n+ println!(\" rankings.entry(game_mode)?.or_default()?.push((player, score))?;\");\n+ println!(\" \");\n+ println!(\" Compare this to the manual key construction required with raw KV stores:\");\n+ println!(\" let key = format!(\\\"leaderboard:{{}}:{{}}:player:{{}}\\\", game_mode, day, player_id);\");\n+ println!(\" db.insert(key.as_bytes(), score.to_le_bytes())?;\");\n+ println!(\" \");\n+ println!(\" Durable provides the ergonomic, type-safe abstraction over RocksDB!\");\n+ \n+ Ok(())\n+}\n\\ No newline at end of file\ndiff --git a/durable/examples/streaming_demo.rs b/durable/examples/streaming_demo.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3a74a5318675f94f89aaf01562a5b28f8a1b664a\n--- /dev/null\n+++ b/durable/examples/streaming_demo.rs\n@@ -0,0 +1,81 @@\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+ let db = Db::open(\"streaming_demo_db\")?;\n+ \n+ // Create collections with a moderate amount of data\n+ let mut map = DurableMap::::new(&db, \"large_map\")?;\n+ let mut vec = DurableVec::::new(&db, \"large_vec\")?;\n+ \n+ println!(\"🚀 Streaming Iterator Demo\\n\");\n+ \n+ // Add 1000 entries to demonstrate streaming\n+ println!(\"Adding 1000 entries to map and vec...\");\n+ for i in 0..1000 {\n+ map.insert(i, format!(\"Value {}\", i))?;\n+ vec.push(format!(\"Item {}\", i))?;\n+ }\n+ \n+ println!(\"\\n📊 Collection sizes:\");\n+ println!(\" Map entries: {}\", map.len()?);\n+ println!(\" Vec elements: {}\", vec.len()?);\n+ \n+ // Demonstrate streaming iteration - memory efficient\n+ println!(\"\\n✨ Streaming iteration (memory efficient):\");\n+ \n+ // Count items without loading into memory\n+ let map_count = map.iter().count();\n+ println!(\" Counted {} map entries without loading into memory\", map_count);\n+ \n+ // Find specific items efficiently\n+ let target = 500;\n+ let found = map.iter()\n+ .find(|item| {\n+ item.as_ref()\n+ .map(|(k, _)| *k == target)\n+ .unwrap_or(false)\n+ });\n+ \n+ if let Some(Ok((k, v))) = found {\n+ println!(\" Found key {} with value '{}' via streaming\", k, v);\n+ }\n+ \n+ // Process only what we need\n+ println!(\"\\n🎯 Processing first 10 items only:\");\n+ for (i, item) in vec.iter()?.take(10).enumerate() {\n+ match item {\n+ Ok(value) => println!(\" [{}] {}\", i, value),\n+ Err(e) => println!(\" [{}] Error: {:?}\", i, e),\n+ }\n+ }\n+ \n+ // Filter and process without loading all data\n+ println!(\"\\n🔍 Filtering even keys without loading all data:\");\n+ let even_count = map.keys()\n+ .filter(|item| {\n+ item.as_ref()\n+ .map(|k| k % 2 == 0)\n+ .unwrap_or(false)\n+ })\n+ .count();\n+ println!(\" Found {} even keys\", even_count);\n+ \n+ // Compare with loading everything into memory\n+ println!(\"\\n⚠️ Loading all data into memory (less efficient for large collections):\");\n+ let all_values = map.values_vec()?;\n+ println!(\" Loaded {} values into a Vec\", all_values.len());\n+ \n+ println!(\"\\n✅ Streaming iterators provide:\");\n+ println!(\" • Constant memory usage regardless of collection size\");\n+ println!(\" • Ability to process data larger than RAM\");\n+ println!(\" • Early termination when finding specific items\");\n+ println!(\" • Efficient filtering and transformation\");\n+ \n+ // Clean up\n+ drop(map);\n+ drop(vec);\n+ drop(db);\n+ std::fs::remove_dir_all(\"streaming_demo_db\").ok();\n+ \n+ Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/examples/vec_example.rs b/durable/examples/vec_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..07d394b5f3964ea1942a645c2f008e9e3c37d6d8\n--- /dev/null\n+++ b/durable/examples/vec_example.rs\n@@ -0,0 +1,66 @@\n+use durable::{Db, DurableVec};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n+struct Task {\n+ id: u64,\n+ title: String,\n+ completed: bool,\n+}\n+\n+fn main() -> Result<(), Box> {\n+ // Open or create a database\n+ let db = Db::open(\"example_db\")?;\n+ \n+ // Create a persistent vector of tasks\n+ let mut tasks = DurableVec::::new(&db, \"tasks\")?;\n+ \n+ // Add some tasks\n+ tasks.push(Task {\n+ id: 1,\n+ title: \"Build Durable library\".to_string(),\n+ completed: true,\n+ })?;\n+ \n+ tasks.push(Task {\n+ id: 2,\n+ title: \"Write comprehensive tests\".to_string(),\n+ completed: true,\n+ })?;\n+ \n+ tasks.push(Task {\n+ id: 3,\n+ title: \"Create documentation\".to_string(),\n+ completed: false,\n+ })?;\n+ \n+ println!(\"Total tasks: {}\", tasks.len()?);\n+ \n+ // Iterate through all tasks\n+ println!(\"\\nAll tasks:\");\n+ for (i, task) in tasks.iter()?.enumerate() {\n+ let task = task?;\n+ println!(\" [{}] {} - {}\", \n+ i, \n+ task.title, \n+ if task.completed { \"✓\" } else { \"○\" }\n+ );\n+ }\n+ \n+ // Get a specific task\n+ if let Some(task) = tasks.get(1)? {\n+ println!(\"\\nTask at index 1: {:?}\", task);\n+ }\n+ \n+ // Mark the last task as completed\n+ if let Some(mut last_task) = tasks.pop()? {\n+ println!(\"\\nCompleting task: {}\", last_task.title);\n+ last_task.completed = true;\n+ tasks.push(last_task)?;\n+ }\n+ \n+ // The data persists even after the program exits!\n+ println!(\"\\nData has been persisted to disk.\");\n+ \n+ Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/repomix.config.json b/durable/repomix.config.json\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..bb52e4fcd383e8937245bd62566db8b7652402c2\n--- /dev/null\n+++ b/durable/repomix.config.json\n@@ -0,0 +1,27 @@\n+{\n+ \"output\": {\n+ \"filePath\": \"all.txt\",\n+ \"style\": \"plain\",\n+ \"parsableStyle\": false,\n+ \"fileSummary\": true,\n+ \"directoryStructure\": true,\n+ \"removeComments\": false,\n+ \"removeEmptyLines\": false,\n+ \"compress\": false,\n+ \"topFilesLength\": 100,\n+ \"showLineNumbers\": false,\n+ \"copyToClipboard\": false\n+ },\n+ \"include\": [],\n+ \"ignore\": {\n+ \"useGitignore\": true,\n+ \"useDefaultPatterns\": true,\n+ \"customPatterns\": []\n+ },\n+ \"security\": {\n+ \"enableSecurityCheck\": true\n+ },\n+ \"tokenCount\": {\n+ \"encoding\": \"o200k_base\"\n+ }\n+}\ndiff --git a/durable/src/lib.rs b/durable/src/lib.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..94e8823be93be1176b0a4cdc9797181b9f8caf60\n--- /dev/null\n+++ b/durable/src/lib.rs\n@@ -0,0 +1,123 @@\n+//! Durable - RocksDB-backed persistent data structures for Rust\n+\n+use std::path::Path;\n+use std::sync::Arc;\n+use rocksdb::{DB as RocksDB, Options, WriteBatch};\n+use thiserror::Error;\n+\n+pub mod vec;\n+pub mod map;\n+pub use vec::DurableVec;\n+pub use map::DurableMap;\n+\n+/// Error types for Durable operations\n+#[derive(Error, Debug)]\n+pub enum DurableError {\n+ #[error(\"RocksDB error: {0}\")]\n+ RocksDB(#[from] rocksdb::Error),\n+ \n+ #[error(\"Serialization error: {0}\")]\n+ Serialization(#[from] bincode::Error),\n+ \n+ #[error(\"Key not found\")]\n+ KeyNotFound,\n+ \n+ #[error(\"Collection not found: {0}\")]\n+ CollectionNotFound(String),\n+ \n+ #[error(\"Data corruption: {0}\")]\n+ Corruption(String),\n+}\n+\n+pub type Result = std::result::Result;\n+\n+/// A trait for types that can be used as nested collections.\n+pub trait DurableCollection {\n+ /// Creates a new instance of the collection from a database handle\n+ /// and a pre-determined, unique key prefix.\n+ /// \n+ /// This is the key method that allows `DurableMap` to instantiate\n+ /// a nested collection handle.\n+ fn from_prefix(db: Db, prefix: Vec) -> Self;\n+}\n+\n+/// The main database handle\n+#[derive(Clone)]\n+pub struct Db {\n+ inner: Arc,\n+}\n+\n+impl Db {\n+ /// Opens or creates a durable database at the given path\n+ pub fn open>(path: P) -> Result {\n+ let mut opts = Options::default();\n+ opts.create_if_missing(true);\n+ opts.create_missing_column_families(true);\n+ \n+ let db = RocksDB::open(&opts, path)?;\n+ Ok(Db { \n+ inner: Arc::new(db),\n+ })\n+ }\n+ \n+ /// Create a new write batch for atomic operations\n+ pub fn batch(&self) -> Batch {\n+ Batch {\n+ db: self.clone(),\n+ inner: WriteBatch::default(),\n+ }\n+ }\n+ \n+ /// Get the underlying RocksDB handle (for advanced usage)\n+ pub(crate) fn rocks(&self) -> &RocksDB {\n+ &self.inner\n+ }\n+ \n+ /// Get a new unique collection ID for nested collections\n+ pub fn new_collection_id(&self) -> Result {\n+ let key = b\"__global_meta:next_collection_id\";\n+ \n+ // Get current value\n+ let current_bytes = self.rocks().get(key)?;\n+ let current_id = match current_bytes {\n+ Some(bytes) => {\n+ if bytes.len() != 8 {\n+ return Err(DurableError::Corruption(\"Invalid collection ID bytes size\".into()));\n+ }\n+ let id_bytes: [u8; 8] = bytes[..8].try_into()\n+ .map_err(|_| DurableError::Corruption(\"Invalid collection ID bytes\".into()))?;\n+ u64::from_le_bytes(id_bytes)\n+ }\n+ None => 0,\n+ };\n+ \n+ let next_id = current_id + 1;\n+ \n+ // Try to atomically update - use compare-and-swap semantics\n+ let mut batch = WriteBatch::default();\n+ batch.put(key, &next_id.to_le_bytes());\n+ \n+ // For now, just write it directly. In a real implementation,\n+ // we'd want proper compare-and-swap to handle concurrent access\n+ self.rocks().write(batch)?;\n+ self.rocks().flush_wal(true)?;\n+ \n+ Ok(current_id)\n+ }\n+}\n+\n+/// A write batch for atomic operations\n+pub struct Batch {\n+ db: Db,\n+ inner: WriteBatch,\n+}\n+\n+impl Batch {\n+ /// Commit all operations in this batch atomically\n+ pub fn commit(self) -> Result<()> {\n+ self.db.rocks().write(self.inner)?;\n+ self.db.rocks().flush_wal(true)?;\n+ Ok(())\n+ }\n+ \n+}\ndiff --git a/durable/src/map.rs b/durable/src/map.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..2629857270b1be346a20fc67dec1cfc57c829403\n--- /dev/null\n+++ b/durable/src/map.rs\n@@ -0,0 +1,1073 @@\n+use crate::{Db, Result, DurableError, DurableCollection};\n+use rocksdb::{IteratorMode, WriteBatch, Direction};\n+use serde::{Serialize, Deserialize};\n+use std::marker::PhantomData;\n+\n+/// A persistent map backed by RocksDB\n+pub struct DurableMap {\n+ db: Db,\n+ prefix: Vec,\n+ _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl DurableMap \n+where \n+ K: Serialize + for<'de> Deserialize<'de>,\n+ V: Serialize + for<'de> Deserialize<'de>,\n+{\n+ /// Create a new DurableMap with the given name\n+ pub fn new(db: &Db, name: &str) -> Result {\n+ let prefix = format!(\"map:{}\", name).into_bytes();\n+ \n+ Ok(DurableMap {\n+ db: db.clone(),\n+ prefix,\n+ _phantom: PhantomData,\n+ })\n+ }\n+ \n+ /// Insert a key-value pair into the map\n+ pub fn insert(&mut self, key: K, value: V) -> Result> {\n+ let key_bytes = bincode::serialize(&key)?;\n+ let value_bytes = bincode::serialize(&value)?;\n+ \n+ // Get the old value if it exists\n+ let old_value = self.get(&key)?;\n+ \n+ let mut batch = WriteBatch::default();\n+ \n+ // Write the new value\n+ let db_key = self.entry_key(&key_bytes);\n+ batch.put(&db_key, &value_bytes);\n+ \n+ // Update length if this is a new key\n+ if old_value.is_none() {\n+ let new_len = self.len()? + 1;\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+ }\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(old_value)\n+ }\n+ \n+ /// Put a key-value pair into the map without returning the old value\n+ /// \n+ /// This is more efficient than `insert` when you don't need the old value,\n+ /// as it only checks for key existence without deserializing the value.\n+ pub fn put(&mut self, key: K, value: V) -> Result<()> {\n+ let key_bytes = bincode::serialize(&key)?;\n+ let value_bytes = bincode::serialize(&value)?;\n+ let db_key = self.entry_key(&key_bytes);\n+ \n+ let mut batch = WriteBatch::default();\n+ \n+ // Check if this is a new key (without deserializing the value)\n+ let is_new = self.db.rocks().get_pinned(&db_key)?.is_none();\n+ \n+ // Write the new value\n+ batch.put(&db_key, &value_bytes);\n+ \n+ // Update length if this is a new key\n+ if is_new {\n+ let new_len = self.len()? + 1;\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+ }\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(())\n+ }\n+ \n+ /// Get a value by key\n+ pub fn get(&self, key: &K) -> Result> {\n+ let key_bytes = bincode::serialize(key)?;\n+ let db_key = self.entry_key(&key_bytes);\n+ \n+ match self.db.rocks().get(&db_key)? {\n+ Some(bytes) => {\n+ let value = bincode::deserialize(&bytes)?;\n+ Ok(Some(value))\n+ }\n+ None => Ok(None),\n+ }\n+ }\n+ \n+ /// Check if a key exists in the map\n+ pub fn contains_key(&self, key: &K) -> Result {\n+ let key_bytes = bincode::serialize(key)?;\n+ let db_key = self.entry_key(&key_bytes);\n+ \n+ Ok(self.db.rocks().get(&db_key)?.is_some())\n+ }\n+ \n+ /// Remove a key-value pair from the map\n+ pub fn remove(&mut self, key: &K) -> Result> {\n+ let key_bytes = bincode::serialize(key)?;\n+ let db_key = self.entry_key(&key_bytes);\n+ \n+ // Get the old value\n+ let old_value = match self.db.rocks().get(&db_key)? {\n+ Some(bytes) => {\n+ let value = bincode::deserialize(&bytes)?;\n+ Some(value)\n+ }\n+ None => None,\n+ };\n+ \n+ // Delete the key if it existed and update length\n+ if old_value.is_some() {\n+ let mut batch = WriteBatch::default();\n+ \n+ // Delete the entry\n+ batch.delete(&db_key);\n+ \n+ // Update length\n+ let new_len = self.len()? - 1;\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ }\n+ \n+ Ok(old_value)\n+ }\n+ \n+\n+ \n+ /// Clear all entries from the map\n+ pub fn clear(&mut self) -> Result<()> {\n+ let prefix = self.entry_prefix();\n+ let mut batch = WriteBatch::default();\n+ \n+ // Collect all keys to delete\n+ let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+ for item in iter {\n+ let (key, _) = item?;\n+ if !key.starts_with(&prefix) {\n+ break;\n+ }\n+ batch.delete(&key);\n+ }\n+ \n+ // Reset length to 0\n+ let len_key = self.meta_key(\"len\");\n+ batch.delete(&len_key);\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(())\n+ }\n+ \n+ /// Iterate over all key-value pairs using a streaming iterator\n+ pub fn iter(&self) -> MapIterator<'_, K, V> {\n+ let prefix = self.entry_prefix();\n+ let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+ \n+ MapIterator {\n+ inner: iter,\n+ prefix,\n+ _phantom: PhantomData,\n+ }\n+ }\n+ \n+ /// Load all key-value pairs into a Vec\n+ /// \n+ /// Note: This loads the entire collection into memory. For large collections,\n+ /// prefer using `iter()` which streams elements.\n+ pub fn to_vec(&self) -> Result> {\n+ let mut result = Vec::new();\n+ for item in self.iter() {\n+ result.push(item?);\n+ }\n+ Ok(result)\n+ }\n+ \n+ /// Iterate over all keys using a streaming iterator\n+ pub fn keys(&self) -> KeyIterator<'_, K, V> {\n+ let prefix = self.entry_prefix();\n+ let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+ \n+ KeyIterator {\n+ inner: iter,\n+ prefix,\n+ _phantom: PhantomData,\n+ }\n+ }\n+ \n+ /// Load all keys into a Vec\n+ /// \n+ /// Note: This loads all keys into memory. For large collections,\n+ /// prefer using `keys()` which streams elements.\n+ pub fn keys_vec(&self) -> Result> {\n+ let mut result = Vec::new();\n+ for item in self.keys() {\n+ result.push(item?);\n+ }\n+ Ok(result)\n+ }\n+ \n+ /// Iterate over all values using a streaming iterator\n+ pub fn values(&self) -> ValueIterator<'_, K, V> {\n+ let prefix = self.entry_prefix();\n+ let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+ \n+ ValueIterator {\n+ inner: iter,\n+ prefix,\n+ _phantom: PhantomData,\n+ }\n+ }\n+ \n+ /// Load all values into a Vec\n+ /// \n+ /// Note: This loads all values into memory. For large collections,\n+ /// prefer using `values()` which streams elements.\n+ pub fn values_vec(&self) -> Result> {\n+ let mut result = Vec::new();\n+ for item in self.values() {\n+ result.push(item?);\n+ }\n+ Ok(result)\n+ }\n+ \n+ /// Insert multiple key-value pairs in a single batch\n+ pub fn extend(&mut self, iter: I) -> Result<()>\n+ where\n+ I: IntoIterator\n+ {\n+ let mut batch = WriteBatch::default();\n+ let current_len = self.len()?;\n+ let mut new_entries = 0;\n+ \n+ for (key, value) in iter {\n+ let key_bytes = bincode::serialize(&key)?;\n+ let value_bytes = bincode::serialize(&value)?;\n+ let db_key = self.entry_key(&key_bytes);\n+ \n+ // Check if this is a new key\n+ if !self.contains_key(&key)? {\n+ new_entries += 1;\n+ }\n+ \n+ batch.put(&db_key, &value_bytes);\n+ }\n+ \n+ // Update length if we added new entries\n+ if new_entries > 0 {\n+ let new_len = current_len + new_entries;\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+ }\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(())\n+ }\n+ \n+\n+ \n+ // Helper methods\n+ \n+ fn entry_key(&self, key_bytes: &[u8]) -> Vec {\n+ let mut db_key = self.prefix.clone();\n+ db_key.extend_from_slice(b\":entry:\");\n+ db_key.extend_from_slice(key_bytes);\n+ db_key\n+ }\n+ \n+ fn entry_prefix(&self) -> Vec {\n+ let mut prefix = self.prefix.clone();\n+ prefix.extend_from_slice(b\":entry:\");\n+ prefix\n+ }\n+}\n+\n+// Additional implementation block for methods that don't require serialization constraints\n+impl DurableMap {\n+ /// Create a new DurableMap for nested collections (no serialization constraints)\n+ pub fn new_nested(db: &Db, name: &str) -> Self {\n+ let prefix = format!(\"map:{}\", name).into_bytes();\n+ \n+ DurableMap {\n+ db: db.clone(),\n+ prefix,\n+ _phantom: PhantomData,\n+ }\n+ }\n+ \n+ /// Create a new DurableMap from a prefix (used for nested collections)\n+ pub fn from_prefix(db: Db, prefix: Vec) -> Self {\n+ Self {\n+ db,\n+ prefix,\n+ _phantom: PhantomData,\n+ }\n+ }\n+ \n+ /// Get the number of entries in the map (unconstrained version for nested collections)\n+ pub fn len(&self) -> Result {\n+ let key = self.meta_key(\"len\");\n+ match self.db.rocks().get(&key)? {\n+ Some(bytes) => {\n+ if bytes.len() != 8 {\n+ return Err(DurableError::Corruption(\"Invalid length bytes size\".into()));\n+ }\n+ let len_bytes: [u8; 8] = bytes[..8].try_into()\n+ .map_err(|_| DurableError::Corruption(\"Invalid length bytes\".into()))?;\n+ Ok(u64::from_le_bytes(len_bytes) as usize)\n+ }\n+ None => Ok(0),\n+ }\n+ }\n+ \n+ /// Check if the map is empty (unconstrained version for nested collections)\n+ pub fn is_empty(&self) -> Result {\n+ Ok(self.len()? == 0)\n+ }\n+\n+ /// Get the entry key for nested collections (needed by entry API)\n+ pub(crate) fn make_entry_key(&self, key_bytes: &[u8]) -> Vec {\n+ let mut db_key = self.prefix.clone();\n+ db_key.extend_from_slice(b\":entry:\");\n+ db_key.extend_from_slice(key_bytes);\n+ db_key\n+ }\n+ \n+ fn meta_key(&self, meta_type: &str) -> Vec {\n+ let mut key = self.prefix.clone();\n+ key.extend_from_slice(b\":__meta:\");\n+ key.extend_from_slice(meta_type.as_bytes());\n+ key\n+ }\n+}\n+\n+// Implement the DurableCollection trait for DurableMap\n+// This implementation is used for nested collections and doesn't require\n+// serialization bounds since nested maps use collection markers, not direct serialization\n+impl DurableCollection for DurableMap {\n+ fn from_prefix(db: Db, prefix: Vec) -> Self {\n+ DurableMap::from_prefix(db, prefix)\n+ }\n+}\n+\n+/// Entry API for DurableMap with nested collections\n+pub enum DurableEntry<'a, K, V> {\n+ Occupied(OccupiedEntry<'a, K, V>),\n+ Vacant(VacantEntry<'a, K, V>),\n+}\n+\n+impl<'a, K, V> DurableEntry<'a, K, V>\n+where\n+ K: Serialize,\n+ V: DurableCollection,\n+{\n+ /// Gets the collection, creating it if it doesn't exist\n+ pub fn or_default(self) -> Result {\n+ match self {\n+ DurableEntry::Occupied(entry) => entry.or_default(),\n+ DurableEntry::Vacant(entry) => entry.or_default(),\n+ }\n+ }\n+}\n+\n+/// Represents an entry that already exists\n+pub struct OccupiedEntry<'a, K, V> {\n+ map: &'a DurableMap,\n+ key_bytes: Vec,\n+ value_marker: Vec, // The bytes read from RocksDB, e.g., [0x02, ...]\n+}\n+\n+impl<'a, K, V> OccupiedEntry<'a, K, V> \n+where\n+ V: DurableCollection,\n+{\n+ /// Gets a handle to the existing nested collection\n+ pub fn get(self) -> Result {\n+ // 1. Parse the collection_id from self.value_marker\n+ if self.value_marker.len() != 9 || self.value_marker[0] != 0x02 {\n+ return Err(DurableError::Corruption(\"Invalid collection marker\".into()));\n+ }\n+ \n+ let col_id_bytes: [u8; 8] = self.value_marker[1..9].try_into()\n+ .map_err(|_| DurableError::Corruption(\"Invalid collection ID\".into()))?;\n+ let col_id = u64::from_le_bytes(col_id_bytes);\n+\n+ // 2. Re-construct the unique prefix for the child collection\n+ let parent_key_prefix = self.map.make_entry_key(&self.key_bytes);\n+ let child_prefix = [parent_key_prefix.as_slice(), &[0x00], &col_id.to_le_bytes()].concat();\n+\n+ // 3. Create the collection handle using the trait method\n+ Ok(V::from_prefix(self.map.db.clone(), child_prefix))\n+ }\n+ \n+ /// Gets a handle to the existing nested collection (same as get but consumes self)\n+ pub fn or_default(self) -> Result {\n+ self.get()\n+ }\n+}\n+\n+/// Represents a slot that is empty\n+pub struct VacantEntry<'a, K, V> {\n+ map: &'a DurableMap,\n+ key: K, // The original key from the user\n+}\n+\n+impl<'a, K, V> VacantEntry<'a, K, V> \n+where\n+ K: Serialize,\n+ V: DurableCollection,\n+{\n+ /// Inserts a new default collection and returns a handle to it\n+ pub fn or_default(self) -> Result {\n+ // 1. Atomically get a new unique ID for the collection\n+ let new_col_id = self.map.db.new_collection_id()?;\n+\n+ // 2. Create the marker value that points to our new collection\n+ let mut value_marker = vec![0x02_u8];\n+ value_marker.extend_from_slice(&new_col_id.to_le_bytes());\n+\n+ // 3. Get the key bytes and construct the full parent entry key\n+ let key_bytes = bincode::serialize(&self.key)?;\n+ let parent_db_key = self.map.make_entry_key(&key_bytes);\n+\n+ // 4. ATOMICALLY write the marker to the parent map\n+ self.map.db.rocks().put(&parent_db_key, &value_marker)?;\n+ self.map.db.rocks().flush_wal(true)?;\n+\n+ // 5. Construct the unique prefix for our new child collection\n+ let child_prefix = [parent_db_key.as_slice(), &[0x00], &new_col_id.to_le_bytes()].concat();\n+\n+ // 6. Create and return the new collection handle\n+ Ok(V::from_prefix(self.map.db.clone(), child_prefix))\n+ }\n+}\n+\n+// API for nested collections\n+impl DurableMap {\n+ /// The entry point for creating or accessing a nested collection.\n+ /// This method is only available when `V` is a `DurableCollection`.\n+ pub fn entry(&self, key: K) -> Result>\n+ where\n+ V: DurableCollection,\n+ K: Serialize,\n+ {\n+ let key_bytes = bincode::serialize(&key)?;\n+ let db_key = self.make_entry_key(&key_bytes);\n+\n+ match self.db.rocks().get(&db_key)? {\n+ Some(value_marker) => {\n+ // Key exists. The value should be a collection marker.\n+ Ok(DurableEntry::Occupied(OccupiedEntry {\n+ map: self,\n+ key_bytes,\n+ value_marker,\n+ }))\n+ }\n+ None => {\n+ // Key doesn't exist.\n+ Ok(DurableEntry::Vacant(VacantEntry { map: self, key }))\n+ }\n+ }\n+ }\n+}\n+\n+/// Iterator over key-value pairs in a DurableMap\n+pub struct MapIterator<'a, K, V> {\n+ inner: rocksdb::DBIterator<'a>,\n+ prefix: Vec,\n+ _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl<'a, K, V> Iterator for MapIterator<'a, K, V>\n+where\n+ K: for<'de> Deserialize<'de>,\n+ V: for<'de> Deserialize<'de>,\n+{\n+ type Item = Result<(K, V)>;\n+ \n+ fn next(&mut self) -> Option {\n+ match self.inner.next() {\n+ Some(Ok((db_key, value_bytes))) => {\n+ // Check if we're still within our prefix\n+ if !db_key.starts_with(&self.prefix) {\n+ return None;\n+ }\n+ \n+ // Extract the key part (skip prefix)\n+ let key_start = self.prefix.len();\n+ let key_bytes = &db_key[key_start..];\n+ \n+ // Deserialize key and value\n+ match (bincode::deserialize(key_bytes), bincode::deserialize(&value_bytes)) {\n+ (Ok(key), Ok(value)) => Some(Ok((key, value))),\n+ (Err(e), _) | (_, Err(e)) => Some(Err(e.into())),\n+ }\n+ }\n+ Some(Err(e)) => Some(Err(e.into())),\n+ None => None,\n+ }\n+ }\n+}\n+\n+/// Iterator over keys in a DurableMap\n+pub struct KeyIterator<'a, K, V> {\n+ inner: rocksdb::DBIterator<'a>,\n+ prefix: Vec,\n+ _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl<'a, K, V> Iterator for KeyIterator<'a, K, V>\n+where\n+ K: for<'de> Deserialize<'de>,\n+{\n+ type Item = Result;\n+ \n+ fn next(&mut self) -> Option {\n+ match self.inner.next() {\n+ Some(Ok((db_key, _))) => {\n+ // Check if we're still within our prefix\n+ if !db_key.starts_with(&self.prefix) {\n+ return None;\n+ }\n+ \n+ // Extract the key part (skip prefix)\n+ let key_start = self.prefix.len();\n+ let key_bytes = &db_key[key_start..];\n+ \n+ // Deserialize key\n+ match bincode::deserialize(key_bytes) {\n+ Ok(key) => Some(Ok(key)),\n+ Err(e) => Some(Err(e.into())),\n+ }\n+ }\n+ Some(Err(e)) => Some(Err(e.into())),\n+ None => None,\n+ }\n+ }\n+}\n+\n+/// Iterator over values in a DurableMap\n+pub struct ValueIterator<'a, K, V> {\n+ inner: rocksdb::DBIterator<'a>,\n+ prefix: Vec,\n+ _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl<'a, K, V> Iterator for ValueIterator<'a, K, V>\n+where\n+ V: for<'de> Deserialize<'de>,\n+{\n+ type Item = Result;\n+ \n+ fn next(&mut self) -> Option {\n+ match self.inner.next() {\n+ Some(Ok((db_key, value_bytes))) => {\n+ // Check if we're still within our prefix\n+ if !db_key.starts_with(&self.prefix) {\n+ return None;\n+ }\n+ \n+ // Deserialize value\n+ match bincode::deserialize(&value_bytes) {\n+ Ok(value) => Some(Ok(value)),\n+ Err(e) => Some(Err(e.into())),\n+ }\n+ }\n+ Some(Err(e)) => Some(Err(e.into())),\n+ None => None,\n+ }\n+ }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+ use super::*;\n+ use tempfile::TempDir;\n+ use std::collections::HashMap;\n+ \n+ fn setup_test_db() -> (TempDir, Db) {\n+ let temp_dir = TempDir::new().unwrap();\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ (temp_dir, db)\n+ }\n+ \n+ #[test]\n+ fn test_insert_and_get() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"test_map\").unwrap();\n+ \n+ // Insert some values\n+ assert_eq!(map.insert(\"one\".to_string(), 1).unwrap(), None);\n+ assert_eq!(map.insert(\"two\".to_string(), 2).unwrap(), None);\n+ assert_eq!(map.insert(\"three\".to_string(), 3).unwrap(), None);\n+ \n+ // Get values\n+ assert_eq!(map.get(&\"one\".to_string()).unwrap(), Some(1));\n+ assert_eq!(map.get(&\"two\".to_string()).unwrap(), Some(2));\n+ assert_eq!(map.get(&\"three\".to_string()).unwrap(), Some(3));\n+ assert_eq!(map.get(&\"four\".to_string()).unwrap(), None);\n+ \n+ // Update existing value\n+ assert_eq!(map.insert(\"two\".to_string(), 22).unwrap(), Some(2));\n+ assert_eq!(map.get(&\"two\".to_string()).unwrap(), Some(22));\n+ }\n+ \n+ #[test]\n+ fn test_remove() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"remove_map\").unwrap();\n+ \n+ // Insert and remove\n+ map.insert(\"key\".to_string(), \"value\".to_string()).unwrap();\n+ assert_eq!(map.remove(&\"key\".to_string()).unwrap(), Some(\"value\".to_string()));\n+ assert_eq!(map.remove(&\"key\".to_string()).unwrap(), None);\n+ assert_eq!(map.get(&\"key\".to_string()).unwrap(), None);\n+ }\n+ \n+ #[test]\n+ fn test_contains_key() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"contains_map\").unwrap();\n+ \n+ map.insert(42, \"answer\".to_string()).unwrap();\n+ \n+ assert!(map.contains_key(&42).unwrap());\n+ assert!(!map.contains_key(&43).unwrap());\n+ }\n+ \n+ #[test]\n+ fn test_len_and_clear() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"len_map\").unwrap();\n+ \n+ // Empty map\n+ assert_eq!(map.len().unwrap(), 0);\n+ assert!(map.is_empty().unwrap());\n+ \n+ // Add items\n+ for i in 0..10 {\n+ map.insert(i, i * 2).unwrap();\n+ }\n+ assert_eq!(map.len().unwrap(), 10);\n+ assert!(!map.is_empty().unwrap());\n+ \n+ // Clear\n+ map.clear().unwrap();\n+ assert_eq!(map.len().unwrap(), 0);\n+ assert!(map.is_empty().unwrap());\n+ }\n+ \n+ #[test]\n+ fn test_persistence() {\n+ let (temp_dir, db) = setup_test_db();\n+ \n+ // Create and populate map\n+ {\n+ let mut map = DurableMap::>::new(&db, \"persist_map\").unwrap();\n+ map.insert(\"binary\".to_string(), vec![1, 2, 3, 4, 5]).unwrap();\n+ map.insert(\"data\".to_string(), vec![10, 20, 30]).unwrap();\n+ }\n+ \n+ // Drop the database\n+ drop(db);\n+ \n+ // Reopen and verify data persists\n+ {\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ let map = DurableMap::>::new(&db, \"persist_map\").unwrap();\n+ \n+ assert_eq!(map.get(&\"binary\".to_string()).unwrap(), Some(vec![1, 2, 3, 4, 5]));\n+ assert_eq!(map.get(&\"data\".to_string()).unwrap(), Some(vec![10, 20, 30]));\n+ assert_eq!(map.len().unwrap(), 2);\n+ }\n+ }\n+ \n+ #[test]\n+ fn test_iteration() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"iter_map\").unwrap();\n+ \n+ // Insert data\n+ let data = vec![\n+ (\"apple\".to_string(), 1),\n+ (\"banana\".to_string(), 2),\n+ (\"cherry\".to_string(), 3),\n+ ];\n+ \n+ for (k, v) in &data {\n+ map.insert(k.clone(), *v).unwrap();\n+ }\n+ \n+ // Test iter()\n+ let mut items = map.to_vec().unwrap();\n+ items.sort_by_key(|(k, _)| k.clone());\n+ assert_eq!(items, data);\n+ \n+ // Test keys()\n+ let mut keys = map.keys_vec().unwrap();\n+ keys.sort();\n+ assert_eq!(keys, vec![\"apple\", \"banana\", \"cherry\"]);\n+ \n+ // Test values()\n+ let mut values = map.values_vec().unwrap();\n+ values.sort();\n+ assert_eq!(values, vec![1, 2, 3]);\n+ }\n+ \n+ #[test]\n+ fn test_extend() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"extend_map\").unwrap();\n+ \n+ // Extend from iterator\n+ let data: HashMap = vec![\n+ (1, \"one\".to_string()),\n+ (2, \"two\".to_string()),\n+ (3, \"three\".to_string()),\n+ ].into_iter().collect();\n+ \n+ map.extend(data.clone()).unwrap();\n+ \n+ // Verify all items were inserted\n+ for (k, v) in data {\n+ assert_eq!(map.get(&k).unwrap(), Some(v));\n+ }\n+ assert_eq!(map.len().unwrap(), 3);\n+ }\n+ \n+ #[test]\n+ fn test_complex_keys() {\n+ use serde::{Serialize, Deserialize};\n+ \n+ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]\n+ struct ComplexKey {\n+ id: u64,\n+ name: String,\n+ }\n+ \n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"complex_map\").unwrap();\n+ \n+ let key1 = ComplexKey { id: 1, name: \"first\".to_string() };\n+ let key2 = ComplexKey { id: 2, name: \"second\".to_string() };\n+ \n+ map.insert(key1.clone(), \"value1\".to_string()).unwrap();\n+ map.insert(key2.clone(), \"value2\".to_string()).unwrap();\n+ \n+ assert_eq!(map.get(&key1).unwrap(), Some(\"value1\".to_string()));\n+ assert_eq!(map.get(&key2).unwrap(), Some(\"value2\".to_string()));\n+ }\n+ \n+ #[test]\n+ fn test_multiple_maps_same_db() {\n+ let (_temp, db) = setup_test_db();\n+ \n+ let mut map1 = DurableMap::::new(&db, \"map1\").unwrap();\n+ let mut map2 = DurableMap::::new(&db, \"map2\").unwrap();\n+ \n+ // Insert different data\n+ map1.insert(\"shared_key\".to_string(), 100).unwrap();\n+ map2.insert(\"shared_key\".to_string(), 200).unwrap();\n+ \n+ // Verify isolation\n+ assert_eq!(map1.get(&\"shared_key\".to_string()).unwrap(), Some(100));\n+ assert_eq!(map2.get(&\"shared_key\".to_string()).unwrap(), Some(200));\n+ }\n+ \n+ #[test]\n+ fn test_streaming_iterators() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"stream_map\").unwrap();\n+ \n+ // Insert test data\n+ let data = vec![\n+ (\"alice\".to_string(), 100),\n+ (\"bob\".to_string(), 200),\n+ (\"charlie\".to_string(), 300),\n+ ];\n+ \n+ for (k, v) in &data {\n+ map.insert(k.clone(), *v).unwrap();\n+ }\n+ \n+ // Test streaming iteration\n+ let mut collected = Vec::new();\n+ for item in map.iter() {\n+ let (k, v) = item.unwrap();\n+ collected.push((k, v));\n+ }\n+ collected.sort_by_key(|(k, _)| k.clone());\n+ assert_eq!(collected, data);\n+ \n+ // Test keys iterator\n+ let mut keys = Vec::new();\n+ for key in map.keys() {\n+ keys.push(key.unwrap());\n+ }\n+ keys.sort();\n+ assert_eq!(keys, vec![\"alice\", \"bob\", \"charlie\"]);\n+ \n+ // Test values iterator\n+ let mut values = Vec::new();\n+ for value in map.values() {\n+ values.push(value.unwrap());\n+ }\n+ values.sort();\n+ assert_eq!(values, vec![100, 200, 300]);\n+ \n+ // Test that iterators properly handle prefix boundaries\n+ let mut map2 = DurableMap::::new(&db, \"stream_map2\").unwrap();\n+ map2.insert(\"dave\".to_string(), 400).unwrap();\n+ \n+ // Each iterator should only see its own data\n+ let collected1: Vec<_> = map.iter().map(Result::unwrap).collect();\n+ let collected2: Vec<_> = map2.iter().map(Result::unwrap).collect();\n+ \n+ assert_eq!(collected1.len(), 3);\n+ assert_eq!(collected2.len(), 1);\n+ assert_eq!(collected2[0], (\"dave\".to_string(), 400));\n+ }\n+ \n+ #[test]\n+ fn test_metadata_length_tracking() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"length_map\").unwrap();\n+ \n+ // Empty map\n+ assert_eq!(map.len().unwrap(), 0);\n+ assert!(map.is_empty().unwrap());\n+ \n+ // Insert operations should update length\n+ map.insert(\"key1\".to_string(), \"value1\".to_string()).unwrap();\n+ assert_eq!(map.len().unwrap(), 1);\n+ \n+ map.insert(\"key2\".to_string(), \"value2\".to_string()).unwrap();\n+ assert_eq!(map.len().unwrap(), 2);\n+ \n+ // Updating existing key should not change length\n+ map.insert(\"key1\".to_string(), \"new_value1\".to_string()).unwrap();\n+ assert_eq!(map.len().unwrap(), 2);\n+ \n+ // Remove operations should update length\n+ map.remove(&\"key1\".to_string()).unwrap();\n+ assert_eq!(map.len().unwrap(), 1);\n+ \n+ // Removing non-existent key should not change length\n+ map.remove(&\"non_existent\".to_string()).unwrap();\n+ assert_eq!(map.len().unwrap(), 1);\n+ \n+ // Extend should update length correctly\n+ let data = vec![\n+ (\"key3\".to_string(), \"value3\".to_string()),\n+ (\"key4\".to_string(), \"value4\".to_string()),\n+ (\"key5\".to_string(), \"value5\".to_string()),\n+ ];\n+ map.extend(data).unwrap();\n+ assert_eq!(map.len().unwrap(), 4); // key2 + 3 new keys\n+ \n+ // Extend with existing keys should only count new ones\n+ let mixed_data = vec![\n+ (\"key2\".to_string(), \"updated_value2\".to_string()), // existing\n+ (\"key6\".to_string(), \"value6\".to_string()), // new\n+ ];\n+ map.extend(mixed_data).unwrap();\n+ assert_eq!(map.len().unwrap(), 5); // only key6 was new\n+ \n+ // Clear should reset length to 0\n+ map.clear().unwrap();\n+ assert_eq!(map.len().unwrap(), 0);\n+ assert!(map.is_empty().unwrap());\n+ }\n+ \n+ #[test]\n+ fn test_put_method() {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"put_map\").unwrap();\n+ \n+ // Put new entries\n+ map.put(\"a\".to_string(), 1).unwrap();\n+ map.put(\"b\".to_string(), 2).unwrap();\n+ map.put(\"c\".to_string(), 3).unwrap();\n+ \n+ // Verify entries exist and length is correct\n+ assert_eq!(map.get(&\"a\".to_string()).unwrap(), Some(1));\n+ assert_eq!(map.get(&\"b\".to_string()).unwrap(), Some(2));\n+ assert_eq!(map.get(&\"c\".to_string()).unwrap(), Some(3));\n+ assert_eq!(map.len().unwrap(), 3);\n+ \n+ // Update existing entry with put\n+ map.put(\"b\".to_string(), 20).unwrap();\n+ assert_eq!(map.get(&\"b\".to_string()).unwrap(), Some(20));\n+ assert_eq!(map.len().unwrap(), 3); // Length should not change\n+ \n+ // Compare put vs insert performance characteristics\n+ // put() doesn't return old value but is more efficient\n+ map.put(\"d\".to_string(), 4).unwrap();\n+ assert_eq!(map.len().unwrap(), 4);\n+ \n+ // insert() returns old value\n+ let old = map.insert(\"d\".to_string(), 40).unwrap();\n+ assert_eq!(old, Some(4));\n+ assert_eq!(map.len().unwrap(), 4);\n+ }\n+}\n+\n+#[cfg(all(test, not(miri)))]\n+mod proptests {\n+ use super::*;\n+ use proptest::prelude::*;\n+ use tempfile::TempDir;\n+ use std::collections::HashMap;\n+ \n+ fn setup_test_db() -> (TempDir, Db) {\n+ let temp_dir = TempDir::new().unwrap();\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ (temp_dir, db)\n+ }\n+ \n+ proptest! {\n+ #[test]\n+ fn prop_insert_get_consistency(data: HashMap) {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"prop_map\").unwrap();\n+ \n+ // Insert all pairs\n+ for (k, v) in &data {\n+ map.insert(k.clone(), *v).unwrap();\n+ }\n+ \n+ // Verify all can be retrieved\n+ for (k, v) in &data {\n+ prop_assert_eq!(map.get(k).unwrap(), Some(*v));\n+ }\n+ \n+ // Verify length\n+ prop_assert_eq!(map.len().unwrap(), data.len());\n+ }\n+ \n+ #[test]\n+ fn prop_remove_consistency(data: HashMap) {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"remove_map\").unwrap();\n+ \n+ // Insert all\n+ map.extend(data.clone()).unwrap();\n+ \n+ // Remove all and verify\n+ for (k, v) in data {\n+ prop_assert_eq!(map.remove(&k).unwrap(), Some(v));\n+ prop_assert_eq!(map.remove(&k).unwrap(), None);\n+ prop_assert!(!map.contains_key(&k).unwrap());\n+ }\n+ \n+ prop_assert!(map.is_empty().unwrap());\n+ }\n+ \n+ #[test]\n+ fn prop_clear_makes_empty(data: HashMap) {\n+ let (_temp, db) = setup_test_db();\n+ let mut map = DurableMap::::new(&db, \"clear_map\").unwrap();\n+ \n+ map.extend(data).unwrap();\n+ map.clear().unwrap();\n+ \n+ prop_assert_eq!(map.len().unwrap(), 0);\n+ prop_assert!(map.is_empty().unwrap());\n+ prop_assert_eq!(map.to_vec().unwrap(), vec![]);\n+ }\n+ }\n+ \n+ #[test]\n+ fn test_nested_collections() {\n+ use crate::DurableVec;\n+ \n+ let (_temp, db) = setup_test_db();\n+ \n+ // Create a map where values are DurableVec\n+ let users_posts: DurableMap> = DurableMap::new_nested(&db, \"user_posts\");\n+ \n+ // Test creating nested collections through the entry API\n+ let mut alice_posts = users_posts.entry(\"alice\".to_string()).unwrap().or_default().unwrap();\n+ alice_posts.push(101).unwrap();\n+ alice_posts.push(102).unwrap();\n+ alice_posts.push(103).unwrap();\n+ \n+ // Test accessing the same collection again\n+ let alice_posts_again = users_posts.entry(\"alice\".to_string()).unwrap().or_default().unwrap();\n+ assert_eq!(alice_posts_again.len().unwrap(), 3);\n+ assert_eq!(alice_posts_again.get(0).unwrap(), Some(101));\n+ assert_eq!(alice_posts_again.get(1).unwrap(), Some(102));\n+ assert_eq!(alice_posts_again.get(2).unwrap(), Some(103));\n+ \n+ // Test creating a different nested collection\n+ let mut bob_posts = users_posts.entry(\"bob\".to_string()).unwrap().or_default().unwrap();\n+ bob_posts.push(201).unwrap();\n+ bob_posts.push(202).unwrap();\n+ \n+ // Verify isolation between nested collections\n+ assert_eq!(alice_posts_again.len().unwrap(), 3);\n+ assert_eq!(bob_posts.len().unwrap(), 2);\n+ \n+ // Test chained calls\n+ users_posts.entry(\"charlie\".to_string()).unwrap().or_default().unwrap().push(301).unwrap();\n+ let charlie_posts = users_posts.entry(\"charlie\".to_string()).unwrap().or_default().unwrap();\n+ assert_eq!(charlie_posts.len().unwrap(), 1);\n+ assert_eq!(charlie_posts.get(0).unwrap(), Some(301));\n+ }\n+ \n+ // Note: Nested DurableMap-in-DurableMap requires implementing Serialize/Deserialize \n+ // for DurableMap, which is not straightforward since it contains database handles.\n+ // For now, let's focus on the more common case of Map-to-Vec nesting.\n+ \n+ // Deep nesting with Map -> Map -> Vec also requires DurableMap serialization\n+ // Let's skip this for now and focus on the fundamental Map -> Vec case\n+ \n+ #[test]\n+ fn test_nested_collection_persistence() {\n+ use crate::DurableVec;\n+ \n+ let (temp_dir, db) = setup_test_db();\n+ \n+ // Create nested structure and populate it\n+ {\n+ let users_data: DurableMap> = DurableMap::new_nested(&db, \"users\");\n+ let mut user1_data = users_data.entry(\"user1\".to_string()).unwrap().or_default().unwrap();\n+ user1_data.push(\"data1\".to_string()).unwrap();\n+ user1_data.push(\"data2\".to_string()).unwrap();\n+ \n+ let mut user2_data = users_data.entry(\"user2\".to_string()).unwrap().or_default().unwrap();\n+ user2_data.push(\"other_data\".to_string()).unwrap();\n+ }\n+ \n+ // Drop the database\n+ drop(db);\n+ \n+ // Reopen and verify persistence\n+ {\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ let users_data: DurableMap> = DurableMap::new_nested(&db, \"users\");\n+ \n+ let user1_data = users_data.entry(\"user1\".to_string()).unwrap().or_default().unwrap();\n+ assert_eq!(user1_data.len().unwrap(), 2);\n+ assert_eq!(user1_data.get(0).unwrap(), Some(\"data1\".to_string()));\n+ assert_eq!(user1_data.get(1).unwrap(), Some(\"data2\".to_string()));\n+ \n+ let user2_data = users_data.entry(\"user2\".to_string()).unwrap().or_default().unwrap();\n+ assert_eq!(user2_data.len().unwrap(), 1);\n+ assert_eq!(user2_data.get(0).unwrap(), Some(\"other_data\".to_string()));\n+ }\n+ }\n+} \n\\ No newline at end of file\ndiff --git a/durable/src/vec.rs b/durable/src/vec.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..28d08fd0786df241aaf9c01b279708e547e4c034\n--- /dev/null\n+++ b/durable/src/vec.rs\n@@ -0,0 +1,658 @@\n+use crate::{Db, Result, DurableError, DurableCollection};\n+use rocksdb::WriteBatch;\n+use serde::{Serialize, Deserialize};\n+use std::marker::PhantomData;\n+\n+/// A persistent vector backed by RocksDB\n+pub struct DurableVec {\n+ db: Db,\n+ prefix: Vec,\n+ _phantom: PhantomData,\n+}\n+\n+impl DurableVec \n+where \n+ T: Serialize + for<'de> Deserialize<'de>\n+{\n+ /// Create a new DurableVec with the given name\n+ pub fn new(db: &Db, name: &str) -> Result {\n+ let prefix = format!(\"vec:{}\", name).into_bytes();\n+ \n+ Ok(DurableVec {\n+ db: db.clone(),\n+ prefix,\n+ _phantom: PhantomData,\n+ })\n+ }\n+ \n+ /// Create a new DurableVec from a prefix (used for nested collections)\n+ pub fn from_prefix(db: Db, prefix: Vec) -> Self {\n+ Self {\n+ db,\n+ prefix,\n+ _phantom: PhantomData,\n+ }\n+ }\n+ \n+ /// Get the length of the vector\n+ pub fn len(&self) -> Result {\n+ let key = self.meta_key(\"len\");\n+ match self.db.rocks().get(&key)? {\n+ Some(bytes) => {\n+ if bytes.len() != 8 {\n+ return Err(DurableError::Corruption(\"Invalid length bytes size\".into()));\n+ }\n+ let len_bytes: [u8; 8] = bytes[..8].try_into()\n+ .map_err(|_| DurableError::Corruption(\"Invalid length bytes\".into()))?;\n+ Ok(u64::from_le_bytes(len_bytes) as usize)\n+ }\n+ None => Ok(0),\n+ }\n+ }\n+ \n+ /// Check if the vector is empty\n+ pub fn is_empty(&self) -> Result {\n+ Ok(self.len()? == 0)\n+ }\n+ \n+ /// Push an element to the end of the vector\n+ pub fn push(&mut self, value: T) -> Result<()> {\n+ let len = self.len()?;\n+ let mut batch = WriteBatch::default();\n+ \n+ // Serialize the value\n+ let value_bytes = bincode::serialize(&value)?;\n+ \n+ // Write the element\n+ let elem_key = self.element_key(len);\n+ batch.put(&elem_key, &value_bytes);\n+ \n+ // Update the length\n+ let new_len = (len + 1) as u64;\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &new_len.to_le_bytes());\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(())\n+ }\n+ \n+ /// Get an element at the given index\n+ pub fn get(&self, index: usize) -> Result> {\n+ let len = self.len()?;\n+ if index >= len {\n+ return Ok(None);\n+ }\n+ \n+ let key = self.element_key(index);\n+ match self.db.rocks().get(&key)? {\n+ Some(bytes) => {\n+ let value = bincode::deserialize(&bytes)?;\n+ Ok(Some(value))\n+ }\n+ None => Err(DurableError::Corruption(\n+ format!(\"Element at index {} not found but index < len\", index)\n+ )),\n+ }\n+ }\n+ \n+ /// Clear all elements from the vector\n+ pub fn clear(&mut self) -> Result<()> {\n+ let len = self.len()?;\n+ let mut batch = WriteBatch::default();\n+ \n+ // Delete all elements\n+ for i in 0..len {\n+ let key = self.element_key(i);\n+ batch.delete(&key);\n+ }\n+ \n+ // Delete the length meta key\n+ let len_key = self.meta_key(\"len\");\n+ batch.delete(&len_key);\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(())\n+ }\n+ \n+ /// Create a streaming iterator over the vector\n+ pub fn iter(&self) -> Result> + '_> {\n+ let prefix = self.element_prefix();\n+ let iter = self.db.rocks().iterator(rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward));\n+ \n+ Ok(VecIterator {\n+ inner: iter,\n+ prefix,\n+ _phantom: PhantomData,\n+ })\n+ }\n+ \n+ /// Convert the entire vector to a Vec in memory\n+ /// \n+ /// Note: This loads the entire collection into memory. For large collections,\n+ /// prefer using `iter()` which streams elements.\n+ pub fn to_vec(&self) -> Result> {\n+ let len = self.len()?;\n+ let mut result = Vec::with_capacity(len);\n+ \n+ for item in self.iter()? {\n+ result.push(item?);\n+ }\n+ \n+ Ok(result)\n+ }\n+ \n+ /// Push multiple elements in a single batch\n+ pub fn extend(&mut self, iter: I) -> Result<()>\n+ where\n+ I: IntoIterator\n+ {\n+ let mut batch = WriteBatch::default();\n+ let mut len = self.len()?;\n+ \n+ for value in iter {\n+ let value_bytes = bincode::serialize(&value)?;\n+ let elem_key = self.element_key(len);\n+ batch.put(&elem_key, &value_bytes);\n+ len += 1;\n+ }\n+ \n+ // Update length\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &(len as u64).to_le_bytes());\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(())\n+ }\n+ \n+ /// Remove and return the last element\n+ pub fn pop(&mut self) -> Result> {\n+ let len = self.len()?;\n+ if len == 0 {\n+ return Ok(None);\n+ }\n+ \n+ let last_idx = len - 1;\n+ let value = self.get(last_idx)?;\n+ \n+ let mut batch = WriteBatch::default();\n+ \n+ // Delete the last element\n+ let elem_key = self.element_key(last_idx);\n+ batch.delete(&elem_key);\n+ \n+ // Update length\n+ let len_key = self.meta_key(\"len\");\n+ batch.put(&len_key, &(last_idx as u64).to_le_bytes());\n+ \n+ // Commit atomically\n+ self.db.rocks().write(batch)?;\n+ self.db.rocks().flush_wal(true)?;\n+ \n+ Ok(value)\n+ }\n+ \n+ // Helper methods\n+ \n+ fn element_key(&self, index: usize) -> Vec {\n+ let mut key = self.prefix.clone();\n+ key.push(b':');\n+ key.extend_from_slice(&(index as u64).to_be_bytes());\n+ key\n+ }\n+ \n+ fn meta_key(&self, meta_type: &str) -> Vec {\n+ let mut key = self.prefix.clone();\n+ key.extend_from_slice(b\":__meta:\");\n+ key.extend_from_slice(meta_type.as_bytes());\n+ key\n+ }\n+ \n+ fn element_prefix(&self) -> Vec {\n+ let mut prefix = self.prefix.clone();\n+ prefix.push(b':');\n+ prefix\n+ }\n+}\n+\n+// Implement the DurableCollection trait for DurableVec\n+impl DurableCollection for DurableVec \n+where\n+ T: Serialize + for<'de> Deserialize<'de>\n+{\n+ fn from_prefix(db: Db, prefix: Vec) -> Self {\n+ DurableVec::from_prefix(db, prefix)\n+ }\n+}\n+\n+/// Iterator over a DurableVec\n+pub struct VecIterator<'a, T> {\n+ inner: rocksdb::DBIterator<'a>,\n+ prefix: Vec,\n+ _phantom: PhantomData,\n+}\n+\n+impl<'a, T> Iterator for VecIterator<'a, T>\n+where\n+ T: for<'de> Deserialize<'de>\n+{\n+ type Item = Result;\n+ \n+ fn next(&mut self) -> Option {\n+ loop {\n+ match self.inner.next() {\n+ Some(Ok((key, value))) => {\n+ // Check if we're still within our prefix\n+ if !key.starts_with(&self.prefix) {\n+ return None;\n+ }\n+ \n+ // Check if this is a meta key (skip it)\n+ // The key pattern is: prefix:element_index or prefix:__meta:type\n+ // We want to skip any key that contains \"__meta:\"\n+ if key.windows(7).any(|w| w == b\"__meta:\") {\n+ continue; // Skip this key and try the next one\n+ }\n+ \n+ // Deserialize the value\n+ match bincode::deserialize(&value) {\n+ Ok(item) => return Some(Ok(item)),\n+ Err(e) => return Some(Err(e.into())),\n+ }\n+ }\n+ Some(Err(e)) => return Some(Err(e.into())),\n+ None => return None,\n+ }\n+ }\n+ }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+ use super::*;\n+ use tempfile::TempDir;\n+ \n+ fn setup_test_db() -> (TempDir, Db) {\n+ let temp_dir = TempDir::new().unwrap();\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ (temp_dir, db)\n+ }\n+ \n+ #[test]\n+ fn test_push_and_get() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"test_vec\").unwrap();\n+ \n+ // Push some values\n+ vec.push(\"first\".to_string()).unwrap();\n+ vec.push(\"second\".to_string()).unwrap();\n+ vec.push(\"third\".to_string()).unwrap();\n+ \n+ // Check length\n+ assert_eq!(vec.len().unwrap(), 3);\n+ \n+ // Get values\n+ assert_eq!(vec.get(0).unwrap(), Some(\"first\".to_string()));\n+ assert_eq!(vec.get(1).unwrap(), Some(\"second\".to_string()));\n+ assert_eq!(vec.get(2).unwrap(), Some(\"third\".to_string()));\n+ assert_eq!(vec.get(3).unwrap(), None);\n+ }\n+ \n+ #[test]\n+ fn test_persistence() {\n+ let (temp_dir, db) = setup_test_db();\n+ \n+ // Create and populate vector\n+ {\n+ let mut vec = DurableVec::::new(&db, \"persist_vec\").unwrap();\n+ vec.push(42).unwrap();\n+ vec.push(100).unwrap();\n+ vec.push(-7).unwrap();\n+ }\n+ \n+ // Drop the database\n+ drop(db);\n+ \n+ // Reopen and verify data persists\n+ {\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ let vec = DurableVec::::new(&db, \"persist_vec\").unwrap();\n+ \n+ assert_eq!(vec.len().unwrap(), 3);\n+ assert_eq!(vec.get(0).unwrap(), Some(42));\n+ assert_eq!(vec.get(1).unwrap(), Some(100));\n+ assert_eq!(vec.get(2).unwrap(), Some(-7));\n+ }\n+ }\n+ \n+ #[test]\n+ fn test_clear() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"clear_vec\").unwrap();\n+ \n+ // Add some elements\n+ vec.extend(vec![1, 2, 3, 4, 5]).unwrap();\n+ assert_eq!(vec.len().unwrap(), 5);\n+ \n+ // Clear\n+ vec.clear().unwrap();\n+ assert_eq!(vec.len().unwrap(), 0);\n+ assert!(vec.is_empty().unwrap());\n+ \n+ // Should be able to push again\n+ vec.push(42).unwrap();\n+ assert_eq!(vec.len().unwrap(), 1);\n+ assert_eq!(vec.get(0).unwrap(), Some(42));\n+ }\n+ \n+ #[test]\n+ fn test_pop() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"pop_vec\").unwrap();\n+ \n+ // Empty pop\n+ assert_eq!(vec.pop().unwrap(), None);\n+ \n+ // Push and pop\n+ vec.push(\"a\".to_string()).unwrap();\n+ vec.push(\"b\".to_string()).unwrap();\n+ vec.push(\"c\".to_string()).unwrap();\n+ \n+ assert_eq!(vec.pop().unwrap(), Some(\"c\".to_string()));\n+ assert_eq!(vec.len().unwrap(), 2);\n+ assert_eq!(vec.pop().unwrap(), Some(\"b\".to_string()));\n+ assert_eq!(vec.len().unwrap(), 1);\n+ assert_eq!(vec.pop().unwrap(), Some(\"a\".to_string()));\n+ assert_eq!(vec.len().unwrap(), 0);\n+ assert_eq!(vec.pop().unwrap(), None);\n+ }\n+ \n+ #[test]\n+ fn test_iteration() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"iter_vec\").unwrap();\n+ \n+ // Add elements\n+ let values = vec![10, 20, 30, 40, 50];\n+ vec.extend(values.clone()).unwrap();\n+ \n+ // Iterate and collect\n+ let collected = vec.to_vec().unwrap();\n+ \n+ assert_eq!(collected, values);\n+ }\n+ \n+ #[test]\n+ fn test_extend() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"extend_vec\").unwrap();\n+ \n+ // Extend with iterator\n+ vec.extend(vec![\"a\", \"b\", \"c\"].into_iter().map(String::from)).unwrap();\n+ assert_eq!(vec.len().unwrap(), 3);\n+ \n+ // Extend again\n+ vec.extend(vec![\"d\", \"e\"].into_iter().map(String::from)).unwrap();\n+ assert_eq!(vec.len().unwrap(), 5);\n+ \n+ // Verify all elements\n+ let all = vec.to_vec().unwrap();\n+ assert_eq!(all, vec![\"a\", \"b\", \"c\", \"d\", \"e\"]);\n+ }\n+ \n+ #[test]\n+ fn test_large_dataset() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"large_vec\").unwrap();\n+ \n+ // Push many elements\n+ let count = 1000;\n+ for i in 0..count {\n+ vec.push(i).unwrap();\n+ }\n+ \n+ assert_eq!(vec.len().unwrap(), count as usize);\n+ \n+ // Verify some random accesses\n+ assert_eq!(vec.get(0).unwrap(), Some(0));\n+ assert_eq!(vec.get(500).unwrap(), Some(500));\n+ assert_eq!(vec.get(999).unwrap(), Some(999));\n+ assert_eq!(vec.get(1000).unwrap(), None);\n+ \n+ // Verify iteration count\n+ let all_values = vec.to_vec().unwrap();\n+ assert_eq!(all_values.len(), count as usize);\n+ }\n+ \n+ #[test] \n+ fn test_complex_types() {\n+ use serde::{Serialize, Deserialize};\n+ \n+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n+ struct User {\n+ id: u64,\n+ name: String,\n+ email: String,\n+ active: bool,\n+ }\n+ \n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"users\").unwrap();\n+ \n+ let user1 = User {\n+ id: 1,\n+ name: \"Alice\".to_string(),\n+ email: \"alice@example.com\".to_string(),\n+ active: true,\n+ };\n+ \n+ let user2 = User {\n+ id: 2,\n+ name: \"Bob\".to_string(),\n+ email: \"bob@example.com\".to_string(),\n+ active: false,\n+ };\n+ \n+ vec.push(user1.clone()).unwrap();\n+ vec.push(user2.clone()).unwrap();\n+ \n+ assert_eq!(vec.get(0).unwrap(), Some(user1));\n+ assert_eq!(vec.get(1).unwrap(), Some(user2));\n+ }\n+ \n+ #[test]\n+ fn test_empty_vec_operations() {\n+ let (_temp, db) = setup_test_db();\n+ let vec = DurableVec::::new(&db, \"empty_vec\").unwrap();\n+ \n+ // Test operations on empty vec\n+ assert_eq!(vec.len().unwrap(), 0);\n+ assert!(vec.is_empty().unwrap());\n+ assert_eq!(vec.get(0).unwrap(), None);\n+ assert_eq!(vec.get(100).unwrap(), None);\n+ assert_eq!(vec.to_vec().unwrap(), Vec::::new());\n+ }\n+ \n+ #[test]\n+ fn test_multiple_vecs_same_db() {\n+ let (_temp, db) = setup_test_db();\n+ \n+ // Create multiple vectors with different names\n+ let mut vec1 = DurableVec::::new(&db, \"vec1\").unwrap();\n+ let mut vec2 = DurableVec::::new(&db, \"vec2\").unwrap();\n+ \n+ // Push different data to each\n+ vec1.push(\"vec1_data\".to_string()).unwrap();\n+ vec2.push(\"vec2_data\".to_string()).unwrap();\n+ \n+ // Verify they don't interfere\n+ assert_eq!(vec1.get(0).unwrap(), Some(\"vec1_data\".to_string()));\n+ assert_eq!(vec2.get(0).unwrap(), Some(\"vec2_data\".to_string()));\n+ assert_eq!(vec1.len().unwrap(), 1);\n+ assert_eq!(vec2.len().unwrap(), 1);\n+ }\n+ \n+ #[test]\n+ fn test_batch_atomicity() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"batch_vec\").unwrap();\n+ \n+ // Add initial data\n+ vec.push(1).unwrap();\n+ vec.push(2).unwrap();\n+ vec.push(3).unwrap();\n+ \n+ // Verify initial state\n+ assert_eq!(vec.len().unwrap(), 3);\n+ \n+ // Clear should be atomic - either all elements deleted or none\n+ vec.clear().unwrap();\n+ assert_eq!(vec.len().unwrap(), 0);\n+ \n+ // Extend should be atomic - either all elements added or none\n+ vec.extend(vec![10, 20, 30, 40, 50]).unwrap();\n+ assert_eq!(vec.len().unwrap(), 5);\n+ let all = vec.to_vec().unwrap();\n+ assert_eq!(all, vec![10, 20, 30, 40, 50]);\n+ }\n+ \n+ #[test]\n+ fn test_unicode_strings() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"unicode_vec\").unwrap();\n+ \n+ let test_strings = vec![\n+ \"Hello, 世界!\".to_string(),\n+ \"🦀 Rust 🚀\".to_string(),\n+ \"Ñoño\".to_string(),\n+ \"🏴‍☠️ Pirates\".to_string(),\n+ ];\n+ \n+ vec.extend(test_strings.clone()).unwrap();\n+ \n+ let retrieved = vec.to_vec().unwrap();\n+ assert_eq!(retrieved, test_strings);\n+ }\n+ \n+ #[test]\n+ fn test_streaming_iterator() {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"stream_vec\").unwrap();\n+ \n+ // Add test data\n+ let values = vec![1, 2, 3, 4, 5];\n+ vec.extend(values.clone()).unwrap();\n+ \n+ // Test streaming iteration\n+ let mut collected = Vec::new();\n+ for item in vec.iter().unwrap() {\n+ collected.push(item.unwrap());\n+ }\n+ \n+ assert_eq!(collected, values);\n+ \n+ // Test that iterator properly handles prefix boundaries\n+ let mut vec2 = DurableVec::::new(&db, \"stream_vec2\").unwrap();\n+ vec2.extend(vec![10, 20, 30]).unwrap();\n+ \n+ // Each iterator should only see its own data\n+ let collected1: Vec<_> = vec.iter().unwrap().collect::>>().unwrap();\n+ let collected2: Vec<_> = vec2.iter().unwrap().collect::>>().unwrap();\n+ \n+ assert_eq!(collected1, values);\n+ assert_eq!(collected2, vec![10, 20, 30]);\n+ }\n+}\n+\n+#[cfg(all(test, not(miri)))] // Skip proptest under miri\n+mod proptests {\n+ use super::*;\n+ use proptest::prelude::*;\n+ use tempfile::TempDir;\n+ \n+ fn setup_test_db() -> (TempDir, Db) {\n+ let temp_dir = TempDir::new().unwrap();\n+ let db = Db::open(temp_dir.path()).unwrap();\n+ (temp_dir, db)\n+ }\n+ \n+ proptest! {\n+ #[test]\n+ fn prop_push_get_consistency(values: Vec) {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"prop_vec\").unwrap();\n+ \n+ // Push all values\n+ for value in &values {\n+ vec.push(*value).unwrap();\n+ }\n+ \n+ // Verify length\n+ prop_assert_eq!(vec.len().unwrap(), values.len());\n+ \n+ // Verify all values can be retrieved correctly\n+ for (i, expected) in values.iter().enumerate() {\n+ prop_assert_eq!(vec.get(i).unwrap(), Some(*expected));\n+ }\n+ }\n+ \n+ #[test]\n+ fn prop_extend_iter_roundtrip(values: Vec) {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"extend_vec\").unwrap();\n+ \n+ // Extend with all values\n+ vec.extend(values.clone()).unwrap();\n+ \n+ // Get back via iteration\n+ let retrieved = vec.to_vec().unwrap();\n+ \n+ prop_assert_eq!(retrieved, values);\n+ }\n+ \n+ #[test]\n+ fn prop_pop_removes_last(mut values: Vec) {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"pop_vec\").unwrap();\n+ \n+ // Add all values\n+ vec.extend(values.clone()).unwrap();\n+ \n+ // Pop values and verify\n+ while let Some(expected) = values.pop() {\n+ let popped = vec.pop().unwrap();\n+ prop_assert_eq!(popped, Some(expected));\n+ prop_assert_eq!(vec.len().unwrap(), values.len());\n+ }\n+ \n+ // Vector should be empty\n+ prop_assert!(vec.is_empty().unwrap());\n+ prop_assert_eq!(vec.pop().unwrap(), None);\n+ }\n+ \n+ #[test]\n+ fn prop_clear_makes_empty(values: Vec) {\n+ let (_temp, db) = setup_test_db();\n+ let mut vec = DurableVec::::new(&db, \"clear_vec\").unwrap();\n+ \n+ // Add values\n+ vec.extend(values).unwrap();\n+ \n+ // Clear\n+ vec.clear().unwrap();\n+ \n+ // Should be empty\n+ prop_assert_eq!(vec.len().unwrap(), 0);\n+ prop_assert!(vec.is_empty().unwrap());\n+ prop_assert_eq!(vec.get(0).unwrap(), None);\n+ }\n+ }\n+} \n\\ No newline at end of file\ndiff --git a/durable/todo.tdsl b/durable/todo.tdsl\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..6c9df9cbb25bb72d17aac07296a0c935ccbe89e7\n--- /dev/null\n+++ b/durable/todo.tdsl\n@@ -0,0 +1,4 @@\n+Add streaming iterators to avoid loading entire collections\n+Implement collection nesting (e.g., DurableMap>)\n+Add benchmarks to measure performance\n+Implement schema versioning and migrations\ndiff --git a/server/Cargo.toml b/server/Cargo.toml\nindex 6fb7bf52fa58f46f5e8fb0f7fd395247b57506d7..34672bd7acb9d165eea290e779c548fa4d2b8e3d 100644\n--- a/server/Cargo.toml\n+++ b/server/Cargo.toml\n@@ -22,6 +22,7 @@ async-stream = \"0.3\"\n futures-util = { version = \"0.3\", default-features = false, features = [\"std\"] }\n rand = \"0.8\"\n urlencoding = \"2\"\n+durable = { path = \"../durable\" }\n \n [dev-dependencies]\n reqwest = { version = \"0.12\", features = [\"json\"] }\ndiff --git a/server/src/entity_store.rs b/server/src/entity_store.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..be3300529e97801a4d42f06f3f669b78ddd6856d\n--- /dev/null\n+++ b/server/src/entity_store.rs\n@@ -0,0 +1,90 @@\n+//! Off-heap storage for full entity payloads (Reddit API JSON).\n+//!\n+//! Derived [`crate::reducer::EntityData`] stays in the in-memory tree; raw JSON\n+//! lives in RocksDB via the workspace `durable` crate.\n+\n+use std::path::Path;\n+use std::sync::{Arc, Mutex};\n+\n+use durable::{Db, DurableMap};\n+use serde_json::Value;\n+\n+use crate::path_types::ItemId;\n+\n+#[derive(Debug, thiserror::Error)]\n+pub enum EntityStoreError {\n+ #[error(\"durable error: {0}\")]\n+ Durable(#[from] durable::DurableError),\n+ #[error(\"json error: {0}\")]\n+ Json(#[from] serde_json::Error),\n+ #[error(\"io error: {0}\")]\n+ Io(#[from] std::io::Error),\n+ #[error(\"store lock poisoned\")]\n+ Poisoned,\n+}\n+\n+struct EntityStoreInner {\n+ _db: Db,\n+ payloads: DurableMap,\n+}\n+\n+/// Disk-backed map of entity id → raw JSON payload.\n+#[derive(Clone)]\n+pub struct EntityStore {\n+ inner: Arc>,\n+}\n+\n+impl EntityStore {\n+ /// Open (or create) the entity database under `dir`.\n+ pub fn open(dir: &Path) -> Result {\n+ std::fs::create_dir_all(dir)?;\n+ let db = Db::open(dir)?;\n+ let payloads = DurableMap::new(&db, \"entity_payloads\")?;\n+ Ok(Self {\n+ inner: Arc::new(Mutex::new(EntityStoreInner { _db: db, payloads })),\n+ })\n+ }\n+\n+ /// Persist a payload for `id` (overwrites any existing entry).\n+ pub fn put(&self, id: &ItemId, payload: &Value) -> Result<(), EntityStoreError> {\n+ let json = serde_json::to_string(payload)?;\n+ let mut inner = self\n+ .inner\n+ .lock()\n+ .map_err(|_| EntityStoreError::Poisoned)?;\n+ inner\n+ .payloads\n+ .put(id.as_str().to_string(), json)\n+ .map_err(EntityStoreError::from)\n+ }\n+\n+ /// Load a stored payload, if present.\n+ pub fn get(&self, id: &ItemId) -> Result, EntityStoreError> {\n+ let inner = self\n+ .inner\n+ .lock()\n+ .map_err(|_| EntityStoreError::Poisoned)?;\n+ match inner.payloads.get(&id.as_str().to_string())? {\n+ Some(json) => Ok(Some(serde_json::from_str(&json)?)),\n+ None => Ok(None),\n+ }\n+ }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+ use super::*;\n+ use serde_json::json;\n+\n+ #[test]\n+ fn round_trip_payload() {\n+ let tmp = tempfile::tempdir().unwrap();\n+ let store = EntityStore::open(tmp.path()).unwrap();\n+ let id = ItemId::parse(\"reddit.com/r/rust\").unwrap();\n+ let payload = json!({\"kind\": \"t5\", \"data\": {\"display_name\": \"rust\"}});\n+\n+ store.put(&id, &payload).unwrap();\n+ let loaded = store.get(&id).unwrap().unwrap();\n+ assert_eq!(loaded, payload);\n+ }\n+}\ndiff --git a/server/src/event_log.rs b/server/src/event_log.rs\nindex 03c86f7f028cdf167b4943951d724da486b1b82c..838c8830b265f28a03442681575271eef539508c 100644\n--- a/server/src/event_log.rs\n+++ b/server/src/event_log.rs\n@@ -7,6 +7,12 @@ use tokio::{\n \n use crate::events::Event;\n \n+#[derive(Debug, Default)]\n+pub struct ReplayStats {\n+ pub applied: usize,\n+ pub bad_lines: usize,\n+}\n+\n #[derive(Debug, thiserror::Error)]\n pub enum EventLogError {\n #[error(\"io error: {0}\")]\n@@ -51,30 +57,87 @@ impl EventLog {\n Ok(())\n }\n \n- pub async fn load_all(&self) -> Result<(Vec, Vec<(usize, String)>), EventLogError> {\n+ /// Stream the log one line at a time — parse each [`Event`], apply, drop before the next line.\n+ pub async fn replay(&self, mut apply: F) -> Result\n+ where\n+ F: FnMut(Event) -> Result<(), EventLogError>,\n+ {\n+ let mut stats = ReplayStats::default();\n if !fs::try_exists(&self.path).await? {\n- return Ok((vec![], vec![]));\n+ return Ok(stats);\n }\n \n let f = fs::File::open(&self.path).await?;\n let mut reader = BufReader::new(f).lines();\n \n- let mut events = Vec::new();\n- let mut bad_lines = Vec::new();\n-\n- let mut line_no: usize = 0;\n while let Some(line) = reader.next_line().await? {\n- line_no += 1;\n let trimmed = line.trim();\n if trimmed.is_empty() {\n continue;\n }\n match serde_json::from_str::(trimmed) {\n- Ok(ev) => events.push(ev),\n- Err(_) => bad_lines.push((line_no, line)),\n+ Ok(ev) => match apply(ev) {\n+ Ok(()) => stats.applied += 1,\n+ Err(e) => return Err(e),\n+ },\n+ Err(_) => stats.bad_lines += 1,\n }\n }\n \n- Ok((events, bad_lines))\n+ Ok(stats)\n+ }\n+\n+ /// Load every event into memory. Prefer [`Self::replay`] for startup.\n+ pub async fn load_all(&self) -> Result<(Vec, Vec<(usize, String)>), EventLogError> {\n+ let mut events = Vec::new();\n+ let stats = self\n+ .replay(|ev| {\n+ events.push(ev);\n+ Ok(())\n+ })\n+ .await?;\n+ let _ = stats;\n+ Ok((events, vec![]))\n+ }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+ use super::*;\n+ use crate::events::Event;\n+\n+ #[tokio::test]\n+ async fn replay_applies_one_line_at_a_time() {\n+ let tmp = tempfile::tempdir().unwrap();\n+ let path = tmp.path().join(\"events.jsonl\");\n+ let log = EventLog::new(&path);\n+ log.append(&Event::NodeEnsured {\n+ id: \"reddit.com/r/rust\".into(),\n+ })\n+ .await\n+ .unwrap();\n+ log.append(&Event::VoteRecorded {\n+ ts: 1,\n+ a: \"a\".into(),\n+ b: \"b\".into(),\n+ ratio_left: 2,\n+ ratio_right: 1,\n+ scope: String::new(),\n+ })\n+ .await\n+ .unwrap();\n+\n+ let mut seen = Vec::new();\n+ let stats = log\n+ .replay(|ev| {\n+ seen.push(ev);\n+ Ok(())\n+ })\n+ .await\n+ .unwrap();\n+\n+ assert_eq!(stats.applied, 2);\n+ assert_eq!(stats.bad_lines, 0);\n+ assert_eq!(seen.len(), 2);\n }\n }\ndiff --git a/server/src/lib.rs b/server/src/lib.rs\nindex 7f7e28c8ac3758de87f1f8e073b24be4132d38da..c9440a1019cb05c410eeb07b0b3ba09e4c6a6c6a 100644\n--- a/server/src/lib.rs\n+++ b/server/src/lib.rs\n@@ -1,4 +1,5 @@\n pub mod api;\n+pub mod entity_store;\n pub mod event_log;\n pub mod events;\n pub mod fetch;\ndiff --git a/server/src/reddit.rs b/server/src/reddit.rs\nindex 626454e5a2f638734193b5190a2beea286af85b6..28fcb83a2dfef01c76b802cec850624197b4b686 100644\n--- a/server/src/reddit.rs\n+++ b/server/src/reddit.rs\n@@ -10,6 +10,7 @@ use serde_json::Value;\n use tokio::sync::{mpsc, oneshot, RwLock};\n \n use crate::{\n+ entity_store::EntityStore,\n event_log::EventLog,\n events::Event,\n fetch::now_ms,\n@@ -80,6 +81,7 @@ impl RedditBroker {\n pub fn spawn(\n tree: Arc>,\n event_log: Arc,\n+ entity_store: EntityStore,\n config: RedditApiConfig,\n ) -> Self {\n let (tx, rx) = mpsc::channel(100);\n@@ -104,7 +106,7 @@ impl RedditBroker {\n \"reddit worker started\"\n );\n \n- tokio::spawn(reddit_worker(rx, tree, event_log, client, config));\n+ tokio::spawn(reddit_worker(rx, tree, event_log, entity_store, client, config));\n \n Self { tx }\n }\n@@ -193,9 +195,16 @@ pub fn entity_view_from_payload(id: &ItemId, payload: &Value) -> Option Result<(), String> {\n let view = entity_view_from_payload(id, &payload);\n- tree.apply_entity_raw(id, payload, view);\n+ store.put(id, &payload).map_err(|e| e.to_string())?;\n+ tree.apply_entity(id, view);\n+ Ok(())\n }\n \n fn notify(done: Option>, result: FetchJobResult) {\n@@ -208,6 +217,7 @@ async fn reddit_worker(\n mut rx: mpsc::Receiver,\n tree: Arc>,\n event_log: Arc,\n+ entity_store: EntityStore,\n client: Client,\n config: RedditApiConfig,\n ) {\n@@ -302,14 +312,16 @@ async fn reddit_worker(\n let mut tree = tree.write().await;\n if kind == FetchKind::Children {\n let view = entity_view_from_payload(&child_id, &child_payload);\n- tree.apply_entity_under_parent(\n- &fetch_id,\n- &child_id,\n- child_payload,\n- view,\n- );\n- } else {\n- apply_entity_import(&mut tree, &child_id, child_payload);\n+ if let Err(e) = entity_store.put(&child_id, &child_payload) {\n+ write_err = Some(e.to_string());\n+ break;\n+ }\n+ tree.apply_entity_under_parent(&fetch_id, &child_id, view);\n+ } else if let Err(e) =\n+ apply_entity_import(&mut tree, &entity_store, &child_id, child_payload)\n+ {\n+ write_err = Some(e);\n+ break;\n }\n }\n written += 1;\ndiff --git a/server/src/reducer.rs b/server/src/reducer.rs\nindex fc23f41137d7df33997afe19c533505c250cc305..cf00f235d02e487439f8767d532929fe329a0084 100644\n--- a/server/src/reducer.rs\n+++ b/server/src/reducer.rs\n@@ -1,7 +1,6 @@\n use std::collections::{HashMap, HashSet, VecDeque};\n \n use serde::{Deserialize, Serialize};\n-use serde_json::Value;\n \n use crate::path_types::ItemId;\n \n@@ -134,9 +133,8 @@ pub struct EntityData {\n #[derive(Debug, Clone, Default)]\n pub struct NodeState {\n pub id: ItemId,\n- /// Full imported API JSON (persisted in the event log).\n- pub entity_raw: Option,\n- /// Domain-specific view derived from `entity_raw` (e.g. Reddit title/author).\n+ /// Domain-specific view derived from imported payload (e.g. Reddit title/author).\n+ /// Raw JSON lives in [`crate::entity_store::EntityStore`].\n pub data: Option,\n pub children: HashSet,\n pub local_ranking: GroupState,\n@@ -206,28 +204,25 @@ impl GlobalTree {\n }\n }\n \n- pub fn apply_entity_raw(&mut self, id: &ItemId, payload: Value, view: Option) {\n+ pub fn apply_entity(&mut self, id: &ItemId, view: Option) {\n self.ensure_path(id);\n if let Some(node) = self.nodes.get_mut(id) {\n- node.entity_raw = Some(payload);\n node.data = view;\n }\n }\n \n- /// Import entity data for `id` and attach it as a direct child of `parent`\n+ /// Import entity view for `id` and attach it as a direct child of `parent`\n /// without running [`Self::ensure_path`] on `id` (avoids Reddit `/comments/`\n /// parent rules pulling intermediate path segments into the subreddit).\n pub fn apply_entity_under_parent(\n &mut self,\n parent: &ItemId,\n id: &ItemId,\n- payload: Value,\n view: Option,\n ) {\n self.ensure_path(parent);\n self.ensure_node(id);\n if let Some(node) = self.nodes.get_mut(id) {\n- node.entity_raw = Some(payload);\n node.data = view;\n }\n if let Some(p) = self.nodes.get_mut(parent) {\ndiff --git a/server/src/state.rs b/server/src/state.rs\nindex 4c3008e73cc74d8483a2064a0a280e73d82a01ec..dce04022de7ad9b0ffe0bac05ea79182b616945a 100644\n--- a/server/src/state.rs\n+++ b/server/src/state.rs\n@@ -3,6 +3,7 @@ use std::sync::Arc;\n use tokio::sync::RwLock;\n \n use crate::{\n+ entity_store::EntityStore,\n event_log::EventLog,\n events::Event,\n journal::JournalClient,\n@@ -43,6 +44,42 @@ fn parent_from_event_scope(scope: &str) -> ItemId {\n }\n }\n \n+fn apply_event(\n+ ev: Event,\n+ tree: &mut GlobalTree,\n+ entity_store: &EntityStore,\n+) -> Result<(), crate::event_log::EventLogError> {\n+ match ev {\n+ Event::VoteRecorded {\n+ ts,\n+ a,\n+ b,\n+ ratio_left,\n+ ratio_right,\n+ scope,\n+ } => {\n+ if let Some(vote) = VoteData::from_recorded(ts, &a, &b, ratio_left, ratio_right) {\n+ let parent = parent_from_event_scope(&scope);\n+ tree.apply_vote(&parent, vote);\n+ }\n+ }\n+ Event::ViewRecorded { .. } => {}\n+ Event::NodeEnsured { id } => {\n+ if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n+ tree.ensure_path(&parsed);\n+ }\n+ }\n+ Event::EntityImported { id, payload, .. } => {\n+ if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n+ if let Err(e) = apply_entity_import(tree, entity_store, &parsed, payload) {\n+ tracing::warn!(item = %id, err = %e, \"entity replay failed\");\n+ }\n+ }\n+ }\n+ }\n+ Ok(())\n+}\n+\n #[derive(Clone)]\n pub struct AppConfig {\n pub data_dir: String,\n@@ -71,6 +108,7 @@ impl AppConfig {\n pub struct AppState {\n pub cfg: Arc,\n pub event_log: Arc,\n+ pub entity_store: EntityStore,\n pub views: ViewStore,\n pub tree: Arc>,\n journal: JournalClient,\n@@ -82,48 +120,31 @@ impl AppState {\n let event_log = Arc::new(EventLog::new(cfg.event_log_path.clone()));\n let views_path = format!(\"{}/views.json\", cfg.data_dir);\n let views = ViewStore::new(&views_path);\n+ let entity_db_path = format!(\"{}/entity_db\", cfg.data_dir);\n+ let entity_store =\n+ EntityStore::open(std::path::Path::new(&entity_db_path)).expect(\"entity store\");\n \n let mut tree = GlobalTree::new();\n- if let Ok((events, _)) = event_log.load_all().await {\n- for ev in events {\n- match ev {\n- Event::VoteRecorded {\n- ts,\n- a,\n- b,\n- ratio_left,\n- ratio_right,\n- scope,\n- } => {\n- if let Some(vote) =\n- VoteData::from_recorded(ts, &a, &b, ratio_left, ratio_right)\n- {\n- let parent = parent_from_event_scope(&scope);\n- tree.apply_vote(&parent, vote);\n- }\n- }\n- Event::ViewRecorded { .. } => {}\n- Event::NodeEnsured { id } => {\n- if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n- tree.ensure_path(&parsed);\n- }\n- }\n- Event::EntityImported { id, payload, .. } => {\n- if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n- apply_entity_import(&mut tree, &parsed, payload);\n- }\n- }\n- }\n- }\n+ if let Err(e) = event_log\n+ .replay(|ev| apply_event(ev, &mut tree, &entity_store))\n+ .await\n+ {\n+ tracing::warn!(err = %e, \"event log replay failed\");\n }\n \n let tree = Arc::new(RwLock::new(tree));\n let journal = JournalClient::spawn(tree.clone(), event_log.clone());\n- let reddit = RedditBroker::spawn(tree.clone(), event_log.clone(), RedditApiConfig::from_env());\n+ let reddit = RedditBroker::spawn(\n+ tree.clone(),\n+ event_log.clone(),\n+ entity_store.clone(),\n+ RedditApiConfig::from_env(),\n+ );\n \n Self {\n cfg: Arc::new(cfg),\n event_log,\n+ entity_store,\n views,\n tree,\n journal,\n@@ -184,10 +205,10 @@ impl AppState {\n mod tests {\n use super::{normalize_scope, parse_item_param};\n use crate::{\n+ entity_store::EntityStore,\n event_log::EventLog,\n events::Event,\n path_types::ItemId,\n- reddit::apply_entity_import,\n reducer::GlobalTree,\n };\n use serde_json::json;\n@@ -197,6 +218,7 @@ mod tests {\n let tmp = tempfile::tempdir().unwrap();\n let log_path = tmp.path().join(\"events.jsonl\");\n let log = EventLog::new(log_path.to_string_lossy().into_owned());\n+ let entity_store = EntityStore::open(&tmp.path().join(\"entity_db\")).unwrap();\n let payload = json!({\"kind\":\"t5\",\"data\":{\"title\":\"Rust\",\"display_name\":\"rust\"}});\n log.append(&Event::EntityImported {\n id: \"reddit.com/r/rust\".into(),\n@@ -207,19 +229,16 @@ mod tests {\n .unwrap();\n \n let mut tree = GlobalTree::new();\n- let (events, _) = log.load_all().await.unwrap();\n- for ev in events {\n- if let Event::EntityImported { id, payload, .. } = ev {\n- let parsed = ItemId::parse(&id).unwrap();\n- apply_entity_import(&mut tree, &parsed, payload);\n- }\n- }\n+ log.replay(|ev| super::apply_event(ev, &mut tree, &entity_store))\n+ .await\n+ .unwrap();\n let node = tree.get(&ItemId::parse(\"reddit.com/r/rust\").unwrap()).unwrap();\n assert_eq!(node.data.as_ref().unwrap().title, \"Rust\");\n- assert_eq!(\n- node.entity_raw.as_ref().unwrap()[\"data\"][\"display_name\"],\n- \"rust\"\n- );\n+ let stored = entity_store\n+ .get(&ItemId::parse(\"reddit.com/r/rust\").unwrap())\n+ .unwrap()\n+ .unwrap();\n+ assert_eq!(stored[\"data\"][\"display_name\"], \"rust\");\n }\n \n #[test]\ndiff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs\nindex 56d2db313b313ffce07eff38a8ccdaa2fbb9498f..32da054b58b97a4751a038c677d213d6c3fa7cdc 100644\n--- a/server/tests/integration_ui.rs\n+++ b/server/tests/integration_ui.rs\n@@ -104,9 +104,17 @@ async fn post_ui_record_vote_morphs_ranking_and_persists() {\n let log = std::fs::read_to_string(tmp.path().join(\"events.jsonl\")).unwrap();\n assert!(log.contains(\"vote_recorded\"));\n \n+ // Replay in a fresh data dir (RocksDB locks entity_db while the server runs).\n+ let replay_tmp = TempDir::new().unwrap();\n+ std::fs::copy(\n+ tmp.path().join(\"events.jsonl\"),\n+ replay_tmp.path().join(\"events.jsonl\"),\n+ )\n+ .unwrap();\n+ let replay_data = replay_tmp.path().to_string_lossy().into_owned();\n let cfg = AppConfig {\n- data_dir: tmp.path().to_string_lossy().into_owned(),\n- event_log_path: tmp.path().join(\"events.jsonl\").to_string_lossy().into_owned(),\n+ data_dir: replay_data.clone(),\n+ event_log_path: format!(\"{replay_data}/events.jsonl\"),\n port: 0,\n };\n let state = create_app_state(cfg).await;\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[595b3850] Fix star-topology ranking by using degree-based d_max (#146).\n\nA pure forward star at the default `>` ratio (2:1) produced uniform 1/3\nscores, and the alphabetical-fallback sort placed the unambiguous winner\nlast. Root cause: `compute_scores_from_edges` divided by the max sum of\npairwise-normalized weights, so every node ended up with P_ii = 0 — a\nbipartite Markov chain whose power iteration oscillated and, after the\nconfigured even iteration count, returned to the uniform initial state.\n\nSwitch the divisor to the unweighted max neighbor degree, matching the\ncanonical Rank Centrality definition in Negahban–Oh–Shah 2012 §3.1\n(arXiv:1209.1688, eq. defP and the d_max definition in §6). This gives\nevery non-saturated node a positive self-loop, makes the chain aperiodic,\nand converges the star to π_zebra = 1/2, π_alpha = π_beta = 1/4.\n\nAdd Rust regression test and a Clojure test that drives the sorterc\nbinary against four .sorter fixtures (star, inverse star, chain, cycle).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) \n\nSide B — unified diff (full patch):\ndiff --git a/server/src/ranking.rs b/server/src/ranking.rs\nindex 3710c9f64437f5bef3b2121905b6f3bcb7611047..38e6d09b4370e5f8cbae09c0e5760b4e7f1ef7db 100644\n--- a/server/src/ranking.rs\n+++ b/server/src/ranking.rs\n@@ -1,4 +1,4 @@\n-use std::collections::HashMap;\n+use std::collections::{HashMap, HashSet};\n \n use crate::path_types::ItemId;\n use crate::reducer::GroupState;\n@@ -143,23 +143,35 @@ fn compute_scores_from_edges(n: usize, edges: impl Iterator 0 for every non-maximum-degree node, and for max-degree\n+ // nodes whenever any neighbor weight is below 1 (i.e. not a unanimous\n+ // loss). Without this, regular comparison graphs (e.g. a pure star at\n+ // ratio 2:1) produce a bipartite chain that oscillates instead of\n+ // converging — see issue #146.\n let mut out_edges: Vec> = vec![Vec::new(); n];\n- let mut out_deg: Vec = vec![0.0; n];\n+ let mut neighbors: Vec> = vec![HashSet::new(); n];\n \n for ((src, dst), w) in &normalized {\n out_edges[*src].push((*dst, *w));\n- out_deg[*src] += w;\n+ neighbors[*src].insert(*dst);\n+ neighbors[*dst].insert(*src);\n }\n \n- let mut max_out = 0.0f64;\n- for &d in &out_deg {\n- if d > max_out {\n- max_out = d;\n- }\n- }\n- if max_out <= 1e-12 {\n+ let weight_sum: Vec = out_edges\n+ .iter()\n+ .map(|es| es.iter().map(|(_, w)| *w).sum())\n+ .collect();\n+ let d_max = neighbors.iter().map(|s| s.len()).max().unwrap_or(0);\n+ if d_max == 0 {\n return vec![1.0 / n as f64; n];\n }\n+ let d_max_f = d_max as f64;\n \n let mut scores = vec![1.0 / n as f64; n];\n let mut next = vec![0.0f64; n];\n@@ -167,14 +179,14 @@ fn compute_scores_from_edges(n: usize, edges: impl Iterator` ratio (2:1).\n+ /// Under the old (sum-of-weights) divisor every node had P_ii = 0 and the\n+ /// chain was bipartite; power iteration oscillated and returned the\n+ /// uniform initial distribution after an even number of steps. Using the\n+ /// paper's degree-based d_max gives every node a positive self-loop and\n+ /// the chain converges to the correct stationary distribution.\n+ #[test]\n+ fn star_topology_winner_at_top_via_subset() {\n+ let mut g = mk_group();\n+ g.apply_vote(vote(1, \"zebra\", \"alpha\", 2, 1));\n+ g.apply_vote(vote(2, \"zebra\", \"beta\", 2, 1));\n+\n+ let mut items: Vec<(usize, String)> = g\n+ .idx_to_item\n+ .iter()\n+ .enumerate()\n+ .map(|(i, it)| (i, it.as_str().to_string()))\n+ .collect();\n+ items.sort_by(|a, b| a.1.cmp(&b.1));\n+ let idxs: Vec = items.iter().map(|(i, _)| *i).collect();\n+\n+ let ranked = ranked_items_subset(&g, &idxs, 10000, 1e-8);\n+ for r in &ranked {\n+ eprintln!(\"{}: {}\", r.item.as_str(), r.score);\n+ }\n+ assert_eq!(\n+ ranked[0].item.as_str(),\n+ \"https://slug.social/zebra\",\n+ \"zebra won both votes and should rank #1\"\n+ );\n+ }\n+\n #[test]\n fn group_ranking_cache_dirty_flow() {\n let mut g = mk_group();\ndiff --git a/test/fixtures/ranking/chain.sorter b/test/fixtures/ranking/chain.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..912a96bdc08f631be27f3c9afc7e05004a2457c0\n--- /dev/null\n+++ b/test/fixtures/ranking/chain.sorter\n@@ -0,0 +1,10 @@\n+#t3\n+\n+~/t3/a { head of chain }\n+~/t3/b { middle }\n+~/t3/c { tail }\n+\n+{ a > b }\n+~/t3/a > ~/t3/b\n+{ b > c }\n+~/t3/b > ~/t3/c\ndiff --git a/test/fixtures/ranking/cycle.sorter b/test/fixtures/ranking/cycle.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..771731ae176e5d77e00ab89767ff2d7465bb7c6b\n--- /dev/null\n+++ b/test/fixtures/ranking/cycle.sorter\n@@ -0,0 +1,12 @@\n+#t4\n+\n+~/t4/a { node a }\n+~/t4/b { node b }\n+~/t4/c { node c }\n+\n+{ a > b }\n+~/t4/a > ~/t4/b\n+{ b > c }\n+~/t4/b > ~/t4/c\n+{ c > a }\n+~/t4/c > ~/t4/a\ndiff --git a/test/fixtures/ranking/star.sorter b/test/fixtures/ranking/star.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..135c9f9097d57b73c7ab18fac737ed3598d76fbe\n--- /dev/null\n+++ b/test/fixtures/ranking/star.sorter\n@@ -0,0 +1,11 @@\n+#repro\n+\n+~/repro/zebra { winner — beats both others }\n+~/repro/alpha { loser — alphabetically first }\n+~/repro/beta { loser — alphabetically middle }\n+\n+{ zebra beats alpha }\n+~/repro/zebra > ~/repro/alpha\n+\n+{ zebra beats beta }\n+~/repro/zebra > ~/repro/beta\ndiff --git a/test/fixtures/ranking/star_inverse.sorter b/test/fixtures/ranking/star_inverse.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..dab842fa924e5124865ee216b9565f6c7471c812\n--- /dev/null\n+++ b/test/fixtures/ranking/star_inverse.sorter\n@@ -0,0 +1,11 @@\n+#t2\n+\n+~/t2/win { source of incoming edges (loses both) }\n+~/t2/loss-a { winner }\n+~/t2/loss-b { winner }\n+\n+{ loss-a beats win }\n+~/t2/loss-a > ~/t2/win\n+\n+{ loss-b beats win }\n+~/t2/loss-b > ~/t2/win\ndiff --git a/test/ranking.clj b/test/ranking.clj\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..e1358783a84a264ee633bd79151ba29750b717cf\n--- /dev/null\n+++ b/test/ranking.clj\n@@ -0,0 +1,74 @@\n+(ns test.ranking\n+ \"Drives sorterc on .sorter fixtures and asserts ranking properties.\n+\n+ Regression coverage for issue #146 — pure forward star at default ratio\n+ (2:1 for `>`) used to produce tied uniform scores because the random walk\n+ on the normalized edge weights was bipartite. Fixed by switching to the\n+ degree-based d_max from Negahban–Oh–Shah rank centrality (§3.1).\"\n+ (:require [clojure.test :refer [deftest is testing]]\n+ [babashka.process :as p]\n+ [cheshire.core :as json]\n+ [clojure.java.io :as io]))\n+\n+(def sorterc-bin\n+ \"Path to the locally-built sorterc binary. Builds on demand if missing.\"\n+ (let [dbg \"target/debug/sorterc\"\n+ release \"target/release/sorterc\"]\n+ (cond\n+ (.exists (io/file release)) release\n+ (.exists (io/file dbg)) dbg\n+ :else\n+ (do (println \"building sorterc…\")\n+ (let [r (p/shell {:out :string :err :string :continue true}\n+ \"cargo build -p sorterc\")]\n+ (when-not (zero? (:exit r))\n+ (throw (ex-info \"cargo build -p sorterc failed\"\n+ {:stderr (:err r)}))))\n+ dbg))))\n+\n+(defn compile-sorter [fixture-path]\n+ (let [{:keys [out exit]} (p/shell {:out :string :err :string :continue true}\n+ sorterc-bin \"compile\" fixture-path)]\n+ (when-not (zero? exit)\n+ (throw (ex-info \"sorterc exit nonzero\" {:fixture fixture-path :out out})))\n+ (json/parse-string out true)))\n+\n+(defn first-component-ranking [result]\n+ (-> result :rankings first :components first :ranking))\n+\n+(defn item-leaf [item]\n+ (last (clojure.string/split item #\"/\")))\n+\n+(deftest chain-ranks-head-first\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/chain.sorter\"))\n+ names (mapv (comp item-leaf :item) ranking)]\n+ (is (= [\"a\" \"b\" \"c\"] names)\n+ \"chain a>b>c should rank a, b, c in order\")\n+ (is (apply > (map :score ranking))\n+ \"scores should attenuate strictly down the chain\")))\n+\n+(deftest inverse-star-puts-winners-on-top\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/star_inverse.sorter\"))\n+ names (mapv (comp item-leaf :item) ranking)]\n+ (is (= \"win\" (last names))\n+ \"the item that lost to both others should be ranked last\")))\n+\n+(deftest cycle-produces-uniform-scores\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/cycle.sorter\"))\n+ scores (map :score ranking)]\n+ (is (every? #(< (Math/abs (- % 1/3)) 1e-3) scores)\n+ \"a perfectly symmetric 3-cycle should give every node ~1/3\")))\n+\n+(deftest star-topology-winner-at-top\n+ ;; Issue #146 regression: source-only star at default `>` ratio (2:1).\n+ ;; Pre-fix produced uniform 1/3 scores; alphabetical fallback put the\n+ ;; unambiguous winner at the bottom. Post-fix the chain is aperiodic and\n+ ;; converges to π_zebra = 1/2, π_alpha = π_beta = 1/4.\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/star.sorter\"))\n+ names (mapv (comp item-leaf :item) ranking)\n+ by-name (into {} (map (juxt (comp item-leaf :item) :score) ranking))]\n+ (is (= \"zebra\" (first names))\n+ \"zebra won both votes and should rank #1\")\n+ (is (< (Math/abs (- (by-name \"zebra\") 0.5)) 1e-3))\n+ (is (< (Math/abs (- (by-name \"alpha\") 0.25)) 1e-3))\n+ (is (< (Math/abs (- (by-name \"beta\") 0.25)) 1e-3))))\n","role":"user"}],"model":"~x-ai/grok-latest"}