diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e40db74..8fabb78e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ All user visible changes to this project will be documented in this file. This p - Configuration: - Rename `[server]` section of Client API HTTP server as `[server.client.http]` ([#33]). +- RPC messaging: + - Reverse `Ping`/`Pong` naming: server sends `Ping` and expects `Pongs` from client now. ([#75]). ### Added @@ -31,16 +33,19 @@ All user visible changes to this project will be documented in this file. This p - Dynamic `Peer`s creation when client connects ([#28]); - Auto-removing `Peer`s when `Member` disconnects ([#28]); - Filter `SetIceCandidate` messages without `candidate` ([#50](/../../pull/50)); - - Send reason of closing WebSocket connection as [Close](https://tools.ietf.org/html/rfc4566#section-5.14) frame's description ([#58](/../../pull/58)). + - Send reason of closing WebSocket connection as [Close](https://tools.ietf.org/html/rfc4566#section-5.14) frame's description ([#58](/../../pull/58)); + - Send `Event::RpcSettingsUpdated` when `Member` connects ([#75]); - Configuration: - `[server.control.grpc]` section to configure Control API gRPC server ([#33]); - - `server.client.http.public_url` option to configure public URL of Client API HTTP server ([#33]). + - `server.client.http.public_url` option to configure public URL of Client API HTTP server ([#33]); + - `rpc.ping_interval` option to configure `Ping`s sending interval ([#75]). - Testing: - E2E tests for signalling ([#28]). [#28]: /../../pull/28 [#33]: /../../pull/33 [#63]: /../../pull/63 +[#75]: /../../pull/75 diff --git a/Cargo.lock b/Cargo.lock index 0451bddd9..40676e050 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,7 +108,7 @@ dependencies = [ "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -287,7 +287,7 @@ dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -316,9 +316,9 @@ name = "actix-web-codegen" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -395,9 +395,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "atty" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -420,7 +421,7 @@ dependencies = [ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", @@ -445,7 +446,7 @@ name = "backtrace-sys" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.49 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -485,7 +486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "blake2b_simd" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -498,7 +499,7 @@ name = "brotli-sys" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.49 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -513,7 +514,7 @@ dependencies = [ [[package]] name = "bumpalo" -version = "2.6.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -540,7 +541,7 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -564,7 +565,7 @@ version = "2.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -585,7 +586,7 @@ name = "cmake" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.49 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -606,7 +607,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 5.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde-hjson 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -621,7 +622,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -768,10 +769,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -781,7 +782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -791,9 +792,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -802,9 +803,9 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -836,9 +837,9 @@ name = "derive_more" version = "0.99.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -919,7 +920,7 @@ name = "env_logger" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -940,9 +941,9 @@ name = "failure_derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1049,9 +1050,9 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1089,12 +1090,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "getrandom" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1106,7 +1107,7 @@ dependencies = [ "grpcio-sys 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1114,7 +1115,7 @@ name = "grpcio-compiler" version = "0.5.0-alpha.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1123,7 +1124,7 @@ name = "grpcio-sys" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.49 (registry+https://github.com/rust-lang/crates.io-index)", "cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1170,7 +1171,7 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1205,7 +1206,7 @@ name = "humantime" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1276,10 +1277,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "js-sys" -version = "0.3.33" +version = "0.3.35" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1342,7 +1343,7 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1405,11 +1406,11 @@ dependencies = [ "grpcio 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "humantime-serde 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "medea-client-api-proto 0.1.1-dev", + "medea-client-api-proto 0.2.0-dev", "medea-control-api-proto 0.1.0-dev", "medea-macro 0.2.0-dev", "mockall 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "redis 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1434,7 +1435,7 @@ dependencies = [ [[package]] name = "medea-client-api-proto" -version = "0.1.1-dev" +version = "0.2.0-dev" dependencies = [ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "medea-macro 0.2.0-dev", @@ -1454,7 +1455,7 @@ dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "medea-control-api-proto 0.1.0-dev", - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1470,7 +1471,7 @@ version = "0.1.0-dev" dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "protoc-grpcio 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1483,18 +1484,18 @@ dependencies = [ "downcast 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "fragile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", - "medea-client-api-proto 0.1.1-dev", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "medea-client-api-proto 0.2.0-dev", "medea-macro 0.2.0-dev", "mockall 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", "tracerr 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-test 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-test 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "wee_alloc 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1504,9 +1505,9 @@ version = "0.2.0-dev" dependencies = [ "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "medea-jason 0.2.0-dev", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1530,7 +1531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "mime" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1538,7 +1539,7 @@ name = "miniz-sys" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.49 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1623,9 +1624,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1634,9 +1635,9 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1665,7 +1666,7 @@ dependencies = [ [[package]] name = "nom" -version = "5.0.1" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lexical-core 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1708,7 +1709,7 @@ name = "num_cpus" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1727,7 +1728,7 @@ name = "parking_lot" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lock_api 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1817,9 +1818,9 @@ name = "proc-macro-hack" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1837,7 +1838,7 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1845,20 +1846,20 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.8.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "protobuf-codegen" -version = "2.8.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "protoc" -version = "2.8.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1871,15 +1872,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio-compiler 0.5.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "protoc 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf-codegen 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "protoc 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "quick-error" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1895,7 +1896,7 @@ name = "quote" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1942,7 +1943,7 @@ name = "rand" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1985,7 +1986,7 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2126,7 +2127,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2135,7 +2136,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "blake2b_simd 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", + "blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2232,9 +2233,9 @@ name = "serde_derive" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2399,7 +2400,7 @@ name = "slog-term" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "term 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2480,10 +2481,10 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2493,9 +2494,9 @@ name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2897,118 +2898,118 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wasi" -version = "0.7.0" +version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wasm-bindgen" -version = "0.2.56" +version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.56" +version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.56" +version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro-support 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.56" +version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.56" +version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wasm-bindgen-test" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-test-macro 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-test-macro 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-webidl" -version = "0.2.56" +version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "web-sys" -version = "0.3.33" +version = "0.3.35" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-webidl 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-webidl 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3144,7 +3145,7 @@ dependencies = [ "checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" "checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" -"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" +"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum awc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "5e995283278dd3bf0449e7534e77184adb1570c0de8b6a50bf7c9d01ad8db8c4" "checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" @@ -3153,14 +3154,14 @@ dependencies = [ "checksum bb8 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0ac73ee3406f475415bba792942016291b87bac08839c38a032de56085a73b2c" "checksum bb8-redis 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39945a33c03edfba8c353d330b921961e2137d4fc4215331c1b2397f32acff80" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum blake2b_simd 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b83b7baab1e671718d78204225800d6b170e648188ac7dc992e9d6bddf87d0c0" +"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" "checksum brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" "checksum brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" -"checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708" +"checksum bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fb8038c1ddc0a5f73787b130f4cc75151e96ed33e417fde765eb5a81e3532f4" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" -"checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" +"checksum cc 1.0.49 (registry+https://github.com/rust-lang/crates.io-index)" = "e450b8da92aa6f274e7c6437692f9f2ce6d701fb73bacfcf87897b3f89a4c20e" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" @@ -3223,7 +3224,7 @@ dependencies = [ "checksum futures-task 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0bae52d6b29cf440e298856fec3965ee6fa71b06aa7495178615953fd669e5f9" "checksum futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d66274fb76985d3c62c886d1da7ac4c0903a8c9f754e8fe0f35a6a6cc39e76" "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" -"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" +"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum grpcio 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9ac757a85603e4f8c40a9f94be06a5ad412acab80b39b4e8895ca931b6619910" "checksum grpcio-compiler 0.5.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3c8c6f6d181ac240958853a3b95a4eac9b23816a0a922b9d30b991142938b9ff" "checksum grpcio-sys 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "7b2f22fb0327f153acccedbe91894dd0fb15bb6f202d8195665cd206af0402b0" @@ -3231,7 +3232,7 @@ dependencies = [ "checksum hashbrown 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "29fba9abe4742d586dfd0c06ae4f7e73a1c2d86b856933509b269d82cdf06e18" "checksum hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" -"checksum hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7" +"checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" "checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" "checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" @@ -3244,7 +3245,7 @@ dependencies = [ "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" -"checksum js-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)" = "367647c532db6f1555d7151e619540ec5f713328235b8c062c6b4f63e84adfe3" +"checksum js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" @@ -3253,7 +3254,7 @@ dependencies = [ "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum lock_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed946d4529956a20f2d63ebe1b69996d5a2137c91913fe3ebbeff957f5bca7ff" -"checksum lock_api 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e57b3997725d2b60dbec1297f6c2e2957cc383db1cebd6be812163f969c7d586" +"checksum lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" @@ -3262,7 +3263,7 @@ dependencies = [ "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" "checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" "checksum memory_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" -"checksum mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "dd1d63acd1b78403cc0c325605908475dd9b9a3acbf65ed8bcab97e27014afcf" +"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" "checksum miniz-sys 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9e3ae51cea1576ceba0dde3d484d30e6e5b86dee0b2d412fe3a16a15c98202" "checksum miniz_oxide 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6f3f74f726ae935c3f514300cc6773a0c9492abc5e972d42ba0c0ebb88757625" "checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" @@ -3275,7 +3276,7 @@ dependencies = [ "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -"checksum nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c618b63422da4401283884e6668d39f819a106ef51f5f59b81add00075da35ca" +"checksum nom 5.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c433f4d505fe6ce7ff78523d2fa13a0b9f2690e181fc26168bcbe5ccc5d14e07" "checksum normalize-line-endings 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2e0a1a39eab95caf4f5556da9289b9e68f0aafac901b2ce80daaf020d3b733a8" "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" @@ -3296,12 +3297,12 @@ dependencies = [ "checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" "checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" -"checksum protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40361836defdd5871ff7e84096c6f6444af7fc157f8ef1789f54f147687caa20" -"checksum protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12c6abd78435445fc86898ebbd0521a68438063d4a73e23527b7134e6bf58b4a" -"checksum protoc 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3998c4bc0af8ccbd3cc68245ee9f72663c5ae2fb78bc48ff7719aef11562edea" +"checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" +"checksum protobuf 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6563a657a014b771e7f69f06447d88d8fbb5a215ffc4cab724afb3acedcc7701" +"checksum protobuf-codegen 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6f1bbc6db30d5d3e730b6e2326e9a64a75ca9c80d6427d6f054dc8cacc79d225" +"checksum protoc 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba4f6bdf43828dd805132c2906b119dc0db2e460579bee6966365df2c3459a4d" "checksum protoc-grpcio 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7bb9b76be44d96453f528030c03713f57fa725565036cc9d72037ad75babadf" -"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" +"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" @@ -3371,7 +3372,7 @@ dependencies = [ "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -"checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238" +"checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum take_mut 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" @@ -3412,17 +3413,17 @@ dependencies = [ "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" -"checksum wasm-bindgen 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)" = "99de4b68939a880d530aed51289a7c7baee154e3ea8ac234b542c49da7134aaf" -"checksum wasm-bindgen-backend 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)" = "b58e66a093a7b7571cb76409763c495b8741ac4319ac20acc2b798f6766d92ee" -"checksum wasm-bindgen-futures 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3bf1b55e0dc85085cfab2c0c520b977afcf16ac5801ee0de8dde42a4f5649b2a" -"checksum wasm-bindgen-macro 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)" = "a80f89daea7b0a67b11f6e9f911422ed039de9963dce00048a653b63d51194bf" -"checksum wasm-bindgen-macro-support 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)" = "4f9dbc3734ad6cff6b76b75b7df98c06982becd0055f651465a08f769bca5c61" -"checksum wasm-bindgen-shared 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)" = "d907984f8506b3554eab48b8efff723e764ddbf76d4cd4a3fe4196bc00c49a70" -"checksum wasm-bindgen-test 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "87d10750e72390ddfaad9420525f4e3ed565408586c859aa2bf91f8ebad65774" -"checksum wasm-bindgen-test-macro 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "5b84bb462de0772abdec0394671153f280483828f883cbaa12d71f6142196a01" -"checksum wasm-bindgen-webidl 0.2.56 (registry+https://github.com/rust-lang/crates.io-index)" = "f85a3825a459cf6a929d03bacb54dca37a614d43032ad1343ef2d4822972947d" -"checksum web-sys 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)" = "2fb60433d0dc12c803b9b017b3902d80c9451bab78d27bc3210bf2a7b96593f1" +"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +"checksum wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" +"checksum wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" +"checksum wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8bbdd49e3e28b40dec6a9ba8d17798245ce32b019513a845369c641b275135d9" +"checksum wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" +"checksum wasm-bindgen-macro-support 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" +"checksum wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" +"checksum wasm-bindgen-test 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "98fd0ec352c44d1726b6c2bec524612b1c81e34a7d858f597a6c71f8e018c82e" +"checksum wasm-bindgen-test-macro 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "97837a6e83ab24a4b3a38d44a257e13335b54f4b4548b2c9d71edd0bf570cb4f" +"checksum wasm-bindgen-webidl 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" +"checksum web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" "checksum wee_alloc 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" "checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" "checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" diff --git a/config.toml b/config.toml index d874768ac..c45967be7 100644 --- a/config.toml +++ b/config.toml @@ -60,6 +60,12 @@ # Default: # reconnect_timeout = "10s" +# Interval between pings that server sends to clients. +# +# Env var: MEDEA_RPC__PING_INTERVAL +# Default: +# ping_interval = "3s" + diff --git a/jason/CHANGELOG.md b/jason/CHANGELOG.md index b364f4ebf..e1128c2a8 100644 --- a/jason/CHANGELOG.md +++ b/jason/CHANGELOG.md @@ -18,6 +18,8 @@ All user visible changes to this project will be documented in this file. This p - Remove error argument from `on_local_stream` callback ([#54]); - Room initialization ([#46]): - Remove `Jason.join_room()`. +- Transport and messaging: + - Reverse `ping`/`pong` mechanism: expect `Ping`s from server and answer with `Pong`s ([#75]). ### Added @@ -40,9 +42,13 @@ All user visible changes to this project will be documented in this file. This p - `Room.join()`; - Ability to inject local video/audio stream into `Room` via `Room.inject_local_stream()` ([#54]); - `Room.on_failed_local_stream` callback ([#54]); +- Room management: + - Library API: + - `Room.on_connection_loss` callback that JS side can start Jason reconnection on connection loss with ([#75]); - `Room.on_close` callback for WebSocket close initiated by server ([#55]). - RPC messaging: - - Cleanup Jason state on normal (`code = 1000`) WebSocket close ([#55]). + - Cleanup Jason state on normal (`code = 1000`) WebSocket close ([#55]); + - `RpcClient` and `RpcTransport` reconnection ([#75]). - Signalling: - Emitting of RPC commands: - `AddPeerConnectionMetrics` with `IceConnectionState` ([#71](/../../pull/71)). @@ -58,6 +64,7 @@ All user visible changes to this project will be documented in this file. This p [#46]: /../../pull/46 [#54]: /../../pull/54 [#55]: /../../pull/55 +[#75]: /../../pull/75 diff --git a/jason/demo/index.html b/jason/demo/index.html index 014dee0d7..b7e92ba62 100644 --- a/jason/demo/index.html +++ b/jason/demo/index.html @@ -4,7 +4,7 @@ Chat - + @@ -156,7 +156,7 @@ let joinCallerButton = document.getElementById('join__join'); let usernameInput = document.getElementById('join__username'); - usernameInput.value = faker.name.firstName(); + usernameInput.value = Fakerator().names.firstNameM(); let room = await jason.init_room(); @@ -206,7 +206,23 @@ }); }); + let connectionState = document.getElementById('connection-state__state'); + room.on_connection_loss(async (reconnectHandle) => { + connectionState.className = 'badge badge-warning'; + connectionState.textContent = 'Reconnecting'; + + try { + await reconnectHandle.reconnect_with_backoff(500, 2.0, 10000); + connectionState.className = 'badge badge-success'; + connectionState.textContent = 'Connected'; + } catch (e) { + console.error(e); + } + }); + room.on_close(function (on_closed) { + connectionState.className = 'badge badge-danger'; + connectionState.textContent = "Closed"; alert( `Call was ended. Reason: ${on_closed.reason()}; @@ -263,6 +279,8 @@ await axios.delete(controlUrl + roomId + '/' + username); console.log("Creating new member."); await room.join(await addNewMember(roomId, username)); + connectionState.className = 'badge badge-success'; + connectionState.textContent = 'Connected'; } catch (e) { logError("Join to room failed", e); @@ -362,8 +380,13 @@ + +
+ Connection state: + Closed
diff --git a/jason/e2e-demo/js/index.js b/jason/e2e-demo/js/index.js index 722fa2539..6946f8d16 100644 --- a/jason/e2e-demo/js/index.js +++ b/jason/e2e-demo/js/index.js @@ -356,6 +356,34 @@ window.onload = async function() { console.error(error); }); + room.on_connection_loss( async (reconnectHandle) => { + let connectionLossNotification = document.getElementsByClassName('connection-loss-notification')[0]; + contentVisibility.show(connectionLossNotification); + + let manualReconnectBtn = document.getElementsByClassName('connection-loss-notification__manual-reconnect')[0]; + let connectionLossMsg = document.getElementsByClassName('connection-loss-notification__msg')[0]; + let connectionLossDefaultText = connectionLossMsg.textContent; + + manualReconnectBtn.onclick = async () => { + try { + connectionLossMsg.textContent = 'Trying to manually reconnect...'; + await reconnectHandle.reconnect_with_delay(0); + contentVisibility.hide(connectionLossNotification); + console.error("Reconnected!"); + } catch (e) { + console.error("Failed to manually reconnect: " + e.message()); + } finally { + connectionLossMsg.textContent = connectionLossDefaultText; + } + }; + try { + await reconnectHandle.reconnect_with_backoff(3000, 2.0, 10000); + } catch (e) { + console.error('Error in reconnection with backoff:\n' + e.message()); + } + contentVisibility.hide(connectionLossNotification); + }); + room.on_close(function (on_closed) { let videos = document.getElementsByClassName('remote-videos')[0]; while (videos.firstChild) { diff --git a/jason/e2e-demo/video-call.html b/jason/e2e-demo/video-call.html index 4ada5d0ea..19e400c8d 100644 --- a/jason/e2e-demo/video-call.html +++ b/jason/e2e-demo/video-call.html @@ -205,6 +205,28 @@ .control-debug__table-result>table>tr>th { font-weight: normal; } + + .connection-loss-notification { + right: 50px; + top: 50px; + position: absolute; + background: #fff; + padding: 10px; + border-radius: 3px; + font-size: 11pt; + } + + .connection-loss-notification__msg { + display: block; + } + + .connection-loss-notification__manual-reconnect { + display: block; + color: #49668C; + text-align: center; + cursor: pointer; + margin-top: 5px; + } @@ -343,6 +365,12 @@ +
+ Connection lost. + Trying to reconnect with backoff... + Reconnect now +
+
Dynamic Control API
diff --git a/jason/src/api/connection.rs b/jason/src/api/connection.rs index 074c52611..34cb78a22 100644 --- a/jason/src/api/connection.rs +++ b/jason/src/api/connection.rs @@ -7,7 +7,10 @@ use std::{ use wasm_bindgen::prelude::*; -use crate::{peer::MediaStreamHandle, utils::Callback}; +use crate::{ + peer::MediaStreamHandle, + utils::{Callback, HandlerDetachedError}, +}; /// Actual data of a connection with a specific remote [`Member`]. /// @@ -31,10 +34,8 @@ impl ConnectionHandle { &mut self, f: js_sys::Function, ) -> Result<(), JsValue> { - map_weak!(self, |inner| inner - .borrow_mut() - .on_remote_stream - .set_func(f)) + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().on_remote_stream.set_func(f)) } } diff --git a/jason/src/api/mod.rs b/jason/src/api/mod.rs index e9237519b..1f3825d02 100644 --- a/jason/src/api/mod.rs +++ b/jason/src/api/mod.rs @@ -12,7 +12,10 @@ use wasm_bindgen_futures::spawn_local; use crate::{ media::{MediaManager, MediaManagerHandle}, peer, - rpc::{ClientDisconnect, RpcClient as _, WebSocketRpcClient}, + rpc::{ + ClientDisconnect, RpcClient as _, RpcTransport, WebSocketRpcClient, + WebSocketRpcTransport, + }, set_panic_hook, }; @@ -44,13 +47,20 @@ impl Jason { /// Returns [`RoomHandle`] for [`Room`]. pub fn init_room(&self) -> RoomHandle { - let rpc = Rc::new(WebSocketRpcClient::new(3000)); + let rpc = WebSocketRpcClient::new(Box::new(|token| { + Box::pin(async move { + let ws = WebSocketRpcTransport::new(&token) + .await + .map_err(|e| tracerr::new!(e))?; + Ok(Rc::new(ws) as Rc) + }) + })); let peer_repository = Box::new(peer::Repository::new(Rc::clone( &self.0.borrow().media_manager, ))); let inner = self.0.clone(); - spawn_local(rpc.on_close().map(move |res| { + spawn_local(rpc.on_normal_close().map(move |res| { // TODO: Don't close all rooms when multiple rpc connections // will be supported. let reason = res.unwrap_or_else(|_| { @@ -64,7 +74,7 @@ impl Jason { inner.borrow_mut().media_manager = Rc::default(); })); - let room = Room::new(rpc, peer_repository); + let room = Room::new(Rc::new(rpc), peer_repository); let handle = room.new_handle(); self.0.borrow_mut().rooms.push(room); handle diff --git a/jason/src/api/room.rs b/jason/src/api/room.rs index 52f40d587..aa707bcc3 100644 --- a/jason/src/api/room.rs +++ b/jason/src/api/room.rs @@ -21,14 +21,17 @@ use web_sys::MediaStream as SysMediaStream; use crate::{ peer::{ - MediaStream, MediaStreamHandle, PeerError, PeerEvent, PeerEventHandler, - PeerRepository, + EnabledAudio, EnabledVideo, MediaStream, MediaStreamHandle, PeerError, + PeerEvent, PeerEventHandler, PeerRepository, }, rpc::{ - ClientDisconnect, CloseReason, RpcClient, RpcClientError, - TransportError, WebSocketRpcTransport, + ClientDisconnect, CloseReason, ReconnectHandle, RpcClient, + RpcClientError, TransportError, + }, + utils::{ + console_error, Callback, HandlerDetachedError, JasonError, JsCaused, + JsError, }, - utils::{Callback, JasonError, JsCaused, JsError}, }; use super::{connection::Connection, ConnectionHandle}; @@ -97,10 +100,9 @@ impl RoomCloseReason { /// Errors that may occur in a [`Room`]. #[derive(Debug, Display, JsCaused)] enum RoomError { - /// Returned if the `on_failed_local_stream` callback was not set before - /// joining the room. - #[display(fmt = "`on_failed_local_stream` callback is not set")] - CallbackNotSet, + /// Returned if the mandatory callback wasn't set. + #[display(fmt = "`{}` callback isn't set.", _0)] + CallbackNotSet(&'static str), /// Returned if unable to init [`RpcTransport`]. #[display(fmt = "Unable to init RPC transport: {}", _0)] @@ -174,27 +176,53 @@ impl RoomHandle { &self, token: String, ) -> impl Future> + 'static { - let inner: Result<_, JasonError> = map_weak!(self, |inner| inner); + let inner = upgrade_or_detached!(self.0, JasonError); async move { let inner = inner?; if !inner.borrow().on_failed_local_stream.is_set() { return Err(JasonError::from(tracerr::new!( - RoomError::CallbackNotSet + RoomError::CallbackNotSet("Room.on_failed_local_stream()") + ))); + } + + if !inner.borrow().on_connection_loss.is_set() { + return Err(JasonError::from(tracerr::new!( + RoomError::CallbackNotSet("Room.on_connection_loss()") ))); } - let websocket = WebSocketRpcTransport::new(&token) - .await - .map_err(tracerr::map_from_and_wrap!(=> RoomError))?; inner .borrow() .rpc - .connect(Rc::new(websocket)) + .connect(token) .await .map_err(tracerr::map_from_and_wrap!(=> RoomError))?; + let mut connection_loss_stream = + inner.borrow().rpc.on_connection_loss(); + let weak_inner = Rc::downgrade(&inner); + spawn_local(async move { + while let Some(_) = connection_loss_stream.next().await { + match upgrade_or_detached!(weak_inner, JsValue) { + Ok(inner) => { + let reconnect_handle = ReconnectHandle::new( + Rc::downgrade(&inner.borrow().rpc), + ); + inner + .borrow() + .on_connection_loss + .call(reconnect_handle); + } + Err(e) => { + console_error(e); + break; + } + } + } + }); + Ok(()) } } @@ -207,23 +235,23 @@ impl RoomHandle { &self, f: js_sys::Function, ) -> Result<(), JsValue> { - map_weak!(self, |inner| inner - .borrow_mut() - .on_new_connection - .set_func(f)) + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().on_new_connection.set_func(f)) } /// Sets `on_close` callback, which will be invoked on [`Room`] close, /// providing [`RoomCloseReason`]. pub fn on_close(&mut self, f: js_sys::Function) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().on_close.set_func(f)) + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().on_close.set_func(f)) } /// Sets `on_local_stream` callback, which will be invoked once media /// acquisition request will resolve successfully. Only invoked if media /// request was initiated by media server. pub fn on_local_stream(&self, f: js_sys::Function) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().on_local_stream.set_func(f)) + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().on_local_stream.set_func(f)) } /// Sets `on_failed_local_stream` callback, which will be invoked on local @@ -232,10 +260,18 @@ impl RoomHandle { &self, f: js_sys::Function, ) -> Result<(), JsValue> { - map_weak!(self, |inner| inner - .borrow_mut() - .on_failed_local_stream - .set_func(f)) + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().on_failed_local_stream.set_func(f)) + } + + /// Sets `on_connection_loss` callback, which will be invoked on + /// [`RpcClient`] connection loss. + pub fn on_connection_loss( + &self, + f: js_sys::Function, + ) -> Result<(), JsValue> { + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().on_connection_loss.set_func(f)) } /// Performs entering to a [`Room`] with the preconfigured authorization @@ -261,27 +297,36 @@ impl RoomHandle { &self, stream: SysMediaStream, ) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().inject_local_stream(stream)) + upgrade_or_detached!(self.0) + .map(|inner| inner.borrow_mut().inject_local_stream(stream)) } /// Mutes outbound audio in this room. pub fn mute_audio(&self) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().toggle_send_audio(false)) + upgrade_or_detached!(self.0).map(|inner| { + inner.borrow_mut().toggle_send_audio(EnabledAudio(false)) + }) } /// Unmutes outbound audio in this room. pub fn unmute_audio(&self) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().toggle_send_audio(true)) + upgrade_or_detached!(self.0).map(|inner| { + inner.borrow_mut().toggle_send_audio(EnabledAudio(true)) + }) } /// Mutes outbound video in this room. pub fn mute_video(&self) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().toggle_send_video(false)) + upgrade_or_detached!(self.0).map(|inner| { + inner.borrow_mut().toggle_send_video(EnabledVideo(false)) + }) } /// Unmutes outbound video in this room. pub fn unmute_video(&self) -> Result<(), JsValue> { - map_weak!(self, |inner| inner.borrow_mut().toggle_send_video(true)) + upgrade_or_detached!(self.0).map(|inner| { + inner.borrow_mut().toggle_send_video(EnabledVideo(true)) + }) } } @@ -322,7 +367,7 @@ impl Room { // happen, actually, since // `InnerSession` should drop its `tx` by unsub from // `RpcClient`. - console_error!("Inner Room dropped unexpectedly") + console_error("Inner Room dropped unexpectedly") } Some(inner) => { match event { @@ -403,11 +448,14 @@ struct InnerRoom { /// [`MediaManager`] or failed inject stream into [`PeerConnection`]. on_failed_local_stream: Rc>, + /// Callback to be invoked when [`RpcClient`] loses connection. + on_connection_loss: Callback, + /// Indicates if outgoing audio is enabled in this [`Room`]. - enabled_audio: bool, + enabled_audio: EnabledAudio, /// Indicates if outgoing video is enabled in this [`Room`]. - enabled_video: bool, + enabled_video: EnabledVideo, /// JS callback which will be called when this [`Room`] will be closed. on_close: Rc>, @@ -437,9 +485,10 @@ impl InnerRoom { connections: HashMap::new(), on_new_connection: Callback::default(), on_local_stream: Callback::default(), + on_connection_loss: Callback::default(), on_failed_local_stream: Rc::new(Callback::default()), - enabled_audio: true, - enabled_video: true, + enabled_audio: EnabledAudio(true), + enabled_video: EnabledVideo(true), on_close: Rc::new(Callback::default()), close_reason: CloseReason::ByClient { reason: ClientDisconnect::RoomUnexpectedlyDropped, @@ -485,7 +534,7 @@ impl InnerRoom { /// Toggles a audio send [`Track`]s of all [`PeerConnection`]s what this /// [`Room`] manage. - fn toggle_send_audio(&mut self, enabled: bool) { + fn toggle_send_audio(&mut self, enabled: EnabledAudio) { for peer in self.peers.get_all() { peer.toggle_send_audio(enabled); } @@ -494,7 +543,7 @@ impl InnerRoom { /// Toggles a video send [`Track`]s of all [`PeerConnection`]s what this /// [`Room`] manage. - fn toggle_send_video(&mut self, enabled: bool) { + fn toggle_send_video(&mut self, enabled: EnabledVideo) { for peer in self.peers.get_all() { peer.toggle_send_video(enabled); } @@ -738,8 +787,6 @@ impl Drop for InnerRoom { self.on_close .call(RoomCloseReason::new(self.close_reason)) - .map(|result| { - result.map_err(|err| console_error!(err.as_string())) - }); + .map(|result| result.map_err(console_error)); } } diff --git a/jason/src/media/constraints.rs b/jason/src/media/constraints.rs index 31218095f..070f84ab5 100644 --- a/jason/src/media/constraints.rs +++ b/jason/src/media/constraints.rs @@ -148,20 +148,21 @@ impl From for MultiSourceMediaStreamConstraints { /// Checks that the [MediaStreamTrack][1] is taken from a device /// with given [deviceId][2]. /// -/// [1]: https://www.w3.org/TR/mediacapture-streams/#mediastreamtrack -/// [2]: https://www.w3.org/TR/mediacapture-streams/#def-constraint-deviceId -macro_rules! satisfies_by_device_id { - ($v:expr, $track:ident) => {{ - match &$v.device_id { - None => true, - Some(device_id) => get_property_by_name( - &$track.get_settings(), - "deviceId", - |val| val.as_string(), - ) - .map_or(false, |id| id.as_str() == device_id), +/// [1]: https://w3.org/TR/mediacapture-streams/#mediastreamtrack +/// [2]: https://w3.org/TR/mediacapture-streams/#def-constraint-deviceId +fn satisfies_by_device_id( + device_id: &Option, + track: &SysMediaStreamTrack, +) -> bool { + match device_id { + None => true, + Some(device_id) => { + get_property_by_name(&track.get_settings(), "deviceId", |v| { + v.as_string() + }) + .map_or(false, |id| id.as_str() == device_id) } - }}; + } } /// Wrapper around [MediaTrackConstraints][1]. @@ -243,7 +244,7 @@ impl AudioTrackConstraints { return false; } - satisfies_by_device_id!(self, track) + satisfies_by_device_id(&self.device_id, track) // TODO returns Result } } @@ -337,7 +338,7 @@ impl VideoTrackConstraints { match &self.0 { None => true, Some(StreamSource::Device(constraints)) => { - satisfies_by_device_id!(constraints, track) + satisfies_by_device_id(&constraints.device_id, track) && !Self::guess_is_from_display(&track) } Some(StreamSource::Display(_)) => { diff --git a/jason/src/media/manager.rs b/jason/src/media/manager.rs index 74cd48f3a..68594c51d 100644 --- a/jason/src/media/manager.rs +++ b/jason/src/media/manager.rs @@ -22,7 +22,7 @@ use web_sys::{ use crate::{ media::{MediaStreamConstraints, MultiSourceMediaStreamConstraints}, - utils::{window, JasonError, JsCaused, JsError}, + utils::{window, HandlerDetachedError, JasonError, JsCaused, JsError}, }; use super::InputDeviceInfo; @@ -341,34 +341,32 @@ impl MediaManager { pub struct MediaManagerHandle(Weak); #[wasm_bindgen] +#[allow(clippy::unused_self)] impl MediaManagerHandle { /// Returns the JS array of [`MediaDeviceInfo`] objects. pub fn enumerate_devices(&self) -> Promise { - match map_weak!(self, |_| InnerMediaManager::enumerate_devices()) { - Ok(devices) => future_to_promise(async { - devices - .await - .map(|devices| { - devices - .into_iter() - .fold(js_sys::Array::new(), |devices_info, info| { - devices_info.push(&JsValue::from(info)); - devices_info - }) - .into() - }) - .map_err(tracerr::wrap!(=> MediaManagerError)) - .map_err(|e| JasonError::from(e).into()) - }), - Err(e) => future_to_promise(future::err(e)), - } + future_to_promise(async { + InnerMediaManager::enumerate_devices() + .await + .map(|devices| { + devices + .into_iter() + .fold(js_sys::Array::new(), |devices_info, info| { + devices_info.push(&JsValue::from(info)); + devices_info + }) + .into() + }) + .map_err(tracerr::wrap!(=> MediaManagerError)) + .map_err(|e| JasonError::from(e).into()) + }) } /// Returns [MediaStream][1] object. /// /// [1]: https://w3.org/TR/mediacapture-streams/#mediastream pub fn init_local_stream(&self, caps: MediaStreamConstraints) -> Promise { - match map_weak!(self, |inner| { inner.get_stream(caps) }) { + match upgrade_or_detached!(self.0).map(|inner| inner.get_stream(caps)) { Ok(stream) => future_to_promise(async { stream .await diff --git a/jason/src/peer/media.rs b/jason/src/peer/media.rs index 007669408..2bc2feee6 100644 --- a/jason/src/peer/media.rs +++ b/jason/src/peer/media.rs @@ -2,7 +2,7 @@ use std::{borrow::ToOwned, cell::RefCell, collections::HashMap, rc::Rc}; -use derive_more::Display; +use derive_more::{Display, From}; use futures::future; use medea_client_api_proto::{Direction, PeerId, Track, TrackId}; use tracerr::Traced; @@ -57,6 +57,14 @@ pub enum MediaConnectionsError { type Result = std::result::Result>; +/// Indicator of audio being switched on or off. +#[derive(Clone, Copy, Debug, Display, Eq, From, PartialEq)] +pub struct EnabledAudio(pub bool); + +/// Indicator of video being switched on or off. +#[derive(Clone, Copy, Debug, Display, Eq, From, PartialEq)] +pub struct EnabledVideo(pub bool); + /// Actual data of [`MediaConnections`] storage. struct InnerMediaConnections { /// Ref to parent [`RtcPeerConnection`]. Used to generate transceivers for @@ -70,10 +78,20 @@ struct InnerMediaConnections { receivers: HashMap, /// Are senders audio tracks muted or not. - enabled_audio: bool, + enabled_audio: EnabledAudio, /// Are senders video tracks muted or not. - enabled_video: bool, + enabled_video: EnabledVideo, +} + +impl InnerMediaConnections { + /// Returns [`Iterator`] over [`Sender`]s with provided [`TransceiverKind`]. + pub fn iter_senders_with_kind( + &self, + kind: TransceiverKind, + ) -> impl Iterator> { + self.senders.values().filter(move |s| s.kind() == kind) + } } /// Storage of [`RtcPeerConnection`]'s [`Sender`] and [`Receiver`] tracks. @@ -85,8 +103,8 @@ impl MediaConnections { #[inline] pub fn new( peer: Rc, - enabled_audio: bool, - enabled_video: bool, + enabled_audio: EnabledAudio, + enabled_video: EnabledVideo, ) -> Self { Self(RefCell::new(InnerMediaConnections { peer, @@ -97,30 +115,44 @@ impl MediaConnections { })) } - /// Enables or disables all [`Sender`]s with specified [`TransceiverKind`] - /// [`MediaTrack`]s. - pub fn toggle_send_media(&self, kind: TransceiverKind, enabled: bool) { - let mut s = self.0.borrow_mut(); - match kind { - TransceiverKind::Audio => s.enabled_audio = enabled, - TransceiverKind::Video => s.enabled_video = enabled, - }; - s.senders - .values() - .filter(|s| s.kind() == kind) - .for_each(|s| s.set_track_enabled(enabled)) + /// Enables or disables all [`Sender`]s with [`TransceiverKind::Audio`]. + pub fn toggle_send_audio(&self, enabled: EnabledAudio) { + self.0.borrow_mut().enabled_audio = enabled; + self.0 + .borrow() + .iter_senders_with_kind(TransceiverKind::Audio) + .for_each(|s| s.set_track_enabled(enabled.0)); } - /// Returns `true` if all [`MediaTrack`]s of all [`Senders`] with given - /// [`TransceiverKind`] are enabled or `false` otherwise. - pub fn are_senders_enabled(&self, kind: TransceiverKind) -> bool { - let conn = self.0.borrow(); - for s in conn.senders.values().filter(|s| s.kind() == kind) { - if !s.is_track_enabled() { - return false; - } - } - true + /// Enables or disables all [`Sender`]s with [`TransceiverKind::Video`]. + pub fn toggle_send_video(&self, enabled: EnabledVideo) { + self.0.borrow_mut().enabled_video = enabled; + self.0 + .borrow() + .iter_senders_with_kind(TransceiverKind::Video) + .for_each(|s| s.set_track_enabled(enabled.0)); + } + + /// Returns `true` if all [`MediaTrack`]s of all [`Senders`] with + /// [`TransceiverKind::Audio`] are enabled or `false` otherwise. + pub fn is_send_audio_enabled(&self) -> bool { + self.0 + .borrow() + .iter_senders_with_kind(TransceiverKind::Audio) + .skip_while(|s| s.is_track_enabled()) + .next() + .is_none() + } + + /// Returns `true` if all [`MediaTrack`]s of all [`Senders`] with + /// [`TransceiverKind::Video`] are enabled or `false` otherwise. + pub fn is_send_video_enabled(&self) -> bool { + self.0 + .borrow() + .iter_senders_with_kind(TransceiverKind::Video) + .skip_while(|s| s.is_track_enabled()) + .next() + .is_none() } /// Returns mapping from a [`MediaTrack`] ID to a `mid` of diff --git a/jason/src/peer/mod.rs b/jason/src/peer/mod.rs index 7cf260c3a..9606c6f9c 100644 --- a/jason/src/peer/mod.rs +++ b/jason/src/peer/mod.rs @@ -25,7 +25,7 @@ use web_sys::{ use crate::{ media::{MediaManager, MediaManagerError}, - utils::{JsCaused, JsError}, + utils::{console_error, JsCaused, JsError}, }; #[cfg(feature = "mockable")] @@ -38,7 +38,9 @@ pub use self::{ IceCandidate, RTCPeerConnectionError, RtcPeerConnection, SdpType, TransceiverDirection, TransceiverKind, }, - media::{MediaConnections, MediaConnectionsError}, + media::{ + EnabledAudio, EnabledVideo, MediaConnections, MediaConnectionsError, + }, stream::{MediaStream, MediaStreamHandle}, stream_request::{SimpleStreamRequest, StreamRequest, StreamRequestError}, track::MediaTrack, @@ -178,8 +180,8 @@ impl PeerConnection { peer_events_sender: mpsc::UnboundedSender, ice_servers: I, media_manager: Rc, - enabled_audio: bool, - enabled_video: bool, + enabled_audio: EnabledAudio, + enabled_video: EnabledVideo, ) -> Result { let peer = Rc::new( RtcPeerConnection::new(ice_servers) @@ -276,7 +278,7 @@ impl PeerConnection { Disconnected => IceConnectionState::Disconnected, Closed => IceConnectionState::Closed, _ => { - console_error!("Unknown ICE connection state"); + console_error("Unknown ICE connection state"); return; } }; @@ -322,27 +324,23 @@ impl PeerConnection { } /// Disables or enables all audio tracks for all [`Sender`]s. - pub fn toggle_send_audio(&self, enabled: bool) { - self.media_connections - .toggle_send_media(TransceiverKind::Audio, enabled) + pub fn toggle_send_audio(&self, enabled: EnabledAudio) { + self.media_connections.toggle_send_audio(enabled) } /// Disables or enables all video tracks for all [`Sender`]s. - pub fn toggle_send_video(&self, enabled: bool) { - self.media_connections - .toggle_send_media(TransceiverKind::Video, enabled) + pub fn toggle_send_video(&self, enabled: EnabledVideo) { + self.media_connections.toggle_send_video(enabled) } /// Returns `true` if all [`Sender`]s audio tracks are enabled. pub fn is_send_audio_enabled(&self) -> bool { - self.media_connections - .are_senders_enabled(TransceiverKind::Audio) + self.media_connections.is_send_audio_enabled() } /// Returns `true` if all [`Sender`]s video tracks are enabled. pub fn is_send_video_enabled(&self) -> bool { - self.media_connections - .are_senders_enabled(TransceiverKind::Video) + self.media_connections.is_send_video_enabled() } /// Track id to mid relations of all send tracks of this diff --git a/jason/src/peer/repo.rs b/jason/src/peer/repo.rs index d7d1f1d15..2ff46d590 100644 --- a/jason/src/peer/repo.rs +++ b/jason/src/peer/repo.rs @@ -4,7 +4,10 @@ use futures::channel::mpsc; use medea_client_api_proto::{IceServer, PeerId}; use tracerr::Traced; -use crate::media::MediaManager; +use crate::{ + media::MediaManager, + peer::media::{EnabledAudio, EnabledVideo}, +}; use super::{PeerConnection, PeerError, PeerEvent}; @@ -20,8 +23,8 @@ pub trait PeerRepository { id: PeerId, ice_servers: Vec, events_sender: mpsc::UnboundedSender, - enabled_audio: bool, - enabled_video: bool, + enabled_audio: EnabledAudio, + enabled_video: EnabledVideo, ) -> Result, Traced>; /// Returns [`PeerConnection`] stored in repository by its ID. @@ -62,8 +65,8 @@ impl PeerRepository for Repository { id: PeerId, ice_servers: Vec, peer_events_sender: mpsc::UnboundedSender, - enabled_audio: bool, - enabled_video: bool, + enabled_audio: EnabledAudio, + enabled_video: EnabledVideo, ) -> Result, Traced> { let peer = Rc::new( PeerConnection::new( diff --git a/jason/src/peer/stream.rs b/jason/src/peer/stream.rs index 841b60b16..33f5e227f 100644 --- a/jason/src/peer/stream.rs +++ b/jason/src/peer/stream.rs @@ -11,7 +11,11 @@ use medea_client_api_proto::TrackId; use wasm_bindgen::{prelude::*, JsValue}; use web_sys::MediaStream as SysMediaStream; -use crate::media::TrackConstraints; +use crate::{ + media::TrackConstraints, + peer::media::{EnabledAudio, EnabledVideo}, + utils::HandlerDetachedError, +}; use super::MediaTrack; @@ -102,16 +106,16 @@ impl MediaStream { } /// Enables or disables all audio [`MediaTrack`]s in this stream. - pub fn toggle_audio_tracks(&self, enabled: bool) { + pub fn toggle_audio_tracks(&self, enabled: EnabledAudio) { for track in self.0.audio_tracks.values() { - track.set_enabled(enabled); + track.set_enabled(enabled.0); } } /// Enables or disables all video [`MediaTrack`]s in this stream. - pub fn toggle_video_tracks(&self, enabled: bool) { + pub fn toggle_video_tracks(&self, enabled: EnabledVideo) { for track in self.0.video_tracks.values() { - track.set_enabled(enabled); + track.set_enabled(enabled.0); } } @@ -135,6 +139,6 @@ pub struct MediaStreamHandle(Weak); impl MediaStreamHandle { /// Returns the underlying [`MediaStream`][`SysMediaStream`] object. pub fn get_media_stream(&self) -> Result { - map_weak!(self, |inner| inner.stream.clone()) + upgrade_or_detached!(self.0).map(|inner| inner.stream.clone()) } } diff --git a/jason/src/rpc/backoff_delayer.rs b/jason/src/rpc/backoff_delayer.rs new file mode 100644 index 000000000..cfd622cb4 --- /dev/null +++ b/jason/src/rpc/backoff_delayer.rs @@ -0,0 +1,67 @@ +//! Delayer that increases delay time by provided multiplier on each call. + +use crate::utils::{delay_for, JsDuration}; + +/// Delayer that increases delay time by provided multiplier on each call. +/// +/// Delay time increasing will be stopped when +/// [`BackoffDelayer::current_interval`] reaches +/// [`BackoffDelayer::max_interval`]. +/// +/// First delay will be [`BackoffDelayer::current_interval`]. +pub struct BackoffDelayer { + /// Delay of next [`BackoffDelayer::delay`] call. + /// + /// Will be increased by [`BackoffDelayer::delay`] call. + current_interval: JsDuration, + + /// Maximum delay for which this [`BackoffDelayer`] may delay. + max_interval: JsDuration, + + /// The multiplier by which [`BackoffDelayer::current_interval`] will be + /// multiplied on [`BackoffDelayer::delay`] call. + interval_multiplier: f32, +} + +impl BackoffDelayer { + /// Creates and returns new [`BackoffDelayer`]. + pub fn new( + starting_interval: JsDuration, + interval_multiplier: f32, + max_interval: JsDuration, + ) -> Self { + Self { + current_interval: starting_interval, + max_interval, + interval_multiplier, + } + } + + /// Resolves after [`BackoffDelayer::current_interval`] delay. + /// + /// Next call of this function will delay + /// [`BackoffDelayer::current_interval`] * + /// [`BackoffDelayer::interval_multiplier`] milliseconds, + /// until [`BackoffDelayer::max_interval`] is reached. + pub async fn delay(&mut self) { + delay_for(self.get_delay()).await; + } + + /// Returns current interval and increases it for next call. + fn get_delay(&mut self) -> JsDuration { + if self.is_max_interval_reached() { + self.max_interval + } else { + let delay = self.current_interval; + self.current_interval = + self.current_interval * self.interval_multiplier; + delay + } + } + + /// Returns `true` when max delay ([`BackoffDelayer::max_interval`]) is + /// reached. + fn is_max_interval_reached(&self) -> bool { + self.current_interval >= self.max_interval + } +} diff --git a/jason/src/rpc/heartbeat.rs b/jason/src/rpc/heartbeat.rs index f4c4b279e..0ee74586d 100644 --- a/jason/src/rpc/heartbeat.rs +++ b/jason/src/rpc/heartbeat.rs @@ -1,126 +1,204 @@ +//! Connection loss detection via ping/pong mechanism. + use std::{cell::RefCell, rc::Rc}; -use derive_more::{Display, From}; -use medea_client_api_proto::ClientMsg; -use tracerr::Traced; -use wasm_bindgen::{prelude::*, JsCast}; +use derive_more::{Display, From, Mul}; +use futures::{ + channel::mpsc, + future::{self, AbortHandle}, + stream::LocalBoxStream, + StreamExt as _, +}; +use medea_client_api_proto::{ClientMsg, ServerMsg}; +use wasm_bindgen_futures::spawn_local; use crate::{ rpc::{RpcTransport, TransportError}, - utils::{window, IntervalHandle, JsCaused, JsError}, + utils::{ + console_error, delay_for, JasonError, JsCaused, JsDuration, JsError, + }, }; /// Errors that may occur in [`Heartbeat`]. #[derive(Debug, Display, From, JsCaused)] -pub enum HeartbeatError { - /// Occurs when `ping` cannot be send because no transport. - #[display(fmt = "unable to ping: no transport")] - NoSocket, - - /// Occurs when a handler cannot be set to send `ping`. - #[display(fmt = "cannot set callback for ping send: {}", _0)] - SetIntervalHandler(JsError), - - /// Occurs when socket failed to send `ping`. - #[display(fmt = "failed to send ping: {}", _0)] - SendPing(#[js(cause)] TransportError), +pub struct HeartbeatError(TransportError); + +/// Wrapper around [`AbortHandle`] which aborts [`Future`] on [`Drop`]. +#[derive(Debug, From)] +struct TaskHandle(AbortHandle); + +impl Drop for TaskHandle { + fn drop(&mut self) { + self.0.abort(); + } } -type Result = std::result::Result>; - -/// Responsible for sending/handling keep-alive requests, detecting connection -/// loss. -// TODO: Implement connection loss detection. -pub struct Heartbeat(Rc>); - -struct InnerHeartbeat { - interval: i32, - /// Sent pings counter. - num: u64, - /// Timestamp of last pong received. - pong_at: Option, - /// Connection with remote RPC server. - transport: Option>, - /// Handler of sending `ping` task. Task is dropped if you drop handler. - ping_task: Option, +/// Idle timeout of [`RpcClient`]. +#[derive(Debug, Copy, Clone)] +pub struct IdleTimeout(pub JsDuration); + +/// Ping interval of [`RpcClient`]. +#[derive(Debug, Copy, Clone, Mul)] +pub struct PingInterval(pub JsDuration); + +/// Inner data of [`Heartbeat`]. +struct Inner { + /// [`RpcTransport`] which heartbeats. + transport: Rc, + + /// Idle timeout of [`RpcClient`]. + idle_timeout: IdleTimeout, + + /// Ping interval of [`RpcClient`]. + ping_interval: PingInterval, + + /// [`Abort`] for [`Future`] which sends [`ClientMsg::Pong`] on + /// [`ServerMsg::Ping`]. + handle_ping_task: Option, + + /// [`Abort`] for idle watchdog. + idle_watchdog_task: Option, + + /// Number of last received [`ServerMsg::Ping`]. + last_ping_num: u64, + + /// [`mpsc::UnboundedSender`]s for a [`Heartbeat::on_idle`]. + on_idle_subs: Vec>, } -impl InnerHeartbeat { - /// Send ping message to RPC server. - /// Returns errors if no open transport found. - fn send_now(&mut self) -> Result<()> { - match self.transport.as_ref() { - None => Err(tracerr::new!(HeartbeatError::NoSocket)), - Some(transport) => { - self.num += 1; - Ok(transport - .send(&ClientMsg::Ping(self.num)) - .map_err(tracerr::map_from_and_wrap!())?) - } - } +impl Inner { + /// Sends [`ClientMsg::Pong`] to a server. + /// + /// If some error happen then it will be printed with [`console_error`]. + fn send_pong(&self, n: u64) { + self.transport + .send(&ClientMsg::Pong(n)) + .map_err(tracerr::wrap!(=> TransportError)) + .map_err(JasonError::from) + .map_err(console_error) + .ok(); } } -/// Handler for binding closure that runs when `ping` is sent. -struct PingTaskHandler { - _closure: Closure, - _interval_handler: IntervalHandle, -} +/// Detector of connection loss via ping/pong mechanism. +pub struct Heartbeat(Rc>); impl Heartbeat { - /// Returns new instance of [`interval`] with given interval for ping in - /// milliseconds. - pub fn new(interval: i32) -> Self { - Self(Rc::new(RefCell::new(InnerHeartbeat { - interval, - num: 0, - pong_at: None, - transport: None, - ping_task: None, - }))) + /// Start this [`Heartbeat`] for the provided [`RpcTransport`] with + /// the provided `idle_timeout` and `ping_interval`. + pub fn start( + transport: Rc, + ping_interval: PingInterval, + idle_timeout: IdleTimeout, + ) -> Self { + let inner = Rc::new(RefCell::new(Inner { + idle_timeout, + ping_interval, + transport, + handle_ping_task: None, + idle_watchdog_task: None, + on_idle_subs: Vec::new(), + last_ping_num: 0, + })); + + let handle_ping_task = spawn_ping_handle_task(Rc::clone(&inner)); + let idle_watchdog_task = spawn_idle_watchdog_task(Rc::clone(&inner)); + + inner.borrow_mut().idle_watchdog_task = Some(idle_watchdog_task); + inner.borrow_mut().handle_ping_task = Some(handle_ping_task); + + Self(inner) } - /// Starts [`Heartbeat`] for given [`RpcTransport`]. - /// - /// Sends first `ping` immediately, so provided [`RpcTransport`] must be - /// active. - pub fn start(&self, transport: Rc) -> Result<()> { - let mut inner = self.0.borrow_mut(); - inner.num = 0; - inner.pong_at = None; - inner.transport = Some(transport); - inner.send_now().map_err(tracerr::wrap!())?; - - let inner_rc = Rc::clone(&self.0); - let do_ping = Closure::wrap(Box::new(move || { - // its_ok if ping fails few times - let _ = inner_rc.borrow_mut().send_now(); - }) as Box); - - let interval_id = window() - .set_interval_with_callback_and_timeout_and_arguments_0( - do_ping.as_ref().unchecked_ref(), - inner.interval, - ) - .map_err(JsError::from) - .map_err(tracerr::from_and_wrap!())?; - - inner.ping_task = Some(PingTaskHandler { - _closure: do_ping, - _interval_handler: IntervalHandle(interval_id), - }); - - Ok(()) + /// Updates this [`Heartbeat`] settings. + pub fn update_settings( + &self, + idle_timeout: IdleTimeout, + ping_interval: PingInterval, + ) { + self.0.borrow_mut().idle_timeout = idle_timeout; + self.0.borrow_mut().ping_interval = ping_interval; } - /// Stops [`Heartbeat`]. - pub fn stop(&self) { - self.0.borrow_mut().ping_task.take(); - self.0.borrow_mut().transport.take(); + /// Returns [`LocalBoxStream`] to which will sent `()` when [`Heartbeat`] + /// considers that [`RpcTransport`] is idle. + pub fn on_idle(&self) -> LocalBoxStream<'static, ()> { + let (on_idle_tx, on_idle_rx) = mpsc::unbounded(); + self.0.borrow_mut().on_idle_subs.push(on_idle_tx); + + Box::pin(on_idle_rx) } +} + +/// Spawns idle watchdog task returning its handle. +/// +/// This task is responsible for throwing [`Heartbeat::on_idle`] when +/// [`ServerMsg`] hasn't been received within `idle_timeout`. +/// +/// Also this watchdog will repeat [`ClientMsg::Pong`] if +/// [`ServerMsg::Ping`] wasn't received within `ping_interval * 2`. +fn spawn_idle_watchdog_task(this: Rc>) -> TaskHandle { + let (idle_watchdog_fut, idle_watchdog_handle) = + future::abortable(async move { + let wait_for_ping = this.borrow().ping_interval * 2; + delay_for(wait_for_ping.0).await; + + let last_ping_num = this.borrow().last_ping_num; + this.borrow().send_pong(last_ping_num + 1); + + let idle_timeout = this.borrow().idle_timeout; + delay_for(idle_timeout.0 - wait_for_ping.0).await; + this.borrow_mut() + .on_idle_subs + .retain(|sub| !sub.is_closed()); + this.borrow() + .on_idle_subs + .iter() + .filter_map(|sub| sub.unbounded_send(()).err()) + .for_each(|err| { + console_error(format!( + "Heartbeat::on_idle subscriber has gone unexpectedly: \ + {:?}", + err, + )) + }); + }); - /// Timestamp of last pong received. - pub fn set_pong_at(&self, at: f64) { - self.0.borrow_mut().pong_at = Some(at); + spawn_local(async move { + idle_watchdog_fut.await.ok(); + }); + + idle_watchdog_handle.into() +} + +/// Spawns ping handle task returning its handle. +/// +/// This task is responsible for answering [`ServerMsg::Ping`] with +/// [`ClientMsg::Pong`] and renewing idle watchdog task. +fn spawn_ping_handle_task(this: Rc>) -> TaskHandle { + let mut on_message_stream = this.borrow().transport.on_message(); + + let (handle_ping_fut, handle_ping_task) = future::abortable(async move { + while let Some(msg) = on_message_stream.next().await { + let idle_task = spawn_idle_watchdog_task(Rc::clone(&this)); + this.borrow_mut().idle_watchdog_task = Some(idle_task); + + if let ServerMsg::Ping(num) = msg { + this.borrow_mut().last_ping_num = num; + this.borrow().send_pong(num); + } + } + }); + spawn_local(async move { + handle_ping_fut.await.ok(); + }); + handle_ping_task.into() +} + +impl Drop for Heartbeat { + fn drop(&mut self) { + let mut inner = self.0.borrow_mut(); + inner.handle_ping_task.take(); + inner.idle_watchdog_task.take(); } } diff --git a/jason/src/rpc/mod.rs b/jason/src/rpc/mod.rs index 241f3258a..b6eab7851 100644 --- a/jason/src/rpc/mod.rs +++ b/jason/src/rpc/mod.rs @@ -1,9 +1,11 @@ //! Abstraction over RPC transport. +mod backoff_delayer; mod heartbeat; +mod reconnect_handle; mod websocket; -use std::{cell::RefCell, rc::Rc, vec}; +use std::{cell::RefCell, rc::Rc, time::Duration, vec}; use derive_more::{Display, From}; use futures::{ @@ -11,22 +13,24 @@ use futures::{ future::LocalBoxFuture, stream::{LocalBoxStream, StreamExt as _}, }; -use js_sys::Date; use medea_client_api_proto::{ ClientMsg, CloseDescription, CloseReason as CloseByServerReason, Command, - Event, ServerMsg, + Event, RpcSettings, ServerMsg, }; use serde::Serialize; use tracerr::Traced; use wasm_bindgen_futures::spawn_local; use web_sys::CloseEvent; -use crate::utils::{JasonError, JsCaused, JsError}; - -use self::heartbeat::{Heartbeat, HeartbeatError}; +use crate::utils::{console_error, JasonError, JsCaused, JsError}; #[doc(inline)] -pub use self::websocket::{TransportError, WebSocketRpcTransport}; +pub use self::{ + backoff_delayer::BackoffDelayer, + heartbeat::{Heartbeat, HeartbeatError, IdleTimeout, PingInterval}, + reconnect_handle::ReconnectHandle, + websocket::{TransportError, WebSocketRpcTransport}, +}; /// Reasons of closing by client side and server side. #[derive(Copy, Clone, Display, Debug, Eq, PartialEq)] @@ -83,7 +87,7 @@ impl Into for ClientDisconnect { } /// Connection with remote was closed. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum CloseMsg { /// Transport was gracefully closed by remote. /// @@ -116,6 +120,100 @@ impl From<&CloseEvent> for CloseMsg { } } +/// State of [`RpcClient`] and [`RpcTransport`]. +#[derive(Clone, Debug)] +pub enum State { + /// Socket has been created. The connection is not open yet. + /// + /// Reflects `CONNECTING` state from JS side [`WebSocket.readyState`][1]. + /// + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/readyState + Connecting, + + /// The connection is open and ready to communicate. + /// + /// Reflects `OPEN` state from JS side [`WebSocket.readyState`][1]. + /// + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/readyState + Open, + + /// The connection is in the process of closing. + /// + /// Reflects `CLOSING` state from JS side [`WebSocket.readyState`][1]. + /// + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/readyState + Closing, + + /// The connection is closed or couldn't be opened. + /// + /// Reflects `CLOSED` state from JS side [`WebSocket.readyState`][1]. + /// + /// [`ClosedStateReason`] is the reason of why + /// [`RpcClient`]/[`RpcTransport`] went into this [`State`]. + /// + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/readyState + Closed(ClosedStateReason), +} + +impl State { + /// Returns the number of [`WebSocket.readyState`][1]. + /// + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/readyState + pub fn id(&self) -> u8 { + match self { + Self::Connecting => 0, + Self::Open => 1, + Self::Closing => 2, + Self::Closed(_) => 3, + } + } +} + +/// The reason of why [`RpcClient`]/[`RpcTransport`] went into +/// [`State::Closed`]. +#[derive(Clone, Debug)] +pub enum ClosedStateReason { + /// Connection with server was lost. + ConnectionLost(CloseMsg), + + /// Error while creating connection between client and server. + ConnectionFailed(TransportError), + + /// [`State`] unexpectedly become [`State::Closed`]. + /// + /// Considered that this [`StateCloseReason`] will be never provided. + Unknown, + + /// Indicates that connection with server has never been established. + NeverConnected, + + /// First received [`ServerMsg`] after [`RpcClient::connect`] is not + /// [`ServerMsg::RpcSettings`]. + FirstServerMsgIsNotRpcSettings, +} + +impl State { + /// Returns `true` if socket can be closed. + pub fn can_close(&self) -> bool { + match self { + Self::Connecting | Self::Open => true, + _ => false, + } + } +} + +impl From for State { + fn from(value: u16) -> Self { + match value { + 0 => Self::Connecting, + 1 => Self::Open, + 2 => Self::Closing, + 3 => Self::Closed(ClosedStateReason::Unknown), + _ => unreachable!(), + } + } +} + /// Errors that may occur in [`RpcClient`]. #[derive(Debug, Display, From, JsCaused)] pub enum RpcClientError { @@ -126,16 +224,42 @@ pub enum RpcClientError { /// Occurs if the heartbeat cannot be started. #[display(fmt = "Start heartbeat failed: {}", _0)] CouldNotStartHeartbeat(#[js(cause)] HeartbeatError), + + /// Occurs if `socket` of [`WebSocketRpcClient`] is unexpectedly `None`. + #[display(fmt = "Socket of 'WebSocketRpcClient' is unexpectedly 'None'.")] + NoSocket, + + /// Occurs if [`Weak`] pointer to the [`RpcClient`] can't be upgraded to + /// [`Rc`]. + #[display(fmt = "RpcClient unexpectedly gone.")] + RpcClientGone, + + /// Occurs if [`RpcClient::connect`] fails. + #[display(fmt = "Connection failed. {:?}", _0)] + ConnectionFailed(ClosedStateReason), } // TODO: consider using async-trait crate, it doesnt work with mockall atm /// Client to talk with server via Client API RPC. #[cfg_attr(feature = "mockable", mockall::automock)] pub trait RpcClient { - /// Establishes connection with RPC server. + /// Tries to upgrade [`State`] of this [`RpcClient`] to [`State::Open`]. + /// + /// This function is also used for reconnection of this [`RpcClient`]. + /// + /// If [`RpcClient`] is closed than this function will try to establish + /// new RPC connection. + /// + /// If [`RpcClient`] already in [`State::Connecting`] then this function + /// will not perform one more connection try. It will subsribe to + /// [`State`] changes and wait for first connection result. And based on + /// this result - this function will be resolved. + /// + /// If [`RpcClient`] already in [`State::Open`] then this function will be + /// instantly resolved. fn connect( &self, - transport: Rc, + token: String, ) -> LocalBoxFuture<'static, Result<(), Traced>>; /// Returns [`Stream`] of all [`Event`]s received by this [`RpcClient`]. @@ -149,44 +273,47 @@ pub trait RpcClient { /// Sends [`Command`] to server. fn send_command(&self, command: Command); - /// Returns [`Future`] which will be resolved with [`CloseReason`] on - /// RPC connection close, caused by underlying transport close. Will not be - /// invoked on [`RpcClient`] drop. - fn on_close( + /// [`Future`] which will be resolved on normal [`RpcClient`] connection + /// closing. + /// + /// This [`Future`] wouldn't be resolved on abnormal closes. On + /// abnormal close [`RpcClient::on_connection_loss`] will be throwed. + fn on_normal_close( &self, ) -> LocalBoxFuture<'static, Result>; /// Sets reason, that will be passed to underlying transport when this /// client will be dropped. fn set_close_reason(&self, close_reason: ClientDisconnect); + + /// Returns [`Stream`] to which will be sent `()` on every connection loss. + /// + /// Connection loss is any unexpected [`RpcTransport`] close. In case of + /// connection loss, JS side user should select reconnection strategy with + /// [`ReconnectHandle`] (or simply close [`Room`]). + fn on_connection_loss(&self) -> LocalBoxStream<'static, ()>; + + /// Returns current token with which this [`RpcClient`] was connected. + /// + /// If token is `None` then [`RpcClient`] never was connected to a server. + fn get_token(&self) -> Option; } -/// RPC transport between client and server. +/// RPC transport between a client and a server. #[cfg_attr(feature = "mockable", mockall::automock)] pub trait RpcTransport { /// Returns [`LocalBoxStream`] of all messages received by this transport. - fn on_message( - &self, - ) -> Result< - LocalBoxStream<'static, Result>>, - Traced, - >; - - /// Returns [`LocalBoxFuture`], that will be resolved when this transport - /// will be closed. - fn on_close( - &self, - ) -> Result< - LocalBoxFuture<'static, Result>, - Traced, - >; + fn on_message(&self) -> LocalBoxStream<'static, ServerMsg>; /// Sets reason, that will be sent to remote server when this transport will /// be dropped. fn set_close_reason(&self, reason: ClientDisconnect); - /// Sends message to server. + /// Sends given [`ClientMsg`] to a server. fn send(&self, msg: &ClientMsg) -> Result<(), Traced>; + + /// Subscribes to a [`RpcTransport`]'s [`State`] changes. + fn on_state_change(&self) -> LocalBoxStream<'static, State>; } /// Inner state of [`WebsocketRpcClient`]. @@ -194,7 +321,8 @@ struct Inner { /// [`WebSocket`] connection to remote media server. sock: Option>, - heartbeat: Heartbeat, + /// Connection loss detector via ping/pong mechanism. + heartbeat: Option, /// Event's subscribers list. subs: Vec>, @@ -211,83 +339,74 @@ struct Inner { /// This reason will be provided to underlying [`RpcTransport`]. close_reason: ClientDisconnect, - /// Indicates that this [`WebSocketRpcClient`] is closed. - is_closed: bool, + /// Senders for [`RpcClient::on_connection_loss`] subscribers. + on_connection_loss_subs: Vec>, + + /// Closure which will create new [`RpcTransport`]s for this [`RpcClient`] + /// on every [`WebSocketRpcClient::establish_connection`] call. + rpc_transport_factory: RpcTransportFactory, + + /// Token with which this [`RpcClient`] was connected. + /// + /// Will be `None` if this [`RpcClient`] was never connected to a sever. + token: Option, + + /// Subscribers on [`State`] changes of this [`RpcClient`]. + on_state_change_subs: Vec>, + + /// Current [`State`] of this [`RpcClient`]. + state: State, } +/// Factory closure which creates [`RpcTransport`] for +/// [`WebSocketRpcClient::establish_connection`] function. +type RpcTransportFactory = Box< + dyn Fn( + String, + ) -> LocalBoxFuture< + 'static, + Result, Traced>, + >, +>; + impl Inner { - fn new(heartbeat_interval: i32) -> Rc> { + fn new(rpc_transport_factory: RpcTransportFactory) -> Rc> { Rc::new(RefCell::new(Self { sock: None, on_close_subscribers: Vec::new(), subs: vec![], - heartbeat: Heartbeat::new(heartbeat_interval), + heartbeat: None, close_reason: ClientDisconnect::RpcClientUnexpectedlyDropped, - is_closed: false, + on_connection_loss_subs: Vec::new(), + rpc_transport_factory, + token: None, + on_state_change_subs: Vec::new(), + state: State::Closed(ClosedStateReason::NeverConnected), })) } -} -/// Handles close message from remote server. -/// -/// This function will be called on every WebSocket close (normal and abnormal) -/// regardless of the [`CloseReason`]. -fn on_close(inner_rc: &RefCell, close_msg: &CloseMsg) { - let mut inner = inner_rc.borrow_mut(); - inner.sock.take(); - inner.heartbeat.stop(); - - // TODO: reconnect on disconnect, propagate error if unable - // to reconnect - - if let CloseMsg::Normal(_, reason) = &close_msg { - if *reason != CloseByServerReason::Reconnected { - inner - .on_close_subscribers - .drain(..) - .filter_map(|sub| { - sub.send(CloseReason::ByServer(*reason)).err() - }) - .for_each(|reason| { - console_error!(format!( - "Failed to send reason of Jason close to subscriber: \ - {:?}", - reason - )) + /// Updates [`State`] of this [`WebSocketRpcClient`] and sends update to all + /// [`RpcClient::on_state_change`] subscribers. + /// + /// Guarantees that two identical [`State`]s in a row won't be sent. + /// + /// Also, outdated subscribers will be cleaned here. + fn update_state(&mut self, state: &State) { + if self.state.id() != state.id() { + self.state = state.clone(); + self.on_state_change_subs.retain(|sub| !sub.is_closed()); + self.on_state_change_subs + .iter() + .filter_map(|sub| sub.unbounded_send(state.clone()).err()) + .for_each(|_| { + console_error( + "RpcClient::on_state_change sub unexpectedly gone.", + ) }); } } } -/// Handles messages from remote server. -fn on_message( - inner_rc: &RefCell, - msg: Result>, -) { - let inner = inner_rc.borrow(); - match msg { - Ok(ServerMsg::Pong(_num)) => { - // TODO: detect no pings - inner.heartbeat.set_pong_at(Date::now()); - } - Ok(ServerMsg::Event(event)) => { - // TODO: many subs, filter messages by session - if let Some(sub) = inner.subs.iter().next() { - if let Err(err) = sub.unbounded_send(event) { - // TODO: receiver is gone, should delete - // this subs tx - console_error!(err.to_string()); - } - } - } - Err(err) => { - // TODO: protocol versions mismatch? should drop - // connection if so - JasonError::from(err).print(); - } - } -} - // TODO: // 1. Proper sub registry. // 2. Reconnect. @@ -297,65 +416,291 @@ fn on_message( pub struct WebSocketRpcClient(Rc>); impl WebSocketRpcClient { - /// Creates new [`WebsocketRpcClient`] with a given `ping_interval` in - /// milliseconds. - pub fn new(ping_interval: i32) -> Self { - Self(Inner::new(ping_interval)) + /// Creates new [`WebSocketRpcClient`] with provided [`RpcTransportFactory`] + /// closure. + pub fn new(rpc_transport_factory: RpcTransportFactory) -> Self { + Self(Inner::new(rpc_transport_factory)) + } + + /// Stops [`Heartbeat`] and notifies all [`RpcClient::on_connection_loss`] + /// subs about connection loss. + fn on_connection_loss(&self) { + self.0.borrow_mut().heartbeat.take(); + self.0 + .borrow_mut() + .on_connection_loss_subs + .retain(|sub| !sub.is_closed()); + + let inner = self.0.borrow(); + for sub in &inner.on_connection_loss_subs { + if sub.unbounded_send(()).is_err() { + console_error( + "RpcClient::on_connection_loss subscriber is unexpectedly \ + gone.", + ); + } + } + } + + /// Handles [`CloseMsg`] from a remote server. + /// + /// This function will be called on every WebSocket close (normal and + /// abnormal) regardless of the [`CloseReason`]. + fn on_close_message(&self, close_msg: &CloseMsg) { + self.0.borrow_mut().heartbeat.take(); + + match &close_msg { + CloseMsg::Normal(_, reason) => match reason { + CloseByServerReason::Reconnected => (), + CloseByServerReason::Idle => { + self.on_connection_loss(); + } + _ => { + self.0.borrow_mut().sock.take(); + if *reason != CloseByServerReason::Reconnected { + self.0 + .borrow_mut() + .on_close_subscribers + .drain(..) + .filter_map(|sub| { + sub.send(CloseReason::ByServer(*reason)).err() + }) + .for_each(|reason| { + console_error(format!( + "Failed to send reason of Jason close to \ + subscriber: {:?}", + reason + )) + }); + } + } + }, + CloseMsg::Abnormal(_) => { + self.on_connection_loss(); + } + } + } + + /// Handles [`ServerMsg`]s from a remote server. + fn on_transport_message(&self, msg: ServerMsg) { + match msg { + ServerMsg::Event(event) => { + // TODO: filter messages by session + self.0.borrow_mut().subs.retain(|sub| !sub.is_closed()); + self.0 + .borrow() + .subs + .iter() + .filter_map(|sub| sub.unbounded_send(event.clone()).err()) + .for_each(|e| console_error(e.to_string())); + } + ServerMsg::RpcSettings(settings) => { + self.update_settings( + IdleTimeout( + Duration::from_millis(settings.idle_timeout_ms).into(), + ), + PingInterval( + Duration::from_millis(settings.ping_interval_ms).into(), + ), + ) + .map_err(tracerr::wrap!(=> RpcClientError)) + .map_err(JasonError::from) + .map_err(console_error) + .ok(); + } + _ => (), + } + } + + /// Starts [`Heartbeat`] with provided [`RpcSettings`] for provided + /// [`RpcTransport`]. + async fn start_heartbeat( + &self, + transport: Rc, + rpc_settings: RpcSettings, + ) -> Result<(), Traced> { + let idle_timeout = IdleTimeout( + Duration::from_millis(rpc_settings.idle_timeout_ms).into(), + ); + let ping_interval = PingInterval( + Duration::from_millis(rpc_settings.ping_interval_ms).into(), + ); + + let heartbeat = + Heartbeat::start(transport, ping_interval, idle_timeout); + + let mut on_idle = heartbeat.on_idle(); + let weak_this = Rc::downgrade(&self.0); + spawn_local(async move { + while let Some(_) = on_idle.next().await { + if let Some(this) = weak_this.upgrade().map(Self) { + this.on_connection_loss(); + } + } + }); + self.0.borrow_mut().heartbeat = Some(heartbeat); + + Ok(()) + } + + /// Tries to establish [`RpcClient`] connection. + async fn establish_connection( + &self, + token: String, + ) -> Result<(), Traced> { + self.0.borrow_mut().token = Some(token.clone()); + self.0.borrow_mut().update_state(&State::Connecting); + + // wait for transport open + let create_transport_fut = + (self.0.borrow().rpc_transport_factory)(token); + let transport = create_transport_fut.await.map_err(|e| { + let transport_err = e.into_inner(); + self.0.borrow_mut().update_state(&State::Closed( + ClosedStateReason::ConnectionFailed(transport_err.clone()), + )); + tracerr::new!(RpcClientError::from( + ClosedStateReason::ConnectionFailed(transport_err) + )) + })?; + + // wait for ServerMsg::RpcSettings + if let Some(msg) = transport.on_message().next().await { + if let ServerMsg::RpcSettings(rpc_settings) = msg { + self.start_heartbeat(Rc::clone(&transport), rpc_settings) + .await?; + self.0.borrow_mut().update_state(&State::Open); + } else { + let close_reason = + ClosedStateReason::FirstServerMsgIsNotRpcSettings; + self.0 + .borrow_mut() + .update_state(&State::Closed(close_reason.clone())); + return Err(tracerr::new!(RpcClientError::ConnectionFailed( + close_reason + ))); + } + } else { + return Err(tracerr::new!(RpcClientError::NoSocket)); + } + + // subscribe to transport close + let mut transport_on_state_change_stream = transport.on_state_change(); + let weak_inner = Rc::downgrade(&self.0); + spawn_local(async move { + while let Some(state) = + transport_on_state_change_stream.next().await + { + if let Some(this) = weak_inner.upgrade().map(Self) { + if let State::Closed(reason) = &state { + if let ClosedStateReason::ConnectionLost(msg) = reason { + this.on_close_message(&msg); + } + } + this.0.borrow_mut().update_state(&state); + } + } + }); + + // subscribe to transport message received + let this_clone = Rc::downgrade(&self.0); + let mut on_socket_message = transport.on_message(); + spawn_local(async move { + while let Some(msg) = on_socket_message.next().await { + if let Some(this) = this_clone.upgrade().map(Self) { + this.on_transport_message(msg) + } + } + }); + + self.0.borrow_mut().sock.replace(transport); + Ok(()) + } + + /// Subscribes to [`RpcClient`]'s [`State`] changes and when + /// [`State::Connecting`] will be changed to something else, then this + /// [`Future`] will be resolved and based on new [`State`] [`Result`] + /// will be returned. + async fn connecting_result(&self) -> Result<(), Traced> { + let mut transport_state_stream = self.on_state_change(); + while let Some(state) = transport_state_stream.next().await { + match state { + State::Open => { + return Ok(()); + } + State::Closed(reason) => { + return Err(tracerr::new!( + RpcClientError::ConnectionFailed(reason) + )); + } + State::Connecting | State::Closing => (), + } + } + Err(tracerr::new!(RpcClientError::RpcClientGone)) + } + + /// Updates RPC settings of this [`RpcClient`]. + fn update_settings( + &self, + idle_timeout: IdleTimeout, + ping_interval: PingInterval, + ) -> Result<(), Traced> { + self.0 + .borrow_mut() + .heartbeat + .as_ref() + .ok_or_else(|| tracerr::new!(RpcClientError::NoSocket)) + .map(|heartbeat| { + heartbeat.update_settings(idle_timeout, ping_interval) + }) + } + + /// Returns current [`State`] of this [`RpcClient`]. + fn get_state(&self) -> State { + self.0.borrow().state.clone() + } + + /// Subscribes to [`RpcClient`]'s [`State`] changes. + /// + /// It guarantees that two identical [`State`]s in a row won't be sent. + fn on_state_change(&self) -> LocalBoxStream<'static, State> { + let (tx, rx) = mpsc::unbounded(); + self.0.borrow_mut().on_state_change_subs.push(tx); + + Box::pin(rx) } } impl RpcClient for WebSocketRpcClient { - /// Creates new WebSocket connection to remote media server. - /// Starts `Heartbeat` if connection succeeds and binds handlers - /// on receiving messages from server and closing socket. fn connect( &self, - transport: Rc, + token: String, ) -> LocalBoxFuture<'static, Result<(), Traced>> { - let inner = Rc::clone(&self.0); + let weak_inner = Rc::downgrade(&self.0); Box::pin(async move { - inner - .borrow_mut() - .heartbeat - .start(Rc::clone(&transport)) - .map_err(tracerr::map_from_and_wrap!())?; - - let inner_rc = Rc::clone(&inner); - let mut on_socket_message = transport - .on_message() - .map_err(tracerr::map_from_and_wrap!())?; - spawn_local(async move { - while let Some(msg) = on_socket_message.next().await { - on_message(&inner_rc, msg) - } - }); - - let inner_rc = Rc::clone(&inner); - let on_socket_close = transport - .on_close() - .map_err(tracerr::map_from_and_wrap!())?; - spawn_local(async move { - match on_socket_close.await { - Ok(msg) => on_close(&inner_rc, &msg), - Err(e) => { - if !inner_rc.borrow().is_closed { - console_error!(format!( - "RPC socket was unexpectedly dropped. {:?}", - e - )); + if let Some(this) = weak_inner.upgrade().map(Self) { + let current_token = this.0.borrow().token.clone(); + if let Some(current_token) = current_token { + if current_token == token { + match this.get_state() { + State::Open => Ok(()), + State::Connecting => this.connecting_result().await, + State::Closed(_) | State::Closing => { + this.establish_connection(token).await + } } + } else { + this.establish_connection(token).await } + } else { + this.establish_connection(token).await } - }); - - inner.borrow_mut().sock.replace(transport); - Ok(()) + } else { + Err(tracerr::new!(RpcClientError::NoSocket)) + } }) } - /// Returns [`Stream`] of all [`Event`]s received by this [`RpcClient`]. - /// - /// [`Stream`]: futures::Stream // TODO: proper sub registry fn subscribe(&self) -> LocalBoxStream<'static, Event> { let (tx, rx) = mpsc::unbounded(); @@ -364,13 +709,11 @@ impl RpcClient for WebSocketRpcClient { Box::pin(rx) } - /// Unsubscribes from this [`RpcClient`]. Drops all subscriptions atm. // TODO: proper sub registry fn unsub(&self) { self.0.borrow_mut().subs.clear(); } - /// Sends [`Command`] to RPC server. // TODO: proper sub registry fn send_command(&self, command: Command) { let socket_borrow = &self.0.borrow().sock; @@ -381,10 +724,7 @@ impl RpcClient for WebSocketRpcClient { } } - /// Returns [`Future`] which will be resolved with [`CloseReason`] on - /// RPC connection close, caused by underlying transport close. Will not be - /// invoked on [`RpcClient`] drop. - fn on_close( + fn on_normal_close( &self, ) -> LocalBoxFuture<'static, Result> { let (tx, rx) = oneshot::channel(); @@ -392,21 +732,27 @@ impl RpcClient for WebSocketRpcClient { Box::pin(rx) } - /// Sets reason, that will be passed to underlying transport when this - /// client will be dropped. fn set_close_reason(&self, close_reason: ClientDisconnect) { self.0.borrow_mut().close_reason = close_reason } + + fn on_connection_loss(&self) -> LocalBoxStream<'static, ()> { + let (tx, rx) = mpsc::unbounded(); + self.0.borrow_mut().on_connection_loss_subs.push(tx); + + Box::pin(rx) + } + + fn get_token(&self) -> Option { + self.0.borrow().token.clone() + } } -impl Drop for WebSocketRpcClient { +impl Drop for Inner { /// Drops related connection and its [`Heartbeat`]. fn drop(&mut self) { - self.0.borrow_mut().is_closed = true; - let mut inner = self.0.borrow_mut(); - if let Some(socket) = inner.sock.take() { - socket.set_close_reason(inner.close_reason.clone()); + if let Some(socket) = self.sock.take() { + socket.set_close_reason(self.close_reason.clone()); } - inner.heartbeat.stop(); } } diff --git a/jason/src/rpc/reconnect_handle.rs b/jason/src/rpc/reconnect_handle.rs new file mode 100644 index 000000000..c55ac1fa7 --- /dev/null +++ b/jason/src/rpc/reconnect_handle.rs @@ -0,0 +1,105 @@ +//! Reconnection for [`RpcClient`]. + +use std::{rc::Weak, time::Duration}; + +use derive_more::Display; +use js_sys::Promise; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{ + rpc::{BackoffDelayer, RpcClient}, + utils::{delay_for, HandlerDetachedError, JasonError, JsCaused, JsError}, +}; + +/// Error which indicates that [`RpcClient`]'s (which this [`ReconnectHandle`] +/// tries to reconnect) token is `None`. +#[derive(Debug, Display, JsCaused)] +struct NoTokenError; + +/// Handle that JS side can reconnect to the Medea media server on +/// a connection loss with. +/// +/// This handle will be provided into `Room.on_connection_loss` callback. +#[wasm_bindgen] +#[derive(Clone)] +pub struct ReconnectHandle(Weak); + +impl ReconnectHandle { + /// Instantiates new [`ReconnectHandle`] from the given [`RpcClient`] + /// reference. + pub fn new(rpc: Weak) -> Self { + Self(rpc) + } +} + +#[wasm_bindgen] +impl ReconnectHandle { + /// Tries to reconnect after the provided delay in milliseconds. + /// + /// If [`RpcClient`] is already reconnecting then new reconnection attempt + /// won't be performed. Instead, it will wait for the first reconnection + /// attempt result and use it here. + pub fn reconnect_with_delay(&self, delay_ms: u32) -> Promise { + let rpc = Clone::clone(&self.0); + future_to_promise(async move { + delay_for(Duration::from_millis(u64::from(delay_ms)).into()).await; + + let rpc = upgrade_or_detached!(rpc, JsValue)?; + let token = rpc + .get_token() + .ok_or_else(|| new_js_error!(NoTokenError => JsValue))?; + rpc.connect(token) + .await + .map_err(|e| JsValue::from(JasonError::from(e)))?; + + Ok(JsValue::UNDEFINED) + }) + } + + /// Tries to reconnect [`RpcClient`] in a loop with a growing backoff delay. + /// + /// The first attempt to reconnect is guaranteed to happen no earlier than + /// `starting_delay_ms`. + /// + /// Also, it guarantees that delay between reconnection attempts won't be + /// greater than `max_delay_ms`. + /// + /// After each reconnection attempt, delay between reconnections will be + /// multiplied by the given `multiplier` until it reaches `max_delay_ms`. + /// + /// If [`RpcClient`] is already reconnecting then new reconnection attempt + /// won't be performed. Instead, it will wait for the first reconnection + /// attempt result and use it here. + /// + /// If `multiplier` is negative number than `multiplier` will be considered + /// as `0.0`. + pub fn reconnect_with_backoff( + &self, + starting_delay_ms: u32, + multiplier: f32, + max_delay: u32, + ) -> Promise { + let rpc = self.0.clone(); + future_to_promise(async move { + let token = upgrade_or_detached!(rpc, JsValue)? + .get_token() + .ok_or_else(|| new_js_error!(NoTokenError => JsValue))?; + + let mut backoff_delayer = BackoffDelayer::new( + Duration::from_millis(u64::from(starting_delay_ms)).into(), + multiplier, + Duration::from_millis(u64::from(max_delay)).into(), + ); + backoff_delayer.delay().await; + while let Err(_) = upgrade_or_detached!(rpc, JsValue)? + .connect(token.clone()) + .await + { + backoff_delayer.delay().await; + } + + Ok(JsValue::UNDEFINED) + }) + } +} diff --git a/jason/src/rpc/websocket.rs b/jason/src/rpc/websocket.rs index c6cf4bf5b..c6fc99d74 100644 --- a/jason/src/rpc/websocket.rs +++ b/jason/src/rpc/websocket.rs @@ -2,25 +2,29 @@ //! //! [WebSocket]: https://developer.mozilla.org/ru/docs/WebSockets -use std::{borrow::Cow, cell::RefCell, convert::TryFrom, rc::Rc}; +use std::{cell::RefCell, convert::TryFrom, rc::Rc}; use derive_more::{Display, From, Into}; use futures::{ channel::{mpsc, oneshot}, - future::{self, LocalBoxFuture}, + future, stream::LocalBoxStream, }; use medea_client_api_proto::{ClientMsg, ServerMsg}; use tracerr::Traced; use web_sys::{CloseEvent, Event, MessageEvent, WebSocket as SysWebSocket}; -use crate::{ - rpc::{ClientDisconnect, CloseMsg, RpcTransport}, - utils::{EventListener, EventListenerBindError, JsCaused, JsError}, +use crate::utils::{ + console_error, EventListener, EventListenerBindError, JasonError, JsCaused, + JsError, +}; + +use super::{ + ClientDisconnect, CloseMsg, ClosedStateReason, RpcTransport, State, }; /// Errors that may occur when working with [`WebSocket`]. -#[derive(Debug, Display, JsCaused)] +#[derive(Clone, Debug, Display, JsCaused)] pub enum TransportError { /// Occurs when the port to which the connection is being attempted /// is being blocked. @@ -33,11 +37,11 @@ pub enum TransportError { /// Occurs when [`ClientMessage`] cannot be parsed. #[display(fmt = "Failed to parse client message: {}", _0)] - ParseClientMessage(serde_json::error::Error), + ParseClientMessage(Rc), /// Occurs when [`ServerMessage`] cannot be parsed. #[display(fmt = "Failed to parse server message: {}", _0)] - ParseServerMessage(serde_json::error::Error), + ParseServerMessage(Rc), /// Occurs if the parsed message is not string. #[display(fmt = "Message is not a string")] @@ -57,10 +61,16 @@ pub enum TransportError { ClosedSocket, } +impl From for TransportError { + fn from(err: EventListenerBindError) -> Self { + Self::WebSocketEventBindError(err) + } +} + /// Wrapper for help to get [`ServerMsg`] from Websocket [MessageEvent][1]. /// /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent -#[derive(From, Into)] +#[derive(Clone, From, Into)] pub struct ServerMessage(ServerMsg); impl TryFrom<&MessageEvent> for ServerMessage { @@ -71,161 +81,126 @@ impl TryFrom<&MessageEvent> for ServerMessage { let payload = msg.data().as_string().ok_or(MessageNotString)?; serde_json::from_str::(&payload) - .map_err(ParseServerMessage) + .map_err(|e| ParseServerMessage(e.into())) .map(Self::from) } } -impl From for TransportError { - fn from(err: EventListenerBindError) -> Self { - Self::WebSocketEventBindError(err) - } -} - type Result> = std::result::Result; -/// State of websocket. -#[derive(Debug)] -enum State { - CONNECTING, - OPEN, - CLOSING, - CLOSED, -} - -impl State { - /// Returns `true` if socket can be closed. - pub fn can_close(&self) -> bool { - match self { - Self::CONNECTING | Self::OPEN => true, - _ => false, - } - } -} - -impl From for State { - fn from(value: u16) -> Self { - match value { - 0 => Self::CONNECTING, - 1 => Self::OPEN, - 2 => Self::CLOSING, - 3 => Self::CLOSED, - _ => unreachable!(), - } - } -} - struct InnerSocket { + /// JS side [WebSocket]. + /// + /// [WebSocket]: https://developer.mozilla.org/docs/Web/API/WebSocket socket: Rc, + + /// State of [`WebSocketTransport`] connection. socket_state: State, - on_open: Option>, - on_message: Option>, - on_close: Option>, - on_error: Option>, + + /// Listener for [WebSocket] [open event][1]. + /// + /// [WebSocket]: https://developer.mozilla.org/docs/Web/API/WebSocket + /// [1]: https://developer.mozilla.org/en-US/Web/API/WebSocket/open_event + on_open_listener: Option>, + + /// Listener for [WebSocket] [message event][1]. + /// + /// [WebSocket]: https://developer.mozilla.org/docs/Web/API/WebSocket + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/message_event + on_message_listener: Option>, + + /// Listener for [WebSocket] [close event][1]. + /// + /// [WebSocket]: https://developer.mozilla.org/docs/Web/API/WebSocket + /// [1]: https://developer.mozilla.org/docs/Web/API/WebSocket/close_event + on_close_listener: Option>, + + /// Subscribers for [`RpcTransport::on_message`] events. + on_message_subs: Vec>, + + /// Subscribers for [`RpcTransport::on_state_change`] events. + on_state_change_subs: Vec>, + + /// Reason of [`WebSocketRpcTransport`] closing. + /// Will be sent in [WebSocket close frame][1]. + /// + /// [1]: https://tools.ietf.org/html/rfc6455#section-5.5.1 close_reason: ClientDisconnect, } -/// WebSocket [`RpcTransport`] between client and server. -pub struct WebSocketRpcTransport(Rc>); - impl InnerSocket { fn new(url: &str) -> Result { - let socket = SysWebSocket::new(url) + let socket = SysWebSocket::new(&url) .map_err(Into::into) .map_err(TransportError::CreateSocket) .map_err(tracerr::wrap!())?; Ok(Self { - socket_state: State::CONNECTING, + socket_state: State::Connecting, socket: Rc::new(socket), - on_open: None, - on_message: None, - on_close: None, - on_error: None, + on_open_listener: None, + on_message_listener: None, + on_close_listener: None, + on_message_subs: Vec::new(), + on_state_change_subs: Vec::new(), close_reason: ClientDisconnect::RpcTransportUnexpectedlyDropped, }) } - /// Checks underlying WebSocket state and updates `socket_state`. - fn update_state(&mut self) { - self.socket_state = self.socket.ready_state().into(); - } -} - -impl RpcTransport for WebSocketRpcTransport { - fn on_message(&self) -> Result>> { - let (tx, rx) = mpsc::unbounded(); - let mut inner_mut = self.0.borrow_mut(); - inner_mut.on_message = Some( - EventListener::new_mut( - Rc::clone(&inner_mut.socket), - "message", - move |msg| { - let parsed = ServerMessage::try_from(&msg) - .map(Into::into) - .map_err(tracerr::wrap!()); - tx.unbounded_send(parsed).unwrap_or_else(|e| { - console_error!(format!( - "WebSocket's 'on_message' callback receiver \ - unexpectedly gone. {:?}", - e - )) - }); - }, - ) - .map_err(tracerr::map_from_and_wrap!(=> TransportError))?, - ); - Ok(Box::pin(rx)) + /// Updates `socket_state` of this [`InnerSocket`] with the provided + /// [`State`]. + /// + /// Sends updated [`State`] to `on_state_change` subscribers. + /// But, if [`State`] is not changed, then nothing will be sent. + fn update_socket_state(&mut self, new: &State) { + if self.socket_state.id() != new.id() { + self.socket_state = new.clone(); + self.on_state_change_subs.retain(|sub| !sub.is_closed()); + + self.on_state_change_subs + .iter() + .filter_map(|sub| sub.unbounded_send(new.clone()).err()) + .for_each(|e| { + console_error(format!( + "'WebSocketRpcTransport::on_state_change' subscriber \ + has gone unexpectedly: {:?}", + e, + )); + }); + } } - fn on_close( - &self, - ) -> Result>> - { - let (tx, rx) = oneshot::channel(); - let mut inner_mut = self.0.borrow_mut(); - let inner = Rc::clone(&self.0); - inner_mut.on_close = Some( - EventListener::new_once( - Rc::clone(&inner_mut.socket), - "close", - move |msg: CloseEvent| { - inner.borrow_mut().update_state(); - tx.send(CloseMsg::from(&msg)).unwrap_or_else(|e| { - console_error!(format!( - "WebSocket's 'on_close' callback receiver \ - unexpectedly gone. {:?}", - e - )) - }); - }, - ) - .map_err(tracerr::map_from_and_wrap!(=> TransportError))?, - ); - Ok(Box::pin(rx)) + /// Checks underlying WebSocket state and updates `socket_state`. + fn sync_socket_state(&mut self) { + self.update_socket_state(&self.socket.ready_state().into()); } +} - fn send(&self, msg: &ClientMsg) -> Result<()> { - let inner = self.0.borrow(); - let message = serde_json::to_string(msg) - .map_err(TransportError::ParseClientMessage) - .map_err(tracerr::wrap!())?; - - match inner.socket_state { - State::OPEN => inner - .socket - .send_with_str(&message) - .map_err(Into::into) - .map_err(TransportError::SendMessage) - .map_err(tracerr::wrap!()), - _ => Err(tracerr::new!(TransportError::ClosedSocket)), +impl Drop for InnerSocket { + fn drop(&mut self) { + if self.socket_state.can_close() { + let rsn = serde_json::to_string(&self.close_reason) + .expect("Could not serialize close message"); + if let Err(e) = self.socket.close_with_code_and_reason(1000, &rsn) { + console_error(e); + } } } - - fn set_close_reason(&self, close_reason: ClientDisconnect) { - self.0.borrow_mut().close_reason = close_reason; - } } +/// WebSocket [`RpcTransport`] between a client and a server. +/// +/// # Drop +/// +/// This structure has __cyclic references__, which are freed in its [`Drop`] +/// implementation. +/// +/// If you're adding new cyclic dependencies, then don't forget to drop them in +/// the [`Drop`] implementation and mention in the list below: +/// 1. [`InnerSocket::on_close_listener`] +/// 2. [`InnerSocket::on_message_listener`] +/// 3. [`InnerSocket::on_open_listener`] +pub struct WebSocketRpcTransport(Rc>); + impl WebSocketRpcTransport { /// Initiates new WebSocket connection. Resolves only when underlying /// connection becomes active. @@ -233,18 +208,17 @@ impl WebSocketRpcTransport { let (tx_close, rx_close) = oneshot::channel(); let (tx_open, rx_open) = oneshot::channel(); - let inner = InnerSocket::new(url)?; - let socket = Rc::new(RefCell::new(inner)); + let socket = Rc::new(RefCell::new(InnerSocket::new(url)?)); { let mut socket_mut = socket.borrow_mut(); let inner = Rc::clone(&socket); - socket_mut.on_close = Some( + socket_mut.on_close_listener = Some( EventListener::new_once( Rc::clone(&socket_mut.socket), "close", move |_| { - inner.borrow_mut().update_state(); + inner.borrow_mut().sync_socket_state(); let _ = tx_close.send(()); }, ) @@ -252,12 +226,12 @@ impl WebSocketRpcTransport { ); let inner = Rc::clone(&socket); - socket_mut.on_open = Some( + socket_mut.on_open_listener = Some( EventListener::new_once( Rc::clone(&socket_mut.socket), "open", move |_| { - inner.borrow_mut().update_state(); + inner.borrow_mut().sync_socket_state(); let _ = tx_open.send(()); }, ) @@ -267,42 +241,130 @@ impl WebSocketRpcTransport { let state = future::select(rx_open, rx_close).await; - socket.borrow_mut().on_open.take(); - socket.borrow_mut().on_close.take(); + let this = Self(socket); + this.set_on_close_listener()?; + this.set_on_message_listener()?; match state { future::Either::Left((opened, _)) => match opened { - Ok(_) => Ok(Self(socket)), + Ok(_) => Ok(this), Err(_) => Err(tracerr::new!(TransportError::InitSocket)), }, - future::Either::Right(_closed) => { + future::Either::Right(_) => { Err(tracerr::new!(TransportError::InitSocket)) } } } + + /// Sets [`WebSocketRpcTransport::on_close_listener`] which will update + /// [`RpcTransport`]'s [`State`] to [`State::Closed`] with a + /// [`ClosedStateReason::ConnectionLoss`]. + fn set_on_close_listener(&self) -> Result<()> { + let this = Rc::clone(&self.0); + let on_close = EventListener::new_once( + Rc::clone(&self.0.borrow().socket), + "close", + move |msg: CloseEvent| { + let close_msg = CloseMsg::from(&msg); + this.borrow_mut().update_socket_state(&State::Closed( + ClosedStateReason::ConnectionLost(close_msg), + )); + }, + ) + .map_err(tracerr::map_from_and_wrap!(=> TransportError))?; + self.0.borrow_mut().on_close_listener = Some(on_close); + + Ok(()) + } + + /// Sets [`WebSocketRpcTransport::on_message_listener`] which will send + /// [`ServerMessage`]s to [`WebSocketRpcTransport::on_message`] subscribers. + fn set_on_message_listener(&self) -> Result<()> { + let this = Rc::clone(&self.0); + let on_message = EventListener::new_mut( + Rc::clone(&self.0.borrow().socket), + "message", + move |msg| { + let msg = + match ServerMessage::try_from(&msg).map(ServerMsg::from) { + Ok(parsed) => parsed, + Err(e) => { + // TODO: protocol versions mismatch? should drop + // connection if so + JasonError::from(tracerr::new!(e)).print(); + return; + } + }; + + let mut this_mut = this.borrow_mut(); + this_mut + .on_message_subs + .retain(|on_message| !on_message.is_closed()); + this_mut.on_message_subs.iter().for_each(|on_message| { + on_message.unbounded_send(msg.clone()).unwrap_or_else( + |e| { + console_error(format!( + "WebSocket's 'on_message' callback receiver \ + unexpectedly gone. {:?}", + e + )) + }, + ); + }) + }, + ) + .map_err(tracerr::map_from_and_wrap!(=> TransportError))?; + + self.0.borrow_mut().on_message_listener = Some(on_message); + + Ok(()) + } +} + +impl RpcTransport for WebSocketRpcTransport { + fn on_message(&self) -> LocalBoxStream<'static, ServerMsg> { + let (tx, rx) = mpsc::unbounded(); + self.0.borrow_mut().on_message_subs.push(tx); + + Box::pin(rx) + } + + fn set_close_reason(&self, close_reason: ClientDisconnect) { + self.0.borrow_mut().close_reason = close_reason; + } + + fn send(&self, msg: &ClientMsg) -> Result<()> { + let inner = self.0.borrow(); + let message = serde_json::to_string(msg) + .map_err(|e| TransportError::ParseClientMessage(e.into())) + .map_err(tracerr::wrap!())?; + + match inner.socket_state { + State::Open => inner + .socket + .send_with_str(&message) + .map_err(Into::into) + .map_err(TransportError::SendMessage) + .map_err(tracerr::wrap!()), + _ => Err(tracerr::new!(TransportError::ClosedSocket)), + } + } + + fn on_state_change(&self) -> LocalBoxStream<'static, State> { + let (tx, rx) = mpsc::unbounded(); + self.0.borrow_mut().on_state_change_subs.push(tx); + + Box::pin(rx) + } } impl Drop for WebSocketRpcTransport { + /// Don't forget that [`WebSocketRpcTransport`] is a [`Rc`] and this + /// [`Drop`] implementation will be called on each drop of its references. fn drop(&mut self) { let mut inner = self.0.borrow_mut(); - if inner.socket_state.can_close() { - inner.on_open.take(); - inner.on_error.take(); - inner.on_message.take(); - inner.on_close.take(); - - let close_reason: Cow<'static, str> = - serde_json::to_string(&inner.close_reason) - .unwrap_or_else(|_| { - "Could not serialize close message".into() - }) - .into(); - - if let Err(err) = - inner.socket.close_with_code_and_reason(1000, &close_reason) - { - console_error!(err); - } - } + inner.on_open_listener.take(); + inner.on_message_listener.take(); + inner.on_close_listener.take(); } } diff --git a/jason/src/utils/errors.rs b/jason/src/utils/errors.rs index 892981276..10122b330 100644 --- a/jason/src/utils/errors.rs +++ b/jason/src/utils/errors.rs @@ -9,11 +9,14 @@ use wasm_bindgen::{prelude::*, JsCast}; pub use medea_macro::JsCaused; -/// Prints `$e` as `console.error()`. -macro_rules! console_error { - ($e:expr) => { - web_sys::console::error_1(&$e.into()) - }; +/// Prints provided message with [`Console.error()`]. +/// +/// [`Console.error()`]: https://tinyurl.com/psv3wqw +pub fn console_error(msg: M) +where + M: Into, +{ + web_sys::console::error_1(&msg.into()); } /// Representation of an error which can caused by error returned from the @@ -30,7 +33,7 @@ pub trait JsCaused { } /// Wrapper for JS value which returned from JS side as error. -#[derive(Debug, Display)] +#[derive(Clone, Debug, Display)] #[display(fmt = "{}: {}", name, message)] pub struct JsError { /// Name of JS error. @@ -85,7 +88,7 @@ pub struct JasonError { impl JasonError { /// Prints error information to `console.error()`. pub fn print(&self) { - console_error!(self.to_string()); + console_error(self.to_string()); } } diff --git a/jason/src/utils/event_listener.rs b/jason/src/utils/event_listener.rs index e1563f398..d5e30b602 100644 --- a/jason/src/utils/event_listener.rs +++ b/jason/src/utils/event_listener.rs @@ -5,12 +5,12 @@ use tracerr::Traced; use wasm_bindgen::{closure::Closure, convert::FromWasmAbi, JsCast}; use web_sys::EventTarget; -use crate::utils::{errors::JsCaused, JsError}; +use crate::utils::{console_error, errors::JsCaused, JsError}; /// Failed to bind to [`EventTarget`][1] event. /// /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget -#[derive(Debug, Display, From, JsCaused)] +#[derive(Clone, Debug, Display, From, JsCaused)] pub struct EventListenerBindError(JsError); /// Wrapper for closure that handles some [`EventTarget`] event. @@ -96,7 +96,7 @@ where self.closure.as_ref().unchecked_ref(), ) { - console_error!(err); + console_error(err); } } } diff --git a/jason/src/utils/mod.rs b/jason/src/utils/mod.rs index 10bf88610..61b942459 100644 --- a/jason/src/utils/mod.rs +++ b/jason/src/utils/mod.rs @@ -6,14 +6,20 @@ mod errors; mod callback; mod event_listener; -use js_sys::Reflect; +use std::{convert::TryInto as _, ops::Mul, time::Duration}; + +use derive_more::{From, Sub}; +use js_sys::{Promise, Reflect}; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; use web_sys::Window; #[doc(inline)] pub use self::{ callback::{Callback, Callback2}, - errors::{HandlerDetachedError, JasonError, JsCaused, JsError}, + errors::{ + console_error, HandlerDetachedError, JasonError, JsCaused, JsError, + }, event_listener::{EventListener, EventListenerBindError}, }; @@ -28,6 +34,50 @@ pub fn window() -> Window { web_sys::window().unwrap() } +/// Wrapper around [`Duration`] which can be transformed into [`i32`] for JS +/// side timers. +/// +/// Also [`JsDuration`] can be multiplied by [`f32`]. +#[derive(Clone, Copy, Debug, From, PartialEq, PartialOrd, Sub)] +pub struct JsDuration(Duration); + +impl JsDuration { + /// Converts this [`JsDuration`] into `i32` milliseconds. + /// + /// Unfortunately, [`web_sys`] believes that only `i32` can be passed to a + /// `setTimeout`. But it is unlikely we will need a duration of more, + /// than 596 hours, so it was decided to simply truncate the number. If we + /// will need a longer duration in the future, then we can implement this + /// with a few `setTimeout`s. + #[inline] + pub fn into_js_duration(self) -> i32 { + self.0.as_millis().try_into().unwrap_or(i32::max_value()) + } +} + +impl Mul for JsDuration { + type Output = Self; + + #[inline] + fn mul(self, rhs: u32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl Mul for JsDuration { + type Output = Self; + + #[inline] + fn mul(self, mut rhs: f32) -> Self::Output { + // Emulation of JS side's 'setTimeout' behavior which will be instantly + // resolved if call it with negative number. + if rhs < 0.0 { + rhs = 0.0; + }; + Self(self.0.mul_f64(rhs.into())) + } +} + /// Wrapper around interval timer ID. pub struct IntervalHandle(pub i32); @@ -38,25 +88,40 @@ impl Drop for IntervalHandle { } } -/// Upgrades newtyped [`Weak`] reference, returning [`HandlerDetachedError`] if -/// failed, or maps the [`Rc`]-referenced value with provided `$closure` -/// otherwise. +/// Upgrades provided [`Weak`] reference, mapping it to a [`Result`] with +/// [`HandlerDetachedError`] and invokes [`Into::into`] on the error. +/// If the errot type cannot be inferred, then you can provide a concrete type +/// (usually being [`JasonError`] or [`JsValue`]). /// -/// [`Rc`]: std::rc::Rc /// [`Weak`]: std::rc::Weak -macro_rules! map_weak { - ($v:ident, $closure:expr) => {{ - $v.0.upgrade() - .ok_or( - $crate::utils::JasonError::from(tracerr::new!( - $crate::utils::HandlerDetachedError - )) - .into(), - ) - .map($closure) +macro_rules! upgrade_or_detached { + ($v:expr) => {{ + $v.upgrade() + .ok_or_else(|| new_js_error!(HandlerDetachedError)) + }}; + ($v:expr, $err:ty) => {{ + $v.upgrade() + .ok_or_else(|| new_js_error!(HandlerDetachedError => $err)) }}; } +/// Adds [`tracerr`] information to the provided error, wraps it into +/// [`JasonError`] and converts it into the expected error type. +/// +/// This macro has two syntaxes: +/// - `new_js_error!(DetachedStateError)` - converts provided error wrapped into +/// [`JasonError`] with [`Into::into`] automatically; +/// - `new_js_error!(DetachedStateError => JsError)` - annotates explicitly +/// which type conversion is required. +macro_rules! new_js_error { + ($e:expr) => { + $crate::utils::JasonError::from(tracerr::new!($e)).into() + }; + ($e:expr => $o:ty) => { + <$o>::from($crate::utils::JasonError::from(tracerr::new!($e))) + }; +} + /// Returns property of JS object by name if its defined. /// Converts the value with a given predicate. pub fn get_property_by_name( @@ -72,3 +137,17 @@ where .ok() .map_or_else(|| None, into) } + +/// [`Future`] which resolves after the provided [`JsDuration`]. +pub async fn delay_for(delay_ms: JsDuration) { + JsFuture::from(Promise::new(&mut |yes, _| { + window() + .set_timeout_with_callback_and_timeout_and_arguments_0( + &yes, + delay_ms.into_js_duration(), + ) + .unwrap(); + })) + .await + .unwrap(); +} diff --git a/jason/tests/api/room.rs b/jason/tests/api/room.rs index 1354fe179..01007624f 100644 --- a/jason/tests/api/room.rs +++ b/jason/tests/api/room.rs @@ -7,7 +7,10 @@ use medea_client_api_proto::{Event, IceServer, PeerId}; use medea_jason::{ api::Room, media::{AudioTrackConstraints, MediaManager, MediaStreamConstraints}, - peer::{MockPeerRepository, PeerConnection, PeerEvent}, + peer::{ + EnabledAudio, EnabledVideo, MockPeerRepository, PeerConnection, + PeerEvent, + }, rpc::MockRpcClient, utils::JasonError, }; @@ -31,8 +34,8 @@ fn get_test_room_and_exist_peer( tx, vec![], Rc::new(MediaManager::default()), - true, - true, + true.into(), + true.into(), ) .unwrap(), ); @@ -84,8 +87,8 @@ async fn mute_unmute_video() { fn get_test_room_and_new_peer( event_rx: mpsc::UnboundedReceiver, - with_enabled_audio: bool, - with_enabled_video: bool, + with_enabled_audio: EnabledAudio, + with_enabled_video: EnabledVideo, ) -> (Room, Rc) { let mut rpc = MockRpcClient::new(); let mut repo = Box::new(MockPeerRepository::new()); @@ -111,8 +114,8 @@ fn get_test_room_and_new_peer( move |id: &PeerId, _ice_servers: &Vec, _peer_events_sender: &mpsc::UnboundedSender, - enabled_audio: &bool, - enabled_video: &bool| { + enabled_audio: &EnabledAudio, + enabled_video: &EnabledVideo| { *id == PeerId(1) && *enabled_audio == with_enabled_audio && *enabled_video == with_enabled_video @@ -130,7 +133,8 @@ fn get_test_room_and_new_peer( #[wasm_bindgen_test] async fn mute_audio_room_before_init_peer() { let (event_tx, event_rx) = mpsc::unbounded(); - let (room, peer) = get_test_room_and_new_peer(event_rx, false, true); + let (room, peer) = + get_test_room_and_new_peer(event_rx, false.into(), true.into()); let (audio_track, video_track) = get_test_tracks(); room.new_handle().mute_audio().unwrap(); @@ -152,7 +156,8 @@ async fn mute_audio_room_before_init_peer() { #[wasm_bindgen_test] async fn mute_video_room_before_init_peer() { let (event_tx, event_rx) = mpsc::unbounded(); - let (room, peer) = get_test_room_and_new_peer(event_rx, true, false); + let (room, peer) = + get_test_room_and_new_peer(event_rx, true.into(), false.into()); let (audio_track, video_track) = get_test_tracks(); room.new_handle().mute_video().unwrap(); @@ -184,7 +189,8 @@ async fn mute_video_room_before_init_peer() { #[wasm_bindgen_test] async fn error_inject_invalid_local_stream_into_new_peer() { let (event_tx, event_rx) = mpsc::unbounded(); - let (room, _peer) = get_test_room_and_new_peer(event_rx, true, true); + let (room, _peer) = + get_test_room_and_new_peer(event_rx, true.into(), true.into()); let room_handle = room.new_handle(); let (cb, test_result) = js_callback!(|err: JasonError| { @@ -259,7 +265,8 @@ async fn error_inject_invalid_local_stream_into_room_on_exists_peer() { #[wasm_bindgen_test] async fn error_get_local_stream_on_new_peer() { let (event_tx, event_rx) = mpsc::unbounded(); - let (room, _peer) = get_test_room_and_new_peer(event_rx, true, true); + let (room, _peer) = + get_test_room_and_new_peer(event_rx, true.into(), true.into()); let room_handle = room.new_handle(); @@ -300,7 +307,7 @@ async fn error_get_local_stream_on_new_peer() { // Assertions: // 1. Room::join returns error. #[wasm_bindgen_test] -async fn error_join_room_without_failed_stream_callback() { +async fn error_join_room_without_on_failed_stream_callback() { let (_, event_rx) = mpsc::unbounded(); let mut rpc = MockRpcClient::new(); rpc.expect_subscribe() @@ -311,13 +318,53 @@ async fn error_join_room_without_failed_stream_callback() { let room = Room::new(Rc::new(rpc), repo); let room_handle = room.new_handle(); + room_handle + .on_connection_loss(js_sys::Function::new_no_args("")) + .unwrap(); + + match room_handle.inner_join(String::from("token")).await { + Ok(_) => unreachable!(), + Err(e) => { + assert_eq!(e.name(), "CallbackNotSet"); + assert_eq!( + e.message(), + "`Room.on_failed_local_stream()` callback isn't set.", + ); + assert!(!e.trace().is_empty()); + } + } +} + +// Tests Room::join without set `on_connection_loss` callback. +// Setup: +// 1. Create Room. +// 2. DO NOT set `on_connection_loss` callback. +// 3. Try join to Room. +// Assertions: +// 1. Room::join returns error. +#[wasm_bindgen_test] +async fn error_join_room_without_on_connection_loss_callback() { + let (_, event_rx) = mpsc::unbounded(); + let mut rpc = MockRpcClient::new(); + rpc.expect_subscribe() + .return_once(move || Box::pin(event_rx)); + rpc.expect_unsub().return_const(()); + rpc.expect_set_close_reason().return_const(()); + let repo = Box::new(MockPeerRepository::new()); + let room = Room::new(Rc::new(rpc), repo); + + let room_handle = room.new_handle(); + room_handle + .on_failed_local_stream(js_sys::Function::new_no_args("")) + .unwrap(); + match room_handle.inner_join(String::from("token")).await { Ok(_) => unreachable!(), Err(e) => { assert_eq!(e.name(), "CallbackNotSet"); assert_eq!( e.message(), - "`on_failed_local_stream` callback is not set", + "`Room.on_connection_loss()` callback isn't set.", ); assert!(!e.trace().is_empty()); } @@ -421,7 +468,7 @@ mod on_close_callback { }); room_handle.on_close(cb.into()).unwrap(); - std::mem::drop(room); + drop(room); wait_and_check_test_result(test_result, || {}).await; } @@ -497,7 +544,7 @@ mod rpc_close_reason_on_room_drop { async fn set_default_close_reason_on_drop() { let (room, test_rx) = get_client().await; - std::mem::drop(room); + drop(room); let close_reason = test_rx.await.unwrap(); assert_eq!( diff --git a/jason/tests/peer/media.rs b/jason/tests/peer/media.rs index 2f064d25f..db699fe0c 100644 --- a/jason/tests/peer/media.rs +++ b/jason/tests/peer/media.rs @@ -7,7 +7,7 @@ use medea_jason::{ media::MediaManager, peer::{ MediaConnections, RtcPeerConnection, SimpleStreamRequest, - TransceiverDirection, TransceiverKind, + TransceiverDirection, }, }; use wasm_bindgen_test::*; @@ -22,8 +22,8 @@ async fn get_test_media_connections( ) -> (MediaConnections, TrackId, TrackId) { let media_connections = MediaConnections::new( Rc::new(RtcPeerConnection::new(vec![]).unwrap()), - enabled_audio, - enabled_video, + enabled_audio.into(), + enabled_video.into(), ); let (audio_track, video_track) = get_test_tracks(); let audio_track_id = audio_track.id; @@ -48,8 +48,8 @@ async fn get_test_media_connections( fn get_stream_request() { let media_connections = MediaConnections::new( Rc::new(RtcPeerConnection::new(vec![]).unwrap()), - true, - true, + true.into(), + true.into(), ); let (audio_track, video_track) = get_test_tracks(); media_connections @@ -60,8 +60,8 @@ fn get_stream_request() { let media_connections = MediaConnections::new( Rc::new(RtcPeerConnection::new(vec![]).unwrap()), - true, - true, + true.into(), + true.into(), ); media_connections.update_tracks(vec![]).unwrap(); let request = media_connections.get_stream_request(); @@ -92,19 +92,19 @@ async fn disable_and_enable_all_tracks_in_media_manager() { assert!(audio_track.is_enabled()); assert!(video_track.is_enabled()); - media_connections.toggle_send_media(TransceiverKind::Audio, false); + media_connections.toggle_send_audio(false.into()); assert!(!audio_track.is_enabled()); assert!(video_track.is_enabled()); - media_connections.toggle_send_media(TransceiverKind::Video, false); + media_connections.toggle_send_video(false.into()); assert!(!audio_track.is_enabled()); assert!(!video_track.is_enabled()); - media_connections.toggle_send_media(TransceiverKind::Audio, true); + media_connections.toggle_send_audio(true.into()); assert!(audio_track.is_enabled()); assert!(!video_track.is_enabled()); - media_connections.toggle_send_media(TransceiverKind::Video, true); + media_connections.toggle_send_video(true.into()); assert!(audio_track.is_enabled()); assert!(video_track.is_enabled()); } diff --git a/jason/tests/peer/mod.rs b/jason/tests/peer/mod.rs index 8c28163df..68b3c566c 100644 --- a/jason/tests/peer/mod.rs +++ b/jason/tests/peer/mod.rs @@ -21,8 +21,15 @@ async fn mute_unmute_audio() { let (tx, _rx) = mpsc::unbounded(); let manager = Rc::new(MediaManager::default()); let (audio_track, video_track) = get_test_tracks(); - let peer = PeerConnection::new(PeerId(1), tx, vec![], manager, true, true) - .unwrap(); + let peer = PeerConnection::new( + PeerId(1), + tx, + vec![], + manager, + true.into(), + true.into(), + ) + .unwrap(); peer.get_offer(vec![audio_track, video_track], None) .await @@ -31,11 +38,11 @@ async fn mute_unmute_audio() { assert!(peer.is_send_audio_enabled()); assert!(peer.is_send_video_enabled()); - peer.toggle_send_audio(false); + peer.toggle_send_audio(false.into()); assert!(!peer.is_send_audio_enabled()); assert!(peer.is_send_video_enabled()); - peer.toggle_send_audio(true); + peer.toggle_send_audio(true.into()); assert!(peer.is_send_audio_enabled()); assert!(peer.is_send_video_enabled()); } @@ -45,8 +52,15 @@ async fn mute_unmute_video() { let (tx, _rx) = mpsc::unbounded(); let manager = Rc::new(MediaManager::default()); let (audio_track, video_track) = get_test_tracks(); - let peer = PeerConnection::new(PeerId(1), tx, vec![], manager, true, true) - .unwrap(); + let peer = PeerConnection::new( + PeerId(1), + tx, + vec![], + manager, + true.into(), + true.into(), + ) + .unwrap(); peer.get_offer(vec![audio_track, video_track], None) .await .unwrap(); @@ -54,11 +68,11 @@ async fn mute_unmute_video() { assert!(peer.is_send_audio_enabled()); assert!(peer.is_send_video_enabled()); - peer.toggle_send_video(false); + peer.toggle_send_video(false.into()); assert!(peer.is_send_audio_enabled()); assert!(!peer.is_send_video_enabled()); - peer.toggle_send_video(true); + peer.toggle_send_video(true.into()); assert!(peer.is_send_audio_enabled()); assert!(peer.is_send_video_enabled()); } @@ -68,8 +82,15 @@ async fn new_with_mute_audio() { let (tx, _rx) = mpsc::unbounded(); let manager = Rc::new(MediaManager::default()); let (audio_track, video_track) = get_test_tracks(); - let peer = PeerConnection::new(PeerId(1), tx, vec![], manager, false, true) - .unwrap(); + let peer = PeerConnection::new( + PeerId(1), + tx, + vec![], + manager, + false.into(), + true.into(), + ) + .unwrap(); peer.get_offer(vec![audio_track, video_track], None) .await @@ -84,8 +105,15 @@ async fn new_with_mute_video() { let (tx, _rx) = mpsc::unbounded(); let manager = Rc::new(MediaManager::default()); let (audio_track, video_track) = get_test_tracks(); - let peer = PeerConnection::new(PeerId(1), tx, vec![], manager, true, false) - .unwrap(); + let peer = PeerConnection::new( + PeerId(1), + tx, + vec![], + manager, + true.into(), + false.into(), + ) + .unwrap(); peer.get_offer(vec![audio_track, video_track], None) .await .unwrap(); @@ -105,13 +133,20 @@ async fn add_candidates_to_answerer_before_offer() { tx1, vec![], Rc::clone(&manager), - true, - true, + true.into(), + true.into(), ) .unwrap(); - let pc2 = PeerConnection::new(PeerId(2), tx2, vec![], manager, true, true) - .unwrap(); + let pc2 = PeerConnection::new( + PeerId(2), + tx2, + vec![], + manager, + true.into(), + true.into(), + ) + .unwrap(); let (audio_track, video_track) = get_test_tracks(); let offer = pc1 .get_offer(vec![audio_track, video_track], None) @@ -140,14 +175,21 @@ async fn add_candidates_to_offerer_before_answer() { tx1, vec![], Rc::clone(&manager), - true, - true, + true.into(), + true.into(), ) .unwrap(), ); let pc2 = Rc::new( - PeerConnection::new(PeerId(2), tx2, vec![], manager, true, true) - .unwrap(), + PeerConnection::new( + PeerId(2), + tx2, + vec![], + manager, + true.into(), + true.into(), + ) + .unwrap(), ); let (audio_track, video_track) = get_test_tracks(); @@ -177,13 +219,19 @@ async fn normal_exchange_of_candidates() { tx1, vec![], Rc::clone(&manager), - true, - true, + true.into(), + true.into(), + ) + .unwrap(); + let peer2 = PeerConnection::new( + PeerId(2), + tx2, + vec![], + manager, + true.into(), + true.into(), ) .unwrap(); - let peer2 = - PeerConnection::new(PeerId(2), tx2, vec![], manager, true, true) - .unwrap(); let (audio_track, video_track) = get_test_tracks(); let offer = peer1 @@ -239,7 +287,8 @@ async fn send_event_on_new_local_stream() { let (audio_track, video_track) = get_test_tracks(); let id = PeerId(1); let peer = - PeerConnection::new(id, tx, vec![], manager, true, false).unwrap(); + PeerConnection::new(id, tx, vec![], manager, true.into(), false.into()) + .unwrap(); peer.get_offer(vec![audio_track, video_track], None) .await .unwrap(); @@ -269,13 +318,19 @@ async fn ice_connection_state_changed_is_emitted() { tx1, vec![], Rc::clone(&manager), - true, - true, + true.into(), + true.into(), + ) + .unwrap(); + let peer2 = PeerConnection::new( + PeerId(2), + tx2, + vec![], + manager, + true.into(), + true.into(), ) .unwrap(); - let peer2 = - PeerConnection::new(PeerId(2), tx2, vec![], manager, true, true) - .unwrap(); let (audio_track, video_track) = get_test_tracks(); let offer = peer1 diff --git a/jason/tests/rpc/backoff_delayer.rs b/jason/tests/rpc/backoff_delayer.rs new file mode 100644 index 000000000..90de2b1b4 --- /dev/null +++ b/jason/tests/rpc/backoff_delayer.rs @@ -0,0 +1,71 @@ +//! Tests for [`medea_jason::rpc::BackoffDelayer`]. + +use std::time::Duration; + +use medea_jason::rpc::BackoffDelayer; +use wasm_bindgen_test::*; + +use crate::await_with_timeout; + +wasm_bindgen_test_configure!(run_in_browser); + +/// Tests that `delay` multiplies by provided `multiplier`. +/// +/// Also this test checks that [`JsDuration`] correctly multiplies by [`f32`]. +#[wasm_bindgen_test] +async fn multiplier_works() { + let mut delayer = BackoffDelayer::new( + Duration::from_millis(10).into(), + 1.5, + Duration::from_millis(100).into(), + ); + await_with_timeout(Box::pin(delayer.delay()), 13) + .await + .unwrap(); + await_with_timeout(Box::pin(delayer.delay()), 18) + .await + .unwrap(); + await_with_timeout(Box::pin(delayer.delay()), 25) + .await + .unwrap(); +} + +/// Tests that `delay` wouldn't be greater than provided `max_delay`. +#[wasm_bindgen_test] +async fn max_delay_works() { + let mut delayer = BackoffDelayer::new( + Duration::from_millis(50).into(), + 2.0, + Duration::from_millis(100).into(), + ); + await_with_timeout(Box::pin(delayer.delay()), 53) + .await + .unwrap(); + await_with_timeout(Box::pin(delayer.delay()), 103) + .await + .unwrap(); + await_with_timeout(Box::pin(delayer.delay()), 103) + .await + .unwrap(); +} + +/// Tests that multiplication of [`JsDuration`] by negative `multiplier` +/// will be calculated as `0` (this is default JS behavior which is recreated in +/// [`JsDuration`] implementation). +#[wasm_bindgen_test] +async fn negative_multiplier() { + let mut delayer = BackoffDelayer::new( + Duration::from_millis(10).into(), + -2.0, + Duration::from_millis(100).into(), + ); + await_with_timeout(Box::pin(delayer.delay()), 13) + .await + .unwrap(); + await_with_timeout(Box::pin(delayer.delay()), 3) + .await + .unwrap(); + await_with_timeout(Box::pin(delayer.delay()), 3) + .await + .unwrap(); +} diff --git a/jason/tests/rpc/heartbeat.rs b/jason/tests/rpc/heartbeat.rs new file mode 100644 index 000000000..093c8ef0a --- /dev/null +++ b/jason/tests/rpc/heartbeat.rs @@ -0,0 +1,152 @@ +//! Tests for [`medea_jason::rpc::Heartbeat`]. + +use std::{rc::Rc, time::Duration}; + +use futures::{ + channel::{mpsc, oneshot}, + stream, FutureExt, StreamExt, +}; +use medea_client_api_proto::{ClientMsg, ServerMsg}; +use medea_jason::rpc::{ + Heartbeat, IdleTimeout, MockRpcTransport, PingInterval, RpcTransport, +}; +use wasm_bindgen_test::*; + +use crate::{await_with_timeout, resolve_after}; + +wasm_bindgen_test_configure!(run_in_browser); + +/// Tests that [`ClientMsg::Pong`] will be sent after received +/// [`ServerMsg::Ping`]. +/// +/// # Algorithm +/// +/// 1. Mock [`RpcClient::on_message`] and send to this [`Stream`] +/// [`ServerMsg::Ping`]. +/// +/// 2. Mock [`RpcClient::send`] and check that [`ClientMsg::Pong`] was sent. +#[wasm_bindgen_test] +async fn sends_pong_on_received_ping() { + let mut transport = MockRpcTransport::new(); + let (on_message_tx, on_message_rx) = mpsc::unbounded(); + transport + .expect_on_message() + .return_once(|| Box::pin(on_message_rx)); + let (test_tx, test_rx) = oneshot::channel(); + transport.expect_send().return_once(move |msg| { + test_tx.send(msg.clone()).unwrap(); + Ok(()) + }); + + let _hb = Heartbeat::start( + Rc::new(transport), + PingInterval(Duration::from_secs(10).into()), + IdleTimeout(Duration::from_secs(10).into()), + ); + + on_message_tx.unbounded_send(ServerMsg::Ping(2)).unwrap(); + await_with_timeout( + Box::pin(async move { + match test_rx.await.unwrap() { + ClientMsg::Pong(_) => (), + ClientMsg::Command(cmd) => { + panic!("Received not pong message! Command: {:?}", cmd) + } + } + }), + 100, + ) + .await + .unwrap(); +} + +/// Tests that idle timeout works. +/// +/// # Algorithm +/// +/// 1. Mock [`RpcTransport::on_message`] to return infinite [`Stream`]. +/// +/// 2. Wait for [`Heartbeat::on_idle`] resolving. +#[wasm_bindgen_test] +async fn on_idle_works() { + let mut transport = MockRpcTransport::new(); + transport + .expect_on_message() + .return_once(|| stream::pending().boxed()); + transport.expect_send().return_once(|_| Ok(())); + + let hb = Heartbeat::start( + Rc::new(transport), + PingInterval(Duration::from_millis(50).into()), + IdleTimeout(Duration::from_millis(100).into()), + ); + + await_with_timeout(Box::pin(hb.on_idle().next()), 110) + .await + .unwrap() + .unwrap(); +} + +/// Tests that [`Heartbeat`] will try send [`ClientMsg::Pong`] if +/// no [`ServerMsg::Ping`]s received within `ping_interval * 2`. +/// +/// # Algorithm +/// +/// 1. Create [`Heartbeat`] with 10 milliseconds `ping_interval`. +/// +/// 2. Mock [`RpcTransport::on_message`] to return infinite [`Stream`]. +/// +/// 3. Mock [`RpcTransport::send`] and wait for [`ClientMsg::Pong`] (with 25 +/// milliseconds timeout). +#[wasm_bindgen_test] +async fn pre_sends_pong() { + let mut transport = MockRpcTransport::new(); + transport + .expect_on_message() + .return_once(|| stream::pending().boxed()); + let (on_message_tx, mut on_message_rx) = mpsc::unbounded(); + transport.expect_send().return_once(move |msg| { + on_message_tx.unbounded_send(msg.clone()).unwrap(); + Ok(()) + }); + + let _hb = Heartbeat::start( + Rc::new(transport), + PingInterval(Duration::from_millis(10).into()), + IdleTimeout(Duration::from_millis(100).into()), + ); + + match await_with_timeout(on_message_rx.next().boxed(), 25) + .await + .unwrap() + .unwrap() + { + ClientMsg::Pong(n) => { + assert_eq!(n, 1); + } + ClientMsg::Command(cmd) => { + panic!("Received not pong message! Command: {:?}", cmd); + } + } +} + +/// Tests that [`RpcTransport`] will be dropped when [`Heartbeat`] was +/// dropped. +#[wasm_bindgen_test] +async fn transport_is_dropped_when_hearbeater_is_dropped() { + let mut transport = MockRpcTransport::new(); + transport + .expect_on_message() + .returning(|| stream::pending().boxed()); + let transport: Rc = Rc::new(transport); + + let hb = Heartbeat::start( + Rc::clone(&transport), + PingInterval(Duration::from_secs(3).into()), + IdleTimeout(Duration::from_secs(10).into()), + ); + assert!(Rc::strong_count(&transport) > 1); + drop(hb); + resolve_after(100).await.unwrap(); + assert_eq!(Rc::strong_count(&transport), 1); +} diff --git a/jason/tests/rpc/mod.rs b/jason/tests/rpc/mod.rs index 48c8e9b56..b297aa98d 100644 --- a/jason/tests/rpc/mod.rs +++ b/jason/tests/rpc/mod.rs @@ -1,34 +1,48 @@ //! Tests for [`medea_jason::rpc::RpcClient`]. +mod backoff_delayer; +mod heartbeat; mod websocket; -use std::{ - collections::HashMap, - rc::Rc, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, -}; +use std::{collections::HashMap, rc::Rc}; use futures::{ channel::{mpsc, oneshot}, - future::{self, Either}, - stream, FutureExt as _, StreamExt as _, + future::{self}, + stream, + stream::LocalBoxStream, + StreamExt as _, }; use medea_client_api_proto::{ - ClientMsg, CloseReason, Command, Event, PeerId, ServerMsg, + ClientMsg, CloseReason, Command, Event, PeerId, RpcSettings, ServerMsg, }; use medea_jason::rpc::{ - ClientDisconnect, CloseMsg, MockRpcTransport, RpcClient, WebSocketRpcClient, + ClientDisconnect, CloseMsg, ClosedStateReason, MockRpcTransport, RpcClient, + RpcTransport, State, WebSocketRpcClient, }; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::*; -use crate::resolve_after; +use crate::{await_with_timeout, resolve_after}; wasm_bindgen_test_configure!(run_in_browser); +/// Creates [`WebSocketRpcClient`] with the provided [`MockRpcTransport`]. +fn new_client(transport: Rc) -> WebSocketRpcClient { + WebSocketRpcClient::new(Box::new(move |_| { + Box::pin(future::ok(transport.clone() as Rc)) + })) +} + +/// Returns result for [`RpcTransport::on_message`] with [`LocalBoxStream`], +/// which will only send [`ServerMsg::RpcSettings`] with the provided +/// [`RpcSettings`]. +fn on_message_mock( + settings: RpcSettings, +) -> LocalBoxStream<'static, ServerMsg> { + stream::once(async move { ServerMsg::RpcSettings(settings) }).boxed() +} + /// Tests [`WebSocketRpcClient::subscribe`] function. /// /// # Algorithm @@ -42,67 +56,34 @@ wasm_bindgen_test_configure!(run_in_browser); /// 4. Check that subscriber from step 2 receives this [`Event`]. #[wasm_bindgen_test] async fn message_received_from_transport_is_transmitted_to_sub() { - let srv_event = Event::PeersRemoved { peer_ids: vec![] }; - let srv_event_cloned = srv_event.clone(); - - let mut transport = MockRpcTransport::new(); - transport.expect_on_message().return_once(move || { - Ok( - stream::once(async move { Ok(ServerMsg::Event(srv_event_cloned)) }) - .boxed(), - ) - }); - transport.expect_send().return_once(|_| Ok(())); - transport - .expect_on_close() - .return_once(|| Ok(future::pending().boxed())); - transport.expect_set_close_reason().return_const(()); + const SRV_EVENT: Event = Event::PeersRemoved { + peer_ids: Vec::new(), + }; - let ws = WebSocketRpcClient::new(10); + let ws = WebSocketRpcClient::new(Box::new(|_| { + let mut transport = MockRpcTransport::new(); + transport + .expect_on_state_change() + .return_once(|| stream::once(async { State::Open }).boxed()); + transport.expect_on_message().returning(|| { + let (tx, rx) = mpsc::unbounded(); + tx.unbounded_send(ServerMsg::RpcSettings(RpcSettings { + idle_timeout_ms: 10_000, + ping_interval_ms: 10_000, + })) + .unwrap(); + tx.unbounded_send(ServerMsg::Event(SRV_EVENT)).unwrap(); + rx.boxed() + }); + transport.expect_send().returning(|_| Ok(())); + transport.expect_set_close_reason().return_const(()); + + Box::pin(future::ok(Rc::new(transport) as Rc)) + })); let mut stream = ws.subscribe(); - ws.connect(Rc::new(transport)).await.unwrap(); - assert_eq!(stream.next().await.unwrap(), srv_event); -} - -/// Tests that [`WebSocketRpcClient`] sends [`Event::Ping`] to a server. -/// -/// # Algorithm -/// -/// 1. Connect [`WebSocketRpcClient`] with [`MockRpcTransport`]. -/// -/// 2. Subscribe to [`ClientMsg`]s which [`WebSocketRpcClient`] will send. -/// -/// 3. Wait `600ms` for [`ClientMsg::Ping`]. -#[wasm_bindgen_test] -async fn heartbeat() { - let mut transport = MockRpcTransport::new(); - transport - .expect_on_message() - .return_once(move || Ok(stream::once(future::pending()).boxed())); - transport - .expect_on_close() - .return_once(move || Ok(future::pending().boxed())); - transport.expect_set_close_reason().return_const(()); - - let counter = Arc::new(AtomicU64::new(1)); - let counter_clone = counter.clone(); - transport - .expect_send() - .times(3) - .withf(move |msg: &ClientMsg| { - if let ClientMsg::Ping(id) = msg { - assert_eq!(*id, counter.fetch_add(1, Ordering::Relaxed)); - }; - true - }) - .returning(|_| Ok(())); - - let ws = WebSocketRpcClient::new(50); - ws.connect(Rc::new(transport)).await.unwrap(); - - resolve_after(120).await.unwrap(); - assert!(counter_clone.load(Ordering::Relaxed) > 2); + ws.connect(String::new()).await.unwrap(); + assert_eq!(stream.next().await.unwrap(), SRV_EVENT); } /// Tests [`WebSocketRpcClient::unsub`] function. @@ -117,7 +98,7 @@ async fn heartbeat() { /// `Stream`. #[wasm_bindgen_test] async fn unsub_drops_subs() { - let ws = WebSocketRpcClient::new(500); + let ws = new_client(Rc::new(MockRpcTransport::new())); let (test_tx, test_rx) = oneshot::channel(); let mut subscriber_stream = ws.subscribe(); spawn_local(async move { @@ -133,14 +114,10 @@ async fn unsub_drops_subs() { }); ws.unsub(); - match future::select(Box::pin(test_rx), Box::pin(resolve_after(1000))).await - { - Either::Left(_) => (), - Either::Right(_) => panic!( - "'unsub_drops_sub' lasts more that 1s. Most likely 'unsub' is \ - broken." - ), - } + await_with_timeout(Box::pin(test_rx), 1000) + .await + .unwrap() + .unwrap(); } /// Tests that [`RpcTransport`] will be dropped when [`WebSocketRpcClient`] was @@ -157,19 +134,24 @@ async fn unsub_drops_subs() { #[wasm_bindgen_test] async fn transport_is_dropped_when_client_is_dropped() { let mut transport = MockRpcTransport::new(); - transport - .expect_on_message() - .return_once(move || Ok(stream::once(future::pending()).boxed())); - transport - .expect_on_close() - .return_once(move || Ok(future::pending().boxed())); - transport.expect_send().return_once(|_| Ok(())); + transport.expect_send().returning(|_| Ok(())); transport.expect_set_close_reason().return_const(()); + transport + .expect_on_state_change() + .return_once(|| stream::once(async { State::Open }).boxed()); + transport.expect_on_message().returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 10_000, + ping_interval_ms: 500, + }) + }); let rpc_transport = Rc::new(transport); - let ws = WebSocketRpcClient::new(500); - ws.connect(rpc_transport.clone()).await.unwrap(); - std::mem::drop(ws); + let ws = new_client(rpc_transport.clone()); + ws.connect(String::new()).await.unwrap(); + ws.set_close_reason(ClientDisconnect::RoomClosed); + drop(ws); + resolve_after(100).await.unwrap(); assert_eq!(Rc::strong_count(&rpc_transport), 1); } @@ -192,19 +174,22 @@ async fn send_goes_to_transport() { // (but we can't do this). let (on_send_tx, mut on_send_rx) = mpsc::unbounded(); transport - .expect_on_message() - .return_once(move || Ok(stream::once(future::pending()).boxed())); - transport - .expect_on_close() - .return_once(move || Ok(future::pending().boxed())); + .expect_on_state_change() + .return_once(|| stream::once(async { State::Open }).boxed()); + transport.expect_on_message().returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 10_000, + ping_interval_ms: 500, + }) + }); transport.expect_send().returning(move |e| { on_send_tx.unbounded_send(e.clone()).unwrap(); Ok(()) }); transport.expect_set_close_reason().return_const(()); - let ws = WebSocketRpcClient::new(500); - ws.connect(Rc::new(transport)).await.unwrap(); + let ws = new_client(Rc::new(transport)); + ws.connect(String::new()).await.unwrap(); let (test_tx, test_rx) = oneshot::channel(); let test_peer_id = PeerId(9999); let test_sdp_offer = "Hello world!".to_string(); @@ -237,18 +222,14 @@ async fn send_goes_to_transport() { ws.send_command(test_cmd); - match future::select(Box::pin(test_rx), Box::pin(resolve_after(1000))).await - { - Either::Left(_) => (), - Either::Right(_) => { - panic!("Command doesn't reach 'RpcTransport' within a 1s.") - } - } + await_with_timeout(Box::pin(test_rx), 1000) + .await + .unwrap() + .unwrap(); } +/// Tests for [`WebSocketRpcClient::on_close`]. mod on_close { - //! Tests for [`WebSocketRpcClient::on_close`]. - use super::*; /// Returns [`WebSocketRpcClient`] which will be resolved @@ -256,16 +237,26 @@ mod on_close { /// [`CloseMsg`]. async fn get_client(close_msg: CloseMsg) -> WebSocketRpcClient { let mut transport = MockRpcTransport::new(); - transport - .expect_on_message() - .return_once(move || Ok(stream::once(future::pending()).boxed())); - transport.expect_send().return_once(|_| Ok(())); - transport - .expect_on_close() - .return_once(move || Ok(Box::pin(async { Ok(close_msg) }))); - - let ws = WebSocketRpcClient::new(500); - ws.connect(Rc::new(transport)).await.unwrap(); + transport.expect_on_state_change().return_once(|| { + let (tx, rx) = mpsc::unbounded(); + tx.unbounded_send(State::Open).unwrap(); + tx.unbounded_send(State::Closed( + ClosedStateReason::ConnectionLost(close_msg), + )) + .unwrap(); + Box::pin(rx) + }); + transport.expect_on_message().returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 10_000, + ping_interval_ms: 500, + }) + }); + transport.expect_send().returning(|_| Ok(())); + transport.expect_set_close_reason().return_const(()); + + let ws = new_client(Rc::new(transport)); + ws.connect(String::new()).await.unwrap(); ws } @@ -289,7 +280,7 @@ mod on_close { get_client(CloseMsg::Normal(1000, CloseReason::Finished)).await; assert_eq!( - ws.on_close().await.unwrap(), + ws.on_normal_close().await.unwrap(), medea_jason::rpc::CloseReason::ByServer(CloseReason::Finished) ); } @@ -310,20 +301,9 @@ mod on_close { let ws = get_client(CloseMsg::Normal(1000, CloseReason::Reconnected)).await; - match future::select( - Box::pin(ws.on_close()), - Box::pin(resolve_after(500)), - ) - .await - { - Either::Left((msg, _)) => { - unreachable!( - "Some CloseMsg was unexpectedly thrown: {:?}.", - msg - ); - } - Either::Right(_) => (), - } + await_with_timeout(Box::pin(ws.on_normal_close()), 500) + .await + .unwrap_err(); } /// Tests that [`WebSocketRpcClient::on_close`]'s [`Future`] don't resolves @@ -341,27 +321,15 @@ mod on_close { async fn dont_resolve_on_abnormal_close() { let ws = get_client(CloseMsg::Abnormal(1500)).await; - match future::select( - Box::pin(ws.on_close()), - Box::pin(resolve_after(500)), - ) - .await - { - Either::Left((msg, _)) => { - unreachable!( - "Some CloseMsg was unexpectedly thrown: {:?}.", - msg - ); - } - Either::Right(_) => (), - } + await_with_timeout(Box::pin(ws.on_normal_close()), 500) + .await + .unwrap_err(); } } +/// Tests which checks that when [`WebSocketRpcClient`] is dropped the right +/// close reason is provided to [`RpcTransport`]. mod transport_close_reason_on_drop { - //! Tests which checks that when [`WebSocketRpcClient`] is dropped the right - //! close reason is provided to [`RpcTransport`]. - use super::*; /// Returns [`WebSocketRpcClient`] and [`oneshot::Receiver`] which will be @@ -371,12 +339,15 @@ mod transport_close_reason_on_drop { ) -> (WebSocketRpcClient, oneshot::Receiver) { let mut transport = MockRpcTransport::new(); transport - .expect_on_message() - .return_once(move || Ok(stream::once(future::pending()).boxed())); + .expect_on_state_change() + .return_once(|| stream::once(async { State::Open }).boxed()); + transport.expect_on_message().returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 10000, + ping_interval_ms: 500, + }) + }); transport.expect_send().return_once(|_| Ok(())); - transport - .expect_on_close() - .return_once(|| Ok(future::pending().boxed())); let (test_tx, test_rx) = oneshot::channel(); transport .expect_set_close_reason() @@ -384,8 +355,8 @@ mod transport_close_reason_on_drop { test_tx.send(reason).unwrap(); }); - let ws = WebSocketRpcClient::new(500); - ws.connect(Rc::new(transport)).await.unwrap(); + let ws = new_client(Rc::new(transport)); + ws.connect(String::new()).await.unwrap(); (ws, test_rx) } @@ -406,7 +377,7 @@ mod transport_close_reason_on_drop { async fn sets_default_close_reason_on_drop() { let (ws, test_rx) = get_client().await; - std::mem::drop(ws); + drop(ws); let close_reason = test_rx.await.unwrap(); assert_eq!( @@ -436,7 +407,7 @@ mod transport_close_reason_on_drop { let (ws, test_rx) = get_client().await; ws.set_close_reason(ClientDisconnect::RoomClosed); - std::mem::drop(ws); + drop(ws); let close_reason = test_rx.await.unwrap(); assert_eq!( @@ -448,3 +419,144 @@ mod transport_close_reason_on_drop { ); } } + +/// Tests for [`RpcClient::connect`]. +mod connect { + use medea_client_api_proto::RpcSettings; + use medea_jason::rpc::State; + + use crate::resolve_after; + + use super::*; + + /// Tests that new connection will be created if [`RpcClient`] is in + /// [`State::Closed`]. + /// + /// # Algorithm + /// + /// 1. Create new [`WebSocketRpcClient`]. + /// + /// 2. Call [`WebSocketRpcClient::connect`] and check that it successfully + /// resolved. + #[wasm_bindgen_test] + async fn closed() { + let (test_tx, mut test_rx) = mpsc::unbounded(); + let ws = WebSocketRpcClient::new(Box::new(move |_| { + test_tx.unbounded_send(()).unwrap(); + let mut transport = MockRpcTransport::new(); + transport.expect_on_message().times(3).returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 3_000, + ping_interval_ms: 3_000, + }) + }); + transport.expect_send().return_once(|_| Ok(())); + transport.expect_set_close_reason().return_once(|_| ()); + transport + .expect_on_state_change() + .return_once(|| stream::once(async { State::Open }).boxed()); + let transport = Rc::new(transport); + Box::pin(future::ok(transport as Rc)) + })); + ws.connect(String::new()).await.unwrap(); + + await_with_timeout(Box::pin(test_rx.next()), 500) + .await + .unwrap() + .unwrap(); + } + + /// Tests that new connection try will be not started if + /// [`WebSocketRpcClient`] is already in [`State::Connecting`]. + /// + /// # Algorithm + /// + /// 1. Create new [`WebSocketRpcClient`] with [`RpcTransport`] factory which + /// will be resolved after 500 milliseconds. + /// + /// 2. Call [`WebSocketRpcClient::connect`] in [`spawn_local`]. + /// + /// 3. Simultaneously with it call another [`WebSocketRpcClient::connect`]. + /// + /// 4. Check that only one [`RpcTransport`] was created. + #[wasm_bindgen_test] + async fn connecting() { + let mut connecting_count: i32 = 0; + let ws = WebSocketRpcClient::new(Box::new(move |_| { + Box::pin(async move { + let mut transport = MockRpcTransport::new(); + transport.expect_on_message().times(3).returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 3_000, + ping_interval_ms: 3_000, + }) + }); + transport.expect_send().return_once(|_| Ok(())); + transport.expect_set_close_reason().return_once(|_| ()); + transport.expect_on_state_change().return_once(|| { + stream::once(async { State::Open }).boxed() + }); + let transport = Rc::new(transport); + connecting_count += 1; + if connecting_count > 1 { + unreachable!("New connection try was performed!"); + } else { + resolve_after(500).await.unwrap(); + Ok(Rc::clone(&transport) as Rc) + } + }) + })); + let first_connect_fut = ws.connect(String::new()); + spawn_local(async move { + first_connect_fut.await.unwrap(); + }); + + await_with_timeout(Box::pin(ws.connect(String::new())), 1000) + .await + .unwrap() + .unwrap(); + } + + /// Tests that [`WebSocketRpcClient::connect`] will be instantly resolved + /// if [`State`] is already [`State::Open`]. + /// + /// # Algorithm + /// + /// 1. Normally connect [`WebSocketRpcClient`]. + /// + /// 2. Call [`WebSocketRpcClient::connect`] again. + /// + /// 3. Check that only one [`RpcTransport`] was created. + #[wasm_bindgen_test] + async fn open() { + let mut connection_count = 0; + let ws = WebSocketRpcClient::new(Box::new(move |_| { + Box::pin(async move { + connection_count += 1; + if connection_count > 1 { + unreachable!("Only one connection should be performed!"); + } + let mut transport = MockRpcTransport::new(); + transport.expect_on_message().times(3).returning(|| { + on_message_mock(RpcSettings { + idle_timeout_ms: 3_000, + ping_interval_ms: 3_000, + }) + }); + transport.expect_send().return_once(|_| Ok(())); + transport.expect_set_close_reason().return_once(|_| ()); + transport.expect_on_state_change().return_once(|| { + stream::once(async { State::Open }).boxed() + }); + let transport = Rc::new(transport); + Ok(transport as Rc) + }) + })); + ws.connect(String::new()).await.unwrap(); + + await_with_timeout(Box::pin(ws.connect(String::new())), 50) + .await + .unwrap() + .unwrap(); + } +} diff --git a/jason/tests/rpc/websocket.rs b/jason/tests/rpc/websocket.rs index 1157048b8..d94eb3322 100644 --- a/jason/tests/rpc/websocket.rs +++ b/jason/tests/rpc/websocket.rs @@ -24,7 +24,7 @@ async fn bad_url_err() { async fn could_not_init_socket_err() { use TransportError::*; - match WebSocketRpcTransport::new("ws://0.0.0.0").await { + match WebSocketRpcTransport::new("ws://0.0.0.0:60000").await { Ok(_) => unreachable!(), Err(err) => match err.into_inner() { InitSocket => {} diff --git a/jason/tests/web.rs b/jason/tests/web.rs index 2f4984c23..a06c92318 100644 --- a/jason/tests/web.rs +++ b/jason/tests/web.rs @@ -79,7 +79,10 @@ mod media; mod peer; mod rpc; -use futures::{channel::oneshot, future::Either}; +use futures::{ + channel::oneshot, + future::{Either, LocalBoxFuture}, +}; use js_sys::Promise; use medea_client_api_proto::{ AudioSettings, Direction, MediaType, PeerId, Track, TrackId, VideoSettings, @@ -169,3 +172,17 @@ async fn wait_and_check_test_result( } }; } + +/// Awaits provided [`LocalBoxFuture`] for `timeout` milliseconds. If within +/// provided `timeout` time this [`LocalBoxFuture`] won'tbe resolved, then +/// `Err(String)` will be returned, otherwise a result of the provided +/// [`LocalBoxFuture`] will be returned. +async fn await_with_timeout( + f: LocalBoxFuture<'_, T>, + timeout: i32, +) -> Result { + match futures::future::select(f, Box::pin(resolve_after(timeout))).await { + Either::Left((res, _)) => Ok(res), + Either::Right((_, _)) => Err("Future timed out.".to_string()), + } +} diff --git a/proto/client-api/CHANGELOG.md b/proto/client-api/CHANGELOG.md index 26876f41d..32811fb2a 100644 --- a/proto/client-api/CHANGELOG.md +++ b/proto/client-api/CHANGELOG.md @@ -6,15 +6,24 @@ All user visible changes to this project will be documented in this file. This p -## TBD [0.1.1] · 2019-??-?? -[0.1.1]: /../../tree/medea-client-api-proto-0.1.1/proto/client-api +## TBD [0.2.0] · 2019-??-?? +[0.2.0]: /../../tree/medea-client-api-proto-0.2.0/proto/client-api + +### BC Breaks + +- RPC messages ([#75](/../../pull/75)): + - Server messages: + - `Pong` is now `Ping`. + - Client messages: + - `Ping` is now `Pong`. ### Added - `TrackId` and `PeerId` types ([#28]); - `Incrementable` trait ([#28]); - `CloseReason` and `CloseDescription` types ([#58](/../../pull/58)); -- `AddPeerConnectionMetrics` client command with `IceConnectionState` metric ([#71](/../../pull/71)). +- `AddPeerConnectionMetrics` client command with `IceConnectionState` metric ([#71](/../../pull/71)); +- `RpcSettings` server message ([#75](/../../pull/75)). [#28]: /../../pull/28 diff --git a/proto/client-api/Cargo.toml b/proto/client-api/Cargo.toml index da0dfde74..9f24dee8e 100644 --- a/proto/client-api/Cargo.toml +++ b/proto/client-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "medea-client-api-proto" -version = "0.1.1-dev" +version = "0.2.0-dev" edition = "2018" description = "Client API protocol implementation for Medea media server" authors = ["Instrumentisto Team "] diff --git a/proto/client-api/src/lib.rs b/proto/client-api/src/lib.rs index 959be5601..859a30d48 100644 --- a/proto/client-api/src/lib.rs +++ b/proto/client-api/src/lib.rs @@ -54,26 +54,48 @@ impl_incrementable!(PeerId); impl_incrementable!(TrackId); // TODO: should be properly shared between medea and jason -#[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug)] /// Message sent by `Media Server` to `Client`. pub enum ServerMsg { - /// `pong` message that server answers with to WebSocket client in response - /// to received `ping` message. - Pong(u64), + /// `ping` message that `Media Server` is expected to send to `Client` + /// periodically for probing its aliveness. + Ping(u64), + /// `Media Server` notifies `Client` about happened facts and it reacts on /// them to reach the proper state. Event(Event), + + /// `Media Server` notifies `Client` about necessity to update its RPC + /// settings. + RpcSettings(RpcSettings), } -#[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Clone)] +/// RPC settings of `Client` received from `Media Server`. +#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RpcSettings { + /// Timeout of considering `Client` as lost by `Media Server` when it + /// doesn't receive [`ClientMsg::Pong`]. + /// + /// Unit: millisecond. + pub idle_timeout_ms: u64, + + /// Interval that `Media Server` sends [`ServerMsg::Ping`] with. + /// + /// Unit: millisecond. + pub ping_interval_ms: u64, +} + +#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug)] /// Message from 'Client' to 'Media Server'. pub enum ClientMsg { - /// `ping` message that WebSocket client is expected to send to the server - /// periodically. - Ping(u64), - /// Request of `Web Client` to change the state on `Media Server`. + /// `pong` message that `Client` answers with to `Media Server` in response + /// to received [`ServerMsg::Ping`]. + Pong(u64), + + /// Request of `Client` to change the state on `Media Server`. Command(Command), } @@ -81,9 +103,9 @@ pub enum ClientMsg { #[dispatchable] #[cfg_attr(feature = "medea", derive(Deserialize))] #[cfg_attr(feature = "jason", derive(Serialize))] -#[cfg_attr(test, derive(Debug, PartialEq))] +#[cfg_attr(test, derive(PartialEq))] #[serde(tag = "command", content = "data")] -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Command { /// Web Client sends SDP Offer. MakeSdpOffer { @@ -114,8 +136,8 @@ pub enum Command { /// Web Client's Peer Connection metrics. #[cfg_attr(feature = "medea", derive(Deserialize))] #[cfg_attr(feature = "jason", derive(Serialize))] -#[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug)] pub enum PeerMetrics { /// Peer Connection's ICE connection state. IceConnectionStateChanged(IceConnectionState), @@ -124,8 +146,8 @@ pub enum PeerMetrics { /// Peer Connection's ICE connection state. #[cfg_attr(feature = "medea", derive(Deserialize))] #[cfg_attr(feature = "jason", derive(Serialize))] -#[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug)] pub enum IceConnectionState { New, Checking, @@ -187,6 +209,7 @@ pub enum Event { tracks: Vec, ice_servers: Vec, }, + /// Media Server notifies Web Client about necessity to apply specified SDP /// Answer to Web Client's RTCPeerConnection. SdpAnswerMade { peer_id: PeerId, sdp_answer: String }, @@ -278,9 +301,9 @@ impl Serialize for ClientMsg { use serde::ser::SerializeStruct; match self { - Self::Ping(n) => { - let mut ping = serializer.serialize_struct("ping", 1)?; - ping.serialize_field("ping", n)?; + Self::Pong(n) => { + let mut ping = serializer.serialize_struct("pong", 1)?; + ping.serialize_field("pong", n)?; ping.end() } Self::Command(command) => command.serialize(serializer), @@ -294,27 +317,30 @@ impl<'de> Deserialize<'de> for ClientMsg { where D: Deserializer<'de>, { - use serde::de::Error; + use serde::de::Error as _; let ev = serde_json::Value::deserialize(deserializer)?; let map = ev.as_object().ok_or_else(|| { - Error::custom(format!("unable to deser ClientMsg [{:?}]", &ev)) + D::Error::custom(format!( + "unable to deserialize ClientMsg [{:?}]", + &ev + )) })?; - if let Some(v) = map.get("ping") { + if let Some(v) = map.get("pong") { let n = v.as_u64().ok_or_else(|| { - Error::custom(format!( - "unable to deser ClientMsg::Ping [{:?}]", + D::Error::custom(format!( + "unable to deserialize ClientMsg::Pong [{:?}]", &ev )) })?; - Ok(Self::Ping(n)) + Ok(Self::Pong(n)) } else { let command = serde_json::from_value::(ev).map_err(|e| { - Error::custom(format!( - "unable to deser ClientMsg::Command [{:?}]", + D::Error::custom(format!( + "unable to deserialize ClientMsg::Command [{:?}]", e )) })?; @@ -332,12 +358,15 @@ impl Serialize for ServerMsg { use serde::ser::SerializeStruct; match self { - Self::Pong(n) => { - let mut ping = serializer.serialize_struct("pong", 1)?; - ping.serialize_field("pong", n)?; + Self::Ping(n) => { + let mut ping = serializer.serialize_struct("ping", 1)?; + ping.serialize_field("ping", n)?; ping.end() } Self::Event(command) => command.serialize(serializer), + Self::RpcSettings(rpc_settings) => { + rpc_settings.serialize(serializer) + } } } } @@ -348,30 +377,39 @@ impl<'de> Deserialize<'de> for ServerMsg { where D: Deserializer<'de>, { - use serde::de::Error; + use serde::de::Error as _; let ev = serde_json::Value::deserialize(deserializer)?; let map = ev.as_object().ok_or_else(|| { - Error::custom(format!("unable to deser ServerMsg [{:?}]", &ev)) + D::Error::custom(format!( + "unable to deserialize ServerMsg [{:?}]", + &ev + )) })?; - if let Some(v) = map.get("pong") { + if let Some(v) = map.get("ping") { let n = v.as_u64().ok_or_else(|| { - Error::custom(format!( - "unable to deser ServerMsg::Pong [{:?}]", + D::Error::custom(format!( + "unable to deserialize ServerMsg::Ping [{:?}]", &ev )) })?; - Ok(Self::Pong(n)) + Ok(Self::Ping(n)) } else { - let event = serde_json::from_value::(ev).map_err(|e| { - Error::custom(format!( - "unable to deser ServerMsg::Event [{:?}]", - e - )) - })?; - Ok(Self::Event(event)) + let msg = serde_json::from_value::(ev.clone()) + .map(Self::Event) + .or_else(move |_| { + serde_json::from_value::(ev) + .map(Self::RpcSettings) + }) + .map_err(|e| { + D::Error::custom(format!( + "unable to deserialize ServerMsg [{:?}]", + e + )) + })?; + Ok(msg) } } } @@ -411,7 +449,7 @@ mod test { #[test] fn ping() { - let ping = ClientMsg::Ping(15); + let ping = ServerMsg::Ping(15); let ping_str = "{\"ping\":15}"; assert_eq!(ping_str, serde_json::to_string(&ping).unwrap()); @@ -448,7 +486,7 @@ mod test { #[test] fn pong() { - let pong = ServerMsg::Pong(5); + let pong = ClientMsg::Pong(5); let pong_str = "{\"pong\":5}"; assert_eq!(pong_str, serde_json::to_string(&pong).unwrap()); diff --git a/proto/control-api/src/grpc/api.rs b/proto/control-api/src/grpc/api.rs index ba225e60a..2c111aa9c 100644 --- a/proto/control-api/src/grpc/api.rs +++ b/proto/control-api/src/grpc/api.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.8.1. Do not edit +// This file is generated by rust-protobuf 2.10.0. Do not edit // @generated // https://github.com/Manishearth/rust-clippy/issues/702 @@ -24,7 +24,7 @@ use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions; /// Generated files are compatible only with the same version /// of protobuf runtime. -const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_1; +const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_10_0; #[derive(PartialEq,Clone,Default)] pub struct CreateRequest { diff --git a/proto/control-api/src/grpc/callback.rs b/proto/control-api/src/grpc/callback.rs index c46195a22..c4e3d31f0 100644 --- a/proto/control-api/src/grpc/callback.rs +++ b/proto/control-api/src/grpc/callback.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.8.1. Do not edit +// This file is generated by rust-protobuf 2.10.0. Do not edit // @generated // https://github.com/Manishearth/rust-clippy/issues/702 @@ -24,7 +24,7 @@ use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions; /// Generated files are compatible only with the same version /// of protobuf runtime. -const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_1; +const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_10_0; #[derive(PartialEq,Clone,Default)] pub struct Request { diff --git a/src/api/client/rpc_connection.rs b/src/api/client/rpc_connection.rs index 0791c58f9..d79935796 100644 --- a/src/api/client/rpc_connection.rs +++ b/src/api/client/rpc_connection.rs @@ -17,7 +17,7 @@ use crate::api::control::MemberId; pub struct CommandMessage(Command); /// Newtype for [`Event`] with actix [`Message`] implementation. -#[derive(From, Into, Message)] +#[derive(Debug, From, Into, Message)] pub struct EventMessage(Event); /// Abstraction over RPC connection with some remote [`Member`]. diff --git a/src/api/client/server.rs b/src/api/client/server.rs index 442ddb754..bc019f5ad 100644 --- a/src/api/client/server.rs +++ b/src/api/client/server.rs @@ -69,6 +69,7 @@ fn ws_index( member_id, Box::new(room), state.config.idle_timeout, + state.config.ping_interval, ), &request, payload, diff --git a/src/api/client/session.rs b/src/api/client/session.rs index 3a574b3d6..736c1ef22 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -1,6 +1,9 @@ //! WebSocket session. -use std::time::{Duration, Instant}; +use std::{ + convert::TryInto as _, + time::{Duration, Instant}, +}; use actix::{ fut::wrap_future, Actor, ActorContext, ActorFuture, Addr, Arbiter, @@ -9,7 +12,7 @@ use actix::{ use actix_web_actors::ws::{self, CloseCode}; use futures::future::Future; use medea_client_api_proto::{ - ClientMsg, CloseDescription, CloseReason, Event, ServerMsg, + ClientMsg, CloseDescription, CloseReason, Event, RpcSettings, ServerMsg, }; use crate::{ @@ -50,6 +53,12 @@ pub struct WsSession { /// from client. last_activity: Instant, + /// Last number of [`ServerMsg::Ping`]. + last_ping_num: u64, + + /// Interval to send [`ServerMsg::Ping`]s to a client with. + ping_interval: Duration, + /// [`WsSession`] closed reason. Should be set by the moment /// `Actor::stopped()` for this [`WsSession`] is called. close_reason: Option, @@ -61,19 +70,22 @@ impl WsSession { member_id: MemberId, room: Box, idle_timeout: Duration, + ping_interval: Duration, ) -> Self { Self { member_id, room, idle_timeout, last_activity: Instant::now(), + last_ping_num: 0, + ping_interval, close_reason: None, } } /// Starts watchdog which will drop connection if `now`-`last_activity` > /// `idle_timeout`. - fn start_watchdog(ctx: &mut ::Context) { + fn start_idle_watchdog(ctx: &mut ::Context) { ctx.run_interval(Duration::new(1, 0), |session, ctx| { if Instant::now().duration_since(session.last_activity) > session.idle_timeout @@ -91,6 +103,34 @@ impl WsSession { } }); } + + /// Starts [`ServerMsg::Ping`] sending. + fn start_pinger(&self, ctx: &mut ::Context) { + ctx.run_interval(self.ping_interval, |session, ctx| { + ctx.text( + serde_json::to_string(&ServerMsg::Ping(session.last_ping_num)) + .unwrap(), + ); + session.last_ping_num += 1; + }); + } + + /// Returns [`RpcSettings`] based on `idle_timeout` and `ping_interval` + /// settled for this [`WsSession`]. + fn get_rpc_settings(&self) -> RpcSettings { + RpcSettings { + idle_timeout_ms: self + .idle_timeout + .as_millis() + .try_into() + .expect("'idle_timeout' should fit into u64"), + ping_interval_ms: self + .ping_interval + .as_millis() + .try_into() + .expect("'ping_interval' should fit into u64"), + } + } } /// [`Actor`] implementation that provides an ergonomic way to deal with @@ -103,13 +143,24 @@ impl Actor for WsSession { fn started(&mut self, ctx: &mut Self::Context) { debug!("Started WsSession for Member [id = {}]", self.member_id); - Self::start_watchdog(ctx); - ctx.wait( wrap_future(self.room.connection_established( self.member_id.clone(), Box::new(ctx.address()), )) + .map( + |_, + session: &mut Self, + ctx: &mut ws::WebsocketContext| { + let rpc_settings_message = + serde_json::to_string(&session.get_rpc_settings()) + .unwrap(); + ctx.text(rpc_settings_message); + + Self::start_idle_watchdog(ctx); + session.start_pinger(ctx); + }, + ) .map_err( move |err, session: &mut Self, @@ -234,11 +285,8 @@ impl StreamHandler for WsSession { ws::Message::Text(text) => { self.last_activity = Instant::now(); match serde_json::from_str::(&text) { - Ok(ClientMsg::Ping(n)) => { - // Answer with Heartbeat::Pong. - ctx.text( - serde_json::to_string(&ServerMsg::Pong(n)).unwrap(), - ); + Ok(ClientMsg::Pong(_)) => { + // do nothing } Ok(ClientMsg::Command(command)) => { ctx.spawn(wrap_future(self.room.send_command(command))); @@ -303,7 +351,10 @@ impl StreamHandler for WsSession { #[cfg(test)] mod test { - use std::{sync::Mutex, time::Duration}; + use std::{ + sync::Mutex, + time::{Duration, Instant}, + }; use actix_http::HttpService; use actix_http_test::{TestServer, TestServerRuntime}; @@ -353,11 +404,15 @@ mod test { .expect_connection_established() .withf(move |member_id, _| *member_id == expected_member_id) .return_once(|_, _| Box::new(future::err(()))); + rpc_server + .expect_connection_closed() + .returning(|_, _| Box::new(future::ok(()))); WsSession::new( member_id, Box::new(rpc_server), Duration::from_secs(5), + Duration::from_secs(5), ) } @@ -376,9 +431,8 @@ mod test { assert_eq!(item, Some(close_frame)); } - // WsSession handles ping requests and answers with pong. #[test] - fn answers_ping_with_pong() { + fn sends_rpc_settings_and_pings() { let mut serv = test_server(|| -> WsSession { let member_id = MemberId::from(String::from("test_member")); let mut rpc_server = MockRpcServer::new(); @@ -386,30 +440,47 @@ mod test { rpc_server .expect_connection_established() .return_once(|_, _| Box::new(future::ok(()))); + rpc_server + .expect_connection_closed() + .returning(|_, _| Box::new(future::ok(()))); WsSession::new( member_id, Box::new(rpc_server), Duration::from_secs(5), + Duration::from_millis(50), ) }); let client = serv.ws().unwrap(); + let (item, client) = + serv.block_on(client.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(Frame::Text(Some( + String::from( + r#"{"idle_timeout_ms":5000,"ping_interval_ms":50}"# + ) + .into() + ))) + ); + + let (item, client) = + serv.block_on(client.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(Frame::Text(Some(String::from(r#"{"ping":0}"#).into()))) + ); - let client = serv - .block_on( - client.send(Message::Text(String::from(r#"{"ping":25}"#))), - ) - .unwrap(); let (item, _) = serv.block_on(client.into_future()).map_err(|_| ()).unwrap(); assert_eq!( item, - Some(Frame::Text(Some(String::from(r#"{"pong":25}"#).into()))) + Some(Frame::Text(Some(String::from(r#"{"ping":1}"#).into()))) ); } - // WsSession is dropped and WebSocket connection is closed if no pings + // WsSession is dropped and WebSocket connection is closed if no pongs // received for idle_timeout. #[test] fn dropped_if_idle() { @@ -434,19 +505,27 @@ mod test { member_id, Box::new(rpc_server), Duration::from_millis(100), + Duration::from_secs(10), ) }); let client = serv.ws().unwrap(); - let (item, _) = - serv.block_on(client.into_future()).map_err(|_| ()).unwrap(); + let start = std::time::Instant::now(); + let (item, _) = serv + .block_on(client.skip(1).into_future()) + .map_err(|_| ()) + .unwrap(); let close_frame = Frame::Close(Some(CloseReason { code: CloseCode::Normal, description: Some(String::from(r#"{"reason":"Idle"}"#)), })); + assert!( + Instant::now().duration_since(start) > Duration::from_millis(99) + ); + assert!(Instant::now().duration_since(start) < Duration::from_secs(2)); assert_eq!(item, Some(close_frame)); } @@ -467,6 +546,9 @@ mod test { rpc_server .expect_connection_established() .return_once(|_, _| Box::new(future::ok(()))); + rpc_server + .expect_connection_closed() + .returning(|_, _| Box::new(future::ok(()))); rpc_server.expect_send_command().return_once(|command| { let _ = CHAN.0.lock().unwrap().take().unwrap().send(command); @@ -477,6 +559,7 @@ mod test { member_id, Box::new(rpc_server), Duration::from_secs(5), + Duration::from_secs(5), ) }); @@ -537,11 +620,15 @@ mod test { Box::new(future::ok(())) }, ); + rpc_server + .expect_connection_closed() + .returning(|_, _| Box::new(future::ok(()))); WsSession::new( member_id, Box::new(rpc_server), Duration::from_secs(5), + Duration::from_secs(5), ) }); @@ -564,8 +651,10 @@ mod test { .wait() .unwrap(); - let (item, _) = - serv.block_on(client.into_future()).map_err(|_| ()).unwrap(); + let (item, _) = serv + .block_on(client.skip(1).into_future()) + .map_err(|_| ()) + .unwrap(); let close_frame = Frame::Close(Some(CloseReason { code: CloseCode::Normal, @@ -597,11 +686,15 @@ mod test { Box::new(future::ok(())) }, ); + rpc_server + .expect_connection_closed() + .returning(|_, _| Box::new(future::ok(()))); WsSession::new( member_id, Box::new(rpc_server), Duration::from_secs(5), + Duration::from_secs(5), ) }); @@ -625,8 +718,10 @@ mod test { .wait() .unwrap(); - let (item, _) = - serv.block_on(client.into_future()).map_err(|_| ()).unwrap(); + let (item, _) = serv + .block_on(client.skip(1).into_future()) + .map_err(|_| ()) + .unwrap(); let event = "{\"event\":\"SdpAnswerMade\",\"data\":{\"peer_id\":77,\"\ sdp_answer\":\"sdp_answer\"}}"; diff --git a/src/conf/rpc.rs b/src/conf/rpc.rs index 0d0f26a96..0ee4c787d 100644 --- a/src/conf/rpc.rs +++ b/src/conf/rpc.rs @@ -20,6 +20,11 @@ pub struct Rpc { #[default(Duration::from_secs(10))] #[serde(with = "humantime_serde")] pub reconnect_timeout: Duration, + + /// Interval of sending `Ping`s from the server to the client. + #[default(Duration::from_secs(3))] + #[serde(with = "humantime_serde")] + pub ping_interval: Duration, } #[cfg(test)] diff --git a/src/signalling/participants.rs b/src/signalling/participants.rs index 169174af1..9c9514fa0 100644 --- a/src/signalling/participants.rs +++ b/src/signalling/participants.rs @@ -176,15 +176,13 @@ impl ParticipantService { member_id: &MemberId, credentials: &str, ) -> Result { - match self.get_member_by_id(member_id) { - Some(member) => { - if member.credentials().eq(credentials) { - Ok(member) - } else { - Err(AuthorizationError::InvalidCredentials) - } - } - None => Err(AuthorizationError::MemberNotExists), + let member = self + .get_member_by_id(member_id) + .ok_or(AuthorizationError::MemberNotExists)?; + if member.credentials() == credentials { + Ok(member) + } else { + Err(AuthorizationError::InvalidCredentials) } } @@ -200,14 +198,13 @@ impl ParticipantService { member_id: MemberId, event: Event, ) -> impl Future { - match self.connections.get(&member_id) { - Some(conn) => Either::A( + if let Some(conn) = self.connections.get(&member_id) { + Either::A( conn.send_event(event) .map_err(move |_| RoomError::UnableToSendEvent(member_id)), - ), - None => Either::B(future::err(RoomError::ConnectionNotExists( - member_id, - ))), + ) + } else { + Either::B(future::err(RoomError::ConnectionNotExists(member_id))) } } @@ -241,6 +238,7 @@ impl ParticipantService { { ctx.cancel_future(handler); } + self.insert_connection(member_id, conn); Box::new(wrap_future( connection .close(CloseDescription::new(CloseReason::Reconnected)) @@ -322,18 +320,18 @@ impl ParticipantService { } /// Deletes [`IceUser`] associated with provided [`Member`]. + // Type inference fails on `.map_or_else()` and cannot coerce + // `Box` into `Box`. + #[allow(clippy::option_map_unwrap_or_else)] fn delete_ice_user( &mut self, member_id: &MemberId, ) -> Box> { - // TODO: rewrite using `Option::flatten` when it will be in stable rust. - match self.get_member_by_id(&member_id) { - Some(member) => match member.take_ice_user() { - Some(ice_user) => self.turn_service.delete(vec![ice_user]), - None => Box::new(future::ok(())), - }, - None => Box::new(future::ok(())), - } + self.get_member_by_id(&member_id) + .map(|member| member.take_ice_user()) + .flatten() + .map(|ice_user| self.turn_service.delete(vec![ice_user])) + .unwrap_or_else(|| Box::new(future::ok(()))) } /// Cancels all connection close tasks, closes all [`RpcConnection`]s and diff --git a/src/signalling/peers.rs b/src/signalling/peers.rs index 1752ef574..c1cdbd33e 100644 --- a/src/signalling/peers.rs +++ b/src/signalling/peers.rs @@ -87,13 +87,14 @@ impl PeerRepository { let first_member_id = first_member.id(); let second_member_id = second_member.id(); - debug!( - "Created peer between {} and {}.", - first_member_id, second_member_id - ); let first_peer_id = self.peers_count.next_id(); let second_peer_id = self.peers_count.next_id(); + debug!( + "Created Peers pair between {} and {}: [{}, {}].", + first_member_id, second_member_id, first_peer_id, second_peer_id, + ); + let first_peer = Peer::new( first_peer_id, first_member_id.clone(),