├── .github └── workflows │ └── main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── code-mirror │ ├── README.md │ ├── frontend │ │ ├── index.html │ │ ├── index.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── rollup.config.js │ └── main.rs └── webrtc-signaling-server │ ├── README.md │ ├── frontend │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── rollup.config.js │ └── main.rs └── src ├── broadcast.rs ├── conn.rs ├── lib.rs ├── signaling.rs └── ws.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | 19 | steps: 20 | 21 | - name: checkout sources 22 | uses: actions/checkout@v2 23 | 24 | - name: install Rust toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | override: true 29 | 30 | - name: build default 31 | run: cargo build --verbose --release 32 | 33 | test-linux: 34 | runs-on: ubuntu-latest 35 | needs: build 36 | steps: 37 | - name: checkout sources 38 | uses: actions/checkout@v2 39 | 40 | - name: test 41 | run: cargo test --release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | /Cargo.lock/ 4 | /**/*/node_modules 5 | /examples/*/frontend/dist -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "arc-swap" 22 | version = "1.7.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 25 | 26 | [[package]] 27 | name = "atomic_refcell" 28 | version = "0.1.13" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.2.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 37 | 38 | [[package]] 39 | name = "backtrace" 40 | version = "0.3.71" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 43 | dependencies = [ 44 | "addr2line", 45 | "cc", 46 | "cfg-if", 47 | "libc", 48 | "miniz_oxide", 49 | "object", 50 | "rustc-demangle", 51 | ] 52 | 53 | [[package]] 54 | name = "base64" 55 | version = "0.21.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 58 | 59 | [[package]] 60 | name = "bitflags" 61 | version = "1.3.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 64 | 65 | [[package]] 66 | name = "block-buffer" 67 | version = "0.10.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 70 | dependencies = [ 71 | "generic-array", 72 | ] 73 | 74 | [[package]] 75 | name = "bumpalo" 76 | version = "3.15.4" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" 79 | 80 | [[package]] 81 | name = "byteorder" 82 | version = "1.5.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 85 | 86 | [[package]] 87 | name = "bytes" 88 | version = "1.6.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 91 | 92 | [[package]] 93 | name = "cc" 94 | version = "1.0.90" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" 97 | 98 | [[package]] 99 | name = "cfg-if" 100 | version = "1.0.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 103 | 104 | [[package]] 105 | name = "cpufeatures" 106 | version = "0.2.12" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 109 | dependencies = [ 110 | "libc", 111 | ] 112 | 113 | [[package]] 114 | name = "crypto-common" 115 | version = "0.1.6" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 118 | dependencies = [ 119 | "generic-array", 120 | "typenum", 121 | ] 122 | 123 | [[package]] 124 | name = "data-encoding" 125 | version = "2.5.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" 128 | 129 | [[package]] 130 | name = "digest" 131 | version = "0.10.7" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 134 | dependencies = [ 135 | "block-buffer", 136 | "crypto-common", 137 | ] 138 | 139 | [[package]] 140 | name = "encoding_rs" 141 | version = "0.8.33" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 144 | dependencies = [ 145 | "cfg-if", 146 | ] 147 | 148 | [[package]] 149 | name = "equivalent" 150 | version = "1.0.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 153 | 154 | [[package]] 155 | name = "fastrand" 156 | version = "2.0.2" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" 159 | dependencies = [ 160 | "getrandom", 161 | ] 162 | 163 | [[package]] 164 | name = "fnv" 165 | version = "1.0.7" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 168 | 169 | [[package]] 170 | name = "form_urlencoded" 171 | version = "1.2.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 174 | dependencies = [ 175 | "percent-encoding", 176 | ] 177 | 178 | [[package]] 179 | name = "futures-channel" 180 | version = "0.3.30" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 183 | dependencies = [ 184 | "futures-core", 185 | "futures-sink", 186 | ] 187 | 188 | [[package]] 189 | name = "futures-core" 190 | version = "0.3.30" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 193 | 194 | [[package]] 195 | name = "futures-macro" 196 | version = "0.3.30" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 199 | dependencies = [ 200 | "proc-macro2", 201 | "quote", 202 | "syn", 203 | ] 204 | 205 | [[package]] 206 | name = "futures-sink" 207 | version = "0.3.30" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 210 | 211 | [[package]] 212 | name = "futures-task" 213 | version = "0.3.30" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 216 | 217 | [[package]] 218 | name = "futures-util" 219 | version = "0.3.30" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 222 | dependencies = [ 223 | "futures-core", 224 | "futures-macro", 225 | "futures-sink", 226 | "futures-task", 227 | "pin-project-lite", 228 | "pin-utils", 229 | "slab", 230 | ] 231 | 232 | [[package]] 233 | name = "generic-array" 234 | version = "0.14.7" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 237 | dependencies = [ 238 | "typenum", 239 | "version_check", 240 | ] 241 | 242 | [[package]] 243 | name = "getrandom" 244 | version = "0.2.12" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 247 | dependencies = [ 248 | "cfg-if", 249 | "js-sys", 250 | "libc", 251 | "wasi", 252 | "wasm-bindgen", 253 | ] 254 | 255 | [[package]] 256 | name = "gimli" 257 | version = "0.28.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 260 | 261 | [[package]] 262 | name = "h2" 263 | version = "0.3.25" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" 266 | dependencies = [ 267 | "bytes", 268 | "fnv", 269 | "futures-core", 270 | "futures-sink", 271 | "futures-util", 272 | "http 0.2.12", 273 | "indexmap", 274 | "slab", 275 | "tokio", 276 | "tokio-util", 277 | "tracing", 278 | ] 279 | 280 | [[package]] 281 | name = "hashbrown" 282 | version = "0.14.3" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 285 | 286 | [[package]] 287 | name = "headers" 288 | version = "0.3.9" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" 291 | dependencies = [ 292 | "base64", 293 | "bytes", 294 | "headers-core", 295 | "http 0.2.12", 296 | "httpdate", 297 | "mime", 298 | "sha1", 299 | ] 300 | 301 | [[package]] 302 | name = "headers-core" 303 | version = "0.2.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 306 | dependencies = [ 307 | "http 0.2.12", 308 | ] 309 | 310 | [[package]] 311 | name = "hermit-abi" 312 | version = "0.3.9" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 315 | 316 | [[package]] 317 | name = "http" 318 | version = "0.2.12" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 321 | dependencies = [ 322 | "bytes", 323 | "fnv", 324 | "itoa", 325 | ] 326 | 327 | [[package]] 328 | name = "http" 329 | version = "1.1.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 332 | dependencies = [ 333 | "bytes", 334 | "fnv", 335 | "itoa", 336 | ] 337 | 338 | [[package]] 339 | name = "http-body" 340 | version = "0.4.6" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 343 | dependencies = [ 344 | "bytes", 345 | "http 0.2.12", 346 | "pin-project-lite", 347 | ] 348 | 349 | [[package]] 350 | name = "httparse" 351 | version = "1.8.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 354 | 355 | [[package]] 356 | name = "httpdate" 357 | version = "1.0.3" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 360 | 361 | [[package]] 362 | name = "hyper" 363 | version = "0.14.28" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" 366 | dependencies = [ 367 | "bytes", 368 | "futures-channel", 369 | "futures-core", 370 | "futures-util", 371 | "h2", 372 | "http 0.2.12", 373 | "http-body", 374 | "httparse", 375 | "httpdate", 376 | "itoa", 377 | "pin-project-lite", 378 | "socket2", 379 | "tokio", 380 | "tower-service", 381 | "tracing", 382 | "want", 383 | ] 384 | 385 | [[package]] 386 | name = "idna" 387 | version = "0.5.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 390 | dependencies = [ 391 | "unicode-bidi", 392 | "unicode-normalization", 393 | ] 394 | 395 | [[package]] 396 | name = "indexmap" 397 | version = "2.2.6" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 400 | dependencies = [ 401 | "equivalent", 402 | "hashbrown", 403 | ] 404 | 405 | [[package]] 406 | name = "itoa" 407 | version = "1.0.11" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 410 | 411 | [[package]] 412 | name = "js-sys" 413 | version = "0.3.69" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 416 | dependencies = [ 417 | "wasm-bindgen", 418 | ] 419 | 420 | [[package]] 421 | name = "libc" 422 | version = "0.2.153" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 425 | 426 | [[package]] 427 | name = "lock_api" 428 | version = "0.4.11" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 431 | dependencies = [ 432 | "autocfg", 433 | "scopeguard", 434 | ] 435 | 436 | [[package]] 437 | name = "log" 438 | version = "0.4.21" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 441 | 442 | [[package]] 443 | name = "memchr" 444 | version = "2.7.2" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 447 | 448 | [[package]] 449 | name = "mime" 450 | version = "0.3.17" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 453 | 454 | [[package]] 455 | name = "mime_guess" 456 | version = "2.0.4" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 459 | dependencies = [ 460 | "mime", 461 | "unicase", 462 | ] 463 | 464 | [[package]] 465 | name = "miniz_oxide" 466 | version = "0.7.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 469 | dependencies = [ 470 | "adler", 471 | ] 472 | 473 | [[package]] 474 | name = "mio" 475 | version = "0.8.11" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 478 | dependencies = [ 479 | "libc", 480 | "wasi", 481 | "windows-sys 0.48.0", 482 | ] 483 | 484 | [[package]] 485 | name = "multer" 486 | version = "2.1.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" 489 | dependencies = [ 490 | "bytes", 491 | "encoding_rs", 492 | "futures-util", 493 | "http 0.2.12", 494 | "httparse", 495 | "log", 496 | "memchr", 497 | "mime", 498 | "spin", 499 | "version_check", 500 | ] 501 | 502 | [[package]] 503 | name = "num_cpus" 504 | version = "1.16.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 507 | dependencies = [ 508 | "hermit-abi", 509 | "libc", 510 | ] 511 | 512 | [[package]] 513 | name = "object" 514 | version = "0.32.2" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 517 | dependencies = [ 518 | "memchr", 519 | ] 520 | 521 | [[package]] 522 | name = "once_cell" 523 | version = "1.19.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 526 | 527 | [[package]] 528 | name = "parking_lot" 529 | version = "0.12.1" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 532 | dependencies = [ 533 | "lock_api", 534 | "parking_lot_core", 535 | ] 536 | 537 | [[package]] 538 | name = "parking_lot_core" 539 | version = "0.9.9" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 542 | dependencies = [ 543 | "cfg-if", 544 | "libc", 545 | "redox_syscall", 546 | "smallvec", 547 | "windows-targets 0.48.5", 548 | ] 549 | 550 | [[package]] 551 | name = "percent-encoding" 552 | version = "2.3.1" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 555 | 556 | [[package]] 557 | name = "pin-project" 558 | version = "1.1.5" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 561 | dependencies = [ 562 | "pin-project-internal", 563 | ] 564 | 565 | [[package]] 566 | name = "pin-project-internal" 567 | version = "1.1.5" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 570 | dependencies = [ 571 | "proc-macro2", 572 | "quote", 573 | "syn", 574 | ] 575 | 576 | [[package]] 577 | name = "pin-project-lite" 578 | version = "0.2.13" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 581 | 582 | [[package]] 583 | name = "pin-utils" 584 | version = "0.1.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 587 | 588 | [[package]] 589 | name = "ppv-lite86" 590 | version = "0.2.17" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 593 | 594 | [[package]] 595 | name = "proc-macro2" 596 | version = "1.0.79" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 599 | dependencies = [ 600 | "unicode-ident", 601 | ] 602 | 603 | [[package]] 604 | name = "quote" 605 | version = "1.0.35" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 608 | dependencies = [ 609 | "proc-macro2", 610 | ] 611 | 612 | [[package]] 613 | name = "rand" 614 | version = "0.8.5" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 617 | dependencies = [ 618 | "libc", 619 | "rand_chacha", 620 | "rand_core", 621 | ] 622 | 623 | [[package]] 624 | name = "rand_chacha" 625 | version = "0.3.1" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 628 | dependencies = [ 629 | "ppv-lite86", 630 | "rand_core", 631 | ] 632 | 633 | [[package]] 634 | name = "rand_core" 635 | version = "0.6.4" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 638 | dependencies = [ 639 | "getrandom", 640 | ] 641 | 642 | [[package]] 643 | name = "redox_syscall" 644 | version = "0.4.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 647 | dependencies = [ 648 | "bitflags", 649 | ] 650 | 651 | [[package]] 652 | name = "rustc-demangle" 653 | version = "0.1.23" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 656 | 657 | [[package]] 658 | name = "rustls-pemfile" 659 | version = "1.0.4" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 662 | dependencies = [ 663 | "base64", 664 | ] 665 | 666 | [[package]] 667 | name = "ryu" 668 | version = "1.0.17" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 671 | 672 | [[package]] 673 | name = "scoped-tls" 674 | version = "1.0.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 677 | 678 | [[package]] 679 | name = "scopeguard" 680 | version = "1.2.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 683 | 684 | [[package]] 685 | name = "serde" 686 | version = "1.0.197" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 689 | dependencies = [ 690 | "serde_derive", 691 | ] 692 | 693 | [[package]] 694 | name = "serde_derive" 695 | version = "1.0.197" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 698 | dependencies = [ 699 | "proc-macro2", 700 | "quote", 701 | "syn", 702 | ] 703 | 704 | [[package]] 705 | name = "serde_json" 706 | version = "1.0.115" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" 709 | dependencies = [ 710 | "itoa", 711 | "ryu", 712 | "serde", 713 | ] 714 | 715 | [[package]] 716 | name = "serde_urlencoded" 717 | version = "0.7.1" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 720 | dependencies = [ 721 | "form_urlencoded", 722 | "itoa", 723 | "ryu", 724 | "serde", 725 | ] 726 | 727 | [[package]] 728 | name = "sha1" 729 | version = "0.10.6" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 732 | dependencies = [ 733 | "cfg-if", 734 | "cpufeatures", 735 | "digest", 736 | ] 737 | 738 | [[package]] 739 | name = "signal-hook-registry" 740 | version = "1.4.1" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 743 | dependencies = [ 744 | "libc", 745 | ] 746 | 747 | [[package]] 748 | name = "slab" 749 | version = "0.4.9" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 752 | dependencies = [ 753 | "autocfg", 754 | ] 755 | 756 | [[package]] 757 | name = "smallstr" 758 | version = "0.3.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" 761 | dependencies = [ 762 | "smallvec", 763 | ] 764 | 765 | [[package]] 766 | name = "smallvec" 767 | version = "1.13.2" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 770 | 771 | [[package]] 772 | name = "socket2" 773 | version = "0.5.6" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 776 | dependencies = [ 777 | "libc", 778 | "windows-sys 0.52.0", 779 | ] 780 | 781 | [[package]] 782 | name = "spin" 783 | version = "0.9.8" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 786 | 787 | [[package]] 788 | name = "syn" 789 | version = "2.0.55" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" 792 | dependencies = [ 793 | "proc-macro2", 794 | "quote", 795 | "unicode-ident", 796 | ] 797 | 798 | [[package]] 799 | name = "thiserror" 800 | version = "1.0.58" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 803 | dependencies = [ 804 | "thiserror-impl", 805 | ] 806 | 807 | [[package]] 808 | name = "thiserror-impl" 809 | version = "1.0.58" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 812 | dependencies = [ 813 | "proc-macro2", 814 | "quote", 815 | "syn", 816 | ] 817 | 818 | [[package]] 819 | name = "tinyvec" 820 | version = "1.6.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 823 | dependencies = [ 824 | "tinyvec_macros", 825 | ] 826 | 827 | [[package]] 828 | name = "tinyvec_macros" 829 | version = "0.1.1" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 832 | 833 | [[package]] 834 | name = "tokio" 835 | version = "1.36.0" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" 838 | dependencies = [ 839 | "backtrace", 840 | "bytes", 841 | "libc", 842 | "mio", 843 | "num_cpus", 844 | "parking_lot", 845 | "pin-project-lite", 846 | "signal-hook-registry", 847 | "socket2", 848 | "tokio-macros", 849 | "windows-sys 0.48.0", 850 | ] 851 | 852 | [[package]] 853 | name = "tokio-macros" 854 | version = "2.2.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 857 | dependencies = [ 858 | "proc-macro2", 859 | "quote", 860 | "syn", 861 | ] 862 | 863 | [[package]] 864 | name = "tokio-stream" 865 | version = "0.1.15" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" 868 | dependencies = [ 869 | "futures-core", 870 | "pin-project-lite", 871 | "tokio", 872 | ] 873 | 874 | [[package]] 875 | name = "tokio-tungstenite" 876 | version = "0.20.1" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" 879 | dependencies = [ 880 | "futures-util", 881 | "log", 882 | "tokio", 883 | "tungstenite 0.20.1", 884 | ] 885 | 886 | [[package]] 887 | name = "tokio-tungstenite" 888 | version = "0.21.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" 891 | dependencies = [ 892 | "futures-util", 893 | "log", 894 | "tokio", 895 | "tungstenite 0.21.0", 896 | ] 897 | 898 | [[package]] 899 | name = "tokio-util" 900 | version = "0.7.10" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 903 | dependencies = [ 904 | "bytes", 905 | "futures-core", 906 | "futures-sink", 907 | "pin-project-lite", 908 | "tokio", 909 | "tracing", 910 | ] 911 | 912 | [[package]] 913 | name = "tower-service" 914 | version = "0.3.2" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 917 | 918 | [[package]] 919 | name = "tracing" 920 | version = "0.1.40" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 923 | dependencies = [ 924 | "log", 925 | "pin-project-lite", 926 | "tracing-attributes", 927 | "tracing-core", 928 | ] 929 | 930 | [[package]] 931 | name = "tracing-attributes" 932 | version = "0.1.27" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 935 | dependencies = [ 936 | "proc-macro2", 937 | "quote", 938 | "syn", 939 | ] 940 | 941 | [[package]] 942 | name = "tracing-core" 943 | version = "0.1.32" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 946 | dependencies = [ 947 | "once_cell", 948 | ] 949 | 950 | [[package]] 951 | name = "try-lock" 952 | version = "0.2.5" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 955 | 956 | [[package]] 957 | name = "tungstenite" 958 | version = "0.20.1" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" 961 | dependencies = [ 962 | "byteorder", 963 | "bytes", 964 | "data-encoding", 965 | "http 0.2.12", 966 | "httparse", 967 | "log", 968 | "rand", 969 | "sha1", 970 | "thiserror", 971 | "url", 972 | "utf-8", 973 | ] 974 | 975 | [[package]] 976 | name = "tungstenite" 977 | version = "0.21.0" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" 980 | dependencies = [ 981 | "byteorder", 982 | "bytes", 983 | "data-encoding", 984 | "http 1.1.0", 985 | "httparse", 986 | "log", 987 | "rand", 988 | "sha1", 989 | "thiserror", 990 | "url", 991 | "utf-8", 992 | ] 993 | 994 | [[package]] 995 | name = "typenum" 996 | version = "1.17.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 999 | 1000 | [[package]] 1001 | name = "unicase" 1002 | version = "2.7.0" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1005 | dependencies = [ 1006 | "version_check", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "unicode-bidi" 1011 | version = "0.3.15" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1014 | 1015 | [[package]] 1016 | name = "unicode-ident" 1017 | version = "1.0.12" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1020 | 1021 | [[package]] 1022 | name = "unicode-normalization" 1023 | version = "0.1.23" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1026 | dependencies = [ 1027 | "tinyvec", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "url" 1032 | version = "2.5.0" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1035 | dependencies = [ 1036 | "form_urlencoded", 1037 | "idna", 1038 | "percent-encoding", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "utf-8" 1043 | version = "0.7.6" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1046 | 1047 | [[package]] 1048 | name = "version_check" 1049 | version = "0.9.4" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1052 | 1053 | [[package]] 1054 | name = "want" 1055 | version = "0.3.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1058 | dependencies = [ 1059 | "try-lock", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "warp" 1064 | version = "0.3.6" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" 1067 | dependencies = [ 1068 | "bytes", 1069 | "futures-channel", 1070 | "futures-util", 1071 | "headers", 1072 | "http 0.2.12", 1073 | "hyper", 1074 | "log", 1075 | "mime", 1076 | "mime_guess", 1077 | "multer", 1078 | "percent-encoding", 1079 | "pin-project", 1080 | "rustls-pemfile", 1081 | "scoped-tls", 1082 | "serde", 1083 | "serde_json", 1084 | "serde_urlencoded", 1085 | "tokio", 1086 | "tokio-stream", 1087 | "tokio-tungstenite 0.20.1", 1088 | "tokio-util", 1089 | "tower-service", 1090 | "tracing", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "wasi" 1095 | version = "0.11.0+wasi-snapshot-preview1" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1098 | 1099 | [[package]] 1100 | name = "wasm-bindgen" 1101 | version = "0.2.92" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1104 | dependencies = [ 1105 | "cfg-if", 1106 | "wasm-bindgen-macro", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "wasm-bindgen-backend" 1111 | version = "0.2.92" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1114 | dependencies = [ 1115 | "bumpalo", 1116 | "log", 1117 | "once_cell", 1118 | "proc-macro2", 1119 | "quote", 1120 | "syn", 1121 | "wasm-bindgen-shared", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "wasm-bindgen-macro" 1126 | version = "0.2.92" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1129 | dependencies = [ 1130 | "quote", 1131 | "wasm-bindgen-macro-support", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "wasm-bindgen-macro-support" 1136 | version = "0.2.92" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1139 | dependencies = [ 1140 | "proc-macro2", 1141 | "quote", 1142 | "syn", 1143 | "wasm-bindgen-backend", 1144 | "wasm-bindgen-shared", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "wasm-bindgen-shared" 1149 | version = "0.2.92" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1152 | 1153 | [[package]] 1154 | name = "windows-sys" 1155 | version = "0.48.0" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1158 | dependencies = [ 1159 | "windows-targets 0.48.5", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "windows-sys" 1164 | version = "0.52.0" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1167 | dependencies = [ 1168 | "windows-targets 0.52.4", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "windows-targets" 1173 | version = "0.48.5" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1176 | dependencies = [ 1177 | "windows_aarch64_gnullvm 0.48.5", 1178 | "windows_aarch64_msvc 0.48.5", 1179 | "windows_i686_gnu 0.48.5", 1180 | "windows_i686_msvc 0.48.5", 1181 | "windows_x86_64_gnu 0.48.5", 1182 | "windows_x86_64_gnullvm 0.48.5", 1183 | "windows_x86_64_msvc 0.48.5", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "windows-targets" 1188 | version = "0.52.4" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 1191 | dependencies = [ 1192 | "windows_aarch64_gnullvm 0.52.4", 1193 | "windows_aarch64_msvc 0.52.4", 1194 | "windows_i686_gnu 0.52.4", 1195 | "windows_i686_msvc 0.52.4", 1196 | "windows_x86_64_gnu 0.52.4", 1197 | "windows_x86_64_gnullvm 0.52.4", 1198 | "windows_x86_64_msvc 0.52.4", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "windows_aarch64_gnullvm" 1203 | version = "0.48.5" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1206 | 1207 | [[package]] 1208 | name = "windows_aarch64_gnullvm" 1209 | version = "0.52.4" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 1212 | 1213 | [[package]] 1214 | name = "windows_aarch64_msvc" 1215 | version = "0.48.5" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1218 | 1219 | [[package]] 1220 | name = "windows_aarch64_msvc" 1221 | version = "0.52.4" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 1224 | 1225 | [[package]] 1226 | name = "windows_i686_gnu" 1227 | version = "0.48.5" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1230 | 1231 | [[package]] 1232 | name = "windows_i686_gnu" 1233 | version = "0.52.4" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 1236 | 1237 | [[package]] 1238 | name = "windows_i686_msvc" 1239 | version = "0.48.5" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1242 | 1243 | [[package]] 1244 | name = "windows_i686_msvc" 1245 | version = "0.52.4" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 1248 | 1249 | [[package]] 1250 | name = "windows_x86_64_gnu" 1251 | version = "0.48.5" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1254 | 1255 | [[package]] 1256 | name = "windows_x86_64_gnu" 1257 | version = "0.52.4" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 1260 | 1261 | [[package]] 1262 | name = "windows_x86_64_gnullvm" 1263 | version = "0.48.5" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1266 | 1267 | [[package]] 1268 | name = "windows_x86_64_gnullvm" 1269 | version = "0.52.4" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 1272 | 1273 | [[package]] 1274 | name = "windows_x86_64_msvc" 1275 | version = "0.48.5" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1278 | 1279 | [[package]] 1280 | name = "windows_x86_64_msvc" 1281 | version = "0.52.4" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1284 | 1285 | [[package]] 1286 | name = "yrs" 1287 | version = "0.18.2" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "a4058d69bbbc97181d53d9d093a4b892001b84601f2fc4e27f48c8862bc8b369" 1290 | dependencies = [ 1291 | "arc-swap", 1292 | "atomic_refcell", 1293 | "fastrand", 1294 | "serde", 1295 | "serde_json", 1296 | "smallstr", 1297 | "smallvec", 1298 | "thiserror", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "yrs-warp" 1303 | version = "0.8.0" 1304 | dependencies = [ 1305 | "bytes", 1306 | "futures-util", 1307 | "serde", 1308 | "serde_json", 1309 | "tokio", 1310 | "tokio-tungstenite 0.21.0", 1311 | "tokio-util", 1312 | "tracing", 1313 | "warp", 1314 | "yrs", 1315 | ] 1316 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yrs-warp" 3 | version = "0.8.0" 4 | edition = "2021" 5 | description = "Yrs synchronization protocol using Warp web sockets" 6 | license = "MIT" 7 | authors = ["Bartosz Sypytkowski "] 8 | keywords = ["crdt", "yrs", "warp"] 9 | homepage = "https://github.com/y-crdt/yrs-warp/" 10 | repository = "https://github.com/y-crdt/yrs-warp/" 11 | readme = "./README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | yrs = "0.18.2" 17 | warp = "0.3" 18 | futures-util = { version = "0.3", features = ["sink"] } 19 | tokio = { version = "1.36", features = ["rt", "net", "sync", "macros"] } 20 | serde = { version = "1.0", features = ["derive", "rc"] } 21 | serde_json = "1.0" 22 | tracing = { version = "0.1", features = ["log"] } 23 | tokio-util = { version = "0.7.10", features = ["codec"] } 24 | 25 | [dev-dependencies] 26 | tokio-tungstenite = "0.21" 27 | tokio = { version = "1", features = ["full"] } 28 | bytes = "1.6" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 4 | - Bartosz Sypytkowski 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yrs web socket connections 2 | 3 | This library is an extension over [Yjs](https://yjs.dev)/[Yrs](https://github.com/y-crdt/y-crdt) Conflict-Free Replicated Data Types (CRDT) message exchange protocol. It provides an utilities connect with Yjs web socket provider using Rust tokio's [warp](https://github.com/seanmonstar/warp) web server. 4 | 5 | ### Demo 6 | 7 | A working demo can be seen under [examples](./examples) subfolder. It integrates this library API with Code Mirror 6, enhancing it with collaborative rich text document editing capabilities. 8 | 9 | ### Example 10 | 11 | In order to gossip updates between different web socket connection from the clients collaborating over the same logical document, a broadcast group can be used: 12 | 13 | ```rust 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | // We're using a single static document shared among all the peers. 18 | let awareness = Arc::new(RwLock::new(Awareness::new(Doc::new()))); 19 | 20 | // open a broadcast group that listens to awareness and document updates 21 | // and has a pending message buffer of up to 32 updates 22 | let bcast = Arc::new(BroadcastGroup::new(awareness, 32).await); 23 | 24 | let ws = warp::path("my-room") 25 | .and(warp::ws()) 26 | .and(warp::any().map(move || bcast.clone())) 27 | .and_then(ws_handler); 28 | 29 | warp::serve(ws).run(([0, 0, 0, 0], 8000)).await; 30 | } 31 | 32 | async fn ws_handler(ws: Ws, bcast: Arc) -> Result { 33 | Ok(ws.on_upgrade(move |socket| peer(socket, bcast))) 34 | } 35 | 36 | async fn peer(ws: WebSocket, bcast: Arc) { 37 | let (sink, stream) = ws.split(); 38 | let sink = Arc::new(Mutex::new(WarpSink::from(sink))); 39 | let stream = WarpStream::from(stream); 40 | let sub = bcast.subscribe(sink, stream); 41 | match sub.completed().await { 42 | Ok(_) => println!("broadcasting for channel finished successfully"), 43 | Err(e) => eprintln!("broadcasting for channel finished abruptly: {}", e), 44 | } 45 | } 46 | ``` 47 | 48 | ## Custom protocol extensions 49 | 50 | [y-sync](https://crates.io/crates/y-sync) protocol enables to extend it's own protocol, and yrs-warp supports this as well. 51 | This can be done by implementing your own protocol, eg.: 52 | 53 | ```rust 54 | use y_sync::sync::Protocol; 55 | 56 | struct EchoProtocol; 57 | impl Protocol for EchoProtocol { 58 | fn missing_handle( 59 | &self, 60 | awareness: &mut Awareness, 61 | tag: u8, 62 | data: Vec, 63 | ) -> Result, Error> { 64 | // all messages prefixed with tags unknown to y-sync protocol 65 | // will be echo-ed back to the sender 66 | Ok(Some(Message::Custom(tag, data))) 67 | } 68 | } 69 | 70 | async fn peer(ws: WebSocket, awareness: AwarenessRef) { 71 | //.. later in code subscribe with custom protocol parameter 72 | let sub = bcast.subscribe_with(sink, stream, EchoProtocol); 73 | // .. rest of the code 74 | } 75 | ``` 76 | 77 | ## y-webrtc and signaling service 78 | 79 | Additionally to performing it's role as a [y-websocket](https://docs.yjs.dev/ecosystem/connection-provider/y-websocket) 80 | server, `yrs-warp` also provides a signaling server implementation used by [y-webrtc](https://github.com/yjs/y-webrtc) 81 | clients to exchange information necessary to connect WebRTC peers together and make them subscribe/unsubscribe from specific rooms. 82 | 83 | ```rust 84 | use warp::{Filter, Rejection, Reply}; 85 | use warp::ws::{Ws, WebSocket}; 86 | use yrs_warp::signaling::{SignalingService, signaling_conn}; 87 | 88 | #[tokio::main] 89 | async fn main() { 90 | let signaling = SignalingService::new(); 91 | let ws = warp::path("signaling") 92 | .and(warp::ws()) 93 | .and(warp::any().map(move || signaling.clone())) 94 | .and_then(ws_handler); 95 | warp::serve(routes).run(([0, 0, 0, 0], 8000)).await; 96 | } 97 | async fn ws_handler(ws: Ws, svc: SignalingService) -> Result { 98 | Ok(ws.on_upgrade(move |socket| peer(socket, svc))) 99 | } 100 | async fn peer(ws: WebSocket, svc: SignalingService) { 101 | match signaling_conn(ws, svc).await { 102 | Ok(_) => println!("signaling connection stopped"), 103 | Err(e) => eprintln!("signaling connection failed: {}", e), 104 | } 105 | } 106 | ``` 107 | 108 | 109 | ## Sponsors 110 | 111 | [![NLNET](https://nlnet.nl/image/logo_nlnet.svg)](https://nlnet.nl/) -------------------------------------------------------------------------------- /examples/code-mirror/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Running an example 3 | 4 | In order to generate static website content, first you need build it. This can be done via npm. 5 | 6 | ```bash 7 | cd examples/code-mirror/frontend 8 | npm install 9 | npm run build 10 | ``` 11 | 12 | These commands will install all dependencies and run [rollup.js](https://rollupjs.org/), which is used for bundling the JavaScript code and dependecies for Code Mirror. 13 | 14 | Once the steps above are done, a `./frontent/dist` directory should appear. If so, all you need to do is to run following command from the *main git repository directory*: 15 | 16 | ```bash 17 | cargo run --example code-mirror 18 | ``` 19 | 20 | It will run a local warp server with an index page at [http://localhost:8000](http://localhost:8000). -------------------------------------------------------------------------------- /examples/code-mirror/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yjs CodeMirror6 Demo + Yrs warp websocket 6 | 10 | 11 | 12 | 13 |

14 |

15 | This is a demo of the Yjs ⇔ 16 | CodeMirror binding: 17 | y-codemirror.next. 18 |

19 |

20 | The content of this editor is shared with every client that visits this 21 | domain. 22 |

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/code-mirror/frontend/index.js: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs' 2 | import { yCollab } from 'y-codemirror.next' 3 | import { EditorView, basicSetup } from "codemirror"; 4 | import { EditorState } from "@codemirror/state"; 5 | import { javascript } from '@codemirror/lang-javascript' 6 | import {syntaxHighlighting, defaultHighlightStyle} from "@codemirror/language" 7 | import * as random from 'lib0/random' 8 | import {WebsocketProvider} from "y-websocket"; 9 | 10 | export const usercolors = [ 11 | { color: '#30bced', light: '#30bced33' }, 12 | { color: '#6eeb83', light: '#6eeb8333' }, 13 | { color: '#ffbc42', light: '#ffbc4233' }, 14 | { color: '#ecd444', light: '#ecd44433' }, 15 | { color: '#ee6352', light: '#ee635233' }, 16 | { color: '#9ac2c9', light: '#9ac2c933' }, 17 | { color: '#8acb88', light: '#8acb8833' }, 18 | { color: '#1be7ff', light: '#1be7ff33' } 19 | ] 20 | 21 | // select a random color for this user 22 | export const userColor = usercolors[random.uint32() % usercolors.length] 23 | 24 | const doc = new Y.Doc() 25 | const ytext = doc.getText('codemirror') 26 | 27 | const provider = new WebsocketProvider('ws://localhost:8000', 'my-room', doc, { disableBc: true }) 28 | 29 | const undoManager = new Y.UndoManager(ytext) 30 | 31 | provider.awareness.setLocalStateField('user', { 32 | name: 'Anonymous ' + Math.floor(Math.random() * 100), 33 | color: userColor.color, 34 | colorLight: userColor.light 35 | }) 36 | 37 | 38 | 39 | const state = EditorState.create({ 40 | doc: ytext.toString(), 41 | extensions: [ 42 | javascript(), 43 | syntaxHighlighting(defaultHighlightStyle), 44 | yCollab(ytext, provider.awareness, { undoManager }) 45 | ] 46 | }) 47 | 48 | const view = new EditorView({ state, parent: document.querySelector('#editor') }) 49 | 50 | // toggle connection by clicking on connect-btn 51 | const toggleButton = document.getElementById('y-connect-btn') 52 | let connected = true 53 | 54 | toggleButton.addEventListener('click', () => { 55 | if (connected) { 56 | provider.disconnect() 57 | connected = false 58 | toggleButton.innerText = 'Reconnect' 59 | } else { 60 | provider.connect() 61 | connected = true 62 | toggleButton.innerText = 'Disconnect' 63 | } 64 | }) -------------------------------------------------------------------------------- /examples/code-mirror/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf ./dist", 8 | "build": "npm run clean && mkdir ./dist && rollup -c && cp ./index.html ./dist/index.html" 9 | }, 10 | "keywords": [ 11 | "yjs", 12 | "yrs", 13 | "codemirror" 14 | ], 15 | "author": "Bartosz Sypytkowski ", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@codemirror/lang-javascript": "^6.0.2", 19 | "@codemirror/language": "github:codemirror/language", 20 | "@codemirror/state": "^6.1.1", 21 | "codemirror": "^6.0.1", 22 | "lib0": "^0.2.52", 23 | "y-codemirror.next": "^0.3.2", 24 | "y-websocket": "^1.4.4", 25 | "yjs": "^13.5.0" 26 | }, 27 | "devDependencies": { 28 | "rollup": "^2.79.0", 29 | "@rollup/plugin-babel": "^5.3.1", 30 | "@rollup/plugin-commonjs": "^22.0.2", 31 | "@rollup/plugin-node-resolve": "^13.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/code-mirror/frontend/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | export default [ 6 | { 7 | input: "index.js", 8 | output: { 9 | file: "dist/main.js", 10 | format: "iife", 11 | }, 12 | plugins: [ 13 | nodeResolve({ browser: true }), 14 | commonjs(), 15 | babel({ 16 | exclude: 'node_modules/**', 17 | babelHelpers: 'bundled' 18 | }) 19 | ] 20 | } 21 | ]; -------------------------------------------------------------------------------- /examples/code-mirror/main.rs: -------------------------------------------------------------------------------- 1 | use futures_util::StreamExt; 2 | use std::sync::Arc; 3 | use tokio::sync::{Mutex, RwLock}; 4 | use warp::ws::{WebSocket, Ws}; 5 | use warp::{Filter, Rejection, Reply}; 6 | use yrs::sync::Awareness; 7 | use yrs::{Doc, Text, Transact}; 8 | use yrs_warp::broadcast::BroadcastGroup; 9 | use yrs_warp::ws::{WarpSink, WarpStream}; 10 | use yrs_warp::AwarenessRef; 11 | 12 | const STATIC_FILES_DIR: &str = "examples/code-mirror/frontend/dist"; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | // We're using a single static document shared among all the peers. 17 | let awareness: AwarenessRef = { 18 | let doc = Doc::new(); 19 | { 20 | // pre-initialize code mirror document with some text 21 | let txt = doc.get_or_insert_text("codemirror"); 22 | let mut txn = doc.transact_mut(); 23 | txt.push( 24 | &mut txn, 25 | r#"function hello() { 26 | console.log('hello world'); 27 | }"#, 28 | ); 29 | } 30 | Arc::new(RwLock::new(Awareness::new(doc))) 31 | }; 32 | 33 | // open a broadcast group that listens to awareness and document updates 34 | // and has a pending message buffer of up to 32 updates 35 | let bcast = Arc::new(BroadcastGroup::new(awareness.clone(), 32).await); 36 | 37 | let static_files = warp::get().and(warp::fs::dir(STATIC_FILES_DIR)); 38 | 39 | let ws = warp::path("my-room") 40 | .and(warp::ws()) 41 | .and(warp::any().map(move || bcast.clone())) 42 | .and_then(ws_handler); 43 | 44 | let routes = ws.or(static_files); 45 | 46 | warp::serve(routes).run(([0, 0, 0, 0], 8000)).await; 47 | } 48 | 49 | async fn ws_handler(ws: Ws, bcast: Arc) -> Result { 50 | Ok(ws.on_upgrade(move |socket| peer(socket, bcast))) 51 | } 52 | 53 | async fn peer(ws: WebSocket, bcast: Arc) { 54 | let (sink, stream) = ws.split(); 55 | let sink = Arc::new(Mutex::new(WarpSink::from(sink))); 56 | let stream = WarpStream::from(stream); 57 | let sub = bcast.subscribe(sink, stream); 58 | match sub.completed().await { 59 | Ok(_) => println!("broadcasting for channel finished successfully"), 60 | Err(e) => eprintln!("broadcasting for channel finished abruptly: {}", e), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/webrtc-signaling-server/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Running an example 3 | 4 | In order to generate static website content, first you need build it. This can be done via npm. 5 | 6 | ```bash 7 | cd examples/webrtc-signaling-server/frontend 8 | npm install 9 | npm run build 10 | ``` 11 | 12 | These commands will install all dependencies and run [rollup.js](https://rollupjs.org/), which is used for bundling the JavaScript code and dependecies for Code Mirror. 13 | 14 | Once the steps above are done, a `./frontent/dist` directory should appear. If so, all you need to do is to run following command from the *main git repository directory*: 15 | 16 | ```bash 17 | cargo run --example webrtc-signaling-server 18 | ``` 19 | 20 | It will run a local warp server with an index page at [http://localhost:8000](http://localhost:8000). -------------------------------------------------------------------------------- /examples/webrtc-signaling-server/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yjs CodeMirror6 Demo + Yrs warp websocket 6 | 10 | 11 | 12 | 13 |

14 |

15 | This is a demo of the Yjs ⇔ 16 | CodeMirror binding: 17 | y-codemirror.next. 18 |

19 |

20 | The content of this editor is shared with every client that visits this 21 | domain. 22 |

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/webrtc-signaling-server/frontend/index.js: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs' 2 | import { yCollab } from 'y-codemirror.next' 3 | import { EditorView, basicSetup } from "codemirror"; 4 | import { EditorState } from "@codemirror/state"; 5 | import { javascript } from '@codemirror/lang-javascript' 6 | import {syntaxHighlighting, defaultHighlightStyle} from "@codemirror/language" 7 | import * as random from 'lib0/random' 8 | import {WebrtcProvider} from "y-webrtc"; 9 | 10 | export const usercolors = [ 11 | { color: '#30bced', light: '#30bced33' }, 12 | { color: '#6eeb83', light: '#6eeb8333' }, 13 | { color: '#ffbc42', light: '#ffbc4233' }, 14 | { color: '#ecd444', light: '#ecd44433' }, 15 | { color: '#ee6352', light: '#ee635233' }, 16 | { color: '#9ac2c9', light: '#9ac2c933' }, 17 | { color: '#8acb88', light: '#8acb8833' }, 18 | { color: '#1be7ff', light: '#1be7ff33' } 19 | ] 20 | 21 | // select a random color for this user 22 | export const userColor = usercolors[random.uint32() % usercolors.length] 23 | 24 | const doc = new Y.Doc() 25 | const ytext = doc.getText('codemirror') 26 | 27 | const provider = new WebrtcProvider('sample', doc, { signaling: ['ws://localhost:8000/signaling'] }) 28 | 29 | const undoManager = new Y.UndoManager(ytext) 30 | 31 | provider.awareness.setLocalStateField('user', { 32 | name: 'Anonymous ' + Math.floor(Math.random() * 100), 33 | color: userColor.color, 34 | colorLight: userColor.light 35 | }) 36 | 37 | 38 | 39 | const state = EditorState.create({ 40 | doc: ytext.toString(), 41 | extensions: [ 42 | javascript(), 43 | syntaxHighlighting(defaultHighlightStyle), 44 | yCollab(ytext, provider.awareness, { undoManager }) 45 | ] 46 | }) 47 | 48 | const view = new EditorView({ state, parent: document.querySelector('#editor') }) 49 | 50 | // toggle connection by clicking on connect-btn 51 | const toggleButton = document.getElementById('y-connect-btn') 52 | let connected = true 53 | 54 | toggleButton.addEventListener('click', () => { 55 | if (connected) { 56 | provider.disconnect() 57 | connected = false 58 | toggleButton.innerText = 'Reconnect' 59 | } else { 60 | provider.connect() 61 | connected = true 62 | toggleButton.innerText = 'Disconnect' 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /examples/webrtc-signaling-server/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf ./dist", 8 | "build": "npm run clean && mkdir ./dist && rollup -c && cp ./index.html ./dist/index.html" 9 | }, 10 | "keywords": [ 11 | "yjs", 12 | "yrs", 13 | "codemirror" 14 | ], 15 | "author": "Bartosz Sypytkowski ", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@codemirror/lang-javascript": "^6.0.2", 19 | "@codemirror/language": "github:codemirror/language", 20 | "@codemirror/state": "^6.1.1", 21 | "codemirror": "^6.0.1", 22 | "lib0": "^0.2.52", 23 | "y-codemirror.next": "^0.3.2", 24 | "y-webrtc": "^10.2.4", 25 | "yjs": "^13.5.0" 26 | }, 27 | "devDependencies": { 28 | "rollup": "^2.79.0", 29 | "@rollup/plugin-babel": "^5.3.1", 30 | "@rollup/plugin-commonjs": "^22.0.2", 31 | "@rollup/plugin-node-resolve": "^13.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/webrtc-signaling-server/frontend/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | export default [ 6 | { 7 | input: "index.js", 8 | output: { 9 | file: "dist/main.js", 10 | format: "iife", 11 | }, 12 | plugins: [ 13 | nodeResolve({ browser: true }), 14 | commonjs(), 15 | babel({ 16 | exclude: 'node_modules/**', 17 | babelHelpers: 'bundled' 18 | }) 19 | ] 20 | } 21 | ]; -------------------------------------------------------------------------------- /examples/webrtc-signaling-server/main.rs: -------------------------------------------------------------------------------- 1 | use warp::ws::{WebSocket, Ws}; 2 | use warp::{Filter, Rejection, Reply}; 3 | use yrs_warp::signaling::{signaling_conn, SignalingService}; 4 | 5 | const STATIC_FILES_DIR: &str = "examples/webrtc-signaling-server/frontend/dist"; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let signaling = SignalingService::new(); 10 | 11 | let static_files = warp::get().and(warp::fs::dir(STATIC_FILES_DIR)); 12 | 13 | let ws = warp::path("signaling") 14 | .and(warp::ws()) 15 | .and(warp::any().map(move || signaling.clone())) 16 | .and_then(ws_handler); 17 | 18 | let routes = ws.or(static_files); 19 | 20 | warp::serve(routes).run(([0, 0, 0, 0], 8000)).await; 21 | } 22 | 23 | async fn ws_handler(ws: Ws, svc: SignalingService) -> Result { 24 | Ok(ws.on_upgrade(move |socket| peer(socket, svc))) 25 | } 26 | 27 | async fn peer(ws: WebSocket, svc: SignalingService) { 28 | println!("new incoming signaling connection"); 29 | match signaling_conn(ws, svc).await { 30 | Ok(_) => println!("signaling connection stopped"), 31 | Err(e) => eprintln!("signaling connection failed: {}", e), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/broadcast.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::AwarenessRef; 3 | use futures_util::{SinkExt, StreamExt}; 4 | use std::sync::Arc; 5 | use tokio::select; 6 | use tokio::sync::broadcast::error::SendError; 7 | use tokio::sync::broadcast::{channel, Receiver, Sender}; 8 | use tokio::sync::Mutex; 9 | use tokio::task::JoinHandle; 10 | use yrs::encoding::write::Write; 11 | use yrs::sync::protocol::{MSG_SYNC, MSG_SYNC_UPDATE}; 12 | use yrs::sync::{DefaultProtocol, Error, Message, Protocol, SyncMessage}; 13 | use yrs::updates::decoder::Decode; 14 | use yrs::updates::encoder::{Encode, Encoder, EncoderV1}; 15 | use yrs::Update; 16 | 17 | /// A broadcast group can be used to propagate updates produced by yrs [yrs::Doc] and [Awareness] 18 | /// structures in a binary form that conforms to a y-sync protocol. 19 | /// 20 | /// New receivers can subscribe to a broadcasting group via [BroadcastGroup::subscribe] method. 21 | pub struct BroadcastGroup { 22 | awareness_sub: yrs::Subscription, 23 | doc_sub: yrs::Subscription, 24 | awareness_ref: AwarenessRef, 25 | sender: Sender>, 26 | receiver: Receiver>, 27 | awareness_updater: JoinHandle<()>, 28 | } 29 | 30 | unsafe impl Send for BroadcastGroup {} 31 | unsafe impl Sync for BroadcastGroup {} 32 | 33 | impl BroadcastGroup { 34 | /// Creates a new [BroadcastGroup] over a provided `awareness` instance. All changes triggered 35 | /// by this awareness structure or its underlying document will be propagated to all subscribers 36 | /// which have been registered via [BroadcastGroup::subscribe] method. 37 | /// 38 | /// The overflow of the incoming events that needs to be propagates will be buffered up to a 39 | /// provided `buffer_capacity` size. 40 | pub async fn new(awareness: AwarenessRef, buffer_capacity: usize) -> Self { 41 | let (sender, receiver) = channel(buffer_capacity); 42 | let awareness_c = Arc::downgrade(&awareness); 43 | let mut lock = awareness.write().await; 44 | let sink = sender.clone(); 45 | let doc_sub = { 46 | lock.doc_mut() 47 | .observe_update_v1(move |_txn, u| { 48 | // we manually construct msg here to avoid update data copying 49 | let mut encoder = EncoderV1::new(); 50 | encoder.write_var(MSG_SYNC); 51 | encoder.write_var(MSG_SYNC_UPDATE); 52 | encoder.write_buf(&u.update); 53 | let msg = encoder.to_vec(); 54 | if let Err(_e) = sink.send(msg) { 55 | // current broadcast group is being closed 56 | } 57 | }) 58 | .unwrap() 59 | }; 60 | let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); 61 | let sink = sender.clone(); 62 | let awareness_sub = lock.on_update(move |e| { 63 | let added = e.added(); 64 | let updated = e.updated(); 65 | let removed = e.removed(); 66 | let mut changed = Vec::with_capacity(added.len() + updated.len() + removed.len()); 67 | changed.extend_from_slice(added); 68 | changed.extend_from_slice(updated); 69 | changed.extend_from_slice(removed); 70 | 71 | if let Err(_) = tx.send(changed) { 72 | tracing::warn!("failed to send awareness update"); 73 | } 74 | }); 75 | drop(lock); 76 | let awareness_updater = tokio::task::spawn(async move { 77 | while let Some(changed_clients) = rx.recv().await { 78 | if let Some(awareness) = awareness_c.upgrade() { 79 | let awareness = awareness.read().await; 80 | match awareness.update_with_clients(changed_clients) { 81 | Ok(update) => { 82 | if let Err(_) = sink.send(Message::Awareness(update).encode_v1()) { 83 | tracing::warn!("couldn't broadcast awareness update"); 84 | } 85 | } 86 | Err(e) => { 87 | tracing::warn!("error while computing awareness update: {}", e) 88 | } 89 | } 90 | } else { 91 | return; 92 | } 93 | } 94 | }); 95 | BroadcastGroup { 96 | awareness_ref: awareness, 97 | awareness_updater, 98 | sender, 99 | receiver, 100 | awareness_sub, 101 | doc_sub, 102 | } 103 | } 104 | 105 | /// Returns a reference to an underlying [Awareness] instance. 106 | pub fn awareness(&self) -> &AwarenessRef { 107 | &self.awareness_ref 108 | } 109 | 110 | /// Broadcasts user message to all active subscribers. Returns error if message could not have 111 | /// been broadcasted. 112 | pub fn broadcast(&self, msg: Vec) -> Result<(), SendError>> { 113 | self.sender.send(msg)?; 114 | Ok(()) 115 | } 116 | 117 | /// Subscribes a new connection - represented by `sink`/`stream` pair implementing a futures 118 | /// Sink and Stream protocols - to a current broadcast group. 119 | /// 120 | /// Returns a subscription structure, which can be dropped in order to unsubscribe or awaited 121 | /// via [Subscription::completed] method in order to complete of its own volition (due to 122 | /// an internal connection error or closed connection). 123 | pub fn subscribe(&self, sink: Arc>, stream: Stream) -> Subscription 124 | where 125 | Sink: SinkExt> + Send + Sync + Unpin + 'static, 126 | Stream: StreamExt, E>> + Send + Sync + Unpin + 'static, 127 | >>::Error: std::error::Error + Send + Sync, 128 | E: std::error::Error + Send + Sync + 'static, 129 | { 130 | self.subscribe_with(sink, stream, DefaultProtocol) 131 | } 132 | 133 | /// Subscribes a new connection - represented by `sink`/`stream` pair implementing a futures 134 | /// Sink and Stream protocols - to a current broadcast group. 135 | /// 136 | /// Returns a subscription structure, which can be dropped in order to unsubscribe or awaited 137 | /// via [Subscription::completed] method in order to complete of its own volition (due to 138 | /// an internal connection error or closed connection). 139 | /// 140 | /// Unlike [BroadcastGroup::subscribe], this method can take [Protocol] parameter that allows to 141 | /// customize the y-sync protocol behavior. 142 | pub fn subscribe_with( 143 | &self, 144 | sink: Arc>, 145 | mut stream: Stream, 146 | protocol: P, 147 | ) -> Subscription 148 | where 149 | Sink: SinkExt> + Send + Sync + Unpin + 'static, 150 | Stream: StreamExt, E>> + Send + Sync + Unpin + 'static, 151 | >>::Error: std::error::Error + Send + Sync, 152 | E: std::error::Error + Send + Sync + 'static, 153 | P: Protocol + Send + Sync + 'static, 154 | { 155 | let sink_task = { 156 | let sink = sink.clone(); 157 | let mut receiver = self.sender.subscribe(); 158 | tokio::spawn(async move { 159 | while let Ok(msg) = receiver.recv().await { 160 | let mut sink = sink.lock().await; 161 | if let Err(e) = sink.send(msg).await { 162 | println!("broadcast failed to sent sync message"); 163 | return Err(Error::Other(Box::new(e))); 164 | } 165 | } 166 | Ok(()) 167 | }) 168 | }; 169 | let stream_task = { 170 | let awareness = self.awareness().clone(); 171 | tokio::spawn(async move { 172 | while let Some(res) = stream.next().await { 173 | let msg = Message::decode_v1(&res.map_err(|e| Error::Other(Box::new(e)))?)?; 174 | let reply = Self::handle_msg(&protocol, &awareness, msg).await?; 175 | match reply { 176 | None => {} 177 | Some(reply) => { 178 | let mut sink = sink.lock().await; 179 | sink.send(reply.encode_v1()) 180 | .await 181 | .map_err(|e| Error::Other(Box::new(e)))?; 182 | } 183 | } 184 | } 185 | Ok(()) 186 | }) 187 | }; 188 | 189 | Subscription { 190 | sink_task, 191 | stream_task, 192 | } 193 | } 194 | 195 | async fn handle_msg( 196 | protocol: &P, 197 | awareness: &AwarenessRef, 198 | msg: Message, 199 | ) -> Result, Error> { 200 | match msg { 201 | Message::Sync(msg) => match msg { 202 | SyncMessage::SyncStep1(state_vector) => { 203 | let awareness = awareness.read().await; 204 | protocol.handle_sync_step1(&*awareness, state_vector) 205 | } 206 | SyncMessage::SyncStep2(update) => { 207 | let mut awareness = awareness.write().await; 208 | let update = Update::decode_v1(&update)?; 209 | protocol.handle_sync_step2(&mut *awareness, update) 210 | } 211 | SyncMessage::Update(update) => { 212 | let mut awareness = awareness.write().await; 213 | let update = Update::decode_v1(&update)?; 214 | protocol.handle_sync_step2(&mut *awareness, update) 215 | } 216 | }, 217 | Message::Auth(deny_reason) => { 218 | let awareness = awareness.read().await; 219 | protocol.handle_auth(&*awareness, deny_reason) 220 | } 221 | Message::AwarenessQuery => { 222 | let awareness = awareness.read().await; 223 | protocol.handle_awareness_query(&*awareness) 224 | } 225 | Message::Awareness(update) => { 226 | let mut awareness = awareness.write().await; 227 | protocol.handle_awareness_update(&mut *awareness, update) 228 | } 229 | Message::Custom(tag, data) => { 230 | let mut awareness = awareness.write().await; 231 | protocol.missing_handle(&mut *awareness, tag, data) 232 | } 233 | } 234 | } 235 | } 236 | 237 | impl Drop for BroadcastGroup { 238 | fn drop(&mut self) { 239 | self.awareness_updater.abort(); 240 | } 241 | } 242 | 243 | /// A subscription structure returned from [BroadcastGroup::subscribe], which represents a 244 | /// subscribed connection. It can be dropped in order to unsubscribe or awaited via 245 | /// [Subscription::completed] method in order to complete of its own volition (due to an internal 246 | /// connection error or closed connection). 247 | #[derive(Debug)] 248 | pub struct Subscription { 249 | sink_task: JoinHandle>, 250 | stream_task: JoinHandle>, 251 | } 252 | 253 | impl Subscription { 254 | /// Consumes current subscription, waiting for it to complete. If an underlying connection was 255 | /// closed because of failure, an error which caused it to happen will be returned. 256 | /// 257 | /// This method doesn't invoke close procedure. If you need that, drop current subscription instead. 258 | pub async fn completed(self) -> Result<(), Error> { 259 | let res = select! { 260 | r1 = self.sink_task => r1, 261 | r2 = self.stream_task => r2, 262 | }; 263 | res.map_err(|e| Error::Other(e.into()))? 264 | } 265 | } 266 | 267 | #[cfg(test)] 268 | mod test { 269 | use crate::broadcast::BroadcastGroup; 270 | use futures_util::{ready, SinkExt, StreamExt}; 271 | use std::collections::HashMap; 272 | use std::pin::Pin; 273 | use std::sync::Arc; 274 | use std::task::{Context, Poll}; 275 | use tokio::sync::{Mutex, RwLock}; 276 | use tokio_util::sync::PollSender; 277 | use yrs::sync::awareness::AwarenessUpdateEntry; 278 | use yrs::sync::{Awareness, AwarenessUpdate, Error, Message, SyncMessage}; 279 | use yrs::updates::decoder::Decode; 280 | use yrs::updates::encoder::Encode; 281 | use yrs::{Doc, StateVector, Text, Transact}; 282 | 283 | #[derive(Debug)] 284 | pub struct ReceiverStream { 285 | inner: tokio::sync::mpsc::Receiver, 286 | } 287 | 288 | impl ReceiverStream { 289 | /// Create a new `ReceiverStream`. 290 | pub fn new(recv: tokio::sync::mpsc::Receiver) -> Self { 291 | Self { inner: recv } 292 | } 293 | } 294 | 295 | impl futures_util::Stream for ReceiverStream { 296 | type Item = Result; 297 | 298 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 299 | match ready!(self.inner.poll_recv(cx)) { 300 | None => Poll::Ready(None), 301 | Some(v) => Poll::Ready(Some(Ok(v))), 302 | } 303 | } 304 | } 305 | 306 | fn test_channel(capacity: usize) -> (PollSender>, ReceiverStream>) { 307 | let (s, r) = tokio::sync::mpsc::channel::>(capacity); 308 | let s = PollSender::new(s); 309 | let r = ReceiverStream::new(r); 310 | (s, r) 311 | } 312 | 313 | #[tokio::test] 314 | async fn broadcast_changes() -> Result<(), Box> { 315 | let doc = Doc::with_client_id(1); 316 | let text = doc.get_or_insert_text("test"); 317 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 318 | let group = BroadcastGroup::new(awareness.clone(), 1).await; 319 | 320 | let (server_sender, mut client_receiver) = test_channel(1); 321 | let (mut client_sender, server_receiver) = test_channel(1); 322 | let _sub1 = group.subscribe(Arc::new(Mutex::new(server_sender)), server_receiver); 323 | 324 | // check update propagation 325 | { 326 | let a = awareness.write().await; 327 | text.push(&mut a.doc().transact_mut(), "a"); 328 | } 329 | let msg = client_receiver.next().await; 330 | let msg = msg.map(|x| Message::decode_v1(&x.unwrap()).unwrap()); 331 | assert_eq!( 332 | msg, 333 | Some(Message::Sync(SyncMessage::Update(vec![ 334 | 1, 1, 1, 0, 4, 1, 4, 116, 101, 115, 116, 1, 97, 0, 335 | ]))) 336 | ); 337 | 338 | // check awareness update propagation 339 | { 340 | let mut a = awareness.write().await; 341 | a.set_local_state(r#"{"key":"value"}"#) 342 | } 343 | 344 | let msg = client_receiver.next().await; 345 | let msg = msg.map(|x| Message::decode_v1(&x.unwrap()).unwrap()); 346 | assert_eq!( 347 | msg, 348 | Some(Message::Awareness(AwarenessUpdate { 349 | clients: HashMap::from([( 350 | 1, 351 | AwarenessUpdateEntry { 352 | clock: 1, 353 | json: r#"{"key":"value"}"#.to_string(), 354 | }, 355 | )]), 356 | })) 357 | ); 358 | 359 | // check sync state request/response 360 | { 361 | client_sender 362 | .send(Message::Sync(SyncMessage::SyncStep1(StateVector::default())).encode_v1()) 363 | .await?; 364 | let msg = client_receiver.next().await; 365 | let msg = msg.map(|x| Message::decode_v1(&x.unwrap()).unwrap()); 366 | assert_eq!( 367 | msg, 368 | Some(Message::Sync(SyncMessage::SyncStep2(vec![ 369 | 1, 1, 1, 0, 4, 1, 4, 116, 101, 115, 116, 1, 97, 0, 370 | ]))) 371 | ); 372 | } 373 | 374 | Ok(()) 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/conn.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use futures_util::sink::SinkExt; 3 | use futures_util::StreamExt; 4 | use std::future::Future; 5 | use std::marker::PhantomData; 6 | use std::pin::Pin; 7 | use std::sync::{Arc, Weak}; 8 | use std::task::{Context, Poll}; 9 | use tokio::spawn; 10 | use tokio::sync::{Mutex, RwLock}; 11 | use tokio::task::JoinHandle; 12 | use yrs::encoding::read::Cursor; 13 | use yrs::sync::Awareness; 14 | use yrs::sync::{DefaultProtocol, Error, Message, MessageReader, Protocol, SyncMessage}; 15 | use yrs::updates::decoder::{Decode, DecoderV1}; 16 | use yrs::updates::encoder::{Encode, Encoder, EncoderV1}; 17 | use yrs::Update; 18 | 19 | /// Connection handler over a pair of message streams, which implements a Yjs/Yrs awareness and 20 | /// update exchange protocol. 21 | /// 22 | /// This connection implements Future pattern and can be awaited upon in order for a caller to 23 | /// recognize whether underlying websocket connection has been finished gracefully or abruptly. 24 | #[derive(Debug)] 25 | pub struct Connection { 26 | processing_loop: JoinHandle>, 27 | awareness: Arc>, 28 | inbox: Arc>, 29 | _stream: PhantomData, 30 | } 31 | 32 | impl Connection 33 | where 34 | Sink: SinkExt, Error = E> + Send + Sync + Unpin + 'static, 35 | E: Into + Send + Sync, 36 | { 37 | pub async fn send(&self, msg: Vec) -> Result<(), Error> { 38 | let mut inbox = self.inbox.lock().await; 39 | match inbox.send(msg).await { 40 | Ok(_) => Ok(()), 41 | Err(err) => Err(err.into()), 42 | } 43 | } 44 | 45 | pub async fn close(self) -> Result<(), E> { 46 | let mut inbox = self.inbox.lock().await; 47 | inbox.close().await 48 | } 49 | 50 | pub fn sink(&self) -> Weak> { 51 | Arc::downgrade(&self.inbox) 52 | } 53 | } 54 | 55 | impl Connection 56 | where 57 | Stream: StreamExt, E>> + Send + Sync + Unpin + 'static, 58 | Sink: SinkExt, Error = E> + Send + Sync + Unpin + 'static, 59 | E: Into + Send + Sync, 60 | { 61 | /// Wraps incoming [WebSocket] connection and supplied [Awareness] accessor into a new 62 | /// connection handler capable of exchanging Yrs/Yjs messages. 63 | /// 64 | /// While creation of new [WarpConn] always succeeds, a connection itself can possibly fail 65 | /// while processing incoming input/output. This can be detected by awaiting for returned 66 | /// [WarpConn] and handling the awaited result. 67 | pub fn new(awareness: Arc>, sink: Sink, stream: Stream) -> Self { 68 | Self::with_protocol(awareness, sink, stream, DefaultProtocol) 69 | } 70 | 71 | /// Returns an underlying [Awareness] structure, that contains client state of that connection. 72 | pub fn awareness(&self) -> &Arc> { 73 | &self.awareness 74 | } 75 | 76 | /// Wraps incoming [WebSocket] connection and supplied [Awareness] accessor into a new 77 | /// connection handler capable of exchanging Yrs/Yjs messages. 78 | /// 79 | /// While creation of new [WarpConn] always succeeds, a connection itself can possibly fail 80 | /// while processing incoming input/output. This can be detected by awaiting for returned 81 | /// [WarpConn] and handling the awaited result. 82 | pub fn with_protocol

( 83 | awareness: Arc>, 84 | sink: Sink, 85 | mut stream: Stream, 86 | protocol: P, 87 | ) -> Self 88 | where 89 | P: Protocol + Send + Sync + 'static, 90 | { 91 | let sink = Arc::new(Mutex::new(sink)); 92 | let inbox = sink.clone(); 93 | let loop_sink = Arc::downgrade(&sink); 94 | let loop_awareness = Arc::downgrade(&awareness); 95 | let processing_loop: JoinHandle> = spawn(async move { 96 | // at the beginning send SyncStep1 and AwarenessUpdate 97 | let payload = { 98 | let awareness = loop_awareness.upgrade().unwrap(); 99 | let mut encoder = EncoderV1::new(); 100 | let awareness = awareness.read().await; 101 | protocol.start(&awareness, &mut encoder)?; 102 | encoder.to_vec() 103 | }; 104 | if !payload.is_empty() { 105 | if let Some(sink) = loop_sink.upgrade() { 106 | let mut s = sink.lock().await; 107 | if let Err(e) = s.send(payload).await { 108 | return Err(e.into()); 109 | } 110 | } else { 111 | return Ok(()); // parent ConnHandler has been dropped 112 | } 113 | } 114 | 115 | while let Some(input) = stream.next().await { 116 | match input { 117 | Ok(data) => { 118 | if let Some(mut sink) = loop_sink.upgrade() { 119 | if let Some(awareness) = loop_awareness.upgrade() { 120 | match Self::process(&protocol, &awareness, &mut sink, data).await { 121 | Ok(()) => { /* continue */ } 122 | Err(e) => { 123 | return Err(e); 124 | } 125 | } 126 | } else { 127 | return Ok(()); // parent ConnHandler has been dropped 128 | } 129 | } else { 130 | return Ok(()); // parent ConnHandler has been dropped 131 | } 132 | } 133 | Err(e) => return Err(e.into()), 134 | } 135 | } 136 | 137 | Ok(()) 138 | }); 139 | Connection { 140 | processing_loop, 141 | awareness, 142 | inbox, 143 | _stream: PhantomData::default(), 144 | } 145 | } 146 | 147 | async fn process( 148 | protocol: &P, 149 | awareness: &Arc>, 150 | sink: &mut Arc>, 151 | input: Vec, 152 | ) -> Result<(), Error> { 153 | let mut decoder = DecoderV1::new(Cursor::new(&input)); 154 | let reader = MessageReader::new(&mut decoder); 155 | for r in reader { 156 | let msg = r?; 157 | if let Some(reply) = handle_msg(protocol, &awareness, msg).await? { 158 | let mut sender = sink.lock().await; 159 | if let Err(e) = sender.send(reply.encode_v1()).await { 160 | println!("connection failed to send back the reply"); 161 | return Err(e.into()); 162 | } else { 163 | println!("connection send back the reply"); 164 | } 165 | } 166 | } 167 | Ok(()) 168 | } 169 | } 170 | 171 | impl Unpin for Connection {} 172 | 173 | impl Future for Connection { 174 | type Output = Result<(), Error>; 175 | 176 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 177 | match Pin::new(&mut self.processing_loop).poll(cx) { 178 | Poll::Pending => Poll::Pending, 179 | Poll::Ready(Err(e)) => Poll::Ready(Err(Error::Other(e.into()))), 180 | Poll::Ready(Ok(r)) => Poll::Ready(r), 181 | } 182 | } 183 | } 184 | 185 | pub async fn handle_msg( 186 | protocol: &P, 187 | a: &Arc>, 188 | msg: Message, 189 | ) -> Result, Error> { 190 | match msg { 191 | Message::Sync(msg) => match msg { 192 | SyncMessage::SyncStep1(sv) => { 193 | let awareness = a.read().await; 194 | protocol.handle_sync_step1(&awareness, sv) 195 | } 196 | SyncMessage::SyncStep2(update) => { 197 | let mut awareness = a.write().await; 198 | protocol.handle_sync_step2(&mut awareness, Update::decode_v1(&update)?) 199 | } 200 | SyncMessage::Update(update) => { 201 | let mut awareness = a.write().await; 202 | protocol.handle_update(&mut awareness, Update::decode_v1(&update)?) 203 | } 204 | }, 205 | Message::Auth(reason) => { 206 | let awareness = a.read().await; 207 | protocol.handle_auth(&awareness, reason) 208 | } 209 | Message::AwarenessQuery => { 210 | let awareness = a.read().await; 211 | protocol.handle_awareness_query(&awareness) 212 | } 213 | Message::Awareness(update) => { 214 | let mut awareness = a.write().await; 215 | protocol.handle_awareness_update(&mut awareness, update) 216 | } 217 | Message::Custom(tag, data) => { 218 | let mut awareness = a.write().await; 219 | protocol.missing_handle(&mut awareness, tag, data) 220 | } 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | mod test { 226 | use crate::broadcast::BroadcastGroup; 227 | use crate::conn::Connection; 228 | use bytes::{Bytes, BytesMut}; 229 | use futures_util::SinkExt; 230 | use std::net::SocketAddr; 231 | use std::str::FromStr; 232 | use std::sync::Arc; 233 | use std::time::Duration; 234 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 235 | use tokio::net::{TcpListener, TcpSocket}; 236 | use tokio::sync::{Mutex, Notify, RwLock}; 237 | use tokio::task; 238 | use tokio::task::JoinHandle; 239 | use tokio::time::{sleep, timeout}; 240 | use tokio_util::codec::{Decoder, Encoder, FramedRead, FramedWrite, LengthDelimitedCodec}; 241 | use yrs::sync::{Awareness, Error, Message, SyncMessage}; 242 | use yrs::updates::encoder::Encode; 243 | use yrs::{Doc, GetString, Subscription, Text, Transact}; 244 | 245 | #[derive(Debug, Default)] 246 | struct YrsCodec(LengthDelimitedCodec); 247 | 248 | impl Encoder> for YrsCodec { 249 | type Error = Error; 250 | 251 | fn encode(&mut self, item: Vec, dst: &mut BytesMut) -> Result<(), Self::Error> { 252 | self.0.encode(Bytes::from(item), dst)?; 253 | Ok(()) 254 | } 255 | } 256 | 257 | impl Decoder for YrsCodec { 258 | type Item = Vec; 259 | type Error = Error; 260 | 261 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 262 | if let Some(bytes) = self.0.decode(src)? { 263 | Ok(Some(bytes.freeze().to_vec())) 264 | } else { 265 | Ok(None) 266 | } 267 | } 268 | } 269 | 270 | type WrappedStream = FramedRead; 271 | type WrappedSink = FramedWrite; 272 | 273 | async fn start_server( 274 | addr: SocketAddr, 275 | bcast: BroadcastGroup, 276 | ) -> Result, Box> { 277 | let server = TcpListener::bind(addr).await?; 278 | Ok(tokio::spawn(async move { 279 | let mut subscribers = Vec::new(); 280 | while let Ok((stream, _)) = server.accept().await { 281 | let (reader, writer) = stream.into_split(); 282 | let stream = WrappedStream::new(reader, YrsCodec::default()); 283 | let sink = WrappedSink::new(writer, YrsCodec::default()); 284 | let sub = bcast.subscribe(Arc::new(Mutex::new(sink)), stream); 285 | subscribers.push(sub); 286 | } 287 | })) 288 | } 289 | 290 | async fn client( 291 | addr: SocketAddr, 292 | doc: Doc, 293 | ) -> Result, Box> { 294 | let stream = TcpSocket::new_v4()?.connect(addr).await?; 295 | let (reader, writer) = stream.into_split(); 296 | let stream: WrappedStream = WrappedStream::new(reader, YrsCodec::default()); 297 | let sink: WrappedSink = WrappedSink::new(writer, YrsCodec::default()); 298 | Ok(Connection::new( 299 | Arc::new(RwLock::new(Awareness::new(doc))), 300 | sink, 301 | stream, 302 | )) 303 | } 304 | 305 | fn create_notifier(doc: &Doc) -> (Arc, Subscription) { 306 | let n = Arc::new(Notify::new()); 307 | let sub = { 308 | let n = n.clone(); 309 | doc.observe_update_v1(move |_, _| n.notify_waiters()) 310 | .unwrap() 311 | }; 312 | (n, sub) 313 | } 314 | 315 | const TIMEOUT: Duration = Duration::from_secs(5); 316 | 317 | #[tokio::test] 318 | async fn change_introduced_by_server_reaches_subscribed_clients( 319 | ) -> Result<(), Box> { 320 | let server_addr = SocketAddr::from_str("127.0.0.1:6600").unwrap(); 321 | let doc = Doc::with_client_id(1); 322 | let text = doc.get_or_insert_text("test"); 323 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 324 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 325 | let _server = start_server(server_addr.clone(), bcast).await?; 326 | 327 | let doc = Doc::new(); 328 | let (n, _sub) = create_notifier(&doc); 329 | let c1 = client(server_addr.clone(), doc).await?; 330 | 331 | { 332 | let lock = awareness.write().await; 333 | text.push(&mut lock.doc().transact_mut(), "abc"); 334 | } 335 | 336 | timeout(TIMEOUT, n.notified()).await?; 337 | 338 | { 339 | let awareness = c1.awareness().read().await; 340 | let doc = awareness.doc(); 341 | let text = doc.get_or_insert_text("test"); 342 | let str = text.get_string(&doc.transact()); 343 | assert_eq!(str, "abc".to_string()); 344 | } 345 | 346 | Ok(()) 347 | } 348 | 349 | #[tokio::test] 350 | async fn subscribed_client_fetches_initial_state() -> Result<(), Box> { 351 | let server_addr = SocketAddr::from_str("127.0.0.1:6601").unwrap(); 352 | let doc = Doc::with_client_id(1); 353 | let text = doc.get_or_insert_text("test"); 354 | 355 | text.push(&mut doc.transact_mut(), "abc"); 356 | 357 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 358 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 359 | let _server = start_server(server_addr.clone(), bcast).await?; 360 | 361 | let doc = Doc::new(); 362 | let (n, _sub) = create_notifier(&doc); 363 | let c1 = client(server_addr.clone(), doc).await?; 364 | 365 | timeout(TIMEOUT, n.notified()).await?; 366 | 367 | { 368 | let awareness = c1.awareness().read().await; 369 | let doc = awareness.doc(); 370 | let text = doc.get_or_insert_text("test"); 371 | let str = text.get_string(&doc.transact()); 372 | assert_eq!(str, "abc".to_string()); 373 | } 374 | 375 | Ok(()) 376 | } 377 | 378 | #[tokio::test] 379 | async fn changes_from_one_client_reach_others() -> Result<(), Box> { 380 | let server_addr = SocketAddr::from_str("127.0.0.1:6602").unwrap(); 381 | let doc = Doc::with_client_id(1); 382 | let _text = doc.get_or_insert_text("test"); 383 | 384 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 385 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 386 | let _server = start_server(server_addr.clone(), bcast).await?; 387 | 388 | let d1 = Doc::with_client_id(2); 389 | let c1 = client(server_addr.clone(), d1).await?; 390 | // by default changes made by document on the client side are not propagated automatically 391 | let _sub11 = { 392 | let sink = c1.sink(); 393 | let a = c1.awareness().write().await; 394 | let doc = a.doc(); 395 | doc.observe_update_v1(move |_, e| { 396 | let update = e.update.to_owned(); 397 | if let Some(sink) = sink.upgrade() { 398 | task::spawn(async move { 399 | let msg = Message::Sync(SyncMessage::Update(update)).encode_v1(); 400 | let mut sink = sink.lock().await; 401 | sink.send(msg).await.unwrap(); 402 | }); 403 | } 404 | }) 405 | .unwrap() 406 | }; 407 | 408 | let d2 = Doc::with_client_id(3); 409 | let (n2, _sub2) = create_notifier(&d2); 410 | let c2 = client(server_addr.clone(), d2).await?; 411 | 412 | { 413 | let a = c1.awareness().write().await; 414 | let doc = a.doc(); 415 | let text = doc.get_or_insert_text("test"); 416 | text.push(&mut doc.transact_mut(), "def"); 417 | } 418 | 419 | timeout(TIMEOUT, n2.notified()).await?; 420 | 421 | { 422 | let awareness = c2.awareness.read().await; 423 | let doc = awareness.doc(); 424 | let text = doc.get_or_insert_text("test"); 425 | let str = text.get_string(&doc.transact()); 426 | assert_eq!(str, "def".to_string()); 427 | } 428 | 429 | Ok(()) 430 | } 431 | 432 | #[tokio::test] 433 | async fn client_failure_doesnt_affect_others() -> Result<(), Box> { 434 | let server_addr = SocketAddr::from_str("127.0.0.1:6604").unwrap(); 435 | let doc = Doc::with_client_id(1); 436 | let _ = doc.get_or_insert_text("test"); 437 | 438 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 439 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 440 | let _server = start_server(server_addr.clone(), bcast).await?; 441 | 442 | let d1 = Doc::with_client_id(2); 443 | let c1 = client(server_addr.clone(), d1).await?; 444 | // by default changes made by document on the client side are not propagated automatically 445 | let _sub11 = { 446 | let sink = c1.sink(); 447 | let a = c1.awareness().write().await; 448 | let doc = a.doc(); 449 | doc.observe_update_v1(move |_, e| { 450 | let update = e.update.to_owned(); 451 | if let Some(sink) = sink.upgrade() { 452 | task::spawn(async move { 453 | let msg = Message::Sync(SyncMessage::Update(update)).encode_v1(); 454 | let mut sink = sink.lock().await; 455 | sink.send(msg).await.unwrap(); 456 | }); 457 | } 458 | }) 459 | .unwrap() 460 | }; 461 | 462 | let d2 = Doc::with_client_id(3); 463 | let (n2, sub2) = create_notifier(&d2); 464 | let c2 = client(server_addr.clone(), d2).await?; 465 | 466 | let d3 = Doc::with_client_id(4); 467 | let (n3, sub3) = create_notifier(&d3); 468 | let c3 = client(server_addr.clone(), d3).await?; 469 | 470 | { 471 | let a = c1.awareness().write().await; 472 | let doc = a.doc(); 473 | let text = doc.get_or_insert_text("test"); 474 | text.push(&mut doc.transact_mut(), "abc"); 475 | } 476 | 477 | // on the first try both C2 and C3 should receive the update 478 | //timeout(TIMEOUT, n2.notified()).await.unwrap(); 479 | //timeout(TIMEOUT, n3.notified()).await.unwrap(); 480 | sleep(TIMEOUT).await; 481 | 482 | { 483 | let awareness = c2.awareness.read().await; 484 | let doc = awareness.doc(); 485 | let text = doc.get_or_insert_text("test"); 486 | let str = text.get_string(&doc.transact()); 487 | assert_eq!(str, "abc".to_string()); 488 | } 489 | { 490 | let awareness = c3.awareness.read().await; 491 | let doc = awareness.doc(); 492 | let text = doc.get_or_insert_text("test"); 493 | let str = text.get_string(&doc.transact()); 494 | assert_eq!(str, "abc".to_string()); 495 | } 496 | 497 | // drop client, causing abrupt ending 498 | drop(c3); 499 | drop(n3); 500 | drop(sub3); 501 | // C2 notification subscription has been realized, we need to refresh it 502 | drop(n2); 503 | drop(sub2); 504 | 505 | let (n2, _sub2) = { 506 | let a = c2.awareness().write().await; 507 | let doc = a.doc(); 508 | create_notifier(doc) 509 | }; 510 | 511 | { 512 | let a = c1.awareness().write().await; 513 | let doc = a.doc(); 514 | let text = doc.get_or_insert_text("test"); 515 | text.push(&mut doc.transact_mut(), "def"); 516 | } 517 | 518 | timeout(TIMEOUT, n2.notified()).await.unwrap(); 519 | 520 | { 521 | let awareness = c2.awareness.read().await; 522 | let doc = awareness.doc(); 523 | let text = doc.get_or_insert_text("test"); 524 | let str = text.get_string(&doc.transact()); 525 | assert_eq!(str, "abcdef".to_string()); 526 | } 527 | 528 | Ok(()) 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tokio::sync::RwLock; 3 | 4 | pub mod broadcast; 5 | pub mod conn; 6 | pub mod signaling; 7 | pub mod ws; 8 | 9 | pub type AwarenessRef = Arc>; 10 | -------------------------------------------------------------------------------- /src/signaling.rs: -------------------------------------------------------------------------------- 1 | use futures_util::stream::SplitSink; 2 | use futures_util::{SinkExt, StreamExt}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::{HashMap, HashSet}; 5 | use std::hash::{Hash, Hasher}; 6 | use std::sync::Arc; 7 | use std::time::Duration; 8 | use tokio::select; 9 | use tokio::sync::{Mutex, RwLock}; 10 | use tokio::time::interval; 11 | use warp::ws::{Message, WebSocket}; 12 | use warp::Error; 13 | 14 | const PING_TIMEOUT: Duration = Duration::from_secs(30); 15 | 16 | /// Signaling service is used by y-webrtc protocol in order to exchange WebRTC offerings between 17 | /// clients subscribing to particular rooms. 18 | /// 19 | /// # Example 20 | /// 21 | /// ```rust 22 | /// use warp::{Filter, Rejection, Reply}; 23 | /// use warp::ws::{Ws, WebSocket}; 24 | /// use yrs_warp::signaling::{SignalingService, signaling_conn}; 25 | /// 26 | /// fn main() { 27 | /// let signaling = SignalingService::new(); 28 | /// let ws = warp::path("signaling") 29 | /// .and(warp::ws()) 30 | /// .and(warp::any().map(move || signaling.clone())) 31 | /// .and_then(ws_handler); 32 | /// 33 | /// //warp::serve(routes).run(([0, 0, 0, 0], 8000)).await; 34 | /// } 35 | /// 36 | /// async fn ws_handler(ws: Ws, svc: SignalingService) -> Result { 37 | /// Ok(ws.on_upgrade(move |socket| peer(socket, svc))) 38 | /// } 39 | /// 40 | /// async fn peer(ws: WebSocket, svc: SignalingService) { 41 | /// match signaling_conn(ws, svc).await { 42 | /// Ok(_) => println!("signaling connection stopped"), 43 | /// Err(e) => eprintln!("signaling connection failed: {}", e), 44 | /// } 45 | /// } 46 | /// ``` 47 | #[derive(Debug, Clone)] 48 | pub struct SignalingService(Topics); 49 | 50 | impl SignalingService { 51 | pub fn new() -> Self { 52 | SignalingService(Arc::new(RwLock::new(Default::default()))) 53 | } 54 | 55 | pub async fn publish(&self, topic: &str, msg: Message) -> Result<(), Error> { 56 | let mut failed = Vec::new(); 57 | { 58 | let topics = self.0.read().await; 59 | if let Some(subs) = topics.get(topic) { 60 | let client_count = subs.len(); 61 | tracing::info!("publishing message to {client_count} clients: {msg:?}"); 62 | for sub in subs { 63 | if let Err(e) = sub.try_send(msg.clone()).await { 64 | tracing::info!("failed to send {msg:?}: {e}"); 65 | failed.push(sub.clone()); 66 | } 67 | } 68 | } 69 | } 70 | if !failed.is_empty() { 71 | let mut topics = self.0.write().await; 72 | if let Some(subs) = topics.get_mut(topic) { 73 | for f in failed { 74 | subs.remove(&f); 75 | } 76 | } 77 | } 78 | Ok(()) 79 | } 80 | 81 | pub async fn close_topic(&self, topic: &str) -> Result<(), Error> { 82 | let mut topics = self.0.write().await; 83 | if let Some(subs) = topics.remove(topic) { 84 | for sub in subs { 85 | if let Err(e) = sub.close().await { 86 | tracing::warn!("failed to close connection on topic '{topic}': {e}"); 87 | } 88 | } 89 | } 90 | Ok(()) 91 | } 92 | 93 | pub async fn close(self) -> Result<(), Error> { 94 | let mut topics = self.0.write_owned().await; 95 | let mut all_conns = HashSet::new(); 96 | for (_, subs) in topics.drain() { 97 | for sub in subs { 98 | all_conns.insert(sub); 99 | } 100 | } 101 | 102 | for conn in all_conns { 103 | if let Err(e) = conn.close().await { 104 | tracing::warn!("failed to close connection: {e}"); 105 | } 106 | } 107 | 108 | Ok(()) 109 | } 110 | } 111 | 112 | impl Default for SignalingService { 113 | fn default() -> Self { 114 | Self::new() 115 | } 116 | } 117 | 118 | type Topics = Arc, HashSet>>>; 119 | 120 | #[derive(Debug, Clone)] 121 | struct WsSink(Arc>>); 122 | 123 | impl WsSink { 124 | fn new(sink: SplitSink) -> Self { 125 | WsSink(Arc::new(Mutex::new(sink))) 126 | } 127 | 128 | async fn try_send(&self, msg: Message) -> Result<(), Error> { 129 | let mut sink = self.0.lock().await; 130 | if let Err(e) = sink.send(msg).await { 131 | sink.close().await?; 132 | Err(e) 133 | } else { 134 | Ok(()) 135 | } 136 | } 137 | 138 | async fn close(&self) -> Result<(), Error> { 139 | let mut sink = self.0.lock().await; 140 | sink.close().await 141 | } 142 | } 143 | 144 | impl Hash for WsSink { 145 | fn hash(&self, state: &mut H) { 146 | let ptr = Arc::as_ptr(&self.0) as usize; 147 | ptr.hash(state); 148 | } 149 | } 150 | 151 | impl PartialEq for WsSink { 152 | fn eq(&self, other: &Self) -> bool { 153 | Arc::ptr_eq(&self.0, &other.0) 154 | } 155 | } 156 | 157 | impl Eq for WsSink {} 158 | 159 | /// Handle incoming signaling connection - it's a websocket connection used by y-webrtc protocol 160 | /// to exchange offering metadata between y-webrtc peers. It also manages topic/room access. 161 | pub async fn signaling_conn(ws: WebSocket, service: SignalingService) -> Result<(), Error> { 162 | let mut topics: Topics = service.0; 163 | let (sink, mut stream) = ws.split(); 164 | let ws = WsSink::new(sink); 165 | let mut ping_interval = interval(PING_TIMEOUT); 166 | let mut state = ConnState::default(); 167 | loop { 168 | select! { 169 | _ = ping_interval.tick() => { 170 | if !state.pong_received { 171 | ws.close().await?; 172 | drop(ping_interval); 173 | return Ok(()); 174 | } else { 175 | state.pong_received = false; 176 | if let Err(e) = ws.try_send(Message::ping(Vec::default())).await { 177 | ws.close().await?; 178 | return Err(e); 179 | } 180 | } 181 | }, 182 | res = stream.next() => { 183 | match res { 184 | None => { 185 | ws.close().await?; 186 | return Ok(()); 187 | }, 188 | Some(Err(e)) => { 189 | ws.close().await?; 190 | return Err(e); 191 | }, 192 | Some(Ok(msg)) => { 193 | process_msg(msg, &ws, &mut state, &mut topics).await?; 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | const PING_MSG: &'static str = r#"{"type":"ping"}"#; 202 | const PONG_MSG: &'static str = r#"{"type":"pong"}"#; 203 | 204 | async fn process_msg( 205 | msg: Message, 206 | ws: &WsSink, 207 | state: &mut ConnState, 208 | topics: &mut Topics, 209 | ) -> Result<(), Error> { 210 | if msg.is_text() { 211 | let json = msg.to_str().unwrap(); 212 | let msg = serde_json::from_str(json).unwrap(); 213 | match msg { 214 | Signal::Subscribe { 215 | topics: topic_names, 216 | } => { 217 | if !topic_names.is_empty() { 218 | let mut topics = topics.write().await; 219 | for topic in topic_names { 220 | tracing::trace!("subscribing new client to '{topic}'"); 221 | if let Some((key, _)) = topics.get_key_value(topic) { 222 | state.subscribed_topics.insert(key.clone()); 223 | let subs = topics.get_mut(topic).unwrap(); 224 | subs.insert(ws.clone()); 225 | } else { 226 | let topic: Arc = topic.into(); 227 | state.subscribed_topics.insert(topic.clone()); 228 | let mut subs = HashSet::new(); 229 | subs.insert(ws.clone()); 230 | topics.insert(topic, subs); 231 | }; 232 | } 233 | } 234 | } 235 | Signal::Unsubscribe { 236 | topics: topic_names, 237 | } => { 238 | if !topic_names.is_empty() { 239 | let mut topics = topics.write().await; 240 | for topic in topic_names { 241 | if let Some(subs) = topics.get_mut(topic) { 242 | tracing::trace!("unsubscribing client from '{topic}'"); 243 | subs.remove(ws); 244 | } 245 | } 246 | } 247 | } 248 | Signal::Publish { topic } => { 249 | let mut failed = Vec::new(); 250 | { 251 | let topics = topics.read().await; 252 | if let Some(receivers) = topics.get(topic) { 253 | let client_count = receivers.len(); 254 | tracing::trace!( 255 | "publishing on {client_count} clients at '{topic}': {json}" 256 | ); 257 | for receiver in receivers.iter() { 258 | if let Err(e) = receiver.try_send(Message::text(json)).await { 259 | tracing::info!( 260 | "failed to publish message {json} on '{topic}': {e}" 261 | ); 262 | failed.push(receiver.clone()); 263 | } 264 | } 265 | } 266 | } 267 | if !failed.is_empty() { 268 | let mut topics = topics.write().await; 269 | if let Some(receivers) = topics.get_mut(topic) { 270 | for f in failed { 271 | receivers.remove(&f); 272 | } 273 | } 274 | } 275 | } 276 | Signal::Ping => { 277 | ws.try_send(Message::text(PONG_MSG)).await?; 278 | } 279 | Signal::Pong => { 280 | ws.try_send(Message::text(PING_MSG)).await?; 281 | } 282 | } 283 | } else if msg.is_close() { 284 | let mut topics = topics.write().await; 285 | for topic in state.subscribed_topics.drain() { 286 | if let Some(subs) = topics.get_mut(&topic) { 287 | subs.remove(ws); 288 | if subs.is_empty() { 289 | topics.remove(&topic); 290 | } 291 | } 292 | } 293 | state.closed = true; 294 | } else if msg.is_ping() { 295 | ws.try_send(Message::ping(Vec::default())).await?; 296 | } 297 | Ok(()) 298 | } 299 | 300 | #[derive(Debug)] 301 | struct ConnState { 302 | closed: bool, 303 | pong_received: bool, 304 | subscribed_topics: HashSet>, 305 | } 306 | 307 | impl Default for ConnState { 308 | fn default() -> Self { 309 | ConnState { 310 | closed: false, 311 | pong_received: true, 312 | subscribed_topics: HashSet::new(), 313 | } 314 | } 315 | } 316 | 317 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 318 | #[serde(tag = "type")] 319 | pub(crate) enum Signal<'a> { 320 | #[serde(rename = "publish")] 321 | Publish { topic: &'a str }, 322 | #[serde(rename = "subscribe")] 323 | Subscribe { topics: Vec<&'a str> }, 324 | #[serde(rename = "unsubscribe")] 325 | Unsubscribe { topics: Vec<&'a str> }, 326 | #[serde(rename = "ping")] 327 | Ping, 328 | #[serde(rename = "pong")] 329 | Pong, 330 | } 331 | -------------------------------------------------------------------------------- /src/ws.rs: -------------------------------------------------------------------------------- 1 | use crate::conn::Connection; 2 | use crate::AwarenessRef; 3 | use futures_util::stream::{SplitSink, SplitStream}; 4 | use futures_util::{Stream, StreamExt}; 5 | use std::pin::Pin; 6 | use std::task::{Context, Poll}; 7 | use warp::ws::{Message, WebSocket}; 8 | use yrs::sync::Error; 9 | 10 | /// Connection Wrapper over a [WebSocket], which implements a Yjs/Yrs awareness and update exchange 11 | /// protocol. 12 | /// 13 | /// This connection implements Future pattern and can be awaited upon in order for a caller to 14 | /// recognize whether underlying websocket connection has been finished gracefully or abruptly. 15 | #[repr(transparent)] 16 | #[derive(Debug)] 17 | pub struct WarpConn(Connection); 18 | 19 | impl WarpConn { 20 | pub fn new(awareness: AwarenessRef, socket: WebSocket) -> Self { 21 | let (sink, stream) = socket.split(); 22 | let conn = Connection::new(awareness, WarpSink(sink), WarpStream(stream)); 23 | WarpConn(conn) 24 | } 25 | } 26 | 27 | impl core::future::Future for WarpConn { 28 | type Output = Result<(), Error>; 29 | 30 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 31 | match Pin::new(&mut self.0).poll(cx) { 32 | Poll::Pending => Poll::Pending, 33 | Poll::Ready(Err(e)) => Poll::Ready(Err(Error::Other(e.into()))), 34 | Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), 35 | } 36 | } 37 | } 38 | 39 | /// A warp websocket sink wrapper, that implements futures `Sink` in a way, that makes it compatible 40 | /// with y-sync protocol, so that it can be used by y-sync crate [BroadcastGroup]. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ```rust 45 | /// use std::net::SocketAddr; 46 | /// use std::str::FromStr; 47 | /// use std::sync::Arc; 48 | /// use futures_util::StreamExt; 49 | /// use tokio::sync::Mutex; 50 | /// use tokio::task::JoinHandle; 51 | /// use warp::{Filter, Rejection, Reply}; 52 | /// use warp::ws::{WebSocket, Ws}; 53 | /// use yrs_warp::broadcast::BroadcastGroup; 54 | /// use yrs_warp::ws::{WarpSink, WarpStream}; 55 | /// 56 | /// async fn start_server( 57 | /// addr: &str, 58 | /// bcast: Arc, 59 | /// ) -> Result, Box> { 60 | /// let addr = SocketAddr::from_str(addr)?; 61 | /// let ws = warp::path("my-room") 62 | /// .and(warp::ws()) 63 | /// .and(warp::any().map(move || bcast.clone())) 64 | /// .and_then(ws_handler); 65 | /// 66 | /// Ok(tokio::spawn(async move { 67 | /// warp::serve(ws).run(addr).await; 68 | /// })) 69 | /// } 70 | /// 71 | /// async fn ws_handler(ws: Ws, bcast: Arc) -> Result { 72 | /// Ok(ws.on_upgrade(move |socket| peer(socket, bcast))) 73 | /// } 74 | /// 75 | /// async fn peer(ws: WebSocket, bcast: Arc) { 76 | /// let (sink, stream) = ws.split(); 77 | /// // convert warp web socket into compatible sink/stream 78 | /// let sink = Arc::new(Mutex::new(WarpSink::from(sink))); 79 | /// let stream = WarpStream::from(stream); 80 | /// // subscribe to broadcast group 81 | /// let sub = bcast.subscribe(sink, stream); 82 | /// // wait for subscribed connection to close itself 83 | /// match sub.completed().await { 84 | /// Ok(_) => println!("broadcasting for channel finished successfully"), 85 | /// Err(e) => eprintln!("broadcasting for channel finished abruptly: {}", e), 86 | /// } 87 | /// } 88 | /// ``` 89 | #[repr(transparent)] 90 | #[derive(Debug)] 91 | pub struct WarpSink(SplitSink); 92 | 93 | impl From> for WarpSink { 94 | fn from(sink: SplitSink) -> Self { 95 | WarpSink(sink) 96 | } 97 | } 98 | 99 | impl Into> for WarpSink { 100 | fn into(self) -> SplitSink { 101 | self.0 102 | } 103 | } 104 | 105 | impl futures_util::Sink> for WarpSink { 106 | type Error = Error; 107 | 108 | fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 109 | match Pin::new(&mut self.0).poll_ready(cx) { 110 | Poll::Pending => Poll::Pending, 111 | Poll::Ready(Err(e)) => Poll::Ready(Err(Error::Other(e.into()))), 112 | Poll::Ready(_) => Poll::Ready(Ok(())), 113 | } 114 | } 115 | 116 | fn start_send(mut self: Pin<&mut Self>, item: Vec) -> Result<(), Self::Error> { 117 | if let Err(e) = Pin::new(&mut self.0).start_send(Message::binary(item)) { 118 | Err(Error::Other(e.into())) 119 | } else { 120 | Ok(()) 121 | } 122 | } 123 | 124 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 125 | match Pin::new(&mut self.0).poll_flush(cx) { 126 | Poll::Pending => Poll::Pending, 127 | Poll::Ready(Err(e)) => Poll::Ready(Err(Error::Other(e.into()))), 128 | Poll::Ready(_) => Poll::Ready(Ok(())), 129 | } 130 | } 131 | 132 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 133 | match Pin::new(&mut self.0).poll_close(cx) { 134 | Poll::Pending => Poll::Pending, 135 | Poll::Ready(Err(e)) => Poll::Ready(Err(Error::Other(e.into()))), 136 | Poll::Ready(_) => Poll::Ready(Ok(())), 137 | } 138 | } 139 | } 140 | 141 | /// A warp websocket stream wrapper, that implements futures `Stream` in a way, that makes it compatible 142 | /// with y-sync protocol, so that it can be used by y-sync crate [BroadcastGroup]. 143 | /// 144 | /// # Examples 145 | /// 146 | /// ```rust 147 | /// use std::net::SocketAddr; 148 | /// use std::str::FromStr; 149 | /// use std::sync::Arc; 150 | /// use futures_util::StreamExt; 151 | /// use tokio::sync::Mutex; 152 | /// use tokio::task::JoinHandle; 153 | /// use warp::{Filter, Rejection, Reply}; 154 | /// use warp::ws::{WebSocket, Ws}; 155 | /// use yrs_warp::broadcast::BroadcastGroup; 156 | /// use yrs_warp::ws::{WarpSink, WarpStream}; 157 | /// 158 | /// async fn start_server( 159 | /// addr: &str, 160 | /// bcast: Arc, 161 | /// ) -> Result, Box> { 162 | /// let addr = SocketAddr::from_str(addr)?; 163 | /// let ws = warp::path("my-room") 164 | /// .and(warp::ws()) 165 | /// .and(warp::any().map(move || bcast.clone())) 166 | /// .and_then(ws_handler); 167 | /// 168 | /// Ok(tokio::spawn(async move { 169 | /// warp::serve(ws).run(addr).await; 170 | /// })) 171 | /// } 172 | /// 173 | /// async fn ws_handler(ws: Ws, bcast: Arc) -> Result { 174 | /// Ok(ws.on_upgrade(move |socket| peer(socket, bcast))) 175 | /// } 176 | /// 177 | /// async fn peer(ws: WebSocket, bcast: Arc) { 178 | /// let (sink, stream) = ws.split(); 179 | /// // convert warp web socket into compatible sink/stream 180 | /// let sink = Arc::new(Mutex::new(WarpSink::from(sink))); 181 | /// let stream = WarpStream::from(stream); 182 | /// // subscribe to broadcast group 183 | /// let sub = bcast.subscribe(sink, stream); 184 | /// // wait for subscribed connection to close itself 185 | /// match sub.completed().await { 186 | /// Ok(_) => println!("broadcasting for channel finished successfully"), 187 | /// Err(e) => eprintln!("broadcasting for channel finished abruptly: {}", e), 188 | /// } 189 | /// } 190 | /// ``` 191 | #[derive(Debug)] 192 | pub struct WarpStream(SplitStream); 193 | 194 | impl From> for WarpStream { 195 | fn from(stream: SplitStream) -> Self { 196 | WarpStream(stream) 197 | } 198 | } 199 | 200 | impl Into> for WarpStream { 201 | fn into(self) -> SplitStream { 202 | self.0 203 | } 204 | } 205 | 206 | impl Stream for WarpStream { 207 | type Item = Result, Error>; 208 | 209 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 210 | match Pin::new(&mut self.0).poll_next(cx) { 211 | Poll::Pending => Poll::Pending, 212 | Poll::Ready(None) => Poll::Ready(None), 213 | Poll::Ready(Some(res)) => match res { 214 | Ok(item) => Poll::Ready(Some(Ok(item.into_bytes()))), 215 | Err(e) => Poll::Ready(Some(Err(Error::Other(e.into())))), 216 | }, 217 | } 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod test { 223 | use crate::broadcast::BroadcastGroup; 224 | use crate::conn::Connection; 225 | use crate::ws::{WarpSink, WarpStream}; 226 | use futures_util::stream::{SplitSink, SplitStream}; 227 | use futures_util::{ready, SinkExt, Stream, StreamExt}; 228 | use std::net::SocketAddr; 229 | use std::pin::Pin; 230 | use std::str::FromStr; 231 | use std::sync::Arc; 232 | use std::task::{Context, Poll}; 233 | use std::time::Duration; 234 | use tokio::net::TcpStream; 235 | use tokio::sync::{Mutex, Notify, RwLock}; 236 | use tokio::task; 237 | use tokio::task::JoinHandle; 238 | use tokio::time::{sleep, timeout}; 239 | use tokio_tungstenite::tungstenite::Message; 240 | use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; 241 | use warp::ws::{WebSocket, Ws}; 242 | use warp::{Filter, Rejection, Reply, Sink}; 243 | use yrs::sync::{Awareness, Error}; 244 | use yrs::updates::encoder::Encode; 245 | use yrs::{Doc, GetString, Subscription, Text, Transact}; 246 | 247 | async fn start_server( 248 | addr: &str, 249 | bcast: Arc, 250 | ) -> Result, Box> { 251 | let addr = SocketAddr::from_str(addr)?; 252 | let ws = warp::path("my-room") 253 | .and(warp::ws()) 254 | .and(warp::any().map(move || bcast.clone())) 255 | .and_then(ws_handler); 256 | 257 | Ok(tokio::spawn(async move { 258 | warp::serve(ws).run(addr).await; 259 | })) 260 | } 261 | 262 | async fn ws_handler(ws: Ws, bcast: Arc) -> Result { 263 | Ok(ws.on_upgrade(move |socket| peer(socket, bcast))) 264 | } 265 | 266 | async fn peer(ws: WebSocket, bcast: Arc) { 267 | let (sink, stream) = ws.split(); 268 | let sink = Arc::new(Mutex::new(WarpSink::from(sink))); 269 | let stream = WarpStream::from(stream); 270 | let sub = bcast.subscribe(sink, stream); 271 | match sub.completed().await { 272 | Ok(_) => println!("broadcasting for channel finished successfully"), 273 | Err(e) => eprintln!("broadcasting for channel finished abruptly: {}", e), 274 | } 275 | } 276 | 277 | struct TungsteniteSink(SplitSink>, Message>); 278 | 279 | impl Sink> for TungsteniteSink { 280 | type Error = Error; 281 | 282 | fn poll_ready( 283 | mut self: Pin<&mut Self>, 284 | cx: &mut Context<'_>, 285 | ) -> Poll> { 286 | let sink = unsafe { Pin::new_unchecked(&mut self.0) }; 287 | let result = ready!(sink.poll_ready(cx)); 288 | match result { 289 | Ok(_) => Poll::Ready(Ok(())), 290 | Err(e) => Poll::Ready(Err(Error::Other(Box::new(e)))), 291 | } 292 | } 293 | 294 | fn start_send(mut self: Pin<&mut Self>, item: Vec) -> Result<(), Self::Error> { 295 | let sink = unsafe { Pin::new_unchecked(&mut self.0) }; 296 | let result = sink.start_send(Message::binary(item)); 297 | match result { 298 | Ok(_) => Ok(()), 299 | Err(e) => Err(Error::Other(Box::new(e))), 300 | } 301 | } 302 | 303 | fn poll_flush( 304 | mut self: Pin<&mut Self>, 305 | cx: &mut Context<'_>, 306 | ) -> Poll> { 307 | let sink = unsafe { Pin::new_unchecked(&mut self.0) }; 308 | let result = ready!(sink.poll_flush(cx)); 309 | match result { 310 | Ok(_) => Poll::Ready(Ok(())), 311 | Err(e) => Poll::Ready(Err(Error::Other(Box::new(e)))), 312 | } 313 | } 314 | 315 | fn poll_close( 316 | mut self: Pin<&mut Self>, 317 | cx: &mut Context<'_>, 318 | ) -> Poll> { 319 | let sink = unsafe { Pin::new_unchecked(&mut self.0) }; 320 | let result = ready!(sink.poll_close(cx)); 321 | match result { 322 | Ok(_) => Poll::Ready(Ok(())), 323 | Err(e) => Poll::Ready(Err(Error::Other(Box::new(e)))), 324 | } 325 | } 326 | } 327 | 328 | struct TungsteniteStream(SplitStream>>); 329 | impl Stream for TungsteniteStream { 330 | type Item = Result, Error>; 331 | 332 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 333 | let stream = unsafe { Pin::new_unchecked(&mut self.0) }; 334 | let result = ready!(stream.poll_next(cx)); 335 | match result { 336 | None => Poll::Ready(None), 337 | Some(Ok(msg)) => Poll::Ready(Some(Ok(msg.into_data()))), 338 | Some(Err(e)) => Poll::Ready(Some(Err(Error::Other(Box::new(e))))), 339 | } 340 | } 341 | } 342 | 343 | async fn client( 344 | addr: &str, 345 | doc: Doc, 346 | ) -> Result, Box> { 347 | let (stream, _) = tokio_tungstenite::connect_async(addr).await?; 348 | let (sink, stream) = stream.split(); 349 | let sink = TungsteniteSink(sink); 350 | let stream = TungsteniteStream(stream); 351 | Ok(Connection::new( 352 | Arc::new(RwLock::new(Awareness::new(doc))), 353 | sink, 354 | stream, 355 | )) 356 | } 357 | 358 | fn create_notifier(doc: &Doc) -> (Arc, Subscription) { 359 | let n = Arc::new(Notify::new()); 360 | let sub = { 361 | let n = n.clone(); 362 | doc.observe_update_v1(move |_, _| n.notify_waiters()) 363 | .unwrap() 364 | }; 365 | (n, sub) 366 | } 367 | 368 | const TIMEOUT: Duration = Duration::from_secs(5); 369 | 370 | #[tokio::test] 371 | async fn change_introduced_by_server_reaches_subscribed_clients() { 372 | let doc = Doc::with_client_id(1); 373 | let text = doc.get_or_insert_text("test"); 374 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 375 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 376 | let _server = start_server("0.0.0.0:6600", Arc::new(bcast)).await.unwrap(); 377 | 378 | let doc = Doc::new(); 379 | let (n, _sub) = create_notifier(&doc); 380 | let c1 = client("ws://localhost:6600/my-room", doc).await.unwrap(); 381 | 382 | { 383 | let lock = awareness.write().await; 384 | text.push(&mut lock.doc().transact_mut(), "abc"); 385 | } 386 | 387 | timeout(TIMEOUT, n.notified()).await.unwrap(); 388 | 389 | { 390 | let awareness = c1.awareness().read().await; 391 | let doc = awareness.doc(); 392 | let text = doc.get_or_insert_text("test"); 393 | let str = text.get_string(&doc.transact()); 394 | assert_eq!(str, "abc".to_string()); 395 | } 396 | } 397 | 398 | #[tokio::test] 399 | async fn subscribed_client_fetches_initial_state() { 400 | let doc = Doc::with_client_id(1); 401 | let text = doc.get_or_insert_text("test"); 402 | 403 | text.push(&mut doc.transact_mut(), "abc"); 404 | 405 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 406 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 407 | let _server = start_server("0.0.0.0:6601", Arc::new(bcast)).await.unwrap(); 408 | 409 | let doc = Doc::new(); 410 | let (n, _sub) = create_notifier(&doc); 411 | let c1 = client("ws://localhost:6601/my-room", doc).await.unwrap(); 412 | 413 | timeout(TIMEOUT, n.notified()).await.unwrap(); 414 | 415 | { 416 | let awareness = c1.awareness().read().await; 417 | let doc = awareness.doc(); 418 | let text = doc.get_or_insert_text("test"); 419 | let str = text.get_string(&doc.transact()); 420 | assert_eq!(str, "abc".to_string()); 421 | } 422 | } 423 | 424 | #[tokio::test] 425 | async fn changes_from_one_client_reach_others() { 426 | let doc = Doc::with_client_id(1); 427 | let _ = doc.get_or_insert_text("test"); 428 | 429 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 430 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 431 | let _server = start_server("0.0.0.0:6602", Arc::new(bcast)).await.unwrap(); 432 | 433 | let d1 = Doc::with_client_id(2); 434 | let c1 = client("ws://localhost:6602/my-room", d1).await.unwrap(); 435 | // by default changes made by document on the client side are not propagated automatically 436 | let _sub11 = { 437 | let sink = c1.sink(); 438 | let a = c1.awareness().write().await; 439 | let doc = a.doc(); 440 | doc.observe_update_v1(move |_, e| { 441 | let update = e.update.to_owned(); 442 | if let Some(sink) = sink.upgrade() { 443 | task::spawn(async move { 444 | let msg = yrs::sync::Message::Sync(yrs::sync::SyncMessage::Update(update)) 445 | .encode_v1(); 446 | let mut sink = sink.lock().await; 447 | sink.send(msg).await.unwrap(); 448 | }); 449 | } 450 | }) 451 | .unwrap() 452 | }; 453 | 454 | let d2 = Doc::with_client_id(3); 455 | let (n2, _sub2) = create_notifier(&d2); 456 | let c2 = client("ws://localhost:6602/my-room", d2).await.unwrap(); 457 | 458 | { 459 | let a = c1.awareness().write().await; 460 | let doc = a.doc(); 461 | let text = doc.get_or_insert_text("test"); 462 | text.push(&mut doc.transact_mut(), "def"); 463 | } 464 | 465 | timeout(TIMEOUT, n2.notified()).await.unwrap(); 466 | 467 | { 468 | let awareness = c2.awareness().read().await; 469 | let doc = awareness.doc(); 470 | let text = doc.get_or_insert_text("test"); 471 | let str = text.get_string(&doc.transact()); 472 | assert_eq!(str, "def".to_string()); 473 | } 474 | } 475 | 476 | #[tokio::test] 477 | async fn client_failure_doesnt_affect_others() { 478 | let doc = Doc::with_client_id(1); 479 | let _text = doc.get_or_insert_text("test"); 480 | 481 | let awareness = Arc::new(RwLock::new(Awareness::new(doc))); 482 | let bcast = BroadcastGroup::new(awareness.clone(), 10).await; 483 | let _server = start_server("0.0.0.0:6603", Arc::new(bcast)).await.unwrap(); 484 | 485 | let d1 = Doc::with_client_id(2); 486 | let c1 = client("ws://localhost:6603/my-room", d1).await.unwrap(); 487 | // by default changes made by document on the client side are not propagated automatically 488 | let _sub11 = { 489 | let sink = c1.sink(); 490 | let a = c1.awareness().write().await; 491 | let doc = a.doc(); 492 | doc.observe_update_v1(move |_, e| { 493 | let update = e.update.to_owned(); 494 | if let Some(sink) = sink.upgrade() { 495 | task::spawn(async move { 496 | let msg = yrs::sync::Message::Sync(yrs::sync::SyncMessage::Update(update)) 497 | .encode_v1(); 498 | let mut sink = sink.lock().await; 499 | sink.send(msg).await.unwrap(); 500 | }); 501 | } 502 | }) 503 | .unwrap() 504 | }; 505 | 506 | let d2 = Doc::with_client_id(3); 507 | let (n2, sub2) = create_notifier(&d2); 508 | let c2 = client("ws://localhost:6603/my-room", d2).await.unwrap(); 509 | 510 | let d3 = Doc::with_client_id(4); 511 | let (n3, sub3) = create_notifier(&d3); 512 | let c3 = client("ws://localhost:6603/my-room", d3).await.unwrap(); 513 | 514 | { 515 | let a = c1.awareness().write().await; 516 | let doc = a.doc(); 517 | let text = doc.get_or_insert_text("test"); 518 | text.push(&mut doc.transact_mut(), "abc"); 519 | } 520 | 521 | // on the first try both C2 and C3 should receive the update 522 | //timeout(TIMEOUT, n2.notified()).await.unwrap(); 523 | //timeout(TIMEOUT, n3.notified()).await.unwrap(); 524 | sleep(TIMEOUT).await; 525 | 526 | { 527 | let awareness = c2.awareness().read().await; 528 | let doc = awareness.doc(); 529 | let text = doc.get_or_insert_text("test"); 530 | let str = text.get_string(&doc.transact()); 531 | assert_eq!(str, "abc".to_string()); 532 | } 533 | { 534 | let awareness = c3.awareness().read().await; 535 | let doc = awareness.doc(); 536 | let text = doc.get_or_insert_text("test"); 537 | let str = text.get_string(&doc.transact()); 538 | assert_eq!(str, "abc".to_string()); 539 | } 540 | 541 | // drop client, causing abrupt ending 542 | drop(c3); 543 | drop(n3); 544 | drop(sub3); 545 | // C2 notification subscription has been realized, we need to refresh it 546 | drop(n2); 547 | drop(sub2); 548 | 549 | let (n2, _sub2) = { 550 | let a = c2.awareness().write().await; 551 | let doc = a.doc(); 552 | create_notifier(doc) 553 | }; 554 | 555 | { 556 | let a = c1.awareness().write().await; 557 | let doc = a.doc(); 558 | let text = doc.get_or_insert_text("test"); 559 | text.push(&mut doc.transact_mut(), "def"); 560 | } 561 | 562 | timeout(TIMEOUT, n2.notified()).await.unwrap(); 563 | 564 | { 565 | let awareness = c2.awareness().read().await; 566 | let doc = awareness.doc(); 567 | let text = doc.get_or_insert_text("test"); 568 | let str = text.get_string(&doc.transact()); 569 | assert_eq!(str, "abcdef".to_string()); 570 | } 571 | } 572 | } 573 | --------------------------------------------------------------------------------