Skip to content

Commit 4a29388

Browse files
niklasad1dvdplmbillylindemanTriplEightdependabot[bot]
authored
[ws server]: support for subscriptions with params (#336)
* [ws server]: draft SubscriptionSinkWithParams * rexport types * PoC design2 * improve example * Update ws-server/src/server.rs Co-authored-by: David <[email protected]> * Subscription example (#324) * Add a test for calling methods with multiple params of multiple types (#308) * Add a test for calling methods with multiple params of multiple types * cargo fmt Co-authored-by: Niklas Adolfsson <[email protected]> * [ws client] RegisterNotification support (#303) * Rename NotifResponse to SubscriptionResponse to make room for new impl * Add support for on_notification Subscription<T> types * Fix handling of NotificationHandler in manager * cleanup * Implement NotificationHandler to replace Subscription<T> and clean up plumbing * More cleanup * impl Drop for NotificationHandler * Address pr feedback #1 * ws client register_notification pr feedback 2 * Fix doc * fix typo * Add tests, get NH working * More cleanup of String/&str * fix doc * Drop notification handler on send_back_sink error * ws client notification auto unsubscribe when channel full test * Change order of type params to register_method (#312) * Change order of type params to register_method * Cleanup and fmt * Update ws-server/src/tests.rs Co-authored-by: Niklas Adolfsson <[email protected]> * CI: optimize caching (#317) * Bump actions/checkout from 2 to 2.3.4 (#315) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v2...v2.3.4) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions-rs/cargo from 1 to 1.0.3 (#314) Bumps [actions-rs/cargo](https://github.com/actions-rs/cargo) from 1 to 1.0.3. - [Release notes](https://github.com/actions-rs/cargo/releases) - [Changelog](https://github.com/actions-rs/cargo/blob/master/CHANGELOG.md) - [Commits](actions-rs/cargo@v1...v1.0.3) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions-rs/toolchain from 1 to 1.0.7 (#313) Bumps [actions-rs/toolchain](https://github.com/actions-rs/toolchain) from 1 to 1.0.7. - [Release notes](https://github.com/actions-rs/toolchain/releases) - [Changelog](https://github.com/actions-rs/toolchain/blob/master/CHANGELOG.md) - [Commits](actions-rs/toolchain@v1...v1.0.7) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [ws server]: add logs (#319) * WIP - hangs * fix example * cleanup * Add certificate_store() to WsClientBuilder (#321) * Add custom_certificate to WsClientBuilder * Use system certs instead of specified file * Cache client_config * Move client_config logic to fn build * Default use_system_certificates to true * Move out connector * Add CertificateStore type * cargo fmt * cargo clippy * Resolve comment: Rename variable * Resolved comments Co-authored-by: Niklas Adolfsson <[email protected]> Co-authored-by: Billy Lindeman <[email protected]> Co-authored-by: Denis Pisarev <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Albin Hedman <[email protected]> * grumbles: impl maciej proposal * fix test build * add test for subscription with param * cargo fmt * Update examples/ws_subscription.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * grumbles * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: David <[email protected]> * Update utils/src/server/rpc_module.rs Co-authored-by: Maciej Hirsz <[email protected]> * fix more grumbles * [subscriptionSink]: introduce into_sinks * use replace * fix more nits * maciej design 2 * fix tests * remove log * [rpc context mod]: register_subscription with ctx * nits * nits again * move subscribers mutex * clippy * [ws subscribe]: avoid send message on unsubscribed * revert unintentional changes * Subscription with context example (#345) * Add weather example to show how to use subscriptions with context * Add note * Cleanup * Additional cleanup (#347) * Add weather example to show how to use subscriptions with context * Add note * Cleanup * fmt * Cleanup and docs * fmt * ignore error on subscription Co-authored-by: David Palm <[email protected]> Co-authored-by: Billy Lindeman <[email protected]> Co-authored-by: Denis Pisarev <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Albin Hedman <[email protected]> Co-authored-by: Maciej Hirsz <[email protected]>
1 parent 13c3a88 commit 4a29388

File tree

12 files changed

+459
-82
lines changed

12 files changed

+459
-82
lines changed

examples/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ env_logger = "0.8"
1212
jsonrpsee = { path = "../jsonrpsee", features = ["full"] }
1313
log = "0.4"
1414
tokio = { version = "1", features = ["full"] }
15+
serde = "1"
16+
serde_json = "1"
17+
restson = "0.7"
1518

1619
[[example]]
1720
name = "http"
@@ -25,6 +28,14 @@ path = "ws.rs"
2528
name = "ws_subscription"
2629
path = "ws_subscription.rs"
2730

31+
[[example]]
32+
name = "ws_sub_with_params"
33+
path = "ws_sub_with_params.rs"
34+
2835
[[example]]
2936
name = "proc_macro"
3037
path = "proc_macro.rs"
38+
39+
[[example]]
40+
name = "weather"
41+
path = "weather.rs"

examples/weather.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2019 Parity Technologies (UK) Ltd.
2+
//
3+
// Permission is hereby granted, free of charge, to any
4+
// person obtaining a copy of this software and associated
5+
// documentation files (the "Software"), to deal in the
6+
// Software without restriction, including without
7+
// limitation the rights to use, copy, modify, merge,
8+
// publish, distribute, sublicense, and/or sell copies of
9+
// the Software, and to permit persons to whom the Software
10+
// is furnished to do so, subject to the following
11+
// conditions:
12+
//
13+
// The above copyright notice and this permission notice
14+
// shall be included in all copies or substantial portions
15+
// of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18+
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19+
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20+
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21+
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22+
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23+
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24+
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25+
// DEALINGS IN THE SOFTWARE.
26+
27+
//! Example of setting up a subscription that polls a remote API, in this case the api.openweathermap.org/weather, and
28+
//! sends the data back to the subscriber whenever the weather in London changes. The openweathermap API client is
29+
//! passed at registration as part of the "context" object. We only want to send data on the subscription when the
30+
//! weather actually changes, so we store the current weather in the context, hence the need for a `Mutex` to allow
31+
//! mutation.
32+
33+
use jsonrpsee::{
34+
ws_client::{traits::SubscriptionClient, v2::params::JsonRpcParams, WsClientBuilder},
35+
ws_server::RpcContextModule,
36+
ws_server::WsServer,
37+
};
38+
use restson::{Error as RestsonError, RestPath};
39+
use serde::{Deserialize, Serialize};
40+
use std::net::SocketAddr;
41+
use std::sync::Mutex;
42+
43+
// Set up the types to deserialize the weather data.
44+
// See https://openweathermap.org/current for the details about the API used in this example.
45+
#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
46+
struct Weather {
47+
name: String,
48+
wind: Wind,
49+
clouds: Clouds,
50+
main: Main,
51+
}
52+
#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
53+
struct Clouds {
54+
all: usize,
55+
}
56+
#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
57+
struct Main {
58+
temp: f64,
59+
pressure: usize,
60+
humidity: usize,
61+
}
62+
#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
63+
struct Wind {
64+
speed: f64,
65+
deg: usize,
66+
}
67+
68+
impl RestPath<&(String, String)> for Weather {
69+
fn get_path(params: &(String, String)) -> Result<String, RestsonError> {
70+
// Set up your own API key at https://openweathermap.org/current
71+
const API_KEY: &'static str = "f6ba475df300d5f91135550da0f4a867";
72+
Ok(String::from(format!("data/2.5/weather?q={}&units={}&appid={}", params.0, params.1, API_KEY,)))
73+
}
74+
}
75+
76+
#[tokio::main]
77+
async fn main() -> anyhow::Result<()> {
78+
env_logger::init();
79+
let addr = run_server().await?;
80+
let url = format!("ws://{}", addr);
81+
82+
let client = WsClientBuilder::default().build(&url).await?;
83+
84+
// Subscription to the London weather
85+
let params = JsonRpcParams::Array(vec!["London,uk".into(), "metric".into()]);
86+
let mut weather_sub = client.subscribe::<Weather>("weather_sub", params, "weather_unsub").await?;
87+
while let Some(w) = weather_sub.next().await {
88+
println!("[client] London weather: {:?}", w);
89+
}
90+
91+
Ok(())
92+
}
93+
94+
/// The context passed on registration, used to store a REST client to query for the current weather and the current
95+
/// "state".
96+
struct WeatherApiCx {
97+
api_client: restson::RestClient,
98+
last_weather: Weather,
99+
}
100+
101+
async fn run_server() -> anyhow::Result<SocketAddr> {
102+
let mut server = WsServer::new("127.0.0.1:0").await?;
103+
104+
let api_client = restson::RestClient::new("http://api.openweathermap.org").unwrap();
105+
let last_weather = Weather::default();
106+
let cx = Mutex::new(WeatherApiCx { api_client, last_weather });
107+
let mut module = RpcContextModule::new(cx);
108+
module
109+
.register_subscription_with_context("weather_sub", "weather_unsub", |params, sink, cx| {
110+
let params: (String, String) = params.parse()?;
111+
log::debug!(target: "server", "Subscribed with params={:?}", params);
112+
std::thread::spawn(move || loop {
113+
let mut cx = cx.lock().unwrap();
114+
let current_weather: Weather = cx.api_client.get(&params).unwrap();
115+
if current_weather != cx.last_weather {
116+
log::debug!(target: "server", "Fetched London weather: {:?}, sending", current_weather);
117+
sink.send(&current_weather).unwrap();
118+
cx.last_weather = current_weather;
119+
} else {
120+
log::trace!(target: "server", "Same weather as before. Not sending.")
121+
}
122+
std::thread::sleep(std::time::Duration::from_millis(500));
123+
});
124+
Ok(())
125+
})
126+
.unwrap();
127+
128+
server.register_module(module.into_module()).unwrap();
129+
130+
let addr = server.local_addr()?;
131+
tokio::spawn(async move { server.start().await });
132+
Ok(addr)
133+
}

examples/ws_sub_with_params.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2019 Parity Technologies (UK) Ltd.
2+
//
3+
// Permission is hereby granted, free of charge, to any
4+
// person obtaining a copy of this software and associated
5+
// documentation files (the "Software"), to deal in the
6+
// Software without restriction, including without
7+
// limitation the rights to use, copy, modify, merge,
8+
// publish, distribute, sublicense, and/or sell copies of
9+
// the Software, and to permit persons to whom the Software
10+
// is furnished to do so, subject to the following
11+
// conditions:
12+
//
13+
// The above copyright notice and this permission notice
14+
// shall be included in all copies or substantial portions
15+
// of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18+
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19+
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20+
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21+
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22+
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23+
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24+
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25+
// DEALINGS IN THE SOFTWARE.
26+
27+
use jsonrpsee::{
28+
ws_client::{traits::SubscriptionClient, v2::params::JsonRpcParams, WsClientBuilder},
29+
ws_server::WsServer,
30+
};
31+
use std::net::SocketAddr;
32+
33+
#[tokio::main]
34+
async fn main() -> anyhow::Result<()> {
35+
env_logger::init();
36+
let addr = run_server().await?;
37+
let url = format!("ws://{}", addr);
38+
39+
let client = WsClientBuilder::default().build(&url).await?;
40+
41+
// Subscription with a single parameter
42+
let params = JsonRpcParams::Array(vec![3.into()]);
43+
let mut sub_params_one = client.subscribe::<Option<char>>("sub_one_param", params, "unsub_one_param").await?;
44+
println!("subscription with one param: {:?}", sub_params_one.next().await);
45+
46+
// Subscription with multiple parameters
47+
let params = JsonRpcParams::Array(vec![2.into(), 5.into()]);
48+
let mut sub_params_two = client.subscribe::<String>("sub_params_two", params, "unsub_params_two").await?;
49+
println!("subscription with two params: {:?}", sub_params_two.next().await);
50+
51+
Ok(())
52+
}
53+
54+
async fn run_server() -> anyhow::Result<SocketAddr> {
55+
const LETTERS: &'static str = "abcdefghijklmnopqrstuvxyz";
56+
let mut server = WsServer::new("127.0.0.1:0").await?;
57+
server
58+
.register_subscription("sub_one_param", "unsub_one_param", |params, sink| {
59+
let idx: usize = params.one()?;
60+
std::thread::spawn(move || loop {
61+
let _ = sink.send(&LETTERS.chars().nth(idx));
62+
std::thread::sleep(std::time::Duration::from_millis(50));
63+
});
64+
Ok(())
65+
})
66+
.unwrap();
67+
server
68+
.register_subscription("sub_params_two", "unsub_params_two", |params, sink| {
69+
let (one, two): (usize, usize) = params.parse()?;
70+
std::thread::spawn(move || loop {
71+
let _ = sink.send(&LETTERS[one..two].to_string());
72+
std::thread::sleep(std::time::Duration::from_millis(100));
73+
});
74+
Ok(())
75+
})
76+
.unwrap();
77+
78+
let addr = server.local_addr()?;
79+
tokio::spawn(async move { server.start().await });
80+
Ok(addr)
81+
}

examples/ws_subscription.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ async fn main() -> anyhow::Result<()> {
5454

5555
async fn run_server() -> anyhow::Result<SocketAddr> {
5656
let mut server = WsServer::new("127.0.0.1:0").await?;
57-
let mut subscription = server.register_subscription("subscribe_hello", "unsubscribe_hello").unwrap();
58-
59-
std::thread::spawn(move || loop {
60-
subscription.send(&"hello my friend").unwrap();
61-
std::thread::sleep(std::time::Duration::from_secs(1));
62-
});
57+
server.register_subscription("subscribe_hello", "unsubscribe_hello", |_, sink| {
58+
std::thread::spawn(move || loop {
59+
sink.send(&"hello my friend").unwrap();
60+
std::thread::sleep(std::time::Duration::from_secs(1));
61+
});
62+
Ok(())
63+
})?;
6364

6465
let addr = server.local_addr()?;
6566
tokio::spawn(async move { server.start().await });

tests/tests/helpers.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,45 @@ pub async fn websocket_server_with_subscription() -> SocketAddr {
3636
let rt = tokio::runtime::Runtime::new().unwrap();
3737

3838
let mut server = rt.block_on(WsServer::new("127.0.0.1:0")).unwrap();
39-
let mut sub_hello = server.register_subscription("subscribe_hello", "unsubscribe_hello").unwrap();
40-
let mut sub_foo = server.register_subscription("subscribe_foo", "unsubscribe_foo").unwrap();
4139

4240
server.register_method("say_hello", |_| Ok("hello")).unwrap();
4341

44-
server_started_tx.send(server.local_addr().unwrap()).unwrap();
45-
46-
rt.spawn(server.start());
42+
server
43+
.register_subscription("subscribe_hello", "unsubscribe_hello", |_, sink| {
44+
std::thread::spawn(move || loop {
45+
let _ = sink.send(&"hello from subscription");
46+
std::thread::sleep(Duration::from_millis(50));
47+
});
48+
Ok(())
49+
})
50+
.unwrap();
51+
52+
server
53+
.register_subscription("subscribe_foo", "unsubscribe_foo", |_, sink| {
54+
std::thread::spawn(move || loop {
55+
let _ = sink.send(&1337);
56+
std::thread::sleep(Duration::from_millis(100));
57+
});
58+
Ok(())
59+
})
60+
.unwrap();
61+
62+
server
63+
.register_subscription("subscribe_add_one", "unsubscribe_add_one", |params, sink| {
64+
let mut count: usize = params.one()?;
65+
std::thread::spawn(move || loop {
66+
count = count.wrapping_add(1);
67+
let _ = sink.send(&count);
68+
std::thread::sleep(Duration::from_millis(100));
69+
});
70+
Ok(())
71+
})
72+
.unwrap();
4773

4874
rt.block_on(async move {
49-
loop {
50-
tokio::time::sleep(Duration::from_millis(100)).await;
75+
server_started_tx.send(server.local_addr().unwrap()).unwrap();
5176

52-
sub_hello.send(&"hello from subscription").unwrap();
53-
sub_foo.send(&1337_u64).unwrap();
54-
}
77+
server.start().await
5578
});
5679
});
5780

tests/tests/integration_tests.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ async fn ws_subscription_works() {
5454
}
5555
}
5656

57+
#[tokio::test]
58+
async fn ws_subscription_with_input_works() {
59+
let server_addr = websocket_server_with_subscription().await;
60+
let server_url = format!("ws://{}", server_addr);
61+
let client = WsClientBuilder::default().build(&server_url).await.unwrap();
62+
let mut add_one: Subscription<u64> =
63+
client.subscribe("subscribe_add_one", vec![1.into()].into(), "unsubscribe_add_one").await.unwrap();
64+
65+
for i in 2..4 {
66+
let next = add_one.next().await.unwrap();
67+
assert_eq!(next, i);
68+
}
69+
}
70+
5771
#[tokio::test]
5872
async fn ws_method_call_works() {
5973
let server_addr = websocket_server().await;

types/src/client.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ where
116116
match self.notifs_rx.next().await {
117117
Some(n) => match serde_json::from_value(n) {
118118
Ok(parsed) => return Some(parsed),
119-
Err(e) => log::debug!("Subscription response error: {:?}", e),
119+
Err(e) => {
120+
log::error!("Subscription response error: {:?}", e);
121+
}
120122
},
121123
None => return None,
122124
}

types/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub enum Error {
4040
Request(String),
4141
/// Frontend/backend channel error.
4242
#[error("Frontend/backend channel error: {0}")]
43-
Internal(#[source] futures_channel::mpsc::SendError),
43+
Internal(#[from] futures_channel::mpsc::SendError),
4444
/// Invalid response,
4545
#[error("Invalid response: {0}")]
4646
InvalidResponse(Mismatch<String>),

types/src/v2/params.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,8 @@ impl<'a> RpcParams<'a> {
8383
where
8484
T: Deserialize<'a>,
8585
{
86-
match self.0 {
87-
None => Err(CallError::InvalidParams),
88-
Some(params) => serde_json::from_str(params).map_err(|_| CallError::InvalidParams),
89-
}
86+
let params = self.0.unwrap_or("null");
87+
serde_json::from_str(params).map_err(|_| CallError::InvalidParams)
9088
}
9189

9290
/// Attempt to parse only the first parameter from an array into type T

0 commit comments

Comments
 (0)