Parakeet is a Rust-based Bluesky AppServer aiming to implement most of the functionality required to support the Bluesky client
appview atproto bluesky rust appserver
67
fork

Configure Feed

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

feat: Parakeet Index - Stat Aggregations

Mia e89ecb4e 636e1ae8

+1341 -193
+1
.gitignore
··· 3 3 .idea/ 4 4 .env 5 5 Config.toml 6 + data/
+293 -16
Cargo.lock
··· 401 401 source = "registry+https://github.com/rust-lang/crates.io-index" 402 402 checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" 403 403 dependencies = [ 404 - "bitflags", 404 + "bitflags 2.8.0", 405 405 "cexpr", 406 406 "clang-sys", 407 407 "itertools 0.12.1", ··· 417 417 "syn", 418 418 "which", 419 419 ] 420 + 421 + [[package]] 422 + name = "bitflags" 423 + version = "1.3.2" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 420 426 421 427 [[package]] 422 428 name = "bitflags" ··· 656 662 "metrics", 657 663 "metrics-exporter-prometheus", 658 664 "parakeet-db", 665 + "parakeet-index", 659 666 "reqwest", 660 667 "serde", 661 668 "serde_bytes", ··· 663 670 "serde_json", 664 671 "tokio", 665 672 "tokio-postgres", 673 + "tokio-stream", 666 674 "tokio-tungstenite", 667 675 "tracing", 668 676 "tracing-subscriber", ··· 710 718 checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 711 719 dependencies = [ 712 720 "libc", 721 + ] 722 + 723 + [[package]] 724 + name = "crc32fast" 725 + version = "1.4.2" 726 + source = "registry+https://github.com/rust-lang/crates.io-index" 727 + checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 728 + dependencies = [ 729 + "cfg-if", 713 730 ] 714 731 715 732 [[package]] ··· 847 864 source = "registry+https://github.com/rust-lang/crates.io-index" 848 865 checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" 849 866 dependencies = [ 850 - "bitflags", 867 + "bitflags 2.8.0", 851 868 "byteorder", 852 869 "chrono", 853 870 "diesel_derives", ··· 1041 1058 ] 1042 1059 1043 1060 [[package]] 1061 + name = "fixedbitset" 1062 + version = "0.5.7" 1063 + source = "registry+https://github.com/rust-lang/crates.io-index" 1064 + checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 1065 + 1066 + [[package]] 1044 1067 name = "flume" 1045 1068 version = "0.11.1" 1046 1069 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1086 1109 checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 1087 1110 dependencies = [ 1088 1111 "percent-encoding", 1112 + ] 1113 + 1114 + [[package]] 1115 + name = "fs2" 1116 + version = "0.4.3" 1117 + source = "registry+https://github.com/rust-lang/crates.io-index" 1118 + checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" 1119 + dependencies = [ 1120 + "libc", 1121 + "winapi", 1089 1122 ] 1090 1123 1091 1124 [[package]] ··· 1197 1230 ] 1198 1231 1199 1232 [[package]] 1233 + name = "fxhash" 1234 + version = "0.2.1" 1235 + source = "registry+https://github.com/rust-lang/crates.io-index" 1236 + checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 1237 + dependencies = [ 1238 + "byteorder", 1239 + ] 1240 + 1241 + [[package]] 1200 1242 name = "generic-array" 1201 1243 version = "0.14.7" 1202 1244 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1335 1377 "ipconfig", 1336 1378 "lru-cache", 1337 1379 "once_cell", 1338 - "parking_lot", 1380 + "parking_lot 0.12.3", 1339 1381 "rand", 1340 1382 "resolv-conf", 1341 1383 "smallvec", ··· 1459 1501 ] 1460 1502 1461 1503 [[package]] 1504 + name = "hyper-timeout" 1505 + version = "0.5.2" 1506 + source = "registry+https://github.com/rust-lang/crates.io-index" 1507 + checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" 1508 + dependencies = [ 1509 + "hyper", 1510 + "hyper-util", 1511 + "pin-project-lite", 1512 + "tokio", 1513 + "tower-service", 1514 + ] 1515 + 1516 + [[package]] 1462 1517 name = "hyper-tls" 1463 1518 version = "0.6.0" 1464 1519 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1684 1739 checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" 1685 1740 1686 1741 [[package]] 1742 + name = "instant" 1743 + version = "0.1.13" 1744 + source = "registry+https://github.com/rust-lang/crates.io-index" 1745 + checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 1746 + dependencies = [ 1747 + "cfg-if", 1748 + ] 1749 + 1750 + [[package]] 1687 1751 name = "ipconfig" 1688 1752 version = "0.3.2" 1689 1753 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1999 2063 ] 2000 2064 2001 2065 [[package]] 2066 + name = "multimap" 2067 + version = "0.10.0" 2068 + source = "registry+https://github.com/rust-lang/crates.io-index" 2069 + checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" 2070 + 2071 + [[package]] 2002 2072 name = "nanorand" 2003 2073 version = "0.7.0" 2004 2074 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2084 2154 source = "registry+https://github.com/rust-lang/crates.io-index" 2085 2155 checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" 2086 2156 dependencies = [ 2087 - "bitflags", 2157 + "bitflags 2.8.0", 2088 2158 "cfg-if", 2089 2159 "foreign-types", 2090 2160 "libc", ··· 2145 2215 "itertools 0.14.0", 2146 2216 "lexica", 2147 2217 "parakeet-db", 2218 + "parakeet-index", 2148 2219 "serde", 2149 2220 "serde_json", 2150 2221 "tokio", ··· 2163 2234 ] 2164 2235 2165 2236 [[package]] 2237 + name = "parakeet-index" 2238 + version = "0.1.0" 2239 + dependencies = [ 2240 + "eyre", 2241 + "figment", 2242 + "itertools 0.14.0", 2243 + "prost", 2244 + "serde", 2245 + "sled", 2246 + "tokio", 2247 + "tonic", 2248 + "tonic-build", 2249 + "tracing", 2250 + "tracing-subscriber", 2251 + ] 2252 + 2253 + [[package]] 2166 2254 name = "parakeet-lexgen" 2167 2255 version = "0.1.0" 2168 2256 dependencies = [ ··· 2182 2270 2183 2271 [[package]] 2184 2272 name = "parking_lot" 2273 + version = "0.11.2" 2274 + source = "registry+https://github.com/rust-lang/crates.io-index" 2275 + checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 2276 + dependencies = [ 2277 + "instant", 2278 + "lock_api", 2279 + "parking_lot_core 0.8.6", 2280 + ] 2281 + 2282 + [[package]] 2283 + name = "parking_lot" 2185 2284 version = "0.12.3" 2186 2285 source = "registry+https://github.com/rust-lang/crates.io-index" 2187 2286 checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 2188 2287 dependencies = [ 2189 2288 "lock_api", 2190 - "parking_lot_core", 2289 + "parking_lot_core 0.9.10", 2290 + ] 2291 + 2292 + [[package]] 2293 + name = "parking_lot_core" 2294 + version = "0.8.6" 2295 + source = "registry+https://github.com/rust-lang/crates.io-index" 2296 + checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" 2297 + dependencies = [ 2298 + "cfg-if", 2299 + "instant", 2300 + "libc", 2301 + "redox_syscall 0.2.16", 2302 + "smallvec", 2303 + "winapi", 2191 2304 ] 2192 2305 2193 2306 [[package]] ··· 2198 2311 dependencies = [ 2199 2312 "cfg-if", 2200 2313 "libc", 2201 - "redox_syscall", 2314 + "redox_syscall 0.5.8", 2202 2315 "smallvec", 2203 2316 "windows-targets 0.52.6", 2204 2317 ] ··· 2239 2352 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 2240 2353 2241 2354 [[package]] 2355 + name = "petgraph" 2356 + version = "0.7.1" 2357 + source = "registry+https://github.com/rust-lang/crates.io-index" 2358 + checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" 2359 + dependencies = [ 2360 + "fixedbitset", 2361 + "indexmap", 2362 + ] 2363 + 2364 + [[package]] 2242 2365 name = "phf" 2243 2366 version = "0.11.3" 2244 2367 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2254 2377 checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2255 2378 dependencies = [ 2256 2379 "siphasher", 2380 + ] 2381 + 2382 + [[package]] 2383 + name = "pin-project" 2384 + version = "1.1.10" 2385 + source = "registry+https://github.com/rust-lang/crates.io-index" 2386 + checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 2387 + dependencies = [ 2388 + "pin-project-internal", 2389 + ] 2390 + 2391 + [[package]] 2392 + name = "pin-project-internal" 2393 + version = "1.1.10" 2394 + source = "registry+https://github.com/rust-lang/crates.io-index" 2395 + checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 2396 + dependencies = [ 2397 + "proc-macro2", 2398 + "quote", 2399 + "syn", 2257 2400 ] 2258 2401 2259 2402 [[package]] ··· 2378 2521 ] 2379 2522 2380 2523 [[package]] 2524 + name = "prost" 2525 + version = "0.13.5" 2526 + source = "registry+https://github.com/rust-lang/crates.io-index" 2527 + checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" 2528 + dependencies = [ 2529 + "bytes", 2530 + "prost-derive", 2531 + ] 2532 + 2533 + [[package]] 2534 + name = "prost-build" 2535 + version = "0.13.5" 2536 + source = "registry+https://github.com/rust-lang/crates.io-index" 2537 + checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" 2538 + dependencies = [ 2539 + "heck", 2540 + "itertools 0.14.0", 2541 + "log", 2542 + "multimap", 2543 + "once_cell", 2544 + "petgraph", 2545 + "prettyplease", 2546 + "prost", 2547 + "prost-types", 2548 + "regex", 2549 + "syn", 2550 + "tempfile", 2551 + ] 2552 + 2553 + [[package]] 2554 + name = "prost-derive" 2555 + version = "0.13.5" 2556 + source = "registry+https://github.com/rust-lang/crates.io-index" 2557 + checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" 2558 + dependencies = [ 2559 + "anyhow", 2560 + "itertools 0.14.0", 2561 + "proc-macro2", 2562 + "quote", 2563 + "syn", 2564 + ] 2565 + 2566 + [[package]] 2567 + name = "prost-types" 2568 + version = "0.13.5" 2569 + source = "registry+https://github.com/rust-lang/crates.io-index" 2570 + checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" 2571 + dependencies = [ 2572 + "prost", 2573 + ] 2574 + 2575 + [[package]] 2381 2576 name = "quanta" 2382 2577 version = "0.12.5" 2383 2578 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2452 2647 source = "registry+https://github.com/rust-lang/crates.io-index" 2453 2648 checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" 2454 2649 dependencies = [ 2455 - "bitflags", 2650 + "bitflags 2.8.0", 2651 + ] 2652 + 2653 + [[package]] 2654 + name = "redox_syscall" 2655 + version = "0.2.16" 2656 + source = "registry+https://github.com/rust-lang/crates.io-index" 2657 + checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 2658 + dependencies = [ 2659 + "bitflags 1.3.2", 2456 2660 ] 2457 2661 2458 2662 [[package]] ··· 2461 2665 source = "registry+https://github.com/rust-lang/crates.io-index" 2462 2666 checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 2463 2667 dependencies = [ 2464 - "bitflags", 2668 + "bitflags 2.8.0", 2465 2669 ] 2466 2670 2467 2671 [[package]] ··· 2580 2784 source = "registry+https://github.com/rust-lang/crates.io-index" 2581 2785 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 2582 2786 dependencies = [ 2583 - "bitflags", 2787 + "bitflags 2.8.0", 2584 2788 "errno", 2585 2789 "libc", 2586 2790 "linux-raw-sys", ··· 2691 2895 source = "registry+https://github.com/rust-lang/crates.io-index" 2692 2896 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2693 2897 dependencies = [ 2694 - "bitflags", 2898 + "bitflags 2.8.0", 2695 2899 "core-foundation 0.9.4", 2696 2900 "core-foundation-sys", 2697 2901 "libc", ··· 2704 2908 source = "registry+https://github.com/rust-lang/crates.io-index" 2705 2909 checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 2706 2910 dependencies = [ 2707 - "bitflags", 2911 + "bitflags 2.8.0", 2708 2912 "core-foundation 0.10.0", 2709 2913 "core-foundation-sys", 2710 2914 "libc", ··· 2886 3090 ] 2887 3091 2888 3092 [[package]] 3093 + name = "sled" 3094 + version = "0.34.7" 3095 + source = "registry+https://github.com/rust-lang/crates.io-index" 3096 + checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" 3097 + dependencies = [ 3098 + "crc32fast", 3099 + "crossbeam-epoch", 3100 + "crossbeam-utils", 3101 + "fs2", 3102 + "fxhash", 3103 + "libc", 3104 + "log", 3105 + "parking_lot 0.11.2", 3106 + ] 3107 + 3108 + [[package]] 2889 3109 name = "smallvec" 2890 3110 version = "1.13.2" 2891 3111 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2976 3196 source = "registry+https://github.com/rust-lang/crates.io-index" 2977 3197 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2978 3198 dependencies = [ 2979 - "bitflags", 3199 + "bitflags 2.8.0", 2980 3200 "core-foundation 0.9.4", 2981 3201 "system-configuration-sys", 2982 3202 ] ··· 3090 3310 "bytes", 3091 3311 "libc", 3092 3312 "mio", 3093 - "parking_lot", 3313 + "parking_lot 0.12.3", 3094 3314 "pin-project-lite", 3095 3315 "signal-hook-registry", 3096 3316 "socket2", ··· 3132 3352 "futures-channel", 3133 3353 "futures-util", 3134 3354 "log", 3135 - "parking_lot", 3355 + "parking_lot 0.12.3", 3136 3356 "percent-encoding", 3137 3357 "phf", 3138 3358 "pin-project-lite", ··· 3156 3376 ] 3157 3377 3158 3378 [[package]] 3379 + name = "tokio-stream" 3380 + version = "0.1.17" 3381 + source = "registry+https://github.com/rust-lang/crates.io-index" 3382 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 3383 + dependencies = [ 3384 + "futures-core", 3385 + "pin-project-lite", 3386 + "tokio", 3387 + ] 3388 + 3389 + [[package]] 3159 3390 name = "tokio-tungstenite" 3160 3391 version = "0.26.1" 3161 3392 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3217 3448 ] 3218 3449 3219 3450 [[package]] 3451 + name = "tonic" 3452 + version = "0.13.0" 3453 + source = "registry+https://github.com/rust-lang/crates.io-index" 3454 + checksum = "85839f0b32fd242bb3209262371d07feda6d780d16ee9d2bc88581b89da1549b" 3455 + dependencies = [ 3456 + "async-trait", 3457 + "axum", 3458 + "base64", 3459 + "bytes", 3460 + "h2", 3461 + "http", 3462 + "http-body", 3463 + "http-body-util", 3464 + "hyper", 3465 + "hyper-timeout", 3466 + "hyper-util", 3467 + "percent-encoding", 3468 + "pin-project", 3469 + "prost", 3470 + "socket2", 3471 + "tokio", 3472 + "tokio-stream", 3473 + "tower", 3474 + "tower-layer", 3475 + "tower-service", 3476 + "tracing", 3477 + ] 3478 + 3479 + [[package]] 3480 + name = "tonic-build" 3481 + version = "0.13.0" 3482 + source = "registry+https://github.com/rust-lang/crates.io-index" 3483 + checksum = "d85f0383fadd15609306383a90e85eaed44169f931a5d2be1b42c76ceff1825e" 3484 + dependencies = [ 3485 + "prettyplease", 3486 + "proc-macro2", 3487 + "prost-build", 3488 + "prost-types", 3489 + "quote", 3490 + "syn", 3491 + ] 3492 + 3493 + [[package]] 3220 3494 name = "tower" 3221 3495 version = "0.5.2" 3222 3496 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3224 3498 dependencies = [ 3225 3499 "futures-core", 3226 3500 "futures-util", 3501 + "indexmap", 3227 3502 "pin-project-lite", 3503 + "slab", 3228 3504 "sync_wrapper", 3229 3505 "tokio", 3506 + "tokio-util", 3230 3507 "tower-layer", 3231 3508 "tower-service", 3232 3509 "tracing", ··· 3238 3515 source = "registry+https://github.com/rust-lang/crates.io-index" 3239 3516 checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" 3240 3517 dependencies = [ 3241 - "bitflags", 3518 + "bitflags 2.8.0", 3242 3519 "bytes", 3243 3520 "http", 3244 3521 "http-body", ··· 3592 3869 source = "registry+https://github.com/rust-lang/crates.io-index" 3593 3870 checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 3594 3871 dependencies = [ 3595 - "redox_syscall", 3872 + "redox_syscall 0.5.8", 3596 3873 "wasite", 3597 3874 "web-sys", 3598 3875 ]
+1
Cargo.toml
··· 8 8 "lexica", 9 9 "parakeet", 10 10 "parakeet-db", 11 + "parakeet-index", 11 12 "parakeet-lexgen" 12 13 ]
+2
consumer/Cargo.toml
··· 20 20 metrics = "0.24.1" 21 21 metrics-exporter-prometheus = "0.16.2" 22 22 parakeet-db = { path = "../parakeet-db" } 23 + parakeet-index = { path = "../parakeet-index" } 23 24 reqwest = { version = "0.12.12", features = ["native-tls"] } 24 25 serde = { version = "1.0.217", features = ["derive"] } 25 26 serde_bytes = "0.11" ··· 27 28 serde_json = "1.0.134" 28 29 tokio = { version = "1.42.0", features = ["full"] } 29 30 tokio-postgres = { version = "0.7.12", features = ["with-chrono-0_4"] } 31 + tokio-stream = "0.1.17" 30 32 tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } 31 33 tracing = "0.1.40" 32 34 tracing-subscriber = "0.3.18"
+1
consumer/run.sh
··· 1 + cargo run
+68 -51
consumer/src/backfill/mod.rs
··· 1 1 use crate::config::HistoryMode; 2 - use crate::indexer::types::{BackfillItem, BackfillItemInner, CollectionType, RecordTypes}; 2 + use crate::indexer::types::{AggregateDeltaStore, BackfillItem, BackfillItemInner}; 3 3 use crate::indexer::{self, db as indexer_db}; 4 4 use did_resolver::Resolver; 5 5 use diesel_async::pooled_connection::deadpool::Pool; ··· 9 9 use metrics::counter; 10 10 use parakeet_db::types::{ActorStatus, ActorSyncState}; 11 11 use reqwest::{Client, StatusCode}; 12 + use std::collections::HashMap; 12 13 use std::str::FromStr; 13 14 use std::sync::Arc; 14 15 use tracing::{instrument, Instrument}; ··· 18 19 mod types; 19 20 20 21 const PDS_SERVICE_ID: &str = "#atproto_pds"; 22 + // There's a 4MiB limit on parakeet-index, so break delta batches up if there's loads. 23 + // this should be plenty low enough to not trigger the size limit. (59k did slightly) 24 + const DELTA_BATCH_SIZE: usize = 32 * 1024; 21 25 22 26 #[derive(Clone)] 23 27 pub struct BackfillManagerInner { 24 28 pool: Pool<AsyncPgConnection>, 25 29 resolver: Arc<Resolver>, 26 30 client: Client, 31 + index_client: parakeet_index::Client, 27 32 } 28 33 29 34 pub struct BackfillManager { ··· 37 42 pool: Pool<AsyncPgConnection>, 38 43 history_mode: HistoryMode, 39 44 resolver: Arc<Resolver>, 45 + index_client: parakeet_index::Client, 40 46 ) -> eyre::Result<(Self, Sender<String>)> { 41 47 let client = Client::new(); 42 48 ··· 47 53 pool, 48 54 resolver, 49 55 client, 56 + index_client, 50 57 }, 51 58 rx, 52 59 do_backfill: history_mode == HistoryMode::BackfillHistory, ··· 61 68 if self.do_backfill { 62 69 for idx in 0..threads { 63 70 let rx = self.rx.clone(); 64 - let inner = self.inner.clone(); 71 + let mut inner = self.inner.clone(); 65 72 66 73 js.spawn( 67 74 async move { 68 75 while let Ok(did) = rx.recv_async().await { 69 76 tracing::trace!("backfilling {did}"); 70 - if let Err(e) = backfill_actor(&inner, &did).await { 77 + if let Err(e) = backfill_actor(&mut inner, &did).await { 71 78 tracing::error!(did, "backfill failed: {e}"); 72 79 counter!("backfill_failure").increment(1); 73 80 } else { ··· 90 97 } 91 98 92 99 #[instrument(skip(inner))] 93 - async fn backfill_actor(inner: &BackfillManagerInner, did: &str) -> eyre::Result<()> { 100 + async fn backfill_actor(inner: &mut BackfillManagerInner, did: &str) -> eyre::Result<()> { 94 101 let mut conn = inner.pool.get().await?; 95 102 96 103 let (status, sync_state) = db::get_actor_status(&mut conn, did).await?; ··· 161 168 162 169 tracing::trace!("repo pulled - inserting"); 163 170 164 - conn.transaction::<(), diesel::result::Error, _>(|t| { 165 - Box::pin(async move { 166 - db::defer(t).await?; 171 + let delta_store = conn 172 + .transaction::<_, diesel::result::Error, _>(|t| { 173 + Box::pin(async move { 174 + let mut delta_store = HashMap::new(); 167 175 168 - indexer_db::update_repo_version(t, did, &rev, cid).await?; 176 + db::defer(t).await?; 169 177 170 - let mut follow_stats = vec![did.to_string()]; 178 + indexer_db::update_repo_version(t, did, &rev, cid).await?; 171 179 172 - for (path, (cid, record)) in records { 173 - let Some((collection, rkey)) = path.split_once("/") else { 174 - tracing::warn!("record contained invalid path {}", path); 175 - return Err(diesel::result::Error::RollbackTransaction); 176 - }; 180 + // let mut follow_stats = vec![did.to_string()]; 177 181 178 - counter!("backfilled_commits", "collection" => collection.to_string()).increment(1); 182 + for (path, (cid, record)) in records { 183 + let Some((collection, rkey)) = path.split_once("/") else { 184 + tracing::warn!("record contained invalid path {}", path); 185 + return Err(diesel::result::Error::RollbackTransaction); 186 + }; 179 187 180 - let full_path = format!("at://{did}/{path}"); 188 + counter!("backfilled_commits", "collection" => collection.to_string()) 189 + .increment(1); 181 190 182 - match record { 183 - RecordTypes::AppBskyGraphFollow(record) => { 184 - follow_stats.push(record.subject.clone()); 185 - indexer_db::insert_follow(t, did, &full_path, record).await?; 186 - } 187 - _ => indexer::index_op(t, did, cid, record, &full_path, rkey).await?, 191 + let full_path = format!("at://{did}/{path}"); 192 + 193 + indexer::index_op(t, &mut delta_store, did, cid, record, &full_path, rkey) 194 + .await? 188 195 } 189 - } 190 196 191 - db::update_repo_sync_state(t, did, ActorSyncState::Synced).await?; 197 + db::update_repo_sync_state(t, did, ActorSyncState::Synced).await?; 192 198 193 - handle_backfill_rows(t, &mut follow_stats, did, &rev).await?; 199 + handle_backfill_rows(t, &mut delta_store, did, &rev).await?; 200 + tracing::trace!("insertion finished"); 194 201 195 - // on second thought, should this be done after the transaction? 196 - // if we're loading a chunky repo, we might be a few seconds+ out of date? 197 - indexer_db::update_follow_stats(t, &follow_stats).await?; 202 + Ok(delta_store) 203 + }) 204 + }) 205 + .await?; 198 206 199 - tracing::trace!("insertion finished"); 200 - Ok(()) 207 + // submit the deltas 208 + let delta_store = delta_store 209 + .into_iter() 210 + .map(|((uri, typ), delta)| parakeet_index::AggregateDeltaReq { 211 + typ, 212 + uri: uri.to_string(), 213 + delta, 201 214 }) 202 - }) 203 - .await?; 215 + .collect::<Vec<_>>(); 216 + 217 + let mut read = 0; 218 + 219 + while read < delta_store.len() { 220 + let rem = delta_store.len() - read; 221 + let take = DELTA_BATCH_SIZE.min(rem); 222 + 223 + tracing::debug!("reading & submitting {take} deltas"); 224 + 225 + let deltas = delta_store[read..read + take].to_vec(); 226 + inner 227 + .index_client 228 + .submit_aggregate_delta_batch(parakeet_index::AggregateDeltaBatchReq { deltas }) 229 + .await?; 230 + 231 + read += take; 232 + tracing::debug!("read {read} of {} deltas", delta_store.len()); 233 + } 204 234 205 235 Ok(()) 206 236 } 207 237 208 238 async fn handle_backfill_rows( 209 239 conn: &mut AsyncPgConnection, 210 - follow_stats: &mut Vec<String>, 240 + deltas: &mut impl AggregateDeltaStore, 211 241 repo: &str, 212 242 rev: &str, 213 243 ) -> diesel::QueryResult<()> { ··· 233 263 continue; 234 264 }; 235 265 236 - match record { 237 - RecordTypes::AppBskyGraphFollow(follow) => { 238 - follow_stats.push(follow.subject.clone()); 239 - indexer_db::insert_follow(conn, repo, &item.at_uri, follow).await?; 240 - } 241 - _ => indexer::index_op(conn, repo, cid, record, &item.at_uri, rkey).await?, 242 - } 266 + indexer::index_op(conn, deltas, repo, cid, record, &item.at_uri, rkey).await? 267 + } 268 + BackfillItemInner::Delete => { 269 + indexer::index_op_delete(conn, deltas, repo, item.collection, &item.at_uri) 270 + .await? 243 271 } 244 - BackfillItemInner::Delete => match item.collection { 245 - CollectionType::BskyFollow => { 246 - if let Some(subject) = indexer_db::delete_follow(conn, &item.at_uri).await? 247 - { 248 - follow_stats.push(subject); 249 - } 250 - } 251 - _ => { 252 - indexer::index_op_delete(conn, repo, item.collection, &item.at_uri).await? 253 - } 254 - }, 255 272 } 256 273 } 257 274 }
+1
consumer/src/config.rs
··· 13 13 14 14 #[derive(Debug, Deserialize)] 15 15 pub struct Config { 16 + pub index_uri: String, 16 17 pub relay_source: String, 17 18 pub database_url: String, 18 19 pub plc_directory: Option<String>,
+33 -4
consumer/src/indexer/db.rs
··· 522 522 .await 523 523 } 524 524 525 + pub async fn get_post_info_for_delete( 526 + conn: &mut AsyncPgConnection, 527 + at_uri: &str, 528 + ) -> QueryResult<Option<(Option<String>, Option<String>)>> { 529 + schema::posts::table 530 + .left_join( 531 + schema::post_embed_record::table 532 + .on(schema::posts::at_uri.eq(schema::post_embed_record::post_uri)), 533 + ) 534 + .select(( 535 + schema::posts::parent_uri, 536 + schema::post_embed_record::uri.nullable(), 537 + )) 538 + .filter(schema::posts::at_uri.eq(at_uri)) 539 + .get_result(conn) 540 + .await 541 + .optional() 542 + } 543 + 525 544 pub async fn upsert_postgate( 526 545 conn: &mut AsyncPgConnection, 527 546 at_uri: &str, ··· 642 661 .await 643 662 } 644 663 645 - pub async fn delete_like(conn: &mut AsyncPgConnection, at_uri: &str) -> QueryResult<usize> { 664 + pub async fn delete_like( 665 + conn: &mut AsyncPgConnection, 666 + at_uri: &str, 667 + ) -> QueryResult<Option<String>> { 646 668 diesel::delete(schema::likes::table) 647 669 .filter(schema::likes::at_uri.eq(at_uri)) 648 - .execute(conn) 670 + .returning(schema::likes::subject) 671 + .get_result(conn) 649 672 .await 673 + .optional() 650 674 } 651 675 652 676 pub async fn insert_repost( ··· 669 693 .await 670 694 } 671 695 672 - pub async fn delete_repost(conn: &mut AsyncPgConnection, at_uri: &str) -> QueryResult<usize> { 696 + pub async fn delete_repost( 697 + conn: &mut AsyncPgConnection, 698 + at_uri: &str, 699 + ) -> QueryResult<Option<String>> { 673 700 diesel::delete(schema::reposts::table) 674 701 .filter(schema::reposts::at_uri.eq(at_uri)) 675 - .execute(conn) 702 + .returning(schema::reposts::post) 703 + .get_result(conn) 676 704 .await 705 + .optional() 677 706 } 678 707 679 708 pub async fn upsert_chat_decl(
+114 -21
consumer/src/indexer/mod.rs
··· 1 1 use crate::config::HistoryMode; 2 2 use crate::firehose::{AtpAccountEvent, AtpCommitEvent, AtpIdentityEvent, CommitOp, FirehoseEvent}; 3 - use crate::indexer::types::{BackfillItem, BackfillItemInner, CollectionType, RecordTypes}; 3 + use crate::indexer::types::{ 4 + AggregateDeltaStore, BackfillItem, BackfillItemInner, CollectionType, RecordTypes, 5 + }; 4 6 use did_resolver::Resolver; 5 7 use diesel_async::pooled_connection::deadpool::Pool; 6 8 use diesel_async::{AsyncConnection, AsyncPgConnection}; ··· 9 11 use ipld_core::cid::Cid; 10 12 use metrics::counter; 11 13 use parakeet_db::types::{ActorStatus, ActorSyncState}; 14 + use parakeet_index::AggregateType; 12 15 use std::collections::HashMap; 13 16 use std::hash::BuildHasher; 14 17 use std::sync::Arc; ··· 22 25 #[derive(Clone)] 23 26 struct RelayIndexerState { 24 27 backfill_tx: flume::Sender<String>, 28 + idxc_tx: Sender<parakeet_index::AggregateDeltaReq>, 25 29 resolver: Arc<Resolver>, 26 30 do_backfill: bool, 27 31 } ··· 37 41 pub async fn new( 38 42 pool: Pool<AsyncPgConnection>, 39 43 backfill_tx: flume::Sender<String>, 44 + idxc_tx: Sender<parakeet_index::AggregateDeltaReq>, 40 45 resolver: Arc<Resolver>, 41 46 history_mode: HistoryMode, 42 47 ) -> eyre::Result<(Self, Sender<FirehoseEvent>)> { ··· 48 53 backfill_tx, 49 54 resolver, 50 55 do_backfill: history_mode == HistoryMode::BackfillHistory, 56 + idxc_tx, 51 57 }, 52 58 rx, 53 59 hasher: RandomState::default(), ··· 60 66 let (submit, _handles) = (0..threads) 61 67 .map(|idx| { 62 68 let pool = self.pool.clone(); 63 - let state = self.state.clone(); 69 + let mut state = self.state.clone(); 64 70 let (tx, mut rx) = channel(16); 65 71 66 72 let handle = tokio::spawn(async move { ··· 76 82 index_account(&state, &mut conn, account).await 77 83 } 78 84 FirehoseEvent::Commit(commit) => { 79 - index_commit(&state, &mut conn, commit).await 85 + index_commit(&mut state, &mut conn, commit).await 80 86 } 81 87 FirehoseEvent::Label(_) => unreachable!(), 82 88 }; ··· 186 192 187 193 #[instrument(skip_all, fields(seq = commit.seq, repo = commit.repo, rev = commit.rev))] 188 194 async fn index_commit( 189 - state: &RelayIndexerState, 195 + state: &mut RelayIndexerState, 190 196 conn: &mut AsyncPgConnection, 191 197 commit: AtpCommitEvent, 192 198 ) -> eyre::Result<()> { ··· 262 268 } 263 269 264 270 for op in &commit.ops { 265 - process_op(t, &commit.repo, op, &blocks).await?; 271 + process_op(t, &mut state.idxc_tx, &commit.repo, op, &blocks).await?; 266 272 } 267 273 } else { 268 274 let items = commit ··· 333 339 #[inline(always)] 334 340 async fn process_op( 335 341 conn: &mut AsyncPgConnection, 342 + deltas: &mut impl AggregateDeltaStore, 336 343 repo: &str, 337 344 op: &CommitOp, 338 345 blocks: &HashMap<Cid, Vec<u8>>, ··· 361 368 return Ok(()); 362 369 }; 363 370 364 - index_op(conn, repo, cid, decoded, &full_path, rkey).await?; 371 + index_op(conn, deltas, repo, cid, decoded, &full_path, rkey).await?; 365 372 } else if op.action == "delete" { 366 - index_op_delete(conn, repo, collection, &full_path).await?; 373 + index_op_delete(conn, deltas, repo, collection, &full_path).await?; 367 374 } else { 368 375 tracing::warn!("op contained invalid action {}", op.action); 369 376 } ··· 388 395 389 396 pub async fn index_op( 390 397 conn: &mut AsyncPgConnection, 398 + deltas: &mut impl AggregateDeltaStore, 391 399 repo: &str, 392 400 cid: Cid, 393 401 record: RecordTypes, ··· 407 415 } 408 416 RecordTypes::AppBskyFeedGenerator(record) => { 409 417 let labels = record.labels.clone(); 410 - db::upsert_feedgen(conn, repo, cid, at_uri, record).await?; 418 + let count = db::upsert_feedgen(conn, repo, cid, at_uri, record).await?; 411 419 412 420 if let Some(labels) = labels { 413 421 db::maintain_self_labels(conn, repo, Some(cid), at_uri, labels).await?; 414 422 } 423 + 424 + deltas 425 + .add_delta(repo, AggregateType::ProfileFeed, count as i32) 426 + .await; 415 427 } 416 428 RecordTypes::AppBskyFeedLike(record) => { 417 - db::insert_like(conn, repo, at_uri, record).await?; 429 + let subject = record.subject.uri.clone(); 430 + let count = db::insert_like(conn, repo, at_uri, record).await?; 431 + 432 + deltas 433 + .add_delta(&subject, AggregateType::Like, count as i32) 434 + .await; 418 435 } 419 436 RecordTypes::AppBskyFeedPost(record) => { 420 437 if let Some(records::AppBskyEmbed::RecordWithMedia(embed)) = &record.embed { ··· 423 440 } 424 441 } 425 442 443 + let maybe_reply = record.reply.as_ref().map(|v| v.parent.uri.clone()); 444 + let maybe_embed = record.embed.as_ref().and_then(|v| match v { 445 + records::AppBskyEmbed::Record(r) => Some(r.record.uri.clone()), 446 + records::AppBskyEmbed::RecordWithMedia(r) => Some(r.record.record.uri.clone()), 447 + _ => None, 448 + }); 449 + 426 450 let labels = record.labels.clone(); 427 451 db::insert_post(conn, repo, cid, at_uri, record).await?; 428 452 if let Some(labels) = labels { 429 453 db::maintain_self_labels(conn, repo, Some(cid), at_uri, labels).await?; 430 454 } 455 + 456 + deltas.incr(repo, AggregateType::ProfilePost).await; 457 + if let Some(reply) = maybe_reply { 458 + deltas.incr(&reply, AggregateType::Reply).await; 459 + } 460 + if let Some(embed) = maybe_embed { 461 + deltas.incr(&embed, AggregateType::Embed).await; 462 + } 431 463 } 432 464 RecordTypes::AppBskyFeedPostgate(record) => { 433 465 let split_aturi = record.post.rsplitn(4, '/').collect::<Vec<_>>(); ··· 452 484 .await?; 453 485 } 454 486 RecordTypes::AppBskyFeedRepost(record) => { 487 + deltas 488 + .incr(&record.subject.uri, AggregateType::Repost) 489 + .await; 455 490 db::insert_repost(conn, repo, at_uri, record).await?; 456 491 } 457 492 RecordTypes::AppBskyFeedThreadgate(record) => { ··· 467 502 db::insert_block(conn, repo, at_uri, record).await?; 468 503 } 469 504 RecordTypes::AppBskyGraphFollow(record) => { 470 - db::insert_follow(conn, repo, at_uri, record).await?; 505 + let subject = record.subject.clone(); 506 + let count = db::insert_follow(conn, repo, at_uri, record).await?; 507 + 508 + deltas 509 + .add_delta(repo, AggregateType::Follow, count as i32) 510 + .await; 511 + deltas 512 + .add_delta(&subject, AggregateType::Follower, count as i32) 513 + .await; 471 514 } 472 515 RecordTypes::AppBskyGraphList(record) => { 473 516 let labels = record.labels.clone(); 474 - db::upsert_list(conn, repo, at_uri, cid, record).await?; 517 + let count = db::upsert_list(conn, repo, at_uri, cid, record).await?; 475 518 476 519 if let Some(labels) = labels { 477 520 db::maintain_self_labels(conn, repo, Some(cid), at_uri, labels).await?; 478 521 } 479 522 480 - // todo: when we have profile stats, update them. 523 + deltas 524 + .add_delta(repo, AggregateType::ProfileList, count as i32) 525 + .await; 481 526 } 482 527 RecordTypes::AppBskyGraphListBlock(record) => { 483 528 db::insert_list_block(conn, repo, at_uri, record).await?; ··· 493 538 db::insert_list_item(conn, at_uri, record).await?; 494 539 } 495 540 RecordTypes::AppBskyGraphStarterPack(record) => { 496 - db::upsert_starterpack(conn, repo, cid, at_uri, record).await?; 541 + let count = db::upsert_starterpack(conn, repo, cid, at_uri, record).await?; 542 + deltas 543 + .add_delta(repo, AggregateType::ProfileStarterpack, count as i32) 544 + .await; 497 545 } 498 546 RecordTypes::AppBskyGraphVerification(record) => { 499 547 db::upsert_verification(conn, repo, cid, at_uri, record).await?; ··· 520 568 521 569 pub async fn index_op_delete( 522 570 conn: &mut AsyncPgConnection, 571 + deltas: &mut impl AggregateDeltaStore, 523 572 repo: &str, 524 573 collection: CollectionType, 525 574 at_uri: &str, ··· 527 576 match collection { 528 577 CollectionType::BskyProfile => db::delete_profile(conn, repo).await?, 529 578 CollectionType::BskyBlock => db::delete_block(conn, at_uri).await?, 530 - CollectionType::BskyFeedGen => db::delete_feedgen(conn, at_uri).await?, 531 - CollectionType::BskyFeedLike => db::delete_like(conn, at_uri).await?, 532 - CollectionType::BskyFeedPost => db::delete_post(conn, at_uri).await?, 579 + CollectionType::BskyFeedGen => { 580 + let count = db::delete_feedgen(conn, at_uri).await?; 581 + deltas 582 + .add_delta(repo, AggregateType::ProfileFeed, -(count as i32)) 583 + .await; 584 + count 585 + } 586 + CollectionType::BskyFeedLike => { 587 + if let Some(subject) = db::delete_like(conn, at_uri).await? { 588 + deltas.decr(&subject, AggregateType::Like).await; 589 + } 590 + 0 591 + } 592 + CollectionType::BskyFeedPost => { 593 + let post_info = db::get_post_info_for_delete(conn, at_uri).await?; 594 + 595 + db::delete_post(conn, at_uri).await?; 596 + 597 + if let Some((reply_to, embed)) = post_info { 598 + deltas.decr(repo, AggregateType::ProfilePost).await; 599 + if let Some(reply_to) = reply_to { 600 + deltas.decr(&reply_to, AggregateType::Reply).await; 601 + } 602 + if let Some(embed) = embed { 603 + deltas.decr(&embed, AggregateType::Embed).await; 604 + } 605 + } 606 + 607 + 0 608 + } 533 609 CollectionType::BskyFeedPostgate => db::delete_postgate(conn, at_uri).await?, 534 - CollectionType::BskyFeedRepost => db::delete_repost(conn, at_uri).await?, 610 + CollectionType::BskyFeedRepost => { 611 + if let Some(subject) = db::delete_repost(conn, at_uri).await? { 612 + deltas.decr(&subject, AggregateType::Repost).await; 613 + } 614 + 0 615 + } 535 616 CollectionType::BskyFeedThreadgate => db::delete_threadgate(conn, at_uri).await?, 536 617 CollectionType::BskyFollow => { 537 - db::delete_follow(conn, at_uri).await?; 618 + if let Some(followee) = db::delete_follow(conn, at_uri).await? { 619 + deltas.decr(&followee, AggregateType::Follower).await; 620 + deltas.decr(repo, AggregateType::Follow).await; 621 + } 538 622 0 539 623 } 540 624 CollectionType::BskyList => { 541 - db::delete_list(conn, at_uri).await? 542 - // todo: when we have profile stats, update them. 625 + let count = db::delete_list(conn, at_uri).await?; 626 + deltas 627 + .add_delta(repo, AggregateType::ProfileList, -(count as i32)) 628 + .await; 629 + count 543 630 } 544 631 CollectionType::BskyListBlock => db::delete_list_block(conn, at_uri).await?, 545 632 CollectionType::BskyListItem => db::delete_list_item(conn, at_uri).await?, 546 - CollectionType::BskyStarterPack => db::delete_starterpack(conn, at_uri).await?, 633 + CollectionType::BskyStarterPack => { 634 + let count = db::delete_starterpack(conn, at_uri).await?; 635 + deltas 636 + .add_delta(repo, AggregateType::ProfileStarterpack, -(count as i32)) 637 + .await; 638 + count 639 + } 547 640 CollectionType::BskyVerification => db::delete_verification(conn, at_uri).await?, 548 641 CollectionType::BskyLabelerService => db::delete_label_service(conn, at_uri).await?, 549 642 CollectionType::ChatActorDecl => db::delete_chat_decl(conn, at_uri).await?,
+33
consumer/src/indexer/types.rs
··· 121 121 Update(RecordTypes), 122 122 Delete, 123 123 } 124 + 125 + pub trait AggregateDeltaStore { 126 + async fn add_delta(&mut self, uri: &str, typ: parakeet_index::AggregateType, delta: i32); 127 + async fn incr(&mut self, uri: &str, typ: parakeet_index::AggregateType) { 128 + self.add_delta(uri, typ, 1).await 129 + } 130 + async fn decr(&mut self, uri: &str, typ: parakeet_index::AggregateType) { 131 + self.add_delta(uri, typ, -1).await 132 + } 133 + } 134 + 135 + impl AggregateDeltaStore for tokio::sync::mpsc::Sender<parakeet_index::AggregateDeltaReq> { 136 + async fn add_delta(&mut self, uri: &str, typ: parakeet_index::AggregateType, delta: i32) { 137 + let res = self 138 + .send(parakeet_index::AggregateDeltaReq { 139 + typ: typ.into(), 140 + uri: uri.to_string(), 141 + delta, 142 + }) 143 + .await; 144 + 145 + if let Err(e) = res { 146 + tracing::error!("failed to send aggregate delta: {e}"); 147 + } 148 + } 149 + } 150 + 151 + impl AggregateDeltaStore for std::collections::HashMap<(String, i32), i32> { 152 + async fn add_delta(&mut self, uri: &str, typ: parakeet_index::AggregateType, delta: i32) { 153 + let key = (uri.to_string(), typ.into()); 154 + self.entry(key).and_modify(|v| *v += delta).or_insert(delta); 155 + } 156 + }
+16 -1
consumer/src/main.rs
··· 31 31 ..Default::default() 32 32 })?); 33 33 34 + let index_client = parakeet_index::Client::connect(conf.index_uri).await?; 35 + 34 36 let (label_mgr, label_svc_tx) = label_indexer::LabelServiceManager::new( 35 37 &conf.database_url, 36 38 resolver.clone(), ··· 42 44 let (backfiller, backfill_tx) = 43 45 backfill::BackfillManager::new(pool.clone(), conf.history_mode, resolver.clone()).await?; 44 46 47 + let (idxc_tx, idxc_rx) = tokio::sync::mpsc::channel(128); 48 + 45 49 let (relay_indexer, tx) = indexer::RelayIndexer::new( 46 50 pool.clone(), 47 51 backfill_tx, 52 + idxc_tx, 48 53 resolver.clone(), 49 54 conf.history_mode, 50 55 ) 51 56 .await?; 52 57 53 - let (firehose_res, indexer_res, backfill_res, label_res) = tokio::try_join! { 58 + let (firehose_res, indexer_res, backfill_res, label_res, idxt_res) = tokio::try_join! { 54 59 tokio::spawn(relay_consumer(relay_firehose, tx)), 55 60 tokio::spawn(relay_indexer.run(conf.indexer_workers)), 56 61 tokio::spawn(backfiller.run(conf.backfill_workers)), 57 62 tokio::spawn(label_mgr.run(conf.initial_label_services)), 63 + tokio::spawn(index_transport(index_client, idxc_rx)), 58 64 }?; 59 65 60 66 firehose_res 61 67 .and(indexer_res) 62 68 .and(backfill_res) 63 69 .and(label_res) 70 + .and(idxt_res) 64 71 } 65 72 66 73 async fn relay_consumer( ··· 83 90 } 84 91 } 85 92 } 93 + 94 + Ok(()) 95 + } 96 + 97 + async fn index_transport(mut idxc: parakeet_index::Client, rx: tokio::sync::mpsc::Receiver<parakeet_index::AggregateDeltaReq>) -> eyre::Result<()> { 98 + use tokio_stream::wrappers::ReceiverStream; 99 + 100 + idxc.submit_aggregate_delta_stream(ReceiverStream::new(rx)).await?; 86 101 87 102 Ok(()) 88 103 }
+27
parakeet-index/Cargo.toml
··· 1 + [package] 2 + name = "parakeet-index" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [[bin]] 7 + name = "parakeet-index" 8 + required-features = ["server"] 9 + 10 + [dependencies] 11 + tonic = "0.13.0" 12 + prost = "0.13.5" 13 + 14 + eyre = { version = "0.6.12", optional = true } 15 + figment = { version = "0.10.19", features = ["env", "toml"], optional = true } 16 + itertools = { version = "0.14.0", optional = true } 17 + serde = { version = "1.0.217", features = ["derive"], optional = true } 18 + sled = { version = "0.34.7", optional = true } 19 + tokio = { version = "1.42.0", features = ["full"], optional = true } 20 + tracing = { version = "0.1.40", optional = true } 21 + tracing-subscriber = { version = "0.3.18", optional = true } 22 + 23 + [build-dependencies] 24 + tonic-build = "0.13.0" 25 + 26 + [features] 27 + server = ["dep:eyre", "dep:figment", "dep:itertools", "dep:serde", "dep:sled", "dep:tokio", "dep:tracing", "dep:tracing-subscriber"]
+5
parakeet-index/build.rs
··· 1 + fn main() -> Result<(), Box<dyn std::error::Error>> { 2 + tonic_build::configure().compile_protos(&["proto/parakeet.proto"], &[""])?; 3 + 4 + Ok(()) 5 + }
+96
parakeet-index/proto/parakeet.proto
··· 1 + syntax = "proto3"; 2 + package parakeet; 3 + 4 + service Index { 5 + rpc SubmitAggregateDelta(AggregateDeltaReq) returns (AggregateDeltaRes); 6 + rpc SubmitAggregateDeltaBatch(AggregateDeltaBatchReq) returns (AggregateDeltaRes); 7 + rpc SubmitAggregateDeltaStream(stream AggregateDeltaReq) returns (AggregateDeltaRes); 8 + 9 + rpc GetProfileStats(GetStatsReq) returns (GetProfileStatsRes); 10 + rpc GetProfileStatsMany(GetStatsManyReq) returns (GetProfileStatsManyRes); 11 + rpc GetPostStats(GetStatsReq) returns (GetPostStatsRes); 12 + rpc GetPostStatsMany(GetStatsManyReq) returns (GetPostStatsManyRes); 13 + rpc GetLikeCount(GetStatsReq) returns (GetLikeCountRes); 14 + rpc GetLikeCountMany(GetStatsManyReq) returns (GetLikeCountManyRes); 15 + } 16 + 17 + enum AggregateType { 18 + UNKNOWN = 0; 19 + FOLLOW = 1; 20 + FOLLOWER = 2; 21 + LIKE = 3; 22 + REPLY = 4; 23 + REPOST = 5; 24 + // aka Quotes (in the context of posts) 25 + EMBED = 6; 26 + PROFILE_POST = 7; 27 + PROFILE_LIST = 8; 28 + PROFILE_FEED = 9; 29 + PROFILE_STARTERPACK = 10; 30 + } 31 + 32 + message AggregateDeltaReq { 33 + // The type of aggregate to change 34 + AggregateType typ = 1; 35 + // The entry to change. Can be a full at:// uri for items or a did for actors/profiles 36 + string uri = 2; 37 + sint32 delta = 3; 38 + } 39 + 40 + message AggregateDeltaBatchReq { 41 + repeated AggregateDeltaReq deltas = 1; 42 + } 43 + 44 + message AggregateDeltaRes {} 45 + 46 + message GetStatsReq { 47 + string uri = 1; 48 + } 49 + 50 + message GetStatsManyReq { 51 + repeated string uris = 1; 52 + } 53 + 54 + message ProfileStats { 55 + int32 followers = 1; 56 + int32 following = 2; 57 + int32 posts = 3; 58 + int32 lists = 4; 59 + int32 feeds = 5; 60 + int32 starterpacks = 6; 61 + } 62 + 63 + message GetProfileStatsRes { 64 + optional ProfileStats stats = 1; 65 + } 66 + 67 + message GetProfileStatsManyRes { 68 + map<string, ProfileStats> entries = 1; 69 + } 70 + 71 + message PostStats { 72 + int32 replies = 1; 73 + int32 likes = 2; 74 + int32 reposts = 3; 75 + int32 quotes = 4; 76 + } 77 + 78 + message GetPostStatsRes { 79 + optional PostStats stats = 1; 80 + } 81 + 82 + message GetPostStatsManyRes { 83 + map<string, PostStats> entries = 1; 84 + } 85 + 86 + message LikeCount { 87 + int32 likes = 1; 88 + } 89 + 90 + message GetLikeCountRes { 91 + optional LikeCount likes = 1; 92 + } 93 + 94 + message GetLikeCountManyRes { 95 + map<string, LikeCount> entries = 1; 96 + }
+1
parakeet-index/run.sh
··· 1 + cargo run --features server
+10
parakeet-index/src/lib.rs
··· 1 + #[allow(clippy::all)] 2 + pub mod index { 3 + tonic::include_proto!("parakeet"); 4 + } 5 + 6 + pub use index::*; 7 + pub type Client = index_client::IndexClient<tonic::transport::Channel>; 8 + 9 + #[cfg(feature = "server")] 10 + pub mod server;
+24
parakeet-index/src/main.rs
··· 1 + use parakeet_index::index_server::IndexServer; 2 + use parakeet_index::server::service::Service; 3 + use parakeet_index::server::{GlobalState, config}; 4 + use std::sync::Arc; 5 + use tonic::transport::Server; 6 + 7 + #[tokio::main] 8 + async fn main() -> eyre::Result<()> { 9 + tracing_subscriber::fmt::init(); 10 + 11 + let conf = config::load_config()?; 12 + 13 + let db_root = conf.index_db_path.parse()?; 14 + let addr = std::net::SocketAddr::new(conf.server.bind_address.parse()?, conf.server.port); 15 + let state = Arc::new(GlobalState::new(db_root)?); 16 + 17 + let service = Service::new(state.clone()); 18 + Server::builder() 19 + .add_service(IndexServer::new(service)) 20 + .serve(addr) 21 + .await?; 22 + 23 + Ok(()) 24 + }
+45
parakeet-index/src/server/config.rs
··· 1 + use figment::Figment; 2 + use figment::providers::{Env, Format, Toml}; 3 + use serde::Deserialize; 4 + 5 + pub fn load_config() -> eyre::Result<Config> { 6 + let conf = Figment::new() 7 + .merge(Toml::file("Config.toml")) 8 + .merge(Env::prefixed("PKI_")) 9 + .extract()?; 10 + 11 + Ok(conf) 12 + } 13 + 14 + #[derive(Debug, Deserialize)] 15 + pub struct Config { 16 + pub database_url: String, 17 + pub index_db_path: String, 18 + #[serde(default)] 19 + pub server: ConfigServer, 20 + } 21 + 22 + #[derive(Debug, Deserialize)] 23 + pub struct ConfigServer { 24 + #[serde(default = "default_bind_address")] 25 + pub bind_address: String, 26 + #[serde(default = "default_port")] 27 + pub port: u16, 28 + } 29 + 30 + impl Default for ConfigServer { 31 + fn default() -> Self { 32 + ConfigServer { 33 + bind_address: default_bind_address(), 34 + port: default_port(), 35 + } 36 + } 37 + } 38 + 39 + fn default_bind_address() -> String { 40 + "0.0.0.0".to_string() 41 + } 42 + 43 + fn default_port() -> u16 { 44 + 6001 45 + }
+103
parakeet-index/src/server/db.rs
··· 1 + use crate::all_none; 2 + use crate::server::utils::{ToIntExt, TreeExt, slice_as_i32}; 3 + use sled::{Db, MergeOperator, Tree}; 4 + use std::path::PathBuf; 5 + 6 + pub struct DbStore { 7 + pub agg_db: Db, 8 + pub label_db: Db, 9 + 10 + pub follows: Tree, 11 + pub followers: Tree, 12 + pub likes: Tree, 13 + pub replies: Tree, 14 + pub reposts: Tree, 15 + pub embeds: Tree, 16 + pub profile_posts: Tree, 17 + pub profile_lists: Tree, 18 + pub profile_feeds: Tree, 19 + pub profile_starterpacks: Tree, 20 + } 21 + 22 + impl DbStore { 23 + pub fn new(db_root: PathBuf) -> eyre::Result<Self> { 24 + let agg_db = sled::open(db_root.join("aggdb"))?; 25 + let label_db = sled::open(db_root.join("labeldb"))?; 26 + 27 + Ok(DbStore { 28 + follows: open_tree(&agg_db, "follows", merge_delta)?, 29 + followers: open_tree(&agg_db, "followers", merge_delta)?, 30 + likes: open_tree(&agg_db, "likes", merge_delta)?, 31 + replies: open_tree(&agg_db, "replies", merge_delta)?, 32 + reposts: open_tree(&agg_db, "reposts", merge_delta)?, 33 + embeds: open_tree(&agg_db, "embeds", merge_delta)?, 34 + profile_posts: open_tree(&agg_db, "profile_posts", merge_delta)?, 35 + profile_lists: open_tree(&agg_db, "profile_lists", merge_delta)?, 36 + profile_feeds: open_tree(&agg_db, "profile_feeds", merge_delta)?, 37 + profile_starterpacks: open_tree(&agg_db, "profile_starterpacks", merge_delta)?, 38 + 39 + agg_db, 40 + label_db, 41 + }) 42 + } 43 + 44 + pub fn get_post_stats(&self, post: &str) -> Option<crate::PostStats> { 45 + let replies = self.replies.get_i32(post); 46 + let likes = self.likes.get_i32(post); 47 + let reposts = self.reposts.get_i32(post); 48 + let quotes = self.embeds.get_i32(post); 49 + 50 + if all_none![replies, likes, reposts, quotes] { 51 + return None; 52 + } 53 + 54 + Some(crate::PostStats { 55 + replies: replies.unwrap_or_default(), 56 + likes: likes.unwrap_or_default(), 57 + reposts: reposts.unwrap_or_default(), 58 + quotes: quotes.unwrap_or_default(), 59 + }) 60 + } 61 + 62 + pub fn get_profile_stats(&self, did: &str) -> Option<crate::ProfileStats> { 63 + let followers = self.followers.get_i32(did); 64 + let following = self.follows.get_i32(did); 65 + let posts = self.profile_posts.get_i32(did); 66 + let lists = self.profile_lists.get_i32(did); 67 + let feeds = self.profile_feeds.get_i32(did); 68 + let starterpacks = self.profile_starterpacks.get_i32(did); 69 + 70 + if all_none![followers, following, posts, lists, feeds, starterpacks] { 71 + return None; 72 + } 73 + 74 + Some(crate::ProfileStats { 75 + followers: followers.unwrap_or_default(), 76 + following: following.unwrap_or_default(), 77 + posts: posts.unwrap_or_default(), 78 + lists: lists.unwrap_or_default(), 79 + feeds: feeds.unwrap_or_default(), 80 + starterpacks: starterpacks.unwrap_or_default(), 81 + }) 82 + } 83 + } 84 + 85 + fn open_tree(db: &Db, name: &str, merge: impl MergeOperator + 'static) -> eyre::Result<Tree> { 86 + let tree = db.open_tree(name)?; 87 + 88 + tree.set_merge_operator(merge); 89 + 90 + Ok(tree) 91 + } 92 + 93 + fn merge_delta(_key: &[u8], old: Option<&[u8]>, new: &[u8]) -> Option<Vec<u8>> { 94 + let old = old.and_then(slice_as_i32); 95 + let new = slice_as_i32(new)?; 96 + 97 + let res = match old { 98 + Some(old) => old + new, 99 + None => new, 100 + }; 101 + 102 + Some(Vec::from_i32(res)) 103 + }
+18
parakeet-index/src/server/mod.rs
··· 1 + use std::path::PathBuf; 2 + 3 + pub mod config; 4 + pub mod db; 5 + pub mod service; 6 + mod utils; 7 + 8 + pub struct GlobalState { 9 + pub dbs: db::DbStore, 10 + } 11 + 12 + impl GlobalState { 13 + pub fn new(db_root: PathBuf) -> eyre::Result<Self> { 14 + let dbs = db::DbStore::new(db_root)?; 15 + 16 + Ok(GlobalState { dbs }) 17 + } 18 + }
+201
parakeet-index/src/server/service.rs
··· 1 + use crate::index::*; 2 + use crate::server::GlobalState; 3 + use crate::server::utils::TreeExt; 4 + use std::collections::HashMap; 5 + use std::ops::Deref; 6 + use std::sync::Arc; 7 + use tonic::codegen::tokio_stream::StreamExt; 8 + use tonic::{Request, Response, Status, Streaming, async_trait}; 9 + 10 + pub struct Service(Arc<GlobalState>); 11 + 12 + impl Service { 13 + pub fn new(state: Arc<GlobalState>) -> Self { 14 + Service(state) 15 + } 16 + 17 + fn apply_delta( 18 + &self, 19 + uri: &str, 20 + typ: AggregateType, 21 + delta: i32, 22 + ) -> sled::Result<Option<sled::IVec>> { 23 + let val = delta.to_le_bytes(); 24 + 25 + match typ { 26 + AggregateType::Unknown => todo!(), 27 + AggregateType::Follow => self.dbs.follows.merge(uri, val), 28 + AggregateType::Follower => self.dbs.followers.merge(uri, val), 29 + AggregateType::Like => self.dbs.likes.merge(uri, val), 30 + AggregateType::Reply => self.dbs.replies.merge(uri, val), 31 + AggregateType::Repost => self.dbs.reposts.merge(uri, val), 32 + AggregateType::Embed => self.dbs.embeds.merge(uri, val), 33 + AggregateType::ProfilePost => self.dbs.profile_posts.merge(uri, val), 34 + AggregateType::ProfileList => self.dbs.profile_lists.merge(uri, val), 35 + AggregateType::ProfileFeed => self.dbs.profile_feeds.merge(uri, val), 36 + AggregateType::ProfileStarterpack => self.dbs.profile_starterpacks.merge(uri, val), 37 + } 38 + } 39 + } 40 + 41 + impl Deref for Service { 42 + type Target = Arc<GlobalState>; 43 + 44 + fn deref(&self) -> &Self::Target { 45 + &self.0 46 + } 47 + } 48 + 49 + #[async_trait] 50 + impl index_server::Index for Service { 51 + async fn submit_aggregate_delta( 52 + &self, 53 + request: Request<AggregateDeltaReq>, 54 + ) -> Result<Response<AggregateDeltaRes>, Status> { 55 + let inner = request.into_inner(); 56 + 57 + let res = self.apply_delta(&inner.uri, inner.typ(), inner.delta); 58 + 59 + if let Err(e) = res { 60 + tracing::error!("failed to update stats DB: {e}"); 61 + return Err(Status::unknown("failed to update stats DB")); 62 + } 63 + 64 + Ok(Response::new(AggregateDeltaRes {})) 65 + } 66 + 67 + async fn submit_aggregate_delta_batch( 68 + &self, 69 + request: Request<AggregateDeltaBatchReq>, 70 + ) -> Result<Response<AggregateDeltaRes>, Status> { 71 + let inner = request.into_inner(); 72 + 73 + for data in inner.deltas { 74 + let res = self.apply_delta(&data.uri, data.typ(), data.delta); 75 + 76 + if let Err(e) = res { 77 + tracing::error!("failed to update stats DB: {e}"); 78 + return Err(Status::unknown("failed to update stats DB")); 79 + } 80 + } 81 + 82 + Ok(Response::new(AggregateDeltaRes {})) 83 + } 84 + 85 + async fn submit_aggregate_delta_stream( 86 + &self, 87 + request: Request<Streaming<AggregateDeltaReq>>, 88 + ) -> Result<Response<AggregateDeltaRes>, Status> { 89 + let mut inner = request.into_inner(); 90 + 91 + while let Some(req) = inner.next().await { 92 + if let Ok(data) = req { 93 + let res = self.apply_delta(&data.uri, data.typ(), data.delta); 94 + 95 + if let Err(e) = res { 96 + tracing::error!("failed to update stats DB: {e}"); 97 + return Err(Status::unknown("failed to update stats DB")); 98 + } 99 + } else { 100 + tracing::error!("failed to read stream item") 101 + } 102 + } 103 + 104 + Ok(Response::new(AggregateDeltaRes {})) 105 + } 106 + 107 + async fn get_profile_stats( 108 + &self, 109 + request: Request<GetStatsReq>, 110 + ) -> Result<Response<GetProfileStatsRes>, Status> { 111 + let inner = request.into_inner(); 112 + 113 + let stats = self.dbs.get_profile_stats(&inner.uri); 114 + 115 + Ok(Response::new(GetProfileStatsRes { stats })) 116 + } 117 + 118 + async fn get_profile_stats_many( 119 + &self, 120 + request: Request<GetStatsManyReq>, 121 + ) -> Result<Response<GetProfileStatsManyRes>, Status> { 122 + let inner = request.into_inner(); 123 + 124 + // idk if this is the best way of doing this???? 125 + let entries = inner 126 + .uris 127 + .into_iter() 128 + .filter_map(|uri| { 129 + let stats = self.dbs.get_profile_stats(&uri)?; 130 + 131 + Some((uri, stats)) 132 + }) 133 + .collect::<HashMap<_, _>>(); 134 + 135 + Ok(Response::new(GetProfileStatsManyRes { entries })) 136 + } 137 + 138 + async fn get_post_stats( 139 + &self, 140 + request: Request<GetStatsReq>, 141 + ) -> Result<Response<GetPostStatsRes>, Status> { 142 + let inner = request.into_inner(); 143 + 144 + let stats = self.dbs.get_post_stats(&inner.uri); 145 + 146 + Ok(Response::new(GetPostStatsRes { stats })) 147 + } 148 + 149 + async fn get_post_stats_many( 150 + &self, 151 + request: Request<GetStatsManyReq>, 152 + ) -> Result<Response<GetPostStatsManyRes>, Status> { 153 + let inner = request.into_inner(); 154 + 155 + let entries = inner 156 + .uris 157 + .into_iter() 158 + .filter_map(|uri| { 159 + let stats = self.dbs.get_post_stats(&uri)?; 160 + 161 + Some((uri, stats)) 162 + }) 163 + .collect::<HashMap<_, _>>(); 164 + 165 + Ok(Response::new(GetPostStatsManyRes { entries })) 166 + } 167 + 168 + async fn get_like_count( 169 + &self, 170 + request: Request<GetStatsReq>, 171 + ) -> Result<Response<GetLikeCountRes>, Status> { 172 + let inner = request.into_inner(); 173 + 174 + let likes = self 175 + .dbs 176 + .likes 177 + .get_i32(inner.uri) 178 + .map(|likes| LikeCount { likes }); 179 + 180 + Ok(Response::new(GetLikeCountRes { likes })) 181 + } 182 + 183 + async fn get_like_count_many( 184 + &self, 185 + request: Request<GetStatsManyReq>, 186 + ) -> Result<Response<GetLikeCountManyRes>, Status> { 187 + let inner = request.into_inner(); 188 + 189 + let entries = inner 190 + .uris 191 + .into_iter() 192 + .filter_map(|uri| { 193 + let likes = self.dbs.likes.get_i32(&uri)?; 194 + 195 + Some((uri, LikeCount { likes })) 196 + }) 197 + .collect(); 198 + 199 + Ok(Response::new(GetLikeCountManyRes { entries })) 200 + } 201 + }
+59
parakeet-index/src/server/utils.rs
··· 1 + use sled::{IVec, Tree}; 2 + 3 + pub trait ToIntExt { 4 + fn as_i32(&self) -> Option<i32>; 5 + fn from_i32(i: i32) -> Self; 6 + } 7 + 8 + impl ToIntExt for IVec { 9 + fn as_i32(&self) -> Option<i32> { 10 + if self.len() == 4 { 11 + let bytes = self[0..4].try_into().ok()?; 12 + Some(i32::from_le_bytes(bytes)) 13 + } else { 14 + None 15 + } 16 + } 17 + 18 + fn from_i32(i: i32) -> Self { 19 + IVec::from(&i.to_le_bytes()) 20 + } 21 + } 22 + 23 + impl ToIntExt for Vec<u8> { 24 + fn as_i32(&self) -> Option<i32> { 25 + if self.len() == 4 { 26 + let bytes = self[0..4].try_into().ok()?; 27 + Some(i32::from_le_bytes(bytes)) 28 + } else { 29 + None 30 + } 31 + } 32 + 33 + fn from_i32(i: i32) -> Self { 34 + Vec::from(&i.to_le_bytes()) 35 + } 36 + } 37 + 38 + pub fn slice_as_i32(data: &[u8]) -> Option<i32> { 39 + let bytes = data[0..4].try_into().ok()?; 40 + 41 + Some(i32::from_le_bytes(bytes)) 42 + } 43 + 44 + pub trait TreeExt { 45 + fn get_i32(&self, key: impl AsRef<[u8]>) -> Option<i32>; 46 + } 47 + 48 + impl TreeExt for Tree { 49 + fn get_i32(&self, key: impl AsRef<[u8]>) -> Option<i32> { 50 + self.get(key).ok().flatten().and_then(|v| v.as_i32()) 51 + } 52 + } 53 + 54 + #[macro_export] 55 + macro_rules! all_none { 56 + ($var0:ident, $($var:ident),*) => { 57 + $var0.is_none() $(&& $var.is_none())* 58 + }; 59 + }
+1
parakeet/Cargo.toml
··· 17 17 itertools = "0.14.0" 18 18 lexica = { path = "../lexica" } 19 19 parakeet-db = { path = "../parakeet-db" } 20 + parakeet-index = { path = "../parakeet-index" } 20 21 serde = { version = "1.0.217", features = ["derive"] } 21 22 serde_json = "1.0.134" 22 23 tokio = { version = "1.42.0", features = ["full"] }
+1
parakeet/run.sh
··· 1 + cargo run
+1
parakeet/src/config.rs
··· 13 13 14 14 #[derive(Debug, Deserialize)] 15 15 pub struct Config { 16 + pub index_uri: String, 16 17 pub database_url: String, 17 18 #[serde(default)] 18 19 pub server: ConfigServer,
+10 -6
parakeet/src/hydration/feedgen.rs
··· 11 11 feedgen: models::FeedGen, 12 12 creator: ProfileView, 13 13 labels: Vec<models::Label>, 14 + likes: Option<i32>, 14 15 ) -> GeneratorView { 15 16 let content_mode = feedgen 16 17 .content_mode ··· 31 32 avatar: feedgen 32 33 .avatar_cid 33 34 .map(|v| format!("https://localhost/feedgen/{v}")), 34 - like_count: 0, 35 + like_count: likes.unwrap_or_default() as i64, 35 36 accepts_interactions: feedgen.accepts_interactions, 36 37 labels: map_labels(labels), 37 38 content_mode, ··· 45 46 apply_labelers: &[LabelConfigItem], 46 47 ) -> Option<GeneratorView> { 47 48 let labels = loaders.label.load(&feedgen, apply_labelers).await; 48 - let feedgen = loaders.feedgen.load(feedgen).await?; 49 + let (feedgen, likes) = loaders.feedgen.load(feedgen).await?; 49 50 let profile = hydrate_profile(loaders, feedgen.owner.clone(), apply_labelers).await?; 50 51 51 - Some(build_feedgen(feedgen, profile, labels)) 52 + Some(build_feedgen(feedgen, profile, labels, likes)) 52 53 } 53 54 54 55 pub async fn hydrate_feedgens( ··· 61 62 62 63 let creators = feedgens 63 64 .values() 64 - .map(|feedgen| feedgen.owner.clone()) 65 + .map(|(feedgen, _)| feedgen.owner.clone()) 65 66 .collect(); 66 67 let creators = hydrate_profiles(loaders, creators, apply_labelers).await; 67 68 68 69 feedgens 69 70 .into_iter() 70 - .filter_map(|(uri, feedgen)| { 71 + .filter_map(|(uri, (feedgen, likes))| { 71 72 let creator = creators.get(&feedgen.owner)?; 72 73 let labels = labels.get(&uri).cloned().unwrap_or_default(); 73 74 74 - Some((uri, build_feedgen(feedgen, creator.to_owned(), labels))) 75 + Some(( 76 + uri, 77 + build_feedgen(feedgen, creator.to_owned(), labels, likes), 78 + )) 75 79 }) 76 80 .collect() 77 81 }
+17 -12
parakeet/src/hydration/labeler.rs
··· 12 12 labeler: models::LabelerService, 13 13 creator: ProfileView, 14 14 labels: Vec<models::Label>, 15 + likes: Option<i32>, 15 16 ) -> LabelerView { 16 17 LabelerView { 17 18 uri: format!("at://{}/app.bsky.labeler.service/self", labeler.did), 18 19 cid: labeler.cid, 19 20 creator, 20 - like_count: 0, 21 + like_count: likes.unwrap_or_default() as i64, 21 22 labels: map_labels(labels), 22 23 indexed_at: labeler.indexed_at, 23 24 } ··· 28 29 defs: Vec<models::LabelDefinition>, 29 30 creator: ProfileView, 30 31 labels: Vec<models::Label>, 32 + likes: Option<i32>, 31 33 ) -> LabelerViewDetailed { 32 34 let reason_types = labeler.reasons.map(|v| { 33 35 v.into_iter() ··· 66 68 uri: format!("at://{}/app.bsky.labeler.service/self", labeler.did), 67 69 cid: labeler.cid, 68 70 creator, 69 - like_count: 0, 71 + like_count: likes.unwrap_or_default() as i64, 70 72 policies: LabelerPolicy { 71 73 label_values, 72 74 label_value_definitions, ··· 85 87 apply_labelers: &[LabelConfigItem], 86 88 ) -> Option<LabelerView> { 87 89 let labels = loaders.label.load(&labeler, apply_labelers).await; 88 - let (labeler, _) = loaders.labeler.load(labeler).await?; 90 + let (labeler, _, likes) = loaders.labeler.load(labeler).await?; 89 91 let creator = hydrate_profile(loaders, labeler.did.clone(), apply_labelers).await?; 90 92 91 - Some(build_view(labeler, creator, labels)) 93 + Some(build_view(labeler, creator, labels, likes)) 92 94 } 93 95 94 96 pub async fn hydrate_labelers( ··· 101 103 102 104 let creators = labelers 103 105 .values() 104 - .map(|(labeler, _)| labeler.did.clone()) 106 + .map(|(labeler, _, _)| labeler.did.clone()) 105 107 .collect(); 106 108 let creators = hydrate_profiles(loaders, creators, apply_labelers).await; 107 109 108 110 labelers 109 111 .into_iter() 110 - .filter_map(|(k, (labeler, _))| { 112 + .filter_map(|(k, (labeler, _, likes))| { 111 113 let creator = creators.get(&labeler.did).cloned()?; 112 114 let labels = labels.get(&k).cloned().unwrap_or_default(); 113 115 114 - Some((k, build_view(labeler, creator, labels))) 116 + Some((k, build_view(labeler, creator, labels, likes))) 115 117 }) 116 118 .collect() 117 119 } ··· 122 124 apply_labelers: &[LabelConfigItem], 123 125 ) -> Option<LabelerViewDetailed> { 124 126 let labels = loaders.label.load(&labeler, apply_labelers).await; 125 - let (labeler, defs) = loaders.labeler.load(labeler).await?; 127 + let (labeler, defs, likes) = loaders.labeler.load(labeler).await?; 126 128 let creator = hydrate_profile(loaders, labeler.did.clone(), apply_labelers).await?; 127 129 128 - Some(build_view_detailed(labeler, defs, creator, labels)) 130 + Some(build_view_detailed(labeler, defs, creator, labels, likes)) 129 131 } 130 132 131 133 pub async fn hydrate_labelers_detailed( ··· 138 140 139 141 let creators = labelers 140 142 .values() 141 - .map(|(labeler, _)| labeler.did.clone()) 143 + .map(|(labeler, _, _)| labeler.did.clone()) 142 144 .collect(); 143 145 let creators = hydrate_profiles(loaders, creators, apply_labelers).await; 144 146 145 147 labelers 146 148 .into_iter() 147 - .filter_map(|(k, (labeler, defs))| { 149 + .filter_map(|(k, (labeler, defs, likes))| { 148 150 let creator = creators.get(&labeler.did).cloned()?; 149 151 let labels = labels.get(&k).cloned().unwrap_or_default(); 150 152 151 - Some((k, build_view_detailed(labeler, defs, creator, labels))) 153 + Some(( 154 + k, 155 + build_view_detailed(labeler, defs, creator, labels, likes), 156 + )) 152 157 }) 153 158 .collect() 154 159 }
+25 -11
parakeet/src/hydration/posts.rs
··· 7 7 use lexica::app_bsky::embed::{AspectRatio, Embed}; 8 8 use lexica::app_bsky::feed::{FeedViewPost, PostView, ReplyRef, ReplyRefPost, ThreadgateView}; 9 9 use lexica::app_bsky::graph::ListViewBasic; 10 + use lexica::app_bsky::RecordStats; 10 11 use parakeet_db::models; 12 + use parakeet_index::PostStats; 11 13 use std::collections::HashMap; 12 14 13 15 fn build_postview( ··· 16 18 labels: Vec<models::Label>, 17 19 embed: Option<Embed>, 18 20 threadgate: Option<ThreadgateView>, 21 + stats: Option<PostStats>, 19 22 ) -> PostView { 23 + let stats = stats 24 + .map(|stats| RecordStats { 25 + reply_count: stats.replies as i64, 26 + repost_count: stats.reposts as i64, 27 + like_count: stats.likes as i64, 28 + quote_count: stats.quotes as i64, 29 + }) 30 + .unwrap_or_default(); 31 + 20 32 PostView { 21 33 uri: post.at_uri, 22 34 cid: post.cid, 23 35 author, 24 36 record: post.record, 25 37 embed, 26 - stats: Default::default(), 38 + stats, 27 39 labels: map_labels(labels), 28 40 threadgate, 29 41 indexed_at: post.created_at, ··· 96 108 post: String, 97 109 apply_labelers: &[LabelConfigItem], 98 110 ) -> Option<PostView> { 99 - let (post, threadgate) = loaders.posts.load(post).await?; 111 + let (post, threadgate, stats) = loaders.posts.load(post).await?; 100 112 let embed = hydrate_embed(loaders, post.at_uri.clone(), apply_labelers).await; 101 113 let author = hydrate_profile_basic(loaders, post.did.clone(), apply_labelers).await?; 102 114 let threadgate = hydrate_threadgate(loaders, threadgate, apply_labelers).await; 103 115 let labels = loaders.label.load(&post.at_uri, apply_labelers).await; 104 116 105 - Some(build_postview(post, author, labels, embed, threadgate)) 117 + Some(build_postview( 118 + post, author, labels, embed, threadgate, stats, 119 + )) 106 120 } 107 121 108 122 pub async fn hydrate_posts( ··· 114 128 115 129 let (authors, post_uris) = posts 116 130 .values() 117 - .map(|(post, _)| (post.did.clone(), post.at_uri.clone())) 131 + .map(|(post, _, _)| (post.did.clone(), post.at_uri.clone())) 118 132 .unzip::<_, _, Vec<_>, Vec<_>>(); 119 133 let authors = hydrate_profiles_basic(loaders, authors, apply_labelers).await; 120 134 ··· 122 136 123 137 let threadgates = posts 124 138 .values() 125 - .filter_map(|(_, threadgate)| threadgate.clone()) 139 + .filter_map(|(_, threadgate, _)| threadgate.clone()) 126 140 .collect(); 127 141 let threadgates = hydrate_threadgates(loaders, threadgates, apply_labelers).await; 128 142 ··· 130 144 131 145 posts 132 146 .into_iter() 133 - .filter_map(|(uri, (post, threadgate))| { 147 + .filter_map(|(uri, (post, threadgate, stats))| { 134 148 let author = authors.get(&post.did)?; 135 149 let embed = embeds.get(&uri).cloned(); 136 150 let threadgate = threadgate.and_then(|tg| threadgates.get(&tg.at_uri).cloned()); ··· 138 152 139 153 Some(( 140 154 uri, 141 - build_postview(post, author.to_owned(), labels, embed, threadgate), 155 + build_postview(post, author.to_owned(), labels, embed, threadgate, stats), 142 156 )) 143 157 }) 144 158 .collect() ··· 153 167 154 168 let (authors, post_uris) = posts 155 169 .values() 156 - .map(|(post, _)| (post.did.clone(), post.at_uri.clone())) 170 + .map(|(post, _, _)| (post.did.clone(), post.at_uri.clone())) 157 171 .unzip::<_, _, Vec<_>, Vec<_>>(); 158 172 let authors = hydrate_profiles_basic(loaders, authors, apply_labelers).await; 159 173 ··· 163 177 164 178 let reply_refs = posts 165 179 .values() 166 - .flat_map(|(post, _)| [post.parent_uri.clone(), post.root_uri.clone()]) 180 + .flat_map(|(post, _, _)| [post.parent_uri.clone(), post.root_uri.clone()]) 167 181 .flatten() 168 182 .collect::<Vec<_>>(); 169 183 ··· 171 185 172 186 posts 173 187 .into_iter() 174 - .filter_map(|(post_uri, (post, threadgate))| { 188 + .filter_map(|(post_uri, (post, threadgate, stats))| { 175 189 let author = authors.get(&post.did)?; 176 190 177 191 let root = post.root_uri.as_ref().and_then(|uri| reply_posts.get(uri)); ··· 203 217 204 218 let embed = embeds.get(&post_uri).cloned(); 205 219 let labels = post_labels.get(&post_uri).cloned().unwrap_or_default(); 206 - let post = build_postview(post, author.to_owned(), labels, embed, None); 220 + let post = build_postview(post, author.to_owned(), labels, embed, None, stats); 207 221 208 222 Some(( 209 223 post_uri,
+32 -43
parakeet/src/hydration/profile.rs
··· 2 2 use crate::loaders::Dataloaders; 3 3 use lexica::app_bsky::actor::*; 4 4 use parakeet_db::models; 5 + use parakeet_index::ProfileStats; 5 6 use std::collections::HashMap; 6 7 use std::sync::OnceLock; 7 8 8 9 pub static TRUSTED_VERIFIERS: OnceLock<Vec<String>> = OnceLock::new(); 9 10 10 - fn build_associated(chat: Option<ChatAllowIncoming>, labeler: bool) -> Option<ProfileAssociated> { 11 - if chat.is_some() || labeler { 11 + fn build_associated( 12 + chat: Option<ChatAllowIncoming>, 13 + labeler: bool, 14 + stats: Option<ProfileStats>, 15 + ) -> Option<ProfileAssociated> { 16 + if chat.is_some() || labeler || stats.is_some() { 17 + let stats = stats.unwrap_or_default(); 18 + 12 19 Some(ProfileAssociated { 13 - lists: 0, 14 - feedgens: 0, 15 - starter_packs: 0, 20 + lists: stats.lists as i64, 21 + feedgens: stats.feeds as i64, 22 + starter_packs: stats.starterpacks as i64, 16 23 labeler, 17 24 chat: chat.map(|v| ProfileAssociatedChat { allow_incoming: v }), 18 25 }) ··· 110 117 is_labeler: bool, 111 118 labels: Vec<models::Label>, 112 119 verifications: Option<Vec<models::VerificationEntry>>, 120 + stats: Option<ProfileStats>, 113 121 ) -> ProfileViewBasic { 114 - let associated = build_associated(chat_decl, is_labeler); 122 + let associated = build_associated(chat_decl, is_labeler, stats); 115 123 let verification = build_verification(&profile, &handle, verifications); 116 124 117 125 ProfileViewBasic { ··· 135 143 is_labeler: bool, 136 144 labels: Vec<models::Label>, 137 145 verifications: Option<Vec<models::VerificationEntry>>, 146 + stats: Option<ProfileStats>, 138 147 ) -> ProfileView { 139 - let associated = build_associated(chat_decl, is_labeler); 148 + let associated = build_associated(chat_decl, is_labeler, stats); 140 149 let verification = build_verification(&profile, &handle, verifications); 141 150 142 151 ProfileView { ··· 158 167 fn build_detailed( 159 168 handle: Option<String>, 160 169 profile: models::Profile, 161 - follow_stats: Option<models::FollowStats>, 162 170 chat_decl: Option<ChatAllowIncoming>, 163 171 is_labeler: bool, 164 172 labels: Vec<models::Label>, 165 173 verifications: Option<Vec<models::VerificationEntry>>, 174 + stats: Option<ProfileStats>, 166 175 ) -> ProfileViewDetailed { 167 - let associated = build_associated(chat_decl, is_labeler); 176 + let associated = build_associated(chat_decl, is_labeler, stats); 168 177 let verification = build_verification(&profile, &handle, verifications); 169 178 170 179 ProfileViewDetailed { ··· 178 187 banner: profile 179 188 .banner_cid 180 189 .map(|v| format!("https://localhost/banner/{v}")), 181 - followers_count: follow_stats 182 - .as_ref() 183 - .map(|v| v.followers as i64) 184 - .unwrap_or_default(), 185 - follows_count: follow_stats 186 - .as_ref() 187 - .map(|v| v.following as i64) 188 - .unwrap_or_default(), 190 + followers_count: stats.map(|v| v.followers as i64).unwrap_or_default(), 191 + follows_count: stats.map(|v| v.following as i64).unwrap_or_default(), 189 192 associated, 190 193 labels: map_labels(labels), 191 194 verification, ··· 201 204 ) -> Option<ProfileViewBasic> { 202 205 let labels = loaders.label.load(&did, apply_labelers).await; 203 206 let verif = loaders.verification.load(did.clone()).await; 204 - let (handle, profile, _, chat_decl, labeler) = loaders.profile.load(did).await?; 207 + let (handle, profile, chat_decl, labeler, stats) = loaders.profile.load(did).await?; 205 208 206 209 Some(build_basic( 207 - handle, profile, chat_decl, labeler, labels, verif, 210 + handle, profile, chat_decl, labeler, labels, verif, stats, 208 211 )) 209 212 } 210 213 ··· 219 222 220 223 profiles 221 224 .into_iter() 222 - .map(|(k, (handle, profile, _, chat_decl, labeler))| { 225 + .map(|(k, (handle, profile, chat_decl, labeler, stats))| { 223 226 let labels = labels.get(&k).cloned().unwrap_or_default(); 224 227 let verif = verif.get(&k).cloned(); 225 228 226 229 ( 227 230 k, 228 - build_basic(handle, profile, chat_decl, labeler, labels, verif), 231 + build_basic(handle, profile, chat_decl, labeler, labels, verif, stats), 229 232 ) 230 233 }) 231 234 .collect() ··· 238 241 ) -> Option<ProfileView> { 239 242 let labels = loaders.label.load(&did, apply_labelers).await; 240 243 let verif = loaders.verification.load(did.clone()).await; 241 - let (handle, profile, _, chat_decl, labeler) = loaders.profile.load(did).await?; 244 + let (handle, profile, chat_decl, labeler, stats) = loaders.profile.load(did).await?; 242 245 243 246 Some(build_profile( 244 - handle, profile, chat_decl, labeler, labels, verif, 247 + handle, profile, chat_decl, labeler, labels, verif, stats, 245 248 )) 246 249 } 247 250 ··· 256 259 257 260 profiles 258 261 .into_iter() 259 - .map(|(k, (handle, profile, _, chat_decl, labeler))| { 262 + .map(|(k, (handle, profile, chat_decl, labeler, stats))| { 260 263 let labels = labels.get(&k).cloned().unwrap_or_default(); 261 264 let verif = verif.get(&k).cloned(); 262 265 263 266 ( 264 267 k, 265 - build_profile(handle, profile, chat_decl, labeler, labels, verif), 268 + build_profile(handle, profile, chat_decl, labeler, labels, verif, stats), 266 269 ) 267 270 }) 268 271 .collect() ··· 275 278 ) -> Option<ProfileViewDetailed> { 276 279 let labels = loaders.label.load(&did, apply_labelers).await; 277 280 let verif = loaders.verification.load(did.clone()).await; 278 - let (handle, profile, follow_stats, chat_decl, labeler) = loaders.profile.load(did).await?; 281 + let (handle, profile, chat_decl, labeler, stats) = loaders.profile.load(did).await?; 279 282 280 283 Some(build_detailed( 281 - handle, 282 - profile, 283 - follow_stats, 284 - chat_decl, 285 - labeler, 286 - labels, 287 - verif, 284 + handle, profile, chat_decl, labeler, labels, verif, stats, 288 285 )) 289 286 } 290 287 ··· 299 296 300 297 profiles 301 298 .into_iter() 302 - .map(|(k, (handle, profile, follow_stats, chat_decl, labeler))| { 299 + .map(|(k, (handle, profile, chat_decl, labeler, stats))| { 303 300 let labels = labels.get(&k).cloned().unwrap_or_default(); 304 301 let verif = verif.get(&k).cloned(); 305 302 306 303 ( 307 304 k, 308 - build_detailed( 309 - handle, 310 - profile, 311 - follow_stats, 312 - chat_decl, 313 - labeler, 314 - labels, 315 - verif, 316 - ), 305 + build_detailed(handle, profile, chat_decl, labeler, labels, verif, stats), 317 306 ) 318 307 }) 319 308 .collect()
+90 -26
parakeet/src/loaders.rs
··· 26 26 impl Dataloaders { 27 27 // for the moment, we set up memory cached loaders 28 28 // we should build a redis/valkey backend at some point in the future. 29 - pub fn new(pool: Pool<AsyncPgConnection>) -> Dataloaders { 29 + pub fn new(pool: Pool<AsyncPgConnection>, idxc: parakeet_index::Client) -> Dataloaders { 30 30 Dataloaders { 31 31 embed: Loader::new(EmbedLoader(pool.clone())), 32 - feedgen: Loader::new(FeedGenLoader(pool.clone())), 32 + feedgen: Loader::new(FeedGenLoader(pool.clone(), idxc.clone())), 33 33 handle: Loader::new(HandleLoader(pool.clone())), 34 34 label: LabelLoader(pool.clone()), // CARE: never cache this. 35 - labeler: Loader::new(LabelServiceLoader(pool.clone())), 35 + labeler: Loader::new(LabelServiceLoader(pool.clone(), idxc.clone())), 36 36 list: Loader::new(ListLoader(pool.clone())), 37 - posts: Loader::new(PostLoader(pool.clone())), 38 - profile: Loader::new(ProfileLoader(pool.clone())), 37 + posts: Loader::new(PostLoader(pool.clone(), idxc.clone())), 38 + profile: Loader::new(ProfileLoader(pool.clone(), idxc.clone())), 39 39 starterpacks: Loader::new(StarterPackLoader(pool.clone())), 40 40 verification: Loader::new(VerificationLoader(pool.clone())), 41 41 } ··· 66 66 } 67 67 } 68 68 69 - pub struct ProfileLoader(Pool<AsyncPgConnection>); 69 + pub struct ProfileLoader(Pool<AsyncPgConnection>, parakeet_index::Client); 70 70 type ProfileLoaderRet = ( 71 71 Option<String>, 72 72 models::Profile, 73 - Option<models::FollowStats>, 74 73 Option<ChatAllowIncoming>, 75 74 bool, 75 + Option<parakeet_index::ProfileStats>, 76 76 ); 77 77 impl BatchFn<String, ProfileLoaderRet> for ProfileLoader { 78 78 async fn load(&mut self, keys: &[String]) -> HashMap<String, ProfileLoaderRet> { ··· 91 91 schema::actors::did, 92 92 schema::actors::handle, 93 93 models::Profile::as_select(), 94 - Option::<models::FollowStats>::as_select(), 95 94 schema::chat_decls::allow_incoming.nullable(), 96 95 schema::labelers::cid.nullable(), 97 96 )) ··· 100 99 String, 101 100 Option<String>, 102 101 models::Profile, 103 - Option<models::FollowStats>, 104 102 Option<String>, 105 103 Option<String>, 106 104 )>(&mut conn) 107 105 .await; 108 106 107 + let stats_req = parakeet_index::GetStatsManyReq { 108 + uris: keys.to_vec(), 109 + }; 110 + let mut stats = self 111 + .1 112 + .get_profile_stats_many(stats_req) 113 + .await 114 + .unwrap() 115 + .into_inner() 116 + .entries; 117 + 109 118 match res { 110 119 Ok(res) => HashMap::from_iter(res.into_iter().map( 111 - |(did, handle, profile, follow_stats, chat_decl, labeler_cid)| { 120 + |(did, handle, profile, chat_decl, labeler_cid)| { 112 121 let chat_decl = chat_decl.and_then(|v| ChatAllowIncoming::from_str(&v).ok()); 113 122 let is_labeler = labeler_cid.is_some(); 123 + let maybe_stats = stats.remove(&did); 114 124 115 - let val = (handle, profile, follow_stats, chat_decl, is_labeler); 125 + let val = (handle, profile, chat_decl, is_labeler, maybe_stats); 116 126 117 127 (did, val) 118 128 }, ··· 157 167 } 158 168 } 159 169 160 - pub struct FeedGenLoader(Pool<AsyncPgConnection>); 161 - type FeedGenLoaderRet = models::FeedGen; //todo: when we have likes, we'll need the count here 170 + pub struct FeedGenLoader(Pool<AsyncPgConnection>, parakeet_index::Client); 171 + type FeedGenLoaderRet = (models::FeedGen, Option<i32>); 162 172 impl BatchFn<String, FeedGenLoaderRet> for FeedGenLoader { 163 173 async fn load(&mut self, keys: &[String]) -> HashMap<String, FeedGenLoaderRet> { 164 174 let mut conn = self.0.get().await.unwrap(); ··· 169 179 .load(&mut conn) 170 180 .await; 171 181 182 + let stats_req = parakeet_index::GetStatsManyReq { 183 + uris: keys.to_vec(), 184 + }; 185 + let mut stats = self 186 + .1 187 + .get_like_count_many(stats_req) 188 + .await 189 + .unwrap() 190 + .into_inner() 191 + .entries; 192 + 172 193 match res { 173 - Ok(res) => HashMap::from_iter( 174 - res.into_iter() 175 - .map(|feedgen| (feedgen.at_uri.clone(), feedgen)), 176 - ), 194 + Ok(res) => HashMap::from_iter(res.into_iter().map(|feedgen| { 195 + let likes = stats.remove(&feedgen.at_uri).map(|v| v.likes); 196 + 197 + (feedgen.at_uri.clone(), (feedgen, likes)) 198 + })), 177 199 Err(e) => { 178 200 tracing::error!("feedgen load failed: {e}"); 179 201 HashMap::new() ··· 182 204 } 183 205 } 184 206 185 - pub struct PostLoader(Pool<AsyncPgConnection>); 186 - type PostLoaderRet = (models::Post, Option<models::Threadgate>); 207 + pub struct PostLoader(Pool<AsyncPgConnection>, parakeet_index::Client); 208 + type PostLoaderRet = ( 209 + models::Post, 210 + Option<models::Threadgate>, 211 + Option<parakeet_index::PostStats>, 212 + ); 187 213 impl BatchFn<String, PostLoaderRet> for PostLoader { 188 214 async fn load(&mut self, keys: &[String]) -> HashMap<String, PostLoaderRet> { 189 215 let mut conn = self.0.get().await.unwrap(); ··· 198 224 .load(&mut conn) 199 225 .await; 200 226 227 + let stats_req = parakeet_index::GetStatsManyReq { 228 + uris: keys.to_vec(), 229 + }; 230 + let mut stats = self 231 + .1 232 + .get_post_stats_many(stats_req) 233 + .await 234 + .unwrap() 235 + .into_inner() 236 + .entries; 237 + 201 238 match res { 202 - Ok(res) => HashMap::from_iter( 203 - res.into_iter() 204 - .map(|(post, threadgate)| (post.at_uri.clone(), (post, threadgate))), 205 - ), 239 + Ok(res) => HashMap::from_iter(res.into_iter().map(|(post, threadgate)| { 240 + let maybe_stats = stats.remove(&post.at_uri); 241 + 242 + (post.at_uri.clone(), (post, threadgate, maybe_stats)) 243 + })), 206 244 Err(e) => { 207 245 tracing::error!("post load failed: {e}"); 208 246 HashMap::new() ··· 323 361 } 324 362 } 325 363 326 - pub struct LabelServiceLoader(Pool<AsyncPgConnection>); 327 - type LabelServiceLoaderRet = (models::LabelerService, Vec<models::LabelDefinition>); 364 + pub struct LabelServiceLoader(Pool<AsyncPgConnection>, parakeet_index::Client); 365 + type LabelServiceLoaderRet = ( 366 + models::LabelerService, 367 + Vec<models::LabelDefinition>, 368 + Option<i32>, 369 + ); 328 370 impl BatchFn<String, LabelServiceLoaderRet> for LabelServiceLoader { 329 371 async fn load(&mut self, keys: &[String]) -> HashMap<String, LabelServiceLoaderRet> { 330 372 let mut conn = self.0.get().await.unwrap(); ··· 343 385 344 386 let defs = defs.grouped_by(&labelers); 345 387 388 + let uris = keys 389 + .iter() 390 + .map(|v| format!("at://{v}/app.bsky.labeler.service/self")) 391 + .collect(); 392 + let stats_req = parakeet_index::GetStatsManyReq { uris }; 393 + let mut stats = self 394 + .1 395 + .get_like_count_many(stats_req) 396 + .await 397 + .unwrap() 398 + .into_inner() 399 + .entries; 400 + 346 401 labelers 347 402 .into_iter() 348 403 .zip(defs) 349 - .map(|(labeler, defs)| (labeler.did.clone(), (labeler, defs))) 404 + .map(|(labeler, defs)| { 405 + let likes = stats 406 + .remove(&format!( 407 + "at://{}/app.bsky.labeler.service/self", 408 + &labeler.did 409 + )) 410 + .map(|v| v.likes); 411 + 412 + (labeler.did.clone(), (labeler, defs, likes)) 413 + }) 350 414 .collect() 351 415 } 352 416 }
+12 -2
parakeet/src/main.rs
··· 14 14 pub struct GlobalState { 15 15 pub pool: Pool<AsyncPgConnection>, 16 16 pub dataloaders: Arc<loaders::Dataloaders>, 17 + pub index_client: parakeet_index::Client, 17 18 } 18 19 19 20 #[tokio::main] ··· 25 26 let db_mgr = AsyncDieselConnectionManager::<AsyncPgConnection>::new(&conf.database_url); 26 27 let pool = Pool::builder(db_mgr).build()?; 27 28 28 - let dataloaders = Arc::new(loaders::Dataloaders::new(pool.clone())); 29 + let index_client = parakeet_index::Client::connect(conf.index_uri).await?; 30 + 31 + let dataloaders = Arc::new(loaders::Dataloaders::new( 32 + pool.clone(), 33 + index_client.clone(), 34 + )); 29 35 30 36 #[allow(unused)] 31 37 hydration::TRUSTED_VERIFIERS.set(conf.trusted_verifiers); ··· 44 50 ) 45 51 .layer(TraceLayer::new_for_http()) 46 52 .layer(cors) 47 - .with_state(GlobalState { pool, dataloaders }); 53 + .with_state(GlobalState { 54 + pool, 55 + dataloaders, 56 + index_client, 57 + }); 48 58 49 59 let addr = std::net::SocketAddr::new(conf.server.bind_address.parse()?, conf.server.port); 50 60 let listener = tokio::net::TcpListener::bind(addr).await?;