Rust wrapper for the ATProto tap utility

First version working

+4789
+7
.gitignore
··· 1 + tap 2 + cache.json 3 + documents.txt 4 + publications.txt 5 + target 6 + tap-example* 7 + test.db*
+1855
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 15 + name = "anstream" 16 + version = "0.6.21" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 19 + dependencies = [ 20 + "anstyle", 21 + "anstyle-parse", 22 + "anstyle-query", 23 + "anstyle-wincon", 24 + "colorchoice", 25 + "is_terminal_polyfill", 26 + "utf8parse", 27 + ] 28 + 29 + [[package]] 30 + name = "anstyle" 31 + version = "1.0.13" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 34 + 35 + [[package]] 36 + name = "anstyle-parse" 37 + version = "0.2.7" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 40 + dependencies = [ 41 + "utf8parse", 42 + ] 43 + 44 + [[package]] 45 + name = "anstyle-query" 46 + version = "1.1.5" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 49 + dependencies = [ 50 + "windows-sys 0.61.2", 51 + ] 52 + 53 + [[package]] 54 + name = "anstyle-wincon" 55 + version = "3.0.11" 56 + source = "registry+https://github.com/rust-lang/crates.io-index" 57 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 58 + dependencies = [ 59 + "anstyle", 60 + "once_cell_polyfill", 61 + "windows-sys 0.61.2", 62 + ] 63 + 64 + [[package]] 65 + name = "atomic-waker" 66 + version = "1.1.2" 67 + source = "registry+https://github.com/rust-lang/crates.io-index" 68 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 69 + 70 + [[package]] 71 + name = "base64" 72 + version = "0.22.1" 73 + source = "registry+https://github.com/rust-lang/crates.io-index" 74 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 75 + 76 + [[package]] 77 + name = "bitflags" 78 + version = "2.10.0" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 81 + 82 + [[package]] 83 + name = "block-buffer" 84 + version = "0.10.4" 85 + source = "registry+https://github.com/rust-lang/crates.io-index" 86 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 87 + dependencies = [ 88 + "generic-array", 89 + ] 90 + 91 + [[package]] 92 + name = "bumpalo" 93 + version = "3.19.1" 94 + source = "registry+https://github.com/rust-lang/crates.io-index" 95 + checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" 96 + 97 + [[package]] 98 + name = "bytes" 99 + version = "1.11.0" 100 + source = "registry+https://github.com/rust-lang/crates.io-index" 101 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 102 + 103 + [[package]] 104 + name = "cc" 105 + version = "1.2.52" 106 + source = "registry+https://github.com/rust-lang/crates.io-index" 107 + checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" 108 + dependencies = [ 109 + "find-msvc-tools", 110 + "shlex", 111 + ] 112 + 113 + [[package]] 114 + name = "cfg-if" 115 + version = "1.0.4" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 118 + 119 + [[package]] 120 + name = "cfg_aliases" 121 + version = "0.2.1" 122 + source = "registry+https://github.com/rust-lang/crates.io-index" 123 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 124 + 125 + [[package]] 126 + name = "colorchoice" 127 + version = "1.0.4" 128 + source = "registry+https://github.com/rust-lang/crates.io-index" 129 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 130 + 131 + [[package]] 132 + name = "core-foundation" 133 + version = "0.10.1" 134 + source = "registry+https://github.com/rust-lang/crates.io-index" 135 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 136 + dependencies = [ 137 + "core-foundation-sys", 138 + "libc", 139 + ] 140 + 141 + [[package]] 142 + name = "core-foundation-sys" 143 + version = "0.8.7" 144 + source = "registry+https://github.com/rust-lang/crates.io-index" 145 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 146 + 147 + [[package]] 148 + name = "cpufeatures" 149 + version = "0.2.17" 150 + source = "registry+https://github.com/rust-lang/crates.io-index" 151 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 152 + dependencies = [ 153 + "libc", 154 + ] 155 + 156 + [[package]] 157 + name = "crypto-common" 158 + version = "0.1.7" 159 + source = "registry+https://github.com/rust-lang/crates.io-index" 160 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 161 + dependencies = [ 162 + "generic-array", 163 + "typenum", 164 + ] 165 + 166 + [[package]] 167 + name = "data-encoding" 168 + version = "2.10.0" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" 171 + 172 + [[package]] 173 + name = "digest" 174 + version = "0.10.7" 175 + source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 177 + dependencies = [ 178 + "block-buffer", 179 + "crypto-common", 180 + ] 181 + 182 + [[package]] 183 + name = "displaydoc" 184 + version = "0.2.5" 185 + source = "registry+https://github.com/rust-lang/crates.io-index" 186 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 187 + dependencies = [ 188 + "proc-macro2", 189 + "quote", 190 + "syn", 191 + ] 192 + 193 + [[package]] 194 + name = "either" 195 + version = "1.15.0" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 198 + 199 + [[package]] 200 + name = "env_filter" 201 + version = "0.1.4" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" 204 + dependencies = [ 205 + "log", 206 + "regex", 207 + ] 208 + 209 + [[package]] 210 + name = "env_home" 211 + version = "0.1.0" 212 + source = "registry+https://github.com/rust-lang/crates.io-index" 213 + checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" 214 + 215 + [[package]] 216 + name = "env_logger" 217 + version = "0.11.8" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 220 + dependencies = [ 221 + "anstream", 222 + "anstyle", 223 + "env_filter", 224 + "jiff", 225 + "log", 226 + ] 227 + 228 + [[package]] 229 + name = "errno" 230 + version = "0.3.14" 231 + source = "registry+https://github.com/rust-lang/crates.io-index" 232 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 233 + dependencies = [ 234 + "libc", 235 + "windows-sys 0.61.2", 236 + ] 237 + 238 + [[package]] 239 + name = "find-msvc-tools" 240 + version = "0.1.7" 241 + source = "registry+https://github.com/rust-lang/crates.io-index" 242 + checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" 243 + 244 + [[package]] 245 + name = "form_urlencoded" 246 + version = "1.2.2" 247 + source = "registry+https://github.com/rust-lang/crates.io-index" 248 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 249 + dependencies = [ 250 + "percent-encoding", 251 + ] 252 + 253 + [[package]] 254 + name = "futures-channel" 255 + version = "0.3.31" 256 + source = "registry+https://github.com/rust-lang/crates.io-index" 257 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 258 + dependencies = [ 259 + "futures-core", 260 + ] 261 + 262 + [[package]] 263 + name = "futures-core" 264 + version = "0.3.31" 265 + source = "registry+https://github.com/rust-lang/crates.io-index" 266 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 267 + 268 + [[package]] 269 + name = "futures-macro" 270 + version = "0.3.31" 271 + source = "registry+https://github.com/rust-lang/crates.io-index" 272 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 273 + dependencies = [ 274 + "proc-macro2", 275 + "quote", 276 + "syn", 277 + ] 278 + 279 + [[package]] 280 + name = "futures-sink" 281 + version = "0.3.31" 282 + source = "registry+https://github.com/rust-lang/crates.io-index" 283 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 284 + 285 + [[package]] 286 + name = "futures-task" 287 + version = "0.3.31" 288 + source = "registry+https://github.com/rust-lang/crates.io-index" 289 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 290 + 291 + [[package]] 292 + name = "futures-util" 293 + version = "0.3.31" 294 + source = "registry+https://github.com/rust-lang/crates.io-index" 295 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 296 + dependencies = [ 297 + "futures-core", 298 + "futures-macro", 299 + "futures-sink", 300 + "futures-task", 301 + "pin-project-lite", 302 + "pin-utils", 303 + "slab", 304 + ] 305 + 306 + [[package]] 307 + name = "generic-array" 308 + version = "0.14.7" 309 + source = "registry+https://github.com/rust-lang/crates.io-index" 310 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 311 + dependencies = [ 312 + "typenum", 313 + "version_check", 314 + ] 315 + 316 + [[package]] 317 + name = "getrandom" 318 + version = "0.2.17" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 321 + dependencies = [ 322 + "cfg-if", 323 + "js-sys", 324 + "libc", 325 + "wasi", 326 + "wasm-bindgen", 327 + ] 328 + 329 + [[package]] 330 + name = "getrandom" 331 + version = "0.3.4" 332 + source = "registry+https://github.com/rust-lang/crates.io-index" 333 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 334 + dependencies = [ 335 + "cfg-if", 336 + "js-sys", 337 + "libc", 338 + "r-efi", 339 + "wasip2", 340 + "wasm-bindgen", 341 + ] 342 + 343 + [[package]] 344 + name = "http" 345 + version = "1.4.0" 346 + source = "registry+https://github.com/rust-lang/crates.io-index" 347 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 348 + dependencies = [ 349 + "bytes", 350 + "itoa", 351 + ] 352 + 353 + [[package]] 354 + name = "http-body" 355 + version = "1.0.1" 356 + source = "registry+https://github.com/rust-lang/crates.io-index" 357 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 358 + dependencies = [ 359 + "bytes", 360 + "http", 361 + ] 362 + 363 + [[package]] 364 + name = "http-body-util" 365 + version = "0.1.3" 366 + source = "registry+https://github.com/rust-lang/crates.io-index" 367 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 368 + dependencies = [ 369 + "bytes", 370 + "futures-core", 371 + "http", 372 + "http-body", 373 + "pin-project-lite", 374 + ] 375 + 376 + [[package]] 377 + name = "httparse" 378 + version = "1.10.1" 379 + source = "registry+https://github.com/rust-lang/crates.io-index" 380 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 381 + 382 + [[package]] 383 + name = "hyper" 384 + version = "1.8.1" 385 + source = "registry+https://github.com/rust-lang/crates.io-index" 386 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 387 + dependencies = [ 388 + "atomic-waker", 389 + "bytes", 390 + "futures-channel", 391 + "futures-core", 392 + "http", 393 + "http-body", 394 + "httparse", 395 + "itoa", 396 + "pin-project-lite", 397 + "pin-utils", 398 + "smallvec", 399 + "tokio", 400 + "want", 401 + ] 402 + 403 + [[package]] 404 + name = "hyper-rustls" 405 + version = "0.27.7" 406 + source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 408 + dependencies = [ 409 + "http", 410 + "hyper", 411 + "hyper-util", 412 + "rustls", 413 + "rustls-pki-types", 414 + "tokio", 415 + "tokio-rustls", 416 + "tower-service", 417 + "webpki-roots", 418 + ] 419 + 420 + [[package]] 421 + name = "hyper-util" 422 + version = "0.1.19" 423 + source = "registry+https://github.com/rust-lang/crates.io-index" 424 + checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 425 + dependencies = [ 426 + "base64", 427 + "bytes", 428 + "futures-channel", 429 + "futures-core", 430 + "futures-util", 431 + "http", 432 + "http-body", 433 + "hyper", 434 + "ipnet", 435 + "libc", 436 + "percent-encoding", 437 + "pin-project-lite", 438 + "socket2", 439 + "tokio", 440 + "tower-service", 441 + "tracing", 442 + ] 443 + 444 + [[package]] 445 + name = "icu_collections" 446 + version = "2.1.1" 447 + source = "registry+https://github.com/rust-lang/crates.io-index" 448 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 449 + dependencies = [ 450 + "displaydoc", 451 + "potential_utf", 452 + "yoke", 453 + "zerofrom", 454 + "zerovec", 455 + ] 456 + 457 + [[package]] 458 + name = "icu_locale_core" 459 + version = "2.1.1" 460 + source = "registry+https://github.com/rust-lang/crates.io-index" 461 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 462 + dependencies = [ 463 + "displaydoc", 464 + "litemap", 465 + "tinystr", 466 + "writeable", 467 + "zerovec", 468 + ] 469 + 470 + [[package]] 471 + name = "icu_normalizer" 472 + version = "2.1.1" 473 + source = "registry+https://github.com/rust-lang/crates.io-index" 474 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 475 + dependencies = [ 476 + "icu_collections", 477 + "icu_normalizer_data", 478 + "icu_properties", 479 + "icu_provider", 480 + "smallvec", 481 + "zerovec", 482 + ] 483 + 484 + [[package]] 485 + name = "icu_normalizer_data" 486 + version = "2.1.1" 487 + source = "registry+https://github.com/rust-lang/crates.io-index" 488 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 489 + 490 + [[package]] 491 + name = "icu_properties" 492 + version = "2.1.2" 493 + source = "registry+https://github.com/rust-lang/crates.io-index" 494 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 495 + dependencies = [ 496 + "icu_collections", 497 + "icu_locale_core", 498 + "icu_properties_data", 499 + "icu_provider", 500 + "zerotrie", 501 + "zerovec", 502 + ] 503 + 504 + [[package]] 505 + name = "icu_properties_data" 506 + version = "2.1.2" 507 + source = "registry+https://github.com/rust-lang/crates.io-index" 508 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 509 + 510 + [[package]] 511 + name = "icu_provider" 512 + version = "2.1.1" 513 + source = "registry+https://github.com/rust-lang/crates.io-index" 514 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 515 + dependencies = [ 516 + "displaydoc", 517 + "icu_locale_core", 518 + "writeable", 519 + "yoke", 520 + "zerofrom", 521 + "zerotrie", 522 + "zerovec", 523 + ] 524 + 525 + [[package]] 526 + name = "idna" 527 + version = "1.1.0" 528 + source = "registry+https://github.com/rust-lang/crates.io-index" 529 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 530 + dependencies = [ 531 + "idna_adapter", 532 + "smallvec", 533 + "utf8_iter", 534 + ] 535 + 536 + [[package]] 537 + name = "idna_adapter" 538 + version = "1.2.1" 539 + source = "registry+https://github.com/rust-lang/crates.io-index" 540 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 541 + dependencies = [ 542 + "icu_normalizer", 543 + "icu_properties", 544 + ] 545 + 546 + [[package]] 547 + name = "ipnet" 548 + version = "2.11.0" 549 + source = "registry+https://github.com/rust-lang/crates.io-index" 550 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 551 + 552 + [[package]] 553 + name = "iri-string" 554 + version = "0.7.10" 555 + source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 557 + dependencies = [ 558 + "memchr", 559 + "serde", 560 + ] 561 + 562 + [[package]] 563 + name = "is_terminal_polyfill" 564 + version = "1.70.2" 565 + source = "registry+https://github.com/rust-lang/crates.io-index" 566 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 567 + 568 + [[package]] 569 + name = "itoa" 570 + version = "1.0.17" 571 + source = "registry+https://github.com/rust-lang/crates.io-index" 572 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 573 + 574 + [[package]] 575 + name = "jiff" 576 + version = "0.2.18" 577 + source = "registry+https://github.com/rust-lang/crates.io-index" 578 + checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" 579 + dependencies = [ 580 + "jiff-static", 581 + "log", 582 + "portable-atomic", 583 + "portable-atomic-util", 584 + "serde_core", 585 + ] 586 + 587 + [[package]] 588 + name = "jiff-static" 589 + version = "0.2.18" 590 + source = "registry+https://github.com/rust-lang/crates.io-index" 591 + checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" 592 + dependencies = [ 593 + "proc-macro2", 594 + "quote", 595 + "syn", 596 + ] 597 + 598 + [[package]] 599 + name = "js-sys" 600 + version = "0.3.85" 601 + source = "registry+https://github.com/rust-lang/crates.io-index" 602 + checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" 603 + dependencies = [ 604 + "once_cell", 605 + "wasm-bindgen", 606 + ] 607 + 608 + [[package]] 609 + name = "libc" 610 + version = "0.2.180" 611 + source = "registry+https://github.com/rust-lang/crates.io-index" 612 + checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" 613 + 614 + [[package]] 615 + name = "linux-raw-sys" 616 + version = "0.11.0" 617 + source = "registry+https://github.com/rust-lang/crates.io-index" 618 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 619 + 620 + [[package]] 621 + name = "litemap" 622 + version = "0.8.1" 623 + source = "registry+https://github.com/rust-lang/crates.io-index" 624 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 625 + 626 + [[package]] 627 + name = "log" 628 + version = "0.4.29" 629 + source = "registry+https://github.com/rust-lang/crates.io-index" 630 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 631 + 632 + [[package]] 633 + name = "lru-slab" 634 + version = "0.1.2" 635 + source = "registry+https://github.com/rust-lang/crates.io-index" 636 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 637 + 638 + [[package]] 639 + name = "memchr" 640 + version = "2.7.6" 641 + source = "registry+https://github.com/rust-lang/crates.io-index" 642 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 643 + 644 + [[package]] 645 + name = "mio" 646 + version = "1.1.1" 647 + source = "registry+https://github.com/rust-lang/crates.io-index" 648 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 649 + dependencies = [ 650 + "libc", 651 + "wasi", 652 + "windows-sys 0.61.2", 653 + ] 654 + 655 + [[package]] 656 + name = "once_cell" 657 + version = "1.21.3" 658 + source = "registry+https://github.com/rust-lang/crates.io-index" 659 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 660 + 661 + [[package]] 662 + name = "once_cell_polyfill" 663 + version = "1.70.2" 664 + source = "registry+https://github.com/rust-lang/crates.io-index" 665 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 666 + 667 + [[package]] 668 + name = "openssl-probe" 669 + version = "0.2.0" 670 + source = "registry+https://github.com/rust-lang/crates.io-index" 671 + checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" 672 + 673 + [[package]] 674 + name = "percent-encoding" 675 + version = "2.3.2" 676 + source = "registry+https://github.com/rust-lang/crates.io-index" 677 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 678 + 679 + [[package]] 680 + name = "pin-project-lite" 681 + version = "0.2.16" 682 + source = "registry+https://github.com/rust-lang/crates.io-index" 683 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 684 + 685 + [[package]] 686 + name = "pin-utils" 687 + version = "0.1.0" 688 + source = "registry+https://github.com/rust-lang/crates.io-index" 689 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 690 + 691 + [[package]] 692 + name = "portable-atomic" 693 + version = "1.13.0" 694 + source = "registry+https://github.com/rust-lang/crates.io-index" 695 + checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" 696 + 697 + [[package]] 698 + name = "portable-atomic-util" 699 + version = "0.2.4" 700 + source = "registry+https://github.com/rust-lang/crates.io-index" 701 + checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 702 + dependencies = [ 703 + "portable-atomic", 704 + ] 705 + 706 + [[package]] 707 + name = "potential_utf" 708 + version = "0.1.4" 709 + source = "registry+https://github.com/rust-lang/crates.io-index" 710 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 711 + dependencies = [ 712 + "zerovec", 713 + ] 714 + 715 + [[package]] 716 + name = "ppv-lite86" 717 + version = "0.2.21" 718 + source = "registry+https://github.com/rust-lang/crates.io-index" 719 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 720 + dependencies = [ 721 + "zerocopy", 722 + ] 723 + 724 + [[package]] 725 + name = "proc-macro2" 726 + version = "1.0.105" 727 + source = "registry+https://github.com/rust-lang/crates.io-index" 728 + checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" 729 + dependencies = [ 730 + "unicode-ident", 731 + ] 732 + 733 + [[package]] 734 + name = "quinn" 735 + version = "0.11.9" 736 + source = "registry+https://github.com/rust-lang/crates.io-index" 737 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 738 + dependencies = [ 739 + "bytes", 740 + "cfg_aliases", 741 + "pin-project-lite", 742 + "quinn-proto", 743 + "quinn-udp", 744 + "rustc-hash", 745 + "rustls", 746 + "socket2", 747 + "thiserror", 748 + "tokio", 749 + "tracing", 750 + "web-time", 751 + ] 752 + 753 + [[package]] 754 + name = "quinn-proto" 755 + version = "0.11.13" 756 + source = "registry+https://github.com/rust-lang/crates.io-index" 757 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 758 + dependencies = [ 759 + "bytes", 760 + "getrandom 0.3.4", 761 + "lru-slab", 762 + "rand", 763 + "ring", 764 + "rustc-hash", 765 + "rustls", 766 + "rustls-pki-types", 767 + "slab", 768 + "thiserror", 769 + "tinyvec", 770 + "tracing", 771 + "web-time", 772 + ] 773 + 774 + [[package]] 775 + name = "quinn-udp" 776 + version = "0.5.14" 777 + source = "registry+https://github.com/rust-lang/crates.io-index" 778 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 779 + dependencies = [ 780 + "cfg_aliases", 781 + "libc", 782 + "once_cell", 783 + "socket2", 784 + "tracing", 785 + "windows-sys 0.60.2", 786 + ] 787 + 788 + [[package]] 789 + name = "quote" 790 + version = "1.0.43" 791 + source = "registry+https://github.com/rust-lang/crates.io-index" 792 + checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" 793 + dependencies = [ 794 + "proc-macro2", 795 + ] 796 + 797 + [[package]] 798 + name = "r-efi" 799 + version = "5.3.0" 800 + source = "registry+https://github.com/rust-lang/crates.io-index" 801 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 802 + 803 + [[package]] 804 + name = "rand" 805 + version = "0.9.2" 806 + source = "registry+https://github.com/rust-lang/crates.io-index" 807 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 808 + dependencies = [ 809 + "rand_chacha", 810 + "rand_core", 811 + ] 812 + 813 + [[package]] 814 + name = "rand_chacha" 815 + version = "0.9.0" 816 + source = "registry+https://github.com/rust-lang/crates.io-index" 817 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 818 + dependencies = [ 819 + "ppv-lite86", 820 + "rand_core", 821 + ] 822 + 823 + [[package]] 824 + name = "rand_core" 825 + version = "0.9.5" 826 + source = "registry+https://github.com/rust-lang/crates.io-index" 827 + checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" 828 + dependencies = [ 829 + "getrandom 0.3.4", 830 + ] 831 + 832 + [[package]] 833 + name = "regex" 834 + version = "1.12.2" 835 + source = "registry+https://github.com/rust-lang/crates.io-index" 836 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 837 + dependencies = [ 838 + "aho-corasick", 839 + "memchr", 840 + "regex-automata", 841 + "regex-syntax", 842 + ] 843 + 844 + [[package]] 845 + name = "regex-automata" 846 + version = "0.4.13" 847 + source = "registry+https://github.com/rust-lang/crates.io-index" 848 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 849 + dependencies = [ 850 + "aho-corasick", 851 + "memchr", 852 + "regex-syntax", 853 + ] 854 + 855 + [[package]] 856 + name = "regex-syntax" 857 + version = "0.8.8" 858 + source = "registry+https://github.com/rust-lang/crates.io-index" 859 + checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 860 + 861 + [[package]] 862 + name = "reqwest" 863 + version = "0.12.28" 864 + source = "registry+https://github.com/rust-lang/crates.io-index" 865 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 866 + dependencies = [ 867 + "base64", 868 + "bytes", 869 + "futures-core", 870 + "http", 871 + "http-body", 872 + "http-body-util", 873 + "hyper", 874 + "hyper-rustls", 875 + "hyper-util", 876 + "js-sys", 877 + "log", 878 + "percent-encoding", 879 + "pin-project-lite", 880 + "quinn", 881 + "rustls", 882 + "rustls-pki-types", 883 + "serde", 884 + "serde_json", 885 + "serde_urlencoded", 886 + "sync_wrapper", 887 + "tokio", 888 + "tokio-rustls", 889 + "tower", 890 + "tower-http", 891 + "tower-service", 892 + "url", 893 + "wasm-bindgen", 894 + "wasm-bindgen-futures", 895 + "web-sys", 896 + "webpki-roots", 897 + ] 898 + 899 + [[package]] 900 + name = "ring" 901 + version = "0.17.14" 902 + source = "registry+https://github.com/rust-lang/crates.io-index" 903 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 904 + dependencies = [ 905 + "cc", 906 + "cfg-if", 907 + "getrandom 0.2.17", 908 + "libc", 909 + "untrusted", 910 + "windows-sys 0.52.0", 911 + ] 912 + 913 + [[package]] 914 + name = "rustc-hash" 915 + version = "2.1.1" 916 + source = "registry+https://github.com/rust-lang/crates.io-index" 917 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 918 + 919 + [[package]] 920 + name = "rustix" 921 + version = "1.1.3" 922 + source = "registry+https://github.com/rust-lang/crates.io-index" 923 + checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 924 + dependencies = [ 925 + "bitflags", 926 + "errno", 927 + "libc", 928 + "linux-raw-sys", 929 + "windows-sys 0.61.2", 930 + ] 931 + 932 + [[package]] 933 + name = "rustls" 934 + version = "0.23.36" 935 + source = "registry+https://github.com/rust-lang/crates.io-index" 936 + checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" 937 + dependencies = [ 938 + "once_cell", 939 + "ring", 940 + "rustls-pki-types", 941 + "rustls-webpki", 942 + "subtle", 943 + "zeroize", 944 + ] 945 + 946 + [[package]] 947 + name = "rustls-native-certs" 948 + version = "0.8.3" 949 + source = "registry+https://github.com/rust-lang/crates.io-index" 950 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" 951 + dependencies = [ 952 + "openssl-probe", 953 + "rustls-pki-types", 954 + "schannel", 955 + "security-framework", 956 + ] 957 + 958 + [[package]] 959 + name = "rustls-pki-types" 960 + version = "1.13.2" 961 + source = "registry+https://github.com/rust-lang/crates.io-index" 962 + checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" 963 + dependencies = [ 964 + "web-time", 965 + "zeroize", 966 + ] 967 + 968 + [[package]] 969 + name = "rustls-webpki" 970 + version = "0.103.8" 971 + source = "registry+https://github.com/rust-lang/crates.io-index" 972 + checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" 973 + dependencies = [ 974 + "ring", 975 + "rustls-pki-types", 976 + "untrusted", 977 + ] 978 + 979 + [[package]] 980 + name = "rustversion" 981 + version = "1.0.22" 982 + source = "registry+https://github.com/rust-lang/crates.io-index" 983 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 984 + 985 + [[package]] 986 + name = "ryu" 987 + version = "1.0.22" 988 + source = "registry+https://github.com/rust-lang/crates.io-index" 989 + checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 990 + 991 + [[package]] 992 + name = "schannel" 993 + version = "0.1.28" 994 + source = "registry+https://github.com/rust-lang/crates.io-index" 995 + checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 996 + dependencies = [ 997 + "windows-sys 0.61.2", 998 + ] 999 + 1000 + [[package]] 1001 + name = "security-framework" 1002 + version = "3.5.1" 1003 + source = "registry+https://github.com/rust-lang/crates.io-index" 1004 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 1005 + dependencies = [ 1006 + "bitflags", 1007 + "core-foundation", 1008 + "core-foundation-sys", 1009 + "libc", 1010 + "security-framework-sys", 1011 + ] 1012 + 1013 + [[package]] 1014 + name = "security-framework-sys" 1015 + version = "2.15.0" 1016 + source = "registry+https://github.com/rust-lang/crates.io-index" 1017 + checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 1018 + dependencies = [ 1019 + "core-foundation-sys", 1020 + "libc", 1021 + ] 1022 + 1023 + [[package]] 1024 + name = "serde" 1025 + version = "1.0.228" 1026 + source = "registry+https://github.com/rust-lang/crates.io-index" 1027 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1028 + dependencies = [ 1029 + "serde_core", 1030 + "serde_derive", 1031 + ] 1032 + 1033 + [[package]] 1034 + name = "serde_core" 1035 + version = "1.0.228" 1036 + source = "registry+https://github.com/rust-lang/crates.io-index" 1037 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1038 + dependencies = [ 1039 + "serde_derive", 1040 + ] 1041 + 1042 + [[package]] 1043 + name = "serde_derive" 1044 + version = "1.0.228" 1045 + source = "registry+https://github.com/rust-lang/crates.io-index" 1046 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1047 + dependencies = [ 1048 + "proc-macro2", 1049 + "quote", 1050 + "syn", 1051 + ] 1052 + 1053 + [[package]] 1054 + name = "serde_json" 1055 + version = "1.0.149" 1056 + source = "registry+https://github.com/rust-lang/crates.io-index" 1057 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 1058 + dependencies = [ 1059 + "itoa", 1060 + "memchr", 1061 + "serde", 1062 + "serde_core", 1063 + "zmij", 1064 + ] 1065 + 1066 + [[package]] 1067 + name = "serde_urlencoded" 1068 + version = "0.7.1" 1069 + source = "registry+https://github.com/rust-lang/crates.io-index" 1070 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1071 + dependencies = [ 1072 + "form_urlencoded", 1073 + "itoa", 1074 + "ryu", 1075 + "serde", 1076 + ] 1077 + 1078 + [[package]] 1079 + name = "sha1" 1080 + version = "0.10.6" 1081 + source = "registry+https://github.com/rust-lang/crates.io-index" 1082 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1083 + dependencies = [ 1084 + "cfg-if", 1085 + "cpufeatures", 1086 + "digest", 1087 + ] 1088 + 1089 + [[package]] 1090 + name = "shlex" 1091 + version = "1.3.0" 1092 + source = "registry+https://github.com/rust-lang/crates.io-index" 1093 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1094 + 1095 + [[package]] 1096 + name = "signal-hook-registry" 1097 + version = "1.4.8" 1098 + source = "registry+https://github.com/rust-lang/crates.io-index" 1099 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 1100 + dependencies = [ 1101 + "errno", 1102 + "libc", 1103 + ] 1104 + 1105 + [[package]] 1106 + name = "slab" 1107 + version = "0.4.11" 1108 + source = "registry+https://github.com/rust-lang/crates.io-index" 1109 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1110 + 1111 + [[package]] 1112 + name = "smallvec" 1113 + version = "1.15.1" 1114 + source = "registry+https://github.com/rust-lang/crates.io-index" 1115 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1116 + 1117 + [[package]] 1118 + name = "socket2" 1119 + version = "0.6.1" 1120 + source = "registry+https://github.com/rust-lang/crates.io-index" 1121 + checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 1122 + dependencies = [ 1123 + "libc", 1124 + "windows-sys 0.60.2", 1125 + ] 1126 + 1127 + [[package]] 1128 + name = "stable_deref_trait" 1129 + version = "1.2.1" 1130 + source = "registry+https://github.com/rust-lang/crates.io-index" 1131 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1132 + 1133 + [[package]] 1134 + name = "subtle" 1135 + version = "2.6.1" 1136 + source = "registry+https://github.com/rust-lang/crates.io-index" 1137 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1138 + 1139 + [[package]] 1140 + name = "syn" 1141 + version = "2.0.114" 1142 + source = "registry+https://github.com/rust-lang/crates.io-index" 1143 + checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" 1144 + dependencies = [ 1145 + "proc-macro2", 1146 + "quote", 1147 + "unicode-ident", 1148 + ] 1149 + 1150 + [[package]] 1151 + name = "sync_wrapper" 1152 + version = "1.0.2" 1153 + source = "registry+https://github.com/rust-lang/crates.io-index" 1154 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1155 + dependencies = [ 1156 + "futures-core", 1157 + ] 1158 + 1159 + [[package]] 1160 + name = "synstructure" 1161 + version = "0.13.2" 1162 + source = "registry+https://github.com/rust-lang/crates.io-index" 1163 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1164 + dependencies = [ 1165 + "proc-macro2", 1166 + "quote", 1167 + "syn", 1168 + ] 1169 + 1170 + [[package]] 1171 + name = "tapped" 1172 + version = "0.1.0" 1173 + dependencies = [ 1174 + "base64", 1175 + "env_logger", 1176 + "futures-util", 1177 + "libc", 1178 + "log", 1179 + "reqwest", 1180 + "serde", 1181 + "serde_json", 1182 + "thiserror", 1183 + "tokio", 1184 + "tokio-tungstenite", 1185 + "tracing", 1186 + "url", 1187 + "which", 1188 + ] 1189 + 1190 + [[package]] 1191 + name = "thiserror" 1192 + version = "2.0.17" 1193 + source = "registry+https://github.com/rust-lang/crates.io-index" 1194 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 1195 + dependencies = [ 1196 + "thiserror-impl", 1197 + ] 1198 + 1199 + [[package]] 1200 + name = "thiserror-impl" 1201 + version = "2.0.17" 1202 + source = "registry+https://github.com/rust-lang/crates.io-index" 1203 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 1204 + dependencies = [ 1205 + "proc-macro2", 1206 + "quote", 1207 + "syn", 1208 + ] 1209 + 1210 + [[package]] 1211 + name = "tinystr" 1212 + version = "0.8.2" 1213 + source = "registry+https://github.com/rust-lang/crates.io-index" 1214 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1215 + dependencies = [ 1216 + "displaydoc", 1217 + "zerovec", 1218 + ] 1219 + 1220 + [[package]] 1221 + name = "tinyvec" 1222 + version = "1.10.0" 1223 + source = "registry+https://github.com/rust-lang/crates.io-index" 1224 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 1225 + dependencies = [ 1226 + "tinyvec_macros", 1227 + ] 1228 + 1229 + [[package]] 1230 + name = "tinyvec_macros" 1231 + version = "0.1.1" 1232 + source = "registry+https://github.com/rust-lang/crates.io-index" 1233 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1234 + 1235 + [[package]] 1236 + name = "tokio" 1237 + version = "1.49.0" 1238 + source = "registry+https://github.com/rust-lang/crates.io-index" 1239 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 1240 + dependencies = [ 1241 + "bytes", 1242 + "libc", 1243 + "mio", 1244 + "pin-project-lite", 1245 + "signal-hook-registry", 1246 + "socket2", 1247 + "tokio-macros", 1248 + "windows-sys 0.61.2", 1249 + ] 1250 + 1251 + [[package]] 1252 + name = "tokio-macros" 1253 + version = "2.6.0" 1254 + source = "registry+https://github.com/rust-lang/crates.io-index" 1255 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1256 + dependencies = [ 1257 + "proc-macro2", 1258 + "quote", 1259 + "syn", 1260 + ] 1261 + 1262 + [[package]] 1263 + name = "tokio-rustls" 1264 + version = "0.26.4" 1265 + source = "registry+https://github.com/rust-lang/crates.io-index" 1266 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 1267 + dependencies = [ 1268 + "rustls", 1269 + "tokio", 1270 + ] 1271 + 1272 + [[package]] 1273 + name = "tokio-tungstenite" 1274 + version = "0.26.2" 1275 + source = "registry+https://github.com/rust-lang/crates.io-index" 1276 + checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" 1277 + dependencies = [ 1278 + "futures-util", 1279 + "log", 1280 + "rustls", 1281 + "rustls-native-certs", 1282 + "rustls-pki-types", 1283 + "tokio", 1284 + "tokio-rustls", 1285 + "tungstenite", 1286 + ] 1287 + 1288 + [[package]] 1289 + name = "tower" 1290 + version = "0.5.3" 1291 + source = "registry+https://github.com/rust-lang/crates.io-index" 1292 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 1293 + dependencies = [ 1294 + "futures-core", 1295 + "futures-util", 1296 + "pin-project-lite", 1297 + "sync_wrapper", 1298 + "tokio", 1299 + "tower-layer", 1300 + "tower-service", 1301 + ] 1302 + 1303 + [[package]] 1304 + name = "tower-http" 1305 + version = "0.6.8" 1306 + source = "registry+https://github.com/rust-lang/crates.io-index" 1307 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 1308 + dependencies = [ 1309 + "bitflags", 1310 + "bytes", 1311 + "futures-util", 1312 + "http", 1313 + "http-body", 1314 + "iri-string", 1315 + "pin-project-lite", 1316 + "tower", 1317 + "tower-layer", 1318 + "tower-service", 1319 + ] 1320 + 1321 + [[package]] 1322 + name = "tower-layer" 1323 + version = "0.3.3" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1326 + 1327 + [[package]] 1328 + name = "tower-service" 1329 + version = "0.3.3" 1330 + source = "registry+https://github.com/rust-lang/crates.io-index" 1331 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1332 + 1333 + [[package]] 1334 + name = "tracing" 1335 + version = "0.1.44" 1336 + source = "registry+https://github.com/rust-lang/crates.io-index" 1337 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1338 + dependencies = [ 1339 + "pin-project-lite", 1340 + "tracing-attributes", 1341 + "tracing-core", 1342 + ] 1343 + 1344 + [[package]] 1345 + name = "tracing-attributes" 1346 + version = "0.1.31" 1347 + source = "registry+https://github.com/rust-lang/crates.io-index" 1348 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 1349 + dependencies = [ 1350 + "proc-macro2", 1351 + "quote", 1352 + "syn", 1353 + ] 1354 + 1355 + [[package]] 1356 + name = "tracing-core" 1357 + version = "0.1.36" 1358 + source = "registry+https://github.com/rust-lang/crates.io-index" 1359 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1360 + dependencies = [ 1361 + "once_cell", 1362 + ] 1363 + 1364 + [[package]] 1365 + name = "try-lock" 1366 + version = "0.2.5" 1367 + source = "registry+https://github.com/rust-lang/crates.io-index" 1368 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1369 + 1370 + [[package]] 1371 + name = "tungstenite" 1372 + version = "0.26.2" 1373 + source = "registry+https://github.com/rust-lang/crates.io-index" 1374 + checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" 1375 + dependencies = [ 1376 + "bytes", 1377 + "data-encoding", 1378 + "http", 1379 + "httparse", 1380 + "log", 1381 + "rand", 1382 + "rustls", 1383 + "rustls-pki-types", 1384 + "sha1", 1385 + "thiserror", 1386 + "utf-8", 1387 + ] 1388 + 1389 + [[package]] 1390 + name = "typenum" 1391 + version = "1.19.0" 1392 + source = "registry+https://github.com/rust-lang/crates.io-index" 1393 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 1394 + 1395 + [[package]] 1396 + name = "unicode-ident" 1397 + version = "1.0.22" 1398 + source = "registry+https://github.com/rust-lang/crates.io-index" 1399 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1400 + 1401 + [[package]] 1402 + name = "untrusted" 1403 + version = "0.9.0" 1404 + source = "registry+https://github.com/rust-lang/crates.io-index" 1405 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1406 + 1407 + [[package]] 1408 + name = "url" 1409 + version = "2.5.8" 1410 + source = "registry+https://github.com/rust-lang/crates.io-index" 1411 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 1412 + dependencies = [ 1413 + "form_urlencoded", 1414 + "idna", 1415 + "percent-encoding", 1416 + "serde", 1417 + "serde_derive", 1418 + ] 1419 + 1420 + [[package]] 1421 + name = "utf-8" 1422 + version = "0.7.6" 1423 + source = "registry+https://github.com/rust-lang/crates.io-index" 1424 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1425 + 1426 + [[package]] 1427 + name = "utf8_iter" 1428 + version = "1.0.4" 1429 + source = "registry+https://github.com/rust-lang/crates.io-index" 1430 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1431 + 1432 + [[package]] 1433 + name = "utf8parse" 1434 + version = "0.2.2" 1435 + source = "registry+https://github.com/rust-lang/crates.io-index" 1436 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1437 + 1438 + [[package]] 1439 + name = "version_check" 1440 + version = "0.9.5" 1441 + source = "registry+https://github.com/rust-lang/crates.io-index" 1442 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1443 + 1444 + [[package]] 1445 + name = "want" 1446 + version = "0.3.1" 1447 + source = "registry+https://github.com/rust-lang/crates.io-index" 1448 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1449 + dependencies = [ 1450 + "try-lock", 1451 + ] 1452 + 1453 + [[package]] 1454 + name = "wasi" 1455 + version = "0.11.1+wasi-snapshot-preview1" 1456 + source = "registry+https://github.com/rust-lang/crates.io-index" 1457 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1458 + 1459 + [[package]] 1460 + name = "wasip2" 1461 + version = "1.0.1+wasi-0.2.4" 1462 + source = "registry+https://github.com/rust-lang/crates.io-index" 1463 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1464 + dependencies = [ 1465 + "wit-bindgen", 1466 + ] 1467 + 1468 + [[package]] 1469 + name = "wasm-bindgen" 1470 + version = "0.2.108" 1471 + source = "registry+https://github.com/rust-lang/crates.io-index" 1472 + checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" 1473 + dependencies = [ 1474 + "cfg-if", 1475 + "once_cell", 1476 + "rustversion", 1477 + "wasm-bindgen-macro", 1478 + "wasm-bindgen-shared", 1479 + ] 1480 + 1481 + [[package]] 1482 + name = "wasm-bindgen-futures" 1483 + version = "0.4.58" 1484 + source = "registry+https://github.com/rust-lang/crates.io-index" 1485 + checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" 1486 + dependencies = [ 1487 + "cfg-if", 1488 + "futures-util", 1489 + "js-sys", 1490 + "once_cell", 1491 + "wasm-bindgen", 1492 + "web-sys", 1493 + ] 1494 + 1495 + [[package]] 1496 + name = "wasm-bindgen-macro" 1497 + version = "0.2.108" 1498 + source = "registry+https://github.com/rust-lang/crates.io-index" 1499 + checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" 1500 + dependencies = [ 1501 + "quote", 1502 + "wasm-bindgen-macro-support", 1503 + ] 1504 + 1505 + [[package]] 1506 + name = "wasm-bindgen-macro-support" 1507 + version = "0.2.108" 1508 + source = "registry+https://github.com/rust-lang/crates.io-index" 1509 + checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" 1510 + dependencies = [ 1511 + "bumpalo", 1512 + "proc-macro2", 1513 + "quote", 1514 + "syn", 1515 + "wasm-bindgen-shared", 1516 + ] 1517 + 1518 + [[package]] 1519 + name = "wasm-bindgen-shared" 1520 + version = "0.2.108" 1521 + source = "registry+https://github.com/rust-lang/crates.io-index" 1522 + checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" 1523 + dependencies = [ 1524 + "unicode-ident", 1525 + ] 1526 + 1527 + [[package]] 1528 + name = "web-sys" 1529 + version = "0.3.85" 1530 + source = "registry+https://github.com/rust-lang/crates.io-index" 1531 + checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" 1532 + dependencies = [ 1533 + "js-sys", 1534 + "wasm-bindgen", 1535 + ] 1536 + 1537 + [[package]] 1538 + name = "web-time" 1539 + version = "1.1.0" 1540 + source = "registry+https://github.com/rust-lang/crates.io-index" 1541 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1542 + dependencies = [ 1543 + "js-sys", 1544 + "wasm-bindgen", 1545 + ] 1546 + 1547 + [[package]] 1548 + name = "webpki-roots" 1549 + version = "1.0.5" 1550 + source = "registry+https://github.com/rust-lang/crates.io-index" 1551 + checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" 1552 + dependencies = [ 1553 + "rustls-pki-types", 1554 + ] 1555 + 1556 + [[package]] 1557 + name = "which" 1558 + version = "7.0.3" 1559 + source = "registry+https://github.com/rust-lang/crates.io-index" 1560 + checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" 1561 + dependencies = [ 1562 + "either", 1563 + "env_home", 1564 + "rustix", 1565 + "winsafe", 1566 + ] 1567 + 1568 + [[package]] 1569 + name = "windows-link" 1570 + version = "0.2.1" 1571 + source = "registry+https://github.com/rust-lang/crates.io-index" 1572 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1573 + 1574 + [[package]] 1575 + name = "windows-sys" 1576 + version = "0.52.0" 1577 + source = "registry+https://github.com/rust-lang/crates.io-index" 1578 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1579 + dependencies = [ 1580 + "windows-targets 0.52.6", 1581 + ] 1582 + 1583 + [[package]] 1584 + name = "windows-sys" 1585 + version = "0.60.2" 1586 + source = "registry+https://github.com/rust-lang/crates.io-index" 1587 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1588 + dependencies = [ 1589 + "windows-targets 0.53.5", 1590 + ] 1591 + 1592 + [[package]] 1593 + name = "windows-sys" 1594 + version = "0.61.2" 1595 + source = "registry+https://github.com/rust-lang/crates.io-index" 1596 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1597 + dependencies = [ 1598 + "windows-link", 1599 + ] 1600 + 1601 + [[package]] 1602 + name = "windows-targets" 1603 + version = "0.52.6" 1604 + source = "registry+https://github.com/rust-lang/crates.io-index" 1605 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1606 + dependencies = [ 1607 + "windows_aarch64_gnullvm 0.52.6", 1608 + "windows_aarch64_msvc 0.52.6", 1609 + "windows_i686_gnu 0.52.6", 1610 + "windows_i686_gnullvm 0.52.6", 1611 + "windows_i686_msvc 0.52.6", 1612 + "windows_x86_64_gnu 0.52.6", 1613 + "windows_x86_64_gnullvm 0.52.6", 1614 + "windows_x86_64_msvc 0.52.6", 1615 + ] 1616 + 1617 + [[package]] 1618 + name = "windows-targets" 1619 + version = "0.53.5" 1620 + source = "registry+https://github.com/rust-lang/crates.io-index" 1621 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1622 + dependencies = [ 1623 + "windows-link", 1624 + "windows_aarch64_gnullvm 0.53.1", 1625 + "windows_aarch64_msvc 0.53.1", 1626 + "windows_i686_gnu 0.53.1", 1627 + "windows_i686_gnullvm 0.53.1", 1628 + "windows_i686_msvc 0.53.1", 1629 + "windows_x86_64_gnu 0.53.1", 1630 + "windows_x86_64_gnullvm 0.53.1", 1631 + "windows_x86_64_msvc 0.53.1", 1632 + ] 1633 + 1634 + [[package]] 1635 + name = "windows_aarch64_gnullvm" 1636 + version = "0.52.6" 1637 + source = "registry+https://github.com/rust-lang/crates.io-index" 1638 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1639 + 1640 + [[package]] 1641 + name = "windows_aarch64_gnullvm" 1642 + version = "0.53.1" 1643 + source = "registry+https://github.com/rust-lang/crates.io-index" 1644 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1645 + 1646 + [[package]] 1647 + name = "windows_aarch64_msvc" 1648 + version = "0.52.6" 1649 + source = "registry+https://github.com/rust-lang/crates.io-index" 1650 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1651 + 1652 + [[package]] 1653 + name = "windows_aarch64_msvc" 1654 + version = "0.53.1" 1655 + source = "registry+https://github.com/rust-lang/crates.io-index" 1656 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1657 + 1658 + [[package]] 1659 + name = "windows_i686_gnu" 1660 + version = "0.52.6" 1661 + source = "registry+https://github.com/rust-lang/crates.io-index" 1662 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1663 + 1664 + [[package]] 1665 + name = "windows_i686_gnu" 1666 + version = "0.53.1" 1667 + source = "registry+https://github.com/rust-lang/crates.io-index" 1668 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1669 + 1670 + [[package]] 1671 + name = "windows_i686_gnullvm" 1672 + version = "0.52.6" 1673 + source = "registry+https://github.com/rust-lang/crates.io-index" 1674 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1675 + 1676 + [[package]] 1677 + name = "windows_i686_gnullvm" 1678 + version = "0.53.1" 1679 + source = "registry+https://github.com/rust-lang/crates.io-index" 1680 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1681 + 1682 + [[package]] 1683 + name = "windows_i686_msvc" 1684 + version = "0.52.6" 1685 + source = "registry+https://github.com/rust-lang/crates.io-index" 1686 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1687 + 1688 + [[package]] 1689 + name = "windows_i686_msvc" 1690 + version = "0.53.1" 1691 + source = "registry+https://github.com/rust-lang/crates.io-index" 1692 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1693 + 1694 + [[package]] 1695 + name = "windows_x86_64_gnu" 1696 + version = "0.52.6" 1697 + source = "registry+https://github.com/rust-lang/crates.io-index" 1698 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1699 + 1700 + [[package]] 1701 + name = "windows_x86_64_gnu" 1702 + version = "0.53.1" 1703 + source = "registry+https://github.com/rust-lang/crates.io-index" 1704 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1705 + 1706 + [[package]] 1707 + name = "windows_x86_64_gnullvm" 1708 + version = "0.52.6" 1709 + source = "registry+https://github.com/rust-lang/crates.io-index" 1710 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1711 + 1712 + [[package]] 1713 + name = "windows_x86_64_gnullvm" 1714 + version = "0.53.1" 1715 + source = "registry+https://github.com/rust-lang/crates.io-index" 1716 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1717 + 1718 + [[package]] 1719 + name = "windows_x86_64_msvc" 1720 + version = "0.52.6" 1721 + source = "registry+https://github.com/rust-lang/crates.io-index" 1722 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1723 + 1724 + [[package]] 1725 + name = "windows_x86_64_msvc" 1726 + version = "0.53.1" 1727 + source = "registry+https://github.com/rust-lang/crates.io-index" 1728 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1729 + 1730 + [[package]] 1731 + name = "winsafe" 1732 + version = "0.0.19" 1733 + source = "registry+https://github.com/rust-lang/crates.io-index" 1734 + checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 1735 + 1736 + [[package]] 1737 + name = "wit-bindgen" 1738 + version = "0.46.0" 1739 + source = "registry+https://github.com/rust-lang/crates.io-index" 1740 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1741 + 1742 + [[package]] 1743 + name = "writeable" 1744 + version = "0.6.2" 1745 + source = "registry+https://github.com/rust-lang/crates.io-index" 1746 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 1747 + 1748 + [[package]] 1749 + name = "yoke" 1750 + version = "0.8.1" 1751 + source = "registry+https://github.com/rust-lang/crates.io-index" 1752 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 1753 + dependencies = [ 1754 + "stable_deref_trait", 1755 + "yoke-derive", 1756 + "zerofrom", 1757 + ] 1758 + 1759 + [[package]] 1760 + name = "yoke-derive" 1761 + version = "0.8.1" 1762 + source = "registry+https://github.com/rust-lang/crates.io-index" 1763 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 1764 + dependencies = [ 1765 + "proc-macro2", 1766 + "quote", 1767 + "syn", 1768 + "synstructure", 1769 + ] 1770 + 1771 + [[package]] 1772 + name = "zerocopy" 1773 + version = "0.8.33" 1774 + source = "registry+https://github.com/rust-lang/crates.io-index" 1775 + checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" 1776 + dependencies = [ 1777 + "zerocopy-derive", 1778 + ] 1779 + 1780 + [[package]] 1781 + name = "zerocopy-derive" 1782 + version = "0.8.33" 1783 + source = "registry+https://github.com/rust-lang/crates.io-index" 1784 + checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" 1785 + dependencies = [ 1786 + "proc-macro2", 1787 + "quote", 1788 + "syn", 1789 + ] 1790 + 1791 + [[package]] 1792 + name = "zerofrom" 1793 + version = "0.1.6" 1794 + source = "registry+https://github.com/rust-lang/crates.io-index" 1795 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1796 + dependencies = [ 1797 + "zerofrom-derive", 1798 + ] 1799 + 1800 + [[package]] 1801 + name = "zerofrom-derive" 1802 + version = "0.1.6" 1803 + source = "registry+https://github.com/rust-lang/crates.io-index" 1804 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1805 + dependencies = [ 1806 + "proc-macro2", 1807 + "quote", 1808 + "syn", 1809 + "synstructure", 1810 + ] 1811 + 1812 + [[package]] 1813 + name = "zeroize" 1814 + version = "1.8.2" 1815 + source = "registry+https://github.com/rust-lang/crates.io-index" 1816 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1817 + 1818 + [[package]] 1819 + name = "zerotrie" 1820 + version = "0.2.3" 1821 + source = "registry+https://github.com/rust-lang/crates.io-index" 1822 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 1823 + dependencies = [ 1824 + "displaydoc", 1825 + "yoke", 1826 + "zerofrom", 1827 + ] 1828 + 1829 + [[package]] 1830 + name = "zerovec" 1831 + version = "0.11.5" 1832 + source = "registry+https://github.com/rust-lang/crates.io-index" 1833 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 1834 + dependencies = [ 1835 + "yoke", 1836 + "zerofrom", 1837 + "zerovec-derive", 1838 + ] 1839 + 1840 + [[package]] 1841 + name = "zerovec-derive" 1842 + version = "0.11.2" 1843 + source = "registry+https://github.com/rust-lang/crates.io-index" 1844 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 1845 + dependencies = [ 1846 + "proc-macro2", 1847 + "quote", 1848 + "syn", 1849 + ] 1850 + 1851 + [[package]] 1852 + name = "zmij" 1853 + version = "1.0.14" 1854 + source = "registry+https://github.com/rust-lang/crates.io-index" 1855 + checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
+29
Cargo.toml
··· 1 + [package] 2 + name = "tapped" 3 + version = "0.1.0" 4 + edition = "2021" 5 + description = "Rust wrapper for the tap ATProto utility" 6 + license = "MIT" 7 + readme = "README.md" 8 + authors = ["Thomas Karpiniec <tom.karpiniec@outlook.com>"] 9 + keywords = ["atproto", "tap"] 10 + repository = "https://github.com/thombles/tapped" 11 + 12 + [dependencies] 13 + tokio = { version = "1", features = ["net", "process", "rt", "sync", "time"] } 14 + reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } 15 + tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] } 16 + serde = { version = "1", features = ["derive"] } 17 + serde_json = "1" 18 + thiserror = "2" 19 + url = { version = "2", features = ["serde"] } 20 + futures-util = "0.3" 21 + tracing = "0.1" 22 + base64 = "0.22" 23 + libc = "0.2" 24 + 25 + [dev-dependencies] 26 + tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 27 + which = "7" 28 + env_logger = "0.11" 29 + log = "0.4"
+254
README.md
··· 1 + # tapped 2 + 3 + A Rust wrapper library for the [`tap`](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) ATProto sync utility. 4 + 5 + `tapped` provides an idiomatic async Rust interface for spawning and communicating with a `tap` subprocess, making it easy to build applications that sync data from the ATProto network. 6 + 7 + ## Features 8 + 9 + - Spawn and manage `tap` subprocesses with graceful shutdown 10 + - Strongly-typed configuration for all tap envvars 11 + - Strongly-typed async Rust functions covering all of tap's HTTP API endpoints 12 + - WebSocket-based event channel with automatic acknowledgment 13 + 14 + ## Installation 15 + 16 + Add to your `Cargo.toml`: 17 + 18 + ```toml 19 + [dependencies] 20 + tapped = "0.1" 21 + ``` 22 + 23 + You'll also need the `tap` binary. Build it from the [indigo repository](https://github.com/bluesky-social/indigo): 24 + 25 + ```bash 26 + cd cmd/tap && go build 27 + ``` 28 + 29 + `tapped` has been most recently tested against: 30 + 31 + ``` 32 + tap version v0.0.0-20260114211028-207c9d49d0de-rev-207c9d4 33 + ``` 34 + 35 + ## Quick Start 36 + 37 + ```rust 38 + use tapped::{TapHandle, TapConfig, Event}; 39 + 40 + #[tokio::main] 41 + async fn main() -> tapped::Result<()> { 42 + let config = TapConfig::builder() 43 + .database_url("sqlite://tap.db") 44 + .collection_filter("app.bsky.feed.post") 45 + .build(); 46 + 47 + // Spawn tap and connect 48 + let handle = TapHandle::spawn_default(config).await?; 49 + 50 + // Subscribe to events 51 + let mut channel = handle.channel().await?; 52 + 53 + while let Ok(received) = channel.recv().await { 54 + match &received.event { 55 + Event::Record(record) => { 56 + println!("[{}] {}/{}", 57 + record.action, 58 + record.collection, 59 + record.rkey 60 + ); 61 + } 62 + Event::Identity(identity) => { 63 + println!("Identity: {} -> {}", identity.did, identity.handle); 64 + } 65 + } 66 + // Event is auto-acknowledged when `received` is dropped 67 + } 68 + 69 + Ok(()) 70 + } 71 + ``` 72 + 73 + ## Usage Patterns 74 + 75 + ### Connect to Existing Instance 76 + 77 + If you have a tap instance already running: 78 + 79 + ```rust 80 + use tapped::TapClient; 81 + 82 + let client = TapClient::new("http://localhost:2480")?; 83 + client.health().await?; 84 + ``` 85 + 86 + ### Spawn with Custom Binary Path 87 + 88 + ```rust 89 + use tapped::{TapProcess, TapConfig}; 90 + 91 + let config = TapConfig::builder() 92 + .database_url("sqlite://my-app.db") 93 + .build(); 94 + 95 + let mut process = TapProcess::spawn("/path/to/tap", config).await?; 96 + let client = process.client(); 97 + 98 + // Use the client... 99 + 100 + process.shutdown().await?; 101 + ``` 102 + 103 + ### Using TapHandle (Recommended) 104 + 105 + `TapHandle` combines process management and client access: 106 + 107 + ```rust 108 + use tapped::{TapHandle, TapConfig}; 109 + 110 + let config = TapConfig::builder() 111 + .database_url("sqlite://app.db") 112 + .full_network(false) 113 + .build(); 114 + 115 + let handle = TapHandle::spawn_default(config).await?; 116 + 117 + // TapHandle derefs to TapClient, so you can call client methods directly 118 + handle.health().await?; 119 + let count = handle.repo_count().await?; 120 + println!("Tracking {} repos", count); 121 + ``` 122 + 123 + ### Configuration Options 124 + 125 + ```rust 126 + use tapped::{TapConfig, LogLevel}; 127 + use std::time::Duration; 128 + 129 + let config = TapConfig::builder() 130 + // Database 131 + .database_url("sqlite://tap.db") 132 + .max_db_conns(10) 133 + 134 + // Network 135 + .bind("127.0.0.1:2480") 136 + .relay_url("wss://bsky.network") 137 + .plc_url("https://plc.directory") 138 + 139 + // Filtering 140 + .signal_collection("app.bsky.feed.post") 141 + .collection_filter("app.bsky.feed.post") 142 + .collection_filter("app.bsky.feed.like") 143 + .full_network(false) 144 + 145 + // Performance 146 + .firehose_parallelism(10) 147 + .resync_parallelism(5) 148 + .outbox_parallelism(10) 149 + .outbox_capacity(10000) 150 + 151 + // Timeouts 152 + .repo_fetch_timeout(Duration::from_secs(30)) 153 + .startup_timeout(Duration::from_secs(60)) 154 + .shutdown_timeout(Duration::from_secs(10)) 155 + 156 + // Logging 157 + .log_level(LogLevel::Info) 158 + 159 + .build(); 160 + ``` 161 + 162 + ### Working with Events 163 + 164 + Events are automatically acknowledged when dropped: 165 + 166 + ```rust 167 + use tapped::{Event, RecordAction}; 168 + 169 + let mut channel = client.channel().await?; 170 + 171 + while let Ok(received) = channel.recv().await { 172 + match &received.event { 173 + Event::Record(record) => { 174 + match record.action { 175 + RecordAction::Create => { 176 + if let Some(ref rec) = record.record { 177 + // Access the raw JSON 178 + println!("Type: {:?}", rec.record_type()); 179 + 180 + // Or deserialize to a specific type 181 + // let post: MyPostType = rec.deserialize_as()?; 182 + } 183 + } 184 + RecordAction::Update => { /* ... */ } 185 + RecordAction::Delete => { /* ... */ } 186 + _ => {} 187 + } 188 + } 189 + Event::Identity(identity) => { 190 + println!("{} is now @{}", identity.did, identity.handle); 191 + } 192 + } 193 + // Ack sent automatically here when `received` goes out of scope 194 + } 195 + ``` 196 + 197 + ### Managing Repositories 198 + 199 + ```rust 200 + // Add repos to track 201 + client.add_repos(&["did:plc:abc123", "did:plc:def456"]).await?; 202 + 203 + // Remove repos 204 + client.remove_repos(&["did:plc:abc123"]).await?; 205 + 206 + // Get info about a specific repo 207 + let info = client.repo_info("did:plc:def456").await?; 208 + println!("State: {:?}, Records: {}", info.state, info.records); 209 + 210 + // Resolve a DID to its document 211 + let doc = client.resolve_did("did:plc:def456").await?; 212 + println!("Handles: {:?}", doc.also_known_as); 213 + ``` 214 + 215 + ### Checking Stats 216 + 217 + ```rust 218 + let repos = client.repo_count().await?; 219 + let records = client.record_count().await?; 220 + let outbox = client.outbox_buffer().await?; 221 + let resync = client.resync_buffer().await?; 222 + let cursors = client.cursors().await?; 223 + 224 + println!("Tracking {} repos with {} records", repos, records); 225 + println!("Outbox buffer: {}, Resync buffer: {}", outbox, resync); 226 + println!("Firehose cursor: {:?}", cursors.firehose); 227 + ``` 228 + 229 + ## Error Handling 230 + 231 + All operations return `tapped::Result<T>`, which uses the `tapped::Error` enum: 232 + 233 + ```rust 234 + use tapped::Error; 235 + 236 + match client.health().await { 237 + Ok(()) => println!("Healthy!"), 238 + Err(Error::Http(e)) => println!("HTTP error: {}", e), 239 + Err(Error::Timeout) => println!("Request timed out"), 240 + Err(e) => println!("Other error: {}", e), 241 + } 242 + ``` 243 + 244 + ## Example: Syncing Standard Site Records 245 + 246 + See [examples/standard_site_sync.rs](examples/standard_site_sync.rs) for a complete example that syncs `site.standard.publication` and `site.standard.document` records to local files. 247 + 248 + ```bash 249 + cargo run --example standard_site_sync 250 + ``` 251 + 252 + ## License 253 + 254 + MIT
+295
examples/standard_site_sync.rs
··· 1 + //! Example: Sync site.standard.publication and site.standard.document records. 2 + //! 3 + //! This example demonstrates using tapped to track publication and document 4 + //! records from the ATProto network. It maintains an on-disk cache and 5 + //! writes URL lists to disk on each change for inspection. 6 + //! 7 + //! Run with: `RUST_LOG=info cargo run --example standard_site_sync` 8 + 9 + use std::collections::HashMap; 10 + use std::fs; 11 + 12 + use log::info; 13 + use serde::{Deserialize, Serialize}; 14 + use tapped::{Event, RecordAction, RecordEvent, TapConfig, TapProcess}; 15 + 16 + /// A site.standard.publication record (subset of fields). 17 + #[derive(Debug, Deserialize)] 18 + struct Publication { 19 + url: String, 20 + name: String, 21 + } 22 + 23 + /// A site.standard.document record (subset of fields). 24 + #[derive(Debug, Deserialize)] 25 + struct Document { 26 + site: String, 27 + title: String, 28 + #[serde(default)] 29 + path: Option<String>, 30 + } 31 + 32 + /// Key for storing records in cache. 33 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 34 + struct RecordKey { 35 + did: String, 36 + collection: String, 37 + rkey: String, 38 + } 39 + 40 + impl RecordKey { 41 + fn new(event: &RecordEvent) -> Self { 42 + Self { 43 + did: event.did.clone(), 44 + collection: event.collection.clone(), 45 + rkey: event.rkey.clone(), 46 + } 47 + } 48 + } 49 + 50 + /// Cached record data. 51 + #[derive(Debug, Clone, Serialize, Deserialize)] 52 + #[serde(tag = "type")] 53 + enum CachedRecord { 54 + Publication { url: String }, 55 + Document { site: String, path: Option<String> }, 56 + } 57 + 58 + fn main() -> Result<(), Box<dyn std::error::Error>> { 59 + tokio::runtime::Builder::new_multi_thread() 60 + .enable_all() 61 + .build()? 62 + .block_on(async_main()) 63 + } 64 + 65 + async fn async_main() -> Result<(), Box<dyn std::error::Error>> { 66 + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) 67 + .format_timestamp_secs() 68 + .init(); 69 + 70 + info!("Starting standard.site sync example"); 71 + 72 + let config = TapConfig::builder() 73 + .database_url("sqlite://./tap-example.db") 74 + .bind(":2480") 75 + .signal_collection("site.standard.publication") 76 + .collection_filters(vec![ 77 + "site.standard.publication".to_string(), 78 + "site.standard.document".to_string(), 79 + ]) 80 + .disable_acks(false) 81 + .inherit_stdio(true) 82 + .build(); 83 + 84 + info!("Spawning tap process..."); 85 + 86 + // Spawn tap (looks for ./tap first, then tap on PATH) 87 + let process = TapProcess::spawn_default(config).await?; 88 + info!("Tap running at {}", process.url()); 89 + 90 + let client = process.client()?; 91 + client.health().await?; 92 + info!("Tap is healthy!"); 93 + 94 + let mut receiver = client.channel().await?; 95 + info!("Connected! Waiting for events..."); 96 + 97 + // In-memory cache - load from disk if available 98 + let mut cache: HashMap<RecordKey, CachedRecord> = load_cache_from_disk(); 99 + let (pub_count, doc_count) = count_cached(&cache); 100 + info!( 101 + "Loaded cache from disk: {} publications, {} documents", 102 + pub_count, doc_count 103 + ); 104 + 105 + let mut live_count = 0u64; 106 + let mut backfill_count = 0u64; 107 + 108 + loop { 109 + match receiver.recv().await { 110 + Ok(received) => { 111 + if let Event::Record(ref record_event) = *received { 112 + // Track live vs backfill 113 + if record_event.live { 114 + live_count += 1; 115 + } else { 116 + backfill_count += 1; 117 + } 118 + 119 + process_record_event(record_event, &mut cache); 120 + write_output_files(&cache)?; 121 + 122 + // Periodically show event source breakdown 123 + if (live_count + backfill_count).is_multiple_of(100) { 124 + info!( 125 + "[Stats] Live events: {}, Backfill events: {}", 126 + live_count, backfill_count 127 + ); 128 + } 129 + } else if let Event::Identity(ref identity_event) = *received { 130 + info!( 131 + "[IDENTITY] {} -> {} (active: {})", 132 + identity_event.did, identity_event.handle, identity_event.is_active 133 + ); 134 + } 135 + // Event is automatically acked when `received` is dropped here 136 + } 137 + Err(e) => { 138 + eprintln!("Error receiving event: {}", e); 139 + break; 140 + } 141 + } 142 + } 143 + 144 + Ok(()) 145 + } 146 + 147 + fn process_record_event(event: &RecordEvent, cache: &mut HashMap<RecordKey, CachedRecord>) { 148 + let key = RecordKey::new(event); 149 + let action_str = match event.action { 150 + RecordAction::Create => "CREATE", 151 + RecordAction::Update => "UPDATE", 152 + RecordAction::Delete => "DELETE", 153 + _ => "UNKNOWN", 154 + }; 155 + 156 + // Show whether this is a live or backfill event 157 + let source = if event.live { "LIVE" } else { "BACKFILL" }; 158 + 159 + info!( 160 + "[{} {}] {} {}/{}", 161 + action_str, source, event.did, event.collection, event.rkey 162 + ); 163 + 164 + match event.action { 165 + RecordAction::Create | RecordAction::Update => { 166 + if let Some(ref record) = event.record { 167 + match event.collection.as_str() { 168 + "site.standard.publication" => match record.deserialize_as::<Publication>() { 169 + Ok(pub_record) => { 170 + info!("Publication: {} ({})", pub_record.name, pub_record.url); 171 + cache.insert( 172 + key, 173 + CachedRecord::Publication { 174 + url: pub_record.url, 175 + }, 176 + ); 177 + } 178 + Err(e) => { 179 + log::error!("Failed to parse publication: {}", e); 180 + } 181 + }, 182 + "site.standard.document" => match record.deserialize_as::<Document>() { 183 + Ok(doc_record) => { 184 + let full_url = match &doc_record.path { 185 + Some(path) if !path.is_empty() => { 186 + format!( 187 + "{}/{}", 188 + doc_record.site.trim_end_matches('/'), 189 + path.trim_start_matches('/') 190 + ) 191 + } 192 + _ => doc_record.site.clone(), 193 + }; 194 + info!("Document: {} ({})", doc_record.title, full_url); 195 + cache.insert( 196 + key, 197 + CachedRecord::Document { 198 + site: doc_record.site, 199 + path: doc_record.path, 200 + }, 201 + ); 202 + } 203 + Err(e) => { 204 + log::error!("Failed to parse document: {}", e); 205 + } 206 + }, 207 + _ => {} 208 + } 209 + } 210 + } 211 + RecordAction::Delete => { 212 + cache.remove(&key); 213 + } 214 + _ => {} 215 + } 216 + 217 + // Log cache stats 218 + let (pub_count, doc_count) = count_cached(cache); 219 + info!( 220 + "Cached: {} publications, {} documents", 221 + pub_count, doc_count 222 + ); 223 + } 224 + 225 + fn count_cached(cache: &HashMap<RecordKey, CachedRecord>) -> (usize, usize) { 226 + let mut publications = 0; 227 + let mut documents = 0; 228 + 229 + for record in cache.values() { 230 + match record { 231 + CachedRecord::Publication { .. } => publications += 1, 232 + CachedRecord::Document { .. } => documents += 1, 233 + } 234 + } 235 + 236 + (publications, documents) 237 + } 238 + 239 + fn write_output_files(cache: &HashMap<RecordKey, CachedRecord>) -> Result<(), std::io::Error> { 240 + let mut publication_urls: Vec<&str> = Vec::new(); 241 + let mut document_urls: Vec<String> = Vec::new(); 242 + 243 + for record in cache.values() { 244 + match record { 245 + CachedRecord::Publication { url } => { 246 + publication_urls.push(url); 247 + } 248 + CachedRecord::Document { site, path } => { 249 + let full_url = match path { 250 + Some(p) if !p.is_empty() => { 251 + format!( 252 + "{}/{}", 253 + site.trim_end_matches('/'), 254 + p.trim_start_matches('/') 255 + ) 256 + } 257 + _ => site.clone(), 258 + }; 259 + document_urls.push(full_url); 260 + } 261 + } 262 + } 263 + 264 + publication_urls.sort(); 265 + document_urls.sort(); 266 + 267 + fs::write("publications.txt", publication_urls.join("\n"))?; 268 + 269 + // Write documents.txt (publication + document URLs combined) 270 + let mut all_urls: Vec<String> = publication_urls.iter().map(|s| s.to_string()).collect(); 271 + all_urls.extend(document_urls); 272 + all_urls.sort(); 273 + all_urls.dedup(); 274 + fs::write("documents.txt", all_urls.join("\n"))?; 275 + 276 + // Write cache.json for persistence 277 + let cache_entries: Vec<(&RecordKey, &CachedRecord)> = cache.iter().collect(); 278 + let cache_json = serde_json::to_string_pretty(&cache_entries).map_err(std::io::Error::other)?; 279 + fs::write("cache.json", cache_json)?; 280 + 281 + Ok(()) 282 + } 283 + 284 + fn load_cache_from_disk() -> HashMap<RecordKey, CachedRecord> { 285 + match fs::read_to_string("cache.json") { 286 + Ok(content) => match serde_json::from_str::<Vec<(RecordKey, CachedRecord)>>(&content) { 287 + Ok(entries) => entries.into_iter().collect(), 288 + Err(e) => { 289 + log::warn!("Failed to parse cache.json: {}", e); 290 + HashMap::new() 291 + } 292 + }, 293 + Err(_) => HashMap::new(), 294 + } 295 + }
+209
src/channel.rs
··· 1 + //! WebSocket event channel and receiver. 2 + 3 + use futures_util::{SinkExt, StreamExt}; 4 + use serde::Serialize; 5 + use tokio::sync::mpsc; 6 + use tokio_tungstenite::{connect_async, tungstenite::Message}; 7 + use url::Url; 8 + 9 + use crate::types::RawEvent; 10 + use crate::{Error, Event, Result}; 11 + 12 + type WsStream = 13 + tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>; 14 + type WsSink = futures_util::stream::SplitSink<WsStream, Message>; 15 + type WsSource = futures_util::stream::SplitStream<WsStream>; 16 + 17 + /// Receiver for events from a tap WebSocket channel. 18 + /// 19 + /// Events are received via the [`recv`](EventReceiver::recv) method. 20 + /// Acknowledgments are sent automatically when events are dropped. 21 + /// 22 + /// This type does not implement auto-reconnection. If the connection 23 + /// closes, `recv()` will return an error and you must create a new 24 + /// `EventReceiver` via [`TapClient::channel()`](crate::TapClient::channel). 25 + pub struct EventReceiver { 26 + event_rx: mpsc::Receiver<Result<EventWithAck>>, 27 + _ack_tx: mpsc::Sender<u64>, 28 + } 29 + 30 + struct EventWithAck { 31 + event: Event, 32 + ack_tx: mpsc::Sender<u64>, 33 + } 34 + 35 + struct AckGuard { 36 + id: u64, 37 + ack_tx: Option<mpsc::Sender<u64>>, 38 + } 39 + 40 + impl Drop for AckGuard { 41 + fn drop(&mut self) { 42 + if let Some(tx) = self.ack_tx.take() { 43 + // Fire and forget - if the channel is closed, we can't ack anyway 44 + let id = self.id; 45 + tokio::spawn(async move { 46 + let _ = tx.send(id).await; 47 + }); 48 + } 49 + } 50 + } 51 + 52 + /// Wrapper around Event that includes the ack trigger. 53 + pub struct ReceivedEvent { 54 + pub event: Event, 55 + _ack_guard: AckGuard, 56 + } 57 + 58 + impl std::ops::Deref for ReceivedEvent { 59 + type Target = Event; 60 + 61 + fn deref(&self) -> &Self::Target { 62 + &self.event 63 + } 64 + } 65 + 66 + impl std::fmt::Debug for ReceivedEvent { 67 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 + self.event.fmt(f) 69 + } 70 + } 71 + 72 + impl EventReceiver { 73 + /// Connect to a tap WebSocket channel. 74 + pub(crate) async fn connect(base_url: &Url, admin_password: Option<&str>) -> Result<Self> { 75 + let mut ws_url = base_url.clone(); 76 + match ws_url.scheme() { 77 + "http" => ws_url.set_scheme("ws").unwrap(), 78 + "https" => ws_url.set_scheme("wss").unwrap(), 79 + _ => {} 80 + } 81 + ws_url.set_path("/channel"); 82 + 83 + if let Some(password) = admin_password { 84 + ws_url 85 + .set_username("admin") 86 + .map_err(|_| Error::InvalidUrl("cannot set username".into()))?; 87 + ws_url 88 + .set_password(Some(password)) 89 + .map_err(|_| Error::InvalidUrl("cannot set password".into()))?; 90 + } 91 + 92 + let (ws_stream, response) = connect_async(ws_url.as_str()) 93 + .await 94 + .map_err(|e| Error::WebSocket(Box::new(e)))?; 95 + 96 + if response.status().as_u16() == 400 { 97 + return Err(Error::WebhookModeActive); 98 + } 99 + 100 + let (write, read) = ws_stream.split(); 101 + 102 + let (event_tx, event_rx) = mpsc::channel::<Result<EventWithAck>>(100); 103 + let (ack_tx, ack_rx) = mpsc::channel::<u64>(1000); 104 + 105 + let ack_tx_clone = ack_tx.clone(); 106 + tokio::spawn(async move { 107 + Self::writer_task(write, ack_rx).await; 108 + }); 109 + 110 + tokio::spawn(async move { 111 + Self::reader_task(read, event_tx, ack_tx_clone).await; 112 + }); 113 + 114 + Ok(Self { 115 + event_rx, 116 + _ack_tx: ack_tx, 117 + }) 118 + } 119 + 120 + /// Receive the next event. 121 + /// 122 + /// Returns the event wrapped in a [`ReceivedEvent`] that automatically 123 + /// sends an acknowledgment when dropped. 124 + /// 125 + /// # Errors 126 + /// 127 + /// Returns [`Error::ChannelClosed`] if the WebSocket connection closes. 128 + pub async fn recv(&mut self) -> Result<ReceivedEvent> { 129 + match self.event_rx.recv().await { 130 + Some(Ok(event_with_ack)) => { 131 + let id = event_with_ack.event.id(); 132 + Ok(ReceivedEvent { 133 + event: event_with_ack.event, 134 + _ack_guard: AckGuard { 135 + id, 136 + ack_tx: Some(event_with_ack.ack_tx), 137 + }, 138 + }) 139 + } 140 + Some(Err(e)) => Err(e), 141 + None => Err(Error::ChannelClosed), 142 + } 143 + } 144 + 145 + /// Writer task: sends ack messages to the WebSocket. 146 + async fn writer_task(mut write: WsSink, mut ack_rx: mpsc::Receiver<u64>) { 147 + #[derive(Serialize)] 148 + struct AckMessage { 149 + #[serde(rename = "type")] 150 + type_: &'static str, 151 + id: u64, 152 + } 153 + 154 + while let Some(id) = ack_rx.recv().await { 155 + let msg = AckMessage { type_: "ack", id }; 156 + let json = match serde_json::to_string(&msg) { 157 + Ok(j) => j, 158 + Err(e) => { 159 + tracing::warn!("Failed to serialize ack: {}", e); 160 + continue; 161 + } 162 + }; 163 + 164 + if let Err(e) = write.send(Message::Text(json.into())).await { 165 + tracing::warn!("Failed to send ack: {}", e); 166 + break; 167 + } 168 + } 169 + } 170 + 171 + /// Reader task: reads events from WebSocket and sends to channel. 172 + async fn reader_task( 173 + mut read: WsSource, 174 + event_tx: mpsc::Sender<Result<EventWithAck>>, 175 + ack_tx: mpsc::Sender<u64>, 176 + ) { 177 + while let Some(msg_result) = read.next().await { 178 + match msg_result { 179 + Ok(Message::Text(text)) => match serde_json::from_str::<RawEvent>(&text) { 180 + Ok(raw) => { 181 + if let Some(event) = raw.into_event() { 182 + let event_with_ack = EventWithAck { 183 + event, 184 + ack_tx: ack_tx.clone(), 185 + }; 186 + if event_tx.send(Ok(event_with_ack)).await.is_err() { 187 + break; 188 + } 189 + } 190 + } 191 + Err(e) => { 192 + tracing::warn!("Failed to parse event: {}", e); 193 + } 194 + }, 195 + Ok(Message::Close(_)) => { 196 + let _ = event_tx.send(Err(Error::ChannelClosed)).await; 197 + break; 198 + } 199 + Ok(_) => { 200 + // Ignore ping/pong/binary 201 + } 202 + Err(e) => { 203 + let _ = event_tx.send(Err(Error::WebSocket(Box::new(e)))).await; 204 + break; 205 + } 206 + } 207 + } 208 + } 209 + }
+362
src/client.rs
··· 1 + //! HTTP client for tap API. 2 + 3 + use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; 4 + use reqwest::Response; 5 + use serde::de::DeserializeOwned; 6 + use serde::Serialize; 7 + use url::Url; 8 + 9 + use crate::channel::EventReceiver; 10 + use crate::types::{ 11 + ApiError, Cursors, DidDocument, OutboxBufferResponse, RecordCountResponse, RepoCountResponse, 12 + RepoInfo, ResyncBufferResponse, 13 + }; 14 + use crate::{Error, Result, TapConfig}; 15 + 16 + /// HTTP client for interacting with a tap instance. 17 + /// 18 + /// Provides methods for all tap HTTP endpoints. The client is cheap to clone 19 + /// and can be shared across tasks. 20 + #[derive(Debug, Clone)] 21 + pub struct TapClient { 22 + client: reqwest::Client, 23 + base_url: Url, 24 + admin_password: Option<String>, 25 + } 26 + 27 + impl TapClient { 28 + /// Create a new client connecting to the given URL. 29 + /// 30 + /// # Example 31 + /// 32 + /// ```no_run 33 + /// use tapped::TapClient; 34 + /// 35 + /// let client = TapClient::new("http://localhost:2480")?; 36 + /// # Ok::<(), tapped::Error>(()) 37 + /// ``` 38 + pub fn new(url: impl AsRef<str>) -> Result<Self> { 39 + Self::with_config(url, &TapConfig::default()) 40 + } 41 + 42 + /// Create a new client with Basic auth. 43 + /// 44 + /// # Example 45 + /// 46 + /// ```no_run 47 + /// use tapped::TapClient; 48 + /// 49 + /// let client = TapClient::with_auth("http://localhost:2480", "secret")?; 50 + /// # Ok::<(), tapped::Error>(()) 51 + /// ``` 52 + pub fn with_auth(url: impl AsRef<str>, password: impl Into<String>) -> Result<Self> { 53 + let config = TapConfig::builder().admin_password(password.into()).build(); 54 + Self::with_config(url, &config) 55 + } 56 + 57 + /// Create a new client with the given configuration. 58 + pub fn with_config(url: impl AsRef<str>, config: &TapConfig) -> Result<Self> { 59 + let base_url: Url = url 60 + .as_ref() 61 + .parse() 62 + .map_err(|_| Error::InvalidUrl(url.as_ref().to_string()))?; 63 + 64 + let timeout = config.request_timeout(); 65 + let client = reqwest::Client::builder() 66 + .timeout(timeout) 67 + .build() 68 + .map_err(Error::Http)?; 69 + 70 + Ok(Self { 71 + client, 72 + base_url, 73 + admin_password: config.admin_password.clone(), 74 + }) 75 + } 76 + 77 + /// Get the base URL of the tap instance. 78 + pub fn url(&self) -> &Url { 79 + &self.base_url 80 + } 81 + 82 + /// Build authorization headers if password is set. 83 + fn auth_headers(&self) -> HeaderMap { 84 + let mut headers = HeaderMap::new(); 85 + if let Some(ref password) = self.admin_password { 86 + use base64::Engine; 87 + let credentials = format!("admin:{}", password); 88 + let encoded = base64::engine::general_purpose::STANDARD.encode(credentials); 89 + if let Ok(value) = HeaderValue::from_str(&format!("Basic {}", encoded)) { 90 + headers.insert(AUTHORIZATION, value); 91 + } 92 + } 93 + headers 94 + } 95 + 96 + /// Handle a response, returning an error for non-success status codes. 97 + async fn handle_response<T: DeserializeOwned>(resp: Response) -> Result<T> { 98 + if resp.status().is_success() { 99 + Ok(resp.json().await?) 100 + } else { 101 + Err(Self::error_from_response(resp).await) 102 + } 103 + } 104 + 105 + /// Handle a response that returns no body on success. 106 + async fn handle_empty_response(resp: Response) -> Result<()> { 107 + if resp.status().is_success() { 108 + let _ = resp.bytes().await; 109 + Ok(()) 110 + } else { 111 + Err(Self::error_from_response(resp).await) 112 + } 113 + } 114 + 115 + /// Extract an error from a failed response. 116 + async fn error_from_response(resp: Response) -> Error { 117 + let status = resp.status().as_u16(); 118 + let message = resp 119 + .json::<ApiError>() 120 + .await 121 + .map(|e| e.message) 122 + .unwrap_or_else(|_| "Unknown error".into()); 123 + Error::Api { status, message } 124 + } 125 + 126 + /// Check if the tap instance is healthy. 127 + /// 128 + /// # Example 129 + /// 130 + /// ```no_run 131 + /// # async fn example() -> tapped::Result<()> { 132 + /// use tapped::TapClient; 133 + /// 134 + /// let client = TapClient::new("http://localhost:2480")?; 135 + /// client.health().await?; 136 + /// println!("Tap is healthy!"); 137 + /// # Ok(()) 138 + /// # } 139 + /// ``` 140 + pub async fn health(&self) -> Result<()> { 141 + let url = self.base_url.join("/health")?; 142 + let resp = self 143 + .client 144 + .get(url) 145 + .headers(self.auth_headers()) 146 + .send() 147 + .await?; 148 + Self::handle_empty_response(resp).await 149 + } 150 + 151 + /// Add DIDs to track. 152 + /// 153 + /// Triggers backfill for newly added repos. 154 + /// 155 + /// # Example 156 + /// 157 + /// ```no_run 158 + /// # async fn example() -> tapped::Result<()> { 159 + /// use tapped::TapClient; 160 + /// 161 + /// let client = TapClient::new("http://localhost:2480")?; 162 + /// client.add_repos(&["did:plc:example1234567890abc"]).await?; 163 + /// # Ok(()) 164 + /// # } 165 + /// ``` 166 + pub async fn add_repos(&self, dids: &[impl AsRef<str>]) -> Result<()> { 167 + #[derive(Serialize)] 168 + struct Payload { 169 + dids: Vec<String>, 170 + } 171 + 172 + let payload = Payload { 173 + dids: dids.iter().map(|d| d.as_ref().to_string()).collect(), 174 + }; 175 + 176 + let url = self.base_url.join("/repos/add")?; 177 + let resp = self 178 + .client 179 + .post(url) 180 + .headers(self.auth_headers()) 181 + .json(&payload) 182 + .send() 183 + .await?; 184 + Self::handle_empty_response(resp).await 185 + } 186 + 187 + /// Remove DIDs from tracking. 188 + /// 189 + /// Stops sync and deletes tracked repo metadata. Does not delete buffered 190 + /// events in the outbox. 191 + /// 192 + /// # Example 193 + /// 194 + /// ```no_run 195 + /// # async fn example() -> tapped::Result<()> { 196 + /// use tapped::TapClient; 197 + /// 198 + /// let client = TapClient::new("http://localhost:2480")?; 199 + /// client.remove_repos(&["did:plc:example1234567890abc"]).await?; 200 + /// # Ok(()) 201 + /// # } 202 + /// ``` 203 + pub async fn remove_repos(&self, dids: &[impl AsRef<str>]) -> Result<()> { 204 + #[derive(Serialize)] 205 + struct Payload { 206 + dids: Vec<String>, 207 + } 208 + 209 + let payload = Payload { 210 + dids: dids.iter().map(|d| d.as_ref().to_string()).collect(), 211 + }; 212 + 213 + let url = self.base_url.join("/repos/remove")?; 214 + let resp = self 215 + .client 216 + .post(url) 217 + .headers(self.auth_headers()) 218 + .json(&payload) 219 + .send() 220 + .await?; 221 + Self::handle_empty_response(resp).await 222 + } 223 + 224 + /// Resolve a DID to its DID document. 225 + /// 226 + /// # Example 227 + /// 228 + /// ```no_run 229 + /// # async fn example() -> tapped::Result<()> { 230 + /// use tapped::TapClient; 231 + /// 232 + /// let client = TapClient::new("http://localhost:2480")?; 233 + /// let doc = client.resolve_did("did:plc:example1234567890abc").await?; 234 + /// println!("Handle: {:?}", doc.also_known_as); 235 + /// # Ok(()) 236 + /// # } 237 + /// ``` 238 + pub async fn resolve_did(&self, did: &str) -> Result<DidDocument> { 239 + let url = self.base_url.join(&format!("/resolve/{}", did))?; 240 + let resp = self 241 + .client 242 + .get(url) 243 + .headers(self.auth_headers()) 244 + .send() 245 + .await?; 246 + Self::handle_response(resp).await 247 + } 248 + 249 + /// Get information about a tracked repository. 250 + /// 251 + /// # Example 252 + /// 253 + /// ```no_run 254 + /// # async fn example() -> tapped::Result<()> { 255 + /// use tapped::TapClient; 256 + /// 257 + /// let client = TapClient::new("http://localhost:2480")?; 258 + /// let info = client.repo_info("did:plc:example1234567890abc").await?; 259 + /// println!("State: {:?}, Records: {}", info.state, info.records); 260 + /// # Ok(()) 261 + /// # } 262 + /// ``` 263 + pub async fn repo_info(&self, did: &str) -> Result<RepoInfo> { 264 + let url = self.base_url.join(&format!("/info/{}", did))?; 265 + let resp = self 266 + .client 267 + .get(url) 268 + .headers(self.auth_headers()) 269 + .send() 270 + .await?; 271 + Self::handle_response(resp).await 272 + } 273 + 274 + /// Get the total number of tracked repositories. 275 + pub async fn repo_count(&self) -> Result<u64> { 276 + let url = self.base_url.join("/stats/repo-count")?; 277 + let resp = self 278 + .client 279 + .get(url) 280 + .headers(self.auth_headers()) 281 + .send() 282 + .await?; 283 + let data: RepoCountResponse = Self::handle_response(resp).await?; 284 + Ok(data.repo_count) 285 + } 286 + 287 + /// Get the total number of tracked records. 288 + pub async fn record_count(&self) -> Result<u64> { 289 + let url = self.base_url.join("/stats/record-count")?; 290 + let resp = self 291 + .client 292 + .get(url) 293 + .headers(self.auth_headers()) 294 + .send() 295 + .await?; 296 + let data: RecordCountResponse = Self::handle_response(resp).await?; 297 + Ok(data.record_count) 298 + } 299 + 300 + /// Get the number of events in the outbox buffer. 301 + pub async fn outbox_buffer(&self) -> Result<u64> { 302 + let url = self.base_url.join("/stats/outbox-buffer")?; 303 + let resp = self 304 + .client 305 + .get(url) 306 + .headers(self.auth_headers()) 307 + .send() 308 + .await?; 309 + let data: OutboxBufferResponse = Self::handle_response(resp).await?; 310 + Ok(data.outbox_buffer) 311 + } 312 + 313 + /// Get the number of events in the resync buffer. 314 + pub async fn resync_buffer(&self) -> Result<u64> { 315 + let url = self.base_url.join("/stats/resync-buffer")?; 316 + let resp = self 317 + .client 318 + .get(url) 319 + .headers(self.auth_headers()) 320 + .send() 321 + .await?; 322 + let data: ResyncBufferResponse = Self::handle_response(resp).await?; 323 + Ok(data.resync_buffer) 324 + } 325 + 326 + /// Get the current cursor positions. 327 + pub async fn cursors(&self) -> Result<Cursors> { 328 + let url = self.base_url.join("/stats/cursors")?; 329 + let resp = self 330 + .client 331 + .get(url) 332 + .headers(self.auth_headers()) 333 + .send() 334 + .await?; 335 + Self::handle_response(resp).await 336 + } 337 + 338 + /// Connect to the WebSocket event channel. 339 + /// 340 + /// Returns an [`EventReceiver`] for receiving events. Events are 341 + /// automatically acknowledged when dropped. 342 + /// 343 + /// # Example 344 + /// 345 + /// ```no_run 346 + /// # async fn example() -> tapped::Result<()> { 347 + /// use tapped::TapClient; 348 + /// 349 + /// let client = TapClient::new("http://localhost:2480")?; 350 + /// let mut receiver = client.channel().await?; 351 + /// 352 + /// while let Ok(event) = receiver.recv().await { 353 + /// println!("Event: {:?}", event); 354 + /// // Event is automatically acknowledged when dropped 355 + /// } 356 + /// # Ok(()) 357 + /// # } 358 + /// ``` 359 + pub async fn channel(&self) -> Result<EventReceiver> { 360 + EventReceiver::connect(&self.base_url, self.admin_password.as_deref()).await 361 + } 362 + }
+444
src/config.rs
··· 1 + //! Configuration types for tap process and client. 2 + 3 + use std::time::Duration; 4 + use url::Url; 5 + 6 + /// Log level for tap process. 7 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 8 + #[non_exhaustive] 9 + pub enum LogLevel { 10 + Debug, 11 + #[default] 12 + Info, 13 + Warn, 14 + Error, 15 + } 16 + 17 + impl LogLevel { 18 + /// Convert to the string value expected by tap. 19 + pub fn as_str(&self) -> &'static str { 20 + match self { 21 + LogLevel::Debug => "debug", 22 + LogLevel::Info => "info", 23 + LogLevel::Warn => "warn", 24 + LogLevel::Error => "error", 25 + } 26 + } 27 + } 28 + 29 + /// Configuration for a tap instance. 30 + /// 31 + /// All fields are optional. When spawning a tap process, unset fields will use 32 + /// tap's built-in defaults. When connecting to an existing instance, only 33 + /// client-side options (timeouts, auth) are relevant. 34 + /// 35 + /// Use [`TapConfig::builder()`] for ergonomic construction. 36 + #[derive(Debug, Clone, Default)] 37 + pub struct TapConfig { 38 + // Database 39 + /// Database connection string (sqlite://path or postgres://...) 40 + pub database_url: Option<String>, 41 + /// Maximum number of database connections 42 + pub max_db_conns: Option<u32>, 43 + 44 + // Server 45 + /// HTTP server bind address (e.g., ":2480", "127.0.0.1:2480", or "[::1]:2480") 46 + pub bind: Option<String>, 47 + /// Basic auth admin password for all requests 48 + pub admin_password: Option<String>, 49 + /// Address for metrics/pprof server 50 + pub metrics_listen: Option<String>, 51 + /// Log verbosity level 52 + pub log_level: Option<LogLevel>, 53 + 54 + // AT Protocol 55 + /// PLC directory URL 56 + pub plc_url: Option<Url>, 57 + /// AT Protocol relay URL 58 + pub relay_url: Option<Url>, 59 + 60 + // Processing 61 + /// Number of parallel firehose event processors 62 + pub firehose_parallelism: Option<u32>, 63 + /// Number of parallel resync workers 64 + pub resync_parallelism: Option<u32>, 65 + /// Number of parallel outbox workers 66 + pub outbox_parallelism: Option<u32>, 67 + /// How often to save firehose cursor 68 + pub cursor_save_interval: Option<Duration>, 69 + /// Timeout for fetching repo CARs from PDS 70 + pub repo_fetch_timeout: Option<Duration>, 71 + /// Size of in-process identity cache 72 + pub ident_cache_size: Option<u32>, 73 + /// Size of outbox before back pressure 74 + pub outbox_capacity: Option<u32>, 75 + /// Timeout before retrying unacked events 76 + pub retry_timeout: Option<Duration>, 77 + 78 + // Network boundary 79 + /// Track all repos on the network 80 + pub full_network: Option<bool>, 81 + /// Track repos with records in this collection 82 + pub signal_collection: Option<String>, 83 + 84 + // Filtering 85 + /// Filter output records by collection (supports wildcards) 86 + pub collection_filters: Option<Vec<String>>, 87 + 88 + // Delivery mode 89 + /// Enable fire-and-forget mode (no client acks) 90 + pub disable_acks: Option<bool>, 91 + /// Webhook URL for event delivery 92 + pub webhook_url: Option<Url>, 93 + /// Run in outbox-only mode 94 + pub outbox_only: Option<bool>, 95 + 96 + // Client-side options (not sent to tap process) 97 + /// Forward tap's stdout/stderr to this process (default: false) 98 + pub inherit_stdio: Option<bool>, 99 + /// Graceful shutdown timeout (default: 5s) 100 + pub shutdown_timeout: Option<Duration>, 101 + /// HTTP request timeout (default: 30s) 102 + pub request_timeout: Option<Duration>, 103 + /// Max wait for tap to become healthy (default: 30s) 104 + pub startup_timeout: Option<Duration>, 105 + } 106 + 107 + impl TapConfig { 108 + /// Create a new empty configuration. 109 + pub fn new() -> Self { 110 + Self::default() 111 + } 112 + 113 + /// Create a builder for ergonomic configuration. 114 + pub fn builder() -> TapConfigBuilder { 115 + TapConfigBuilder::default() 116 + } 117 + 118 + /// Get the shutdown timeout, or the default (5 seconds). 119 + pub fn shutdown_timeout(&self) -> Duration { 120 + self.shutdown_timeout.unwrap_or(Duration::from_secs(5)) 121 + } 122 + 123 + /// Get the request timeout, or the default (30 seconds). 124 + pub fn request_timeout(&self) -> Duration { 125 + self.request_timeout.unwrap_or(Duration::from_secs(30)) 126 + } 127 + 128 + /// Get the startup timeout, or the default (30 seconds). 129 + pub fn startup_timeout(&self) -> Duration { 130 + self.startup_timeout.unwrap_or(Duration::from_secs(30)) 131 + } 132 + 133 + /// Whether to inherit stdio from the parent process (default: false). 134 + pub fn inherit_stdio(&self) -> bool { 135 + self.inherit_stdio.unwrap_or(false) 136 + } 137 + 138 + /// Convert configuration to environment variables for subprocess. 139 + pub fn to_env_vars(&self) -> Vec<(String, String)> { 140 + let mut vars = Vec::new(); 141 + 142 + /// Helper macro to push an env var if the field is Some. 143 + macro_rules! push_env { 144 + ($field:expr, $name:literal, clone) => { 145 + if let Some(ref v) = $field { 146 + vars.push(($name.into(), v.clone())); 147 + } 148 + }; 149 + ($field:expr, $name:literal, string) => { 150 + if let Some(v) = $field { 151 + vars.push(($name.into(), v.to_string())); 152 + } 153 + }; 154 + ($field:expr, $name:literal, ref_string) => { 155 + if let Some(ref v) = $field { 156 + vars.push(($name.into(), v.to_string())); 157 + } 158 + }; 159 + ($field:expr, $name:literal, as_str) => { 160 + if let Some(ref v) = $field { 161 + vars.push(($name.into(), v.as_str().into())); 162 + } 163 + }; 164 + ($field:expr, $name:literal, duration) => { 165 + if let Some(v) = $field { 166 + vars.push(($name.into(), format_duration(v))); 167 + } 168 + }; 169 + } 170 + 171 + push_env!(self.database_url, "TAP_DATABASE_URL", clone); 172 + push_env!(self.max_db_conns, "TAP_MAX_DB_CONNS", string); 173 + push_env!(self.bind, "TAP_BIND", clone); 174 + push_env!(self.admin_password, "TAP_ADMIN_PASSWORD", clone); 175 + push_env!(self.metrics_listen, "TAP_METRICS_LISTEN", clone); 176 + push_env!(self.log_level, "TAP_LOG_LEVEL", as_str); 177 + push_env!(self.plc_url, "TAP_PLC_URL", ref_string); 178 + push_env!(self.relay_url, "TAP_RELAY_URL", ref_string); 179 + push_env!( 180 + self.firehose_parallelism, 181 + "TAP_FIREHOSE_PARALLELISM", 182 + string 183 + ); 184 + push_env!(self.resync_parallelism, "TAP_RESYNC_PARALLELISM", string); 185 + push_env!(self.outbox_parallelism, "TAP_OUTBOX_PARALLELISM", string); 186 + push_env!( 187 + self.cursor_save_interval, 188 + "TAP_CURSOR_SAVE_INTERVAL", 189 + duration 190 + ); 191 + push_env!(self.repo_fetch_timeout, "TAP_REPO_FETCH_TIMEOUT", duration); 192 + push_env!(self.ident_cache_size, "RELAY_IDENT_CACHE_SIZE", string); 193 + push_env!(self.outbox_capacity, "TAP_OUTBOX_CAPACITY", string); 194 + push_env!(self.retry_timeout, "TAP_RETRY_TIMEOUT", duration); 195 + push_env!(self.full_network, "TAP_FULL_NETWORK", string); 196 + push_env!(self.signal_collection, "TAP_SIGNAL_COLLECTION", clone); 197 + push_env!(self.disable_acks, "TAP_DISABLE_ACKS", string); 198 + push_env!(self.webhook_url, "TAP_WEBHOOK_URL", ref_string); 199 + push_env!(self.outbox_only, "TAP_OUTBOX_ONLY", string); 200 + 201 + if let Some(ref v) = self.collection_filters { 202 + vars.push(("TAP_COLLECTION_FILTERS".into(), v.join(","))); 203 + } 204 + 205 + vars 206 + } 207 + } 208 + 209 + /// Format a Duration as a Go-style duration string (e.g., "30s", "5m"). 210 + fn format_duration(d: Duration) -> String { 211 + let secs = d.as_secs(); 212 + let millis = d.subsec_millis(); 213 + 214 + if millis == 0 { 215 + if secs > 0 && secs.is_multiple_of(3600) { 216 + format!("{}h", secs / 3600) 217 + } else if secs > 0 && secs.is_multiple_of(60) { 218 + format!("{}m", secs / 60) 219 + } else { 220 + format!("{}s", secs) 221 + } 222 + } else { 223 + format!("{}ms", d.as_millis()) 224 + } 225 + } 226 + 227 + /// Builder for [`TapConfig`]. 228 + #[derive(Debug, Clone, Default)] 229 + pub struct TapConfigBuilder { 230 + config: TapConfig, 231 + } 232 + 233 + impl TapConfigBuilder { 234 + /// Set the database URL. 235 + pub fn database_url(mut self, url: impl Into<String>) -> Self { 236 + self.config.database_url = Some(url.into()); 237 + self 238 + } 239 + 240 + /// Set the maximum number of database connections. 241 + pub fn max_db_conns(mut self, n: u32) -> Self { 242 + self.config.max_db_conns = Some(n); 243 + self 244 + } 245 + 246 + /// Set the HTTP server bind address. 247 + pub fn bind(mut self, addr: impl Into<String>) -> Self { 248 + self.config.bind = Some(addr.into()); 249 + self 250 + } 251 + 252 + /// Set the admin password for Basic auth. 253 + pub fn admin_password(mut self, password: impl Into<String>) -> Self { 254 + self.config.admin_password = Some(password.into()); 255 + self 256 + } 257 + 258 + /// Set the metrics server listen address. 259 + pub fn metrics_listen(mut self, addr: impl Into<String>) -> Self { 260 + self.config.metrics_listen = Some(addr.into()); 261 + self 262 + } 263 + 264 + /// Set the log level. 265 + pub fn log_level(mut self, level: LogLevel) -> Self { 266 + self.config.log_level = Some(level); 267 + self 268 + } 269 + 270 + /// Set the PLC directory URL. 271 + pub fn plc_url(mut self, url: Url) -> Self { 272 + self.config.plc_url = Some(url); 273 + self 274 + } 275 + 276 + /// Set the relay URL. 277 + pub fn relay_url(mut self, url: Url) -> Self { 278 + self.config.relay_url = Some(url); 279 + self 280 + } 281 + 282 + /// Set the firehose parallelism. 283 + pub fn firehose_parallelism(mut self, n: u32) -> Self { 284 + self.config.firehose_parallelism = Some(n); 285 + self 286 + } 287 + 288 + /// Set the resync parallelism. 289 + pub fn resync_parallelism(mut self, n: u32) -> Self { 290 + self.config.resync_parallelism = Some(n); 291 + self 292 + } 293 + 294 + /// Set the outbox parallelism. 295 + pub fn outbox_parallelism(mut self, n: u32) -> Self { 296 + self.config.outbox_parallelism = Some(n); 297 + self 298 + } 299 + 300 + /// Set how often to save the firehose cursor. 301 + pub fn cursor_save_interval(mut self, d: Duration) -> Self { 302 + self.config.cursor_save_interval = Some(d); 303 + self 304 + } 305 + 306 + /// Set the repo fetch timeout. 307 + pub fn repo_fetch_timeout(mut self, d: Duration) -> Self { 308 + self.config.repo_fetch_timeout = Some(d); 309 + self 310 + } 311 + 312 + /// Set the identity cache size. 313 + pub fn ident_cache_size(mut self, n: u32) -> Self { 314 + self.config.ident_cache_size = Some(n); 315 + self 316 + } 317 + 318 + /// Set the outbox capacity. 319 + pub fn outbox_capacity(mut self, n: u32) -> Self { 320 + self.config.outbox_capacity = Some(n); 321 + self 322 + } 323 + 324 + /// Set the retry timeout for unacked events. 325 + pub fn retry_timeout(mut self, d: Duration) -> Self { 326 + self.config.retry_timeout = Some(d); 327 + self 328 + } 329 + 330 + /// Enable full network mode. 331 + pub fn full_network(mut self, enabled: bool) -> Self { 332 + self.config.full_network = Some(enabled); 333 + self 334 + } 335 + 336 + /// Set the signal collection for repo discovery. 337 + pub fn signal_collection(mut self, collection: impl Into<String>) -> Self { 338 + self.config.signal_collection = Some(collection.into()); 339 + self 340 + } 341 + 342 + /// Add a collection filter. 343 + /// 344 + /// This can be called multiple times to add multiple filters. 345 + /// Supports wildcards (e.g., "app.bsky.feed.*"). 346 + pub fn collection_filter(mut self, filter: impl Into<String>) -> Self { 347 + self.config 348 + .collection_filters 349 + .get_or_insert_with(Vec::new) 350 + .push(filter.into()); 351 + self 352 + } 353 + 354 + /// Set collection filters. 355 + pub fn collection_filters(mut self, filters: Vec<String>) -> Self { 356 + self.config.collection_filters = Some(filters); 357 + self 358 + } 359 + 360 + /// Disable acknowledgments (fire-and-forget mode). 361 + pub fn disable_acks(mut self, disabled: bool) -> Self { 362 + self.config.disable_acks = Some(disabled); 363 + self 364 + } 365 + 366 + /// Set the webhook URL for event delivery. 367 + pub fn webhook_url(mut self, url: Url) -> Self { 368 + self.config.webhook_url = Some(url); 369 + self 370 + } 371 + 372 + /// Enable outbox-only mode. 373 + pub fn outbox_only(mut self, enabled: bool) -> Self { 374 + self.config.outbox_only = Some(enabled); 375 + self 376 + } 377 + 378 + /// Set the graceful shutdown timeout. 379 + pub fn shutdown_timeout(mut self, d: Duration) -> Self { 380 + self.config.shutdown_timeout = Some(d); 381 + self 382 + } 383 + 384 + /// Set the HTTP request timeout. 385 + pub fn request_timeout(mut self, d: Duration) -> Self { 386 + self.config.request_timeout = Some(d); 387 + self 388 + } 389 + 390 + /// Set the startup health check timeout. 391 + pub fn startup_timeout(mut self, d: Duration) -> Self { 392 + self.config.startup_timeout = Some(d); 393 + self 394 + } 395 + 396 + /// Forward tap's stdout/stderr to this process. 397 + /// 398 + /// When enabled, tap's output will be visible in the terminal. 399 + /// When disabled (default), tap's output is discarded. 400 + pub fn inherit_stdio(mut self, inherit: bool) -> Self { 401 + self.config.inherit_stdio = Some(inherit); 402 + self 403 + } 404 + 405 + /// Build the configuration. 406 + pub fn build(self) -> TapConfig { 407 + self.config 408 + } 409 + } 410 + 411 + #[cfg(test)] 412 + mod tests { 413 + use super::*; 414 + 415 + #[test] 416 + fn test_format_duration() { 417 + assert_eq!(format_duration(Duration::from_secs(30)), "30s"); 418 + assert_eq!(format_duration(Duration::from_secs(60)), "1m"); 419 + assert_eq!(format_duration(Duration::from_secs(3600)), "1h"); 420 + assert_eq!(format_duration(Duration::from_secs(90)), "90s"); 421 + assert_eq!(format_duration(Duration::from_millis(500)), "500ms"); 422 + } 423 + 424 + #[test] 425 + fn test_config_to_env_vars() { 426 + let config = TapConfig::builder() 427 + .database_url("sqlite://./test.db") 428 + .bind(":3000") 429 + .signal_collection("app.bsky.feed.post") 430 + .collection_filters(vec!["app.bsky.feed.post".into(), "app.bsky.graph.*".into()]) 431 + .disable_acks(true) 432 + .build(); 433 + 434 + let vars = config.to_env_vars(); 435 + assert!(vars.contains(&("TAP_DATABASE_URL".into(), "sqlite://./test.db".into()))); 436 + assert!(vars.contains(&("TAP_BIND".into(), ":3000".into()))); 437 + assert!(vars.contains(&("TAP_SIGNAL_COLLECTION".into(), "app.bsky.feed.post".into()))); 438 + assert!(vars.contains(&( 439 + "TAP_COLLECTION_FILTERS".into(), 440 + "app.bsky.feed.post,app.bsky.graph.*".into() 441 + ))); 442 + assert!(vars.contains(&("TAP_DISABLE_ACKS".into(), "true".into()))); 443 + } 444 + }
+67
src/error.rs
··· 1 + //! Error types for the tapped crate. 2 + 3 + use std::io; 4 + 5 + /// The error type for tapped operations. 6 + #[derive(Debug, thiserror::Error)] 7 + #[non_exhaustive] 8 + pub enum Error { 9 + /// I/O error (process/file operations) 10 + #[error("I/O error: {0}")] 11 + Io(#[from] io::Error), 12 + 13 + /// HTTP client error 14 + #[error("HTTP error: {0}")] 15 + Http(#[from] reqwest::Error), 16 + 17 + /// WebSocket error 18 + #[error("WebSocket error: {0}")] 19 + WebSocket(#[from] Box<tokio_tungstenite::tungstenite::Error>), 20 + 21 + /// JSON serialisation/deserialisation error 22 + #[error("JSON error: {0}")] 23 + Json(#[from] serde_json::Error), 24 + 25 + /// API error returned by tap server 26 + #[error("API error (status {status}): {message}")] 27 + Api { 28 + /// HTTP status code 29 + status: u16, 30 + /// Error message from server 31 + message: String, 32 + }, 33 + 34 + /// Failed to start the tap process 35 + #[error("Failed to start tap process: {message}")] 36 + ProcessStart { 37 + /// Description of the failure 38 + message: String, 39 + }, 40 + 41 + /// The tap process exited unexpectedly 42 + #[error("Tap process exited with code: {code:?}")] 43 + ProcessExited { 44 + /// Exit code, if available 45 + code: Option<i32>, 46 + }, 47 + 48 + /// The event channel was closed 49 + #[error("Event channel closed")] 50 + ChannelClosed, 51 + 52 + /// WebSocket is not available because tap is in webhook mode 53 + #[error("WebSocket not available: tap is in webhook mode")] 54 + WebhookModeActive, 55 + 56 + /// Invalid URL provided 57 + #[error("Invalid URL: {0}")] 58 + InvalidUrl(String), 59 + 60 + /// Operation timed out 61 + #[error("Operation timed out")] 62 + Timeout, 63 + 64 + /// URL parse error 65 + #[error("URL parse error: {0}")] 66 + UrlParse(#[from] url::ParseError), 67 + }
+120
src/handle.rs
··· 1 + //! Convenience type combining process and client. 2 + 3 + use std::ops::Deref; 4 + use std::path::Path; 5 + 6 + use crate::client::TapClient; 7 + use crate::config::TapConfig; 8 + use crate::process::TapProcess; 9 + use crate::Result; 10 + 11 + /// A convenience type that owns both a tap process and its client. 12 + /// 13 + /// `TapHandle` manages the lifecycle of a tap subprocess and provides 14 + /// access to a [`TapClient`] for interacting with it. When dropped, 15 + /// the process is gracefully shut down. 16 + /// 17 + /// # Example 18 + /// 19 + /// ```no_run 20 + /// use tapped::{TapHandle, TapConfig}; 21 + /// 22 + /// #[tokio::main] 23 + /// async fn main() -> tapped::Result<()> { 24 + /// let config = TapConfig::builder() 25 + /// .database_url("sqlite://tap.db") 26 + /// .build(); 27 + /// 28 + /// // Spawn tap and get a handle 29 + /// let handle = TapHandle::spawn_default(config).await?; 30 + /// 31 + /// // Use the client methods directly on the handle 32 + /// handle.health().await?; 33 + /// 34 + /// let mut channel = handle.channel().await?; 35 + /// while let Ok(event) = channel.recv().await { 36 + /// println!("Event: {:?}", event.event); 37 + /// } 38 + /// 39 + /// Ok(()) 40 + /// } 41 + /// ``` 42 + pub struct TapHandle { 43 + process: TapProcess, 44 + client: TapClient, 45 + } 46 + 47 + impl TapHandle { 48 + /// Spawn a tap process at the given path and create a client for it. 49 + /// 50 + /// This combines [`TapProcess::spawn`] and [`TapClient`] creation into 51 + /// a single convenient call. 52 + /// 53 + /// # Arguments 54 + /// 55 + /// * `path` - Path to the tap binary 56 + /// * `config` - Configuration for the tap instance 57 + /// 58 + /// # Errors 59 + /// 60 + /// Returns an error if the process fails to start or become healthy. 61 + pub async fn spawn(path: impl AsRef<Path>, config: TapConfig) -> Result<Self> { 62 + let process = TapProcess::spawn(path, config).await?; 63 + let client = process.client()?; 64 + Ok(Self { process, client }) 65 + } 66 + 67 + /// Spawn a tap process using the default binary location. 68 + /// 69 + /// This looks for `tap` in the current directory first, then on the PATH. 70 + /// Combines [`TapProcess::spawn_default`] and [`TapClient`] creation. 71 + /// 72 + /// # Arguments 73 + /// 74 + /// * `config` - Configuration for the tap instance 75 + /// 76 + /// # Errors 77 + /// 78 + /// Returns an error if no tap binary is found or it fails to start. 79 + pub async fn spawn_default(config: TapConfig) -> Result<Self> { 80 + let process = TapProcess::spawn_default(config).await?; 81 + let client = process.client()?; 82 + Ok(Self { process, client }) 83 + } 84 + 85 + /// Get a reference to the underlying process. 86 + pub fn process(&self) -> &TapProcess { 87 + &self.process 88 + } 89 + 90 + /// Get a mutable reference to the underlying process. 91 + pub fn process_mut(&mut self) -> &mut TapProcess { 92 + &mut self.process 93 + } 94 + 95 + /// Get a reference to the client. 96 + pub fn client(&self) -> &TapClient { 97 + &self.client 98 + } 99 + 100 + /// Check if the tap process is still running. 101 + pub fn is_running(&mut self) -> bool { 102 + self.process.is_running() 103 + } 104 + 105 + /// Gracefully shut down the tap process. 106 + /// 107 + /// This sends SIGTERM and waits for the process to exit, then 108 + /// sends SIGKILL if it doesn't exit within the shutdown timeout. 109 + pub async fn shutdown(&mut self) -> Result<()> { 110 + self.process.shutdown().await 111 + } 112 + } 113 + 114 + impl Deref for TapHandle { 115 + type Target = TapClient; 116 + 117 + fn deref(&self) -> &Self::Target { 118 + &self.client 119 + } 120 + }
+63
src/lib.rs
··· 1 + //! # tapped 2 + //! 3 + //! A Rust wrapper for the `tap` ATProto sync utility. 4 + //! 5 + //! Tap simplifies ATProto sync by handling the firehose connection, verification, 6 + //! backfill, and filtering. This crate provides an idiomatic async Rust interface 7 + //! to tap's HTTP API and WebSocket event stream. 8 + //! 9 + //! ## Features 10 + //! 11 + //! - Connect to an existing tap instance or spawn one as a subprocess 12 + //! - Strongly-typed configuration with builder pattern 13 + //! - Async event streaming with automatic acknowledgment 14 + //! - Full HTTP API coverage for repo management and statistics 15 + //! 16 + //! ## Example 17 + //! 18 + //! ```no_run 19 + //! use tapped::{TapClient, TapConfig, Result}; 20 + //! 21 + //! #[tokio::main] 22 + //! async fn main() -> Result<()> { 23 + //! // Connect to an existing tap instance 24 + //! let client = TapClient::new("http://localhost:2480")?; 25 + //! 26 + //! // Check health 27 + //! client.health().await?; 28 + //! 29 + //! // Add repos to track 30 + //! client.add_repos(&["did:plc:example1234567890abc"]).await?; 31 + //! 32 + //! // Stream events 33 + //! let mut receiver = client.channel().await?; 34 + //! while let Ok(event) = receiver.recv().await { 35 + //! println!("Received event: {:?}", event); 36 + //! // Event is automatically acknowledged when dropped 37 + //! } 38 + //! 39 + //! Ok(()) 40 + //! } 41 + //! ``` 42 + 43 + mod channel; 44 + mod client; 45 + mod config; 46 + mod error; 47 + mod handle; 48 + mod process; 49 + mod types; 50 + 51 + pub use channel::{EventReceiver, ReceivedEvent}; 52 + pub use client::TapClient; 53 + pub use config::{LogLevel, TapConfig, TapConfigBuilder}; 54 + pub use error::Error; 55 + pub use handle::TapHandle; 56 + pub use process::TapProcess; 57 + pub use types::{ 58 + AccountStatus, Cursors, DidDocument, Event, IdentityEvent, Record, RecordAction, RecordEvent, 59 + RepoInfo, RepoState, Service, VerificationMethod, 60 + }; 61 + 62 + /// A specialised Result type for tapped operations. 63 + pub type Result<T> = std::result::Result<T, Error>;
+236
src/process.rs
··· 1 + //! Subprocess management for spawning and managing a tap process. 2 + 3 + use std::path::{Path, PathBuf}; 4 + use std::process::Stdio; 5 + use std::time::Duration; 6 + 7 + use tokio::process::{Child, Command}; 8 + use tokio::time::{sleep, timeout}; 9 + use url::Url; 10 + 11 + use crate::{Error, Result, TapClient, TapConfig}; 12 + 13 + /// A running tap process. 14 + /// 15 + /// The process is gracefully shut down when this struct is dropped. 16 + pub struct TapProcess { 17 + child: Child, 18 + url: Url, 19 + config: TapConfig, 20 + } 21 + 22 + impl TapProcess { 23 + /// Spawn a tap process at the given path with the given configuration. 24 + /// 25 + /// The path should point to the `tap` binary. The process will be started 26 + /// with the `run` subcommand and configuration passed as environment 27 + /// variables. 28 + /// 29 + /// # Example 30 + /// 31 + /// ```no_run 32 + /// # async fn example() -> tapped::Result<()> { 33 + /// use tapped::{TapProcess, TapConfig}; 34 + /// 35 + /// let config = TapConfig::builder() 36 + /// .database_url("sqlite://./my-tap.db") 37 + /// .build(); 38 + /// 39 + /// let process = TapProcess::spawn("./tap", config).await?; 40 + /// let client = process.client()?; 41 + /// # Ok(()) 42 + /// # } 43 + /// ``` 44 + pub async fn spawn(path: impl AsRef<Path>, config: TapConfig) -> Result<Self> { 45 + let path = path.as_ref(); 46 + 47 + if !path.exists() { 48 + return Err(Error::ProcessStart { 49 + message: format!("tap binary not found at: {}", path.display()), 50 + }); 51 + } 52 + 53 + Self::spawn_inner(path.to_path_buf(), config).await 54 + } 55 + 56 + /// Spawn a tap process using the default path discovery. 57 + /// 58 + /// Checks for `./tap` first, then falls back to `tap` on PATH. 59 + /// 60 + /// # Example 61 + /// 62 + /// ```no_run 63 + /// # async fn example() -> tapped::Result<()> { 64 + /// use tapped::{TapProcess, TapConfig}; 65 + /// 66 + /// let config = TapConfig::builder() 67 + /// .database_url("sqlite://./my-tap.db") 68 + /// .build(); 69 + /// 70 + /// let process = TapProcess::spawn_default(config).await?; 71 + /// # Ok(()) 72 + /// # } 73 + /// ``` 74 + pub async fn spawn_default(config: TapConfig) -> Result<Self> { 75 + let local_tap = PathBuf::from("./tap"); 76 + if local_tap.exists() { 77 + return Self::spawn_inner(local_tap, config).await; 78 + } 79 + 80 + Self::spawn_inner(PathBuf::from("tap"), config).await 81 + } 82 + 83 + async fn spawn_inner(path: PathBuf, config: TapConfig) -> Result<Self> { 84 + let bind = config.bind.clone().unwrap_or_else(|| ":2480".to_string()); 85 + let port = parse_port(&bind).unwrap_or(2480); 86 + 87 + // Spawned processes are always local - connect to localhost regardless 88 + // of what interface tap binds to. 89 + let url: Url = format!("http://127.0.0.1:{}", port) 90 + .parse() 91 + .map_err(|_| Error::InvalidUrl(format!("http://127.0.0.1:{}", port)))?; 92 + 93 + let mut cmd = Command::new(&path); 94 + cmd.arg("run").stdin(Stdio::null()).kill_on_drop(true); 95 + 96 + if config.inherit_stdio() { 97 + cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit()); 98 + } else { 99 + cmd.stdout(Stdio::null()).stderr(Stdio::null()); 100 + } 101 + 102 + for (key, value) in config.to_env_vars() { 103 + cmd.env(key, value); 104 + } 105 + 106 + let child = cmd.spawn().map_err(|e| Error::ProcessStart { 107 + message: format!("Failed to spawn {}: {}", path.display(), e), 108 + })?; 109 + 110 + let mut process = Self { 111 + child, 112 + url: url.clone(), 113 + config, 114 + }; 115 + 116 + if let Err(e) = process.wait_for_healthy().await { 117 + // Kill the process if health check fails 118 + let _ = process.child.kill().await; 119 + return Err(e); 120 + } 121 + 122 + Ok(process) 123 + } 124 + 125 + /// Wait for the tap process to become healthy. 126 + async fn wait_for_healthy(&self) -> Result<()> { 127 + let startup_timeout = self.config.startup_timeout(); 128 + let client = reqwest::Client::builder() 129 + .timeout(Duration::from_secs(2)) 130 + .build() 131 + .map_err(Error::Http)?; 132 + 133 + let health_url = self.url.join("/health")?; 134 + 135 + let result = timeout(startup_timeout, async { 136 + loop { 137 + match client.get(health_url.clone()).send().await { 138 + Ok(resp) if resp.status().is_success() => return Ok(()), 139 + _ => sleep(Duration::from_millis(100)).await, 140 + } 141 + } 142 + }) 143 + .await; 144 + 145 + match result { 146 + Ok(Ok(())) => Ok(()), 147 + Ok(Err(e)) => Err(e), 148 + Err(_) => Err(Error::Timeout), 149 + } 150 + } 151 + 152 + /// Get the URL of the running tap instance. 153 + pub fn url(&self) -> &Url { 154 + &self.url 155 + } 156 + 157 + /// Create a client connected to this tap process. 158 + pub fn client(&self) -> Result<TapClient> { 159 + TapClient::with_config(self.url.as_str(), &self.config) 160 + } 161 + 162 + /// Check if the process is still running. 163 + pub fn is_running(&mut self) -> bool { 164 + matches!(self.child.try_wait(), Ok(None)) 165 + } 166 + 167 + /// Gracefully shut down the tap process. 168 + /// 169 + /// Sends SIGTERM and waits up to `shutdown_timeout` before sending SIGKILL. 170 + pub async fn shutdown(&mut self) -> Result<()> { 171 + #[cfg(unix)] 172 + { 173 + // Send SIGTERM 174 + if let Some(pid) = self.child.id() { 175 + unsafe { 176 + libc::kill(pid as i32, libc::SIGTERM); 177 + } 178 + } 179 + 180 + // Wait for graceful shutdown 181 + let shutdown_timeout = self.config.shutdown_timeout(); 182 + match timeout(shutdown_timeout, self.child.wait()).await { 183 + Ok(Ok(_)) => return Ok(()), 184 + Ok(Err(e)) => return Err(Error::Io(e)), 185 + Err(_) => { 186 + // Timeout, send SIGKILL 187 + let _ = self.child.kill().await; 188 + } 189 + } 190 + } 191 + 192 + #[cfg(not(unix))] 193 + { 194 + let _ = self.child.kill().await; 195 + } 196 + 197 + Ok(()) 198 + } 199 + } 200 + 201 + impl Drop for TapProcess { 202 + fn drop(&mut self) { 203 + #[cfg(unix)] 204 + { 205 + if let Some(pid) = self.child.id() { 206 + unsafe { 207 + libc::kill(pid as i32, libc::SIGTERM); 208 + } 209 + } 210 + } 211 + } 212 + } 213 + 214 + /// Extract the port from a bind address. 215 + /// 216 + /// The bind format is passed directly to tap (Go's net.Listen format). 217 + /// We just need the port to construct a localhost URL for health checks. 218 + fn parse_port(bind: &str) -> Option<u16> { 219 + // The port is always after the last colon, even for IPv6 like [::1]:2480 220 + bind.rsplit(':').next()?.parse().ok() 221 + } 222 + 223 + #[cfg(test)] 224 + mod tests { 225 + use super::*; 226 + 227 + #[test] 228 + fn test_parse_port() { 229 + assert_eq!(parse_port(":2480"), Some(2480)); 230 + assert_eq!(parse_port("127.0.0.1:3000"), Some(3000)); 231 + assert_eq!(parse_port("0.0.0.0:8080"), Some(8080)); 232 + assert_eq!(parse_port("[::1]:2480"), Some(2480)); 233 + assert_eq!(parse_port("[2001:db8::1]:8080"), Some(8080)); 234 + assert_eq!(parse_port("invalid"), None); 235 + } 236 + }
+601
src/types.rs
··· 1 + //! Type definitions for tap events and API responses. 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + /// A record's JSON data with helper methods. 6 + /// 7 + /// Wraps the raw JSON value and provides convenient accessors. 8 + #[derive(Debug, Clone, Serialize, Deserialize)] 9 + #[serde(transparent)] 10 + pub struct Record(serde_json::Value); 11 + 12 + impl Record { 13 + /// Create a new Record from a JSON value. 14 + pub fn new(value: serde_json::Value) -> Self { 15 + Self(value) 16 + } 17 + 18 + /// Access the raw JSON value. 19 + pub fn json(&self) -> &serde_json::Value { 20 + &self.0 21 + } 22 + 23 + /// Consume and return the inner JSON value. 24 + pub fn into_json(self) -> serde_json::Value { 25 + self.0 26 + } 27 + 28 + /// Returns the `$type` field (e.g., "app.bsky.feed.post"). 29 + pub fn record_type(&self) -> Option<&str> { 30 + self.0.get("$type")?.as_str() 31 + } 32 + 33 + /// Deserialize the record into a user-provided type. 34 + pub fn deserialize_as<T: serde::de::DeserializeOwned>(&self) -> crate::Result<T> { 35 + Ok(serde_json::from_value(self.0.clone())?) 36 + } 37 + } 38 + 39 + /// Action performed on a record. 40 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 41 + #[serde(rename_all = "lowercase")] 42 + #[non_exhaustive] 43 + pub enum RecordAction { 44 + Create, 45 + Update, 46 + Delete, 47 + } 48 + 49 + /// Account status. 50 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 51 + #[serde(rename_all = "lowercase")] 52 + #[non_exhaustive] 53 + pub enum AccountStatus { 54 + Active, 55 + Takendown, 56 + Suspended, 57 + Deactivated, 58 + Deleted, 59 + } 60 + 61 + /// Repository sync state. 62 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 63 + #[serde(rename_all = "lowercase")] 64 + #[non_exhaustive] 65 + pub enum RepoState { 66 + Pending, 67 + Desynchronized, 68 + Resyncing, 69 + Active, 70 + Takendown, 71 + Suspended, 72 + Deactivated, 73 + Error, 74 + } 75 + 76 + /// A record event from the tap stream. 77 + #[derive(Debug, Clone)] 78 + pub struct RecordEvent { 79 + /// Unique event ID for acknowledgment. 80 + pub id: u64, 81 + /// True if from live firehose, false if from backfill/resync. 82 + pub live: bool, 83 + /// DID of the repository. 84 + pub did: String, 85 + /// Repository revision (TID format). 86 + pub rev: String, 87 + /// Collection NSID (e.g., "app.bsky.feed.post"). 88 + pub collection: String, 89 + /// Record key (usually TID format). 90 + pub rkey: String, 91 + /// Action performed on the record. 92 + pub action: RecordAction, 93 + /// CID of the record (None on delete). 94 + pub cid: Option<String>, 95 + /// The record data (None on delete). 96 + pub record: Option<Record>, 97 + } 98 + 99 + /// An identity event from the tap stream. 100 + #[derive(Debug, Clone)] 101 + pub struct IdentityEvent { 102 + /// Unique event ID for acknowledgment. 103 + pub id: u64, 104 + /// DID of the account. 105 + pub did: String, 106 + /// Current handle. 107 + pub handle: String, 108 + /// Whether the account is active. 109 + pub is_active: bool, 110 + /// Account status. 111 + pub status: AccountStatus, 112 + } 113 + 114 + /// An event from the tap stream. 115 + #[derive(Debug, Clone)] 116 + #[non_exhaustive] 117 + pub enum Event { 118 + /// A record create/update/delete event. 119 + Record(RecordEvent), 120 + /// An identity (handle/status) change event. 121 + Identity(IdentityEvent), 122 + } 123 + 124 + impl Event { 125 + /// Get the event ID. 126 + pub fn id(&self) -> u64 { 127 + match self { 128 + Event::Record(e) => e.id, 129 + Event::Identity(e) => e.id, 130 + } 131 + } 132 + 133 + /// Get the DID associated with this event. 134 + pub fn did(&self) -> &str { 135 + match self { 136 + Event::Record(e) => &e.did, 137 + Event::Identity(e) => &e.did, 138 + } 139 + } 140 + } 141 + 142 + /// Information about a tracked repository. 143 + #[derive(Debug, Clone, Serialize, Deserialize)] 144 + pub struct RepoInfo { 145 + /// DID of the repository. 146 + pub did: String, 147 + /// Current handle (may be empty). 148 + pub handle: String, 149 + /// Sync state. 150 + pub state: RepoState, 151 + /// Current revision (TID format, empty if not synced). 152 + pub rev: String, 153 + /// Error message if in error state. 154 + pub error: String, 155 + /// Number of failed retry attempts. 156 + pub retries: u32, 157 + /// Total number of tracked records. 158 + pub records: u64, 159 + } 160 + 161 + /// Cursor positions. 162 + #[derive(Debug, Clone, Serialize, Deserialize)] 163 + pub struct Cursors { 164 + /// Firehose sequence number (None if not consuming). 165 + pub firehose: Option<i64>, 166 + /// List repos enumeration cursor (None if not enumerating). 167 + pub list_repos: Option<String>, 168 + } 169 + 170 + /// A DID document. 171 + #[derive(Debug, Clone, Serialize, Deserialize)] 172 + pub struct DidDocument { 173 + /// The DID itself. 174 + pub id: String, 175 + /// Also known as (ATProto handles, etc.). 176 + #[serde(default, rename = "alsoKnownAs")] 177 + pub also_known_as: Vec<String>, 178 + /// Verification methods (signing keys). 179 + #[serde(default, rename = "verificationMethod")] 180 + pub verification_method: Vec<VerificationMethod>, 181 + /// Services (PDS endpoint, etc.). 182 + #[serde(default)] 183 + pub service: Vec<Service>, 184 + /// Additional fields not explicitly modelled. 185 + #[serde(flatten)] 186 + pub extra: serde_json::Value, 187 + } 188 + 189 + /// A verification method in a DID document. 190 + #[derive(Debug, Clone, Serialize, Deserialize)] 191 + pub struct VerificationMethod { 192 + /// Method ID. 193 + pub id: String, 194 + /// Method type. 195 + #[serde(rename = "type")] 196 + pub type_: String, 197 + /// Controller DID. 198 + pub controller: String, 199 + /// Public key in multibase format. 200 + #[serde(rename = "publicKeyMultibase")] 201 + pub public_key_multibase: Option<String>, 202 + } 203 + 204 + /// A service in a DID document. 205 + #[derive(Debug, Clone, Serialize, Deserialize)] 206 + pub struct Service { 207 + /// Service ID. 208 + pub id: String, 209 + /// Service type. 210 + #[serde(rename = "type")] 211 + pub type_: String, 212 + /// Service endpoint URL. 213 + #[serde(rename = "serviceEndpoint")] 214 + pub service_endpoint: String, 215 + } 216 + 217 + // Internal deserialisation structures for parsing tap's JSON format 218 + 219 + #[derive(Deserialize)] 220 + pub(crate) struct RawEvent { 221 + pub id: u64, 222 + #[serde(rename = "type")] 223 + pub type_: String, 224 + pub record: Option<RawRecordEvent>, 225 + pub identity: Option<RawIdentityEvent>, 226 + } 227 + 228 + #[derive(Deserialize)] 229 + pub(crate) struct RawRecordEvent { 230 + pub live: bool, 231 + pub did: String, 232 + pub rev: String, 233 + pub collection: String, 234 + pub rkey: String, 235 + pub action: RecordAction, 236 + pub cid: Option<String>, 237 + pub record: Option<serde_json::Value>, 238 + } 239 + 240 + #[derive(Deserialize)] 241 + pub(crate) struct RawIdentityEvent { 242 + pub did: String, 243 + pub handle: String, 244 + #[serde(rename = "is_active")] 245 + pub is_active: bool, 246 + pub status: AccountStatus, 247 + } 248 + 249 + impl RawEvent { 250 + /// Convert to the public Event type. 251 + pub fn into_event(self) -> Option<Event> { 252 + match self.type_.as_str() { 253 + "record" => { 254 + let r = self.record?; 255 + Some(Event::Record(RecordEvent { 256 + id: self.id, 257 + live: r.live, 258 + did: r.did, 259 + rev: r.rev, 260 + collection: r.collection, 261 + rkey: r.rkey, 262 + action: r.action, 263 + cid: r.cid, 264 + record: r.record.map(Record::new), 265 + })) 266 + } 267 + "identity" => { 268 + let i = self.identity?; 269 + Some(Event::Identity(IdentityEvent { 270 + id: self.id, 271 + did: i.did, 272 + handle: i.handle, 273 + is_active: i.is_active, 274 + status: i.status, 275 + })) 276 + } 277 + _ => None, 278 + } 279 + } 280 + } 281 + 282 + // Response types for stats endpoints 283 + 284 + #[derive(Deserialize)] 285 + pub(crate) struct RepoCountResponse { 286 + pub repo_count: u64, 287 + } 288 + 289 + #[derive(Deserialize)] 290 + pub(crate) struct RecordCountResponse { 291 + pub record_count: u64, 292 + } 293 + 294 + #[derive(Deserialize)] 295 + pub(crate) struct OutboxBufferResponse { 296 + pub outbox_buffer: u64, 297 + } 298 + 299 + #[derive(Deserialize)] 300 + pub(crate) struct ResyncBufferResponse { 301 + pub resync_buffer: u64, 302 + } 303 + 304 + #[derive(Deserialize)] 305 + pub(crate) struct ApiError { 306 + pub message: String, 307 + } 308 + 309 + #[cfg(test)] 310 + mod tests { 311 + use super::*; 312 + use serde_json::json; 313 + 314 + #[test] 315 + fn record_type_extraction() { 316 + let record = Record::new(json!({ 317 + "$type": "app.bsky.feed.post", 318 + "text": "Hello, world!", 319 + "createdAt": "2024-01-01T00:00:00Z" 320 + })); 321 + 322 + assert_eq!(record.record_type(), Some("app.bsky.feed.post")); 323 + } 324 + 325 + #[test] 326 + fn record_type_missing() { 327 + let record = Record::new(json!({ 328 + "text": "No type field" 329 + })); 330 + 331 + assert_eq!(record.record_type(), None); 332 + } 333 + 334 + #[test] 335 + fn record_deserialize_as() { 336 + #[derive(Debug, Deserialize, PartialEq)] 337 + struct SimpleRecord { 338 + text: String, 339 + } 340 + 341 + let record = Record::new(json!({ 342 + "$type": "test.record", 343 + "text": "Hello!" 344 + })); 345 + 346 + let parsed: SimpleRecord = record.deserialize_as().unwrap(); 347 + assert_eq!(parsed.text, "Hello!"); 348 + } 349 + 350 + /// Helper macro for testing enum variant deserialisation. 351 + macro_rules! assert_deserialize { 352 + ($type:ty, $($json:literal => $variant:expr),+ $(,)?) => { 353 + $( 354 + assert_eq!( 355 + serde_json::from_str::<$type>($json).unwrap(), 356 + $variant 357 + ); 358 + )+ 359 + }; 360 + } 361 + 362 + #[test] 363 + fn record_action_deserialize() { 364 + assert_deserialize!(RecordAction, 365 + r#""create""# => RecordAction::Create, 366 + r#""update""# => RecordAction::Update, 367 + r#""delete""# => RecordAction::Delete, 368 + ); 369 + } 370 + 371 + #[test] 372 + fn account_status_deserialize() { 373 + assert_deserialize!(AccountStatus, 374 + r#""active""# => AccountStatus::Active, 375 + r#""takendown""# => AccountStatus::Takendown, 376 + r#""suspended""# => AccountStatus::Suspended, 377 + r#""deactivated""# => AccountStatus::Deactivated, 378 + r#""deleted""# => AccountStatus::Deleted, 379 + ); 380 + } 381 + 382 + #[test] 383 + fn repo_state_deserialize() { 384 + assert_deserialize!(RepoState, 385 + r#""pending""# => RepoState::Pending, 386 + r#""active""# => RepoState::Active, 387 + r#""error""# => RepoState::Error, 388 + ); 389 + } 390 + 391 + #[test] 392 + fn repo_info_deserialize() { 393 + let json = json!({ 394 + "did": "did:plc:abc123", 395 + "handle": "test.bsky.social", 396 + "state": "active", 397 + "rev": "3abc123", 398 + "error": "", 399 + "retries": 0, 400 + "records": 42 401 + }); 402 + 403 + let info: RepoInfo = serde_json::from_value(json).unwrap(); 404 + assert_eq!(info.did, "did:plc:abc123"); 405 + assert_eq!(info.handle, "test.bsky.social"); 406 + assert_eq!(info.state, RepoState::Active); 407 + assert_eq!(info.records, 42); 408 + } 409 + 410 + #[test] 411 + fn cursors_deserialize() { 412 + let json = json!({ 413 + "firehose": 12345678, 414 + "list_repos": "some-cursor" 415 + }); 416 + 417 + let cursors: Cursors = serde_json::from_value(json).unwrap(); 418 + assert_eq!(cursors.firehose, Some(12345678)); 419 + assert_eq!(cursors.list_repos, Some("some-cursor".to_string())); 420 + } 421 + 422 + #[test] 423 + fn cursors_deserialize_nulls() { 424 + let json = json!({ 425 + "firehose": null, 426 + "list_repos": null 427 + }); 428 + 429 + let cursors: Cursors = serde_json::from_value(json).unwrap(); 430 + assert_eq!(cursors.firehose, None); 431 + assert_eq!(cursors.list_repos, None); 432 + } 433 + 434 + #[test] 435 + fn did_document_deserialize() { 436 + let json = json!({ 437 + "id": "did:plc:example1234567890abc", 438 + "alsoKnownAs": ["at://alice.test"], 439 + "verificationMethod": [{ 440 + "id": "did:plc:example1234567890abc#atproto", 441 + "type": "Multikey", 442 + "controller": "did:plc:example1234567890abc", 443 + "publicKeyMultibase": "zDnaekeGCpVsdvDCrGNa9t3bXYUs45MHX1hLwqvaKLtPU9m7X" 444 + }], 445 + "service": [{ 446 + "id": "#atproto_pds", 447 + "type": "AtprotoPersonalDataServer", 448 + "serviceEndpoint": "https://pds.example.com" 449 + }] 450 + }); 451 + 452 + let doc: DidDocument = serde_json::from_value(json).unwrap(); 453 + assert_eq!(doc.id, "did:plc:example1234567890abc"); 454 + assert_eq!(doc.also_known_as, vec!["at://alice.test"]); 455 + assert_eq!(doc.verification_method.len(), 1); 456 + assert_eq!(doc.verification_method[0].type_, "Multikey"); 457 + assert_eq!(doc.service.len(), 1); 458 + assert_eq!(doc.service[0].type_, "AtprotoPersonalDataServer"); 459 + } 460 + 461 + #[test] 462 + fn did_document_with_extra_fields() { 463 + let json = json!({ 464 + "id": "did:plc:test", 465 + "alsoKnownAs": [], 466 + "@context": ["https://www.w3.org/ns/did/v1"], 467 + "customField": "some value" 468 + }); 469 + 470 + let doc: DidDocument = serde_json::from_value(json).unwrap(); 471 + assert_eq!(doc.id, "did:plc:test"); 472 + assert!(doc.extra.get("@context").is_some()); 473 + assert!(doc.extra.get("customField").is_some()); 474 + } 475 + 476 + #[test] 477 + fn raw_record_event_deserialize() { 478 + let json = json!({ 479 + "id": 12345, 480 + "type": "record", 481 + "record": { 482 + "live": true, 483 + "did": "did:plc:abc123", 484 + "rev": "3abc", 485 + "collection": "app.bsky.feed.post", 486 + "rkey": "3def", 487 + "action": "create", 488 + "cid": "bafyreid...", 489 + "record": { 490 + "$type": "app.bsky.feed.post", 491 + "text": "Hello!" 492 + } 493 + } 494 + }); 495 + 496 + let raw: RawEvent = serde_json::from_value(json).unwrap(); 497 + assert_eq!(raw.id, 12345); 498 + assert_eq!(raw.type_, "record"); 499 + 500 + let event = raw.into_event().unwrap(); 501 + match event { 502 + Event::Record(r) => { 503 + assert_eq!(r.id, 12345); 504 + assert!(r.live); 505 + assert_eq!(r.did, "did:plc:abc123"); 506 + assert_eq!(r.collection, "app.bsky.feed.post"); 507 + assert_eq!(r.action, RecordAction::Create); 508 + assert!(r.record.is_some()); 509 + assert_eq!(r.record.unwrap().record_type(), Some("app.bsky.feed.post")); 510 + } 511 + _ => panic!("Expected Record event"), 512 + } 513 + } 514 + 515 + #[test] 516 + fn raw_identity_event_deserialize() { 517 + let json = json!({ 518 + "id": 99999, 519 + "type": "identity", 520 + "identity": { 521 + "did": "did:plc:xyz789", 522 + "handle": "alice.bsky.social", 523 + "is_active": true, 524 + "status": "active" 525 + } 526 + }); 527 + 528 + let raw: RawEvent = serde_json::from_value(json).unwrap(); 529 + let event = raw.into_event().unwrap(); 530 + 531 + match event { 532 + Event::Identity(i) => { 533 + assert_eq!(i.id, 99999); 534 + assert_eq!(i.did, "did:plc:xyz789"); 535 + assert_eq!(i.handle, "alice.bsky.social"); 536 + assert!(i.is_active); 537 + assert_eq!(i.status, AccountStatus::Active); 538 + } 539 + _ => panic!("Expected Identity event"), 540 + } 541 + } 542 + 543 + #[test] 544 + fn raw_delete_event_no_record() { 545 + let json = json!({ 546 + "id": 55555, 547 + "type": "record", 548 + "record": { 549 + "live": false, 550 + "did": "did:plc:deleted", 551 + "rev": "3xyz", 552 + "collection": "app.bsky.feed.post", 553 + "rkey": "3abc", 554 + "action": "delete", 555 + "cid": null, 556 + "record": null 557 + } 558 + }); 559 + 560 + let raw: RawEvent = serde_json::from_value(json).unwrap(); 561 + let event = raw.into_event().unwrap(); 562 + 563 + match event { 564 + Event::Record(r) => { 565 + assert_eq!(r.action, RecordAction::Delete); 566 + assert!(r.cid.is_none()); 567 + assert!(r.record.is_none()); 568 + } 569 + _ => panic!("Expected Record event"), 570 + } 571 + } 572 + 573 + #[test] 574 + fn event_helper_methods() { 575 + let record_event = Event::Record(RecordEvent { 576 + id: 123, 577 + live: true, 578 + did: "did:plc:record".to_string(), 579 + rev: "abc".to_string(), 580 + collection: "test".to_string(), 581 + rkey: "key".to_string(), 582 + action: RecordAction::Create, 583 + cid: None, 584 + record: None, 585 + }); 586 + 587 + assert_eq!(record_event.id(), 123); 588 + assert_eq!(record_event.did(), "did:plc:record"); 589 + 590 + let identity_event = Event::Identity(IdentityEvent { 591 + id: 456, 592 + did: "did:plc:identity".to_string(), 593 + handle: "test".to_string(), 594 + is_active: true, 595 + status: AccountStatus::Active, 596 + }); 597 + 598 + assert_eq!(identity_event.id(), 456); 599 + assert_eq!(identity_event.did(), "did:plc:identity"); 600 + } 601 + }
+247
tests/integration.rs
··· 1 + //! Integration tests for tapped. 2 + //! 3 + //! These tests require a tap binary to be available on the PATH or in the 4 + //! current directory. They spawn real tap processes and test the full 5 + //! client functionality. 6 + //! 7 + //! Run with: `cargo test --test integration -- --ignored` 8 + 9 + use std::path::Path; 10 + use std::sync::atomic::{AtomicU16, Ordering}; 11 + use std::time::Duration; 12 + use tapped::{TapClient, TapConfig, TapHandle, TapProcess}; 13 + 14 + /// Atomic counter for unique test instance assignment to avoid test conflicts. 15 + static TEST_COUNTER: AtomicU16 = AtomicU16::new(15000); 16 + 17 + /// Get a unique port and database URL for each test. 18 + /// 19 + /// Note: file-based SQLite databases are used because tap's internal 20 + /// components cannot share an in-memory database. 21 + fn unique_test_config() -> (u16, String) { 22 + let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst); 23 + let db_url = format!("sqlite:///tmp/tap-test-{}.db", id); 24 + (id, db_url) 25 + } 26 + 27 + /// Check if tap binary is available. 28 + fn tap_available() -> bool { 29 + Path::new("./tap").exists() || which::which("tap").is_ok() 30 + } 31 + 32 + /// Skip test if tap is not available. 33 + macro_rules! require_tap { 34 + () => { 35 + if !tap_available() { 36 + eprintln!("Skipping test: tap binary not found"); 37 + return; 38 + } 39 + }; 40 + } 41 + 42 + #[tokio::test] 43 + #[ignore = "requires tap binary"] 44 + async fn test_process_spawn_default() { 45 + require_tap!(); 46 + 47 + let (port, db_url) = unique_test_config(); 48 + let config = TapConfig::builder() 49 + .database_url(db_url) 50 + .bind(format!("127.0.0.1:{}", port)) 51 + .build(); 52 + 53 + let mut process = TapProcess::spawn_default(config) 54 + .await 55 + .expect("Failed to spawn tap"); 56 + 57 + assert!(process.is_running()); 58 + 59 + let client = process.client().expect("Failed to create client"); 60 + client.health().await.expect("Health check failed"); 61 + 62 + process.shutdown().await.expect("Shutdown failed"); 63 + assert!(!process.is_running()); 64 + } 65 + 66 + #[tokio::test] 67 + #[ignore = "requires tap binary"] 68 + async fn test_stats_endpoints() { 69 + require_tap!(); 70 + 71 + let (port, db_url) = unique_test_config(); 72 + let config = TapConfig::builder() 73 + .database_url(db_url) 74 + .bind(format!("127.0.0.1:{}", port)) 75 + .full_network(false) 76 + .build(); 77 + 78 + let handle = TapHandle::spawn_default(config) 79 + .await 80 + .expect("Failed to spawn tap"); 81 + 82 + let repo_count = handle.repo_count().await.expect("repo_count failed"); 83 + assert_eq!(repo_count, 0); 84 + 85 + let record_count = handle.record_count().await.expect("record_count failed"); 86 + assert_eq!(record_count, 0); 87 + 88 + let outbox = handle.outbox_buffer().await.expect("outbox_buffer failed"); 89 + let _ = outbox; 90 + 91 + let resync = handle.resync_buffer().await.expect("resync_buffer failed"); 92 + let _ = resync; 93 + 94 + let cursors = handle.cursors().await.expect("cursors failed"); 95 + let _ = cursors; 96 + } 97 + 98 + #[tokio::test] 99 + #[ignore = "requires tap binary"] 100 + async fn test_add_and_query_repos() { 101 + require_tap!(); 102 + 103 + let (port, db_url) = unique_test_config(); 104 + let config = TapConfig::builder() 105 + .database_url(db_url) 106 + .bind(format!("127.0.0.1:{}", port)) 107 + .full_network(false) 108 + .build(); 109 + 110 + let handle = TapHandle::spawn_default(config) 111 + .await 112 + .expect("Failed to spawn tap"); 113 + 114 + handle 115 + .add_repos(&["did:plc:ewvi7nxzyoun6zhxrhs64oiz"]) 116 + .await 117 + .expect("add_repos failed"); 118 + 119 + let count = handle.repo_count().await.expect("repo_count failed"); 120 + assert_eq!(count, 1); 121 + 122 + let info = handle 123 + .repo_info("did:plc:ewvi7nxzyoun6zhxrhs64oiz") 124 + .await 125 + .expect("repo_info failed"); 126 + assert_eq!(info.did, "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); 127 + 128 + handle 129 + .remove_repos(&["did:plc:ewvi7nxzyoun6zhxrhs64oiz"]) 130 + .await 131 + .expect("remove_repos failed"); 132 + 133 + let count = handle.repo_count().await.expect("repo_count failed"); 134 + assert_eq!(count, 0); 135 + } 136 + 137 + #[tokio::test] 138 + #[ignore = "requires tap binary"] 139 + async fn test_resolve_did() { 140 + require_tap!(); 141 + 142 + let (port, db_url) = unique_test_config(); 143 + let config = TapConfig::builder() 144 + .database_url(db_url) 145 + .bind(format!("127.0.0.1:{}", port)) 146 + .build(); 147 + 148 + let handle = TapHandle::spawn_default(config) 149 + .await 150 + .expect("Failed to spawn tap"); 151 + 152 + let doc = handle 153 + .resolve_did("did:plc:ewvi7nxzyoun6zhxrhs64oiz") 154 + .await 155 + .expect("resolve_did failed"); 156 + 157 + assert_eq!(doc.id, "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); 158 + assert!(!doc.also_known_as.is_empty()); 159 + assert!(!doc.service.is_empty()); 160 + } 161 + 162 + #[tokio::test] 163 + #[ignore = "requires tap binary"] 164 + async fn test_channel_connection() { 165 + require_tap!(); 166 + 167 + let (port, db_url) = unique_test_config(); 168 + let config = TapConfig::builder() 169 + .database_url(db_url) 170 + .bind(format!("127.0.0.1:{}", port)) 171 + .startup_timeout(Duration::from_secs(60)) 172 + .build(); 173 + 174 + let handle = TapHandle::spawn_default(config) 175 + .await 176 + .expect("Failed to spawn tap"); 177 + 178 + let _channel = handle.channel().await.expect("channel connection failed"); 179 + } 180 + 181 + #[tokio::test] 182 + #[ignore = "requires tap binary"] 183 + async fn test_graceful_shutdown() { 184 + require_tap!(); 185 + 186 + let (port, db_url) = unique_test_config(); 187 + let config = TapConfig::builder() 188 + .database_url(db_url) 189 + .bind(format!("127.0.0.1:{}", port)) 190 + .startup_timeout(Duration::from_secs(60)) 191 + .shutdown_timeout(Duration::from_secs(2)) 192 + .build(); 193 + 194 + let mut process = TapProcess::spawn_default(config) 195 + .await 196 + .expect("Failed to spawn tap"); 197 + 198 + assert!(process.is_running()); 199 + 200 + process.shutdown().await.expect("Shutdown failed"); 201 + 202 + assert!(!process.is_running()); 203 + } 204 + 205 + #[tokio::test] 206 + #[ignore = "requires tap binary"] 207 + async fn test_client_from_url() { 208 + require_tap!(); 209 + 210 + let (port, db_url) = unique_test_config(); 211 + let config = TapConfig::builder() 212 + .database_url(db_url) 213 + .bind(format!("127.0.0.1:{}", port)) 214 + .build(); 215 + 216 + let _process = TapProcess::spawn_default(config) 217 + .await 218 + .expect("Failed to spawn tap"); 219 + 220 + let client = 221 + TapClient::new(format!("http://127.0.0.1:{}", port)).expect("Failed to create client"); 222 + 223 + client.health().await.expect("Health check failed"); 224 + } 225 + 226 + #[tokio::test] 227 + #[ignore = "requires tap binary"] 228 + async fn test_multiple_collection_filters() { 229 + require_tap!(); 230 + 231 + let (port, db_url) = unique_test_config(); 232 + let config = TapConfig::builder() 233 + .database_url(db_url) 234 + .bind(format!("127.0.0.1:{}", port)) 235 + .startup_timeout(Duration::from_secs(60)) 236 + .collection_filter("app.bsky.feed.post") 237 + .collection_filter("app.bsky.feed.like") 238 + .collection_filter("app.bsky.feed.repost") 239 + .full_network(false) 240 + .build(); 241 + 242 + let handle = TapHandle::spawn_default(config) 243 + .await 244 + .expect("Failed to spawn tap with multiple filters"); 245 + 246 + handle.health().await.expect("Health check failed"); 247 + }