From 3e6c97f7a52b45142df3a2de2326ce088ef9f911 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Fri, 19 Jan 2024 09:07:37 +0200 Subject: [PATCH 01/11] feat(listener): change the real-time event handling interface feat(presence-state): state maintained after set state is used `user_id` state for specified channels will be maintained by the SDK. State with subscribe calls has been improved. feat(api): adding first-class citizens to access subscription 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. feat(auto-retry): added ability to exclude endpoints from retry Added ability to configure request retry policies to exclude specific endpoints from retry. --- Cargo.toml | 7 +- examples/presence_state.rs | 41 +- examples/subscribe.rs | 139 ++- examples/subscribe_raw.rs | 2 +- examples/subscribe_raw_blocking.rs | 2 +- src/core/channel.rs | 217 ++++ src/core/channel_group.rs | 218 ++++ src/core/channel_metadata.rs | 215 ++++ src/core/data_stream.rs | 199 ++++ src/core/entity.rs | 196 ++++ src/core/error.rs | 7 + src/core/event_engine/effect.rs | 3 + src/core/event_engine/effect_dispatcher.rs | 78 +- src/core/event_engine/effect_invocation.rs | 8 +- src/core/event_engine/mod.rs | 63 +- src/core/event_engine/state.rs | 2 +- src/core/event_engine/transition.rs | 2 +- src/core/mod.rs | 30 +- src/core/retry_policy.rs | 437 +++++-- src/core/runtime.rs | 18 + src/core/serialize.rs | 9 +- src/core/transport_request.rs | 48 +- src/core/uuid_metadata.rs | 215 ++++ src/dx/access/builders/grant_token.rs | 7 +- src/dx/access/builders/revoke.rs | 8 +- src/dx/access/mod.rs | 6 +- .../presence/builders/get_presence_state.rs | 8 +- src/dx/presence/builders/heartbeat.rs | 130 ++- src/dx/presence/builders/here_now.rs | 13 +- src/dx/presence/builders/leave.rs | 8 +- .../presence/builders/set_presence_state.rs | 41 +- src/dx/presence/builders/where_now.rs | 10 +- .../presence/event_engine/effect_handler.rs | 11 +- .../event_engine/effects/heartbeat.rs | 75 +- src/dx/presence/event_engine/effects/mod.rs | 95 +- src/dx/presence/event_engine/event.rs | 30 +- src/dx/presence/event_engine/invocation.rs | 13 +- src/dx/presence/event_engine/state.rs | 180 ++- src/dx/presence/mod.rs | 381 ++++-- src/dx/presence/presence_manager.rs | 85 +- src/dx/presence/result.rs | 38 +- src/dx/publish/mod.rs | 79 +- src/dx/pubnub_client.rs | 778 ++++++++++++- src/dx/subscribe/builders/mod.rs | 7 - src/dx/subscribe/builders/raw.rs | 31 +- src/dx/subscribe/builders/subscribe.rs | 92 +- src/dx/subscribe/builders/subscription.rs | 596 ---------- src/dx/subscribe/event_dispatcher.rs | 550 +++++++++ .../subscribe/event_engine/effect_handler.rs | 16 +- .../event_engine/effects/emit_messages.rs | 10 +- .../event_engine/effects/emit_status.rs | 8 +- .../event_engine/effects/handshake.rs | 14 +- .../effects/handshake_reconnection.rs | 90 +- src/dx/subscribe/event_engine/effects/mod.rs | 164 ++- .../subscribe/event_engine/effects/receive.rs | 12 +- .../effects/receive_reconnection.rs | 77 +- src/dx/subscribe/event_engine/event.rs | 16 +- src/dx/subscribe/event_engine/invocation.rs | 57 +- src/dx/subscribe/event_engine/mod.rs | 2 +- src/dx/subscribe/event_engine/state.rs | 1028 +++++++++++------ src/dx/subscribe/event_engine/types.rs | 102 +- src/dx/subscribe/mod.rs | 628 +++++++--- src/dx/subscribe/result.rs | 41 +- src/dx/subscribe/subscription.rs | 751 ++++++++++++ src/dx/subscribe/subscription_manager.rs | 456 +++++--- src/dx/subscribe/subscription_set.rs | 953 +++++++++++++++ src/dx/subscribe/traits/event_emitter.rs | 36 + src/dx/subscribe/traits/event_handler.rs | 52 + src/dx/subscribe/traits/event_subscribe.rs | 19 + src/dx/subscribe/traits/mod.rs | 24 + src/dx/subscribe/traits/subscribable.rs | 43 + src/dx/subscribe/traits/subscriber.rs | 25 + src/dx/subscribe/types.rs | 249 +++- src/lib.rs | 86 +- src/providers/deserialization_serde.rs | 3 +- src/providers/futures_tokio.rs | 4 + src/providers/serialization_serde.rs | 7 +- src/transport/reqwest.rs | 39 + tests/common/common_steps.rs | 76 +- tests/contract_test.rs | 64 +- tests/presence/mod.rs | 1 + tests/presence/presence_steps.rs | 221 ++++ tests/subscribe/subscribe_steps.rs | 36 +- 83 files changed, 8585 insertions(+), 2253 deletions(-) create mode 100644 src/core/channel.rs create mode 100644 src/core/channel_group.rs create mode 100644 src/core/channel_metadata.rs create mode 100644 src/core/data_stream.rs create mode 100644 src/core/entity.rs create mode 100644 src/core/uuid_metadata.rs delete mode 100644 src/dx/subscribe/builders/subscription.rs create mode 100644 src/dx/subscribe/event_dispatcher.rs create mode 100644 src/dx/subscribe/subscription.rs create mode 100644 src/dx/subscribe/subscription_set.rs create mode 100644 src/dx/subscribe/traits/event_emitter.rs create mode 100644 src/dx/subscribe/traits/event_handler.rs create mode 100644 src/dx/subscribe/traits/event_subscribe.rs create mode 100644 src/dx/subscribe/traits/mod.rs create mode 100644 src/dx/subscribe/traits/subscribable.rs create mode 100644 src/dx/subscribe/traits/subscriber.rs create mode 100644 tests/presence/mod.rs create mode 100644 tests/presence/presence_steps.rs diff --git a/Cargo.toml b/Cargo.toml index 1905825c..0dc46e84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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,13 +106,14 @@ 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 } # extra_platforms portable-atomic = { version = "1.3", optional = true, default-features = false, features = ["require-cas", "critical-section"] } +backtrace = "0.3.69" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } @@ -165,7 +166,7 @@ required-features = ["default"] [[example]] name = "subscribe" -required-features = ["default", "subscribe"] +required-features = ["default", "subscribe", "presence"] [[example]] name = "subscribe_raw" 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..0309a173 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -1,9 +1,16 @@ -use futures::StreamExt; -use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; -use pubnub::{Keyset, PubNubClientBuilder}; +use std::collections::HashMap; + +use futures::{FutureExt, StreamExt}; use serde::Deserialize; use std::env; +use pubnub::subscribe::SubscriptionOptions; +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 @@ -16,8 +23,8 @@ struct Message { #[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 = "demo"; //env::var("SDK_PUB_KEY")?; + let subscribe_key = "demo"; //env::var("SDK_SUB_KEY")?; let client = PubNubClientBuilder::with_reqwest_transport() .with_keyset(Keyset { @@ -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( + Some(&["my_channel", "other_channel"]), + None, + 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!("defined message: {:?}", message), + Err(_) => { + println!("other message: {:?}", String::from_utf8(message.data)) } - Update::MessageAction(action) => { - println!("message action: {:?}", action) - } - Update::File(file) => { - println!("file: {:?}", file) + } + } + 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) + } + } + })); + + 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!("~~~~~> defined message: {:?}", message), + Err(_) => { + println!("other message: {:?}", String::from_utf8(message.data)) } } } - 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) + } } })); @@ -73,13 +126,29 @@ 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!("~~~~~~~~> DISCONNECT"); + client.disconnect(); + + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + println!("~~~~~~~~> RECONNECT"); + client.reconnect(None); // Let event engine process unsubscribe request + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + println!("~~~~~~~~> UNSUBSCRIBE ALL..."); + + // Clean up before complete work with PubNub client instance. + client.unsubscribe_all(); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + println!("~~~~~~~~> UNSUBSCRIBE ALL. DONE"); + 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..6bbbcf26 --- /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(unused)] + 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>) -> Arc> { + 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..e296262d --- /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(unused)] + 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>) -> Arc> { + 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..8e2650c0 --- /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(unused)] + 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>) -> Arc> { + 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..ad9978e9 --- /dev/null +++ b/src/core/entity.rs @@ -0,0 +1,196 @@ +//! # 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::{ + alloc::string::String, + core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, + }, + }, + Channel, ChannelGroup, ChannelMetadata, UuidMetadata, +}; + +#[cfg(all(feature = "subscribe", feature = "std"))] +use crate::{ + core::{Deserializer, Transport}, + lib::alloc::{sync::Arc, vec::Vec}, + subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, +}; + +pub(crate) trait PubNubEntity2 { + /// Unique entity identifier. + /// + /// Identifier is important for the [`PubNub API`] and used as target + /// identifier for used API. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + fn id(&self) -> String; +} + +pub(crate) enum PubNubEntity { + Channel(Channel), + ChannelGroup(ChannelGroup), + ChannelMetadata(ChannelMetadata), + UuidMetadata(UuidMetadata), +} + +#[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::UuidMetadata(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::UuidMetadata(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::UuidMetadata(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::UuidMetadata(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::UuidMetadata(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::UuidMetadata(uuid_metadata) => Self::UuidMetadata(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::UuidMetadata(uuid_metadata_a) => { + let Self::UuidMetadata(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::UuidMetadata(uuid_metadata) => write!(f, "UuidMetadata({uuid_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>) -> Arc> { + 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::UuidMetadata(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..8d6eda3d 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; @@ -44,7 +46,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, @@ -62,39 +64,62 @@ where { let mut started_slot = self.started.write(); let runtime_clone = runtime.clone(); - let cloned_self = self.clone(); + let cloned_self = Arc::downgrade(self); runtime.spawn(async move { - log::info!("Subscribe engine has started!"); + log::info!("Event engine has started!"); + let mut is_active = true; loop { - match cloned_self.invocations_channel.recv().await { + let Some(strong_self) = cloned_self.upgrade() else { + break; + }; + + let invocation = strong_self.invocations_channel.recv().await; + match invocation { Ok(invocation) => { - log::debug!("Received invocation: {}", invocation.id()); + log::debug!( + "~~~~~~~ INVOCATION: {} | IS ACTIVE? {is_active:?}", + invocation.id() + ); + if !is_active || invocation.is_terminating() { + log::debug!("Received event engine termination invocation"); + break; + } - let effect = cloned_self.dispatch(&invocation); + let effect = strong_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 let Some(strong_self) = cloned_self.upgrade() { + let events = effect.run().await; - if invocation.managed() { - cloned_self.remove_managed_effect(effect.id()); - } + if invocation.is_managed() { + strong_self.remove_managed_effect(effect.id()); + } - task_completion(events); + task_completion(events); + } else { + task_completion(vec![]) + } }); + } else if invocation.is_cancelling() { + log::debug!("Dispatched effect: {}", invocation.id()); } } Err(err) => { + is_active = false; log::error!("Receive error: {err:?}"); + break; } } } + is_active = false; + log::info!("Event engine has stopped!"); }); *started_slot = true; @@ -105,14 +130,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); } @@ -127,6 +152,7 @@ where fn cancel_effect(&self, invocation: &EI) { let mut managed = self.managed.write(); if let Some(position) = managed.iter().position(|e| invocation.cancelling_effect(e)) { + log::debug!("~~~~~~ CANCELLING"); managed.remove(position).cancel(); } } @@ -165,6 +191,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 +236,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 +250,10 @@ mod should { _ => false, } } + + fn is_terminating(&self) -> bool { + false + } } struct TestEffectHandler {} @@ -246,6 +284,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..2591af1f 100644 --- a/src/core/event_engine/mod.rs +++ b/src/core/event_engine/mod.rs @@ -73,13 +73,11 @@ 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 { @@ -123,9 +121,9 @@ where /// * update current state /// * call effects dispatcher to process effect invocation fn process_transition(&self, transition: Transition) { - { + 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 +151,18 @@ where runtime, ); } + + /// Stop state machine using specific invocation. + /// + /// > Note: Should be provided effect information which respond with `true` + /// for `is_terminating` method call. + #[allow(dead_code)] + pub fn stop(&self, invocation: EI) { + self.effect_dispatcher_channel.close(); + if let Err(error) = self.effect_dispatcher_channel.send_blocking(invocation) { + error!("Unable dispatch invocation: {error:?}") + } + } } #[cfg(test)] @@ -191,34 +201,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 +269,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 +313,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 +357,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..85861804 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 uuid_metadata::UuidMetadata; +pub mod uuid_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..24dfb5aa 100644 --- a/src/core/transport_request.rs +++ b/src/core/transport_request.rs @@ -80,6 +80,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 +95,8 @@ impl TransportRequest { &self, transport: &T, deserializer: Arc, + + #[cfg(feature = "std")] guard: &DetachedClientsGuard, ) -> Result where B: for<'de> super::Deserialize<'de>, @@ -98,12 +104,36 @@ 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 channel = guard.notify_channel_rx.clone(); + guard.increase_detached_count_by(1); + + // Request configured endpoint. + select_biased! { + _ = channel.recv().fuse() => { + guard.decrease_detached_count_by(1); + Err(PubNubError::RequestCancel { details: "PubNub client instance dropped".into() }) + } + response = transport.send(self.clone()).fuse() => { + guard.decrease_detached_count_by(1); + return Self::deserialize( + response?.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } + } + } + + #[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. @@ -122,13 +152,13 @@ impl TransportRequest { D: super::Deserializer + 'static, { // Request configured endpoint. - let response = transport.send(self.clone()).await?; - + let response = transport.send(self.clone()).await; Self::deserialize( - response.clone(), + 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/uuid_metadata.rs b/src/core/uuid_metadata.rs new file mode 100644 index 00000000..e2403946 --- /dev/null +++ b/src/core/uuid_metadata.rs @@ -0,0 +1,215 @@ +//! # UuidMetadata entity module +//! +//! This module contains the [`UuidMetadata`] 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}, +}; + +/// UUID metadata entity. +/// +/// Entity as a first-class citizen provides access to the entity-specific API. +pub struct UuidMetadata { + inner: Arc>, +} + +/// UUID metadata entity reference. +/// +/// This struct contains the actual UUID metadata state. It is wrapped in an +/// Arc by [`UuidMetadata`] and uses internal mutability for its internal +/// state. +/// +/// Not intended to be used directly. Use [`UuidMetadata`] instead. +#[derive(Debug)] +pub struct UuidMetadataRef { + /// Reference on backing [`PubNubClientInstance`] client. + /// + /// Client is used to support entity-specific actions like: + /// * subscription + /// + /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(unused)] + client: Arc>, + + /// Unique uuid metadata object identifier. + /// + /// Uuid 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 UuidMetadata { + /// Creates a new instance of an uuid metadata object. + /// + /// # Arguments + /// + /// * `client` - The client instance used to access [`PubNub API`]. + /// * `id` - The identifier of the uuid metadata object. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) fn new(client: &PubNubClientInstance, id: S) -> UuidMetadata + where + S: Into, + { + Self { + inner: Arc::new(UuidMetadataRef { + 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 UuidMetadata { + type Target = UuidMetadataRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for UuidMetadata { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to the UuidMetadata are not allowed") + } +} + +impl Clone for UuidMetadata { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl PartialEq for UuidMetadata { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl From> for PubNubEntity { + fn from(value: UuidMetadata) -> Self { + PubNubEntity::UuidMetadata(value) + } +} + +impl Debug for UuidMetadata { + #[cfg(all(feature = "subscribe", feature = "std"))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "UuidMetadata {{ id: {}, subscriptions_count: {} }}", + self.id, + self.subscriptions_count() + ) + } + + #[cfg(not(all(feature = "subscribe", feature = "std")))] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "UuidMetadata {{ id: {} }}", self.id) + } +} + +#[cfg(all(feature = "subscribe", feature = "std"))] +impl Subscribable for UuidMetadata { + 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 UuidMetadata +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscription(&self, options: Option>) -> Arc> { + 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..5448e544 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(), body: if !body.is_empty() { Some(body) } else { None }, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, } } } @@ -150,6 +152,7 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request .send::(&client.transport, deserializer) .await diff --git a/src/dx/access/builders/revoke.rs b/src/dx/access/builders/revoke.rs index e63e399f..f311d2c0 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(), + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, ..Default::default() } } @@ -86,6 +89,7 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request .send::(&client.transport, deserializer) .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/presence/builders/get_presence_state.rs b/src/dx/presence/builders/get_presence_state.rs index e90d88c9..5eeef253 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,7 +122,8 @@ 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) ), @@ -130,6 +131,8 @@ impl GetStateRequest { method: TransportMethod::Get, headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } @@ -145,6 +148,7 @@ where let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request .send::(&client.transport, deserializer) .await diff --git a/src/dx/presence/builders/heartbeat.rs b/src/dx/presence/builders/heartbeat.rs index 32d96dac..dae3ca76 100644 --- a/src/dx/presence/builders/heartbeat.rs +++ b/src/dx/presence/builders/heartbeat.rs @@ -1,8 +1,7 @@ //! # 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")] @@ -18,7 +17,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 +80,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 +109,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 +159,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 +168,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,31 +202,31 @@ 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 } } @@ -239,6 +243,7 @@ where let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request .send::(&client.transport, deserializer) .await @@ -339,4 +344,45 @@ mod it_should { assert!(matches!(result, Err(PubNubError::EffectCanceled))); } + + // TODO: Make request cancelable + // #[cfg(feature = "std")] + // #[tokio::test] + // async fn be_able_to_cancel_request() { + // struct MockTransport; + // + // #[async_trait::async_trait] + // impl Transport for MockTransport { + // async fn send(&self, _req: TransportRequest) -> + // Result { + // tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // + // Simulate long request. + // + // Ok(TransportResponse::default()) + // } + // } + // + // let client = PubNubClientBuilder::with_transport(MockTransport) + // .with_keyset(crate::Keyset { + // subscribe_key: "test", + // publish_key: Some("test"), + // secret_key: None, + // }) + // .with_user_id("test") + // .build() + // .unwrap(); + // let _ = &client + // .detached_guard + // .notify_channel_tx + // .send_blocking(1) + // .unwrap(); + // + // let result = client + // .heartbeat() + // .channels(vec!["test".into()]) + // .execute() + // .await; + // + // assert!(matches!(result, Err(PubNubError::RequestCancel { .. }))); + // } } diff --git a/src/dx/presence/builders/here_now.rs b/src/dx/presence/builders/here_now.rs index cc6b4a7c..21557bd9 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 @@ -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,13 +129,16 @@ 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(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } diff --git a/src/dx/presence/builders/leave.rs b/src/dx/presence/builders/leave.rs index 3c46140d..0b0297e0 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,13 +117,16 @@ 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(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } @@ -139,6 +142,7 @@ where let transport_request = request.transport_request()?; let client = request.pubnub_client.clone(); let deserializer = client.deserializer.clone(); + transport_request .send::(&client.transport, deserializer) .await diff --git a/src/dx/presence/builders/set_presence_state.rs b/src/dx/presence/builders/set_presence_state.rs index bf81041b..be6e1b45 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,14 +170,17 @@ 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, }) } } @@ -173,9 +193,16 @@ where /// 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.clone()(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) .await diff --git a/src/dx/presence/builders/where_now.rs b/src/dx/presence/builders/where_now.rs index 1c2201df..7fcbdf5b 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,13 +88,16 @@ 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(), body: None, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, }) } } 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..2227c712 100644 --- a/src/dx/presence/event_engine/effects/heartbeat.rs +++ b/src/dx/presence/event_engine/effects/heartbeat.rs @@ -4,8 +4,11 @@ //! 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, @@ -14,24 +17,18 @@ use crate::{ }, }; -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 +83,7 @@ mod it_should { 0, None, "id", - &Some(RequestRetryPolicy::None), + &RequestRetryConfiguration::None, &mocked_heartbeat_function, ) .await; @@ -127,7 +124,11 @@ mod it_should { })), }), "id", - &Some(RequestRetryPolicy::None), + &RequestRetryConfiguration::Linear { + max_retry: 5, + delay: 2, + excluded_endpoints: None, + }, &mocked_heartbeat_function, ) .await; @@ -168,10 +169,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/mod.rs b/src/dx/presence/event_engine/effects/mod.rs index eeb60f79..8e4d04ad 100644 --- a/src/dx/presence/event_engine/effects/mod.rs +++ b/src/dx/presence/event_engine/effects/mod.rs @@ -6,7 +6,7 @@ use futures::future::BoxFuture; use crate::{ core::{ event_engine::{Effect, EffectInvocation}, - PubNubError, RequestRetryPolicy, + PubNubError, RequestRetryConfiguration, }, lib::{ alloc::{string::String, sync::Arc, vec::Vec}, @@ -57,6 +57,9 @@ pub(in crate::dx::presence) type LeaveEffectExecutor = dyn Fn(PresenceParameters 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 +74,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 +92,7 @@ pub(crate) enum PresenceEffect { reason: PubNubError, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Executor function. /// @@ -101,6 +107,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 +124,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 +150,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 +181,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 +202,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 +230,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 +267,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 +285,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..0b04961d 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. @@ -44,12 +52,18 @@ pub(crate) enum PresenceEvent { /// /// 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 +72,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,20 +79,17 @@ 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. @@ -95,9 +105,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..27322c4a 100644 --- a/src/dx/presence/event_engine/invocation.rs +++ b/src/dx/presence/event_engine/invocation.rs @@ -61,6 +61,9 @@ pub(crate) enum PresenceEffectInvocation { /// Cancel delay effect invocation. CancelWait, + + /// Terminate Presence Event Engine processing loop. + TerminateEventEngine, } impl EffectInvocation for PresenceEffectInvocation { @@ -75,14 +78,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 +96,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 +111,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/state.rs b/src/dx/presence/event_engine/state.rs index 6af5ecc3..afb4158f 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, @@ -325,6 +346,7 @@ impl State for PresenceState { } fn exit(&self) -> Option> { + log::debug!("~~~~~~~~~~ EXIT: {self:?}"); match self { PresenceState::Cooldown { .. } => Some(vec![CancelWait]), PresenceState::Reconnecting { .. } => Some(vec![CancelDelayedHeartbeat]), @@ -338,14 +360,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 +387,25 @@ 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(); + + log::debug!("~~~~~~>> COLLECTED INVOCATIONS: {invocations:?}"); + + Transition { invocations, state } } } @@ -383,7 +415,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 +446,7 @@ mod it_should { delayed_heartbeat_call, leave_call, wait_call, - RequestRetryPolicy::None, + RequestRetryConfiguration::None, tx, ), start_state, @@ -425,6 +457,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 +477,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 +511,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -487,6 +531,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr1".to_string()]), }, @@ -506,6 +551,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 +617,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 +631,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -602,6 +651,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -655,6 +705,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -674,6 +725,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string()]), channel_groups: None, }, @@ -693,6 +745,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 +791,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 +805,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -769,6 +825,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -846,6 +903,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 +925,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 +947,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 +1020,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 +1036,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 +1060,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 +1118,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), }, @@ -1073,6 +1138,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: Some(vec!["ch1".to_string()]), channel_groups: None, }, @@ -1092,6 +1158,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 +1188,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 +1202,7 @@ mod it_should { ) }, PresenceEvent::Joined { + heartbeat_interval: 10, channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), }, @@ -1152,6 +1222,7 @@ mod it_should { ) }, PresenceEvent::Left { + suppress_leave_events: false, channels: None, channel_groups: Some(vec!["gr3".to_string()]), }, @@ -1204,6 +1275,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 +1296,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 +1317,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 +1366,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 +1381,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 +1403,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/mod.rs b/src/dx/presence/mod.rs index 33758b99..e25080fe 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,6 +26,7 @@ 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::{ @@ -39,15 +34,28 @@ pub(crate) use event_engine::{ }; #[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,59 +458,70 @@ 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().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().read().as_ref() { presence.announce_left(channels, channel_groups); - } + }; }; } - /// Complete presence configuration. + /// Presence manager which maintains Presence EE. + /// + /// # Returns /// - /// Presence configuration used only with presence event engine. - #[cfg(feature = "std")] - pub(crate) fn configure_presence(&self) -> Arc>> { + /// Returns an [`PresenceManager`] which represents the manager. + #[cfg(all(feature = "presence", feature = "std"))] + pub(crate) fn presence_manager(&self) -> Arc>> { { let mut slot = self.presence.write(); if slot.is_none() { - *slot = Some(PresenceManager::new(self.presence_event_engine(), None)); - } + *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 +531,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 +541,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 +552,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 +579,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 +594,7 @@ where wait_cancel_rx.clone(), ) }), - request_retry_policy, + request_retry, cancel_tx, ), PresenceState::Inactive, @@ -427,16 +603,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 +636,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 +658,6 @@ where } /// Heartbeat idle. - #[cfg(feature = "std")] pub(crate) fn wait_call( effect_id: &str, delay: Arc, @@ -497,25 +679,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 +692,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 +707,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 +794,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 +828,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..f29cb552 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,29 @@ 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. + #[allow(dead_code)] + pub fn terminate(&self) { + self.event_engine + .stop(PresenceEffectInvocation::TerminateEventEngine); + } } impl Deref for PresenceManager { @@ -64,65 +80,52 @@ 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` 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 +133,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..b2b2385c 100644 --- a/src/dx/publish/mod.rs +++ b/src/dx/publish/mod.rs @@ -171,6 +171,7 @@ where self.prepare_context_with_request()? .map(|some| async move { let deserializer = some.client.deserializer.clone(); + some.data .send::(&some.client.transport, deserializer) .await @@ -292,7 +293,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 +312,8 @@ where ), method: TransportMethod::Get, query_parameters: query_params, + #[cfg(feature = "std")] + timeout: config.transport.request_timeout, ..Default::default() }) } @@ -400,11 +405,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 +486,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 +510,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..bbffd503 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -7,14 +7,28 @@ //! [`PubNub API`]: https://www.pubnub.com/docs //! [`pubnub`]: ../index.html -#[cfg(all(feature = "presence", feature = "std"))] -use crate::presence::PresenceManager; +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::{ - core::runtime::RuntimeSupport, providers::futures_tokio::RuntimeTokio, - subscribe::SubscriptionManager, + core::{runtime::RuntimeSupport, Deserializer}, + subscribe::{EventDispatcher, SubscriptionCursor, SubscriptionManager}, }; +#[cfg(feature = "presence")] +use crate::lib::alloc::vec::Vec; +#[cfg(all(feature = "presence", feature = "std"))] +use crate::presence::PresenceManager; + #[cfg(not(feature = "serde"))] use crate::core::Deserializer; #[cfg(feature = "serde")] @@ -24,23 +38,24 @@ 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::RequestRetryConfiguration; +use crate::core::{Deserialize, Transport}; use crate::{ - core::{CryptoProvider, PubNubError}, + core::{CryptoProvider, PubNubEntity, PubNubError}, lib::{ alloc::{ + borrow::ToOwned, + format, string::{String, ToString}, sync::Arc, }, + collections::HashMap, core::ops::{Deref, DerefMut}, }, transport::middleware::{PubNubMiddleware, SignatureKeySet}, + Channel, ChannelGroup, ChannelMetadata, UuidMetadata, }; -use derive_builder::Builder; -use log::info; -use spin::{Mutex, RwLock}; -use uuid::Uuid; /// PubNub client /// @@ -123,7 +138,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 +237,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 +266,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 +330,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"))] #[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.create_channel("my_channel"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_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.create_channels(&["my_channel_1", "my_channel_2"]); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_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.create_channel_group("my_group"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_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.create_channel_groups(&["my_group_1", "my_group_2"]); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_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.create_channel_metadata("channel_meta"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_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.create_channels_metadata( + /// &["channel_meta_1", "channel_meta_2"] + /// ); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_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 uuid metadata object with the specified identifier. + /// + /// # Arguments + /// + /// * `id` - The identifier of the uuid metadata object as a string. + /// + /// # Returns + /// + /// Returns a `UuidMetadata` 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 uuid_metadata = client.create_uuid_metadata("uuid_meta"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_uuid_metadata(&self, id: S) -> UuidMetadata + where + S: Into, + { + let mut entities_slot = self.entities.write(); + let id = id.into(); + let entity = entities_slot + .entry(format!("{}_uidm", &id)) + .or_insert(UuidMetadata::new(self, id).into()); + + match entity { + PubNubEntity::UuidMetadata(uuid_metadata) => uuid_metadata.clone(), + _ => panic!("Unexpected entry type for UuidMetadata"), + } + } + + /// Creates a list of uuid metadata objects with the specified identifier. + /// + /// # Arguments + /// + /// * `id` - A list of identifiers for the uuid metadata objects as a + /// string. + /// + /// # Returns + /// + /// Returns a list of `UuidMetadata` 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 uuids_metadata = client.create_uuids_metadata(&["uuid_meta_1", "uuid_meta_2"]); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub fn create_uuids_metadata(&self, ids: &[S]) -> Vec> + where + S: Into + Clone, + { + let mut uuids_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(UuidMetadata::new(self, id).into()); + + match entity { + PubNubEntity::UuidMetadata(uuid_metadata) => { + uuids_metadata.push(uuid_metadata.clone()) + } + _ => panic!("Unexpected entry type for UuidMetadata"), + } + } + + uuids_metadata + } + /// Update currently used authentication token. /// /// # Examples @@ -381,6 +877,24 @@ impl PubNubClientInstance { } } +impl PubNubClientInstance +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub fn terminate(&self) { + #[cfg(all(feature = "subscribe", feature = "std"))] + if let Some(manager) = self.subscription_manager().read().as_ref() { + manager.terminate(); + } + #[cfg(all(feature = "presence", feature = "std"))] + if let Some(manager) = self.presence_manager().read().as_ref() { + manager.terminate(); + } + } +} + impl PubNubClientConfigBuilder { /// Set client authentication key. /// @@ -409,8 +923,12 @@ 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); + configuration.presence.heartbeat_value = value; + + #[cfg(feature = "std")] + { + configuration.presence.heartbeat_interval = Some(value / 2 - 1); + } } self } @@ -423,26 +941,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 +1003,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 +1034,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 +1046,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 +1063,120 @@ impl PubNubClientConfigBuilder { config: pre_build.config, cryptor: pre_build.cryptor.clone(), + #[cfg(feature = "subscribe")] + filter_expression: pre_build.filter_expression, + + #[cfg(feature = "presence")] + state: Arc::new(RwLock::new(HashMap::new())), + #[cfg(all(any(feature = "subscribe", feature = "presence"), 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 +1193,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 +1253,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 +1303,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 +1353,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 +1406,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 +1526,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 +1543,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 +1569,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 +1631,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> { @@ -1229,13 +1896,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 +1987,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..0a4a5dfc 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,15 +227,17 @@ 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() - } + }) } } @@ -186,9 +249,10 @@ where /// 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) .await @@ -241,7 +305,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..ebab23a2 --- /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 signal_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.signal_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..4d68ee4e 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::{ @@ -29,7 +30,7 @@ pub(crate) struct SubscribeEffectHandler { emit_messages: Arc, /// Retry policy. - retry_policy: RequestRetryPolicy, + retry_policy: RequestRetryConfiguration, /// Cancellation channel. cancellation_channel: Sender, @@ -41,7 +42,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 +60,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 +73,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 +84,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 +97,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 +107,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..15d5c703 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 { @@ -38,7 +40,7 @@ pub(super) async fn execute( }, |subscribe_result| { vec![SubscribeEvent::HandshakeSuccess { - cursor: subscribe_result.cursor, + cursor: cursor.clone().unwrap_or(subscribe_result.cursor), }] }, ) @@ -71,10 +73,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 +103,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..eadbe0d6 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 }]; } @@ -49,7 +53,7 @@ pub(super) async fn execute( }, |subscribe_result| { vec![SubscribeEvent::HandshakeReconnectSuccess { - cursor: subscribe_result.cursor, + cursor: cursor.clone().unwrap_or(subscribe_result.cursor), }] }, ) @@ -59,6 +63,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 +78,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 +96,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 +127,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..98c17f53 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}, }; @@ -28,7 +28,7 @@ pub(crate) enum SubscribeEvent { SubscriptionRestored { channels: Option>, channel_groups: Option>, - cursor: SubscribeCursor, + cursor: SubscriptionCursor, }, /// Handshake completed successfully. @@ -37,7 +37,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 +54,7 @@ pub(crate) enum SubscribeEvent { /// loop. /// /// [`PubNub`]: https://www.pubnub.com/ - HandshakeReconnectSuccess { cursor: SubscribeCursor }, + HandshakeReconnectSuccess { cursor: SubscriptionCursor }, /// Handshake reconnect completed with error. /// @@ -78,7 +78,7 @@ pub(crate) enum SubscribeEvent { /// /// [`PubNub`]: https://www.pubnub.com/ ReceiveSuccess { - cursor: SubscribeCursor, + cursor: SubscriptionCursor, messages: Vec, }, @@ -98,7 +98,7 @@ pub(crate) enum SubscribeEvent { /// /// [`PubNub`]: https://www.pubnub.com/ ReceiveReconnectSuccess { - cursor: SubscribeCursor, + cursor: SubscriptionCursor, messages: Vec, }, @@ -130,7 +130,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 +155,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..10c143f9 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,7 +14,7 @@ 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 { @@ -24,13 +24,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 +42,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 +68,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 +86,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 +107,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 +123,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 +146,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 +166,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 +183,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..3de1e248 100644 --- a/src/dx/subscribe/event_engine/mod.rs +++ b/src/dx/subscribe/event_engine/mod.rs @@ -28,7 +28,7 @@ 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 = diff --git a/src/dx/subscribe/event_engine/state.rs b/src/dx/subscribe/event_engine/state.rs index f097c383..3a57ccd6 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,7 +20,7 @@ use crate::{ SubscribeEvent, }, result::Update, - SubscribeCursor, SubscribeStatus, + ConnectionStatus, SubscriptionCursor, }, lib::alloc::{string::String, vec, vec::Vec}, }; @@ -44,13 +44,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 +61,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 +84,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 +102,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 +116,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 +125,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 +142,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 +165,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 +182,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 +204,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 +260,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 +316,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 +344,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 +372,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 +385,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 +407,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 +426,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 +448,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 +476,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 +489,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 +511,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 +532,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 +556,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 +593,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 +686,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 +722,7 @@ mod should { use super::*; use crate::{ - core::{event_engine::EventEngine, RequestRetryPolicy}, + core::{event_engine::EventEngine, RequestRetryConfiguration}, dx::subscribe::{ event_engine::{ effects::{ @@ -701,7 +757,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 +766,7 @@ mod should { call, emit_status, emit_message, - RequestRetryPolicy::None, + RequestRetryConfiguration::None, tx, ), start_state, @@ -725,7 +781,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 +794,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 +830,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 +841,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 +851,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 +882,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 +894,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 +916,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 +924,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 +934,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 +1001,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 +1046,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 +1070,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 +1082,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 +1094,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 +1106,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 +1118,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 +1131,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 +1141,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 +1154,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 +1174,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 +1184,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 +1216,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 +1227,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 +1239,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 +1259,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 +1305,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 +1351,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 +1381,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 +1393,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 +1403,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 +1415,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 +1444,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 +1515,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 +1572,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 +1601,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 +1652,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 +1810,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 +1896,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 +1960,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 +1972,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 +1984,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 +2020,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 +2086,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 +2149,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 +2161,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 +2283,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..5436313c 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -10,28 +10,53 @@ 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::{ + types::SubscriptionParams, 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,18 +66,103 @@ 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 { + /// Stream used to notify connection state change events. + pub fn status_stream(&self) -> DataStream { + self.event_dispatcher.status_stream() + } + + /// Handle connection status change. + /// + /// # Arguments + /// + /// * `status` - Current connection status. + pub(crate) fn handle_status(&self, status: ConnectionStatus) { + self.event_dispatcher.handle_status(status) + } + + /// 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<(), 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(()) + /// # } + /// ``` + /// + /// # Returns + /// + /// 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), + } + } +} #[cfg(feature = "std")] impl PubNubClientInstance @@ -60,54 +170,75 @@ where T: Transport + Send + 'static, D: Deserializer + 'static, { - /// Create subscription listener. + /// Creates multiplexed subscriptions. /// - /// Listeners configure [`PubNubClient`] to receive real-time updates for - /// specified list of channels and groups. + /// # Arguments /// - /// ```no_run // Starts listening for real-time updates + /// * `entities` - A `Vec` of known subscribable entities. + /// * `options` - Optional subscription options. + /// + /// # Returns + /// + /// The created [`SubscriptionSet`] object. + /// + /// # Example + /// + /// ```rust,no_run /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// use pubnub::{PubNubClient, PubNubClientBuilder, Keyset, subscribe::EventEmitter}; /// /// # #[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> { + /// use pubnub::subscribe::EventSubscriber; + /// 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( + /// Some(&["my_channel_1", "my_channel_2", "my_channel_3"]), + /// None, + /// None, + /// ); + /// // Message stream for handling real-time `Message` events. + /// let stream = subscription.messages_stream(); + /// # Ok(()) /// # } /// ``` - /// - /// For more examples see our [examples directory](https://github.com/pubnub/rust/tree/master/examples). - /// - /// Instance of [`SubscriptionBuilder`] returned. - /// [`PubNubClient`]: crate::PubNubClient - pub fn subscribe(&self) -> SubscriptionBuilder { - self.configure_subscribe(); - - SubscriptionBuilder { - subscription: Some(self.subscription.clone()), - ..Default::default() + pub fn subscription( + &self, + channels: Option<&[N]>, + channel_groups: Option<&[N]>, + options: Option>, + ) -> Arc> + where + N: Into + Clone, + { + let mut entities: Vec> = vec![]; + if let Some(channel_names) = channels { + entities.extend( + channel_names + .iter() + .cloned() + .map(|name| self.create_channel(name).into()) + .collect::>>(), + ); + } + if let Some(channel_group_names) = channel_groups { + entities.extend( + channel_group_names + .iter() + .cloned() + .map(|name| self.create_channel_group(name).into()) + .collect::>>(), + ); } + + SubscriptionSet::new(entities, options) } /// Stop receiving real-time updates. @@ -117,26 +248,23 @@ 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}; /// # - /// # 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(Some(&["channel"]), None, None); + /// # let stream = // DataStream + /// # subscription.messages_stream(); /// client.disconnect(); /// # Ok(()) /// # } @@ -144,11 +272,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().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 +291,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 +303,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().read().as_ref() { presence.disconnect(); } } @@ -183,41 +316,42 @@ 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}; /// # - /// # 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(Some(&["channel"]), None, 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().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 +360,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,27 +372,47 @@ 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().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().write().as_mut() { + manager.unregister_all() + } + } + } + + /// Subscription manager which maintains Subscription EE. + /// + /// # Returns + /// + /// Returns an [`SubscriptionManager`] which represents the manager. + #[cfg(all(feature = "subscribe", feature = "std"))] + pub(crate) fn subscription_manager(&self) -> Arc>>> { { // Initialize subscription module when it will be first required. let mut slot = self.subscription.write(); if slot.is_none() { #[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(), + #[cfg(feature = "presence")] Arc::new(move |channels, groups| { Self::subscribe_heartbeat_call(heartbeat_self.clone(), channels, groups); }), + #[cfg(feature = "presence")] Arc::new(move |channels, groups| { Self::subscribe_leave_call(leave_self.clone(), channels, groups); }), @@ -274,26 +428,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 +461,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, @@ -333,6 +492,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,28 +515,17 @@ 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); - } + let channels = Self::presence_filtered_entries(channels); + let channel_groups = Self::presence_filtered_entries(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); - } + if let Some(presence) = client.presence_manager().read().as_ref() { + presence.announce_join(channels, channel_groups); } } @@ -378,6 +535,7 @@ where /// presence event engine state: /// * can operate - call `leave` announcement /// * can't operate (heartbeat interval not set) - make direct `leave` call. + #[cfg(feature = "presence")] fn subscribe_leave_call( client: Self, channels: Option>, @@ -385,31 +543,22 @@ where ) { #[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); - } + let channels = Self::presence_filtered_entries(channels); + let channel_groups = Self::presence_filtered_entries(channel_groups); - client.runtime.spawn(async { - let _ = request.execute().await; - }) - } else if let Some(presence) = client.presence.clone().read().as_ref() { + if let Some(presence) = client.presence_manager().read().as_ref() { presence.announce_left(channels, channel_groups); } } } - 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().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 +568,21 @@ where messages }; - if let Some(manager) = client.subscription.read().as_ref() { - manager.notify_new_messages(messages) + if let Some(manager) = client.subscription_manager().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 +627,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 +663,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 signal_stream(&self) -> DataStream { + self.event_dispatcher.signal_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 +725,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 +834,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 +853,34 @@ mod should { } #[tokio::test] - async fn create_builder() { - let _ = client().subscribe(); + async fn create_subscription_set() { + let _ = client().subscription(Some(&["channel_a"]), Some(&["group_a"]), 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( + Some(&["my-channel"]), + Some(&["group_a"]), + 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 +889,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..a3aecfe8 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. /// @@ -234,7 +234,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 +304,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. @@ -550,11 +550,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 +583,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..64307981 --- /dev/null +++ b/src/dx/subscribe/subscription.rs @@ -0,0 +1,751 @@ +//! # 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::{Deref, DerefMut}, + }, + }, + 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.create_channel("my_channel"); +/// 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.create_channels(&["my_channel_1", "my_channel_2"]); +/// let subscription = channels[0].subscription(None).add(channels[1].subscription(None)); +/// # Ok(()) +/// # } +/// ``` +pub struct Subscription { + /// Unique event handler instance identifier. + /// + /// [`Subscription`] can be cloned, but the internal state is always bound + /// to the same reference of [`SubscriptionRef`] with the same `id`. + instance_id: String, + + /// Subscription reference. + inner: Arc>, + + /// Real-time event dispatcher. + event_dispatcher: EventDispatcher, +} + +/// 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. +#[derive(Debug)] +pub struct SubscriptionRef { + /// 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 [`Subscription`] 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 `Subscription2` for the given `entity` and `options`. + pub(crate) fn new( + client: Weak>, + entity: PubNubEntity, + options: Option>, + ) -> Arc { + let subscription_ref = SubscriptionRef::new(client, entity, options); + let subscription_id = Uuid::new_v4().to_string(); + let subscription = Arc::new(Self { + instance_id: subscription_id.clone(), + inner: Arc::new(subscription_ref), + event_dispatcher: Default::default(), + }); + subscription.store_clone(subscription_id, Arc::downgrade(&subscription)); + subscription + } + + /// 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() + } + + /// Clones the [`Subscription`] and returns a new `Arc` reference to it. + /// + /// # Returns + /// + /// A new `Arc` reference to a cloned [`Subscription` ] instance. + /// + /// # Panics + /// + /// This method will panic if [`Subscription`] clone could not be found in + /// the reference counter storage or if there are no strong references + /// to the [`Subscription`] instance. + pub fn clone_arc(&self) -> Arc { + self.get_clone_by_id(&self.instance_id) + .expect("Subscription clone should be stored with SubscriptionRef") + .upgrade() + .expect("At least one strong reference should exist for Subscription") + .clone() + } + + /// 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.create_channel("my_channel"); + /// 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) -> Arc { + let instance_id = Uuid::new_v4().to_string(); + let instance = Arc::new(Self { + instance_id: instance_id.clone(), + inner: Arc::clone(&self.inner), + event_dispatcher: Default::default(), + }); + self.store_clone(instance_id, Arc::downgrade(&instance)); + instance + } + + /// Adds two [`Subscription`] and produce [`SubscriptionSet`]. + /// + /// # Arguments + /// + /// * `rhs` - The subscription to be added. + /// + /// # Returns + /// + /// [`SubscriptionSet`] with added subscriptions in the set. + /// + /// # Panics + /// + /// This function will panic if the current subscription set does not have + /// at least one clone of [`Subscription`] with strong reference to the + /// original. + pub fn add(&self, rhs: Arc) -> Arc> { + let options = self.options.clone(); + let lhs_clones = self.clones.read(); + let (_, lhs) = lhs_clones + .iter() + .next() + .expect("At least one clone of Subscription should exist."); + let lhs = lhs + .upgrade() + .clone() + .expect("At least one strong reference should exist for Subscription"); + SubscriptionSet::new_with_subscriptions(vec![lhs, rhs], options) + } + + /// 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 Subscription +where + T: Send + Sync, + D: Send + Sync, +{ + type Target = SubscriptionRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Subscription +where + T: Send + Sync, + D: 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 PartialEq for Subscription +where + T: Send + Sync, + D: 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 EventSubscriber for Subscription +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().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().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 Subscription +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 Subscription +where + T: Send + Sync, + D: Send + Sync, +{ + fn messages_stream(&self) -> DataStream { + self.event_dispatcher.messages_stream() + } + + fn signal_stream(&self) -> DataStream { + self.event_dispatcher.signal_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 SubscriptionRef +where + T: Send + Sync, + D: Send + Sync, +{ + fn new( + client: Weak>, + entity: PubNubEntity, + options: Option>, + ) -> SubscriptionRef { + 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 [`Subscription`] instance with a given instance ID. + /// + /// # Arguments + /// + /// * `instance_id` - The instance ID to associate with the clone. + /// * `instance` - The weak reference to the subscription 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)); + } + + /// Retrieves a cloned instance of a [`Subscription`] by its `instance_id`. + /// + /// # Arguments + /// + /// * `instance_id` - A reference to the unique identifier of the instance. + /// + /// # Returns + /// + /// An `Option` containing a weak reference to the cloned [`Subscription`] + /// instance if found, or `None` if no instance with the specified + /// `instance_id` exists. + fn get_clone_by_id(&self, instance_id: &String) -> Option>> { + self.clones.read().get(instance_id).cloned() + } +} + +#[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 subscription = Subscription::new( + Arc::downgrade(&client), + PubNubEntity::Channel(channel), + None, + ); + + 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..aebad384 100644 --- a/src/dx/subscribe/subscription_manager.rs +++ b/src/dx/subscribe/subscription_manager.rs @@ -3,27 +3,36 @@ //! This module contains manager which is responsible for tracking and updating //! active subscription streams. +use spin::RwLock; + +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; -/// 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 +41,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 +86,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: Send + Sync, + D: Send + Sync, +{ + 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 +171,63 @@ 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<&[Arc>]>, + ) { + let Some(upgraded_event_handler) = event_handler.upgrade().clone() else { + return; + }; + + if !self + .event_handlers + .read() + .contains_key(upgraded_event_handler.id()) + { + return; + } + + // Handle subscriptions' set subscriptions subset which has been removed from + // it. + let removed = removed.map(|removed| { + removed + .iter() + .filter(|subscription| subscription.entity.subscriptions_count().gt(&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.subscribers.swap_remove(position); + self.event_handlers.write().remove(event_handler_id); } - self.change_subscription(Some(&subscription.input)); + 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.event_handlers.write().clear(); + } - self.subscribers - .iter_mut() - .for_each(|subscription| subscription.invalidate()); - self.subscribers.clear(); self.change_subscription(Some(&inputs)); } @@ -157,90 +235,143 @@ 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()) }); - // } - // } + /// Terminate subscription manager. + /// + /// Gracefully terminate all ongoing tasks including detached event engine + /// loop. + #[allow(dead_code)] + 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())); + + if let Some(removed) = removed { + if !removed.is_empty { + self.leave_call.as_ref()(removed.channels(), removed.channel_groups()); + } + } + } 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()); + } 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 +379,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 +403,84 @@ mod should { #[tokio::test] async fn register_subscription() { + let client = client(); let mut manager = SubscriptionManager::new( event_engine(), + #[cfg(feature = "presence")] Arc::new(|channels, _| { assert!(channels.is_some()); assert_eq!(channels.unwrap().len(), 1); }), + #[cfg(feature = "presence")] Arc::new(|_, _| {}), ); - let dummy_manager = - SubscriptionManager::new(event_engine(), Arc::new(|_, _| {}), Arc::new(|_, _| {})); + let channel = client.create_channel("test"); + let subscription = channel.subscription(None); + let weak_subscription = &Arc::downgrade(&subscription); + let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); - let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), - ..Default::default() - } - .channels(["test".into()]) - .execute() - .unwrap(); - - 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(), + #[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 channel = client.create_channel("test"); + let subscription = channel.subscription(None); + let weak_subscription = &Arc::downgrade(&subscription); + let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); - let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), - ..Default::default() - } - .channels(["test".into()]) - .execute() - .unwrap(); + manager.register(&weak_handler, None); + manager.unregister(&weak_handler); - manager.register(subscription.clone()); - manager.unregister(subscription); - - 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.create_channel("test"); + let subscription = channel.subscription(None); + let weak_subscription = Arc::downgrade(&subscription); + 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..743f678f --- /dev/null +++ b/src/dx/subscribe/subscription_set.rs @@ -0,0 +1,953 @@ +//! # 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 std::collections::HashMap; +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, + }, + core::{ + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, + 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> { +/// 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(Some(&["my_channel_1", "my_channel_2"]), Some(&["my_group"]), 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.create_channels(&["my_channel_1", "my_channel_2"]); +/// let subscription = channels[0].subscription(None).add(channels[1].subscription(None)); +/// # Ok(()) +/// # } +/// ``` +pub struct SubscriptionSet< + 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 [`SubscriptionSetRef`] with the same + /// `id`. + pub(super) instance_id: String, + + /// Subscriptions set reference. + inner: Arc>, + + /// Real-time event dispatcher. + event_dispatcher: EventDispatcher, +} + +/// 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. +#[derive(Debug)] +pub struct SubscriptionSetRef< + 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>, + ) -> Arc { + let subscriptions = entities + .into_iter() + .map(|entity| entity.subscription(options.clone())) + .collect::>>>(); + + Self::new_with_subscriptions(subscriptions, options) + } + + /// Create subscription set 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>, + ) -> Arc { + let subscription = subscriptions + .first() + .expect("At least one subscription expected."); + let subscription_set_ref = + SubscriptionSetRef::new(subscription.client(), subscriptions, options); + let subscription_set_id = Uuid::new_v4().to_string(); + let subscription_set = Arc::new(Self { + instance_id: subscription_set_id.clone(), + inner: Arc::new(subscription_set_ref), + event_dispatcher: Default::default(), + }); + subscription_set.store_clone(subscription_set_id, Arc::downgrade(&subscription_set)); + subscription_set + } + + /// Creates a clone of the [`SubscriptionSet`] and returns it as an `Arc`. + /// + /// # Returns + /// + /// A new `Arc` reference to a cloned [`SubscriptionSet` ] instance. + /// + /// # Panics + /// + /// This method will panic if [`SubscriptionSet`] clone could not be found + /// in the reference counter storage or if there are no strong + /// references to the [`SubscriptionSet`] instance. + pub fn clone_arc(&self) -> Arc { + self.get_clone_by_id(&self.instance_id) + .expect("SubscriptionSet clone should be stored with SubscriptionSetRef") + .upgrade() + .expect("At least one strong reference should exist for SubscriptionSet") + .clone() + } + + /// 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> { + /// 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(Some(&["my_channel_1", "my_channel_2"]), Some(&["my_group"]), 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(), + inner: Arc::clone(&self.inner), + event_dispatcher: Default::default(), + }); + self.store_clone(instance_id, Arc::downgrade(&instance)); + instance + } + + /// Adds the [`Subscription`] from another [`SubscriptionSet`] to the + /// current [`SubscriptionSet`]. + /// + /// # Arguments + /// + /// * `rhs` - Another instance of [`SubscriptionSet`], whose subscriptions + /// will be added. + pub fn add_assign(&self, rhs: Arc) { + self.add_subscriptions(rhs.subscriptions.read().clone()); + } + + /// Subtracts the [`Subscription`] of the given [`SubscriptionSet`] from the + /// current [`SubscriptionSet`]. + /// + /// # Arguments + /// + /// * `rhs` - Another instance of [`SubscriptionSet`], whose subscriptions + /// should be subtracted. + pub fn sub_assign(&self, rhs: Arc) { + self.sub_subscriptions(rhs.subscriptions.read().clone()); + } + + /// Add more managed subscriptions. + /// + /// After the [`Subscription`] list is added, [`SubscriptionSet`] listeners + /// will start notifying them about real-time events from newly added + /// subscriptions. + /// + /// # Arguments + /// + /// * `subscriptions` - A vector of [`Subscription`] items to be added. + /// + /// # Example + /// + /// ```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.create_channels(&["my_channel_3", "my_channel_4"]); + /// let mut subscription = client.subscription(Some(&["my_channel_1", "my_channel_2"]), None, None); + /// subscription.add_subscriptions(vec![channels[0].subscription(None), channels[1].subscription(None)]); + /// # Ok(()) + /// # } + /// ``` + pub fn add_subscriptions(&self, subscriptions: Vec>>) { + let unique_subscriptions = + SubscriptionSet::unique_subscriptions_from_list(Some(self), subscriptions); + let mut subscription_input = self.subscription_input.write(); + let mut subscriptions_slot = self.subscriptions.write(); + *subscription_input += Self::subscription_input_from_list(&unique_subscriptions, true); + subscriptions_slot.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; + }; + + // let manager = client.subscription_manager(); + if let Some(manager) = client.subscription_manager().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); + } + }; + } + + /// Remove managed subscriptions. + /// + /// After the [`Subscription`] list is removed, [`SubscriptionSet`] + /// listeners will stop receiving real-time updates from removed + /// subscriptions. + /// + /// # Arguments + /// + /// * `subscriptions` - A vector of [`Subscription`] items to be removed. + /// + /// # Example + /// + /// ```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.create_channels(&["my_channel_2", "my_channel_3"]); + /// let mut subscription = client.subscription(Some(&["my_channel_1", "my_channel_2", "my_channel_3", "my_channel_4"]), None, None); + /// subscription.sub_subscriptions(vec![channels[0].subscription(None), channels[1].subscription(None)]); + /// # Ok(()) + /// # } + /// ``` + pub fn sub_subscriptions(&self, subscriptions: Vec>>) { + let mut subscription_input = self.subscription_input.write(); + let mut subscriptions_slot = self.subscriptions.write(); + let removed: Vec>> = + Self::unique_subscriptions_from_list(None, subscriptions) + .into_iter() + .filter(|subscription| subscriptions_slot.contains(subscription)) + .collect(); + subscriptions_slot.retain(|subscription| !removed.contains(subscription)); + *subscription_input -= Self::subscription_input_from_list(&removed, true); + + // Check whether subscription change required or not. + if !self.is_subscribed() || removed.is_empty() { + return; + } + + let Some(client) = self.client().upgrade().clone() else { + return; + }; + + if let Some(manager) = client.subscription_manager().write().as_mut() { + // Mark entities as "not in-use" by subscription. + removed.iter().for_each(|subscription| { + subscription.entity.decrease_subscriptions_count(); + }); + + // 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)); + } + }; + } + + /// 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::>() + } + + /// 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: &[Arc>], + 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 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 EventSubscriber for SubscriptionSet +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn subscribe(&self, cursor: Option) { + log::debug!("~~~~~~~>>>> 3"); + let mut is_subscribed = self.is_subscribed.write(); + log::debug!("~~~~~~~>>>> 4"); + if *is_subscribed { + log::debug!("~~~~~~~>>>> 5"); + return; + } + log::debug!("~~~~~~~>>>> 6"); + *is_subscribed = true; + + if cursor.is_some() { + log::debug!("~~~~~~~>>>> 7"); + let mut cursor_slot = self.cursor.write(); + if let Some(current_cursor) = cursor_slot.as_ref() { + log::debug!("~~~~~~~>>>> 8"); + let catchup_cursor = cursor.clone().unwrap_or_default(); + catchup_cursor + .gt(current_cursor) + .then(|| *cursor_slot = Some(catchup_cursor)); + } else { + log::debug!("~~~~~~~>>>> 9"); + *cursor_slot = cursor.clone(); + } + log::debug!("~~~~~~~>>>> 10"); + } + log::debug!("~~~~~~~>>>> 11"); + + let Some(client) = self.client().upgrade().clone() else { + log::debug!("~~~~~~~>>>> 12"); + return; + }; + + log::debug!("~~~~~~~>>>> 13"); + let manager = client.subscription_manager(); + { + log::debug!("~~~~~~~>>>> 14: {:?}", manager.read().is_some()); + } + if let Some(manager) = manager.write().as_mut() { + log::debug!("~~~~~~~>>>> 15"); + // Mark entities as "in-use" by subscription. + self.subscriptions.read().iter().for_each(|subscription| { + log::debug!("~~~~~~~>>>> 16: {:?}", subscription.entity); + subscription.entity.increase_subscriptions_count(); + }); + log::debug!("~~~~~~~>>>> 17"); + + if let Some((_, handler)) = self.clones.read().iter().next() { + log::debug!("~~~~~~~>>>> 18"); + let handler: Weak + Send + Sync> = handler.clone(); + log::debug!("~~~~~~~>>>> 19: {handler:?}"); + manager.register(&handler, cursor); + log::debug!("~~~~~~~>>>> 20"); + } + }; + } + + 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().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 SubscriptionSet +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 { + Self::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.entity.decrease_subscriptions_count()); + + self.event_dispatcher.invalidate(); + } + + fn id(&self) -> &String { + &self.id + } + + fn client(&self) -> Weak> { + self.client.clone() + } +} + +impl EventEmitter for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn messages_stream(&self) -> DataStream { + self.event_dispatcher.messages_stream() + } + + fn signal_stream(&self) -> DataStream { + self.event_dispatcher.signal_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 SubscriptionSetRef +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn new( + client: Weak>, + subscriptions: Vec>>, + options: Option>, + ) -> SubscriptionSetRef { + 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 [`SubscriptionSet`] 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)); + } + + /// Retrieves a cloned instance of a [`SubscriptionSet`] by its + /// `instance_id`. + /// + /// # Arguments + /// + /// * `instance_id` - A reference to the unique identifier of the instance. + /// + /// # Returns + /// + /// An `Option` containing a weak reference to the cloned + /// [`SubscriptionSet`] instance if found, or `None` if no instance with + /// the specified `instance_id` exists. + fn get_clone_by_id(&self, instance_id: &String) -> Option>> { + self.clones.read().get(instance_id).cloned() + } +} + +#[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.create_channel(name).subscription(None)) + .collect::>>>(); + let channels_2_subscriptions = vec!["channel_3", "channel_4"] + .into_iter() + .map(|name| client.create_channel(name).subscription(None)) + .collect::>>>(); + let channels_3_subscriptions = vec![ + channels_1_subscriptions[0].clone(), + channels_2_subscriptions[1].clone(), + ]; + let subscription_set_1 = channels_1_subscriptions[0] + .clone() + .add(channels_1_subscriptions[1].clone()); + let subscription_set_2 = channels_2_subscriptions[0] + .clone() + .add(channels_2_subscriptions[1].clone()); + let subscription_set_3 = channels_3_subscriptions[0] + .clone() + .add(channels_3_subscriptions[1].clone()); + + subscription_set_1.add_assign(subscription_set_2); + assert!(subscription_set_1 + .subscription_input(true) + .contains_channel("channel_3")); + subscription_set_1.sub_assign(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..5e8911db --- /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 signal_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..976bad3b --- /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`], [`UuidMetadata`]) +/// * `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..659e075c --- /dev/null +++ b/src/dx/subscribe/traits/subscriber.rs @@ -0,0 +1,25 @@ +//! # 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::{ + lib::alloc::{sync::Arc, 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>) -> Arc>; +} diff --git a/src/dx/subscribe/types.rs b/src/dx/subscribe/types.rs index 62153e46..8f68f3b7 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,27 @@ 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, +} + /// 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 +113,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 +174,9 @@ pub enum SubscribeStatus { /// Connection attempt failed. ConnectionError(PubNubError), + + /// Unexpected disconnection. + DisconnectedUnexpectedly(PubNubError), } /// Presence update information. @@ -135,6 +206,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 +240,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 +266,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 +302,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 +334,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 +441,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 +544,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 +558,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 +587,7 @@ pub enum MessageActionEvent { Delete, } -impl Default for SubscribeCursor { +impl Default for SubscriptionCursor { fn default() -> Self { Self { timetoken: "0".into(), @@ -492,6 +596,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 +633,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 +668,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 +775,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 +801,8 @@ impl TryFrom for Presence { channel, subscription, occupancy: occupancy.unwrap_or(0), + data, + event_timestamp, }), "leave" => Ok(Self::Leave { timestamp, @@ -655,6 +812,7 @@ impl TryFrom for Presence { channel, subscription, occupancy: occupancy.unwrap_or(0), + event_timestamp, }), "timeout" => Ok(Self::Timeout { timestamp, @@ -664,6 +822,7 @@ impl TryFrom for Presence { channel, subscription, occupancy: occupancy.unwrap_or(0), + event_timestamp, }), "interval" => Ok(Self::Interval { timestamp, @@ -673,6 +832,7 @@ impl TryFrom for Presence { join, leave, timeout, + event_timestamp, }), _ => Ok(Self::StateChange { timestamp, @@ -681,7 +841,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 +856,7 @@ impl TryFrom for Presence { } } -impl TryFrom for Object { +impl TryFrom for AppContext { type Error = PubNubError; fn try_from(value: Envelope) -> Result { @@ -778,7 +942,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 +1079,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..a2f729d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,53 +60,49 @@ //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! let publish_key = "my_publish_key"; +//! use pubnub::subscribe::EventEmitter; +//! 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(Some(&["my_channel"]), None, 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) +//! } //! } //! })); //! @@ -254,6 +250,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, UuidMetadata}; 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..9eb5a4ce 100644 --- a/src/transport/reqwest.rs +++ b/src/transport/reqwest.rs @@ -81,7 +81,30 @@ impl Transport for TransportReqwest { "Sending data to pubnub: {} {:?} {}", request.method, request.headers, request_url ); + log::debug!( + "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 +407,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..fecbd85c 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, @@ -12,6 +15,7 @@ use pubnub::{ Keyset, PubNubClient, PubNubClientBuilder, }; use std::fmt::Debug; +use std::sync::Arc; /// Type of resource for which permissions currently configured. #[derive(Default, Debug)] @@ -135,11 +139,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 +161,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 +172,30 @@ 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.subscription = None; + self.retry_policy = None; + self.pubnub.as_ref().unwrap().terminate(); + // self.pubnub = None; + log::debug!("\n\n\n\n\n\n\n\n~~~~~~~~~~~~~~~~~~~~~ WE ARE DONE"); + // tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + } + pub fn get_pubnub(&self, keyset: Keyset) -> PubNubClient { let transport = { let mut transport = TransportReqwest::default(); @@ -182,18 +205,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 +240,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..dc0751f0 100644 --- a/tests/contract_test.rs +++ b/tests/contract_test.rs @@ -1,10 +1,13 @@ use cucumber::{writer, World, WriterExt}; use std::fs::{create_dir_all, read_to_string, File, OpenOptions}; +use std::io::Write; use std::process; +use uuid::Uuid; mod access; mod common; mod crypto; +mod presence; mod publish; mod subscribe; use common::PubNubWorld; @@ -24,7 +27,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 +48,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) { @@ -58,15 +73,19 @@ fn is_ignored_feature_set_tag(feature: &str, tags: &[String]) -> bool { fn is_ignored_scenario_tag(feature: &str, tags: &[String]) -> bool { // If specific contract should be tested, it's name should be added below. - let tested_contract = ""; + let tested_contract = "presenceJoinWithAnError"; tags.contains(&"na=rust".to_string()) || !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 +94,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 +140,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 +158,13 @@ async fn main() { } }) }) + .after(|_feature, _, _rule, _scenario, world| { + futures::FutureExt::boxed(async move { + world.unwrap().reset().await; + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + // await; await; clear_log_file(); + }) + }) .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..36410ca1 --- /dev/null +++ b/tests/presence/presence_steps.rs @@ -0,0 +1,221 @@ +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> { + 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 +} + +#[allow(dead_code)] +fn event_occurrence_count(history: Vec>, event: String) -> usize { + history + .iter() + .filter(|pair| pair[0].eq("event") && pair[1].eq(&event)) + .count() +} + +#[allow(dead_code)] +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) { + (!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.create_channel(channel).subscription(options.clone()), + ); + acc + }); + log::debug!("~~~~~~~>>>> 1"); + let subscription = SubscriptionSet::new_with_subscriptions( + subscriptions.values().cloned().collect(), + options.clone(), + ); + log::debug!("~~~~~~~>>>> 2: {subscription:?}"); + 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 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.sub_subscriptions(subscriptions); +} + +#[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 { .. } => println!("Presence events received from server"), + _ => panic!("Unexpected presence update received: {update:?}"), + }; + // Flush rest of the presence events. + 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(2)).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 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..04c9162a 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}; use std::fs::read_to_string; /// Extract list of events and invocations from log. @@ -81,25 +82,31 @@ 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(Some(&["test"]), None, 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(Some(&["test"]), None, 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,17 +116,16 @@ 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(1)).fuse() => log::debug!("One 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, }); From e46801ce8004cc912d24b64d04f5f9bc478c4044 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 23 Jan 2024 16:00:33 +0200 Subject: [PATCH 02/11] refactor: clean up after debug session --- Cargo.toml | 3 +- examples/subscribe.rs | 6 --- src/core/channel.rs | 1 - src/core/channel_group.rs | 1 - src/core/channel_metadata.rs | 1 - src/core/event_engine/effect_dispatcher.rs | 42 +++++---------- src/core/event_engine/mod.rs | 28 ++++++++-- src/core/uuid_metadata.rs | 1 - src/dx/parse_token.rs | 3 -- .../presence/builders/get_presence_state.rs | 1 - src/dx/presence/builders/heartbeat.rs | 2 - src/dx/presence/builders/leave.rs | 1 - .../presence/builders/set_presence_state.rs | 3 +- .../event_engine/effects/heartbeat.rs | 4 +- src/dx/presence/event_engine/effects/leave.rs | 4 +- src/dx/presence/event_engine/effects/mod.rs | 6 +-- src/dx/presence/event_engine/event.rs | 2 - src/dx/presence/event_engine/invocation.rs | 1 - src/dx/presence/event_engine/mod.rs | 9 +--- src/dx/presence/event_engine/state.rs | 3 -- src/dx/presence/event_engine/types.rs | 4 +- src/dx/presence/mod.rs | 17 ++++-- src/dx/presence/presence_manager.rs | 8 ++- src/dx/pubnub_client.rs | 9 ++-- .../subscribe/event_engine/effect_handler.rs | 1 - .../event_engine/effects/handshake.rs | 13 +++-- .../effects/handshake_reconnection.rs | 13 +++-- src/dx/subscribe/event_engine/event.rs | 1 - src/dx/subscribe/event_engine/invocation.rs | 1 - src/dx/subscribe/event_engine/mod.rs | 3 -- src/dx/subscribe/event_engine/state.rs | 1 - src/dx/subscribe/mod.rs | 30 +++++------ src/dx/subscribe/result.rs | 3 -- src/dx/subscribe/subscription_manager.rs | 4 +- src/dx/subscribe/subscription_set.rs | 48 +++++++---------- tests/common/common_steps.rs | 11 ++-- tests/contract_test.rs | 8 +-- tests/presence/presence_steps.rs | 16 +++--- tests/subscribe/subscribe_steps.rs | 53 +++++++++++++++---- 39 files changed, 185 insertions(+), 181 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0dc46e84..08382542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,6 @@ async-channel = { version = "1.8", optional = true } # extra_platforms portable-atomic = { version = "1.3", optional = true, default-features = false, features = ["require-cas", "critical-section"] } -backtrace = "0.3.69" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } @@ -123,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"] } diff --git a/examples/subscribe.rs b/examples/subscribe.rs index 0309a173..0677dbcc 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -131,24 +131,18 @@ async fn main() -> Result<(), Box> { // You can also cancel the subscription at any time. // subscription.unsubscribe(); - println!("~~~~~~~~> DISCONNECT"); client.disconnect(); tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - println!("~~~~~~~~> RECONNECT"); client.reconnect(None); // Let event engine process unsubscribe request tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - println!("~~~~~~~~> UNSUBSCRIBE ALL..."); - // Clean up before complete work with PubNub client instance. client.unsubscribe_all(); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - println!("~~~~~~~~> UNSUBSCRIBE ALL. DONE"); - Ok(()) } diff --git a/src/core/channel.rs b/src/core/channel.rs index 6bbbcf26..382340ce 100644 --- a/src/core/channel.rs +++ b/src/core/channel.rs @@ -49,7 +49,6 @@ pub struct ChannelRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance - #[allow(unused)] client: Arc>, /// Unique channel name. diff --git a/src/core/channel_group.rs b/src/core/channel_group.rs index e296262d..b282f284 100644 --- a/src/core/channel_group.rs +++ b/src/core/channel_group.rs @@ -49,7 +49,6 @@ pub struct ChannelGroupRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance - #[allow(unused)] client: Arc>, /// Unique channel group name. diff --git a/src/core/channel_metadata.rs b/src/core/channel_metadata.rs index 8e2650c0..8a61bebb 100644 --- a/src/core/channel_metadata.rs +++ b/src/core/channel_metadata.rs @@ -50,7 +50,6 @@ pub struct ChannelMetadataRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance - #[allow(unused)] client: Arc>, /// Unique channel metadata object identifier. diff --git a/src/core/event_engine/effect_dispatcher.rs b/src/core/event_engine/effect_dispatcher.rs index 8d6eda3d..e3a8f26d 100644 --- a/src/core/event_engine/effect_dispatcher.rs +++ b/src/core/event_engine/effect_dispatcher.rs @@ -6,11 +6,11 @@ use crate::{ lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; use async_channel::Receiver; +use log::Log; use spin::rwlock::RwLock; /// State machine effects dispatcher. #[derive(Debug)] -#[allow(dead_code)] pub(crate) struct EffectDispatcher where EI: EffectInvocation + Send + Sync, @@ -62,32 +62,23 @@ 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 = Arc::downgrade(self); + let cloned_self = self.clone(); runtime.spawn(async move { log::info!("Event engine has started!"); - let mut is_active = true; loop { - let Some(strong_self) = cloned_self.upgrade() else { - break; - }; - - let invocation = strong_self.invocations_channel.recv().await; + let invocation = cloned_self.invocations_channel.recv().await; match invocation { Ok(invocation) => { - log::debug!( - "~~~~~~~ INVOCATION: {} | IS ACTIVE? {is_active:?}", - invocation.id() - ); - if !is_active || invocation.is_terminating() { + if invocation.is_terminating() { log::debug!("Received event engine termination invocation"); break; } - let effect = strong_self.dispatch(&invocation); + log::debug!("Received invocation: {}", invocation.id()); + let effect = cloned_self.dispatch(&invocation); let task_completion = completion.clone(); if let Some(effect) = effect { @@ -95,34 +86,29 @@ where let cloned_self = cloned_self.clone(); runtime_clone.spawn(async move { - if let Some(strong_self) = cloned_self.upgrade() { - let events = effect.run().await; - - if invocation.is_managed() { - strong_self.remove_managed_effect(effect.id()); - } + let events = effect.run().await; - task_completion(events); - } else { - task_completion(vec![]) + 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) => { - is_active = false; log::error!("Receive error: {err:?}"); break; } } } - is_active = false; + *cloned_self.started.write() = false; log::info!("Event engine has stopped!"); }); - *started_slot = true; + *self.started.write() = true; } /// Dispatch effect associated with `invocation`. @@ -152,13 +138,11 @@ where fn cancel_effect(&self, invocation: &EI) { let mut managed = self.managed.write(); if let Some(position) = managed.iter().position(|e| invocation.cancelling_effect(e)) { - log::debug!("~~~~~~ CANCELLING"); managed.remove(position).cancel(); } } /// 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) { diff --git a/src/core/event_engine/mod.rs b/src/core/event_engine/mod.rs index 2591af1f..759c2cdd 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 @@ -84,6 +91,7 @@ where effect_dispatcher, effect_dispatcher_channel: channel_tx, current_state: RwLock::new(state), + active: RwLock::new(true), }); engine.start(runtime); @@ -92,7 +100,6 @@ where } /// Retrieve current engine state. - #[allow(dead_code)] pub fn current_state(&self) -> S { (*self.current_state.read()).clone() } @@ -101,8 +108,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 = { @@ -121,6 +132,11 @@ 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 = state; @@ -156,9 +172,11 @@ where /// /// > Note: Should be provided effect information which respond with `true` /// for `is_terminating` method call. - #[allow(dead_code)] pub fn stop(&self, invocation: EI) { - self.effect_dispatcher_channel.close(); + { + *self.active.write() = false; + } + if let Err(error) = self.effect_dispatcher_channel.send_blocking(invocation) { error!("Unable dispatch invocation: {error:?}") } diff --git a/src/core/uuid_metadata.rs b/src/core/uuid_metadata.rs index e2403946..7b69a076 100644 --- a/src/core/uuid_metadata.rs +++ b/src/core/uuid_metadata.rs @@ -50,7 +50,6 @@ pub struct UuidMetadataRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance - #[allow(unused)] client: Arc>, /// Unique uuid metadata object identifier. 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 5eeef253..0c9ef250 100644 --- a/src/dx/presence/builders/get_presence_state.rs +++ b/src/dx/presence/builders/get_presence_state.rs @@ -155,7 +155,6 @@ where } } -#[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 dae3ca76..dad3353a 100644 --- a/src/dx/presence/builders/heartbeat.rs +++ b/src/dx/presence/builders/heartbeat.rs @@ -231,7 +231,6 @@ impl HeartbeatRequestBuilder { } } -#[allow(dead_code)] impl HeartbeatRequestBuilder where T: Transport, @@ -284,7 +283,6 @@ where } } -#[allow(dead_code)] #[cfg(feature = "blocking")] impl HeartbeatRequestBuilder where diff --git a/src/dx/presence/builders/leave.rs b/src/dx/presence/builders/leave.rs index 0b0297e0..b6bb66f0 100644 --- a/src/dx/presence/builders/leave.rs +++ b/src/dx/presence/builders/leave.rs @@ -149,7 +149,6 @@ where } } -#[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 be6e1b45..fe94cc20 100644 --- a/src/dx/presence/builders/set_presence_state.rs +++ b/src/dx/presence/builders/set_presence_state.rs @@ -196,7 +196,7 @@ where #[cfg(feature = "std")] if !request.channels.is_empty() { - request.on_execute.clone()(request.channels.clone(), request.state.clone()); + (request.on_execute)(request.channels.clone(), request.state.clone()); } let transport_request = request.transport_request()?; @@ -209,7 +209,6 @@ where } } -#[allow(dead_code)] #[cfg(feature = "blocking")] impl SetStateRequestBuilder where diff --git a/src/dx/presence/event_engine/effects/heartbeat.rs b/src/dx/presence/event_engine/effects/heartbeat.rs index 2227c712..e987b76d 100644 --- a/src/dx/presence/event_engine/effects/heartbeat.rs +++ b/src/dx/presence/event_engine/effects/heartbeat.rs @@ -11,9 +11,7 @@ use crate::{ core::{PubNubError, RequestRetryConfiguration}, lib::alloc::{sync::Arc, vec, vec::Vec}, presence::event_engine::{ - effects::HeartbeatEffectExecutor, - types::{PresenceInput, PresenceParameters}, - PresenceEvent, + effects::HeartbeatEffectExecutor, PresenceEvent, PresenceInput, PresenceParameters, }, }; 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 8e4d04ad..4dceb748 100644 --- a/src/dx/presence/event_engine/effects/mod.rs +++ b/src/dx/presence/event_engine/effects/mod.rs @@ -13,10 +13,7 @@ use crate::{ core::fmt::{Debug, Formatter}, }, presence::{ - event_engine::{ - types::{PresenceInput, PresenceParameters}, - PresenceEffectInvocation, - }, + event_engine::{PresenceEffectInvocation, PresenceInput, PresenceParameters}, HeartbeatResult, LeaveResult, }, }; @@ -53,7 +50,6 @@ 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 { diff --git a/src/dx/presence/event_engine/event.rs b/src/dx/presence/event_engine/event.rs index 0b04961d..7addcec9 100644 --- a/src/dx/presence/event_engine/event.rs +++ b/src/dx/presence/event_engine/event.rs @@ -51,7 +51,6 @@ pub(crate) enum PresenceEvent { /// Announce leave on all channels and groups. /// /// Announce `user_id` leave from all channels and groups. - #[allow(dead_code)] LeftAll { /// Whether `user_id` leave should be announced or not. /// @@ -96,7 +95,6 @@ pub(crate) enum PresenceEvent { /// /// Emitted when `delay` reaches the end and should transit to the next /// state. - #[allow(dead_code)] TimesUp, } diff --git a/src/dx/presence/event_engine/invocation.rs b/src/dx/presence/event_engine/invocation.rs index 27322c4a..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 { 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 afb4158f..076bb4c2 100644 --- a/src/dx/presence/event_engine/state.rs +++ b/src/dx/presence/event_engine/state.rs @@ -346,7 +346,6 @@ impl State for PresenceState { } fn exit(&self) -> Option> { - log::debug!("~~~~~~~~~~ EXIT: {self:?}"); match self { PresenceState::Cooldown { .. } => Some(vec![CancelWait]), PresenceState::Reconnecting { .. } => Some(vec![CancelDelayedHeartbeat]), @@ -403,8 +402,6 @@ impl State for PresenceState { .chain(on_enter_invocations) .collect(); - log::debug!("~~~~~~>> COLLECTED INVOCATIONS: {invocations:?}"); - Transition { invocations, state } } } 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 e25080fe..eb078125 100644 --- a/src/dx/presence/mod.rs +++ b/src/dx/presence/mod.rs @@ -30,7 +30,7 @@ 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; @@ -479,7 +479,6 @@ where D: Deserializer + 'static, { /// Announce `join` for `user_id` on provided channels and groups. - #[allow(dead_code)] pub(crate) fn announce_join( &self, channels: Option>, @@ -493,7 +492,6 @@ where } /// Announce `leave` for `user_id` on provided channels and groups. - #[allow(dead_code)] pub(crate) fn announce_left( &self, channels: Option>, @@ -506,6 +504,15 @@ where }; } + /// 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().read().as_ref() { + presence.announce_left_all(); + } + } + } + /// Presence manager which maintains Presence EE. /// /// # Returns @@ -513,6 +520,10 @@ where /// Returns an [`PresenceManager`] which represents the manager. #[cfg(all(feature = "presence", feature = "std"))] pub(crate) fn presence_manager(&self) -> 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() { diff --git a/src/dx/presence/presence_manager.rs b/src/dx/presence/presence_manager.rs index f29cb552..4c0429df 100644 --- a/src/dx/presence/presence_manager.rs +++ b/src/dx/presence/presence_manager.rs @@ -43,7 +43,6 @@ impl PresenceManager { /// /// Gracefully terminate all ongoing tasks including detached event engine /// loop. - #[allow(dead_code)] pub fn terminate(&self) { self.event_engine .stop(PresenceEffectInvocation::TerminateEventEngine); @@ -118,6 +117,13 @@ impl PresenceManagerRef { }) } + /// 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) { self.event_engine.process(&PresenceEvent::Disconnect); diff --git a/src/dx/pubnub_client.rs b/src/dx/pubnub_client.rs index bbffd503..5886313c 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -10,6 +10,7 @@ use derive_builder::Builder; use log::info; use spin::{Mutex, RwLock}; +use std::cmp::max; use uuid::Uuid; #[cfg(all( @@ -40,9 +41,8 @@ use crate::transport::TransportReqwest; #[cfg(feature = "std")] use crate::core::RequestRetryConfiguration; -use crate::core::{Deserialize, Transport}; use crate::{ - core::{CryptoProvider, PubNubEntity, PubNubError}, + core::{CryptoProvider, PubNubEntity, PubNubError, Transport}, lib::{ alloc::{ borrow::ToOwned, @@ -884,11 +884,11 @@ where { #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] pub fn terminate(&self) { - #[cfg(all(feature = "subscribe", feature = "std"))] + #[cfg(feature = "subscribe")] if let Some(manager) = self.subscription_manager().read().as_ref() { manager.terminate(); } - #[cfg(all(feature = "presence", feature = "std"))] + #[cfg(feature = "presence")] if let Some(manager) = self.presence_manager().read().as_ref() { manager.terminate(); } @@ -923,6 +923,7 @@ 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() { + let value = max(20, value); configuration.presence.heartbeat_value = value; #[cfg(feature = "std")] diff --git a/src/dx/subscribe/event_engine/effect_handler.rs b/src/dx/subscribe/event_engine/effect_handler.rs index 4d68ee4e..5c5b2104 100644 --- a/src/dx/subscribe/event_engine/effect_handler.rs +++ b/src/dx/subscribe/event_engine/effect_handler.rs @@ -18,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, diff --git a/src/dx/subscribe/event_engine/effects/handshake.rs b/src/dx/subscribe/event_engine/effects/handshake.rs index 15d5c703..cc0a3315 100644 --- a/src/dx/subscribe/event_engine/effects/handshake.rs +++ b/src/dx/subscribe/event_engine/effects/handshake.rs @@ -39,9 +39,16 @@ pub(super) async fn execute( vec![SubscribeEvent::HandshakeFailure { reason: error }] }, |subscribe_result| { - vec![SubscribeEvent::HandshakeSuccess { - cursor: cursor.clone().unwrap_or(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 diff --git a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs index eadbe0d6..8a89ba9f 100644 --- a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs @@ -52,9 +52,16 @@ pub(super) async fn execute( .unwrap_or(vec![]) }, |subscribe_result| { - vec![SubscribeEvent::HandshakeReconnectSuccess { - cursor: cursor.clone().unwrap_or(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 diff --git a/src/dx/subscribe/event_engine/event.rs b/src/dx/subscribe/event_engine/event.rs index 98c17f53..bd3ccfd0 100644 --- a/src/dx/subscribe/event_engine/event.rs +++ b/src/dx/subscribe/event_engine/event.rs @@ -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. diff --git a/src/dx/subscribe/event_engine/invocation.rs b/src/dx/subscribe/event_engine/invocation.rs index 10c143f9..7ff81b48 100644 --- a/src/dx/subscribe/event_engine/invocation.rs +++ b/src/dx/subscribe/event_engine/invocation.rs @@ -16,7 +16,6 @@ use crate::{ /// Invocation is form of intention to call some action without any information /// about its implementation. #[derive(Debug)] -#[allow(dead_code)] pub(crate) enum SubscribeEffectInvocation { /// Initial subscribe effect invocation. Handshake { diff --git a/src/dx/subscribe/event_engine/mod.rs b/src/dx/subscribe/event_engine/mod.rs index 3de1e248..f29da177 100644 --- a/src/dx/subscribe/event_engine/mod.rs +++ b/src/dx/subscribe/event_engine/mod.rs @@ -22,12 +22,10 @@ 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::{SubscriptionInput, SubscriptionParams}; pub(in crate::dx::subscribe) mod types; @@ -37,7 +35,6 @@ pub(crate) type SubscribeEventEngine = impl EventEngine { - #[allow(dead_code)] pub(in crate::dx::subscribe) fn current_subscription( &self, ) -> (Option>, Option>) { diff --git a/src/dx/subscribe/event_engine/state.rs b/src/dx/subscribe/event_engine/state.rs index 3a57ccd6..6174e196 100644 --- a/src/dx/subscribe/event_engine/state.rs +++ b/src/dx/subscribe/event_engine/state.rs @@ -27,7 +27,6 @@ use crate::{ /// States of subscribe state machine. #[derive(Debug, Clone, PartialEq)] -#[allow(dead_code)] pub(crate) enum SubscribeState { /// Unsubscribed state. /// diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index 5436313c..c97c1925 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -388,6 +388,9 @@ where manager.unregister_all() } } + + #[cfg(feature = "presence")] + self.announce_left_all(); } /// Subscription manager which maintains Subscription EE. @@ -395,7 +398,7 @@ where /// # Returns /// /// Returns an [`SubscriptionManager`] which represents the manager. - #[cfg(all(feature = "subscribe", feature = "std"))] + #[cfg(feature = "subscribe")] pub(crate) fn subscription_manager(&self) -> Arc>>> { { // Initialize subscription module when it will be first required. @@ -521,12 +524,10 @@ where channels: Option>, channel_groups: Option>, ) { - let channels = Self::presence_filtered_entries(channels); - let channel_groups = Self::presence_filtered_entries(channel_groups); - - if let Some(presence) = client.presence_manager().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. @@ -535,21 +536,16 @@ where /// presence event engine state: /// * can operate - call `leave` announcement /// * can't operate (heartbeat interval not set) - make direct `leave` call. - #[cfg(feature = "presence")] + #[cfg(all(feature = "presence", feature = "std"))] fn subscribe_leave_call( client: Self, channels: Option>, channel_groups: Option>, ) { - #[cfg(feature = "presence")] - { - let channels = Self::presence_filtered_entries(channels); - let channel_groups = Self::presence_filtered_entries(channel_groups); - - if let Some(presence) = client.presence_manager().read().as_ref() { - presence.announce_left(channels, channel_groups); - } - } + client.announce_left( + Self::presence_filtered_entries(channels), + Self::presence_filtered_entries(channel_groups), + ); } fn emit_status(client: Self, status: &ConnectionStatus) { diff --git a/src/dx/subscribe/result.rs b/src/dx/subscribe/result.rs index a3aecfe8..cfbd58f9 100644 --- a/src/dx/subscribe/result.rs +++ b/src/dx/subscribe/result.rs @@ -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"))] @@ -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 { @@ -536,7 +534,6 @@ impl TryFrom for SubscribeResult { impl Envelope { /// Default message type. - #[allow(dead_code)] fn default_message_type() -> SubscribeMessageType { SubscribeMessageType::Message } diff --git a/src/dx/subscribe/subscription_manager.rs b/src/dx/subscribe/subscription_manager.rs index aebad384..01159b79 100644 --- a/src/dx/subscribe/subscription_manager.rs +++ b/src/dx/subscribe/subscription_manager.rs @@ -193,7 +193,7 @@ where let removed = removed.map(|removed| { removed .iter() - .filter(|subscription| subscription.entity.subscriptions_count().gt(&0)) + .filter(|subscription| subscription.entity.subscriptions_count().eq(&0)) .fold(SubscriptionInput::default(), |mut acc, subscription| { acc += subscription.subscription_input.clone(); acc @@ -221,7 +221,6 @@ where } // TODO: why call it on drop fails tests? - #[allow(dead_code)] pub fn unregister_all(&mut self) { let inputs = self.current_input(); { @@ -261,7 +260,6 @@ where /// /// Gracefully terminate all ongoing tasks including detached event engine /// loop. - #[allow(dead_code)] pub fn terminate(&self) { self.event_engine .stop(SubscribeEffectInvocation::TerminateEventEngine); diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs index 743f678f..63cfdfec 100644 --- a/src/dx/subscribe/subscription_set.rs +++ b/src/dx/subscribe/subscription_set.rs @@ -333,10 +333,13 @@ where pub fn add_subscriptions(&self, subscriptions: Vec>>) { let unique_subscriptions = SubscriptionSet::unique_subscriptions_from_list(Some(self), subscriptions); - let mut subscription_input = self.subscription_input.write(); - let mut subscriptions_slot = self.subscriptions.write(); - *subscription_input += Self::subscription_input_from_list(&unique_subscriptions, true); - subscriptions_slot.extend(unique_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() { @@ -398,15 +401,20 @@ where /// # } /// ``` pub fn sub_subscriptions(&self, subscriptions: Vec>>) { - let mut subscription_input = self.subscription_input.write(); - let mut subscriptions_slot = self.subscriptions.write(); - let removed: Vec>> = + let removed: Vec>> = { + let subscriptions_slot = self.subscriptions.read(); Self::unique_subscriptions_from_list(None, subscriptions) .into_iter() .filter(|subscription| subscriptions_slot.contains(subscription)) - .collect(); - subscriptions_slot.retain(|subscription| !removed.contains(subscription)); - *subscription_input -= Self::subscription_input_from_list(&removed, true); + .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() { @@ -597,58 +605,38 @@ where D: Deserializer + Send + Sync + 'static, { fn subscribe(&self, cursor: Option) { - log::debug!("~~~~~~~>>>> 3"); let mut is_subscribed = self.is_subscribed.write(); - log::debug!("~~~~~~~>>>> 4"); if *is_subscribed { - log::debug!("~~~~~~~>>>> 5"); return; } - log::debug!("~~~~~~~>>>> 6"); *is_subscribed = true; if cursor.is_some() { - log::debug!("~~~~~~~>>>> 7"); let mut cursor_slot = self.cursor.write(); if let Some(current_cursor) = cursor_slot.as_ref() { - log::debug!("~~~~~~~>>>> 8"); let catchup_cursor = cursor.clone().unwrap_or_default(); catchup_cursor .gt(current_cursor) .then(|| *cursor_slot = Some(catchup_cursor)); } else { - log::debug!("~~~~~~~>>>> 9"); *cursor_slot = cursor.clone(); } - log::debug!("~~~~~~~>>>> 10"); } - log::debug!("~~~~~~~>>>> 11"); let Some(client) = self.client().upgrade().clone() else { - log::debug!("~~~~~~~>>>> 12"); return; }; - log::debug!("~~~~~~~>>>> 13"); let manager = client.subscription_manager(); - { - log::debug!("~~~~~~~>>>> 14: {:?}", manager.read().is_some()); - } if let Some(manager) = manager.write().as_mut() { - log::debug!("~~~~~~~>>>> 15"); // Mark entities as "in-use" by subscription. self.subscriptions.read().iter().for_each(|subscription| { - log::debug!("~~~~~~~>>>> 16: {:?}", subscription.entity); subscription.entity.increase_subscriptions_count(); }); - log::debug!("~~~~~~~>>>> 17"); if let Some((_, handler)) = self.clones.read().iter().next() { - log::debug!("~~~~~~~>>>> 18"); let handler: Weak + Send + Sync> = handler.clone(); - log::debug!("~~~~~~~>>>> 19: {handler:?}"); manager.register(&handler, cursor); - log::debug!("~~~~~~~>>>> 20"); } }; } diff --git a/tests/common/common_steps.rs b/tests/common/common_steps.rs index fecbd85c..0e2132df 100644 --- a/tests/common/common_steps.rs +++ b/tests/common/common_steps.rs @@ -188,12 +188,13 @@ impl Default for PubNubWorld { impl PubNubWorld { pub async fn reset(&mut self) { - self.subscription = None; + // self.subscription = None; self.retry_policy = None; - self.pubnub.as_ref().unwrap().terminate(); - // self.pubnub = None; - log::debug!("\n\n\n\n\n\n\n\n~~~~~~~~~~~~~~~~~~~~~ WE ARE DONE"); - // tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + if let Some(pubnub) = self.pubnub.as_ref() { + pubnub.terminate(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + self.pubnub = None; } pub fn get_pubnub(&self, keyset: Keyset) -> PubNubClient { diff --git a/tests/contract_test.rs b/tests/contract_test.rs index dc0751f0..1bbe07aa 100644 --- a/tests/contract_test.rs +++ b/tests/contract_test.rs @@ -73,7 +73,7 @@ fn is_ignored_feature_set_tag(feature: &str, tags: &[String]) -> bool { fn is_ignored_scenario_tag(feature: &str, tags: &[String]) -> bool { // If specific contract should be tested, it's name should be added below. - let tested_contract = "presenceJoinWithAnError"; + let tested_contract = ""; tags.contains(&"na=rust".to_string()) || !feature_allows_beta(feature) && tags.iter().any(|tag| tag.starts_with("beta")) @@ -159,11 +159,7 @@ async fn main() { }) }) .after(|_feature, _, _rule, _scenario, world| { - futures::FutureExt::boxed(async move { - world.unwrap().reset().await; - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - // await; await; clear_log_file(); - }) + futures::FutureExt::boxed(async move { world.unwrap().reset().await }) }) .with_writer( writer::Basic::stdout() diff --git a/tests/presence/presence_steps.rs b/tests/presence/presence_steps.rs index 36410ca1..021fc4a7 100644 --- a/tests/presence/presence_steps.rs +++ b/tests/presence/presence_steps.rs @@ -1,6 +1,7 @@ use cucumber::gherkin::Table; use cucumber::{codegen::Regex, gherkin::Step, then, when}; use futures::{select_biased, FutureExt, StreamExt}; +use log::Log; use std::collections::HashMap; use std::fs::read_to_string; @@ -61,7 +62,6 @@ fn events_and_invocations_history() -> Vec> { lines } -#[allow(dead_code)] fn event_occurrence_count(history: Vec>, event: String) -> usize { history .iter() @@ -69,7 +69,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() @@ -79,6 +78,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 @@ -133,12 +134,10 @@ async fn join( ); acc }); - log::debug!("~~~~~~~>>>> 1"); let subscription = SubscriptionSet::new_with_subscriptions( subscriptions.values().cloned().collect(), options.clone(), ); - log::debug!("~~~~~~~>>>> 2: {subscription:?}"); subscription.subscribe(None); world.subscription = Some(subscription); world.subscriptions = Some(subscriptions); @@ -178,10 +177,10 @@ async fn wait_presence_join(world: &mut PubNubWorld) { _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).fuse() => panic!("No service response"), update = subscription.next().fuse() => { match update.clone().unwrap() { - Presence::Join { .. } => println!("Presence events received from server"), + Presence::Join { .. } => {}, _ => panic!("Unexpected presence update received: {update:?}"), }; - // Flush rest of the presence events. + subscription.next().await; subscription.next().await; } @@ -209,6 +208,11 @@ async fn receive_an_error_heartbeat_retry(world: &mut PubNubWorld) { ); } +#[then("I don't observe any Events and Invocations of the Presence EE")] +async fn event_engine_history_empty(_world: &mut PubNubWorld, step: &Step) { + 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(); diff --git a/tests/subscribe/subscribe_steps.rs b/tests/subscribe/subscribe_steps.rs index 04c9162a..5dff5da3 100644 --- a/tests/subscribe/subscribe_steps.rs +++ b/tests/subscribe/subscribe_steps.rs @@ -14,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 ") { @@ -22,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() @@ -42,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() @@ -52,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 @@ -65,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") @@ -119,7 +151,7 @@ async fn receive_an_error_subscribe_retry(world: &mut PubNubWorld) { 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") } @@ -155,15 +187,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" ); } From 2050b719e2a588dca3134852c8f232592593d29a Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 01:01:48 +0200 Subject: [PATCH 03/11] refactor: changes per-review Changes requested in review and changes in specs. --- examples/subscribe.rs | 6 +- src/core/channel.rs | 3 +- src/core/channel_group.rs | 3 +- src/core/channel_metadata.rs | 3 +- src/core/entity.rs | 47 +- src/core/event_engine/effect_dispatcher.rs | 1 - src/core/event_engine/mod.rs | 5 +- src/core/mod.rs | 4 +- .../{uuid_metadata.rs => user_metadata.rs} | 65 +- src/dx/presence/builders/heartbeat.rs | 46 +- src/dx/presence/builders/here_now.rs | 2 +- src/dx/presence/mod.rs | 21 +- src/dx/pubnub_client.rs | 104 +-- src/dx/subscribe/event_engine/mod.rs | 27 +- src/dx/subscribe/mod.rs | 61 +- src/dx/subscribe/result.rs | 1 + src/dx/subscribe/subscription.rs | 366 +++++---- src/dx/subscribe/subscription_manager.rs | 41 +- src/dx/subscribe/subscription_set.rs | 701 ++++++++++-------- src/dx/subscribe/traits/subscribable.rs | 2 +- src/dx/subscribe/traits/subscriber.rs | 4 +- src/lib.rs | 2 +- src/transport/reqwest.rs | 6 - tests/common/common_steps.rs | 5 +- tests/contract_test.rs | 2 - tests/presence/presence_steps.rs | 6 +- 26 files changed, 832 insertions(+), 702 deletions(-) rename src/core/{uuid_metadata.rs => user_metadata.rs} (75%) diff --git a/examples/subscribe.rs b/examples/subscribe.rs index 0677dbcc..ee9908b2 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use serde::Deserialize; use std::env; @@ -23,8 +23,8 @@ struct Message { #[tokio::main] async fn main() -> Result<(), Box> { - let publish_key = "demo"; //env::var("SDK_PUB_KEY")?; - let subscribe_key = "demo"; //env::var("SDK_SUB_KEY")?; + let publish_key = env::var("SDK_PUB_KEY")?; + let subscribe_key = env::var("SDK_SUB_KEY")?; let client = PubNubClientBuilder::with_reqwest_transport() .with_keyset(Keyset { diff --git a/src/core/channel.rs b/src/core/channel.rs index 382340ce..6795e414 100644 --- a/src/core/channel.rs +++ b/src/core/channel.rs @@ -49,6 +49,7 @@ pub struct ChannelRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. client: Arc>, /// Unique channel name. @@ -210,7 +211,7 @@ where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { - fn subscription(&self, options: Option>) -> Arc> { + 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 index b282f284..2a843788 100644 --- a/src/core/channel_group.rs +++ b/src/core/channel_group.rs @@ -49,6 +49,7 @@ pub struct ChannelGroupRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. client: Arc>, /// Unique channel group name. @@ -211,7 +212,7 @@ where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { - fn subscription(&self, options: Option>) -> Arc> { + 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 index 8a61bebb..35159929 100644 --- a/src/core/channel_metadata.rs +++ b/src/core/channel_metadata.rs @@ -50,6 +50,7 @@ pub struct ChannelMetadataRef { /// * subscription /// /// [`PubNubClientInstance`]: PubNubClientInstance + #[allow(dead_code)] // Field used conditionally only for `subscription` feature. client: Arc>, /// Unique channel metadata object identifier. @@ -208,7 +209,7 @@ where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { - fn subscription(&self, options: Option>) -> Arc> { + fn subscription(&self, options: Option>) -> Subscription { Subscription::new(self.client(), self.clone().into(), options) } } diff --git a/src/core/entity.rs b/src/core/entity.rs index ad9978e9..96e78ac4 100644 --- a/src/core/entity.rs +++ b/src/core/entity.rs @@ -7,38 +7,25 @@ //! [`PubNub API`]: https://www.pubnub.com/docs use crate::{ - lib::{ - alloc::string::String, - core::{ - cmp::PartialEq, - fmt::{Debug, Formatter, Result}, - }, + lib::core::{ + cmp::PartialEq, + fmt::{Debug, Formatter, Result}, }, - Channel, ChannelGroup, ChannelMetadata, UuidMetadata, + Channel, ChannelGroup, ChannelMetadata, UserMetadata, }; #[cfg(all(feature = "subscribe", feature = "std"))] use crate::{ core::{Deserializer, Transport}, - lib::alloc::{sync::Arc, vec::Vec}, + lib::alloc::{string::String, vec::Vec}, subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, }; -pub(crate) trait PubNubEntity2 { - /// Unique entity identifier. - /// - /// Identifier is important for the [`PubNub API`] and used as target - /// identifier for used API. - /// - /// [`PubNub API`]: https://www.pubnub.com/docs - fn id(&self) -> String; -} - pub(crate) enum PubNubEntity { Channel(Channel), ChannelGroup(ChannelGroup), ChannelMetadata(ChannelMetadata), - UuidMetadata(UuidMetadata), + UserMetadata(UserMetadata), } #[cfg(all(feature = "subscribe", feature = "std"))] @@ -48,7 +35,7 @@ impl PubNubEntity { Self::Channel(channel) => channel.names(presence), Self::ChannelGroup(channel_group) => channel_group.names(presence), Self::ChannelMetadata(channel_metadata) => channel_metadata.names(presence), - Self::UuidMetadata(uuid_metadata) => uuid_metadata.names(presence), + Self::UserMetadata(uuid_metadata) => uuid_metadata.names(presence), } } @@ -57,7 +44,7 @@ impl PubNubEntity { Self::Channel(channel) => channel.r#type(), Self::ChannelGroup(channel_group) => channel_group.r#type(), Self::ChannelMetadata(channel_metadata) => channel_metadata.r#type(), - Self::UuidMetadata(uuid_metadata) => uuid_metadata.r#type(), + Self::UserMetadata(uuid_metadata) => uuid_metadata.r#type(), } } @@ -75,7 +62,7 @@ impl PubNubEntity { Self::ChannelMetadata(channel_metadata) => { channel_metadata.increase_subscriptions_count() } - Self::UuidMetadata(uuid_metadata) => uuid_metadata.increase_subscriptions_count(), + Self::UserMetadata(uuid_metadata) => uuid_metadata.increase_subscriptions_count(), } } @@ -97,7 +84,7 @@ impl PubNubEntity { Self::ChannelMetadata(channel_metadata) => { channel_metadata.decrease_subscriptions_count() } - Self::UuidMetadata(uuid_metadata) => uuid_metadata.decrease_subscriptions_count(), + Self::UserMetadata(uuid_metadata) => uuid_metadata.decrease_subscriptions_count(), } } @@ -115,7 +102,7 @@ impl PubNubEntity { Self::Channel(channel) => channel.subscriptions_count(), Self::ChannelGroup(channel_group) => channel_group.subscriptions_count(), Self::ChannelMetadata(channel_metadata) => channel_metadata.subscriptions_count(), - Self::UuidMetadata(uuid_metadata) => uuid_metadata.subscriptions_count(), + Self::UserMetadata(uuid_metadata) => uuid_metadata.subscriptions_count(), } } } @@ -128,7 +115,7 @@ impl Clone for PubNubEntity { Self::ChannelMetadata(channel_metadata) => { Self::ChannelMetadata(channel_metadata.clone()) } - Self::UuidMetadata(uuid_metadata) => Self::UuidMetadata(uuid_metadata.clone()), + Self::UserMetadata(uuid_metadata) => Self::UserMetadata(uuid_metadata.clone()), } } } @@ -154,8 +141,8 @@ impl PartialEq for PubNubEntity { }; channel_metadata_a.eq(channel_metadata_b) } - Self::UuidMetadata(uuid_metadata_a) => { - let Self::UuidMetadata(uuid_metadata_b) = other else { + Self::UserMetadata(uuid_metadata_a) => { + let Self::UserMetadata(uuid_metadata_b) = other else { return false; }; uuid_metadata_a.eq(uuid_metadata_b) @@ -172,7 +159,7 @@ impl Debug for PubNubEntity { Self::ChannelMetadata(channel_metadata) => { write!(f, "ChannelMetadata({channel_metadata:?})") } - Self::UuidMetadata(uuid_metadata) => write!(f, "UuidMetadata({uuid_metadata:?})"), + Self::UserMetadata(user_metadata) => write!(f, "UserMetadata({user_metadata:?})"), } } } @@ -183,14 +170,14 @@ where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { - fn subscription(&self, options: Option>) -> Arc> { + 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::UuidMetadata(uuid_metadata) => uuid_metadata.subscription(options), + PubNubEntity::UserMetadata(uuid_metadata) => uuid_metadata.subscription(options), } } } diff --git a/src/core/event_engine/effect_dispatcher.rs b/src/core/event_engine/effect_dispatcher.rs index e3a8f26d..153eb1f5 100644 --- a/src/core/event_engine/effect_dispatcher.rs +++ b/src/core/event_engine/effect_dispatcher.rs @@ -6,7 +6,6 @@ use crate::{ lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; use async_channel::Receiver; -use log::Log; use spin::rwlock::RwLock; /// State machine effects dispatcher. diff --git a/src/core/event_engine/mod.rs b/src/core/event_engine/mod.rs index 759c2cdd..7cda7a88 100644 --- a/src/core/event_engine/mod.rs +++ b/src/core/event_engine/mod.rs @@ -100,7 +100,10 @@ where } /// Retrieve current engine state. - pub fn current_state(&self) -> S { + /// + /// > Note: Code actually used in tests. + #[allow(dead_code)] + pub(crate) fn current_state(&self) -> S { (*self.current_state.read()).clone() } diff --git a/src/core/mod.rs b/src/core/mod.rs index 85861804..a44e8b85 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -101,7 +101,7 @@ pub use channel_metadata::ChannelMetadata; pub mod channel_metadata; #[doc(inline)] -pub use uuid_metadata::UuidMetadata; -pub mod uuid_metadata; +pub use user_metadata::UserMetadata; +pub mod user_metadata; pub mod types; diff --git a/src/core/uuid_metadata.rs b/src/core/user_metadata.rs similarity index 75% rename from src/core/uuid_metadata.rs rename to src/core/user_metadata.rs index 7b69a076..613da256 100644 --- a/src/core/uuid_metadata.rs +++ b/src/core/user_metadata.rs @@ -1,6 +1,6 @@ -//! # UuidMetadata entity module +//! # UserMetadata entity module //! -//! This module contains the [`UuidMetadata`] type, which can be used as a +//! 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 @@ -28,33 +28,34 @@ use crate::{ subscribe::{Subscribable, SubscribableType, Subscriber, Subscription, SubscriptionOptions}, }; -/// UUID metadata entity. +/// User metadata entity. /// /// Entity as a first-class citizen provides access to the entity-specific API. -pub struct UuidMetadata { - inner: Arc>, +pub struct UserMetadata { + inner: Arc>, } -/// UUID metadata entity reference. +/// User metadata entity reference. /// -/// This struct contains the actual UUID metadata state. It is wrapped in an -/// Arc by [`UuidMetadata`] and uses internal mutability for its internal +/// 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 [`UuidMetadata`] instead. +/// Not intended to be used directly. Use [`UserMetadata`] instead. #[derive(Debug)] -pub struct UuidMetadataRef { +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 uuid metadata object identifier. + /// Unique user metadata object identifier. /// - /// Uuid metadata object identifier used by the [`PubNub API`] as unique + /// 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 @@ -68,21 +69,21 @@ pub struct UuidMetadataRef { subscriptions_count: RwLock, } -impl UuidMetadata { - /// Creates a new instance of an uuid metadata object. +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 uuid metadata object. + /// * `id` - The identifier of the user metadata object. /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub(crate) fn new(client: &PubNubClientInstance, id: S) -> UuidMetadata + pub(crate) fn new(client: &PubNubClientInstance, id: S) -> UserMetadata where S: Into, { Self { - inner: Arc::new(UuidMetadataRef { + inner: Arc::new(UserMetadataRef { client: Arc::new(client.clone()), id: id.into(), #[cfg(all(feature = "subscribe", feature = "std"))] @@ -135,22 +136,22 @@ impl UuidMetadata { } } -impl Deref for UuidMetadata { - type Target = UuidMetadataRef; +impl Deref for UserMetadata { + type Target = UserMetadataRef; fn deref(&self) -> &Self::Target { &self.inner } } -impl DerefMut for UuidMetadata { +impl DerefMut for UserMetadata { fn deref_mut(&mut self) -> &mut Self::Target { Arc::get_mut(&mut self.inner) - .expect("Multiple mutable references to the UuidMetadata are not allowed") + .expect("Multiple mutable references to the UserMetadata are not allowed") } } -impl Clone for UuidMetadata { +impl Clone for UserMetadata { fn clone(&self) -> Self { Self { inner: Arc::clone(&self.inner), @@ -158,24 +159,24 @@ impl Clone for UuidMetadata { } } -impl PartialEq for UuidMetadata { +impl PartialEq for UserMetadata { fn eq(&self, other: &Self) -> bool { self.id.eq(&other.id) } } -impl From> for PubNubEntity { - fn from(value: UuidMetadata) -> Self { - PubNubEntity::UuidMetadata(value) +impl From> for PubNubEntity { + fn from(value: UserMetadata) -> Self { + PubNubEntity::UserMetadata(value) } } -impl Debug for UuidMetadata { +impl Debug for UserMetadata { #[cfg(all(feature = "subscribe", feature = "std"))] fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( f, - "UuidMetadata {{ id: {}, subscriptions_count: {} }}", + "UserMetadata {{ id: {}, subscriptions_count: {} }}", self.id, self.subscriptions_count() ) @@ -183,12 +184,12 @@ impl Debug for UuidMetadata { #[cfg(not(all(feature = "subscribe", feature = "std")))] fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "UuidMetadata {{ id: {} }}", self.id) + write!(f, "UserMetadata {{ id: {} }}", self.id) } } #[cfg(all(feature = "subscribe", feature = "std"))] -impl Subscribable for UuidMetadata { +impl Subscribable for UserMetadata { fn names(&self, _presence: bool) -> Vec { vec![self.id.clone()] } @@ -203,12 +204,12 @@ impl Subscribable for UuidMetadata { } #[cfg(all(feature = "subscribe", feature = "std"))] -impl Subscriber for UuidMetadata +impl Subscriber for UserMetadata where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { - fn subscription(&self, options: Option>) -> Arc> { + fn subscription(&self, options: Option>) -> Subscription { Subscription::new(self.client(), self.clone().into(), options) } } diff --git a/src/dx/presence/builders/heartbeat.rs b/src/dx/presence/builders/heartbeat.rs index dad3353a..7da855dc 100644 --- a/src/dx/presence/builders/heartbeat.rs +++ b/src/dx/presence/builders/heartbeat.rs @@ -5,10 +5,7 @@ use derive_builder::Builder; #[cfg(feature = "std")] -use futures::{ - future::BoxFuture, - {select_biased, FutureExt}, -}; +use futures::{future::BoxFuture, select_biased, FutureExt}; use crate::{ core::{ @@ -342,45 +339,4 @@ mod it_should { assert!(matches!(result, Err(PubNubError::EffectCanceled))); } - - // TODO: Make request cancelable - // #[cfg(feature = "std")] - // #[tokio::test] - // async fn be_able_to_cancel_request() { - // struct MockTransport; - // - // #[async_trait::async_trait] - // impl Transport for MockTransport { - // async fn send(&self, _req: TransportRequest) -> - // Result { - // tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // - // Simulate long request. - // - // Ok(TransportResponse::default()) - // } - // } - // - // let client = PubNubClientBuilder::with_transport(MockTransport) - // .with_keyset(crate::Keyset { - // subscribe_key: "test", - // publish_key: Some("test"), - // secret_key: None, - // }) - // .with_user_id("test") - // .build() - // .unwrap(); - // let _ = &client - // .detached_guard - // .notify_channel_tx - // .send_blocking(1) - // .unwrap(); - // - // let result = client - // .heartbeat() - // .channels(vec!["test".into()]) - // .execute() - // .await; - // - // assert!(matches!(result, Err(PubNubError::RequestCancel { .. }))); - // } } diff --git a/src/dx/presence/builders/here_now.rs b/src/dx/presence/builders/here_now.rs index 21557bd9..fcd9f5b5 100644 --- a/src/dx/presence/builders/here_now.rs +++ b/src/dx/presence/builders/here_now.rs @@ -65,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), diff --git a/src/dx/presence/mod.rs b/src/dx/presence/mod.rs index eb078125..b0baeb76 100644 --- a/src/dx/presence/mod.rs +++ b/src/dx/presence/mod.rs @@ -485,7 +485,7 @@ where channel_groups: Option>, ) { { - if let Some(presence) = self.presence_manager().read().as_ref() { + if let Some(presence) = self.presence_manager(true).read().as_ref() { presence.announce_join(channels, channel_groups); }; }; @@ -498,7 +498,7 @@ where channel_groups: Option>, ) { { - if let Some(presence) = self.presence_manager().read().as_ref() { + if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.announce_left(channels, channel_groups); }; }; @@ -507,7 +507,7 @@ where /// 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().read().as_ref() { + if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.announce_left_all(); } } @@ -515,18 +515,29 @@ where /// Presence manager which maintains Presence EE. /// + /// # Arguments + /// + /// `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) -> Arc>> { + 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 manager = self.presence.read(); + if manager.is_some() || !create { + return self.presence.clone(); + } + } + { let mut slot = self.presence.write(); - if slot.is_none() { + if slot.is_none() && create { *slot = Some(PresenceManager::new( self.presence_event_engine(), self.config.presence.heartbeat_interval.unwrap_or_default(), diff --git a/src/dx/pubnub_client.rs b/src/dx/pubnub_client.rs index 5886313c..582a6814 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -10,7 +10,6 @@ use derive_builder::Builder; use log::info; use spin::{Mutex, RwLock}; -use std::cmp::max; use uuid::Uuid; #[cfg(all( @@ -21,7 +20,7 @@ use uuid::Uuid; use crate::providers::futures_tokio::RuntimeTokio; #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] use crate::{ - core::{runtime::RuntimeSupport, Deserializer}, + core::runtime::RuntimeSupport, subscribe::{EventDispatcher, SubscriptionCursor, SubscriptionManager}, }; @@ -42,7 +41,7 @@ use crate::transport::TransportReqwest; use crate::core::RequestRetryConfiguration; use crate::{ - core::{CryptoProvider, PubNubEntity, PubNubError, Transport}, + core::{CryptoProvider, PubNubEntity, PubNubError}, lib::{ alloc::{ borrow::ToOwned, @@ -51,10 +50,13 @@ use crate::{ sync::Arc, }, collections::HashMap, - core::ops::{Deref, DerefMut}, + core::{ + cmp::max, + ops::{Deref, DerefMut}, + }, }, transport::middleware::{PubNubMiddleware, SignatureKeySet}, - Channel, ChannelGroup, ChannelMetadata, UuidMetadata, + Channel, ChannelGroup, ChannelMetadata, UserMetadata, }; /// PubNub client @@ -427,13 +429,13 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channel = client.create_channel("my_channel"); + /// let channel = client.channel("my_channel"); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_channel(&self, name: S) -> Channel + pub fn channel(&self, name: S) -> Channel where S: Into, { @@ -474,13 +476,13 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channel = client.create_channels(&["my_channel_1", "my_channel_2"]); + /// let channel = client.channels(&["my_channel_1", "my_channel_2"]); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_channels(&self, names: &[S]) -> Vec> + pub fn channels(&self, names: &[S]) -> Vec> where S: Into + Clone, { @@ -527,13 +529,13 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channel_group = client.create_channel_group("my_group"); + /// let channel_group = client.channel_group("my_group"); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_channel_group(&self, name: S) -> ChannelGroup + pub fn channel_group(&self, name: S) -> ChannelGroup where S: Into, { @@ -575,13 +577,13 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channel_groups = client.create_channel_groups(&["my_group_1", "my_group_2"]); + /// let channel_groups = client.channel_groups(&["my_group_1", "my_group_2"]); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_channel_groups(&self, names: &[S]) -> Vec> + pub fn channel_groups(&self, names: &[S]) -> Vec> where S: Into + Clone, { @@ -630,13 +632,13 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channel_metadata = client.create_channel_metadata("channel_meta"); + /// let channel_metadata = client.channel_metadata("channel_meta"); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_channel_metadata(&self, id: S) -> ChannelMetadata + pub fn channel_metadata(&self, id: S) -> ChannelMetadata where S: Into, { @@ -680,7 +682,7 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channels_metadata = client.create_channels_metadata( + /// let channels_metadata = client.channels_metadata( /// &["channel_meta_1", "channel_meta_2"] /// ); /// # Ok(()) @@ -688,7 +690,7 @@ impl PubNubClientInstance { /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_channels_metadata(&self, ids: &[S]) -> Vec> + pub fn channels_metadata(&self, ids: &[S]) -> Vec> where S: Into + Clone, { @@ -712,15 +714,15 @@ impl PubNubClientInstance { channels_metadata } - /// Creates a new uuid metadata object with the specified identifier. + /// Creates a new user metadata object with the specified identifier. /// /// # Arguments /// - /// * `id` - The identifier of the uuid metadata object as a string. + /// * `id` - The identifier of the user metadata object as a string. /// /// # Returns /// - /// Returns a `UuidMetadata` which can be used with the [`PubNub API`]. + /// Returns a `UserMetadata` which can be used with the [`PubNub API`]. /// /// # Example /// @@ -737,13 +739,13 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let uuid_metadata = client.create_uuid_metadata("uuid_meta"); + /// let user_metadata = client.user_metadata("user_meta"); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_uuid_metadata(&self, id: S) -> UuidMetadata + pub fn user_metadata(&self, id: S) -> UserMetadata where S: Into, { @@ -751,25 +753,25 @@ impl PubNubClientInstance { let id = id.into(); let entity = entities_slot .entry(format!("{}_uidm", &id)) - .or_insert(UuidMetadata::new(self, id).into()); + .or_insert(UserMetadata::new(self, id).into()); match entity { - PubNubEntity::UuidMetadata(uuid_metadata) => uuid_metadata.clone(), - _ => panic!("Unexpected entry type for UuidMetadata"), + PubNubEntity::UserMetadata(user_metadata) => user_metadata.clone(), + _ => panic!("Unexpected entry type for UserMetadata"), } } - /// Creates a list of uuid metadata objects with the specified identifier. + /// Creates a list of user metadata objects with the specified identifier. /// /// # Arguments /// - /// * `id` - A list of identifiers for the uuid metadata objects as a + /// * `id` - A list of identifiers for the user metadata objects as a /// string. /// /// # Returns /// - /// Returns a list of `UuidMetadata` which can be used with the [`PubNub - /// API`]. + /// Returns a list of `UserMetadata` which can be used with the + /// [`PubNub API`]. /// /// # Example /// @@ -786,34 +788,34 @@ impl PubNubClientInstance { /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let uuids_metadata = client.create_uuids_metadata(&["uuid_meta_1", "uuid_meta_2"]); + /// let users_metadata = client.users_metadata(&["user_meta_1", "user_meta_2"]); /// # Ok(()) /// # } /// ``` /// /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn create_uuids_metadata(&self, ids: &[S]) -> Vec> + pub fn users_metadata(&self, ids: &[S]) -> Vec> where S: Into + Clone, { - let mut uuids_metadata = Vec::with_capacity(ids.len()); + 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(UuidMetadata::new(self, id).into()); + .or_insert(UserMetadata::new(self, id).into()); match entity { - PubNubEntity::UuidMetadata(uuid_metadata) => { - uuids_metadata.push(uuid_metadata.clone()) + PubNubEntity::UserMetadata(user_metadata) => { + users_metadata.push(user_metadata.clone()) } - _ => panic!("Unexpected entry type for UuidMetadata"), + _ => panic!("Unexpected entry type for UserMetadata"), } } - uuids_metadata + users_metadata } /// Update currently used authentication token. @@ -879,18 +881,31 @@ impl PubNubClientInstance { impl PubNubClientInstance where - T: Transport + Send + Sync + 'static, - D: Deserializer + Send + Sync + 'static, + 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")] - if let Some(manager) = self.subscription_manager().read().as_ref() { - manager.terminate(); + { + 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")] - if let Some(manager) = self.presence_manager().read().as_ref() { - manager.terminate(); + { + 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; } } } @@ -1874,7 +1889,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. diff --git a/src/dx/subscribe/event_engine/mod.rs b/src/dx/subscribe/event_engine/mod.rs index f29da177..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; @@ -31,25 +28,3 @@ pub(in crate::dx::subscribe) mod types; pub(crate) type SubscribeEventEngine = EventEngine; - -impl - EventEngine -{ - 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/mod.rs b/src/dx/subscribe/mod.rs index c97c1925..c478afe8 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -84,7 +84,11 @@ pub use traits::{EventEmitter, EventSubscriber, Subscribable, SubscribableType, pub(crate) mod traits; #[cfg(feature = "std")] -impl PubNubClientInstance { +impl PubNubClientInstance +where + T: Transport + Send + 'static, + D: Deserializer + Send + 'static, +{ /// Stream used to notify connection state change events. pub fn status_stream(&self) -> DataStream { self.event_dispatcher.status_stream() @@ -96,7 +100,20 @@ impl PubNubClientInstance { /// /// * `status` - Current connection status. pub(crate) fn handle_status(&self, status: ConnectionStatus) { - self.event_dispatcher.handle_status(status) + 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. @@ -168,7 +185,7 @@ impl PubNubClientInstance { impl PubNubClientInstance where T: Transport + Send + 'static, - D: Deserializer + 'static, + D: Deserializer + Send + 'static, { /// Creates multiplexed subscriptions. /// @@ -214,7 +231,7 @@ where channels: Option<&[N]>, channel_groups: Option<&[N]>, options: Option>, - ) -> Arc> + ) -> SubscriptionSet where N: Into + Clone, { @@ -224,7 +241,7 @@ where channel_names .iter() .cloned() - .map(|name| self.create_channel(name).into()) + .map(|name| self.channel(name).into()) .collect::>>(), ); } @@ -233,7 +250,7 @@ where channel_group_names .iter() .cloned() - .map(|name| self.create_channel_group(name).into()) + .map(|name| self.channel_group(name).into()) .collect::>>(), ); } @@ -275,7 +292,7 @@ where #[cfg(feature = "presence")] let mut input: Option = None; - if let Some(manager) = self.subscription_manager().read().as_ref() { + if let Some(manager) = self.subscription_manager(false).read().as_ref() { #[cfg(feature = "presence")] { let current_input = manager.current_input(); @@ -303,7 +320,7 @@ where self.runtime.spawn(async { let _ = request.execute().await; }) - } else if let Some(presence) = self.presence_manager().read().as_ref() { + } else if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.disconnect(); } } @@ -345,7 +362,7 @@ where #[cfg(feature = "presence")] let mut input: Option = None; - if let Some(manager) = self.subscription_manager().read().as_ref() { + if let Some(manager) = self.subscription_manager(false).read().as_ref() { #[cfg(feature = "presence")] { let current_input = manager.current_input(); @@ -372,7 +389,7 @@ where self.runtime.spawn(async { let _ = request.execute().await; }) - } else if let Some(presence) = self.presence_manager().read().as_ref() { + } else if let Some(presence) = self.presence_manager(false).read().as_ref() { presence.reconnect(); } } @@ -384,7 +401,7 @@ where /// created [`Subscription`] and [`SubscriptionSet`]. pub fn unsubscribe_all(&self) { { - if let Some(manager) = self.subscription_manager().write().as_mut() { + if let Some(manager) = self.subscription_manager(false).write().as_mut() { manager.unregister_all() } } @@ -395,15 +412,29 @@ where /// 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) -> Arc>>> { + 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")] let heartbeat_self = self.clone(); #[cfg(feature = "presence")] @@ -549,7 +580,7 @@ where } fn emit_status(client: Self, status: &ConnectionStatus) { - if let Some(manager) = client.subscription_manager().read().as_ref() { + if let Some(manager) = client.subscription_manager(false).read().as_ref() { manager.notify_new_status(status) } } @@ -564,7 +595,7 @@ where messages }; - if let Some(manager) = client.subscription_manager().read().as_ref() { + if let Some(manager) = client.subscription_manager(false).read().as_ref() { manager.notify_new_messages(cursor, messages.clone()) } } diff --git a/src/dx/subscribe/result.rs b/src/dx/subscribe/result.rs index cfbd58f9..a393d901 100644 --- a/src/dx/subscribe/result.rs +++ b/src/dx/subscribe/result.rs @@ -532,6 +532,7 @@ impl TryFrom for SubscribeResult { } } +#[cfg(feature = "serde")] impl Envelope { /// Default message type. fn default_message_type() -> SubscribeMessageType { diff --git a/src/dx/subscribe/subscription.rs b/src/dx/subscribe/subscription.rs index 64307981..b1a0e091 100644 --- a/src/dx/subscribe/subscription.rs +++ b/src/dx/subscribe/subscription.rs @@ -22,7 +22,7 @@ use crate::{ core::{ cmp::PartialEq, fmt::{Debug, Formatter, Result}, - ops::{Deref, DerefMut}, + ops::{Add, Deref, DerefMut}, }, }, subscribe::{ @@ -53,7 +53,9 @@ use crate::{ /// # }) /// # .with_user_id("uuid") /// # .build()?; -/// let channel = client.create_channel("my_channel"); +/// 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])); @@ -79,34 +81,49 @@ use crate::{ /// # }) /// # .with_user_id("uuid") /// # .build()?; -/// let channels = client.create_channels(&["my_channel_1", "my_channel_2"]); -/// let subscription = channels[0].subscription(None).add(channels[1].subscription(None)); +/// 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 { + /// Subscription reference. + pub(super) inner: Arc>, +} + +/// 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 [`SubscriptionRef`] with the same `id`. + /// to the same reference of [`SubscriptionState`] with the same `id`. instance_id: String, - /// Subscription reference. - inner: Arc>, + /// Subscription state. + state: Arc>, /// Real-time event dispatcher. - event_dispatcher: EventDispatcher, + event_dispatcher: Arc, } -/// Subscription reference +/// Shared subscription state /// -/// This struct contains the actual subscription state. -/// It's wrapped in `Arc` by [`Subscription`] and uses interior mutability for -/// its internal 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 SubscriptionRef { +pub struct SubscriptionState { /// Unique event handler identifier. pub(super) id: String, @@ -135,9 +152,9 @@ pub struct SubscriptionRef { /// processing. options: Option>, - /// The list of weak references to all [`Subscription`] clones created for - /// this reference. - clones: RwLock>>>, + /// The list of weak references to all [`SubscriptionRef`] clones created + /// for this reference. + clones: RwLock>>>, } impl Subscription @@ -157,63 +174,15 @@ where /// /// # Returns /// - /// A new `Subscription2` for the given `entity` and `options`. + /// A new `Subscription` for the given `entity` and `options`. pub(crate) fn new( client: Weak>, entity: PubNubEntity, options: Option>, - ) -> Arc { - let subscription_ref = SubscriptionRef::new(client, entity, options); - let subscription_id = Uuid::new_v4().to_string(); - let subscription = Arc::new(Self { - instance_id: subscription_id.clone(), - inner: Arc::new(subscription_ref), - event_dispatcher: Default::default(), - }); - subscription.store_clone(subscription_id, Arc::downgrade(&subscription)); - subscription - } - - /// 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() - } - - /// Clones the [`Subscription`] and returns a new `Arc` reference to it. - /// - /// # Returns - /// - /// A new `Arc` reference to a cloned [`Subscription` ] instance. - /// - /// # Panics - /// - /// This method will panic if [`Subscription`] clone could not be found in - /// the reference counter storage or if there are no strong references - /// to the [`Subscription`] instance. - pub fn clone_arc(&self) -> Arc { - self.get_clone_by_id(&self.instance_id) - .expect("Subscription clone should be stored with SubscriptionRef") - .upgrade() - .expect("At least one strong reference should exist for Subscription") - .clone() + ) -> Self { + Self { + inner: SubscriptionRef::new(client, entity, options), + } } /// Creates a clone of the subscription set with an empty event dispatcher. @@ -239,7 +208,9 @@ where /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let channel = client.create_channel("my_channel"); + /// 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 @@ -254,44 +225,166 @@ where /// /// A new instance of the subscription object with an empty event /// dispatcher. + pub fn clone_empty(&self) -> Self { + Self { + inner: self.inner.clone_empty(), + } + } +} + +impl Deref for Subscription +where + T: Send + Sync, + D: Send + Sync, +{ + type Target = SubscriptionRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Subscription +where + T: Send + Sync, + D: 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: Send + Sync, + D: Send + Sync, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +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: Send + Sync, + D: 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(), - inner: Arc::clone(&self.inner), + state: Arc::clone(&self.state), event_dispatcher: Default::default(), }); self.store_clone(instance_id, Arc::downgrade(&instance)); instance } - /// Adds two [`Subscription`] and produce [`SubscriptionSet`]. - /// - /// # Arguments - /// - /// * `rhs` - The subscription to be added. + /// Retrieves the current timetoken value. /// /// # Returns /// - /// [`SubscriptionSet`] with added subscriptions in the set. + /// 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. /// - /// # Panics + /// # Returns /// - /// This function will panic if the current subscription set does not have - /// at least one clone of [`Subscription`] with strong reference to the - /// original. - pub fn add(&self, rhs: Arc) -> Arc> { - let options = self.options.clone(); - let lhs_clones = self.clones.read(); - let (_, lhs) = lhs_clones - .iter() - .next() - .expect("At least one clone of Subscription should exist."); - let lhs = lhs - .upgrade() - .clone() - .expect("At least one strong reference should exist for Subscription"); - SubscriptionSet::new_with_subscriptions(vec![lhs, rhs], options) + /// 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 @@ -323,61 +416,30 @@ where } } -impl Deref for Subscription +impl Deref for SubscriptionRef where T: Send + Sync, D: Send + Sync, { - type Target = SubscriptionRef; + type Target = SubscriptionState; fn deref(&self) -> &Self::Target { - &self.inner + &self.state } } -impl DerefMut for Subscription +impl DerefMut for SubscriptionRef where T: Send + Sync, D: 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 PartialEq for Subscription -where - T: Send + Sync, - D: 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 - ) + Arc::get_mut(&mut self.state) + .expect("Multiple mutable references to the SubscriptionRef are not allowed") } } -impl EventSubscriber for Subscription +impl EventSubscriber for SubscriptionRef where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, @@ -402,7 +464,7 @@ where } if let Some(client) = self.client().upgrade().clone() { - if let Some(manager) = client.subscription_manager().write().as_mut() { + if let Some(manager) = client.subscription_manager(true).write().as_mut() { // Mark entities as "in use" by subscription. self.entity.increase_subscriptions_count(); @@ -424,7 +486,7 @@ where } if let Some(client) = self.client().upgrade().clone() { - if let Some(manager) = client.subscription_manager().write().as_mut() { + if let Some(manager) = client.subscription_manager(false).write().as_mut() { // Mark entities as "not in-use" by subscription. self.entity.decrease_subscriptions_count(); @@ -437,7 +499,7 @@ where } } -impl EventHandler for Subscription +impl EventHandler for SubscriptionRef where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, @@ -507,7 +569,7 @@ where } } -impl EventEmitter for Subscription +impl EventEmitter for SubscriptionRef where T: Send + Sync, D: Send + Sync, @@ -541,7 +603,7 @@ where } } -impl SubscriptionRef +impl SubscriptionState where T: Send + Sync, D: Send + Sync, @@ -550,7 +612,7 @@ where client: Weak>, entity: PubNubEntity, options: Option>, - ) -> SubscriptionRef { + ) -> SubscriptionState { let is_channel_type = matches!(entity.r#type(), SubscribableType::Channel); let with_presence = if let Some(options) = &options { options @@ -578,32 +640,18 @@ where } } - /// Store a clone of a [`Subscription`] instance with a given instance ID. + /// 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 instance to store + /// * `instance` - The weak reference to the subscription reference to store /// as a clone. - fn store_clone(&self, instance_id: String, instance: Weak>) { + 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)); } - - /// Retrieves a cloned instance of a [`Subscription`] by its `instance_id`. - /// - /// # Arguments - /// - /// * `instance_id` - A reference to the unique identifier of the instance. - /// - /// # Returns - /// - /// An `Option` containing a weak reference to the cloned [`Subscription`] - /// instance if found, or `None` if no instance with the specified - /// `instance_id` exists. - fn get_clone_by_id(&self, instance_id: &String) -> Option>> { - self.clones.read().get(instance_id).cloned() - } } #[cfg(test)] @@ -627,11 +675,19 @@ mod it_should { 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")); diff --git a/src/dx/subscribe/subscription_manager.rs b/src/dx/subscribe/subscription_manager.rs index 01159b79..1e261f4b 100644 --- a/src/dx/subscribe/subscription_manager.rs +++ b/src/dx/subscribe/subscription_manager.rs @@ -5,6 +5,7 @@ use spin::RwLock; +use crate::core::{Deserializer, Transport}; use crate::subscribe::traits::EventHandler; use crate::{ dx::subscribe::{ @@ -120,8 +121,8 @@ pub(crate) struct SubscriptionManagerRef { impl SubscriptionManagerRef where - T: Send + Sync, - D: Send + Sync, + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, { pub fn notify_new_status(&self, status: &ConnectionStatus) { if let Some(client) = self.client() { @@ -174,7 +175,7 @@ where pub fn update( &self, event_handler: &Weak + Send + Sync>, - removed: Option<&[Arc>]>, + removed: Option<&[Subscription]>, ) { let Some(upgraded_event_handler) = event_handler.upgrade().clone() else { return; @@ -220,11 +221,19 @@ where self.change_subscription(Some(&upgraded_event_handler.subscription_input(false))); } - // TODO: why call it on drop fails tests? pub fn unregister_all(&mut self) { let inputs = self.current_input(); + + // Invalidate current event handler state (subscribed and entity usage). { - self.event_handlers.write().clear(); + 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)); @@ -256,6 +265,16 @@ where .sum() } + /// 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 @@ -412,9 +431,9 @@ mod should { #[cfg(feature = "presence")] Arc::new(|_, _| {}), ); - let channel = client.create_channel("test"); + let channel = client.channel("test"); let subscription = channel.subscription(None); - let weak_subscription = &Arc::downgrade(&subscription); + let weak_subscription = &Arc::downgrade(&subscription.inner); let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); manager.register(&weak_handler, None); @@ -435,9 +454,9 @@ mod should { assert_eq!(channels.unwrap().len(), 1); }), ); - let channel = client.create_channel("test"); + let channel = client.channel("test"); let subscription = channel.subscription(None); - let weak_subscription = &Arc::downgrade(&subscription); + let weak_subscription = &Arc::downgrade(&subscription.inner); let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); manager.register(&weak_handler, None); @@ -457,9 +476,9 @@ mod should { Arc::new(|_, _| {}), ); let cursor: SubscriptionCursor = "15800701771129796".to_string().into(); - let channel = client.create_channel("test"); + let channel = client.channel("test"); let subscription = channel.subscription(None); - let weak_subscription = Arc::downgrade(&subscription); + let weak_subscription = Arc::downgrade(&subscription.inner); let weak_handler: Weak + Send + Sync> = weak_subscription.clone(); // Simulate `.subscribe()` call. diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs index 63cfdfec..0aacb5e5 100644 --- a/src/dx/subscribe/subscription_set.rs +++ b/src/dx/subscribe/subscription_set.rs @@ -5,7 +5,6 @@ //! attach listeners to the specific event types. use spin::RwLock; -use std::collections::HashMap; use uuid::Uuid; use crate::core::{Deserializer, Transport}; @@ -20,9 +19,10 @@ use crate::{ vec, vec::Vec, }, + collections::HashMap, core::{ fmt::{Debug, Formatter, Result}, - ops::{Deref, DerefMut}, + ops::{Add, AddAssign, Deref, DerefMut, Sub, SubAssign}, }, }, subscribe::{ @@ -76,30 +76,48 @@ use crate::{ /// # }) /// # .with_user_id("uuid") /// # .build()?; -/// let channels = client.create_channels(&["my_channel_1", "my_channel_2"]); -/// let subscription = channels[0].subscription(None).add(channels[1].subscription(None)); +/// 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>, +} + +/// 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 [`SubscriptionSetRef`] with the same - /// `id`. + /// bound to the same reference of [`SubscriptionSetState`] with the + /// same `id`. pub(super) instance_id: String, /// Subscriptions set reference. - inner: Arc>, + state: Arc>, /// Real-time event dispatcher. event_dispatcher: EventDispatcher, } -/// Entities subscriptions set reference. +/// 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 @@ -107,7 +125,7 @@ pub struct SubscriptionSet< /// /// Not intended to be used directly. Use [`SubscriptionSet`] instead. #[derive(Debug)] -pub struct SubscriptionSetRef< +pub struct SubscriptionSetState< T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, > { @@ -118,7 +136,7 @@ pub struct SubscriptionSetRef< pub(super) client: Weak>, /// Grouped subscriptions list. - pub(crate) subscriptions: RwLock>>>, + pub(crate) subscriptions: RwLock>>, /// Whether set is currently subscribed and active. pub(super) is_subscribed: Arc>, @@ -138,7 +156,7 @@ pub struct SubscriptionSetRef< /// The list of weak references to all [`SubscriptionSet`] clones created /// for this reference. - clones: RwLock>>>, + clones: RwLock>>>, } impl SubscriptionSet @@ -162,16 +180,13 @@ where 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) + ) -> Self { + Self { + inner: SubscriptionSetRef::new(entities, options), + } } - /// Create subscription set from given subscriptions list. + /// Create subscription set reference from given subscriptions list. /// /// # Arguments /// @@ -188,41 +203,12 @@ where /// /// This function will panic if the `subscriptions` vector is empty. pub fn new_with_subscriptions( - subscriptions: Vec>>, + subscriptions: Vec>, options: Option>, - ) -> Arc { - let subscription = subscriptions - .first() - .expect("At least one subscription expected."); - let subscription_set_ref = - SubscriptionSetRef::new(subscription.client(), subscriptions, options); - let subscription_set_id = Uuid::new_v4().to_string(); - let subscription_set = Arc::new(Self { - instance_id: subscription_set_id.clone(), - inner: Arc::new(subscription_set_ref), - event_dispatcher: Default::default(), - }); - subscription_set.store_clone(subscription_set_id, Arc::downgrade(&subscription_set)); - subscription_set - } - - /// Creates a clone of the [`SubscriptionSet`] and returns it as an `Arc`. - /// - /// # Returns - /// - /// A new `Arc` reference to a cloned [`SubscriptionSet` ] instance. - /// - /// # Panics - /// - /// This method will panic if [`SubscriptionSet`] clone could not be found - /// in the reference counter storage or if there are no strong - /// references to the [`SubscriptionSet`] instance. - pub fn clone_arc(&self) -> Arc { - self.get_clone_by_id(&self.instance_id) - .expect("SubscriptionSet clone should be stored with SubscriptionSetRef") - .upgrade() - .expect("At least one strong reference should exist for SubscriptionSet") - .clone() + ) -> Self { + Self { + inner: SubscriptionSetRef::new_with_subscriptions(subscriptions, options), + } } /// Creates a clone of the subscription set with an empty event dispatcher. @@ -262,77 +248,164 @@ where /// /// 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(), - inner: Arc::clone(&self.inner), - event_dispatcher: Default::default(), - }); - self.store_clone(instance_id, Arc::downgrade(&instance)); - instance + pub fn clone_empty(&self) -> Self { + Self { + inner: self.inner.clone_empty(), + } } - /// Adds the [`Subscription`] from another [`SubscriptionSet`] to the - /// current [`SubscriptionSet`]. + /// Aggregate subscriptions' input. /// /// # Arguments /// - /// * `rhs` - Another instance of [`SubscriptionSet`], whose subscriptions - /// will be added. - pub fn add_assign(&self, rhs: Arc) { - self.add_subscriptions(rhs.subscriptions.read().clone()); - } - - /// Subtracts the [`Subscription`] of the given [`SubscriptionSet`] from the - /// current [`SubscriptionSet`]. + /// * `subscriptions` - A slice of `Subscription` representing a list + /// of subscriptions. + /// * `include_inactive` - Whether _unused_ entities should be included into + /// the subscription input or not. /// - /// # Arguments + /// # Returns /// - /// * `rhs` - Another instance of [`SubscriptionSet`], whose subscriptions - /// should be subtracted. - pub fn sub_assign(&self, rhs: Arc) { - self.sub_subscriptions(rhs.subscriptions.read().clone()); + /// `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 } - /// Add more managed subscriptions. + /// Filter unique subscriptions. /// - /// After the [`Subscription`] list is added, [`SubscriptionSet`] listeners - /// will start notifying them about real-time events from newly added - /// subscriptions. + /// Filter out duplicates and subscriptions which is already part of the + /// `set`. /// /// # Arguments /// - /// * `subscriptions` - A vector of [`Subscription`] items to be added. - /// - /// # Example + /// * `set` - An optional reference to a subscription set. + /// * `subscriptions` - Vector of [`Subscription`] which should be filtered. /// - /// ```rust - /// use pubnub::{ - /// subscribe::{Subscriber, Subscription}, - /// Keyset, PubNubClient, PubNubClientBuilder, - /// }; + /// # Returns /// - /// # #[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.create_channels(&["my_channel_3", "my_channel_4"]); - /// let mut subscription = client.subscription(Some(&["my_channel_1", "my_channel_2"]), None, None); - /// subscription.add_subscriptions(vec![channels[0].subscription(None), channels[1].subscription(None)]); - /// # Ok(()) - /// # } - /// ``` - pub fn add_subscriptions(&self, subscriptions: Vec>>) { - let unique_subscriptions = - SubscriptionSet::unique_subscriptions_from_list(Some(self), subscriptions); + /// 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(), + } + } +} + +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); @@ -350,8 +423,7 @@ where return; }; - // let manager = client.subscription_manager(); - if let Some(manager) = client.subscription_manager().write().as_mut() { + 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(); @@ -364,46 +436,39 @@ where } }; } +} +impl Sub for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + type Output = SubscriptionSet; - /// Remove managed subscriptions. - /// - /// After the [`Subscription`] list is removed, [`SubscriptionSet`] - /// listeners will stop receiving real-time updates from removed - /// subscriptions. - /// - /// # Arguments - /// - /// * `subscriptions` - A vector of [`Subscription`] items to be removed. - /// - /// # Example - /// - /// ```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.create_channels(&["my_channel_2", "my_channel_3"]); - /// let mut subscription = client.subscription(Some(&["my_channel_1", "my_channel_2", "my_channel_3", "my_channel_4"]), None, None); - /// subscription.sub_subscriptions(vec![channels[0].subscription(None), channels[1].subscription(None)]); - /// # Ok(()) - /// # } - /// ``` - pub fn sub_subscriptions(&self, subscriptions: Vec>>) { - let removed: Vec>> = { + 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, subscriptions) + 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() @@ -425,12 +490,12 @@ where return; }; - if let Some(manager) = client.subscription_manager().write().as_mut() { - // Mark entities as "not in-use" by subscription. - removed.iter().for_each(|subscription| { - subscription.entity.decrease_subscriptions_count(); - }); + // 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(); @@ -438,6 +503,122 @@ where } }; } +} + +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> { + /// 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(Some(&["my_channel_1", "my_channel_2"]), Some(&["my_group"]), 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. /// @@ -489,117 +670,32 @@ where .cloned() .collect::>() } - - /// 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: &[Arc>], - 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 +impl Deref for SubscriptionSetRef where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { - type Target = SubscriptionSetRef; + type Target = SubscriptionSetState; fn deref(&self) -> &Self::Target { - &self.inner + &self.state } } -impl DerefMut for SubscriptionSet +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.inner) - .expect("Multiple mutable references to the SubscriptionSet are not allowed") - } -} - -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 - ) + Arc::get_mut(&mut self.state) + .expect("Multiple mutable references to the SubscriptionSetRef are not allowed") } } -impl EventSubscriber for SubscriptionSet +impl EventSubscriber for SubscriptionSetRef where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, @@ -627,18 +723,20 @@ where return; }; - let manager = client.subscription_manager(); - 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); - } - }; + { + 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) { @@ -654,21 +752,23 @@ where return; }; - if let Some(manager) = client.subscription_manager().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); - } - }; + { + 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 SubscriptionSet +impl EventHandler for SubscriptionSetRef where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, @@ -702,7 +802,7 @@ where } fn subscription_input(&self, include_inactive: bool) -> SubscriptionInput { - Self::subscription_input_from_list(&self.subscriptions.read(), include_inactive) + SubscriptionSet::subscription_input_from_list(&self.subscriptions.read(), include_inactive) } fn invalidate(&self) { @@ -717,7 +817,7 @@ where self.subscriptions .read() .iter() - .for_each(|subscription| subscription.entity.decrease_subscriptions_count()); + .for_each(|subscription| subscription.invalidate()); self.event_dispatcher.invalidate(); } @@ -731,7 +831,7 @@ where } } -impl EventEmitter for SubscriptionSet +impl EventEmitter for SubscriptionSetRef where T: Transport + Send + Sync, D: Deserializer + Send + Sync, @@ -765,16 +865,16 @@ where } } -impl SubscriptionSetRef +impl SubscriptionSetState where T: Transport + Send + Sync + 'static, D: Deserializer + Send + Sync + 'static, { fn new( client: Weak>, - subscriptions: Vec>>, + subscriptions: Vec>, options: Option>, - ) -> SubscriptionSetRef { + ) -> SubscriptionSetState { Self { id: Uuid::new_v4().to_string(), client, @@ -793,7 +893,7 @@ where } } - /// Store a clone of a [`SubscriptionSet`] instance with a given instance + /// Store a clone of a [`SubscriptionSetRef`] instance with a given instance /// ID. /// /// # Arguments @@ -801,26 +901,10 @@ where /// * `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>) { + 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)); } - - /// Retrieves a cloned instance of a [`SubscriptionSet`] by its - /// `instance_id`. - /// - /// # Arguments - /// - /// * `instance_id` - A reference to the unique identifier of the instance. - /// - /// # Returns - /// - /// An `Option` containing a weak reference to the cloned - /// [`SubscriptionSet`] instance if found, or `None` if no instance with - /// the specified `instance_id` exists. - fn get_clone_by_id(&self, instance_id: &String) -> Option>> { - self.clones.read().get(instance_id).cloned() - } } #[cfg(test)] @@ -909,31 +993,28 @@ mod it_should { let client = Arc::new(client()); let channels_1_subscriptions = vec!["channel_1", "channel_2"] .into_iter() - .map(|name| client.create_channel(name).subscription(None)) - .collect::>>>(); + .map(|name| client.channel(name).subscription(None)) + .collect::>>(); let channels_2_subscriptions = vec!["channel_3", "channel_4"] .into_iter() - .map(|name| client.create_channel(name).subscription(None)) - .collect::>>>(); + .map(|name| client.channel(name).subscription(None)) + .collect::>>(); let channels_3_subscriptions = vec![ channels_1_subscriptions[0].clone(), channels_2_subscriptions[1].clone(), ]; - let subscription_set_1 = channels_1_subscriptions[0] - .clone() - .add(channels_1_subscriptions[1].clone()); - let subscription_set_2 = channels_2_subscriptions[0] - .clone() - .add(channels_2_subscriptions[1].clone()); - let subscription_set_3 = channels_3_subscriptions[0] - .clone() - .add(channels_3_subscriptions[1].clone()); - - subscription_set_1.add_assign(subscription_set_2); + 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.sub_assign(subscription_set_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/subscribable.rs b/src/dx/subscribe/traits/subscribable.rs index 976bad3b..019362e2 100644 --- a/src/dx/subscribe/traits/subscribable.rs +++ b/src/dx/subscribe/traits/subscribable.rs @@ -14,7 +14,7 @@ use crate::{ /// /// Subscribable can be separated by their place in subscribe REST API: /// * `URI path` - channel-like objects which represent single entity -/// ([`Channel`], [`ChannelMetadata`], [`UuidMetadata`]) +/// ([`Channel`], [`ChannelMetadata`], [`UserMetadata`]) /// * `query parameter` - entities which represent group of entities /// ([`ChannelGroup`]) pub enum SubscribableType { diff --git a/src/dx/subscribe/traits/subscriber.rs b/src/dx/subscribe/traits/subscriber.rs index 659e075c..a4c7591a 100644 --- a/src/dx/subscribe/traits/subscriber.rs +++ b/src/dx/subscribe/traits/subscriber.rs @@ -4,7 +4,7 @@ //! which is used by types to provide ability to subscribe for real-time events. use crate::{ - lib::alloc::{sync::Arc, vec::Vec}, + lib::alloc::vec::Vec, subscribe::{Subscription, SubscriptionOptions}, }; @@ -21,5 +21,5 @@ pub trait Subscriber { /// /// A [`Subscription`] object representing the newly created subscription to /// receiver's data stream events. - fn subscription(&self, options: Option>) -> Arc>; + fn subscription(&self, options: Option>) -> Subscription; } diff --git a/src/lib.rs b/src/lib.rs index a2f729d5..4076f5ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,7 +255,7 @@ pub use dx::PubNubClient; pub use core::RequestRetryConfiguration; #[doc(inline)] -pub use core::{Channel, ChannelGroup, ChannelMetadata, UuidMetadata}; +pub use core::{Channel, ChannelGroup, ChannelMetadata, UserMetadata}; pub mod core; pub mod dx; pub mod providers; diff --git a/src/transport/reqwest.rs b/src/transport/reqwest.rs index 9eb5a4ce..18bb30fc 100644 --- a/src/transport/reqwest.rs +++ b/src/transport/reqwest.rs @@ -81,12 +81,6 @@ impl Transport for TransportReqwest { "Sending data to pubnub: {} {:?} {}", request.method, request.headers, request_url ); - log::debug!( - "Sending data to pubnub: {} {:?} {}", - request.method, - request.headers, - request_url - ); let headers = prepare_headers(&request.headers)?; #[cfg(feature = "std")] diff --git a/tests/common/common_steps.rs b/tests/common/common_steps.rs index 0e2132df..466878e7 100644 --- a/tests/common/common_steps.rs +++ b/tests/common/common_steps.rs @@ -15,7 +15,6 @@ use pubnub::{ Keyset, PubNubClient, PubNubClientBuilder, }; use std::fmt::Debug; -use std::sync::Arc; /// Type of resource for which permissions currently configured. #[derive(Default, Debug)] @@ -144,9 +143,9 @@ pub struct PubNubWorld { pub keyset: pubnub::Keyset, pub publish_result: Result, pub subscription: - Option, DeserializerSerde>>>, + Option, DeserializerSerde>>, pub subscriptions: Option< - HashMap, DeserializerSerde>>>, + HashMap, DeserializerSerde>>, >, pub retry_policy: Option, pub heartbeat_value: Option, diff --git a/tests/contract_test.rs b/tests/contract_test.rs index 1bbe07aa..d2245ef8 100644 --- a/tests/contract_test.rs +++ b/tests/contract_test.rs @@ -1,8 +1,6 @@ use cucumber::{writer, World, WriterExt}; use std::fs::{create_dir_all, read_to_string, File, OpenOptions}; -use std::io::Write; use std::process; -use uuid::Uuid; mod access; mod common; diff --git a/tests/presence/presence_steps.rs b/tests/presence/presence_steps.rs index 021fc4a7..8a7e5006 100644 --- a/tests/presence/presence_steps.rs +++ b/tests/presence/presence_steps.rs @@ -130,7 +130,7 @@ async fn join( .fold(HashMap::new(), |mut acc, channel| { acc.insert( channel.clone(), - client.create_channel(channel).subscription(options.clone()), + client.channel(channel).subscription(options.clone()), ); acc }); @@ -155,7 +155,7 @@ async fn leave( channel_b: String, with_presence: String, ) { - let subscription = world.subscription.clone().unwrap(); + let mut subscription = world.subscription.clone().unwrap(); let subscriptions = world.subscriptions.clone().unwrap().iter().fold( vec![], |mut acc, (channel, subscription)| { @@ -166,7 +166,7 @@ async fn leave( }, ); - subscription.sub_subscriptions(subscriptions); + subscription -= SubscriptionSet::new_with_subscriptions(subscriptions, None); } #[then("I wait for getting Presence joined events")] From 8e18cb3ef72749c37360a2534f747fda05497d69 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 01:14:44 +0200 Subject: [PATCH 04/11] fix(test): fix thumbv7m target test --- examples/no_std/src/subscribe.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) 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), }; From e21b5b69e997f8d7844756c66c2daa9ad9393d8e Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 11:55:45 +0200 Subject: [PATCH 05/11] refactor(example): added explanation about subscription and scope Added information about how `Subscription` and `SubscriptionSet` instance leaving the scope can affect subscription loop. refactor(presence): leave from all channels trigger different event Made changes which will trigger different event when unsubscribing from all channels and groups. --- examples/subscribe.rs | 32 +++++++++++------- src/dx/subscribe/mod.rs | 22 ++++++------ src/dx/subscribe/subscription.rs | 43 ++++++++++++++++++------ src/dx/subscribe/subscription_manager.rs | 27 +++++++++------ src/dx/subscribe/subscription_set.rs | 20 +++++++++++ src/dx/subscribe/traits/subscriber.rs | 3 +- 6 files changed, 103 insertions(+), 44 deletions(-) diff --git a/examples/subscribe.rs b/examples/subscribe.rs index ee9908b2..14dad7b5 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -75,23 +75,23 @@ async fn main() -> Result<(), Box> { 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), + Ok(message) => println!("(a) defined message: {:?}", message), Err(_) => { - println!("other message: {:?}", String::from_utf8(message.data)) + println!("(a) other message: {:?}", String::from_utf8(message.data)) } } } Update::Presence(presence) => { - println!("presence: {:?}", presence) + println!("(a) presence: {:?}", presence) } Update::AppContext(object) => { - println!("object: {:?}", object) + println!("(a) object: {:?}", object) } Update::MessageAction(action) => { - println!("message action: {:?}", action) + println!("(a) message action: {:?}", action) } Update::File(file) => { - println!("file: {:?}", file) + println!("(a) file: {:?}", file) } } })); @@ -101,23 +101,23 @@ async fn main() -> Result<(), Box> { 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), + Ok(message) => println!("(b) defined message: {:?}", message), Err(_) => { - println!("other message: {:?}", String::from_utf8(message.data)) + println!("(b) other message: {:?}", String::from_utf8(message.data)) } } } Update::Presence(presence) => { - println!("~~~~~> presence: {:?}", presence) + println!("(b) presence: {:?}", presence) } Update::AppContext(object) => { - println!("~~~~~> object: {:?}", object) + println!("(b) object: {:?}", object) } Update::MessageAction(action) => { - println!("~~~~~> message action: {:?}", action) + println!("(b) message action: {:?}", action) } Update::File(file) => { - println!("~~~~~> file: {:?}", file) + println!("(b) file: {:?}", file) } } })); @@ -131,15 +131,23 @@ async fn main() -> Result<(), Box> { // You can also cancel the subscription at any time. // 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; diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index c478afe8..88a8c17f 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -405,9 +405,6 @@ where manager.unregister_all() } } - - #[cfg(feature = "presence")] - self.announce_left_all(); } /// Subscription manager which maintains Subscription EE. @@ -443,12 +440,12 @@ where *slot = Some(SubscriptionManager::new( self.subscribe_event_engine(), #[cfg(feature = "presence")] - Arc::new(move |channels, groups| { + Arc::new(move |channels, groups, _all| { Self::subscribe_heartbeat_call(heartbeat_self.clone(), channels, groups); }), #[cfg(feature = "presence")] - Arc::new(move |channels, groups| { - Self::subscribe_leave_call(leave_self.clone(), channels, groups); + Arc::new(move |channels, groups, all| { + Self::subscribe_leave_call(leave_self.clone(), channels, groups, all); }), )); } @@ -572,11 +569,16 @@ where client: Self, channels: Option>, channel_groups: Option>, + all: bool, ) { - client.announce_left( - Self::presence_filtered_entries(channels), - Self::presence_filtered_entries(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: &ConnectionStatus) { diff --git a/src/dx/subscribe/subscription.rs b/src/dx/subscribe/subscription.rs index b1a0e091..610d9e24 100644 --- a/src/dx/subscribe/subscription.rs +++ b/src/dx/subscribe/subscription.rs @@ -22,7 +22,7 @@ use crate::{ core::{ cmp::PartialEq, fmt::{Debug, Formatter, Result}, - ops::{Add, Deref, DerefMut}, + ops::{Add, Deref, DerefMut, Drop}, }, }, subscribe::{ @@ -89,7 +89,10 @@ use crate::{ /// # Ok(()) /// # } /// ``` -pub struct Subscription { +pub struct Subscription< + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +> { /// Subscription reference. pub(super) inner: Arc>, } @@ -234,8 +237,8 @@ where impl Deref for Subscription where - T: Send + Sync, - D: Send + Sync, + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, { type Target = SubscriptionRef; @@ -246,8 +249,8 @@ where impl DerefMut for Subscription where - T: Send + Sync, - D: Send + Sync, + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, { fn deref_mut(&mut self) -> &mut Self::Target { Arc::get_mut(&mut self.inner) @@ -257,8 +260,8 @@ where impl Clone for Subscription where - T: Send + Sync, - D: Send + Sync, + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, { fn clone(&self) -> Self { Self { @@ -267,6 +270,26 @@ where } } +impl Drop for Subscription +where + T: Transport + Send + Sync + 'static, + D: Deserializer + Send + Sync + 'static, +{ + fn drop(&mut self) { + // 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() { + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.unregister(&handler); + } + } + } +} + impl Add for Subscription where T: Transport + Send + Sync + 'static, @@ -284,8 +307,8 @@ where impl PartialEq for Subscription where - T: Send + Sync, - D: Send + Sync, + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, { fn eq(&self, other: &Self) -> bool { self.id.eq(&other.id) diff --git a/src/dx/subscribe/subscription_manager.rs b/src/dx/subscribe/subscription_manager.rs index 1e261f4b..93cc16aa 100644 --- a/src/dx/subscribe/subscription_manager.rs +++ b/src/dx/subscribe/subscription_manager.rs @@ -31,7 +31,7 @@ use crate::{ #[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. /// @@ -296,12 +296,17 @@ where #[cfg(feature = "presence")] { - (!inputs.is_empty && removed.is_none()) - .then(|| self.heartbeat_call.as_ref()(channels.clone(), channel_groups.clone())); + (!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()); + self.leave_call.as_ref()( + removed.channels(), + removed.channel_groups(), + inputs.is_empty, + ); } } } @@ -318,7 +323,7 @@ where #[cfg(feature = "presence")] if !inputs.is_empty { - self.heartbeat_call.as_ref()(inputs.channels(), inputs.channel_groups()); + self.heartbeat_call.as_ref()(inputs.channels(), inputs.channel_groups(), false); } self.event_engine @@ -424,12 +429,12 @@ mod should { let mut manager = SubscriptionManager::new( event_engine(), #[cfg(feature = "presence")] - Arc::new(|channels, _| { + Arc::new(|channels, _, _| { assert!(channels.is_some()); assert_eq!(channels.unwrap().len(), 1); }), #[cfg(feature = "presence")] - Arc::new(|_, _| {}), + Arc::new(|_, _, _| {}), ); let channel = client.channel("test"); let subscription = channel.subscription(None); @@ -447,9 +452,9 @@ mod should { let mut manager = SubscriptionManager::new( event_engine(), #[cfg(feature = "presence")] - Arc::new(|_, _| {}), + Arc::new(|_, _, _| {}), #[cfg(feature = "presence")] - Arc::new(|channels, _| { + Arc::new(|channels, _, _| { assert!(channels.is_some()); assert_eq!(channels.unwrap().len(), 1); }), @@ -471,9 +476,9 @@ mod should { let mut manager = SubscriptionManager::new( event_engine(), #[cfg(feature = "presence")] - Arc::new(|_, _| {}), + Arc::new(|_, _, _| {}), #[cfg(feature = "presence")] - Arc::new(|_, _| {}), + Arc::new(|_, _, _| {}), ); let cursor: SubscriptionCursor = "15800701771129796".to_string().into(); let channel = client.channel("test"); diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs index 0aacb5e5..05a7699b 100644 --- a/src/dx/subscribe/subscription_set.rs +++ b/src/dx/subscribe/subscription_set.rs @@ -355,6 +355,26 @@ where } } +impl Drop for SubscriptionSet +where + T: Transport + Send + Sync, + D: Deserializer + Send + Sync, +{ + fn drop(&mut self) { + // 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() { + if let Some((_, handler)) = self.clones.read().iter().next() { + let handler: Weak + Send + Sync> = handler.clone(); + manager.unregister(&handler); + } + } + } +} + impl Debug for SubscriptionSet where T: Transport + Send + Sync + 'static, diff --git a/src/dx/subscribe/traits/subscriber.rs b/src/dx/subscribe/traits/subscriber.rs index a4c7591a..a94abee1 100644 --- a/src/dx/subscribe/traits/subscriber.rs +++ b/src/dx/subscribe/traits/subscriber.rs @@ -4,12 +4,13 @@ //! 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 { +pub trait Subscriber { /// Creates a new subscription with the specified options. /// /// # Arguments From 5269c953b70a1a7dd4d5e3c7cb20f20c3b30256c Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 13:07:14 +0200 Subject: [PATCH 06/11] fix: fix issue with clone unregister Make sure that `drop()` won't trigger `unregister` for `Subscription` and `SubscriptionSet` clones (which has been created with `Clone::clone()`). --- src/dx/subscribe/subscription.rs | 17 ++++++++++++++++- src/dx/subscribe/subscription_set.rs | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/dx/subscribe/subscription.rs b/src/dx/subscribe/subscription.rs index 610d9e24..5ee0370e 100644 --- a/src/dx/subscribe/subscription.rs +++ b/src/dx/subscribe/subscription.rs @@ -95,6 +95,9 @@ pub struct Subscription< > { /// Subscription reference. pub(super) inner: Arc>, + + /// Whether subscription is `Clone::clone()` method call result or not. + is_clone: bool, } /// Subscription reference @@ -185,6 +188,7 @@ where ) -> Self { Self { inner: SubscriptionRef::new(client, entity, options), + is_clone: false, } } @@ -231,6 +235,7 @@ where pub fn clone_empty(&self) -> Self { Self { inner: self.inner.clone_empty(), + is_clone: false, } } } @@ -266,6 +271,7 @@ where fn clone(&self) -> Self { Self { inner: self.inner.clone(), + is_clone: true, } } } @@ -276,13 +282,22 @@ where 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() { - if let Some((_, handler)) = self.clones.read().iter().next() { + 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); } diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs index 05a7699b..52c58d45 100644 --- a/src/dx/subscribe/subscription_set.rs +++ b/src/dx/subscribe/subscription_set.rs @@ -90,6 +90,9 @@ pub struct SubscriptionSet< > { /// 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. @@ -183,6 +186,7 @@ where ) -> Self { Self { inner: SubscriptionSetRef::new(entities, options), + is_clone: false, } } @@ -208,6 +212,7 @@ where ) -> Self { Self { inner: SubscriptionSetRef::new_with_subscriptions(subscriptions, options), + is_clone: false, } } @@ -251,6 +256,7 @@ where pub fn clone_empty(&self) -> Self { Self { inner: self.inner.clone_empty(), + is_clone: false, } } @@ -351,6 +357,7 @@ where fn clone(&self) -> Self { Self { inner: self.inner.clone(), + is_clone: true, } } } @@ -361,13 +368,22 @@ where 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() { - if let Some((_, handler)) = self.clones.read().iter().next() { + 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); } From a27211dfd3049668e1c98471a9834e23f2e90445 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 13:38:49 +0200 Subject: [PATCH 07/11] feat: add user facing `SubscriptionParams` type Add new type to make it clear which parameters should be passed to `client.subscription(...)`. --- examples/subscribe.rs | 12 +++--- src/dx/subscribe/mod.rs | 64 +++++++++++++++------------- src/dx/subscribe/subscription_set.rs | 21 +++++++-- src/dx/subscribe/types.rs | 15 +++++++ src/lib.rs | 8 +++- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/examples/subscribe.rs b/examples/subscribe.rs index 14dad7b5..fd9ab7c4 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -4,7 +4,7 @@ use futures::StreamExt; use serde::Deserialize; use std::env; -use pubnub::subscribe::SubscriptionOptions; +use pubnub::subscribe::{SubscriptionOptions, SubscriptionParams}; use pubnub::{ dx::subscribe::Update, subscribe::{EventEmitter, EventSubscriber}, @@ -55,11 +55,11 @@ async fn main() -> Result<(), Box> { tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - let subscription = client.subscription( - Some(&["my_channel", "other_channel"]), - None, - Some(vec![SubscriptionOptions::ReceivePresenceEvents]), - ); + 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(); diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index 88a8c17f..d03864ae 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -35,9 +35,7 @@ use crate::{ #[cfg(all(feature = "presence", feature = "std"))] use event_engine::SubscriptionInput; #[cfg(feature = "std")] -use event_engine::{ - types::SubscriptionParams, SubscribeEffectHandler, SubscribeEventEngine, SubscribeState, -}; +use event_engine::{SubscribeEffectHandler, SubscribeEventEngine, SubscribeState}; #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] pub(crate) mod event_engine; @@ -191,8 +189,7 @@ where /// /// # Arguments /// - /// * `entities` - A `Vec` of known subscribable entities. - /// * `options` - Optional subscription options. + /// * `parameters` - [`SubscriptionParams`] configuration object. /// /// # Returns /// @@ -206,7 +203,7 @@ where /// /// # #[tokio::main] /// # async fn main() -> Result<(), pubnub::core::PubNubError> { - /// use pubnub::subscribe::EventSubscriber; + /// use pubnub::subscribe::{EventSubscriber, SubscriptionParams}; /// let client = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -216,27 +213,22 @@ where /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let subscription = client.subscription( - /// Some(&["my_channel_1", "my_channel_2", "my_channel_3"]), - /// None, - /// None, - /// ); + /// 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, - channels: Option<&[N]>, - channel_groups: Option<&[N]>, - options: Option>, - ) -> SubscriptionSet + pub fn subscription(&self, parameters: SubscriptionParams) -> SubscriptionSet where N: Into + Clone, { let mut entities: Vec> = vec![]; - if let Some(channel_names) = channels { + if let Some(channel_names) = parameters.channels { entities.extend( channel_names .iter() @@ -245,7 +237,7 @@ where .collect::>>(), ); } - if let Some(channel_group_names) = channel_groups { + if let Some(channel_group_names) = parameters.channel_groups { entities.extend( channel_group_names .iter() @@ -255,7 +247,7 @@ where ); } - SubscriptionSet::new(entities, options) + SubscriptionSet::new(entities, parameters.options) } /// Stop receiving real-time updates. @@ -270,6 +262,7 @@ where /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// use pubnub::subscribe::SubscriptionParams; /// # /// # let client = PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -279,7 +272,11 @@ where /// # }) /// # .with_user_id("user_id") /// # .build()?; - /// # let subscription = client.subscription(Some(&["channel"]), None, None); + /// # let subscription = client.subscription(SubscriptionParams { + /// # channels: Some(&["channel"]), + /// # channel_groups: None, + /// # options: None + /// # }); /// # let stream = // DataStream /// # subscription.messages_stream(); /// client.disconnect(); @@ -338,6 +335,7 @@ where /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// use pubnub::subscribe::SubscriptionParams; /// # /// # let client = PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -347,7 +345,11 @@ where /// # }) /// # .with_user_id("user_id") /// # .build()?; - /// # let subscription = client.subscription(Some(&["channel"]), None, None); + /// # let subscription = client.subscription(SubscriptionParams { + /// # channels: Some(&["channel"]), + /// # channel_groups: None, + /// # options: None + /// # }); /// # let stream = // DataStream /// # subscription.messages_stream(); /// # // ..... @@ -505,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> @@ -883,17 +885,21 @@ mod should { #[tokio::test] async fn create_subscription_set() { - let _ = client().subscription(Some(&["channel_a"]), Some(&["group_a"]), None); + let _ = client().subscription(SubscriptionParams { + channels: Some(&["channel_a"]), + channel_groups: Some(&["group_a"]), + options: None, + }); } #[tokio::test] async fn subscribe() { let client = client(); - let subscription = client.subscription( - Some(&["my-channel"]), - Some(&["group_a"]), - Some(vec![SubscriptionOptions::ReceivePresenceEvents]), - ); + let subscription = client.subscription(SubscriptionParams { + channels: Some(&["my-channel"]), + channel_groups: Some(&["group_a"]), + options: Some(vec![SubscriptionOptions::ReceivePresenceEvents]), + }); subscription.subscribe(None); let status = client.status_stream().next().await.unwrap(); diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs index 52c58d45..ec17a320 100644 --- a/src/dx/subscribe/subscription_set.rs +++ b/src/dx/subscribe/subscription_set.rs @@ -43,6 +43,7 @@ use crate::{ /// /// # #[tokio::main] /// # async fn main() -> Result<(), pubnub::core::PubNubError> { +/// use pubnub::subscribe::SubscriptionParams; /// let client = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -52,7 +53,11 @@ use crate::{ /// # }) /// # .with_user_id("uuid") /// # .build()?; -/// let subscription = client.subscription(Some(&["my_channel_1", "my_channel_2"]), Some(&["my_group"]), None); +/// let subscription = client.subscription(SubscriptionParams { +/// channels: Some(&["my_channel_1", "my_channel_2"]), +/// channel_groups:Some(&["my_group"]), +/// options:None +/// }); /// # Ok(()) /// # } /// ``` @@ -230,6 +235,7 @@ where /// /// # #[tokio::main] /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// use pubnub::subscribe::SubscriptionParams; /// let client = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -239,7 +245,11 @@ where /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let subscription = client.subscription(Some(&["my_channel_1", "my_channel_2"]), Some(&["my_group"]), None); + /// 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. @@ -622,6 +632,7 @@ where /// /// # #[tokio::main] /// # async fn main() -> Result<(), pubnub::core::PubNubError> { + /// use pubnub::subscribe::SubscriptionParams; /// let client = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset { @@ -631,7 +642,11 @@ where /// # }) /// # .with_user_id("uuid") /// # .build()?; - /// let subscription = client.subscription(Some(&["my_channel_1", "my_channel_2"]), Some(&["my_group"]), None); + /// 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. diff --git a/src/dx/subscribe/types.rs b/src/dx/subscribe/types.rs index 8f68f3b7..23cee281 100644 --- a/src/dx/subscribe/types.rs +++ b/src/dx/subscribe/types.rs @@ -94,6 +94,21 @@ pub enum SubscriptionOptions { 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 diff --git a/src/lib.rs b/src/lib.rs index 4076f5ed..f667cd04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! use pubnub::subscribe::EventEmitter; +//! use pubnub::subscribe::{EventEmitter, SubscriptionParams}; //! let publish_key = "my_publish_key"; //! let subscribe_key = "my_subscribe_key"; //! let client = PubNubClientBuilder::with_reqwest_transport() @@ -73,7 +73,11 @@ //! .build()?; //! println!("PubNub instance created"); //! -//! let subscription = client.subscription(Some(&["my_channel"]), None, None); +//! let subscription = client.subscription(SubscriptionParams { +//! channels: Some(&["my_channel"]), +//! channel_groups: None, +//! options: None +//! }); //! //! println!("Subscribed to channel"); //! From b718ec5b6d766d22bf2c31245fcabb70854d8665 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 13:53:10 +0200 Subject: [PATCH 08/11] test: fix contract tests --- tests/presence/presence_steps.rs | 5 ++--- tests/subscribe/subscribe_steps.rs | 14 +++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/presence/presence_steps.rs b/tests/presence/presence_steps.rs index 8a7e5006..180561b8 100644 --- a/tests/presence/presence_steps.rs +++ b/tests/presence/presence_steps.rs @@ -1,7 +1,6 @@ use cucumber::gherkin::Table; use cucumber::{codegen::Regex, gherkin::Step, then, when}; use futures::{select_biased, FutureExt, StreamExt}; -use log::Log; use std::collections::HashMap; use std::fs::read_to_string; @@ -153,7 +152,7 @@ async fn leave( world: &mut PubNubWorld, channel_a: String, channel_b: String, - with_presence: String, + _with_presence: String, ) { let mut subscription = world.subscription.clone().unwrap(); let subscriptions = world.subscriptions.clone().unwrap().iter().fold( @@ -209,7 +208,7 @@ async fn receive_an_error_heartbeat_retry(world: &mut PubNubWorld) { } #[then("I don't observe any Events and Invocations of the Presence EE")] -async fn event_engine_history_empty(_world: &mut PubNubWorld, step: &Step) { +async fn event_engine_history_empty(_world: &mut PubNubWorld, _step: &Step) { assert_eq!(events_and_invocations_history().len(), 0); } diff --git a/tests/subscribe/subscribe_steps.rs b/tests/subscribe/subscribe_steps.rs index 5dff5da3..fd8f76d7 100644 --- a/tests/subscribe/subscribe_steps.rs +++ b/tests/subscribe/subscribe_steps.rs @@ -4,7 +4,7 @@ use cucumber::gherkin::Table; use cucumber::{codegen::Regex, gherkin::Step, then, when}; use futures::{select_biased, FutureExt, StreamExt}; use pubnub::core::RequestRetryConfiguration; -use pubnub::subscribe::{EventEmitter, EventSubscriber}; +use pubnub::subscribe::{EventEmitter, EventSubscriber, SubscriptionParams}; use std::fs::read_to_string; /// Extract list of events and invocations from log. @@ -117,7 +117,11 @@ async fn subscribe(world: &mut PubNubWorld) { world.pubnub = Some(world.get_pubnub(world.keyset.to_owned())); world.pubnub.clone().map(|pubnub| { - let subscription = pubnub.subscription(Some(&["test"]), None, None); + let subscription = pubnub.subscription(SubscriptionParams { + channels: Some(&["test"]), + channel_groups: None, + options: None, + }); subscription.subscribe(None); world.subscription = Some(subscription); }); @@ -130,7 +134,11 @@ async fn subscribe_with_timetoken(world: &mut PubNubWorld, timetoken: u64) { world.pubnub = Some(world.get_pubnub(world.keyset.to_owned())); world.pubnub.clone().map(|pubnub| { - let subscription = pubnub.subscription(Some(&["test"]), None, None); + let subscription = pubnub.subscription(SubscriptionParams { + channels: Some(&["test"]), + channel_groups: None, + options: None, + }); subscription.subscribe(Some(timetoken.to_string().into())); world.subscription = Some(subscription); }); From 455fd3a58428e37e5f5e8800752082a3e0cd4e94 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 25 Jan 2024 14:14:35 +0200 Subject: [PATCH 09/11] test: update contrtact tests to wait for log entries --- tests/common/common_steps.rs | 3 ++- tests/presence/presence_steps.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/common/common_steps.rs b/tests/common/common_steps.rs index 466878e7..05d7524a 100644 --- a/tests/common/common_steps.rs +++ b/tests/common/common_steps.rs @@ -187,12 +187,13 @@ impl Default for PubNubWorld { impl PubNubWorld { pub async fn reset(&mut self) { - // self.subscription = None; 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; } diff --git a/tests/presence/presence_steps.rs b/tests/presence/presence_steps.rs index 180561b8..51a6fa41 100644 --- a/tests/presence/presence_steps.rs +++ b/tests/presence/presence_steps.rs @@ -13,6 +13,7 @@ use pubnub::subscribe::{ /// 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"); @@ -188,7 +189,7 @@ async fn wait_presence_join(world: &mut PubNubWorld) { #[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(2)).await; + 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() { @@ -208,7 +209,7 @@ async fn receive_an_error_heartbeat_retry(world: &mut PubNubWorld) { } #[then("I don't observe any Events and Invocations of the Presence EE")] -async fn event_engine_history_empty(_world: &mut PubNubWorld, _step: &Step) { +async fn event_engine_history_empty(_world: &mut PubNubWorld) { assert_eq!(events_and_invocations_history().len(), 0); } From 20399498e7393d4b5b53f32a29710a3612e8bb71 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Fri, 26 Jan 2024 00:44:27 +0200 Subject: [PATCH 10/11] refactor: auto retry update --- src/core/transport_request.rs | 113 ++++++++++++++---- src/dx/access/builders/grant_token.rs | 19 ++- src/dx/access/builders/revoke.rs | 13 +- .../presence/builders/get_presence_state.rs | 13 +- src/dx/presence/builders/heartbeat.rs | 11 +- src/dx/presence/builders/here_now.rs | 13 +- src/dx/presence/builders/leave.rs | 13 +- .../presence/builders/set_presence_state.rs | 11 +- src/dx/presence/builders/where_now.rs | 13 +- src/dx/publish/mod.rs | 11 +- src/dx/pubnub_client.rs | 11 +- src/dx/subscribe/builders/subscribe.rs | 11 +- src/dx/subscribe/event_dispatcher.rs | 4 +- src/dx/subscribe/mod.rs | 4 +- src/dx/subscribe/subscription.rs | 4 +- src/dx/subscribe/subscription_set.rs | 4 +- src/dx/subscribe/traits/event_emitter.rs | 2 +- tests/subscribe/subscribe_steps.rs | 1 + 18 files changed, 204 insertions(+), 67 deletions(-) diff --git a/src/core/transport_request.rs b/src/core/transport_request.rs index 24dfb5aa..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. @@ -95,8 +98,8 @@ impl TransportRequest { &self, transport: &T, deserializer: Arc, - - #[cfg(feature = "std")] guard: &DetachedClientsGuard, + #[cfg(feature = "std")] retry_configuration: RequestRetryConfiguration, + #[cfg(feature = "std")] runtime: RuntimeSupport, ) -> Result where B: for<'de> super::Deserialize<'de>, @@ -106,23 +109,43 @@ impl TransportRequest { { #[cfg(feature = "std")] { - let channel = guard.notify_channel_rx.clone(); - guard.increase_detached_count_by(1); + let mut last_result; + let mut retry_attempt = 0_u8; - // Request configured endpoint. - select_biased! { - _ = channel.recv().fuse() => { - guard.decrease_detached_count_by(1); - Err(PubNubError::RequestCancel { details: "PubNub client instance dropped".into() }) + 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; } - response = transport.send(self.clone()).fuse() => { - guard.decrease_detached_count_by(1); - return Self::deserialize( - response?.clone(), - Box::new(move |bytes| deserializer.deserialize(bytes)), - ) + + 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"))] @@ -144,19 +167,65 @@ 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; - 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. diff --git a/src/dx/access/builders/grant_token.rs b/src/dx/access/builders/grant_token.rs index 5448e544..0d2f3af7 100644 --- a/src/dx/access/builders/grant_token.rs +++ b/src/dx/access/builders/grant_token.rs @@ -116,7 +116,7 @@ where 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, @@ -139,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, { @@ -154,7 +154,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 } } @@ -191,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 f311d2c0..87fd3b0a 100644 --- a/src/dx/access/builders/revoke.rs +++ b/src/dx/access/builders/revoke.rs @@ -56,7 +56,7 @@ impl RevokeTokenRequest { 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() @@ -76,7 +76,7 @@ impl RevokeTokenRequestBuilder { impl RevokeTokenRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -91,7 +91,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/builders/get_presence_state.rs b/src/dx/presence/builders/get_presence_state.rs index 0c9ef250..8cab7e4d 100644 --- a/src/dx/presence/builders/get_presence_state.rs +++ b/src/dx/presence/builders/get_presence_state.rs @@ -129,7 +129,7 @@ impl GetStateRequest { ), 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, @@ -139,7 +139,7 @@ impl GetStateRequest { impl GetStateRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -150,7 +150,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/builders/heartbeat.rs b/src/dx/presence/builders/heartbeat.rs index 7da855dc..7169287e 100644 --- a/src/dx/presence/builders/heartbeat.rs +++ b/src/dx/presence/builders/heartbeat.rs @@ -230,7 +230,7 @@ impl HeartbeatRequestBuilder { impl HeartbeatRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -241,7 +241,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/builders/here_now.rs b/src/dx/presence/builders/here_now.rs index fcd9f5b5..6d58381c 100644 --- a/src/dx/presence/builders/here_now.rs +++ b/src/dx/presence/builders/here_now.rs @@ -135,7 +135,7 @@ impl HereNowRequest { ), 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, @@ -145,7 +145,7 @@ impl HereNowRequest { impl HereNowRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -161,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 b6bb66f0..519d4b42 100644 --- a/src/dx/presence/builders/leave.rs +++ b/src/dx/presence/builders/leave.rs @@ -123,7 +123,7 @@ impl LeaveRequest { ), 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, @@ -133,7 +133,7 @@ impl LeaveRequest { impl LeaveRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -144,7 +144,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/builders/set_presence_state.rs b/src/dx/presence/builders/set_presence_state.rs index fe94cc20..b3e8b9a1 100644 --- a/src/dx/presence/builders/set_presence_state.rs +++ b/src/dx/presence/builders/set_presence_state.rs @@ -187,7 +187,7 @@ impl SetStateRequest { impl SetStateRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -204,7 +204,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/builders/where_now.rs b/src/dx/presence/builders/where_now.rs index 7fcbdf5b..5d018f77 100644 --- a/src/dx/presence/builders/where_now.rs +++ b/src/dx/presence/builders/where_now.rs @@ -94,7 +94,7 @@ impl WhereNowRequest { ), 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, @@ -104,7 +104,7 @@ impl WhereNowRequest { impl WhereNowRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -115,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/publish/mod.rs b/src/dx/publish/mod.rs index b2b2385c..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, { @@ -173,7 +173,14 @@ where 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 diff --git a/src/dx/pubnub_client.rs b/src/dx/pubnub_client.rs index 582a6814..b5df8a8f 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -19,10 +19,7 @@ use uuid::Uuid; ))] use crate::providers::futures_tokio::RuntimeTokio; #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] -use crate::{ - core::runtime::RuntimeSupport, - subscribe::{EventDispatcher, SubscriptionCursor, SubscriptionManager}, -}; +use crate::subscribe::{EventDispatcher, SubscriptionCursor, SubscriptionManager}; #[cfg(feature = "presence")] use crate::lib::alloc::vec::Vec; @@ -38,7 +35,7 @@ use crate::transport::TransportReqwest; // TODO: Retry policy would be implemented for `no_std` event engine #[cfg(feature = "std")] -use crate::core::RequestRetryConfiguration; +use crate::core::{runtime::RuntimeSupport, RequestRetryConfiguration}; use crate::{ core::{CryptoProvider, PubNubEntity, PubNubError}, @@ -372,7 +369,7 @@ pub struct PubNubClientRef { 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, @@ -1085,7 +1082,7 @@ impl PubNubClientConfigBuilder { #[cfg(feature = "presence")] state: Arc::new(RwLock::new(HashMap::new())), - #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + #[cfg(feature = "std")] runtime: pre_build.runtime, #[cfg(all(feature = "subscribe", feature = "std"))] diff --git a/src/dx/subscribe/builders/subscribe.rs b/src/dx/subscribe/builders/subscribe.rs index 0a4a5dfc..0bfc769e 100644 --- a/src/dx/subscribe/builders/subscribe.rs +++ b/src/dx/subscribe/builders/subscribe.rs @@ -243,7 +243,7 @@ impl SubscribeRequest { impl SubscribeRequestBuilder where - T: Transport, + T: Transport + 'static, D: Deserializer + 'static, { /// Build and call asynchronous request. @@ -254,7 +254,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/subscribe/event_dispatcher.rs b/src/dx/subscribe/event_dispatcher.rs index ebab23a2..dfd603a7 100644 --- a/src/dx/subscribe/event_dispatcher.rs +++ b/src/dx/subscribe/event_dispatcher.rs @@ -357,7 +357,7 @@ impl EventEmitter for EventDispatcher { self.create_stream_in_list(self.message_streams.write(), messages) } - fn signal_stream(&self) -> DataStream { + fn signals_stream(&self) -> DataStream { let signals = self.dequeue_matching_events(|event| match event { SubscribeStreamEvent::Update(Update::Signal(signal)) => Some(signal.clone()), _ => None, @@ -503,7 +503,7 @@ mod it_should { assert_eq!(events_count, 2); let mut events_count = 0; - let mut stream = dispatcher.signal_stream().take(10); + let mut stream = dispatcher.signals_stream().take(10); loop { match timeout(Duration::from_millis(500), stream.next()).await { Ok(Some(_)) => events_count += 1, diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index d03864ae..e01a8c33 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -710,8 +710,8 @@ impl EventEmitter for PubNubClientInstance { self.event_dispatcher.messages_stream() } - fn signal_stream(&self) -> DataStream { - self.event_dispatcher.signal_stream() + fn signals_stream(&self) -> DataStream { + self.event_dispatcher.signals_stream() } fn message_actions_stream(&self) -> DataStream { diff --git a/src/dx/subscribe/subscription.rs b/src/dx/subscribe/subscription.rs index 5ee0370e..3196b248 100644 --- a/src/dx/subscribe/subscription.rs +++ b/src/dx/subscribe/subscription.rs @@ -616,8 +616,8 @@ where self.event_dispatcher.messages_stream() } - fn signal_stream(&self) -> DataStream { - self.event_dispatcher.signal_stream() + fn signals_stream(&self) -> DataStream { + self.event_dispatcher.signals_stream() } fn message_actions_stream(&self) -> DataStream { diff --git a/src/dx/subscribe/subscription_set.rs b/src/dx/subscribe/subscription_set.rs index ec17a320..3e6d58ce 100644 --- a/src/dx/subscribe/subscription_set.rs +++ b/src/dx/subscribe/subscription_set.rs @@ -891,8 +891,8 @@ where self.event_dispatcher.messages_stream() } - fn signal_stream(&self) -> DataStream { - self.event_dispatcher.signal_stream() + fn signals_stream(&self) -> DataStream { + self.event_dispatcher.signals_stream() } fn message_actions_stream(&self) -> DataStream { diff --git a/src/dx/subscribe/traits/event_emitter.rs b/src/dx/subscribe/traits/event_emitter.rs index 5e8911db..56525d3b 100644 --- a/src/dx/subscribe/traits/event_emitter.rs +++ b/src/dx/subscribe/traits/event_emitter.rs @@ -17,7 +17,7 @@ pub trait EventEmitter { fn messages_stream(&self) -> DataStream; /// Stream used to notify signals. - fn signal_stream(&self) -> DataStream; + fn signals_stream(&self) -> DataStream; /// Stream used to notify message action updates. fn message_actions_stream(&self) -> DataStream; diff --git a/tests/subscribe/subscribe_steps.rs b/tests/subscribe/subscribe_steps.rs index fd8f76d7..529c39eb 100644 --- a/tests/subscribe/subscribe_steps.rs +++ b/tests/subscribe/subscribe_steps.rs @@ -168,6 +168,7 @@ async fn receive_an_error_subscribe_retry(world: &mut PubNubWorld) { | 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(); From d9e91ac10eabc69131583e6c54df46c939f3aa16 Mon Sep 17 00:00:00 2001 From: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:55:53 +0000 Subject: [PATCH 11/11] PubNub SDK 0.5.0 release. --- .pubnub.yml | 13 +++++++- Cargo.toml | 2 +- README.md | 94 ++++++++++++++++++++++++++--------------------------- src/lib.rs | 10 +++--- 4 files changed, 65 insertions(+), 54 deletions(-) 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 08382542..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 "] 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/src/lib.rs b/src/lib.rs index f667cd04..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 @@ -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"] } //! ``` //!