Highly ambitious ATProtocol AppView service and sdks

add redis for jetstream actor, collection, domain caching as well as auth requests and sync

+4
api/.env.example
··· 18 18 19 19 # Logging level 20 20 RUST_LOG=debug 21 + 22 + # Redis configuration (optional - if not set, falls back to in-memory cache) 23 + REDIS_URL=redis://localhost:6379 24 + REDIS_TTL_SECONDS=3600
+22
api/.sqlx/query-8fe482ef09e83d77ac7679d98e5c36c10c6b00c30cc289616a6da9c86ac11006.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT time_us\n FROM jetstream_cursor\n WHERE id = $1\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "time_us", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + false 19 + ] 20 + }, 21 + "hash": "8fe482ef09e83d77ac7679d98e5c36c10c6b00c30cc289616a6da9c86ac11006" 22 + }
+15
api/.sqlx/query-ae961e575c6f9d0761401115344ae5993753bd5064fc66344b402aa4ee8cc177.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n INSERT INTO jetstream_cursor (id, time_us, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (id)\n DO UPDATE SET time_us = $2, updated_at = NOW()\n ", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Int8" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "ae961e575c6f9d0761401115344ae5993753bd5064fc66344b402aa4ee8cc177" 15 + }
+15
api/.sqlx/query-e167d0d1f047f25f6116097ff025352727645c99faf8fc451678d2a130df7e78.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n INSERT INTO jetstream_cursor (id, time_us, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (id)\n DO UPDATE SET time_us = $2, updated_at = NOW()\n ", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Int8" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "e167d0d1f047f25f6116097ff025352727645c99faf8fc451678d2a130df7e78" 15 + }
+43
api/CLAUDE.md
··· 85 85 stable pagination 86 86 - **OAuth DPoP authentication** integrated with AIP server for ATProto 87 87 authentication 88 + - **Multi-tier caching** with Redis (if configured) or in-memory fallback for 89 + performance optimization 88 90 89 91 ### Module Organization 90 92 ··· 95 97 - `src/jetstream.rs` - Real-time event processing from ATProto firehose 96 98 - `src/sync.rs` - Bulk synchronization operations with ATProto relay 97 99 - `src/auth.rs` - OAuth verification and DPoP authentication setup 100 + - `src/cache.rs` - Generic caching interface and in-memory cache implementation 101 + - `src/redis_cache.rs` - Redis cache implementation for distributed caching 98 102 - `src/errors.rs` - Error type definitions (reference for new errors) 99 103 100 104 ## Error Handling ··· 131 135 132 136 - After updating, run `cargo check` to fix errors and warnings 133 137 - Don't use dead code, if it's not used remove it 138 + 139 + ## Caching Architecture 140 + 141 + The application uses a flexible caching system that supports both Redis and in-memory caching with automatic fallback. 142 + 143 + ### Cache Configuration 144 + 145 + Configure caching via environment variables: 146 + 147 + ```bash 148 + # Redis configuration (optional) 149 + REDIS_URL=redis://localhost:6379 150 + REDIS_TTL_SECONDS=3600 151 + ``` 152 + 153 + If `REDIS_URL` is not set, the application automatically falls back to in-memory caching. 154 + 155 + ### Cache Types and TTLs 156 + 157 + - **Actor Cache** (Jetstream): No TTL (permanent cache for slice actors) 158 + - **Lexicon Cache**: 2 hours (7200s) - lexicons change infrequently 159 + - **Domain Cache**: 4 hours (14400s) - slice domain mappings rarely change 160 + - **Collections Cache**: 2 hours (7200s) - slice collections change infrequently 161 + - **Auth Cache**: 5 minutes (300s) - OAuth tokens and AT Protocol sessions 162 + - **DID Resolution Cache**: 24 hours (86400s) - DID documents change rarely 163 + 164 + ### Cache Implementation 165 + 166 + - `src/cache.rs` - Defines the `Cache` trait and `SliceCache` wrapper with domain-specific methods 167 + - `src/redis_cache.rs` - Redis implementation of the `Cache` trait 168 + - Both implementations provide the same interface through `SliceCache` 169 + - Cache keys use prefixed formats (e.g., `actor:{did}:{slice_uri}`, `oauth_userinfo:{token}`) 170 + 171 + ### Cache Usage Patterns 172 + 173 + - **Jetstream Consumer**: Creates 4 separate cache instances for actors, lexicons, domains, and collections 174 + - **Auth System**: Uses dedicated auth cache for OAuth and AT Protocol session caching 175 + - **Actor Resolution**: Caches DID resolution results to avoid repeated lookups 176 + - **Automatic Fallback**: Redis failures automatically fall back to in-memory caching without errors
+350 -347
api/Cargo.lock
··· 4 4 5 5 [[package]] 6 6 name = "addr2line" 7 - version = "0.24.2" 7 + version = "0.25.1" 8 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 9 + checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 10 dependencies = [ 11 11 "gimli", 12 12 ] ··· 33 33 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 34 34 35 35 [[package]] 36 - name = "android-tzdata" 37 - version = "0.1.1" 38 - source = "registry+https://github.com/rust-lang/crates.io-index" 39 - checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 40 - 41 - [[package]] 42 36 name = "android_system_properties" 43 37 version = "0.1.5" 44 38 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 49 43 50 44 [[package]] 51 45 name = "anyhow" 52 - version = "1.0.99" 46 + version = "1.0.100" 53 47 source = "registry+https://github.com/rust-lang/crates.io-index" 54 - checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 48 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 55 49 56 50 [[package]] 57 51 name = "anymap2" 58 52 version = "0.13.0" 59 53 source = "registry+https://github.com/rust-lang/crates.io-index" 60 54 checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" 55 + 56 + [[package]] 57 + name = "arc-swap" 58 + version = "1.7.1" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 61 61 62 62 [[package]] 63 63 name = "async-trait" ··· 87 87 88 88 [[package]] 89 89 name = "atproto-client" 90 - version = "0.13.0" 90 + version = "0.12.0" 91 91 source = "registry+https://github.com/rust-lang/crates.io-index" 92 - checksum = "c34ed7ebeec01cd7775c1c7841838c142d123d403983ed7179b31850435b5c7c" 92 + checksum = "9f388d83aa9552d7c7b80cc131558fb86be5a46a104742fb4ccf38e22a3c8620" 93 93 dependencies = [ 94 94 "anyhow", 95 95 "atproto-identity", ··· 101 101 "reqwest-middleware", 102 102 "serde", 103 103 "serde_json", 104 - "thiserror 2.0.14", 104 + "thiserror 2.0.16", 105 105 "tokio", 106 106 "tracing", 107 107 "urlencoding", ··· 109 109 110 110 [[package]] 111 111 name = "atproto-identity" 112 - version = "0.13.0" 112 + version = "0.12.0" 113 113 source = "registry+https://github.com/rust-lang/crates.io-index" 114 - checksum = "b956c07726fce812630be63c5cb31b1961cbb70f0a05614278523102d78c3a48" 114 + checksum = "65e405e13a96ce91d1e832f56b90ae3f1fcbe29a5d9731e417074bfe1df71a5f" 115 115 dependencies = [ 116 116 "anyhow", 117 117 "async-trait", ··· 128 128 "serde", 129 129 "serde_ipld_dagcbor", 130 130 "serde_json", 131 - "thiserror 2.0.14", 131 + "thiserror 2.0.16", 132 132 "tokio", 133 133 "tracing", 134 134 "urlencoding", ··· 136 136 137 137 [[package]] 138 138 name = "atproto-jetstream" 139 - version = "0.13.0" 139 + version = "0.12.0" 140 140 source = "registry+https://github.com/rust-lang/crates.io-index" 141 - checksum = "7b1897fb2f7c6d02d46f7b8d25d653c141cee4a68a10efd135d46201a95034db" 141 + checksum = "b36d0d4fec207d04563bdb151ca793dbfbb9c4708b5c9320d65da4b564b186d2" 142 142 dependencies = [ 143 143 "anyhow", 144 144 "async-trait", ··· 147 147 "http", 148 148 "serde", 149 149 "serde_json", 150 - "thiserror 2.0.14", 150 + "thiserror 2.0.16", 151 151 "tokio", 152 152 "tokio-util", 153 153 "tokio-websockets", ··· 159 159 160 160 [[package]] 161 161 name = "atproto-oauth" 162 - version = "0.13.0" 162 + version = "0.12.0" 163 163 source = "registry+https://github.com/rust-lang/crates.io-index" 164 - checksum = "3ea205901c33d074a1b498591d0511bcd788b6772ec0ca6e09a92c4327ddbdff" 164 + checksum = "3f7a82388b59f83c2c141af434afa96d41841e4e7c1b361afc83193f2ee75d63" 165 165 dependencies = [ 166 166 "anyhow", 167 167 "async-trait", ··· 183 183 "serde_ipld_dagcbor", 184 184 "serde_json", 185 185 "sha2", 186 - "thiserror 2.0.14", 186 + "thiserror 2.0.16", 187 187 "tokio", 188 188 "tracing", 189 189 "ulid", ··· 191 191 192 192 [[package]] 193 193 name = "atproto-record" 194 - version = "0.13.0" 194 + version = "0.12.0" 195 195 source = "registry+https://github.com/rust-lang/crates.io-index" 196 - checksum = "0550f74423ca745132dc07ba1cb01f2f08243a7bf7497f5c3f2185a774c92ca2" 196 + checksum = "accec4d63f3f653e947b051b8c9f56f6d939d7a646e44b845eeabc03c713dad5" 197 197 dependencies = [ 198 198 "anyhow", 199 199 "atproto-identity", ··· 202 202 "serde", 203 203 "serde_ipld_dagcbor", 204 204 "serde_json", 205 - "thiserror 2.0.14", 205 + "thiserror 2.0.16", 206 206 ] 207 207 208 208 [[package]] ··· 306 306 ] 307 307 308 308 [[package]] 309 + name = "backon" 310 + version = "1.5.2" 311 + source = "registry+https://github.com/rust-lang/crates.io-index" 312 + checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" 313 + dependencies = [ 314 + "fastrand", 315 + ] 316 + 317 + [[package]] 309 318 name = "backtrace" 310 - version = "0.3.75" 319 + version = "0.3.76" 311 320 source = "registry+https://github.com/rust-lang/crates.io-index" 312 - checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 321 + checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 313 322 dependencies = [ 314 323 "addr2line", 315 324 "cfg-if", ··· 317 326 "miniz_oxide", 318 327 "object", 319 328 "rustc-demangle", 320 - "windows-targets 0.52.6", 329 + "windows-link 0.2.0", 321 330 ] 322 331 323 332 [[package]] ··· 352 361 353 362 [[package]] 354 363 name = "bitflags" 355 - version = "2.9.1" 364 + version = "2.9.4" 356 365 source = "registry+https://github.com/rust-lang/crates.io-index" 357 - checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 366 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 358 367 dependencies = [ 359 368 "serde", 360 369 ] ··· 397 406 398 407 [[package]] 399 408 name = "cc" 400 - version = "1.2.33" 409 + version = "1.2.39" 401 410 source = "registry+https://github.com/rust-lang/crates.io-index" 402 - checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" 411 + checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" 403 412 dependencies = [ 413 + "find-msvc-tools", 404 414 "jobserver", 405 415 "libc", 406 416 "shlex", ··· 408 418 409 419 [[package]] 410 420 name = "cfg-if" 411 - version = "1.0.1" 421 + version = "1.0.3" 412 422 source = "registry+https://github.com/rust-lang/crates.io-index" 413 - checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 423 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 414 424 415 425 [[package]] 416 426 name = "cfg_aliases" ··· 420 430 421 431 [[package]] 422 432 name = "chrono" 423 - version = "0.4.41" 433 + version = "0.4.42" 424 434 source = "registry+https://github.com/rust-lang/crates.io-index" 425 - checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 435 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 426 436 dependencies = [ 427 - "android-tzdata", 428 437 "iana-time-zone", 429 438 "js-sys", 430 439 "num-traits", 431 440 "serde", 432 441 "wasm-bindgen", 433 - "windows-link", 442 + "windows-link 0.2.0", 434 443 ] 435 444 436 445 [[package]] ··· 448 457 ] 449 458 450 459 [[package]] 460 + name = "combine" 461 + version = "4.6.7" 462 + source = "registry+https://github.com/rust-lang/crates.io-index" 463 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 464 + dependencies = [ 465 + "bytes", 466 + "futures-core", 467 + "memchr", 468 + "pin-project-lite", 469 + "tokio", 470 + "tokio-util", 471 + ] 472 + 473 + [[package]] 451 474 name = "concurrent-queue" 452 475 version = "2.5.0" 453 476 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 725 748 726 749 [[package]] 727 750 name = "errno" 728 - version = "0.3.13" 751 + version = "0.3.14" 729 752 source = "registry+https://github.com/rust-lang/crates.io-index" 730 - checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 753 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 731 754 dependencies = [ 732 755 "libc", 733 - "windows-sys 0.60.2", 756 + "windows-sys 0.61.1", 734 757 ] 735 758 736 759 [[package]] ··· 772 795 ] 773 796 774 797 [[package]] 798 + name = "find-msvc-tools" 799 + version = "0.1.2" 800 + source = "registry+https://github.com/rust-lang/crates.io-index" 801 + checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" 802 + 803 + [[package]] 775 804 name = "flume" 776 805 version = "0.11.1" 777 806 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 811 840 812 841 [[package]] 813 842 name = "form_urlencoded" 814 - version = "1.2.1" 843 + version = "1.2.2" 815 844 source = "registry+https://github.com/rust-lang/crates.io-index" 816 - checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 845 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 817 846 dependencies = [ 818 847 "percent-encoding", 819 848 ] ··· 919 948 ] 920 949 921 950 [[package]] 922 - name = "generator" 923 - version = "0.8.5" 924 - source = "registry+https://github.com/rust-lang/crates.io-index" 925 - checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" 926 - dependencies = [ 927 - "cc", 928 - "cfg-if", 929 - "libc", 930 - "log", 931 - "rustversion", 932 - "windows", 933 - ] 934 - 935 - [[package]] 936 951 name = "generic-array" 937 952 version = "0.14.7" 938 953 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 966 981 "js-sys", 967 982 "libc", 968 983 "r-efi", 969 - "wasi 0.14.2+wasi-0.2.4", 984 + "wasi 0.14.7+wasi-0.2.4", 970 985 "wasm-bindgen", 971 986 ] 972 987 973 988 [[package]] 974 989 name = "gimli" 975 - version = "0.31.1" 990 + version = "0.32.3" 976 991 source = "registry+https://github.com/rust-lang/crates.io-index" 977 - checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 992 + checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 978 993 979 994 [[package]] 980 995 name = "group" ··· 1018 1033 ] 1019 1034 1020 1035 [[package]] 1036 + name = "hashbrown" 1037 + version = "0.16.0" 1038 + source = "registry+https://github.com/rust-lang/crates.io-index" 1039 + checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 1040 + 1041 + [[package]] 1021 1042 name = "hashlink" 1022 1043 version = "0.10.0" 1023 1044 source = "registry+https://github.com/rust-lang/crates.io-index" 1024 1045 checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1025 1046 dependencies = [ 1026 - "hashbrown", 1047 + "hashbrown 0.15.5", 1027 1048 ] 1028 1049 1029 1050 [[package]] ··· 1056 1077 "once_cell", 1057 1078 "rand 0.9.2", 1058 1079 "ring", 1059 - "thiserror 2.0.14", 1080 + "thiserror 2.0.16", 1060 1081 "tinyvec", 1061 1082 "tokio", 1062 1083 "tracing", ··· 1079 1100 "rand 0.9.2", 1080 1101 "resolv-conf", 1081 1102 "smallvec", 1082 - "thiserror 2.0.14", 1103 + "thiserror 2.0.16", 1083 1104 "tokio", 1084 1105 "tracing", 1085 1106 ] ··· 1159 1180 1160 1181 [[package]] 1161 1182 name = "hyper" 1162 - version = "1.6.0" 1183 + version = "1.7.0" 1163 1184 source = "registry+https://github.com/rust-lang/crates.io-index" 1164 - checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 1185 + checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 1165 1186 dependencies = [ 1187 + "atomic-waker", 1166 1188 "bytes", 1167 1189 "futures-channel", 1168 - "futures-util", 1190 + "futures-core", 1169 1191 "h2", 1170 1192 "http", 1171 1193 "http-body", ··· 1173 1195 "httpdate", 1174 1196 "itoa", 1175 1197 "pin-project-lite", 1198 + "pin-utils", 1176 1199 "smallvec", 1177 1200 "tokio", 1178 1201 "want", ··· 1213 1236 1214 1237 [[package]] 1215 1238 name = "hyper-util" 1216 - version = "0.1.16" 1239 + version = "0.1.17" 1217 1240 source = "registry+https://github.com/rust-lang/crates.io-index" 1218 - checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 1241 + checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 1219 1242 dependencies = [ 1220 1243 "base64 0.22.1", 1221 1244 "bytes", ··· 1239 1262 1240 1263 [[package]] 1241 1264 name = "iana-time-zone" 1242 - version = "0.1.63" 1265 + version = "0.1.64" 1243 1266 source = "registry+https://github.com/rust-lang/crates.io-index" 1244 - checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1267 + checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 1245 1268 dependencies = [ 1246 1269 "android_system_properties", 1247 1270 "core-foundation-sys", ··· 1349 1372 1350 1373 [[package]] 1351 1374 name = "idna" 1352 - version = "1.0.3" 1375 + version = "1.1.0" 1353 1376 source = "registry+https://github.com/rust-lang/crates.io-index" 1354 - checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1377 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1355 1378 dependencies = [ 1356 1379 "idna_adapter", 1357 1380 "smallvec", ··· 1370 1393 1371 1394 [[package]] 1372 1395 name = "indexmap" 1373 - version = "2.10.0" 1396 + version = "2.11.4" 1374 1397 source = "registry+https://github.com/rust-lang/crates.io-index" 1375 - checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 1398 + checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 1376 1399 dependencies = [ 1377 1400 "equivalent", 1378 - "hashbrown", 1401 + "hashbrown 0.16.0", 1379 1402 ] 1380 1403 1381 1404 [[package]] 1382 1405 name = "io-uring" 1383 - version = "0.7.9" 1406 + version = "0.7.10" 1384 1407 source = "registry+https://github.com/rust-lang/crates.io-index" 1385 - checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" 1408 + checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 1386 1409 dependencies = [ 1387 1410 "bitflags", 1388 1411 "cfg-if", ··· 1446 1469 1447 1470 [[package]] 1448 1471 name = "js-sys" 1449 - version = "0.3.77" 1472 + version = "0.3.81" 1450 1473 source = "registry+https://github.com/rust-lang/crates.io-index" 1451 - checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1474 + checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" 1452 1475 dependencies = [ 1453 1476 "once_cell", 1454 1477 "wasm-bindgen", ··· 1479 1502 1480 1503 [[package]] 1481 1504 name = "libc" 1482 - version = "0.2.175" 1505 + version = "0.2.176" 1483 1506 source = "registry+https://github.com/rust-lang/crates.io-index" 1484 - checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1507 + checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 1485 1508 1486 1509 [[package]] 1487 1510 name = "libm" ··· 1491 1514 1492 1515 [[package]] 1493 1516 name = "libredox" 1494 - version = "0.1.9" 1517 + version = "0.1.10" 1495 1518 source = "registry+https://github.com/rust-lang/crates.io-index" 1496 - checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 1519 + checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 1497 1520 dependencies = [ 1498 1521 "bitflags", 1499 1522 "libc", ··· 1512 1535 1513 1536 [[package]] 1514 1537 name = "linux-raw-sys" 1515 - version = "0.9.4" 1538 + version = "0.11.0" 1516 1539 source = "registry+https://github.com/rust-lang/crates.io-index" 1517 - checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1540 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 1518 1541 1519 1542 [[package]] 1520 1543 name = "litemap" ··· 1534 1557 1535 1558 [[package]] 1536 1559 name = "log" 1537 - version = "0.4.27" 1538 - source = "registry+https://github.com/rust-lang/crates.io-index" 1539 - checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1540 - 1541 - [[package]] 1542 - name = "loom" 1543 - version = "0.7.2" 1560 + version = "0.4.28" 1544 1561 source = "registry+https://github.com/rust-lang/crates.io-index" 1545 - checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 1546 - dependencies = [ 1547 - "cfg-if", 1548 - "generator", 1549 - "scoped-tls", 1550 - "tracing", 1551 - "tracing-subscriber", 1552 - ] 1562 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1553 1563 1554 1564 [[package]] 1555 1565 name = "lru" ··· 1557 1567 source = "registry+https://github.com/rust-lang/crates.io-index" 1558 1568 checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 1559 1569 dependencies = [ 1560 - "hashbrown", 1570 + "hashbrown 0.15.5", 1561 1571 ] 1562 1572 1563 1573 [[package]] ··· 1568 1578 1569 1579 [[package]] 1570 1580 name = "matchers" 1571 - version = "0.1.0" 1581 + version = "0.2.0" 1572 1582 source = "registry+https://github.com/rust-lang/crates.io-index" 1573 - checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1583 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 1574 1584 dependencies = [ 1575 - "regex-automata 0.1.10", 1585 + "regex-automata", 1576 1586 ] 1577 1587 1578 1588 [[package]] ··· 1593 1603 1594 1604 [[package]] 1595 1605 name = "memchr" 1596 - version = "2.7.5" 1606 + version = "2.7.6" 1597 1607 source = "registry+https://github.com/rust-lang/crates.io-index" 1598 - checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1608 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 1599 1609 1600 1610 [[package]] 1601 1611 name = "mime" ··· 1635 1645 1636 1646 [[package]] 1637 1647 name = "moka" 1638 - version = "0.12.10" 1648 + version = "0.12.11" 1639 1649 source = "registry+https://github.com/rust-lang/crates.io-index" 1640 - checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" 1650 + checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" 1641 1651 dependencies = [ 1642 1652 "crossbeam-channel", 1643 1653 "crossbeam-epoch", 1644 1654 "crossbeam-utils", 1645 - "loom", 1655 + "equivalent", 1646 1656 "parking_lot", 1647 1657 "portable-atomic", 1648 1658 "rustc_version", 1649 1659 "smallvec", 1650 1660 "tagptr", 1651 - "thiserror 1.0.69", 1652 1661 "uuid", 1653 1662 ] 1654 1663 ··· 1710 1719 1711 1720 [[package]] 1712 1721 name = "nu-ansi-term" 1713 - version = "0.46.0" 1722 + version = "0.50.1" 1714 1723 source = "registry+https://github.com/rust-lang/crates.io-index" 1715 - checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1724 + checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 1725 + dependencies = [ 1726 + "windows-sys 0.52.0", 1727 + ] 1728 + 1729 + [[package]] 1730 + name = "num-bigint" 1731 + version = "0.4.6" 1732 + source = "registry+https://github.com/rust-lang/crates.io-index" 1733 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 1716 1734 dependencies = [ 1717 - "overload", 1718 - "winapi", 1735 + "num-integer", 1736 + "num-traits", 1719 1737 ] 1720 1738 1721 1739 [[package]] ··· 1767 1785 1768 1786 [[package]] 1769 1787 name = "object" 1770 - version = "0.36.7" 1788 + version = "0.37.3" 1771 1789 source = "registry+https://github.com/rust-lang/crates.io-index" 1772 - checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1790 + checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 1773 1791 dependencies = [ 1774 1792 "memchr", 1775 1793 ] ··· 1829 1847 ] 1830 1848 1831 1849 [[package]] 1832 - name = "overload" 1833 - version = "0.1.1" 1834 - source = "registry+https://github.com/rust-lang/crates.io-index" 1835 - checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1836 - 1837 - [[package]] 1838 1850 name = "p256" 1839 1851 version = "0.13.2" 1840 1852 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1900 1912 1901 1913 [[package]] 1902 1914 name = "percent-encoding" 1903 - version = "2.3.1" 1915 + version = "2.3.2" 1904 1916 source = "registry+https://github.com/rust-lang/crates.io-index" 1905 - checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1917 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1906 1918 1907 1919 [[package]] 1908 1920 name = "pin-project-lite" ··· 1951 1963 1952 1964 [[package]] 1953 1965 name = "potential_utf" 1954 - version = "0.1.2" 1966 + version = "0.1.3" 1955 1967 source = "registry+https://github.com/rust-lang/crates.io-index" 1956 - checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1968 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 1957 1969 dependencies = [ 1958 1970 "zerovec", 1959 1971 ] ··· 1979 1991 1980 1992 [[package]] 1981 1993 name = "proc-macro2" 1982 - version = "1.0.97" 1994 + version = "1.0.101" 1983 1995 source = "registry+https://github.com/rust-lang/crates.io-index" 1984 - checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" 1996 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 1985 1997 dependencies = [ 1986 1998 "unicode-ident", 1987 1999 ] 1988 2000 1989 2001 [[package]] 1990 2002 name = "quinn" 1991 - version = "0.11.8" 2003 + version = "0.11.9" 1992 2004 source = "registry+https://github.com/rust-lang/crates.io-index" 1993 - checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 2005 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 1994 2006 dependencies = [ 1995 2007 "bytes", 1996 2008 "cfg_aliases", ··· 1999 2011 "quinn-udp", 2000 2012 "rustc-hash", 2001 2013 "rustls", 2002 - "socket2 0.5.10", 2003 - "thiserror 2.0.14", 2014 + "socket2 0.6.0", 2015 + "thiserror 2.0.16", 2004 2016 "tokio", 2005 2017 "tracing", 2006 2018 "web-time", ··· 2008 2020 2009 2021 [[package]] 2010 2022 name = "quinn-proto" 2011 - version = "0.11.12" 2023 + version = "0.11.13" 2012 2024 source = "registry+https://github.com/rust-lang/crates.io-index" 2013 - checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 2025 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 2014 2026 dependencies = [ 2015 2027 "bytes", 2016 2028 "getrandom 0.3.3", ··· 2021 2033 "rustls", 2022 2034 "rustls-pki-types", 2023 2035 "slab", 2024 - "thiserror 2.0.14", 2036 + "thiserror 2.0.16", 2025 2037 "tinyvec", 2026 2038 "tracing", 2027 2039 "web-time", ··· 2029 2041 2030 2042 [[package]] 2031 2043 name = "quinn-udp" 2032 - version = "0.5.13" 2044 + version = "0.5.14" 2033 2045 source = "registry+https://github.com/rust-lang/crates.io-index" 2034 - checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" 2046 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 2035 2047 dependencies = [ 2036 2048 "cfg_aliases", 2037 2049 "libc", 2038 2050 "once_cell", 2039 - "socket2 0.5.10", 2051 + "socket2 0.6.0", 2040 2052 "tracing", 2041 - "windows-sys 0.59.0", 2053 + "windows-sys 0.60.2", 2042 2054 ] 2043 2055 2044 2056 [[package]] ··· 2116 2128 ] 2117 2129 2118 2130 [[package]] 2131 + name = "redis" 2132 + version = "0.32.6" 2133 + source = "registry+https://github.com/rust-lang/crates.io-index" 2134 + checksum = "15965fbccb975c38a08a68beca6bdb57da9081cd0859417c5975a160d968c3cb" 2135 + dependencies = [ 2136 + "arc-swap", 2137 + "backon", 2138 + "bytes", 2139 + "cfg-if", 2140 + "combine", 2141 + "futures-channel", 2142 + "futures-util", 2143 + "itoa", 2144 + "num-bigint", 2145 + "percent-encoding", 2146 + "pin-project-lite", 2147 + "ryu", 2148 + "sha1_smol", 2149 + "socket2 0.6.0", 2150 + "tokio", 2151 + "tokio-util", 2152 + "url", 2153 + ] 2154 + 2155 + [[package]] 2119 2156 name = "redox_syscall" 2120 2157 version = "0.5.17" 2121 2158 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2126 2163 2127 2164 [[package]] 2128 2165 name = "regex" 2129 - version = "1.11.2" 2166 + version = "1.11.3" 2130 2167 source = "registry+https://github.com/rust-lang/crates.io-index" 2131 - checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" 2168 + checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" 2132 2169 dependencies = [ 2133 2170 "aho-corasick", 2134 2171 "memchr", 2135 - "regex-automata 0.4.9", 2136 - "regex-syntax 0.8.5", 2137 - ] 2138 - 2139 - [[package]] 2140 - name = "regex-automata" 2141 - version = "0.1.10" 2142 - source = "registry+https://github.com/rust-lang/crates.io-index" 2143 - checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 2144 - dependencies = [ 2145 - "regex-syntax 0.6.29", 2172 + "regex-automata", 2173 + "regex-syntax", 2146 2174 ] 2147 2175 2148 2176 [[package]] 2149 2177 name = "regex-automata" 2150 - version = "0.4.9" 2178 + version = "0.4.11" 2151 2179 source = "registry+https://github.com/rust-lang/crates.io-index" 2152 - checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 2180 + checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" 2153 2181 dependencies = [ 2154 2182 "aho-corasick", 2155 2183 "memchr", 2156 - "regex-syntax 0.8.5", 2184 + "regex-syntax", 2157 2185 ] 2158 2186 2159 2187 [[package]] 2160 2188 name = "regex-syntax" 2161 - version = "0.6.29" 2162 - source = "registry+https://github.com/rust-lang/crates.io-index" 2163 - checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 2164 - 2165 - [[package]] 2166 - name = "regex-syntax" 2167 - version = "0.8.5" 2189 + version = "0.8.6" 2168 2190 source = "registry+https://github.com/rust-lang/crates.io-index" 2169 - checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 2191 + checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 2170 2192 2171 2193 [[package]] 2172 2194 name = "reqwest" ··· 2245 2267 2246 2268 [[package]] 2247 2269 name = "resolv-conf" 2248 - version = "0.7.4" 2270 + version = "0.7.5" 2249 2271 source = "registry+https://github.com/rust-lang/crates.io-index" 2250 - checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 2272 + checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" 2251 2273 2252 2274 [[package]] 2253 2275 name = "rfc6979" ··· 2316 2338 2317 2339 [[package]] 2318 2340 name = "rustix" 2319 - version = "1.0.8" 2341 + version = "1.1.2" 2320 2342 source = "registry+https://github.com/rust-lang/crates.io-index" 2321 - checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 2343 + checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 2322 2344 dependencies = [ 2323 2345 "bitflags", 2324 2346 "errno", 2325 2347 "libc", 2326 2348 "linux-raw-sys", 2327 - "windows-sys 0.60.2", 2349 + "windows-sys 0.61.1", 2328 2350 ] 2329 2351 2330 2352 [[package]] 2331 2353 name = "rustls" 2332 - version = "0.23.31" 2354 + version = "0.23.32" 2333 2355 source = "registry+https://github.com/rust-lang/crates.io-index" 2334 - checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 2356 + checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" 2335 2357 dependencies = [ 2336 2358 "once_cell", 2337 2359 "ring", ··· 2350 2372 "openssl-probe", 2351 2373 "rustls-pki-types", 2352 2374 "schannel", 2353 - "security-framework 3.3.0", 2375 + "security-framework 3.5.0", 2354 2376 ] 2355 2377 2356 2378 [[package]] ··· 2365 2387 2366 2388 [[package]] 2367 2389 name = "rustls-webpki" 2368 - version = "0.103.4" 2390 + version = "0.103.6" 2369 2391 source = "registry+https://github.com/rust-lang/crates.io-index" 2370 - checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 2392 + checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" 2371 2393 dependencies = [ 2372 2394 "ring", 2373 2395 "rustls-pki-types", ··· 2388 2410 2389 2411 [[package]] 2390 2412 name = "schannel" 2391 - version = "0.1.27" 2413 + version = "0.1.28" 2392 2414 source = "registry+https://github.com/rust-lang/crates.io-index" 2393 - checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 2415 + checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 2394 2416 dependencies = [ 2395 - "windows-sys 0.59.0", 2417 + "windows-sys 0.61.1", 2396 2418 ] 2397 - 2398 - [[package]] 2399 - name = "scoped-tls" 2400 - version = "1.0.1" 2401 - source = "registry+https://github.com/rust-lang/crates.io-index" 2402 - checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 2403 2419 2404 2420 [[package]] 2405 2421 name = "scopeguard" ··· 2437 2453 2438 2454 [[package]] 2439 2455 name = "security-framework" 2440 - version = "3.3.0" 2456 + version = "3.5.0" 2441 2457 source = "registry+https://github.com/rust-lang/crates.io-index" 2442 - checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" 2458 + checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" 2443 2459 dependencies = [ 2444 2460 "bitflags", 2445 2461 "core-foundation 0.10.1", ··· 2450 2466 2451 2467 [[package]] 2452 2468 name = "security-framework-sys" 2453 - version = "2.14.0" 2469 + version = "2.15.0" 2454 2470 source = "registry+https://github.com/rust-lang/crates.io-index" 2455 - checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 2471 + checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 2456 2472 dependencies = [ 2457 2473 "core-foundation-sys", 2458 2474 "libc", ··· 2460 2476 2461 2477 [[package]] 2462 2478 name = "semver" 2463 - version = "1.0.26" 2479 + version = "1.0.27" 2464 2480 source = "registry+https://github.com/rust-lang/crates.io-index" 2465 - checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 2481 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 2466 2482 2467 2483 [[package]] 2468 2484 name = "serde" 2469 - version = "1.0.219" 2485 + version = "1.0.227" 2470 2486 source = "registry+https://github.com/rust-lang/crates.io-index" 2471 - checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 2487 + checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" 2472 2488 dependencies = [ 2489 + "serde_core", 2473 2490 "serde_derive", 2474 2491 ] 2475 2492 2476 2493 [[package]] 2477 2494 name = "serde_bytes" 2478 - version = "0.11.17" 2495 + version = "0.11.19" 2479 2496 source = "registry+https://github.com/rust-lang/crates.io-index" 2480 - checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" 2497 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 2481 2498 dependencies = [ 2482 2499 "serde", 2500 + "serde_core", 2501 + ] 2502 + 2503 + [[package]] 2504 + name = "serde_core" 2505 + version = "1.0.227" 2506 + source = "registry+https://github.com/rust-lang/crates.io-index" 2507 + checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" 2508 + dependencies = [ 2509 + "serde_derive", 2483 2510 ] 2484 2511 2485 2512 [[package]] 2486 2513 name = "serde_derive" 2487 - version = "1.0.219" 2514 + version = "1.0.227" 2488 2515 source = "registry+https://github.com/rust-lang/crates.io-index" 2489 - checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 2516 + checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" 2490 2517 dependencies = [ 2491 2518 "proc-macro2", 2492 2519 "quote", ··· 2495 2522 2496 2523 [[package]] 2497 2524 name = "serde_html_form" 2498 - version = "0.2.7" 2525 + version = "0.2.8" 2499 2526 source = "registry+https://github.com/rust-lang/crates.io-index" 2500 - checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" 2527 + checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 2501 2528 dependencies = [ 2502 2529 "form_urlencoded", 2503 2530 "indexmap", 2504 2531 "itoa", 2505 2532 "ryu", 2506 - "serde", 2533 + "serde_core", 2507 2534 ] 2508 2535 2509 2536 [[package]] 2510 2537 name = "serde_ipld_dagcbor" 2511 - version = "0.6.3" 2538 + version = "0.6.4" 2512 2539 source = "registry+https://github.com/rust-lang/crates.io-index" 2513 - checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc" 2540 + checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" 2514 2541 dependencies = [ 2515 2542 "cbor4ii", 2516 2543 "ipld-core", ··· 2520 2547 2521 2548 [[package]] 2522 2549 name = "serde_json" 2523 - version = "1.0.142" 2550 + version = "1.0.145" 2524 2551 source = "registry+https://github.com/rust-lang/crates.io-index" 2525 - checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 2552 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 2526 2553 dependencies = [ 2527 2554 "itoa", 2528 2555 "memchr", 2529 2556 "ryu", 2530 2557 "serde", 2558 + "serde_core", 2531 2559 ] 2532 2560 2533 2561 [[package]] 2534 2562 name = "serde_path_to_error" 2535 - version = "0.1.17" 2563 + version = "0.1.20" 2536 2564 source = "registry+https://github.com/rust-lang/crates.io-index" 2537 - checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 2565 + checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 2538 2566 dependencies = [ 2539 2567 "itoa", 2540 2568 "serde", 2569 + "serde_core", 2541 2570 ] 2542 2571 2543 2572 [[package]] ··· 2574 2603 ] 2575 2604 2576 2605 [[package]] 2606 + name = "sha1_smol" 2607 + version = "1.0.1" 2608 + source = "registry+https://github.com/rust-lang/crates.io-index" 2609 + checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" 2610 + 2611 + [[package]] 2577 2612 name = "sha2" 2578 2613 version = "0.10.9" 2579 2614 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2646 2681 "chrono", 2647 2682 "dotenvy", 2648 2683 "futures-util", 2684 + "redis", 2649 2685 "regex", 2650 2686 "reqwest", 2651 2687 "reqwest-chain", ··· 2674 2710 "regex", 2675 2711 "serde", 2676 2712 "serde_json", 2677 - "thiserror 2.0.14", 2713 + "thiserror 2.0.16", 2678 2714 "unicode-segmentation", 2679 2715 ] 2680 2716 ··· 2756 2792 "futures-intrusive", 2757 2793 "futures-io", 2758 2794 "futures-util", 2759 - "hashbrown", 2795 + "hashbrown 0.15.5", 2760 2796 "hashlink", 2761 2797 "indexmap", 2762 2798 "log", ··· 2769 2805 "serde_json", 2770 2806 "sha2", 2771 2807 "smallvec", 2772 - "thiserror 2.0.14", 2808 + "thiserror 2.0.16", 2773 2809 "tokio", 2774 2810 "tokio-stream", 2775 2811 "tracing", ··· 2854 2890 "smallvec", 2855 2891 "sqlx-core", 2856 2892 "stringprep", 2857 - "thiserror 2.0.14", 2893 + "thiserror 2.0.16", 2858 2894 "tracing", 2859 2895 "uuid", 2860 2896 "whoami", ··· 2893 2929 "smallvec", 2894 2930 "sqlx-core", 2895 2931 "stringprep", 2896 - "thiserror 2.0.14", 2932 + "thiserror 2.0.16", 2897 2933 "tracing", 2898 2934 "uuid", 2899 2935 "whoami", ··· 2919 2955 "serde", 2920 2956 "serde_urlencoded", 2921 2957 "sqlx-core", 2922 - "thiserror 2.0.14", 2958 + "thiserror 2.0.16", 2923 2959 "tracing", 2924 2960 "url", 2925 2961 "uuid", ··· 3048 3084 3049 3085 [[package]] 3050 3086 name = "tempfile" 3051 - version = "3.20.0" 3087 + version = "3.23.0" 3052 3088 source = "registry+https://github.com/rust-lang/crates.io-index" 3053 - checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 3089 + checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 3054 3090 dependencies = [ 3055 3091 "fastrand", 3056 3092 "getrandom 0.3.3", 3057 3093 "once_cell", 3058 3094 "rustix", 3059 - "windows-sys 0.59.0", 3095 + "windows-sys 0.61.1", 3060 3096 ] 3061 3097 3062 3098 [[package]] ··· 3070 3106 3071 3107 [[package]] 3072 3108 name = "thiserror" 3073 - version = "2.0.14" 3109 + version = "2.0.16" 3074 3110 source = "registry+https://github.com/rust-lang/crates.io-index" 3075 - checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" 3111 + checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 3076 3112 dependencies = [ 3077 - "thiserror-impl 2.0.14", 3113 + "thiserror-impl 2.0.16", 3078 3114 ] 3079 3115 3080 3116 [[package]] ··· 3090 3126 3091 3127 [[package]] 3092 3128 name = "thiserror-impl" 3093 - version = "2.0.14" 3129 + version = "2.0.16" 3094 3130 source = "registry+https://github.com/rust-lang/crates.io-index" 3095 - checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" 3131 + checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 3096 3132 dependencies = [ 3097 3133 "proc-macro2", 3098 3134 "quote", ··· 3120 3156 3121 3157 [[package]] 3122 3158 name = "tinyvec" 3123 - version = "1.9.0" 3159 + version = "1.10.0" 3124 3160 source = "registry+https://github.com/rust-lang/crates.io-index" 3125 - checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 3161 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 3126 3162 dependencies = [ 3127 3163 "tinyvec_macros", 3128 3164 ] ··· 3176 3212 3177 3213 [[package]] 3178 3214 name = "tokio-rustls" 3179 - version = "0.26.2" 3215 + version = "0.26.4" 3180 3216 source = "registry+https://github.com/rust-lang/crates.io-index" 3181 - checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 3217 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 3182 3218 dependencies = [ 3183 3219 "rustls", 3184 3220 "tokio", ··· 3335 3371 3336 3372 [[package]] 3337 3373 name = "tracing-subscriber" 3338 - version = "0.3.19" 3374 + version = "0.3.20" 3339 3375 source = "registry+https://github.com/rust-lang/crates.io-index" 3340 - checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 3376 + checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 3341 3377 dependencies = [ 3342 3378 "matchers", 3343 3379 "nu-ansi-term", 3344 3380 "once_cell", 3345 - "regex", 3381 + "regex-automata", 3346 3382 "sharded-slab", 3347 3383 "smallvec", 3348 3384 "thread_local", ··· 3405 3441 3406 3442 [[package]] 3407 3443 name = "unicode-ident" 3408 - version = "1.0.18" 3444 + version = "1.0.19" 3409 3445 source = "registry+https://github.com/rust-lang/crates.io-index" 3410 - checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 3446 + checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 3411 3447 3412 3448 [[package]] 3413 3449 name = "unicode-normalization" ··· 3444 3480 3445 3481 [[package]] 3446 3482 name = "url" 3447 - version = "2.5.4" 3483 + version = "2.5.7" 3448 3484 source = "registry+https://github.com/rust-lang/crates.io-index" 3449 - checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 3485 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 3450 3486 dependencies = [ 3451 3487 "form_urlencoded", 3452 3488 "idna", 3453 3489 "percent-encoding", 3490 + "serde", 3454 3491 ] 3455 3492 3456 3493 [[package]] ··· 3473 3510 3474 3511 [[package]] 3475 3512 name = "uuid" 3476 - version = "1.18.0" 3513 + version = "1.18.1" 3477 3514 source = "registry+https://github.com/rust-lang/crates.io-index" 3478 - checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" 3515 + checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 3479 3516 dependencies = [ 3480 3517 "getrandom 0.3.3", 3481 3518 "js-sys", ··· 3518 3555 3519 3556 [[package]] 3520 3557 name = "wasi" 3521 - version = "0.14.2+wasi-0.2.4" 3558 + version = "0.14.7+wasi-0.2.4" 3559 + source = "registry+https://github.com/rust-lang/crates.io-index" 3560 + checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" 3561 + dependencies = [ 3562 + "wasip2", 3563 + ] 3564 + 3565 + [[package]] 3566 + name = "wasip2" 3567 + version = "1.0.1+wasi-0.2.4" 3522 3568 source = "registry+https://github.com/rust-lang/crates.io-index" 3523 - checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 3569 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 3524 3570 dependencies = [ 3525 - "wit-bindgen-rt", 3571 + "wit-bindgen", 3526 3572 ] 3527 3573 3528 3574 [[package]] ··· 3533 3579 3534 3580 [[package]] 3535 3581 name = "wasm-bindgen" 3536 - version = "0.2.100" 3582 + version = "0.2.104" 3537 3583 source = "registry+https://github.com/rust-lang/crates.io-index" 3538 - checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 3584 + checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" 3539 3585 dependencies = [ 3540 3586 "cfg-if", 3541 3587 "once_cell", 3542 3588 "rustversion", 3543 3589 "wasm-bindgen-macro", 3590 + "wasm-bindgen-shared", 3544 3591 ] 3545 3592 3546 3593 [[package]] 3547 3594 name = "wasm-bindgen-backend" 3548 - version = "0.2.100" 3595 + version = "0.2.104" 3549 3596 source = "registry+https://github.com/rust-lang/crates.io-index" 3550 - checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 3597 + checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" 3551 3598 dependencies = [ 3552 3599 "bumpalo", 3553 3600 "log", ··· 3559 3606 3560 3607 [[package]] 3561 3608 name = "wasm-bindgen-futures" 3562 - version = "0.4.50" 3609 + version = "0.4.54" 3563 3610 source = "registry+https://github.com/rust-lang/crates.io-index" 3564 - checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 3611 + checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" 3565 3612 dependencies = [ 3566 3613 "cfg-if", 3567 3614 "js-sys", ··· 3572 3619 3573 3620 [[package]] 3574 3621 name = "wasm-bindgen-macro" 3575 - version = "0.2.100" 3622 + version = "0.2.104" 3576 3623 source = "registry+https://github.com/rust-lang/crates.io-index" 3577 - checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 3624 + checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" 3578 3625 dependencies = [ 3579 3626 "quote", 3580 3627 "wasm-bindgen-macro-support", ··· 3582 3629 3583 3630 [[package]] 3584 3631 name = "wasm-bindgen-macro-support" 3585 - version = "0.2.100" 3632 + version = "0.2.104" 3586 3633 source = "registry+https://github.com/rust-lang/crates.io-index" 3587 - checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 3634 + checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" 3588 3635 dependencies = [ 3589 3636 "proc-macro2", 3590 3637 "quote", ··· 3595 3642 3596 3643 [[package]] 3597 3644 name = "wasm-bindgen-shared" 3598 - version = "0.2.100" 3645 + version = "0.2.104" 3599 3646 source = "registry+https://github.com/rust-lang/crates.io-index" 3600 - checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 3647 + checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" 3601 3648 dependencies = [ 3602 3649 "unicode-ident", 3603 3650 ] ··· 3617 3664 3618 3665 [[package]] 3619 3666 name = "web-sys" 3620 - version = "0.3.77" 3667 + version = "0.3.81" 3621 3668 source = "registry+https://github.com/rust-lang/crates.io-index" 3622 - checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 3669 + checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" 3623 3670 dependencies = [ 3624 3671 "js-sys", 3625 3672 "wasm-bindgen", ··· 3670 3717 checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 3671 3718 3672 3719 [[package]] 3673 - name = "winapi" 3674 - version = "0.3.9" 3675 - source = "registry+https://github.com/rust-lang/crates.io-index" 3676 - checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 3677 - dependencies = [ 3678 - "winapi-i686-pc-windows-gnu", 3679 - "winapi-x86_64-pc-windows-gnu", 3680 - ] 3681 - 3682 - [[package]] 3683 - name = "winapi-i686-pc-windows-gnu" 3684 - version = "0.4.0" 3685 - source = "registry+https://github.com/rust-lang/crates.io-index" 3686 - checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3687 - 3688 - [[package]] 3689 - name = "winapi-x86_64-pc-windows-gnu" 3690 - version = "0.4.0" 3691 - source = "registry+https://github.com/rust-lang/crates.io-index" 3692 - checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 3693 - 3694 - [[package]] 3695 - name = "windows" 3696 - version = "0.61.3" 3697 - source = "registry+https://github.com/rust-lang/crates.io-index" 3698 - checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 3699 - dependencies = [ 3700 - "windows-collections", 3701 - "windows-core", 3702 - "windows-future", 3703 - "windows-link", 3704 - "windows-numerics", 3705 - ] 3706 - 3707 - [[package]] 3708 - name = "windows-collections" 3709 - version = "0.2.0" 3710 - source = "registry+https://github.com/rust-lang/crates.io-index" 3711 - checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 3712 - dependencies = [ 3713 - "windows-core", 3714 - ] 3715 - 3716 - [[package]] 3717 3720 name = "windows-core" 3718 - version = "0.61.2" 3721 + version = "0.62.1" 3719 3722 source = "registry+https://github.com/rust-lang/crates.io-index" 3720 - checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 3723 + checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" 3721 3724 dependencies = [ 3722 3725 "windows-implement", 3723 3726 "windows-interface", 3724 - "windows-link", 3725 - "windows-result", 3726 - "windows-strings", 3727 - ] 3728 - 3729 - [[package]] 3730 - name = "windows-future" 3731 - version = "0.2.1" 3732 - source = "registry+https://github.com/rust-lang/crates.io-index" 3733 - checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 3734 - dependencies = [ 3735 - "windows-core", 3736 - "windows-link", 3737 - "windows-threading", 3727 + "windows-link 0.2.0", 3728 + "windows-result 0.4.0", 3729 + "windows-strings 0.5.0", 3738 3730 ] 3739 3731 3740 3732 [[package]] 3741 3733 name = "windows-implement" 3742 - version = "0.60.0" 3734 + version = "0.60.1" 3743 3735 source = "registry+https://github.com/rust-lang/crates.io-index" 3744 - checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 3736 + checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" 3745 3737 dependencies = [ 3746 3738 "proc-macro2", 3747 3739 "quote", ··· 3750 3742 3751 3743 [[package]] 3752 3744 name = "windows-interface" 3753 - version = "0.59.1" 3745 + version = "0.59.2" 3754 3746 source = "registry+https://github.com/rust-lang/crates.io-index" 3755 - checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 3747 + checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" 3756 3748 dependencies = [ 3757 3749 "proc-macro2", 3758 3750 "quote", ··· 3766 3758 checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 3767 3759 3768 3760 [[package]] 3769 - name = "windows-numerics" 3761 + name = "windows-link" 3770 3762 version = "0.2.0" 3771 3763 source = "registry+https://github.com/rust-lang/crates.io-index" 3772 - checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 3773 - dependencies = [ 3774 - "windows-core", 3775 - "windows-link", 3776 - ] 3764 + checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 3777 3765 3778 3766 [[package]] 3779 3767 name = "windows-registry" ··· 3781 3769 source = "registry+https://github.com/rust-lang/crates.io-index" 3782 3770 checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 3783 3771 dependencies = [ 3784 - "windows-link", 3785 - "windows-result", 3786 - "windows-strings", 3772 + "windows-link 0.1.3", 3773 + "windows-result 0.3.4", 3774 + "windows-strings 0.4.2", 3787 3775 ] 3788 3776 3789 3777 [[package]] ··· 3792 3780 source = "registry+https://github.com/rust-lang/crates.io-index" 3793 3781 checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 3794 3782 dependencies = [ 3795 - "windows-link", 3783 + "windows-link 0.1.3", 3784 + ] 3785 + 3786 + [[package]] 3787 + name = "windows-result" 3788 + version = "0.4.0" 3789 + source = "registry+https://github.com/rust-lang/crates.io-index" 3790 + checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" 3791 + dependencies = [ 3792 + "windows-link 0.2.0", 3796 3793 ] 3797 3794 3798 3795 [[package]] ··· 3801 3798 source = "registry+https://github.com/rust-lang/crates.io-index" 3802 3799 checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 3803 3800 dependencies = [ 3804 - "windows-link", 3801 + "windows-link 0.1.3", 3802 + ] 3803 + 3804 + [[package]] 3805 + name = "windows-strings" 3806 + version = "0.5.0" 3807 + source = "registry+https://github.com/rust-lang/crates.io-index" 3808 + checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" 3809 + dependencies = [ 3810 + "windows-link 0.2.0", 3805 3811 ] 3806 3812 3807 3813 [[package]] ··· 3837 3843 source = "registry+https://github.com/rust-lang/crates.io-index" 3838 3844 checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3839 3845 dependencies = [ 3840 - "windows-targets 0.53.3", 3846 + "windows-targets 0.53.4", 3847 + ] 3848 + 3849 + [[package]] 3850 + name = "windows-sys" 3851 + version = "0.61.1" 3852 + source = "registry+https://github.com/rust-lang/crates.io-index" 3853 + checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" 3854 + dependencies = [ 3855 + "windows-link 0.2.0", 3841 3856 ] 3842 3857 3843 3858 [[package]] ··· 3873 3888 3874 3889 [[package]] 3875 3890 name = "windows-targets" 3876 - version = "0.53.3" 3891 + version = "0.53.4" 3877 3892 source = "registry+https://github.com/rust-lang/crates.io-index" 3878 - checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 3893 + checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" 3879 3894 dependencies = [ 3880 - "windows-link", 3895 + "windows-link 0.2.0", 3881 3896 "windows_aarch64_gnullvm 0.53.0", 3882 3897 "windows_aarch64_msvc 0.53.0", 3883 3898 "windows_i686_gnu 0.53.0", ··· 3886 3901 "windows_x86_64_gnu 0.53.0", 3887 3902 "windows_x86_64_gnullvm 0.53.0", 3888 3903 "windows_x86_64_msvc 0.53.0", 3889 - ] 3890 - 3891 - [[package]] 3892 - name = "windows-threading" 3893 - version = "0.1.0" 3894 - source = "registry+https://github.com/rust-lang/crates.io-index" 3895 - checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 3896 - dependencies = [ 3897 - "windows-link", 3898 3904 ] 3899 3905 3900 3906 [[package]] ··· 4046 4052 ] 4047 4053 4048 4054 [[package]] 4049 - name = "wit-bindgen-rt" 4050 - version = "0.39.0" 4055 + name = "wit-bindgen" 4056 + version = "0.46.0" 4051 4057 source = "registry+https://github.com/rust-lang/crates.io-index" 4052 - checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 4053 - dependencies = [ 4054 - "bitflags", 4055 - ] 4058 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 4056 4059 4057 4060 [[package]] 4058 4061 name = "writeable" ··· 4086 4089 4087 4090 [[package]] 4088 4091 name = "zerocopy" 4089 - version = "0.8.26" 4092 + version = "0.8.27" 4090 4093 source = "registry+https://github.com/rust-lang/crates.io-index" 4091 - checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 4094 + checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 4092 4095 dependencies = [ 4093 4096 "zerocopy-derive", 4094 4097 ] 4095 4098 4096 4099 [[package]] 4097 4100 name = "zerocopy-derive" 4098 - version = "0.8.26" 4101 + version = "0.8.27" 4099 4102 source = "registry+https://github.com/rust-lang/crates.io-index" 4100 - checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 4103 + checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 4101 4104 dependencies = [ 4102 4105 "proc-macro2", 4103 4106 "quote", ··· 4184 4187 4185 4188 [[package]] 4186 4189 name = "zstd-sys" 4187 - version = "2.0.15+zstd.1.5.7" 4190 + version = "2.0.16+zstd.1.5.7" 4188 4191 source = "registry+https://github.com/rust-lang/crates.io-index" 4189 - checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 4192 + checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 4190 4193 dependencies = [ 4191 4194 "cc", 4192 4195 "pkg-config",
+7 -4
api/Cargo.toml
··· 49 49 async-trait = "0.1" 50 50 51 51 # AT Protocol client 52 - atproto-client = "0.13.0" 53 - atproto-identity = "0.13.0" 54 - atproto-oauth = "0.13.0" 55 - atproto-jetstream = "0.13.0" 52 + atproto-client = "0.12.0" 53 + atproto-identity = "0.12.0" 54 + atproto-oauth = "0.12.0" 55 + atproto-jetstream = "0.12.0" 56 56 57 57 58 58 # Middleware for HTTP requests with retry logic ··· 62 62 # Job queue 63 63 sqlxmq = "0.6" 64 64 regex = "1.11.2" 65 + 66 + # Redis for caching 67 + redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] }
+1 -1
api/flake.nix
··· 73 73 74 74 # Fix for linker issues in Nix 75 75 CC = "${pkgs.stdenv.cc}/bin/cc"; 76 - 76 + 77 77 cargoExtraArgs = "--bin slices"; 78 78 }; 79 79
+65 -1
api/src/actor_resolver.rs
··· 5 5 web::query as web_query, 6 6 }; 7 7 use thiserror::Error; 8 + use std::sync::Arc; 9 + use tokio::sync::Mutex; 10 + use serde::{Serialize, Deserialize}; 11 + use crate::cache::SliceCache; 8 12 9 13 #[derive(Error, Debug)] 10 14 pub enum ActorResolverError { ··· 18 22 InvalidSubject, 19 23 } 20 24 21 - #[derive(Debug, Clone)] 25 + #[derive(Debug, Clone, Serialize, Deserialize)] 22 26 pub struct ActorData { 23 27 pub did: String, 24 28 pub handle: Option<String>, ··· 26 30 } 27 31 28 32 pub async fn resolve_actor_data(client: &Client, did: &str) -> Result<ActorData, ActorResolverError> { 33 + resolve_actor_data_cached(client, did, None).await 34 + } 35 + 36 + pub async fn resolve_actor_data_cached( 37 + client: &Client, 38 + did: &str, 39 + cache: Option<Arc<Mutex<SliceCache>>> 40 + ) -> Result<ActorData, ActorResolverError> { 41 + // Try cache first if provided 42 + if let Some(cache) = &cache { 43 + let cached_result = { 44 + let mut cache_lock = cache.lock().await; 45 + cache_lock.get_cached_did_resolution(did).await 46 + }; 47 + 48 + if let Ok(Some(actor_data_value)) = cached_result 49 + && let Ok(actor_data) = serde_json::from_value::<ActorData>(actor_data_value) { 50 + return Ok(actor_data); 51 + } 52 + } 53 + 54 + // Cache miss - resolve from PLC/web 55 + let actor_data = resolve_actor_data_impl(client, did).await?; 56 + 57 + // Cache the result if cache is provided 58 + if let Some(cache) = &cache 59 + && let Ok(actor_data_value) = serde_json::to_value(&actor_data) { 60 + let mut cache_lock = cache.lock().await; 61 + let _ = cache_lock.cache_did_resolution(did, &actor_data_value).await; 62 + } 63 + 64 + Ok(actor_data) 65 + } 66 + 67 + pub async fn resolve_actor_data_with_retry( 68 + client: &Client, 69 + did: &str, 70 + cache: Option<Arc<Mutex<SliceCache>>>, 71 + invalidate_cache_on_retry: bool 72 + ) -> Result<ActorData, ActorResolverError> { 73 + match resolve_actor_data_cached(client, did, cache.clone()).await { 74 + Ok(actor_data) => Ok(actor_data), 75 + Err(e) => { 76 + // If we should invalidate cache on retry and we have a cache 77 + if invalidate_cache_on_retry { 78 + if let Some(cache) = &cache { 79 + let mut cache_lock = cache.lock().await; 80 + let _ = cache_lock.invalidate_did_resolution(did).await; 81 + } 82 + 83 + // Retry once with fresh resolution 84 + resolve_actor_data_cached(client, did, cache).await 85 + } else { 86 + Err(e) 87 + } 88 + } 89 + } 90 + } 91 + 92 + async fn resolve_actor_data_impl(client: &Client, did: &str) -> Result<ActorData, ActorResolverError> { 29 93 let (pds_url, handle) = match parse_input(did) { 30 94 Ok(InputType::Plc(did_str)) => { 31 95 match plc_query(client, "plc.directory", &did_str).await {
+10 -10
api/src/api/xrpc_dynamic.rs
··· 11 11 use serde::Deserialize; 12 12 13 13 use crate::AppState; 14 - use crate::auth::{extract_bearer_token, get_atproto_auth_for_user, verify_oauth_token}; 14 + use crate::auth::{extract_bearer_token, get_atproto_auth_for_user_cached, verify_oauth_token_cached}; 15 15 use crate::models::{ 16 16 IndexedRecord, Record, SliceRecordsOutput, SliceRecordsParams, SortField, WhereCondition, 17 17 }; ··· 526 526 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 527 527 // Extract and verify OAuth token 528 528 let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 529 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 529 + let user_info = verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())) 530 530 .await 531 531 .map_err(status_to_error_response)?; 532 532 533 - // Get AT Protocol DPoP auth and PDS URL 534 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url) 533 + // Get AT Protocol DPoP auth and PDS URL (with caching) 534 + let (dpop_auth, pds_url) = get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())) 535 535 .await 536 536 .map_err(status_to_error_response)?; 537 537 ··· 644 644 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 645 645 // Extract and verify OAuth token 646 646 let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 647 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 647 + let user_info = verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())) 648 648 .await 649 649 .map_err(status_to_error_response)?; 650 650 651 - // Get AT Protocol DPoP auth and PDS URL 652 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url) 651 + // Get AT Protocol DPoP auth and PDS URL (with caching) 652 + let (dpop_auth, pds_url) = get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())) 653 653 .await 654 654 .map_err(status_to_error_response)?; 655 655 ··· 762 762 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 763 763 // Extract and verify OAuth token 764 764 let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 765 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 765 + let user_info = verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())) 766 766 .await 767 767 .map_err(status_to_error_response)?; 768 768 769 - // Get AT Protocol DPoP auth and PDS URL 770 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url) 769 + // Get AT Protocol DPoP auth and PDS URL (with caching) 770 + let (dpop_auth, pds_url) = get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())) 771 771 .await 772 772 .map_err(status_to_error_response)?; 773 773
+92 -18
api/src/auth.rs
··· 3 3 use atproto_client::client::DPoPAuth; 4 4 use atproto_identity::key::KeyData; 5 5 use atproto_oauth::jwk::WrappedJsonWebKey; 6 + use std::sync::Arc; 7 + use tokio::sync::Mutex; 8 + use crate::cache::SliceCache; 6 9 7 10 #[derive(Serialize, Deserialize, Debug)] 8 11 pub struct UserInfoResponse { ··· 10 13 pub did: Option<String>, 11 14 } 12 15 16 + #[derive(Serialize, Deserialize, Debug, Clone)] 17 + struct CachedSession { 18 + pds_url: String, 19 + atproto_access_token: String, 20 + dpop_jwk: serde_json::Value, 21 + } 22 + 13 23 // Extract bearer token from Authorization header 14 24 pub fn extract_bearer_token(headers: &HeaderMap) -> Result<String, StatusCode> { 15 25 let auth_header = headers ··· 26 36 } 27 37 28 38 // Verify OAuth token with auth server 29 - pub async fn verify_oauth_token(token: &str, auth_base_url: &str) -> Result<UserInfoResponse, StatusCode> { 39 + 40 + // Verify OAuth token with auth server with optional caching 41 + pub async fn verify_oauth_token_cached( 42 + token: &str, 43 + auth_base_url: &str, 44 + cache: Option<Arc<Mutex<SliceCache>>>, 45 + ) -> Result<UserInfoResponse, StatusCode> { 46 + 47 + // Try cache first if provided 48 + if let Some(cache) = &cache { 49 + let cached_result = { 50 + let mut cache_lock = cache.lock().await; 51 + cache_lock.get_cached_oauth_userinfo(token).await 52 + }; 53 + 54 + if let Ok(Some(user_info_value)) = cached_result { 55 + let user_info: UserInfoResponse = serde_json::from_value(user_info_value) 56 + .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 57 + return Ok(user_info); 58 + } 59 + } 60 + 61 + // Cache miss - verify with auth server 30 62 let client = reqwest::Client::new(); 31 63 let userinfo_url = format!("{}/oauth/userinfo", auth_base_url); 32 64 ··· 36 68 .send() 37 69 .await 38 70 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 39 - 40 71 41 72 if !response.status().is_success() { 42 73 return Err(StatusCode::UNAUTHORIZED); ··· 47 78 .await 48 79 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 49 80 81 + // Cache the userinfo if cache is provided (5 minute TTL) 82 + if let Some(cache) = &cache { 83 + let user_info_value = serde_json::to_value(&user_info) 84 + .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 85 + let mut cache_lock = cache.lock().await; 86 + let _ = cache_lock.cache_oauth_userinfo(token, &user_info_value, 300).await; 87 + } 88 + 50 89 Ok(user_info) 51 90 } 52 91 53 92 // Get AT Protocol DPoP auth and PDS URL for the user 54 - pub async fn get_atproto_auth_for_user( 93 + 94 + // Get AT Protocol DPoP auth and PDS URL for the user with optional caching 95 + pub async fn get_atproto_auth_for_user_cached( 55 96 token: &str, 56 97 auth_base_url: &str, 98 + cache: Option<Arc<Mutex<SliceCache>>>, 57 99 ) -> Result<(DPoPAuth, String), StatusCode> { 58 - // First get session info from auth server 100 + 101 + // Try cache first if provided 102 + if let Some(cache) = &cache { 103 + let cached_result = { 104 + let mut cache_lock = cache.lock().await; 105 + cache_lock.get_cached_atproto_session(token).await 106 + }; 107 + 108 + if let Ok(Some(session_value)) = cached_result { 109 + let cached_session: CachedSession = serde_json::from_value(session_value) 110 + .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 111 + 112 + // Convert cached data back to DPoP auth 113 + let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(cached_session.dpop_jwk) 114 + .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 115 + 116 + let dpop_private_key_data = KeyData::try_from(dpop_jwk) 117 + .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 118 + 119 + let dpop_auth = DPoPAuth { 120 + dpop_private_key_data, 121 + oauth_access_token: cached_session.atproto_access_token, 122 + }; 123 + 124 + return Ok((dpop_auth, cached_session.pds_url)); 125 + } 126 + } 127 + 128 + // Cache miss - fetch from auth server 59 129 let client = reqwest::Client::new(); 60 130 let session_url = format!("{}/api/atprotocol/session", auth_base_url); 61 131 ··· 66 136 .await 67 137 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 68 138 69 - 70 139 if !session_response.status().is_success() { 71 140 return Err(StatusCode::UNAUTHORIZED); 72 141 } ··· 75 144 .json() 76 145 .await 77 146 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 78 - 79 147 80 148 // Extract PDS URL from session 81 149 let pds_url = session_data["pds_endpoint"] 82 150 .as_str() 83 - .ok_or({ 84 - StatusCode::INTERNAL_SERVER_ERROR 85 - })? 151 + .ok_or(StatusCode::INTERNAL_SERVER_ERROR)? 86 152 .to_string(); 87 - 88 153 89 154 // Extract AT Protocol access token from session data 90 155 let atproto_access_token = session_data["access_token"] 91 156 .as_str() 92 - .ok_or({ 93 - StatusCode::INTERNAL_SERVER_ERROR 94 - })? 157 + .ok_or(StatusCode::INTERNAL_SERVER_ERROR)? 95 158 .to_string(); 96 - 97 159 98 160 // Extract DPoP private key from session data - convert JWK to KeyData 99 - let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(session_data["dpop_jwk"].clone()) 161 + let dpop_jwk_value = session_data["dpop_jwk"].clone(); 162 + let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(dpop_jwk_value.clone()) 100 163 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 101 - 102 164 103 165 let dpop_private_key_data = KeyData::try_from(dpop_jwk) 104 166 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 105 167 106 - 107 168 let dpop_auth = DPoPAuth { 108 169 dpop_private_key_data, 109 - oauth_access_token: atproto_access_token, 170 + oauth_access_token: atproto_access_token.clone(), 110 171 }; 172 + 173 + // Cache the session data if cache is provided (5 minute TTL) 174 + if let Some(cache) = &cache { 175 + let cached_session = CachedSession { 176 + pds_url: pds_url.clone(), 177 + atproto_access_token, 178 + dpop_jwk: dpop_jwk_value, 179 + }; 180 + let session_value = serde_json::to_value(&cached_session) 181 + .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; 182 + let mut cache_lock = cache.lock().await; 183 + let _ = cache_lock.cache_atproto_session(token, &session_value, 300).await; 184 + } 111 185 112 186 Ok((dpop_auth, pds_url)) 113 187 }
+455
api/src/cache.rs
··· 1 + use async_trait::async_trait; 2 + use anyhow::Result; 3 + use serde::{Serialize, Deserialize}; 4 + use std::collections::{HashMap, HashSet}; 5 + use std::sync::Arc; 6 + use tokio::sync::RwLock; 7 + use tracing::{debug, info, warn}; 8 + use std::time::{Duration, Instant}; 9 + 10 + /// Generic cache trait for different backend implementations 11 + #[async_trait] 12 + pub trait Cache: Send + Sync { 13 + /// Get a value from cache 14 + async fn get<T>(&mut self, key: &str) -> Result<Option<T>> 15 + where 16 + T: for<'de> Deserialize<'de> + Send; 17 + 18 + /// Set a value in cache with optional TTL 19 + async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()> 20 + where 21 + T: Serialize + Send + Sync; 22 + 23 + 24 + /// Delete a key from cache 25 + async fn delete(&mut self, key: &str) -> Result<()>; 26 + 27 + /// Set multiple key-value pairs 28 + async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()> 29 + where 30 + T: Serialize + Send + Sync; 31 + 32 + /// Test cache connection/health 33 + async fn ping(&mut self) -> Result<bool>; 34 + 35 + /// Get cache info/statistics 36 + async fn get_info(&mut self) -> Result<String>; 37 + } 38 + 39 + /// Cache entry type: (serialized_value, expiry) 40 + type CacheEntry = (String, Option<Instant>); 41 + 42 + /// In-memory cache implementation with TTL support 43 + pub struct InMemoryCache { 44 + data: Arc<RwLock<HashMap<String, CacheEntry>>>, 45 + default_ttl_seconds: u64, 46 + } 47 + 48 + impl InMemoryCache { 49 + pub fn new(default_ttl_seconds: Option<u64>) -> Self { 50 + Self { 51 + data: Arc::new(RwLock::new(HashMap::new())), 52 + default_ttl_seconds: default_ttl_seconds.unwrap_or(3600), 53 + } 54 + } 55 + 56 + } 57 + 58 + #[async_trait] 59 + impl Cache for InMemoryCache { 60 + async fn get<T>(&mut self, key: &str) -> Result<Option<T>> 61 + where 62 + T: for<'de> Deserialize<'de> + Send, 63 + { 64 + let data = self.data.read().await; 65 + 66 + if let Some((serialized, expiry)) = data.get(key) { 67 + // Check if expired 68 + if let Some(exp) = expiry 69 + && *exp <= Instant::now() { 70 + debug!(cache_key = %key, "Cache entry expired"); 71 + return Ok(None); 72 + } 73 + 74 + match serde_json::from_str::<T>(serialized) { 75 + Ok(value) => { 76 + // Cache hit - no logging needed 77 + Ok(Some(value)) 78 + } 79 + Err(e) => { 80 + warn!( 81 + error = ?e, 82 + cache_key = %key, 83 + "Failed to deserialize cached value" 84 + ); 85 + Ok(None) 86 + } 87 + } 88 + } else { 89 + // Cache miss - no logging needed 90 + Ok(None) 91 + } 92 + } 93 + 94 + async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()> 95 + where 96 + T: Serialize + Send + Sync, 97 + { 98 + let ttl = ttl_seconds.unwrap_or(self.default_ttl_seconds); 99 + 100 + match serde_json::to_string(value) { 101 + Ok(serialized) => { 102 + let expiry = if ttl > 0 { 103 + Some(Instant::now() + Duration::from_secs(ttl)) 104 + } else { 105 + None // No expiry 106 + }; 107 + 108 + let mut data = self.data.write().await; 109 + data.insert(key.to_string(), (serialized, expiry)); 110 + 111 + debug!( 112 + cache_key = %key, 113 + ttl_seconds = ttl, 114 + "Cached value in memory" 115 + ); 116 + Ok(()) 117 + } 118 + Err(e) => { 119 + warn!( 120 + error = ?e, 121 + cache_key = %key, 122 + "Failed to serialize value for caching" 123 + ); 124 + Ok(()) 125 + } 126 + } 127 + } 128 + 129 + 130 + async fn delete(&mut self, key: &str) -> Result<()> { 131 + let mut data = self.data.write().await; 132 + data.remove(key); 133 + debug!(cache_key = %key, "Deleted key from in-memory cache"); 134 + Ok(()) 135 + } 136 + 137 + async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()> 138 + where 139 + T: Serialize + Send + Sync, 140 + { 141 + if items.is_empty() { 142 + return Ok(()); 143 + } 144 + 145 + let mut data = self.data.write().await; 146 + let mut success_count = 0; 147 + 148 + for (key, value, ttl) in &items { 149 + match serde_json::to_string(value) { 150 + Ok(serialized) => { 151 + let ttl_to_use = ttl.unwrap_or(self.default_ttl_seconds); 152 + let expiry = if ttl_to_use > 0 { 153 + Some(Instant::now() + Duration::from_secs(ttl_to_use)) 154 + } else { 155 + None 156 + }; 157 + 158 + data.insert(key.to_string(), (serialized, expiry)); 159 + success_count += 1; 160 + } 161 + Err(e) => { 162 + warn!( 163 + error = ?e, 164 + cache_key = %key, 165 + "Failed to serialize value for bulk caching" 166 + ); 167 + } 168 + } 169 + } 170 + 171 + debug!( 172 + items_count = success_count, 173 + total_items = items.len(), 174 + "Successfully bulk cached items in memory" 175 + ); 176 + Ok(()) 177 + } 178 + 179 + async fn ping(&mut self) -> Result<bool> { 180 + // Always healthy for in-memory cache 181 + Ok(true) 182 + } 183 + 184 + async fn get_info(&mut self) -> Result<String> { 185 + let data = self.data.read().await; 186 + let now = Instant::now(); 187 + 188 + let mut total_entries = 0; 189 + let mut expired_entries = 0; 190 + 191 + for (_, expiry) in data.values() { 192 + total_entries += 1; 193 + if let Some(exp) = expiry 194 + && *exp <= now { 195 + expired_entries += 1; 196 + } 197 + } 198 + 199 + Ok(format!( 200 + "InMemoryCache: {} total entries, {} expired, {} active", 201 + total_entries, 202 + expired_entries, 203 + total_entries - expired_entries 204 + )) 205 + } 206 + } 207 + 208 + /// Cache backend enum to avoid dyn trait issues 209 + pub enum CacheBackendImpl { 210 + InMemory(InMemoryCache), 211 + Redis(crate::redis_cache::RedisCache), 212 + } 213 + 214 + impl CacheBackendImpl { 215 + pub async fn get<T>(&mut self, key: &str) -> Result<Option<T>> 216 + where 217 + T: for<'de> Deserialize<'de> + Send, 218 + { 219 + match self { 220 + CacheBackendImpl::InMemory(cache) => cache.get(key).await, 221 + CacheBackendImpl::Redis(cache) => cache.get(key).await, 222 + } 223 + } 224 + 225 + pub async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()> 226 + where 227 + T: Serialize + Send + Sync, 228 + { 229 + match self { 230 + CacheBackendImpl::InMemory(cache) => cache.set(key, value, ttl_seconds).await, 231 + CacheBackendImpl::Redis(cache) => cache.set(key, value, ttl_seconds).await, 232 + } 233 + } 234 + 235 + pub async fn delete(&mut self, key: &str) -> Result<()> { 236 + match self { 237 + CacheBackendImpl::InMemory(cache) => cache.delete(key).await, 238 + CacheBackendImpl::Redis(cache) => cache.delete(key).await, 239 + } 240 + } 241 + 242 + pub async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()> 243 + where 244 + T: Serialize + Send + Sync, 245 + { 246 + match self { 247 + CacheBackendImpl::InMemory(cache) => cache.set_multiple(items).await, 248 + CacheBackendImpl::Redis(cache) => cache.set_multiple(items).await, 249 + } 250 + } 251 + 252 + pub async fn ping(&mut self) -> Result<bool> { 253 + match self { 254 + CacheBackendImpl::InMemory(cache) => cache.ping().await, 255 + CacheBackendImpl::Redis(cache) => cache.ping().await, 256 + } 257 + } 258 + 259 + pub async fn get_info(&mut self) -> Result<String> { 260 + match self { 261 + CacheBackendImpl::InMemory(cache) => cache.get_info().await, 262 + CacheBackendImpl::Redis(cache) => cache.get_info().await, 263 + } 264 + } 265 + } 266 + 267 + /// Cache-specific helper methods for slice operations 268 + pub struct SliceCache { 269 + cache: CacheBackendImpl, 270 + } 271 + 272 + impl SliceCache { 273 + pub fn new(cache: CacheBackendImpl) -> Self { 274 + Self { cache } 275 + } 276 + 277 + /// Actor cache methods 278 + pub async fn is_actor(&mut self, did: &str, slice_uri: &str) -> Result<Option<bool>> { 279 + let key = format!("actor:{}:{}", did, slice_uri); 280 + self.cache.get::<bool>(&key).await 281 + } 282 + 283 + pub async fn cache_actor_exists(&mut self, did: &str, slice_uri: &str) -> Result<()> { 284 + let key = format!("actor:{}:{}", did, slice_uri); 285 + self.cache.set(&key, &true, None).await 286 + } 287 + 288 + pub async fn remove_actor(&mut self, did: &str, slice_uri: &str) -> Result<()> { 289 + let key = format!("actor:{}:{}", did, slice_uri); 290 + self.cache.delete(&key).await 291 + } 292 + 293 + pub async fn preload_actors(&mut self, actors: Vec<(String, String)>) -> Result<()> { 294 + if actors.is_empty() { 295 + return Ok(()); 296 + } 297 + 298 + let items: Vec<(String, bool, Option<u64>)> = actors 299 + .into_iter() 300 + .map(|(did, slice_uri)| { 301 + (format!("actor:{}:{}", did, slice_uri), true, None) 302 + }) 303 + .collect(); 304 + 305 + let items_ref: Vec<(&str, &bool, Option<u64>)> = items 306 + .iter() 307 + .map(|(key, value, ttl)| (key.as_str(), value, *ttl)) 308 + .collect(); 309 + 310 + self.cache.set_multiple(items_ref).await 311 + } 312 + 313 + /// Lexicon cache methods 314 + pub async fn cache_lexicons(&mut self, slice_uri: &str, lexicons: &Vec<serde_json::Value>) -> Result<()> { 315 + let key = format!("lexicons:{}", slice_uri); 316 + let lexicons_ttl = 7200; // 2 hours for lexicons 317 + self.cache.set(&key, lexicons, Some(lexicons_ttl)).await 318 + } 319 + 320 + pub async fn get_lexicons(&mut self, slice_uri: &str) -> Result<Option<Vec<serde_json::Value>>> { 321 + let key = format!("lexicons:{}", slice_uri); 322 + self.cache.get::<Vec<serde_json::Value>>(&key).await 323 + } 324 + 325 + /// Domain cache methods 326 + pub async fn cache_slice_domain(&mut self, slice_uri: &str, domain: &str) -> Result<()> { 327 + let key = format!("domain:{}", slice_uri); 328 + let domain_ttl = 14400; // 4 hours for domains 329 + self.cache.set(&key, &domain.to_string(), Some(domain_ttl)).await 330 + } 331 + 332 + pub async fn get_slice_domain(&mut self, slice_uri: &str) -> Result<Option<String>> { 333 + let key = format!("domain:{}", slice_uri); 334 + self.cache.get::<String>(&key).await 335 + } 336 + 337 + /// Collections cache methods 338 + pub async fn cache_slice_collections(&mut self, slice_uri: &str, collections: &HashSet<String>) -> Result<()> { 339 + let key = format!("collections:{}", slice_uri); 340 + let collections_ttl = 7200; // 2 hours for collections 341 + self.cache.set(&key, collections, Some(collections_ttl)).await 342 + } 343 + 344 + pub async fn get_slice_collections(&mut self, slice_uri: &str) -> Result<Option<HashSet<String>>> { 345 + let key = format!("collections:{}", slice_uri); 346 + self.cache.get::<HashSet<String>>(&key).await 347 + } 348 + 349 + /// Utility methods 350 + pub async fn ping(&mut self) -> Result<bool> { 351 + self.cache.ping().await 352 + } 353 + 354 + pub async fn get_info(&mut self) -> Result<String> { 355 + self.cache.get_info().await 356 + } 357 + 358 + /// Auth cache methods (5 minute TTL) 359 + pub async fn get_cached_oauth_userinfo(&mut self, token: &str) -> Result<Option<serde_json::Value>> { 360 + let key = format!("oauth_userinfo:{}", token); 361 + self.cache.get(&key).await 362 + } 363 + 364 + pub async fn cache_oauth_userinfo(&mut self, token: &str, userinfo: &serde_json::Value, ttl_seconds: u64) -> Result<()> { 365 + let key = format!("oauth_userinfo:{}", token); 366 + self.cache.set(&key, userinfo, Some(ttl_seconds)).await 367 + } 368 + 369 + pub async fn get_cached_atproto_session(&mut self, token: &str) -> Result<Option<serde_json::Value>> { 370 + let key = format!("atproto_session:{}", token); 371 + self.cache.get(&key).await 372 + } 373 + 374 + pub async fn cache_atproto_session(&mut self, token: &str, session: &serde_json::Value, ttl_seconds: u64) -> Result<()> { 375 + let key = format!("atproto_session:{}", token); 376 + self.cache.set(&key, session, Some(ttl_seconds)).await 377 + } 378 + 379 + /// DID resolution cache methods (24 hour TTL - DIDs change infrequently) 380 + pub async fn get_cached_did_resolution(&mut self, did: &str) -> Result<Option<serde_json::Value>> { 381 + let key = format!("did_resolution:{}", did); 382 + self.cache.get(&key).await 383 + } 384 + 385 + pub async fn cache_did_resolution(&mut self, did: &str, actor_data: &serde_json::Value) -> Result<()> { 386 + let key = format!("did_resolution:{}", did); 387 + let ttl_seconds = 24 * 60 * 60; // 24 hours 388 + self.cache.set(&key, actor_data, Some(ttl_seconds)).await 389 + } 390 + 391 + pub async fn invalidate_did_resolution(&mut self, did: &str) -> Result<()> { 392 + let key = format!("did_resolution:{}", did); 393 + self.cache.delete(&key).await 394 + } 395 + 396 + /// Generic get/set for custom caching needs 397 + pub async fn get<T>(&mut self, key: &str) -> Result<Option<T>> 398 + where 399 + T: for<'de> Deserialize<'de> + Send, 400 + { 401 + self.cache.get(key).await 402 + } 403 + 404 + pub async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()> 405 + where 406 + T: Serialize + Send + Sync, 407 + { 408 + self.cache.set(key, value, ttl_seconds).await 409 + } 410 + } 411 + 412 + /// Cache backend configuration 413 + #[derive(Debug, Clone)] 414 + pub enum CacheBackend { 415 + InMemory { ttl_seconds: Option<u64> }, 416 + Redis { url: String, ttl_seconds: Option<u64> }, 417 + } 418 + 419 + /// Cache factory for creating cache instances 420 + pub struct CacheFactory; 421 + 422 + impl CacheFactory { 423 + /// Create a cache instance based on configuration 424 + pub async fn create_cache(backend: CacheBackend) -> Result<CacheBackendImpl> { 425 + match backend { 426 + CacheBackend::InMemory { ttl_seconds } => { 427 + let ttl_display = ttl_seconds.map(|t| format!("{}s", t)).unwrap_or_else(|| "default".to_string()); 428 + info!("Creating in-memory cache with TTL: {}", ttl_display); 429 + Ok(CacheBackendImpl::InMemory(InMemoryCache::new(ttl_seconds))) 430 + } 431 + CacheBackend::Redis { url, ttl_seconds } => { 432 + info!("Attempting to create Redis cache at: {}", url); 433 + match crate::redis_cache::RedisCache::new(&url, ttl_seconds).await { 434 + Ok(redis_cache) => { 435 + info!("✓ Created Redis cache successfully"); 436 + Ok(CacheBackendImpl::Redis(redis_cache)) 437 + } 438 + Err(e) => { 439 + warn!( 440 + error = ?e, 441 + "Failed to create Redis cache, falling back to in-memory" 442 + ); 443 + Ok(CacheBackendImpl::InMemory(InMemoryCache::new(ttl_seconds))) 444 + } 445 + } 446 + } 447 + } 448 + } 449 + 450 + /// Create a SliceCache with the specified backend 451 + pub async fn create_slice_cache(backend: CacheBackend) -> Result<SliceCache> { 452 + let cache = Self::create_cache(backend).await?; 453 + Ok(SliceCache::new(cache)) 454 + } 455 + }
+4
api/src/errors.rs
··· 64 64 65 65 #[error("error-slices-app-8 Forbidden: {0}")] 66 66 Forbidden(String), 67 + 68 + #[error("error-slices-app-9 Cache error: {0}")] 69 + Cache(#[from] anyhow::Error), 67 70 } 68 71 69 72 impl From<StatusCode> for AppError { ··· 99 102 AppError::DatabaseConnection(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()), 100 103 AppError::Migration(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()), 101 104 AppError::ServerBind(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()), 105 + AppError::Cache(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()), 102 106 }; 103 107 104 108 let body = Json(serde_json::json!({
+314 -254
api/src/jetstream.rs
··· 2 2 use async_trait::async_trait; 3 3 use anyhow::Result; 4 4 use chrono::Utc; 5 - use std::collections::{HashMap, HashSet}; 5 + use std::collections::HashSet; 6 6 use std::sync::Arc; 7 - use tokio::sync::RwLock; 8 - use tracing::{error, info}; 7 + use tokio::sync::{Mutex, RwLock}; 8 + use tracing::{error, info, warn}; 9 9 use reqwest::Client; 10 10 11 11 use crate::actor_resolver::resolve_actor_data; ··· 14 14 use crate::models::{Record, Actor}; 15 15 use crate::errors::SliceError; 16 16 use crate::logging::{Logger, LogLevel}; 17 + use crate::cache::{SliceCache, CacheFactory, CacheBackend}; 17 18 18 19 pub struct JetstreamConsumer { 19 20 consumer: Consumer, 20 21 database: Database, 21 22 http_client: Client, 22 - slice_collections: Arc<RwLock<HashMap<String, HashSet<String>>>>, 23 - slice_domains: Arc<RwLock<HashMap<String, String>>>, 24 - actor_cache: Arc<RwLock<HashMap<(String, String), bool>>>, 25 - slice_lexicons: Arc<RwLock<HashMap<String, Vec<serde_json::Value>>>>, 23 + actor_cache: Arc<Mutex<SliceCache>>, 24 + lexicon_cache: Arc<Mutex<SliceCache>>, 25 + domain_cache: Arc<Mutex<SliceCache>>, 26 + collections_cache: Arc<Mutex<SliceCache>>, 26 27 pub event_count: Arc<std::sync::atomic::AtomicU64>, 27 28 cursor_handler: Option<Arc<PostgresCursorHandler>>, 29 + slices_list: Arc<RwLock<Vec<String>>>, 28 30 } 29 31 30 32 // Event handler that implements the EventHandler trait 31 33 struct SliceEventHandler { 32 34 database: Database, 33 35 http_client: Client, 34 - slice_collections: Arc<RwLock<HashMap<String, HashSet<String>>>>, 35 - slice_domains: Arc<RwLock<HashMap<String, String>>>, 36 36 event_count: Arc<std::sync::atomic::AtomicU64>, 37 - actor_cache: Arc<RwLock<HashMap<(String, String), bool>>>, 38 - slice_lexicons: Arc<RwLock<HashMap<String, Vec<serde_json::Value>>>>, 37 + actor_cache: Arc<Mutex<SliceCache>>, 38 + lexicon_cache: Arc<Mutex<SliceCache>>, 39 + domain_cache: Arc<Mutex<SliceCache>>, 40 + collections_cache: Arc<Mutex<SliceCache>>, 39 41 cursor_handler: Option<Arc<PostgresCursorHandler>>, 42 + slices_list: Arc<RwLock<Vec<String>>>, 40 43 } 41 44 42 45 #[async_trait] ··· 101 104 } 102 105 103 106 impl SliceEventHandler { 107 + /// Check if DID is an actor for the given slice 108 + async fn is_actor_cached(&self, did: &str, slice_uri: &str) -> Result<Option<bool>, anyhow::Error> { 109 + match self.actor_cache.lock().await.is_actor(did, slice_uri).await { 110 + Ok(result) => Ok(result), 111 + Err(e) => { 112 + warn!( 113 + error = ?e, 114 + did = did, 115 + slice_uri = slice_uri, 116 + "Actor cache error" 117 + ); 118 + Ok(None) 119 + } 120 + } 121 + } 122 + 123 + /// Cache that an actor exists 124 + async fn cache_actor_exists(&self, did: &str, slice_uri: &str) { 125 + if let Err(e) = self.actor_cache.lock().await.cache_actor_exists(did, slice_uri).await { 126 + warn!( 127 + error = ?e, 128 + did = did, 129 + slice_uri = slice_uri, 130 + "Failed to cache actor exists" 131 + ); 132 + } 133 + } 134 + 135 + /// Remove actor from cache 136 + async fn remove_actor_from_cache(&self, did: &str, slice_uri: &str) { 137 + if let Err(e) = self.actor_cache.lock().await.remove_actor(did, slice_uri).await { 138 + warn!( 139 + error = ?e, 140 + did = did, 141 + slice_uri = slice_uri, 142 + "Failed to remove actor from cache" 143 + ); 144 + } 145 + } 146 + 147 + /// Get slice collections from cache with database fallback 148 + async fn get_slice_collections(&self, slice_uri: &str) -> Result<Option<HashSet<String>>, anyhow::Error> { 149 + // Try cache first 150 + let cache_result = { 151 + let mut cache = self.collections_cache.lock().await; 152 + cache.get_slice_collections(slice_uri).await 153 + }; 154 + 155 + match cache_result { 156 + Ok(Some(collections)) => Ok(Some(collections)), 157 + Ok(None) => { 158 + // Cache miss - load from database 159 + match self.database.get_slice_collections_list(slice_uri).await { 160 + Ok(collections) => { 161 + let collections_set: HashSet<String> = collections.into_iter().collect(); 162 + // Cache the result 163 + let _ = self.collections_cache.lock().await.cache_slice_collections(slice_uri, &collections_set).await; 164 + Ok(Some(collections_set)) 165 + } 166 + Err(e) => Err(e.into()) 167 + } 168 + } 169 + Err(e) => Err(e) 170 + } 171 + } 172 + 173 + /// Get slice domain from cache with database fallback 174 + async fn get_slice_domain(&self, slice_uri: &str) -> Result<Option<String>, anyhow::Error> { 175 + // Try cache first 176 + let cache_result = { 177 + let mut cache = self.domain_cache.lock().await; 178 + cache.get_slice_domain(slice_uri).await 179 + }; 180 + 181 + match cache_result { 182 + Ok(Some(domain)) => Ok(Some(domain)), 183 + Ok(None) => { 184 + // Cache miss - load from database 185 + match self.database.get_slice_domain(slice_uri).await { 186 + Ok(Some(domain)) => { 187 + // Cache the result 188 + let _ = self.domain_cache.lock().await.cache_slice_domain(slice_uri, &domain).await; 189 + Ok(Some(domain)) 190 + } 191 + Ok(None) => Ok(None), 192 + Err(e) => Err(e.into()) 193 + } 194 + } 195 + Err(e) => Err(e) 196 + } 197 + } 198 + 199 + /// Get slice lexicons from cache with database fallback 200 + async fn get_slice_lexicons(&self, slice_uri: &str) -> Result<Option<Vec<serde_json::Value>>, anyhow::Error> { 201 + // Try cache first 202 + let cache_result = { 203 + let mut cache = self.lexicon_cache.lock().await; 204 + cache.get_lexicons(slice_uri).await 205 + }; 206 + 207 + match cache_result { 208 + Ok(Some(lexicons)) => Ok(Some(lexicons)), 209 + Ok(None) => { 210 + // Cache miss - load from database 211 + match self.database.get_lexicons_by_slice(slice_uri).await { 212 + Ok(lexicons) if !lexicons.is_empty() => { 213 + // Cache the result 214 + let _ = self.lexicon_cache.lock().await.cache_lexicons(slice_uri, &lexicons).await; 215 + Ok(Some(lexicons)) 216 + } 217 + Ok(_) => Ok(None), // Empty lexicons 218 + Err(e) => Err(e.into()) 219 + } 220 + } 221 + Err(e) => Err(e) 222 + } 223 + } 104 224 async fn handle_commit_event( 105 225 &self, 106 226 did: &str, 107 227 commit: atproto_jetstream::JetstreamEventCommit, 108 228 ) -> Result<()> { 109 - let slice_collections = self.slice_collections.read().await; 110 - let slice_domains = self.slice_domains.read().await; 111 - let slice_lexicons = self.slice_lexicons.read().await; 112 - 113 - for (slice_uri, collections) in slice_collections.iter() { 229 + // Get all slices from cached list 230 + let slices = self.slices_list.read().await.clone(); 231 + 232 + // Process each slice 233 + for slice_uri in slices { 234 + // Get collections for this slice (with caching) 235 + let collections = match self.get_slice_collections(&slice_uri).await { 236 + Ok(Some(collections)) => collections, 237 + Ok(None) => continue, // No collections for this slice 238 + Err(e) => { 239 + error!("Failed to get collections for slice {}: {}", slice_uri, e); 240 + continue; 241 + } 242 + }; 243 + 114 244 if collections.contains(&commit.collection) { 115 245 // Special handling for network.slices.lexicon records 116 246 // These should only be indexed to the slice specified in their JSON data ··· 125 255 continue; 126 256 } 127 257 } 128 - // Get the domain for this slice 129 - let domain = match slice_domains.get(slice_uri) { 130 - Some(d) => d, 131 - None => continue, // No domain, skip 258 + // Get the domain for this slice (with caching) 259 + let domain = match self.get_slice_domain(&slice_uri).await { 260 + Ok(Some(domain)) => domain, 261 + Ok(None) => continue, // No domain, skip 262 + Err(e) => { 263 + error!("Failed to get domain for slice {}: {}", slice_uri, e); 264 + continue; 265 + } 132 266 }; 133 - 267 + 134 268 // Check if this is a primary collection (starts with slice domain) 135 - let is_primary_collection = commit.collection.starts_with(domain); 136 - 269 + let is_primary_collection = commit.collection.starts_with(&domain); 270 + 137 271 // For external collections, check actor status BEFORE expensive validation 138 272 if !is_primary_collection { 139 - let cache_key = (did.to_string(), slice_uri.clone()); 140 - let is_actor = { 141 - let cache = self.actor_cache.read().await; 142 - cache.get(&cache_key).copied() 143 - }; 144 - 145 - let is_actor: Result<bool, anyhow::Error> = match is_actor { 146 - Some(cached_result) => Ok(cached_result), 147 - None => { 273 + let is_actor = match self.is_actor_cached(did, &slice_uri).await { 274 + Ok(Some(cached_result)) => cached_result, 275 + Ok(None) => { 148 276 // Cache miss means this DID is not an actor we've synced 149 277 // For external collections, we only care about actors we've already added 150 - // Don't cache negative results to avoid memory bloat 151 - Ok(false) 152 - } 153 - }; 154 - 155 - match is_actor { 156 - Ok(false) => { 157 - // Not an actor - skip validation entirely for external collections 158 - continue; 159 - } 160 - Ok(true) => { 161 - // Actor found - continue to validation 278 + false 162 279 } 163 280 Err(e) => { 164 281 error!("Error checking actor status: {}", e); 165 282 continue; 166 283 } 284 + }; 285 + 286 + if !is_actor { 287 + // Not an actor - skip validation entirely for external collections 288 + continue; 167 289 } 168 290 } 169 - 291 + 170 292 // Get lexicons for validation (after actor check for external collections) 171 - let lexicons = match slice_lexicons.get(slice_uri) { 172 - Some(lexicons) => lexicons.clone(), 173 - None => { 174 - // Fallback: Try to load fresh lexicons from database for this slice 175 - info!("No cached lexicons for slice {} - attempting database fallback", slice_uri); 176 - match self.database.get_lexicons_by_slice(slice_uri).await { 177 - Ok(fresh_lexicons) if !fresh_lexicons.is_empty() => { 178 - info!("✓ Loaded fresh lexicons for slice {} from database", slice_uri); 179 - // Cache the fresh lexicons for future use 180 - { 181 - let mut lexicons_cache = self.slice_lexicons.write().await; 182 - lexicons_cache.insert(slice_uri.clone(), fresh_lexicons.clone()); 183 - } 184 - fresh_lexicons 185 - } 186 - _ => { 187 - info!("No lexicons found for slice {} - skipping validation", slice_uri); 188 - continue; 189 - } 190 - } 293 + let lexicons = match self.get_slice_lexicons(&slice_uri).await { 294 + Ok(Some(lexicons)) => lexicons, 295 + Ok(None) => { 296 + info!("No lexicons found for slice {} - skipping validation", slice_uri); 297 + continue; 298 + } 299 + Err(e) => { 300 + error!("Failed to get lexicons for slice {}: {}", slice_uri, e); 301 + continue; 191 302 } 192 303 }; 193 - 304 + 194 305 // Validate the record against the slice's lexicons 195 306 let validation_result = match slices_lexicon::validate_record(lexicons.clone(), &commit.collection, commit.record.clone()) { 196 307 Ok(_) => { ··· 198 309 true 199 310 } 200 311 Err(e) => { 201 - info!("Validation failed with cached validator for collection {} in slice {}: {} - trying database fallback", 202 - commit.collection, slice_uri, e); 203 - 204 - // Try database fallback in case lexicons were updated 205 - match self.database.get_lexicons_by_slice(slice_uri).await { 206 - Ok(fresh_lexicons) if !fresh_lexicons.is_empty() => { 207 - match slices_lexicon::validate_record(fresh_lexicons.clone(), &commit.collection, commit.record.clone()) { 208 - Ok(_) => { 209 - info!("✓ Record validated with fresh lexicons for collection {} in slice {}", 210 - commit.collection, slice_uri); 211 - // Update cache with fresh lexicons 212 - { 213 - let mut lexicons_cache = self.slice_lexicons.write().await; 214 - lexicons_cache.insert(slice_uri.clone(), fresh_lexicons); 215 - } 216 - true 217 - } 218 - Err(fresh_e) => { 219 - let message = format!("Validation failed for collection {} in slice {}", commit.collection, slice_uri); 220 - error!("✗ {}: {}", message, fresh_e); 221 - Logger::global().log_jetstream_with_slice(LogLevel::Warn, &message, Some(serde_json::json!({ 222 - "collection": commit.collection, 223 - "slice_uri": slice_uri, 224 - "did": did 225 - })), Some(slice_uri)); 226 - false 227 - } 228 - } 229 - } 230 - Ok(_) => { 231 - // Empty lexicons - skip logging as this is expected for many slices 232 - false 233 - } 234 - Err(_) => { 235 - // Database error - skip logging for missing lexicons 236 - false 237 - } 238 - } 312 + let message = format!("Validation failed for collection {} in slice {}", commit.collection, slice_uri); 313 + error!("✗ {}: {}", message, e); 314 + Logger::global().log_jetstream_with_slice(LogLevel::Warn, &message, Some(serde_json::json!({ 315 + "collection": commit.collection, 316 + "slice_uri": slice_uri, 317 + "did": did 318 + })), Some(&slice_uri)); 319 + false 239 320 } 240 321 }; 241 - 322 + 242 323 if !validation_result { 243 324 continue; // Skip this slice if validation fails 244 325 } 245 - 326 + 246 327 if is_primary_collection { 247 328 // Primary collection - ensure actor exists and index ALL records 248 329 info!("✓ Primary collection {} for slice {} (domain: {}) - indexing record", 249 330 commit.collection, slice_uri, domain); 250 331 251 332 // Ensure actor exists for primary collections 252 - let cache_key = (did.to_string(), slice_uri.clone()); 253 - let is_cached = { 254 - let cache = self.actor_cache.read().await; 255 - cache.contains_key(&cache_key) 256 - }; 333 + let is_cached = matches!(self.is_actor_cached(did, &slice_uri).await, Ok(Some(_))); 257 334 258 335 if !is_cached { 259 336 // Actor not in cache - create it ··· 274 351 error!("Failed to create actor {}: {}", did, e); 275 352 } else { 276 353 // Add to cache after successful database insert 277 - let mut cache = self.actor_cache.write().await; 278 - cache.insert(cache_key, true); 354 + self.cache_actor_exists(did, &slice_uri).await; 279 355 info!("✓ Created actor {} for slice {}", did, slice_uri); 280 356 } 281 357 } ··· 284 360 } 285 361 } 286 362 } 287 - 363 + 288 364 let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey); 289 - 365 + 290 366 let record = Record { 291 367 uri: uri.clone(), 292 368 cid: commit.cid.clone(), ··· 296 372 indexed_at: Utc::now(), 297 373 slice_uri: Some(slice_uri.clone()), 298 374 }; 299 - 375 + 300 376 match self.database.upsert_record(&record).await { 301 377 Ok(is_insert) => { 302 - let message = if is_insert { 378 + let message = if is_insert { 303 379 format!("Record inserted in {}", commit.collection) 304 - } else { 380 + } else { 305 381 format!("Record updated in {}", commit.collection) 306 382 }; 307 383 let operation = if is_insert { "insert" } else { "update" }; ··· 311 387 "slice_uri": slice_uri, 312 388 "did": did, 313 389 "record_type": "primary" 314 - })), Some(slice_uri)); 390 + })), Some(&slice_uri)); 315 391 } 316 392 Err(e) => { 317 393 let message = "Failed to insert/update record"; ··· 322 398 "did": did, 323 399 "error": e.to_string(), 324 400 "record_type": "primary" 325 - })), Some(slice_uri)); 401 + })), Some(&slice_uri)); 326 402 return Err(anyhow::anyhow!("Database error: {}", e)); 327 403 } 328 404 } 329 - 330 - info!("✓ Successfully indexed {} record from primary collection: {}", 405 + 406 + info!("✓ Successfully indexed {} record from primary collection: {}", 331 407 commit.operation, uri); 332 408 break; 333 409 } else { 334 410 // External collection - we already checked actor status, so just index 335 - info!("✓ External collection {} - DID {} is actor in slice {} - indexing", 411 + info!("✓ External collection {} - DID {} is actor in slice {} - indexing", 336 412 commit.collection, did, slice_uri); 337 - 413 + 338 414 let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey); 339 - 415 + 340 416 let record = Record { 341 417 uri: uri.clone(), 342 418 cid: commit.cid.clone(), ··· 346 422 indexed_at: Utc::now(), 347 423 slice_uri: Some(slice_uri.clone()), 348 424 }; 349 - 425 + 350 426 match self.database.upsert_record(&record).await { 351 427 Ok(is_insert) => { 352 - let message = if is_insert { 428 + let message = if is_insert { 353 429 format!("Record inserted in {}", commit.collection) 354 - } else { 430 + } else { 355 431 format!("Record updated in {}", commit.collection) 356 432 }; 357 433 let operation = if is_insert { "insert" } else { "update" }; ··· 361 437 "slice_uri": slice_uri, 362 438 "did": did, 363 439 "record_type": "external" 364 - })), Some(slice_uri)); 440 + })), Some(&slice_uri)); 365 441 } 366 442 Err(e) => { 367 443 let message = "Failed to insert/update record"; ··· 372 448 "did": did, 373 449 "error": e.to_string(), 374 450 "record_type": "external" 375 - })), Some(slice_uri)); 451 + })), Some(&slice_uri)); 376 452 return Err(anyhow::anyhow!("Database error: {}", e)); 377 453 } 378 454 } 379 - 380 - info!("✓ Successfully indexed {} record from external collection: {}", 455 + 456 + info!("✓ Successfully indexed {} record from external collection: {}", 381 457 commit.operation, uri); 382 458 break; 383 459 } 384 460 } 385 461 } 386 - 462 + 387 463 Ok(()) 388 464 } 389 465 ··· 394 470 ) -> Result<()> { 395 471 let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey); 396 472 397 - // Get slices that track this collection 398 - let slice_collections = self.slice_collections.read().await; 399 - let slice_domains = self.slice_domains.read().await; 400 - let actor_cache = self.actor_cache.read().await; 473 + // Get all slices from cached list 474 + let slices = self.slices_list.read().await.clone(); 401 475 402 476 let mut relevant_slices: Vec<String> = Vec::new(); 403 477 404 - for (slice_uri, collections) in slice_collections.iter() { 478 + for slice_uri in slices { 479 + // Get collections for this slice (with caching) 480 + let collections = match self.get_slice_collections(&slice_uri).await { 481 + Ok(Some(collections)) => collections, 482 + Ok(None) => continue, // No collections for this slice 483 + Err(e) => { 484 + error!("Failed to get collections for slice {}: {}", slice_uri, e); 485 + continue; 486 + } 487 + }; 488 + 405 489 if !collections.contains(&commit.collection) { 406 490 continue; 407 491 } 408 492 409 - // Get the domain for this slice 410 - let domain = match slice_domains.get(slice_uri) { 411 - Some(d) => d, 412 - None => continue, 493 + // Get the domain for this slice (with caching) 494 + let domain = match self.get_slice_domain(&slice_uri).await { 495 + Ok(Some(domain)) => domain, 496 + Ok(None) => continue, // No domain, skip 497 + Err(e) => { 498 + error!("Failed to get domain for slice {}: {}", slice_uri, e); 499 + continue; 500 + } 413 501 }; 414 502 415 503 // Check if this is a primary collection (starts with slice domain) 416 - let is_primary_collection = commit.collection.starts_with(domain); 504 + let is_primary_collection = commit.collection.starts_with(&domain); 417 505 418 506 if is_primary_collection { 419 507 // Primary collection - always process deletes 420 508 relevant_slices.push(slice_uri.clone()); 421 509 } else { 422 510 // External collection - only process if DID is an actor in this slice 423 - let cache_key = (did.to_string(), slice_uri.clone()); 424 - if actor_cache.get(&cache_key).copied().unwrap_or(false) { 511 + let is_actor = match self.is_actor_cached(did, &slice_uri).await { 512 + Ok(Some(cached_result)) => cached_result, 513 + _ => false, 514 + }; 515 + if is_actor { 425 516 relevant_slices.push(slice_uri.clone()); 426 517 } 427 518 } ··· 466 557 if deleted > 0 { 467 558 info!("✓ Cleaned up actor {} from slice {} (no records remaining)", did, slice_uri); 468 559 // Remove from cache 469 - let cache_key = (did.to_string(), slice_uri.clone()); 470 - let mut cache = self.actor_cache.write().await; 471 - cache.remove(&cache_key); 560 + self.remove_actor_from_cache(did, slice_uri).await; 472 561 } 473 562 } 474 563 Err(e) => { ··· 512 601 } 513 602 514 603 impl JetstreamConsumer { 515 - /// Create a new Jetstream consumer with optional cursor support 604 + /// Create a new Jetstream consumer with optional cursor support and Redis cache 516 605 /// 517 606 /// # Arguments 518 607 /// * `database` - Database connection for slice configurations and record storage 519 608 /// * `jetstream_hostname` - Optional custom jetstream hostname 520 609 /// * `cursor_handler` - Optional cursor handler for resumable event processing 521 610 /// * `initial_cursor` - Optional starting cursor position (time_us) to resume from 611 + /// * `redis_url` - Optional Redis URL for caching (falls back to in-memory if not provided) 522 612 pub async fn new( 523 613 database: Database, 524 614 jetstream_hostname: Option<String>, 525 615 cursor_handler: Option<Arc<PostgresCursorHandler>>, 526 616 initial_cursor: Option<i64>, 617 + redis_url: Option<String>, 527 618 ) -> Result<Self, SliceError> { 528 619 let config = ConsumerTaskConfig { 529 620 user_agent: "slice-server/1.0".to_string(), ··· 541 632 let consumer = Consumer::new(config); 542 633 let http_client = Client::new(); 543 634 635 + // Determine cache backend based on Redis URL 636 + let cache_backend = if let Some(redis_url) = redis_url { 637 + CacheBackend::Redis { url: redis_url, ttl_seconds: None } 638 + } else { 639 + CacheBackend::InMemory { ttl_seconds: None } 640 + }; 641 + 642 + // Create cache instances 643 + let actor_cache = Arc::new(Mutex::new( 644 + CacheFactory::create_slice_cache(cache_backend.clone()).await 645 + .map_err(|e| SliceError::JetstreamError { 646 + message: format!("Failed to create actor cache: {}", e) 647 + })? 648 + )); 649 + 650 + let lexicon_cache = Arc::new(Mutex::new( 651 + CacheFactory::create_slice_cache(cache_backend.clone()).await 652 + .map_err(|e| SliceError::JetstreamError { 653 + message: format!("Failed to create lexicon cache: {}", e) 654 + })? 655 + )); 656 + 657 + let domain_cache = Arc::new(Mutex::new( 658 + CacheFactory::create_slice_cache(cache_backend.clone()).await 659 + .map_err(|e| SliceError::JetstreamError { 660 + message: format!("Failed to create domain cache: {}", e) 661 + })? 662 + )); 663 + 664 + let collections_cache = Arc::new(Mutex::new( 665 + CacheFactory::create_slice_cache(cache_backend).await 666 + .map_err(|e| SliceError::JetstreamError { 667 + message: format!("Failed to create collections cache: {}", e) 668 + })? 669 + )); 670 + 544 671 Ok(Self { 545 672 consumer, 546 673 database, 547 674 http_client, 548 - slice_collections: Arc::new(RwLock::new(HashMap::new())), 549 - slice_domains: Arc::new(RwLock::new(HashMap::new())), 550 - actor_cache: Arc::new(RwLock::new(HashMap::new())), 551 - slice_lexicons: Arc::new(RwLock::new(HashMap::new())), 675 + actor_cache, 676 + lexicon_cache, 677 + domain_cache, 678 + collections_cache, 552 679 event_count: Arc::new(std::sync::atomic::AtomicU64::new(0)), 553 680 cursor_handler, 681 + slices_list: Arc::new(RwLock::new(Vec::new())), 554 682 }) 555 683 } 556 684 557 - /// Load slice configurations to know which collections to index 685 + /// Load slice configurations 558 686 pub async fn load_slice_configurations(&self) -> Result<(), SliceError> { 559 - info!("Loading slice configurations for Jetstream indexing"); 560 - 561 - // Get all slices that have lexicon definitions 562 - let slices = self.database.get_all_slices().await?; 563 - info!("Found {} total slices in database", slices.len()); 564 - 565 - let mut collections_map = HashMap::new(); 566 - let mut domains_map = HashMap::new(); 567 - let mut total_collections = 0; 568 - 569 - for slice_uri in &slices { 570 - info!("Checking slice: {}", slice_uri); 571 - 572 - // Get the domain for this slice 573 - if let Ok(Some(domain)) = self.database.get_slice_domain(slice_uri).await { 574 - info!("Slice {} has domain: {}", slice_uri, domain); 575 - domains_map.insert(slice_uri.clone(), domain.clone()); 576 - 577 - // Get collections defined in this slice's lexicons 578 - let collections = self.database.get_slice_collections_list(slice_uri).await?; 579 - 580 - if !collections.is_empty() { 581 - // Categorize collections as primary or external 582 - let mut primary = Vec::new(); 583 - let mut external = Vec::new(); 584 - 585 - for collection in &collections { 586 - if collection.starts_with(&domain) { 587 - primary.push(collection.clone()); 588 - } else { 589 - external.push(collection.clone()); 590 - } 591 - } 592 - 593 - info!("Slice {} has {} primary collections: {:?}", slice_uri, primary.len(), primary); 594 - info!("Slice {} has {} external collections: {:?}", slice_uri, external.len(), external); 595 - 596 - total_collections += collections.len(); 597 - collections_map.insert(slice_uri.clone(), collections.into_iter().collect()); 598 - } else { 599 - info!("Slice {} has no collections defined (no lexicons or empty lexicons)", slice_uri); 600 - } 601 - } else { 602 - info!("Slice {} has no domain defined - skipping", slice_uri); 603 - } 604 - } 605 - 606 - let mut slice_collections = self.slice_collections.write().await; 607 - *slice_collections = collections_map; 608 - 609 - let mut slice_domains = self.slice_domains.write().await; 610 - *slice_domains = domains_map; 611 - 612 - // Load lexicons for each slice 613 - let mut lexicons_map = HashMap::new(); 614 - for slice_uri in slice_collections.keys() { 615 - info!("Loading lexicons for slice: {}", slice_uri); 616 - 617 - // Get all lexicons for this slice 618 - match self.database.get_lexicons_by_slice(slice_uri).await { 619 - Ok(lexicons) if !lexicons.is_empty() => { 620 - lexicons_map.insert(slice_uri.clone(), lexicons); 621 - info!("✓ Loaded lexicons for slice {}", slice_uri); 622 - } 623 - Ok(_) => { 624 - info!("No lexicons found for slice {}", slice_uri); 625 - } 626 - Err(e) => { 627 - error!("Failed to load lexicons for slice {}: {}", slice_uri, e); 628 - } 629 - } 630 - } 687 + info!("Jetstream consumer now uses on-demand loading with caching"); 631 688 632 - let mut slice_lexicons = self.slice_lexicons.write().await; 633 - *slice_lexicons = lexicons_map; 689 + // Get all slices and update cached list 690 + let slices = self.database.get_all_slices().await?; 691 + *self.slices_list.write().await = slices.clone(); 692 + info!("Found {} total slices in database - data will be loaded on-demand", slices.len()); 634 693 635 - info!("Jetstream consumer will monitor {} total collections across {} slices with {} lexicon sets loaded", 636 - total_collections, slice_collections.len(), slice_lexicons.len()); 637 - 638 694 Ok(()) 639 695 } 640 696 641 697 /// Preload actor cache to avoid database hits during event processing 642 698 async fn preload_actor_cache(&self) -> Result<(), SliceError> { 643 699 info!("Preloading actor cache..."); 644 - 700 + 645 701 let actors = self.database.get_all_actors().await?; 646 702 info!("Found {} actors to cache", actors.len()); 647 - 648 - let mut cache = self.actor_cache.write().await; 649 - cache.clear(); // Clear existing cache 650 - for (did, slice_uri) in actors { 651 - cache.insert((did, slice_uri), true); 703 + 704 + match self.actor_cache.lock().await.preload_actors(actors).await { 705 + Ok(_) => { 706 + info!("✓ Actor cache preloaded successfully"); 707 + Ok(()) 708 + } 709 + Err(e) => { 710 + warn!(error = ?e, "Failed to preload actors to cache"); 711 + Ok(()) // Don't fail startup if preload fails 712 + } 652 713 } 653 - 654 - info!("Actor cache preloaded with {} entries", cache.len()); 655 - Ok(()) 656 714 } 657 - 715 + 658 716 659 717 /// Start consuming events from Jetstream 660 718 pub async fn start_consuming(&self, cancellation_token: CancellationToken) -> Result<(), SliceError> { 661 719 info!("Starting Jetstream consumer"); 662 - 720 + 663 721 // Load initial slice configurations 664 722 self.load_slice_configurations().await?; 665 - 723 + 666 724 // Preload actor cache 667 725 self.preload_actor_cache().await?; 668 - 726 + 669 727 // Create and register the event handler 670 728 let handler = Arc::new(SliceEventHandler { 671 729 database: self.database.clone(), 672 730 http_client: self.http_client.clone(), 673 - slice_collections: self.slice_collections.clone(), 674 - slice_domains: self.slice_domains.clone(), 675 731 event_count: self.event_count.clone(), 676 732 actor_cache: self.actor_cache.clone(), 677 - slice_lexicons: self.slice_lexicons.clone(), 733 + lexicon_cache: self.lexicon_cache.clone(), 734 + domain_cache: self.domain_cache.clone(), 735 + collections_cache: self.collections_cache.clone(), 678 736 cursor_handler: self.cursor_handler.clone(), 737 + slices_list: self.slices_list.clone(), 679 738 }); 680 - 739 + 681 740 self.consumer.register_handler(handler).await 682 741 .map_err(|e| SliceError::JetstreamError { 683 742 message: format!("Failed to register event handler: {}", e), 684 743 })?; 685 - 744 + 686 745 // Start periodic status reporting 687 746 let event_count_for_status = self.event_count.clone(); 688 747 tokio::spawn(async move { ··· 693 752 info!("Jetstream consumer status: {} total events processed", count); 694 753 } 695 754 }); 696 - 755 + 697 756 // Start the consumer 698 757 info!("Starting Jetstream background consumer..."); 699 758 let result = self.consumer.run_background(cancellation_token).await ··· 718 777 pub fn start_configuration_reloader(consumer: Arc<Self>) { 719 778 tokio::spawn(async move { 720 779 let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // Reload every 5 minutes 721 - 780 + interval.tick().await; // Skip first immediate tick 781 + 722 782 loop { 723 783 interval.tick().await; 724 - 784 + 725 785 if let Err(e) = consumer.load_slice_configurations().await { 726 786 error!("Failed to reload slice configurations: {}", e); 727 787 } 728 - 788 + 729 789 if let Err(e) = consumer.preload_actor_cache().await { 730 790 error!("Failed to reload actor cache: {}", e); 731 791 } 732 792 } 733 793 }); 734 794 } 735 - } 795 + }
+18 -6
api/src/jobs.rs
··· 5 5 use crate::sync::SyncService; 6 6 use crate::models::BulkSyncParams; 7 7 use crate::logging::LogLevel; 8 + use crate::cache; 8 9 use serde_json::json; 9 10 use tracing::{info, error}; 11 + use std::sync::Arc; 12 + use tokio::sync::Mutex; 10 13 11 14 /// Payload for sync jobs 12 15 #[derive(Debug, Clone, Serialize, Deserialize)] ··· 65 68 })) 66 69 ); 67 70 68 - // Create sync service with logging 71 + // Create sync service with logging and cache 69 72 let database = crate::database::Database::from_pool(pool.clone()); 70 73 let relay_endpoint = std::env::var("RELAY_ENDPOINT") 71 74 .unwrap_or_else(|_| "https://relay1.us-west.bsky.network".to_string()); 72 - let sync_service = SyncService::with_logging( 73 - database.clone(), 74 - relay_endpoint, 75 - logger.clone(), 75 + 76 + // Create cache for DID resolution (24 hour TTL) 77 + let cache = Arc::new(Mutex::new( 78 + cache::CacheFactory::create_slice_cache( 79 + cache::CacheBackend::InMemory { ttl_seconds: Some(24 * 60 * 60) } 80 + ).await? 81 + )); 82 + 83 + let sync_service = SyncService::with_logging_and_cache( 84 + database.clone(), 85 + relay_endpoint, 86 + logger.clone(), 76 87 payload.job_id, 77 - payload.user_did.clone() 88 + payload.user_did.clone(), 89 + cache 78 90 ); 79 91 80 92 // Track progress
+19 -1
api/src/main.rs
··· 2 2 mod api; 3 3 mod atproto_extensions; 4 4 mod auth; 5 + mod cache; 5 6 mod database; 6 7 mod errors; 7 8 mod jetstream; ··· 9 10 mod jobs; 10 11 mod logging; 11 12 mod models; 13 + mod redis_cache; 12 14 mod sync; 13 15 mod xrpc; 14 16 ··· 20 22 use std::env; 21 23 use std::sync::Arc; 22 24 use std::sync::atomic::AtomicBool; 25 + use tokio::sync::Mutex; 23 26 use tower_http::{cors::CorsLayer, trace::TraceLayer}; 24 27 use tracing::info; 25 28 ··· 42 45 database_pool: PgPool, 43 46 config: Config, 44 47 pub jetstream_connected: Arc<AtomicBool>, 48 + pub auth_cache: Arc<Mutex<cache::SliceCache>>, 45 49 } 46 50 47 51 #[tokio::main] ··· 126 130 let jetstream_connected_clone = jetstream_connected.clone(); 127 131 tokio::spawn(async move { 128 132 let jetstream_hostname = env::var("JETSTREAM_HOSTNAME").ok(); 133 + let redis_url = env::var("REDIS_URL").ok(); 129 134 let cursor_write_interval = env::var("JETSTREAM_CURSOR_WRITE_INTERVAL_SECS") 130 135 .unwrap_or_else(|_| "5".to_string()) 131 136 .parse::<u64>() ··· 179 184 cursor_write_interval, 180 185 )); 181 186 182 - // Create consumer with cursor support 187 + // Create consumer with cursor support and Redis cache 183 188 let consumer_result = JetstreamConsumer::new( 184 189 database_for_jetstream.clone(), 185 190 jetstream_hostname.clone(), 186 191 Some(cursor_handler.clone()), 187 192 initial_cursor, 193 + redis_url.clone(), 188 194 ).await; 189 195 190 196 let consumer_arc = match consumer_result { ··· 231 237 } 232 238 }); 233 239 240 + // Create auth cache for token/session caching (5 minute TTL) 241 + let redis_url = env::var("REDIS_URL").ok(); 242 + let auth_cache_backend = if let Some(redis_url) = redis_url { 243 + cache::CacheBackend::Redis { url: redis_url, ttl_seconds: Some(300) } 244 + } else { 245 + cache::CacheBackend::InMemory { ttl_seconds: Some(300) } 246 + }; 247 + let auth_cache = Arc::new(Mutex::new( 248 + cache::CacheFactory::create_slice_cache(auth_cache_backend).await? 249 + )); 250 + 234 251 let state = AppState { 235 252 database: database.clone(), 236 253 database_pool: pool, 237 254 config, 238 255 jetstream_connected, 256 + auth_cache, 239 257 }; 240 258 241 259 // Build application with routes
+259
api/src/redis_cache.rs
··· 1 + use redis::{Client, AsyncCommands}; 2 + use redis::aio::ConnectionManager; 3 + use anyhow::Result; 4 + use tracing::{debug, error, warn}; 5 + use serde::{Serialize, Deserialize}; 6 + use async_trait::async_trait; 7 + use crate::cache::Cache; 8 + 9 + /// Generic Redis cache for scalable caching across multiple instances 10 + pub struct RedisCache { 11 + conn: ConnectionManager, 12 + default_ttl_seconds: u64, 13 + } 14 + 15 + impl RedisCache { 16 + /// Create a new Redis cache 17 + /// 18 + /// # Arguments 19 + /// * `redis_url` - Redis connection URL (e.g., "redis://localhost:6379") 20 + /// * `default_ttl_seconds` - Default time-to-live for cache entries (default: 3600 = 1 hour) 21 + pub async fn new(redis_url: &str, default_ttl_seconds: Option<u64>) -> Result<Self> { 22 + let client = Client::open(redis_url)?; 23 + let conn = ConnectionManager::new(client).await?; 24 + 25 + Ok(Self { 26 + conn, 27 + default_ttl_seconds: default_ttl_seconds.unwrap_or(3600), 28 + }) 29 + } 30 + 31 + /// Get a value from cache 32 + /// 33 + /// Returns: 34 + /// - Some(T) if key exists and can be deserialized 35 + /// - None if key doesn't exist or deserialization fails 36 + pub async fn get_value<T>(&mut self, key: &str) -> Result<Option<T>> 37 + where 38 + T: for<'de> Deserialize<'de>, 39 + { 40 + match self.conn.get::<_, Option<String>>(key).await { 41 + Ok(Some(value)) => { 42 + match serde_json::from_str::<T>(&value) { 43 + Ok(parsed) => { 44 + // Cache hit - no logging needed 45 + Ok(Some(parsed)) 46 + } 47 + Err(e) => { 48 + error!( 49 + error = ?e, 50 + cache_key = %key, 51 + "Failed to deserialize cached value" 52 + ); 53 + // Remove corrupted entry 54 + let _ = self.conn.del::<_, ()>(key).await; 55 + Ok(None) 56 + } 57 + } 58 + } 59 + Ok(None) => { 60 + // Cache miss - no logging needed 61 + Ok(None) 62 + } 63 + Err(e) => { 64 + error!( 65 + error = ?e, 66 + cache_key = %key, 67 + "Redis error during get" 68 + ); 69 + // Return cache miss on Redis error 70 + Ok(None) 71 + } 72 + } 73 + } 74 + 75 + /// Set a value in cache with optional TTL 76 + pub async fn set_value<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()> 77 + where 78 + T: Serialize, 79 + { 80 + let ttl = ttl_seconds.unwrap_or(self.default_ttl_seconds); 81 + 82 + match serde_json::to_string(value) { 83 + Ok(serialized) => { 84 + match self.conn.set_ex::<_, _, ()>(key, serialized, ttl).await { 85 + Ok(_) => { 86 + debug!( 87 + cache_key = %key, 88 + ttl_seconds = ttl, 89 + "Cached value in Redis" 90 + ); 91 + Ok(()) 92 + } 93 + Err(e) => { 94 + error!( 95 + error = ?e, 96 + cache_key = %key, 97 + "Failed to cache value in Redis" 98 + ); 99 + // Don't fail the operation if Redis is down 100 + Ok(()) 101 + } 102 + } 103 + } 104 + Err(e) => { 105 + error!( 106 + error = ?e, 107 + cache_key = %key, 108 + "Failed to serialize value for caching" 109 + ); 110 + Ok(()) 111 + } 112 + } 113 + } 114 + 115 + /// Check if a key exists in cache 116 + pub async fn key_exists(&mut self, key: &str) -> Result<bool> { 117 + match self.conn.exists(key).await { 118 + Ok(exists) => { 119 + debug!(cache_key = %key, exists = exists, "Redis exists check"); 120 + Ok(exists) 121 + } 122 + Err(e) => { 123 + error!( 124 + error = ?e, 125 + cache_key = %key, 126 + "Redis error during exists check" 127 + ); 128 + Ok(false) 129 + } 130 + } 131 + } 132 + 133 + /// Delete a key from cache 134 + pub async fn delete_key(&mut self, key: &str) -> Result<()> { 135 + match self.conn.del::<_, ()>(key).await { 136 + Ok(_) => { 137 + debug!(cache_key = %key, "Deleted key from Redis cache"); 138 + Ok(()) 139 + } 140 + Err(e) => { 141 + error!( 142 + error = ?e, 143 + cache_key = %key, 144 + "Failed to delete key from Redis cache" 145 + ); 146 + Ok(()) 147 + } 148 + } 149 + } 150 + 151 + /// Set multiple key-value pairs using pipeline for efficiency 152 + pub async fn set_multiple_values<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()> 153 + where 154 + T: Serialize, 155 + { 156 + if items.is_empty() { 157 + return Ok(()); 158 + } 159 + 160 + let mut pipe = redis::pipe(); 161 + let mut serialization_errors = 0; 162 + 163 + for (key, value, ttl) in &items { 164 + match serde_json::to_string(value) { 165 + Ok(serialized) => { 166 + let ttl_to_use = ttl.unwrap_or(self.default_ttl_seconds); 167 + pipe.set_ex(key, serialized, ttl_to_use); 168 + } 169 + Err(e) => { 170 + error!( 171 + error = ?e, 172 + cache_key = %key, 173 + "Failed to serialize value for bulk caching" 174 + ); 175 + serialization_errors += 1; 176 + } 177 + } 178 + } 179 + 180 + match pipe.query_async::<()>(&mut self.conn).await { 181 + Ok(_) => { 182 + debug!( 183 + items_count = items.len() - serialization_errors, 184 + serialization_errors = serialization_errors, 185 + "Successfully bulk cached items in Redis" 186 + ); 187 + Ok(()) 188 + } 189 + Err(e) => { 190 + error!( 191 + error = ?e, 192 + items_count = items.len(), 193 + "Failed to bulk cache items in Redis" 194 + ); 195 + Ok(()) 196 + } 197 + } 198 + } 199 + 200 + /// Test Redis connection 201 + pub async fn ping(&mut self) -> Result<bool> { 202 + match self.conn.ping::<String>().await { 203 + Ok(response) => Ok(response == "PONG"), 204 + Err(e) => { 205 + error!(error = ?e, "Redis ping failed"); 206 + Ok(false) 207 + } 208 + } 209 + } 210 + 211 + /// Get cache statistics (for monitoring) 212 + pub async fn get_info(&mut self) -> Result<String> { 213 + match redis::cmd("INFO").arg("memory").query_async::<String>(&mut self.conn).await { 214 + Ok(info) => Ok(info), 215 + Err(e) => { 216 + warn!(error = ?e, "Failed to get Redis info"); 217 + Ok("Redis info unavailable".to_string()) 218 + } 219 + } 220 + } 221 + } 222 + 223 + #[async_trait] 224 + impl Cache for RedisCache { 225 + async fn get<T>(&mut self, key: &str) -> Result<Option<T>> 226 + where 227 + T: for<'de> Deserialize<'de> + Send, 228 + { 229 + self.get_value(key).await 230 + } 231 + 232 + async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()> 233 + where 234 + T: Serialize + Send + Sync, 235 + { 236 + self.set_value(key, value, ttl_seconds).await 237 + } 238 + 239 + 240 + async fn delete(&mut self, key: &str) -> Result<()> { 241 + self.delete_key(key).await 242 + } 243 + 244 + async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()> 245 + where 246 + T: Serialize + Send + Sync, 247 + { 248 + self.set_multiple_values(items).await 249 + } 250 + 251 + async fn ping(&mut self) -> Result<bool> { 252 + RedisCache::ping(self).await 253 + } 254 + 255 + async fn get_info(&mut self) -> Result<String> { 256 + RedisCache::get_info(self).await 257 + } 258 + } 259 +
+34 -29
api/src/sync.rs
··· 8 8 resolve::{resolve_subject, HickoryDnsResolver}, 9 9 }; 10 10 11 - use crate::actor_resolver::resolve_actor_data; 11 + use crate::actor_resolver::{resolve_actor_data_cached, resolve_actor_data_with_retry}; 12 + use crate::cache::SliceCache; 12 13 use crate::database::Database; 13 14 use crate::errors::SyncError; 14 15 use crate::models::{Actor, Record}; 15 16 use crate::logging::LogLevel; 16 17 use crate::logging::Logger; 17 18 use serde_json::json; 19 + use std::sync::Arc; 20 + use tokio::sync::Mutex; 18 21 use uuid::Uuid; 19 22 20 23 ··· 66 69 client: Client, 67 70 database: Database, 68 71 relay_endpoint: String, 69 - atp_cache: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, AtpData>>>, 72 + cache: Option<Arc<Mutex<SliceCache>>>, 70 73 logger: Option<Logger>, 71 74 job_id: Option<Uuid>, 72 75 user_did: Option<String>, 73 76 } 74 77 75 78 impl SyncService { 76 - pub fn new(database: Database, relay_endpoint: String) -> Self { 79 + 80 + pub fn with_cache(database: Database, relay_endpoint: String, cache: Arc<Mutex<SliceCache>>) -> Self { 77 81 Self { 78 82 client: Client::new(), 79 83 database, 80 84 relay_endpoint, 81 - atp_cache: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), 85 + cache: Some(cache), 82 86 logger: None, 83 87 job_id: None, 84 88 user_did: None, 85 89 } 86 90 } 87 91 88 - /// Create a new SyncService with logging enabled for a specific job 89 - pub fn with_logging(database: Database, relay_endpoint: String, logger: Logger, job_id: Uuid, user_did: String) -> Self { 92 + 93 + /// Create a new SyncService with logging and cache enabled for a specific job 94 + pub fn with_logging_and_cache(database: Database, relay_endpoint: String, logger: Logger, job_id: Uuid, user_did: String, cache: Arc<Mutex<SliceCache>>) -> Self { 90 95 Self { 91 96 client: Client::new(), 92 97 database, 93 98 relay_endpoint, 94 - atp_cache: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), 99 + cache: Some(cache), 95 100 logger: Some(logger), 96 101 job_id: Some(job_id), 97 102 user_did: Some(user_did), ··· 126 131 127 132 let all_collections = [&primary_collections[..], &external_collections[..]].concat(); 128 133 129 - // Clear cache at start of each backfill operation 130 - self.clear_atp_cache(); 134 + // DID resolution cache is now handled by SliceCache 131 135 132 136 let all_repos = if let Some(provided_repos) = repos { 133 137 info!("📋 Using {} provided repositories", provided_repos.len()); ··· 474 478 475 479 async fn fetch_records_for_repo_collection_with_atp_map(&self, repo: &str, collection: &str, atp_map: &std::collections::HashMap<String, AtpData>, slice_uri: &str) -> Result<Vec<Record>, SyncError> { 476 480 let atp_data = atp_map.get(repo).ok_or_else(|| SyncError::Generic(format!("No ATP data found for repo: {}", repo)))?; 477 - self.fetch_records_for_repo_collection(repo, collection, &atp_data.pds, slice_uri).await 481 + 482 + match self.fetch_records_for_repo_collection(repo, collection, &atp_data.pds, slice_uri).await { 483 + Ok(records) => Ok(records), 484 + Err(SyncError::ListRecords { status }) if (400..600).contains(&status) => { 485 + // 4xx/5xx error from PDS - try invalidating cache and retrying once 486 + debug!("PDS error {} for repo {}, attempting cache invalidation and retry", status, repo); 487 + 488 + match resolve_actor_data_with_retry(&self.client, repo, self.cache.clone(), true).await { 489 + Ok(fresh_actor_data) => { 490 + debug!("Successfully re-resolved actor data for {}, retrying with PDS: {}", repo, fresh_actor_data.pds); 491 + self.fetch_records_for_repo_collection(repo, collection, &fresh_actor_data.pds, slice_uri).await 492 + } 493 + Err(e) => { 494 + debug!("Failed to re-resolve actor data for {}: {:?}", repo, e); 495 + Err(SyncError::ListRecords { status }) // Return original error 496 + } 497 + } 498 + } 499 + Err(e) => Err(e), // Other errors (network, etc.) - don't retry 500 + } 478 501 } 479 502 480 503 async fn fetch_records_for_repo_collection(&self, repo: &str, collection: &str, pds_url: &str, slice_uri: &str) -> Result<Vec<Record>, SyncError> { ··· 605 628 async fn resolve_atp_data(&self, did: &str) -> Result<AtpData, SyncError> { 606 629 debug!("Resolving ATP data for DID: {}", did); 607 630 608 - { 609 - let cache = self.atp_cache.lock().unwrap(); 610 - if let Some(cached_data) = cache.get(did) { 611 - debug!("Using cached ATP data for DID: {}", did); 612 - return Ok(cached_data.clone()); 613 - } 614 - } 615 - 616 631 let dns_resolver = HickoryDnsResolver::create_resolver(&[]); 617 632 618 633 match resolve_subject(&self.client, &dns_resolver, did).await { 619 634 Ok(resolved_did) => { 620 635 debug!("Successfully resolved subject: {}", resolved_did); 621 636 622 - let actor_data = resolve_actor_data(&self.client, &resolved_did).await 637 + let actor_data = resolve_actor_data_cached(&self.client, &resolved_did, self.cache.clone()).await 623 638 .map_err(|e| SyncError::Generic(e.to_string()))?; 624 639 625 640 let atp_data = AtpData { ··· 628 643 handle: actor_data.handle, 629 644 }; 630 645 631 - // Cache the result 632 - { 633 - let mut cache = self.atp_cache.lock().unwrap(); 634 - cache.insert(did.to_string(), atp_data.clone()); 635 - } 636 - 637 646 Ok(atp_data) 638 647 } 639 648 Err(e) => { ··· 664 673 Ok(()) 665 674 } 666 675 667 - pub fn clear_atp_cache(&self) { 668 - let mut cache = self.atp_cache.lock().unwrap(); 669 - cache.clear(); 670 - } 671 676 672 677 /// Get external collections for a slice (collections that don't start with the slice's domain) 673 678 async fn get_external_collections_for_slice(&self, slice_uri: &str) -> Result<Vec<String>, SyncError> {
+2 -2
api/src/xrpc/com/atproto/repo/upload_blob.rs
··· 9 9 let headers = request.headers().clone(); 10 10 11 11 let token = auth::extract_bearer_token(&headers)?; 12 - let _user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 12 + let _user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 13 13 14 14 let (dpop_auth, pds_url) = 15 - auth::get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?; 15 + auth::get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 16 16 17 17 let mime_type = headers 18 18 .get("content-type")
+1 -1
api/src/xrpc/network/slices/slice/create_oauth_client.rs
··· 102 102 } 103 103 104 104 let token = auth::extract_bearer_token(&headers)?; 105 - let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 105 + let user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 106 106 107 107 let user_did = user_info.sub; 108 108
+1 -1
api/src/xrpc/network/slices/slice/delete_oauth_client.rs
··· 21 21 Json(params): Json<Params>, 22 22 ) -> Result<Json<Output>, AppError> { 23 23 let token = auth::extract_bearer_token(&headers)?; 24 - auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 24 + auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 25 25 26 26 let oauth_client = state 27 27 .database
+1 -1
api/src/xrpc/network/slices/slice/get_oauth_clients.rs
··· 55 55 Query(params): Query<Params>, 56 56 ) -> Result<Json<Output>, AppError> { 57 57 let token = auth::extract_bearer_token(&headers)?; 58 - auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 58 + auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 59 59 60 60 let clients = state 61 61 .database
+1 -1
api/src/xrpc/network/slices/slice/start_sync.rs
··· 24 24 Json(params): Json<Params>, 25 25 ) -> Result<Json<Output>, AppError> { 26 26 let token = auth::extract_bearer_token(&headers)?; 27 - let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 27 + let user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 28 28 29 29 let user_did = user_info.sub; 30 30 let slice_uri = params.slice;
+2 -2
api/src/xrpc/network/slices/slice/sync_user_collections.rs
··· 20 20 Json(params): Json<Params>, 21 21 ) -> Result<Json<crate::sync::SyncUserCollectionsResult>, AppError> { 22 22 let token = auth::extract_bearer_token(&headers)?; 23 - let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 23 + let user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 24 24 25 25 let user_did = user_info.did.unwrap_or(user_info.sub); 26 26 ··· 38 38 ); 39 39 40 40 let sync_service = 41 - crate::sync::SyncService::new(state.database.clone(), state.config.relay_endpoint.clone()); 41 + crate::sync::SyncService::with_cache(state.database.clone(), state.config.relay_endpoint.clone(), state.auth_cache.clone()); 42 42 43 43 let result = sync_service 44 44 .sync_user_collections(&user_did, &params.slice, params.timeout_seconds)
+1 -1
api/src/xrpc/network/slices/slice/update_oauth_client.rs
··· 73 73 Json(params): Json<Params>, 74 74 ) -> Result<Json<Output>, AppError> { 75 75 let token = auth::extract_bearer_token(&headers)?; 76 - auth::verify_oauth_token(&token, &state.config.auth_base_url).await?; 76 + auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?; 77 77 78 78 let oauth_client = state 79 79 .database
+13
docker-compose.yml
··· 28 28 networks: 29 29 - default 30 30 31 + redis: 32 + image: redis:7-alpine 33 + ports: 34 + - "6379:6379" 35 + volumes: 36 + - redis_data:/data 37 + healthcheck: 38 + test: ["CMD", "redis-cli", "ping"] 39 + interval: 5s 40 + timeout: 3s 41 + retries: 5 42 + 31 43 aip: 32 44 image: ghcr.io/bigmoves/aip/aip-sqlite:main-e445b82 33 45 environment: ··· 54 66 55 67 volumes: 56 68 postgres_data: 69 + redis_data: 57 70 aip_data: