Highly ambitious ATProtocol AppView service and sdks
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+1749 -680
+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: