diff --git a/.pubnub.yml b/.pubnub.yml index ab736364..da0930e9 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,9 +1,20 @@ name: rust -version: 0.4.1 +version: 0.5.0 schema: 1 scm: github.com/pubnub/rust files: [] changelog: + - date: 2024-01-25 + version: 0.5.0 + changes: + - type: feature + text: "Change the real-time event handling interface." + - type: feature + text: "`user_id` state for specified channels will be maintained by the SDK. State with subscribe calls has been improved." + - type: feature + text: "Adding `Channel`, `ChannelGroup`, `ChannelMetadata` and `UuidMetadata` entities to be first-class citizens to access APIs related to them. Currently, access is provided only for subscription APIs." + - type: feature + text: "Added ability to configure request retry policies to exclude specific endpoints from retry." - date: 2023-11-03 version: 0.4.1 changes: diff --git a/Cargo.toml b/Cargo.toml index 1905825c..afcc3265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pubnub" -version = "0.4.1" +version = "0.5.0" edition = "2021" license-file = "LICENSE" authors = ["PubNub "] @@ -63,7 +63,7 @@ std = ["derive_builder/std", "log/std", "uuid/std", "base64/std", "spin/std", "s extra_platforms = ["spin/portable_atomic", "dep:portable-atomic"] # [Internal features] (not intended for use outside of the library) -contract_test = ["parse_token", "publish", "access", "crypto"] +contract_test = ["parse_token", "publish", "access", "crypto", "std", "subscribe", "presence", "tokio"] full_no_std = ["serde", "reqwest", "crypto", "parse_token", "blocking", "publish", "access", "subscribe", "tokio", "presence"] full_no_std_platform_independent = ["serde", "crypto", "parse_token", "blocking", "publish", "access", "subscribe", "presence"] pubnub_only = ["crypto", "parse_token", "blocking", "publish", "access", "subscribe", "presence"] @@ -106,7 +106,7 @@ getrandom = { version = "0.2", optional = true } # parse_token ciborium = { version = "0.2.1", default-features = false, optional = true } -# subscribe +# subscribe, presence futures = { version = "0.3.28", default-features = false, optional = true } tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros", "time"] } async-channel = { version = "1.8", optional = true } @@ -122,7 +122,7 @@ async-trait = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } wiremock = "0.5" env_logger = "0.10" -cucumber = { version = "0.20.0", features = ["output-junit"] } +cucumber = { version = "0.20.2", features = ["output-junit"] } reqwest = { version = "0.11", features = ["json"] } test-case = "3.0" hashbrown = { version = "0.14.0", features = ["serde"] } @@ -165,7 +165,7 @@ required-features = ["default"] [[example]] name = "subscribe" -required-features = ["default", "subscribe"] +required-features = ["default", "subscribe", "presence"] [[example]] name = "subscribe_raw" diff --git a/README.md b/README.md index 04e96d2b..51ede15b 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ Add `pubnub` to your Rust project in the `Cargo.toml` file: ```toml # default features [dependencies] -pubnub = "0.4.1" +pubnub = "0.5.0" # all features [dependencies] -pubnub = { version = "0.4.1", features = ["full"] } +pubnub = { version = "0.5.0", features = ["full"] } ``` ### Example @@ -57,53 +57,53 @@ use serde_json; #[tokio::main] async fn main() -> Result<(), Box> { - let publish_key = "my_publish_key"; + use pubnub::subscribe::{EventEmitter, SubscriptionParams}; +let publish_key = "my_publish_key"; let subscribe_key = "my_subscribe_key"; let client = PubNubClientBuilder::with_reqwest_transport() - .with_keyset(Keyset { - subscribe_key, - publish_key: Some(publish_key), - secret_key: None, - }) - .with_user_id("user_id") - .build()?; - println!("PubNub instance created"); - - let subscription = client - .subscribe() - .channels(["my_channel".into()].to_vec()) - .execute()?; - - println!("Subscribed to channel"); - - // Launch a new task to print out each received message - tokio::spawn(subscription.stream().for_each(|event| async move { - match event { - SubscribeStreamEvent::Update(update) => { - match update { - Update::Message(message) | Update::Signal(message) => { - // Silently log if UTF-8 conversion fails - if let Ok(utf8_message) = String::from_utf8(message.data.clone()) { - if let Ok(cleaned) = serde_json::from_str::(&utf8_message) { - println!("message: {}", cleaned); - } - } - } - Update::Presence(presence) => { - println!("presence: {:?}", presence) - } - Update::Object(object) => { - println!("object: {:?}", object) - } - Update::MessageAction(action) => { - println!("message action: {:?}", action) - } - Update::File(file) => { - println!("file: {:?}", file) + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + println!("PubNub instance created"); + + let subscription = client.subscription(SubscriptionParams { + channels: Some(&["my_channel"]), + channel_groups: None, + options: None + }); + + println!("Subscribed to channel"); + + // Launch a new task to print out each received message + tokio::spawn(client.status_stream().for_each(|status| async move { + println!("\nStatus: {:?}", status) + })); + tokio::spawn(subscription.stream().for_each(|event| async move { + match event { + Update::Message(message) | Update::Signal(message) => { + // Silently log if UTF-8 conversion fails + if let Ok(utf8_message) = String::from_utf8(message.data.clone()) { + if let Ok(cleaned) = serde_json::from_str::(&utf8_message) { + println!("message: {}", cleaned); } } } - SubscribeStreamEvent::Status(status) => println!("\nstatus: {:?}", status), + Update::Presence(presence) => { + println!("presence: {:?}", presence) + } + Update::AppContext(object) => { + println!("object: {:?}", object) + } + Update::MessageAction(action) => { + println!("message action: {:?}", action) + } + Update::File(file) => { + println!("file: {:?}", file) + } } })); @@ -132,11 +132,11 @@ disable them in the `Cargo.toml` file, like so: ```toml # only blocking and access + default features [dependencies] -pubnub = { version = "0.4.1", features = ["blocking", "access"] } +pubnub = { version = "0.5.0", features = ["blocking", "access"] } # only parse_token + default features [dependencies] -pubnub = { version = "0.4.1", features = ["parse_token"] } +pubnub = { version = "0.5.0", features = ["parse_token"] } ``` ### Available features @@ -175,7 +175,7 @@ you need, for example: ```toml [dependencies] -pubnub = { version = "0.4.1", default-features = false, features = ["serde", "publish", +pubnub = { version = "0.5.0", default-features = false, features = ["serde", "publish", "blocking"] } ``` diff --git a/examples/no_std/src/subscribe.rs b/examples/no_std/src/subscribe.rs index f4d87295..64606394 100644 --- a/examples/no_std/src/subscribe.rs +++ b/examples/no_std/src/subscribe.rs @@ -38,7 +38,8 @@ getrandom::register_custom_getrandom!(custom_random); fn custom_random(buf: &mut [u8]) -> Result<(), getrandom::Error> { // We're using `42` as a random number, because it's the answer // to the Ultimate Question of Life, the Universe, and Everything. - // In your program, you should use proper random number generator that is supported by your target. + // In your program, you should use proper random number generator that is + // supported by your target. for i in buf.iter_mut() { *i = 42; } @@ -48,7 +49,8 @@ fn custom_random(buf: &mut [u8]) -> Result<(), getrandom::Error> { // Many targets have very specific requirements for networking, so it's hard to // provide a generic implementation. -// Depending on the target, you will probably need to implement `Transport` trait. +// Depending on the target, you will probably need to implement `Transport` +// trait. struct MyTransport; impl Transport for MyTransport { @@ -64,8 +66,8 @@ impl Transport for MyTransport { // As our target does not have `std` library, we need to provide custom // implementation of `GlobalAlloc` trait. // -// In your program, you should use proper allocator that is supported by your target. -// Here you have dummy implementation that does nothing. +// In your program, you should use proper allocator that is supported by your +// target. Here you have dummy implementation that does nothing. #[derive(Default)] pub struct Allocator; @@ -82,23 +84,23 @@ static GLOBAL_ALLOCATOR: Allocator = Allocator; // As our target does not have `std` library, we need to provide custom // implementation of `panic_handler`. // -// In your program, you should use proper panic handler that is supported by your target. -// Here you have dummy implementation that does nothing. +// In your program, you should use proper panic handler that is supported by +// your target. Here you have dummy implementation that does nothing. #[panic_handler] fn panicking(_: &PanicInfo) -> ! { loop {} } -// As we're using `no_main` attribute, we need to define `main` function manually. -// For this example we're using `extern "C"` ABI to make it work. +// As we're using `no_main` attribute, we need to define `main` function +// manually. For this example we're using `extern "C"` ABI to make it work. #[no_mangle] pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> usize { publish_example().map(|_| 0).unwrap() } -// In standard subscribe examples we use `println` macro to print the result of the operation -// and it shows the idea of the example. `no_std` does not support `println` macro, -// so we're using `do_a_thing` function instead. +// In standard subscribe examples we use `println` macro to print the result of +// the operation and it shows the idea of the example. `no_std` does not support +// `println` macro, so we're using `do_a_thing` function instead. fn do_a_thing(_: T) {} // As `no_std` does not support `Error` trait, we use `PubNubError` instead. @@ -133,7 +135,7 @@ fn publish_example() -> Result<(), PubNubError> { match update? { Update::Message(message) | Update::Signal(message) => do_a_thing(message), Update::Presence(presence) => do_a_thing(presence), - Update::Object(object) => do_a_thing(object), + Update::AppContext(object) => do_a_thing(object), Update::MessageAction(action) => do_a_thing(action), Update::File(file) => do_a_thing(file), }; diff --git a/examples/presence_state.rs b/examples/presence_state.rs index 4bd2826f..25b97d4f 100644 --- a/examples/presence_state.rs +++ b/examples/presence_state.rs @@ -1,16 +1,24 @@ +use std::collections::HashMap; + use pubnub::{Keyset, PubNubClientBuilder}; -use serde::Serialize; -use std::env; -#[derive(Debug, Serialize)] +#[derive(Debug, serde::Serialize)] struct State { is_doing: String, + flag: bool, +} +#[derive(Debug, serde::Serialize)] +struct State2 { + is_doing: String, + business: String, } #[tokio::main] async fn main() -> Result<(), Box> { - let publish_key = env::var("SDK_PUB_KEY")?; - let subscribe_key = env::var("SDK_SUB_KEY")?; + // let publish_key = env::var("SDK_PUB_KEY")?; + // let subscribe_key = env::var("SDK_SUB_KEY")?; + let publish_key = "demo"; + let subscribe_key = "demo"; let client = PubNubClientBuilder::with_reqwest_transport() .with_keyset(Keyset { @@ -23,9 +31,32 @@ async fn main() -> Result<(), Box> { println!("running!"); + client + .set_presence_state_with_heartbeat(HashMap::from([ + ( + "my_channel".to_string(), + State { + is_doing: "Something".to_string(), + flag: true, + }, + ), + ( + "other_channel".to_string(), + State { + is_doing: "Oh no".to_string(), + flag: false, + }, + ), + ])) + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .user_id("user_id") + .execute() + .await?; + client .set_presence_state(State { is_doing: "Nothing... Just hanging around...".into(), + flag: false, }) .channels(["my_channel".into(), "other_channel".into()].to_vec()) .user_id("user_id") diff --git a/examples/subscribe.rs b/examples/subscribe.rs index 04d3a3cf..fd9ab7c4 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -1,9 +1,16 @@ +use std::collections::HashMap; + use futures::StreamExt; -use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; -use pubnub::{Keyset, PubNubClientBuilder}; use serde::Deserialize; use std::env; +use pubnub::subscribe::{SubscriptionOptions, SubscriptionParams}; +use pubnub::{ + dx::subscribe::Update, + subscribe::{EventEmitter, EventSubscriber}, + Keyset, PubNubClientBuilder, +}; + #[derive(Debug, Deserialize)] struct Message { // Allowing dead code because we don't use these fields @@ -26,46 +33,92 @@ async fn main() -> Result<(), Box> { secret_key: None, }) .with_user_id("user_id") + .with_filter_expression("some_filter") + .with_heartbeat_value(100) + .with_heartbeat_interval(5) .build()?; println!("running!"); - let subscription = client - .subscribe() + client + .set_presence_state(HashMap::::from([ + ( + "is_doing".to_string(), + "Nothing... Just hanging around...".to_string(), + ), + ("flag".to_string(), "false".to_string()), + ])) .channels(["my_channel".into(), "other_channel".into()].to_vec()) - .heartbeat(10) - .filter_expression("some_filter") - .execute()?; + .user_id("user_id") + .execute() + .await?; + + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + let subscription = client.subscription(SubscriptionParams { + channels: Some(&["my_channel", "other_channel"]), + channel_groups: None, + options: Some(vec![SubscriptionOptions::ReceivePresenceEvents]), + }); + subscription.subscribe(None); + let subscription_clone = subscription.clone_empty(); + + // Attach connection status to the PubNub client instance. + tokio::spawn( + client + .status_stream() + .for_each(|status| async move { println!("\nstatus: {:?}", status) }), + ); tokio::spawn(subscription.stream().for_each(|event| async move { match event { - SubscribeStreamEvent::Update(update) => { - println!("\nupdate: {:?}", update); - match update { - Update::Message(message) | Update::Signal(message) => { - // Deserialize the message payload as you wish - match serde_json::from_slice::(&message.data) { - Ok(message) => println!("defined message: {:?}", message), - Err(_) => { - println!("other message: {:?}", String::from_utf8(message.data)) - } - } - } - Update::Presence(presence) => { - println!("presence: {:?}", presence) - } - Update::Object(object) => { - println!("object: {:?}", object) + Update::Message(message) | Update::Signal(message) => { + // Deserialize the message payload as you wish + match serde_json::from_slice::(&message.data) { + Ok(message) => println!("(a) defined message: {:?}", message), + Err(_) => { + println!("(a) other message: {:?}", String::from_utf8(message.data)) } - Update::MessageAction(action) => { - println!("message action: {:?}", action) - } - Update::File(file) => { - println!("file: {:?}", file) + } + } + Update::Presence(presence) => { + println!("(a) presence: {:?}", presence) + } + Update::AppContext(object) => { + println!("(a) object: {:?}", object) + } + Update::MessageAction(action) => { + println!("(a) message action: {:?}", action) + } + Update::File(file) => { + println!("(a) file: {:?}", file) + } + } + })); + + tokio::spawn(subscription_clone.stream().for_each(|event| async move { + match event { + Update::Message(message) | Update::Signal(message) => { + // Deserialize the message payload as you wish + match serde_json::from_slice::(&message.data) { + Ok(message) => println!("(b) defined message: {:?}", message), + Err(_) => { + println!("(b) other message: {:?}", String::from_utf8(message.data)) } } } - SubscribeStreamEvent::Status(status) => println!("\nstatus: {:?}", status), + Update::Presence(presence) => { + println!("(b) presence: {:?}", presence) + } + Update::AppContext(object) => { + println!("(b) object: {:?}", object) + } + Update::MessageAction(action) => { + println!("(b) message action: {:?}", action) + } + Update::File(file) => { + println!("(b) file: {:?}", file) + } } })); @@ -73,12 +126,30 @@ async fn main() -> Result<(), Box> { // "my_channel" and "other_channel" and see them printed in the console. // You can use the publish example or [PubNub console](https://www.pubnub.com/docs/console/) // to send messages. - tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + tokio::time::sleep(tokio::time::Duration::from_secs(15)).await; // You can also cancel the subscription at any time. - subscription.unsubscribe().await; + // subscription.unsubscribe(); + + println!("\nDisconnect from the real-time data stream"); + client.disconnect(); + + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + println!("\nReconnect to the real-time data stream"); + client.reconnect(None); // Let event engine process unsubscribe request + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + // If Subscription or Subscription will go out of scope they will unsubscribe. + // drop(subscription); + // drop(subscription_clone); + + println!( + "\nUnsubscribe from all data streams. To restore requires `subscription.subscribe(None)` call." ); + // Clean up before complete work with PubNub client instance. + client.unsubscribe_all(); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; Ok(()) diff --git a/examples/subscribe_raw.rs b/examples/subscribe_raw.rs index 32054780..3a857e67 100644 --- a/examples/subscribe_raw.rs +++ b/examples/subscribe_raw.rs @@ -54,7 +54,7 @@ async fn main() -> Result<(), Box> { Update::Presence(presence) => { println!("presence: {:?}", presence) } - Update::Object(object) => { + Update::AppContext(object) => { println!("object: {:?}", object) } Update::MessageAction(action) => { diff --git a/examples/subscribe_raw_blocking.rs b/examples/subscribe_raw_blocking.rs index 6edfe134..a063d0d4 100644 --- a/examples/subscribe_raw_blocking.rs +++ b/examples/subscribe_raw_blocking.rs @@ -53,7 +53,7 @@ fn main() -> Result<(), Box> { Update::Presence(presence) => { println!("presence: {:?}", presence) } - Update::Object(object) => { + Update::AppContext(object) => { println!("object: {:?}", object) } Update::MessageAction(action) => { diff --git a/src/core/channel.rs b/src/core/channel.rs new file mode 100644 index 00000000..6795e414 --- /dev/null +++ b/src/core/channel.rs @@ -0,0 +1,217 @@ +//! # Channel entity module +//! +//! This module contains the [`Channel`] type, which can be used as a +//! first-class citizen to access the [`PubNub API`]. +//! +//! [`PubNub API`]: https://www.pubnub.com/docs + +#[cfg(all(feature = "subscribe", feature = "std"))] +use spin::RwLock; + +use crate::{ + core::PubNubEntity, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{string::String, sync::Arc}, + core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, +}; + +#[cfg(all(feature = "subscribe", feature = "std"))] +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::{format, sync::Weak, vec, vec::Vec}, + subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, +}; + +/// Channel entity. +/// +/// Entity as a first-class citizen provides access to the entity-specific API. +pub struct Channel { + inner: Arc>, +} + +/// Channel entity reference. +/// +/// This struct contains the actual channel state. It is wrapped in an Arc by +/// [`Channel`] and uses internal mutability for its internal state. +/// +/// Not intended to be used directly. Use [`Channel`] instead. +#[derive(Debug)] +pub struct ChannelRef { + /// Reference on backing [`PubNubClientInstance`] client. + /// + /// Client is used to support entity-specific actions like: + /// * subscription + /// + /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. + client: Arc>, + + /// Unique channel name. + /// + /// Channel names are used by the [`PubNub API`] as a unique identifier for + /// resources on which a certain operation should be performed. + /// + /// [`PubNub API`]: https://pubnub.com/docs + pub name: String, + + /// Active subscriptions count. + /// + /// Track number of [`Subscription`] which use this entity to receive + /// real-time updates. + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock, +} + +impl Channel { + /// Creates a new instance of a channel. + /// + /// # Arguments + /// + /// * `client` - The client instance used to access [`PubNub API`]. + /// * `name` - The name of the channel. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) fn new(client: &PubNubClientInstance, name: S) -> Channel + where + S: Into, + { + Self { + inner: Arc::new(ChannelRef { + client: Arc::new(client.clone()), + name: name.into(), + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock::new(0), + }), + } + } + + /// Increase the subscriptions count. + /// + /// Increments the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn increase_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + *subscriptions_count_slot += 1; + } + + /// Decrease the subscriptions count. + /// + /// Decrements the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// > As long as entity used by at least one subscription it can't be + /// > removed from subscription + /// loop. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn decrease_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + if *subscriptions_count_slot > 0 { + *subscriptions_count_slot -= 1; + } + } + + /// Current count of subscriptions. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// # Returns + /// + /// Returns the current count of subscriptions. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn subscriptions_count(&self) -> usize { + *self.subscriptions_count.read() + } +} + +impl Deref for Channel { + type Target = ChannelRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Channel { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the Channel are not allowed") + } +} + +impl Clone for Channel { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl PartialEq for Channel { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl From> for PubNubEntity { + fn from(value: Channel) -> Self { + PubNubEntity::Channel(value) + } +} + +impl Debug for Channel { + #[cfg(all(feature = "subscribe", feature = "std"))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "Channel {{ name: {}, subscriptions_count: {} }}", + self.name, + self.subscriptions_count() + ) + } + + #[cfg(not(all(feature = "subscribe", feature = "std")))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "Channel {{ name: {} }}", self.name) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscribable for Channel { + fn names(&self, presence: bool) -> Vec { + let mut names = vec![self.name.clone()]; + presence.then(|| names.push(format!("{}-pnpres", self.name))); + + names + } + + fn r#type(&self) -> SubscribableType { + SubscribableType::Channel + } + + fn client(&self) -> Weak> { + Arc::downgrade(&self.client) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscriber for Channel +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscription(&self, options: Option>) -> Subscription { + Subscription::new(self.client(), self.clone().into(), options) + } +} diff --git a/src/core/channel_group.rs b/src/core/channel_group.rs new file mode 100644 index 00000000..2a843788 --- /dev/null +++ b/src/core/channel_group.rs @@ -0,0 +1,218 @@ +//! # ChannelGroup entity module +//! +//! This module contains the [`ChannelGroup`] type, which can be used as a +//! first-class citizen to access the [`PubNub API`]. +//! +//! [`PubNub API`]: https://www.pubnub.com/docs + +#[cfg(all(feature = "subscribe", feature = "std"))] +use spin::RwLock; + +use crate::{ + core::PubNubEntity, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{string::String, sync::Arc}, + core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, +}; + +#[cfg(all(feature = "subscribe", feature = "std"))] +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::{format, sync::Weak, vec, vec::Vec}, + subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, +}; + +/// Channel group entity. +/// +/// Entity as a first-class citizen provides access to the entity-specific API. +pub struct ChannelGroup { + inner: Arc>, +} + +/// Channel group entity reference. +/// +/// This struct contains the actual channel group state. It is wrapped in an Arc +/// by [`ChannelGroup`] and uses internal mutability for its internal state. +/// +/// Not intended to be used directly. Use [`ChannelGroup`] instead. +#[derive(Debug)] +pub struct ChannelGroupRef { + /// Reference on backing [`PubNubClientInstance`] client. + /// + /// Client is used to support entity-specific actions like: + /// * subscription + /// + /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. + client: Arc>, + + /// Unique channel group name. + /// + /// Channel group names are used by the [`PubNub API`] as a unique + /// identifier for resources on which a certain operation should be + /// performed. + /// + /// [`PubNub API`]: https://pubnub.com/docs + pub name: String, + + /// Active subscriptions count. + /// + /// Track number of [`Subscription`] which use this entity to receive + /// real-time updates. + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock, +} + +impl ChannelGroup { + /// Creates a new instance of a channel group. + /// + /// # Arguments + /// + /// * `client` - The client instance used to access [`PubNub API`]. + /// * `name` - The name of the channel group. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) fn new(client: &PubNubClientInstance, name: S) -> ChannelGroup + where + S: Into, + { + Self { + inner: Arc::new(ChannelGroupRef { + client: Arc::new(client.clone()), + name: name.into(), + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock::new(0), + }), + } + } + + /// Increase the subscriptions count. + /// + /// Increments the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn increase_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + *subscriptions_count_slot += 1; + } + + /// Decrease the subscriptions count. + /// + /// Decrements the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// > As long as entity used by at least one subscription it can't be + /// > removed from subscription + /// loop. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn decrease_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + if *subscriptions_count_slot > 0 { + *subscriptions_count_slot -= 1; + } + } + + /// Current count of subscriptions. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// # Returns + /// + /// Returns the current count of subscriptions. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn subscriptions_count(&self) -> usize { + *self.subscriptions_count.read() + } +} + +impl Deref for ChannelGroup { + type Target = ChannelGroupRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for ChannelGroup { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the ChannelGroup are not allowed") + } +} + +impl Clone for ChannelGroup { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl PartialEq for ChannelGroup { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl From> for PubNubEntity { + fn from(value: ChannelGroup) -> Self { + PubNubEntity::ChannelGroup(value) + } +} + +impl Debug for ChannelGroup { + #[cfg(all(feature = "subscribe", feature = "std"))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "ChannelGroup {{ name: {}, subscriptions_count: {} }}", + self.name, + self.subscriptions_count() + ) + } + + #[cfg(not(all(feature = "subscribe", feature = "std")))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "ChannelGroup {{ name: {} }}", self.name) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscribable for ChannelGroup { + fn names(&self, presence: bool) -> Vec { + let mut names = vec![self.name.clone()]; + presence.then(|| names.push(format!("{}-pnpres", self.name))); + + names + } + + fn r#type(&self) -> SubscribableType { + SubscribableType::ChannelGroup + } + + fn client(&self) -> Weak> { + Arc::downgrade(&self.client) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscriber for ChannelGroup +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscription(&self, options: Option>) -> Subscription { + Subscription::new(self.client(), self.clone().into(), options) + } +} diff --git a/src/core/channel_metadata.rs b/src/core/channel_metadata.rs new file mode 100644 index 00000000..35159929 --- /dev/null +++ b/src/core/channel_metadata.rs @@ -0,0 +1,215 @@ +//! # ChannelMetadata entity module +//! +//! This module contains the [`ChannelMetadata`] type, which can be used as a +//! first-class citizen to access the [`PubNub API`]. +//! +//! [`PubNub API`]: https://www.pubnub.com/docs + +#[cfg(all(feature = "subscribe", feature = "std"))] +use spin::RwLock; + +use crate::{ + core::PubNubEntity, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{string::String, sync::Arc}, + core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, +}; + +#[cfg(all(feature = "subscribe", feature = "std"))] +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::{sync::Weak, vec, vec::Vec}, + subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, +}; + +/// Channel metadata entity. +/// +/// Entity as a first-class citizen provides access to the entity-specific API. +pub struct ChannelMetadata { + inner: Arc>, +} + +/// Channel metadata entity reference. +/// +/// This struct contains the actual channel metadata state. It is wrapped in an +/// Arc by [`ChannelMetadata`] and uses internal mutability for its internal +/// state. +/// +/// Not intended to be used directly. Use [`ChannelMetadata`] instead. +#[derive(Debug)] +pub struct ChannelMetadataRef { + /// Reference on backing [`PubNubClientInstance`] client. + /// + /// Client is used to support entity-specific actions like: + /// * subscription + /// + /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. + client: Arc>, + + /// Unique channel metadata object identifier. + /// + /// Channel metadata object identifier used by the [`PubNub API`] as unique + /// for resources on which a certain operation should be performed. + /// + /// [`PubNub API`]: https://pubnub.com/docs + pub id: String, + + /// Active subscriptions count. + /// + /// Track number of [`Subscription`] which use this entity to receive + /// real-time updates. + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock, +} + +impl ChannelMetadata { + /// Creates a new instance of a channel metadata object. + /// + /// # Arguments + /// + /// * `client` - The client instance used to access [`PubNub API`]. + /// * `id` - The identifier of the channel metadata object. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) fn new(client: &PubNubClientInstance, id: S) -> ChannelMetadata + where + S: Into, + { + Self { + inner: Arc::new(ChannelMetadataRef { + client: Arc::new(client.clone()), + id: id.into(), + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock::new(0), + }), + } + } + + /// Increase the subscriptions count. + /// + /// Increments the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn increase_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + *subscriptions_count_slot += 1; + } + + /// Decrease the subscriptions count. + /// + /// Decrements the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// > As long as entity used by at least one subscription it can't be + /// > removed from subscription + /// loop. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn decrease_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + if *subscriptions_count_slot > 0 { + *subscriptions_count_slot -= 1; + } + } + + /// Current count of subscriptions. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// # Returns + /// + /// Returns the current count of subscriptions. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn subscriptions_count(&self) -> usize { + *self.subscriptions_count.read() + } +} + +impl Deref for ChannelMetadata { + type Target = ChannelMetadataRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for ChannelMetadata { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the ChannelMetadata are not allowed") + } +} + +impl Clone for ChannelMetadata { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl PartialEq for ChannelMetadata { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl From> for PubNubEntity { + fn from(value: ChannelMetadata) -> Self { + PubNubEntity::ChannelMetadata(value) + } +} + +impl Debug for ChannelMetadata { + #[cfg(all(feature = "subscribe", feature = "std"))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "ChannelMetadata {{ id: {}, subscriptions_count: {} }}", + self.id, + self.subscriptions_count() + ) + } + + #[cfg(not(all(feature = "subscribe", feature = "std")))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "ChannelMetadata {{ id: {} }}", self.id) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscribable for ChannelMetadata { + fn names(&self, _presence: bool) -> Vec { + vec![self.id.clone()] + } + + fn r#type(&self) -> SubscribableType { + SubscribableType::Channel + } + + fn client(&self) -> Weak> { + Arc::downgrade(&self.client) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscriber for ChannelMetadata +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscription(&self, options: Option>) -> Subscription { + Subscription::new(self.client(), self.clone().into(), options) + } +} diff --git a/src/core/data_stream.rs b/src/core/data_stream.rs new file mode 100644 index 00000000..7ec8d1a3 --- /dev/null +++ b/src/core/data_stream.rs @@ -0,0 +1,199 @@ +//! # Data stream module +//! +//! This module contains the [`DataStream`] struct. + +use futures::Stream; +use spin::RwLock; + +use crate::lib::{ + alloc::{collections::VecDeque, sync::Arc}, + core::{ + ops::{Deref, DerefMut}, + pin::Pin, + task::{Context, Poll, Waker}, + }, +}; + +/// A generic data stream. +/// +/// [`DataStream`] provides functionality which allows to `poll` any new data +/// which has been pushed into data queue. +#[derive(Debug, Default)] +pub struct DataStream { + inner: Arc>, +} + +impl Deref for DataStream { + type Target = DataStreamRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for DataStream { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the DataStream are not allowed") + } +} + +impl Clone for DataStream { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +/// A generic data stream reference. +/// +/// This struct contains the actual data stream state. +/// It is wrapped in an `Arc` by [`DataStream`] and uses interior mutability for +/// its internal state. +/// +/// Not intended to be used directly. Use [`DataStream`] instead. +#[derive(Debug, Default)] +pub struct DataStreamRef { + /// Queue with data for stream listener. + queue: RwLock>, + + /// Data stream waker. + /// + /// Handler used each time when new data available for a stream listener. + waker: RwLock>, + + /// Whether data stream still valid or not. + is_valid: RwLock, +} + +impl DataStream { + /// Creates a new `DataStream` with a default queue size of 100. + /// + /// # Example + /// + /// ``` + /// use pubnub::core::DataStream; + /// + /// let stream: DataStream = DataStream::new(); + /// ``` + pub fn new() -> DataStream { + Self::with_queue_size(100) + } + + /// Creates a new `DataStream` with a specified queue size. + /// + /// The `with_queue_size` function creates a new `DataStream` with an empty + /// queue and the specified `size`. The `size` parameter determines the + /// maximum number of elements that can be stored in the queue before + /// old elements are dropped to make room for new ones. + /// + /// # Arguments + /// + /// * `size` - The maximum number of elements that can be stored in the + /// queue. + /// + /// # Returns + /// + /// A new `DataStream` with the specified queue size. + /// + /// # Example + /// + /// ```rust + /// use std::collections::VecDeque; + /// use pubnub::core::DataStream; + /// + /// let data_stream = DataStream::::with_queue_size(10); + /// ``` + pub fn with_queue_size(size: usize) -> DataStream { + Self::with_queue_data(VecDeque::new(), size) + } + + /// Creates a new `DataStream` with a given queue `data` and `size`. + /// The `data` is put into a `VecDeque` with capacity `size`. + /// + /// # Arguments + /// + /// * `data` - A `VecDeque` of type `D` that contains the initial data to be + /// put into the queue. + /// * `size` - The maximum capacity of the queue. + /// + /// # Return value + /// + /// Returns a new `DataStream` containing the queue data. + /// + /// # Examples + /// ``` + /// use std::collections::VecDeque; + /// use pubnub::core::DataStream; + /// + /// let data: VecDeque = VecDeque::from(vec![1, 2, 3]); + /// let stream: DataStream = DataStream::with_queue_data(data, 5); + /// ``` + pub fn with_queue_data(data: VecDeque, size: usize) -> DataStream { + let mut queue_data = VecDeque::with_capacity(size); + + if !data.is_empty() { + queue_data.extend(data.into_iter().take(queue_data.capacity())); + } + + Self { + inner: Arc::new(DataStreamRef { + queue: RwLock::new(queue_data), + waker: RwLock::new(None), + is_valid: RwLock::new(true), + }), + } + } + + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn push_data(&self, data: D) { + if !*self.is_valid.read() { + return; + } + + let mut queue_data_slot = self.queue.write(); + + // Dropping the earliest entry to prevent the queue from growing too large. + if queue_data_slot.len() == queue_data_slot.capacity() { + queue_data_slot.pop_front(); + } + + queue_data_slot.push_back(data); + + self.wake_stream(); + } + + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn invalidate(&self) { + let mut is_valid = self.is_valid.write(); + *is_valid = false; + self.wake_stream(); + } + + #[cfg(all(feature = "subscribe", feature = "std"))] + fn wake_stream(&self) { + if let Some(waker) = self.waker.write().take() { + waker.wake(); + } + } +} + +impl Stream for DataStream { + type Item = D; + + fn poll_next(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + if !*self.is_valid.read() { + return Poll::Ready(None); + } + + let mut waker_slot = self.waker.write(); + *waker_slot = Some(ctx.waker().clone()); + + if let Some(data) = self.queue.write().pop_front() { + Poll::Ready(Some(data)) + } else { + Poll::Pending + } + } +} diff --git a/src/core/entity.rs b/src/core/entity.rs new file mode 100644 index 00000000..96e78ac4 --- /dev/null +++ b/src/core/entity.rs @@ -0,0 +1,183 @@ +//! # PubNub entity module +//! +//! This module contains the [`PubNubEntity`] trait, which is used to implement +//! a PubNub entity that can be used as a first-class citizen to access the +//! [`PubNub API`]. +//! +//! [`PubNub API`]: https://www.pubnub.com/docs + +use crate::{ + lib::core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + }, + Channel, ChannelGroup, ChannelMetadata, UserMetadata, +}; + +#[cfg(all(feature = "subscribe", feature = "std"))] +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::{string::String, vec::Vec}, + subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, +}; + +pub(crate) enum PubNubEntity { + Channel(Channel), + ChannelGroup(ChannelGroup), + ChannelMetadata(ChannelMetadata), + UserMetadata(UserMetadata), +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl PubNubEntity { + pub(crate) fn names(&self, presence: bool) -> Vec { + match self { + Self::Channel(channel) => channel.names(presence), + Self::ChannelGroup(channel_group) => channel_group.names(presence), + Self::ChannelMetadata(channel_metadata) => channel_metadata.names(presence), + Self::UserMetadata(uuid_metadata) => uuid_metadata.names(presence), + } + } + + pub(crate) fn r#type(&self) -> SubscribableType { + match self { + Self::Channel(channel) => channel.r#type(), + Self::ChannelGroup(channel_group) => channel_group.r#type(), + Self::ChannelMetadata(channel_metadata) => channel_metadata.r#type(), + Self::UserMetadata(uuid_metadata) => uuid_metadata.r#type(), + } + } + + /// Increase the subscriptions count. + /// + /// Increments the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn increase_subscriptions_count(&self) { + match self { + Self::Channel(channel) => channel.increase_subscriptions_count(), + Self::ChannelGroup(channel_group) => channel_group.increase_subscriptions_count(), + Self::ChannelMetadata(channel_metadata) => { + channel_metadata.increase_subscriptions_count() + } + Self::UserMetadata(uuid_metadata) => uuid_metadata.increase_subscriptions_count(), + } + } + + /// Decrease the subscriptions count. + /// + /// Decrements the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// > As long as entity used by at least one subscription it can't be + /// > removed from subscription + /// loop. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn decrease_subscriptions_count(&self) { + match self { + Self::Channel(channel) => channel.decrease_subscriptions_count(), + Self::ChannelGroup(channel_group) => channel_group.decrease_subscriptions_count(), + Self::ChannelMetadata(channel_metadata) => { + channel_metadata.decrease_subscriptions_count() + } + Self::UserMetadata(uuid_metadata) => uuid_metadata.decrease_subscriptions_count(), + } + } + + /// Current count of subscriptions. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// # Returns + /// + /// Returns the current count of subscriptions. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn subscriptions_count(&self) -> usize { + match self { + Self::Channel(channel) => channel.subscriptions_count(), + Self::ChannelGroup(channel_group) => channel_group.subscriptions_count(), + Self::ChannelMetadata(channel_metadata) => channel_metadata.subscriptions_count(), + Self::UserMetadata(uuid_metadata) => uuid_metadata.subscriptions_count(), + } + } +} + +impl Clone for PubNubEntity { + fn clone(&self) -> Self { + match self { + Self::Channel(channel) => Self::Channel(channel.clone()), + Self::ChannelGroup(channel_group) => Self::ChannelGroup(channel_group.clone()), + Self::ChannelMetadata(channel_metadata) => { + Self::ChannelMetadata(channel_metadata.clone()) + } + Self::UserMetadata(uuid_metadata) => Self::UserMetadata(uuid_metadata.clone()), + } + } +} + +impl PartialEq for PubNubEntity { + fn eq(&self, other: &Self) -> bool { + match self { + Self::Channel(channel_a) => { + let Self::Channel(channel_b) = other else { + return false; + }; + channel_a.eq(channel_b) + } + Self::ChannelGroup(channel_group_a) => { + let Self::ChannelGroup(channel_group_b) = other else { + return false; + }; + channel_group_a.eq(channel_group_b) + } + Self::ChannelMetadata(channel_metadata_a) => { + let Self::ChannelMetadata(channel_metadata_b) = other else { + return false; + }; + channel_metadata_a.eq(channel_metadata_b) + } + Self::UserMetadata(uuid_metadata_a) => { + let Self::UserMetadata(uuid_metadata_b) = other else { + return false; + }; + uuid_metadata_a.eq(uuid_metadata_b) + } + } + } +} + +impl Debug for PubNubEntity { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::Channel(channel) => write!(f, "Channel({channel:?})"), + Self::ChannelGroup(channel_group) => write!(f, "ChannelGroup({channel_group:?})"), + Self::ChannelMetadata(channel_metadata) => { + write!(f, "ChannelMetadata({channel_metadata:?})") + } + Self::UserMetadata(user_metadata) => write!(f, "UserMetadata({user_metadata:?})"), + } + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscriber for PubNubEntity +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscription(&self, options: Option>) -> Subscription { + match self { + PubNubEntity::Channel(channel) => channel.subscription(options), + PubNubEntity::ChannelGroup(channel_group) => channel_group.subscription(options), + PubNubEntity::ChannelMetadata(channel_metadata) => { + channel_metadata.subscription(options) + } + PubNubEntity::UserMetadata(uuid_metadata) => uuid_metadata.subscription(options), + } + } +} diff --git a/src/core/error.rs b/src/core/error.rs index e5b439b3..17718e1d 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -43,6 +43,13 @@ pub enum PubNubError { response: Option>, }, + /// this error is returned when request has been cancelled. + #[snafu(display("Request cancelled: {details}"))] + RequestCancel { + /// Information with reason why request has been cancelled. + details: String, + }, + /// this error is returned when the publication of the request fails #[snafu(display("Publish error: {details}"))] PublishError { diff --git a/src/core/event_engine/effect.rs b/src/core/event_engine/effect.rs index cc280143..3e632977 100644 --- a/src/core/event_engine/effect.rs +++ b/src/core/event_engine/effect.rs @@ -7,6 +7,9 @@ use crate::{ pub(crate) trait Effect: Send + Sync { type Invocation: EffectInvocation; + /// Effect name. + fn name(&self) -> String; + /// Unique effect identifier. fn id(&self) -> String; diff --git a/src/core/event_engine/effect_dispatcher.rs b/src/core/event_engine/effect_dispatcher.rs index fa3acc25..153eb1f5 100644 --- a/src/core/event_engine/effect_dispatcher.rs +++ b/src/core/event_engine/effect_dispatcher.rs @@ -1,6 +1,8 @@ -use crate::core::runtime::Runtime; use crate::{ - core::event_engine::{Effect, EffectHandler, EffectInvocation}, + core::{ + event_engine::{Effect, EffectHandler, EffectInvocation}, + runtime::Runtime, + }, lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; use async_channel::Receiver; @@ -8,7 +10,6 @@ use spin::rwlock::RwLock; /// State machine effects dispatcher. #[derive(Debug)] -#[allow(dead_code)] pub(crate) struct EffectDispatcher where EI: EffectInvocation + Send + Sync, @@ -44,7 +45,7 @@ where EH: EffectHandler + Send + Sync + 'static, EF: Effect + 'static, { - /// Create new effects dispatcher. + /// Create new an effects dispatcher. pub fn new(handler: EH, channel: Receiver) -> Self { EffectDispatcher { handler, @@ -60,44 +61,53 @@ where R: Runtime + 'static, C: Fn(Vec<::Event>) + Clone + Send + 'static, { - let mut started_slot = self.started.write(); let runtime_clone = runtime.clone(); let cloned_self = self.clone(); runtime.spawn(async move { - log::info!("Subscribe engine has started!"); + log::info!("Event engine has started!"); loop { - match cloned_self.invocations_channel.recv().await { + let invocation = cloned_self.invocations_channel.recv().await; + match invocation { Ok(invocation) => { - log::debug!("Received invocation: {}", invocation.id()); + if invocation.is_terminating() { + log::debug!("Received event engine termination invocation"); + break; + } + log::debug!("Received invocation: {}", invocation.id()); let effect = cloned_self.dispatch(&invocation); let task_completion = completion.clone(); if let Some(effect) = effect { - log::debug!("Dispatched effect: {}", effect.id()); + log::debug!("Dispatched effect: {}", effect.name()); let cloned_self = cloned_self.clone(); runtime_clone.spawn(async move { let events = effect.run().await; - if invocation.managed() { + if invocation.is_managed() { cloned_self.remove_managed_effect(effect.id()); } task_completion(events); }); + } else if invocation.is_cancelling() { + log::debug!("Dispatched effect: {}", invocation.id()); } } Err(err) => { log::error!("Receive error: {err:?}"); + break; } } } + *cloned_self.started.write() = false; + log::info!("Event engine has stopped!"); }); - *started_slot = true; + *self.started.write() = true; } /// Dispatch effect associated with `invocation`. @@ -105,14 +115,14 @@ where if let Some(effect) = self.handler.create(invocation) { let effect = Arc::new(effect); - if invocation.managed() { + if invocation.is_managed() { let mut managed = self.managed.write(); managed.push(effect.clone()); } Some(effect) } else { - if invocation.cancelling() { + if invocation.is_cancelling() { self.cancel_effect(invocation); } @@ -132,7 +142,6 @@ where } /// Remove managed effect. - #[allow(dead_code)] fn remove_managed_effect(&self, effect_id: String) { let mut managed = self.managed.write(); if let Some(position) = managed.iter().position(|ef| ef.id() == effect_id) { @@ -165,6 +174,14 @@ mod should { impl Effect for TestEffect { type Invocation = TestInvocation; + fn name(&self) -> String { + match self { + Self::One => "EFFECT_ONE".into(), + Self::Two => "EFFECT_TWO".into(), + Self::Three => "EFFECT_THREE".into(), + } + } + fn id(&self) -> String { match self { Self::One => "EFFECT_ONE".into(), @@ -202,11 +219,11 @@ mod should { } } - fn managed(&self) -> bool { + fn is_managed(&self) -> bool { matches!(self, Self::Two | Self::Three) } - fn cancelling(&self) -> bool { + fn is_cancelling(&self) -> bool { matches!(self, Self::CancelThree) } @@ -216,6 +233,10 @@ mod should { _ => false, } } + + fn is_terminating(&self) -> bool { + false + } } struct TestEffectHandler {} @@ -246,6 +267,10 @@ mod should { async fn sleep(self, _delay: u64) { // Do nothing. } + + async fn sleep_microseconds(self, _delay: u64) { + // Do nothing. + } } #[test] diff --git a/src/core/event_engine/effect_invocation.rs b/src/core/event_engine/effect_invocation.rs index bf377aca..254603a9 100644 --- a/src/core/event_engine/effect_invocation.rs +++ b/src/core/event_engine/effect_invocation.rs @@ -12,11 +12,15 @@ pub(crate) trait EffectInvocation { fn id(&self) -> &str; /// Whether invoked effect lifetime should be managed by dispatcher or not. - fn managed(&self) -> bool; + fn is_managed(&self) -> bool; /// Whether effect invocation cancels managed effect or not. - fn cancelling(&self) -> bool; + fn is_cancelling(&self) -> bool; /// Whether effect invocation cancels specific managed effect or not. fn cancelling_effect(&self, effect: &Self::Effect) -> bool; + + /// Whether invoked effect invocation should terminate current Event Engine + /// processing loop or not. + fn is_terminating(&self) -> bool; } diff --git a/src/core/event_engine/mod.rs b/src/core/event_engine/mod.rs index 450d5684..7cda7a88 100644 --- a/src/core/event_engine/mod.rs +++ b/src/core/event_engine/mod.rs @@ -43,7 +43,6 @@ pub(crate) mod cancel; /// [`EventEngine`] is the core of state machines used in PubNub client and /// manages current system state and handles external events. #[derive(Debug)] -#[allow(dead_code)] pub(crate) struct EventEngine where EI: EffectInvocation + Send + Sync, @@ -63,6 +62,14 @@ where /// Current event engine state. current_state: RwLock, + + /// Whether Event Engine still active. + /// + /// Event Engine can be used as long as it is active. + /// + /// > Note: Activity can be changed in case of whole stack termination. Can + /// > be in case of call to unsubscribe all. + active: RwLock, } impl EventEngine @@ -73,19 +80,18 @@ where EI: EffectInvocation + Send + Sync + 'static, { /// Create [`EventEngine`] with initial state for state machine. - #[allow(dead_code)] pub fn new(handler: EH, state: S, runtime: R) -> Arc where R: Runtime + 'static, { let (channel_tx, channel_rx) = async_channel::bounded::(100); - let effect_dispatcher = Arc::new(EffectDispatcher::new(handler, channel_rx)); let engine = Arc::new(EventEngine { effect_dispatcher, effect_dispatcher_channel: channel_tx, current_state: RwLock::new(state), + active: RwLock::new(true), }); engine.start(runtime); @@ -94,8 +100,10 @@ where } /// Retrieve current engine state. + /// + /// > Note: Code actually used in tests. #[allow(dead_code)] - pub fn current_state(&self) -> S { + pub(crate) fn current_state(&self) -> S { (*self.current_state.read()).clone() } @@ -103,8 +111,12 @@ where /// /// Process event passed to the system and perform required transitions to /// new state if required. - #[allow(dead_code)] pub fn process(&self, event: &EI::Event) { + if !*self.active.read() { + log::debug!("Can't process events because the event engine is not active."); + return; + }; + log::debug!("Processing event: {}", event.id()); let transition = { @@ -123,9 +135,14 @@ where /// * update current state /// * call effects dispatcher to process effect invocation fn process_transition(&self, transition: Transition) { - { + if !*self.active.read() { + log::debug!("Can't process transition because the event engine is not active."); + return; + }; + + if let Some(state) = transition.state { let mut writable_state = self.current_state.write(); - *writable_state = transition.state; + *writable_state = state; } transition.invocations.into_iter().for_each(|invocation| { @@ -153,6 +170,20 @@ where runtime, ); } + + /// Stop state machine using specific invocation. + /// + /// > Note: Should be provided effect information which respond with `true` + /// for `is_terminating` method call. + pub fn stop(&self, invocation: EI) { + { + *self.active.write() = false; + } + + if let Err(error) = self.effect_dispatcher_channel.send_blocking(invocation) { + error!("Unable dispatch invocation: {error:?}") + } + } } #[cfg(test)] @@ -191,34 +222,41 @@ mod should { match event { TestEvent::One => { if matches!(self, Self::NotStarted) { - Some(self.transition_to(Self::Started, None)) + Some(self.transition_to(Some(Self::Started), None)) } else if matches!(self, Self::Completed) { - Some( - self.transition_to(Self::NotStarted, Some(vec![TestInvocation::Three])), - ) + Some(self.transition_to( + Some(Self::NotStarted), + Some(vec![TestInvocation::Three]), + )) } else { None } } TestEvent::Two => matches!(self, Self::Started) - .then(|| self.transition_to(Self::InProgress, None)), - TestEvent::Three => matches!(self, Self::InProgress) - .then(|| self.transition_to(Self::Completed, Some(vec![TestInvocation::One]))), + .then(|| self.transition_to(Some(Self::InProgress), None)), + TestEvent::Three => matches!(self, Self::InProgress).then(|| { + self.transition_to(Some(Self::Completed), Some(vec![TestInvocation::One])) + }), } } fn transition_to( &self, - state: Self::State, + state: Option, invocations: Option>, ) -> Transition { + let on_enter_invocations = match state.clone() { + Some(state) => state.enter().unwrap_or_default(), + None => vec![], + }; + Transition { invocations: self .exit() .unwrap_or_default() .into_iter() .chain(invocations.unwrap_or_default()) - .chain(state.enter().unwrap_or_default()) + .chain(on_enter_invocations) .collect(), state, } @@ -252,6 +290,14 @@ mod should { impl Effect for TestEffect { type Invocation = TestInvocation; + fn name(&self) -> String { + match self { + Self::One => "EFFECT_ONE".into(), + Self::Two => "EFFECT_TWO".into(), + Self::Three => "EFFECT_THREE".into(), + } + } + fn id(&self) -> String { match self { Self::One => "EFFECT_ONE".into(), @@ -288,17 +334,21 @@ mod should { } } - fn managed(&self) -> bool { + fn is_managed(&self) -> bool { matches!(self, Self::Two | Self::Three) } - fn cancelling(&self) -> bool { + fn is_cancelling(&self) -> bool { false } fn cancelling_effect(&self, _effect: &Self::Effect) -> bool { false } + + fn is_terminating(&self) -> bool { + false + } } struct TestEffectHandler {} @@ -328,6 +378,10 @@ mod should { async fn sleep(self, delay: u64) { tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await } + + async fn sleep_microseconds(self, _delay: u64) { + // Do nothing. + } } #[tokio::test] diff --git a/src/core/event_engine/state.rs b/src/core/event_engine/state.rs index aa3ceaf4..e3e6fa40 100644 --- a/src/core/event_engine/state.rs +++ b/src/core/event_engine/state.rs @@ -45,7 +45,7 @@ pub(crate) trait State: Clone + PartialEq { /// effect invocations of target state. fn transition_to( &self, - state: Self::State, + state: Option, invocations: Option>, ) -> Transition; } diff --git a/src/core/event_engine/transition.rs b/src/core/event_engine/transition.rs index 6fc4050c..b334f934 100644 --- a/src/core/event_engine/transition.rs +++ b/src/core/event_engine/transition.rs @@ -14,7 +14,7 @@ where I: EffectInvocation, { /// Target state machine state. - pub state: S, + pub state: Option, /// List of effect invocation which should be scheduled during transition. pub invocations: Vec, diff --git a/src/core/mod.rs b/src/core/mod.rs index dedec70f..a44e8b85 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -38,9 +38,9 @@ pub mod transport_response; // TODO: Retry policy can be implemented for `no_std` subscribe // when `no_std` event engine is implemented. -#[doc(inline)] #[cfg(feature = "std")] -pub use retry_policy::RequestRetryPolicy; +#[doc(inline)] +pub use retry_policy::RequestRetryConfiguration; #[cfg(feature = "std")] pub mod retry_policy; @@ -70,12 +70,38 @@ pub mod cryptor; pub(crate) mod event_engine; #[cfg(all(feature = "std", feature = "subscribe"))] +#[doc(inline)] pub use runtime::Runtime; #[cfg(all(feature = "std", feature = "subscribe"))] pub mod runtime; +#[doc(inline)] +pub use data_stream::DataStream; +pub mod data_stream; + pub(crate) mod utils; #[doc(inline)] pub use types::ScalarValue; + +#[doc(inline)] +pub(crate) use entity::PubNubEntity; +pub(crate) mod entity; + +#[doc(inline)] +pub use channel::Channel; +pub mod channel; + +#[doc(inline)] +pub use channel_group::ChannelGroup; +pub mod channel_group; + +#[doc(inline)] +pub use channel_metadata::ChannelMetadata; +pub mod channel_metadata; + +#[doc(inline)] +pub use user_metadata::UserMetadata; +pub mod user_metadata; + pub mod types; diff --git a/src/core/retry_policy.rs b/src/core/retry_policy.rs index b1f42e5c..37dd0146 100644 --- a/src/core/retry_policy.rs +++ b/src/core/retry_policy.rs @@ -1,69 +1,228 @@ //! # Request retry policy //! -//! This module contains the [`RequestRetryPolicy`] struct. +//! This module contains the [`RequestRetryConfiguration`] struct. //! It is used to calculate delays between failed requests to the [`PubNub API`] //! for next retry attempt. //! It is intended to be used by the [`pubnub`] crate. //! //! [`PubNub API`]: https://www.pubnub.com/docs //! [`pubnub`]: ../index.html -//! -use crate::core::PubNubError; + +use getrandom::getrandom; + +use crate::{core::PubNubError, lib::alloc::vec::Vec}; + +/// List of known endpoint groups (by context) +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Endpoint { + /// Unknown endpoint. + Unknown, + + /// The endpoints to send messages. + MessageSend, + + /// The endpoint for real-time update retrieval. + Subscribe, + + /// The endpoint to access and manage `user_id` presence and fetch channel + /// presence information. + Presence, + + /// The endpoint to access and manage files in channel-specific storage. + Files, + + /// The endpoint to access and manage messages for a specific channel(s) in + /// the persistent storage. + MessageStorage, + + /// The endpoint to access and manage channel groups. + ChannelGroups, + + /// The endpoint to access and manage device registration for channel push + /// notifications. + DevicePushNotifications, + + /// The endpoint to access and manage App Context objects. + AppContext, + + /// The endpoint to access and manage reactions for a specific message. + MessageReactions, +} /// Request retry policy. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum RequestRetryPolicy { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RequestRetryConfiguration { /// Requests shouldn't be tried again. None, /// Retry the request after the same amount of time. Linear { - /// The delay between failed retry attempts. + /// The delay between failed retry attempts in seconds. delay: u64, /// Number of times a request can be retried. max_retry: u8, + + /// Optional list of excluded endpoint groups. + /// + /// Endpoint groups for which automatic retry shouldn't be used. + excluded_endpoints: Option>, }, /// Retry the request using exponential amount of time. Exponential { - /// Minimum delay between failed retry attempts. + /// Minimum delay between failed retry attempts in seconds. min_delay: u64, - /// Maximum delay between failed retry attempts. + /// Maximum delay between failed retry attempts in seconds. max_delay: u64, /// Number of times a request can be retried. max_retry: u8, + + /// Optional list of excluded endpoint groups. + /// + /// Endpoint groups for which automatic retry shouldn't be used. + excluded_endpoints: Option>, }, } -impl RequestRetryPolicy { +impl RequestRetryConfiguration { + /// Creates a new instance of the `RequestRetryConfiguration` enum with a + /// default linear policy. + /// + /// The `Linear` policy retries the operation with a fixed delay between + /// each retry. The default delay is 2 seconds and the default maximum + /// number of retries is 10. + /// + /// # Example + /// + /// ``` + /// use pubnub::{Keyset, PubNubClientBuilder, RequestRetryConfiguration}; + /// + /// # fn main() -> Result<(), Box> { + /// let retry_configuration = RequestRetryConfiguration::default_linear(); + /// let client = PubNubClientBuilder::with_reqwest_transport() + /// .with_keyset(Keyset { + /// publish_key: Some("pub-c-abc123"), + /// subscribe_key: "sub-c-abc123", + /// secret_key: None, + /// }) + /// .with_user_id("my-user-id") + /// .with_retry_configuration(retry_configuration) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Returns + /// + /// A new instance of the `RetryPolicy` enum with the default linear policy. + pub fn default_linear() -> Self { + Self::Linear { + delay: 2, + max_retry: 10, + excluded_endpoints: None, + } + } + + /// Creates a new instance of the `RequestRetryConfiguration` enum with a + /// default exponential backoff policy. + /// + /// The `Exponential` backoff policy increases the delay between retries + /// exponentially. It starts with a minimum delay of 2 seconds and a + /// maximum delay of 150 seconds. It allows a maximum number of 6 + /// retries. + /// + /// # Example + /// + /// ``` + /// use pubnub::{Keyset, PubNubClientBuilder, RequestRetryConfiguration}; + /// + /// # fn main() -> Result<(), Box> { + /// let retry_configuration = RequestRetryConfiguration::default_exponential(); + /// let client = PubNubClientBuilder::with_reqwest_transport() + /// .with_keyset(Keyset { + /// publish_key: Some("pub-c-abc123"), + /// subscribe_key: "sub-c-abc123", + /// secret_key: None, + /// }) + /// .with_user_id("my-user-id") + /// .with_retry_configuration(retry_configuration) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn default_exponential() -> Self { + Self::Exponential { + min_delay: 2, + max_delay: 150, + max_retry: 6, + excluded_endpoints: None, + } + } + /// Check whether next retry `attempt` is allowed. - pub(crate) fn retriable(&self, attempt: &u8, error: Option<&PubNubError>) -> bool { - if self.reached_max_retry(attempt) { + /// + /// # Arguments + /// + /// * `path` - Optional path of the failed request. + /// * `attempt` - The attempt count of the request. + /// * `error` - An optional `PubNubError` representing the error response. + /// If `None`, the request cannot be retried. + /// + /// # Returns + /// + /// `true` if it is allowed to retry request one more time. + pub(crate) fn retriable( + &self, + path: Option, + attempt: &u8, + error: Option<&PubNubError>, + ) -> bool + where + S: Into, + { + if self.is_excluded_endpoint(path) + || self.reached_max_retry(attempt) + || matches!(self, RequestRetryConfiguration::None) + { return false; } error .and_then(|e| e.transport_response()) .map(|response| matches!(response.status, 429 | 500..=599)) - .unwrap_or(!matches!(self, RequestRetryPolicy::None)) + .unwrap_or(false) } - /// Check whether reached maximum retry count or not. - pub(crate) fn reached_max_retry(&self, attempt: &u8) -> bool { - match self { - Self::Linear { max_retry, .. } | Self::Exponential { max_retry, .. } => { - attempt.gt(max_retry) - } - _ => false, - } - } - - #[cfg(feature = "std")] - pub(crate) fn retry_delay(&self, attempt: &u8, error: Option<&PubNubError>) -> Option { - if !self.retriable(attempt, error) { + /// Calculate the delay before retrying a request. + /// + /// If the request can be retried based on the given attempt count and + /// error, the delay is calculated based on the error response status code. + /// - If the status code is 429 (Too Many Requests), the delay is determined + /// by the `retry-after` header in the response, if present. + /// - If the status code is in the range 500-599 (Server Error), the delay + /// is calculated based on the configured retry strategy. + /// + /// # Arguments + /// + /// * `path` - Optional path of the failed request. + /// * `attempt` - The attempt count of the request. + /// * `error` - An optional `PubNubError` representing the error response. + /// If `None`, the request cannot be retried. + /// + /// # Returns + /// + /// An optional `u64` representing the delay in microseconds before retrying + /// the request. `None` if the request should not be retried. + pub(crate) fn retry_delay( + &self, + path: Option, + attempt: &u8, + error: Option<&PubNubError>, + ) -> Option { + if !self.retriable(path, attempt, error) { return None; } @@ -71,10 +230,12 @@ impl RequestRetryPolicy { .and_then(|err| err.transport_response()) .map(|response| match response.status { // Respect service requested delay. - 429 => (!matches!(self, Self::None)) - .then(|| response.headers.get("retry-after")) - .flatten() - .and_then(|value| value.parse::().ok()), + 429 if response.headers.contains_key("retry-after") => { + (!matches!(self, Self::None)) + .then(|| response.headers.get("retry-after")) + .flatten() + .and_then(|value| value.parse::().ok()) + } 500..=599 => match self { Self::None => None, Self::Linear { delay, .. } => Some(*delay), @@ -82,25 +243,112 @@ impl RequestRetryPolicy { min_delay, max_delay, .. - } => Some((*min_delay).pow((*attempt).into()).min(*max_delay)), + } => Some((*min_delay * 2_u64.pow((*attempt - 1) as u32)).min(*max_delay)), }, _ => None, }) + .map(Self::delay_in_microseconds) .unwrap_or(None) } - #[cfg(not(feature = "std"))] - pub(crate) fn retry_delay(&self, _attempt: &u8, _error: Option<&PubNubError>) -> Option { - None + /// Check whether failed endpoint has been excluded or not. + /// + /// # Arguments + /// + /// * `path` - Optional path of the failed request. + /// + /// # Returns + /// + /// `true` in case if endpoint excluded from retry list or no path passed. + fn is_excluded_endpoint(&self, path: Option) -> bool + where + S: Into, + { + let Some(path) = path.map(|p| Endpoint::from(p.into())) else { + return false; + }; + + let Some(excluded_endpoints) = (match self { + Self::Linear { + excluded_endpoints, .. + } + | Self::Exponential { + excluded_endpoints, .. + } => excluded_endpoints, + _ => &None, + }) else { + return false; + }; + + excluded_endpoints.contains(&path) + } + + /// Check whether reached maximum retry count or not. + fn reached_max_retry(&self, attempt: &u8) -> bool { + match self { + Self::Linear { max_retry, .. } | Self::Exponential { max_retry, .. } => { + attempt.gt(max_retry) + } + _ => false, + } + } + + /// Calculates the delay in microseconds given a delay in seconds. + /// + /// # Arguments + /// + /// * `delay_in_seconds` - The delay in seconds. If `None`, returns `None`. + /// + /// # Returns + /// + /// * `Some(delay_in_microseconds)` - The delay in microseconds. + /// * `None` - If `delay_in_seconds` is `None`. + fn delay_in_microseconds(delay_in_seconds: Option) -> Option { + let Some(delay_in_seconds) = delay_in_seconds else { + return None; + }; + + const MICROS_IN_SECOND: u64 = 1_000_000; + let delay = delay_in_seconds * MICROS_IN_SECOND; + let mut random_bytes = [0u8; 8]; + + if getrandom(&mut random_bytes).is_err() { + return Some(delay); + } + + Some(delay + u64::from_be_bytes(random_bytes) % MICROS_IN_SECOND) } } -impl Default for RequestRetryPolicy { +impl Default for RequestRetryConfiguration { fn default() -> Self { Self::None } } +impl From for Endpoint { + fn from(value: String) -> Self { + match value.as_str() { + path if path.starts_with("/v2/subscribe") => Endpoint::Subscribe, + path if path.starts_with("/publish/") || path.starts_with("/signal/") => { + Endpoint::MessageSend + } + path if path.starts_with("/v2/presence") => Endpoint::Presence, + path if path.starts_with("/v2/history/") || path.starts_with("/v3/history/") => { + Endpoint::MessageStorage + } + path if path.starts_with("/v1/message-actions/") => Endpoint::MessageReactions, + path if path.starts_with("/v1/channel-registration/") => Endpoint::ChannelGroups, + path if path.starts_with("/v2/objects/") => Endpoint::AppContext, + path if path.starts_with("/v1/push/") || path.starts_with("/v2/push/") => { + Endpoint::DevicePushNotifications + } + path if path.starts_with("/v1/files/") => Endpoint::Files, + _ => Endpoint::Unknown, + } + } +} + #[cfg(test)] mod should { use super::*; @@ -119,7 +367,7 @@ mod should { fn too_many_requests_error_response() -> TransportResponse { TransportResponse { status: 429, - headers: HashMap::from([("retry-after".into(), "150".into())]), + headers: HashMap::from([(String::from("retry-after"), String::from("150"))]), ..Default::default() } } @@ -131,10 +379,29 @@ mod should { } } + fn is_equal_with_accuracy(lhv: Option, rhv: Option) -> bool { + if lhv.is_none() && rhv.is_none() { + return true; + } + + let Some(lhv) = lhv else { return false }; + let Some(rhv) = rhv else { return false }; + + if !(rhv * 1_000_000..=rhv * 1_000_000 + 999_999).contains(&lhv) { + panic!( + "{lhv} is not within expected range {}..{}", + rhv * 1_000_000, + rhv * 1_000_000 + 999_999 + ) + } + + true + } + #[test] fn create_none_by_default() { - let policy: RequestRetryPolicy = Default::default(); - assert!(matches!(policy, RequestRetryPolicy::None)); + let policy: RequestRetryConfiguration = Default::default(); + assert!(matches!(policy, RequestRetryConfiguration::None)); } mod none_policy { @@ -143,7 +410,8 @@ mod should { #[test] fn return_none_delay_for_client_error_response() { assert_eq!( - RequestRetryPolicy::None.retry_delay( + RequestRetryConfiguration::None.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -158,7 +426,8 @@ mod should { #[test] fn return_none_delay_for_server_error_response() { assert_eq!( - RequestRetryPolicy::None.retry_delay( + RequestRetryConfiguration::None.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -173,7 +442,8 @@ mod should { #[test] fn return_none_delay_for_too_many_requests_error_response() { assert_eq!( - RequestRetryPolicy::None.retry_delay( + RequestRetryConfiguration::None.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -191,13 +461,15 @@ mod should { #[test] fn return_none_delay_for_client_error_response() { - let policy = RequestRetryPolicy::Linear { + let policy = RequestRetryConfiguration::Linear { delay: 10, max_retry: 5, + excluded_endpoints: None, }; assert_eq!( policy.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -212,13 +484,15 @@ mod should { #[test] fn return_same_delay_for_server_error_response() { let expected_delay: u64 = 10; - let policy = RequestRetryPolicy::Linear { + let policy = RequestRetryConfiguration::Linear { delay: expected_delay, max_retry: 5, + excluded_endpoints: None, }; - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -227,10 +501,11 @@ mod should { )) ), Some(expected_delay) - ); + )); - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &2, Some(&PubNubError::general_api_error( "test", @@ -239,19 +514,21 @@ mod should { )) ), Some(expected_delay) - ); + )); } #[test] fn return_none_delay_when_reach_max_retry_for_server_error_response() { let expected_delay: u64 = 10; - let policy = RequestRetryPolicy::Linear { + let policy = RequestRetryConfiguration::Linear { delay: expected_delay, max_retry: 3, + excluded_endpoints: None, }; - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &2, Some(&PubNubError::general_api_error( "test", @@ -260,10 +537,11 @@ mod should { )) ), Some(expected_delay) - ); + )); assert_eq!( policy.retry_delay( + None, &4, Some(&PubNubError::general_api_error( "test", @@ -277,14 +555,16 @@ mod should { #[test] fn return_service_delay_for_too_many_requests_error_response() { - let policy = RequestRetryPolicy::Linear { + let policy = RequestRetryConfiguration::Linear { delay: 10, max_retry: 2, + excluded_endpoints: None, }; // 150 is from 'server_error_response' `Retry-After` header. - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &2, Some(&PubNubError::general_api_error( "test", @@ -293,7 +573,7 @@ mod should { )) ), Some(150) - ); + )); } } @@ -303,14 +583,16 @@ mod should { #[test] fn return_none_delay_for_client_error_response() { let expected_delay = 8; - let policy = RequestRetryPolicy::Exponential { + let policy = RequestRetryConfiguration::Exponential { min_delay: expected_delay, max_delay: 100, max_retry: 2, + excluded_endpoints: None, }; assert_eq!( policy.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -325,14 +607,16 @@ mod should { #[test] fn return_exponential_delay_for_server_error_response() { let expected_delay = 8; - let policy = RequestRetryPolicy::Exponential { + let policy = RequestRetryConfiguration::Exponential { min_delay: expected_delay, max_delay: 100, max_retry: 3, + excluded_endpoints: None, }; - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -341,10 +625,11 @@ mod should { )) ), Some(expected_delay) - ); + )); - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &2, Some(&PubNubError::general_api_error( "test", @@ -352,21 +637,23 @@ mod should { Some(Box::new(server_error_response())) )) ), - Some(expected_delay.pow(2)) - ); + Some(expected_delay * 2_u64.pow(2 - 1)) + )); } #[test] fn return_none_delay_when_reach_max_retry_for_server_error_response() { let expected_delay = 8; - let policy = RequestRetryPolicy::Exponential { + let policy = RequestRetryConfiguration::Exponential { min_delay: expected_delay, max_delay: 100, max_retry: 3, + excluded_endpoints: None, }; - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &2, Some(&PubNubError::general_api_error( "test", @@ -374,11 +661,12 @@ mod should { Some(Box::new(server_error_response())) )) ), - Some(expected_delay.pow(2)) - ); + Some(expected_delay * 2_u64.pow(2 - 1)) + )); assert_eq!( policy.retry_delay( + None, &4, Some(&PubNubError::general_api_error( "test", @@ -394,14 +682,16 @@ mod should { fn return_max_delay_when_reach_max_value_for_server_error_response() { let expected_delay = 8; let max_delay = 50; - let policy = RequestRetryPolicy::Exponential { + let policy = RequestRetryConfiguration::Exponential { min_delay: expected_delay, max_delay, max_retry: 5, + excluded_endpoints: None, }; - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &1, Some(&PubNubError::general_api_error( "test", @@ -410,11 +700,12 @@ mod should { )) ), Some(expected_delay) - ); + )); - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( - &2, + None, + &4, Some(&PubNubError::general_api_error( "test", None, @@ -422,20 +713,22 @@ mod should { )) ), Some(max_delay) - ); + )); } #[test] fn return_service_delay_for_too_many_requests_error_response() { - let policy = RequestRetryPolicy::Exponential { + let policy = RequestRetryConfiguration::Exponential { min_delay: 10, max_delay: 100, max_retry: 2, + excluded_endpoints: None, }; // 150 is from 'server_error_response' `Retry-After` header. - assert_eq!( + assert!(is_equal_with_accuracy( policy.retry_delay( + None, &2, Some(&PubNubError::general_api_error( "test", @@ -444,7 +737,7 @@ mod should { )) ), Some(150) - ); + )); } } } diff --git a/src/core/runtime.rs b/src/core/runtime.rs index 43864c7b..b3f8efa4 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -36,6 +36,10 @@ use futures::future::{BoxFuture, FutureExt}; /// async fn sleep(self, _delay: u64) { /// // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await /// } +/// +/// async fn sleep_microseconds(self, _delay: u64) { +/// // e.g. tokio::time::sleep(tokio::time::Duration::from_micros(delay)).await +/// } /// } /// ``` #[async_trait::async_trait] @@ -51,12 +55,18 @@ pub trait Runtime: Clone + Send { /// /// Sleep current task for specified amount of time (in seconds). async fn sleep(self, delay: u64); + + /// Put current task to "sleep". + /// + /// Sleep current task for specified amount of time (in microseconds). + async fn sleep_microseconds(self, delay: u64); } #[derive(Clone)] pub(crate) struct RuntimeSupport { spawner: Arc) + Send + Sync>, sleeper: Arc BoxFuture<'static, ()> + Send + Sync>, + sleeper_microseconds: Arc BoxFuture<'static, ()> + Send + Sync>, } impl RuntimeSupport { @@ -66,9 +76,13 @@ impl RuntimeSupport { { let spawn_runtime = runtime.clone(); let sleep_runtime = runtime.clone(); + let sleep_microseconds_runtime = runtime.clone(); Self { sleeper: Arc::new(move |delay| sleep_runtime.sleep(delay).boxed()), + sleeper_microseconds: Arc::new(move |delay| { + sleep_microseconds_runtime.sleep_microseconds(delay).boxed() + }), spawner: Arc::new(Box::new(move |future| { spawn_runtime.spawn(future); })), @@ -93,6 +107,10 @@ impl Runtime for RuntimeSupport { async fn sleep(self, delay: u64) { (self.sleeper)(delay).await } + + async fn sleep_microseconds(self, delay: u64) { + (self.sleeper_microseconds)(delay).await + } } impl Debug for RuntimeSupport { diff --git a/src/core/serialize.rs b/src/core/serialize.rs index 4f916ca9..3dfff9e8 100644 --- a/src/core/serialize.rs +++ b/src/core/serialize.rs @@ -26,7 +26,7 @@ use crate::lib::alloc::vec::Vec; /// } /// /// impl Serialize for Foo { -/// fn serialize(self) -> Result, PubNubError> { +/// fn serialize(&self) -> Result, PubNubError> { /// Ok(format!("{{\"bar\":\"{}\"}}", self.bar).into_bytes()) /// } /// } @@ -40,7 +40,8 @@ pub trait Serialize { /// Serialize the value /// /// # Errors - /// Should return an [`PubNubError::SerializeError`] if the value cannot be serialized. + /// Should return an [`PubNubError::SerializeError`] if the value cannot be + /// serialized. /// /// # Examples /// ``` @@ -49,12 +50,12 @@ pub trait Serialize { /// struct Foo; /// /// impl Serialize for Foo { - /// fn serialize(self) -> Result, PubNubError> { + /// fn serialize(&self) -> Result, PubNubError> { /// Ok(vec![1, 2, 3]) /// } /// } /// ``` /// /// [`PubNubError::SerializeError`]: ../error/enum.PubNubError.html#variant.SerializeError - fn serialize(self) -> Result, PubNubError>; + fn serialize(&self) -> Result, PubNubError>; } diff --git a/src/core/transport_request.rs b/src/core/transport_request.rs index 7fc4d47a..bcd918f4 100644 --- a/src/core/transport_request.rs +++ b/src/core/transport_request.rs @@ -20,6 +20,9 @@ use crate::{ }, }; +#[cfg(feature = "std")] +use crate::core::{runtime::RuntimeSupport, RequestRetryConfiguration, Runtime}; + type DeserializerClosure = Box Result>; /// The method to use for a request. @@ -80,6 +83,10 @@ pub struct TransportRequest { /// body to be sent with the request pub body: Option>, + + /// request timeout + #[cfg(feature = "std")] + pub timeout: u64, } impl TransportRequest { @@ -91,6 +98,8 @@ impl TransportRequest { &self, transport: &T, deserializer: Arc, + #[cfg(feature = "std")] retry_configuration: RequestRetryConfiguration, + #[cfg(feature = "std")] runtime: RuntimeSupport, ) -> Result where B: for<'de> super::Deserialize<'de>, @@ -98,12 +107,56 @@ impl TransportRequest { T: super::Transport, D: super::Deserializer + 'static, { - // Request configured endpoint. - let response = transport.send(self.clone()).await?; - Self::deserialize( - response.clone(), - Box::new(move |bytes| deserializer.deserialize(bytes)), - ) + #[cfg(feature = "std")] + { + let mut last_result; + let mut retry_attempt = 0_u8; + + loop { + let deserializer_clone = deserializer.clone(); + + // Request configured endpoint. + let response = transport.send(self.clone()).await; + last_result = Self::deserialize( + response?.clone(), + Box::new(move |bytes| deserializer_clone.deserialize(bytes)), + ); + + let Err(error) = last_result.as_ref() else { + break; + }; + + // Subscribe and heartbeat handled by Event Engine. + if self.path.starts_with("/v2/subscribe") + || (self.path.starts_with("/v2/presence") && self.path.contains("/heartbeat")) + { + break; + } + + if let Some(delay) = retry_configuration.retry_delay( + Some(self.path.clone()), + &retry_attempt, + Some(error), + ) { + retry_attempt += 1; + runtime.clone().sleep_microseconds(delay).await; + } else { + break; + } + } + + last_result + } + + #[cfg(not(feature = "std"))] + { + // Request configured endpoint. + let response = transport.send(self.clone()).await; + Self::deserialize( + response?.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } } /// Send async request and process [`PubNub API`] response. @@ -114,21 +167,67 @@ impl TransportRequest { &self, transport: &T, deserializer: Arc, + #[cfg(feature = "std")] retry_configuration: &RequestRetryConfiguration, + #[cfg(feature = "std")] runtime: &RuntimeSupport, ) -> Result where B: for<'de> serde::Deserialize<'de>, R: TryFrom, - T: super::Transport, + T: super::Transport + 'static, D: super::Deserializer + 'static, { - // Request configured endpoint. - let response = transport.send(self.clone()).await?; + #[cfg(feature = "std")] + { + let mut last_result; + let mut retry_attempt = 0_u8; - Self::deserialize( - response.clone(), - Box::new(move |bytes| deserializer.deserialize(bytes)), - ) + loop { + let deserializer_clone = deserializer.clone(); + + // Request configured endpoint. + let response = transport.send(self.clone()).await; + last_result = Self::deserialize( + response?.clone(), + Box::new(move |bytes| deserializer_clone.deserialize(bytes)), + ); + + let Err(error) = last_result.as_ref() else { + break; + }; + + // Subscribe and heartbeat handled by Event Engine. + if self.path.starts_with("/v2/subscribe") + || (self.path.starts_with("/v2/presence") && self.path.contains("/heartbeat")) + { + break; + } + + if let Some(delay) = retry_configuration.retry_delay( + Some(self.path.clone()), + &retry_attempt, + Some(error), + ) { + retry_attempt += 1; + runtime.clone().sleep_microseconds(delay).await; + } else { + break; + } + } + + last_result + } + + #[cfg(not(feature = "std"))] + { + // Request configured endpoint. + let response = transport.send(self.clone()).await; + Self::deserialize( + response?.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } } + /// Send async request and process [`PubNub API`] response. /// /// [`PubNub API`]: https://www.pubnub.com/docs diff --git a/src/core/user_metadata.rs b/src/core/user_metadata.rs new file mode 100644 index 00000000..613da256 --- /dev/null +++ b/src/core/user_metadata.rs @@ -0,0 +1,215 @@ +//! # UserMetadata entity module +//! +//! This module contains the [`UserMetadata`] type, which can be used as a +//! first-class citizen to access the [`PubNub API`]. +//! +//! [`PubNub API`]: https://www.pubnub.com/docs + +#[cfg(all(feature = "subscribe", feature = "std"))] +use spin::RwLock; + +use crate::{ + core::PubNubEntity, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{string::String, sync::Arc}, + core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, +}; + +#[cfg(all(feature = "subscribe", feature = "std"))] +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::{sync::Weak, vec, vec::Vec}, + subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, +}; + +/// User metadata entity. +/// +/// Entity as a first-class citizen provides access to the entity-specific API. +pub struct UserMetadata { + inner: Arc>, +} + +/// User metadata entity reference. +/// +/// This struct contains the actual User metadata state. It is wrapped in an +/// Arc by [`UserMetadata`] and uses internal mutability for its internal +/// state. +/// +/// Not intended to be used directly. Use [`UserMetadata`] instead. +#[derive(Debug)] +pub struct UserMetadataRef { + /// Reference on backing [`PubNubClientInstance`] client. + /// + /// Client is used to support entity-specific actions like: + /// * subscription + /// + /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. + client: Arc>, + + /// Unique user metadata object identifier. + /// + /// User metadata object identifier used by the [`PubNub API`] as unique + /// resources on which a certain operation should be performed. + /// + /// [`PubNub API`]: https://pubnub.com/docs + pub id: String, + + /// Active subscriptions count. + /// + /// Track number of [`Subscription`] which use this entity to receive + /// real-time updates. + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock, +} + +impl UserMetadata { + /// Creates a new instance of an user metadata object. + /// + /// # Arguments + /// + /// * `client` - The client instance used to access [`PubNub API`]. + /// * `id` - The identifier of the user metadata object. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) fn new(client: &PubNubClientInstance, id: S) -> UserMetadata + where + S: Into, + { + Self { + inner: Arc::new(UserMetadataRef { + client: Arc::new(client.clone()), + id: id.into(), + #[cfg(all(feature = "subscribe", feature = "std"))] + subscriptions_count: RwLock::new(0), + }), + } + } + + /// Increase the subscriptions count. + /// + /// Increments the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn increase_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + *subscriptions_count_slot += 1; + } + + /// Decrease the subscriptions count. + /// + /// Decrements the value of the subscriptions count by 1. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// > As long as entity used by at least one subscription it can't be + /// > removed from subscription + /// loop. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn decrease_subscriptions_count(&self) { + let mut subscriptions_count_slot = self.subscriptions_count.write(); + if *subscriptions_count_slot > 0 { + *subscriptions_count_slot -= 1; + } + } + + /// Current count of subscriptions. + /// + /// > This function is only available when both the `subscribe` and `std` + /// > features are enabled. + /// + /// # Returns + /// + /// Returns the current count of subscriptions. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn subscriptions_count(&self) -> usize { + *self.subscriptions_count.read() + } +} + +impl Deref for UserMetadata { + type Target = UserMetadataRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for UserMetadata { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the UserMetadata are not allowed") + } +} + +impl Clone for UserMetadata { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl PartialEq for UserMetadata { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl From> for PubNubEntity { + fn from(value: UserMetadata) -> Self { + PubNubEntity::UserMetadata(value) + } +} + +impl Debug for UserMetadata { + #[cfg(all(feature = "subscribe", feature = "std"))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "UserMetadata {{ id: {}, subscriptions_count: {} }}", + self.id, + self.subscriptions_count() + ) + } + + #[cfg(not(all(feature = "subscribe", feature = "std")))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "UserMetadata {{ id: {} }}", self.id) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscribable for UserMetadata { + fn names(&self, _presence: bool) -> Vec { + vec![self.id.clone()] + } + + fn r#type(&self) -> SubscribableType { + SubscribableType::Channel + } + + fn client(&self) -> Weak> { + Arc::downgrade(&self.client) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscriber for UserMetadata +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscription(&self, options: Option>) -> Subscription { + Subscription::new(self.client(), self.clone().into(), options) + } +} diff --git a/src/dx/access/builders/grant_token.rs b/src/dx/access/builders/grant_token.rs index ed088fe6..0d2f3af7 100644 --- a/src/dx/access/builders/grant_token.rs +++ b/src/dx/access/builders/grant_token.rs @@ -108,16 +108,18 @@ where { /// Create transport request from the request builder. pub(in crate::dx::access) fn transport_request(&self) -> TransportRequest { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let payload = GrantTokenPayload::new(self); let body = self.serializer.serialize(&payload).unwrap_or(vec![]); TransportRequest { - path: format!("/v3/pam/{}/grant", sub_key), + path: format!("/v3/pam/{}/grant", &config.subscribe_key), query_parameters: Default::default(), method: TransportMethod::Post, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: if !body.is_empty() { Some(body) } else { None }, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, } } } @@ -137,7 +139,7 @@ where impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where - T: Transport, + T: Transport + 'static, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, D: Deserializer + 'static, { @@ -150,8 +152,16 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } } @@ -188,9 +198,9 @@ where /// .grant_token(10) /// .resources(&[permissions::channel("test-channel").read().write()]) /// .meta(HashMap::from([ - /// ("role".into(), "administrator".into()), - /// ("access-duration".into(), 2800.into()), - /// ("ping-interval".into(), 1754.88.into()), + /// ("role".to_string(), "administrator".into()), + /// ("access-duration".to_string(), 2800.into()), + /// ("ping-interval".to_string(), 1754.88.into()), /// ])) /// .execute_blocking()?; /// # Ok(()) diff --git a/src/dx/access/builders/revoke.rs b/src/dx/access/builders/revoke.rs index e63e399f..87fd3b0a 100644 --- a/src/dx/access/builders/revoke.rs +++ b/src/dx/access/builders/revoke.rs @@ -47,15 +47,18 @@ pub struct RevokeTokenRequest { impl RevokeTokenRequest { /// Create transport request from the request builder. pub(in crate::dx::access) fn transport_request(&self) -> TransportRequest { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; TransportRequest { path: format!( - "/v3/pam/{sub_key}/grant/{}", + "/v3/pam/{}/grant/{}", + &config.subscribe_key, url_encode(self.token.as_bytes()) ), method: TransportMethod::Delete, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, ..Default::default() } } @@ -73,7 +76,7 @@ impl RevokeTokenRequestBuilder { impl RevokeTokenRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -86,8 +89,16 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } } diff --git a/src/dx/access/mod.rs b/src/dx/access/mod.rs index 1f666ffe..03bf7f35 100644 --- a/src/dx/access/mod.rs +++ b/src/dx/access/mod.rs @@ -109,8 +109,8 @@ impl PubNubClientInstance { /// } /// } /// - /// impl<'de> Deserializer<'de, GrantTokenResponseBody> for MyDeserializer { - /// fn deserialize(&self, response: &'de [u8]) -> Result { + /// impl<'de> Deserializer for MyDeserializer { + /// fn deserialize(&self, response: &[u8]) -> Result { /// // ... /// # Ok(GrantTokenResult { token: "".into() }) /// } @@ -131,7 +131,7 @@ impl PubNubClientInstance { /// pubnub /// .grant_token(10) /// .serialize_with(MySerializer) - /// .derialize_with(MyDeserializer) + /// .deserialize_with(MyDeserializer) /// .resources(&[permissions::channel("test-channel").read().write()]) /// .meta(HashMap::from([ /// ("role".into(), "administrator".into()), diff --git a/src/dx/parse_token.rs b/src/dx/parse_token.rs index 469d2f60..10fc9428 100644 --- a/src/dx/parse_token.rs +++ b/src/dx/parse_token.rs @@ -73,7 +73,6 @@ pub enum Token { /// permissions. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[allow(dead_code)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct TokenV2 { /// Access token version (version 2). @@ -107,7 +106,6 @@ pub struct TokenV2 { /// Typed resource permissions map. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[allow(dead_code)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct TokenResources { /// `Channel`-based endpoints permission map between channel name / regexp @@ -148,7 +146,6 @@ impl From for ResourcePermissions { #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[cfg_attr(test, derive(PartialEq, Eq))] -#[allow(dead_code)] #[cfg_attr(feature = "serde", serde(from = "u8"))] pub struct ResourcePermissions { /// Whether or not the resource has **read** permission. diff --git a/src/dx/presence/builders/get_presence_state.rs b/src/dx/presence/builders/get_presence_state.rs index e90d88c9..8cab7e4d 100644 --- a/src/dx/presence/builders/get_presence_state.rs +++ b/src/dx/presence/builders/get_presence_state.rs @@ -113,7 +113,7 @@ impl GetStateRequest { pub(in crate::dx::presence) fn transport_request( &self, ) -> Result { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let mut query: HashMap = HashMap::new(); // Serialize list of channel groups and add into query parameters list. @@ -122,21 +122,24 @@ impl GetStateRequest { Ok(TransportRequest { path: format!( - "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}", + "/v2/presence/sub-key/{}/channel/{}/uuid/{}", + &config.subscribe_key, url_encoded_channels(&self.channels), url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) ), query_parameters: query, method: TransportMethod::Get, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } impl GetStateRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -145,13 +148,20 @@ where let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } } -#[allow(dead_code)] #[cfg(feature = "blocking")] impl GetStateRequestBuilder where diff --git a/src/dx/presence/builders/heartbeat.rs b/src/dx/presence/builders/heartbeat.rs index 32d96dac..7169287e 100644 --- a/src/dx/presence/builders/heartbeat.rs +++ b/src/dx/presence/builders/heartbeat.rs @@ -1,15 +1,11 @@ //! # PubNub heartbeat module. //! -//! The [`HeartbeatRequestBuilder`] lets you to make and execute requests that -//! will announce specified `user_id` presence in the provided channels and -//! groups. +//! The [`HeartbeatRequestBuilder`] lets you make and execute requests that will +//! announce specified `user_id` presence in the provided channels and groups. use derive_builder::Builder; #[cfg(feature = "std")] -use futures::{ - future::BoxFuture, - {select_biased, FutureExt}, -}; +use futures::{future::BoxFuture, select_biased, FutureExt}; use crate::{ core::{ @@ -18,7 +14,7 @@ use crate::{ encoding::{url_encoded_channel_groups, url_encoded_channels}, headers::{APPLICATION_JSON, CONTENT_TYPE}, }, - Deserializer, PubNubError, Serialize, Transport, TransportMethod, TransportRequest, + Deserializer, PubNubError, Transport, TransportMethod, TransportRequest, }, dx::{ presence::{builders, HeartbeatResponseBody, HeartbeatResult}, @@ -81,20 +77,22 @@ pub struct HeartbeatRequest { /// A state that should be associated with the `user_id`. /// /// `state` object should be a `HashMap` with channel names as keys and - /// nested `HashMap` with values. State with heartbeat can be set **only** + /// serialized `state` as values. State with heartbeat can be set **only** /// for channels. /// /// # Example: /// ```rust,no_run /// # use std::collections::HashMap; - /// # fn main() { - /// let state = HashMap::>::from([( - /// "announce".into(), - /// HashMap::from([ - /// ("is_owner".into(), false), - /// ("is_admin".into(), true) - /// ]) + /// # use pubnub::core::Serialize; + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let state = HashMap::>::from([( + /// "announce".to_string(), + /// HashMap::::from([ + /// ("is_owner".to_string(), false), + /// ("is_admin".to_string(), true) + /// ]).serialize()? /// )]); + /// # Ok(()) /// # } /// ``` #[builder( @@ -108,11 +106,11 @@ pub struct HeartbeatRequest { /// /// A heartbeat is a period of time during which `user_id` is visible /// `online`. - /// If, within the heartbeat period, another heartbeat request or a - /// subscribe (for an implicit heartbeat) request `timeout` will be - /// announced for `user_id`. + /// If, within the heartbeat period, another heartbeat request or subscribe + /// (for an implicit heartbeat) request `timeout` will be announced for + /// `user_id`. /// - /// By default it is set to **300** seconds. + /// By default, it is set to **300** seconds. #[builder( field(vis = "pub(in crate::dx::presence)"), setter(strip_option), @@ -158,7 +156,7 @@ impl HeartbeatRequest { pub(in crate::dx::presence) fn transport_request( &self, ) -> Result { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let mut query: HashMap = HashMap::new(); query.insert("heartbeat".into(), self.heartbeat.to_string()); query.insert("uuid".into(), self.user_id.to_string()); @@ -167,23 +165,26 @@ impl HeartbeatRequest { url_encoded_channel_groups(&self.channel_groups) .and_then(|groups| query.insert("channel-group".into(), groups)); - if let Some(state) = &self.state { - let serialized_state = + if let Some(state) = self.state.as_ref() { + let state_json = String::from_utf8(state.clone()).map_err(|err| PubNubError::Serialization { details: err.to_string(), })?; - query.insert("state".into(), serialized_state); + query.insert("state".into(), state_json); } Ok(TransportRequest { path: format!( - "/v2/presence/sub_key/{sub_key}/channel/{}/heartbeat", + "/v2/presence/sub_key/{}/channel/{}/heartbeat", + &config.subscribe_key, url_encoded_channels(&self.channels) ), query_parameters: query, method: TransportMethod::Get, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } @@ -198,39 +199,38 @@ impl HeartbeatRequestBuilder { /// # Example: /// ```rust,no_run /// # use std::collections::HashMap; - /// # fn main() { - /// let state = HashMap::>::from([( - /// "announce".into(), - /// HashMap::from([ - /// ("is_owner".into(), false), - /// ("is_admin".into(), true) + /// # use pubnub::core::Serialize; + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let state: HashMap> = HashMap::from([( + /// "announce".to_string(), + /// HashMap::::from([ + /// ("is_owner".to_string(), false), + /// ("is_admin".to_string(), true) /// ]) /// )]); + /// # Ok(()) /// # } /// ``` - pub fn state(mut self, state: U) -> Self - where - U: Serialize + Send + Sync + 'static, - { - self.state = Some(state.serialize().ok()); - self - } + pub fn state(mut self, state: HashMap>) -> Self { + let mut serialized_state = vec![b'{']; + for (key, mut value) in state { + serialized_state.append(&mut format!("\"{}\":", key).as_bytes().to_vec()); + serialized_state.append(&mut value); + serialized_state.push(b','); + } + if serialized_state.last() == Some(&b',') { + serialized_state.pop(); + } + serialized_state.push(b'}'); - /// A state that should be associated with the `user_id`. - /// - /// The presence event engine has already pre-processed `state` object, - /// which can be passed to the builder as is. - #[cfg(all(feature = "presence", feature = "std"))] - pub(crate) fn state_serialized(mut self, state: Option>) -> Self { - self.state = Some(state); + self.state = Some(Some(serialized_state)); self } } -#[allow(dead_code)] impl HeartbeatRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -239,8 +239,16 @@ where let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } @@ -279,7 +287,6 @@ where } } -#[allow(dead_code)] #[cfg(feature = "blocking")] impl HeartbeatRequestBuilder where diff --git a/src/dx/presence/builders/here_now.rs b/src/dx/presence/builders/here_now.rs index cc6b4a7c..6d58381c 100644 --- a/src/dx/presence/builders/here_now.rs +++ b/src/dx/presence/builders/here_now.rs @@ -28,7 +28,8 @@ use crate::{ /// The Here Now request builder. /// -/// Allows you to build a Here Now request that is sent to the [`PubNub`] network. +/// Allows you to build a Here Now request that is sent to the [`PubNub`] +/// network. /// /// This struct is used by the [`here_now`] method of the [`PubNubClient`]. /// The [`here_now`] method is used to acquire information about the current @@ -64,7 +65,7 @@ pub struct HereNowRequest { )] pub(in crate::dx::presence) channel_groups: Vec, - /// Whether to include UUIDs of users subscribed to the channel(s). + /// Whether to include identifiers of users subscribed to the channel(s). #[builder( field(vis = "pub(in crate::dx::presence)"), setter(strip_option), @@ -72,7 +73,8 @@ pub struct HereNowRequest { )] pub(in crate::dx::presence) include_user_id: bool, - /// Whether to include state information of users subscribed to the channel(s). + /// Whether to include state information of users subscribed to the + /// channel(s). #[builder( field(vis = "pub(in crate::dx::presence)"), setter(strip_option), @@ -111,7 +113,7 @@ impl HereNowRequest { pub(in crate::dx::presence) fn transport_request( &self, ) -> Result { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let mut query: HashMap = HashMap::new(); // Serialize list of channel groups and add into query parameters list. @@ -127,20 +129,23 @@ impl HereNowRequest { Ok(TransportRequest { path: format!( - "/v2/presence/sub-key/{sub_key}/channel/{}", + "/v2/presence/sub-key/{}/channel/{}", + &config.subscribe_key, url_encoded_channels(&self.channels), ), query_parameters: query, method: TransportMethod::Get, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } impl HereNowRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -156,7 +161,14 @@ where let deserializer = client.deserializer.clone(); transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await .map(|mut result: HereNowResult| { name_replacement.is_some().then(|| { diff --git a/src/dx/presence/builders/leave.rs b/src/dx/presence/builders/leave.rs index 3c46140d..519d4b42 100644 --- a/src/dx/presence/builders/leave.rs +++ b/src/dx/presence/builders/leave.rs @@ -107,7 +107,7 @@ impl LeaveRequest { pub(in crate::dx::presence) fn transport_request( &self, ) -> Result { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let mut query: HashMap = HashMap::new(); query.insert("uuid".into(), self.user_id.to_string()); @@ -117,20 +117,23 @@ impl LeaveRequest { Ok(TransportRequest { path: format!( - "/v2/presence/sub_key/{sub_key}/channel/{}/leave", + "/v2/presence/sub_key/{}/channel/{}/leave", + &config.subscribe_key, url_encoded_channels(&self.channels) ), query_parameters: query, method: TransportMethod::Get, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } impl LeaveRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -139,13 +142,20 @@ where let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } } -#[allow(dead_code)] #[cfg(feature = "blocking")] impl LeaveRequestBuilder where diff --git a/src/dx/presence/builders/set_presence_state.rs b/src/dx/presence/builders/set_presence_state.rs index bf81041b..b3e8b9a1 100644 --- a/src/dx/presence/builders/set_presence_state.rs +++ b/src/dx/presence/builders/set_presence_state.rs @@ -6,6 +6,9 @@ use derive_builder::Builder; +#[cfg(feature = "std")] +use crate::lib::alloc::sync::Arc; + use crate::{ core::{ utils::{ @@ -35,6 +38,10 @@ use crate::{ }, }; +#[cfg(feature = "std")] +/// Request execution handling closure. +pub(crate) type SetStateExecuteCall = Arc, Option>)>; + /// The [`SetStateRequestBuilder`] is used to build `user_id` associated state /// update request that is sent to the [`PubNub`] network. /// @@ -83,16 +90,18 @@ pub struct SetStateRequest { /// # Example: /// ```rust,no_run /// # use std::collections::HashMap; + /// # use pubnub::core::Serialize; /// # fn main() { /// let state = HashMap::::from([ - /// ("is_owner".into(), false), - /// ("is_admin".into(), true) - /// ]); + /// ("is_owner".to_string(), false), + /// ("is_admin".to_string(), true) + /// ]).serialize(); /// # } /// ``` #[builder( field(vis = "pub(in crate::dx::presence)"), - setter(custom, strip_option) + setter(custom, strip_option), + default = "None" )] pub(in crate::dx::presence) state: Option>, @@ -100,6 +109,14 @@ pub struct SetStateRequest { /// Identifier for which `state` should be associated for provided list of /// channels and groups. pub(in crate::dx::presence) user_id: String, + + #[cfg(feature = "std")] + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(custom, strip_option) + )] + /// Set presence state request execution callback. + pub(in crate::dx::presence) on_execute: SetStateExecuteCall, } impl SetStateRequestBuilder { @@ -136,7 +153,7 @@ impl SetStateRequest { pub(in crate::dx::presence) fn transport_request( &self, ) -> Result { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let mut query: HashMap = HashMap::new(); // Serialize list of channel groups and add into query parameters list. @@ -153,36 +170,52 @@ impl SetStateRequest { Ok(TransportRequest { path: format!( - "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}/data", + "/v2/presence/sub-key/{}/channel/{}/uuid/{}/data", + &config.subscribe_key, url_encoded_channels(&self.channels), url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) ), query_parameters: query, method: TransportMethod::Get, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } impl SetStateRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. pub async fn execute(self) -> Result { let request = self.request()?; + + #[cfg(feature = "std")] + if !request.channels.is_empty() { + (request.on_execute)(request.channels.clone(), request.state.clone()); + } + let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } } -#[allow(dead_code)] #[cfg(feature = "blocking")] impl SetStateRequestBuilder where diff --git a/src/dx/presence/builders/where_now.rs b/src/dx/presence/builders/where_now.rs index 1c2201df..5d018f77 100644 --- a/src/dx/presence/builders/where_now.rs +++ b/src/dx/presence/builders/where_now.rs @@ -26,7 +26,8 @@ use crate::{ /// The Here Now request builder. /// -/// Allows you to build a Here Now request that is sent to the [`PubNub`] network. +/// Allows you to build a Here Now request that is sent to the [`PubNub`] +/// network. /// /// This struct is used by the [`here_now`] method of the [`PubNubClient`]. /// The [`here_now`] method is used to acquire information about the current @@ -77,7 +78,7 @@ impl WhereNowRequest { pub(in crate::dx::presence) fn transport_request( &self, ) -> Result { - let sub_key = &self.pubnub_client.config.subscribe_key; + let config = &self.pubnub_client.config; let user_id = if self.user_id.is_empty() { &*self.pubnub_client.config.user_id @@ -87,20 +88,23 @@ impl WhereNowRequest { Ok(TransportRequest { path: format!( - "/v2/presence/sub-key/{sub_key}/uuid/{}", + "/v2/presence/sub-key/{}/uuid/{}", + &config.subscribe_key, url_encode_extended(user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) ), query_parameters: HashMap::new(), method: TransportMethod::Get, - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } impl WhereNowRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -111,7 +115,14 @@ where let deserializer = client.deserializer.clone(); transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } } diff --git a/src/dx/presence/event_engine/effect_handler.rs b/src/dx/presence/event_engine/effect_handler.rs index e0b10a09..633043c8 100644 --- a/src/dx/presence/event_engine/effect_handler.rs +++ b/src/dx/presence/event_engine/effect_handler.rs @@ -4,9 +4,10 @@ //! event engine for use async_channel::Sender; +use uuid::Uuid; use crate::{ - core::{event_engine::EffectHandler, RequestRetryPolicy}, + core::{event_engine::EffectHandler, RequestRetryConfiguration}, lib::{ alloc::sync::Arc, core::fmt::{Debug, Formatter, Result}, @@ -35,7 +36,7 @@ pub(crate) struct PresenceEffectHandler { wait_call: Arc, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Cancellation channel. cancellation_channel: Sender, @@ -48,7 +49,7 @@ impl PresenceEffectHandler { delayed_heartbeat_call: Arc, leave_call: Arc, wait_call: Arc, - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, cancellation_channel: Sender, ) -> Self { Self { @@ -66,6 +67,7 @@ impl EffectHandler for PresenceEffectH fn create(&self, invocation: &PresenceEffectInvocation) -> Option { match invocation { PresenceEffectInvocation::Heartbeat { input } => Some(PresenceEffect::Heartbeat { + id: Uuid::new_v4().to_string(), input: input.clone(), executor: self.heartbeat_call.clone(), }), @@ -74,6 +76,7 @@ impl EffectHandler for PresenceEffectH attempts, reason, } => Some(PresenceEffect::DelayedHeartbeat { + id: Uuid::new_v4().to_string(), input: input.clone(), attempts: *attempts, reason: reason.clone(), @@ -82,10 +85,12 @@ impl EffectHandler for PresenceEffectH cancellation_channel: self.cancellation_channel.clone(), }), PresenceEffectInvocation::Leave { input } => Some(PresenceEffect::Leave { + id: Uuid::new_v4().to_string(), input: input.clone(), executor: self.leave_call.clone(), }), PresenceEffectInvocation::Wait { input } => Some(PresenceEffect::Wait { + id: Uuid::new_v4().to_string(), input: input.clone(), executor: self.wait_call.clone(), cancellation_channel: self.cancellation_channel.clone(), diff --git a/src/dx/presence/event_engine/effects/heartbeat.rs b/src/dx/presence/event_engine/effects/heartbeat.rs index dbc9943b..e987b76d 100644 --- a/src/dx/presence/event_engine/effects/heartbeat.rs +++ b/src/dx/presence/event_engine/effects/heartbeat.rs @@ -4,34 +4,29 @@ //! which is used to announce `user_id` presence on specified channels and //! groups. +use futures::TryFutureExt; +use log::info; + use crate::{ - core::{PubNubError, RequestRetryPolicy}, + core::{PubNubError, RequestRetryConfiguration}, lib::alloc::{sync::Arc, vec, vec::Vec}, presence::event_engine::{ - effects::HeartbeatEffectExecutor, - types::{PresenceInput, PresenceParameters}, - PresenceEvent, + effects::HeartbeatEffectExecutor, PresenceEvent, PresenceInput, PresenceParameters, }, }; -use futures::TryFutureExt; -use log::info; - #[allow(clippy::too_many_arguments)] pub(super) async fn execute( input: &PresenceInput, attempt: u8, reason: Option, effect_id: &str, - retry_policy: &Option, + retry_policy: &RequestRetryConfiguration, executor: &Arc, ) -> Vec { - if let Some(retry_policy) = retry_policy { - match reason { - Some(reason) if !retry_policy.retriable(&attempt, Some(&reason)) => { - return vec![PresenceEvent::HeartbeatGiveUp { reason }]; - } - _ => {} + if let Some(reason) = reason.clone() { + if !retry_policy.retriable(Some("/v2/presence"), &attempt, Some(&reason)) { + return vec![PresenceEvent::HeartbeatGiveUp { reason }]; } } @@ -86,7 +81,7 @@ mod it_should { 0, None, "id", - &Some(RequestRetryPolicy::None), + &RequestRetryConfiguration::None, &mocked_heartbeat_function, ) .await; @@ -127,7 +122,11 @@ mod it_should { })), }), "id", - &Some(RequestRetryPolicy::None), + &RequestRetryConfiguration::Linear { + max_retry: 5, + delay: 2, + excluded_endpoints: None, + }, &mocked_heartbeat_function, ) .await; @@ -168,10 +167,56 @@ mod it_should { })), }), "id", - &Some(RequestRetryPolicy::Linear { + &RequestRetryConfiguration::Linear { delay: 0, max_retry: 1, + excluded_endpoints: None, + }, + &mocked_heartbeat_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + PresenceEvent::HeartbeatGiveUp { .. } + )); + } + + #[tokio::test] + async fn return_heartbeat_give_up_event_on_error_with_none_auto_retry_policy() { + let mocked_heartbeat_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + 5, + Some(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), }), + "id", + &RequestRetryConfiguration::Linear { + delay: 0, + max_retry: 1, + excluded_endpoints: None, + }, &mocked_heartbeat_function, ) .await; diff --git a/src/dx/presence/event_engine/effects/leave.rs b/src/dx/presence/event_engine/effects/leave.rs index 55345322..d5c9cdf1 100644 --- a/src/dx/presence/event_engine/effects/leave.rs +++ b/src/dx/presence/event_engine/effects/leave.rs @@ -8,9 +8,7 @@ use log::info; use crate::{ lib::alloc::{sync::Arc, vec, vec::Vec}, presence::event_engine::{ - effects::LeaveEffectExecutor, - types::{PresenceInput, PresenceParameters}, - PresenceEvent, + effects::LeaveEffectExecutor, PresenceEvent, PresenceInput, PresenceParameters, }, }; diff --git a/src/dx/presence/event_engine/effects/mod.rs b/src/dx/presence/event_engine/effects/mod.rs index eeb60f79..4dceb748 100644 --- a/src/dx/presence/event_engine/effects/mod.rs +++ b/src/dx/presence/event_engine/effects/mod.rs @@ -6,17 +6,14 @@ use futures::future::BoxFuture; use crate::{ core::{ event_engine::{Effect, EffectInvocation}, - PubNubError, RequestRetryPolicy, + PubNubError, RequestRetryConfiguration, }, lib::{ alloc::{string::String, sync::Arc, vec::Vec}, core::fmt::{Debug, Formatter}, }, presence::{ - event_engine::{ - types::{PresenceInput, PresenceParameters}, - PresenceEffectInvocation, - }, + event_engine::{PresenceEffectInvocation, PresenceInput, PresenceParameters}, HeartbeatResult, LeaveResult, }, }; @@ -53,10 +50,12 @@ pub(in crate::dx::presence) type LeaveEffectExecutor = dyn Fn(PresenceParameters + Sync; /// Presence state machine effects. -#[allow(dead_code)] pub(crate) enum PresenceEffect { /// Heartbeat effect invocation. Heartbeat { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and groups for which `user_id` @@ -71,6 +70,9 @@ pub(crate) enum PresenceEffect { /// Delayed heartbeat effect invocation. DelayedHeartbeat { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and groups for which `user_id` @@ -86,7 +88,7 @@ pub(crate) enum PresenceEffect { reason: PubNubError, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Executor function. /// @@ -101,6 +103,9 @@ pub(crate) enum PresenceEffect { /// Leave effect invocation. Leave { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and groups for which `user_id` @@ -115,6 +120,9 @@ pub(crate) enum PresenceEffect { /// Delay effect invocation. Wait { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and groups for which `user_id` @@ -138,29 +146,25 @@ impl Debug for PresenceEffect { match self { Self::Heartbeat { input, .. } => write!( f, - "PresenceEffect::Heartbeat {{ channels: {:?}, channel groups: \ - {:?}}}", + "PresenceEffect::Heartbeat {{ channels: {:?}, channel groups: {:?}}}", input.channels(), input.channel_groups() ), Self::DelayedHeartbeat { input, .. } => write!( f, - "PresenceEffect::DelayedHeartbeat {{ channels: {:?}, channel groups: \ - {:?}}}", + "PresenceEffect::DelayedHeartbeat {{ channels: {:?}, channel groups: {:?}}}", input.channels(), input.channel_groups() ), Self::Leave { input, .. } => write!( f, - "PresenceEffect::Leave {{ channels: {:?}, channel groups: \ - {:?}}}", + "PresenceEffect::Leave {{ channels: {:?}, channel groups: {:?}}}", input.channels(), input.channel_groups() ), Self::Wait { input, .. } => write!( f, - "PresenceEffect::Wait {{ channels: {:?}, channel groups: \ - {:?}}}", + "PresenceEffect::Wait {{ channels: {:?}, channel groups: {:?}}}", input.channels(), input.channel_groups() ), @@ -173,6 +177,16 @@ impl Effect for PresenceEffect { type Invocation = PresenceEffectInvocation; fn id(&self) -> String { + match self { + Self::Heartbeat { id, .. } + | Self::DelayedHeartbeat { id, .. } + | Self::Leave { id, .. } + | Self::Wait { id, .. } => id, + } + .into() + } + + fn name(&self) -> String { match self { Self::Heartbeat { .. } => "HEARTBEAT", Self::DelayedHeartbeat { .. } => "DELAYED_HEARTBEAT", @@ -184,10 +198,23 @@ impl Effect for PresenceEffect { async fn run(&self) -> Vec<::Event> { match self { - Self::Heartbeat { input, executor } => { - heartbeat::execute(input, 0, None, &self.id(), &None, executor).await + Self::Heartbeat { + id, + input, + executor, + } => { + heartbeat::execute( + input, + 0, + None, + id, + &RequestRetryConfiguration::None, + executor, + ) + .await } Self::DelayedHeartbeat { + id, input, attempts, reason, @@ -199,29 +226,35 @@ impl Effect for PresenceEffect { input, *attempts, Some(reason.clone()), - &self.id(), - &Some(retry_policy.clone()), + id, + &retry_policy.clone(), executor, ) .await } - Self::Leave { input, executor } => leave::execute(input, &self.id(), executor).await, - Self::Wait { executor, .. } => wait::execute(&self.id(), executor).await, + Self::Leave { + id, + input, + executor, + } => leave::execute(input, id, executor).await, + Self::Wait { id, executor, .. } => wait::execute(id, executor).await, } } fn cancel(&self) { match self { PresenceEffect::DelayedHeartbeat { + id, cancellation_channel, .. } | PresenceEffect::Wait { + id, cancellation_channel, .. } => { cancellation_channel - .send_blocking(self.id()) + .send_blocking(id.clone()) .expect("Cancellation pipe is broken!"); } _ => { /* cannot cancel other effects */ } @@ -230,14 +263,16 @@ impl Effect for PresenceEffect { } #[cfg(test)] -mod should { +mod it_should { use super::*; + use uuid::Uuid; #[tokio::test] - async fn send_cancellation_notification() { + async fn send_wait_cancellation_wait_notification() { let (tx, rx) = async_channel::bounded(1); let effect = PresenceEffect::Wait { + id: Uuid::new_v4().to_string(), input: PresenceInput::new(&None, &None), executor: Arc::new(|_| Box::pin(async move { Ok(()) })), cancellation_channel: tx, @@ -246,4 +281,22 @@ mod should { effect.cancel(); assert_eq!(rx.recv().await.unwrap(), effect.id()) } + + #[tokio::test] + async fn send_delayed_heartbeat_cancellation_notification() { + let (tx, rx) = async_channel::bounded(1); + + let effect = PresenceEffect::DelayedHeartbeat { + id: Uuid::new_v4().to_string(), + input: PresenceInput::new(&None, &None), + attempts: 0, + reason: PubNubError::EffectCanceled, + retry_policy: Default::default(), + executor: Arc::new(|_| Box::pin(async move { Err(PubNubError::EffectCanceled) })), + cancellation_channel: tx, + }; + + effect.cancel(); + assert_eq!(rx.recv().await.unwrap(), effect.id()) + } } diff --git a/src/dx/presence/event_engine/event.rs b/src/dx/presence/event_engine/event.rs index 5e50fb1e..7addcec9 100644 --- a/src/dx/presence/event_engine/event.rs +++ b/src/dx/presence/event_engine/event.rs @@ -10,8 +10,10 @@ pub(crate) enum PresenceEvent { /// Announce join to channels and groups. /// /// Announce `user_id` presence on new channels and groups. - #[allow(dead_code)] Joined { + /// `user_id` presence announcement interval. + heartbeat_interval: u64, + /// Optional list of channels. /// /// List of channels for which `user_id` presence should be announced. @@ -27,8 +29,14 @@ pub(crate) enum PresenceEvent { /// Announce leave on channels and groups. /// /// Announce `user_id` leave from channels and groups. - #[allow(dead_code)] Left { + /// Whether `user_id` leave should be announced or not. + /// + /// When set to `true` and `user_id` will unsubscribe, the client + /// wouldn't announce `leave`, and as a result, there will be no + /// `leave` presence event generated. + suppress_leave_events: bool, + /// Optional list of channels. /// /// List of channels for which `user_id` should leave. @@ -43,13 +51,18 @@ pub(crate) enum PresenceEvent { /// Announce leave on all channels and groups. /// /// Announce `user_id` leave from all channels and groups. - #[allow(dead_code)] - LeftAll, + LeftAll { + /// Whether `user_id` leave should be announced or not. + /// + /// When set to `true` and `user_id` will unsubscribe, the client + /// wouldn't announce `leave`, and as a result, there will be no + /// `leave` presence event generated. + suppress_leave_events: bool, + }, /// Heartbeat completed successfully. /// /// Emitted when [`PubNub`] network returned `OK` response. - #[allow(dead_code)] HeartbeatSuccess, /// Heartbeat completed with an error. @@ -58,7 +71,6 @@ pub(crate) enum PresenceEvent { /// response from [`PubNub`] network (network or permission issues). /// /// [`PubNub`]: https://www.pubnub.com/ - #[allow(dead_code)] HeartbeatFailure { reason: PubNubError }, /// All heartbeat attempts was unsuccessful. @@ -66,27 +78,23 @@ pub(crate) enum PresenceEvent { /// Emitted when heartbeat attempts reached maximum allowed count (according /// to retry / reconnection policy) and all following attempts should be /// stopped. - #[allow(dead_code)] HeartbeatGiveUp { reason: PubNubError }, /// Restore heartbeating. /// /// Re-launch heartbeat event engine. - #[allow(dead_code)] Reconnect, /// Temporarily stop event engine. /// /// Suspend any delayed and waiting heartbeat endpoint calls till /// `Reconnect` event will be triggered again. - #[allow(dead_code)] Disconnect, /// Delay times up event. /// /// Emitted when `delay` reaches the end and should transit to the next /// state. - #[allow(dead_code)] TimesUp, } @@ -95,9 +103,9 @@ impl Event for PresenceEvent { match self { Self::Joined { .. } => "JOINED", Self::Left { .. } => "LEFT", - Self::LeftAll => "LEFT_ALL", + Self::LeftAll { .. } => "LEFT_ALL", Self::HeartbeatSuccess => "HEARTBEAT_SUCCESS", - Self::HeartbeatFailure { .. } => "HEARTBEAT_FAILED", + Self::HeartbeatFailure { .. } => "HEARTBEAT_FAILURE", Self::HeartbeatGiveUp { .. } => "HEARTBEAT_GIVEUP", Self::Reconnect => "RECONNECT", Self::Disconnect => "DISCONNECT", diff --git a/src/dx/presence/event_engine/invocation.rs b/src/dx/presence/event_engine/invocation.rs index c81e7696..611fdec3 100644 --- a/src/dx/presence/event_engine/invocation.rs +++ b/src/dx/presence/event_engine/invocation.rs @@ -10,7 +10,6 @@ use crate::{ }; #[derive(Debug)] -#[allow(dead_code)] pub(crate) enum PresenceEffectInvocation { /// Heartbeat effect invocation. Heartbeat { @@ -61,6 +60,9 @@ pub(crate) enum PresenceEffectInvocation { /// Cancel delay effect invocation. CancelWait, + + /// Terminate Presence Event Engine processing loop. + TerminateEventEngine, } impl EffectInvocation for PresenceEffectInvocation { @@ -75,14 +77,15 @@ impl EffectInvocation for PresenceEffectInvocation { Self::Leave { .. } => "LEAVE", Self::Wait { .. } => "WAIT", Self::CancelWait => "CANCEL_WAIT", + Self::TerminateEventEngine => "TERMINATE_EVENT_ENGINE", } } - fn managed(&self) -> bool { + fn is_managed(&self) -> bool { matches!(self, Self::Wait { .. } | Self::DelayedHeartbeat { .. }) } - fn cancelling(&self) -> bool { + fn is_cancelling(&self) -> bool { matches!(self, Self::CancelDelayedHeartbeat | Self::CancelWait) } @@ -92,6 +95,10 @@ impl EffectInvocation for PresenceEffectInvocation { || (matches!(effect, PresenceEffect::Wait { .. }) && matches!(self, Self::CancelWait { .. })) } + + fn is_terminating(&self) -> bool { + matches!(self, Self::TerminateEventEngine) + } } impl Display for PresenceEffectInvocation { @@ -103,6 +110,7 @@ impl Display for PresenceEffectInvocation { Self::Leave { .. } => write!(f, "LEAVE"), Self::Wait { .. } => write!(f, "WAIT"), Self::CancelWait => write!(f, "CANCEL_WAIT"), + Self::TerminateEventEngine => write!(f, "TERMINATE_EVENT_ENGINE"), } } } diff --git a/src/dx/presence/event_engine/mod.rs b/src/dx/presence/event_engine/mod.rs index 498e77c6..bf2bf122 100644 --- a/src/dx/presence/event_engine/mod.rs +++ b/src/dx/presence/event_engine/mod.rs @@ -3,7 +3,6 @@ use crate::core::event_engine::EventEngine; #[doc(inline)] -#[allow(unused_imports)] pub(crate) use effects::PresenceEffect; pub(crate) mod effects; @@ -12,24 +11,20 @@ pub(crate) use effect_handler::PresenceEffectHandler; pub(crate) mod effect_handler; #[doc(inline)] -#[allow(unused_imports)] pub(crate) use invocation::PresenceEffectInvocation; pub(crate) mod invocation; #[doc(inline)] -#[allow(unused_imports)] pub(crate) use event::PresenceEvent; pub(crate) mod event; #[doc(inline)] -#[allow(unused_imports)] pub(crate) use state::PresenceState; pub(crate) mod state; #[doc(inline)] -#[allow(unused_imports)] -pub(in crate::dx::presence) use types::{PresenceInput, PresenceParameters}; -pub(in crate::dx::presence) mod types; +pub(crate) use types::{PresenceInput, PresenceParameters}; +mod types; pub(crate) type PresenceEventEngine = EventEngine; diff --git a/src/dx/presence/event_engine/state.rs b/src/dx/presence/event_engine/state.rs index 6af5ecc3..076bb4c2 100644 --- a/src/dx/presence/event_engine/state.rs +++ b/src/dx/presence/event_engine/state.rs @@ -41,7 +41,7 @@ pub(crate) enum PresenceState { /// Cooling down state. /// - /// Heartbeating idle state in which it stay for configured amount of time. + /// Heartbeating idle state in which it stays for configured amount of time. Cooldown { /// User input with channels and groups. /// @@ -101,14 +101,19 @@ impl PresenceState { /// Handle `joined` event. fn presence_joined_transition( &self, + heartbeat_interval: u64, channels: &Option>, channel_groups: &Option>, ) -> Option> { + if heartbeat_interval == 0 { + return None; + } + let event_input = PresenceInput::new(channels, channel_groups); match self { Self::Inactive => { - Some(self.transition_to(Self::Heartbeating { input: event_input }, None)) + Some(self.transition_to(Some(Self::Heartbeating { input: event_input }), None)) } Self::Heartbeating { input } | Self::Cooldown { input } @@ -119,11 +124,11 @@ impl PresenceState { { let input = input.clone() + event_input; Some(self.transition_to( - if !matches!(self, Self::Stopped { .. }) { + Some(if !matches!(self, Self::Stopped { .. }) { Self::Heartbeating { input } } else { Self::Stopped { input } - }, + }), None, )) } @@ -134,13 +139,13 @@ impl PresenceState { /// Handle `left` event. fn presence_left_transition( &self, + suppress_leave_events: bool, channels: &Option>, channel_groups: &Option>, ) -> Option> { let event_input = PresenceInput::new(channels, channel_groups); match self { - Self::Inactive => Some(self.transition_to(Self::Stopped { input: event_input }, None)), Self::Heartbeating { input } | Self::Cooldown { input } | Self::Reconnecting { input, .. } @@ -152,7 +157,7 @@ impl PresenceState { (!channels_to_leave.is_empty).then(|| { self.transition_to( - if !channels_for_heartbeating.is_empty { + Some(if !channels_for_heartbeating.is_empty { if !matches!(self, Self::Stopped { .. }) { Self::Heartbeating { input: channels_for_heartbeating, @@ -164,29 +169,39 @@ impl PresenceState { } } else { Self::Inactive - }, - Some(vec![Leave { - input: channels_to_leave, - }]), + }), + (!matches!(self, Self::Stopped { .. }) && !suppress_leave_events).then( + || { + vec![Leave { + input: channels_to_leave, + }] + }, + ), ) }) } + _ => None, } } /// Handle `left all` event. - fn presence_left_all_transition(&self) -> Option> { + fn presence_left_all_transition( + &self, + suppress_leave_events: bool, + ) -> Option> { match self { Self::Heartbeating { input } | Self::Cooldown { input } | Self::Reconnecting { input, .. } | Self::Failed { input, .. } => Some(self.transition_to( - Self::Inactive, - Some(vec![Leave { - input: input.clone(), - }]), + Some(Self::Inactive), + (!suppress_leave_events).then(|| { + vec![Leave { + input: input.clone(), + }] + }), )), - Self::Stopped { .. } => Some(self.transition_to(Self::Inactive, None)), + Self::Stopped { .. } => Some(self.transition_to(Some(Self::Inactive), None)), _ => None, } } @@ -198,9 +213,9 @@ impl PresenceState { match self { Self::Heartbeating { input } | Self::Reconnecting { input, .. } => { Some(self.transition_to( - Self::Cooldown { + Some(Self::Cooldown { input: input.clone(), - }, + }), None, )) } @@ -213,23 +228,29 @@ impl PresenceState { &self, reason: &PubNubError, ) -> Option> { + // Request cancellation shouldn't cause any transition because there + // will be another event after this. + if matches!(reason, PubNubError::RequestCancel { .. }) { + return None; + } + match self { Self::Heartbeating { input } => Some(self.transition_to( - Self::Reconnecting { + Some(Self::Reconnecting { input: input.clone(), attempts: 1, reason: reason.clone(), - }, + }), None, )), Self::Reconnecting { input, attempts, .. } => Some(self.transition_to( - Self::Reconnecting { + Some(Self::Reconnecting { input: input.clone(), attempts: attempts + 1, reason: reason.clone(), - }, + }), None, )), _ => None, @@ -243,10 +264,10 @@ impl PresenceState { ) -> Option> { match self { Self::Reconnecting { input, .. } => Some(self.transition_to( - Self::Failed { + Some(Self::Failed { input: input.clone(), reason: reason.clone(), - }, + }), None, )), _ => None, @@ -257,9 +278,9 @@ impl PresenceState { fn presence_reconnect_transition(&self) -> Option> { match self { Self::Stopped { input } | Self::Failed { input, .. } => Some(self.transition_to( - Self::Heartbeating { + Some(Self::Heartbeating { input: input.clone(), - }, + }), None, )), _ => None, @@ -273,9 +294,9 @@ impl PresenceState { | Self::Cooldown { input } | Self::Reconnecting { input, .. } | Self::Failed { input, .. } => Some(self.transition_to( - Self::Stopped { + Some(Self::Stopped { input: input.clone(), - }, + }), Some(vec![Leave { input: input.clone(), }]), @@ -288,9 +309,9 @@ impl PresenceState { fn presence_times_up_transition(&self) -> Option> { match self { Self::Cooldown { input } => Some(self.transition_to( - Self::Heartbeating { + Some(Self::Heartbeating { input: input.clone(), - }, + }), None, )), _ => None, @@ -338,14 +359,18 @@ impl State for PresenceState { ) -> Option> { match event { PresenceEvent::Joined { + heartbeat_interval, channels, channel_groups, - } => self.presence_joined_transition(channels, channel_groups), + } => self.presence_joined_transition(*heartbeat_interval, channels, channel_groups), PresenceEvent::Left { + suppress_leave_events, channels, channel_groups, - } => self.presence_left_transition(channels, channel_groups), - PresenceEvent::LeftAll => self.presence_left_all_transition(), + } => self.presence_left_transition(*suppress_leave_events, channels, channel_groups), + PresenceEvent::LeftAll { + suppress_leave_events, + } => self.presence_left_all_transition(*suppress_leave_events), PresenceEvent::HeartbeatSuccess => self.presence_heartbeat_success_transition(), PresenceEvent::HeartbeatFailure { reason } => { self.presence_heartbeat_failed_transition(reason) @@ -361,19 +386,23 @@ impl State for PresenceState { fn transition_to( &self, - state: Self::State, + state: Option, invocations: Option>, ) -> Transition { - Transition { - invocations: self - .exit() - .unwrap_or_default() - .into_iter() - .chain(invocations.unwrap_or_default()) - .chain(state.enter().unwrap_or_default()) - .collect(), - state, - } + let on_enter_invocations = match state.clone() { + Some(state) => state.enter().unwrap_or_default(), + None => vec![], + }; + + let invocations = self + .exit() + .unwrap_or_default() + .into_iter() + .chain(invocations.unwrap_or_default()) + .chain(on_enter_invocations) + .collect(); + + Transition { invocations, state } } } @@ -383,7 +412,7 @@ mod it_should { use crate::presence::event_engine::effects::LeaveEffectExecutor; use crate::presence::LeaveResult; use crate::{ - core::{event_engine::EventEngine, RequestRetryPolicy}, + core::{event_engine::EventEngine, RequestRetryConfiguration}, lib::alloc::sync::Arc, presence::{ event_engine::{ @@ -414,7 +443,7 @@ mod it_should { delayed_heartbeat_call, leave_call, wait_call, - RequestRetryPolicy::None, + RequestRetryConfiguration::None, tx, ), start_state, @@ -425,6 +454,7 @@ mod it_should { #[test_case( PresenceState::Inactive, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -444,6 +474,16 @@ mod it_should { PresenceState::Inactive; "to not change on unexpected event" )] + #[test_case( + PresenceState::Inactive, + PresenceEvent::Joined { + heartbeat_interval: 0, + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Inactive; + "to not change with 0 presence interval" + )] #[tokio::test] async fn transition_for_inactive_state( init_state: PresenceState, @@ -468,6 +508,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -487,6 +528,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr1".to_string()]), }, @@ -506,6 +548,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), }, @@ -571,7 +614,9 @@ mod it_should { &Some(vec!["gr1".to_string()]) ) }, - PresenceEvent::LeftAll, + PresenceEvent::LeftAll { + suppress_leave_events: false + }, PresenceState::Inactive; "to inactive on left all" )] @@ -583,6 +628,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -602,6 +648,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -655,6 +702,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -674,6 +722,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string()]), channel_groups: None, }, @@ -693,6 +742,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), }, @@ -738,7 +788,9 @@ mod it_should { &Some(vec!["gr1".to_string()]) ) }, - PresenceEvent::LeftAll, + PresenceEvent::LeftAll { + suppress_leave_events: false, + }, PresenceState::Inactive; "to inactive on left all" )] @@ -750,6 +802,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -769,6 +822,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -846,6 +900,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -867,6 +922,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string()]), channel_groups: None, }, @@ -888,6 +944,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), }, @@ -960,7 +1017,9 @@ mod it_should { attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, - PresenceEvent::LeftAll, + PresenceEvent::LeftAll { + suppress_leave_events: false, + }, PresenceState::Inactive; "to inactive on left all" )] @@ -974,6 +1033,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -997,6 +1057,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -1054,6 +1115,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -1073,6 +1135,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string()]), channel_groups: None, }, @@ -1092,6 +1155,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), }, @@ -1121,7 +1185,9 @@ mod it_should { &Some(vec!["gr1".to_string()]) ) }, - PresenceEvent::LeftAll, + PresenceEvent::LeftAll { + suppress_leave_events: false, + }, PresenceState::Inactive; "to inactive on left all" )] @@ -1133,6 +1199,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -1152,6 +1219,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -1204,6 +1272,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -1224,6 +1293,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string()]), channel_groups: None, }, @@ -1244,6 +1314,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), }, @@ -1292,7 +1363,9 @@ mod it_should { ), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, - PresenceEvent::LeftAll, + PresenceEvent::LeftAll { + suppress_leave_events: false, + }, PresenceState::Inactive; "to inactive on left all" )] @@ -1305,6 +1378,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -1326,6 +1400,7 @@ mod it_should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, diff --git a/src/dx/presence/event_engine/types.rs b/src/dx/presence/event_engine/types.rs index 299a1a38..edf01f2d 100644 --- a/src/dx/presence/event_engine/types.rs +++ b/src/dx/presence/event_engine/types.rs @@ -17,7 +17,7 @@ use crate::{ /// Object contains information about channels and groups which should be used /// with presence event engine states. #[derive(Clone, Debug, PartialEq)] -pub(crate) struct PresenceInput { +pub struct PresenceInput { /// Optional list of channels. /// /// List of channels for which `user_id` presence should be managed. @@ -133,7 +133,7 @@ impl Sub for PresenceInput { /// /// Data objects are used by the presence event engine to communicate between /// components. -pub(crate) struct PresenceParameters<'execution> { +pub struct PresenceParameters<'execution> { /// List of channel for which `user_id` presence should be announced. pub channels: &'execution Option>, diff --git a/src/dx/presence/mod.rs b/src/dx/presence/mod.rs index 33758b99..b0baeb76 100644 --- a/src/dx/presence/mod.rs +++ b/src/dx/presence/mod.rs @@ -13,12 +13,6 @@ use futures::{ #[cfg(feature = "std")] use spin::RwLock; -use crate::{ - core::{Deserializer, Serialize, Transport}, - dx::pubnub_client::PubNubClientInstance, - lib::alloc::string::ToString, -}; - #[doc(inline)] pub use builders::*; pub mod builders; @@ -32,22 +26,36 @@ pub mod result; pub(crate) use presence_manager::PresenceManager; #[cfg(feature = "std")] pub(crate) mod presence_manager; + #[cfg(feature = "std")] #[doc(inline)] pub(crate) use event_engine::{ - types::PresenceParameters, PresenceEffectHandler, PresenceEventEngine, PresenceState, + PresenceEffectHandler, PresenceEventEngine, PresenceParameters, PresenceState, }; #[cfg(feature = "std")] pub(crate) mod event_engine; + #[cfg(feature = "std")] use crate::{ core::{ event_engine::{cancel::CancellationTask, EventEngine}, - PubNubError, Runtime, + Deserializer, PubNubError, Runtime, Transport, }, lib::alloc::sync::Arc, }; +use crate::{ + core::Serialize, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{ + string::{String, ToString}, + vec::Vec, + }, + collections::HashMap, + }, +}; + impl PubNubClientInstance { /// Create a heartbeat request builder. /// @@ -59,28 +67,28 @@ impl PubNubClientInstance { /// # Example /// ```rust /// use pubnub::presence::*; - /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use pubnub::{core::Serialize, Keyset, PubNubClientBuilder}; /// # use std::collections::HashMap; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { /// let mut pubnub = // PubNubClient - /// # PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: None, - /// # secret_key: None - /// # }) - /// # .with_user_id("uuid") - /// # .build()?; + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; /// pubnub /// .heartbeat() /// .channels(["lobby".into(), "announce".into()]) /// .channel_groups(["area-51".into()]) - /// .state(HashMap::>::from( + /// .state(HashMap::>::from( /// [( - /// "lobby".into(), - /// HashMap::from([("is_admin".into(), false)]) + /// String::from("lobby"), + /// HashMap::from([("is_admin".to_string(), false)]).serialize()? /// )] /// )) /// .execute() @@ -91,7 +99,7 @@ impl PubNubClientInstance { pub fn heartbeat(&self) -> HeartbeatRequestBuilder { HeartbeatRequestBuilder { pubnub_client: Some(self.clone()), - heartbeat: Some(self.config.heartbeat_value), + heartbeat: Some(self.config.presence.heartbeat_value), user_id: Some(self.config.user_id.clone().to_string()), ..Default::default() } @@ -104,6 +112,32 @@ impl PubNubClientInstance { /// `user_id` on channels. /// /// Instance of [`LeaveRequestBuilder`] returned. + /// + /// # Example + /// ```rust + /// use pubnub::presence::*; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let mut pubnub = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// pubnub + /// .leave() + /// .channels(["lobby".into(), "announce".into()]) + /// .channel_groups(["area-51".into()]) + /// .execute() + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub fn leave(&self) -> LeaveRequestBuilder { LeaveRequestBuilder { pubnub_client: Some(self.clone()), @@ -115,7 +149,75 @@ impl PubNubClientInstance { /// Create a set state request builder. /// /// This method is used to update state associated with `user_id` on - /// channels and and channels registered with channel groups. + /// channels and channels registered with channel groups. + /// + /// Instance of [`SetStateRequestBuilder`] returned. + /// + /// # Example + /// ```rust + /// use pubnub::presence::*; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::collections::HashMap; + /// + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let mut pubnub = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// pubnub + /// .set_presence_state(HashMap::::from( + /// [(String::from("is_admin"), false)] + /// )) + /// .channels(["lobby".into(), "announce".into()]) + /// .channel_groups(["area-51".into()]) + /// .execute() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "serde")] + pub fn set_presence_state(&self, state: S) -> SetStateRequestBuilder + where + T: 'static, + D: 'static, + S: serde::Serialize, + { + #[cfg(feature = "std")] + let client = self.clone(); + + SetStateRequestBuilder { + pubnub_client: Some(self.clone()), + state: Some(serde_json::to_vec(&state).ok()), + user_id: Some(self.config.user_id.clone().to_string()), + + #[cfg(feature = "std")] + on_execute: Some(Arc::new(move |channels, state| { + let Some(state) = state else { + return; + }; + + client.update_presence_state(channels.into_iter().fold( + HashMap::new(), + |mut acc, channel| { + acc.insert(channel, state.clone()); + acc + }, + )) + })), + ..Default::default() + } + } + + /// Create a set state request builder. + /// + /// This method is used to update state associated with `user_id` on + /// channels and channels registered with channel groups. /// /// Instance of [`SetStateRequestBuilder`] returned. /// @@ -127,7 +229,6 @@ impl PubNubClientInstance { /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use std::sync::Arc; /// let mut pubnub = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -139,7 +240,7 @@ impl PubNubClientInstance { /// # .build()?; /// pubnub /// .set_presence_state(HashMap::::from( - /// [("is_admin".into(), false)] + /// [(String::from("is_admin"), false)] /// )) /// .channels(["lobby".into(), "announce".into()]) /// .channel_groups(["area-51".into()]) @@ -148,14 +249,35 @@ impl PubNubClientInstance { /// # Ok(()) /// # } /// ``` + #[cfg(not(feature = "serde"))] pub fn set_presence_state(&self, state: S) -> SetStateRequestBuilder where - S: Serialize + Send + Sync + 'static, + T: 'static, + D: 'static, + S: Serialize, { + #[cfg(feature = "std")] + let client = self.clone(); + SetStateRequestBuilder { pubnub_client: Some(self.clone()), state: Some(state.serialize().ok()), user_id: Some(self.config.user_id.clone().to_string()), + + #[cfg(feature = "std")] + on_execute: Some(Arc::new(move |channels, state| { + let Some(state) = state else { + return; + }; + + client.update_presence_state(channels.into_iter().fold( + HashMap::new(), + |mut acc, channel| { + acc.insert(channel, state.clone()); + acc + }, + )) + })), ..Default::default() } } @@ -163,32 +285,74 @@ impl PubNubClientInstance { /// Create a heartbeat request builder. /// /// This method is used to update state associated with `user_id` on - /// channels using `heartbeat` operation endpoint. + /// channels using `heartbeat` operation endpoint. State with heartbeat can + /// be set **only** for channels. /// /// Instance of [`HeartbeatRequestsBuilder`] returned. - pub fn set_state_with_heartbeat(&self, state: U) -> HeartbeatRequestBuilder + /// + /// # Example + /// ```rust + /// use pubnub::presence::*; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::collections::HashMap; + /// + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let mut pubnub = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// pubnub + /// .set_presence_state_with_heartbeat(HashMap::from([ + /// ("lobby".to_string(), HashMap::from([("key".to_string(), "value".to_string())])), + /// ("announce".to_string(), HashMap::from([("key".to_string(), "value".to_string())])), + /// ])) + /// .channels(["lobby".into(), "announce".into()]) + /// .channel_groups(["area-51".into()]) + /// .execute() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub fn set_presence_state_with_heartbeat( + &self, + state: HashMap, + ) -> HeartbeatRequestBuilder where - U: Serialize + Send + Sync + 'static, + S: Serialize, { - self.heartbeat().state(state) + let mapped = state + .iter() + .fold(HashMap::new(), |mut acc, (channel, state)| { + if let Ok(serialized_state) = state.serialize() { + acc.insert(channel.clone(), serialized_state); + } + acc + }); + + self.update_presence_state(mapped.clone()); + self.heartbeat().state(mapped) } /// Create a get state request builder. /// /// This method is used to get state associated with `user_id` on - /// channels and and channels registered with channel groups. + /// channels and channels registered with channel groups. /// - /// Instance of [`SetStateRequestBuilder`] returned. + /// Instance of [`GetStateRequestBuilder`] returned. /// /// # Example /// ```rust /// use pubnub::presence::*; /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # use std::collections::HashMap; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use std::sync::Arc; /// let mut pubnub = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -226,7 +390,6 @@ impl PubNubClientInstance { /// ```rust /// use pubnub::presence::*; /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # use std::collections::HashMap; /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -270,11 +433,9 @@ impl PubNubClientInstance { /// ```rust /// use pubnub::presence::*; /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # use std::collections::HashMap; /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use std::sync::Arc; /// let mut pubnub = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -285,7 +446,6 @@ impl PubNubClientInstance { /// # .with_user_id("uuid") /// # .build()?; /// let response = pubnub.where_now().user_id("user_id").execute().await?; - /// /// println!("User channels: {:?}", response); /// @@ -298,61 +458,94 @@ impl PubNubClientInstance { ..Default::default() } } + + /// Update presence state associated with `user_id`. + pub(crate) fn update_presence_state(&self, accumulated_state: HashMap>) { + if accumulated_state.is_empty() { + return; + } + + let mut current_state = self.state.write(); + accumulated_state.into_iter().for_each(|(channel, state)| { + current_state.insert(channel, state.clone()); + }); + } } +#[cfg(feature = "std")] impl PubNubClientInstance where T: Transport + Send + 'static, D: Deserializer + 'static, { /// Announce `join` for `user_id` on provided channels and groups. - #[cfg(feature = "std")] - #[allow(dead_code)] pub(crate) fn announce_join( &self, channels: Option>, channel_groups: Option>, ) { - self.configure_presence(); - { - let slot = self.presence.read(); - if let Some(presence) = slot.as_ref() { + if let Some(presence) = self.presence_manager(true).read().as_ref() { presence.announce_join(channels, channel_groups); - } + }; }; } /// Announce `leave` for `user_id` on provided channels and groups. - #[cfg(feature = "std")] - #[allow(dead_code)] pub(crate) fn announce_left( &self, channels: Option>, channel_groups: Option>, ) { - self.configure_presence(); - { - let slot = self.presence.read(); - if let Some(presence) = slot.as_ref() { + if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.announce_left(channels, channel_groups); - } + }; }; } - /// Complete presence configuration. + /// Announce `leave` for `user_id` on all active channels and groups. + pub(crate) fn announce_left_all(&self) { + { + if let Some(presence) = self.presence_manager(false).read().as_ref() { + presence.announce_left_all(); + } + } + } + + /// Presence manager which maintains Presence EE. + /// + /// # Arguments /// - /// Presence configuration used only with presence event engine. - #[cfg(feature = "std")] - pub(crate) fn configure_presence(&self) -> Arc>> { + /// `create` - Whether manager should be created if not initialized. + /// + /// # Returns + /// + /// Returns an [`PresenceManager`] which represents the manager. + #[cfg(all(feature = "presence", feature = "std"))] + pub(crate) fn presence_manager(&self, create: bool) -> Arc>> { + if self.config.presence.heartbeat_interval.unwrap_or(0).eq(&0) { + return self.presence.clone(); + } + { - let mut slot = self.presence.write(); - if slot.is_none() { - *slot = Some(PresenceManager::new(self.presence_event_engine(), None)); + let manager = self.presence.read(); + if manager.is_some() || !create { + return self.presence.clone(); } } + { + let mut slot = self.presence.write(); + if slot.is_none() && create { + *slot = Some(PresenceManager::new( + self.presence_event_engine(), + self.config.presence.heartbeat_interval.unwrap_or_default(), + self.config.presence.suppress_leave_events, + )); + }; + } + self.presence.clone() } @@ -360,7 +553,6 @@ where /// /// Prepare presence event engine instance which will be used for `user_id` /// presence announcement and management. - #[cfg(feature = "std")] fn presence_event_engine(&self) -> Arc { let channel_bound = 3; let (cancel_tx, cancel_rx) = async_channel::bounded::(channel_bound); @@ -371,8 +563,8 @@ where let heartbeat_call_client = self.clone(); let leave_call_client = self.clone(); let wait_call_client = self.clone(); - let request_retry_delay_policy = self.config.retry_policy.clone(); - let request_retry_policy = self.config.retry_policy.clone(); + let request_retry = self.config.transport.retry_configuration.clone(); + let request_delayed_retry = request_retry.clone(); let delayed_heartbeat_runtime_sleep = runtime.clone(); let wait_runtime_sleep = runtime.clone(); @@ -382,16 +574,22 @@ where Self::heartbeat_call(heartbeat_call_client.clone(), parameters.clone()) }), Arc::new(move |parameters| { - let delay_in_secs = request_retry_delay_policy - .retry_delay(¶meters.attempt, parameters.reason.as_ref()); + let delay_in_microseconds = request_delayed_retry.retry_delay( + Some("/v2/presence".to_string()), + ¶meters.attempt, + parameters.reason.as_ref(), + ); let inner_runtime_sleep = delayed_heartbeat_runtime_sleep.clone(); Self::delayed_heartbeat_call( delayed_heartbeat_call_client.clone(), parameters.clone(), Arc::new(move || { - if let Some(delay) = delay_in_secs { - inner_runtime_sleep.clone().sleep(delay).boxed() + if let Some(delay) = delay_in_microseconds { + inner_runtime_sleep + .clone() + .sleep_microseconds(delay) + .boxed() } else { ready(()).boxed() } @@ -403,7 +601,7 @@ where Self::leave_call(leave_call_client.clone(), parameters.clone()) }), Arc::new(move |effect_id| { - let delay_in_secs = wait_call_client.config.heartbeat_interval; + let delay_in_secs = wait_call_client.config.presence.heartbeat_interval; let inner_runtime_sleep = wait_runtime_sleep.clone(); Self::wait_call( @@ -418,7 +616,7 @@ where wait_cancel_rx.clone(), ) }), - request_retry_policy, + request_retry, cancel_tx, ), PresenceState::Inactive, @@ -427,16 +625,20 @@ where } /// Call to announce `user_id` presence. - #[cfg(feature = "std")] pub(crate) fn heartbeat_call( client: Self, params: PresenceParameters, ) -> BoxFuture<'static, Result> { - client.heartbeat_request(params).execute().boxed() + let mut request = client.heartbeat_request(params); + let state = client.state.read(); + if !state.is_empty() { + request = request.state(state.clone()); + } + + request.execute().boxed() } /// Call delayed announce of `user_id` presence. - #[cfg(feature = "std")] pub(crate) fn delayed_heartbeat_call( client: Self, params: PresenceParameters, @@ -456,11 +658,14 @@ where } /// Call announce `leave` for `user_id`. - #[cfg(feature = "std")] pub(crate) fn leave_call( client: Self, params: PresenceParameters, ) -> BoxFuture<'static, Result> { + if client.config.presence.suppress_leave_events { + return ready(Ok(LeaveResult)).boxed(); + } + let mut request = client.leave(); if let Some(channels) = params.channels.clone() { @@ -475,7 +680,6 @@ where } /// Heartbeat idle. - #[cfg(feature = "std")] pub(crate) fn wait_call( effect_id: &str, delay: Arc, @@ -497,25 +701,6 @@ where .boxed() } - #[cfg(feature = "std")] - /// Call to update `state` associated with `user_id`. - #[allow(dead_code)] - pub(crate) fn set_heartbeat_call(client: Self, _params: PresenceParameters, state: U) - where - U: Serialize + Send + Sync + 'static, - { - // TODO: This is still under development and will be part of EE. - { - client.configure_presence(); - - let state = state.serialize().ok(); - if let Some(presence) = client.presence.clone().write().as_mut() { - presence.state = state; - } - } - } - - #[cfg(feature = "std")] pub(crate) fn heartbeat_request( &self, params: PresenceParameters, @@ -529,10 +714,10 @@ where if let Some(channel_groups) = params.channel_groups.clone() { request = request.channel_groups(channel_groups); } - - if let Some(presence) = self.presence.clone().read().as_ref() { - request = request.state_serialized(presence.state.clone()) - } + // + // if let Some(presence) = self.presence.clone().read().as_ref() { + // request = request.state_serialized(presence.state.clone()) + // } request } @@ -544,7 +729,10 @@ mod it_should { use crate::core::{PubNubError, Transport, TransportRequest, TransportResponse}; use crate::providers::deserialization_serde::DeserializerSerde; use crate::transport::middleware::PubNubMiddleware; - use crate::{lib::collections::HashMap, Keyset, PubNubClientBuilder}; + use crate::{ + lib::{alloc::vec::Vec, collections::HashMap}, + Keyset, PubNubClientBuilder, + }; /// Requests handler function type. type RequestHandler = Box; @@ -628,9 +816,12 @@ mod it_should { let result = client .heartbeat() - .state(HashMap::>::from([( - "hello".into(), - HashMap::from([("is_admin".into(), false)]), + .state(HashMap::>::from([( + String::from("hello"), + HashMap::::from([(String::from("is_admin"), false)]) + .serialize() + .ok() + .unwrap(), )])) .channels(["hello".into()]) .user_id("my_user") @@ -659,14 +850,26 @@ mod it_should { let _ = client(true, Some(transport)) .heartbeat() - .state(HashMap::>::from([ + .state(HashMap::>::from([ ( - "channel_a".into(), - HashMap::::from([("value_a".into(), "secret_a".into())]), + String::from("channel_a"), + HashMap::::from([( + String::from("value_a"), + String::from("secret_a"), + )]) + .serialize() + .ok() + .unwrap(), ), ( - "channel_c".into(), - HashMap::::from([("value_c".into(), "secret_c".into())]), + String::from("channel_c"), + HashMap::::from([( + String::from("value_c"), + String::from("secret_c"), + )]) + .serialize() + .ok() + .unwrap(), ), ])) .channels(["channel_a".into(), "channel_b".into(), "channel_c".into()]) diff --git a/src/dx/presence/presence_manager.rs b/src/dx/presence/presence_manager.rs index 8b05d72b..4c0429df 100644 --- a/src/dx/presence/presence_manager.rs +++ b/src/dx/presence/presence_manager.rs @@ -3,8 +3,9 @@ //! This module contains [`PresenceManager`] which allow user to configure //! presence / heartbeat module components. +use crate::presence::event_engine::PresenceEffectInvocation; use crate::{ - dx::presence::event_engine::PresenceEventEngine, + dx::presence::event_engine::{PresenceEvent, PresenceEventEngine}, lib::{ alloc::{string::String, sync::Arc, vec::Vec}, core::{ @@ -24,14 +25,28 @@ pub(crate) struct PresenceManager { } impl PresenceManager { - pub fn new(event_engine: Arc, state: Option>) -> Self { + pub fn new( + event_engine: Arc, + heartbeat_interval: u64, + suppress_leave_events: bool, + ) -> Self { Self { inner: Arc::new(PresenceManagerRef { event_engine, - state, + heartbeat_interval, + suppress_leave_events, }), } } + + /// Terminate subscription manager. + /// + /// Gracefully terminate all ongoing tasks including detached event engine + /// loop. + pub fn terminate(&self) { + self.event_engine + .stop(PresenceEffectInvocation::TerminateEventEngine); + } } impl Deref for PresenceManager { @@ -64,65 +79,59 @@ pub(crate) struct PresenceManagerRef { /// Presence event engine. pub event_engine: Arc, - /// A state that should be associated with the `user_id`. - /// - /// `state` object should be a `HashMap` with channel names as keys and - /// nested `HashMap` with values. State with heartbeat can be set **only** - /// for channels. + /// `user_id` presence announcement interval. + heartbeat_interval: u64, + + /// Whether `user_id` leave should be announced or not. /// - /// # Example: - /// ```rust,no_run - /// # use std::collections::HashMap; - /// # fn main() { - /// let state = HashMap::>::from([( - /// "announce".into(), - /// HashMap::from([ - /// ("is_owner".into(), false), - /// ("is_admin".into(), true) - /// ]) - /// )]); - /// # } - /// ``` - pub state: Option>, + /// When set to `true` and `user_id` will unsubscribe, the client wouldn't + /// announce `leave`, and as a result, there will be no `leave` presence + /// event generated. + suppress_leave_events: bool, } impl PresenceManagerRef { /// Announce `join` for `user_id` on provided channels and groups. pub(crate) fn announce_join( &self, - _channels: Option>, - _channel_groups: Option>, + channels: Option>, + channel_groups: Option>, ) { - // TODO: Uncomment after contract test server fix. - // self.event_engine.process(&PresenceEvent::Joined { - // channels, - // channel_groups, - // }) + self.event_engine.process(&PresenceEvent::Joined { + heartbeat_interval: self.heartbeat_interval, + channels, + channel_groups, + }); } /// Announce `leave` for `user_id` on provided channels and groups. pub(crate) fn announce_left( &self, - _channels: Option>, - _channel_groups: Option>, + channels: Option>, + channel_groups: Option>, ) { - // TODO: Uncomment after contract test server fix. - // self.event_engine.process(&PresenceEvent::Left { - // channels, - // channel_groups, - // }) + self.event_engine.process(&PresenceEvent::Left { + suppress_leave_events: self.suppress_leave_events, + channels, + channel_groups, + }) + } + + /// Announce `leave` for `user_id` on all active channels and groups. + pub(crate) fn announce_left_all(&self) { + self.event_engine.process(&PresenceEvent::LeftAll { + suppress_leave_events: self.suppress_leave_events, + }) } /// Announce `leave` while client disconnected. pub(crate) fn disconnect(&self) { - // TODO: Uncomment after contract test server fix. - // self.event_engine.process(&PresenceEvent::Disconnect); + self.event_engine.process(&PresenceEvent::Disconnect); } /// Announce `join` upon client connection. pub(crate) fn reconnect(&self) { - // TODO: Uncomment after contract test server fix. - // self.event_engine.process(&PresenceEvent::Reconnect); + self.event_engine.process(&PresenceEvent::Reconnect); } } @@ -130,7 +139,7 @@ impl Debug for PresenceManagerRef { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( f, - "PresenceConfiguration {{\n\tevent_engine: {:?}\n}}", + "PresenceConfiguration {{ event_engine: {:?} }}", self.event_engine ) } diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index c67b4294..ecb571ec 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -22,26 +22,9 @@ use crate::{ pub struct HeartbeatResult; /// Presence service response body for heartbeat. -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum HeartbeatResponseBody { - /// This is a success response body for a announce heartbeat operation in - /// the Presence service. - /// - /// It contains information about the service that have the response and - /// operation result message. - /// - /// # Example - /// ```json - /// { - /// "status": 200, - /// "message": "OK", - /// "service": "Presence" - /// } - /// ``` - SuccessResponse(APISuccessBodyWithMessage), - /// This is an error response body for a announce heartbeat operation in the /// Presence service. /// It contains information about the service that provided the response and @@ -66,6 +49,22 @@ pub enum HeartbeatResponseBody { /// } /// ``` ErrorResponse(APIErrorBody), + + /// This is a success response body for a announce heartbeat operation in + /// the Presence service. + /// + /// It contains information about the service that have the response and + /// operation result message. + /// + /// # Example + /// ```json + /// { + /// "status": 200, + /// "message": "OK", + /// "service": "Presence" + /// } + /// ``` + SuccessResponse(APISuccessBodyWithMessage), } impl TryFrom for HeartbeatResult { @@ -84,8 +83,7 @@ impl TryFrom for HeartbeatResult { pub struct LeaveResult; /// Presence service response body for leave. -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum LeaveResponseBody { /// This is a success response body for a announce leave operation in diff --git a/src/dx/publish/mod.rs b/src/dx/publish/mod.rs index d73ef31a..28378ad7 100644 --- a/src/dx/publish/mod.rs +++ b/src/dx/publish/mod.rs @@ -132,7 +132,7 @@ where impl PublishMessageViaChannelBuilder where - T: Transport, + T: Transport + 'static, M: Serialize, D: Deserializer + 'static, { @@ -171,8 +171,16 @@ where self.prepare_context_with_request()? .map(|some| async move { let deserializer = some.client.deserializer.clone(); + some.data - .send::(&some.client.transport, deserializer) + .send::( + &some.client.transport, + deserializer, + #[cfg(feature = "std")] + &some.client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &some.client.runtime, + ) .await }) .await @@ -292,7 +300,9 @@ where method: TransportMethod::Post, query_parameters: query_params, body: Some(m_vec), - headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + headers: [(CONTENT_TYPE.to_string(), APPLICATION_JSON.to_string())].into(), + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } else { String::from_utf8(m_vec) @@ -309,6 +319,8 @@ where ), method: TransportMethod::Get, query_parameters: query_params, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, ..Default::default() }) } @@ -400,11 +412,8 @@ mod should { use crate::providers::deserialization_serde::DeserializerSerde; use crate::{ core::TransportResponse, - dx::pubnub_client::{PubNubClientInstance, PubNubClientRef, PubNubConfig}, - lib::{ - alloc::{sync::Arc, vec}, - collections::HashMap, - }, + dx::pubnub_client::PubNubClientInstance, + lib::{alloc::vec, collections::HashMap}, transport::middleware::PubNubMiddleware, Keyset, PubNubClientBuilder, }; @@ -484,13 +493,13 @@ mod should { assert_eq!( HashMap::::from([ - ("norep".into(), "true".into()), - ("store".into(), "1".into()), - ("space-id".into(), "space_id".into()), - ("type".into(), "message_type".into()), - ("meta".into(), "{\"k\":\"v\"}".into()), - ("ttl".into(), "50".into()), - ("seqn".into(), "1".into()) + ("norep".to_string(), "true".to_string()), + ("store".to_string(), "1".to_string()), + ("space-id".to_string(), "space_id".to_string()), + ("type".to_string(), "message_type".to_string()), + ("meta".to_string(), "{\"k\":\"v\"}".to_string()), + ("ttl".to_string(), "50".to_string()), + ("seqn".to_string(), "1".to_string()) ]), result.data.query_parameters ); @@ -508,30 +517,33 @@ mod should { assert_eq!(vec![1, 2], received_sequence_numbers); } - #[tokio::test] - async fn return_err_if_publish_key_is_not_provided() { - let client = { - let default_client = client(); - let ref_client = Arc::try_unwrap(default_client.inner).unwrap(); - - PubNubClientInstance { - inner: Arc::new(PubNubClientRef { - config: PubNubConfig { - publish_key: None, - ..ref_client.config - }, - ..ref_client - }), - } - }; - - assert!(client - .publish_message("message") - .channel("chan") - .execute() - .await - .is_err()); - } + // TODO: REMOVE THIS TEST + // #[tokio::test] + // async fn return_err_if_publish_key_is_not_provided() { + // let client = { + // let default_client = client(); + // let ref_client = default_client.inner.as_ref(); + // // i.config + // // let ref_client = Arc::try_unwrap(default_client.inner).unwrap(); + // + // PubNubClientInstance { + // inner: Arc::new(PubNubClientRef { + // config: PubNubConfig { + // publish_key: None, + // ..default_client.config.clone() + // }, + // transport: default_client.clone().transport, + // }), + // } + // }; + // + // assert!(client + // .publish_message("message") + // .channel("chan") + // .execute() + // .await + // .is_err()); + // } #[test] fn test_send_string_when_get() { diff --git a/src/dx/pubnub_client.rs b/src/dx/pubnub_client.rs index 86bab779..b5df8a8f 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -7,13 +7,24 @@ //! [`PubNub API`]: https://www.pubnub.com/docs //! [`pubnub`]: ../index.html +use derive_builder::Builder; +use log::info; +use spin::{Mutex, RwLock}; +use uuid::Uuid; + +#[cfg(all( + any(feature = "subscribe", feature = "presence"), + feature = "std", + feature = "tokio" +))] +use crate::providers::futures_tokio::RuntimeTokio; +#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] +use crate::subscribe::{EventDispatcher, SubscriptionCursor, SubscriptionManager}; + +#[cfg(feature = "presence")] +use crate::lib::alloc::vec::Vec; #[cfg(all(feature = "presence", feature = "std"))] use crate::presence::PresenceManager; -#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] -use crate::{ - core::runtime::RuntimeSupport, providers::futures_tokio::RuntimeTokio, - subscribe::SubscriptionManager, -}; #[cfg(not(feature = "serde"))] use crate::core::Deserializer; @@ -24,23 +35,26 @@ use crate::transport::TransportReqwest; // TODO: Retry policy would be implemented for `no_std` event engine #[cfg(feature = "std")] -use crate::core::RequestRetryPolicy; +use crate::core::{runtime::RuntimeSupport, RequestRetryConfiguration}; use crate::{ - core::{CryptoProvider, PubNubError}, + core::{CryptoProvider, PubNubEntity, PubNubError}, lib::{ alloc::{ + borrow::ToOwned, + format, string::{String, ToString}, sync::Arc, }, - core::ops::{Deref, DerefMut}, + collections::HashMap, + core::{ + cmp::max, + ops::{Deref, DerefMut}, + }, }, transport::middleware::{PubNubMiddleware, SignatureKeySet}, + Channel, ChannelGroup, ChannelMetadata, UserMetadata, }; -use derive_builder::Builder; -use log::info; -use spin::{Mutex, RwLock}; -use uuid::Uuid; /// PubNub client /// @@ -123,7 +137,7 @@ use uuid::Uuid; /// /// [`selected`]: ../index.html#features /// [`Keyset`]: ../core/struct.Keyset.html -/// [`PubNubClient::with_transport`]: struct.PubNubClientBuilder.html#method.with_transport`] +/// [`PubNubClient::with_transport`]: struct.PubNubClientBuilder.html#method.with_transport` pub type PubNubGenericClient = PubNubClientInstance, D>; /// PubNub client @@ -222,6 +236,14 @@ pub type PubNubClient = PubNubGenericClient #[derive(Debug)] pub struct PubNubClientInstance { pub(crate) inner: Arc>, + + /// Subscription time cursor. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) cursor: Arc>>, + + /// Real-time event dispatcher. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) event_dispatcher: Arc, } impl Deref for PubNubClientInstance { @@ -243,6 +265,12 @@ impl Clone for PubNubClientInstance { fn clone(&self) -> Self { Self { inner: Arc::clone(&self.inner), + + #[cfg(all(feature = "subscribe", feature = "std"))] + cursor: Default::default(), + + #[cfg(all(feature = "subscribe", feature = "std"))] + event_dispatcher: Arc::clone(&self.event_dispatcher), } } } @@ -301,25 +329,492 @@ pub struct PubNubClientRef { )] pub(crate) auth_token: Arc>, + /// Real-time data filtering expression. + #[cfg(feature = "subscribe")] + #[builder( + setter(custom, strip_option), + field(vis = "pub(crate)"), + default = "Arc::new(spin::RwLock::new(String::new()))" + )] + pub(crate) filter_expression: Arc>, + + /// A state that should be associated with the `user_id`. + /// + /// `state` object should be a `HashMap` with channel names as keys and + /// serialized `state` as values. State with heartbeat can be set **only** + /// for channels. + /// + /// # Example: + /// ```rust,no_run + /// # use std::collections::HashMap; + /// # use pubnub::core::Serialize; + /// # + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let state = HashMap::>::from([( + /// String::from("announce"), + /// HashMap::::from([ + /// (String::from("is_owner"), false), + /// (String::from("is_admin"), true) + /// ]).serialize()? + /// )]); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "presence")] + #[builder( + setter(custom), + field(vis = "pub(crate)"), + default = "Arc::new(spin::RwLock::new(HashMap::new()))" + )] + pub(crate) state: Arc>>>, + /// Runtime environment - #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + #[cfg(feature = "std")] #[builder(setter(custom), field(vis = "pub(crate)"))] pub(crate) runtime: RuntimeSupport, /// Subscription module configuration + /// + /// > **Important**: Use `.subscription_manager()` to access it instead of + /// > field. #[cfg(all(feature = "subscribe", feature = "std"))] #[builder(setter(skip), field(vis = "pub(crate)"))] - pub(crate) subscription: Arc>>, + pub(crate) subscription: Arc>>>, /// Presence / heartbeat event engine. /// /// State machine which is responsible for `user_id` presence maintenance. + /// + /// > **Important**: Use `.presence_manager()` to access it instead of + /// > field. #[cfg(all(feature = "presence", feature = "std"))] #[builder(setter(skip), field(vis = "pub(crate)"))] pub(crate) presence: Arc>>, + + /// Created entities. + /// + /// Map of entities which has been created to access [`PubNub API`]. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[builder(setter(skip), field(vis = "pub(crate)"))] + pub(crate) entities: RwLock>>, } impl PubNubClientInstance { + /// Creates a new channel with the specified name. + /// + /// # Arguments + /// + /// * `name` - The name of the channel as a string. + /// + /// # Returns + /// + /// Returns a `Channel` which can be used with the [`PubNub API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channel = client.channel("my_channel"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn channel(&self, name: S) -> Channel + where + S: Into, + { + let mut entities_slot = self.entities.write(); + let name = name.into(); + let entity = entities_slot + .entry(format!("{}_ch", &name)) + .or_insert(Channel::new(self, name).into()); + + match entity { + PubNubEntity::Channel(channel) => channel.clone(), + _ => panic!("Unexpected entry type for Channel"), + } + } + + /// Creates a list of channels with the specified names. + /// + /// # Arguments + /// + /// * `names` - A list of names for the channels as a string. + /// + /// # Returns + /// + /// Returns a list of `Channel` which can be used with the [`PubNub API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channel = client.channels(&["my_channel_1", "my_channel_2"]); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn channels(&self, names: &[S]) -> Vec> + where + S: Into + Clone, + { + let mut channels = Vec::with_capacity(names.len()); + let mut entities_slot = self.entities.write(); + + for name in names.iter() { + let name = name.to_owned().into(); + let entity = entities_slot + .entry(format!("{}_ch", name)) + .or_insert(Channel::new(self, name).into()); + + match entity { + PubNubEntity::Channel(channel) => channels.push(channel.clone()), + _ => panic!("Unexpected entry type for Channel"), + } + } + + channels + } + + /// Creates a new channel group with the specified name. + /// + /// # Arguments + /// + /// * `name` - The name of the channel group as a string. + /// + /// # Returns + /// + /// Returns a `ChannelGroup` which can be used with the [`PubNub API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channel_group = client.channel_group("my_group"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn channel_group(&self, name: S) -> ChannelGroup + where + S: Into, + { + let mut entities_slot = self.entities.write(); + let name = name.into(); + let entity = entities_slot + .entry(format!("{}_chg", &name)) + .or_insert(ChannelGroup::new(self, name).into()); + + match entity { + PubNubEntity::ChannelGroup(channel_group) => channel_group.clone(), + _ => panic!("Unexpected entry type for ChannelGroup"), + } + } + + /// Creates a list of channel groups with the specified names. + /// + /// # Arguments + /// + /// * `name` - A list of names for the channel groups as a string. + /// + /// # Returns + /// + /// Returns a list of `ChannelGroup` which can be used with the [`PubNub + /// API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channel_groups = client.channel_groups(&["my_group_1", "my_group_2"]); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn channel_groups(&self, names: &[S]) -> Vec> + where + S: Into + Clone, + { + let mut channel_groups = Vec::with_capacity(names.len()); + let mut entities_slot = self.entities.write(); + + for name in names.iter() { + let name = name.clone().into(); + let entity = entities_slot + .entry(format!("{}_chg", name)) + .or_insert(ChannelGroup::new(self, name).into()); + + match entity { + PubNubEntity::ChannelGroup(channel_group) => { + channel_groups.push(channel_group.clone()) + } + _ => panic!("Unexpected entry type for ChannelGroup"), + } + } + + channel_groups + } + + /// Creates a new channel metadata object with the specified identifier. + /// + /// # Arguments + /// + /// * `id` - The identifier of the channel metadata object as a string. + /// + /// # Returns + /// + /// Returns a `ChannelMetadata` which can be used with the [`PubNub API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channel_metadata = client.channel_metadata("channel_meta"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn channel_metadata(&self, id: S) -> ChannelMetadata + where + S: Into, + { + let mut entities_slot = self.entities.write(); + let id = id.into(); + let entity = entities_slot + .entry(format!("{}_chm", &id)) + .or_insert(ChannelMetadata::new(self, id).into()); + + match entity { + PubNubEntity::ChannelMetadata(channel_metadata) => channel_metadata.clone(), + _ => panic!("Unexpected entry type for ChannelMetadata"), + } + } + + /// Creates a list of channel metadata objects with the specified + /// identifiers. + /// + /// # Arguments + /// + /// * `id` - A list of identifiers for the channel metadata objects as a + /// string. + /// + /// # Returns + /// + /// Returns a list of `ChannelMetadata` which can be used with the [`PubNub + /// API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channels_metadata = client.channels_metadata( + /// &["channel_meta_1", "channel_meta_2"] + /// ); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn channels_metadata(&self, ids: &[S]) -> Vec> + where + S: Into + Clone, + { + let mut channels_metadata = Vec::with_capacity(ids.len()); + let mut entities_slot = self.entities.write(); + + for id in ids.iter() { + let id = id.clone().into(); + let entity = entities_slot + .entry(format!("{}_chm", id)) + .or_insert(ChannelMetadata::new(self, id).into()); + + match entity { + PubNubEntity::ChannelMetadata(channel_metadata) => { + channels_metadata.push(channel_metadata.clone()) + } + _ => panic!("Unexpected entry type for ChannelMetadata"), + } + } + + channels_metadata + } + + /// Creates a new user metadata object with the specified identifier. + /// + /// # Arguments + /// + /// * `id` - The identifier of the user metadata object as a string. + /// + /// # Returns + /// + /// Returns a `UserMetadata` which can be used with the [`PubNub API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let user_metadata = client.user_metadata("user_meta"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn user_metadata(&self, id: S) -> UserMetadata + where + S: Into, + { + let mut entities_slot = self.entities.write(); + let id = id.into(); + let entity = entities_slot + .entry(format!("{}_uidm", &id)) + .or_insert(UserMetadata::new(self, id).into()); + + match entity { + PubNubEntity::UserMetadata(user_metadata) => user_metadata.clone(), + _ => panic!("Unexpected entry type for UserMetadata"), + } + } + + /// Creates a list of user metadata objects with the specified identifier. + /// + /// # Arguments + /// + /// * `id` - A list of identifiers for the user metadata objects as a + /// string. + /// + /// # Returns + /// + /// Returns a list of `UserMetadata` which can be used with the + /// [`PubNub API`]. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let users_metadata = client.users_metadata(&["user_meta_1", "user_meta_2"]); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn users_metadata(&self, ids: &[S]) -> Vec> + where + S: Into + Clone, + { + let mut users_metadata = Vec::with_capacity(ids.len()); + let mut entities_slot = self.entities.write(); + + for id in ids.iter() { + let id = id.clone().into(); + let entity = entities_slot + .entry(format!("{}_uidm", id)) + .or_insert(UserMetadata::new(self, id).into()); + + match entity { + PubNubEntity::UserMetadata(user_metadata) => { + users_metadata.push(user_metadata.clone()) + } + _ => panic!("Unexpected entry type for UserMetadata"), + } + } + + users_metadata + } + /// Update currently used authentication token. /// /// # Examples @@ -381,6 +876,37 @@ impl PubNubClientInstance { } } +impl PubNubClientInstance +where + T: crate::core::Transport + Send + Sync + 'static, + D: crate::core::Deserializer + Send + Sync + 'static, +{ + /// Terminates the subscription and presence managers if the corresponding + /// features are enabled. + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub fn terminate(&self) { + #[cfg(feature = "subscribe")] + { + let manager = self.subscription_manager(false); + let mut manager_slot = manager.write(); + if let Some(manager) = manager_slot.as_ref() { + manager.terminate(); + } + // Free up resources used by subscription event engine. + *manager_slot = None; + } + #[cfg(feature = "presence")] + { + let manager = self.presence_manager(false); + let mut manager_slot = manager.write(); + if let Some(manager) = manager_slot.as_ref() { + manager.terminate(); + } + *manager_slot = None; + } + } +} + impl PubNubClientConfigBuilder { /// Set client authentication key. /// @@ -409,8 +935,13 @@ impl PubNubClientConfigBuilder { #[cfg(any(feature = "subscribe", feature = "presence"))] pub fn with_heartbeat_value(mut self, value: u64) -> Self { if let Some(configuration) = self.config.as_mut() { - configuration.heartbeat_value = value; - configuration.heartbeat_interval = Some(value / 2 - 1); + let value = max(20, value); + configuration.presence.heartbeat_value = value; + + #[cfg(feature = "std")] + { + configuration.presence.heartbeat_interval = Some(value / 2 - 1); + } } self } @@ -423,26 +954,46 @@ impl PubNubClientConfigBuilder { /// It returns [`PubNubClientConfigBuilder`] that you can use to set the /// configuration for the client. This is a part of the /// [`PubNubClientConfigBuilder`]. - #[cfg(any(feature = "subscribe", feature = "presence"))] + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] pub fn with_heartbeat_interval(mut self, interval: u64) -> Self { if let Some(configuration) = self.config.as_mut() { - configuration.heartbeat_interval = Some(interval); + configuration.presence.heartbeat_interval = Some(interval); } self } - /// Requests retry policy. + /// Whether `user_id` leave should be announced or not. /// - /// The retry policy regulates the frequency of request retry attempts and - /// the number of failed attempts that should be retried. + /// When set to `true` and `user_id` will unsubscribe, the client wouldn't + /// announce `leave`, and as a result, there will be no `leave` presence + /// event generated. + /// + /// It returns [`PubNubClientConfigBuilder`] that you can use to set the + /// configuration for the client. This is a part of the + /// [`PubNubClientConfigBuilder`]. + #[cfg(any(feature = "subscribe", feature = "presence"))] + pub fn with_suppress_leave_events(mut self, suppress_leave_events: bool) -> Self { + if let Some(configuration) = self.config.as_mut() { + configuration.presence.suppress_leave_events = suppress_leave_events; + } + self + } + + /// Requests automatic retry configuration. + /// + /// The retry configuration regulates the frequency of request retry + /// attempts and the number of failed attempts that should be retried. /// /// It returns [`PubNubClientConfigBuilder`] that you can use to set the /// configuration for the client. This is a part of the /// [`PubNubClientConfigBuilder`]. #[cfg(feature = "std")] - pub fn with_retry_policy(mut self, policy: RequestRetryPolicy) -> Self { + pub fn with_retry_configuration( + mut self, + retry_configuration: RequestRetryConfiguration, + ) -> Self { if let Some(configuration) = self.config.as_mut() { - configuration.retry_policy = policy; + configuration.transport.retry_configuration = retry_configuration; } self @@ -465,6 +1016,29 @@ impl PubNubClientConfigBuilder { self } + /// Real-time events filtering expression. + /// + /// # Arguments + /// + /// * `expression` - A `String` representing the filter expression. + /// + /// # Returns + /// + /// [`PubNubClientConfigBuilder`] that you can use to set the configuration + /// for the client. This is a part of the [`PubNubClientConfigBuilder`]. + #[cfg(feature = "subscribe")] + pub fn with_filter_expression(self, expression: S) -> Self + where + S: Into, + { + if let Some(filter_expression) = &self.filter_expression { + let mut filter_expression = filter_expression.write(); + *filter_expression = expression.into(); + } + + self + } + /// Build a [`PubNubClient`] from the builder pub fn build(self) -> Result, D>, PubNubError> { self.build_internal() @@ -473,6 +1047,11 @@ impl PubNubClientConfigBuilder { }) .and_then(|pre_build| { let token = Arc::new(RwLock::new(String::new())); + #[cfg(all(feature = "subscribe", feature = "std"))] + let subscription = Arc::new(RwLock::new(None)); + #[cfg(all(feature = "presence", feature = "std"))] + let presence: Arc>> = Arc::new(RwLock::new(None)); + info!( "Client Configuration: \n publish_key: {:?}\n subscribe_key: {}\n user_id: {}\n instance_id: {:?}", pre_build.config.publish_key, @@ -480,6 +1059,7 @@ impl PubNubClientConfigBuilder { pre_build.config.user_id, pre_build.instance_id ); + Ok(PubNubClientRef { transport: PubNubMiddleware { signature_keys: pre_build.config.clone().signature_key_set()?, @@ -496,27 +1076,120 @@ impl PubNubClientConfigBuilder { config: pre_build.config, cryptor: pre_build.cryptor.clone(), - #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + #[cfg(feature = "subscribe")] + filter_expression: pre_build.filter_expression, + + #[cfg(feature = "presence")] + state: Arc::new(RwLock::new(HashMap::new())), + + #[cfg(feature = "std")] runtime: pre_build.runtime, #[cfg(all(feature = "subscribe", feature = "std"))] - subscription: Arc::new(RwLock::new(None)), + subscription: subscription.clone(), #[cfg(all(feature = "presence", feature = "std"))] - presence: Arc::new(RwLock::new(None)), + presence: presence.clone(), + + entities: RwLock::new(HashMap::new()), }) }) - .map(|client| PubNubClientInstance { - inner: Arc::new(client), + .map(|client| { + PubNubClientInstance { + inner: Arc::new(client), + + #[cfg(all(feature = "subscribe", feature = "std"))] + cursor: Default::default(), + + #[cfg(all(feature = "subscribe", feature = "std"))] + event_dispatcher: Default::default(), + } }) } } +/// Transport specific configuration +/// +/// Configuration let specify timeouts for two types of requests: +/// * `subscribe` - long-poll requests +/// * `non-subscribe` - any non-subscribe requests. +#[cfg(feature = "std")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransportConfiguration { + /// Timeout after which subscribe request will be cancelled by timeout. + pub subscribe_request_timeout: u64, + + /// Timeout after which any non-subscribe request will be cancelled by + /// timeout. + pub request_timeout: u64, + + /// Request automatic retry configuration. + /// + /// Automatic retry configuration contains a retry policy that should be + /// used to calculate retry delays and the number of attempts that + /// should be made. + pub(crate) retry_configuration: RequestRetryConfiguration, +} + +#[cfg(feature = "std")] +impl Default for TransportConfiguration { + fn default() -> Self { + Self { + subscribe_request_timeout: 310, + request_timeout: 10, + retry_configuration: RequestRetryConfiguration::None, + } + } +} + +/// `user_id` presence behaviour configuration. +/// +/// The configuration contains parameters to control when the timeout may occur +/// or whether any updates should be sent when leaving. +#[cfg(any(feature = "subscribe", feature = "presence"))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct PresenceConfiguration { + /// `user_id` presence heartbeat. + /// + /// Used to set the presence timeout period. It overrides the default value + /// of 300 seconds for Presence timeout. + pub heartbeat_value: u64, + + /// `user_id` presence announcement interval. + /// + /// Intervals at which `user_id` presence should be announced and should + /// follow this optimal formula: `heartbeat_value / 2 - 1`. + #[cfg(feature = "std")] + pub heartbeat_interval: Option, + + /// Whether `user_id` leave should be announced or not. + /// + /// When set to `true` and `user_id` will unsubscribe, the client wouldn't + /// announce `leave`, and as a result, there will be no `leave` presence + /// event generated. + /// + /// **Default:** `false` + pub suppress_leave_events: bool, +} + +#[cfg(any(feature = "subscribe", feature = "presence"))] +impl Default for PresenceConfiguration { + fn default() -> Self { + Self { + heartbeat_value: 300, + suppress_leave_events: false, + + #[cfg(feature = "std")] + heartbeat_interval: None, + } + } +} + /// PubNub configuration /// /// Configuration for [`PubNubClient`]. /// This struct separates the configuration from the actual client. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PubNubConfig { /// Subscribe key pub(crate) subscribe_key: String, @@ -533,23 +1206,22 @@ pub struct PubNubConfig { /// Authorization key pub(crate) auth_key: Option>, - /// Request retry policy - #[cfg(feature = "std")] - pub(crate) retry_policy: RequestRetryPolicy, - - /// `user_id` presence heartbeat. + /// Transport configuration. /// - /// Used to set the presence timeout period. It overrides the default value - /// of 300 seconds for Presence timeout. - #[cfg(any(feature = "subscribe", feature = "presence"))] - pub heartbeat_value: u64, + /// Configuration allow to configure request processing aspects like: + /// * timeout + /// * automatic retry. + #[cfg(feature = "std")] + pub transport: TransportConfiguration, - /// `user_id` presence announcement interval. + /// Presence configuration. /// - /// Intervals at which `user_id` presence should be announced and should - /// follow this optimal formula: `heartbeat_value / 2 - 1`. + /// The configuration allows you to set up `user_id` channels presence: + /// * the period after which an `user_id` _timeout_ event will occur + /// * how often the server should be notified about user presence + /// * whether `user_id` _leave_ event should be announced or not. #[cfg(any(feature = "subscribe", feature = "presence"))] - pub heartbeat_interval: Option, + pub presence: PresenceConfiguration, } impl PubNubConfig { @@ -594,7 +1266,7 @@ pub struct PubNubClientBuilder; impl PubNubClientBuilder { /// Set the transport layer for the client. /// - /// Returns [`PubNubClientRuntimeBuilder`] where depending from enabled + /// Returns [`PubNubClientRuntimeBuilder`] where depending on from enabled /// `features` following can be set: /// * runtime environment /// * API ket set to access [`PubNub API`]. @@ -644,8 +1316,8 @@ impl PubNubClientBuilder { /// Set the transport layer for the client. /// - /// Returns [`PubNubClientDeserializerBuilder`] where depending from enabled - /// `features` following can be set: + /// Returns [`PubNubClientDeserializerBuilder`] where depending on from + /// enabled `features` following can be set: /// * [`PubNub API`] response deserializer /// * API ket set to access [`PubNub API`]. /// @@ -694,7 +1366,7 @@ impl PubNubClientBuilder { /// Set the blocking transport layer for the client. /// - /// Returns [`PubNubClientRuntimeBuilder`] where depending from enabled + /// Returns [`PubNubClientRuntimeBuilder`] where depending on from enabled /// `features` following can be set: /// * runtime environment /// * API ket set to access [`PubNub API`]. @@ -747,8 +1419,8 @@ impl PubNubClientBuilder { /// Set the blocking transport layer for the client. /// - /// Returns [`PubNubClientDeserializerBuilder`] where depending from enabled - /// `features` following can be set: + /// Returns [`PubNubClientDeserializerBuilder`] where depending on from + /// enabled `features` following can be set: /// * [`PubNub API`] response deserializer /// * API ket set to access [`PubNub API`]. /// @@ -867,7 +1539,7 @@ impl PubNubClientKeySetBuilder { /// /// Runtime will be used for detached tasks spawning and delayed task execution. /// -/// Depending from enabled `features` methods may return: +/// Depending on from enabled `features` methods may return: /// * [`PubNubClientDeserializerBuilder`] to set custom [`PubNub API`] /// deserializer /// * [`PubNubClientKeySetBuilder`] to set API keys set to access [`PubNub API`] @@ -884,8 +1556,8 @@ pub struct PubNubClientRuntimeBuilder { impl PubNubClientRuntimeBuilder { /// Set runtime environment. /// - /// Returns [`PubNubClientDeserializerBuilder`] where depending from enabled - /// `features` following can be set: + /// Returns [`PubNubClientDeserializerBuilder`] where depending on from + /// enabled `features` following can be set: /// * [`PubNub API`] response deserializer /// * API ket set to access [`PubNub API`]. /// @@ -910,6 +1582,10 @@ impl PubNubClientRuntimeBuilder { /// # async fn sleep(self, _delay: u64) { /// # // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await /// # } + /// # + /// # async fn sleep_microseconds(self, delay: u64) { + /// # // e.g. tokio::time::sleep(tokio::time::Duration::from_micros(delay)).await + /// # } /// # } /// /// # fn main() -> Result<(), Box> { @@ -968,6 +1644,10 @@ impl PubNubClientRuntimeBuilder { /// # async fn sleep(self, _delay: u64) { /// # // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await /// # } + /// # + /// # async fn sleep_microseconds(self, delay: u64) { + /// # // e.g. tokio::time::sleep(tokio::time::Duration::from_micros(delay)).await + /// # } /// # } /// /// # fn main() -> Result<(), Box> { @@ -1206,7 +1886,8 @@ impl PubNubClientUserIdBuilder where S: Into, { - /// Set UUID for the client + /// Set user id for the client. + /// /// It returns [`PubNubClientConfigBuilder`] that you can use /// to set the configuration for the client. This is a part /// the PubNubClientConfigBuilder. @@ -1229,13 +1910,10 @@ where auth_key: None, #[cfg(feature = "std")] - retry_policy: Default::default(), - - #[cfg(any(feature = "subscribe", feature = "presence"))] - heartbeat_value: 300, + transport: Default::default(), #[cfg(any(feature = "subscribe", feature = "presence"))] - heartbeat_interval: None, + presence: Default::default(), }), #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] @@ -1323,12 +2001,12 @@ mod should { secret_key: Some("sec_key".into()), user_id: Arc::new("".into()), auth_key: None, + #[cfg(feature = "std")] - retry_policy: Default::default(), - #[cfg(any(feature = "subscribe", feature = "presence"))] - heartbeat_value: 300, + transport: Default::default(), + #[cfg(any(feature = "subscribe", feature = "presence"))] - heartbeat_interval: None, + presence: Default::default(), }; assert!(config.signature_key_set().is_err()); diff --git a/src/dx/subscribe/builders/mod.rs b/src/dx/subscribe/builders/mod.rs index 04f5c226..a6e82abf 100644 --- a/src/dx/subscribe/builders/mod.rs +++ b/src/dx/subscribe/builders/mod.rs @@ -9,13 +9,6 @@ pub(crate) use subscribe::SubscribeRequestBuilder; pub(crate) mod subscribe; -#[cfg(feature = "std")] -#[doc(inline)] -pub use subscription::{SubscriptionBuilder, SubscriptionBuilderError}; - -#[cfg(feature = "std")] -pub mod subscription; - pub mod raw; use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; diff --git a/src/dx/subscribe/builders/raw.rs b/src/dx/subscribe/builders/raw.rs index 9e428d34..06ad01ae 100644 --- a/src/dx/subscribe/builders/raw.rs +++ b/src/dx/subscribe/builders/raw.rs @@ -18,7 +18,7 @@ use crate::{ core::{blocking, Deserializer, PubNubError, Transport}, dx::{ pubnub_client::PubNubClientInstance, - subscribe::{SubscribeCursor, Update}, + subscribe::{SubscriptionCursor, Update}, }, lib::alloc::{collections::VecDeque, string::String, string::ToString, vec::Vec}, }; @@ -90,12 +90,8 @@ pub struct RawSubscription { /// announced for `user_id`. /// /// By default it is set to **300** seconds. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(strip_option), - default = "Some(300)" - )] - pub(in crate::dx::subscribe) heartbeat: Option, + #[builder(field(vis = "pub(in crate::dx::subscribe)"))] + pub(in crate::dx::subscribe) heartbeat: u64, /// Message filtering predicate. /// @@ -183,7 +179,7 @@ where pub fn stream(self) -> impl futures::Stream> { let cursor = self .cursor - .map(|tt| SubscribeCursor { + .map(|tt| SubscriptionCursor { timetoken: tt.to_string(), region: 0, }) @@ -203,11 +199,8 @@ where .subscribe_request() .cursor(ctx.cursor.clone()) .channels(ctx.subscription.channels.clone()) - .channel_groups(ctx.subscription.channel_groups.clone()); - - if let Some(heartbeat) = ctx.subscription.heartbeat { - request = request.heartbeat(heartbeat); - } + .channel_groups(ctx.subscription.channel_groups.clone()) + .heartbeat(ctx.subscription.heartbeat); if let Some(filter_expr) = ctx.subscription.filter_expression.clone() { request = request.filter_expression(filter_expr); @@ -246,7 +239,7 @@ where pub fn iter(self) -> RawSubscriptionIter { let cursor = self .cursor - .map(|tt| SubscribeCursor { + .map(|tt| SubscriptionCursor { timetoken: tt.to_string(), region: 0, }) @@ -279,11 +272,8 @@ where .subscribe_request() .cursor(ctx.cursor.clone()) .channels(ctx.subscription.channels.clone()) - .channel_groups(ctx.subscription.channel_groups.clone()); - - if let Some(heartbeat) = ctx.subscription.heartbeat { - request = request.heartbeat(heartbeat); - } + .channel_groups(ctx.subscription.channel_groups.clone()) + .heartbeat(ctx.subscription.heartbeat); if let Some(filter_expr) = ctx.subscription.filter_expression.clone() { request = request.filter_expression(filter_expr); @@ -322,7 +312,7 @@ where struct SubscriptionContext { subscription: RawSubscription, - cursor: SubscribeCursor, + cursor: SubscriptionCursor, messages: VecDeque>, } @@ -377,6 +367,7 @@ mod should { fn sut() -> RawSubscriptionBuilder, DeserializerSerde> { RawSubscriptionBuilder { pubnub_client: Some(client()), + heartbeat: Some(300), ..Default::default() } } diff --git a/src/dx/subscribe/builders/subscribe.rs b/src/dx/subscribe/builders/subscribe.rs index 7dd57095..0bfc769e 100644 --- a/src/dx/subscribe/builders/subscribe.rs +++ b/src/dx/subscribe/builders/subscribe.rs @@ -21,7 +21,7 @@ use crate::{ }, dx::{ pubnub_client::PubNubClientInstance, - subscribe::{builders, result::SubscribeResult, SubscribeCursor, SubscribeResponseBody}, + subscribe::{builders, result::SubscribeResult, SubscribeResponseBody, SubscriptionCursor}, }, lib::{ alloc::{ @@ -33,6 +33,8 @@ use crate::{ }, }; +#[cfg(all(feature = "presence", feature = "std"))] +use crate::lib::alloc::vec; #[cfg(feature = "std")] use crate::{core::event_engine::cancel::CancellationTask, lib::alloc::sync::Arc}; @@ -84,7 +86,36 @@ pub(crate) struct SubscribeRequest { setter(strip_option), default = "Default::default()" )] - pub(in crate::dx::subscribe) cursor: SubscribeCursor, + pub(in crate::dx::subscribe) cursor: SubscriptionCursor, + + /// A state that should be associated with the `user_id`. + /// + /// `state` object should be a `HashMap` with channel names as keys and + /// serialized `state` as values. State with heartbeat can be set **only** + /// for channels. + /// + /// # Example: + /// ```rust,no_run + /// # use std::collections::HashMap; + /// # use pubnub::core::Serialize; + /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// let state = HashMap::>::from([( + /// "announce".to_string(), + /// HashMap::::from([ + /// ("is_owner".to_string(), false), + /// ("is_admin".to_string(), true) + /// ]).serialize()? + /// )]); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "presence")] + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(custom, strip_option), + default = "None" + )] + pub(in crate::dx::subscribe) state: Option>, /// `user_id`presence timeout period. /// @@ -95,12 +126,8 @@ pub(crate) struct SubscribeRequest { /// announced for `user_id`. /// /// By default it is set to **300** seconds. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(strip_option), - default = "300" - )] - pub(in crate::dx::subscribe) heartbeat: u32, + #[builder(field(vis = "pub(in crate::dx::subscribe)"))] + pub(in crate::dx::subscribe) heartbeat: u64, /// Message filtering predicate. /// @@ -119,6 +146,28 @@ pub(crate) struct SubscribeRequest { } impl SubscribeRequestBuilder { + /// A state that should be associated with the `user_id`. + /// + /// `state` object should be a `HashMap` with channel names as keys and + /// nested `HashMap` with values. State with subscribe can be set **only** + /// for channels. + #[cfg(all(feature = "presence", feature = "std"))] + pub(in crate::dx::subscribe) fn state(mut self, state: HashMap>) -> Self { + let mut serialized_state = vec![b'{']; + for (key, mut value) in state { + serialized_state.append(&mut format!("\"{}\":", key).as_bytes().to_vec()); + serialized_state.append(&mut value); + serialized_state.push(b','); + } + if serialized_state.last() == Some(&b',') { + serialized_state.pop(); + } + serialized_state.push(b'}'); + + self.state = Some(Some(serialized_state)); + self + } + /// Validate user-provided data for request builder. /// /// Validator ensure that list of provided data is enough to build valid @@ -145,8 +194,11 @@ impl SubscribeRequestBuilder { impl SubscribeRequest { /// Create transport request from the request builder. - pub(in crate::dx::subscribe) fn transport_request(&self) -> TransportRequest { - let sub_key = &self.pubnub_client.config.subscribe_key; + pub(in crate::dx::subscribe) fn transport_request( + &self, + ) -> Result { + let config = &self.pubnub_client.config; + let sub_key = &config.subscribe_key; let mut query: HashMap = HashMap::new(); query.extend::>(self.cursor.clone().into()); @@ -154,6 +206,15 @@ impl SubscribeRequest { url_encoded_channel_groups(&self.channel_groups) .and_then(|groups| query.insert("channel-group".into(), groups)); + #[cfg(feature = "presence")] + if let Some(state) = self.state.as_ref() { + let state_json = + String::from_utf8(state.clone()).map_err(|err| PubNubError::Serialization { + details: err.to_string(), + })?; + query.insert("state".into(), state_json); + } + self.filter_expression .as_ref() .filter(|e| !e.is_empty()) @@ -166,31 +227,41 @@ impl SubscribeRequest { query.insert("heartbeat".into(), self.heartbeat.to_string()); - TransportRequest { + Ok(TransportRequest { path: format!( "/v2/subscribe/{sub_key}/{}/0", url_encoded_channels(&self.channels) ), query_parameters: query, method: TransportMethod::Get, + #[cfg(feature = "std")] + timeout: config.transport.subscribe_request_timeout, ..Default::default() - } + }) } } impl SubscribeRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. pub async fn execute(self) -> Result { let request = self.request()?; - let transport_request = request.transport_request(); + let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request - .send::(&client.transport, deserializer) + .send::( + &client.transport, + deserializer, + #[cfg(feature = "std")] + &client.config.transport.retry_configuration, + #[cfg(feature = "std")] + &client.runtime, + ) .await } @@ -241,7 +312,7 @@ where .build() .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; - let transport_request = request.transport_request(); + let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); transport_request diff --git a/src/dx/subscribe/builders/subscription.rs b/src/dx/subscribe/builders/subscription.rs deleted file mode 100644 index fd7e4291..00000000 --- a/src/dx/subscribe/builders/subscription.rs +++ /dev/null @@ -1,596 +0,0 @@ -//! Subscription module. -//! -//! Subscription module is responsible for handling real-time updates from -//! PubNub. It is responsible for handshake and receiving messages. -//! It is also responsible for delivering messages to the user. - -use derive_builder::Builder; -use futures::Stream; -use spin::RwLock; -use uuid::Uuid; - -use crate::subscribe::event_engine::SubscribeInput; -use crate::{ - core::PubNubError, - dx::subscribe::{result::Update, types::SubscribeStreamEvent, SubscribeStatus}, - lib::{ - alloc::{ - collections::VecDeque, - string::{String, ToString}, - sync::Arc, - vec::Vec, - }, - core::{ - fmt::{Debug, Formatter}, - ops::{Deref, DerefMut}, - pin::Pin, - task::{Context, Poll, Waker}, - }, - }, - subscribe::SubscriptionManager, -}; - -/// Subscription stream. -/// -/// Stream delivers changes in subscription status: -/// * `connected` - client connected to real-time [`PubNub`] network. -/// * `disconnected` - client has been disconnected from real-time [`PubNub`] -/// network. -/// * `connection error` - client was unable to subscribe to specified channels -/// and groups -/// -/// and regular messages / signals. -/// -/// [`PubNub`]:https://www.pubnub.com/ -#[derive(Debug)] -pub struct SubscriptionStream { - inner: Arc>, -} - -impl Deref for SubscriptionStream { - type Target = SubscriptionStreamRef; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for SubscriptionStream { - fn deref_mut(&mut self) -> &mut Self::Target { - Arc::get_mut(&mut self.inner).expect("Subscription stream is not unique") - } -} - -impl Clone for SubscriptionStream { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -/// Subscription stream. -/// -/// Stream delivers changes in subscription status: -/// * `connected` - client connected to real-time [`PubNub`] network. -/// * `disconnected` - client has been disconnected from real-time [`PubNub`] -/// network. -/// * `connection error` - client was unable to subscribe to specified channels -/// and groups -/// -/// and regular messages / signals. -/// -/// [`PubNub`]:https://www.pubnub.com/ -#[derive(Debug, Default)] -pub struct SubscriptionStreamRef { - /// Update to be delivered to stream listener. - updates: RwLock>, - - /// Subscription stream waker. - /// - /// Handler used each time when new data available for a stream listener. - waker: RwLock>, - - /// Whether stream still valid or not. - is_valid: bool, -} - -/// Subscription that is responsible for getting messages from PubNub. -/// -/// Subscription provides a way to get messages from PubNub. It is responsible -/// for handshake and receiving messages. -#[derive(Debug)] -pub struct Subscription { - inner: Arc, -} - -impl Deref for Subscription { - type Target = SubscriptionRef; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for Subscription { - fn deref_mut(&mut self) -> &mut Self::Target { - Arc::get_mut(&mut self.inner).expect("Subscription is not unique") - } -} - -impl Clone for Subscription { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -/// Subscription that is responsible for getting messages from PubNub. -/// -/// Subscription provides a way to get messages from PubNub. It is responsible -/// for handshake and receiving messages. -/// -/// It should not be created directly, but via [`PubNubClient::subscribe`] -/// and wrapped in [`Subscription`] struct. -#[derive(Builder)] -#[builder( - pattern = "owned", - name = "SubscriptionBuilder", - build_fn(private, name = "build_internal", validate = "Self::validate"), - no_std -)] -pub struct SubscriptionRef { - /// Subscription module configuration. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom, strip_option) - )] - pub(in crate::dx::subscribe) subscription: Arc>>, - - /// User input with channels and groups. - /// - /// Object contains list of channels and channel groups on which - /// [`PubNubClient`] will subscribe and notify about received real-time - /// updates. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom), - default = "SubscribeInput::new(&None, &None)" - )] - pub(in crate::dx::subscribe) input: SubscribeInput, - - /// Time cursor. - /// - /// Cursor used by subscription loop to identify point in time after - /// which updates will be delivered. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(strip_option), - default = "Default::default()" - )] - pub(in crate::dx::subscribe) cursor: Option, - - /// Heartbeat interval. - /// - /// Interval in seconds that informs the server that the client should - /// be considered alive. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(strip_option), - default = "Some(300)" - )] - pub(in crate::dx::subscribe) heartbeat: Option, - - /// Expression used to filter received messages. - /// - /// Expression used to filter received messages before they are delivered - /// to the client. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(strip_option, into), - default = "None" - )] - pub(in crate::dx::subscribe) filter_expression: Option, - - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom), - default = "Uuid::new_v4().to_string()" - )] - pub(in crate::dx::subscribe) id: String, - - /// List of updates to be delivered to stream listener. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom), - default = "RwLock::new(VecDeque::with_capacity(100))" - )] - pub(in crate::dx::subscribe) updates: RwLock>, - - /// General subscription stream. - /// - /// Stream used to deliver all real-time updates. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom), - default = "RwLock::new(None)" - )] - pub(in crate::dx::subscribe) stream: RwLock>>, - - /// Messages / updates stream. - /// - /// Stream used to deliver only real-time updates. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom), - default = "RwLock::new(None)" - )] - pub(in crate::dx::subscribe) updates_stream: RwLock>>, - - /// Status stream. - /// - /// Stream used to deliver only subscription status changes. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(custom), - default = "RwLock::new(None)" - )] - pub(in crate::dx::subscribe) status_stream: RwLock>>, -} - -impl SubscriptionBuilder { - /// Validate user-provided data for request builder. - /// - /// Validator ensure that list of provided data is enough to build valid - /// request instance. - fn validate(&self) -> Result<(), String> { - let input = self - .input - .as_ref() - .unwrap_or_else(|| panic!("Subscription input should be set by default")); - - if input.is_empty { - return Err("Either channels or channel groups should be provided".into()); - } - - Ok(()) - } - /// Channels from which real-time updates should be received. - /// - /// List of channels on which [`PubNubClient`] will subscribe and notify - /// about received real-time updates. - pub fn channels(mut self, channels: L) -> Self - where - L: Into>, - { - let user_input = SubscribeInput::new(&Some(channels.into()), &None); - if let Some(input) = self.input { - self.input = Some(input + user_input) - } else { - self.input = Some(user_input); - } - - self - } - - /// Channel groups from which real-time updates should be received. - /// - /// List of groups of channels on which [`PubNubClient`] will subscribe and - /// notify about received real-time updates. - pub fn channel_groups(mut self, channel_groups: L) -> Self - where - L: Into>, - { - let user_input = SubscribeInput::new(&None, &Some(channel_groups.into())); - if let Some(input) = self.input { - self.input = Some(input + user_input) - } else { - self.input = Some(user_input); - } - - self - } -} - -impl SubscriptionBuilder { - /// Construct subscription object. - pub fn execute(self) -> Result { - self.build_internal() - .map(|subscription| Subscription { - inner: Arc::new(subscription), - }) - .map(|subscription| { - if let Some(manager) = subscription.subscription.write().as_mut() { - manager.register(subscription.clone()) - } - subscription - }) - .map_err(|e| PubNubError::SubscribeInitialization { - details: e.to_string(), - }) - } -} - -impl Debug for SubscriptionRef { - fn fmt(&self, f: &mut Formatter<'_>) -> crate::lib::core::fmt::Result { - write!( - f, - "Subscription {{ \nchannels: {:?}, \nchannel-groups: {:?}, \ncursor: {:?}, \nheartbeat: {:?}, \nfilter_expression: {:?}}}", - self.input.channels(), self.input.channel_groups(), self.cursor, self.heartbeat, self.filter_expression - ) - } -} - -impl Subscription { - /// Unsubscribed current subscription. - /// - /// Cancel current subscription and remove it from the list of active - /// subscriptions. - /// - /// # Examples - /// ``` - /// ``` - pub async fn unsubscribe(self) { - if let Some(manager) = self.subscription.write().as_mut() { - manager.unregister(self.clone()) - } - } - - /// Stream of all subscription updates. - /// - /// Stream is used to deliver following updates: - /// * received messages / updates - /// * changes in subscription status - pub fn stream(&self) -> SubscriptionStream { - let mut stream = self.stream.write(); - - if let Some(stream) = stream.clone() { - stream - } else { - let events_stream = { - let mut updates = self.updates.write(); - let stream = SubscriptionStream::new(updates.clone()); - updates.clear(); - stream - }; - - *stream = Some(events_stream.clone()); - - events_stream - } - } - - /// Stream with message / updates. - /// - /// Stream will deliver filtered set of updates which include only messages. - pub fn message_stream(&self) -> SubscriptionStream { - let mut stream = self.updates_stream.write(); - - if let Some(stream) = stream.clone() { - stream - } else { - let events_stream = { - let mut updates = self.updates.write(); - let updates_len = updates.len(); - let stream_updates = updates.iter().fold( - VecDeque::::with_capacity(updates_len), - |mut acc, event| { - if let SubscribeStreamEvent::Update(update) = event { - acc.push_back(update.clone()); - } - acc - }, - ); - - let stream = SubscriptionStream::new(stream_updates); - updates.clear(); - - stream - }; - - *stream = Some(events_stream.clone()); - events_stream - } - } - - /// Stream with subscription status updates. - /// - /// Stream will deliver filtered set of updates which include only - /// subscription status change. - pub fn status_stream(&self) -> SubscriptionStream { - let mut stream = self.status_stream.write(); - - if let Some(stream) = stream.clone() { - stream - } else { - let events_stream = { - let mut updates = self.updates.write(); - let updates_len = updates.len(); - let stream_statuses = updates.iter().fold( - VecDeque::::with_capacity(updates_len), - |mut acc, event| { - if let SubscribeStreamEvent::Status(update) = event { - acc.push_back(update.clone()); - } - acc - }, - ); - - let stream = SubscriptionStream::new(stream_statuses); - updates.clear(); - - stream - }; - - *stream = Some(events_stream.clone()); - events_stream - } - } - - /// Handle received real-time updates. - pub(in crate::dx::subscribe) fn handle_messages(&self, messages: &[Update]) { - // Filter out updates for this subscriber. - let messages = messages - .iter() - .cloned() - .filter(|update| self.subscribed_for_update(update)) - .collect::>(); - - let common_stream = self.stream.read(); - let stream = self.updates_stream.read(); - let accumulate = common_stream.is_none() && stream.is_none(); - - if accumulate { - let mut updates_slot = self.updates.write(); - updates_slot.extend(messages.into_iter().map(SubscribeStreamEvent::Update)); - } else { - if let Some(stream) = common_stream.clone() { - let mut updates_slot = stream.updates.write(); - let updates_len = updates_slot.len(); - updates_slot.extend( - messages - .clone() - .into_iter() - .map(SubscribeStreamEvent::Update), - ); - updates_slot - .len() - .ne(&updates_len) - .then(|| stream.wake_task()); - } - - if let Some(stream) = stream.clone() { - let mut updates_slot = stream.updates.write(); - let updates_len = updates_slot.len(); - updates_slot.extend(messages); - updates_slot - .len() - .ne(&updates_len) - .then(|| stream.wake_task()); - } - } - } - - /// Handle received real-time updates. - pub(in crate::dx::subscribe) fn handle_status(&self, status: SubscribeStatus) { - let common_stream = self.stream.read(); - let stream = self.status_stream.read(); - let accumulate = common_stream.is_none() && stream.is_none(); - - if accumulate { - let mut updates_slot = self.updates.write(); - updates_slot.push_back(SubscribeStreamEvent::Status(status)); - } else { - if let Some(stream) = common_stream.clone() { - let mut updates_slot = stream.updates.write(); - let updates_len = updates_slot.len(); - updates_slot.push_back(SubscribeStreamEvent::Status(status.clone())); - updates_slot - .len() - .ne(&updates_len) - .then(|| stream.wake_task()); - } - - if let Some(stream) = stream.clone() { - let mut updates_slot = stream.updates.write(); - let updates_len = updates_slot.len(); - updates_slot.push_back(status.clone()); - updates_slot - .len() - .ne(&updates_len) - .then(|| stream.wake_task()); - } - } - } - - fn subscribed_for_update(&self, update: &Update) -> bool { - let subscription = &update.subscription(); - self.input.contains_channel(subscription) || self.input.contains_channel_group(subscription) - } - - /// Invalidate all streams. - pub(crate) fn invalidate(&mut self) { - let mut stream_slot = self.stream.write(); - if let Some(mut stream) = stream_slot.clone() { - stream.invalidate() - } - *stream_slot = None; - - let mut stream_slot = self.status_stream.write(); - if let Some(mut stream) = stream_slot.clone() { - stream.invalidate() - } - *stream_slot = None; - - let mut stream_slot = self.updates_stream.write(); - if let Some(mut stream) = stream_slot.clone() { - stream.invalidate() - } - *stream_slot = None; - } -} - -impl SubscriptionStream { - fn new(updates: VecDeque) -> Self { - let mut stream_updates = VecDeque::with_capacity(100); - stream_updates.extend(updates); - - Self { - inner: Arc::new(SubscriptionStreamRef { - updates: RwLock::new(stream_updates), - waker: RwLock::new(None), - is_valid: true, - }), - } - } - - pub(crate) fn invalidate(&mut self) { - self.is_valid = false; - self.wake_task(); - } - - fn wake_task(&self) { - if let Some(waker) = self.waker.write().take() { - waker.wake(); - } - } -} - -impl Stream for SubscriptionStream { - type Item = D; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if !self.is_valid { - return Poll::Ready(None); - } - - let mut waker_slot = self.waker.write(); - *waker_slot = Some(cx.waker().clone()); - - if let Some(update) = self.updates.write().pop_front() { - Poll::Ready(Some(update)) - } else { - Poll::Pending - } - } -} -// -// impl Subscription { -// pub(crate) fn subscribe() -> Self { -// // // TODO: implementation is a part of the different task -// // let handshake: HandshakeFunction = |&_, &_, _, _| Ok(vec![]); -// // let receive: ReceiveFunction = |&_, &_, &_, _, _| Ok(vec![]); -// // -// // Self { -// // engine: SubscribeEngine::new( -// // SubscribeEffectHandler::new(handshake, receive), -// // SubscribeState::Unsubscribed, -// // ), -// // } -// Self { /* fields */ } -// } -// } - -#[cfg(test)] -mod should {} diff --git a/src/dx/subscribe/event_dispatcher.rs b/src/dx/subscribe/event_dispatcher.rs new file mode 100644 index 00000000..dfd603a7 --- /dev/null +++ b/src/dx/subscribe/event_dispatcher.rs @@ -0,0 +1,550 @@ +//! # Event dispatcher module +//! +//! This module contains the [`EventDispatcher`] type, which is used by +//! [`PubNubClientInstance`], [`Subscription2`] and [`SubscriptionSet`] to let +//! users attach listeners to the specific event types. + +use spin::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::fmt::Debug; + +use crate::{ + core::DataStream, + lib::{ + alloc::{collections::VecDeque, vec::Vec}, + core::{default::Default, ops::Drop}, + }, + subscribe::{ + AppContext, ConnectionStatus, EventEmitter, File, Message, MessageAction, Presence, + SubscribeStreamEvent, Update, + }, +}; + +#[derive(Debug)] +pub(crate) struct EventDispatcher { + /// Whether listener streams has been created or not. + has_streams: RwLock, + + /// A collection of data streams for message events. + /// + /// This struct holds a vector of `DataStream` instances, which + /// provide a way to handle message events in a streaming fashion. + pub(crate) message_streams: RwLock>>>, + + /// A collection of data streams for signal events. + /// + /// This struct holds a vector of `DataStream` instances, which + /// provide a way to handle signal events in a streaming fashion. + pub(crate) signal_streams: RwLock>>>, + + /// A collection of data streams for message reaction events. + /// + /// This struct holds a vector of `DataStream` instances, + /// which provide a way to handle message reactions events in a streaming + /// fashion. + pub(crate) message_reaction_streams: RwLock>>>, + + /// A collection of data streams for file events. + /// + /// This struct holds a vector of `DataStream` instances, which + /// provide a way to handle file events in a streaming fashion. + pub(crate) file_streams: RwLock>>>, + + /// A collection of data streams for application context (Channel and User) + /// events. + /// + /// This struct holds a vector of `DataStream` instances, + /// which allow for handling application context events in a streaming + /// fashion. + pub(crate) app_context_streams: RwLock>>>, + + /// A collection of data streams for presence events. + /// + /// This struct holds a vector of `DataStream` instances, which + /// provide a way to handle presence events in a streaming fashion. + pub(crate) presence_streams: RwLock>>>, + + /// A collection of data streams for connection status change events. + /// + /// This struct holds a vector of `DataStream` instances, + /// which provide a way to handle connection status change events in a + /// streaming fashion. + pub(crate) status_streams: RwLock>>>, + + /// A collection of data streams for update events. + /// + /// This struct holds a vector of `DataStream` instances, which + /// provide a way to handle update events in a streaming fashion. + pub(crate) streams: RwLock>>>, + + /// List of updates to be delivered to stream listener. + pub(crate) updates: RwLock>, +} + +impl EventDispatcher { + /// Create event dispatcher instance. + /// + /// Dispatcher, responsible for handling status and events and pushing them + /// to the specific data streams that listen to them. Internal event queues + /// prevent situations when events have been received before any listener + /// has been attached (as soon as there will be at least one listener, the + /// queue won't be filled). + /// + /// # Returns + /// + /// Returns [`EventDispatcher`] instance with pre-configured set of data + /// streams. + pub(crate) fn new() -> Self { + Self { + has_streams: Default::default(), + message_streams: Default::default(), + signal_streams: Default::default(), + message_reaction_streams: Default::default(), + file_streams: Default::default(), + app_context_streams: Default::default(), + presence_streams: Default::default(), + status_streams: Default::default(), + streams: Default::default(), + updates: RwLock::new(VecDeque::with_capacity(100)), + } + } + pub fn status_stream(&self) -> DataStream { + let statuses = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Status(status) => Some(status.clone()), + _ => None, + }); + + self.create_stream_in_list(self.status_streams.write(), statuses) + } + + /// Dispatch received connection status change. + /// + /// Dispatch events to the designated stream types. + pub fn handle_status(&self, status: ConnectionStatus) { + if !*self.has_streams.read() { + let mut updates_slot = self.updates.write(); + updates_slot.push_back(SubscribeStreamEvent::Status(status)); + return; + } + + self.push_event_to_stream(&status, &self.status_streams.read()); + } + + /// Dispatch received updates. + /// + /// Dispatch events to the designated stream types. + pub fn handle_events(&self, events: Vec) { + if !*self.has_streams.read() { + let mut updates_slot = self.updates.write(); + updates_slot.extend(events.into_iter().map(SubscribeStreamEvent::Update)); + return; + } + + let message_streams = self.message_streams.read(); + let signal_streams = self.signal_streams.read(); + let message_reactions_streams = self.message_reaction_streams.read(); + let file_streams = self.file_streams.read(); + let app_context_streams = self.app_context_streams.read(); + let presence_streams = self.presence_streams.read(); + let streams = self.streams.read(); + + for event in events { + match event.clone() { + Update::Message(message) if message_streams.is_some() => { + self.push_event_to_stream(&message, &message_streams) + } + Update::Signal(signal) if signal_streams.is_some() => { + self.push_event_to_stream(&signal, &signal_streams) + } + Update::MessageAction(action) if message_reactions_streams.is_some() => { + self.push_event_to_stream(&action, &message_reactions_streams) + } + Update::File(file) if file_streams.is_some() => { + self.push_event_to_stream(&file, &file_streams) + } + Update::AppContext(object) if app_context_streams.is_some() => { + self.push_event_to_stream(&object, &app_context_streams) + } + Update::Presence(presence) if presence_streams.is_some() => { + self.push_event_to_stream(&presence, &presence_streams) + } + _ => {} + } + + self.push_event_to_stream(&event, &streams); + } + } + + /// Create a new `DataStream` and add it to the given list of streams. + /// + /// # Arguments + /// + /// - `streams`: A mutable reference to an `Option>>`, + /// representing the list of streams. + /// + /// # Returns + /// + /// Returns the newly created `DataStream`. + fn create_stream_in_list( + &self, + mut streams: RwLockWriteGuard>>>, + data: Option>, + ) -> DataStream + where + S: Debug, + { + let mut has_streams_slot = self.has_streams.write(); + *has_streams_slot = true; + let stream = if let Some(data) = data { + DataStream::with_queue_data(data, 100) + } else { + DataStream::new() + }; + + if let Some(streams) = streams.as_mut() { + streams.push(stream.clone()) + } else { + *streams = Some(vec![stream.clone()]) + } + + stream + } + + /// Pushes an event to each stream in the provided `streams`. + /// + /// # Arguments + /// + /// * `event` - A reference to the event to be pushed to the streams. + /// * `streams` - A read-only lock guard that provides access to the option + /// containing the list of data streams. + fn push_event_to_stream( + &self, + event: &S, + streams: &RwLockReadGuard>>>, + ) where + S: Clone, + { + let Some(streams) = streams.as_ref() else { + return; + }; + + streams + .iter() + .for_each(|stream| stream.push_data(event.clone())) + } + + /// Dequeues and returns a vector of matching events from the queue. + /// + /// The `dequeue_matching_events` function takes a closure `condition` as + /// input, which is used to determine whether an event matches the + /// condition. The closure should accept a reference to a + /// `SubscribeStreamEvent` and return a `bool`. + /// + /// If the event queue is not empty, the function iterates over the events + /// in the queue and checks if each event matches the condition. If an + /// event matches the condition, it is removed from the queue and added + /// to a new `VecDeque` called `filtered`. + /// + /// After iterating over all events, if no events match the condition, the + /// function returns `None`. Otherwise, it returns `Some(filtered)`, + /// where `filtered` is a `VecDeque` containing the matching events. + /// + /// # Arguments + /// + /// * `condition` - A closure that determines whether an event matches the + /// condition. + /// It should accept a reference to a `SubscribeStreamEvent` and return a + /// `bool`. + /// + /// # Returns + /// + /// An `Option>` - `Some(filtered)` if there are matching + /// events, or `None` if no events match the condition. + fn dequeue_matching_events(&self, condition_map: C) -> Option> + where + C: Fn(&SubscribeStreamEvent) -> Option, + { + let mut updates = self.updates.write(); + let mut events: Option> = None; + + if !updates.is_empty() { + let mut filtered = VecDeque::with_capacity(100); + let mut idx: usize = 0; + + while idx != updates.len() { + if let Some(update) = condition_map(&updates[idx]) { + updates.remove(idx); + filtered.push_back(update); + } else { + idx += 1; + } + } + + (!filtered.is_empty()).then(|| events = Some(filtered)); + } + + events + } + + /// Invalidates all streams in the instance. + pub(crate) fn invalidate(&self) { + let mut has_streams_slot = self.has_streams.write(); + if !*has_streams_slot { + return; + } + *has_streams_slot = false; + + if let Some(streams) = self.message_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.signal_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.message_reaction_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.file_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.app_context_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.presence_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.status_streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + + if let Some(streams) = self.streams.write().as_mut() { + streams.iter_mut().for_each(|stream| stream.invalidate()); + streams.clear(); + } + } +} + +impl Default for EventDispatcher { + fn default() -> Self { + Self::new() + } +} + +impl Drop for EventDispatcher { + fn drop(&mut self) { + self.invalidate() + } +} + +impl EventEmitter for EventDispatcher { + fn messages_stream(&self) -> DataStream { + let messages = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(Update::Message(message)) => Some(message.clone()), + _ => None, + }); + + self.create_stream_in_list(self.message_streams.write(), messages) + } + + fn signals_stream(&self) -> DataStream { + let signals = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(Update::Signal(signal)) => Some(signal.clone()), + _ => None, + }); + + self.create_stream_in_list(self.signal_streams.write(), signals) + } + + fn message_actions_stream(&self) -> DataStream { + let reactions = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(Update::MessageAction(reaction)) => Some(reaction.clone()), + _ => None, + }); + + self.create_stream_in_list(self.message_reaction_streams.write(), reactions) + } + + fn files_stream(&self) -> DataStream { + let files = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(Update::File(file)) => Some(file.clone()), + _ => None, + }); + + self.create_stream_in_list(self.file_streams.write(), files) + } + + fn app_context_stream(&self) -> DataStream { + let app_context = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(Update::AppContext(app_context)) => { + Some(app_context.clone()) + } + _ => None, + }); + + self.create_stream_in_list(self.app_context_streams.write(), app_context) + } + + fn presence_stream(&self) -> DataStream { + let presence = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(Update::Presence(presence)) => Some(presence.clone()), + _ => None, + }); + + self.create_stream_in_list(self.presence_streams.write(), presence) + } + + fn stream(&self) -> DataStream { + let updates = self.dequeue_matching_events(|event| match event { + SubscribeStreamEvent::Update(update) => Some(update.clone()), + _ => None, + }); + + self.create_stream_in_list(self.streams.write(), updates) + } +} + +#[cfg(test)] +mod it_should { + use futures::StreamExt; + use tokio::time::{timeout, Duration}; + + use super::*; + use crate::core::PubNubError; + + fn events() -> Vec { + vec![ + Update::Message(Message { + sender: Some("test-user-a".into()), + timestamp: 0, + channel: "test-channel".to_string(), + subscription: "test-channel".to_string(), + data: "Test message 1".to_string().into_bytes(), + r#type: None, + space_id: None, + decryption_error: None, + }), + Update::Signal(Message { + sender: Some("test-user-b".into()), + timestamp: 0, + channel: "test-channel".to_string(), + subscription: "test-channel".to_string(), + data: "Test signal 1".to_string().into_bytes(), + r#type: None, + space_id: None, + decryption_error: None, + }), + Update::Presence(Presence::Join { + timestamp: 0, + uuid: "test-user-c".to_string(), + channel: "test-channel".to_string(), + subscription: "test-channel".to_string(), + occupancy: 1, + data: None, + event_timestamp: 0, + }), + Update::Message(Message { + sender: Some("test-user-c".into()), + timestamp: 0, + channel: "test-channel".to_string(), + subscription: "test-channel".to_string(), + data: "Test message 2".to_string().into_bytes(), + r#type: None, + space_id: None, + decryption_error: None, + }), + ] + } + + #[test] + fn create_event_dispatcher() { + let dispatcher = EventDispatcher::new(); + assert!(!*dispatcher.has_streams.read()); + } + + #[test] + fn queue_events_when_there_no_listeners() { + let dispatcher = EventDispatcher::new(); + let events = events(); + + dispatcher.handle_status(ConnectionStatus::Connected); + dispatcher.handle_events(events.clone()); + + assert_eq!(dispatcher.updates.read().len(), events.len() + 1); + } + + #[tokio::test] + async fn dequeue_events_into_created_listener_streams() -> Result<(), PubNubError> { + let dispatcher = EventDispatcher::new(); + let events = events(); + + dispatcher.handle_status(ConnectionStatus::Connected); + dispatcher.handle_events(events); + + let mut events_count = 0; + let mut stream = dispatcher.messages_stream().take(10); + loop { + match timeout(Duration::from_millis(500), stream.next()).await { + Ok(Some(_)) => events_count += 1, + Err(_) => break, + _ => {} + } + } + assert_eq!(events_count, 2); + + let mut events_count = 0; + let mut stream = dispatcher.signals_stream().take(10); + loop { + match timeout(Duration::from_millis(500), stream.next()).await { + Ok(Some(_)) => events_count += 1, + Err(_) => break, + _ => {} + } + } + assert_eq!(events_count, 1); + + let mut events_count = 0; + let mut stream = dispatcher.presence_stream().take(10); + loop { + match timeout(Duration::from_millis(500), stream.next()).await { + Ok(Some(_)) => events_count += 1, + Err(_) => break, + _ => {} + } + } + assert_eq!(events_count, 1); + + let mut events_count = 0; + let mut stream = dispatcher.status_stream().take(10); + loop { + match timeout(Duration::from_millis(500), stream.next()).await { + Ok(Some(_)) => events_count += 1, + Err(_) => break, + _ => {} + } + } + + assert_eq!(events_count, 1); + let mut events_count = 0; + let mut stream = dispatcher.messages_stream().take(10); + loop { + match timeout(Duration::from_millis(500), stream.next()).await { + Ok(Some(_)) => events_count += 1, + Err(_) => break, + _ => {} + } + } + assert_eq!(events_count, 0); + Ok(()) + } +} diff --git a/src/dx/subscribe/event_engine/effect_handler.rs b/src/dx/subscribe/event_engine/effect_handler.rs index 8c100b71..5c5b2104 100644 --- a/src/dx/subscribe/event_engine/effect_handler.rs +++ b/src/dx/subscribe/event_engine/effect_handler.rs @@ -1,6 +1,7 @@ use async_channel::Sender; +use uuid::Uuid; -use crate::core::RequestRetryPolicy; +use crate::core::RequestRetryConfiguration; use crate::{ core::event_engine::EffectHandler, dx::subscribe::event_engine::{ @@ -17,7 +18,6 @@ use crate::{ /// /// Handler responsible for effects implementation and creation in response on /// effect invocation. -#[allow(dead_code)] pub(crate) struct SubscribeEffectHandler { /// Subscribe call function pointer. subscribe_call: Arc, @@ -29,7 +29,7 @@ pub(crate) struct SubscribeEffectHandler { emit_messages: Arc, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Cancellation channel. cancellation_channel: Sender, @@ -41,7 +41,7 @@ impl SubscribeEffectHandler { subscribe_call: Arc, emit_status: Arc, emit_messages: Arc, - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, cancellation_channel: Sender, ) -> Self { Self { @@ -59,6 +59,7 @@ impl EffectHandler for SubscribeEffe match invocation { SubscribeEffectInvocation::Handshake { input, cursor } => { Some(SubscribeEffect::Handshake { + id: Uuid::new_v4().to_string(), input: input.clone(), cursor: cursor.clone(), executor: self.subscribe_call.clone(), @@ -71,6 +72,7 @@ impl EffectHandler for SubscribeEffe attempts, reason, } => Some(SubscribeEffect::HandshakeReconnect { + id: Uuid::new_v4().to_string(), input: input.clone(), cursor: cursor.clone(), attempts: *attempts, @@ -81,6 +83,7 @@ impl EffectHandler for SubscribeEffe }), SubscribeEffectInvocation::Receive { input, cursor } => { Some(SubscribeEffect::Receive { + id: Uuid::new_v4().to_string(), input: input.clone(), cursor: cursor.clone(), executor: self.subscribe_call.clone(), @@ -93,6 +96,7 @@ impl EffectHandler for SubscribeEffe attempts, reason, } => Some(SubscribeEffect::ReceiveReconnect { + id: Uuid::new_v4().to_string(), input: input.clone(), cursor: cursor.clone(), attempts: *attempts, @@ -102,11 +106,14 @@ impl EffectHandler for SubscribeEffe cancellation_channel: self.cancellation_channel.clone(), }), SubscribeEffectInvocation::EmitStatus(status) => Some(SubscribeEffect::EmitStatus { + id: Uuid::new_v4().to_string(), status: status.clone(), executor: self.emit_status.clone(), }), - SubscribeEffectInvocation::EmitMessages(messages) => { + SubscribeEffectInvocation::EmitMessages(messages, cursor) => { Some(SubscribeEffect::EmitMessages { + id: Uuid::new_v4().to_string(), + next_cursor: cursor.clone(), updates: messages.clone(), executor: self.emit_messages.clone(), }) diff --git a/src/dx/subscribe/event_engine/effects/emit_messages.rs b/src/dx/subscribe/event_engine/effects/emit_messages.rs index 8ab16b8b..a8c542c6 100644 --- a/src/dx/subscribe/event_engine/effects/emit_messages.rs +++ b/src/dx/subscribe/event_engine/effects/emit_messages.rs @@ -1,19 +1,22 @@ +use log::info; + use crate::{ dx::subscribe::{ event_engine::{effects::EmitMessagesEffectExecutor, SubscribeEvent}, result::Update, + SubscriptionCursor, }, lib::alloc::{sync::Arc, vec, vec::Vec}, }; -use log::info; pub(super) async fn execute( + cursor: SubscriptionCursor, updates: Vec, executor: &Arc, ) -> Vec { info!("Emit updates: {updates:?}"); - executor(updates); + executor(updates, cursor); vec![] } @@ -36,7 +39,7 @@ mod should { decryption_error: None, }; - let emit_message_function: Arc = Arc::new(|updates| { + let emit_message_function: Arc = Arc::new(|updates, _| { let emitted_update = updates.first().expect("update should be passed"); assert!(matches!(emitted_update, Update::Message(_))); @@ -46,6 +49,7 @@ mod should { }); execute( + Default::default(), vec![Update::Message(message.clone())], &emit_message_function, ) diff --git a/src/dx/subscribe/event_engine/effects/emit_status.rs b/src/dx/subscribe/event_engine/effects/emit_status.rs index a5b85ca9..65a17d33 100644 --- a/src/dx/subscribe/event_engine/effects/emit_status.rs +++ b/src/dx/subscribe/event_engine/effects/emit_status.rs @@ -1,14 +1,14 @@ use crate::{ dx::subscribe::{ event_engine::{effects::EmitStatusEffectExecutor, SubscribeEvent}, - SubscribeStatus, + ConnectionStatus, }, lib::alloc::{sync::Arc, vec, vec::Vec}, }; use log::info; pub(super) async fn execute( - status: SubscribeStatus, + status: ConnectionStatus, executor: &Arc, ) -> Vec { info!("Emit status: {status:?}"); @@ -25,9 +25,9 @@ mod should { #[tokio::test] async fn emit_expected_status() { let emit_status_function: Arc = Arc::new(|status| { - assert!(matches!(status, SubscribeStatus::Connected)); + assert!(matches!(status, ConnectionStatus::Connected)); }); - execute(SubscribeStatus::Connected, &emit_status_function).await; + execute(ConnectionStatus::Connected, &emit_status_function).await; } } diff --git a/src/dx/subscribe/event_engine/effects/handshake.rs b/src/dx/subscribe/event_engine/effects/handshake.rs index 5327c635..cc0a3315 100644 --- a/src/dx/subscribe/event_engine/effects/handshake.rs +++ b/src/dx/subscribe/event_engine/effects/handshake.rs @@ -1,15 +1,17 @@ use futures::TryFutureExt; use log::info; +use crate::subscribe::SubscriptionCursor; use crate::{ dx::subscribe::event_engine::{ - effects::SubscribeEffectExecutor, SubscribeEvent, SubscribeInput, SubscriptionParams, + effects::SubscribeEffectExecutor, SubscribeEvent, SubscriptionInput, SubscriptionParams, }, lib::alloc::{sync::Arc, vec, vec::Vec}, }; pub(super) async fn execute( - input: &SubscribeInput, + input: &SubscriptionInput, + cursor: &Option, effect_id: &str, executor: &Arc, ) -> Vec { @@ -37,9 +39,16 @@ pub(super) async fn execute( vec![SubscribeEvent::HandshakeFailure { reason: error }] }, |subscribe_result| { - vec![SubscribeEvent::HandshakeSuccess { - cursor: subscribe_result.cursor, - }] + let cursor = { + if cursor.is_none() { + subscribe_result.cursor + } else { + let mut cursor = cursor.clone().unwrap_or_default(); + cursor.region = subscribe_result.cursor.region; + cursor + } + }; + vec![SubscribeEvent::HandshakeSuccess { cursor }] }, ) .await @@ -71,10 +80,11 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), + &None, "id", &mock_handshake_function, ) @@ -100,10 +110,11 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), + &None, "id", &mock_handshake_function, ) diff --git a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs index 2cf5a6a5..8a89ba9f 100644 --- a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs @@ -2,22 +2,26 @@ use futures::TryFutureExt; use log::info; use crate::{ - core::{PubNubError, RequestRetryPolicy}, - dx::subscribe::event_engine::{ - effects::SubscribeEffectExecutor, SubscribeEvent, SubscribeInput, SubscriptionParams, + core::{PubNubError, RequestRetryConfiguration}, + dx::subscribe::{ + event_engine::{ + effects::SubscribeEffectExecutor, SubscribeEvent, SubscriptionInput, SubscriptionParams, + }, + SubscriptionCursor, }, lib::alloc::{sync::Arc, vec, vec::Vec}, }; pub(super) async fn execute( - input: &SubscribeInput, + input: &SubscriptionInput, + cursor: &Option, attempt: u8, reason: PubNubError, effect_id: &str, - retry_policy: &RequestRetryPolicy, + retry_policy: &RequestRetryConfiguration, executor: &Arc, ) -> Vec { - if !retry_policy.retriable(&attempt, Some(&reason)) { + if !retry_policy.retriable(Some("/v2/subscribe"), &attempt, Some(&reason)) { return vec![SubscribeEvent::HandshakeReconnectGiveUp { reason }]; } @@ -48,9 +52,16 @@ pub(super) async fn execute( .unwrap_or(vec![]) }, |subscribe_result| { - vec![SubscribeEvent::HandshakeReconnectSuccess { - cursor: subscribe_result.cursor, - }] + let cursor = { + if cursor.is_none() { + subscribe_result.cursor + } else { + let mut cursor = cursor.clone().unwrap_or_default(); + cursor.region = subscribe_result.cursor.region; + cursor + } + }; + vec![SubscribeEvent::HandshakeReconnectSuccess { cursor }] }, ) .await @@ -59,6 +70,7 @@ pub(super) async fn execute( #[cfg(test)] mod should { use super::*; + use crate::core::TransportResponse; use crate::{core::PubNubError, dx::subscribe::result::SubscribeResult}; use futures::FutureExt; @@ -73,7 +85,10 @@ mod should { params.reason.unwrap(), PubNubError::Transport { details: "test".into(), - response: None + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })) } ); assert_eq!(params.effect_id, "id"); @@ -88,19 +103,24 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), + &None, 1, PubNubError::Transport { details: "test".into(), - response: None, + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), }, "id", - &RequestRetryPolicy::Linear { + &RequestRetryConfiguration::Linear { delay: 0, max_retry: 1, + excluded_endpoints: None, }, &mock_handshake_function, ) @@ -114,29 +134,76 @@ mod should { } #[tokio::test] - async fn return_handshake_reconnect_failure_event_on_err() { + async fn return_handshake_reconnect_give_up_event_on_err() { + let mock_handshake_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + &None, + 11, + PubNubError::Transport { + details: "test".into(), + response: None, + }, + "id", + &RequestRetryConfiguration::Linear { + max_retry: 10, + delay: 0, + excluded_endpoints: None, + }, + &mock_handshake_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + SubscribeEvent::HandshakeReconnectGiveUp { .. } + )); + } + + #[tokio::test] + async fn return_handshake_reconnect_give_up_event_on_err_with_none_auto_retry_policy() { let mock_handshake_function: Arc = Arc::new(move |_| { async move { Err(PubNubError::Transport { details: "test".into(), - response: None, + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), }) } .boxed() }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), + &None, 1, PubNubError::Transport { details: "test".into(), response: None, }, "id", - &RequestRetryPolicy::None, + &RequestRetryConfiguration::None, &mock_handshake_function, ) .await; diff --git a/src/dx/subscribe/event_engine/effects/mod.rs b/src/dx/subscribe/event_engine/effects/mod.rs index 90da1c5c..a51f9199 100644 --- a/src/dx/subscribe/event_engine/effects/mod.rs +++ b/src/dx/subscribe/event_engine/effects/mod.rs @@ -4,17 +4,17 @@ use async_channel::Sender; use futures::future::BoxFuture; use crate::{ - core::{event_engine::Effect, PubNubError, RequestRetryPolicy}, + core::{event_engine::Effect, PubNubError, RequestRetryConfiguration}, dx::subscribe::{ event_engine::{ - types::{SubscribeInput, SubscriptionParams}, + types::{SubscriptionInput, SubscriptionParams}, SubscribeEffectInvocation, SubscribeEvent, }, result::{SubscribeResult, Update}, - SubscribeCursor, SubscribeStatus, + ConnectionStatus, SubscriptionCursor, }, lib::{ - alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}, + alloc::{string::String, sync::Arc, vec::Vec}, core::fmt::{Debug, Formatter}, }, }; @@ -26,30 +26,56 @@ mod handshake_reconnection; mod receive; mod receive_reconnection; +/// `SubscribeEffectExecutor` is a trait alias representing a type that executes +/// subscribe effects. +/// +/// It takes a `SubscriptionParams` as input and returns a `BoxFuture` that +/// resolves to a `Result` of `SubscribeResult` or `PubNubError`. +/// +/// This trait alias is `Send` and `Sync`, allowing it to be used across +/// multiple threads safely. pub(in crate::dx::subscribe) type SubscribeEffectExecutor = dyn Fn(SubscriptionParams) -> BoxFuture<'static, Result> + Send + Sync; -pub(in crate::dx::subscribe) type EmitStatusEffectExecutor = dyn Fn(SubscribeStatus) + Send + Sync; -pub(in crate::dx::subscribe) type EmitMessagesEffectExecutor = dyn Fn(Vec) + Send + Sync; +/// `EmitStatusEffectExecutor` is a trait alias representing a type that +/// executes emit status effects. +/// +/// It takes a `SubscribeStatus` as input and does not return any value. +/// +/// This trait alias is `Send` and `Sync`, allowing it to be used across +/// multiple threads safely. +pub(in crate::dx::subscribe) type EmitStatusEffectExecutor = dyn Fn(ConnectionStatus) + Send + Sync; + +/// `EmitMessagesEffectExecutor` is a trait alias representing a type that +/// executes the effect of emitting messages. +/// +/// It takes a vector of `Update` objects as input and does not return any +/// value. +/// +/// This trait alias is `Send` and `Sync`, allowing it to be used across +/// multiple threads safely. +pub(in crate::dx::subscribe) type EmitMessagesEffectExecutor = + dyn Fn(Vec, SubscriptionCursor) + Send + Sync; // TODO: maybe move executor and cancellation_channel to super struct? -/// Subscription state machine effects. -#[allow(dead_code)] pub(crate) enum SubscribeEffect { /// Initial subscribe effect invocation. Handshake { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and channel groups which will be /// source of real-time updates after initial subscription completion. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: Option, + cursor: Option, /// Executor function. /// @@ -64,17 +90,20 @@ pub(crate) enum SubscribeEffect { /// Retry initial subscribe effect invocation. HandshakeReconnect { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and channel groups which has been /// used during recently failed initial subscription. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: Option, + cursor: Option, /// Current initial subscribe retry attempt. /// @@ -85,7 +114,7 @@ pub(crate) enum SubscribeEffect { reason: PubNubError, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Executor function. /// @@ -100,17 +129,20 @@ pub(crate) enum SubscribeEffect { /// Receive updates effect invocation. Receive { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and channel groups for which /// real-time updates will be delivered. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, /// Executor function. /// @@ -125,17 +157,20 @@ pub(crate) enum SubscribeEffect { /// Retry receive updates effect invocation. ReceiveReconnect { + /// Unique effect identifier. + id: String, + /// User input with channels and groups. /// /// Object contains list of channels and channel groups which has been /// used during recently failed receive updates. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, /// Current receive retry attempt. /// @@ -146,7 +181,7 @@ pub(crate) enum SubscribeEffect { reason: PubNubError, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Executor function. /// @@ -161,8 +196,11 @@ pub(crate) enum SubscribeEffect { /// Status change notification effect invocation. EmitStatus { + /// Unique effect identifier. + id: String, + /// Status which should be emitted. - status: SubscribeStatus, + status: ConnectionStatus, /// Executor function. /// @@ -172,6 +210,14 @@ pub(crate) enum SubscribeEffect { /// Received updates notification effect invocation. EmitMessages { + /// Unique effect identifier. + id: String, + + /// Next time cursor. + /// + /// Cursor which should be used for next subscription loop. + next_cursor: SubscriptionCursor, + /// Updates which should be emitted. updates: Vec, @@ -239,14 +285,26 @@ impl Debug for SubscribeEffect { impl Effect for SubscribeEffect { type Invocation = SubscribeEffectInvocation; + fn name(&self) -> String { + match self { + Self::Handshake { .. } => "HANDSHAKE", + Self::HandshakeReconnect { .. } => "HANDSHAKE_RECONNECT", + Self::Receive { .. } => "RECEIVE_MESSAGES", + Self::ReceiveReconnect { .. } => "RECEIVE_RECONNECT", + Self::EmitStatus { .. } => "EMIT_STATUS", + Self::EmitMessages { .. } => "EMIT_MESSAGES", + } + .into() + } + fn id(&self) -> String { match self { - Self::Handshake { .. } => "HANDSHAKE_EFFECT", - Self::HandshakeReconnect { .. } => "HANDSHAKE_RECONNECT_EFFECT", - Self::Receive { .. } => "RECEIVE_EFFECT", - Self::ReceiveReconnect { .. } => "RECEIVE_RECONNECT_EFFECT", - Self::EmitStatus { .. } => "EMIT_STATUS_EFFECT", - Self::EmitMessages { .. } => "EMIT_MESSAGES_EFFECT", + Self::Handshake { id, .. } + | Self::HandshakeReconnect { id, .. } + | Self::Receive { id, .. } + | Self::ReceiveReconnect { id, .. } + | Self::EmitStatus { id, .. } + | Self::EmitMessages { id, .. } => id, } .into() } @@ -254,10 +312,16 @@ impl Effect for SubscribeEffect { async fn run(&self) -> Vec { match self { Self::Handshake { - input, executor, .. - } => handshake::execute(input, &self.id(), executor).await, + id, + input, + cursor, + executor, + .. + } => handshake::execute(input, cursor, id, executor).await, Self::HandshakeReconnect { + id, input, + cursor, attempts, reason, retry_policy, @@ -266,22 +330,25 @@ impl Effect for SubscribeEffect { } => { handshake_reconnection::execute( input, + cursor, *attempts, reason.clone(), /* TODO: Does run function need to borrow self? Or we can * consume it? */ - &self.id(), + id, retry_policy, executor, ) .await } Self::Receive { + id, input, cursor, executor, .. - } => receive::execute(input, cursor, &self.id(), executor).await, + } => receive::execute(input, cursor, id, executor).await, Self::ReceiveReconnect { + id, input, cursor, attempts, @@ -296,41 +363,48 @@ impl Effect for SubscribeEffect { *attempts, reason.clone(), /* TODO: Does run function need to borrow self? Or we can * consume it? */ - &self.id(), + id, retry_policy, executor, ) .await } - Self::EmitStatus { status, executor } => { - emit_status::execute(status.clone(), executor).await - } - Self::EmitMessages { updates, executor } => { - emit_messages::execute(updates.clone(), executor).await - } + Self::EmitStatus { + status, executor, .. + } => emit_status::execute(status.clone(), executor).await, + Self::EmitMessages { + updates, + executor, + next_cursor, + .. + } => emit_messages::execute(next_cursor.clone(), updates.clone(), executor).await, } } fn cancel(&self) { match self { Self::Handshake { + id, cancellation_channel, .. } | Self::HandshakeReconnect { + id, cancellation_channel, .. } | Self::Receive { + id, cancellation_channel, .. } | Self::ReceiveReconnect { + id, cancellation_channel, .. } => { cancellation_channel - .send_blocking(self.id()) + .send_blocking(id.clone()) .expect("cancellation pipe is broken!"); } _ => { /* cannot cancel other effects */ } @@ -341,27 +415,31 @@ impl Effect for SubscribeEffect { #[cfg(test)] mod should { use super::*; + use futures::FutureExt; + use uuid::Uuid; #[tokio::test] async fn send_cancellation_notification() { - let (tx, rx) = async_channel::bounded(1); + let (tx, rx) = async_channel::bounded::(1); let effect = SubscribeEffect::Handshake { - input: SubscribeInput::new(&None, &None), + id: Uuid::new_v4().to_string(), + input: SubscriptionInput::new(&None, &None), cursor: None, executor: Arc::new(|_| { - Box::pin(async move { + async move { Ok(SubscribeResult { - cursor: SubscribeCursor::default(), + cursor: SubscriptionCursor::default(), messages: vec![], }) - }) + } + .boxed() }), cancellation_channel: tx, }; effect.cancel(); - assert_eq!(rx.recv().await.unwrap(), effect.id()) + assert_eq!(rx.recv().await.unwrap(), effect.id()); } } diff --git a/src/dx/subscribe/event_engine/effects/receive.rs b/src/dx/subscribe/event_engine/effects/receive.rs index 0f2d7830..c3dc2402 100644 --- a/src/dx/subscribe/event_engine/effects/receive.rs +++ b/src/dx/subscribe/event_engine/effects/receive.rs @@ -6,16 +6,16 @@ use crate::{ dx::subscribe::{ event_engine::{ effects::SubscribeEffectExecutor, types::SubscriptionParams, SubscribeEvent, - SubscribeInput, + SubscriptionInput, }, - SubscribeCursor, + SubscriptionCursor, }, lib::alloc::{sync::Arc, vec, vec::Vec}, }; pub(crate) async fn execute( - input: &SubscribeInput, - cursor: &SubscribeCursor, + input: &SubscriptionInput, + cursor: &SubscriptionCursor, effect_id: &str, executor: &Arc, ) -> Vec { @@ -82,7 +82,7 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), @@ -112,7 +112,7 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), diff --git a/src/dx/subscribe/event_engine/effects/receive_reconnection.rs b/src/dx/subscribe/event_engine/effects/receive_reconnection.rs index f3875f49..ca7efa7d 100644 --- a/src/dx/subscribe/event_engine/effects/receive_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/receive_reconnection.rs @@ -2,28 +2,28 @@ use futures::TryFutureExt; use log::info; use crate::{ - core::{PubNubError, RequestRetryPolicy}, + core::{PubNubError, RequestRetryConfiguration}, dx::subscribe::{ event_engine::{ effects::SubscribeEffectExecutor, types::SubscriptionParams, SubscribeEvent, - SubscribeInput, + SubscriptionInput, }, - SubscribeCursor, + SubscriptionCursor, }, lib::alloc::{sync::Arc, vec, vec::Vec}, }; #[allow(clippy::too_many_arguments)] pub(crate) async fn execute( - input: &SubscribeInput, - cursor: &SubscribeCursor, + input: &SubscriptionInput, + cursor: &SubscriptionCursor, attempt: u8, reason: PubNubError, effect_id: &str, - retry_policy: &RequestRetryPolicy, + retry_policy: &RequestRetryConfiguration, executor: &Arc, ) -> Vec { - if !retry_policy.retriable(&attempt, Some(&reason)) { + if !retry_policy.retriable(Some("/v2/subscribe"), &attempt, Some(&reason)) { return vec![SubscribeEvent::ReceiveReconnectGiveUp { reason }]; } @@ -103,7 +103,7 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), @@ -117,7 +117,11 @@ mod should { })), }, "id", - &RequestRetryPolicy::None, + &RequestRetryConfiguration::Linear { + max_retry: 20, + delay: 0, + excluded_endpoints: None, + }, &mock_receive_function, ) .await; @@ -145,7 +149,7 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), @@ -159,7 +163,11 @@ mod should { })), }, "id", - &RequestRetryPolicy::None, + &RequestRetryConfiguration::Linear { + max_retry: 10, + delay: 0, + excluded_endpoints: None, + }, &mock_receive_function, ) .await; @@ -187,7 +195,7 @@ mod should { }); let result = execute( - &SubscribeInput::new( + &SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), ), @@ -201,9 +209,10 @@ mod should { })), }, "id", - &RequestRetryPolicy::Linear { + &RequestRetryConfiguration::Linear { delay: 0, max_retry: 1, + excluded_endpoints: None, }, &mock_receive_function, ) @@ -215,4 +224,46 @@ mod should { SubscribeEvent::ReceiveReconnectGiveUp { .. } )); } + + #[tokio::test] + async fn return_receive_reconnect_give_up_event_on_err_with_none_auto_retry_policy() { + let mock_receive_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + &Default::default(), + 10, + PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }, + "id", + &RequestRetryConfiguration::None, + &mock_receive_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + SubscribeEvent::ReceiveReconnectGiveUp { .. } + )); + } } diff --git a/src/dx/subscribe/event_engine/event.rs b/src/dx/subscribe/event_engine/event.rs index ed61e2f9..bd3ccfd0 100644 --- a/src/dx/subscribe/event_engine/event.rs +++ b/src/dx/subscribe/event_engine/event.rs @@ -1,7 +1,7 @@ use crate::dx::subscribe::result::Update; use crate::{ core::{event_engine::Event, PubNubError}, - dx::subscribe::SubscribeCursor, + dx::subscribe::SubscriptionCursor, lib::alloc::{string::String, vec::Vec}, }; @@ -9,7 +9,6 @@ use crate::{ /// /// Subscribe state machine behaviour depends from external events which it /// receives. -#[allow(dead_code)] #[derive(Debug)] pub(crate) enum SubscribeEvent { /// Current list of channels / groups has been changed. @@ -28,7 +27,7 @@ pub(crate) enum SubscribeEvent { SubscriptionRestored { channels: Option>, channel_groups: Option>, - cursor: SubscribeCursor, + cursor: SubscriptionCursor, }, /// Handshake completed successfully. @@ -37,7 +36,7 @@ pub(crate) enum SubscribeEvent { /// be used for subscription loop. /// /// [`PubNub`]: https://www.pubnub.com/ - HandshakeSuccess { cursor: SubscribeCursor }, + HandshakeSuccess { cursor: SubscriptionCursor }, /// Handshake completed with error. /// @@ -54,7 +53,7 @@ pub(crate) enum SubscribeEvent { /// loop. /// /// [`PubNub`]: https://www.pubnub.com/ - HandshakeReconnectSuccess { cursor: SubscribeCursor }, + HandshakeReconnectSuccess { cursor: SubscriptionCursor }, /// Handshake reconnect completed with error. /// @@ -78,7 +77,7 @@ pub(crate) enum SubscribeEvent { /// /// [`PubNub`]: https://www.pubnub.com/ ReceiveSuccess { - cursor: SubscribeCursor, + cursor: SubscriptionCursor, messages: Vec, }, @@ -98,7 +97,7 @@ pub(crate) enum SubscribeEvent { /// /// [`PubNub`]: https://www.pubnub.com/ ReceiveReconnectSuccess { - cursor: SubscribeCursor, + cursor: SubscriptionCursor, messages: Vec, }, @@ -130,7 +129,7 @@ pub(crate) enum SubscribeEvent { /// Emitted when explicitly requested to restore real-time updates receive. /// /// [`PubNub`]: https://www.pubnub.com/ - Reconnect, + Reconnect { cursor: Option }, /// Unsubscribe from all channels and groups. /// @@ -155,7 +154,7 @@ impl Event for SubscribeEvent { Self::ReceiveReconnectFailure { .. } => "RECEIVE_RECONNECT_FAILURE", Self::ReceiveReconnectGiveUp { .. } => "RECEIVE_RECONNECT_GIVEUP", Self::Disconnect => "DISCONNECT", - Self::Reconnect => "RECONNECT", + Self::Reconnect { .. } => "RECONNECT", Self::UnsubscribeAll => "UNSUBSCRIBE_ALL", } } diff --git a/src/dx/subscribe/event_engine/invocation.rs b/src/dx/subscribe/event_engine/invocation.rs index 7528c6d5..7ff81b48 100644 --- a/src/dx/subscribe/event_engine/invocation.rs +++ b/src/dx/subscribe/event_engine/invocation.rs @@ -1,9 +1,9 @@ use crate::{ core::{event_engine::EffectInvocation, PubNubError}, dx::subscribe::{ - event_engine::{SubscribeEffect, SubscribeEvent, SubscribeInput}, + event_engine::{SubscribeEffect, SubscribeEvent, SubscriptionInput}, result::Update, - SubscribeCursor, SubscribeStatus, + ConnectionStatus, SubscriptionCursor, }, lib::{ alloc::vec::Vec, @@ -14,9 +14,8 @@ use crate::{ /// Subscribe effect invocations /// /// Invocation is form of intention to call some action without any information -/// about it's implementation. +/// about its implementation. #[derive(Debug)] -#[allow(dead_code)] pub(crate) enum SubscribeEffectInvocation { /// Initial subscribe effect invocation. Handshake { @@ -24,13 +23,13 @@ pub(crate) enum SubscribeEffectInvocation { /// /// Object contains list of channels and groups which will be source of /// real-time updates after initial subscription completion. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: Option, + cursor: Option, }, /// Cancel initial subscribe effect invocation. @@ -42,13 +41,13 @@ pub(crate) enum SubscribeEffectInvocation { /// /// Object contains list of channels and groups which has been used /// during recently failed initial subscription. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: Option, + cursor: Option, /// Current initial subscribe retry attempt. /// @@ -68,13 +67,13 @@ pub(crate) enum SubscribeEffectInvocation { /// /// Object contains list of channels and groups which real-time updates /// will be delivered. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, }, /// Cancel receive updates effect invocation. @@ -86,13 +85,13 @@ pub(crate) enum SubscribeEffectInvocation { /// /// Object contains list of channels and groups which has been used /// during recently failed receive updates. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, /// Current receive retry attempt. /// @@ -107,10 +106,13 @@ pub(crate) enum SubscribeEffectInvocation { CancelReceiveReconnect, /// Status change notification effect invocation. - EmitStatus(SubscribeStatus), + EmitStatus(ConnectionStatus), /// Received updates notification effect invocation. - EmitMessages(Vec), + EmitMessages(Vec, SubscriptionCursor), + + /// Terminate Subscribe Event Engine processing loop. + TerminateEventEngine, } impl EffectInvocation for SubscribeEffectInvocation { @@ -120,19 +122,20 @@ impl EffectInvocation for SubscribeEffectInvocation { fn id(&self) -> &str { match self { Self::Handshake { .. } => "HANDSHAKE", - Self::CancelHandshake => "CANCEL_HANDSHAKE", + Self::CancelHandshake { .. } => "CANCEL_HANDSHAKE", Self::HandshakeReconnect { .. } => "HANDSHAKE_RECONNECT", - Self::CancelHandshakeReconnect => "CANCEL_HANDSHAKE_RECONNECT", + Self::CancelHandshakeReconnect { .. } => "CANCEL_HANDSHAKE_RECONNECT", Self::Receive { .. } => "RECEIVE_MESSAGES", Self::CancelReceive { .. } => "CANCEL_RECEIVE_MESSAGES", Self::ReceiveReconnect { .. } => "RECEIVE_RECONNECT", Self::CancelReceiveReconnect { .. } => "CANCEL_RECEIVE_RECONNECT", - Self::EmitStatus(_status) => "EMIT_STATUS", - Self::EmitMessages(_messages) => "EMIT_MESSAGES", + Self::EmitStatus(_) => "EMIT_STATUS", + Self::EmitMessages(_, _) => "EMIT_MESSAGES", + Self::TerminateEventEngine => "TERMINATE_EVENT_ENGINE", } } - fn managed(&self) -> bool { + fn is_managed(&self) -> bool { matches!( self, Self::Handshake { .. } @@ -142,12 +145,12 @@ impl EffectInvocation for SubscribeEffectInvocation { ) } - fn cancelling(&self) -> bool { + fn is_cancelling(&self) -> bool { matches!( self, - Self::CancelHandshake - | Self::CancelHandshakeReconnect - | Self::CancelReceive + Self::CancelHandshake { .. } + | Self::CancelHandshakeReconnect { .. } + | Self::CancelReceive { .. } | Self::CancelReceiveReconnect ) } @@ -162,6 +165,10 @@ impl EffectInvocation for SubscribeEffectInvocation { || (matches!(effect, SubscribeEffect::ReceiveReconnect { .. }) && matches!(self, Self::CancelReceiveReconnect { .. })) } + + fn is_terminating(&self) -> bool { + matches!(self, Self::TerminateEventEngine) + } } impl Display for SubscribeEffectInvocation { @@ -175,8 +182,9 @@ impl Display for SubscribeEffectInvocation { Self::CancelReceive { .. } => write!(f, "CANCEL_RECEIVE_MESSAGES"), Self::ReceiveReconnect { .. } => write!(f, "RECEIVE_RECONNECT"), Self::CancelReceiveReconnect { .. } => write!(f, "CANCEL_RECEIVE_RECONNECT"), - Self::EmitStatus(status) => write!(f, "EMIT_STATUS({})", status), - Self::EmitMessages(messages) => write!(f, "EMIT_MESSAGES({:?})", messages), + Self::EmitStatus(status) => write!(f, "EMIT_STATUS({status:?})"), + Self::EmitMessages(messages, _) => write!(f, "EMIT_MESSAGES({messages:?})"), + Self::TerminateEventEngine => write!(f, "TERMINATE_EVENT_ENGINE"), } } } diff --git a/src/dx/subscribe/event_engine/mod.rs b/src/dx/subscribe/event_engine/mod.rs index 0e4dfdae..16c10c47 100644 --- a/src/dx/subscribe/event_engine/mod.rs +++ b/src/dx/subscribe/event_engine/mod.rs @@ -1,9 +1,6 @@ //! Subscribe Event Engine module -use crate::{ - core::event_engine::EventEngine, - lib::alloc::{string::String, vec::Vec}, -}; +use crate::core::event_engine::EventEngine; #[doc(inline)] pub(crate) use effects::SubscribeEffect; @@ -22,37 +19,12 @@ pub(crate) use event::SubscribeEvent; pub(crate) mod event; #[doc(inline)] -#[allow(unused_imports)] pub(crate) use state::SubscribeState; pub(crate) mod state; #[doc(inline)] -#[allow(unused_imports)] -pub(in crate::dx::subscribe) use types::{SubscribeInput, SubscriptionParams}; +pub(in crate::dx::subscribe) use types::{SubscriptionInput, SubscriptionParams}; pub(in crate::dx::subscribe) mod types; pub(crate) type SubscribeEventEngine = EventEngine; - -impl - EventEngine -{ - #[allow(dead_code)] - pub(in crate::dx::subscribe) fn current_subscription( - &self, - ) -> (Option>, Option>) { - match self.current_state() { - SubscribeState::Handshaking { input, .. } - | SubscribeState::HandshakeReconnecting { input, .. } - | SubscribeState::HandshakeStopped { input, .. } - | SubscribeState::HandshakeFailed { input, .. } - | SubscribeState::Receiving { input, .. } - | SubscribeState::ReceiveReconnecting { input, .. } - | SubscribeState::ReceiveStopped { input, .. } - | SubscribeState::ReceiveFailed { input, .. } => { - (input.channels(), input.channel_groups()) - } - _ => (None, None), - } - } -} diff --git a/src/dx/subscribe/event_engine/state.rs b/src/dx/subscribe/event_engine/state.rs index f097c383..6174e196 100644 --- a/src/dx/subscribe/event_engine/state.rs +++ b/src/dx/subscribe/event_engine/state.rs @@ -11,7 +11,7 @@ use crate::{ }, dx::subscribe::{ event_engine::{ - types::SubscribeInput, + types::SubscriptionInput, SubscribeEffectInvocation::{ self, CancelHandshake, CancelHandshakeReconnect, CancelReceive, CancelReceiveReconnect, EmitMessages, EmitStatus, Handshake, HandshakeReconnect, @@ -20,14 +20,13 @@ use crate::{ SubscribeEvent, }, result::Update, - SubscribeCursor, SubscribeStatus, + ConnectionStatus, SubscriptionCursor, }, lib::alloc::{string::String, vec, vec::Vec}, }; /// States of subscribe state machine. #[derive(Debug, Clone, PartialEq)] -#[allow(dead_code)] pub(crate) enum SubscribeState { /// Unsubscribed state. /// @@ -44,13 +43,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups which will be source of /// real-time updates after initial subscription completion. - input: SubscribeInput, + input: SubscriptionInput, /// Custom time cursor. /// /// Custom cursor used by subscription loop to identify point in time /// after which updates will be delivered. - cursor: Option, + cursor: Option, }, /// Subscription recover state. @@ -61,13 +60,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups which has been used /// during recently failed initial subscription. - input: SubscribeInput, + input: SubscriptionInput, /// Custom time cursor. /// /// Custom cursor used by subscription loop to identify point in time /// after which updates will be delivered. - cursor: Option, + cursor: Option, /// Current initial subscribe retry attempt. /// @@ -84,13 +83,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups for which initial /// subscription stopped. - input: SubscribeInput, + input: SubscriptionInput, /// Custom time cursor. /// /// Custom cursor used by subscription loop to identify point in time /// after which updates will be delivered. - cursor: Option, + cursor: Option, }, /// Initial subscription failure state. @@ -102,13 +101,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups which has been used /// during recently failed initial subscription. - input: SubscribeInput, + input: SubscriptionInput, /// Custom time cursor. /// /// Custom cursor used by subscription loop to identify point in time /// after which updates will be delivered. - cursor: Option, + cursor: Option, /// Initial subscribe attempt failure reason. reason: PubNubError, @@ -116,7 +115,7 @@ pub(crate) enum SubscribeState { /// Receiving updates state. /// - /// Subscription state machine is in state where it receive real-time + /// Subscription state machine is in state where it receives real-time /// updates from [`PubNub`] network. /// /// [`PubNub`]:https://www.pubnub.com/ @@ -125,13 +124,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups which real-time updates /// will be delivered. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, }, /// Subscription recover state. @@ -142,13 +141,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups which has been used /// during recently failed receive updates. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, /// Current receive retry attempt. /// @@ -165,13 +164,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups for which updates /// receive stopped. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, }, /// Updates receiving failure state. @@ -182,13 +181,13 @@ pub(crate) enum SubscribeState { /// /// Object contains list of channels and groups which has been used /// during recently failed receive updates. - input: SubscribeInput, + input: SubscriptionInput, /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. - cursor: SubscribeCursor, + cursor: SubscriptionCursor, /// Receive updates attempt failure reason. reason: PubNubError, @@ -204,49 +203,49 @@ impl SubscribeState { ) -> Option> { match self { Self::Unsubscribed => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Handshaking { + input: SubscriptionInput::new(channels, channel_groups), cursor: None, - }, + }), None, )), Self::Handshaking { cursor, .. } | Self::HandshakeReconnecting { cursor, .. } | Self::HandshakeFailed { cursor, .. } => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Handshaking { + input: SubscriptionInput::new(channels, channel_groups), cursor: cursor.clone(), - }, + }), None, )), Self::HandshakeStopped { cursor, .. } => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::HandshakeStopped { + input: SubscriptionInput::new(channels, channel_groups), cursor: cursor.clone(), - }, + }), None, )), Self::Receiving { cursor, .. } | Self::ReceiveReconnecting { cursor, .. } => { Some(self.transition_to( - Self::Receiving { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Receiving { + input: SubscriptionInput::new(channels, channel_groups), cursor: cursor.clone(), - }, + }), None, )) } Self::ReceiveFailed { cursor, .. } => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Handshaking { + input: SubscriptionInput::new(channels, channel_groups), cursor: Some(cursor.clone()), - }, + }), None, )), Self::ReceiveStopped { cursor, .. } => Some(self.transition_to( - Self::ReceiveStopped { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::ReceiveStopped { + input: SubscriptionInput::new(channels, channel_groups), cursor: cursor.clone(), - }, + }), None, )), } @@ -260,45 +259,51 @@ impl SubscribeState { &self, channels: &Option>, channel_groups: &Option>, - restore_cursor: &SubscribeCursor, + restore_cursor: &SubscriptionCursor, ) -> Option> { match self { Self::Unsubscribed => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Handshaking { + input: SubscriptionInput::new(channels, channel_groups), cursor: Some(restore_cursor.clone()), - }, + }), None, )), - Self::Handshaking { cursor, .. } - | Self::HandshakeReconnecting { cursor, .. } - | Self::HandshakeFailed { cursor, .. } - | Self::HandshakeStopped { cursor, .. } => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), - cursor: Some(cursor.clone().unwrap_or(restore_cursor.clone())), - }, + Self::Handshaking { .. } + | Self::HandshakeReconnecting { .. } + | Self::HandshakeFailed { .. } => Some(self.transition_to( + Some(Self::Handshaking { + input: SubscriptionInput::new(channels, channel_groups), + cursor: Some(restore_cursor.clone()), + }), + None, + )), + Self::HandshakeStopped { .. } => Some(self.transition_to( + Some(Self::HandshakeStopped { + input: SubscriptionInput::new(channels, channel_groups), + cursor: Some(restore_cursor.clone()), + }), None, )), Self::Receiving { .. } | Self::ReceiveReconnecting { .. } => Some(self.transition_to( - Self::Receiving { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Receiving { + input: SubscriptionInput::new(channels, channel_groups), cursor: restore_cursor.clone(), - }, + }), None, )), Self::ReceiveFailed { .. } => Some(self.transition_to( - Self::Handshaking { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::Handshaking { + input: SubscriptionInput::new(channels, channel_groups), cursor: Some(restore_cursor.clone()), - }, + }), None, )), Self::ReceiveStopped { .. } => Some(self.transition_to( - Self::ReceiveStopped { - input: SubscribeInput::new(channels, channel_groups), + Some(Self::ReceiveStopped { + input: SubscriptionInput::new(channels, channel_groups), cursor: restore_cursor.clone(), - }, + }), None, )), } @@ -310,17 +315,25 @@ impl SubscribeState { /// first time. fn handshake_success_transition( &self, - next_cursor: &SubscribeCursor, + next_cursor: &SubscriptionCursor, ) -> Option> { match self { Self::Handshaking { input, cursor } - | Self::HandshakeReconnecting { input, cursor, .. } => Some(self.transition_to( - Self::Receiving { - input: input.clone(), - cursor: cursor.clone().unwrap_or(next_cursor.clone()), - }, - Some(vec![EmitStatus(SubscribeStatus::Connected)]), - )), + | Self::HandshakeReconnecting { input, cursor, .. } => { + // Merge stored cursor with service-provided. + let mut next_cursor = next_cursor.clone(); + if let Some(cursor) = cursor { + next_cursor.timetoken = cursor.timetoken.clone(); + } + + Some(self.transition_to( + Some(Self::Receiving { + input: input.clone(), + cursor: next_cursor, + }), + Some(vec![EmitStatus(ConnectionStatus::Connected)]), + )) + } _ => None, } } @@ -330,14 +343,20 @@ impl SubscribeState { &self, reason: &PubNubError, ) -> Option> { + // Request cancellation shouldn't cause any transition because there + // will be another event after this. + if matches!(reason, PubNubError::RequestCancel { .. }) { + return None; + } + match self { Self::Handshaking { input, cursor } => Some(self.transition_to( - Self::HandshakeReconnecting { + Some(Self::HandshakeReconnecting { input: input.clone(), cursor: cursor.clone(), attempts: 1, reason: reason.clone(), - }, + }), None, )), _ => None, @@ -352,6 +371,12 @@ impl SubscribeState { &self, reason: &PubNubError, ) -> Option> { + // Request cancellation shouldn't cause any transition because there + // will be another event after this. + if matches!(reason, PubNubError::RequestCancel { .. }) { + return None; + } + match self { Self::HandshakeReconnecting { input, @@ -359,12 +384,12 @@ impl SubscribeState { attempts, .. } => Some(self.transition_to( - Self::HandshakeReconnecting { + Some(Self::HandshakeReconnecting { input: input.clone(), cursor: cursor.clone(), attempts: attempts + 1, reason: reason.clone(), - }, + }), None, )), _ => None, @@ -381,12 +406,12 @@ impl SubscribeState { ) -> Option> { match self { Self::HandshakeReconnecting { input, cursor, .. } => Some(self.transition_to( - Self::HandshakeFailed { + Some(Self::HandshakeFailed { input: input.clone(), cursor: cursor.clone(), reason: reason.clone(), - }, - Some(vec![EmitStatus(SubscribeStatus::ConnectionError( + }), + Some(vec![EmitStatus(ConnectionStatus::ConnectionError( reason.clone(), ))]), )), @@ -400,17 +425,17 @@ impl SubscribeState { /// channels / groups. fn receive_success_transition( &self, - cursor: &SubscribeCursor, + cursor: &SubscriptionCursor, messages: &[Update], ) -> Option> { match self { Self::Receiving { input, .. } | Self::ReceiveReconnecting { input, .. } => { Some(self.transition_to( - Self::Receiving { + Some(Self::Receiving { input: input.clone(), cursor: cursor.clone(), - }, - Some(vec![EmitMessages(messages.to_vec())]), + }), + Some(vec![EmitMessages(messages.to_vec(), cursor.clone())]), )) } _ => None, @@ -422,14 +447,20 @@ impl SubscribeState { &self, reason: &PubNubError, ) -> Option> { + // Request cancellation shouldn't cause any transition because there + // will be another event after this. + if matches!(reason, PubNubError::RequestCancel { .. }) { + return None; + } + match self { Self::Receiving { input, cursor, .. } => Some(self.transition_to( - Self::ReceiveReconnecting { + Some(Self::ReceiveReconnecting { input: input.clone(), cursor: cursor.clone(), attempts: 1, reason: reason.clone(), - }, + }), None, )), _ => None, @@ -444,6 +475,12 @@ impl SubscribeState { &self, reason: &PubNubError, ) -> Option> { + // Request cancellation shouldn't cause any transition because there + // will be another event after this. + if matches!(reason, PubNubError::RequestCancel { .. }) { + return None; + } + match self { Self::ReceiveReconnecting { input, @@ -451,12 +488,12 @@ impl SubscribeState { cursor, .. } => Some(self.transition_to( - Self::ReceiveReconnecting { + Some(Self::ReceiveReconnecting { input: input.clone(), cursor: cursor.clone(), attempts: attempts + 1, reason: reason.clone(), - }, + }), None, )), _ => None, @@ -473,12 +510,14 @@ impl SubscribeState { ) -> Option> { match self { Self::ReceiveReconnecting { input, cursor, .. } => Some(self.transition_to( - Self::ReceiveFailed { + Some(Self::ReceiveFailed { input: input.clone(), cursor: cursor.clone(), reason: reason.clone(), - }, - Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), + }), + Some(vec![EmitStatus( + ConnectionStatus::DisconnectedUnexpectedly(reason.clone()), + )]), )), _ => None, } @@ -492,19 +531,19 @@ impl SubscribeState { match self { Self::Handshaking { input, cursor } | Self::HandshakeReconnecting { input, cursor, .. } => Some(self.transition_to( - Self::HandshakeStopped { + Some(Self::HandshakeStopped { input: input.clone(), cursor: cursor.clone(), - }, + }), None, )), Self::Receiving { input, cursor } | Self::ReceiveReconnecting { input, cursor, .. } => { Some(self.transition_to( - Self::ReceiveStopped { + Some(Self::ReceiveStopped { input: input.clone(), cursor: cursor.clone(), - }, - Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), + }), + Some(vec![EmitStatus(ConnectionStatus::Disconnected)]), )) } _ => None, @@ -516,22 +555,33 @@ impl SubscribeState { /// Event is sent each time when client asked to restore activity for /// channels / groups after which previously temporally stopped or restore /// after reconnection failures. - fn reconnect_transition(&self) -> Option> { + fn reconnect_transition( + &self, + restore_cursor: &Option, + ) -> Option> { match self { Self::HandshakeStopped { input, cursor } | Self::HandshakeFailed { input, cursor, .. } => Some(self.transition_to( - Self::Handshaking { + Some(Self::Handshaking { input: input.clone(), - cursor: cursor.clone(), - }, + cursor: if restore_cursor.is_some() { + restore_cursor.clone() + } else { + cursor.clone() + }, + }), None, )), Self::ReceiveStopped { input, cursor } | Self::ReceiveFailed { input, cursor, .. } => { Some(self.transition_to( - Self::Handshaking { + Some(Self::Handshaking { input: input.clone(), - cursor: Some(cursor.clone()), - }, + cursor: if restore_cursor.is_some() { + restore_cursor.clone() + } else { + Some(cursor.clone()) + }, + }), None, )) } @@ -542,8 +592,8 @@ impl SubscribeState { /// Handle unsubscribe all event. fn unsubscribe_all_transition(&self) -> Option> { Some(self.transition_to( - Self::Unsubscribed, - Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), + Some(Self::Unsubscribed), + Some(vec![EmitStatus(ConnectionStatus::Disconnected)]), )) } } @@ -635,23 +685,28 @@ impl State for SubscribeState { self.receive_reconnect_give_up_transition(reason) } SubscribeEvent::Disconnect => self.disconnect_transition(), - SubscribeEvent::Reconnect => self.reconnect_transition(), + SubscribeEvent::Reconnect { cursor } => self.reconnect_transition(cursor), SubscribeEvent::UnsubscribeAll => self.unsubscribe_all_transition(), } } fn transition_to( &self, - state: Self::State, + state: Option, invocations: Option>, ) -> Transition { + let on_enter_invocations = match state.clone() { + Some(state) => state.enter().unwrap_or_default(), + None => vec![], + }; + Transition { invocations: self .exit() .unwrap_or_default() .into_iter() .chain(invocations.unwrap_or_default()) - .chain(state.enter().unwrap_or_default()) + .chain(on_enter_invocations) .collect(), state, } @@ -666,7 +721,7 @@ mod should { use super::*; use crate::{ - core::{event_engine::EventEngine, RequestRetryPolicy}, + core::{event_engine::EventEngine, RequestRetryConfiguration}, dx::subscribe::{ event_engine::{ effects::{ @@ -701,7 +756,7 @@ mod should { }); let emit_status: Arc = Arc::new(|_| {}); - let emit_message: Arc = Arc::new(|_| {}); + let emit_message: Arc = Arc::new(|_, _| {}); let (tx, _) = async_channel::bounded(1); @@ -710,7 +765,7 @@ mod should { call, emit_status, emit_message, - RequestRetryPolicy::None, + RequestRetryConfiguration::None, tx, ), start_state, @@ -725,7 +780,7 @@ mod should { channel_groups: Some(vec!["gr1".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -738,14 +793,14 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" )] @@ -774,7 +829,7 @@ mod should { #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -785,7 +840,7 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), @@ -795,28 +850,28 @@ mod should { )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription changed" )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -826,7 +881,7 @@ mod should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -838,21 +893,21 @@ mod should { )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::HandshakeFailure { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }; @@ -860,7 +915,7 @@ mod should { )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]), ), @@ -868,7 +923,7 @@ mod should { }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -878,65 +933,65 @@ mod should { )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; "to handshake stopped with custom cursor on disconnect" )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), cursor: None, }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }; "to receiving on handshake success" )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 2 } }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "20".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "20".into(), region: 2 } }; "to receiving with custom cursor on handshake success" )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -945,42 +1000,42 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 2 } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }), }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -990,7 +1045,7 @@ mod should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1014,7 +1069,7 @@ mod should { #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1026,7 +1081,7 @@ mod should { reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1038,11 +1093,11 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1050,11 +1105,11 @@ mod should { reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 2, reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }; @@ -1062,7 +1117,7 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1075,7 +1130,7 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), @@ -1085,11 +1140,11 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1098,17 +1153,17 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription change" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1118,7 +1173,7 @@ mod should { }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1128,27 +1183,27 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; "to handshake stopped with custom cursor on disconnect" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1160,7 +1215,7 @@ mod should { reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }, SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1171,11 +1226,11 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1183,18 +1238,18 @@ mod should { reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }, SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }; "to handshake failed with custom cursor on give up" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1203,42 +1258,42 @@ mod should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::HandshakeReconnectSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }; "to receiving on reconnect success" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::HandshakeReconnectSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 2 } }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "20".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "20".into(), region: 2 } }; "to receiving with custom cursor on reconnect success" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1249,44 +1304,44 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 2 } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }), }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1295,11 +1350,11 @@ mod should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, messages: vec![] }, SubscribeState::HandshakeReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1325,7 +1380,7 @@ mod should { #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1337,7 +1392,7 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), @@ -1347,11 +1402,11 @@ mod should { )] #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::SubscriptionChanged { @@ -1359,26 +1414,26 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription changed" )] #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), cursor: None, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, - SubscribeEvent::Reconnect, + SubscribeEvent::Reconnect { cursor: None }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1388,26 +1443,68 @@ mod should { )] #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: None, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + SubscribeEvent::Reconnect { + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }) + }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + }; + "to handshaking on reconnect with custom cursor" + )] + #[test_case( + SubscribeState::HandshakeFailed { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, - SubscribeEvent::Reconnect, + SubscribeEvent::Reconnect { cursor: None }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on reconnect" )] #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + SubscribeEvent::Reconnect { + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }) + }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }), + }; + "to handshaking with custom cursor on reconnect with custom cursor" + )] + #[test_case( + SubscribeState::HandshakeFailed { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1417,43 +1514,56 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 2 } }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }) + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }) }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: None, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + SubscribeEvent::UnsubscribeAll, + SubscribeState::Unsubscribed; + "to unsubscribed on unsubscribe all" + )] + #[test_case( + SubscribeState::HandshakeFailed { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1461,11 +1571,11 @@ mod should { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, messages: vec![] }, SubscribeState::HandshakeFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1490,43 +1600,49 @@ mod should { #[test_case( SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), cursor: None, }, - SubscribeEvent::Reconnect, - SubscribeState::Handshaking { - input: SubscribeInput::new( - &Some(vec!["ch1".to_string()]), - &Some(vec!["gr1".to_string()]) + SubscribeEvent::SubscriptionChanged { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]) + }, + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) ), - cursor: None, + cursor: None }; - "to handshaking on reconnect" + "to handshaking stopped on subscription changed" )] #[test_case( SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, - SubscribeEvent::Reconnect, - SubscribeState::Handshaking { - input: SubscribeInput::new( - &Some(vec!["ch1".to_string()]), - &Some(vec!["gr1".to_string()]) + SubscribeEvent::SubscriptionChanged { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]) + }, + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }; - "to handshaking with custom cursor on reconnect" + "to handshaking stopped with custom cursor on subscription changed" )] #[test_case( SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1535,53 +1651,141 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 } }, - SubscribeState::Handshaking { - input: SubscribeInput::new( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }) }; - "to handshaking on subscription restored" + "to handshaking stopped on subscription restored" )] #[test_case( SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "10".into(), region: 2 } }, - SubscribeState::Handshaking { - input: SubscribeInput::new( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }), }; - "to handshaking with custom cursor on subscription restored" + "to handshaking stopped with custom cursor on subscription restored" )] #[test_case( SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: None, + }, + SubscribeEvent::Reconnect { cursor: None }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: None, + }; + "to handshaking on reconnect" + )] + #[test_case( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: None, + }, + SubscribeEvent::Reconnect { + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }) + }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + }; + "to handshaking on reconnect with custom cursor" + )] + #[test_case( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + }, + SubscribeEvent::Reconnect { cursor: None }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + }; + "to handshaking with custom cursor on reconnect" + )] + #[test_case( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + }, + SubscribeEvent::Reconnect { + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }) + }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 2 }), + }; + "to handshaking with custom cursor on reconnect with custom cursor" + )] + #[test_case( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 1 }), + }, + SubscribeEvent::UnsubscribeAll, + SubscribeState::Unsubscribed; + "to unsubscribed on unsubscribe all" + )] + #[test_case( + SubscribeState::HandshakeStopped { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), cursor: None, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, messages: vec![] }, SubscribeState::HandshakeStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), @@ -1605,85 +1809,85 @@ mod should { #[test_case( SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] #[test_case( SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 2 }, }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 2 }, }; "to receiving on subscription restored" )] #[test_case( SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 2 }, messages: vec![] }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 2 }, }; "to receiving on receive success" )] #[test_case( SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::ReceiveFailure { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }, SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }; @@ -1691,39 +1895,51 @@ mod should { )] #[test_case( SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::Disconnect, SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on disconnect" )] #[test_case( SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + }, + SubscribeEvent::UnsubscribeAll, + SubscribeState::Unsubscribed; + "to unsubscribed on unsubscribe all" + )] + #[test_case( + SubscribeState::Receiving { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to not change on unexpected event" )] @@ -1743,11 +1959,11 @@ mod should { #[test_case( SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, @@ -1755,11 +1971,11 @@ mod should { reason: PubNubError::Transport { details: "Test reconnect error".to_string(), response: None, } }, SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 2, reason: PubNubError::Transport { details: "Test reconnect error".to_string(), response: None, } }; @@ -1767,11 +1983,34 @@ mod should { )] #[test_case( SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } + }, + SubscribeEvent::ReceiveReconnectSuccess { + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, + messages: vec![] + }, + SubscribeState::Receiving { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, + }; + "to receiving on reconnect success" + )] + #[test_case( + SubscribeState::ReceiveReconnecting { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, @@ -1780,65 +2019,65 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] #[test_case( SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on subscription restored" )] #[test_case( SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::Disconnect, SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on disconnect" )] #[test_case( SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, @@ -1846,34 +2085,48 @@ mod should { reason: PubNubError::Transport { details: "Test give up error".to_string(), response: None, } }, SubscribeState::ReceiveFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test give up error".to_string(), response: None, } }; "to receive failed on give up" )] #[test_case( SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } + }, + SubscribeEvent::UnsubscribeAll, + SubscribeState::Unsubscribed; + "to unsubscribed on unsubscribe all" + )] + #[test_case( + SubscribeState::ReceiveReconnecting { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::ReceiveReconnecting { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }; @@ -1895,11 +2148,11 @@ mod should { #[test_case( SubscribeState::ReceiveFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::SubscriptionChanged { @@ -1907,74 +2160,108 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on subscription changed" )] #[test_case( SubscribeState::ReceiveFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "100".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "100".into(), region: 1 }), }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::ReceiveFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, - SubscribeEvent::Reconnect, + SubscribeEvent::Reconnect { cursor: None }, SubscribeState::Handshaking { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on reconnect" )] #[test_case( SubscribeState::ReceiveFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } + }, + SubscribeEvent::Reconnect { + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 3 }) + }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 3 }), + }; + "to handshaking on reconnect with custom cursor" + )] + #[test_case( + SubscribeState::ReceiveFailed { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } + }, + SubscribeEvent::UnsubscribeAll, + SubscribeState::Unsubscribed; + "to unsubscribed on unsubscribe all" + )] + #[test_case( + SubscribeState::ReceiveFailed { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 } }, SubscribeState::ReceiveFailed { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }; "to not change on unexpected event" @@ -1995,82 +2282,114 @@ mod should { #[test_case( SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, - }, - SubscribeEvent::Reconnect, - SubscribeState::Handshaking { - input: SubscribeInput::new( - &Some(vec!["ch1".to_string()]), - &Some(vec!["gr1".to_string()]) - ), - cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), - }; - "to handshaking on reconnect" - )] - #[test_case( - SubscribeState::ReceiveStopped { - input: SubscribeInput::new( - &Some(vec!["ch1".to_string()]), - &Some(vec!["gr1".to_string()]) - ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on subscription changed" )] #[test_case( SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch2".to_string()]), &Some(vec!["gr2".to_string()]) ), - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 }, }; "to receive stopped on subscription restored" )] #[test_case( SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + }, + SubscribeEvent::Reconnect { cursor: None }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "10".into(), region: 1 }), + }; + "to handshaking on reconnect" + )] + #[test_case( + SubscribeState::ReceiveStopped { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + }, + SubscribeEvent::Reconnect { + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 3 }) + }, + SubscribeState::Handshaking { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: Some(SubscriptionCursor { timetoken: "20".into(), region: 3 }), + }; + "to handshaking on reconnect with custom cursor" + )] + #[test_case( + SubscribeState::ReceiveStopped { + input: SubscriptionInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, + }, + SubscribeEvent::UnsubscribeAll, + SubscribeState::Unsubscribed; + "to unsubscribed on unsubscribe all" + )] + #[test_case( + SubscribeState::ReceiveStopped { + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: "100".into(), region: 1 } + cursor: SubscriptionCursor { timetoken: "100".into(), region: 1 } }, SubscribeState::ReceiveStopped { - input: SubscribeInput::new( + input: SubscriptionInput::new( &Some(vec!["ch1".to_string()]), &Some(vec!["gr1".to_string()]) ), - cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + cursor: SubscriptionCursor { timetoken: "10".into(), region: 1 }, }; "to not change on unexpected event" )] diff --git a/src/dx/subscribe/event_engine/types.rs b/src/dx/subscribe/event_engine/types.rs index 070edc32..758d9b00 100644 --- a/src/dx/subscribe/event_engine/types.rs +++ b/src/dx/subscribe/event_engine/types.rs @@ -1,6 +1,6 @@ //! Subscribe event engine module types. //! -//! This module contains the [`SubscribeInput`] type, which represents +//! This module contains the [`SubscriptionInput`] type, which represents //! user-provided channels and groups for which real-time updates should be //! retrieved from the [`PubNub`] network. //! @@ -10,9 +10,12 @@ use crate::{ core::PubNubError, lib::{ alloc::collections::HashSet, - core::ops::{Add, AddAssign, Sub, SubAssign}, + core::{ + iter::Sum, + ops::{Add, AddAssign, Sub, SubAssign}, + }, }, - subscribe::SubscribeCursor, + subscribe::SubscriptionCursor, }; /// User-provided channels and groups for subscription. @@ -22,7 +25,7 @@ use crate::{ /// /// [`PubNub`]:https://www.pubnub.com/ #[derive(Clone, Debug, PartialEq)] -pub struct SubscribeInput { +pub struct SubscriptionInput { /// Optional list of channels. /// /// List of channels for which real-time updates should be retrieved @@ -45,7 +48,7 @@ pub struct SubscribeInput { pub is_empty: bool, } -impl SubscribeInput { +impl SubscriptionInput { pub fn new(channels: &Option>, channel_groups: &Option>) -> Self { let channels = channels.as_ref().map(|channels| { channels.iter().fold(HashSet::new(), |mut acc, channel| { @@ -70,11 +73,25 @@ impl SubscribeInput { } } + /// Check if the given name is contained in the channel or channel group. + /// + /// # Arguments + /// + /// * `name` - A string reference containing the name to be checked. + /// + /// # Returns + /// + /// Returns `true` if the name is found in the channel or channel group, + /// `false` otherwise. + pub fn contains(&self, name: &str) -> bool { + self.contains_channel(name) || self.contains_channel_group(name) + } + pub fn channels(&self) -> Option> { self.channels.clone().map(|ch| ch.into_iter().collect()) } - pub fn contains_channel(&self, channel: &String) -> bool { + pub fn contains_channel(&self, channel: &str) -> bool { self.channels .as_ref() .map_or(false, |channels| channels.contains(channel)) @@ -86,7 +103,7 @@ impl SubscribeInput { .map(|ch| ch.into_iter().collect()) } - pub fn contains_channel_group(&self, channel_group: &String) -> bool { + pub fn contains_channel_group(&self, channel_group: &str) -> bool { self.channel_groups .as_ref() .map_or(false, |channel_groups| { @@ -120,7 +137,7 @@ impl SubscribeInput { } } -impl Add for SubscribeInput { +impl Add for SubscriptionInput { type Output = Self; fn add(self, rhs: Self) -> Self::Output { @@ -137,13 +154,13 @@ impl Add for SubscribeInput { } } -impl Default for SubscribeInput { +impl Default for SubscriptionInput { fn default() -> Self { - SubscribeInput::new(&None, &None) + SubscriptionInput::new(&None, &None) } } -impl AddAssign for SubscribeInput { +impl AddAssign for SubscriptionInput { fn add_assign(&mut self, rhs: Self) { let channel_groups = self.join_sets(&self.channel_groups, &rhs.channel_groups); let channels = self.join_sets(&self.channels, &rhs.channels); @@ -156,7 +173,7 @@ impl AddAssign for SubscribeInput { } } -impl Sub for SubscribeInput { +impl Sub for SubscriptionInput { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -173,7 +190,7 @@ impl Sub for SubscribeInput { } } -impl SubAssign for SubscribeInput { +impl SubAssign for SubscriptionInput { fn sub_assign(&mut self, rhs: Self) { let channel_groups = self.sub_sets(&self.channel_groups, &rhs.channel_groups); let channels = self.sub_sets(&self.channels, &rhs.channels); @@ -186,6 +203,12 @@ impl SubAssign for SubscribeInput { } } +impl Sum for SubscriptionInput { + fn sum>(iter: I) -> Self { + iter.fold(Default::default(), Add::add) + } +} + #[cfg(feature = "std")] #[derive(Clone)] /// Subscribe event engine data. @@ -200,7 +223,7 @@ pub(crate) struct SubscriptionParams<'execution> { pub channel_groups: &'execution Option>, /// Time cursor. - pub cursor: Option<&'execution SubscribeCursor>, + pub cursor: Option<&'execution SubscriptionCursor>, /// How many consequent retry attempts has been made. pub attempt: u8, @@ -220,13 +243,13 @@ mod it_should { #[test] fn create_empty_input() { - let input = SubscribeInput::new(&None, &None); + let input = SubscriptionInput::new(&None, &None); assert!(input.is_empty); } #[test] fn create_input_with_unique_channels() { - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -251,7 +274,7 @@ mod it_should { #[test] fn create_input_with_unique_channel_groups() { - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &None, &Some(vec![ "channel-group-1".into(), @@ -276,8 +299,8 @@ mod it_should { #[test] fn add_unique_channels_to_empty_input() { - let empty_input = SubscribeInput::new(&None, &None); - let input = SubscribeInput::new( + let empty_input = SubscriptionInput::new(&None, &None); + let input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -307,8 +330,8 @@ mod it_should { #[test] fn add_unique_channel_groups_to_empty_input() { - let empty_input = SubscribeInput::new(&None, &None); - let input = SubscribeInput::new( + let empty_input = SubscriptionInput::new(&None, &None); + let input = SubscriptionInput::new( &None, &Some(vec![ "channel-group-1".into(), @@ -338,7 +361,7 @@ mod it_should { #[test] fn add_unique_channels_and_channel_groups_to_existing_input() { - let existing_input = SubscribeInput::new( + let existing_input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-4".into(), @@ -350,7 +373,7 @@ mod it_should { "channel-group-5".into(), ]), ); - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -404,7 +427,7 @@ mod it_should { #[test] fn add_assign_unique_channels_and_channel_groups_to_existing_input() { - let mut existing_input = SubscribeInput::new( + let mut existing_input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-4".into(), @@ -416,7 +439,7 @@ mod it_should { "channel-group-5".into(), ]), ); - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -470,8 +493,8 @@ mod it_should { #[test] fn remove_channels_from_empty_input() { - let empty_input = SubscribeInput::new(&None, &None); - let input = SubscribeInput::new( + let empty_input = SubscriptionInput::new(&None, &None); + let input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -491,8 +514,8 @@ mod it_should { #[test] fn remove_channel_groups_from_empty_input() { - let empty_input = SubscribeInput::new(&None, &None); - let input = SubscribeInput::new( + let empty_input = SubscriptionInput::new(&None, &None); + let input = SubscriptionInput::new( &None, &Some(vec![ "channel-group-1".into(), @@ -512,7 +535,7 @@ mod it_should { #[test] fn remove_unique_channels_from_existing_input() { - let existing_input = SubscribeInput::new( + let existing_input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -524,7 +547,8 @@ mod it_should { "channel-group-3".into(), ]), ); - let input = SubscribeInput::new(&Some(vec!["channel-2".into(), "channel-2".into()]), &None); + let input = + SubscriptionInput::new(&Some(vec!["channel-2".into(), "channel-2".into()]), &None); assert!(!existing_input.is_empty); assert!(!input.is_empty); @@ -562,7 +586,7 @@ mod it_should { #[test] fn remove_unique_channel_groups_from_existing_input() { - let existing_input = SubscribeInput::new( + let existing_input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -574,7 +598,7 @@ mod it_should { "channel-group-3".into(), ]), ); - let input = SubscribeInput::new(&None, &Some(vec!["channel-group-1".into()])); + let input = SubscriptionInput::new(&None, &Some(vec!["channel-group-1".into()])); assert!(!existing_input.is_empty); assert!(!input.is_empty); @@ -612,7 +636,7 @@ mod it_should { #[test] fn remove_unique_channels_and_channel_groups_from_existing_input() { - let existing_input = SubscribeInput::new( + let existing_input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -624,7 +648,7 @@ mod it_should { "channel-group-3".into(), ]), ); - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &Some(vec!["channel-3".into()]), &Some(vec!["channel-group-2".into(), "channel-group-3".into()]), ); @@ -661,7 +685,7 @@ mod it_should { #[test] fn remove_assign_unique_channels_and_channel_groups_from_existing_input() { - let mut existing_input = SubscribeInput::new( + let mut existing_input = SubscriptionInput::new( &Some(vec![ "channel-1".into(), "channel-2".into(), @@ -673,7 +697,7 @@ mod it_should { "channel-group-3".into(), ]), ); - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &Some(vec!["channel-3".into()]), &Some(vec!["channel-group-2".into(), "channel-group-3".into()]), ); @@ -710,11 +734,11 @@ mod it_should { #[test] fn remove_all_channels_and_channel_groups_from_existing_input() { - let existing_input = SubscribeInput::new( + let existing_input = SubscriptionInput::new( &Some(vec!["channel-1".into(), "channel-2".into()]), &Some(vec!["channel-group-1".into(), "channel-group-2".into()]), ); - let input = SubscribeInput::new( + let input = SubscriptionInput::new( &Some(vec!["channel-1".into(), "channel-2".into()]), &Some(vec!["channel-group-1".into(), "channel-group-2".into()]), ); diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index 28ee9b71..e01a8c33 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -10,28 +10,51 @@ use futures::{ #[cfg(feature = "std")] use spin::RwLock; -use crate::dx::{pubnub_client::PubNubClientInstance, subscribe::raw::RawSubscriptionBuilder}; - -#[doc(inline)] -pub use types::{ - File, MessageAction, Object, Presence, SubscribeCursor, SubscribeMessageType, SubscribeStatus, - SubscribeStreamEvent, -}; -pub mod types; - #[cfg(feature = "std")] use crate::{ core::{Deserializer, PubNubError, Transport}, - lib::alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}, + lib::alloc::{boxed::Box, sync::Arc, vec::Vec}, subscribe::result::SubscribeResult, }; #[cfg(feature = "std")] -use crate::core::{ - event_engine::{CancellationTask, EventEngine}, - runtime::Runtime, +use crate::{ + core::{ + event_engine::{CancellationTask, EventEngine}, + runtime::Runtime, + DataStream, PubNubEntity, + }, + lib::alloc::string::ToString, }; +use crate::{ + dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String, + subscribe::raw::RawSubscriptionBuilder, +}; + +#[cfg(all(feature = "presence", feature = "std"))] +use event_engine::SubscriptionInput; +#[cfg(feature = "std")] +use event_engine::{SubscribeEffectHandler, SubscribeEventEngine, SubscribeState}; + +#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] +pub(crate) mod event_engine; + +#[cfg(feature = "std")] +pub(crate) use subscription_manager::SubscriptionManager; +#[cfg(feature = "std")] +pub(crate) mod subscription_manager; + +#[cfg(feature = "std")] +#[doc(inline)] +pub(crate) use event_dispatcher::EventDispatcher; +#[cfg(feature = "std")] +mod event_dispatcher; + +#[doc(inline)] +pub use types::*; +pub mod types; + #[doc(inline)] pub use builders::*; pub mod builders; @@ -41,73 +64,190 @@ pub use result::{SubscribeResponseBody, Update}; pub mod result; #[cfg(feature = "std")] -pub(crate) use subscription_manager::SubscriptionManager; +#[doc(inline)] +pub use subscription::Subscription; #[cfg(feature = "std")] -pub(crate) mod subscription_manager; +mod subscription; + #[cfg(feature = "std")] #[doc(inline)] -use event_engine::{ - types::SubscriptionParams, SubscribeEffectHandler, SubscribeEventEngine, SubscribeInput, - SubscribeState, -}; +pub use subscription_set::SubscriptionSet; +#[cfg(feature = "std")] +mod subscription_set; #[cfg(feature = "std")] -pub(crate) mod event_engine; +#[doc(inline)] +pub use traits::{EventEmitter, EventSubscriber, Subscribable, SubscribableType, Subscriber}; +#[cfg(feature = "std")] +pub(crate) mod traits; #[cfg(feature = "std")] impl PubNubClientInstance where T: Transport + Send + 'static, - D: Deserializer + 'static, + D: Deserializer + Send + 'static, { - /// Create subscription listener. + /// Stream used to notify connection state change events. + pub fn status_stream(&self) -> DataStream { + self.event_dispatcher.status_stream() + } + + /// Handle connection status change. /// - /// Listeners configure [`PubNubClient`] to receive real-time updates for - /// specified list of channels and groups. + /// # Arguments /// - /// ```no_run // Starts listening for real-time updates - /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// * `status` - Current connection status. + pub(crate) fn handle_status(&self, status: ConnectionStatus) { + self.event_dispatcher.handle_status(status.clone()); + let mut should_terminate = false; + + { + if let Some(manager) = self.subscription_manager(false).read().as_ref() { + should_terminate = !manager.has_handlers(); + } + } + + // Terminate event engine because there is no event listeners (registered + // Subscription and SubscriptionSet instances). + if matches!(status, ConnectionStatus::Disconnected) && should_terminate { + self.terminate() + } + } + + /// Handles the given events. + /// + /// # Arguments + /// + /// * `cursor` - A time cursor for next portion of events. + /// * `events` - A slice of real-time events from multiplexed subscription. + pub(crate) fn handle_events(&self, cursor: SubscriptionCursor, events: &[Update]) { + let mut cursor_slot = self.cursor.write(); + if let Some(current_cursor) = cursor_slot.as_ref() { + cursor + .gt(current_cursor) + .then(|| *cursor_slot = Some(cursor)); + } else { + *cursor_slot = Some(cursor); + } + + self.event_dispatcher.handle_events(events.to_vec()) + } + + /// Creates a clone of the [`PubNubClientInstance`] with an empty event + /// dispatcher. + /// + /// Empty clones have the same client state but an empty list of + /// real-time event listeners, which makes it possible to attach listeners + /// specific to the context. When the cloned client set goes out of scope, + /// all associated listeners will be invalidated and released. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; /// /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// client - /// .subscribe() - /// .channels(["hello".into(), "world".into()].to_vec()) - /// .execute()? - /// .stream() - /// .for_each(|event| async move { - /// match event { - /// SubscribeStreamEvent::Update(update) => println!("update: {:?}", update), - /// SubscribeStreamEvent::Status(status) => println!("status: {:?}", status), - /// } - /// }) - /// .await; - /// # Ok(()) + /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// // ... + /// // We need to pass client into other component which would like to + /// // have own listeners to handle real-time events. + /// let empty_client = client.clone_empty(); + /// // self.other_component(empty_client); + /// # Ok(()) /// # } /// ``` /// - /// For more examples see our [examples directory](https://github.com/pubnub/rust/tree/master/examples). + /// # Returns /// - /// Instance of [`SubscriptionBuilder`] returned. - /// [`PubNubClient`]: crate::PubNubClient - pub fn subscribe(&self) -> SubscriptionBuilder { - self.configure_subscribe(); + /// A new instance of the subscription object with an empty event + /// dispatcher. + pub fn clone_empty(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + cursor: Arc::clone(&self.cursor), + event_dispatcher: Arc::clone(&self.event_dispatcher), + } + } +} - SubscriptionBuilder { - subscription: Some(self.subscription.clone()), - ..Default::default() +#[cfg(feature = "std")] +impl PubNubClientInstance +where + T: Transport + Send + 'static, + D: Deserializer + Send + 'static, +{ + /// Creates multiplexed subscriptions. + /// + /// # Arguments + /// + /// * `parameters` - [`SubscriptionParams`] configuration object. + /// + /// # Returns + /// + /// The created [`SubscriptionSet`] object. + /// + /// # Example + /// + /// ```rust,no_run + /// use futures::StreamExt; + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset, subscribe::EventEmitter}; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// use pubnub::subscribe::{EventSubscriber, SubscriptionParams}; + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let subscription = client.subscription(SubscriptionParams { + /// channels: Some(&["my_channel_1", "my_channel_2", "my_channel_3"]), + /// channel_groups: None, + /// options: None + /// }); + /// // Message stream for handling real-time `Message` events. + /// let stream = subscription.messages_stream(); + /// # Ok(()) + /// # } + /// ``` + pub fn subscription(&self, parameters: SubscriptionParams) -> SubscriptionSet + where + N: Into + Clone, + { + let mut entities: Vec> = vec![]; + if let Some(channel_names) = parameters.channels { + entities.extend( + channel_names + .iter() + .cloned() + .map(|name| self.channel(name).into()) + .collect::>>(), + ); + } + if let Some(channel_group_names) = parameters.channel_groups { + entities.extend( + channel_group_names + .iter() + .cloned() + .map(|name| self.channel_group(name).into()) + .collect::>>(), + ); } + + SubscriptionSet::new(entities, parameters.options) } /// Stop receiving real-time updates. @@ -117,26 +257,28 @@ where /// /// ```no_run /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// use pubnub::dx::subscribe::{EventEmitter, SubscribeStreamEvent, Update}; /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// use pubnub::subscribe::SubscriptionParams; /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// # let stream = // SubscriptionStream - /// # client - /// # .subscribe() - /// # .channels(["hello".into(), "world".into()].to_vec()) - /// # .execute()? - /// # .stream(); + /// # let client = PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: None, + /// # }) + /// # .with_user_id("user_id") + /// # .build()?; + /// # let subscription = client.subscription(SubscriptionParams { + /// # channels: Some(&["channel"]), + /// # channel_groups: None, + /// # options: None + /// # }); + /// # let stream = // DataStream + /// # subscription.messages_stream(); /// client.disconnect(); /// # Ok(()) /// # } @@ -144,11 +286,16 @@ where /// /// [`PubNub`]: https://www.pubnub.com pub fn disconnect(&self) { - let mut input: Option = None; + #[cfg(feature = "presence")] + let mut input: Option = None; + + if let Some(manager) = self.subscription_manager(false).read().as_ref() { + #[cfg(feature = "presence")] + { + let current_input = manager.current_input(); + input = (!current_input.is_empty).then_some(current_input); + } - if let Some(manager) = self.subscription.read().as_ref() { - let current_input = manager.current_input(); - input = (!current_input.is_empty).then_some(current_input); manager.disconnect() } @@ -158,7 +305,7 @@ where return; }; - if self.config.heartbeat_interval.is_none() { + if self.config.presence.heartbeat_interval.is_none() { let mut request = self.leave(); if let Some(channels) = input.channels() { request = request.channels(channels); @@ -170,7 +317,7 @@ where self.runtime.spawn(async { let _ = request.execute().await; }) - } else if let Some(presence) = self.presence.clone().read().as_ref() { + } else if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.disconnect(); } } @@ -183,41 +330,47 @@ where /// /// ```no_run /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// use pubnub::dx::subscribe::{EventEmitter, SubscribeStreamEvent, Update}; /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// use pubnub::subscribe::SubscriptionParams; /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// # let stream = // SubscriptionStream - /// # client - /// # .subscribe() - /// # .channels(["hello".into(), "world".into()].to_vec()) - /// # .execute()? - /// # .stream(); + /// # let client = PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: None, + /// # }) + /// # .with_user_id("user_id") + /// # .build()?; + /// # let subscription = client.subscription(SubscriptionParams { + /// # channels: Some(&["channel"]), + /// # channel_groups: None, + /// # options: None + /// # }); + /// # let stream = // DataStream + /// # subscription.messages_stream(); /// # // ..... /// # client.disconnect(); - /// client.reconnect(); + /// client.reconnect(None); /// # Ok(()) /// # } /// ``` /// /// [`PubNub`]: https://www.pubnub.com - pub fn reconnect(&self) { - let mut input: Option = None; + pub fn reconnect(&self, cursor: Option) { + #[cfg(feature = "presence")] + let mut input: Option = None; - if let Some(manager) = self.subscription.read().as_ref() { - let current_input = manager.current_input(); - input = (!current_input.is_empty).then_some(current_input); - manager.reconnect() + if let Some(manager) = self.subscription_manager(false).read().as_ref() { + #[cfg(feature = "presence")] + { + let current_input = manager.current_input(); + input = (!current_input.is_empty).then_some(current_input); + } + manager.reconnect(cursor); } #[cfg(feature = "presence")] @@ -226,7 +379,7 @@ where return; }; - if self.config.heartbeat_interval.is_none() { + if self.config.presence.heartbeat_interval.is_none() { let mut request = self.heartbeat(); if let Some(channels) = input.channels() { request = request.channels(channels); @@ -238,29 +391,63 @@ where self.runtime.spawn(async { let _ = request.execute().await; }) - } else if let Some(presence) = self.presence.clone().read().as_ref() { + } else if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.reconnect(); } } } - pub(crate) fn configure_subscribe(&self) -> Arc>> { + /// Unsubscribes from all real-time events. + /// + /// Stop any actions for receiving real-time events processing for all + /// created [`Subscription`] and [`SubscriptionSet`]. + pub fn unsubscribe_all(&self) { + { + if let Some(manager) = self.subscription_manager(false).write().as_mut() { + manager.unregister_all() + } + } + } + + /// Subscription manager which maintains Subscription EE. + /// + /// # Arguments + /// + /// `create` - Whether manager should be created if not initialized. + /// + /// # Returns + /// + /// Returns an [`SubscriptionManager`] which represents the manager. + #[cfg(feature = "subscribe")] + pub(crate) fn subscription_manager( + &self, + create: bool, + ) -> Arc>>> { + { + let manager = self.subscription.read(); + if manager.is_some() || !create { + return self.subscription.clone(); + } + } + { // Initialize subscription module when it will be first required. let mut slot = self.subscription.write(); - if slot.is_none() { + if slot.is_none() && create { #[cfg(feature = "presence")] - self.configure_presence(); - let heartbeat_self = self.clone(); + #[cfg(feature = "presence")] let leave_self = self.clone(); + *slot = Some(SubscriptionManager::new( self.subscribe_event_engine(), - Arc::new(move |channels, groups| { + #[cfg(feature = "presence")] + Arc::new(move |channels, groups, _all| { Self::subscribe_heartbeat_call(heartbeat_self.clone(), channels, groups); }), - Arc::new(move |channels, groups| { - Self::subscribe_leave_call(leave_self.clone(), channels, groups); + #[cfg(feature = "presence")] + Arc::new(move |channels, groups, all| { + Self::subscribe_leave_call(leave_self.clone(), channels, groups, all); }), )); } @@ -274,26 +461,31 @@ where let emit_messages_client = self.clone(); let emit_status_client = self.clone(); let subscribe_client = self.clone(); - let request_retry_delay_policy = self.config.retry_policy.clone(); - let request_retry_policy = self.config.retry_policy.clone(); + let request_retry = self.config.transport.retry_configuration.clone(); + let request_subscribe_retry = request_retry.clone(); let runtime = self.runtime.clone(); let runtime_sleep = runtime.clone(); - let (cancel_tx, cancel_rx) = async_channel::bounded::(channel_bound); EventEngine::new( SubscribeEffectHandler::new( Arc::new(move |params| { - let delay_in_secs = request_retry_delay_policy - .retry_delay(¶ms.attempt, params.reason.as_ref()); + let delay_in_microseconds = request_subscribe_retry.retry_delay( + Some("/v2/subscribe".to_string()), + ¶ms.attempt, + params.reason.as_ref(), + ); let inner_runtime_sleep = runtime_sleep.clone(); Self::subscribe_call( subscribe_client.clone(), params.clone(), Arc::new(move || { - if let Some(delay) = delay_in_secs { - inner_runtime_sleep.clone().sleep(delay).boxed() + if let Some(delay) = delay_in_microseconds { + inner_runtime_sleep + .clone() + .sleep_microseconds(delay) + .boxed() } else { ready(()).boxed() } @@ -302,10 +494,10 @@ where ) }), Arc::new(move |status| Self::emit_status(emit_status_client.clone(), &status)), - Arc::new(Box::new(move |updates| { - Self::emit_messages(emit_messages_client.clone(), updates) + Arc::new(Box::new(move |updates, cursor: SubscriptionCursor| { + Self::emit_messages(emit_messages_client.clone(), updates, cursor) })), - request_retry_policy, + request_retry, cancel_tx, ), SubscribeState::Unsubscribed, @@ -315,7 +507,7 @@ where fn subscribe_call( client: Self, - params: SubscriptionParams, + params: event_engine::types::SubscriptionParams, delay: Arc, cancel_rx: async_channel::Receiver, ) -> BoxFuture<'static, Result> @@ -333,6 +525,15 @@ where if let Some(channel_groups) = params.channel_groups.clone() { request = request.channel_groups(channel_groups); } + + #[cfg(feature = "presence")] + { + let state = client.state.read(); + if params.cursor.is_none() && !state.is_empty() { + request = request.state(state.clone()); + } + } + let cancel_task = CancellationTask::new(cancel_rx, params.effect_id.to_owned()); // TODO: needs to be owned? request @@ -347,29 +548,16 @@ where /// * can operate - call `join` announcement /// * can't operate (heartbeat interval not set) - make direct `heartbeat` /// call. + #[cfg(all(feature = "presence", feature = "std"))] fn subscribe_heartbeat_call( client: Self, channels: Option>, channel_groups: Option>, ) { - #[cfg(feature = "presence")] - { - if client.config.heartbeat_interval.is_none() { - let mut request = client.heartbeat(); - if let Some(channels) = channels { - request = request.channels(channels); - } - if let Some(channel_groups) = channel_groups { - request = request.channel_groups(channel_groups); - } - - client.runtime.spawn(async { - let _ = request.execute().await; - }) - } else if let Some(presence) = client.presence.clone().read().as_ref() { - presence.announce_join(channels, channel_groups); - } - } + client.announce_join( + Self::presence_filtered_entries(channels), + Self::presence_filtered_entries(channel_groups), + ); } /// Subscription event engine presence `leave` announcement. @@ -378,38 +566,30 @@ where /// presence event engine state: /// * can operate - call `leave` announcement /// * can't operate (heartbeat interval not set) - make direct `leave` call. + #[cfg(all(feature = "presence", feature = "std"))] fn subscribe_leave_call( client: Self, channels: Option>, channel_groups: Option>, + all: bool, ) { - #[cfg(feature = "presence")] - { - if client.config.heartbeat_interval.is_none() { - let mut request = client.leave(); - if let Some(channels) = channels { - request = request.channels(channels); - } - if let Some(channel_groups) = channel_groups { - request = request.channel_groups(channel_groups); - } - - client.runtime.spawn(async { - let _ = request.execute().await; - }) - } else if let Some(presence) = client.presence.clone().read().as_ref() { - presence.announce_left(channels, channel_groups); - } + if !all { + client.announce_left( + Self::presence_filtered_entries(channels), + Self::presence_filtered_entries(channel_groups), + ); + } else { + client.announce_left_all() } } - fn emit_status(client: Self, status: &SubscribeStatus) { - if let Some(manager) = client.subscription.read().as_ref() { + fn emit_status(client: Self, status: &ConnectionStatus) { + if let Some(manager) = client.subscription_manager(false).read().as_ref() { manager.notify_new_status(status) } } - fn emit_messages(client: Self, messages: Vec) { + fn emit_messages(client: Self, messages: Vec, cursor: SubscriptionCursor) { let messages = if let Some(cryptor) = &client.cryptor { messages .into_iter() @@ -419,10 +599,21 @@ where messages }; - if let Some(manager) = client.subscription.read().as_ref() { - manager.notify_new_messages(messages) + if let Some(manager) = client.subscription_manager(false).read().as_ref() { + manager.notify_new_messages(cursor, messages.clone()) } } + + /// Filter out `-pnpres` entries from the list. + #[cfg(feature = "presence")] + fn presence_filtered_entries(entries: Option>) -> Option> { + entries.map(|channels| { + channels + .into_iter() + .filter(|channel| !channel.ends_with("-pnpres")) + .collect::>() + }) + } } impl PubNubClientInstance { @@ -467,10 +658,34 @@ impl PubNubClientInstance { pub fn subscribe_raw(&self) -> RawSubscriptionBuilder { RawSubscriptionBuilder { pubnub_client: Some(self.clone()), + heartbeat: Some(self.config.presence.heartbeat_value), ..Default::default() } } + /// Update real-time events filtering expression. + /// + /// # Arguments + /// + /// * `expression` - A `String` representing the filter expression. + pub fn set_filter_expression(&self, expression: S) + where + S: Into, + { + let mut filter_expression = self.filter_expression.write(); + *filter_expression = expression.into(); + } + + /// Get real-time events filtering expression. + /// + /// # Returns + /// + /// Current real-time events filtering expression. + pub fn get_filter_expression(&self) -> Option { + let expression = self.filter_expression.read(); + (!expression.is_empty()).then(|| expression.clone()) + } + /// Create subscribe request builder. /// This method is used to create events stream for real-time updates on /// passed list of channels and groups. @@ -479,14 +694,53 @@ impl PubNubClientInstance { pub(crate) fn subscribe_request(&self) -> SubscribeRequestBuilder { SubscribeRequestBuilder { pubnub_client: Some(self.clone()), + heartbeat: Some(self.config.presence.heartbeat_value), ..Default::default() } } } +// =========================================================== +// EventEmitter implementation for module PubNubClientInstance +// =========================================================== + +#[cfg(feature = "std")] +impl EventEmitter for PubNubClientInstance { + fn messages_stream(&self) -> DataStream { + self.event_dispatcher.messages_stream() + } + + fn signals_stream(&self) -> DataStream { + self.event_dispatcher.signals_stream() + } + + fn message_actions_stream(&self) -> DataStream { + self.event_dispatcher.message_actions_stream() + } + + fn files_stream(&self) -> DataStream { + self.event_dispatcher.files_stream() + } + + fn app_context_stream(&self) -> DataStream { + self.event_dispatcher.app_context_stream() + } + + fn presence_stream(&self) -> DataStream { + self.event_dispatcher.presence_stream() + } + + fn stream(&self) -> DataStream { + self.event_dispatcher.stream() + } +} + #[cfg(feature = "std")] #[cfg(test)] mod should { + use futures::StreamExt; + use spin::RwLock; + use super::*; use crate::{ core::{blocking, PubNubError, TransportRequest, TransportResponse}, @@ -502,32 +756,65 @@ mod should { pub display_name: String, } - struct MockTransport; + struct MockTransport { + responses_count: RwLock, + } + + impl Default for MockTransport { + fn default() -> Self { + Self { + responses_count: RwLock::new(0), + } + } + } #[async_trait::async_trait] impl Transport for MockTransport { async fn send(&self, _request: TransportRequest) -> Result { + let mut count_slot = self.responses_count.write(); + let response_body = generate_body(*count_slot); + *count_slot += 1; + + if response_body.is_none() { + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + } + Ok(TransportResponse { status: 200, headers: [].into(), - body: generate_body(), + body: response_body, }) } } impl blocking::Transport for MockTransport { fn send(&self, _req: TransportRequest) -> Result { + let mut count_slot = self.responses_count.write(); + let response_body = generate_body(*count_slot); + *count_slot += 1; + Ok(TransportResponse { status: 200, headers: [].into(), - body: generate_body(), + body: response_body, }) } } - fn generate_body() -> Option> { - Some( - r#"{ + fn generate_body(response_count: u16) -> Option> { + match response_count { + 0 => Some( + r#"{ + "t": { + "t": "15628652479902717", + "r": 4 + }, + "m": [] + }"# + .into(), + ), + 1 => Some( + r#"{ "t": { "t": "15628652479932717", "r": 4 @@ -578,12 +865,14 @@ mod should { } ] }"# - .into(), - ) + .into(), + ), + _ => None, + } } fn client() -> PubNubGenericClient { - PubNubClientBuilder::with_transport(MockTransport) + PubNubClientBuilder::with_transport(Default::default()) .with_keyset(Keyset { subscribe_key: "demo", publish_key: Some("demo"), @@ -595,42 +884,38 @@ mod should { } #[tokio::test] - async fn create_builder() { - let _ = client().subscribe(); + async fn create_subscription_set() { + let _ = client().subscription(SubscriptionParams { + channels: Some(&["channel_a"]), + channel_groups: Some(&["group_a"]), + options: None, + }); } #[tokio::test] async fn subscribe() { - let subscription = client() - .subscribe() - .channels(["my-channel".into(), "my-channel-pnpres".into()].to_vec()) - .execute() - .unwrap(); + let client = client(); + let subscription = client.subscription(SubscriptionParams { + channels: Some(&["my-channel"]), + channel_groups: Some(&["group_a"]), + options: Some(vec![SubscriptionOptions::ReceivePresenceEvents]), + }); + subscription.subscribe(None); - use futures::StreamExt; - let status = subscription.stream().next().await.unwrap(); - let message = subscription.stream().next().await.unwrap(); - let presence = subscription.stream().next().await.unwrap(); - - assert!(matches!( - status, - SubscribeStreamEvent::Status(SubscribeStatus::Connected) - )); - assert!(matches!( - message, - SubscribeStreamEvent::Update(Update::Message(_)) - )); - assert!(matches!( - presence, - SubscribeStreamEvent::Update(Update::Presence(_)) - )); - if let SubscribeStreamEvent::Update(Update::Presence(Presence::StateChange { + let status = client.status_stream().next().await.unwrap(); + let _ = subscription.messages_stream().next().await.unwrap(); + let presence = subscription.presence_stream().next().await.unwrap(); + + assert!(matches!(status, ConnectionStatus::Connected)); + + if let Presence::StateChange { timestamp: _, channel: _, subscription: _, uuid: _, data, - })) = presence + .. + } = presence { let user_data: UserStateData = serde_json::from_value(data) .expect("Should successfully deserialize user state object."); @@ -639,6 +924,8 @@ mod should { } else { panic!("Expected to receive presence update.") } + + client.unsubscribe_all(); } #[tokio::test] diff --git a/src/dx/subscribe/result.rs b/src/dx/subscribe/result.rs index 24546434..a393d901 100644 --- a/src/dx/subscribe/result.rs +++ b/src/dx/subscribe/result.rs @@ -8,7 +8,7 @@ use crate::{ core::{service_response::APIErrorBody, PubNubError, ScalarValue}, dx::subscribe::{ types::Message, - File, MessageAction, Object, Presence, {SubscribeCursor, SubscribeMessageType}, + AppContext, File, MessageAction, Presence, {SubscribeMessageType, SubscriptionCursor}, }, lib::{ alloc::{ @@ -30,7 +30,7 @@ pub struct SubscribeResult { /// /// Next time cursor which can be used to fetch newer updates or /// catchup / restore subscription from specific point in time. - pub cursor: SubscribeCursor, + pub cursor: SubscriptionCursor, /// Received real-time updates. /// @@ -64,7 +64,7 @@ pub enum Update { Presence(Presence), /// Object real-time update. - Object(Object), + AppContext(AppContext), /// Message's actions real-time update. MessageAction(MessageAction), @@ -184,7 +184,7 @@ pub struct APISuccessBody { /// The cursor contains information about the start of the next real-time /// update timeframe. #[cfg_attr(feature = "serde", serde(rename = "t"))] - pub cursor: SubscribeCursor, + pub cursor: SubscriptionCursor, /// List of updates. /// @@ -197,7 +197,6 @@ pub struct APISuccessBody { /// Single entry from subscribe response #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[allow(dead_code)] pub struct Envelope { /// Shard number on which the event has been stored. #[cfg_attr(feature = "serde", serde(rename = "a"))] @@ -234,7 +233,7 @@ pub struct Envelope { /// /// [`PubNub`]: https://www.pubnub.com #[cfg_attr(feature = "serde", serde(rename = "p"))] - pub published: SubscribeCursor, + pub published: SubscriptionCursor, /// Name of channel where update received. #[cfg_attr(feature = "serde", serde(rename = "c"))] @@ -304,11 +303,11 @@ pub enum EnvelopePayload { /// The user's state associated with the channel has been updated. #[cfg(feature = "serde")] - data: serde_json::Value, + data: Option, /// The user's state associated with the channel has been updated. #[cfg(not(feature = "serde"))] - data: Vec, + data: Option>, /// The list of unique user identifiers that `joined` the channel since /// the last interval presence update. @@ -390,7 +389,6 @@ pub enum EnvelopePayload { /// Information about object for which update has been generated. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] -#[allow(dead_code)] pub enum ObjectDataBody { /// `Channel` object update payload body. Channel { @@ -534,9 +532,9 @@ impl TryFrom for SubscribeResult { } } +#[cfg(feature = "serde")] impl Envelope { /// Default message type. - #[allow(dead_code)] fn default_message_type() -> SubscribeMessageType { SubscribeMessageType::Message } @@ -550,11 +548,26 @@ impl Update { /// which real-time update has been delivered. pub(crate) fn subscription(&self) -> String { match self { - Update::Presence(presence) => presence.subscription(), - Update::Object(object) => object.subscription(), - Update::MessageAction(action) => action.subscription.clone(), - Update::File(file) => file.subscription.clone(), - Update::Message(message) | Update::Signal(message) => message.subscription.clone(), + Self::Presence(presence) => presence.subscription(), + Self::AppContext(object) => object.subscription(), + Self::MessageAction(reaction) => reaction.subscription.clone(), + Self::File(file) => file.subscription.clone(), + Self::Message(message) | Self::Signal(message) => message.subscription.clone(), + } + } + + /// PubNub high-precision event timestamp. + /// + /// # Returns + /// + /// Returns time when event has been emitted. + pub(crate) fn event_timestamp(&self) -> usize { + match self { + Self::Presence(presence) => presence.event_timestamp(), + Self::AppContext(object) => object.event_timestamp(), + Self::MessageAction(reaction) => reaction.timestamp, + Self::File(file) => file.timestamp, + Self::Message(message) | Self::Signal(message) => message.timestamp, } } } @@ -568,7 +581,7 @@ impl TryFrom for Update { EnvelopePayload::Object { .. } if matches!(value.message_type, SubscribeMessageType::Object) => { - Ok(Update::Object(value.try_into()?)) + Ok(Update::AppContext(value.try_into()?)) } EnvelopePayload::MessageAction { .. } if matches!(value.message_type, SubscribeMessageType::MessageAction) => diff --git a/src/dx/subscribe/subscription.rs b/src/dx/subscribe/subscription.rs new file mode 100644 index 00000000..3196b248 --- /dev/null +++ b/src/dx/subscribe/subscription.rs @@ -0,0 +1,845 @@ +//! # Subscription module. +//! +//! This module contains the [`Subscription`] type, which is used to manage +//! subscription to the specific entity and attach listeners to process +//! real-time events triggered for the `entity`. + +use spin::RwLock; +use uuid::Uuid; + +use crate::core::{Deserializer, Transport}; +use crate::{ + core::{DataStream, PubNubEntity}, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{ + string::String, + sync::{Arc, Weak}, + vec, + vec::Vec, + }, + collections::HashMap, + core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + ops::{Add, Deref, DerefMut, Drop}, + }, + }, + subscribe::{ + event_engine::SubscriptionInput, traits::EventHandler, AppContext, EventDispatcher, + EventEmitter, EventSubscriber, File, Message, MessageAction, Presence, SubscribableType, + SubscriptionCursor, SubscriptionOptions, SubscriptionSet, Update, + }, +}; + +/// Entity subscription. +/// +/// # Example +/// +/// ### Multiplexed subscription +/// +/// ```rust +/// use pubnub::{ +/// subscribe::{Subscriber, SubscriptionOptions}, Keyset, PubNubClient, PubNubClientBuilder, +/// }; +/// +/// # fn main() -> Result<(), pubnub::core::PubNubError> { +/// let client = // PubNubClient +/// # PubNubClientBuilder::with_reqwest_transport() +/// # .with_keyset(Keyset { +/// # subscribe_key: "demo", +/// # publish_key: Some("demo"), +/// # secret_key: Some("demo") +/// # }) +/// # .with_user_id("uuid") +/// # .build()?; +/// let channel = client.channel("my_channel"); +/// // Creating Subscription instance for the Channel entity to subscribe and listen +/// // for real-time events. +/// let subscription = channel.subscription(None); +/// // Subscription with presence announcements +/// let subscription_with_presence = channel.subscription(Some(vec![SubscriptionOptions::ReceivePresenceEvents])); +/// # Ok(()) +/// # } +/// ``` +/// +/// ### Sum of subscriptions +/// +/// ```rust +/// use pubnub::{ +/// subscribe::{Subscriber, Subscription, SubscriptionSet}, +/// Keyset, PubNubClient, PubNubClientBuilder, +/// }; +/// +/// # fn main() -> Result<(), pubnub::core::PubNubError> { +/// let client = // PubNubClient +/// # PubNubClientBuilder::with_reqwest_transport() +/// # .with_keyset(Keyset { +/// # subscribe_key: "demo", +/// # publish_key: Some("demo"), +/// # secret_key: Some("demo") +/// # }) +/// # .with_user_id("uuid") +/// # .build()?; +/// let channels = client.channels(&["my_channel_1", "my_channel_2"]); +/// // Two `Subscription` instances can be added to create `SubscriptionSet` which can be used +/// // to attach listeners and subscribe in one place for both subscriptions used in addition +/// // operation. +/// let subscription = channels[0].subscription(None) + channels[1].subscription(None); +/// # Ok(()) +/// # } +/// ``` +pub struct Subscription< + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +> { + /// Subscription reference. + pub(super) inner: Arc>, + + /// Whether subscription is `Clone::clone()` method call result or not. + is_clone: bool, +} + +/// Subscription reference +/// +/// This struct contains the actual subscription state. +/// It's wrapped in `Arc` by [`Subscription`] and uses interior mutability for +/// its internal state. +/// +/// Not intended to be used directly. Use [`Subscription`] instead. +pub struct SubscriptionRef { + /// Unique event handler instance identifier. + /// + /// [`Subscription`] can be cloned, but the internal state is always bound + /// to the same reference of [`SubscriptionState`] with the same `id`. + instance_id: String, + + /// Subscription state. + state: Arc>, + + /// Real-time event dispatcher. + event_dispatcher: Arc, +} + +/// Shared subscription state +/// +/// This struct contains state shared across all [`Subscription`] clones. +/// It's wrapped in `Arc` by [`SubscriptionRef`] and uses interior mutability +/// for its internal state. +/// +/// Not intended to be used directly. Use [`Subscription`] instead. +#[derive(Debug)] +pub struct SubscriptionState { + /// Unique event handler identifier. + pub(super) id: String, + + /// [`PubNubClientInstance`] which is backing which subscription. + pub(super) client: Weak>, + + /// Subscribable entity. + /// + /// Entity with information that is required to receive real-time updates + /// for it. + pub(super) entity: PubNubEntity, + + /// Whether set is currently subscribed and active. + pub(super) is_subscribed: Arc>, + + /// List of strings which represent data stream identifiers for entity + /// real-time events. + pub(super) subscription_input: SubscriptionInput, + + /// Subscription time cursor. + cursor: RwLock>, + + /// Subscription listener options. + /// + /// Options used to set up listener behavior and real-time events + /// processing. + options: Option>, + + /// The list of weak references to all [`SubscriptionRef`] clones created + /// for this reference. + clones: RwLock>>>, +} + +impl Subscription +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + /// Creates a new subscription for specified entity. + /// + /// # Arguments + /// + /// * `client` - Weak reference on [`PubNubClientInstance`] to access shared + /// resources. + /// * `entities` - A `PubNubEntity` representing the entity to subscribe to. + /// * `options` - An optional list of `SubscriptionOptions` specifying the + /// subscription behaviour. + /// + /// # Returns + /// + /// A new `Subscription` for the given `entity` and `options`. + pub(crate) fn new( + client: Weak>, + entity: PubNubEntity, + options: Option>, + ) -> Self { + Self { + inner: SubscriptionRef::new(client, entity, options), + is_clone: false, + } + } + + /// Creates a clone of the subscription set with an empty event dispatcher. + /// + /// Empty clones have the same subscription state but an empty list of + /// real-time event listeners, which makes it possible to attach + /// listeners specific to the context. When the cloned subscription goes out + /// of scope, all associated listeners will be invalidated and released. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{subscribe::Subscriber, PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let channel = client.channel("my_channel"); + /// // Creating Subscription instance for the Channel entity to subscribe and listen + /// // for real-time events. + /// let subscription = channel.subscription(None); + /// // ... + /// // We need to pass subscription into other component which would like to + /// // have own listeners to handle real-time events. + /// let empty_subscription = subscription.clone_empty(); + /// // self.other_component(empty_subscription); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Returns + /// + /// A new instance of the subscription object with an empty event + /// dispatcher. + pub fn clone_empty(&self) -> Self { + Self { + inner: self.inner.clone_empty(), + is_clone: false, + } + } +} + +impl Deref for Subscription +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + type Target = SubscriptionRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Subscription +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the Subscription are not allowed") + } +} + +impl Clone for Subscription +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + is_clone: true, + } + } +} + +impl Drop for Subscription +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn drop(&mut self) { + // Nothing should be done for regular subscription clone. + if self.is_clone { + return; + } + + // Unregistering self to clean up subscriptions list if required. + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + if let Some(manager) = client.subscription_manager(false).write().as_mut() { + let mut clones = self.clones.write(); + + if clones.len().gt(&1) { + clones.retain(|instance_id, _| instance_id.ne(&self.instance_id)); + } else if let Some((_, handler)) = clones.iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.unregister(&handler); + } + } + } +} + +impl Add for Subscription +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + type Output = SubscriptionSet; + + fn add(self, rhs: Self) -> Self::Output { + SubscriptionSet::new_with_subscriptions( + vec![self.clone(), rhs.clone()], + self.options.clone(), + ) + } +} + +impl PartialEq for Subscription +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Debug for Subscription +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "Subscription {{ id: {}, instance_id: {}, entity: {:?}, subscription_input: {:?}, \ + is_subscribed: {}, cursor: {:?}, options: {:?}}}", + self.id, + self.instance_id, + self.entity, + self.subscription_input, + self.is_subscribed(), + self.cursor.read().clone(), + self.options + ) + } +} + +impl SubscriptionRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + /// Creates a new subscription reference for specified entity. + /// + /// # Arguments + /// + /// * `client` - Weak reference on [`PubNubClientInstance`] to access shared + /// resources. + /// * `entities` - A `PubNubEntity` representing the entity to subscribe to. + /// * `options` - An optional list of `SubscriptionOptions` specifying the + /// subscription behaviour. + /// + /// # Returns + /// + /// A new [`SubscriptionRef`] for the given `entity` and `options`. + pub(crate) fn new( + client: Weak>, + entity: PubNubEntity, + options: Option>, + ) -> Arc { + let subscription_ref = SubscriptionState::new(client, entity, options); + let subscription_id = Uuid::new_v4().to_string(); + let subscription = Arc::new(Self { + instance_id: subscription_id.clone(), + state: Arc::new(subscription_ref), + event_dispatcher: Default::default(), + }); + subscription.store_clone(subscription_id, Arc::downgrade(&subscription)); + subscription + } + + /// Creates a clone of the subscription set with an empty event dispatcher. + /// + /// # Returns + /// + /// A new instance of the subscription object with an empty event + /// dispatcher. + pub fn clone_empty(&self) -> Arc { + let instance_id = Uuid::new_v4().to_string(); + let instance = Arc::new(Self { + instance_id: instance_id.clone(), + state: Arc::clone(&self.state), + event_dispatcher: Default::default(), + }); + self.store_clone(instance_id, Arc::downgrade(&instance)); + instance + } + + /// Retrieves the current timetoken value. + /// + /// # Returns + /// + /// The current timetoken value as an `usize`, or 0 if the timetoken cannot + /// be parsed. + pub(super) fn current_timetoken(&self) -> usize { + self.cursor + .read() + .as_ref() + .and_then(|cursor| cursor.timetoken.parse::().ok()) + .unwrap_or(0) + } + + /// Checks if the [`Subscription`] is active or not. + /// + /// # Returns + /// + /// Returns `true` if the active, otherwise `false`. + pub(super) fn is_subscribed(&self) -> bool { + *self.is_subscribed.read() + } + + /// Filters the given list of `Update` events based on the subscription + /// input and the current timetoken. + /// + /// # Arguments + /// + /// * `events` - A slice of `Update` events to filter. + /// + /// # Returns + /// + /// A new `Vec` containing only the events that satisfy the + /// following conditions: + /// 1. The event's subscription is present in the subscription input. + /// 2. The event's timestamp is greater than or equal to the current + /// timetoken. + fn filtered_events(&self, events: &[Update]) -> Vec { + let subscription_input = self.subscription_input(true); + let current_timetoken = self.current_timetoken(); + + events + .iter() + .filter(|event| { + subscription_input.contains(&event.subscription()) + && event.event_timestamp().ge(¤t_timetoken) + }) + .cloned() + .collect::>() + } +} + +impl Deref for SubscriptionRef +where + T: Send + Sync, + D: Send + Sync, +{ + type Target = SubscriptionState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for SubscriptionRef +where + T: Send + Sync, + D: Send + Sync, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.state) + .expect("Multiple mutable references to the SubscriptionRef are not allowed") + } +} + +impl EventSubscriber for SubscriptionRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscribe(&self, cursor: Option) { + let mut is_subscribed = self.is_subscribed.write(); + if *is_subscribed { + return; + } + *is_subscribed = true; + + if cursor.is_some() { + let mut cursor_slot = self.cursor.write(); + if let Some(current_cursor) = cursor_slot.as_ref() { + let catchup_cursor = cursor.clone().unwrap_or_default(); + catchup_cursor + .gt(current_cursor) + .then(|| *cursor_slot = Some(catchup_cursor)); + } else { + *cursor_slot = cursor.clone(); + } + } + + if let Some(client) = self.client().upgrade().clone() { + if let Some(manager) = client.subscription_manager(true).write().as_mut() { + // Mark entities as "in use" by subscription. + self.entity.increase_subscriptions_count(); + + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.register(&handler, cursor); + } + } + } + } + + fn unsubscribe(&self) { + { + let mut is_subscribed_slot = self.is_subscribed.write(); + if !*is_subscribed_slot { + return; + } + *is_subscribed_slot = false; + } + + if let Some(client) = self.client().upgrade().clone() { + if let Some(manager) = client.subscription_manager(false).write().as_mut() { + // Mark entities as "not in-use" by subscription. + self.entity.decrease_subscriptions_count(); + + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.unregister(&handler); + } + } + } + } +} + +impl EventHandler for SubscriptionRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn handle_events(&self, cursor: SubscriptionCursor, events: &[Update]) { + if !self.is_subscribed() { + return; + } + + let filtered_events = self.filtered_events(events); + + let mut cursor_slot = self.cursor.write(); + if let Some(current_cursor) = cursor_slot.as_ref() { + cursor + .gt(current_cursor) + .then(|| *cursor_slot = Some(cursor)); + } else { + *cursor_slot = Some(cursor); + } + + // Go through subscription clones and trigger events for them. + self.clones.write().retain(|_, handler| { + if let Some(handler) = handler.upgrade().clone() { + handler + .event_dispatcher + .handle_events(filtered_events.clone()); + return true; + } + false + }); + } + + fn subscription_input(&self, include_inactive: bool) -> SubscriptionInput { + if !include_inactive && self.entity.subscriptions_count().eq(&0) { + Default::default() + } + + self.subscription_input.clone() + } + + fn invalidate(&self) { + { + let mut is_subscribed = self.is_subscribed.write(); + if !*is_subscribed { + return; + } + *is_subscribed = false; + } + self.entity.decrease_subscriptions_count(); + + // Go through subscription clones and invalidate them. + self.clones.write().retain(|_, handler| { + if let Some(handler) = handler.upgrade().clone() { + handler.event_dispatcher.invalidate(); + return true; + } + false + }); + } + + fn id(&self) -> &String { + &self.id + } + + fn client(&self) -> Weak> { + self.client.clone() + } +} + +impl EventEmitter for SubscriptionRef +where + T: Send + Sync, + D: Send + Sync, +{ + fn messages_stream(&self) -> DataStream { + self.event_dispatcher.messages_stream() + } + + fn signals_stream(&self) -> DataStream { + self.event_dispatcher.signals_stream() + } + + fn message_actions_stream(&self) -> DataStream { + self.event_dispatcher.message_actions_stream() + } + + fn files_stream(&self) -> DataStream { + self.event_dispatcher.files_stream() + } + + fn app_context_stream(&self) -> DataStream { + self.event_dispatcher.app_context_stream() + } + + fn presence_stream(&self) -> DataStream { + self.event_dispatcher.presence_stream() + } + + fn stream(&self) -> DataStream { + self.event_dispatcher.stream() + } +} + +impl SubscriptionState +where + T: Send + Sync, + D: Send + Sync, +{ + fn new( + client: Weak>, + entity: PubNubEntity, + options: Option>, + ) -> SubscriptionState { + let is_channel_type = matches!(entity.r#type(), SubscribableType::Channel); + let with_presence = if let Some(options) = &options { + options + .iter() + .any(|option| matches!(option, SubscriptionOptions::ReceivePresenceEvents)) + } else { + false + }; + let entity_names = entity.names(with_presence); + + let input = SubscriptionInput::new( + &is_channel_type.then(|| entity_names.clone()), + &(!is_channel_type).then_some(entity_names), + ); + + Self { + id: Uuid::new_v4().to_string(), + client, + entity, + is_subscribed: Default::default(), + subscription_input: input, + cursor: Default::default(), + options, + clones: Default::default(), + } + } + + /// Store a clone of a [`SubscriptionRef`] instance with a given instance + /// ID. + /// + /// # Arguments + /// + /// * `instance_id` - The instance ID to associate with the clone. + /// * `instance` - The weak reference to the subscription reference to store + /// as a clone. + fn store_clone(&self, instance_id: String, instance: Weak>) { + let mut clones = self.clones.write(); + (!clones.contains_key(&instance_id)).then(|| clones.insert(instance_id, instance)); + } +} + +#[cfg(test)] +mod it_should { + use super::*; + use crate::{Channel, ChannelGroup, Keyset, PubNubClient, PubNubClientBuilder}; + + fn client() -> PubNubClient { + PubNubClientBuilder::with_reqwest_transport() + .with_keyset(Keyset { + subscribe_key: "demo", + publish_key: Some("demo"), + secret_key: None, + }) + .with_user_id("user") + .build() + .unwrap() + } + + #[test] + fn create_subscription_from_channel_entity() { + let client = Arc::new(client()); + let channel = Channel::new(&client, "channel"); + let channel2 = Channel::new(&client, "channel2"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel), + None, + ); + let subscription2 = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel2), + None, + ); + + let _ = subscription.clone() + subscription2; + + assert!(!subscription.is_subscribed()); + assert!(subscription.subscription_input.contains_channel("channel")); + } + + #[test] + fn create_subscription_from_channel_entity_with_options() { + let client = Arc::new(client()); + let channel = Channel::new(&client, "channel"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel), + Some(vec![SubscriptionOptions::ReceivePresenceEvents]), + ); + + assert!(!subscription.is_subscribed()); + assert!(subscription.subscription_input.contains_channel("channel")); + assert!(subscription + .subscription_input + .contains_channel("channel-pnpres")); + } + + #[test] + fn create_subscription_from_channel_group_entity() { + let client = Arc::new(client()); + let channel_group = ChannelGroup::new(&client, "channel-group"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::ChannelGroup(channel_group), + None, + ); + + assert!(!subscription.is_subscribed()); + assert!(subscription + .subscription_input + .contains_channel_group("channel-group")); + assert!(!subscription + .subscription_input + .contains_channel_group("channel-group-pnpres")); + } + + #[test] + fn create_subscription_from_channel_group_entity_with_options() { + let client = Arc::new(client()); + let channel_group = ChannelGroup::new(&client, "channel-group"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::ChannelGroup(channel_group), + Some(vec![SubscriptionOptions::ReceivePresenceEvents]), + ); + + assert!(!subscription.is_subscribed()); + assert!(subscription + .subscription_input + .contains_channel_group("channel-group")); + assert!(subscription + .subscription_input + .contains_channel_group("channel-group-pnpres")); + } + + #[test] + fn preserve_id_between_clones() { + let client = Arc::new(client()); + let channel = Channel::new(&client, "channel"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel), + None, + ); + assert_eq!(subscription.clone().id.clone(), subscription.id.clone()); + } + + #[test] + fn preserve_options_between_clones() { + let client = Arc::new(client()); + let channel = Channel::new(&client, "channel"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel), + Some(vec![SubscriptionOptions::ReceivePresenceEvents]), + ); + assert_eq!( + subscription.clone().options.clone(), + subscription.options.clone() + ); + } + + #[test] + fn not_preserve_listeners_between_clones() { + let client = Arc::new(client()); + let channel = Channel::new(&client, "channel"); + let subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel), + None, + ); + let _ = subscription.messages_stream(); + + assert_eq!( + subscription + .event_dispatcher + .message_streams + .read() + .as_ref() + .unwrap() + .len(), + 1 + ); + assert!(subscription + .clone_empty() + .event_dispatcher + .message_streams + .read() + .as_ref() + .is_none()); + } +} diff --git a/src/dx/subscribe/subscription_manager.rs b/src/dx/subscribe/subscription_manager.rs index ac3015bb..93cc16aa 100644 --- a/src/dx/subscribe/subscription_manager.rs +++ b/src/dx/subscribe/subscription_manager.rs @@ -3,27 +3,37 @@ //! This module contains manager which is responsible for tracking and updating //! active subscription streams. +use spin::RwLock; + +use crate::core::{Deserializer, Transport}; +use crate::subscribe::traits::EventHandler; use crate::{ dx::subscribe::{ - event_engine::{event::SubscribeEvent, SubscribeEventEngine, SubscribeInput}, + event_engine::{ + event::SubscribeEvent, SubscribeEffectInvocation, SubscribeEventEngine, + SubscriptionInput, + }, result::Update, - subscription::Subscription, - SubscribeCursor, SubscribeStatus, + ConnectionStatus, PubNubClientInstance, Subscription, SubscriptionCursor, }, lib::{ - alloc::{sync::Arc, vec::Vec}, + alloc::{ + sync::{Arc, Weak}, + vec::Vec, + }, + collections::HashMap, core::{ - fmt::Debug, + fmt::{Debug, Formatter}, ops::{Deref, DerefMut}, }, }, }; -use std::fmt::Formatter; +#[cfg(feature = "presence")] pub(in crate::dx::subscribe) type PresenceCall = - dyn Fn(Option>, Option>) + Send + Sync; + dyn Fn(Option>, Option>, bool) + Send + Sync; -/// Active subscriptions manager. +/// Active subscriptions' manager. /// /// [`PubNubClient`] allows to have multiple [`subscription`] objects which will /// be used to deliver real-time updates on channels and groups specified during @@ -32,42 +42,44 @@ pub(in crate::dx::subscribe) type PresenceCall = /// [`subscription`]: crate::Subscription /// [`PubNubClient`]: crate::PubNubClient #[derive(Debug)] -pub(crate) struct SubscriptionManager { - pub(crate) inner: Arc, +pub(crate) struct SubscriptionManager { + pub(crate) inner: Arc>, } -impl SubscriptionManager { +impl SubscriptionManager { pub fn new( event_engine: Arc, - heartbeat_call: Arc, - leave_call: Arc, + #[cfg(feature = "presence")] heartbeat_call: Arc, + #[cfg(feature = "presence")] leave_call: Arc, ) -> Self { Self { inner: Arc::new(SubscriptionManagerRef { event_engine, - subscribers: Default::default(), - _heartbeat_call: heartbeat_call, - _leave_call: leave_call, + event_handlers: Default::default(), + #[cfg(feature = "presence")] + heartbeat_call, + #[cfg(feature = "presence")] + leave_call, }), } } } -impl Deref for SubscriptionManager { - type Target = SubscriptionManagerRef; +impl Deref for SubscriptionManager { + type Target = SubscriptionManagerRef; fn deref(&self) -> &Self::Target { &self.inner } } -impl DerefMut for SubscriptionManager { +impl DerefMut for SubscriptionManager { fn deref_mut(&mut self) -> &mut Self::Target { Arc::get_mut(&mut self.inner).expect("Presence configuration is not unique.") } } -impl Clone for SubscriptionManager { +impl Clone for SubscriptionManager { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -75,52 +87,83 @@ impl Clone for SubscriptionManager { } } -/// Active subscriptions manager. +/// Active subscriptions' manager reference. /// -/// [`PubNubClient`] allows to have multiple [`subscription`] objects which will -/// be used to deliver real-time updates on channels and groups specified during -/// [`subscribe`] method call. +/// This struct contains the actual subscriptions' manager state. It is wrapped +/// in an Arc by [`SubscriptionManager`] and uses internal mutability for its +/// internal state. /// -/// [`subscription`]: crate::Subscription -/// [`PubNubClient`]: crate::PubNubClient -pub(crate) struct SubscriptionManagerRef { +/// Not intended to be used directly. Use [`SubscriptionManager`] instead. +pub(crate) struct SubscriptionManagerRef { /// Subscription event engine. /// /// State machine which is responsible for subscription loop maintenance. event_engine: Arc, - /// List of registered subscribers. + /// List of registered event handlers. /// - /// List of subscribers which will receive real-time updates. - subscribers: Vec, + /// List of handlers which will receive real-time events and dispatch them + /// to the listeners. + event_handlers: RwLock + Send + Sync>>>, /// Presence `join` announcement. /// /// Announces `user_id` presence on specified channels and groups. - _heartbeat_call: Arc, + #[cfg(feature = "presence")] + heartbeat_call: Arc, /// Presence `leave` announcement. /// /// Announces `user_id` `leave` from specified channels and groups. - _leave_call: Arc, + #[cfg(feature = "presence")] + leave_call: Arc, } -impl SubscriptionManagerRef { - pub fn notify_new_status(&self, status: &SubscribeStatus) { - self.subscribers.iter().for_each(|subscription| { - subscription.handle_status(status.clone()); - }); +impl SubscriptionManagerRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + pub fn notify_new_status(&self, status: &ConnectionStatus) { + if let Some(client) = self.client() { + client.handle_status(status.clone()) + } } - pub fn notify_new_messages(&self, messages: Vec) { - self.subscribers.iter().for_each(|subscription| { - subscription.handle_messages(&messages); + pub fn notify_new_messages(&self, cursor: SubscriptionCursor, events: Vec) { + if let Some(client) = self.client() { + client.handle_events(cursor.clone(), &events) + } + + self.event_handlers.write().retain(|_, weak_handler| { + if let Some(handler) = weak_handler.upgrade().clone() { + handler.handle_events(cursor.clone(), &events); + true + } else { + false + } }); } - pub fn register(&mut self, subscription: Subscription) { - let cursor = subscription.cursor; - self.subscribers.push(subscription); + pub fn register( + &mut self, + event_handler: &Weak + Send + Sync>, + cursor: Option, + ) { + let Some(upgraded_event_handler) = event_handler.upgrade().clone() else { + return; + }; + + let event_handler_id = upgraded_event_handler.id(); + if self.event_handlers.read().contains_key(event_handler_id) { + return; + } + + { + self.event_handlers + .write() + .insert(event_handler_id.clone(), event_handler.clone()); + } if let Some(cursor) = cursor { self.restore_subscription(cursor); @@ -129,27 +172,70 @@ impl SubscriptionManagerRef { } } - pub fn unregister(&mut self, subscription: Subscription) { - if let Some(position) = self - .subscribers - .iter() - .position(|val| val.id.eq(&subscription.id)) + pub fn update( + &self, + event_handler: &Weak + Send + Sync>, + removed: Option<&[Subscription]>, + ) { + let Some(upgraded_event_handler) = event_handler.upgrade().clone() else { + return; + }; + + if !self + .event_handlers + .read() + .contains_key(upgraded_event_handler.id()) { - self.subscribers.swap_remove(position); + return; } - self.change_subscription(Some(&subscription.input)); + // Handle subscriptions' set subscriptions subset which has been removed from + // it. + let removed = removed.map(|removed| { + removed + .iter() + .filter(|subscription| subscription.entity.subscriptions_count().eq(&0)) + .fold(SubscriptionInput::default(), |mut acc, subscription| { + acc += subscription.subscription_input.clone(); + acc + }) + }); + + self.change_subscription(removed.as_ref()); + } + + pub fn unregister(&mut self, event_handler: &Weak + Send + Sync>) { + let Some(upgraded_event_handler) = event_handler.upgrade().clone() else { + return; + }; + + let event_handler_id = upgraded_event_handler.id(); + if !self.event_handlers.read().contains_key(event_handler_id) { + return; + } + + { + self.event_handlers.write().remove(event_handler_id); + } + + self.change_subscription(Some(&upgraded_event_handler.subscription_input(false))); } - // TODO: why call it on drop fails tests? - #[allow(dead_code)] pub fn unregister_all(&mut self) { let inputs = self.current_input(); - self.subscribers - .iter_mut() - .for_each(|subscription| subscription.invalidate()); - self.subscribers.clear(); + // Invalidate current event handler state (subscribed and entity usage). + { + let mut handlers = self.event_handlers.write(); + + handlers.iter().for_each(|(_, handler)| { + if let Some(handler) = handler.upgrade() { + handler.invalidate(); + } + }); + handlers.clear(); + } + self.change_subscription(Some(&inputs)); } @@ -157,90 +243,157 @@ impl SubscriptionManagerRef { self.event_engine.process(&SubscribeEvent::Disconnect); } - pub fn reconnect(&self) { - self.event_engine.process(&SubscribeEvent::Reconnect); + pub fn reconnect(&self, cursor: Option) { + self.event_engine + .process(&SubscribeEvent::Reconnect { cursor }); } - fn change_subscription(&self, _removed: Option<&SubscribeInput>) { - let inputs = self.current_input(); + /// Returns the current subscription input. + /// + /// Gather subscriptions from all registered (active) event handlers. + /// + /// # Returns + /// + /// - [`SubscriptionInput`]: The sum of all subscription inputs from the + /// event handlers. + pub fn current_input(&self) -> SubscriptionInput { + self.event_handlers + .read() + .values() + .filter_map(|weak_handler| weak_handler.upgrade().clone()) + .map(|handler| handler.subscription_input(false).clone()) + .sum() + } - // TODO: Uncomment after contract test server fix. - // #[cfg(feature = "presence")] - // { - // (!inputs.is_empty) - // .then(|| self.heartbeat_call.as_ref()(inputs.channels(), - // inputs.channel_groups())); - // - // if let Some(removed) = removed { - // (!removed.is_empty).then(|| { - // self.leave_call.as_ref()(removed.channels(), - // removed.channel_groups()) }); - // } - // } + /// Checks if there are any event handlers registered. + /// + /// # Returns + /// + /// Returns `true` if there are registered event handlers, `false` + /// otherwise. + pub fn has_handlers(&self) -> bool { + !self.event_handlers.read().is_empty() + } + + /// Terminate subscription manager. + /// + /// Gracefully terminate all ongoing tasks including detached event engine + /// loop. + pub fn terminate(&self) { + self.event_engine + .stop(SubscribeEffectInvocation::TerminateEventEngine); + } + + fn change_subscription(&self, removed: Option<&SubscriptionInput>) { + let mut inputs = self.current_input(); + + if let Some(removed) = removed { + inputs -= removed.clone(); + } + + let channels = inputs.channels(); + let channel_groups = inputs.channel_groups(); + + #[cfg(feature = "presence")] + { + (!inputs.is_empty && removed.is_none()).then(|| { + self.heartbeat_call.as_ref()(channels.clone(), channel_groups.clone(), false) + }); + + if let Some(removed) = removed { + if !removed.is_empty { + self.leave_call.as_ref()( + removed.channels(), + removed.channel_groups(), + inputs.is_empty, + ); + } + } + } self.event_engine .process(&SubscribeEvent::SubscriptionChanged { - channels: inputs.channels(), - channel_groups: inputs.channel_groups(), + channels, + channel_groups, }); } - fn restore_subscription(&self, cursor: u64) { + fn restore_subscription(&self, cursor: SubscriptionCursor) { let inputs = self.current_input(); - // TODO: Uncomment after contract test server fix. - // #[cfg(feature = "presence")] - // if !inputs.is_empty { - // self.heartbeat_call.as_ref()(inputs.channels(), inputs.channel_groups()); - // } + #[cfg(feature = "presence")] + if !inputs.is_empty { + self.heartbeat_call.as_ref()(inputs.channels(), inputs.channel_groups(), false); + } self.event_engine .process(&SubscribeEvent::SubscriptionRestored { channels: inputs.channels(), channel_groups: inputs.channel_groups(), - cursor: SubscribeCursor { - timetoken: cursor.to_string(), - region: 0, - }, + cursor, }); } - pub(crate) fn current_input(&self) -> SubscribeInput { - self.subscribers.iter().fold( - SubscribeInput::new(&None, &None), - |mut input, subscription| { - input += subscription.input.clone(); - input - }, - ) + /// [`PubNubClientInstance`] associated with any of the event handlers. + /// + /// # Returns + /// + /// Reference on the underlying [`PubNubClientInstance`] instance of event + /// handler. + fn client(&self) -> Option>> { + let event_handlers = self.event_handlers.read(); + let mut client = None; + if !event_handlers.is_empty() { + if let Some((_, handler)) = event_handlers.iter().next() { + if let Some(handler) = handler.upgrade().clone() { + client = handler.client().upgrade().clone(); + } + } + } + + client } } -impl Debug for SubscriptionManagerRef { +impl Debug for SubscriptionManagerRef { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "SubscriptionManagerRef {{\n\tevent_engine: {:?}\n\tsubscribers: {:?}\n}}", - self.event_engine, self.subscribers + "SubscriptionManagerRef {{ event_engine: {:?}, event handlers: {:?} }}", + self.event_engine, self.event_handlers ) } } #[cfg(test)] mod should { + use futures::{FutureExt, StreamExt}; + use super::*; use crate::{ - core::RequestRetryPolicy, + core::RequestRetryConfiguration, dx::subscribe::{ event_engine::{SubscribeEffectHandler, SubscribeState}, result::SubscribeResult, - subscription::SubscriptionBuilder, types::Message, + EventEmitter, Subscriber, Update, }, lib::alloc::sync::Arc, providers::futures_tokio::RuntimeTokio, + Keyset, PubNubClient, PubNubClientBuilder, }; - use spin::RwLock; + + fn client() -> PubNubClient { + PubNubClientBuilder::with_reqwest_transport() + .with_keyset(Keyset { + subscribe_key: "", + publish_key: Some(""), + secret_key: None, + }) + .with_user_id("user_id") + .build() + .unwrap() + } fn event_engine() -> Arc { let (cancel_tx, _) = async_channel::bounded(1); @@ -248,20 +401,21 @@ mod should { SubscribeEventEngine::new( SubscribeEffectHandler::new( Arc::new(move |_| { - Box::pin(async move { + async move { Ok(SubscribeResult { cursor: Default::default(), messages: Default::default(), }) - }) + } + .boxed() }), Arc::new(|_| { // Do nothing yet }), - Arc::new(Box::new(|_| { + Arc::new(Box::new(|_, _| { // Do nothing yet })), - RequestRetryPolicy::None, + RequestRetryConfiguration::None, cancel_tx, ), SubscribeState::Unsubscribed, @@ -271,112 +425,84 @@ mod should { #[tokio::test] async fn register_subscription() { + let client = client(); let mut manager = SubscriptionManager::new( event_engine(), - Arc::new(|channels, _| { + #[cfg(feature = "presence")] + Arc::new(|channels, _, _| { assert!(channels.is_some()); assert_eq!(channels.unwrap().len(), 1); }), - Arc::new(|_, _| {}), + #[cfg(feature = "presence")] + Arc::new(|_, _, _| {}), ); - let dummy_manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); - - let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), - ..Default::default() - } - .channels(["test".into()]) - .execute() - .unwrap(); + let channel = client.channel("test"); + let subscription = channel.subscription(None); + let weak_subscription = &Arc::downgrade(&subscription.inner); + let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); - manager.register(subscription); + manager.register(&weak_handler, None); - assert_eq!(manager.subscribers.len(), 1); + assert_eq!(manager.event_handlers.read().len(), 1); } #[tokio::test] async fn unregister_subscription() { + let client = client(); let mut manager = SubscriptionManager::new( event_engine(), - Arc::new(|_, _| {}), - Arc::new(|channels, _| { + #[cfg(feature = "presence")] + Arc::new(|_, _, _| {}), + #[cfg(feature = "presence")] + Arc::new(|channels, _, _| { assert!(channels.is_some()); assert_eq!(channels.unwrap().len(), 1); }), ); - let dummy_manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); - - let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), - ..Default::default() - } - .channels(["test".into()]) - .execute() - .unwrap(); + let channel = client.channel("test"); + let subscription = channel.subscription(None); + let weak_subscription = &Arc::downgrade(&subscription.inner); + let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); - manager.register(subscription.clone()); - manager.unregister(subscription); + manager.register(&weak_handler, None); + manager.unregister(&weak_handler); - assert_eq!(manager.subscribers.len(), 0); + assert_eq!(manager.event_handlers.read().len(), 0); } #[tokio::test] - async fn notify_subscription_about_statuses() { - let mut manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); - let dummy_manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); - - let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), - ..Default::default() - } - .channels(["test".into()]) - .execute() - .unwrap(); - - manager.register(subscription.clone()); - manager.notify_new_status(&SubscribeStatus::Connected); - - use futures::StreamExt; - assert_eq!( - subscription - .status_stream() - .next() - .await - .iter() - .next() - .unwrap(), - &SubscribeStatus::Connected + async fn notify_subscription_about_updates() { + let client = client(); + let mut manager = SubscriptionManager::new( + event_engine(), + #[cfg(feature = "presence")] + Arc::new(|_, _, _| {}), + #[cfg(feature = "presence")] + Arc::new(|_, _, _| {}), ); - } + let cursor: SubscriptionCursor = "15800701771129796".to_string().into(); + let channel = client.channel("test"); + let subscription = channel.subscription(None); + let weak_subscription = Arc::downgrade(&subscription.inner); + let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); - #[tokio::test] - async fn notify_subscription_about_updates() { - let mut manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); - let dummy_manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); - - let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), - ..Default::default() + // Simulate `.subscribe()` call. + { + let mut is_subscribed = subscription.is_subscribed.write(); + *is_subscribed = true; } - .channels(["test".into()]) - .execute() - .unwrap(); - - manager.register(subscription.clone()); - - manager.notify_new_messages(vec![Update::Message(Message { - channel: "test".into(), - subscription: "test".into(), - ..Default::default() - })]); + manager.register(&weak_handler, Some(cursor.clone())); + + manager.notify_new_messages( + cursor.clone(), + vec![Update::Message(Message { + channel: "test".into(), + subscription: "test".into(), + timestamp: cursor.timetoken.parse::().ok().unwrap(), + ..Default::default() + })], + ); - use futures::StreamExt; - assert!(subscription.message_stream().next().await.is_some()); + assert!(subscription.messages_stream().next().await.is_some()); } } diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs new file mode 100644 index 00000000..3e6d58ce --- /dev/null +++ b/src/dx/subscribe/subscription_set.rs @@ -0,0 +1,1073 @@ +//! # Subscription set module +//! +//! This module contains the [`SubscriptionSet`] type, which can be used to +//! manage subscription to the entities represented by managed subscriptions and +//! attach listeners to the specific event types. + +use spin::RwLock; +use uuid::Uuid; + +use crate::core::{Deserializer, Transport}; +use crate::subscribe::traits::EventHandler; +use crate::{ + core::{DataStream, PubNubEntity}, + dx::pubnub_client::PubNubClientInstance, + lib::{ + alloc::{ + string::String, + sync::{Arc, Weak}, + vec, + vec::Vec, + }, + collections::HashMap, + core::{ + fmt::{Debug, Formatter, Result}, + ops::{Add, AddAssign, Deref, DerefMut, Sub, SubAssign}, + }, + }, + subscribe::{ + event_engine::SubscriptionInput, AppContext, EventDispatcher, EventEmitter, + EventSubscriber, File, Message, MessageAction, Presence, Subscriber, Subscription, + SubscriptionCursor, SubscriptionOptions, Update, + }, +}; + +/// Entities subscriptions set. +/// +/// # Example +/// +/// ### Multiplexed subscription +/// +/// ```rust +/// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<(), pubnub::core::PubNubError> { +/// use pubnub::subscribe::SubscriptionParams; +/// let client = // PubNubClient +/// # PubNubClientBuilder::with_reqwest_transport() +/// # .with_keyset(Keyset { +/// # subscribe_key: "demo", +/// # publish_key: Some("demo"), +/// # secret_key: Some("demo") +/// # }) +/// # .with_user_id("uuid") +/// # .build()?; +/// let subscription = client.subscription(SubscriptionParams { +/// channels: Some(&["my_channel_1", "my_channel_2"]), +/// channel_groups:Some(&["my_group"]), +/// options:None +/// }); +/// # Ok(()) +/// # } +/// ``` +/// +/// ### Sum of subscriptions +/// +/// ```rust +/// use pubnub::{ +/// subscribe::{Subscriber, Subscription}, +/// Keyset, PubNubClient, PubNubClientBuilder, +/// }; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<(), pubnub::core::PubNubError> { +/// let client = // PubNubClient +/// # PubNubClientBuilder::with_reqwest_transport() +/// # .with_keyset(Keyset { +/// # subscribe_key: "demo", +/// # publish_key: Some("demo"), +/// # secret_key: Some("demo") +/// # }) +/// # .with_user_id("uuid") +/// # .build()?; +/// let channels = client.channels(&["my_channel_1", "my_channel_2"]); +/// // Two `Subscription` instances can be added to create `SubscriptionSet` which can be used +/// // to attach listeners and subscribe in one place for both subscriptions used in addition +/// // operation. +/// let subscription = channels[0].subscription(None) + channels[1].subscription(None); +/// # Ok(()) +/// # } +/// ``` +pub struct SubscriptionSet< + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +> { + /// Subscriptions set reference. + pub(super) inner: Arc>, + + /// Whether subscription set is `Clone::clone()` method call result or not. + is_clone: bool, +} + +/// Entities subscriptions set reference. +/// +/// This struct contains the actual entities subscriptions set state. +/// It's wrapped in `Arc` by [`SubscriptionSet`] and uses interior mutability +/// for its internal state. +/// +/// Not intended to be used directly. Use [`SubscriptionSet`] instead. +pub struct SubscriptionSetRef< + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +> { + /// Unique event handler instance identifier. + /// + /// [`SubscriptionSet`] can be cloned, but the internal state is always + /// bound to the same reference of [`SubscriptionSetState`] with the + /// same `id`. + pub(super) instance_id: String, + + /// Subscriptions set reference. + state: Arc>, + + /// Real-time event dispatcher. + event_dispatcher: EventDispatcher, +} + +/// Shared entities subscriptions set state. +/// +/// This struct contains the actual entities subscriptions set state. +/// It's wrapped in `Arc` by [`SubscriptionSet`] and uses interior mutability +/// for its internal state. +/// +/// Not intended to be used directly. Use [`SubscriptionSet`] instead. +#[derive(Debug)] +pub struct SubscriptionSetState< + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +> { + /// Unique event handler identifier. + pub(super) id: String, + + /// [`PubNubClientInstance`] which is backing which subscription set. + pub(super) client: Weak>, + + /// Grouped subscriptions list. + pub(crate) subscriptions: RwLock>>, + + /// Whether set is currently subscribed and active. + pub(super) is_subscribed: Arc>, + + /// List of strings which represent data stream identifiers for + /// subscriptions' entity real-time events. + pub(crate) subscription_input: RwLock, + + /// Subscription time cursor. + cursor: RwLock>, + + /// Subscription set listener options. + /// + /// Options used to set up listener behavior and real-time events + /// processing. + options: Option>, + + /// The list of weak references to all [`SubscriptionSet`] clones created + /// for this reference. + clones: RwLock>>>, +} + +impl SubscriptionSet +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + /// Create subscription set from PubNub entities list. + /// + /// # Arguments + /// + /// * `entities` - A vector of [`PubNubEntity`] representing the entities to + /// subscribe to. + /// * `options` - An optional [`SubscriptionOptions`] specifying the + /// subscription options. + /// + /// # Returns + /// + /// A new [`SubscriptionSet`] containing the subscriptions initialized from + /// the given `entities` and `options`. + pub(crate) fn new( + entities: Vec>, + options: Option>, + ) -> Self { + Self { + inner: SubscriptionSetRef::new(entities, options), + is_clone: false, + } + } + + /// Create subscription set reference from given subscriptions list. + /// + /// # Arguments + /// + /// * `subscriptions` - A vector of [`Subscription`] which should be grouped + /// in set. + /// * `options` - An optional vector of [`SubscriptionOptions`] representing + /// the options for the subscriptions. + /// + /// # Returns + /// + /// A new [`SubscriptionSet`] containing given subscriptions and `options`. + /// + /// # Panics + /// + /// This function will panic if the `subscriptions` vector is empty. + pub fn new_with_subscriptions( + subscriptions: Vec>, + options: Option>, + ) -> Self { + Self { + inner: SubscriptionSetRef::new_with_subscriptions(subscriptions, options), + is_clone: false, + } + } + + /// Creates a clone of the subscription set with an empty event dispatcher. + /// + /// Empty clones have the same subscription set state but an empty list of + /// real-time event listeners, which makes it possible to attach listeners + /// specific to the context. When the cloned subscription set goes out of + /// scope, all associated listeners will be invalidated and released. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// use pubnub::subscribe::SubscriptionParams; + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let subscription = client.subscription(SubscriptionParams { + /// channels: Some(&["my_channel_1", "my_channel_2"]), + /// channel_groups: Some(&["my_group"]), + /// options: None + /// }); + /// // ... + /// // We need to pass subscription into other component which would like to + /// // have own listeners to handle real-time events. + /// let empty_subscription = subscription.clone_empty(); + /// // self.other_component(empty_subscription); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Returns + /// + /// A new instance of the subscription object with an empty event + /// dispatcher. + pub fn clone_empty(&self) -> Self { + Self { + inner: self.inner.clone_empty(), + is_clone: false, + } + } + + /// Aggregate subscriptions' input. + /// + /// # Arguments + /// + /// * `subscriptions` - A slice of `Subscription` representing a list + /// of subscriptions. + /// * `include_inactive` - Whether _unused_ entities should be included into + /// the subscription input or not. + /// + /// # Returns + /// + /// `SubscriptionInput` which contains input from all `subscriptions`. + fn subscription_input_from_list( + subscriptions: &[Subscription], + include_inactive: bool, + ) -> SubscriptionInput { + let input = subscriptions + .iter() + .map(|subscription| { + if !include_inactive && subscription.entity.subscriptions_count().eq(&0) { + return Default::default(); + } + + subscription.subscription_input.clone() + }) + .sum(); + + input + } + + /// Filter unique subscriptions. + /// + /// Filter out duplicates and subscriptions which is already part of the + /// `set`. + /// + /// # Arguments + /// + /// * `set` - An optional reference to a subscription set. + /// * `subscriptions` - Vector of [`Subscription`] which should be filtered. + /// + /// # Returns + /// + /// Vector with unique subscriptions which is not part of the `set`. + fn unique_subscriptions_from_list( + set: Option<&Self>, + subscriptions: Vec>, + ) -> Vec> { + let subscriptions_slot = if let Some(set) = set { + set.subscriptions.read().clone() + } else { + vec![] + }; + + let mut unique_subscriptions = Vec::with_capacity(subscriptions.len()); + subscriptions.into_iter().for_each(|subscription| { + if !unique_subscriptions.contains(&subscription) + && !subscriptions_slot.contains(&subscription) + { + unique_subscriptions.push(subscription); + } + }); + + unique_subscriptions + } +} + +impl Deref for SubscriptionSet +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + type Target = SubscriptionSetRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the SubscriptionSet are not allowed") + } +} + +impl Clone for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + is_clone: true, + } + } +} + +impl Drop for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn drop(&mut self) { + // Nothing should be done for regular subscription clone. + if self.is_clone { + return; + } + + // Unregistering self to clean up subscriptions list if required. + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + if let Some(manager) = client.subscription_manager(false).write().as_mut() { + let mut clones = self.clones.write(); + + if clones.len().gt(&1) { + clones.retain(|instance_id, _| instance_id.ne(&self.instance_id)); + } else if let Some((_, handler)) = clones.iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.unregister(&handler); + } + } + } +} + +impl Debug for SubscriptionSet +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "SubscriptionSet {{ id: {}, subscription_input: {:?}, is_subscribed: {}, cursor: {:?}, \ + options: {:?}, subscriptions: {:?}}}", + self.id, + self.subscription_input, + self.is_subscribed(), + self.cursor.read().clone(), + self.options, + self.subscriptions + ) + } +} + +impl Add for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + type Output = SubscriptionSet; + + fn add(self, rhs: Self) -> Self::Output { + let mut subscriptions = { + let other_subscriptions = rhs.subscriptions.read(); + SubscriptionSet::unique_subscriptions_from_list( + Some(&self), + other_subscriptions.clone(), + ) + }; + subscriptions.extend(self.subscriptions.read().clone()); + + SubscriptionSet::new_with_subscriptions(subscriptions, None) + } +} +impl AddAssign for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn add_assign(&mut self, rhs: Self) { + let unique_subscriptions = { + let other_subscriptions = rhs.subscriptions.read(); + SubscriptionSet::unique_subscriptions_from_list(Some(self), other_subscriptions.clone()) + }; + + { + let mut subscription_input = self.subscription_input.write(); + *subscription_input += Self::subscription_input_from_list(&unique_subscriptions, true); + self.subscriptions + .write() + .extend(unique_subscriptions.clone()); + } + + // Check whether subscription change required or not. + if !self.is_subscribed() || unique_subscriptions.is_empty() { + return; + } + + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + if let Some(manager) = client.subscription_manager(true).write().as_mut() { + // Mark entities as "in-use" by subscription. + unique_subscriptions.iter().for_each(|subscription| { + subscription.entity.increase_subscriptions_count(); + }); + + // Notify manager to update its state with new subscriptions. + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.update(&handler, None); + } + }; + } +} +impl Sub for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + type Output = SubscriptionSet; + + fn sub(self, rhs: Self) -> Self::Output { + let removed: Vec> = { + let other_subscriptions = rhs.subscriptions.read(); + let subscriptions_slot = self.subscriptions.read(); + Self::unique_subscriptions_from_list(None, other_subscriptions.clone()) + .into_iter() + .filter(|subscription| subscriptions_slot.contains(subscription)) + .collect() + }; + let mut subscriptions = self.subscriptions.read().clone(); + subscriptions.retain(|subscription| !removed.contains(subscription)); + + SubscriptionSet::new_with_subscriptions(subscriptions, None) + } +} +impl SubAssign for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn sub_assign(&mut self, rhs: Self) { + let removed: Vec> = { + let other_subscriptions = rhs.subscriptions.read(); + let subscriptions_slot = self.subscriptions.read(); + Self::unique_subscriptions_from_list(None, other_subscriptions.clone()) + .into_iter() + .filter(|subscription| subscriptions_slot.contains(subscription)) + .collect() + }; + + { + let mut subscription_input = self.subscription_input.write(); + *subscription_input -= Self::subscription_input_from_list(&removed, true); + let mut subscription_slot = self.subscriptions.write(); + subscription_slot.retain(|subscription| !removed.contains(subscription)); + } + + // Check whether subscription change required or not. + if !self.is_subscribed() || removed.is_empty() { + return; + } + + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + // Mark entities as "not in-use" by subscription. + removed.iter().for_each(|subscription| { + subscription.entity.decrease_subscriptions_count(); + }); + + if let Some(manager) = client.subscription_manager(true).write().as_mut() { + // Notify manager to update its state with removed subscriptions. + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.update(&handler, Some(&removed)); + } + }; + } +} + +impl SubscriptionSetRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + /// Create subscription set reference from PubNub entities list. + /// + /// # Arguments + /// + /// * `entities` - A vector of [`PubNubEntity`] representing the entities to + /// subscribe to. + /// * `options` - An optional [`SubscriptionOptions`] specifying the + /// subscription options. + /// + /// # Returns + /// + /// A new [`SubscriptionSetRef`] containing the subscriptions initialized + /// from the given `entities` and `options`. + pub(crate) fn new( + entities: Vec>, + options: Option>, + ) -> Arc { + let subscriptions = entities + .into_iter() + .map(|entity| entity.subscription(options.clone())) + .collect::>>(); + + Self::new_with_subscriptions(subscriptions, options) + } + + /// Create subscription set reference from given subscriptions list. + /// + /// # Arguments + /// + /// * `subscriptions` - A vector of [`Subscription`] which should be grouped + /// in set. + /// * `options` - An optional vector of [`SubscriptionOptions`] representing + /// the options for the subscriptions. + /// + /// # Returns + /// + /// A new [`SubscriptionSet`] containing given subscriptions and `options`. + /// + /// # Panics + /// + /// This function will panic if the `subscriptions` vector is empty. + pub(crate) fn new_with_subscriptions( + subscriptions: Vec>, + options: Option>, + ) -> Arc { + let subscription = subscriptions + .first() + .expect("At least one subscription expected."); + let subscription_state = + SubscriptionSetState::new(subscription.client(), subscriptions, options); + let subscription_set = Arc::new(Self { + instance_id: Uuid::new_v4().to_string(), + state: Arc::new(subscription_state), + event_dispatcher: Default::default(), + }); + subscription_set.store_clone( + subscription_set.instance_id.clone(), + Arc::downgrade(&subscription_set), + ); + subscription_set + } + + /// Creates a clone of the subscription set with an empty event dispatcher. + /// + /// Empty clones have the same subscription set state but an empty list of + /// real-time event listeners, which makes it possible to attach listeners + /// specific to the context. When the cloned subscription set goes out of + /// scope, all associated listeners will be invalidated and released. + /// + /// # Example + /// + /// ```rust + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset}; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// use pubnub::subscribe::SubscriptionParams; + /// let client = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: Some("demo"), + /// # secret_key: Some("demo") + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// let subscription = client.subscription(SubscriptionParams { + /// channels: Some(&["my_channel_1", "my_channel_2"]), + /// channel_groups: Some(&["my_group"]), + /// options: None + /// }); + /// // ... + /// // We need to pass subscription into other component which would like to + /// // have own listeners to handle real-time events. + /// let empty_subscription = subscription.clone_empty(); + /// // self.other_component(empty_subscription); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Returns + /// + /// A new instance of the subscription object with an empty event + /// dispatcher. + pub fn clone_empty(&self) -> Arc { + let instance_id = Uuid::new_v4().to_string(); + let instance = Arc::new(Self { + instance_id: instance_id.clone(), + state: Arc::clone(&self.state), + event_dispatcher: Default::default(), + }); + self.store_clone(instance_id, Arc::downgrade(&instance)); + instance + } + + /// Retrieves the current timetoken value. + /// + /// # Returns + /// + /// The current timetoken value as an `usize`, or 0 if the timetoken cannot + /// be parsed. + pub(super) fn current_timetoken(&self) -> usize { + let cursor = self.cursor.read(); + cursor + .as_ref() + .and_then(|cursor| cursor.timetoken.parse::().ok()) + .unwrap_or(0) + } + + /// Checks if the [`Subscription`] is active or not. + /// + /// # Returns + /// + /// Returns `true` if the active, otherwise `false`. + pub(super) fn is_subscribed(&self) -> bool { + *self.is_subscribed.read() + } + + /// Filters the given list of `Update` events based on the subscription + /// input and the current timetoken. + /// + /// # Arguments + /// + /// * `events` - A slice of `Update` events to filter. + /// + /// # Returns + /// + /// A new `Vec` containing only the events that satisfy the + /// following conditions: + /// 1. The event's subscription is present in the subscription input. + /// 2. The event's timestamp is greater than or equal to the current + /// timetoken. + fn filtered_events(&self, events: &[Update]) -> Vec { + let subscription_input = self.subscription_input(true); + let current_timetoken = self.current_timetoken(); + + events + .iter() + .filter(|event| { + subscription_input.contains(&event.subscription()) + && event.event_timestamp().ge(¤t_timetoken) + }) + .cloned() + .collect::>() + } +} + +impl Deref for SubscriptionSetRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + type Target = SubscriptionSetState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for SubscriptionSetRef +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.state) + .expect("Multiple mutable references to the SubscriptionSetRef are not allowed") + } +} + +impl EventSubscriber for SubscriptionSetRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscribe(&self, cursor: Option) { + let mut is_subscribed = self.is_subscribed.write(); + if *is_subscribed { + return; + } + *is_subscribed = true; + + if cursor.is_some() { + let mut cursor_slot = self.cursor.write(); + if let Some(current_cursor) = cursor_slot.as_ref() { + let catchup_cursor = cursor.clone().unwrap_or_default(); + catchup_cursor + .gt(current_cursor) + .then(|| *cursor_slot = Some(catchup_cursor)); + } else { + *cursor_slot = cursor.clone(); + } + } + + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + { + let manager = client.subscription_manager(true); + if let Some(manager) = manager.write().as_mut() { + // Mark entities as "in-use" by subscription. + self.subscriptions.read().iter().for_each(|subscription| { + subscription.entity.increase_subscriptions_count(); + }); + + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.register(&handler, cursor); + } + }; + } + } + + fn unsubscribe(&self) { + { + let mut is_subscribed_slot = self.is_subscribed.write(); + if !*is_subscribed_slot { + return; + } + *is_subscribed_slot = false; + } + + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + { + if let Some(manager) = client.subscription_manager(false).write().as_mut() { + // Mark entities as "not in-use" by subscription. + self.subscriptions.read().iter().for_each(|subscription| { + subscription.entity.increase_subscriptions_count(); + }); + + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.unregister(&handler); + } + }; + } + } +} + +impl EventHandler for SubscriptionSetRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn handle_events(&self, cursor: SubscriptionCursor, events: &[Update]) { + if !self.is_subscribed() { + return; + } + + let filtered_events = self.filtered_events(events); + + let mut cursor_slot = self.cursor.write(); + if let Some(current_cursor) = cursor_slot.as_ref() { + cursor + .gt(current_cursor) + .then(|| *cursor_slot = Some(cursor)); + } else { + *cursor_slot = Some(cursor); + } + + // Go through subscription clones and trigger events for them. + self.clones.write().retain(|_, handler| { + if let Some(handler) = handler.upgrade().clone() { + handler + .event_dispatcher + .handle_events(filtered_events.clone()); + return true; + } + false + }); + } + + fn subscription_input(&self, include_inactive: bool) -> SubscriptionInput { + SubscriptionSet::subscription_input_from_list(&self.subscriptions.read(), include_inactive) + } + + fn invalidate(&self) { + { + let mut is_subscribed = self.is_subscribed.write(); + if !*is_subscribed { + return; + } + *is_subscribed = false; + } + + self.subscriptions + .read() + .iter() + .for_each(|subscription| subscription.invalidate()); + + self.event_dispatcher.invalidate(); + } + + fn id(&self) -> &String { + &self.id + } + + fn client(&self) -> Weak> { + self.client.clone() + } +} + +impl EventEmitter for SubscriptionSetRef +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn messages_stream(&self) -> DataStream { + self.event_dispatcher.messages_stream() + } + + fn signals_stream(&self) -> DataStream { + self.event_dispatcher.signals_stream() + } + + fn message_actions_stream(&self) -> DataStream { + self.event_dispatcher.message_actions_stream() + } + + fn files_stream(&self) -> DataStream { + self.event_dispatcher.files_stream() + } + + fn app_context_stream(&self) -> DataStream { + self.event_dispatcher.app_context_stream() + } + + fn presence_stream(&self) -> DataStream { + self.event_dispatcher.presence_stream() + } + + fn stream(&self) -> DataStream { + self.event_dispatcher.stream() + } +} + +impl SubscriptionSetState +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn new( + client: Weak>, + subscriptions: Vec>, + options: Option>, + ) -> SubscriptionSetState { + Self { + id: Uuid::new_v4().to_string(), + client, + subscription_input: RwLock::new(SubscriptionSet::subscription_input_from_list( + &subscriptions, + true, + )), + is_subscribed: Default::default(), + cursor: Default::default(), + subscriptions: RwLock::new(SubscriptionSet::unique_subscriptions_from_list( + None, + subscriptions, + )), + options, + clones: Default::default(), + } + } + + /// Store a clone of a [`SubscriptionSetRef`] instance with a given instance + /// ID. + /// + /// # Arguments + /// + /// * `instance_id` - The instance ID to associate with the clone. + /// * `instance` - The weak reference to the subscription set instance to + /// store as a clone. + fn store_clone(&self, instance_id: String, instance: Weak>) { + let mut clones = self.clones.write(); + (!clones.contains_key(&instance_id)).then(|| clones.insert(instance_id, instance)); + } +} + +#[cfg(test)] +mod it_should { + use super::*; + use crate::{Channel, Keyset, PubNubClient, PubNubClientBuilder}; + + fn client() -> PubNubClient { + PubNubClientBuilder::with_reqwest_transport() + .with_keyset(Keyset { + subscribe_key: "demo", + publish_key: Some("demo"), + secret_key: None, + }) + .with_user_id("user") + .build() + .unwrap() + } + + #[test] + fn create_subscription_set_from_entities() { + let client = Arc::new(client()); + let channels = vec!["channel_1", "channel_2"] + .into_iter() + .map(|name| PubNubEntity::Channel(Channel::new(&client, name))) + .collect(); + let subscription_set = SubscriptionSet::new(channels, None); + + assert!(!subscription_set.is_subscribed()); + assert!(subscription_set + .subscription_input + .read() + .contains("channel_1")); + assert!(subscription_set + .subscription_input + .read() + .contains("channel_2")); + } + + #[test] + fn preserve_id_between_clones() { + let client = Arc::new(client()); + let channels = vec!["channel_1", "channel_2"] + .into_iter() + .map(|name| PubNubEntity::Channel(Channel::new(&client, name))) + .collect(); + let subscription_set = SubscriptionSet::new(channels, None); + assert_eq!( + subscription_set.clone().id.clone(), + subscription_set.id.clone() + ); + } + + #[test] + fn not_preserve_listeners_between_clones() { + let client = Arc::new(client()); + let channels = vec!["channel_1", "channel_2"] + .into_iter() + .map(|name| PubNubEntity::Channel(Channel::new(&client, name))) + .collect(); + let subscription_set = SubscriptionSet::new(channels, None); + let _ = subscription_set.messages_stream(); + + assert_eq!( + subscription_set + .clone() + .event_dispatcher + .message_streams + .read() + .as_ref() + .unwrap() + .len(), + 1 + ); + assert!(subscription_set + .clone_empty() + .event_dispatcher + .message_streams + .read() + .as_ref() + .is_none()); + } + + #[test] + fn concat_subscriptions() { + let client = Arc::new(client()); + let channels_1_subscriptions = vec!["channel_1", "channel_2"] + .into_iter() + .map(|name| client.channel(name).subscription(None)) + .collect::>>(); + let channels_2_subscriptions = vec!["channel_3", "channel_4"] + .into_iter() + .map(|name| client.channel(name).subscription(None)) + .collect::>>(); + let channels_3_subscriptions = vec![ + channels_1_subscriptions[0].clone(), + channels_2_subscriptions[1].clone(), + ]; + let mut subscription_set_1 = + channels_1_subscriptions[0].clone() + channels_1_subscriptions[1].clone(); + let subscription_set_2 = + channels_2_subscriptions[0].clone() + channels_2_subscriptions[1].clone(); + let subscription_set_3 = + channels_3_subscriptions[0].clone() + channels_3_subscriptions[1].clone(); + + subscription_set_1 += subscription_set_2; + assert!(subscription_set_1 + .subscription_input(true) + .contains_channel("channel_3")); + subscription_set_1 -= subscription_set_3; + assert!(!subscription_set_1 + .subscription_input(true) + .contains_channel("channel_1")); + } +} diff --git a/src/dx/subscribe/traits/event_emitter.rs b/src/dx/subscribe/traits/event_emitter.rs new file mode 100644 index 00000000..56525d3b --- /dev/null +++ b/src/dx/subscribe/traits/event_emitter.rs @@ -0,0 +1,36 @@ +//! # Event emitter module. +//! +//! This module contains the [`EventEmitter`] trait, which is used to implement +//! a real-time event emitter. + +use crate::{ + core::DataStream, + subscribe::{AppContext, File, Message, MessageAction, Presence, Update}, +}; + +/// Events emitter trait. +/// +/// Types that implement this trait provide various streams, which are dedicated +/// to specific events. to specific events. +pub trait EventEmitter { + /// Stream used to notify regular messages. + fn messages_stream(&self) -> DataStream; + + /// Stream used to notify signals. + fn signals_stream(&self) -> DataStream; + + /// Stream used to notify message action updates. + fn message_actions_stream(&self) -> DataStream; + + /// Stream used to notify about file receive. + fn files_stream(&self) -> DataStream; + + /// Stream used to notify about App Context (Channel and User) updates. + fn app_context_stream(&self) -> DataStream; + + /// Stream used to notify about subscribers' presence updates. + fn presence_stream(&self) -> DataStream; + + /// Generic stream used to notify all updates mentioned above. + fn stream(&self) -> DataStream; +} diff --git a/src/dx/subscribe/traits/event_handler.rs b/src/dx/subscribe/traits/event_handler.rs new file mode 100644 index 00000000..11d57758 --- /dev/null +++ b/src/dx/subscribe/traits/event_handler.rs @@ -0,0 +1,52 @@ +use crate::{ + dx::pubnub_client::PubNubClientInstance, + lib::alloc::sync::Weak, + subscribe::{event_engine::SubscriptionInput, SubscriptionCursor, Update}, +}; + +pub trait EventHandler { + /// Handles the given events. + /// + /// The implementation should identify the intended recipients and let them + /// know about a fresh, real-time event if it matches the intended entity. + /// + /// # Arguments + /// + /// * `cursor` - A time cursor for next portion of events. + /// * `events` - A slice of real-time events from multiplexed subscription. + fn handle_events(&self, cursor: SubscriptionCursor, events: &[Update]); + + /// Returns a reference to the subscription input associated with this event + /// handler. + /// + /// # Arguments + /// + /// * `include_inactive` - Whether _unused_ entities should be included into + /// the subscription input or not. + /// + /// # Returns + /// + /// A reference to the [`SubscriptionInput`] enum variant. + fn subscription_input(&self, include_inactive: bool) -> SubscriptionInput; + + /// Invalidates the event handler. + /// + /// This method is called to invalidate the event handler, causing any + /// subscriptions it contains to be invalidated as well. + fn invalidate(&self); + + /// Returns a reference to the ID associated with the underlying handler. + /// + /// # Returns + /// + /// Event handler unique identifier. + fn id(&self) -> &String; + + /// [`PubNubClientInstance`] which is backing event handler. + /// + /// # Returns + /// + /// Reference on the underlying [`PubNubClientInstance`] instance of + /// [`Subscription`]. + fn client(&self) -> Weak>; +} diff --git a/src/dx/subscribe/traits/event_subscribe.rs b/src/dx/subscribe/traits/event_subscribe.rs new file mode 100644 index 00000000..9778fee4 --- /dev/null +++ b/src/dx/subscribe/traits/event_subscribe.rs @@ -0,0 +1,19 @@ +//! # Event subscriber module. +//! +//! This module contains the [`EventSubscriber`] trait, which is used to +//! implement objects that provides functionality to subscribe and unsubscribe +//! from real-time events stream. + +use crate::subscribe::SubscriptionCursor; + +/// Subscriber trait. +/// +/// Types that implement this trait can change activity of real-time events +/// processing for specific or set of entities. +pub trait EventSubscriber { + /// Use receiver to subscribe for real-time updates. + fn subscribe(&self, cursor: Option); + + /// Use receiver to stop receiving real-time updates. + fn unsubscribe(&self); +} diff --git a/src/dx/subscribe/traits/mod.rs b/src/dx/subscribe/traits/mod.rs new file mode 100644 index 00000000..58f11322 --- /dev/null +++ b/src/dx/subscribe/traits/mod.rs @@ -0,0 +1,24 @@ +//! # Subscription traits module +//! +//! This module provides set of traits which is implemented by types to support +//! `subscribe` and `presence` features. + +#[doc(inline)] +pub use event_subscribe::EventSubscriber; +mod event_subscribe; + +#[doc(inline)] +pub use subscriber::Subscriber; +mod subscriber; + +#[doc(inline)] +pub use subscribable::{Subscribable, SubscribableType}; +mod subscribable; + +#[doc(inline)] +pub use event_emitter::EventEmitter; +mod event_emitter; + +#[doc(inline)] +pub(super) use event_handler::EventHandler; +mod event_handler; diff --git a/src/dx/subscribe/traits/subscribable.rs b/src/dx/subscribe/traits/subscribable.rs new file mode 100644 index 00000000..019362e2 --- /dev/null +++ b/src/dx/subscribe/traits/subscribable.rs @@ -0,0 +1,43 @@ +//! # Subscribable module. +//! +//! This module contains the [`Subscribable`] trait, which is used to implement +//! objects that can deliver real-time updates from the [`PubNub`] network. +//! +//! [`PubNub`]: https://www.pubnub.com + +use crate::{ + dx::pubnub_client::PubNubClientInstance, + lib::alloc::{string::String, sync::Weak, vec::Vec}, +}; + +/// Types of subscribable objects. +/// +/// Subscribable can be separated by their place in subscribe REST API: +/// * `URI path` - channel-like objects which represent single entity +/// ([`Channel`], [`ChannelMetadata`], [`UserMetadata`]) +/// * `query parameter` - entities which represent group of entities +/// ([`ChannelGroup`]) +pub enum SubscribableType { + /// Channel identifier, which is part of the URI path. + Channel, + + /// Channel group identifiers, which is part of the query parameters. + ChannelGroup, +} + +/// Subscribable entities' trait. +/// +/// Only entities that implement this trait can subscribe to their real-time +/// events. +pub trait Subscribable { + /// Names for object to be used in subscription. + /// + /// Provided strings will be used with multiplexed subscribe REST API call. + fn names(&self, presence: bool) -> Vec; + + /// Type of subscription object. + fn r#type(&self) -> SubscribableType; + + /// PubNub client instance which created entity. + fn client(&self) -> Weak>; +} diff --git a/src/dx/subscribe/traits/subscriber.rs b/src/dx/subscribe/traits/subscriber.rs new file mode 100644 index 00000000..a94abee1 --- /dev/null +++ b/src/dx/subscribe/traits/subscriber.rs @@ -0,0 +1,26 @@ +//! # Events subscriber module. +//! +//! This module contains the [`Subscriber`] and [`MultiplexSubscriber`] traits +//! which is used by types to provide ability to subscribe for real-time events. + +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::vec::Vec, + subscribe::{Subscription, SubscriptionOptions}, +}; + +/// Trait representing a subscriber. +pub trait Subscriber { + /// Creates a new subscription with the specified options. + /// + /// # Arguments + /// + /// * `options` - The subscription options. Pass `None` if no specific + /// options should be applied. + /// + /// # Returns + /// + /// A [`Subscription`] object representing the newly created subscription to + /// receiver's data stream events. + fn subscription(&self, options: Option>) -> Subscription; +} diff --git a/src/dx/subscribe/types.rs b/src/dx/subscribe/types.rs index 62153e46..23cee281 100644 --- a/src/dx/subscribe/types.rs +++ b/src/dx/subscribe/types.rs @@ -1,5 +1,7 @@ //! Subscription types module. +use base64::{engine::general_purpose, Engine}; + use crate::{ core::{CryptoProvider, PubNubError, ScalarValue}, dx::subscribe::result::{Envelope, EnvelopePayload, ObjectDataBody, Update}, @@ -12,10 +14,16 @@ use crate::{ vec::Vec, }, collections::HashMap, - core::{fmt::Formatter, result::Result}, + core::{ + cmp::{Ord, Ordering, PartialOrd}, + fmt::{Debug, Formatter}, + result::Result, + }, }, }; -use base64::{engine::general_purpose, Engine}; + +#[cfg(not(feature = "serde"))] +use crate::lib::alloc::vec; /// Subscription event. /// @@ -25,7 +33,7 @@ use base64::{engine::general_purpose, Engine}; #[derive(Debug, Clone)] pub enum SubscribeStreamEvent { /// Subscription status update. - Status(SubscribeStatus), + Status(ConnectionStatus), /// Real-time update. Update(Update), @@ -72,13 +80,42 @@ pub enum SubscribeMessageType { File = 4, } +/// Subscription behaviour options. +/// +/// Subscription behaviour with real-time events can be adjusted using provided +/// options. Currently, subscription can be instructed to: +/// * listen presence events for channels and groups +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SubscriptionOptions { + /// Whether presence events should be received. + /// + /// Whether presence updates for `userId` should be delivered through + /// [`Subscription2`] listener streams or not. + ReceivePresenceEvents, +} + +/// [`PubNubClientInstance`] multiplex subscription parameters. +/// +/// Multiplexed subscription configuration parameters. +pub struct SubscriptionParams<'subscription, N: Into> { + /// List of channel names for multiplexed subscription. + pub channels: Option<&'subscription [N]>, + + /// List of channel group names for multiplexed subscription. + pub channel_groups: Option<&'subscription [N]>, + + /// An optional list of `SubscriptionOptions` specifying the subscription + /// behaviour. + pub options: Option>, +} + /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize))] -pub struct SubscribeCursor { +pub struct SubscriptionCursor { /// PubNub high-precision timestamp. /// /// Aside of specifying exact time of receiving data / event this token used @@ -91,9 +128,55 @@ pub struct SubscribeCursor { pub region: u32, } +impl PartialOrd for SubscriptionCursor { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + + fn lt(&self, other: &Self) -> bool { + let lhs = self.timetoken.parse::().expect("Invalid timetoken"); + let rhs = other.timetoken.parse::().expect("Invalid timetoken"); + lhs < rhs + } + + fn le(&self, other: &Self) -> bool { + let lhs = self.timetoken.parse::().expect("Invalid timetoken"); + let rhs = other.timetoken.parse::().expect("Invalid timetoken"); + lhs <= rhs + } + + fn gt(&self, other: &Self) -> bool { + let lhs = self.timetoken.parse::().expect("Invalid timetoken"); + let rhs = other.timetoken.parse::().expect("Invalid timetoken"); + lhs > rhs + } + + fn ge(&self, other: &Self) -> bool { + let lhs = self.timetoken.parse::().expect("Invalid timetoken"); + let rhs = other.timetoken.parse::().expect("Invalid timetoken"); + lhs >= rhs + } +} + +impl Ord for SubscriptionCursor { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Equal) + } +} + +impl Debug for SubscriptionCursor { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!( + f, + "SubscriptionCursor {{ timetoken: {}, region: {} }}", + self.timetoken, self.region + ) + } +} + /// Subscription statuses. -#[derive(Debug, Clone, PartialEq)] -pub enum SubscribeStatus { +#[derive(Clone, PartialEq)] +pub enum ConnectionStatus { /// Successfully connected and receiving real-time updates. Connected, @@ -106,6 +189,9 @@ pub enum SubscribeStatus { /// Connection attempt failed. ConnectionError(PubNubError), + + /// Unexpected disconnection. + DisconnectedUnexpectedly(PubNubError), } /// Presence update information. @@ -135,6 +221,19 @@ pub enum Presence { /// Current channel occupancy after user joined. occupancy: usize, + + /// The user's state associated with the channel has been updated. + #[cfg(feature = "serde")] + data: Option, + + /// The user's state associated with the channel has been updated. + #[cfg(not(feature = "serde"))] + data: Option>, + + /// PubNub high-precision timestamp. + /// + /// Time when event has been emitted. + event_timestamp: usize, }, /// Remote user `leave` update. @@ -156,6 +255,11 @@ pub enum Presence { /// Unique identification of the user which left the channel. uuid: String, + + /// PubNub high-precision timestamp. + /// + /// Time when event has been emitted. + event_timestamp: usize, }, /// Remote user `timeout` update. @@ -177,6 +281,11 @@ pub enum Presence { /// Unique identification of the user which timeout the channel. uuid: String, + + /// PubNub high-precision timestamp. + /// + /// Time when event has been emitted. + event_timestamp: usize, }, /// Channel `interval` presence update. @@ -208,6 +317,11 @@ pub enum Presence { /// The list of unique user identifiers that `timeout` the channel since /// the last interval presence update. timeout: Option>, + + /// PubNub high-precision timestamp. + /// + /// Time when event has been emitted. + event_timestamp: usize, }, /// Remote user `state` change update. @@ -235,28 +349,34 @@ pub enum Presence { /// The user's state associated with the channel has been updated. #[cfg(not(feature = "serde"))] data: Vec, + + /// PubNub high-precision timestamp. + /// + /// Time when event has been emitted. + event_timestamp: usize, }, } -/// Objects update information. +/// App Context object update information. /// -/// Enum provides [`Object::Channel`], [`Object::Uuid`] and -/// [`Object::Membership`] variants for updates listener. These variants allow -/// listener understand how objects and their relationship changes. +/// Enum provides [`AppContext::Channel`], [`AppContext::Uuid`] and +/// [`AppContext::Membership`] variants for updates listener. These variants +/// allow listener understand how App Context objects and their relationship +/// changes. #[derive(Debug, Clone)] -pub enum Object { - /// `Channel` object update. +pub enum AppContext { + /// `Channel` metadata object update. Channel { - /// The type of event that happened during the object update. + /// The type of event that happened during the metadata object update. event: Option, - /// Time when `channel` object has been updated. + /// Time when metadata has been updated. timestamp: Option, - /// Given name of the channel object. + /// Given name of the metadata object. name: Option, - /// `Channel` object additional description. + /// Metadata additional description. description: Option, /// `Channel` object type information. @@ -336,7 +456,7 @@ pub enum Object { timestamp: Option, /// `Channel` object within which `uuid` object registered as member. - channel: Box, + channel: Box, /// Flatten `HashMap` with additional information associated with /// `membership` object. @@ -439,7 +559,6 @@ pub struct MessageAction { /// [`File`] type provides to the updates listener information about shared /// files. #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct File { /// Identifier of client which sent shared file. pub sender: String, @@ -454,13 +573,13 @@ pub struct File { pub subscription: String, /// Message which has been associated with uploaded file. - message: String, + pub message: String, /// Unique identifier of uploaded file. - id: String, + pub id: String, /// Actual name with which file has been stored. - name: String, + pub name: String, } /// Object update event types. @@ -483,7 +602,7 @@ pub enum MessageActionEvent { Delete, } -impl Default for SubscribeCursor { +impl Default for SubscriptionCursor { fn default() -> Self { Self { timetoken: "0".into(), @@ -492,6 +611,15 @@ impl Default for SubscribeCursor { } } +impl From for SubscriptionCursor { + fn from(value: String) -> Self { + Self { + timetoken: value, + ..Default::default() + } + } +} + impl TryFrom for ObjectEvent { type Error = PubNubError; @@ -520,26 +648,29 @@ impl TryFrom for MessageActionEvent { } } -impl From for HashMap { - fn from(value: SubscribeCursor) -> Self { +impl From for HashMap { + fn from(value: SubscriptionCursor) -> Self { if value.timetoken.eq(&"0") { - HashMap::from([("tt".into(), value.timetoken)]) + HashMap::from([(String::from("tt"), value.timetoken)]) } else { HashMap::from([ - ("tt".into(), value.timetoken.to_string()), - ("tr".into(), value.region.to_string()), + (String::from("tt"), value.timetoken.to_string()), + (String::from("tr"), value.region.to_string()), ]) } } } -impl core::fmt::Display for SubscribeStatus { +impl Debug for ConnectionStatus { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { Self::Connected => write!(f, "Connected"), Self::Reconnected => write!(f, "Reconnected"), Self::Disconnected => write!(f, "Disconnected"), Self::ConnectionError(err) => write!(f, "ConnectionError({err:?})"), + ConnectionStatus::DisconnectedUnexpectedly(err) => { + write!(f, "DisconnectedUnexpectedly({err:?})") + } } } } @@ -552,26 +683,64 @@ impl Presence { /// which presence update has been delivered. pub(crate) fn subscription(&self) -> String { match self { - Presence::Join { subscription, .. } - | Presence::Leave { subscription, .. } - | Presence::Timeout { subscription, .. } - | Presence::Interval { subscription, .. } - | Presence::StateChange { subscription, .. } => subscription.clone(), + Self::Join { subscription, .. } + | Self::Leave { subscription, .. } + | Self::Timeout { subscription, .. } + | Self::Interval { subscription, .. } + | Self::StateChange { subscription, .. } => subscription.clone(), + } + } + + /// PubNub high-precision presence event timestamp. + /// + /// # Returns + /// + /// Returns time when presence event has been emitted. + pub(crate) fn event_timestamp(&self) -> usize { + match self { + Self::Join { + event_timestamp, .. + } + | Self::Leave { + event_timestamp, .. + } + | Self::Timeout { + event_timestamp, .. + } + | Self::Interval { + event_timestamp, .. + } + | Self::StateChange { + event_timestamp, .. + } => *event_timestamp, } } } #[cfg(feature = "std")] -impl Object { +impl AppContext { /// Name of subscription. /// /// Name of channel or channel group on which client subscribed and through /// which object update has been triggered. pub(crate) fn subscription(&self) -> String { match self { - Object::Channel { subscription, .. } - | Object::Uuid { subscription, .. } - | Object::Membership { subscription, .. } => subscription.clone(), + Self::Channel { subscription, .. } + | Self::Uuid { subscription, .. } + | Self::Membership { subscription, .. } => subscription.clone(), + } + } + + /// PubNub high-precision AppContext event timestamp. + /// + /// # Returns + /// + /// Returns time when AppContext event has been emitted. + pub(crate) fn event_timestamp(&self) -> usize { + match self { + Self::Channel { timestamp, .. } + | Self::Uuid { timestamp, .. } + | Self::Membership { timestamp, .. } => timestamp.unwrap_or(0), } } } @@ -621,6 +790,7 @@ impl TryFrom for Presence { type Error = PubNubError; fn try_from(value: Envelope) -> Result { + let event_timestamp = value.published.timetoken.parse::().ok().unwrap_or(0); if let EnvelopePayload::Presence { action, timestamp, @@ -646,6 +816,8 @@ impl TryFrom for Presence { channel, subscription, occupancy: occupancy.unwrap_or(0), + data, + event_timestamp, }), "leave" => Ok(Self::Leave { timestamp, @@ -655,6 +827,7 @@ impl TryFrom for Presence { channel, subscription, occupancy: occupancy.unwrap_or(0), + event_timestamp, }), "timeout" => Ok(Self::Timeout { timestamp, @@ -664,6 +837,7 @@ impl TryFrom for Presence { channel, subscription, occupancy: occupancy.unwrap_or(0), + event_timestamp, }), "interval" => Ok(Self::Interval { timestamp, @@ -673,6 +847,7 @@ impl TryFrom for Presence { join, leave, timeout, + event_timestamp, }), _ => Ok(Self::StateChange { timestamp, @@ -681,7 +856,11 @@ impl TryFrom for Presence { uuid: uuid.unwrap_or("".to_string()), channel, subscription, - data, + #[cfg(feature = "serde")] + data: data.unwrap_or(serde_json::Value::Null), + #[cfg(not(feature = "serde"))] + data: data.unwrap_or(vec![]), + event_timestamp, }), } } else { @@ -692,7 +871,7 @@ impl TryFrom for Presence { } } -impl TryFrom for Object { +impl TryFrom for AppContext { type Error = PubNubError; fn try_from(value: Envelope) -> Result { @@ -778,7 +957,7 @@ impl TryFrom for Object { Ok(Self::Membership { event: Some(event.try_into()?), timestamp: timestamp.ok(), - channel: Box::new(Object::Channel { + channel: Box::new(AppContext::Channel { event: None, timestamp: None, name, @@ -915,9 +1094,10 @@ fn resolve_subscription_value(subscription: Option, channel: &str) -> St // TODO: add tests for complicated forms. #[cfg(test)] mod should { - use super::*; use test_case::test_case; + use super::*; + #[test_case( None, "channel" => "channel".to_string(); diff --git a/src/lib.rs b/src/lib.rs index 97a88273..0e86640b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,11 +39,11 @@ //! ```toml //! # default features //! [dependencies] -//! pubnub = "0.4.1" +//! pubnub = "0.5.0" //! //! # all features //! [dependencies] -//! pubnub = { version = "0.4.1", features = ["full"] } +//! pubnub = { version = "0.5.0", features = ["full"] } //! ``` //! //! ### Example @@ -60,53 +60,53 @@ //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! let publish_key = "my_publish_key"; +//! use pubnub::subscribe::{EventEmitter, SubscriptionParams}; +//! let publish_key = "my_publish_key"; //! let subscribe_key = "my_subscribe_key"; //! let client = PubNubClientBuilder::with_reqwest_transport() -//! .with_keyset(Keyset { -//! subscribe_key, -//! publish_key: Some(publish_key), -//! secret_key: None, -//! }) -//! .with_user_id("user_id") -//! .build()?; -//! println!("PubNub instance created"); -//! -//! let subscription = client -//! .subscribe() -//! .channels(["my_channel".into()].to_vec()) -//! .execute()?; -//! -//! println!("Subscribed to channel"); -//! -//! // Launch a new task to print out each received message -//! tokio::spawn(subscription.stream().for_each(|event| async move { -//! match event { -//! SubscribeStreamEvent::Update(update) => { -//! match update { -//! Update::Message(message) | Update::Signal(message) => { -//! // Silently log if UTF-8 conversion fails -//! if let Ok(utf8_message) = String::from_utf8(message.data.clone()) { -//! if let Ok(cleaned) = serde_json::from_str::(&utf8_message) { -//! println!("message: {}", cleaned); -//! } -//! } -//! } -//! Update::Presence(presence) => { -//! println!("presence: {:?}", presence) -//! } -//! Update::Object(object) => { -//! println!("object: {:?}", object) -//! } -//! Update::MessageAction(action) => { -//! println!("message action: {:?}", action) -//! } -//! Update::File(file) => { -//! println!("file: {:?}", file) +//! .with_keyset(Keyset { +//! subscribe_key, +//! publish_key: Some(publish_key), +//! secret_key: None, +//! }) +//! .with_user_id("user_id") +//! .build()?; +//! println!("PubNub instance created"); +//! +//! let subscription = client.subscription(SubscriptionParams { +//! channels: Some(&["my_channel"]), +//! channel_groups: None, +//! options: None +//! }); +//! +//! println!("Subscribed to channel"); +//! +//! // Launch a new task to print out each received message +//! tokio::spawn(client.status_stream().for_each(|status| async move { +//! println!("\nStatus: {:?}", status) +//! })); +//! tokio::spawn(subscription.stream().for_each(|event| async move { +//! match event { +//! Update::Message(message) | Update::Signal(message) => { +//! // Silently log if UTF-8 conversion fails +//! if let Ok(utf8_message) = String::from_utf8(message.data.clone()) { +//! if let Ok(cleaned) = serde_json::from_str::(&utf8_message) { +//! println!("message: {}", cleaned); //! } //! } //! } -//! SubscribeStreamEvent::Status(status) => println!("\nstatus: {:?}", status), +//! Update::Presence(presence) => { +//! println!("presence: {:?}", presence) +//! } +//! Update::AppContext(object) => { +//! println!("object: {:?}", object) +//! } +//! Update::MessageAction(action) => { +//! println!("message action: {:?}", action) +//! } +//! Update::File(file) => { +//! println!("file: {:?}", file) +//! } //! } //! })); //! @@ -135,11 +135,11 @@ //! ```toml //! # only blocking and access + default features //! [dependencies] -//! pubnub = { version = "0.4.1", features = ["blocking", "access"] } +//! pubnub = { version = "0.5.0", features = ["blocking", "access"] } //! //! # only parse_token + default features //! [dependencies] -//! pubnub = { version = "0.4.1", features = ["parse_token"] } +//! pubnub = { version = "0.5.0", features = ["parse_token"] } //! ``` //! //! ### Available features @@ -178,7 +178,7 @@ //! //! ```toml //! [dependencies] -//! pubnub = { version = "0.4.1", default-features = false, features = ["serde", "publish", +//! pubnub = { version = "0.5.0", default-features = false, features = ["serde", "publish", //! "blocking"] } //! ``` //! @@ -254,6 +254,12 @@ pub use dx::{Keyset, PubNubClientBuilder, PubNubGenericClient}; #[doc(inline)] pub use dx::PubNubClient; +#[cfg(feature = "std")] +#[doc(inline)] +pub use core::RequestRetryConfiguration; + +#[doc(inline)] +pub use core::{Channel, ChannelGroup, ChannelMetadata, UserMetadata}; pub mod core; pub mod dx; pub mod providers; diff --git a/src/providers/deserialization_serde.rs b/src/providers/deserialization_serde.rs index 08943ba0..2c2bb983 100644 --- a/src/providers/deserialization_serde.rs +++ b/src/providers/deserialization_serde.rs @@ -5,9 +5,8 @@ //! # Examples //! ``` //! use pubnub::core::Serialize as _; -//! use serde::{Serialize, Deserialize}; //! -//! #[derive(Serialize, Deserialize, Debug, PartialEq)] +//! #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] //! struct Foo { //! bar: String, //! } diff --git a/src/providers/futures_tokio.rs b/src/providers/futures_tokio.rs index 95e09393..e734fce0 100644 --- a/src/providers/futures_tokio.rs +++ b/src/providers/futures_tokio.rs @@ -24,4 +24,8 @@ impl Runtime for RuntimeTokio { async fn sleep(self, delay: u64) { tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await } + + async fn sleep_microseconds(self, delay: u64) { + tokio::time::sleep(tokio::time::Duration::from_micros(delay)).await + } } diff --git a/src/providers/serialization_serde.rs b/src/providers/serialization_serde.rs index 50846d22..6e2963dc 100644 --- a/src/providers/serialization_serde.rs +++ b/src/providers/serialization_serde.rs @@ -5,9 +5,8 @@ //! # Examples //! ``` //! use pubnub::core::Serialize as _; -//! use serde::{Serialize, Deserialize}; //! -//! #[derive(Serialize, Deserialize, Debug, PartialEq)] +//! #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] //! struct Foo { //! bar: String, //! } @@ -46,8 +45,8 @@ impl crate::core::Serialize for S where S: serde::Serialize, { - fn serialize(self) -> Result, crate::core::PubNubError> { - serde_json::to_vec(&self).map_err(|e| PubNubError::Serialization { + fn serialize(&self) -> Result, crate::core::PubNubError> { + serde_json::to_vec(self).map_err(|e| PubNubError::Serialization { details: e.to_string(), }) } diff --git a/src/transport/reqwest.rs b/src/transport/reqwest.rs index e504b0a8..18bb30fc 100644 --- a/src/transport/reqwest.rs +++ b/src/transport/reqwest.rs @@ -81,7 +81,24 @@ impl Transport for TransportReqwest { "Sending data to pubnub: {} {:?} {}", request.method, request.headers, request_url ); + let headers = prepare_headers(&request.headers)?; + #[cfg(feature = "std")] + let timeout = request.timeout; + + #[cfg(feature = "std")] + let mut builder = match request.method { + TransportMethod::Get => self.prepare_get_method(request, request_url), + TransportMethod::Post => self.prepare_post_method(request, request_url), + TransportMethod::Delete => self.prepare_delete_method(request, request_url), + }?; + + #[cfg(feature = "std")] + if timeout.gt(&0) { + builder = builder.timeout(core::time::Duration::from_secs(timeout)); + } + + #[cfg(not(feature = "std"))] let builder = match request.method { TransportMethod::Get => self.prepare_get_method(request, request_url), TransportMethod::Post => self.prepare_post_method(request, request_url), @@ -384,6 +401,22 @@ pub mod blocking { request.method, request.headers, request_url ); let headers = prepare_headers(&request.headers)?; + #[cfg(feature = "std")] + let timeout = request.timeout; + + #[cfg(feature = "std")] + let mut builder = match request.method { + TransportMethod::Get => self.prepare_get_method(request, request_url), + TransportMethod::Post => self.prepare_post_method(request, request_url), + TransportMethod::Delete => self.prepare_delete_method(request, request_url), + }?; + + #[cfg(feature = "std")] + if timeout.gt(&0) { + builder = builder.timeout(core::time::Duration::from_micros(timeout)) + } + + #[cfg(not(feature = "std"))] let builder = match request.method { TransportMethod::Get => self.prepare_get_method(request, request_url), TransportMethod::Post => self.prepare_post_method(request, request_url), diff --git a/tests/common/common_steps.rs b/tests/common/common_steps.rs index 38f0b5a3..05d7524a 100644 --- a/tests/common/common_steps.rs +++ b/tests/common/common_steps.rs @@ -1,9 +1,12 @@ use cucumber::gherkin::Scenario; use cucumber::{given, then, World}; -use pubnub::core::RequestRetryPolicy; -use pubnub::dx::subscribe::subscription::Subscription; +use std::collections::HashMap; + +use pubnub::providers::deserialization_serde::DeserializerSerde; +use pubnub::subscribe::{Subscription, SubscriptionSet}; +use pubnub::transport::middleware::PubNubMiddleware; use pubnub::{ - core::PubNubError, + core::{PubNubError, RequestRetryConfiguration}, dx::{ access::{permissions, GrantTokenResult, RevokeTokenResult}, publish::PublishResult, @@ -135,11 +138,19 @@ impl Default for CryptoModuleState { #[derive(Debug, World)] pub struct PubNubWorld { + pub pubnub: Option, pub scenario: Option, pub keyset: pubnub::Keyset, pub publish_result: Result, - pub subscription: Result, - pub retry_policy: Option, + pub subscription: + Option, DeserializerSerde>>, + pub subscriptions: Option< + HashMap, DeserializerSerde>>, + >, + pub retry_policy: Option, + pub heartbeat_value: Option, + pub heartbeat_interval: Option, + pub suppress_leave_events: bool, pub pam_state: PAMState, pub crypto_state: CryptoModuleState, pub api_error: Option, @@ -149,6 +160,7 @@ pub struct PubNubWorld { impl Default for PubNubWorld { fn default() -> Self { PubNubWorld { + pubnub: None, scenario: None, keyset: Keyset:: { subscribe_key: "demo".to_owned(), @@ -159,20 +171,32 @@ impl Default for PubNubWorld { details: "This is default value".into(), response: None, }), - subscription: Err(PubNubError::Transport { - details: "This is default value".into(), - response: None, - }), + subscription: None, + subscriptions: None, is_succeed: false, pam_state: PAMState::default(), crypto_state: CryptoModuleState::default(), api_error: None, retry_policy: None, + heartbeat_value: None, + heartbeat_interval: None, + suppress_leave_events: false, } } } impl PubNubWorld { + pub async fn reset(&mut self) { + self.retry_policy = None; + if let Some(pubnub) = self.pubnub.as_ref() { + log::debug!("Terminate PubNub instance"); + pubnub.terminate(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + self.subscription = None; + self.pubnub = None; + } + pub fn get_pubnub(&self, keyset: Keyset) -> PubNubClient { let transport = { let mut transport = TransportReqwest::default(); @@ -182,18 +206,30 @@ impl PubNubWorld { let mut builder = PubNubClientBuilder::with_transport(transport) .with_keyset(keyset) - .with_user_id("test"); + .with_user_id("test") + .with_suppress_leave_events(self.suppress_leave_events); if let Some(retry_policy) = &self.retry_policy { - builder = builder.with_retry_policy(retry_policy.clone()); + builder = builder.with_retry_configuration(retry_policy.clone()); } - builder.build().unwrap() + if let Some(heartbeat_value) = &self.heartbeat_value { + builder = builder.with_heartbeat_value(*heartbeat_value); + } + + if let Some(heartbeat_interval) = &self.heartbeat_interval { + builder = builder.with_heartbeat_interval(*heartbeat_interval); + } + + let instance = builder.build().unwrap(); + + instance } } #[given("the demo keyset")] #[given("the demo keyset with event engine enabled")] +#[given("the demo keyset with Presence EE enabled")] fn set_keyset(world: &mut PubNubWorld) { world.keyset = Keyset { subscribe_key: "demo".into(), @@ -205,13 +241,28 @@ fn set_keyset(world: &mut PubNubWorld) { #[given(regex = r"^a (.*) reconnection policy with ([0-9]+) retries")] fn set_with_retries(world: &mut PubNubWorld, retry_type: String, max_retry: u8) { if retry_type.eq("linear") { - world.retry_policy = Some(RequestRetryPolicy::Linear { + world.retry_policy = Some(RequestRetryConfiguration::Linear { max_retry, delay: 0, + excluded_endpoints: None, }) } } +#[given( + regex = r"^heartbeatInterval set to '([0-9]+)', timeout set to '([0-9]+)' and suppressLeaveEvents set to '(true|false)'" +)] +fn set_presence_options( + world: &mut PubNubWorld, + interval: u64, + timeout: u64, + suppress_leave: bool, +) { + world.heartbeat_interval = Some(interval); + world.heartbeat_value = Some(timeout); + world.suppress_leave_events = suppress_leave; +} + #[given(regex = r"^I have a keyset with access manager enabled(.*)?")] fn i_have_keyset_with_access_manager_enabled(world: &mut PubNubWorld, info: String) { world.keyset = Keyset { diff --git a/tests/contract_test.rs b/tests/contract_test.rs index 7afb8f69..d2245ef8 100644 --- a/tests/contract_test.rs +++ b/tests/contract_test.rs @@ -5,6 +5,7 @@ use std::process; mod access; mod common; mod crypto; +mod presence; mod publish; mod subscribe; use common::PubNubWorld; @@ -24,7 +25,13 @@ fn get_feature_set(tags: &[String]) -> String { } fn feature_allows_beta(feature: &str) -> bool { - let features: Vec<&str> = vec!["access", "publish", "eventEngine", "cryptoModule"]; + let features: Vec<&str> = vec![ + "access", + "publish", + "eventEngine", + "presenceEventEngine", + "cryptoModule", + ]; features.contains(&feature) } @@ -39,7 +46,13 @@ fn feature_allows_contract_less(feature: &str) -> bool { } fn is_ignored_feature_set_tag(feature: &str, tags: &[String]) -> bool { - let supported_features = ["access", "publish", "eventEngine", "cryptoModule"]; + let supported_features = [ + "access", + "publish", + "eventEngine", + "presenceEventEngine", + "cryptoModule", + ]; let mut ignored_tags = vec!["na=rust"]; if !feature_allows_beta(feature) { @@ -64,9 +77,13 @@ fn is_ignored_scenario_tag(feature: &str, tags: &[String]) -> bool { || !feature_allows_beta(feature) && tags.iter().any(|tag| tag.starts_with("beta")) || !feature_allows_skipped(feature) && tags.iter().any(|tag| tag.starts_with("skip")) || (!feature_allows_contract_less(feature) || !tested_contract.is_empty()) - && !tags - .iter() - .any(|tag| tag.starts_with(format!("contract={tested_contract}").as_str())) + && !tags.iter().any(|tag| { + if !tested_contract.is_empty() { + tag == format!("contract={tested_contract}").as_str() + } else { + tag.starts_with("contract=") + } + }) } pub fn scenario_name(world: &mut PubNubWorld) -> String { @@ -75,12 +92,25 @@ pub fn scenario_name(world: &mut PubNubWorld) -> String { pub fn clear_log_file() { create_dir_all("tests/logs").expect("Unable to create required directories for logs"); - if let Ok(file) = OpenOptions::new() - .read(true) - .write(true) - .open("tests/logs/log.txt") - { - file.set_len(0).expect("Can't clean up the file"); + if let Ok(_) = std::fs::metadata("tests/logs/log.txt") { + // let file_content: String = read_to_string("tests/logs/log.txt") + // .expect("Can't open log file for read.") + // .chars() + // .filter(|&c| c != '\0') + // .collect(); + // File::create(format!("tests/logs/log-{}.txt", Uuid::new_v4().to_string())) + // .expect("Can't open file to write content") + // .write_all(file_content.as_bytes()) + // .expect("Can't write log file"); + + OpenOptions::new() + .read(true) + .write(true) + .truncate(true) + .open("tests/logs/log.txt") + .expect("Unable to open log file.") + .set_len(0) + .expect("Can't truncate log file"); } } @@ -108,6 +138,7 @@ async fn main() { .max_concurrent_scenarios(1) // sequential execution because tomato waits for a specific request at a time for which a // script is initialised. .before(|_feature, _rule, scenario, world| { + // world.reset(); world.scenario = Some(scenario.clone()); futures::FutureExt::boxed(async move { @@ -125,6 +156,9 @@ async fn main() { } }) }) + .after(|_feature, _, _rule, _scenario, world| { + futures::FutureExt::boxed(async move { world.unwrap().reset().await }) + }) .with_writer( writer::Basic::stdout() .summarized() diff --git a/tests/presence/mod.rs b/tests/presence/mod.rs new file mode 100644 index 00000000..9d8426a2 --- /dev/null +++ b/tests/presence/mod.rs @@ -0,0 +1 @@ +pub mod presence_steps; diff --git a/tests/presence/presence_steps.rs b/tests/presence/presence_steps.rs new file mode 100644 index 00000000..51a6fa41 --- /dev/null +++ b/tests/presence/presence_steps.rs @@ -0,0 +1,225 @@ +use cucumber::gherkin::Table; +use cucumber::{codegen::Regex, gherkin::Step, then, when}; +use futures::{select_biased, FutureExt, StreamExt}; +use std::collections::HashMap; +use std::fs::read_to_string; + +use crate::clear_log_file; +use crate::common::PubNubWorld; +use pubnub::core::RequestRetryConfiguration; +use pubnub::subscribe::{ + EventEmitter, EventSubscriber, Presence, Subscriber, SubscriptionOptions, SubscriptionSet, +}; + +/// Extract list of events and invocations from log. +fn events_and_invocations_history() -> Vec> { + log::logger().flush(); + let mut lines: Vec> = Vec::new(); + let written_log = + read_to_string("tests/logs/log.txt").expect("Unable to read history from log"); + let event_regex = Regex::new(r" DEBUG .* Processing event: (.+)$").unwrap(); + let invocation_regex = Regex::new(r" DEBUG .* Received invocation: (.+)$").unwrap(); + let known_events = [ + "JOINED", + "LEFT", + "LEFT_ALL", + "HEARTBEAT_SUCCESS", + "HEARTBEAT_FAILURE", + "HEARTBEAT_GIVEUP", + "RECONNECT", + "DISCONNECT", + "TIMES_UP", + ]; + let known_invocations = [ + "HEARTBEAT", + "DELAYED_HEARTBEAT", + "CANCEL_DELAYED_HEARTBEAT", + "LEAVE", + "WAIT", + "CANCEL_WAIT", + ]; + + for line in written_log.lines() { + if !line.contains(" DEBUG ") { + continue; + } + + if let Some(matched) = event_regex.captures(line) { + let (_, [captured]) = matched.extract(); + if known_events.contains(&captured) { + lines.push(["event".into(), captured.into()].to_vec()); + } + } + + if let Some(matched) = invocation_regex.captures(line) { + let (_, [captured]) = matched.extract(); + if known_invocations.contains(&captured) { + lines.push(["invocation".into(), captured.into()].to_vec()); + } + } + } + + lines +} + +fn event_occurrence_count(history: Vec>, event: String) -> usize { + history + .iter() + .filter(|pair| pair[0].eq("event") && pair[1].eq(&event)) + .count() +} + +fn invocation_occurrence_count(history: Vec>, invocation: String) -> usize { + history + .iter() + .filter(|pair| pair[0].eq("invocation") && pair[1].eq(&invocation)) + .count() +} + +/// Match list of events and invocations pairs to table defined in step. +fn match_history_to_feature(history: Vec>, table: &Table) { + log::logger().flush(); + + (!table.rows.iter().skip(1).eq(history.iter())).then(|| { + let expected = { + table + .rows + .iter() + .skip(1) + .map(|pair| format!(" ({}) {}", pair[0], pair[1])) + .collect::>() + .join("\n") + }; + let received = { + history + .iter() + .skip(1) + .map(|pair| format!(" ({}) {}", pair[0], pair[1])) + .collect::>() + .join("\n") + }; + + panic!( + "Unexpected set of events and invocations:\n -expected:\n{}\n\n -got:\n{}\n", + expected, received + ) + }); +} + +#[when(regex = r"^I join '(.*)', '(.*)', '(.*)' channels( with presence)?$")] +async fn join( + world: &mut PubNubWorld, + channel_a: String, + channel_b: String, + channel_c: String, + with_presence: String, +) { + // Start recording subscription session. + clear_log_file(); + + world.pubnub = Some(world.get_pubnub(world.keyset.to_owned())); + let Some(client) = world.pubnub.clone() else { + panic!("Unable to get PubNub client instance"); + }; + + let options = + (!with_presence.is_empty()).then_some(vec![SubscriptionOptions::ReceivePresenceEvents]); + let subscriptions = + vec![channel_a, channel_b, channel_c] + .iter() + .fold(HashMap::new(), |mut acc, channel| { + acc.insert( + channel.clone(), + client.channel(channel).subscription(options.clone()), + ); + acc + }); + let subscription = SubscriptionSet::new_with_subscriptions( + subscriptions.values().cloned().collect(), + options.clone(), + ); + subscription.subscribe(None); + world.subscription = Some(subscription); + world.subscriptions = Some(subscriptions); +} + +#[then(regex = r"^I wait '([0-9]+)' seconds$")] +async fn wait(_world: &mut PubNubWorld, delay: u64) { + tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await; +} + +#[then(regex = r"^I leave '(.*)' and '(.*)' channels( with presence)?$")] +async fn leave( + world: &mut PubNubWorld, + channel_a: String, + channel_b: String, + _with_presence: String, +) { + let mut subscription = world.subscription.clone().unwrap(); + let subscriptions = world.subscriptions.clone().unwrap().iter().fold( + vec![], + |mut acc, (channel, subscription)| { + if channel.eq(&channel_a) || channel.eq(&channel_b) { + acc.push(subscription.clone()) + } + acc + }, + ); + + subscription -= SubscriptionSet::new_with_subscriptions(subscriptions, None); +} + +#[then("I wait for getting Presence joined events")] +async fn wait_presence_join(world: &mut PubNubWorld) { + let mut subscription = world.subscription.clone().unwrap().presence_stream(); + + select_biased! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).fuse() => panic!("No service response"), + update = subscription.next().fuse() => { + match update.clone().unwrap() { + Presence::Join { .. } => {}, + _ => panic!("Unexpected presence update received: {update:?}"), + }; + + subscription.next().await; + subscription.next().await; + } + } +} + +#[then("I receive an error in my heartbeat response")] +async fn receive_an_error_heartbeat_retry(world: &mut PubNubWorld) { + tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; + + let history = events_and_invocations_history(); + let expected_retry_count: usize = usize::from(match &world.retry_policy.clone().unwrap() { + RequestRetryConfiguration::Linear { max_retry, .. } + | RequestRetryConfiguration::Exponential { max_retry, .. } => *max_retry, + _ => 0, + }); + + assert_eq!( + event_occurrence_count(history.clone(), "HEARTBEAT_FAILURE".into()), + expected_retry_count + 1 + ); + assert_eq!( + event_occurrence_count(history, "HEARTBEAT_GIVEUP".into()), + 1 + ); +} + +#[then("I don't observe any Events and Invocations of the Presence EE")] +async fn event_engine_history_empty(_world: &mut PubNubWorld) { + assert_eq!(events_and_invocations_history().len(), 0); +} + +#[then("I observe the following Events and Invocations of the Presence EE:")] +async fn event_engine_history(_world: &mut PubNubWorld, step: &Step) { + let history = events_and_invocations_history(); + + if let Some(table) = step.table.as_ref() { + match_history_to_feature(history, table); + } else { + panic!("Unable table content.") + } +} diff --git a/tests/subscribe/subscribe_steps.rs b/tests/subscribe/subscribe_steps.rs index a473ba22..529c39eb 100644 --- a/tests/subscribe/subscribe_steps.rs +++ b/tests/subscribe/subscribe_steps.rs @@ -3,7 +3,8 @@ use crate::{clear_log_file, scenario_name}; use cucumber::gherkin::Table; use cucumber::{codegen::Regex, gherkin::Step, then, when}; use futures::{select_biased, FutureExt, StreamExt}; -use pubnub::core::RequestRetryPolicy; +use pubnub::core::RequestRetryConfiguration; +use pubnub::subscribe::{EventEmitter, EventSubscriber, SubscriptionParams}; use std::fs::read_to_string; /// Extract list of events and invocations from log. @@ -13,6 +14,35 @@ fn events_and_invocations_history() -> Vec> { read_to_string("tests/logs/log.txt").expect("Unable to read history from log"); let event_regex = Regex::new(r" DEBUG .* Processing event: (.+)$").unwrap(); let invocation_regex = Regex::new(r" DEBUG .* Received invocation: (.+)$").unwrap(); + let known_events = [ + "SUBSCRIPTION_CHANGED", + "SUBSCRIPTION_RESTORED", + "HANDSHAKE_SUCCESS", + "HANDSHAKE_FAILURE", + "HANDSHAKE_RECONNECT_SUCCESS", + "HANDSHAKE_RECONNECT_FAILURE", + "HANDSHAKE_RECONNECT_GIVEUP", + "RECEIVE_SUCCESS", + "RECEIVE_FAILURE", + "RECEIVE_RECONNECT_SUCCESS", + "RECEIVE_RECONNECT_FAILURE", + "RECEIVE_RECONNECT_GIVEUP", + "DISCONNECT", + "RECONNECT", + "UNSUBSCRIBE_ALL", + ]; + let known_invocations = [ + "HANDSHAKE", + "CANCEL_HANDSHAKE", + "HANDSHAKE_RECONNECT", + "CANCEL_HANDSHAKE_RECONNECT", + "RECEIVE_MESSAGES", + "CANCEL_RECEIVE_MESSAGES", + "RECEIVE_RECONNECT", + "CANCEL_RECEIVE_RECONNECT", + "EMIT_STATUS", + "EMIT_MESSAGES", + ]; for line in written_log.lines() { if !line.contains(" DEBUG ") { @@ -21,19 +51,22 @@ fn events_and_invocations_history() -> Vec> { if let Some(matched) = event_regex.captures(line) { let (_, [captured]) = matched.extract(); - lines.push(["event".into(), captured.into()].to_vec()); + if known_events.contains(&captured) { + lines.push(["event".into(), captured.into()].to_vec()); + } } if let Some(matched) = invocation_regex.captures(line) { let (_, [captured]) = matched.extract(); - lines.push(["invocation".into(), captured.into()].to_vec()); + if known_invocations.contains(&captured) { + lines.push(["invocation".into(), captured.into()].to_vec()); + } } } lines } -#[allow(dead_code)] fn event_occurrence_count(history: Vec>, event: String) -> usize { history .iter() @@ -41,7 +74,6 @@ fn event_occurrence_count(history: Vec>, event: String) -> usize { .count() } -#[allow(dead_code)] fn invocation_occurrence_count(history: Vec>, invocation: String) -> usize { history .iter() @@ -51,6 +83,8 @@ fn invocation_occurrence_count(history: Vec>, invocation: String) -> /// Match list of events and invocations pairs to table defined in step. fn match_history_to_feature(history: Vec>, table: &Table) { + log::logger().flush(); + (!table.rows.iter().skip(1).eq(history.iter())).then(|| { let expected = { table @@ -64,7 +98,6 @@ fn match_history_to_feature(history: Vec>, table: &Table) { let received = { history .iter() - .skip(1) .map(|pair| format!(" ({}) {}", pair[0], pair[1])) .collect::>() .join("\n") @@ -81,25 +114,39 @@ fn match_history_to_feature(history: Vec>, table: &Table) { async fn subscribe(world: &mut PubNubWorld) { // Start recording subscription session. clear_log_file(); - let client = world.get_pubnub(world.keyset.to_owned()); - world.subscription = client.subscribe().channels(["test".into()]).execute(); + world.pubnub = Some(world.get_pubnub(world.keyset.to_owned())); + + world.pubnub.clone().map(|pubnub| { + let subscription = pubnub.subscription(SubscriptionParams { + channels: Some(&["test"]), + channel_groups: None, + options: None, + }); + subscription.subscribe(None); + world.subscription = Some(subscription); + }); } #[when(regex = r"^I subscribe with timetoken ([0-9]+)$")] async fn subscribe_with_timetoken(world: &mut PubNubWorld, timetoken: u64) { // Start recording subscription session. clear_log_file(); - let client = world.get_pubnub(world.keyset.to_owned()); - world.subscription = client - .subscribe() - .channels(["test".into()]) - .cursor(timetoken) - .execute(); + world.pubnub = Some(world.get_pubnub(world.keyset.to_owned())); + + world.pubnub.clone().map(|pubnub| { + let subscription = pubnub.subscription(SubscriptionParams { + channels: Some(&["test"]), + channel_groups: None, + options: None, + }); + subscription.subscribe(Some(timetoken.to_string().into())); + world.subscription = Some(subscription); + }); } #[then("I receive the message in my subscribe response")] async fn receive_message(world: &mut PubNubWorld) { - let mut subscription = world.subscription.clone().unwrap().message_stream(); + let mut subscription = world.subscription.clone().unwrap().messages_stream(); select_biased! { _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).fuse() => panic!("No service response"), @@ -109,19 +156,19 @@ async fn receive_message(world: &mut PubNubWorld) { #[then("I receive an error in my subscribe response")] async fn receive_an_error_subscribe_retry(world: &mut PubNubWorld) { - let mut subscription = world.subscription.clone().unwrap().message_stream(); + let mut subscription = world.subscription.clone().unwrap().messages_stream(); select_biased! { - _ = tokio::time::sleep(tokio::time::Duration::from_secs(1)).fuse() => log::debug!("One \ - second is done"), + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).fuse() => log::debug!("Two second is done"), _ = subscription.next().fuse() => panic!("Message update from server") } let expected_retry_count: usize = usize::from(match &world.retry_policy.clone().unwrap() { - RequestRetryPolicy::Linear { max_retry, .. } - | RequestRetryPolicy::Exponential { max_retry, .. } => *max_retry, + RequestRetryConfiguration::Linear { max_retry, .. } + | RequestRetryConfiguration::Exponential { max_retry, .. } => *max_retry, _ => 0, }); + tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; let handshake_test = scenario_name(world).to_lowercase().contains("handshake"); let history = events_and_invocations_history(); @@ -149,15 +196,18 @@ async fn receive_an_error_subscribe_retry(world: &mut PubNubWorld) { assert_eq!( event_occurrence_count(history.clone(), normal_operation_name.into()), - 1 + 1, + "{normal_operation_name} should appear at least once" ); assert_eq!( event_occurrence_count(history.clone(), reconnect_operation_name.into()), - expected_retry_count + expected_retry_count, + "{reconnect_operation_name} should appear {expected_retry_count} times" ); assert_eq!( event_occurrence_count(history, give_up_operation_name.into()), - 1 + 1, + "{give_up_operation_name} should appear at least once" ); }